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:
		
					parent
					
						
							
								1afc70c990
							
						
					
				
			
			
				commit
				
					
						04fef7b888
					
				
			
		
					 15 changed files with 164 additions and 17 deletions
				
			
		
							
								
								
									
										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,9 +28,13 @@ class Auth::SessionsController < Devise::SessionsController | |||
|     if session[:otp_user_id] | ||||
|       User.find(session[:otp_user_id]) | ||||
|     elsif 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 | ||||
| 
 | ||||
|   def user_params | ||||
|     params.require(:user).permit(:email, :password, :otp_attempt) | ||||
|  |  | |||
|  | @ -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,8 +1,10 @@ | |||
| - 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| | ||||
|   = simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| | ||||
|     = render 'shared/error_messages', object: resource | ||||
| 
 | ||||
|     - if use_pam? || current_user.encrypted_password.present? | ||||
|       = f.input :reset_password_token, as: :hidden | ||||
| 
 | ||||
|       = 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' } | ||||
|  | @ -10,5 +12,7 @@ | |||
| 
 | ||||
|       .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,6 +4,7 @@ | |||
| = 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 | ||||
| 
 | ||||
|   - 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' } | ||||
|  | @ -11,6 +12,8 @@ | |||
| 
 | ||||
|     .actions | ||||
|       = f.button :button, t('generic.save_changes'), type: :submit | ||||
|   - else | ||||
|     = t('simple_form.labels.defaults.pam_account') | ||||
| 
 | ||||
| %hr/ | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,6 +5,9 @@ | |||
|   = render partial: 'shared/og' | ||||
| 
 | ||||
| = simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| | ||||
|   - 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' } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue