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