about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Gemfile3
-rw-r--r--Gemfile.lock11
-rw-r--r--app/controllers/activitypub/outboxes_controller.rb4
-rw-r--r--app/controllers/application_controller.rb5
-rw-r--r--app/controllers/auth/registrations_controller.rb5
-rw-r--r--app/controllers/auth/sessions_controller.rb6
-rw-r--r--app/controllers/concerns/signature_authentication.rb11
-rw-r--r--app/controllers/statuses_controller.rb1
-rw-r--r--app/controllers/stream_entries_controller.rb5
-rw-r--r--app/javascript/mastodon/components/column_header.js4
-rw-r--r--app/javascript/mastodon/features/compose/index.js7
-rw-r--r--app/javascript/styles/mastodon/components.scss36
-rw-r--r--app/lib/activitypub/activity/announce.rb5
-rw-r--r--app/models/user.rb69
-rw-r--r--app/services/fetch_link_card_service.rb10
-rw-r--r--app/validators/unreserved_username_validator.rb6
-rw-r--r--app/views/about/_links.html.haml16
-rw-r--r--app/views/about/more.html.haml17
-rw-r--r--app/views/about/show.html.haml17
-rw-r--r--app/views/about/terms.html.haml14
-rw-r--r--app/views/auth/passwords/edit.html.haml18
-rw-r--r--app/views/auth/registrations/edit.html.haml15
-rw-r--r--app/views/auth/sessions/new.html.haml5
-rw-r--r--app/views/settings/preferences/show.html.haml3
-rw-r--r--config/initializers/devise.rb34
-rw-r--r--config/locales/simple_form.de.yml1
-rw-r--r--config/locales/simple_form.en.yml1
-rw-r--r--db/migrate/20180109143959_add_remember_token_to_users.rb5
-rw-r--r--db/schema.rb3
-rw-r--r--spec/models/setting_spec.rb2
30 files changed, 244 insertions, 95 deletions
diff --git a/Gemfile b/Gemfile
index ba9095378..a610d0a79 100644
--- a/Gemfile
+++ b/Gemfile
@@ -31,6 +31,9 @@ gem 'iso-639'
 gem 'cld3', '~> 3.2.0'
 gem 'devise', '~> 4.4'
 gem 'devise-two-factor', '~> 3.0'
+
+gem 'devise_pam_authenticatable2', '~> 8.0'
+
 gem 'doorkeeper', '~> 4.2'
 gem 'fast_blank', '~> 1.0'
 gem 'goldfinger', '~> 2.1'
diff --git a/Gemfile.lock b/Gemfile.lock
index 5f291d46a..d8a853b04 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -137,6 +137,9 @@ GEM
       devise (~> 4.0)
       railties (< 5.2)
       rotp (~> 2.0)
+    devise_pam_authenticatable2 (8.0.1)
+      devise (>= 4.0.0)
+      rpam2 (~> 3.0)
     diff-lcs (1.3)
     docile (1.1.5)
     domain_name (0.5.20170404)
@@ -215,7 +218,7 @@ GEM
     httplog (0.99.7)
       colorize
       rack
-    i18n (0.9.1)
+    i18n (0.9.3)
       concurrent-ruby (~> 1.0)
     i18n-tasks (0.9.19)
       activesupport (>= 4.0.2)
@@ -284,7 +287,7 @@ GEM
     mimemagic (0.3.2)
     mini_mime (1.0.0)
     mini_portile2 (2.3.0)
-    minitest (5.10.3)
+    minitest (5.11.3)
     msgpack (1.1.0)
     multi_json (1.12.2)
     net-scp (1.2.1)
@@ -307,7 +310,7 @@ GEM
       http (~> 3.0)
       nokogiri (~> 1.8)
     ox (2.8.2)
-    paperclip (5.1.0)
+    paperclip (5.2.1)
       activemodel (>= 4.2.0)
       activesupport (>= 4.2.0)
       cocaine (~> 0.5.5)
@@ -421,6 +424,7 @@ GEM
       actionpack (>= 4.2.0, < 5.3)
       railties (>= 4.2.0, < 5.3)
     rotp (2.1.2)
