about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2018-02-28 19:04:53 +0100
committerGitHub <noreply@github.com>2018-02-28 19:04:53 +0100
commit47bdb9b33b021c92bdfc6698914776eda13f6f77 (patch)
tree3827da1ebcf051b42f3b78df000955a93cd3498e
parente85287284611f7de431d9c51353125c265ebfe3f (diff)
Fix #942: Seamless LDAP login (#6556)
-rw-r--r--Gemfile1
-rw-r--r--Gemfile.lock2
-rw-r--r--app/controllers/application_controller.rb6
-rw-r--r--app/controllers/auth/sessions_controller.rb2
-rw-r--r--app/models/user.rb24
-rw-r--r--app/views/auth/passwords/edit.html.haml4
-rw-r--r--app/views/auth/registrations/edit.html.haml4
-rw-r--r--app/views/auth/sessions/new.html.haml2
-rw-r--r--config/application.rb1
-rw-r--r--config/initializers/devise.rb34
-rw-r--r--config/locales/en.yml1
-rw-r--r--lib/devise/ldap_authenticatable.rb49
12 files changed, 117 insertions, 13 deletions
diff --git a/Gemfile b/Gemfile
index fef7758cc..ed68534d4 100644
--- a/Gemfile
+++ b/Gemfile
@@ -33,6 +33,7 @@ gem 'devise', '~> 4.4'
 gem 'devise-two-factor', '~> 3.0'
 
 gem 'devise_pam_authenticatable2', '~> 8.0', install_if: -> { ENV['PAM_ENABLED'] == 'true' }
+gem 'net-ldap', '~> 0.10', install_if: -> { ENV['LDAP_ENABLED'] == 'true' }
 gem 'omniauth-cas', '~> 1.1', install_if: -> { ENV['CAS_ENABLED'] == 'true' }
 gem 'omniauth-saml', '~> 1.8', install_if: -> { ENV['SAML_ENABLED'] == 'true' }
 gem 'omniauth', '~> 1.2'
diff --git a/Gemfile.lock b/Gemfile.lock
index 14f713604..8af55e432 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -316,6 +316,7 @@ GEM
     multi_json (1.12.2)
     multipart-post (2.0.0)
     necromancer (0.4.0)
+    net-ldap (0.16.1)
     net-scp (1.2.1)
       net-ssh (>= 2.6.5)
     net-ssh (4.2.0)
@@ -666,6 +667,7 @@ DEPENDENCIES
   memory_profiler
   microformats (~> 4.0)
   mime-types (~> 3.1)
+  net-ldap (~> 0.10)
   nokogiri (~> 1.8)
   nsa (~> 0.2)
   oj (~> 3.3)
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 17c9dade8..6e5042617 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -14,7 +14,7 @@ class ApplicationController < ActionController::Base
   helper_method :current_session
   helper_method :current_theme
   helper_method :single_user_mode?
-  helper_method :use_pam?
+  helper_method :use_seamless_external_login?
 
   rescue_from ActionController::RoutingError, with: :not_found
   rescue_from ActiveRecord::RecordNotFound, with: :not_found
@@ -76,8 +76,8 @@ class ApplicationController < ActionController::Base
     @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.exists?
   end
 
-  def use_pam?
-    Devise.pam_authentication
+  def use_seamless_external_login?
+    Devise.pam_authentication || Devise.ldap_authentication
   end
 
   def current_account
diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb
index 42a3cb62c..02447dde0 100644
--- a/app/controllers/auth/sessions_controller.rb
+++ b/app/controllers/auth/sessions_controller.rb
@@ -37,7 +37,7 @@ 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?
+      if use_seamless_external_login? && 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])
diff --git a/app/models/user.rb b/app/models/user.rb
index b053292da..2995d6d54 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -52,7 +52,6 @@ class User < ApplicationRecord
   devise :registerable, :recoverable, :rememberable, :trackable, :validatable,
          :confirmable
 
-  devise :pam_authenticatable if Devise.pam_authentication
   devise :omniauthable
 
   belongs_to :account, inverse_of: :user
