]>
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 | }, | |
e03c2aef WB |
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 | }, | |
eba326d2 DC |
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 | }, | |
d29d2d4a TL |
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 | }, | |
5bb4e06a DM |
120 | }; |
121 | } | |
122 | ||
123 | sub options { | |
124 | return { | |
125 | server1 => {}, | |
126 | server2 => { optional => 1 }, | |
127 | base_dn => {}, | |
b5040b42 | 128 | bind_dn => { optional => 1 }, |
5bb4e06a DM |
129 | user_attr => {}, |
130 | port => { optional => 1 }, | |
131 | secure => { optional => 1 }, | |
07dd90d7 | 132 | sslversion => { optional => 1 }, |
5bb4e06a DM |
133 | default => { optional => 1 }, |
134 | comment => { optional => 1 }, | |
96f8ebd6 | 135 | tfa => { optional => 1 }, |
e03c2aef WB |
136 | verify => { optional => 1 }, |
137 | capath => { optional => 1 }, | |
138 | cert => { optional => 1 }, | |
139 | certkey => { optional => 1 }, | |
eba326d2 DC |
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 }, | |
d29d2d4a | 147 | 'sync-defaults-options' => { optional => 1 }, |
5bb4e06a DM |
148 | }; |
149 | } | |
150 | ||
30aad017 DC |
151 | sub connect_and_bind { |
152 | my ($class, $config, $realm) = @_; | |
d9e93d2e DC |
153 | |
154 | my $servers = [$config->{server1}]; | |
155 | push @$servers, $config->{server2} if $config->{server2}; | |
5bb4e06a DM |
156 | |
157 | my $default_port = $config->{secure} ? 636: 389; | |
d9e93d2e | 158 | my $port = $config->{port} // $default_port; |
5bb4e06a | 159 | my $scheme = $config->{secure} ? 'ldaps' : 'ldap'; |
5bb4e06a | 160 | |
e03c2aef WB |
161 | my %ldap_args; |
162 | if ($config->{verify}) { | |
163 | $ldap_args{verify} = 'require'; | |
d9e93d2e DC |
164 | $ldap_args{clientcert} = $config->{cert} if $config->{cert}; |
165 | $ldap_args{clientkey} = $config->{certkey} if $config->{certkey}; | |
e03c2aef WB |
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 | ||
07dd90d7 | 177 | if ($config->{secure}) { |
3b7eaef1 | 178 | $ldap_args{sslversion} = $config->{sslversion} || 'tlsv1_2'; |
07dd90d7 AD |
179 | } |
180 | ||
d9e93d2e DC |
181 | my $ldap = PVE::LDAP::ldap_connect($servers, $scheme, $port, \%ldap_args); |
182 | ||
183 | my $bind_dn; | |
184 | my $bind_pass; | |
b5040b42 | 185 | |
d9e93d2e DC |
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"); | |
b5040b42 | 189 | die "missing password for realm $realm\n" if !defined($bind_pass); |
b5040b42 WB |
190 | } |
191 | ||
d9e93d2e | 192 | PVE::LDAP::ldap_bind($ldap, $bind_dn, $bind_pass); |
30aad017 DC |
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 | ||
2c6e956e DC |
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 | ||
30aad017 DC |
334 | sub authenticate_user { |
335 | my ($class, $config, $realm, $username, $password) = @_; | |
336 | ||
337 | my $ldap = $class->connect_and_bind($config, $realm); | |
338 | ||
d9e93d2e DC |
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); | |
5bb4e06a DM |
341 | |
342 | $ldap->unbind(); | |
d9e93d2e | 343 | return 1; |
5bb4e06a DM |
344 | } |
345 | ||
346 | 1; |