From 47bdb9b33b021c92bdfc6698914776eda13f6f77 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 28 Feb 2018 19:04:53 +0100 Subject: [PATCH] Fix #942: Seamless LDAP login (#6556) --- Gemfile | 1 + Gemfile.lock | 2 + app/controllers/application_controller.rb | 6 +-- app/controllers/auth/sessions_controller.rb | 2 +- app/models/user.rb | 24 ++++++++-- app/views/auth/passwords/edit.html.haml | 4 +- app/views/auth/registrations/edit.html.haml | 4 +- app/views/auth/sessions/new.html.haml | 2 +- config/application.rb | 1 + config/initializers/devise.rb | 34 ++++++++++++++ config/locales/en.yml | 1 + lib/devise/ldap_authenticatable.rb | 49 +++++++++++++++++++++ 12 files changed, 117 insertions(+), 13 deletions(-) create mode 100644 lib/devise/ldap_authenticatable.rb 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