+    rpam2 (3.1.0)
     rqrcode (0.10.1)
       chunky_png (~> 1.0)
     rspec-core (3.7.0)
@@ -571,6 +575,7 @@ DEPENDENCIES
   climate_control (~> 0.2)
   devise (~> 4.4)
   devise-two-factor (~> 3.0)
+  devise_pam_authenticatable2 (~> 8.0)
   doorkeeper (~> 4.2)
   dotenv-rails (~> 2.2)
   fabrication (~> 2.18)
diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb
index 9f97ff622..a431e3557 100644
--- a/app/controllers/activitypub/outboxes_controller.rb
+++ b/app/controllers/activitypub/outboxes_controller.rb
@@ -1,10 +1,12 @@
 # frozen_string_literal: true
 
 class ActivityPub::OutboxesController < Api::BaseController
+  include SignatureVerification
+
   before_action :set_account
 
   def show
-    @statuses = @account.statuses.permitted_for(@account, current_account).paginate_by_max_id(20, params[:max_id], params[:since_id])
+    @statuses = @account.statuses.permitted_for(@account, signed_request_account).paginate_by_max_id(20, params[:max_id], params[:since_id])
     @statuses = cache_collection(@statuses, Status)
 
     render json: outbox_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 276c6b012..7534b5375 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -15,6 +15,7 @@ class ApplicationController < ActionController::Base
   helper_method :current_flavour
   helper_method :current_skin
   helper_method :single_user_mode?
+  helper_method :use_pam?
 
   rescue_from ActionController::RoutingError, with: :not_found
   rescue_from ActiveRecord::RecordNotFound, with: :not_found
@@ -145,6 +146,10 @@ class ApplicationController < ActionController::Base
     @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.exists?
   end
 
+  def use_pam?
+    Devise.pam_authentication
+  end
+
   def current_account
     @current_account ||= current_user.try(:account)
   end
diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb
index 2b6a1bdbc..9b3ea4f27 100644
--- a/app/controllers/auth/registrations_controller.rb
+++ b/app/controllers/auth/registrations_controller.rb
@@ -15,6 +15,11 @@ class Auth::RegistrationsController < Devise::RegistrationsController
 
   protected
 
+  def update_resource(resource, params)
+    params[:password] = nil if Devise.pam_authentication && resource.encrypted_password.blank?
+    super
+  end
+
   def build_resource(hash = nil)
     super(hash)
 
diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb
index f45d77b88..ce9cf98d7 100644
--- a/app/controllers/auth/sessions_controller.rb
+++ b/app/controllers/auth/sessions_controller.rb
@@ -29,7 +29,11 @@ class Auth::SessionsController < Devise::SessionsController
     if session[:otp_user_id]
       User.find(session[:otp_user_id])
     elsif user_params[:email]
-      User.find_for_authentication(email: user_params[:email])
+      if use_pam? && Devise.check_at_sign && user_params[:email].index('@').nil?
+        User.joins(:account).find_by(accounts: { username: user_params[:email] })
+      else
+        User.find_for_authentication(email: user_params[:email])
+      end
     end
   end
 
diff --git a/app/controllers/concerns/signature_authentication.rb b/app/controllers/concerns/signature_authentication.rb
new file mode 100644
index 000000000..beec93223
--- /dev/null
+++ b/app/controllers/concerns/signature_authentication.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module SignatureAuthentication
+  extend ActiveSupport::Concern
+
+  include SignatureVerification
+
+  def current_account
+    super || signed_request_account
+  end
+end
diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb
index d67fac0e5..61ffb97d9 100644
--- a/app/controllers/statuses_controller.rb
+++ b/app/controllers/statuses_controller.rb
@@ -1,6 +1,7 @@
 # frozen_string_literal: true
 
 class StatusesController < ApplicationController
+  include SignatureAuthentication
   include Authorization
 
   layout 'public'
diff --git a/app/controllers/stream_entries_controller.rb b/app/controllers/stream_entries_controller.rb
index b597ba4bb..e2ea45c83 100644
--- a/app/controllers/stream_entries_controller.rb
+++ b/app/controllers/stream_entries_controller.rb
@@ -10,6 +10,7 @@ class StreamEntriesController < ApplicationController
   before_action :set_stream_entry
   before_action :set_link_headers
   before_action :check_account_suspension
