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 __PACKAGE__-
>register_method ({
24 description
=> "Authentication domain index.",
26 description
=> "Anyone can access that, because we need that list for the login box (before the user is authenticated).",
30 additionalProperties
=> 0,
38 realm
=> { type
=> 'string' },
39 type
=> { type
=> 'string' },
41 description
=> "Two-factor authentication provider.",
43 enum
=> [ 'yubico', 'oath' ],
47 description
=> "A comment. The GUI use this text when you select a domain (Realm) on the login window.",
53 links
=> [ { rel
=> 'child', href
=> "{realm}" } ],
60 my $cfg = cfs_read_file
($domainconfigfile);
61 my $ids = $cfg->{ids
};
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
};
77 __PACKAGE__-
>register_method ({
83 check
=> ['perm', '/access/realm', ['Realm.Allocate']],
85 description
=> "Add an authentication server.",
86 parameters
=> PVE
::Auth
::Plugin-
>createSchema(),
87 returns
=> { type
=> 'null' },
91 # always extract, add it with hook
92 my $password = extract_param
($param, 'password');
94 PVE
::Auth
::Plugin
::lock_domain_config
(
97 my $cfg = cfs_read_file
($domainconfigfile);
98 my $ids = $cfg->{ids
};
100 my $realm = extract_param
($param, 'realm');
101 my $type = $param->{type
};
103 die "domain '$realm' already exists\n"
106 die "unable to use reserved name '$realm'\n"
107 if ($realm eq 'pam' || $realm eq 'pve');
109 die "unable to create builtin type '$type'\n"
110 if ($type eq 'pam' || $type eq 'pve');
112 my $plugin = PVE
::Auth
::Plugin-
>lookup($type);
113 my $config = $plugin->check_config($realm, $param, 1, 1);
115 if ($config->{default}) {
116 foreach my $r (keys %$ids) {
117 delete $ids->{$r}->{default};
121 $ids->{$realm} = $config;
123 my $opts = $plugin->options();
124 if (defined($password) && !defined($opts->{password
})) {
126 warn "ignoring password parameter";
128 $plugin->on_add_hook($realm, $config, password
=> $password);
130 cfs_write_file
($domainconfigfile, $cfg);
131 }, "add auth server failed");
136 __PACKAGE__-
>register_method ({
141 check
=> ['perm', '/access/realm', ['Realm.Allocate']],
143 description
=> "Update authentication server settings.",
145 parameters
=> PVE
::Auth
::Plugin-
>updateSchema(),
146 returns
=> { type
=> 'null' },
150 # always extract, update in hook
151 my $password = extract_param
($param, 'password');
153 PVE
::Auth
::Plugin
::lock_domain_config
(
156 my $cfg = cfs_read_file
($domainconfigfile);
157 my $ids = $cfg->{ids
};
159 my $digest = extract_param
($param, 'digest');
160 PVE
::SectionConfig
::assert_if_modified
($cfg, $digest);
162 my $realm = extract_param
($param, 'realm');
164 die "domain '$realm' does not exist\n"
167 my $delete_str = extract_param
($param, 'delete');
168 die "no options specified\n" if !$delete_str && !scalar(keys %$param);
171 foreach my $opt (PVE
::Tools
::split_list
($delete_str)) {
172 delete $ids->{$realm}->{$opt};
173 $delete_pw = 1 if $opt eq 'password';
176 my $plugin = PVE
::Auth
::Plugin-
>lookup($ids->{$realm}->{type
});
177 my $config = $plugin->check_config($realm, $param, 0, 1);
179 if ($config->{default}) {
180 foreach my $r (keys %$ids) {
181 delete $ids->{$r}->{default};
185 foreach my $p (keys %$config) {
186 $ids->{$realm}->{$p} = $config->{$p};
189 my $opts = $plugin->options();
190 if ($delete_pw || defined($password)) {
191 $plugin->on_update_hook($realm, $config, password
=> $password);
193 $plugin->on_update_hook($realm, $config);
196 cfs_write_file
($domainconfigfile, $cfg);
197 }, "update auth server failed");
202 # fixme: return format!
203 __PACKAGE__-
>register_method ({
207 description
=> "Get auth server configuration.",
209 check
=> ['perm', '/access/realm', ['Realm.Allocate', 'Sys.Audit'], any
=> 1],
212 additionalProperties
=> 0,
214 realm
=> get_standard_option
('realm'),
221 my $cfg = cfs_read_file
($domainconfigfile);
223 my $realm = $param->{realm
};
225 my $data = $cfg->{ids
}->{$realm};
226 die "domain '$realm' does not exist\n" if !$data;
228 $data->{digest
} = $cfg->{digest
};
234 __PACKAGE__-
>register_method ({
239 check
=> ['perm', '/access/realm', ['Realm.Allocate']],
241 description
=> "Delete an authentication server.",
244 additionalProperties
=> 0,
246 realm
=> get_standard_option
('realm'),
249 returns
=> { type
=> 'null' },
253 PVE
::Auth
::Plugin
::lock_domain_config
(
256 my $cfg = cfs_read_file
($domainconfigfile);
257 my $ids = $cfg->{ids
};
258 my $realm = $param->{realm
};
260 die "authentication domain '$realm' does not exist\n" if !$ids->{$realm};
262 my $plugin = PVE
::Auth
::Plugin-
>lookup($ids->{$realm}->{type
});
264 $plugin->on_delete_hook($realm, $ids->{$realm});
266 delete $ids->{$realm};
268 cfs_write_file
($domainconfigfile, $cfg);
269 }, "delete auth server failed");
274 my $update_users = sub {
275 my ($usercfg, $realm, $synced_users, $opts) = @_;
277 print "syncing users\n";
278 $usercfg->{users
} = {} if !defined($usercfg->{users
});
279 my $users = $usercfg->{users
};
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$/;
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";
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;
302 my $olduser = $oldusers->{$userid} // {};
303 if (defined(my $enabled = $olduser->{enable
})) {
304 $user->{enable
} = $enabled;
305 } elsif ($opts->{'enable-new'}) {
309 if (defined($olduser->{tokens
})) {
310 $user->{tokens
} = $olduser->{tokens
};
312 if (defined($oldusers->{$userid})) {
313 print "updated user '$userid'\n";
315 print "added user '$userid'\n";
318 my $olduser = $users->{$userid};
319 foreach my $attr (keys %$synced_user) {
320 $olduser->{$attr} = $synced_user->{$attr};
322 print "updated user '$userid'\n";
327 my $update_groups = sub {
328 my ($usercfg, $realm, $synced_groups, $opts) = @_;
330 print "syncing groups\n";
331 $usercfg->{groups
} = {} if !defined($usercfg->{groups
});
332 my $groups = $usercfg->{groups
};
336 print "full sync, deleting outdated existing groups first\n";
337 foreach my $groupid (sort keys %$groups) {
338 next if $groupid !~ m/\-$realm$/;
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";
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";
357 print "added group '$groupid'\n";
360 my $group = $groups->{$groupid};
361 foreach my $attr (keys %$synced_group) {
362 $group->{$attr} = $synced_group->{$attr};
364 print "updated group '$groupid'\n";
369 my $parse_sync_opts = sub {
370 my ($param, $realmconfig) = @_;
372 my $sync_opts_fmt = PVE
::JSONSchema
::get_format
('realm-sync-options');
376 if (defined(my $cfg_opts = $realmconfig->{'sync-defaults-options'})) {
377 $defaults = PVE
::JSONSchema
::parse_property_string
($sync_opts_fmt, $cfg_opts);
380 for my $opt (sort keys %$sync_opts_fmt) {
381 my $fmt = $sync_opts_fmt->{$opt};
383 $res->{$opt} = $param->{$opt} // $defaults->{$opt} // $fmt->{default};
385 "$opt" => 'Not passed as parameter and not defined in realm default sync options.'
386 }) if !defined($res->{$opt});
391 __PACKAGE__-
>register_method ({
393 path
=> '{realm}/sync',
396 description
=> "'Realm.AllocateUser' on '/access/realm/<realm>' and "
397 ." 'User.Modify' permissions to '/access/groups/'.",
399 [ 'userid-param', 'Realm.AllocateUser' ],
400 [ 'userid-group', ['User.Modify'] ],
403 description
=> "Syncs users and/or groups from the configured LDAP to user.cfg."
404 ." NOTE: Synced groups will have the name 'name-\$realm', so make sure"
405 ." those groups do not exist to prevent overwriting.",
408 additionalProperties
=> 0,
409 properties
=> get_standard_option
('realm-sync-options', {
410 realm
=> get_standard_option
('realm'),
414 description
=> 'Worker Task-UPID',
420 my $rpcenv = PVE
::RPCEnvironment
::get
();
421 my $authuser = $rpcenv->get_user();
423 my $realm = $param->{realm
};
424 my $cfg = cfs_read_file
($domainconfigfile);
425 my $realmconfig = $cfg->{ids
}->{$realm};
427 raise_param_exc
({ 'realm' => 'Realm does not exist.' }) if !defined($realmconfig);
428 my $type = $realmconfig->{type
};
430 if ($type ne 'ldap' && $type ne 'ad') {
431 die "Cannot sync realm type '$type'! Only LDAP/AD realms can be synced.\n";
434 my $opts = $parse_sync_opts->($param, $realmconfig); # can throw up
436 my $scope = $opts->{scope
};
437 my $whatstring = $scope eq 'both' ?
"users and groups" : $scope;
439 my $plugin = PVE
::Auth
::Plugin-
>lookup($type);
442 print "starting sync for realm $realm\n";
444 my ($synced_users, $dnmap) = $plugin->get_users($realmconfig, $realm);
445 my $synced_groups = {};
446 if ($scope eq 'groups' || $scope eq 'both') {
447 $synced_groups = $plugin->get_groups($realmconfig, $realm, $dnmap);
450 PVE
::AccessControl
::lock_user_config
(sub {
451 my $usercfg = cfs_read_file
("user.cfg");
452 print "got data from server, updating $whatstring\n";
454 if ($scope eq 'users' || $scope eq 'both') {
455 $update_users->($usercfg, $realm, $synced_users, $opts);
458 if ($scope eq 'groups' || $scope eq 'both') {
459 $update_groups->($usercfg, $realm, $synced_groups, $opts);
462 cfs_write_file
("user.cfg", $usercfg);
463 print "successfully updated $whatstring configuration\n";
464 }, "syncing $whatstring failed");
467 return $rpcenv->fork_worker('auth-realm-sync', $realm, $authuser, $worker);