1 package PVE
::API2
::Domains
;
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);
14 use PVE
::Auth
::Plugin
;
16 my $domainconfigfile = "domains.cfg";
18 use base
qw(PVE::RESTHandler);
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) = @_;
25 if (!defined($opt->{'remove-vanished'}) && ($opt->{full
} || $opt->{purge
})) {
27 push @$props, 'entry', 'properties' if $opt->{full
};
28 push @$props, 'acl' if $opt->{purge
};
29 $opt->{'remove-vanished'} = join(';', @$props);
32 if ($delete_deprecated) {
40 my $map_sync_default_options = sub {
41 my ($cfg, $delete_deprecated) = @_;
43 my $opt = $cfg->{'sync-defaults-options'};
44 return if !defined($opt);
45 my $sync_opts_fmt = PVE
::JSONSchema
::get_format
('realm-sync-options');
47 my $old_opt = PVE
::JSONSchema
::parse_property_string
($sync_opts_fmt, $opt);
49 my $new_opt = $map_remove_vanished->($old_opt, $delete_deprecated);
51 $cfg->{'sync-defaults-options'} = PVE
::JSONSchema
::print_property_string
($new_opt, $sync_opts_fmt);
54 __PACKAGE__-
>register_method ({
58 description
=> "Authentication domain index.",
60 description
=> "Anyone can access that, because we need that list for the login box (before the user is authenticated).",
64 additionalProperties
=> 0,
72 realm
=> { type
=> 'string' },
73 type
=> { type
=> 'string' },
75 description
=> "Two-factor authentication provider.",
77 enum
=> [ 'yubico', 'oath' ],
81 description
=> "A comment. The GUI use this text when you select a domain (Realm) on the login window.",
87 links
=> [ { rel
=> 'child', href
=> "{realm}" } ],
94 my $cfg = cfs_read_file
($domainconfigfile);
95 my $ids = $cfg->{ids
};
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
};
111 __PACKAGE__-
>register_method ({
117 check
=> ['perm', '/access/realm', ['Realm.Allocate']],
119 description
=> "Add an authentication server.",
120 parameters
=> PVE
::Auth
::Plugin-
>createSchema(),
121 returns
=> { type
=> 'null' },
125 # always extract, add it with hook
126 my $password = extract_param
($param, 'password');
128 PVE
::Auth
::Plugin
::lock_domain_config
(
131 my $cfg = cfs_read_file
($domainconfigfile);
132 my $ids = $cfg->{ids
};
134 my $realm = extract_param
($param, 'realm');
135 my $type = $param->{type
};
137 die "domain '$realm' already exists\n"
140 die "unable to use reserved name '$realm'\n"
141 if ($realm eq 'pam' || $realm eq 'pve');
143 die "unable to create builtin type '$type'\n"
144 if ($type eq 'pam' || $type eq 'pve');
146 if ($type eq 'ad' || $type eq 'ldap') {
147 $map_sync_default_options->($param, 1);
150 my $plugin = PVE
::Auth
::Plugin-
>lookup($type);
151 my $config = $plugin->check_config($realm, $param, 1, 1);
153 if ($config->{default}) {
154 foreach my $r (keys %$ids) {
155 delete $ids->{$r}->{default};
159 $ids->{$realm} = $config;
161 my $opts = $plugin->options();
162 if (defined($password) && !defined($opts->{password
})) {
164 warn "ignoring password parameter";
166 $plugin->on_add_hook($realm, $config, password
=> $password);
168 cfs_write_file
($domainconfigfile, $cfg);
169 }, "add auth server failed");
174 __PACKAGE__-
>register_method ({
179 check
=> ['perm', '/access/realm', ['Realm.Allocate']],
181 description
=> "Update authentication server settings.",
183 parameters
=> PVE
::Auth
::Plugin-
>updateSchema(),
184 returns
=> { type
=> 'null' },
188 # always extract, update in hook
189 my $password = extract_param
($param, 'password');
191 PVE
::Auth
::Plugin
::lock_domain_config
(
194 my $cfg = cfs_read_file
($domainconfigfile);
195 my $ids = $cfg->{ids
};
197 my $digest = extract_param
($param, 'digest');
198 PVE
::SectionConfig
::assert_if_modified
($cfg, $digest);
200 my $realm = extract_param
($param, 'realm');
202 die "domain '$realm' does not exist\n"
205 my $delete_str = extract_param
($param, 'delete');
206 die "no options specified\n" if !$delete_str && !scalar(keys %$param);
209 foreach my $opt (PVE
::Tools
::split_list
($delete_str)) {
210 delete $ids->{$realm}->{$opt};
211 $delete_pw = 1 if $opt eq 'password';
214 my $type = $ids->{$realm}->{type
};
215 if ($type eq 'ad' || $type eq 'ldap') {
216 $map_sync_default_options->($param, 1);
219 my $plugin = PVE
::Auth
::Plugin-
>lookup($type);
220 my $config = $plugin->check_config($realm, $param, 0, 1);
222 if ($config->{default}) {
223 foreach my $r (keys %$ids) {
224 delete $ids->{$r}->{default};
228 foreach my $p (keys %$config) {
229 $ids->{$realm}->{$p} = $config->{$p};
232 my $opts = $plugin->options();
233 if ($delete_pw || defined($password)) {
234 $plugin->on_update_hook($realm, $config, password
=> $password);
236 $plugin->on_update_hook($realm, $config);
239 cfs_write_file
($domainconfigfile, $cfg);
240 }, "update auth server failed");
245 # fixme: return format!
246 __PACKAGE__-
>register_method ({
250 description
=> "Get auth server configuration.",
252 check
=> ['perm', '/access/realm', ['Realm.Allocate', 'Sys.Audit'], any
=> 1],
255 additionalProperties
=> 0,
257 realm
=> get_standard_option
('realm'),
264 my $cfg = cfs_read_file
($domainconfigfile);
266 my $realm = $param->{realm
};
268 my $data = $cfg->{ids
}->{$realm};
269 die "domain '$realm' does not exist\n" if !$data;
271 my $type = $data->{type
};
272 if ($type eq 'ad' || $type eq 'ldap') {
273 $map_sync_default_options->($data);
276 $data->{digest
} = $cfg->{digest
};
282 __PACKAGE__-
>register_method ({
287 check
=> ['perm', '/access/realm', ['Realm.Allocate']],
289 description
=> "Delete an authentication server.",
292 additionalProperties
=> 0,
294 realm
=> get_standard_option
('realm'),
297 returns
=> { type
=> 'null' },
301 PVE
::Auth
::Plugin
::lock_domain_config
(
304 my $cfg = cfs_read_file
($domainconfigfile);
305 my $ids = $cfg->{ids
};
306 my $realm = $param->{realm
};
308 die "authentication domain '$realm' does not exist\n" if !$ids->{$realm};
310 my $plugin = PVE
::Auth
::Plugin-
>lookup($ids->{$realm}->{type
});
312 $plugin->on_delete_hook($realm, $ids->{$realm});
314 delete $ids->{$realm};
316 cfs_write_file
($domainconfigfile, $cfg);
317 }, "delete auth server failed");
322 my $update_users = sub {
323 my ($usercfg, $realm, $synced_users, $opts) = @_;
325 if (defined(my $vanished = $opts->{'remove-vanished'})) {
326 print "syncing users (remove-vanished opts: $vanished)\n";
328 print "syncing users\n";
331 $usercfg->{users
} = {} if !defined($usercfg->{users
});
332 my $users = $usercfg->{users
};
333 my $to_remove = { map { $_ => 1 } split(';', $opts->{'remove-vanished'} // '') };
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});
340 if ($to_remove->{entry
}) {
341 print "remove user '$userid'\n";
342 delete $users->{$userid};
345 if ($to_remove->{acl
}) {
346 print "purge users '$userid' ACL entries\n";
347 PVE
::AccessControl
::delete_user_acl
($userid, $usercfg);
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";
360 print "adding user '$userid'\n";
362 my $user = $users->{$userid} = $synced_user;
364 my $enabled = $olduser->{enable
} // $opts->{'enable-new'};
365 $user->{enable
} = $enabled if defined($enabled);
366 $user->{tokens
} = $olduser->{tokens
} if defined($olduser->{tokens
});
369 foreach my $attr (keys %$synced_user) {
370 $olduser->{$attr} = $synced_user->{$attr};
372 print "updating user '$userid'\n";
377 my $update_groups = sub {
378 my ($usercfg, $realm, $synced_groups, $opts) = @_;
380 if (defined(my $vanished = $opts->{'remove-vanished'})) {
381 print "syncing groups (remove-vanished opts: $vanished)\n";
383 print "syncing groups\n";
386 $usercfg->{groups
} = {} if !defined($usercfg->{groups
});
387 my $groups = $usercfg->{groups
};
388 my $to_remove = { map { $_ => 1 } split(';', $opts->{'remove-vanished'} // '') };
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});
395 if ($to_remove->{entry
}) {
396 print "remove group '$groupid'\n";
397 delete $groups->{$groupid};
400 if ($to_remove->{acl
}) {
401 print "purge groups '$groupid' ACL entries\n";
402 PVE
::AccessControl
::delete_group_acl
($groupid, $usercfg);
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";
413 print "adding group '$groupid'\n";
415 $groups->{$groupid} = $synced_group;
417 foreach my $attr (keys %$synced_group) {
418 $oldgroup->{$attr} = $synced_group->{$attr};
420 print "updating group '$groupid'\n";
425 my $parse_sync_opts = sub {
426 my ($param, $realmconfig) = @_;
428 my $sync_opts_fmt = PVE
::JSONSchema
::get_format
('realm-sync-options');
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);
436 for my $opt (sort keys %$sync_opts_fmt) {
437 my $fmt = $sync_opts_fmt->{$opt};
439 $res->{$opt} = $param->{$opt} // $cfg_defaults->{$opt} // $fmt->{default};
442 $map_remove_vanished->($res, 1);
444 # only scope has no implicit value
446 "scope" => 'Not passed as parameter and not defined in realm default sync options.'
447 }) if !defined($res->{scope
});
452 __PACKAGE__-
>register_method ({
454 path
=> '{realm}/sync',
457 description
=> "'Realm.AllocateUser' on '/access/realm/<realm>' and "
458 ." 'User.Modify' permissions to '/access/groups/'.",
460 ['perm', '/access/realm/{realm}', ['Realm.AllocateUser']],
461 ['perm', '/access/groups', ['User.Modify']],
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.",
469 additionalProperties
=> 0,
470 properties
=> get_standard_option
('realm-sync-options', {
471 realm
=> get_standard_option
('realm'),
473 description
=> "If set, does not write anything.",
481 description
=> 'Worker Task-UPID',
487 my $rpcenv = PVE
::RPCEnvironment
::get
();
488 my $authuser = $rpcenv->get_user();
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};
495 raise_param_exc
({ 'realm' => 'Realm does not exist.' }) if !defined($realmconfig);
496 my $type = $realmconfig->{type
};
498 if ($type ne 'ldap' && $type ne 'ad') {
499 die "Cannot sync realm type '$type'! Only LDAP/AD realms can be synced.\n";
502 my $opts = $parse_sync_opts->($param, $realmconfig); # can throw up
504 my $scope = $opts->{scope
};
505 my $whatstring = $scope eq 'both' ?
"users and groups" : $scope;
507 my $plugin = PVE
::Auth
::Plugin-
>lookup($type);
510 print "(dry test run) " if $dry_run;
511 print "starting sync for realm $realm\n";
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);
519 PVE
::AccessControl
::lock_user_config
(sub {
520 my $usercfg = cfs_read_file
("user.cfg");
521 print "got data from server, updating $whatstring\n";
523 if ($scope eq 'users' || $scope eq 'both') {
524 $update_users->($usercfg, $realm, $synced_users, $opts);
527 if ($scope eq 'groups' || $scope eq 'both') {
528 $update_groups->($usercfg, $realm, $synced_groups, $opts);
532 print "\nNOTE: Dry test run, changes were NOT written to the configuration.\n";
535 cfs_write_file
("user.cfg", $usercfg);
536 print "successfully updated $whatstring configuration\n";
537 }, "syncing $whatstring failed");
540 my $workerid = !$dry_run ?
'auth-realm-sync' : 'auth-realm-sync-test';
541 return $rpcenv->fork_worker($workerid, $realm, $authuser, $worker);