]>
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 | }, | |
72a9742b DC |
125 | mode => { |
126 | description => "LDAP protocol mode.", | |
127 | type => 'string', | |
128 | enum => [ 'ldap', 'ldaps', 'ldap+starttls'], | |
129 | optional => 1, | |
130 | default => 'ldap', | |
131 | }, | |
5bb4e06a DM |
132 | }; |
133 | } | |
134 | ||
135 | sub options { | |
136 | return { | |
137 | server1 => {}, | |
138 | server2 => { optional => 1 }, | |
139 | base_dn => {}, | |
b5040b42 | 140 | bind_dn => { optional => 1 }, |
782b702d | 141 | password => { optional => 1 }, |
5bb4e06a DM |
142 | user_attr => {}, |
143 | port => { optional => 1 }, | |
144 | secure => { optional => 1 }, | |
07dd90d7 | 145 | sslversion => { optional => 1 }, |
5bb4e06a DM |
146 | default => { optional => 1 }, |
147 | comment => { optional => 1 }, | |
96f8ebd6 | 148 | tfa => { optional => 1 }, |
e03c2aef WB |
149 | verify => { optional => 1 }, |
150 | capath => { optional => 1 }, | |
151 | cert => { optional => 1 }, | |
152 | certkey => { optional => 1 }, | |
eba326d2 DC |
153 | filter => { optional => 1 }, |
154 | sync_attributes => { optional => 1 }, | |
155 | user_classes => { optional => 1 }, | |
156 | group_dn => { optional => 1 }, | |
157 | group_name_attr => { optional => 1 }, | |
158 | group_filter => { optional => 1 }, | |
159 | group_classes => { optional => 1 }, | |
d29d2d4a | 160 | 'sync-defaults-options' => { optional => 1 }, |
72a9742b | 161 | mode => { optional => 1 }, |
5bb4e06a DM |
162 | }; |
163 | } | |
164 | ||
72a9742b DC |
165 | sub get_scheme_and_port { |
166 | my ($class, $config) = @_; | |
167 | ||
168 | my $scheme = $config->{mode} // ($config->{secure} ? 'ldaps' : 'ldap'); | |
169 | ||
170 | my $default_port = $scheme eq 'ldaps' ? 636 : 389; | |
171 | my $port = $config->{port} // $default_port; | |
172 | ||
173 | return ($scheme, $port); | |
174 | } | |
175 | ||
30aad017 DC |
176 | sub connect_and_bind { |
177 | my ($class, $config, $realm) = @_; | |
d9e93d2e DC |
178 | |
179 | my $servers = [$config->{server1}]; | |
180 | push @$servers, $config->{server2} if $config->{server2}; | |
5bb4e06a | 181 | |
72a9742b | 182 | my ($scheme, $port) = $class->get_scheme_and_port($config); |
5bb4e06a | 183 | |
e03c2aef WB |
184 | my %ldap_args; |
185 | if ($config->{verify}) { | |
186 | $ldap_args{verify} = 'require'; | |
d9e93d2e DC |
187 | $ldap_args{clientcert} = $config->{cert} if $config->{cert}; |
188 | $ldap_args{clientkey} = $config->{certkey} if $config->{certkey}; | |
e03c2aef WB |
189 | if (defined(my $capath = $config->{capath})) { |
190 | if (-d $capath) { | |
191 | $ldap_args{capath} = $capath; | |
192 | } else { | |
193 | $ldap_args{cafile} = $capath; | |
194 | } | |
195 | } | |
196 | } else { | |
197 | $ldap_args{verify} = 'none'; | |
198 | } | |
199 | ||
72a9742b | 200 | if ($scheme ne 'ldap') { |
3b7eaef1 | 201 | $ldap_args{sslversion} = $config->{sslversion} || 'tlsv1_2'; |
07dd90d7 AD |
202 | } |
203 | ||
d9e93d2e DC |
204 | my $ldap = PVE::LDAP::ldap_connect($servers, $scheme, $port, \%ldap_args); |
205 | ||
206 | my $bind_dn; | |
207 | my $bind_pass; | |
b5040b42 | 208 | |
d9e93d2e DC |
209 | if ($config->{bind_dn}) { |
210 | $bind_dn = $config->{bind_dn}; | |
782b702d | 211 | $bind_pass = ldap_get_credentials($realm); |
b5040b42 | 212 | die "missing password for realm $realm\n" if !defined($bind_pass); |
b5040b42 WB |
213 | } |
214 | ||
d9e93d2e | 215 | PVE::LDAP::ldap_bind($ldap, $bind_dn, $bind_pass); |
30aad017 DC |
216 | |
217 | if (!$config->{base_dn}) { | |
218 | my $root = $ldap->root_dse(attrs => [ 'defaultNamingContext' ]); | |
219 | $config->{base_dn} = $root->get_value('defaultNamingContext'); | |
220 | } | |
221 | ||
222 | return $ldap; | |
223 | } | |
224 | ||
2c6e956e DC |
225 | # returns: |
226 | # { | |
227 | # 'username@realm' => { | |
228 | # 'attr1' => 'value1', | |
229 | # 'attr2' => 'value2', | |
230 | # ... | |
231 | # }, | |
232 | # ... | |
233 | # } | |
234 | # | |
235 | # or in list context: | |
236 | # ( | |
237 | # { | |
238 | # 'username@realm' => { | |
239 | # 'attr1' => 'value1', | |
240 | # 'attr2' => 'value2', | |
241 | # ... | |
242 | # }, | |
243 | # ... | |
244 | # }, | |
245 | # { | |
246 | # 'uid=username,dc=....' => 'username@realm', | |
247 | # ... | |
248 | # } | |
249 | # ) | |
250 | # the map of dn->username is needed for group membership sync | |
251 | sub get_users { | |
252 | my ($class, $config, $realm) = @_; | |
253 | ||
254 | my $ldap = $class->connect_and_bind($config, $realm); | |
255 | ||
256 | my $user_name_attr = $config->{user_attr} // 'uid'; | |
257 | my $ldap_attribute_map = { | |
258 | $user_name_attr => 'username', | |
259 | enable => 'enable', | |
260 | expire => 'expire', | |
261 | firstname => 'firstname', | |
262 | lastname => 'lastname', | |
263 | email => 'email', | |
264 | comment => 'comment', | |
265 | keys => 'keys', | |
266 | }; | |
267 | ||
268 | foreach my $attr (PVE::Tools::split_list($config->{sync_attributes})) { | |
269 | my ($ours, $ldap) = ($attr =~ m/^\s*(\w+)=(.*)\s*$/); | |
270 | $ldap_attribute_map->{$ldap} = $ours; | |
271 | } | |
272 | ||
273 | my $filter = $config->{filter}; | |
274 | my $basedn = $config->{base_dn}; | |
275 | ||
276 | $config->{user_classes} //= 'inetorgperson, posixaccount, person, user'; | |
277 | my $classes = [PVE::Tools::split_list($config->{user_classes})]; | |
278 | ||
279 | my $users = PVE::LDAP::query_users($ldap, $filter, [keys %$ldap_attribute_map], $basedn, $classes); | |
280 | ||
281 | my $ret = {}; | |
282 | my $dnmap = {}; | |
283 | ||
284 | foreach my $user (@$users) { | |
285 | my $user_attributes = $user->{attributes}; | |
286 | my $userid = $user_attributes->{$user_name_attr}->[0]; | |
287 | my $username = "$userid\@$realm"; | |
288 | ||
289 | # we cannot sync usernames that do not meet our criteria | |
290 | eval { PVE::Auth::Plugin::verify_username($username) }; | |
291 | if (my $err = $@) { | |
292 | warn "$err"; | |
293 | next; | |
294 | } | |
295 | ||
296 | $ret->{$username} = {}; | |
297 | ||
298 | foreach my $attr (keys %$user_attributes) { | |
299 | if (my $ours = $ldap_attribute_map->{$attr}) { | |
300 | $ret->{$username}->{$ours} = $user_attributes->{$attr}->[0]; | |
301 | } | |
302 | } | |
303 | ||
304 | if (wantarray) { | |
305 | my $dn = $user->{dn}; | |
306 | $dnmap->{$dn} = $username; | |
307 | } | |
308 | } | |
309 | ||
310 | return wantarray ? ($ret, $dnmap) : $ret; | |
311 | } | |
312 | ||
313 | # needs a map for dn -> username, we get this from the get_users call | |
314 | # otherwise we cannot determine the group membership | |
315 | sub get_groups { | |
316 | my ($class, $config, $realm, $dnmap) = @_; | |
317 | ||
318 | my $filter = $config->{group_filter}; | |
319 | my $basedn = $config->{group_dn} // $config->{base_dn}; | |
320 | my $attr = $config->{group_name_attr}; | |
321 | $config->{group_classes} //= 'groupOfNames, group, univentionGroup, ipausergroup'; | |
322 | my $classes = [PVE::Tools::split_list($config->{group_classes})]; | |
323 | ||
324 | my $ldap = $class->connect_and_bind($config, $realm); | |
325 | ||
326 | my $groups = PVE::LDAP::query_groups($ldap, $basedn, $classes, $filter, $attr); | |
327 | ||
328 | my $ret = {}; | |
329 | ||
330 | foreach my $group (@$groups) { | |
331 | my $name = $group->{name}; | |
332 | if (!$name && $group->{dn} =~ m/^[^=]+=([^,]+),/){ | |
333 | $name = PVE::Tools::trim($1); | |
334 | } | |
335 | if ($name) { | |
336 | $name .= "-$realm"; | |
337 | ||
338 | # we cannot sync groups that do not meet our criteria | |
339 | eval { PVE::AccessControl::verify_groupname($name) }; | |
340 | if (my $err = $@) { | |
341 | warn "$err"; | |
342 | next; | |
343 | } | |
344 | ||
345 | $ret->{$name} = { users => {} }; | |
346 | foreach my $member (@{$group->{members}}) { | |
347 | if (my $user = $dnmap->{$member}) { | |
348 | $ret->{$name}->{users}->{$user} = 1; | |
349 | } | |
350 | } | |
351 | } | |
352 | } | |
353 | ||
354 | return $ret; | |
355 | } | |
356 | ||
30aad017 DC |
357 | sub authenticate_user { |
358 | my ($class, $config, $realm, $username, $password) = @_; | |
359 | ||
360 | my $ldap = $class->connect_and_bind($config, $realm); | |
361 | ||
d9e93d2e DC |
362 | my $user_dn = PVE::LDAP::get_user_dn($ldap, $username, $config->{user_attr}, $config->{base_dn}); |
363 | PVE::LDAP::auth_user_dn($ldap, $user_dn, $password); | |
5bb4e06a DM |
364 | |
365 | $ldap->unbind(); | |
d9e93d2e | 366 | return 1; |
5bb4e06a DM |
367 | } |
368 | ||
782b702d DC |
369 | my $ldap_pw_dir = "/etc/pve/priv/realm"; |
370 | ||
371 | sub ldap_cred_file_name { | |
372 | my ($realmid) = @_; | |
373 | return "${ldap_pw_dir}/${realmid}.pw"; | |
374 | } | |
375 | ||
376 | sub get_cred_file { | |
377 | my ($realmid) = @_; | |
378 | ||
379 | my $cred_file = ldap_cred_file_name($realmid); | |
380 | if (-e $cred_file) { | |
381 | return $cred_file; | |
382 | } elsif (-e "/etc/pve/priv/ldap/${realmid}.pw") { | |
383 | # FIXME: remove fallback with 7.0 by doing a rename on upgrade from 6.x | |
384 | return "/etc/pve/priv/ldap/${realmid}.pw"; | |
385 | } | |
386 | ||
387 | return $cred_file; | |
388 | } | |
389 | ||
390 | sub ldap_set_credentials { | |
391 | my ($password, $realmid) = @_; | |
392 | ||
393 | my $cred_file = ldap_cred_file_name($realmid); | |
394 | mkdir $ldap_pw_dir; | |
395 | ||
396 | PVE::Tools::file_set_contents($cred_file, $password); | |
397 | ||
398 | return $cred_file; | |
399 | } | |
400 | ||
401 | sub ldap_get_credentials { | |
402 | my ($realmid) = @_; | |
403 | ||
404 | if (my $cred_file = get_cred_file($realmid)) { | |
405 | return PVE::Tools::file_read_firstline($cred_file); | |
406 | } | |
407 | return undef; | |
408 | } | |
409 | ||
410 | sub ldap_delete_credentials { | |
411 | my ($realmid) = @_; | |
412 | ||
413 | if (my $cred_file = get_cred_file($realmid)) { | |
414 | unlink($cred_file) or warn "removing LDAP credentials '$cred_file' failed: $!\n"; | |
415 | } | |
416 | } | |
417 | ||
418 | sub on_add_hook { | |
419 | my ($class, $realm, $config, %param) = @_; | |
420 | ||
421 | if (defined($param{password})) { | |
422 | ldap_set_credentials($param{password}, $realm); | |
423 | } else { | |
424 | ldap_delete_credentials($realm); | |
425 | } | |
426 | } | |
427 | ||
428 | sub on_update_hook { | |
429 | my ($class, $realm, $config, %param) = @_; | |
430 | ||
431 | return if !exists($param{password}); | |
432 | ||
433 | if (defined($param{password})) { | |
434 | ldap_set_credentials($param{password}, $realm); | |
435 | } else { | |
436 | ldap_delete_credentials($realm); | |
437 | } | |
438 | } | |
439 | ||
440 | sub on_delete_hook { | |
441 | my ($class, $realm, $config) = @_; | |
442 | ||
443 | ldap_delete_credentials($realm); | |
444 | } | |
445 | ||
5bb4e06a | 446 | 1; |