]> git.proxmox.com Git - pve-access-control.git/blame - PVE/Auth/LDAP.pm
auth ldap/ad: make password a parameter for the api
[pve-access-control.git] / PVE / Auth / LDAP.pm
CommitLineData
5bb4e06a
DM
1package PVE::Auth::LDAP;
2
3use strict;
7c410d63
DM
4use warnings;
5
5bb4e06a 6use PVE::Auth::Plugin;
d29d2d4a 7use PVE::JSONSchema;
d9e93d2e 8use PVE::LDAP;
d29d2d4a
TL
9use PVE::Tools;
10
5bb4e06a
DM
11use base qw(PVE::Auth::Plugin);
12
13sub type {
14 return 'ldap';
15}
16
17sub 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
128sub 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
157sub 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
234sub 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
298sub 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
340sub 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
352my $ldap_pw_dir = "/etc/pve/priv/realm";
353
354sub ldap_cred_file_name {
355 my ($realmid) = @_;
356 return "${ldap_pw_dir}/${realmid}.pw";
357}
358
359sub 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
373sub 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
384sub 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
393sub 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
401sub 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
411sub 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
423sub on_delete_hook {
424 my ($class, $realm, $config) = @_;
425
426 ldap_delete_credentials($realm);
427}
428
5bb4e06a 4291;