]> git.proxmox.com Git - pve-access-control.git/blame - PVE/API2/Domains.pm
d/control: bump debhelper compat to >= 12
[pve-access-control.git] / 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
20__PACKAGE__->register_method ({
32449f35
DC
21 name => 'index',
22 path => '',
2c3a6c0a
DM
23 method => 'GET',
24 description => "Authentication domain index.",
32449f35 25 permissions => {
82b63965 26 description => "Anyone can access that, because we need that list for the login box (before the user is authenticated).",
32449f35 27 user => 'world',
82b63965 28 },
2c3a6c0a
DM
29 parameters => {
30 additionalProperties => 0,
31 properties => {},
32 },
33 returns => {
34 type => 'array',
35 items => {
36 type => "object",
37 properties => {
38 realm => { type => 'string' },
f3c87f9b 39 type => { type => 'string' },
96f8ebd6
DM
40 tfa => {
41 description => "Two-factor authentication provider.",
42 type => 'string',
1abc2c0a 43 enum => [ 'yubico', 'oath' ],
96f8ebd6
DM
44 optional => 1,
45 },
52b2eff3
DM
46 comment => {
47 description => "A comment. The GUI use this text when you select a domain (Realm) on the login window.",
48 type => 'string',
49 optional => 1,
50 },
2c3a6c0a
DM
51 },
52 },
53 links => [ { rel => 'child', href => "{realm}" } ],
54 },
55 code => sub {
56 my ($param) = @_;
32449f35 57
2c3a6c0a
DM
58 my $res = [];
59
60 my $cfg = cfs_read_file($domainconfigfile);
5bb4e06a
DM
61 my $ids = $cfg->{ids};
62
63 foreach my $realm (keys %$ids) {
64 my $d = $ids->{$realm};
2c3a6c0a
DM
65 my $entry = { realm => $realm, type => $d->{type} };
66 $entry->{comment} = $d->{comment} if $d->{comment};
67 $entry->{default} = 1 if $d->{default};
96f8ebd6
DM
68 if ($d->{tfa} && (my $tfa_cfg = PVE::Auth::Plugin::parse_tfa_config($d->{tfa}))) {
69 $entry->{tfa} = $tfa_cfg->{type};
70 }
2c3a6c0a
DM
71 push @$res, $entry;
72 }
73
74 return $res;
75 }});
76
77__PACKAGE__->register_method ({
32449f35 78 name => 'create',
2c3a6c0a 79 protected => 1,
32449f35 80 path => '',
2c3a6c0a 81 method => 'POST',
32449f35 82 permissions => {
82b63965 83 check => ['perm', '/access/realm', ['Realm.Allocate']],
96919234 84 },
2c3a6c0a 85 description => "Add an authentication server.",
5bb4e06a 86 parameters => PVE::Auth::Plugin->createSchema(),
2c3a6c0a
DM
87 returns => { type => 'null' },
88 code => sub {
89 my ($param) = @_;
90
89338e4d
TL
91 # always extract, add it with hook
92 my $password = extract_param($param, 'password');
93
5bb4e06a 94 PVE::Auth::Plugin::lock_domain_config(
2c3a6c0a 95 sub {
32449f35 96
2c3a6c0a 97 my $cfg = cfs_read_file($domainconfigfile);
5bb4e06a 98 my $ids = $cfg->{ids};
2c3a6c0a 99
5bb4e06a
DM
100 my $realm = extract_param($param, 'realm');
101 my $type = $param->{type};
32449f35
DC
102
103 die "domain '$realm' already exists\n"
5bb4e06a 104 if $ids->{$realm};
2c3a6c0a
DM
105
106 die "unable to use reserved name '$realm'\n"
107 if ($realm eq 'pam' || $realm eq 'pve');
108
5bb4e06a
DM
109 die "unable to create builtin type '$type'\n"
110 if ($type eq 'pam' || $type eq 'pve');
af4a8a85 111
5bb4e06a
DM
112 my $plugin = PVE::Auth::Plugin->lookup($type);
113 my $config = $plugin->check_config($realm, $param, 1, 1);
2c3a6c0a 114
5bb4e06a
DM
115 if ($config->{default}) {
116 foreach my $r (keys %$ids) {
117 delete $ids->{$r}->{default};
0c156363 118 }
af4a8a85
DM
119 }
120
5bb4e06a
DM
121 $ids->{$realm} = $config;
122
89338e4d
TL
123 my $opts = $plugin->options();
124 if (defined($password) && !defined($opts->{password})) {
125 $password = undef;
126 warn "ignoring password parameter";
127 }
128 $plugin->on_add_hook($realm, $config, password => $password);
129
2c3a6c0a
DM
130 cfs_write_file($domainconfigfile, $cfg);
131 }, "add auth server failed");
132
133 return undef;
134 }});
135
136__PACKAGE__->register_method ({
32449f35
DC
137 name => 'update',
138 path => '{realm}',
2c3a6c0a 139 method => 'PUT',
32449f35 140 permissions => {
82b63965 141 check => ['perm', '/access/realm', ['Realm.Allocate']],
96919234 142 },
2c3a6c0a
DM
143 description => "Update authentication server settings.",
144 protected => 1,
5bb4e06a 145 parameters => PVE::Auth::Plugin->updateSchema(),
2c3a6c0a
DM
146 returns => { type => 'null' },
147 code => sub {
148 my ($param) = @_;
149
89338e4d
TL
150 # always extract, update in hook
151 my $password = extract_param($param, 'password');
152
5bb4e06a 153 PVE::Auth::Plugin::lock_domain_config(
2c3a6c0a 154 sub {
32449f35 155
2c3a6c0a 156 my $cfg = cfs_read_file($domainconfigfile);
5bb4e06a 157 my $ids = $cfg->{ids};
2c3a6c0a 158
5bb4e06a
DM
159 my $digest = extract_param($param, 'digest');
160 PVE::SectionConfig::assert_if_modified($cfg, $digest);
161
162 my $realm = extract_param($param, 'realm');
2c3a6c0a 163
32449f35 164 die "domain '$realm' does not exist\n"
5bb4e06a
DM
165 if !$ids->{$realm};
166
167 my $delete_str = extract_param($param, 'delete');
168 die "no options specified\n" if !$delete_str && !scalar(keys %$param);
2c3a6c0a 169
89338e4d 170 my $delete_pw = 0;
5bb4e06a
DM
171 foreach my $opt (PVE::Tools::split_list($delete_str)) {
172 delete $ids->{$realm}->{$opt};
89338e4d 173 $delete_pw = 1 if $opt eq 'password';
2c3a6c0a 174 }
32449f35 175
5bb4e06a
DM
176 my $plugin = PVE::Auth::Plugin->lookup($ids->{$realm}->{type});
177 my $config = $plugin->check_config($realm, $param, 0, 1);
2c3a6c0a 178
5bb4e06a
DM
179 if ($config->{default}) {
180 foreach my $r (keys %$ids) {
181 delete $ids->{$r}->{default};
2c3a6c0a
DM
182 }
183 }
184
5bb4e06a
DM
185 foreach my $p (keys %$config) {
186 $ids->{$realm}->{$p} = $config->{$p};
af4a8a85
DM
187 }
188
89338e4d
TL
189 my $opts = $plugin->options();
190 if ($delete_pw || defined($password)) {
191 $plugin->on_update_hook($realm, $config, password => $password);
192 } else {
193 $plugin->on_update_hook($realm, $config);
194 }
195
2c3a6c0a
DM
196 cfs_write_file($domainconfigfile, $cfg);
197 }, "update auth server failed");
198
199 return undef;
200 }});
201
202# fixme: return format!
203__PACKAGE__->register_method ({
32449f35
DC
204 name => 'read',
205 path => '{realm}',
2c3a6c0a
DM
206 method => 'GET',
207 description => "Get auth server configuration.",
32449f35 208 permissions => {
82b63965 209 check => ['perm', '/access/realm', ['Realm.Allocate', 'Sys.Audit'], any => 1],
96919234 210 },
2c3a6c0a
DM
211 parameters => {
212 additionalProperties => 0,
213 properties => {
214 realm => get_standard_option('realm'),
215 },
216 },
217 returns => {},
218 code => sub {
219 my ($param) = @_;
220
221 my $cfg = cfs_read_file($domainconfigfile);
222
223 my $realm = $param->{realm};
32449f35 224
5bb4e06a 225 my $data = $cfg->{ids}->{$realm};
2c3a6c0a
DM
226 die "domain '$realm' does not exist\n" if !$data;
227
5bb4e06a
DM
228 $data->{digest} = $cfg->{digest};
229
2c3a6c0a
DM
230 return $data;
231 }});
232
233
234__PACKAGE__->register_method ({
32449f35
DC
235 name => 'delete',
236 path => '{realm}',
2c3a6c0a 237 method => 'DELETE',
32449f35 238 permissions => {
82b63965 239 check => ['perm', '/access/realm', ['Realm.Allocate']],
96919234 240 },
2c3a6c0a
DM
241 description => "Delete an authentication server.",
242 protected => 1,
243 parameters => {
244 additionalProperties => 0,
245 properties => {
246 realm => get_standard_option('realm'),
247 }
248 },
249 returns => { type => 'null' },
250 code => sub {
251 my ($param) = @_;
252
5bb4e06a 253 PVE::Auth::Plugin::lock_domain_config(
2c3a6c0a
DM
254 sub {
255
256 my $cfg = cfs_read_file($domainconfigfile);
5bb4e06a 257 my $ids = $cfg->{ids};
2c3a6c0a 258 my $realm = $param->{realm};
32449f35 259
89338e4d
TL
260 die "authentication domain '$realm' does not exist\n" if !$ids->{$realm};
261
262 my $plugin = PVE::Auth::Plugin->lookup($ids->{$realm}->{type});
263
264 $plugin->on_delete_hook($realm, $ids->{$realm});
2c3a6c0a 265
5bb4e06a 266 delete $ids->{$realm};
2c3a6c0a
DM
267
268 cfs_write_file($domainconfigfile, $cfg);
269 }, "delete auth server failed");
32449f35 270
2c3a6c0a
DM
271 return undef;
272 }});
273
415179b0
TL
274my $update_users = sub {
275 my ($usercfg, $realm, $synced_users, $opts) = @_;
276
277 print "syncing users\n";
278 $usercfg->{users} = {} if !defined($usercfg->{users});
279 my $users = $usercfg->{users};
280
281 my $oldusers = {};
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$/;
286
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";
293 }
294 }
295 }
296
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;
301
302 my $olduser = $oldusers->{$userid} // {};
303 if (defined(my $enabled = $olduser->{enable})) {
304 $user->{enable} = $enabled;
d29d2d4a 305 } elsif ($opts->{'enable-new'}) {
415179b0
TL
306 $user->{enable} = 1;
307 }
308
309 if (defined($olduser->{tokens})) {
310 $user->{tokens} = $olduser->{tokens};
311 }
312 if (defined($oldusers->{$userid})) {
313 print "updated user '$userid'\n";
314 } else {
315 print "added user '$userid'\n";
316 }
317 } else {
318 my $olduser = $users->{$userid};
319 foreach my $attr (keys %$synced_user) {
320 $olduser->{$attr} = $synced_user->{$attr};
321 }
322 print "updated user '$userid'\n";
323 }
324 }
325};
326
327my $update_groups = sub {
328 my ($usercfg, $realm, $synced_groups, $opts) = @_;
329
330 print "syncing groups\n";
331 $usercfg->{groups} = {} if !defined($usercfg->{groups});
332 my $groups = $usercfg->{groups};
333 my $oldgroups = {};
334
335 if ($opts->{full}) {
336 print "full sync, deleting outdated existing groups first\n";
337 foreach my $groupid (sort keys %$groups) {
338 next if $groupid !~ m/\-$realm$/;
339
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";
346 }
347 }
348 }
349
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";
356 } else {
357 print "added group '$groupid'\n";
358 }
359 } else {
360 my $group = $groups->{$groupid};
361 foreach my $attr (keys %$synced_group) {
362 $group->{$attr} = $synced_group->{$attr};
363 }
364 print "updated group '$groupid'\n";
365 }
366 }
367};
368
d29d2d4a
TL
369my $parse_sync_opts = sub {
370 my ($param, $realmconfig) = @_;
371
372 my $sync_opts_fmt = PVE::JSONSchema::get_format('realm-sync-options');
373
6c42a103 374 my $cfg_defaults = {};
d29d2d4a 375 if (defined(my $cfg_opts = $realmconfig->{'sync-defaults-options'})) {
6c42a103 376 $cfg_defaults = PVE::JSONSchema::parse_property_string($sync_opts_fmt, $cfg_opts);
d29d2d4a
TL
377 }
378
6c42a103 379 my $res = {};
d29d2d4a
TL
380 for my $opt (sort keys %$sync_opts_fmt) {
381 my $fmt = $sync_opts_fmt->{$opt};
382
6c42a103
TL
383 $res->{$opt} = $param->{$opt} // $cfg_defaults->{$opt} // $fmt->{default};
384
055c54b5
DC
385 raise_param_exc({
386 "$opt" => 'Not passed as parameter and not defined in realm default sync options.'
387 }) if !defined($res->{$opt});
d29d2d4a
TL
388 }
389 return $res;
390};
391
673d2bf2
DC
392__PACKAGE__->register_method ({
393 name => 'sync',
394 path => '{realm}/sync',
395 method => 'POST',
396 permissions => {
cf109814
TL
397 description => "'Realm.AllocateUser' on '/access/realm/<realm>' and "
398 ." 'User.Modify' permissions to '/access/groups/'.",
673d2bf2 399 check => [ 'and',
cf109814
TL
400 [ 'userid-param', 'Realm.AllocateUser' ],
401 [ 'userid-group', ['User.Modify'] ],
402 ],
673d2bf2 403 },
cf109814
TL
404 description => "Syncs users and/or groups from the configured LDAP to user.cfg."
405 ." NOTE: Synced groups will have the name 'name-\$realm', so make sure"
406 ." those groups do not exist to prevent overwriting.",
673d2bf2
DC
407 protected => 1,
408 parameters => {
409 additionalProperties => 0,
d29d2d4a
TL
410 properties => get_standard_option('realm-sync-options', {
411 realm => get_standard_option('realm'),
38691d98
DC
412 'dry-run' => {
413 description => "If set, does not write anything.",
414 type => 'boolean',
415 optional => 1,
416 default => 0,
417 },
418 }),
673d2bf2 419 },
cf109814
TL
420 returns => {
421 description => 'Worker Task-UPID',
422 type => 'string'
423 },
673d2bf2
DC
424 code => sub {
425 my ($param) = @_;
426
427 my $rpcenv = PVE::RPCEnvironment::get();
428 my $authuser = $rpcenv->get_user();
429
417309d7 430 my $dry_run = extract_param($param, 'dry-run');
673d2bf2
DC
431 my $realm = $param->{realm};
432 my $cfg = cfs_read_file($domainconfigfile);
cf109814 433 my $realmconfig = $cfg->{ids}->{$realm};
673d2bf2 434
cf109814
TL
435 raise_param_exc({ 'realm' => 'Realm does not exist.' }) if !defined($realmconfig);
436 my $type = $realmconfig->{type};
673d2bf2
DC
437
438 if ($type ne 'ldap' && $type ne 'ad') {
cf109814 439 die "Cannot sync realm type '$type'! Only LDAP/AD realms can be synced.\n";
673d2bf2 440 }
673d2bf2 441
d29d2d4a 442 my $opts = $parse_sync_opts->($param, $realmconfig); # can throw up
673d2bf2 443
d29d2d4a 444 my $scope = $opts->{scope};
cf109814 445 my $whatstring = $scope eq 'both' ? "users and groups" : $scope;
673d2bf2 446
cf109814 447 my $plugin = PVE::Auth::Plugin->lookup($type);
673d2bf2
DC
448
449 my $worker = sub {
417309d7
TL
450 print "(dry test run) " if $dry_run;
451 print "starting sync for realm $realm\n";
cf109814
TL
452
453 my ($synced_users, $dnmap) = $plugin->get_users($realmconfig, $realm);
454 my $synced_groups = {};
455 if ($scope eq 'groups' || $scope eq 'both') {
456 $synced_groups = $plugin->get_groups($realmconfig, $realm, $dnmap);
673d2bf2
DC
457 }
458
cf109814 459 PVE::AccessControl::lock_user_config(sub {
673d2bf2 460 my $usercfg = cfs_read_file("user.cfg");
cf109814 461 print "got data from server, updating $whatstring\n";
673d2bf2 462
415179b0 463 if ($scope eq 'users' || $scope eq 'both') {
d29d2d4a 464 $update_users->($usercfg, $realm, $synced_users, $opts);
673d2bf2
DC
465 }
466
415179b0 467 if ($scope eq 'groups' || $scope eq 'both') {
d29d2d4a 468 $update_groups->($usercfg, $realm, $synced_groups, $opts);
673d2bf2 469 }
cf109814 470
417309d7
TL
471 if ($dry_run) {
472 print "\nNOTE: Dry test run, changes were NOT written to the configuration.\n";
473 return;
38691d98 474 }
417309d7
TL
475 cfs_write_file("user.cfg", $usercfg);
476 print "successfully updated $whatstring configuration\n";
cf109814 477 }, "syncing $whatstring failed");
673d2bf2
DC
478 };
479
417309d7 480 my $workerid = !$dry_run ? 'auth-realm-sync' : 'auth-realm-sync-test';
38691d98 481 return $rpcenv->fork_worker($workerid, $realm, $authuser, $worker);
673d2bf2
DC
482 }});
483
2c3a6c0a 4841;