+  before_action :set_cache_headers
 
   def show
     respond_to do |format|
@@ -20,6 +21,10 @@ class StreamEntriesController < ApplicationController
       end
 
       format.atom do
+        unless @stream_entry.hidden?
+          skip_session!
+          expires_in 3.minutes, public: true
+        end
         render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true))
       end
     end
diff --git a/app/javascript/mastodon/components/column_header.js b/app/javascript/mastodon/components/column_header.js
index c300db89b..6b79ec02d 100644
--- a/app/javascript/mastodon/components/column_header.js
+++ b/app/javascript/mastodon/components/column_header.js
@@ -133,9 +133,7 @@ export default class ColumnHeader extends React.PureComponent {
         <h1 className={buttonClassName}>
           <button onClick={this.handleTitleClick}>
             <i className={`fa fa-fw fa-${icon} column-header__icon`} />
-            <span className='column-header__title'>
-              {title}
-            </span>
+            {title}
           </button>
 
           <div className='column-header__buttons'>
diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js
index 84e3a2338..138bc4e2e 100644
--- a/app/javascript/mastodon/features/compose/index.js
+++ b/app/javascript/mastodon/features/compose/index.js
@@ -12,6 +12,7 @@ import Motion from '../ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import SearchResultsContainer from './containers/search_results_container';
 import { changeComposing } from '../../actions/compose';
+import elephantUIPlane from '../../../images/elephant_ui_plane.svg';
 
 const messages = defineMessages({
   start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
@@ -94,7 +95,11 @@ export default class Compose extends React.PureComponent {
           <div className='drawer__inner' onFocus={this.onFocus}>
             <NavigationContainer onClose={this.onBlur} />
             <ComposeFormContainer />
-            {multiColumn && <div className='mastodon' />}
+            {multiColumn && (
+              <div className='drawer__inner__mastodon'>
+                <img alt='' src={elephantUIPlane} />
+              </div>
+            )}
           </div>
 
           <Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index b023b208d..91f9739d7 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -1766,7 +1766,7 @@
   position: absolute;
   top: 0;
   left: 0;
-  background: lighten($ui-base-color, 13%) url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-color)}"/></svg>') no-repeat bottom / 100% auto;
+  background: lighten($ui-base-color, 13%);
   box-sizing: border-box;
   padding: 0;
   display: flex;
@@ -1779,10 +1779,19 @@
   &.darker {
     background: $ui-base-color;
   }
+}
 
-  > .mastodon {
-    background: url('~images/elephant_ui_plane.svg') no-repeat left bottom / contain;
-    flex: 1;
+.drawer__inner__mastodon {
+  background: lighten($ui-base-color, 13%) url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-color)}"/></svg>') no-repeat bottom / 100% auto;
+  flex: 1;
+  min-height: 47px;
+
+  > img { 
+    display: block;
+    object-fit: contain;
+    object-position: bottom left;
+    width: 100%;
+    height: 100%;
   }
 }
 
@@ -1913,7 +1922,7 @@
   font-family: inherit;
   color: $ui-highlight-color;
   cursor: pointer;
-  flex: 0 0 auto;
+  white-space: nowrap;
   font-size: 16px;
   padding: 0 5px 0 0;
   z-index: 3;
@@ -2403,15 +2412,17 @@
   overflow: hidden;
 
   & > button {
-    display: flex;
-    flex: auto;
     margin: 0;
     border: none;
-    padding: 15px;
+    padding: 15px 0 15px 15px;
     color: inherit;
     background: transparent;
     font: inherit;
     text-align: left;
+    text-overflow: ellipsis;
+    overflow: hidden;
+    white-space: nowrap;
+    flex: 1;
   }
 
   &.active {
@@ -2432,7 +2443,6 @@
 .column-header__buttons {
   height: 48px;
   display: flex;
-  margin-left: 0;
 }
 
 .column-header__links .text-btn {
@@ -2512,14 +2522,6 @@
   }
 }
 
