commit: 04fef7b8886bb78f3473e143894a521ca578f1db
parent: 1afc70c990d4d23e5fac57de9cb579c396a82b5c
Author: Alexander <devkral@web.de>
Date: Fri, 2 Feb 2018 10:18:55 +0100
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
Diffstat:
15 files changed, 164 insertions(+), 17 deletions(-)
diff --git a/Gemfile b/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'
diff --git 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)
@@ -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)
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
@@ -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
diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb
@@ -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)
diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb
@@ -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
diff --git 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/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/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
@@ -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
@@ -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/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
@@ -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
@@ -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
diff --git a/db/migrate/20180109143959_add_remember_token_to_users.rb 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
@@ -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