pam authentication (#5303)
* add pam support, without extra column * bugfixes for pam login * document options * fix code style * fix codestyle * fix tests * don't call remember_me without password * fix codestyle * improve checks for pam usage (should fix tests) * fix remember_me part 1 * add remember_token column because :rememberable requires either a password or this column. * migrate db for remember_token * move pam_authentication to the right place, fix logic bug in edit.html.haml * fix tests * fix pam authentication, improve username lookup, add comment * valid? is sometimes not honored, return nil instead trying to authenticate with pam * update devise_pam_authenticatable2 and adjust code. Fixes sideeffects observed in tests * update devise_pam_authenticatable gem, fixes for codeconventions, fix finding user * codeconvention fixes * code convention fixes * fix idention * update dependency, explicit conflict check * fix disabled password updates if in pam mode * fix check password if password is present, fix templates * block registration if account is maintained by pam * Revert "block registration if account is maintained by pam" This reverts commit 8e7a083d650240b6fac414926744b4b90b435f20. * fix identation error introduced by rebase * block usernames maintained by pam * document pam settings better * fix code style
This commit is contained in:
		
							
								
								
									
										3
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								Gemfile
									
									
									
									
									
								
							@@ -30,6 +30,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'
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
@@ -420,6 +423,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)
 | 
			
		||||
@@ -570,6 +574,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)
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ class ApplicationController < ActionController::Base
 | 
			
		||||
  helper_method :current_session
 | 
			
		||||
  helper_method :current_theme
 | 
			
		||||
  helper_method :single_user_mode?
 | 
			
		||||
  helper_method :use_pam?
 | 
			
		||||
 | 
			
		||||
  rescue_from ActionController::RoutingError, with: :not_found
 | 
			
		||||
  rescue_from ActiveRecord::RecordNotFound, with: :not_found
 | 
			
		||||
@@ -75,6 +76,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
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,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)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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'
 | 
			
		||||
 
 | 
			
		||||
@@ -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/
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -53,6 +53,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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								db/migrate/20180109143959_add_remember_token_to_users.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								db/migrate/20180109143959_add_remember_token_to_users.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
class AddRememberTokenToUsers < ActiveRecord::Migration[5.1]
 | 
			
		||||
  def change
 | 
			
		||||
    add_column :users, :remember_token, :string, null: true
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -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"
 | 
			
		||||
@@ -486,6 +486,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
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user