]> git.proxmox.com Git - pve-access-control.git/blame - src/PVE/API2/Domains.pm
api: realm sync: avoid separate log line for "remove-vanished" opt
[pve-access-control.git] / src / PVE / API2 / Domains.pm
CommitLineData
2c3a6c0a
DM
1package PVE::API2::Domains;
2
3use strict;
4use warnings;
b49abe2d 5
673d2bf2 6use PVE::Exception qw(raise_param_exc);
5bb4e06a 7use PVE::Tools qw(extract_param);
2c3a6c0a
DM
8use PVE::Cluster qw (cfs_read_file cfs_write_file);
9use PVE::AccessControl;
10use PVE::JSONSchema qw(get_standard_option);
11
12use PVE::SafeSyslog;
2c3a6c0a 13use PVE::RESTHandler;
5bb4e06a 14use PVE::Auth::Plugin;
2c3a6c0a
DM
15
16my $domainconfigfile = "domains.cfg";
17
18use base qw(PVE::RESTHandler);
19
98df2ebc
DC
20# maps old 'full'/'purge' parameters to new 'remove-vanished'
21# TODO remove when we delete the 'full'/'purge' parameters
22my $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
40my $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
2c3a6c0a 54__PACKAGE__->register_method ({
32449f35
DC
55 name => 'index',
56 path => '',
2c3a6c0a
DM
57 method => 'GET',
58 description => "Authentication domain index.",
32449f35 59 permissions => {
82b63965 60 description => "Anyone can access that, because we need that list for the login box (before the user is authenticated).",
32449f35 61 user => 'world',
82b63965 62 },
2c3a6c0a
DM
63 parameters => {
64 additionalProperties => 0,
65 properties => {},
66 },
67 returns => {
68 type => 'array',
69 items => {
70 type => "object",
71 properties => {
72 realm => { type => 'string' },
f3c87f9b 73 type => { type => 'string' },
96f8ebd6
DM
74 tfa => {
75 description => "Two-factor authentication provider.",
76 type => 'string',
1abc2c0a 77 enum => [ 'yubico', 'oath' ],
96f8ebd6
DM
78 optional => 1,
79 },
52b2eff3
DM
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 },
2c3a6c0a
DM
85 },
86 },
87 links => [ { rel => 'child', href => "{realm}" } ],
88 },
89 code => sub {
90 my ($param) = @_;
32449f35 91
2c3a6c0a
DM
92 my $res = [];
93
94 my $cfg = cfs_read_file($domainconfigfile);
5bb4e06a
DM
95 my $ids = $cfg->{ids};
96
97 foreach my $realm (keys %$ids) {
98 my $d = $ids->{$realm};
2c3a6c0a
DM
99 my $entry = { realm => $realm, type => $d->{type} };
100 $entry->{comment} = $d->{comment} if $d->{comment};
101 $entry->{default} = 1 if $d->{default};
96f8ebd6
DM
102 if ($d->{tfa} && (my $tfa_cfg = PVE::Auth::Plugin::parse_tfa_config($d->{tfa}))) {
103 $entry->{tfa} = $tfa_cfg->{type};
104 }
2c3a6c0a
DM
105 push @$res, $entry;
106 }
107
108 return $res;
109 }});
110
111__PACKAGE__->register_method ({
32449f35 112 name => 'create',
2c3a6c0a 113 protected => 1,
32449f35 114 path => '',
2c3a6c0a 115 method => 'POST',
32449f35 116 permissions => {
82b63965 117 check => ['perm', '/access/realm', ['Realm.Allocate']],
96919234 118 },
2c3a6c0a 119 description => "Add an authentication server.",
5bb4e06a 120 parameters => PVE::Auth::Plugin->createSchema(),
2c3a6c0a
DM
121 returns => { type => 'null' },
122 code => sub {
123 my ($param) = @_;
124
89338e4d
TL
125 # always extract, add it with hook
126 my $password = extract_param($param, 'password');
127
5bb4e06a 128 PVE::Auth::Plugin::lock_domain_config(
2c3a6c0a 129 sub {
32449f35 130
2c3a6c0a 131 my $cfg = cfs_read_file($domainconfigfile);
5bb4e06a 132 my $ids = $cfg->{ids};
2c3a6c0a 133
5bb4e06a
DM
134 my $realm = extract_param($param, 'realm');
135 my $type = $param->{type};
32449f35
DC
136
137 die "domain '$realm' already exists\n"
5bb4e06a 138 if $ids->{$realm};
2c3a6c0a
DM
139
140 die "unable to use reserved name '$realm'\n"
141 if ($realm eq 'pam' || $realm eq 'pve');
142
5bb4e06a
DM
143 die "unable to create builtin type '$type'\n"
144 if ($type eq 'pam' || $type eq 'pve');
af4a8a85 145
98df2ebc
DC
146 if ($type eq 'ad' || $type eq 'ldap') {
147 $map_sync_default_options->($param, 1);
148 }
149
5bb4e06a
DM
150 my $plugin = PVE::Auth::Plugin->lookup($type);
151 my $config = $plugin->check_config($realm, $param, 1, 1);
2c3a6c0a 152
5bb4e06a
DM
153 if ($config->{default}) {
154 foreach my $r (keys %$ids) {
155 delete $ids->{$r}->{default};
0c156363 156 }
af4a8a85
DM
157 }
158
5bb4e06a
DM
159 $ids->{$realm} = $config;
160
89338e4d
TL
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
2c3a6c0a
DM
168 cfs_write_file($domainconfigfile, $cfg);
169 }, "add auth server failed");
170
171 return undef;
172 }});
173
174__PACKAGE__->register_method ({
32449f35
DC
175 name => 'update',
176 path => '{realm}',
2c3a6c0a 177 method => 'PUT',
32449f35 178 permissions => {
82b63965 179 check => ['perm', '/access/realm', ['Realm.Allocate']],
96919234 180 },
2c3a6c0a
DM
181 description => "Update authentication server settings.",
182 protected => 1,
5bb4e06a 183 parameters => PVE::Auth::Plugin->updateSchema(),
2c3a6c0a
DM
184 returns => { type => 'null' },
185 code => sub {
186 my ($param) = @_;
187
89338e4d
TL
188 # always extract, update in hook
189 my $password = extract_param($param, 'password');
190
5bb4e06a 191 PVE::Auth::Plugin::lock_domain_config(
2c3a6c0a 192 sub {
32449f35 193
2c3a6c0a 194 my $cfg = cfs_read_file($domainconfigfile);
5bb4e06a 195 my $ids = $cfg->{ids};
2c3a6c0a 196
5bb4e06a
DM
197 my $digest = extract_param($param, 'digest');
198 PVE::SectionConfig::assert_if_modified($cfg, $digest);
199
200 my $realm = extract_param($param, 'realm');
2c3a6c0a 201
32449f35 202 die "domain '$realm' does not exist\n"
5bb4e06a
DM
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);
2c3a6c0a 207
89338e4d 208 my $delete_pw = 0;
5bb4e06a
DM
209 foreach my $opt (PVE::Tools::split_list($delete_str)) {
210 delete $ids->{$realm}->{$opt};
89338e4d 211 $delete_pw = 1 if $opt eq 'password';
2c3a6c0a 212 }
32449f35 213
98df2ebc
DC
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);
5bb4e06a 220 my $config = $plugin->check_config($realm, $param, 0, 1);
2c3a6c0a 221
5bb4e06a
DM
222 if ($config->{default}) {
223 foreach my $r (keys %$ids) {
224 delete $ids->{$r}->{default};
2c3a6c0a
DM
225 }
226 }
227
5bb4e06a
DM
228 foreach my $p (keys %$config) {
229 $ids->{$realm}->{$p} = $config->{$p};
af4a8a85
DM
230 }
231
89338e4d
TL
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
2c3a6c0a
DM
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 ({
32449f35
DC
247 name => 'read',
248 path => '{realm}',
2c3a6c0a
DM
249 method => 'GET',
250 description => "Get auth server configuration.",
32449f35 251 permissions => {
82b63965 252 check => ['perm', '/access/realm', ['Realm.Allocate', 'Sys.Audit'], any => 1],
96919234 253 },
2c3a6c0a
DM
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};
32449f35 267
5bb4e06a 268 my $data = $cfg->{ids}->{$realm};
2c3a6c0a
DM
269 die "domain '$realm' does not exist\n" if !$data;
270
98df2ebc
DC
271 my $type = $data->{type};
272 if ($type eq 'ad' || $type eq 'ldap') {
273 $map_sync_default_options->($data);
274 }
275
5bb4e06a
DM
276 $data->{digest} = $cfg->{digest};
277
2c3a6c0a
DM
278 return $data;
279 }});
280
281
282__PACKAGE__->register_method ({
32449f35
DC
283 name => 'delete',
284 path => '{realm}',
2c3a6c0a 285 method => 'DELETE',
32449f35 286 permissions => {
82b63965 287 check => ['perm', '/access/realm', ['Realm.Allocate']],
96919234 288 },
2c3a6c0a
DM
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
5bb4e06a 301 PVE::Auth::Plugin::lock_domain_config(
2c3a6c0a
DM
302 sub {
303
304 my $cfg = cfs_read_file($domainconfigfile);
5bb4e06a 305 my $ids = $cfg->{ids};
2c3a6c0a 306 my $realm = $param->{realm};
32449f35 307
89338e4d
TL
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});
2c3a6c0a 313
5bb4e06a 314 delete $ids->{$realm};
2c3a6c0a
DM
315
316 cfs_write_file($domainconfigfile, $cfg);
317 }, "delete auth server failed");
32449f35 318
2c3a6c0a
DM
319 return undef;
320 }});
321
415179b0
TL
322my $update_users = sub {
323 my ($usercfg, $realm, $synced_users, $opts) = @_;
324
187cb854
TL
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 }
98df2ebc 330
415179b0
TL
331 $usercfg->{users} = {} if !defined($usercfg->{users});
332 my $users = $usercfg->{users};
98df2ebc 333 my $to_remove = { map { $_ => 1 } split(';', $opts->{'remove-vanished'} // '') };
415179b0 334
98df2ebc
DC
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);
415179b0
TL
348 }
349 }
350
351 foreach my $userid (sort keys %$synced_users) {
352 my $synced_user = $synced_users->{$userid} // {};
98df2ebc
DC
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 }
415179b0
TL
362 my $user = $users->{$userid} = $synced_user;
363
98df2ebc
DC
364 my $enabled = $olduser->{enable} // $opts->{'enable-new'};
365 $user->{enable} = $enabled if defined($enabled);
366 $user->{tokens} = $olduser->{tokens} if defined($olduser->{tokens});
415179b0 367
415179b0 368 } else {
415179b0
TL
369 foreach my $attr (keys %$synced_user) {
370 $olduser->{$attr} = $synced_user->{$attr};
371 }
98df2ebc 372 print "updating user '$userid'\n";
415179b0
TL
373 }
374 }
375};
376
377my $update_groups = sub {
378 my ($usercfg, $realm, $synced_groups, $opts) = @_;
379
187cb854
TL
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 }
98df2ebc 385
415179b0
TL
386 $usercfg->{groups} = {} if !defined($usercfg->{groups});
387 my $groups = $usercfg->{groups};
98df2ebc
DC
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);
415179b0
TL
403 }
404 }
405
406 foreach my $groupid (sort keys %$synced_groups) {
407 my $synced_group = $synced_groups->{$groupid};
98df2ebc
DC
408 my $oldgroup = $groups->{$groupid};
409 if ($to_remove->{properties} || !defined($oldgroup)) {
410 if (defined($oldgroup)) {
411 print "overwriting group '$groupid'\n";
415179b0 412 } else {
98df2ebc 413 print "adding group '$groupid'\n";
415179b0 414 }
98df2ebc 415 $groups->{$groupid} = $synced_group;
415179b0 416 } else {
415179b0 417 foreach my $attr (keys %$synced_group) {
98df2ebc 418 $oldgroup->{$attr} = $synced_group->{$attr};
415179b0 419 }
98df2ebc 420 print "updating group '$groupid'\n";
415179b0
TL
421 }
422 }
423};
424
d29d2d4a
TL
425my $parse_sync_opts = sub {
426 my ($param, $realmconfig) = @_;
427
428 my $sync_opts_fmt = PVE::JSONSchema::get_format('realm-sync-options');
429
6c42a103 430 my $cfg_defaults = {};
d29d2d4a 431 if (defined(my $cfg_opts = $realmconfig->{'sync-defaults-options'})) {
6c42a103 432 $cfg_defaults = PVE::JSONSchema::parse_property_string($sync_opts_fmt, $cfg_opts);
d29d2d4a
TL
433 }
434
6c42a103 435 my $res = {};
d29d2d4a
TL
436 for my $opt (sort keys %$sync_opts_fmt) {
437 my $fmt = $sync_opts_fmt->{$opt};
438
6c42a103 439 $res->{$opt} = $param->{$opt} // $cfg_defaults->{$opt} // $fmt->{default};
d29d2d4a 440 }
98df2ebc
DC
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
d29d2d4a
TL
449 return $res;
450};
451
673d2bf2
DC
452__PACKAGE__->register_method ({
453 name => 'sync',
454 path => '{realm}/sync',
455 method => 'POST',
456 permissions => {
cf109814
TL
457 description => "'Realm.AllocateUser' on '/access/realm/<realm>' and "
458 ." 'User.Modify' permissions to '/access/groups/'.",
673d2bf2 459 check => [ 'and',
0c503211
WB
460 ['perm', '/access/realm/{realm}', ['Realm.AllocateUser']],
461 ['perm', '/access/groups', ['User.Modify']],
cf109814 462 ],
673d2bf2 463 },
cf109814
TL
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.",
673d2bf2
DC
467 protected => 1,
468 parameters => {
469 additionalProperties => 0,
d29d2d4a
TL
470 properties => get_standard_option('realm-sync-options', {
471 realm => get_standard_option('realm'),
38691d98
DC
472 'dry-run' => {
473 description => "If set, does not write anything.",
474 type => 'boolean',
475 optional => 1,
476 default => 0,
477 },
478 }),
673d2bf2 479 },
cf109814
TL
480 returns => {
481 description => 'Worker Task-UPID',
482 type => 'string'
483 },
673d2bf2
DC
484 code => sub {
485 my ($param) = @_;
486
487 my $rpcenv = PVE::RPCEnvironment::get();
488 my $authuser = $rpcenv->get_user();
489
417309d7 490 my $dry_run = extract_param($param, 'dry-run');
673d2bf2
DC
491 my $realm = $param->{realm};
492 my $cfg = cfs_read_file($domainconfigfile);
cf109814 493 my $realmconfig = $cfg->{ids}->{$realm};
673d2bf2 494
cf109814
TL
495 raise_param_exc({ 'realm' => 'Realm does not exist.' }) if !defined($realmconfig);
496 my $type = $realmconfig->{type};
673d2bf2
DC
497
498 if ($type ne 'ldap' && $type ne 'ad') {
cf109814 499 die "Cannot sync realm type '$type'! Only LDAP/AD realms can be synced.\n";
673d2bf2 500 }
673d2bf2 501
d29d2d4a 502 my $opts = $parse_sync_opts->($param, $realmconfig); # can throw up
673d2bf2 503
d29d2d4a 504 my $scope = $opts->{scope};
cf109814 505 my $whatstring = $scope eq 'both' ? "users and groups" : $scope;
673d2bf2 506
cf109814 507 my $plugin = PVE::Auth::Plugin->lookup($type);
673d2bf2
DC
508
509 my $worker = sub {
417309d7
TL
510 print "(dry test run) " if $dry_run;
511 print "starting sync for realm $realm\n";
cf109814
TL
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);
673d2bf2
DC
517 }
518
cf109814 519 PVE::AccessControl::lock_user_config(sub {
673d2bf2 520 my $usercfg = cfs_read_file("user.cfg");
cf109814 521 print "got data from server, updating $whatstring\n";
673d2bf2 522
415179b0 523 if ($scope eq 'users' || $scope eq 'both') {
d29d2d4a 524 $update_users->($usercfg, $realm, $synced_users, $opts);
673d2bf2
DC
525 }
526
415179b0 527 if ($scope eq 'groups' || $scope eq 'both') {
d29d2d4a 528 $update_groups->($usercfg, $realm, $synced_groups, $opts);
673d2bf2 529 }
cf109814 530
417309d7
TL
531 if ($dry_run) {
532 print "\nNOTE: Dry test run, changes were NOT written to the configuration.\n";
533 return;
38691d98 534 }
417309d7
TL
535 cfs_write_file("user.cfg", $usercfg);
536 print "successfully updated $whatstring configuration\n";
cf109814 537 }, "syncing $whatstring failed");
673d2bf2
DC
538 };
539
417309d7 540 my $workerid = !$dry_run ? 'auth-realm-sync' : 'auth-realm-sync-test';
38691d98 541 return $rpcenv->fork_worker($workerid, $realm, $authuser, $worker);
673d2bf2
DC
542 }});
543
2c3a6c0a 5441;