]> git.proxmox.com Git - pve-access-control.git/blob - PVE/Auth/LDAP.pm
realm: add default-sync-options to config
[pve-access-control.git] / PVE / Auth / LDAP.pm
1 package PVE::Auth::LDAP;
2
3 use strict;
4 use warnings;
5
6 use PVE::Auth::Plugin;
7 use PVE::JSONSchema;
8 use PVE::LDAP;
9 use PVE::Tools;
10
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 },
33 bind_dn => {
34 description => "LDAP bind domain name",
35 type => 'string',
36 pattern => '\w+=[^,]+(,\s*\w+=[^,]+)*',
37 optional => 1,
38 maxLength => 256,
39 },
40 verify => {
41 description => "Verify the server's SSL certificate",
42 type => 'boolean',
43 optional => 1,
44 default => 0,
45 },
46 capath => {
47 description => "Path to the CA certificate store",
48 type => 'string',
49 optional => 1,
50 default => '/etc/ssl/certs',
51 },
52 cert => {
53 description => "Path to the client certificate",
54 type => 'string',
55 optional => 1,
56 },
57 certkey => {
58 description => "Path to the client certificate key",
59 type => 'string',
60 optional => 1,
61 },
62 filter => {
63 description => "LDAP filter for user sync.",
64 type => 'string',
65 optional => 1,
66 maxLength => 2048,
67 },
68 sync_attributes => {
69 description => "Comma separated list of key=value pairs for specifying"
70 ." which LDAP attributes map to which PVE user field. For example,"
71 ." to map the LDAP attribute 'mail' to PVEs 'email', write "
72 ." 'email=mail'. By default, each PVE user field is represented "
73 ." by an LDAP attribute of the same name.",
74 optional => 1,
75 type => 'string',
76 pattern => '\w+=[^,]+(,\s*\w+=[^,]+)*',
77 },
78 user_classes => {
79 description => "The objectclasses for users.",
80 type => 'string',
81 default => 'inetorgperson, posixaccount, person, user',
82 format => 'ldap-simple-attr-list',
83 optional => 1,
84 },
85 group_dn => {
86 description => "LDAP base domain name for group sync. If not set, the"
87 ." base_dn will be used.",
88 type => 'string',
89 pattern => '\w+=[^,]+(,\s*\w+=[^,]+)*',
90 optional => 1,
91 maxLength => 256,
92 },
93 group_name_attr => {
94 description => "LDAP attribute representing a groups name. If not set"
95 ." or found, the first value of the DN will be used as name.",
96 type => 'string',
97 format => 'ldap-simple-attr',
98 optional => 1,
99 maxLength => 256,
100 },
101 group_filter => {
102 description => "LDAP filter for group sync.",
103 type => 'string',
104 optional => 1,
105 maxLength => 2048,
106 },
107 group_classes => {
108 description => "The objectclasses for groups.",
109 type => 'string',
110 default => 'groupOfNames, group, univentionGroup, ipausergroup',
111 format => 'ldap-simple-attr-list',
112 optional => 1,
113 },
114 'sync-defaults-options' => {
115 description => "The default options for behavior of synchronizations.",
116 type => 'string',
117 format => 'realm-sync-options',
118 optional => 1,
119 },
120 };
121 }
122
123 sub options {
124 return {
125 server1 => {},
126 server2 => { optional => 1 },
127 base_dn => {},
128 bind_dn => { optional => 1 },
129 user_attr => {},
130 port => { optional => 1 },
131 secure => { optional => 1 },
132 sslversion => { optional => 1 },
133 default => { optional => 1 },
134 comment => { optional => 1 },
135 tfa => { optional => 1 },
136 verify => { optional => 1 },
137 capath => { optional => 1 },
138 cert => { optional => 1 },
139 certkey => { optional => 1 },
140 filter => { optional => 1 },
141 sync_attributes => { optional => 1 },
142 user_classes => { optional => 1 },
143 group_dn => { optional => 1 },
144 group_name_attr => { optional => 1 },
145 group_filter => { optional => 1 },
146 group_classes => { optional => 1 },
147 'sync-defaults-options' => { optional => 1 },
148 };
149 }
150
151 sub connect_and_bind {
152 my ($class, $config, $realm) = @_;
153
154 my $servers = [$config->{server1}];
155 push @$servers, $config->{server2} if $config->{server2};
156
157 my $default_port = $config->{secure} ? 636: 389;
158 my $port = $config->{port} // $default_port;
159 my $scheme = $config->{secure} ? 'ldaps' : 'ldap';
160
161 my %ldap_args;
162 if ($config->{verify}) {
163 $ldap_args{verify} = 'require';
164 $ldap_args{clientcert} = $config->{cert} if $config->{cert};
165 $ldap_args{clientkey} = $config->{certkey} if $config->{certkey};
166 if (defined(my $capath = $config->{capath})) {
167 if (-d $capath) {
168 $ldap_args{capath} = $capath;
169 } else {
170 $ldap_args{cafile} = $capath;
171 }
172 }
173 } else {
174 $ldap_args{verify} = 'none';
175 }
176
177 if ($config->{secure}) {
178 $ldap_args{sslversion} = $config->{sslversion} || 'tlsv1_2';
179 }
180
181 my $ldap = PVE::LDAP::ldap_connect($servers, $scheme, $port, \%ldap_args);
182
183 my $bind_dn;
184 my $bind_pass;
185
186 if ($config->{bind_dn}) {
187 $bind_dn = $config->{bind_dn};
188 $bind_pass = PVE::Tools::file_read_firstline("/etc/pve/priv/ldap/${realm}.pw");
189 die "missing password for realm $realm\n" if !defined($bind_pass);
190 }
191
192 PVE::LDAP::ldap_bind($ldap, $bind_dn, $bind_pass);
193
194 if (!$config->{base_dn}) {
195 my $root = $ldap->root_dse(attrs => [ 'defaultNamingContext' ]);
196 $config->{base_dn} = $root->get_value('defaultNamingContext');
197 }
198
199 return $ldap;
200 }
201
202 # returns:
203 # {
204 # 'username@realm' => {
205 # 'attr1' => 'value1',
206 # 'attr2' => 'value2',
207 # ...
208 # },
209 # ...
210 # }
211 #
212 # or in list context:
213 # (
214 # {
215 # 'username@realm' => {
216 # 'attr1' => 'value1',
217 # 'attr2' => 'value2',
218 # ...
219 # },
220 # ...
221 # },
222 # {
223 # 'uid=username,dc=....' => 'username@realm',
224 # ...
225 # }
226 # )
227 # the map of dn->username is needed for group membership sync
228 sub get_users {
229 my ($class, $config, $realm) = @_;
230
231 my $ldap = $class->connect_and_bind($config, $realm);
232
233 my $user_name_attr = $config->{user_attr} // 'uid';
234 my $ldap_attribute_map = {
235 $user_name_attr => 'username',
236 enable => 'enable',
237 expire => 'expire',
238 firstname => 'firstname',
239 lastname => 'lastname',
240 email => 'email',
241 comment => 'comment',
242 keys => 'keys',
243 };
244
245 foreach my $attr (PVE::Tools::split_list($config->{sync_attributes})) {
246 my ($ours, $ldap) = ($attr =~ m/^\s*(\w+)=(.*)\s*$/);
247 $ldap_attribute_map->{$ldap} = $ours;
248 }
249
250 my $filter = $config->{filter};
251 my $basedn = $config->{base_dn};
252
253 $config->{user_classes} //= 'inetorgperson, posixaccount, person, user';
254 my $classes = [PVE::Tools::split_list($config->{user_classes})];
255
256 my $users = PVE::LDAP::query_users($ldap, $filter, [keys %$ldap_attribute_map], $basedn, $classes);
257
258 my $ret = {};
259 my $dnmap = {};
260
261 foreach my $user (@$users) {
262 my $user_attributes = $user->{attributes};
263 my $userid = $user_attributes->{$user_name_attr}->[0];
264 my $username = "$userid\@$realm";
265
266 # we cannot sync usernames that do not meet our criteria
267 eval { PVE::Auth::Plugin::verify_username($username) };
268 if (my $err = $@) {
269 warn "$err";
270 next;
271 }
272
273 $ret->{$username} = {};
274
275 foreach my $attr (keys %$user_attributes) {
276 if (my $ours = $ldap_attribute_map->{$attr}) {
277 $ret->{$username}->{$ours} = $user_attributes->{$attr}->[0];
278 }
279 }
280
281 if (wantarray) {
282 my $dn = $user->{dn};
283 $dnmap->{$dn} = $username;
284 }
285 }
286
287 return wantarray ? ($ret, $dnmap) : $ret;
288 }
289
290 # needs a map for dn -> username, we get this from the get_users call
291 # otherwise we cannot determine the group membership
292 sub get_groups {
293 my ($class, $config, $realm, $dnmap) = @_;
294
295 my $filter = $config->{group_filter};
296 my $basedn = $config->{group_dn} // $config->{base_dn};
297 my $attr = $config->{group_name_attr};
298 $config->{group_classes} //= 'groupOfNames, group, univentionGroup, ipausergroup';
299 my $classes = [PVE::Tools::split_list($config->{group_classes})];
300
301 my $ldap = $class->connect_and_bind($config, $realm);
302
303 my $groups = PVE::LDAP::query_groups($ldap, $basedn, $classes, $filter, $attr);
304
305 my $ret = {};
306
307 foreach my $group (@$groups) {
308 my $name = $group->{name};
309 if (!$name && $group->{dn} =~ m/^[^=]+=([^,]+),/){
310 $name = PVE::Tools::trim($1);
311 }
312 if ($name) {
313 $name .= "-$realm";
314
315 # we cannot sync groups that do not meet our criteria
316 eval { PVE::AccessControl::verify_groupname($name) };
317 if (my $err = $@) {
318 warn "$err";
319 next;
320 }
321
322 $ret->{$name} = { users => {} };
323 foreach my $member (@{$group->{members}}) {
324 if (my $user = $dnmap->{$member}) {
325 $ret->{$name}->{users}->{$user} = 1;
326 }
327 }
328 }
329 }
330
331 return $ret;
332 }
333
334 sub authenticate_user {
335 my ($class, $config, $realm, $username, $password) = @_;
336
337 my $ldap = $class->connect_and_bind($config, $realm);
338
339 my $user_dn = PVE::LDAP::get_user_dn($ldap, $username, $config->{user_attr}, $config->{base_dn});
340 PVE::LDAP::auth_user_dn($ldap, $user_dn, $password);
341
342 $ldap->unbind();
343 return 1;
344 }
345
346 1;