]>
Commit | Line | Data |
---|---|---|
5bb4e06a DM |
1 | package PVE::Auth::LDAP; |
2 | ||
3 | use strict; | |
7c410d63 DM |
4 | use warnings; |
5 | ||
5bb4e06a | 6 | use PVE::Auth::Plugin; |
d29d2d4a | 7 | use PVE::JSONSchema; |
d9e93d2e | 8 | use PVE::LDAP; |
d29d2d4a TL |
9 | use PVE::Tools; |
10 | ||
5bb4e06a DM |
11 | use base qw(PVE::Auth::Plugin); |
12 | ||
13 | sub type { | |
14 | return 'ldap'; | |
15 | } | |
16 | ||
17 | sub properties { | |
18 | return { | |
19 | base_dn => { | |
20 | description => "LDAP base domain name", | |
21 | type => 'string', | |
22 | pattern => '\w+=[^,]+(,\s*\w+=[^,]+)*', | |
23 | optional => 1, | |
24 | maxLength => 256, | |
25 | }, | |
26 | user_attr => { | |
27 | description => "LDAP user attribute name", | |
28 | type => 'string', | |
29 | pattern => '\S{2,}', | |
30 | optional => 1, | |
31 | maxLength => 256, | |
32 | }, | |
b5040b42 WB |
33 | bind_dn => { |
34 | description => "LDAP bind domain name", | |
35 | type => 'string', | |
36 | pattern => '\w+=[^,]+(,\s*\w+=[^,]+)*', | |
37 | optional => 1, | |
38 | maxLength => 256, | |
39 | }, | |
782b702d DC |
40 | password => { |
41 | description => "LDAP bind password. Will be stored in '/etc/pve/priv/realm/<REALM>.pw'.", | |
42 | type => 'string', | |
43 | optional => 1, | |
44 | }, | |
e03c2aef WB |
45 | verify => { |
46 | description => "Verify the server's SSL certificate", | |
47 | type => 'boolean', | |
48 | optional => 1, | |
49 | default => 0, | |
50 | }, | |
51 | capath => { | |
52 | description => "Path to the CA certificate store", | |
53 | type => 'string', | |
54 | optional => 1, | |
55 | default => '/etc/ssl/certs', | |
56 | }, | |
57 | cert => { | |
58 | description => "Path to the client certificate", | |
59 | type => 'string', | |
60 | optional => 1, | |
61 | }, | |
62 | certkey => { | |
63 | description => "Path to the client certificate key", | |
64 | type => 'string', | |
65 | optional => 1, | |
66 | }, | |
eba326d2 DC |
67 | filter => { |
68 | description => "LDAP filter for user sync.", | |
69 | type => 'string', | |
70 | optional => 1, | |
71 | maxLength => 2048, | |
72 | }, | |
73 | sync_attributes => { | |
74 | description => "Comma separated list of key=value pairs for specifying" | |
75 | ." which LDAP attributes map to which PVE user field. For example," | |
76 | ." to map the LDAP attribute 'mail' to PVEs 'email', write " | |
77 | ." 'email=mail'. By default, each PVE user field is represented " | |
78 | ." by an LDAP attribute of the same name.", | |
79 | optional => 1, | |
80 | type => 'string', | |
81 | pattern => '\w+=[^,]+(,\s*\w+=[^,]+)*', | |
82 | }, | |
83 | user_classes => { | |
84 | description => "The objectclasses for users.", | |
85 | type => 'string', | |
86 | default => 'inetorgperson, posixaccount, person, user', | |
87 | format => 'ldap-simple-attr-list', | |
88 | optional => 1, | |
89 | }, | |
90 | group_dn => { | |
91 | description => "LDAP base domain name for group sync. If not set, the" | |
92 | ." base_dn will be used.", | |
93 | type => 'string', | |
94 | pattern => '\w+=[^,]+(,\s*\w+=[^,]+)*', | |
95 | optional => 1, | |
96 | maxLength => 256, | |
97 | }, | |
98 | group_name_attr => { | |
99 | description => "LDAP attribute representing a groups name. If not set" | |
100 | ." or found, the first value of the DN will be used as name.", | |
101 | type => 'string', | |
102 | format => 'ldap-simple-attr', | |
103 | optional => 1, | |
104 | maxLength => 256, | |
105 | }, | |
106 | group_filter => { | |
107 | description => "LDAP filter for group sync.", | |
108 | type => 'string', | |
109 | optional => 1, | |
110 | maxLength => 2048, | |
111 | }, | |
112 | group_classes => { | |
113 | description => "The objectclasses for groups.", | |
114 | type => 'string', | |
115 | default => 'groupOfNames, group, univentionGroup, ipausergroup', | |
116 | format => 'ldap-simple-attr-list', | |
117 | optional => 1, | |
118 | }, | |
d29d2d4a TL |
119 | 'sync-defaults-options' => { |
120 | description => "The default options for behavior of synchronizations.", | |
121 | type => 'string', | |
122 | format => 'realm-sync-options', | |
123 | optional => 1, | |
124 | }, | |
5bb4e06a DM |
125 | }; |
126 | } | |
127 | ||
128 | sub options { | |
129 | return { | |
130 | server1 => {}, | |
131 | server2 => { optional => 1 }, | |
132 | base_dn => {}, | |
b5040b42 | 133 | bind_dn => { optional => 1 }, |
782b702d | 134 | password => { optional => 1 }, |
5bb4e06a DM |
135 | user_attr => {}, |
136 | port => { optional => 1 }, | |
137 | secure => { optional => 1 }, | |
07dd90d7 | 138 | sslversion => { optional => 1 }, |
5bb4e06a DM |
139 | default => { optional => 1 }, |
140 | comment => { optional => 1 }, | |
96f8ebd6 | 141 | tfa => { optional => 1 }, |
e03c2aef WB |
142 | verify => { optional => 1 }, |
143 | capath => { optional => 1 }, | |
144 | cert => { optional => 1 }, | |
145 | certkey => { optional => 1 }, | |
eba326d2 DC |
146 | filter => { optional => 1 }, |
147 | sync_attributes => { optional => 1 }, | |
148 | user_classes => { optional => 1 }, | |
149 | group_dn => { optional => 1 }, | |
150 | group_name_attr => { optional => 1 }, | |
151 | group_filter => { optional => 1 }, | |
152 | group_classes => { optional => 1 }, | |
d29d2d4a | 153 | 'sync-defaults-options' => { optional => 1 }, |
5bb4e06a DM |
154 | }; |
155 | } | |
156 | ||
30aad017 DC |
157 | sub connect_and_bind { |
158 | my ($class, $config, $realm) = @_; | |
d9e93d2e DC |
159 | |
160 | my $servers = [$config->{server1}]; | |
161 | push @$servers, $config->{server2} if $config->{server2}; | |
5bb4e06a DM |
162 | |
163 | my $default_port = $config->{secure} ? 636: 389; | |
d9e93d2e | 164 | my $port = $config->{port} // $default_port; |
5bb4e06a | 165 | my $scheme = $config->{secure} ? 'ldaps' : 'ldap'; |
5bb4e06a | 166 | |
e03c2aef WB |
167 | my %ldap_args; |
168 | if ($config->{verify}) { | |
169 | $ldap_args{verify} = 'require'; | |
d9e93d2e DC |
170 | $ldap_args{clientcert} = $config->{cert} if $config->{cert}; |
171 | $ldap_args{clientkey} = $config->{certkey} if $config->{certkey}; | |
e03c2aef WB |
172 | if (defined(my $capath = $config->{capath})) { |
173 | if (-d $capath) { | |
174 | $ldap_args{capath} = $capath; | |
175 | } else { | |
176 | $ldap_args{cafile} = $capath; | |
177 | } | |
178 | } | |
179 | } else { | |
180 | $ldap_args{verify} = 'none'; | |
181 | } | |
182 | ||
07dd90d7 | 183 | if ($config->{secure}) { |
3b7eaef1 | 184 | $ldap_args{sslversion} = $config->{sslversion} || 'tlsv1_2'; |
07dd90d7 AD |
185 | } |
186 | ||
d9e93d2e DC |
187 | my $ldap = PVE::LDAP::ldap_connect($servers, $scheme, $port, \%ldap_args); |
188 | ||
189 | my $bind_dn; | |
190 | my $bind_pass; | |
b5040b42 | 191 | |
d9e93d2e DC |
192 | if ($config->{bind_dn}) { |
193 | $bind_dn = $config->{bind_dn}; | |
782b702d | 194 | $bind_pass = ldap_get_credentials($realm); |
b5040b42 | 195 | die "missing password for realm $realm\n" if !defined($bind_pass); |
b5040b42 WB |
196 | } |
197 | ||
d9e93d2e | 198 | PVE::LDAP::ldap_bind($ldap, $bind_dn, $bind_pass); |
30aad017 DC |
199 | |
200 | if (!$config->{base_dn}) { | |
201 | my $root = $ldap->root_dse(attrs => [ 'defaultNamingContext' ]); | |
202 | $config->{base_dn} = $root->get_value('defaultNamingContext'); | |
203 | } | |
204 | ||
205 | return $ldap; | |
206 | } | |
207 | ||
2c6e956e DC |
208 | # returns: |
209 | # { | |
210 | # 'username@realm' => { | |
211 | # 'attr1' => 'value1', | |
212 | # 'attr2' => 'value2', | |
213 | # ... | |
214 | # }, | |
215 | # ... | |
216 | # } | |
217 | # | |
218 | # or in list context: | |
219 | # ( | |
220 | # { | |
221 | # 'username@realm' => { | |
222 | # 'attr1' => 'value1', | |
223 | # 'attr2' => 'value2', | |
224 | # ... | |
225 | # }, | |
226 | # ... | |
227 | # }, | |
228 | # { | |
229 | # 'uid=username,dc=....' => 'username@realm', | |
230 | # ... | |
231 | # } | |
232 | # ) | |
233 | # the map of dn->username is needed for group membership sync | |
234 | sub get_users { | |
235 | my ($class, $config, $realm) = @_; | |
236 | ||
237 | my $ldap = $class->connect_and_bind($config, $realm); | |
238 | ||
239 | my $user_name_attr = $config->{user_attr} // 'uid'; | |
240 | my $ldap_attribute_map = { | |
241 | $user_name_attr => 'username', | |
242 | enable => 'enable', | |
243 | expire => 'expire', | |
244 | firstname => 'firstname', | |
245 | lastname => 'lastname', | |
246 | email => 'email', | |
247 | comment => 'comment', | |
248 | keys => 'keys', | |
249 | }; | |
250 | ||
251 | foreach my $attr (PVE::Tools::split_list($config->{sync_attributes})) { | |
252 | my ($ours, $ldap) = ($attr =~ m/^\s*(\w+)=(.*)\s*$/); | |
253 | $ldap_attribute_map->{$ldap} = $ours; | |
254 | } | |
255 | ||
256 | my $filter = $config->{filter}; | |
257 | my $basedn = $config->{base_dn}; | |
258 | ||
259 | $config->{user_classes} //= 'inetorgperson, posixaccount, person, user'; | |
260 | my $classes = [PVE::Tools::split_list($config->{user_classes})]; | |
261 | ||
262 | my $users = PVE::LDAP::query_users($ldap, $filter, [keys %$ldap_attribute_map], $basedn, $classes); | |
263 | ||
264 | my $ret = {}; | |
265 | my $dnmap = {}; | |
266 | ||
267 | foreach my $user (@$users) { | |
268 | my $user_attributes = $user->{attributes}; | |
269 | my $userid = $user_attributes->{$user_name_attr}->[0]; | |
270 | my $username = "$userid\@$realm"; | |
271 | ||
272 | # we cannot sync usernames that do not meet our criteria | |
273 | eval { PVE::Auth::Plugin::verify_username($username) }; | |
274 | if (my $err = $@) { | |
275 | warn "$err"; | |
276 | next; | |
277 | } | |
278 | ||
279 | $ret->{$username} = {}; | |
280 | ||
281 | foreach my $attr (keys %$user_attributes) { | |
282 | if (my $ours = $ldap_attribute_map->{$attr}) { | |
283 | $ret->{$username}->{$ours} = $user_attributes->{$attr}->[0]; | |
284 | } | |
285 | } | |
286 | ||
287 | if (wantarray) { | |
288 | my $dn = $user->{dn}; | |
289 | $dnmap->{$dn} = $username; | |
290 | } | |
291 | } | |
292 | ||
293 | return wantarray ? ($ret, $dnmap) : $ret; | |
294 | } | |
295 | ||
296 | # needs a map for dn -> username, we get this from the get_users call | |
297 | # otherwise we cannot determine the group membership | |
298 | sub get_groups { | |
299 | my ($class, $config, $realm, $dnmap) = @_; | |
300 | ||
301 | my $filter = $config->{group_filter}; | |
302 | my $basedn = $config->{group_dn} // $config->{base_dn}; | |
303 | my $attr = $config->{group_name_attr}; | |
304 | $config->{group_classes} //= 'groupOfNames, group, univentionGroup, ipausergroup'; | |
305 | my $classes = [PVE::Tools::split_list($config->{group_classes})]; | |
306 | ||
307 | my $ldap = $class->connect_and_bind($config, $realm); | |
308 | ||
309 | my $groups = PVE::LDAP::query_groups($ldap, $basedn, $classes, $filter, $attr); | |
310 | ||
311 | my $ret = {}; | |
312 | ||
313 | foreach my $group (@$groups) { | |
314 | my $name = $group->{name}; | |
315 | if (!$name && $group->{dn} =~ m/^[^=]+=([^,]+),/){ | |
316 | $name = PVE::Tools::trim($1); | |
317 | } | |
318 | if ($name) { | |
319 | $name .= "-$realm"; | |
320 | ||
321 | # we cannot sync groups that do not meet our criteria | |
322 | eval { PVE::AccessControl::verify_groupname($name) }; | |
323 | if (my $err = $@) { | |
324 | warn "$err"; | |
325 | next; | |
326 | } | |
327 | ||
328 | $ret->{$name} = { users => {} }; | |
329 | foreach my $member (@{$group->{members}}) { | |
330 | if (my $user = $dnmap->{$member}) { | |
331 | $ret->{$name}->{users}->{$user} = 1; | |
332 | } | |
333 | } | |
334 | } | |
335 | } | |
336 | ||
337 | return $ret; | |
338 | } | |
339 | ||
30aad017 DC |
340 | sub authenticate_user { |
341 | my ($class, $config, $realm, $username, $password) = @_; | |
342 | ||
343 | my $ldap = $class->connect_and_bind($config, $realm); | |
344 | ||
d9e93d2e DC |
345 | my $user_dn = PVE::LDAP::get_user_dn($ldap, $username, $config->{user_attr}, $config->{base_dn}); |
346 | PVE::LDAP::auth_user_dn($ldap, $user_dn, $password); | |
5bb4e06a DM |
347 | |
348 | $ldap->unbind(); | |
d9e93d2e | 349 | return 1; |
5bb4e06a DM |
350 | } |
351 | ||
782b702d DC |
352 | my $ldap_pw_dir = "/etc/pve/priv/realm"; |
353 | ||
354 | sub ldap_cred_file_name { | |
355 | my ($realmid) = @_; | |
356 | return "${ldap_pw_dir}/${realmid}.pw"; | |
357 | } | |
358 | ||
359 | sub get_cred_file { | |
360 | my ($realmid) = @_; | |
361 | ||
362 | my $cred_file = ldap_cred_file_name($realmid); | |
363 | if (-e $cred_file) { | |
364 | return $cred_file; | |
365 | } elsif (-e "/etc/pve/priv/ldap/${realmid}.pw") { | |
366 | # FIXME: remove fallback with 7.0 by doing a rename on upgrade from 6.x | |
367 | return "/etc/pve/priv/ldap/${realmid}.pw"; | |
368 | } | |
369 | ||
370 | return $cred_file; | |
371 | } | |
372 | ||
373 | sub ldap_set_credentials { | |
374 | my ($password, $realmid) = @_; | |
375 | ||
376 | my $cred_file = ldap_cred_file_name($realmid); | |
377 | mkdir $ldap_pw_dir; | |
378 | ||
379 | PVE::Tools::file_set_contents($cred_file, $password); | |
380 | ||
381 | return $cred_file; | |
382 | } | |
383 | ||
384 | sub ldap_get_credentials { | |
385 | my ($realmid) = @_; | |
386 | ||
387 | if (my $cred_file = get_cred_file($realmid)) { | |
388 | return PVE::Tools::file_read_firstline($cred_file); | |
389 | } | |
390 | return undef; | |
391 | } | |
392 | ||
393 | sub ldap_delete_credentials { | |
394 | my ($realmid) = @_; | |
395 | ||
396 | if (my $cred_file = get_cred_file($realmid)) { | |
397 | unlink($cred_file) or warn "removing LDAP credentials '$cred_file' failed: $!\n"; | |
398 | } | |
399 | } | |
400 | ||
401 | sub on_add_hook { | |
402 | my ($class, $realm, $config, %param) = @_; | |
403 | ||
404 | if (defined($param{password})) { | |
405 | ldap_set_credentials($param{password}, $realm); | |
406 | } else { | |
407 | ldap_delete_credentials($realm); | |
408 | } | |
409 | } | |
410 | ||
411 | sub on_update_hook { | |
412 | my ($class, $realm, $config, %param) = @_; | |
413 | ||
414 | return if !exists($param{password}); | |
415 | ||
416 | if (defined($param{password})) { | |
417 | ldap_set_credentials($param{password}, $realm); | |
418 | } else { | |
419 | ldap_delete_credentials($realm); | |
420 | } | |
421 | } | |
422 | ||
423 | sub on_delete_hook { | |
424 | my ($class, $realm, $config) = @_; | |
425 | ||
426 | ldap_delete_credentials($realm); | |
427 | } | |
428 | ||
5bb4e06a | 429 | 1; |