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 stylegh/stable
parent
1afc70c990
commit
04fef7b888
3
Gemfile
3
Gemfile
|
@ -30,6 +30,9 @@ gem 'iso-639'
|
||||||
gem 'cld3', '~> 3.2.0'
|
gem 'cld3', '~> 3.2.0'
|
||||||
gem 'devise', '~> 4.4'
|
gem 'devise', '~> 4.4'
|
||||||
gem 'devise-two-factor', '~> 3.0'
|
gem 'devise-two-factor', '~> 3.0'
|
||||||
|
|
||||||
|
gem 'devise_pam_authenticatable2', '~> 8.0'
|
||||||
|
|
||||||
gem 'doorkeeper', '~> 4.2'
|
gem 'doorkeeper', '~> 4.2'
|
||||||
gem 'fast_blank', '~> 1.0'
|
gem 'fast_blank', '~> 1.0'
|
||||||
gem 'goldfinger', '~> 2.1'
|
gem 'goldfinger', '~> 2.1'
|
||||||
|
|
|
@ -137,6 +137,9 @@ GEM
|
||||||
devise (~> 4.0)
|
devise (~> 4.0)
|
||||||
railties (< 5.2)
|
railties (< 5.2)
|
||||||
rotp (~> 2.0)
|
rotp (~> 2.0)
|
||||||
|
devise_pam_authenticatable2 (8.0.1)
|
||||||
|
devise (>= 4.0.0)
|
||||||
|
rpam2 (~> 3.0)
|
||||||
diff-lcs (1.3)
|
diff-lcs (1.3)
|
||||||
docile (1.1.5)
|
docile (1.1.5)
|
||||||
domain_name (0.5.20170404)
|
domain_name (0.5.20170404)
|
||||||
|
@ -420,6 +423,7 @@ GEM
|
||||||
actionpack (>= 4.2.0, < 5.3)
|
actionpack (>= 4.2.0, < 5.3)
|
||||||
railties (>= 4.2.0, < 5.3)
|
railties (>= 4.2.0, < 5.3)
|
||||||
rotp (2.1.2)
|
rotp (2.1.2)
|
||||||
|
rpam2 (3.1.0)
|
||||||
rqrcode (0.10.1)
|
rqrcode (0.10.1)
|
||||||
chunky_png (~> 1.0)
|
chunky_png (~> 1.0)
|
||||||
rspec-core (3.7.0)
|
rspec-core (3.7.0)
|
||||||
|
@ -570,6 +574,7 @@ DEPENDENCIES
|
||||||
climate_control (~> 0.2)
|
climate_control (~> 0.2)
|
||||||
devise (~> 4.4)
|
devise (~> 4.4)
|
||||||
devise-two-factor (~> 3.0)
|
devise-two-factor (~> 3.0)
|
||||||
|
devise_pam_authenticatable2 (~> 8.0)
|
||||||
doorkeeper (~> 4.2)
|
doorkeeper (~> 4.2)
|
||||||
dotenv-rails (~> 2.2)
|
dotenv-rails (~> 2.2)
|
||||||
fabrication (~> 2.18)
|
fabrication (~> 2.18)
|
||||||
|
|
|
@ -14,6 +14,7 @@ class ApplicationController < ActionController::Base
|
||||||
helper_method :current_session
|
helper_method :current_session
|
||||||
helper_method :current_theme
|
helper_method :current_theme
|
||||||
helper_method :single_user_mode?
|
helper_method :single_user_mode?
|
||||||
|
helper_method :use_pam?
|
||||||
|
|
||||||
rescue_from ActionController::RoutingError, with: :not_found
|
rescue_from ActionController::RoutingError, with: :not_found
|
||||||
rescue_from ActiveRecord::RecordNotFound, 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?
|
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.exists?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def use_pam?
|
||||||
|
Devise.pam_authentication
|
||||||
|
end
|
||||||
|
|
||||||
def current_account
|
def current_account
|
||||||
@current_account ||= current_user.try(:account)
|
@current_account ||= current_user.try(:account)
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,6 +14,11 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
|
def update_resource(resource, params)
|
||||||
|
params[:password] = nil if Devise.pam_authentication && resource.encrypted_password.blank?
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
def build_resource(hash = nil)
|
def build_resource(hash = nil)
|
||||||
super(hash)
|
super(hash)
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,11 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
if session[:otp_user_id]
|
if session[:otp_user_id]
|
||||||
User.find(session[:otp_user_id])
|
User.find(session[:otp_user_id])
|
||||||
elsif user_params[:email]
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
# disabled :boolean default(FALSE), not null
|
# disabled :boolean default(FALSE), not null
|
||||||
# moderator :boolean default(FALSE), not null
|
# moderator :boolean default(FALSE), not null
|
||||||
# invite_id :integer
|
# invite_id :integer
|
||||||
|
# remember_token :string
|
||||||
#
|
#
|
||||||
|
|
||||||
class User < ApplicationRecord
|
class User < ApplicationRecord
|
||||||
|
@ -50,6 +51,8 @@ class User < ApplicationRecord
|
||||||
devise :registerable, :recoverable, :rememberable, :trackable, :validatable,
|
devise :registerable, :recoverable, :rememberable, :trackable, :validatable,
|
||||||
:confirmable
|
:confirmable
|
||||||
|
|
||||||
|
devise :pam_authenticatable
|
||||||
|
|
||||||
belongs_to :account, inverse_of: :user
|
belongs_to :account, inverse_of: :user
|
||||||
belongs_to :invite, counter_cache: :uses, optional: true
|
belongs_to :invite, counter_cache: :uses, optional: true
|
||||||
accepts_nested_attributes_for :account
|
accepts_nested_attributes_for :account
|
||||||
|
@ -84,6 +87,33 @@ class User < ApplicationRecord
|
||||||
|
|
||||||
attr_accessor :invite_code
|
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?
|
def confirmed?
|
||||||
confirmed_at.present?
|
confirmed_at.present?
|
||||||
end
|
end
|
||||||
|
@ -213,6 +243,45 @@ class User < ApplicationRecord
|
||||||
@invite_code = code
|
@invite_code = code
|
||||||
end
|
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
|
protected
|
||||||
|
|
||||||
def send_devise_notification(notification, *args)
|
def send_devise_notification(notification, *args)
|
||||||
|
|
|
@ -8,7 +8,13 @@ class UnreservedUsernameValidator < ActiveModel::Validator
|
||||||
|
|
||||||
private
|
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)
|
def reserved_username?(value)
|
||||||
|
return true if pam_controlled?(value)
|
||||||
return false unless Setting.reserved_usernames
|
return false unless Setting.reserved_usernames
|
||||||
Setting.reserved_usernames.include?(value.downcase)
|
Setting.reserved_usernames.include?(value.downcase)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
- content_for :page_title do
|
- content_for :page_title do
|
||||||
= t('auth.set_new_password')
|
= 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
|
= render 'shared/error_messages', object: resource
|
||||||
= 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' }
|
- if use_pam? || current_user.encrypted_password.present?
|
||||||
= 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 :reset_password_token, as: :hidden
|
||||||
|
|
||||||
.actions
|
= 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.button :button, t('auth.set_new_password'), type: :submit
|
= 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'
|
.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|
|
= 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
|
= 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') }
|
- if !use_pam? || current_user.encrypted_password.present?
|
||||||
= 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 :email, placeholder: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
|
||||||
= 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 :password, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.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' }
|
= 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
|
.actions
|
||||||
= f.button :button, t('generic.save_changes'), type: :submit
|
= f.button :button, t('generic.save_changes'), type: :submit
|
||||||
|
- else
|
||||||
|
= t('simple_form.labels.defaults.pam_account')
|
||||||
|
|
||||||
%hr/
|
%hr/
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,10 @@
|
||||||
= render partial: 'shared/og'
|
= render partial: 'shared/og'
|
||||||
|
|
||||||
= simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
|
= 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' }
|
= 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
|
.actions
|
||||||
|
|
|
@ -30,6 +30,19 @@ Warden::Manager.before_logout do |_, warden|
|
||||||
warden.cookies.delete('_session_id')
|
warden.cookies.delete('_session_id')
|
||||||
end
|
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|
|
Devise.setup do |config|
|
||||||
config.warden do |manager|
|
config.warden do |manager|
|
||||||
manager.default_strategies(scope: :user).unshift :two_factor_authenticatable
|
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
|
# given strategies, for example, `config.http_authenticatable = [:database]` will
|
||||||
# enable it only for database authentication. The supported strategies are:
|
# enable it only for database authentication. The supported strategies are:
|
||||||
# :database = Support basic authentication with authentication key + password
|
# :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.
|
# If 401 status code should be returned for AJAX requests. True by default.
|
||||||
# config.http_authenticatable_on_xhr = true
|
# config.http_authenticatable_on_xhr = true
|
||||||
|
@ -301,4 +314,23 @@ Devise.setup do |config|
|
||||||
# When using OmniAuth, Devise cannot automatically set OmniAuth path,
|
# When using OmniAuth, Devise cannot automatically set OmniAuth path,
|
||||||
# so you need to do it manually. For the users scope, it would be:
|
# so you need to do it manually. For the users scope, it would be:
|
||||||
# config.omniauth_path_prefix = '/my_engine/users/auth'
|
# 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
|
end
|
||||||
|
|
|
@ -53,6 +53,7 @@ de:
|
||||||
severity: Gewichtung
|
severity: Gewichtung
|
||||||
type: Importtyp
|
type: Importtyp
|
||||||
username: Profilname
|
username: Profilname
|
||||||
|
username_or_email: Profilname oder Email
|
||||||
interactions:
|
interactions:
|
||||||
must_be_follower: Benachrichtigungen von Nicht-Folgenden blockieren
|
must_be_follower: Benachrichtigungen von Nicht-Folgenden blockieren
|
||||||
must_be_following: Benachrichtigungen von Profilen blockieren, denen ich nicht folge
|
must_be_following: Benachrichtigungen von Profilen blockieren, denen ich nicht folge
|
||||||
|
|
|
@ -53,6 +53,7 @@ en:
|
||||||
severity: Severity
|
severity: Severity
|
||||||
type: Import type
|
type: Import type
|
||||||
username: Username
|
username: Username
|
||||||
|
username_or_email: Username or Email
|
||||||
interactions:
|
interactions:
|
||||||
must_be_follower: Block notifications from non-followers
|
must_be_follower: Block notifications from non-followers
|
||||||
must_be_following: Block notifications from people you don't follow
|
must_be_following: Block notifications from people you don't follow
|
||||||
|
|
|
@ -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.
|
# 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
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -486,6 +486,7 @@ ActiveRecord::Schema.define(version: 20180106000232) do
|
||||||
t.boolean "disabled", default: false, null: false
|
t.boolean "disabled", default: false, null: false
|
||||||
t.boolean "moderator", default: false, null: false
|
t.boolean "moderator", default: false, null: false
|
||||||
t.bigint "invite_id"
|
t.bigint "invite_id"
|
||||||
|
t.string "remember_token"
|
||||||
t.index ["account_id"], name: "index_users_on_account_id"
|
t.index ["account_id"], name: "index_users_on_account_id"
|
||||||
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
|
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
|
||||||
t.index ["email"], name: "index_users_on_email", unique: true
|
t.index ["email"], name: "index_users_on_email", unique: true
|
||||||
|
|
Reference in New Issue