mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
d17da65477
rubocop -A --only Style/FrozenStringLiteralComment,Layout/EmptyLineAfterMagicComment,Style/RedundantFreeze app/models
356 lines
9.9 KiB
Ruby
356 lines
9.9 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
#-- copyright
|
|
# OpenProject is an open source project management software.
|
|
# Copyright (C) the OpenProject GmbH
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License version 3.
|
|
#
|
|
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
|
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
# Copyright (C) 2010-2013 the ChiliProject Team
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License
|
|
# as published by the Free Software Foundation; either version 2
|
|
# of the License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
#
|
|
# See COPYRIGHT and LICENSE files for more details.
|
|
#++
|
|
|
|
require "net/ldap"
|
|
|
|
class LdapAuthSource < ApplicationRecord
|
|
class Error < ::StandardError; end
|
|
|
|
include Redmine::Ciphering
|
|
|
|
has_many :users,
|
|
dependent: :nullify
|
|
|
|
validates :name,
|
|
uniqueness: { case_sensitive: false },
|
|
length: { maximum: 60 }
|
|
|
|
def self.unique_attribute
|
|
:name
|
|
end
|
|
|
|
prepend ::Mixins::UniqueFinder
|
|
|
|
enum :tls_mode, {
|
|
plain_ldap: 0,
|
|
simple_tls: 1,
|
|
start_tls: 2
|
|
}, default: :start_tls
|
|
validates :tls_mode, inclusion: { in: tls_modes.keys }
|
|
|
|
validates :host, :port, :attr_login, presence: true
|
|
validates :name, :host, length: { maximum: 60, allow_nil: true }
|
|
validates :account, :account_password, :base_dn, length: { maximum: 255, allow_nil: true }
|
|
validates :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :attr_admin, length: { maximum: 30, allow_nil: true }
|
|
validates :port, numericality: { only_integer: true }
|
|
|
|
validate :validate_filter_string
|
|
validate :validate_tls_certificate_string, if: -> { tls_certificate_string.present? }
|
|
|
|
after_initialize :set_default_port
|
|
before_validation :strip_ldap_attributes
|
|
|
|
# Try to authenticate a user not yet registered against available sources
|
|
def self.authenticate(login, password)
|
|
where(onthefly_register: true).find_each do |source|
|
|
begin
|
|
Rails.logger.debug { "Authenticating '#{login}' against '#{source.name}'" }
|
|
user = source.authenticate(login, password)
|
|
rescue StandardError => e
|
|
Rails.logger.error "Error during authentication: #{e.message}"
|
|
user = nil
|
|
end
|
|
return user if user
|
|
end
|
|
nil
|
|
end
|
|
|
|
##
|
|
# Find a user by login in any of the available sources.
|
|
# If it's an onthefly_register ldap connection, this might implictly create the user.
|
|
def self.find_user(login)
|
|
find_each do |source|
|
|
Rails.logger.debug { "Looking up '#{login}' in '#{source.name}'" }
|
|
user = source.find_user login
|
|
return user if user
|
|
rescue StandardError => e
|
|
Rails.logger.error "Error during authentication: #{e.message}"
|
|
end
|
|
|
|
nil
|
|
end
|
|
|
|
def self.get_user_attributes(login)
|
|
where(onthefly_register: true).find_each do |source|
|
|
begin
|
|
Rails.logger.debug { "Looking up '#{login}' in '#{source.name}'" }
|
|
attrs = source.get_user_attributes login
|
|
rescue StandardError => e
|
|
Rails.logger.error "Error during authentication: #{e.message}"
|
|
attrs = nil
|
|
end
|
|
|
|
return attrs.except(:dn) if attrs
|
|
end
|
|
nil
|
|
end
|
|
|
|
def seeded_from_env?
|
|
Setting.seed_ldap&.key?(name)
|
|
end
|
|
|
|
def account_password
|
|
read_ciphered_attribute(:account_password)
|
|
end
|
|
|
|
def account_password=(arg)
|
|
write_ciphered_attribute(:account_password, arg)
|
|
end
|
|
|
|
def authenticate(login, password)
|
|
return nil if login.blank? || password.blank?
|
|
|
|
attrs = get_user_attributes(login)
|
|
|
|
if attrs && attrs[:dn] && authenticate_dn(attrs[:dn], password)
|
|
Rails.logger.debug { "Authentication successful for '#{login}'" }
|
|
synchronize_user(login, attrs)
|
|
end
|
|
rescue Net::LDAP::Error => e
|
|
raise LdapAuthSource::Error, "LdapError: #{e.message}"
|
|
end
|
|
|
|
def find_user(login)
|
|
return nil if login.blank?
|
|
|
|
attrs = get_user_attributes(login)
|
|
|
|
if attrs && attrs[:dn]
|
|
Rails.logger.debug { "Lookup successful for '#{login}'" }
|
|
synchronize_user(login, attrs)
|
|
end
|
|
rescue Net::LDAP::Error => e
|
|
raise LdapAuthSource::Error, "LdapError: #{e.message}"
|
|
end
|
|
|
|
# Get the user's dn and any attributes for them, given their login
|
|
def get_user_attributes(login)
|
|
ldap_con = initialize_ldap_con(account, account_password)
|
|
|
|
attrs = {}
|
|
|
|
filter = login_filter(login)
|
|
Rails.logger.debug do
|
|
"LDAP initializing search (BASE=#{base_dn}), (FILTER=#{filter})"
|
|
end
|
|
|
|
ldap_con.search(base: base_dn,
|
|
filter:,
|
|
attributes: search_attributes) do |entry|
|
|
attrs = get_user_attributes_from_ldap_entry(entry)
|
|
Rails.logger.debug { "DN found for #{login}: #{attrs[:dn]}" }
|
|
end
|
|
|
|
attrs
|
|
rescue Net::LDAP::Error => e
|
|
raise LdapAuthSource::Error, "LdapError: #{e.message}"
|
|
end
|
|
|
|
# Open and return a system connection
|
|
def with_connection
|
|
yield initialize_ldap_con(account, account_password)
|
|
end
|
|
|
|
# test the connection to the LDAP
|
|
def test_connection
|
|
unless authenticate_dn(account, account_password)
|
|
raise LdapAuthSource::Error,
|
|
I18n.t("ldap_auth_sources.ldap_error", error_message: I18n.t("ldap_auth_sources.ldap_auth_failed"))
|
|
end
|
|
rescue Net::LDAP::Error => e
|
|
raise LdapAuthSource::Error,
|
|
I18n.t("ldap_auth_sources.ldap_error", error_message: e.to_s)
|
|
end
|
|
|
|
def get_user_attributes_from_ldap_entry(entry)
|
|
base_attributes = {
|
|
dn: entry.dn,
|
|
ldap_auth_source_id: id
|
|
}
|
|
|
|
base_attributes.merge mapped_attributes(entry)
|
|
end
|
|
|
|
def mapped_attributes(entry)
|
|
%i[login firstname lastname mail admin].each_with_object({}) do |key, hash|
|
|
ldap_attribute = send(:"attr_#{key}")
|
|
next if ldap_attribute.blank?
|
|
|
|
val = LdapAuthSource.get_attr(entry, ldap_attribute)
|
|
val = !!ActiveRecord::Type::Boolean.new.cast(val) if key == :admin
|
|
hash[key] = val
|
|
end
|
|
end
|
|
|
|
# Return the attributes needed for the LDAP search.
|
|
#
|
|
def search_attributes
|
|
["dn", attr_login, attr_firstname, attr_lastname, attr_mail, attr_admin].compact
|
|
end
|
|
|
|
##
|
|
# Returns the filter object used for searching
|
|
def default_filter
|
|
object_filter = Net::LDAP::Filter.eq("objectClass", "*")
|
|
parsed_filter_string || object_filter
|
|
end
|
|
|
|
##
|
|
# Returns the filter object to search for a login
|
|
# adding the optional default filter
|
|
def login_filter(login)
|
|
Net::LDAP::Filter.eq(attr_login, login) & default_filter
|
|
end
|
|
|
|
def parsed_filter_string
|
|
Net::LDAP::Filter.from_rfc2254(filter_string) if filter_string.present?
|
|
end
|
|
|
|
def ldap_connection_options
|
|
{
|
|
host:,
|
|
port:,
|
|
force_no_page: OpenProject::Configuration.ldap_force_no_page,
|
|
encryption: ldap_encryption
|
|
}
|
|
end
|
|
|
|
def read_ldap_certificates
|
|
return if tls_certificate_string.blank?
|
|
|
|
# Using load will allow multiple PEM certificates to be passed
|
|
OpenSSL::X509::Certificate.load(tls_certificate_string)
|
|
end
|
|
|
|
private
|
|
|
|
def synchronize_user(login, attrs)
|
|
user = mapped_user(login)
|
|
|
|
# If onthefly_register is false, and the user is not found, do nothing
|
|
return if user.nil?
|
|
|
|
::Ldap::PostLoginSyncService
|
|
.new(self, user:, attributes: attrs.except(:dn))
|
|
.call
|
|
.result
|
|
end
|
|
|
|
def mapped_user(login)
|
|
User.find_by(login:, ldap_auth_source_id: id) || onthefly_user(login)
|
|
end
|
|
|
|
def onthefly_user(login)
|
|
return unless onthefly_register?
|
|
return if User.by_login(login).exists?
|
|
|
|
User.new(login:, ldap_auth_source_id: id)
|
|
end
|
|
|
|
def strip_ldap_attributes
|
|
%i[attr_login attr_firstname attr_lastname attr_mail attr_admin].each do |attr|
|
|
self[attr] = self[attr].strip unless self[attr].nil?
|
|
end
|
|
end
|
|
|
|
def initialize_ldap_con(ldap_user, ldap_password)
|
|
unless plain_ldap? || verify_peer?
|
|
Rails.logger.info { "SSL connection to LDAP host #{host} is set up to skip certificate verification." }
|
|
end
|
|
|
|
options = ldap_connection_options
|
|
unless ldap_user.blank? && ldap_password.blank?
|
|
options[:auth] = { method: :simple, username: ldap_user, password: ldap_password }
|
|
end
|
|
Net::LDAP.new options
|
|
end
|
|
|
|
def ldap_encryption
|
|
return nil if plain_ldap?
|
|
|
|
{
|
|
method: tls_mode.to_sym,
|
|
tls_options:
|
|
}
|
|
end
|
|
|
|
def cert_store
|
|
@cert_store ||= OpenSSL::X509::Store.new.tap do |store|
|
|
store.set_default_paths
|
|
provided_certs = Array(read_ldap_certificates)
|
|
provided_certs.each { |cert| store.add_cert cert }
|
|
end
|
|
end
|
|
|
|
def tls_options
|
|
{
|
|
verify_mode: tls_verify_mode,
|
|
cert_store:
|
|
}.compact
|
|
end
|
|
|
|
def tls_verify_mode
|
|
if verify_peer?
|
|
OpenSSL::SSL::VERIFY_PEER
|
|
else
|
|
OpenSSL::SSL::VERIFY_NONE
|
|
end
|
|
end
|
|
|
|
# Check if a DN (user record) authenticates with the password
|
|
def authenticate_dn(dn, password)
|
|
if dn.present? && password.present?
|
|
initialize_ldap_con(dn, password).bind
|
|
end
|
|
end
|
|
|
|
def self.get_attr(entry, attr_name)
|
|
if attr_name.present?
|
|
entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name]
|
|
end
|
|
end
|
|
|
|
def set_default_port
|
|
self.port = 389 if port.to_i == 0
|
|
end
|
|
|
|
def validate_filter_string
|
|
parsed_filter_string
|
|
rescue Net::LDAP::FilterSyntaxInvalidError
|
|
errors.add :filter_string, :invalid
|
|
end
|
|
|
|
def validate_tls_certificate_string
|
|
read_ldap_certificates
|
|
rescue OpenSSL::X509::CertificateError => e
|
|
errors.add :tls_certificate_string, :invalid_certificate, additional_message: e.message
|
|
end
|
|
end
|