]>
Commit | Line | Data |
---|---|---|
1 | package PVE::API2::Domains; | |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | ||
6 | use PVE::Exception qw(raise_param_exc); | |
7 | use PVE::Tools qw(extract_param); | |
8 | use PVE::Cluster qw (cfs_read_file cfs_write_file); | |
9 | use PVE::AccessControl; | |
10 | use PVE::JSONSchema qw(get_standard_option); | |
11 | ||
12 | use PVE::SafeSyslog; | |
13 | use PVE::RESTHandler; | |
14 | use PVE::Auth::Plugin; | |
15 | ||
16 | my $domainconfigfile = "domains.cfg"; | |
17 | ||
18 | use base qw(PVE::RESTHandler); | |
19 | ||
20 | __PACKAGE__->register_method ({ | |
21 | name => 'index', | |
22 | path => '', | |
23 | method => 'GET', | |
24 | description => "Authentication domain index.", | |
25 | permissions => { | |
26 | description => "Anyone can access that, because we need that list for the login box (before the user is authenticated).", | |
27 | user => 'world', | |
28 | }, | |
29 | parameters => { | |
30 | additionalProperties => 0, | |
31 | properties => {}, | |
32 | }, | |
33 | returns => { | |
34 | type => 'array', | |
35 | items => { | |
36 | type => "object", | |
37 | properties => { | |
38 | realm => { type => 'string' }, | |
39 | type => { type => 'string' }, | |
40 | tfa => { | |
41 | description => "Two-factor authentication provider.", | |
42 | type => 'string', | |
43 | enum => [ 'yubico', 'oath' ], | |
44 | optional => 1, | |
45 | }, | |
46 | comment => { | |
47 | description => "A comment. The GUI use this text when you select a domain (Realm) on the login window.", | |
48 | type => 'string', | |
49 | optional => 1, | |
50 | }, | |
51 | }, | |
52 | }, | |
53 | links => [ { rel => 'child', href => "{realm}" } ], | |
54 | }, | |
55 | code => sub { | |
56 | my ($param) = @_; | |
57 | ||
58 | my $res = []; | |
59 | ||
60 | my $cfg = cfs_read_file($domainconfigfile); | |
61 | my $ids = $cfg->{ids}; | |
62 | ||
63 | foreach my $realm (keys %$ids) { | |
64 | my $d = $ids->{$realm}; | |
65 | my $entry = { realm => $realm, type => $d->{type} }; | |
66 | $entry->{comment} = $d->{comment} if $d->{comment}; | |
67 | $entry->{default} = 1 if $d->{default}; | |
68 | if ($d->{tfa} && (my $tfa_cfg = PVE::Auth::Plugin::parse_tfa_config($d->{tfa}))) { | |
69 | $entry->{tfa} = $tfa_cfg->{type}; | |
70 | } | |
71 | push @$res, $entry; | |
72 | } | |
73 | ||
74 | return $res; | |
75 | }}); | |
76 | ||
77 | __PACKAGE__->register_method ({ | |
78 | name => 'create', | |
79 | protected => 1, | |
80 | path => '', | |
81 | method => 'POST', | |
82 | permissions => { | |
83 | check => ['perm', '/access/realm', ['Realm.Allocate']], | |
84 | }, | |
85 | description => "Add an authentication server.", | |
86 | parameters => PVE::Auth::Plugin->createSchema(), | |
87 | returns => { type => 'null' }, | |
88 | code => sub { | |
89 | my ($param) = @_; | |
90 | ||
91 | # always extract, add it with hook | |
92 | my $password = extract_param($param, 'password'); | |
93 | ||
94 | PVE::Auth::Plugin::lock_domain_config( | |
95 | sub { | |
96 | ||
97 | my $cfg = cfs_read_file($domainconfigfile); | |
98 | my $ids = $cfg->{ids}; | |
99 | ||
100 | my $realm = extract_param($param, 'realm'); | |
101 | my $type = $param->{type}; | |
102 | ||
103 | die "domain '$realm' already exists\n" | |
104 | if $ids->{$realm}; | |
105 | ||
106 | die "unable to use reserved name '$realm'\n" | |
107 | if ($realm eq 'pam' || $realm eq 'pve'); | |
108 | ||
109 | die "unable to create builtin type '$type'\n" | |
110 | if ($type eq 'pam' || $type eq 'pve'); | |
111 | ||
112 | my $plugin = PVE::Auth::Plugin->lookup($type); | |
113 | my $config = $plugin->check_config($realm, $param, 1, 1); | |
114 | ||
115 | if ($config->{default}) { | |
116 | foreach my $r (keys %$ids) { | |
117 | delete $ids->{$r}->{default}; | |
118 | } | |
119 | } | |
120 | ||
121 | $ids->{$realm} = $config; | |
122 | ||
123 | my $opts = $plugin->options(); | |
124 | if (defined($password) && !defined($opts->{password})) { | |
125 | $password = undef; | |
126 | warn "ignoring password parameter"; | |
127 | } | |
128 | $plugin->on_add_hook($realm, $config, password => $password); | |
129 | ||
130 | cfs_write_file($domainconfigfile, $cfg); | |
131 | }, "add auth server failed"); | |
132 | ||
133 | return undef; | |
134 | }}); | |
135 | ||
136 | __PACKAGE__->register_method ({ | |
137 | name => 'update', | |
138 | path => '{realm}', | |
139 | method => 'PUT', | |
140 | permissions => { | |
141 | check => ['perm', '/access/realm', ['Realm.Allocate']], | |
142 | }, | |
143 | description => "Update authentication server settings.", | |
144 | protected => 1, | |
145 | parameters => PVE::Auth::Plugin->updateSchema(), | |
146 | returns => { type => 'null' }, | |
147 | code => sub { | |
148 | my ($param) = @_; | |
149 | ||
150 | # always extract, update in hook | |
151 | my $password = extract_param($param, 'password'); | |
152 | ||
153 | PVE::Auth::Plugin::lock_domain_config( | |
154 | sub { | |
155 | ||
156 | my $cfg = cfs_read_file($domainconfigfile); | |
157 | my $ids = $cfg->{ids}; | |
158 | ||
159 | my $digest = extract_param($param, 'digest'); | |
160 | PVE::SectionConfig::assert_if_modified($cfg, $digest); | |
161 | ||
162 | my $realm = extract_param($param, 'realm'); | |
163 | ||
164 | die "domain '$realm' does not exist\n" | |
165 | if !$ids->{$realm}; | |
166 | ||
167 | my $delete_str = extract_param($param, 'delete'); | |
168 | die "no options specified\n" if !$delete_str && !scalar(keys %$param); | |
169 | ||
170 | my $delete_pw = 0; | |
171 | foreach my $opt (PVE::Tools::split_list($delete_str)) { | |
172 | delete $ids->{$realm}->{$opt}; | |
173 | $delete_pw = 1 if $opt eq 'password'; | |
174 | } | |
175 | ||
176 | my $plugin = PVE::Auth::Plugin->lookup($ids->{$realm}->{type}); | |
177 | my $config = $plugin->check_config($realm, $param, 0, 1); | |
178 | ||
179 | if ($config->{default}) { | |
180 | foreach my $r (keys %$ids) { | |
181 | delete $ids->{$r}->{default}; | |
182 | } | |
183 | } | |
184 | ||
185 | foreach my $p (keys %$config) { | |
186 | $ids->{$realm}->{$p} = $config->{$p}; | |
187 | } | |
188 | ||
189 | my $opts = $plugin->options(); | |
190 | if ($delete_pw || defined($password)) { | |
191 | $plugin->on_update_hook($realm, $config, password => $password); | |
192 | } else { | |
193 | $plugin->on_update_hook($realm, $config); | |
194 | } | |
195 | ||
196 | cfs_write_file($domainconfigfile, $cfg); | |
197 | }, "update auth server failed"); | |
198 | ||
199 | return undef; | |
200 | }}); | |
201 | ||
202 | # fixme: return format! | |
203 | __PACKAGE__->register_method ({ | |
204 | name => 'read', | |
205 | path => '{realm}', | |
206 | method => 'GET', | |
207 | description => "Get auth server configuration.", | |
208 | permissions => { | |
209 | check => ['perm', '/access/realm', ['Realm.Allocate', 'Sys.Audit'], any => 1], | |
210 | }, | |
211 | parameters => { | |
212 | additionalProperties => 0, | |
213 | properties => { | |
214 | realm => get_standard_option('realm'), | |
215 | }, | |
216 | }, | |
217 | returns => {}, | |
218 | code => sub { | |
219 | my ($param) = @_; | |
220 | ||
221 | my $cfg = cfs_read_file($domainconfigfile); | |
222 | ||
223 | my $realm = $param->{realm}; | |
224 | ||
225 | my $data = $cfg->{ids}->{$realm}; | |
226 | die "domain '$realm' does not exist\n" if !$data; | |
227 | ||
228 | $data->{digest} = $cfg->{digest}; | |
229 | ||
230 | return $data; | |
231 | }}); | |
232 | ||
233 | ||
234 | __PACKAGE__->register_method ({ | |
235 | name => 'delete', | |
236 | path => '{realm}', | |
237 | method => 'DELETE', | |
238 | permissions => { | |
239 | check => ['perm', '/access/realm', ['Realm.Allocate']], | |
240 | }, | |
241 | description => "Delete an authentication server.", | |
242 | protected => 1, | |
243 | parameters => { | |
244 | additionalProperties => 0, | |
245 | properties => { | |
246 | realm => get_standard_option('realm'), | |
247 | } | |
248 | }, | |
249 | returns => { type => 'null' }, | |
250 | code => sub { | |
251 | my ($param) = @_; | |
252 | ||
253 | PVE::Auth::Plugin::lock_domain_config( | |
254 | sub { | |
255 | ||
256 | my $cfg = cfs_read_file($domainconfigfile); | |
257 | my $ids = $cfg->{ids}; | |
258 | my $realm = $param->{realm}; | |
259 | ||
260 | die "authentication domain '$realm' does not exist\n" if !$ids->{$realm}; | |
261 | ||
262 | my $plugin = PVE::Auth::Plugin->lookup($ids->{$realm}->{type}); | |
263 | ||
264 | $plugin->on_delete_hook($realm, $ids->{$realm}); | |
265 | ||
266 | delete $ids->{$realm}; | |
267 | ||
268 | cfs_write_file($domainconfigfile, $cfg); | |
269 | }, "delete auth server failed"); | |
270 | ||
271 | return undef; | |
272 | }}); | |
273 | ||
274 | my $update_users = sub { | |
275 | my ($usercfg, $realm, $synced_users, $opts) = @_; | |
276 | ||
277 | print "syncing users\n"; | |
278 | $usercfg->{users} = {} if !defined($usercfg->{users}); | |
279 | my $users = $usercfg->{users}; | |
280 | ||
281 | my $oldusers = {}; | |
282 | if ($opts->{'full'}) { | |
283 | print "full sync, deleting outdated existing users first\n"; | |
284 | foreach my $userid (sort keys %$users) { | |
285 | next if $userid !~ m/\@$realm$/; | |
286 | ||
287 | $oldusers->{$userid} = delete $users->{$userid}; | |
288 | if ($opts->{'purge'} && !$synced_users->{$userid}) { | |
289 | PVE::AccessControl::delete_user_acl($userid, $usercfg); | |
290 | print "purged user '$userid' and all its ACL entries\n"; | |
291 | } elsif (!defined($synced_users->{$userid})) { | |
292 | print "remove user '$userid'\n"; | |
293 | } | |
294 | } | |
295 | } | |
296 | ||
297 | foreach my $userid (sort keys %$synced_users) { | |
298 | my $synced_user = $synced_users->{$userid} // {}; | |
299 | if (!defined($users->{$userid})) { | |
300 | my $user = $users->{$userid} = $synced_user; | |
301 | ||
302 | my $olduser = $oldusers->{$userid} // {}; | |
303 | if (defined(my $enabled = $olduser->{enable})) { | |
304 | $user->{enable} = $enabled; | |
305 | } elsif ($opts->{'enable-new'}) { | |
306 | $user->{enable} = 1; | |
307 | } | |
308 | ||
309 | if (defined($olduser->{tokens})) { | |
310 | $user->{tokens} = $olduser->{tokens}; | |
311 | } | |
312 | if (defined($oldusers->{$userid})) { | |
313 | print "updated user '$userid'\n"; | |
314 | } else { | |
315 | print "added user '$userid'\n"; | |
316 | } | |
317 | } else { | |
318 | my $olduser = $users->{$userid}; | |
319 | foreach my $attr (keys %$synced_user) { | |
320 | $olduser->{$attr} = $synced_user->{$attr}; | |
321 | } | |
322 | print "updated user '$userid'\n"; | |
323 | } | |
324 | } | |
325 | }; | |
326 | ||
327 | my $update_groups = sub { | |
328 | my ($usercfg, $realm, $synced_groups, $opts) = @_; | |
329 | ||
330 | print "syncing groups\n"; | |
331 | $usercfg->{groups} = {} if !defined($usercfg->{groups}); | |
332 | my $groups = $usercfg->{groups}; | |
333 | my $oldgroups = {}; | |
334 | ||
335 | if ($opts->{full}) { | |
336 | print "full sync, deleting outdated existing groups first\n"; | |
337 | foreach my $groupid (sort keys %$groups) { | |
338 | next if $groupid !~ m/\-$realm$/; | |
339 | ||
340 | my $oldgroups->{$groupid} = delete $groups->{$groupid}; | |
341 | if ($opts->{purge} && !$synced_groups->{$groupid}) { | |
342 | print "purged group '$groupid' and all its ACL entries\n"; | |
343 | PVE::AccessControl::delete_group_acl($groupid, $usercfg) | |
344 | } elsif (!defined($synced_groups->{$groupid})) { | |
345 | print "removed group '$groupid'\n"; | |
346 | } | |
347 | } | |
348 | } | |
349 | ||
350 | foreach my $groupid (sort keys %$synced_groups) { | |
351 | my $synced_group = $synced_groups->{$groupid}; | |
352 | if (!defined($groups->{$groupid})) { | |
353 | $groups->{$groupid} = $synced_group; | |
354 | if (defined($oldgroups->{$groupid})) { | |
355 | print "updated group '$groupid'\n"; | |
356 | } else { | |
357 | print "added group '$groupid'\n"; | |
358 | } | |
359 | } else { | |
360 | my $group = $groups->{$groupid}; | |
361 | foreach my $attr (keys %$synced_group) { | |
362 | $group->{$attr} = $synced_group->{$attr}; | |
363 | } | |
364 | print "updated group '$groupid'\n"; | |
365 | } | |
366 | } | |
367 | }; | |
368 | ||
369 | my $parse_sync_opts = sub { | |
370 | my ($param, $realmconfig) = @_; | |
371 | ||
372 | my $sync_opts_fmt = PVE::JSONSchema::get_format('realm-sync-options'); | |
373 | ||
374 | my $cfg_defaults = {}; | |
375 | if (defined(my $cfg_opts = $realmconfig->{'sync-defaults-options'})) { | |
376 | $cfg_defaults = PVE::JSONSchema::parse_property_string($sync_opts_fmt, $cfg_opts); | |
377 | } | |
378 | ||
379 | my $res = {}; | |
380 | for my $opt (sort keys %$sync_opts_fmt) { | |
381 | my $fmt = $sync_opts_fmt->{$opt}; | |
382 | ||
383 | $res->{$opt} = $param->{$opt} // $cfg_defaults->{$opt} // $fmt->{default}; | |
384 | ||
385 | raise_param_exc({ | |
386 | "$opt" => 'Not passed as parameter and not defined in realm default sync options.' | |
387 | }) if !defined($res->{$opt}); | |
388 | } | |
389 | return $res; | |
390 | }; | |
391 | ||
392 | __PACKAGE__->register_method ({ | |
393 | name => 'sync', | |
394 | path => '{realm}/sync', | |
395 | method => 'POST', | |
396 | permissions => { | |
397 | description => "'Realm.AllocateUser' on '/access/realm/<realm>' and " | |
398 | ." 'User.Modify' permissions to '/access/groups/'.", | |
399 | check => [ 'and', | |
400 | [ 'userid-param', 'Realm.AllocateUser' ], | |
401 | [ 'userid-group', ['User.Modify'] ], | |
402 | ], | |
403 | }, | |
404 | description => "Syncs users and/or groups from the configured LDAP to user.cfg." | |
405 | ." NOTE: Synced groups will have the name 'name-\$realm', so make sure" | |
406 | ." those groups do not exist to prevent overwriting.", | |
407 | protected => 1, | |
408 | parameters => { | |
409 | additionalProperties => 0, | |
410 | properties => get_standard_option('realm-sync-options', { | |
411 | realm => get_standard_option('realm'), | |
412 | 'dry-run' => { | |
413 | description => "If set, does not write anything.", | |
414 | type => 'boolean', | |
415 | optional => 1, | |
416 | default => 0, | |
417 | }, | |
418 | }), | |
419 | }, | |
420 | returns => { | |
421 | description => 'Worker Task-UPID', | |
422 | type => 'string' | |
423 | }, | |
424 | code => sub { | |
425 | my ($param) = @_; | |
426 | ||
427 | my $rpcenv = PVE::RPCEnvironment::get(); | |
428 | my $authuser = $rpcenv->get_user(); | |
429 | ||
430 | my $dry_run = extract_param($param, 'dry-run'); | |
431 | my $realm = $param->{realm}; | |
432 | my $cfg = cfs_read_file($domainconfigfile); | |
433 | my $realmconfig = $cfg->{ids}->{$realm}; | |
434 | ||
435 | raise_param_exc({ 'realm' => 'Realm does not exist.' }) if !defined($realmconfig); | |
436 | my $type = $realmconfig->{type}; | |
437 | ||
438 | if ($type ne 'ldap' && $type ne 'ad') { | |
439 | die "Cannot sync realm type '$type'! Only LDAP/AD realms can be synced.\n"; | |
440 | } | |
441 | ||
442 | my $opts = $parse_sync_opts->($param, $realmconfig); # can throw up | |
443 | ||
444 | my $scope = $opts->{scope}; | |
445 | my $whatstring = $scope eq 'both' ? "users and groups" : $scope; | |
446 | ||
447 | my $plugin = PVE::Auth::Plugin->lookup($type); | |
448 | ||
449 | my $worker = sub { | |
450 | print "(dry test run) " if $dry_run; | |
451 | print "starting sync for realm $realm\n"; | |
452 | ||
453 | my ($synced_users, $dnmap) = $plugin->get_users($realmconfig, $realm); | |
454 | my $synced_groups = {}; | |
455 | if ($scope eq 'groups' || $scope eq 'both') { | |
456 | $synced_groups = $plugin->get_groups($realmconfig, $realm, $dnmap); | |
457 | } | |
458 | ||
459 | PVE::AccessControl::lock_user_config(sub { | |
460 | my $usercfg = cfs_read_file("user.cfg"); | |
461 | print "got data from server, updating $whatstring\n"; | |
462 | ||
463 | if ($scope eq 'users' || $scope eq 'both') { | |
464 | $update_users->($usercfg, $realm, $synced_users, $opts); | |
465 | } | |
466 | ||
467 | if ($scope eq 'groups' || $scope eq 'both') { | |
468 | $update_groups->($usercfg, $realm, $synced_groups, $opts); | |
469 | } | |
470 | ||
471 | if ($dry_run) { | |
472 | print "\nNOTE: Dry test run, changes were NOT written to the configuration.\n"; | |
473 | return; | |
474 | } | |
475 | cfs_write_file("user.cfg", $usercfg); | |
476 | print "successfully updated $whatstring configuration\n"; | |
477 | }, "syncing $whatstring failed"); | |
478 | }; | |
479 | ||
480 | my $workerid = !$dry_run ? 'auth-realm-sync' : 'auth-realm-sync-test'; | |
481 | return $rpcenv->fork_worker($workerid, $realm, $authuser, $worker); | |
482 | }}); | |
483 | ||
484 | 1; |