@@ -117,6 +116,12 @@ class User < ApplicationRecord
     acc.destroy! unless save
   end
 
+  def ldap_setup(_attributes)
+    self.confirmed_at = Time.now.utc
+    self.admin = false
+    save!
+  end
+
   def confirmed?
     confirmed_at.present?
   end
@@ -247,17 +252,17 @@ class User < ApplicationRecord
   end
 
   def password_required?
-    return false if Devise.pam_authentication
+    return false if Devise.pam_authentication || Devise.ldap_authentication
     super
   end
 
   def send_reset_password_instructions
-    return false if encrypted_password.blank? && Devise.pam_authentication
+    return false if encrypted_password.blank? && (Devise.pam_authentication || Devise.ldap_authentication)
     super
   end
 
   def reset_password!(new_password, new_password_confirmation)
-    return false if encrypted_password.blank? && Devise.pam_authentication
+    return false if encrypted_password.blank? && (Devise.pam_authentication || Devise.ldap_authentication)
     super
   end
 
@@ -280,6 +285,17 @@ class User < ApplicationRecord
     end
   end
 
+  def self.ldap_get_user(attributes = {})
+    resource = joins(:account).find_by(accounts: { username: attributes[Devise.ldap_uid.to_sym].first })
+
+    if resource.blank?
+      resource = new(email: attributes[:mail].first, account_attributes: { username: attributes[Devise.ldap_uid.to_sym].first })
+      resource.ldap_setup(attributes)
+    end
+
+    resource
+  end
+
   def self.authenticate_with_pam(attributes = {})
     return nil unless Devise.pam_authentication
     super
diff --git a/app/views/auth/passwords/edit.html.haml b/app/views/auth/passwords/edit.html.haml
index 703c821c0..12880c227 100644
--- a/app/views/auth/passwords/edit.html.haml
+++ b/app/views/auth/passwords/edit.html.haml
@@ -4,7 +4,7 @@
 = 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? || resource.encrypted_password.present?
+  - if !use_seamless_external_login?? || resource.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' }
@@ -13,6 +13,6 @@
     .actions
       = f.button :button, t('auth.set_new_password'), type: :submit
   - else
-    = t('simple_form.labels.defaults.pam_account')
+    %p.hint= t('users.seamless_external_login')
 
 .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 ca18caa56..fac702b38 100644
--- a/app/views/auth/registrations/edit.html.haml
+++ b/app/views/auth/registrations/edit.html.haml
@@ -4,7 +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? || resource.encrypted_password.present?
+  - if !use_seamless_external_login? || resource.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' }
@@ -13,7 +13,7 @@
     .actions
       = f.button :button, t('generic.save_changes'), type: :submit
   - else
-    = t('simple_form.labels.defaults.pam_account')
+    %p.hint= t('users.seamless_external_login')
 
 %hr/
 
diff --git a/app/views/auth/sessions/new.html.haml b/app/views/auth/sessions/new.html.haml
index 1c3a0b6b4..0c9f9d5fe 100644
--- a/app/views/auth/sessions/new.html.haml
+++ b/app/views/auth/sessions/new.html.haml
@@ -5,7 +5,7 @@
   = render partial: 'shared/og'
 
 = simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
-  - if use_pam?
+  - if use_seamless_external_login?
     = 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') }
diff --git a/config/application.rb b/config/application.rb
index cd180782c..34b9dcf48 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -12,6 +12,7 @@ require_relative '../lib/paperclip/gif_transcoder'
 require_relative '../lib/paperclip/video_transcoder'
 require_relative '../lib/mastodon/snowflake'
 require_relative '../lib/mastodon/version'
+require_relative '../lib/devise/ldap_authenticatable'
 
 Dotenv::Railtie.load
 
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index ba7ad9e6c..0dc202976 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -36,6 +36,26 @@ module Devise
   mattr_accessor :pam_controlled_service
   @@pam_controlled_service = nil
 