-.column-header__title {
-  display: inline-block;
-  text-overflow: ellipsis;
-  overflow: hidden;
-  white-space: nowrap;
-  flex: 1;
-}
-
 .text-btn {
   display: inline-block;
   padding: 0;
diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb
index abf2b9b80..c8a358195 100644
--- a/app/lib/activitypub/activity/announce.rb
+++ b/app/lib/activitypub/activity/announce.rb
@@ -15,7 +15,8 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
       account: @account,
       reblog: original_status,
       uri: @json['id'],
-      created_at: @options[:override_timestamps] ? nil : @json['published']
+      created_at: @options[:override_timestamps] ? nil : @json['published'],
+      visibility: original_status.visibility
     )
 
     distribute(status)
@@ -35,6 +36,6 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
   end
 
   def announceable?(status)
-    status.public_visibility? || status.unlisted_visibility?
+    status.account_id == @account.id || status.public_visibility? || status.unlisted_visibility?
   end
 end
diff --git a/app/models/user.rb b/app/models/user.rb
index 603b72e2b..6ef6db915 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -34,6 +34,7 @@
 #  disabled                  :boolean          default(FALSE), not null
 #  moderator                 :boolean          default(FALSE), not null
 #  invite_id                 :integer
+#  remember_token            :string
 #
 
 class User < ApplicationRecord
@@ -50,6 +51,8 @@ class User < ApplicationRecord
   devise :registerable, :recoverable, :rememberable, :trackable, :validatable,
          :confirmable
 
+  devise :pam_authenticatable
+
   belongs_to :account, inverse_of: :user
   belongs_to :invite, counter_cache: :uses, optional: true
   accepts_nested_attributes_for :account
@@ -84,6 +87,33 @@ class User < ApplicationRecord
 
   attr_accessor :invite_code
 
+  def pam_conflict(_)
+    # block pam login tries on traditional account
+    nil
+  end
+
+  def pam_conflict?
+    return false unless Devise.pam_authentication
+    encrypted_password.present? && is_pam_account?
+  end
+
+  def pam_get_name
+    return account.username if account.present?
+    super
+  end
+
+  def pam_setup(_attributes)
+    acc = Account.new(username: pam_get_name)
+    acc.save!(validate: false)
+
+    self.email = "#{acc.username}@#{find_pam_suffix}" if email.nil? && find_pam_suffix
+    self.confirmed_at = Time.now.utc
+    self.admin = false
+    self.account = acc
+
+    acc.destroy! unless save
+  end
+
   def confirmed?
     confirmed_at.present?
   end
@@ -213,6 +243,45 @@ class User < ApplicationRecord
     @invite_code = code
   end
 
+  def password_required?
+    return false if Devise.pam_authentication
+    super
+  end
+
+  def send_reset_password_instructions
+    return false if encrypted_password.blank? && Devise.pam_authentication
+    super
+  end
+
+  def reset_password!(new_password, new_password_confirmation)
+    return false if encrypted_password.blank? && Devise.pam_authentication
+    super
+  end
+
+  def self.pam_get_user(attributes = {})
+    if attributes[:email]
+      resource =
+        if Devise.check_at_sign && !attributes[:email].index('@')
+          joins(:account).find_by(accounts: { username: attributes[:email] })
+        else
+          find_by(email: attributes[:email])
+        end
+
+      if resource.blank?
+        resource = new(email: attributes[:email])
+        if Devise.check_at_sign && !resource[:email].index('@')
+          resource[:email] = "#{attributes[:email]}@#{resource.find_pam_suffix}"
+        end
+      end
+      resource
+    end
+  end
+
+  def self.authenticate_with_pam(attributes = {})
+    return nil unless Devise.pam_authentication
+    super
+  end
+
   protected
 
   def send_devise_notification(notification, *args)
diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index d0472a1d7..3e31a4145 100644
--- a/app/services/fetch_link_card_service.rb
+++ b/app/services/fetch_link_card_service.rb
@@ -91,13 +91,13 @@ class FetchLinkCardService < BaseService
 
     case @card.type
     when 'link'
-      @card.image = URI.parse(embed.thumbnail_url) if embed.respond_to?(:thumbnail_url)
+      @card.image_remote_url = embed.thumbnail_url if embed.respond_to?(:thumbnail_url)
     when 'photo'
       return false unless embed.respond_to?(:url)
