1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
|
module Gitlab
module LDAP
class Person
# Active Directory-specific LDAP filter that checks if bit 2 of the
# userAccountControl attribute is set.
# Source: http://ctogonewild.com/2009/09/03/bitmask-searches-in-ldap/
AD_USER_DISABLED = Net::LDAP::Filter.ex("userAccountControl:1.2.840.113556.1.4.803", "2")
attr_accessor :entry, :provider
def self.find_by_uid(uid, adapter)
uid = Net::LDAP::Filter.escape(uid)
adapter.user(adapter.config.uid, uid)
end
def self.find_by_dn(dn, adapter)
adapter.user('dn', dn)
end
def self.find_by_email(email, adapter)
email_fields = adapter.config.attributes['email']
adapter.user(email_fields, email)
end
def self.disabled_via_active_directory?(dn, adapter)
adapter.dn_matches_filter?(dn, AD_USER_DISABLED)
end
def self.ldap_attributes(config)
[
'dn', # Used in `dn`
config.uid, # Used in `uid`
*config.attributes['name'], # Used in `name`
*config.attributes['email'] # Used in `email`
]
end
# Returns the UID or DN in a normalized form
def self.normalize_uid_or_dn(uid_or_dn)
if dn?(uid_or_dn)
normalize_dn(uid_or_dn)
else
normalize_uid(uid_or_dn)
end
end
# Returns true if the string looks like a DN rather than a UID.
#
# An empty string is technically a valid DN (null DN), although we should
# never need to worry about that.
def self.dn?(uid_or_dn)
uid_or_dn.blank? || !!uid_or_dn.match(/(?<!\\)=/)
end
# Returns the UID in a normalized form.
#
# 1. Excess spaces are stripped
# 2. The string is downcased (for case-insensitivity)
def self.normalize_uid(uid)
normalize_dn_part(uid)
end
# Returns the DN in a normalized form.
#
# 1. Excess spaces around attribute names and values are stripped
# 2. The string is downcased (for case-insensitivity)
def self.normalize_dn(dn)
dn.split(/(?<!\\)([,+=])/).map do |part|
normalize_dn_part(part)
end.join('')
end
def initialize(entry, provider)
Rails.logger.debug { "Instantiating #{self.class.name} with LDIF:\n#{entry.to_ldif}" }
@entry = entry
@provider = provider
end
def name
attribute_value(:name).first
end
def uid
entry.public_send(config.uid).first # rubocop:disable GitlabSecurity/PublicSend
end
def username
uid
end
def email
attribute_value(:email)
end
def dn
self.class.normalize_dn(entry.dn)
end
private
def self.normalize_dn_part(part)
cleaned = part.strip.downcase
if cleaned.ends_with?('\\')
# If it ends with an escape character that is not followed by a
# character to be escaped, then this part may be malformed. But let's
# not worry too much about it, and just return it unmodified.
#
# Why? Because the reason we clean DNs is to make our simplistic
# string comparisons work better, even though there are all kinds of
# ways that equivalent DNs can vary as strings. If we run into a
# strange DN, we should just try to work with it.
#
# See https://www.ldap.com/ldap-dns-and-rdns for more.
return part unless part.ends_with?(' ')
# Ends with an escaped space (which is valid).
cleaned = cleaned + ' '
end
# Get rid of blanks. This can happen if a split character is followed by
# whitespace and then another split character.
#
# E.g. this DN: 'uid=john+telephoneNumber= +1 555-555-5555'
#
# Should be returned as: 'uid=john+telephoneNumber=+1 555-555-5555'
cleaned = '' if cleaned.blank?
cleaned
end
def entry
@entry
end
def config
@config ||= Gitlab::LDAP::Config.new(provider)
end
# Using the LDAP attributes configuration, find and return the first
# attribute with a value. For example, by default, when given 'email',
# this method looks for 'mail', 'email' and 'userPrincipalName' and
# returns the first with a value.
def attribute_value(attribute)
attributes = Array(config.attributes[attribute.to_s])
selected_attr = attributes.find { |attr| entry.respond_to?(attr) }
return nil unless selected_attr
entry.public_send(selected_attr) # rubocop:disable GitlabSecurity/PublicSend
end
end
end
end
|