]> git.proxmox.com Git - pve-access-control.git/blame - src/PVE/API2/Domains.pm
fix #3668: realm-sync: replace 'full' & 'purge' with 'remove-vanished'
[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
325 print "syncing users\n";
98df2ebc
DC
326 print "remove-vanished: $opts->{'remove-vanished'}\n" if defined($opts->{'remove-vanished'});
327
415179b0
TL
328 $usercfg->{users} = {} if !defined($usercfg->{users});
329 my $users = $usercfg->{users};
98df2ebc 330 my $to_remove = { map { $_ => 1 } split(';', $opts->{'remove-vanished'} // '') };
415179b0 331
98df2ebc
DC
332 print "deleting outdated existing users first\n" if $to_remove->{entry};
333 foreach my $userid (sort keys %$users) {
334 next if $userid !~ m/\@$realm$/;
335 next if defined($synced_users->{$userid});
336
337 if ($to_remove->{entry}) {
338 print "remove user '$userid'\n";
339 delete $users->{$userid};
340 }
341
342 if ($to_remove->{acl}) {
343 print "purge users '$userid' ACL entries\n";
344 PVE::AccessControl::delete_user_acl($userid, $usercfg);
415179b0
TL
345 }
346 }
347
348 foreach my $userid (sort keys %$synced_users) {
349 my $synced_user = $synced_users->{$userid} // {};
98df2ebc
DC
350 my $olduser = $users->{$userid};
351 if ($to_remove->{properties} || !defined($olduser)) {
352 # we use the synced user, but want to keep some properties on update
353 if (defined($olduser)) {
354 print "overwriting user '$userid'\n";
355 } else {
356 $olduser = {};
357 print "adding user '$userid'\n";
358 }
415179b0
TL
359 my $user = $users->{$userid} = $synced_user;
360
98df2ebc
DC
361 my $enabled = $olduser->{enable} // $opts->{'enable-new'};
362 $user->{enable} = $enabled if defined($enabled);
363 $user->{tokens} = $olduser->{tokens} if defined($olduser->{tokens});
415179b0 364
415179b0 365 } else {
415179b0
TL
366 foreach my $attr (keys %$synced_user) {
367 $olduser->{$attr} = $synced_user->{$attr};
368 }
98df2ebc 369 print "updating user '$userid'\n";
415179b0
TL
370 }
371 }
372};
373
374my $update_groups = sub {
375 my ($usercfg, $realm, $synced_groups, $opts) = @_;
376
377 print "syncing groups\n";
98df2ebc
DC
378 print "remove-vanished: $opts->{'remove-vanished'}\n" if defined($opts->{'remove-vanished'});
379
415179b0
TL
380 $usercfg->{groups} = {} if !defined($usercfg->{groups});
381 my $groups = $usercfg->{groups};
98df2ebc
DC
382 my $to_remove = { map { $_ => 1 } split(';', $opts->{'remove-vanished'} // '') };
383
384 print "deleting outdated existing groups first\n" if $to_remove->{entry};
385 foreach my $groupid (sort keys %$groups) {
386 next if $groupid !~ m/\-$realm$/;
387 next if defined($synced_groups->{$groupid});
388
389 if ($to_remove->{entry}) {
390 print "remove group '$groupid'\n";
391 delete $groups->{$groupid};
392 }
393
394 if ($to_remove->{acl}) {
395 print "purge groups '$groupid' ACL entries\n";
396 PVE::AccessControl::delete_group_acl($groupid, $usercfg);
415179b0
TL
397 }
398 }
399
400 foreach my $groupid (sort keys %$synced_groups) {
401 my $synced_group = $synced_groups->{$groupid};
98df2ebc
DC
402 my $oldgroup = $groups->{$groupid};
403 if ($to_remove->{properties} || !defined($oldgroup)) {
404 if (defined($oldgroup)) {
405 print "overwriting group '$groupid'\n";
415179b0 406 } else {
98df2ebc 407 print "adding group '$groupid'\n";
415179b0 408 }
98df2ebc 409 $groups->{$groupid} = $synced_group;
415179b0 410 } else {
415179b0 411 foreach my $attr (keys %$synced_group) {
98df2ebc 412 $oldgroup->{$attr} = $synced_group->{$attr};
415179b0 413 }
98df2ebc 414 print "updating group '$groupid'\n";
415179b0
TL
415 }
416 }
417};
418
d29d2d4a
TL
419my $parse_sync_opts = sub {
420 my ($param, $realmconfig) = @_;
421
422 my $sync_opts_fmt = PVE::JSONSchema::get_format('realm-sync-options');
423
6c42a103 424 my $cfg_defaults = {};
d29d2d4a 425 if (defined(my $cfg_opts = $realmconfig->{'sync-defaults-options'})) {
6c42a103 426 $cfg_defaults = PVE::JSONSchema::parse_property_string($sync_opts_fmt, $cfg_opts);
d29d2d4a
TL
427 }
428
6c42a103 429 my $res = {};
d29d2d4a
TL
430 for my $opt (sort keys %$sync_opts_fmt) {
431 my $fmt = $sync_opts_fmt->{$opt};
432
6c42a103 433 $res->{$opt} = $param->{$opt} // $cfg_defaults->{$opt} // $fmt->{default};
d29d2d4a 434 }
98df2ebc
DC
435
436 $map_remove_vanished->($res, 1);
437
438 # only scope has no implicit value
439 raise_param_exc({
440 "scope" => 'Not passed as parameter and not defined in realm default sync options.'
441 }) if !defined($res->{scope});
442
d29d2d4a
TL
443 return $res;
444};
445
673d2bf2
DC
446__PACKAGE__->register_method ({
447 name => 'sync',
448 path => '{realm}/sync',
449 method => 'POST',
450 permissions => {
cf109814
TL
451 description => "'Realm.AllocateUser' on '/access/realm/<realm>' and "
452 ." 'User.Modify' permissions to '/access/groups/'.",
673d2bf2 453 check => [ 'and',
0c503211
WB
454 ['perm', '/access/realm/{realm}', ['Realm.AllocateUser']],
455 ['perm', '/access/groups', ['User.Modify']],
cf109814 456 ],
673d2bf2 457 },
cf109814
TL
458 description => "Syncs users and/or groups from the configured LDAP to user.cfg."
459 ." NOTE: Synced groups will have the name 'name-\$realm', so make sure"
460 ." those groups do not exist to prevent overwriting.",
673d2bf2
DC
461 protected => 1,
462 parameters => {
463 additionalProperties => 0,
d29d2d4a
TL
464 properties => get_standard_option('realm-sync-options', {
465 realm => get_standard_option('realm'),
38691d98
DC
466 'dry-run' => {
467 description => "If set, does not write anything.",
468 type => 'boolean',
469 optional => 1,
470 default => 0,
471 },
472 }),
673d2bf2 473 },
cf109814
TL
474 returns => {
475 description => 'Worker Task-UPID',
476 type => 'string'
477 },
673d2bf2
DC
478 code => sub {
479 my ($param) = @_;
480
481 my $rpcenv = PVE::RPCEnvironment::get();
482 my $authuser = $rpcenv->get_user();
483
417309d7 484 my $dry_run = extract_param($param, 'dry-run');
673d2bf2
DC
485 my $realm = $param->{realm};
486 my $cfg = cfs_read_file($domainconfigfile);
cf109814 487 my $realmconfig = $cfg->{ids}->{$realm};
673d2bf2 488
cf109814
TL
489 raise_param_exc({ 'realm' => 'Realm does not exist.' }) if !defined($realmconfig);
490 my $type = $realmconfig->{type};
673d2bf2
DC
491
492 if ($type ne 'ldap' && $type ne 'ad') {
cf109814 493 die "Cannot sync realm type '$type'! Only LDAP/AD realms can be synced.\n";
673d2bf2 494 }
673d2bf2 495
d29d2d4a 496 my $opts = $parse_sync_opts->($param, $realmconfig); # can throw up
673d2bf2 497
d29d2d4a 498 my $scope = $opts->{scope};
cf109814 499 my $whatstring = $scope eq 'both' ? "users and groups" : $scope;
673d2bf2 500
cf109814 501 my $plugin = PVE::Auth::Plugin->lookup($type);
673d2bf2
DC
502
503 my $worker = sub {
417309d7
TL
504 print "(dry test run) " if $dry_run;
505 print "starting sync for realm $realm\n";
cf109814
TL
506
507 my ($synced_users, $dnmap) = $plugin->get_users($realmconfig, $realm);
508 my $synced_groups = {};
509 if ($scope eq 'groups' || $scope eq 'both') {
510 $synced_groups = $plugin->get_groups($realmconfig, $realm, $dnmap);
673d2bf2
DC
511 }
512
cf109814 513 PVE::AccessControl::lock_user_config(sub {
673d2bf2 514 my $usercfg = cfs_read_file("user.cfg");
cf109814 515 print "got data from server, updating $whatstring\n";
673d2bf2 516
415179b0 517 if ($scope eq 'users' || $scope eq 'both') {
d29d2d4a 518 $update_users->($usercfg, $realm, $synced_users, $opts);
673d2bf2
DC
519 }
520
415179b0 521 if ($scope eq 'groups' || $scope eq 'both') {
d29d2d4a 522 $update_groups->($usercfg, $realm, $synced_groups, $opts);
673d2bf2 523 }
cf109814 524
417309d7
TL
525 if ($dry_run) {
526 print "\nNOTE: Dry test run, changes were NOT written to the configuration.\n";
527 return;
38691d98 528 }
417309d7
TL
529 cfs_write_file("user.cfg", $usercfg);
530 print "successfully updated $whatstring configuration\n";
cf109814 531 }, "syncing $whatstring failed");
673d2bf2
DC
532 };
533
417309d7 534 my $workerid = !$dry_run ? 'auth-realm-sync' : 'auth-realm-sync-test';
38691d98 535 return $rpcenv->fork_worker($workerid, $realm, $authuser, $worker);
673d2bf2
DC
536 }});
537
2c3a6c0a 5381;