-      @card.embed_url = embed.url
-      @card.image     = URI.parse(embed.url)
-      @card.width     = embed.width.presence  || 0
-      @card.height    = embed.height.presence || 0
+      @card.embed_url        = embed.url
+      @card.image_remote_url = embed.url
+      @card.width            = embed.width.presence  || 0
+      @card.height           = embed.height.presence || 0
     when 'video'
       @card.width  = embed.width.presence  || 0
       @card.height = embed.height.presence || 0
diff --git a/app/validators/unreserved_username_validator.rb b/app/validators/unreserved_username_validator.rb
index 44ea4359b..c2311a89a 100644
--- a/app/validators/unreserved_username_validator.rb
+++ b/app/validators/unreserved_username_validator.rb
@@ -8,7 +8,13 @@ class UnreservedUsernameValidator < ActiveModel::Validator
 
   private
 
+  def pam_controlled?(value)
+    return false unless Devise.pam_authentication && Devise.pam_controlled_service
+    Rpam2.account(Devise.pam_controlled_service, value).present?
+  end
+
   def reserved_username?(value)
+    return true if pam_controlled?(value)
     return false unless Setting.reserved_usernames
     Setting.reserved_usernames.include?(value.downcase)
   end
diff --git a/app/views/about/_links.html.haml b/app/views/about/_links.html.haml
new file mode 100644
index 000000000..ccf4f08b9
--- /dev/null
+++ b/app/views/about/_links.html.haml
@@ -0,0 +1,16 @@
+.container.links
+  .brand
+    = link_to root_url do
+      = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
+
+  %ul.nav
+    %li
+      - if user_signed_in?
+        = link_to t('settings.back'), root_url, class: 'webapp-btn'
+      - else
+        = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
+    %li= link_to t('about.about_this'), about_more_path
+    %li
+      = link_to 'https://joinmastodon.org/' do
+        = "#{t('about.other_instances')}"
+        %i.fa.fa-external-link{ style: 'padding-left: 5px;' }
diff --git a/app/views/about/more.html.haml b/app/views/about/more.html.haml
index d92362bd7..84daadba8 100644
--- a/app/views/about/more.html.haml
+++ b/app/views/about/more.html.haml
@@ -7,22 +7,7 @@
 .landing-page
   .header-wrapper.compact
     .header
-      .container.links
-        .brand
-          = link_to root_url do
-            = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
-
-        %ul.nav
-          %li
-            - if user_signed_in?
-              = link_to t('settings.back'), root_url, class: 'webapp-btn'
-            - else
-              = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
-          %li= link_to t('about.about_this'), about_more_path
-          %li
-            = link_to 'https://joinmastodon.org/' do
-              = "#{t('about.other_instances')}"
-              %i.fa.fa-external-link{ style: 'padding-left: 5px;' }
+      = render 'links'
 
       .container.hero
         .heading
diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml
index 4f5b53470..487c8429b 100644
--- a/app/views/about/show.html.haml
+++ b/app/views/about/show.html.haml
@@ -11,22 +11,7 @@
       = image_tag asset_pack_path('elephant-fren.png'), alt: '', role: 'presentation', class: 'mascot'
 
     .header
-      .container.links
-        .brand
-          = link_to root_url do
-            = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
-
-        %ul.nav
-          %li
-            - if user_signed_in?
-              = link_to t('settings.back'), root_url, class: 'webapp-btn'
-            - else
-              = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
-          %li= link_to t('about.about_this'), about_more_path
-          %li
-            = link_to 'https://joinmastodon.org/' do
-              = "#{t('about.other_instances')}"
-              %i.fa.fa-external-link{ style: 'padding-left: 5px;' }
+      = render 'links'
 
       .container.hero
         .floats
diff --git a/app/views/about/terms.html.haml b/app/views/about/terms.html.haml
index 7004cb0b1..ba780759c 100644
--- a/app/views/about/terms.html.haml
+++ b/app/views/about/terms.html.haml
@@ -4,19 +4,7 @@
 .landing-page
   .header-wrapper.compact
     .header
