1 package PVE
::Auth
::LDAP
;
11 use base
qw(PVE::Auth::Plugin);
20 description
=> "LDAP base domain name",
26 description
=> "LDAP user attribute name",
33 description
=> "LDAP bind domain name",
39 description
=> "LDAP bind password. Will be stored in '/etc/pve/priv/realm/<REALM>.pw'.",
44 description
=> "Verify the server's SSL certificate",
50 description
=> "Path to the CA certificate store",
53 default => '/etc/ssl/certs',
56 description
=> "Path to the client certificate",
61 description
=> "Path to the client certificate key",
66 description
=> "LDAP filter for user sync.",
72 description
=> "Comma separated list of key=value pairs for specifying"
73 ." which LDAP attributes map to which PVE user field. For example,"
74 ." to map the LDAP attribute 'mail' to PVEs 'email', write "
75 ." 'email=mail'. By default, each PVE user field is represented "
76 ." by an LDAP attribute of the same name.",
79 pattern
=> '\w+=[^,]+(,\s*\w+=[^,]+)*',
82 description
=> "The objectclasses for users.",
84 default => 'inetorgperson, posixaccount, person, user',
85 format
=> 'ldap-simple-attr-list',
89 description
=> "LDAP base domain name for group sync. If not set, the"
90 ." base_dn will be used.",
96 description
=> "LDAP attribute representing a groups name. If not set"
97 ." or found, the first value of the DN will be used as name.",
99 format
=> 'ldap-simple-attr',
104 description
=> "LDAP filter for group sync.",
110 description
=> "The objectclasses for groups.",
112 default => 'groupOfNames, group, univentionGroup, ipausergroup',
113 format
=> 'ldap-simple-attr-list',
116 'sync-defaults-options' => {
117 description
=> "The default options for behavior of synchronizations.",
119 format
=> 'realm-sync-options',
123 description
=> "LDAP protocol mode.",
125 enum
=> [ 'ldap', 'ldaps', 'ldap+starttls'],
129 'case-sensitive' => {
130 description
=> "username is case-sensitive",
141 server2
=> { optional
=> 1 },
143 bind_dn
=> { optional
=> 1 },
144 password
=> { optional
=> 1 },
146 port
=> { optional
=> 1 },
147 secure
=> { optional
=> 1 },
148 sslversion
=> { optional
=> 1 },
149 default => { optional
=> 1 },
150 comment
=> { optional
=> 1 },
151 tfa
=> { optional
=> 1 },
152 verify
=> { optional
=> 1 },
153 capath
=> { optional
=> 1 },
154 cert
=> { optional
=> 1 },
155 certkey
=> { optional
=> 1 },
156 filter
=> { optional
=> 1 },
157 sync_attributes
=> { optional
=> 1 },
158 user_classes
=> { optional
=> 1 },
159 group_dn
=> { optional
=> 1 },
160 group_name_attr
=> { optional
=> 1 },
161 group_filter
=> { optional
=> 1 },
162 group_classes
=> { optional
=> 1 },
163 'sync-defaults-options' => { optional
=> 1 },
164 mode
=> { optional
=> 1 },
165 'case-sensitive' => { optional
=> 1 },
169 my sub verify_sync_attribute_value
{
170 my ($attr, $value) = @_;
172 # The attribute does not include the realm, so can't use PVE::Auth::Plugin::verify_username
173 if ($attr eq 'username') {
174 die "value '$value' does not look like a valid user name\n"
175 if $value !~ m/${PVE::Auth::Plugin::user_regex}/;
179 return if $attr eq 'enable'; # for backwards compat, don't parse/validate
181 my $schema = PVE
::JSONSchema
::get_standard_option
("user-$attr");
182 PVE
::JSONSchema
::validate
($value, $schema, "invalid value '$value'\n");
185 sub get_scheme_and_port
{
186 my ($class, $config) = @_;
188 my $scheme = $config->{mode
} // ($config->{secure
} ?
'ldaps' : 'ldap');
190 my $default_port = $scheme eq 'ldaps' ?
636 : 389;
191 my $port = $config->{port
} // $default_port;
193 return ($scheme, $port);
196 sub connect_and_bind
{
197 my ($class, $config, $realm, $param) = @_;
199 my $servers = [$config->{server1
}];
200 push @$servers, $config->{server2
} if $config->{server2
};
202 my ($scheme, $port) = $class->get_scheme_and_port($config);
205 if ($config->{verify
}) {
206 $ldap_args{verify
} = 'require';
207 $ldap_args{clientcert
} = $config->{cert
} if $config->{cert
};
208 $ldap_args{clientkey
} = $config->{certkey
} if $config->{certkey
};
209 if (defined(my $capath = $config->{capath
})) {
211 $ldap_args{capath
} = $capath;
213 $ldap_args{cafile
} = $capath;
217 $ldap_args{verify
} = 'none';
220 if ($scheme ne 'ldap') {
221 $ldap_args{sslversion
} = $config->{sslversion
} || 'tlsv1_2';
224 my $ldap = PVE
::LDAP
::ldap_connect
($servers, $scheme, $port, \
%ldap_args);
226 if ($config->{bind_dn
}) {
227 my $bind_dn = $config->{bind_dn
};
228 my $bind_pass = $param->{password
} || ldap_get_credentials
($realm);
229 die "missing password for realm $realm\n" if !defined($bind_pass);
230 PVE
::LDAP
::ldap_bind
($ldap, $bind_dn, $bind_pass);
231 } elsif ($config->{cert
} && $config->{certkey
}) {
232 warn "skipping anonymous bind with clientcert\n";
234 PVE
::LDAP
::ldap_bind
($ldap);
237 if (!$config->{base_dn
}) {
238 my $root = $ldap->root_dse(attrs
=> [ 'defaultNamingContext' ]);
239 $config->{base_dn
} = $root->get_value('defaultNamingContext');
247 # 'username@realm' => {
248 # 'attr1' => 'value1',
249 # 'attr2' => 'value2',
255 # or in list context:
258 # 'username@realm' => {
259 # 'attr1' => 'value1',
260 # 'attr2' => 'value2',
266 # 'uid=username,dc=....' => 'username@realm',
270 # the map of dn->username is needed for group membership sync
272 my ($class, $config, $realm) = @_;
274 my $ldap = $class->connect_and_bind($config, $realm);
276 my $user_name_attr = $config->{user_attr
} // 'uid';
277 my $ldap_attribute_map = {
278 $user_name_attr => 'username',
281 firstname
=> 'firstname',
282 lastname
=> 'lastname',
284 comment
=> 'comment',
287 # build on the fly as this is small and only called once per realm in a ldap-sync anyway
288 my $valid_sync_attributes = map { $_ => 1 } values $ldap_attribute_map->%*;
290 foreach my $attr (PVE
::Tools
::split_list
($config->{sync_attributes
})) {
291 my ($ours, $ldap) = ($attr =~ m/^\s*(\w+)=(.*)\s*$/);
292 if (!$valid_sync_attributes->{$ours}) {
293 warn "skipping bad 'sync_attributes' entry – '$ours' is not a valid target attribute\n";
296 $ldap_attribute_map->{$ldap} = $ours;
299 my $filter = $config->{filter
};
300 my $basedn = $config->{base_dn
};
302 $config->{user_classes
} //= 'inetorgperson, posixaccount, person, user';
303 my $classes = [PVE
::Tools
::split_list
($config->{user_classes
})];
305 my $users = PVE
::LDAP
::query_users
($ldap, $filter, [keys %$ldap_attribute_map], $basedn, $classes);
310 foreach my $user (@$users) {
311 my $user_attributes = $user->{attributes
};
312 my $userid = $user_attributes->{$user_name_attr}->[0];
313 my $username = "$userid\@$realm";
315 # we cannot sync usernames that do not meet our criteria
316 eval { PVE
::Auth
::Plugin
::verify_username
($username) };
322 $ret->{$username} = {};
324 foreach my $attr (keys %$user_attributes) {
325 if (my $ours = $ldap_attribute_map->{$attr}) {
326 my $value = $user_attributes->{$attr}->[0];
327 eval { verify_sync_attribute_value
($ours, $value) };
329 warn "skipping attribute mapping '$attr'->'$ours' for user '$username' - $err";
332 $ret->{$username}->{$ours} = $value;
337 my $dn = $user->{dn
};
338 $dnmap->{lc($dn)} = $username;
342 return wantarray ?
($ret, $dnmap) : $ret;
345 # needs a map for dn -> username, we get this from the get_users call
346 # otherwise we cannot determine the group membership
348 my ($class, $config, $realm, $dnmap) = @_;
350 my $filter = $config->{group_filter
};
351 my $basedn = $config->{group_dn
} // $config->{base_dn
};
352 my $attr = $config->{group_name_attr
};
353 $config->{group_classes
} //= 'groupOfNames, group, univentionGroup, ipausergroup';
354 my $classes = [PVE
::Tools
::split_list
($config->{group_classes
})];
356 my $ldap = $class->connect_and_bind($config, $realm);
358 my $groups = PVE
::LDAP
::query_groups
($ldap, $basedn, $classes, $filter, $attr);
362 foreach my $group (@$groups) {
363 my $name = $group->{name
};
364 if (!$name && $group->{dn
} =~ m/^[^=]+=([^,]+),/){
365 $name = PVE
::Tools
::trim
($1);
370 # we cannot sync groups that do not meet our criteria
371 eval { PVE
::AccessControl
::verify_groupname
($name) };
377 $ret->{$name} = { users
=> {} };
378 foreach my $member (@{$group->{members
}}) {
379 if (my $user = $dnmap->{lc($member)}) {
380 $ret->{$name}->{users
}->{$user} = 1;
389 sub authenticate_user
{
390 my ($class, $config, $realm, $username, $password) = @_;
392 my $ldap = $class->connect_and_bind($config, $realm);
394 my $user_dn = PVE
::LDAP
::get_user_dn
($ldap, $username, $config->{user_attr
}, $config->{base_dn
});
395 PVE
::LDAP
::auth_user_dn
($ldap, $user_dn, $password);
401 my $ldap_pw_dir = "/etc/pve/priv/realm";
403 sub ldap_cred_file_name
{
405 return "${ldap_pw_dir}/${realmid}.pw";
411 my $cred_file = ldap_cred_file_name
($realmid);
414 } elsif (-e
"/etc/pve/priv/ldap/${realmid}.pw") {
415 # FIXME: remove fallback with 7.0 by doing a rename on upgrade from 6.x
416 return "/etc/pve/priv/ldap/${realmid}.pw";
422 sub ldap_set_credentials
{
423 my ($password, $realmid) = @_;
425 my $cred_file = ldap_cred_file_name
($realmid);
428 PVE
::Tools
::file_set_contents
($cred_file, $password);
433 sub ldap_get_credentials
{
436 if (my $cred_file = get_cred_file
($realmid)) {
437 return PVE
::Tools
::file_read_firstline
($cred_file);
442 sub ldap_delete_credentials
{
445 if (my $cred_file = get_cred_file
($realmid)) {
446 return if ! -e
$cred_file; # nothing to do
447 unlink($cred_file) or warn "removing LDAP credentials '$cred_file' failed: $!\n";
452 my ($class, $realm, $config, %param) = @_;
454 if (defined($param{password
})) {
455 ldap_set_credentials
($param{password
}, $realm);
457 ldap_delete_credentials
($realm);
462 my ($class, $realm, $config, %param) = @_;
464 return if !exists($param{password
});
466 if (defined($param{password
})) {
467 ldap_set_credentials
($param{password
}, $realm);
469 ldap_delete_credentials
($realm);
474 my ($class, $realm, $config) = @_;
476 ldap_delete_credentials
($realm);
479 sub check_connection
{
480 my ($class, $realm, $config, %param) = @_;
482 $class->connect_and_bind($config, $realm, \
%param);