/
ldap.rb
169 lines (141 loc) · 6.68 KB
/
ldap.rb
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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
require 'casserver/authenticators/base'
begin
require 'net/ldap'
rescue LoadError
require 'rubygems'
begin
gem 'net-ldap', '~> 0.1.1'
rescue Gem::LoadError
$stderr.puts
$stderr.puts "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
$stderr.puts
$stderr.puts "To use the LDAP/AD authenticator, you must first install the 'net-ldap' gem."
$stderr.puts " See http://github.com/RoryO/ruby-net-ldap for details."
$stderr.puts
$stderr.puts "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
exit 1
end
require 'net/ldap'
end
# Basic LDAP authenticator. Should be compatible with OpenLDAP and other similar LDAP servers,
# although it hasn't been officially tested. See example config file for details on how
# to configure it.
class CASServer::Authenticators::LDAP < CASServer::Authenticators::Base
def validate(credentials)
read_standard_credentials(credentials)
return false if @password.blank?
raise CASServer::AuthenticatorError, "Cannot validate credentials because the authenticator hasn't yet been configured" unless @options
raise CASServer::AuthenticatorError, "Invalid LDAP authenticator configuration!" unless @options[:ldap]
raise CASServer::AuthenticatorError, "You must specify a server host in the LDAP configuration!" unless @options[:ldap][:host] || @options[:ldap][:server]
raise CASServer::AuthenticatorError, "The username '#{@username}' contains invalid characters." if (@username =~ /[*\(\)\0\/]/)
preprocess_username
@ldap = Net::LDAP.new
@options[:ldap][:host] ||= @options[:ldap][:server]
@ldap.host = @options[:ldap][:host]
@ldap.port = @options[:ldap][:port] if @options[:ldap][:port]
@ldap.encryption(@options[:ldap][:encryption].intern) if @options[:ldap][:encryption]
begin
if @options[:ldap][:auth_user]
bind_success = bind_by_username_with_preauthentication
else
bind_success = bind_by_username
end
return false unless bind_success
entry = find_user
extract_extra_attributes(entry)
return true
rescue Net::LDAP::LdapError => e
raise CASServer::AuthenticatorError,
"LDAP authentication failed with '#{e}'. Check your authenticator configuration."
end
end
protected
def default_username_attribute
"cn"
end
private
# Add prefix to username, if :username_prefix was specified in the :ldap config.
def preprocess_username
@username = @options[:ldap][:username_prefix] + @username if @options[:ldap][:username_prefix]
end
# Attempt to bind with the LDAP server using the username and password entered by
# the user. If a :filter was specified in the :ldap config, the filter will be
# added to the LDAP query for the username.
def bind_by_username
username_attribute = options[:ldap][:username_attribute] || default_username_attribute
@ldap.bind_as(:base => @options[:ldap][:base], :password => @password, :filter => user_filter)
end
# If an auth_user is specified, we will connect ("pre-authenticate") with the
# LDAP server using the authenticator account, and then attempt to bind as the
# user who is actually trying to authenticate. Note that you need to set up
# the special authenticator account first. Also, auth_user must be the authenticator
# user's full CN, which is probably not the same as their username.
#
# This pre-authentication process is necessary because binding can only be done
# using the CN, so having just the username is not enough. We connect as auth_user,
# and then try to find the target user's CN based on the given username. Then we bind
# as the target user to validate their credentials.
def bind_by_username_with_preauthentication
raise CASServer::AuthenticatorError, "A password must be specified in the configuration for the authenticator user!" unless
@options[:ldap][:auth_password]
@ldap.authenticate(@options[:ldap][:auth_user], @options[:ldap][:auth_password])
@ldap.bind_as(:base => @options[:ldap][:base], :password => @password, :filter => user_filter)
end
# Combine the filter for finding the user with the optional extra filter specified in the config
# (if any).
def user_filter
username_attribute = options[:ldap][:username_attribute] || default_username_attribute
filter = Array(username_attribute).map { |ua| Net::LDAP::Filter.eq(ua, @username) }.reduce(:|)
unless @options[:ldap][:filter].blank?
filter &= Net::LDAP::Filter.construct(@options[:ldap][:filter])
end
filter
end
# Finds the user based on the user_filter (this is called after authentication).
# We do this to make it possible to extract extra_attributes.
def find_user
results = @ldap.search( :base => options[:ldap][:base], :filter => user_filter)
return results.first
end
def find_groups(groups)
filter = Array(groups).map { |group| Net::LDAP::Filter.eq('distinguishedName', group) }.reduce(:|)
return @ldap.search(:base => options[:ldap][:base], :filter => filter)
end
def expand_indirect_membership(groups)
indirect_memberof = find_groups(groups).collect { |group| group['memberof'] }.flatten - groups
if indirect_memberof.any?
$LOG.debug "#{self.class}: Indirect memberof: #{indirect_memberof.inspect}"
groups |= indirect_memberof
expand_indirect_membership(groups)
else
groups
end
end
def extract_extra_attributes(ldap_entry)
@extra_attributes = {}
extra_attributes_to_extract.each do |attr|
v = !ldap_entry[attr].blank? && ldap_entry[attr].first
if v
if ldap_entry[attr].kind_of?(Array)
@extra_attributes[attr] = []
ldap_entry[attr].each do |a|
@extra_attributes[attr] << a.to_s
end
else
@extra_attributes[attr] = v.to_s
end
if attr == 'memberof' && @options[:ldap][:expand_indirect_membership]
$LOG.debug "#{self.class}: Direct memberof: #{@extra_attributes[attr].inspect}"
@extra_attributes[attr] = expand_indirect_membership(@extra_attributes[attr])
$LOG.debug "#{self.class}: memberof: #{@extra_attributes[attr].inspect}"
end
end
end
if @extra_attributes.empty?
$LOG.warn("#{self.class}: Did not read any extra_attributes for user #{@username.inspect} even though an :extra_attributes option was provided.")
else
$LOG.debug("#{self.class}: Read the following extra_attributes for user #{@username.inspect}: #{@extra_attributes.inspect}")
end
ldap_entry
end
end