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