-      .container.links
-        .brand
-          = link_to root_url do
-            = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
-
-        %ul.nav
-          %li
-            - if user_signed_in?
-              = link_to t('settings.back'), root_url, class: 'webapp-btn'
-            - else
-              = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
-          %li= link_to t('about.about_this'), about_more_path
-          %li= link_to t('about.other_instances'), 'https://joinmastodon.org/'
+      = render 'links'
 
   .extended-description
     .container
diff --git a/app/views/auth/passwords/edit.html.haml b/app/views/auth/passwords/edit.html.haml
index 5ef3de976..d8fed9e77 100644
--- a/app/views/auth/passwords/edit.html.haml
+++ b/app/views/auth/passwords/edit.html.haml
@@ -1,14 +1,18 @@
 - content_for :page_title do
   = t('auth.set_new_password')
 
-= simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f|
-  = render 'shared/error_messages', object: resource
-  = f.input :reset_password_token, as: :hidden
+  = simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f|
+    = render 'shared/error_messages', object: resource
 
-  = f.input :password, autofocus: true, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' }
-  = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' }
+    - if use_pam? || current_user.encrypted_password.present?
+      = f.input :reset_password_token, as: :hidden
 
-  .actions
-    = f.button :button, t('auth.set_new_password'), type: :submit
+      = f.input :password, autofocus: true, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' }
+      = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' }
+
+      .actions
+        = f.button :button, t('auth.set_new_password'), type: :submit
+    - else
+      = t('simple_form.labels.defaults.pam_account')
 
 .form-footer= render 'auth/shared/links'
diff --git a/app/views/auth/registrations/edit.html.haml b/app/views/auth/registrations/edit.html.haml
index 145f5cd9e..102199f81 100644
--- a/app/views/auth/registrations/edit.html.haml
+++ b/app/views/auth/registrations/edit.html.haml
@@ -4,13 +4,16 @@
 = simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: 'auth_edit' }) do |f|
   = render 'shared/error_messages', object: resource
 
-  = f.input :email, placeholder: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
-  = f.input :password, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' }
-  = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' }
-  = f.input :current_password, placeholder: t('simple_form.labels.defaults.current_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' }
+  - if !use_pam? || current_user.encrypted_password.present?
+    = f.input :email, placeholder: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
+    = f.input :password, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' }
+    = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' }
+    = f.input :current_password, placeholder: t('simple_form.labels.defaults.current_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' }
 
-  .actions
-    = f.button :button, t('generic.save_changes'), type: :submit
+    .actions
+      = f.button :button, t('generic.save_changes'), type: :submit
+  - else
+    = t('simple_form.labels.defaults.pam_account')
 
 %hr/
 
diff --git a/app/views/auth/sessions/new.html.haml b/app/views/auth/sessions/new.html.haml
index a52b0053b..3edb0d2d4 100644
--- a/app/views/auth/sessions/new.html.haml
+++ b/app/views/auth/sessions/new.html.haml
@@ -5,7 +5,10 @@
   = render partial: 'shared/og'
 
 = simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
-  = f.input :email, autofocus: true, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
+  - if use_pam?
+    = f.input :email, autofocus: true, placeholder: t('simple_form.labels.defaults.username_or_email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username_or_email') }
+  - else
+    = f.input :email, autofocus: true, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
   = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }
 
   .actions
diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml
index d1459d93c..a2a17d0d6 100644
--- a/app/views/settings/preferences/show.html.haml
+++ b/app/views/settings/preferences/show.html.haml
@@ -4,6 +4,9 @@
 = simple_form_for current_user, url: settings_preferences_path, html: { method: :put } do |f|
   = render 'shared/error_messages', object: current_user
 
+  .actions
+    = f.button :button, t('generic.save_changes'), type: :submit
+
   %h4= t 'preferences.languages'
 
   .fields-group
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index 07912c28b..f2f7f1ba3 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -30,6 +30,19 @@ Warden::Manager.before_logout do |_, warden|
   warden.cookies.delete('_session_id')
 end
 
