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 PVE
::Auth
::Plugin
::lock_domain_config
(
94 my $cfg = cfs_read_file
($domainconfigfile);
95 my $ids = $cfg->{ids
};
97 my $realm = extract_param
($param, 'realm');
98 my $type = $param->{type
};
100 die "domain '$realm' already exists\n"
103 die "unable to use reserved name '$realm'\n"
104 if ($realm eq 'pam' || $realm eq 'pve');
106 die "unable to create builtin type '$type'\n"
107 if ($type eq 'pam' || $type eq 'pve');
109 my $plugin = PVE
::Auth
::Plugin-
>lookup($type);
110 my $config = $plugin->check_config($realm, $param, 1, 1);
112 if ($config->{default}) {
113 foreach my $r (keys %$ids) {
114 delete $ids->{$r}->{default};
118 $ids->{$realm} = $config;
120 cfs_write_file
($domainconfigfile, $cfg);
121 }, "add auth server failed");
126 __PACKAGE__-
>register_method ({
131 check
=> ['perm', '/access/realm', ['Realm.Allocate']],
133 description
=> "Update authentication server settings.",
135 parameters
=> PVE
::Auth
::Plugin-
>updateSchema(),
136 returns
=> { type
=> 'null' },
140 PVE
::Auth
::Plugin
::lock_domain_config
(
143 my $cfg = cfs_read_file
($domainconfigfile);
144 my $ids = $cfg->{ids
};
146 my $digest = extract_param
($param, 'digest');
147 PVE
::SectionConfig
::assert_if_modified
($cfg, $digest);
149 my $realm = extract_param
($param, 'realm');
151 die "domain '$realm' does not exist\n"
154 my $delete_str = extract_param
($param, 'delete');
155 die "no options specified\n" if !$delete_str && !scalar(keys %$param);
157 foreach my $opt (PVE
::Tools
::split_list
($delete_str)) {
158 delete $ids->{$realm}->{$opt};
161 my $plugin = PVE
::Auth
::Plugin-
>lookup($ids->{$realm}->{type
});
162 my $config = $plugin->check_config($realm, $param, 0, 1);
164 if ($config->{default}) {
165 foreach my $r (keys %$ids) {
166 delete $ids->{$r}->{default};
170 foreach my $p (keys %$config) {
171 $ids->{$realm}->{$p} = $config->{$p};
174 cfs_write_file
($domainconfigfile, $cfg);
175 }, "update auth server failed");
180 # fixme: return format!
181 __PACKAGE__-
>register_method ({
185 description
=> "Get auth server configuration.",
187 check
=> ['perm', '/access/realm', ['Realm.Allocate', 'Sys.Audit'], any
=> 1],
190 additionalProperties
=> 0,
192 realm
=> get_standard_option
('realm'),
199 my $cfg = cfs_read_file
($domainconfigfile);
201 my $realm = $param->{realm
};
203 my $data = $cfg->{ids
}->{$realm};
204 die "domain '$realm' does not exist\n" if !$data;
206 $data->{digest
} = $cfg->{digest
};
212 __PACKAGE__-
>register_method ({
217 check
=> ['perm', '/access/realm', ['Realm.Allocate']],
219 description
=> "Delete an authentication server.",
222 additionalProperties
=> 0,
224 realm
=> get_standard_option
('realm'),
227 returns
=> { type
=> 'null' },
231 PVE
::Auth
::Plugin
::lock_domain_config
(
234 my $cfg = cfs_read_file
($domainconfigfile);
235 my $ids = $cfg->{ids
};
237 my $realm = $param->{realm
};
239 die "domain '$realm' does not exist\n" if !$ids->{$realm};
241 delete $ids->{$realm};
243 cfs_write_file
($domainconfigfile, $cfg);
244 }, "delete auth server failed");
249 my $update_users = sub {
250 my ($usercfg, $realm, $synced_users, $opts) = @_;
252 print "syncing users\n";
253 $usercfg->{users
} = {} if !defined($usercfg->{users
});
254 my $users = $usercfg->{users
};
257 if ($opts->{'full'}) {
258 print "full sync, deleting outdated existing users first\n";
259 foreach my $userid (sort keys %$users) {
260 next if $userid !~ m/\@$realm$/;
262 $oldusers->{$userid} = delete $users->{$userid};
263 if ($opts->{'purge'} && !$synced_users->{$userid}) {
264 PVE
::AccessControl
::delete_user_acl
($userid, $usercfg);
265 print "purged user '$userid' and all its ACL entries\n";
266 } elsif (!defined($synced_users->{$userid})) {
267 print "remove user '$userid'\n";
272 foreach my $userid (sort keys %$synced_users) {
273 my $synced_user = $synced_users->{$userid} // {};
274 if (!defined($users->{$userid})) {
275 my $user = $users->{$userid} = $synced_user;
277 my $olduser = $oldusers->{$userid} // {};
278 if (defined(my $enabled = $olduser->{enable
})) {
279 $user->{enable
} = $enabled;
280 } elsif ($opts->{'enable-new'}) {
284 if (defined($olduser->{tokens
})) {
285 $user->{tokens
} = $olduser->{tokens
};
287 if (defined($oldusers->{$userid})) {
288 print "updated user '$userid'\n";
290 print "added user '$userid'\n";
293 my $olduser = $users->{$userid};
294 foreach my $attr (keys %$synced_user) {
295 $olduser->{$attr} = $synced_user->{$attr};
297 print "updated user '$userid'\n";
302 my $update_groups = sub {
303 my ($usercfg, $realm, $synced_groups, $opts) = @_;
305 print "syncing groups\n";
306 $usercfg->{groups
} = {} if !defined($usercfg->{groups
});
307 my $groups = $usercfg->{groups
};
311 print "full sync, deleting outdated existing groups first\n";
312 foreach my $groupid (sort keys %$groups) {
313 next if $groupid !~ m/\-$realm$/;
315 my $oldgroups->{$groupid} = delete $groups->{$groupid};
316 if ($opts->{purge
} && !$synced_groups->{$groupid}) {
317 print "purged group '$groupid' and all its ACL entries\n";
318 PVE
::AccessControl
::delete_group_acl
($groupid, $usercfg)
319 } elsif (!defined($synced_groups->{$groupid})) {
320 print "removed group '$groupid'\n";
325 foreach my $groupid (sort keys %$synced_groups) {
326 my $synced_group = $synced_groups->{$groupid};
327 if (!defined($groups->{$groupid})) {
328 $groups->{$groupid} = $synced_group;
329 if (defined($oldgroups->{$groupid})) {
330 print "updated group '$groupid'\n";
332 print "added group '$groupid'\n";
335 my $group = $groups->{$groupid};
336 foreach my $attr (keys %$synced_group) {
337 $group->{$attr} = $synced_group->{$attr};
339 print "updated group '$groupid'\n";
344 my $parse_sync_opts = sub {
345 my ($param, $realmconfig) = @_;
347 my $sync_opts_fmt = PVE
::JSONSchema
::get_format
('realm-sync-options');
350 if (defined(my $cfg_opts = $realmconfig->{'sync-defaults-options'})) {
351 $res = PVE
::JSONSchema
::parse_property_string
($sync_opts_fmt, $cfg_opts);
354 for my $opt (sort keys %$sync_opts_fmt) {
355 my $fmt = $sync_opts_fmt->{$opt};
357 if (exists $param->{$opt}) {
358 $res->{$opt} = $param->{$opt};
359 } elsif (!exists $res->{$opt}) {
361 "$opt" => 'Not passed as parameter and not defined in realm default sync options.'
362 }) if !$fmt->{optional
};
363 $res->{$opt} = $fmt->{default} if exists $fmt->{default};
369 __PACKAGE__-
>register_method ({
371 path
=> '{realm}/sync',
374 description
=> "'Realm.AllocateUser' on '/access/realm/<realm>' and "
375 ." 'User.Modify' permissions to '/access/groups/'.",
377 [ 'userid-param', 'Realm.AllocateUser' ],
378 [ 'userid-group', ['User.Modify'] ],
381 description
=> "Syncs users and/or groups from the configured LDAP to user.cfg."
382 ." NOTE: Synced groups will have the name 'name-\$realm', so make sure"
383 ." those groups do not exist to prevent overwriting.",
386 additionalProperties
=> 0,
387 properties
=> get_standard_option
('realm-sync-options', {
388 realm
=> get_standard_option
('realm'),
392 description
=> 'Worker Task-UPID',
398 my $rpcenv = PVE
::RPCEnvironment
::get
();
399 my $authuser = $rpcenv->get_user();
401 my $realm = $param->{realm
};
402 my $cfg = cfs_read_file
($domainconfigfile);
403 my $realmconfig = $cfg->{ids
}->{$realm};
405 raise_param_exc
({ 'realm' => 'Realm does not exist.' }) if !defined($realmconfig);
406 my $type = $realmconfig->{type
};
408 if ($type ne 'ldap' && $type ne 'ad') {
409 die "Cannot sync realm type '$type'! Only LDAP/AD realms can be synced.\n";
412 my $opts = $parse_sync_opts->($param, $realmconfig); # can throw up
414 my $scope = $opts->{scope
};
415 my $whatstring = $scope eq 'both' ?
"users and groups" : $scope;
417 my $plugin = PVE
::Auth
::Plugin-
>lookup($type);
420 print "starting sync for realm $realm\n";
422 my ($synced_users, $dnmap) = $plugin->get_users($realmconfig, $realm);
423 my $synced_groups = {};
424 if ($scope eq 'groups' || $scope eq 'both') {
425 $synced_groups = $plugin->get_groups($realmconfig, $realm, $dnmap);
428 PVE
::AccessControl
::lock_user_config
(sub {
429 my $usercfg = cfs_read_file
("user.cfg");
430 print "got data from server, updating $whatstring\n";
432 if ($scope eq 'users' || $scope eq 'both') {
433 $update_users->($usercfg, $realm, $synced_users, $opts);
436 if ($scope eq 'groups' || $scope eq 'both') {
437 $update_groups->($usercfg, $realm, $synced_groups, $opts);
440 cfs_write_file
("user.cfg", $usercfg);
441 print "successfully updated $whatstring configuration\n";
442 }, "syncing $whatstring failed");
445 return $rpcenv->fork_worker('auth-realm-sync', $realm, $authuser, $worker);