about summary refs log tree commit diff
diff options
context:
space:
mode:
authorAlexander <devkral@web.de>2018-02-02 10:18:55 +0100
committerEugen Rochko <eugen@zeonfederated.com>2018-02-02 10:18:55 +0100
commit04fef7b8886bb78f3473e143894a521ca578f1db (patch)
treed73cf1a48aeb370e752078f43dbd423043229dbc
parent1afc70c990d4d23e5fac57de9cb579c396a82b5c (diff)
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
-rw-r--r--Gemfile3
-rw-r--r--Gemfile.lock5
-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/models/user.rb69
-rw-r--r--app/validators/unreserved_username_validator.rb6
-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--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
15 files changed, 164 insertions, 17 deletions
diff --git a/Gemfile b/Gemfile
index eaa1d29de..f3844aca6 100644
--- 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
index b3bd6fcb0..7da9bfe39 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)
@@ -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
index e1aae0b67..b38a68467 100644
--- 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
index b8ff4e54f..417e2b63b 100644
--- 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
index a5acb6c36..4fc41b378 100644
--- 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
index 40c298b1a..fa4ebfc71 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/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/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/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 143daaa29..c56334d56 100644
--- 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
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 d1722fa29..a411de20f 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"
@@ -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