Mercurial > prosody-modules
annotate mod_lib_ldap/ldap.lib.lua @ 5298:12f7d8b901e0
mod_audit: Support for adding location (GeoIP) to audit events
This can be more privacy-friendly than logging full IP addresses, and also
more informative to a user - IP addresses don't mean much to the average
person, however if they see activity from outside their expected country, they
can immediately identify suspicious activity.
As with IPs, this field is configurable for deployments that would like to
disable it. Location is also not logged when the geoip library is not
available.
author | Matthew Wild <mwild1@gmail.com> |
---|---|
date | Sat, 01 Apr 2023 13:11:53 +0100 |
parents | 66b3085ecc49 |
children |
rev | line source |
---|---|
809 | 1 -- vim:sts=4 sw=4 |
2 | |
3 -- Prosody IM | |
4 -- Copyright (C) 2008-2010 Matthew Wild | |
5 -- Copyright (C) 2008-2010 Waqas Hussain | |
6 -- Copyright (C) 2012 Rob Hoelz | |
7 -- | |
8 -- This project is MIT/X11 licensed. Please see the | |
9 -- COPYING file in the source package for more information. | |
10 -- | |
11 | |
12 local ldap; | |
13 local connection; | |
14 local params = module:get_option("ldap"); | |
15 local format = string.format; | |
16 local tconcat = table.concat; | |
17 | |
18 local _M = {}; | |
19 | |
20 local config_params = { | |
21 hostname = 'string', | |
22 user = { | |
23 basedn = 'string', | |
24 namefield = 'string', | |
25 filter = 'string', | |
26 usernamefield = 'string', | |
27 }, | |
28 groups = { | |
29 basedn = 'string', | |
30 namefield = 'string', | |
31 memberfield = 'string', | |
32 | |
33 _member = { | |
34 name = 'string', | |
35 admin = 'boolean?', | |
36 }, | |
37 }, | |
38 admin = { | |
39 _optional = true, | |
40 basedn = 'string', | |
41 namefield = 'string', | |
42 filter = 'string', | |
43 } | |
44 } | |
45 | |
46 local function run_validation(params, config, prefix) | |
47 prefix = prefix or ''; | |
48 | |
49 -- verify that every required member of config is present in params | |
50 for k, v in pairs(config) do | |
51 if type(k) == 'string' and k:sub(1, 1) ~= '_' then | |
52 local is_optional; | |
53 if type(v) == 'table' then | |
54 is_optional = v._optional; | |
55 else | |
56 is_optional = v:sub(-1) == '?'; | |
57 end | |
58 | |
59 if not is_optional and params[k] == nil then | |
60 return nil, prefix .. k .. ' is required'; | |
61 end | |
62 end | |
63 end | |
64 | |
65 for k, v in pairs(params) do | |
66 local expected_type = config[k]; | |
67 | |
68 local ok, err = true; | |
69 | |
70 if type(k) == 'string' then | |
71 -- verify that this key is present in config | |
72 if k:sub(1, 1) == '_' or expected_type == nil then | |
73 return nil, 'invalid parameter ' .. prefix .. k; | |
74 end | |
75 | |
76 -- type validation | |
77 if type(expected_type) == 'string' then | |
78 if expected_type:sub(-1) == '?' then | |
79 expected_type = expected_type:sub(1, -2); | |
80 end | |
81 | |
82 if type(v) ~= expected_type then | |
83 return nil, 'invalid type for parameter ' .. prefix .. k; | |
84 end | |
85 else -- it's a table (or had better be) | |
86 if type(v) ~= 'table' then | |
87 return nil, 'invalid type for parameter ' .. prefix .. k; | |
88 end | |
89 | |
90 -- recurse into child | |
91 ok, err = run_validation(v, expected_type, prefix .. k .. '.'); | |
92 end | |
93 else -- it's an integer (or had better be) | |
94 if not config._member then | |
95 return nil, 'invalid parameter ' .. prefix .. tostring(k); | |
96 end | |
97 ok, err = run_validation(v, config._member, prefix .. tostring(k) .. '.'); | |
98 end | |
99 | |
100 if not ok then | |
101 return ok, err; | |
102 end | |
103 end | |
104 | |
105 return true; | |
106 end | |
107 | |
108 local function validate_config() | |
109 if true then | |
110 return true; -- XXX for now | |
111 end | |
112 | |
113 -- this is almost too clever (I mean that in a bad | |
114 -- maintainability sort of way) | |
115 -- | |
116 -- basically this allows a free pass for a key in group members | |
117 -- equal to params.groups.namefield | |
118 setmetatable(config_params.groups._member, { | |
119 __index = function(_, k) | |
120 if k == params.groups.namefield then | |
121 return 'string'; | |
122 end | |
123 end | |
124 }); | |
125 | |
126 local ok, err = run_validation(params, config_params); | |
127 | |
128 setmetatable(config_params.groups._member, nil); | |
129 | |
130 if ok then | |
131 -- a little extra validation that doesn't fit into | |
132 -- my recursive checker | |
133 local group_namefield = params.groups.namefield; | |
134 for i, group in ipairs(params.groups) do | |
135 if not group[group_namefield] then | |
136 return nil, format('groups.%d.%s is required', i, group_namefield); | |
137 end | |
138 end | |
139 | |
140 -- fill in params.admin if you can | |
141 if not params.admin and params.groups then | |
142 local admingroup; | |
143 | |
144 for _, groupconfig in ipairs(params.groups) do | |
145 if groupconfig.admin then | |
146 admingroup = groupconfig; | |
147 break; | |
148 end | |
149 end | |
150 | |
151 if admingroup then | |
152 params.admin = { | |
153 basedn = params.groups.basedn, | |
154 namefield = params.groups.memberfield, | |
155 filter = group_namefield .. '=' .. admingroup[group_namefield], | |
156 }; | |
157 end | |
158 end | |
159 end | |
160 | |
161 return ok, err; | |
162 end | |
163 | |
164 -- what to do if connection isn't available? | |
165 local function connect() | |
166 return ldap.open_simple(params.hostname, params.bind_dn, params.bind_password, params.use_tls); | |
167 end | |
168 | |
169 -- this is abstracted so we can maintain persistent connections at a later time | |
170 function _M.getconnection() | |
3195
66b3085ecc49
mod_lib_ldap: assert() connection for hopefully better error reporting (thanks adac)
Matthew Wild <mwild1@gmail.com>
parents:
877
diff
changeset
|
171 return assert(connect()); |
809 | 172 end |
173 | |
174 function _M.getparams() | |
175 return params; | |
176 end | |
177 | |
178 -- XXX consider renaming this...it doesn't bind the current connection | |
179 function _M.bind(username, password) | |
877
cd2262969d2e
Make sure we use the user filter for bind
Rob Hoelz <rob@hoelz.ro>
parents:
871
diff
changeset
|
180 local conn = _M.getconnection(); |
cd2262969d2e
Make sure we use the user filter for bind
Rob Hoelz <rob@hoelz.ro>
parents:
871
diff
changeset
|
181 local filter = format('%s=%s', params.user.usernamefield, username); |
cd2262969d2e
Make sure we use the user filter for bind
Rob Hoelz <rob@hoelz.ro>
parents:
871
diff
changeset
|
182 |
cd2262969d2e
Make sure we use the user filter for bind
Rob Hoelz <rob@hoelz.ro>
parents:
871
diff
changeset
|
183 if filter then |
cd2262969d2e
Make sure we use the user filter for bind
Rob Hoelz <rob@hoelz.ro>
parents:
871
diff
changeset
|
184 filter = _M.filter.combine_and(filter, params.user.filter); |
cd2262969d2e
Make sure we use the user filter for bind
Rob Hoelz <rob@hoelz.ro>
parents:
871
diff
changeset
|
185 end |
cd2262969d2e
Make sure we use the user filter for bind
Rob Hoelz <rob@hoelz.ro>
parents:
871
diff
changeset
|
186 |
cd2262969d2e
Make sure we use the user filter for bind
Rob Hoelz <rob@hoelz.ro>
parents:
871
diff
changeset
|
187 local who = _M.singlematch { |
864
16b007c7706c
We must search for dn before trying to bind
Guilhem LETTRON <guilhem.lettron@gmail.com>
parents:
809
diff
changeset
|
188 attrs = params.user.usernamefield, |
16b007c7706c
We must search for dn before trying to bind
Guilhem LETTRON <guilhem.lettron@gmail.com>
parents:
809
diff
changeset
|
189 base = params.user.basedn, |
16b007c7706c
We must search for dn before trying to bind
Guilhem LETTRON <guilhem.lettron@gmail.com>
parents:
809
diff
changeset
|
190 filter = filter, |
16b007c7706c
We must search for dn before trying to bind
Guilhem LETTRON <guilhem.lettron@gmail.com>
parents:
809
diff
changeset
|
191 }; |
16b007c7706c
We must search for dn before trying to bind
Guilhem LETTRON <guilhem.lettron@gmail.com>
parents:
809
diff
changeset
|
192 |
870
13e645340767
Use singlematch to find user record in ldap.bind
Rob Hoelz <rob@hoelz.ro>
parents:
869
diff
changeset
|
193 if who then |
13e645340767
Use singlematch to find user record in ldap.bind
Rob Hoelz <rob@hoelz.ro>
parents:
869
diff
changeset
|
194 who = who.dn; |
13e645340767
Use singlematch to find user record in ldap.bind
Rob Hoelz <rob@hoelz.ro>
parents:
869
diff
changeset
|
195 module:log('debug', '_M.bind - who: %s', who); |
871
e4a03e58f896
Log and return failure if user record not found in bind
Rob Hoelz <rob@hoelz.ro>
parents:
870
diff
changeset
|
196 else |
e4a03e58f896
Log and return failure if user record not found in bind
Rob Hoelz <rob@hoelz.ro>
parents:
870
diff
changeset
|
197 module:log('debug', '_M.bind - no DN found for username = %s', username); |
e4a03e58f896
Log and return failure if user record not found in bind
Rob Hoelz <rob@hoelz.ro>
parents:
870
diff
changeset
|
198 return nil, format('no DN found for username = %s', username); |
864
16b007c7706c
We must search for dn before trying to bind
Guilhem LETTRON <guilhem.lettron@gmail.com>
parents:
809
diff
changeset
|
199 end |
16b007c7706c
We must search for dn before trying to bind
Guilhem LETTRON <guilhem.lettron@gmail.com>
parents:
809
diff
changeset
|
200 |
809 | 201 local conn, err = ldap.open_simple(params.hostname, who, password, params.use_tls); |
202 | |
203 if conn then | |
204 conn:close(); | |
205 return true; | |
206 end | |
207 | |
208 return conn, err; | |
209 end | |
210 | |
211 function _M.singlematch(query) | |
212 local ld = _M.getconnection(); | |
213 | |
214 query.sizelimit = 1; | |
868
0017518c94a0
Change singlematch to search subtrees
Rob Hoelz <rob@hoelz.ro>
parents:
864
diff
changeset
|
215 query.scope = 'subtree'; |
809 | 216 |
217 for dn, attribs in ld:search(query) do | |
869
ec791fd8ce87
Return DN in the attributes table with singlematch
Rob Hoelz <rob@hoelz.ro>
parents:
868
diff
changeset
|
218 attribs.dn = dn; |
809 | 219 return attribs; |
220 end | |
221 end | |
222 | |
223 _M.filter = {}; | |
224 | |
225 function _M.filter.combine_and(...) | |
226 local parts = { '(&' }; | |
227 | |
228 local arg = { ... }; | |
229 | |
230 for _, filter in ipairs(arg) do | |
231 if filter:sub(1, 1) ~= '(' and filter:sub(-1) ~= ')' then | |
232 filter = '(' .. filter .. ')' | |
233 end | |
234 parts[#parts + 1] = filter; | |
235 end | |
236 | |
237 parts[#parts + 1] = ')'; | |
238 | |
239 return tconcat(parts, ''); | |
240 end | |
241 | |
242 do | |
243 local ok, err; | |
244 | |
245 prosody.unlock_globals(); | |
246 ok, ldap = pcall(require, 'lualdap'); | |
247 prosody.lock_globals(); | |
248 if not ok then | |
249 module:log("error", "Failed to load the LuaLDAP library for accessing LDAP: %s", ldap); | |
250 module:log("error", "More information on install LuaLDAP can be found at http://www.keplerproject.org/lualdap"); | |
251 return; | |
252 end | |
253 | |
254 if not params then | |
255 module:log("error", "LDAP configuration required to use the LDAP storage module"); | |
256 return; | |
257 end | |
258 | |
259 ok, err = validate_config(); | |
260 | |
261 if not ok then | |
262 module:log("error", "LDAP configuration is invalid: %s", tostring(err)); | |
263 return; | |
264 end | |
265 end | |
266 | |
267 return _M; |