+  mattr_accessor :check_at_sign
+  @@check_at_sign = false
+
+  mattr_accessor :ldap_authentication
+  @@ldap_authentication = false
+  mattr_accessor :ldap_host
+  @@ldap_host = nil
+  mattr_accessor :ldap_port
+  @@ldap_port = nil
+  mattr_accessor :ldap_method
+  @@ldap_method = nil
+  mattr_accessor :ldap_base
+  @@ldap_base = nil
+  mattr_accessor :ldap_uid
+  @@ldap_uid = nil
+  mattr_accessor :ldap_bind_dn
+  @@ldap_bind_dn = nil
+  mattr_accessor :ldap_password
+  @@ldap_password = nil
+
   class Strategies::PamAuthenticatable
     def valid?
       super && ::Devise.pam_authentication
@@ -45,6 +65,8 @@ end
 
 Devise.setup do |config|
   config.warden do |manager|
+    manager.default_strategies(scope: :user).unshift :ldap_authenticatable if Devise.ldap_authentication
+    manager.default_strategies(scope: :user).unshift :pam_authenticatable  if Devise.pam_authentication
     manager.default_strategies(scope: :user).unshift :two_factor_authenticatable
     manager.default_strategies(scope: :user).unshift :two_factor_backupable
   end
@@ -324,4 +346,16 @@ Devise.setup do |config|
     config.pam_default_service    = ENV.fetch('PAM_DEFAULT_SERVICE') { 'rpam' }
     config.pam_controlled_service = ENV.fetch('PAM_CONTROLLED_SERVICE') { 'rpam' }
   end
+
+  if ENV['LDAP_ENABLED'] == 'true'
+    config.ldap_authentication = true
+    config.check_at_sign       = true
+    config.ldap_host           = ENV.fetch('LDAP_HOST', 'localhost')
+    config.ldap_port           = ENV.fetch('LDAP_PORT', 389).to_i
+    config.ldap_method         = ENV.fetch('LDAP_METHOD', :simple_tls).to_sym
+    config.ldap_base           = ENV.fetch('LDAP_BASE')
+    config.ldap_bind_dn        = ENV.fetch('LDAP_BIND_DN')
+    config.ldap_password       = ENV.fetch('LDAP_PASSWORD')
+    config.ldap_uid            = ENV.fetch('LDAP_UID', 'cn')
+  end
 end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 026426c84..797ec6ac1 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -769,4 +769,5 @@ en:
   users:
     invalid_email: The e-mail address is invalid
     invalid_otp_token: Invalid two-factor code
+    seamless_external_login: You are logged in via an external service, so password and e-mail settings are not available.
     signed_in_as: 'Signed in as:'
diff --git a/lib/devise/ldap_authenticatable.rb b/lib/devise/ldap_authenticatable.rb
new file mode 100644
index 000000000..531abdbbe
--- /dev/null
+++ b/lib/devise/ldap_authenticatable.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+if ENV['LDAP_ENABLED'] == 'true'
+  require 'net/ldap'
+  require 'devise/strategies/authenticatable'
+
+  module Devise
+    module Strategies
+      class LdapAuthenticatable < Authenticatable
+        def authenticate!
+          if params[:user]
+            ldap = Net::LDAP.new(
+              host: Devise.ldap_host,
+              port: Devise.ldap_port,
+              base: Devise.ldap_base,
+              encryption: {
+                method: Devise.ldap_method,
+                tls_options: OpenSSL::SSL::SSLContext::DEFAULT_PARAMS,
+              },
+              auth: {
+                method: :simple,
+                username: Devise.ldap_bind_dn,
+                password: Devise.ldap_password,
+              },
+              connect_timeout: 10
+            )
+
+            if (user_info = ldap.bind_as(base: Devise.ldap_base, filter: "(#{Devise.ldap_uid}=#{email})", password: password))
+              user = User.ldap_get_user(user_info.first)
+              success!(user)
+            else
+              return fail(:invalid_login)
+            end
+          end
+        end
+
+        def email
+          params[:user][:email]
+        end
+
+        def password
+          params[:user][:password]
+        end
+      end
+    end
+  end
+
+  Warden::Strategies.add(:ldap_authenticatable, Devise::Strategies::LdapAuthenticatable)
+end