+module Devise
+  mattr_accessor :pam_authentication
+  @@pam_authentication = false
+  mattr_accessor :pam_controlled_service
+  @@pam_controlled_service = nil
+
+  class Strategies::PamAuthenticatable
+    def valid?
+      super && ::Devise.pam_authentication
+    end
+  end
+end
+
 Devise.setup do |config|
   config.warden do |manager|
     manager.default_strategies(scope: :user).unshift :two_factor_authenticatable
@@ -96,7 +109,7 @@ Devise.setup do |config|
   # given strategies, for example, `config.http_authenticatable = [:database]` will
   # enable it only for database authentication. The supported strategies are:
   # :database      = Support basic authentication with authentication key + password
-  config.http_authenticatable = [:database]
+  config.http_authenticatable = [:pam, :database]
 
   # If 401 status code should be returned for AJAX requests. True by default.
   # config.http_authenticatable_on_xhr = true
@@ -301,4 +314,23 @@ Devise.setup do |config|
   # When using OmniAuth, Devise cannot automatically set OmniAuth path,
   # so you need to do it manually. For the users scope, it would be:
   # config.omniauth_path_prefix = '/my_engine/users/auth'
+
+  # PAM: only look for email field
+  config.usernamefield = nil
+  config.emailfield = "email"
+
+  # authentication with pam possible
+  # if not enabled, all pam settings are ignored
+  #config.pam_authentication = true
+  # check if email is actually a username
+  config.check_at_sign = true
+  # suffix for email address generation (warning: without pam must provide email in the pam environment)
+  config.pam_default_suffix = "pam"
+  # name of the pam service
+  # pam "auth" section is evaluated
+  config.pam_default_service = "rpam"
+  # name of the pam service used for checking if an user can register
+  # pam "account" section is evaluated
+  # nil for allowing registration of pam names (not recommended)
+  config.pam_controlled_service = "rpam"
 end
diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml
index 3c5e467a2..bb78ae21a 100644
--- a/config/locales/simple_form.de.yml
+++ b/config/locales/simple_form.de.yml
@@ -53,6 +53,7 @@ de:
         severity: Gewichtung
         type: Importtyp
         username: Profilname
+        username_or_email: Profilname oder Email
       interactions:
         must_be_follower: Benachrichtigungen von Nicht-Folgenden blockieren
         must_be_following: Benachrichtigungen von Profilen blockieren, denen ich nicht folge
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index ceb015282..e097e80ae 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -54,6 +54,7 @@ en:
         severity: Severity
         type: Import type
         username: Username
+        username_or_email: Username or Email
       interactions:
         must_be_follower: Block notifications from non-followers
         must_be_following: Block notifications from people you don't follow
diff --git a/db/migrate/20180109143959_add_remember_token_to_users.rb b/db/migrate/20180109143959_add_remember_token_to_users.rb
new file mode 100644
index 000000000..662905bcb
--- /dev/null
+++ b/db/migrate/20180109143959_add_remember_token_to_users.rb
@@ -0,0 +1,5 @@
+class AddRememberTokenToUsers < ActiveRecord::Migration[5.1]
+  def change
+    add_column :users, :remember_token, :string, null: true
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 11ca12235..816b3a030 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 20180106000232) do
+ActiveRecord::Schema.define(version: 20180109143959) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -496,6 +496,7 @@ ActiveRecord::Schema.define(version: 20180106000232) do
     t.boolean "disabled", default: false, null: false
     t.boolean "moderator", default: false, null: false
     t.bigint "invite_id"
+    t.string "remember_token"
     t.index ["account_id"], name: "index_users_on_account_id"
     t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
     t.index ["email"], name: "index_users_on_email", unique: true
diff --git a/spec/models/setting_spec.rb b/spec/models/setting_spec.rb
index e99dfc0d7..bbba5f98d 100644
--- a/spec/models/setting_spec.rb
+++ b/spec/models/setting_spec.rb
@@ -52,7 +52,7 @@ RSpec.describe Setting, type: :model do
           allow(RailsSettings::Settings).to receive(:object).with(key).and_return(object)
           allow(described_class).to receive(:default_settings).and_return(default_settings)
           allow_any_instance_of(Settings::ScopedSettings).to receive(:thing_scoped).and_return(records)
-          Rails.cache.clear(cache_key)
+          Rails.cache.delete(cache_key)
         end
 
         let(:object)           { nil }