2021-06-01 12:20:20 +00:00
|
|
|
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
|
|
|
|
2017-04-19 10:09:54 +00:00
|
|
|
class Ldap
|
|
|
|
|
|
|
|
# Class for handling LDAP Groups.
|
|
|
|
class User
|
|
|
|
include Ldap::FilterLookup
|
|
|
|
|
2021-07-12 13:18:31 +00:00
|
|
|
IGNORED_ATTRIBUTES = %i[
|
2017-11-23 08:09:44 +00:00
|
|
|
admincount
|
|
|
|
accountexpires
|
|
|
|
badpasswordtime
|
|
|
|
badpwdcount
|
|
|
|
countrycode
|
|
|
|
distinguishedname
|
|
|
|
dnshostname
|
|
|
|
dscorepropagationdata
|
|
|
|
instancetype
|
|
|
|
iscriticalsystemobject
|
|
|
|
useraccountcontrol
|
|
|
|
usercertificate
|
|
|
|
objectclass
|
|
|
|
objectcategory
|
|
|
|
objectsid
|
|
|
|
primarygroupid
|
|
|
|
pwdlastset
|
|
|
|
lastlogoff
|
|
|
|
lastlogon
|
|
|
|
lastlogontimestamp
|
|
|
|
localpolicyflags
|
|
|
|
lockouttime
|
|
|
|
logoncount
|
|
|
|
logonhours
|
|
|
|
msdfsr-computerreferencebl
|
|
|
|
msds-supportedencryptiontypes
|
|
|
|
ridsetreferences
|
|
|
|
samaccounttype
|
|
|
|
memberof
|
|
|
|
serverreferencebl
|
|
|
|
serviceprincipalname
|
|
|
|
showinadvancedviewonly
|
|
|
|
usnchanged
|
|
|
|
usncreated
|
|
|
|
whenchanged
|
|
|
|
whencreated
|
2017-04-19 10:09:54 +00:00
|
|
|
].freeze
|
|
|
|
|
|
|
|
# Returns the uid attribute.
|
|
|
|
#
|
|
|
|
# @param attributes [Hash{Symbol=>Array<String>}] A list of LDAP User attributes which should get checked for available uids.
|
|
|
|
#
|
|
|
|
# @example
|
|
|
|
# Ldap::User.uid_attribute(attributes)
|
|
|
|
#
|
|
|
|
# @return [String] The uid attribute.
|
|
|
|
def self.uid_attribute(attributes)
|
|
|
|
result = nil
|
2018-01-12 11:53:43 +00:00
|
|
|
%i[objectguid entryuuid samaccountname userprincipalname uid dn].each do |attribute|
|
2017-04-19 10:09:54 +00:00
|
|
|
next if attributes[attribute].blank?
|
2018-10-09 06:17:41 +00:00
|
|
|
|
2017-04-19 10:09:54 +00:00
|
|
|
result = attribute.to_s
|
|
|
|
break
|
2017-10-01 12:25:52 +00:00
|
|
|
end
|
2017-04-19 10:09:54 +00:00
|
|
|
result
|
|
|
|
end
|
|
|
|
|
|
|
|
# Initializes a wrapper around Net::LDAP and ::Ldap to handle LDAP users.
|
|
|
|
#
|
|
|
|
# @param [Hash] config the configuration for establishing a LDAP connection. Default is Setting 'ldap_config'.
|
|
|
|
# @option config [String] :uid_attribute The uid attribute. Default is determined automatically.
|
|
|
|
# @option config [String] :filter The filter for LDAP users. Default is determined automatically.
|
|
|
|
# @param ldap [Ldap] An optional existing Ldap class instance. Default is a new connection with given configuration.
|
|
|
|
#
|
|
|
|
# @example
|
|
|
|
# ldap_user = Ldap::User.new
|
|
|
|
#
|
|
|
|
# @return [nil]
|
|
|
|
def initialize(config = nil, ldap: nil)
|
2018-01-22 15:53:53 +00:00
|
|
|
@config = config || Setting.get('ldap_config')
|
|
|
|
@ldap = ldap || ::Ldap.new(@config)
|
2017-04-19 10:09:54 +00:00
|
|
|
|
2018-01-22 15:53:53 +00:00
|
|
|
handle_config
|
2017-04-19 10:09:54 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# Checks if given username and password combination is valid for the connected LDAP.
|
|
|
|
#
|
|
|
|
# @param username [String] The username.
|
|
|
|
# @param password [String] The password.
|
|
|
|
#
|
|
|
|
# @example
|
|
|
|
# ldap_user.valid?('example_user', 'pw1234')
|
|
|
|
# #=> true
|
|
|
|
#
|
|
|
|
# @return [Boolean] The valid state of the username and password combination.
|
|
|
|
def valid?(username, password)
|
|
|
|
bind_success = @ldap.connection.bind_as(
|
2018-12-19 17:31:51 +00:00
|
|
|
base: @ldap.base_dn,
|
2021-09-08 10:02:59 +00:00
|
|
|
filter: @user_filter ? "(&(#{login_attribute}=#{username})#{@user_filter})" : "(#{login_attribute}=#{username})",
|
2017-04-19 10:09:54 +00:00
|
|
|
password: password
|
|
|
|
)
|
|
|
|
|
|
|
|
message = bind_success ? 'successful' : 'failed'
|
2018-01-22 15:53:53 +00:00
|
|
|
Rails.logger.info "ldap authentication for user '#{username}' (#{login_attribute}) #{message}!"
|
2017-04-19 10:09:54 +00:00
|
|
|
bind_success.present?
|
|
|
|
end
|
|
|
|
|
|
|
|
# Determines possible User attributes with example values.
|
|
|
|
#
|
|
|
|
# @param filter [String] The filter for listing users. Default is initialization parameter.
|
|
|
|
# @param base_dn [String] The applied base DN for listing users. Default is Ldap#base_dn.
|
|
|
|
#
|
|
|
|
# @example
|
|
|
|
# ldap_user.attributes
|
|
|
|
# #=> {:dn=>"dn (e. g. CN=Administrator,CN=Users,DC=domain,DC=tld)", ...}
|
|
|
|
#
|
|
|
|
# @return [Hash{Symbol=>String}] The available User attributes as key and the name and an example as value.
|
2018-08-31 15:27:57 +00:00
|
|
|
def attributes(custom_filter: nil, base_dn: nil)
|
|
|
|
@attributes ||= begin
|
|
|
|
attributes = {}.with_indifferent_access
|
|
|
|
lookup_counter = 0
|
2017-04-19 10:09:54 +00:00
|
|
|
|
2018-08-31 15:27:57 +00:00
|
|
|
# collect sample attributes
|
|
|
|
@ldap.search(custom_filter || filter, base: base_dn) do |entry|
|
|
|
|
pre_merge_count = attributes.count
|
2017-04-19 10:09:54 +00:00
|
|
|
|
2018-08-31 15:27:57 +00:00
|
|
|
attributes.reverse_merge!(entry.to_h
|
2021-07-12 13:18:31 +00:00
|
|
|
.except(*IGNORED_ATTRIBUTES)
|
2018-08-31 15:27:57 +00:00
|
|
|
.transform_values(&:first)
|
|
|
|
.compact)
|
2017-04-19 10:09:54 +00:00
|
|
|
|
2018-08-31 15:27:57 +00:00
|
|
|
# check max 50 entries with the same attributes in a row
|
|
|
|
lookup_counter = (pre_merge_count < attributes.count ? 0 : lookup_counter.next)
|
|
|
|
break if lookup_counter >= 50
|
2017-04-19 10:09:54 +00:00
|
|
|
end
|
|
|
|
|
2018-08-31 15:27:57 +00:00
|
|
|
# format sample values for presentation
|
|
|
|
attributes.each do |name, value|
|
|
|
|
attributes[name] = if value.encoding == Encoding.find('ascii-8bit')
|
|
|
|
"#{name} (binary data)"
|
|
|
|
else
|
|
|
|
"#{name} (e.g., #{value.utf8_encode})"
|
|
|
|
end
|
2017-04-19 10:09:54 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# The active filter of the instance. If none give on initialization an automatic lookup is performed.
|
|
|
|
#
|
|
|
|
# @example
|
|
|
|
# ldap_user.filter
|
|
|
|
# #=> '(objectClass=user)'
|
|
|
|
#
|
|
|
|
# @return [String, nil] The active or found filter or nil if none could be found.
|
|
|
|
def filter
|
2017-06-12 08:04:27 +00:00
|
|
|
@filter ||= lookup_filter(['(&(objectClass=user)(samaccountname=*)(!(samaccountname=*$)))', '(objectClass=user)', '(objectClass=posixaccount)', '(objectClass=person)'])
|
2017-04-19 10:09:54 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# The active uid attribute of the instance. If none give on initialization an automatic lookup is performed.
|
|
|
|
#
|
|
|
|
# @example
|
|
|
|
# ldap_user.uid_attribute
|
|
|
|
# #=> 'samaccountname'
|
|
|
|
#
|
|
|
|
# @return [String, nil] The active or found uid attribute or nil if none could be found.
|
|
|
|
def uid_attribute
|
|
|
|
@uid_attribute ||= self.class.uid_attribute(attributes)
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
2018-01-22 15:53:53 +00:00
|
|
|
attr_reader :config
|
|
|
|
|
|
|
|
def login_attribute
|
|
|
|
@login_attribute ||= config[:user_attributes]&.key('login') || uid_attribute
|
|
|
|
end
|
|
|
|
|
|
|
|
def handle_config
|
2017-04-19 10:09:54 +00:00
|
|
|
return if config.blank?
|
2018-10-09 06:17:41 +00:00
|
|
|
|
2017-04-19 10:09:54 +00:00
|
|
|
@uid_attribute = config[:uid_attribute]
|
|
|
|
@filter = config[:filter]
|
2021-09-08 10:02:59 +00:00
|
|
|
@user_filter = config[:user_filter]
|
2017-04-19 10:09:54 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|