]> git.proxmox.com Git - pve-access-control.git/blob - PVE/API2/Domains.pm
domains: dry-run: adapt log messages and improve variable name
[pve-access-control.git] / PVE / API2 / Domains.pm
1 package PVE::API2::Domains;
2
3 use strict;
4 use warnings;
5
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);
11
12 use PVE::SafeSyslog;
13 use PVE::RESTHandler;
14 use PVE::Auth::Plugin;
15
16 my $domainconfigfile = "domains.cfg";
17
18 use base qw(PVE::RESTHandler);
19
20 __PACKAGE__->register_method ({
21 name => 'index',
22 path => '',
23 method => 'GET',
24 description => "Authentication domain index.",
25 permissions => {
26 description => "Anyone can access that, because we need that list for the login box (before the user is authenticated).",
27 user => 'world',
28 },
29 parameters => {
30 additionalProperties => 0,
31 properties => {},
32 },
33 returns => {
34 type => 'array',
35 items => {
36 type => "object",
37 properties => {
38 realm => { type => 'string' },
39 type => { type => 'string' },
40 tfa => {
41 description => "Two-factor authentication provider.",
42 type => 'string',
43 enum => [ 'yubico', 'oath' ],
44 optional => 1,
45 },
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 },
51 },
52 },
53 links => [ { rel => 'child', href => "{realm}" } ],
54 },
55 code => sub {
56 my ($param) = @_;
57
58 my $res = [];
59
60 my $cfg = cfs_read_file($domainconfigfile);
61 my $ids = $cfg->{ids};
62
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};
70 }
71 push @$res, $entry;
72 }
73
74 return $res;
75 }});
76
77 __PACKAGE__->register_method ({
78 name => 'create',
79 protected => 1,
80 path => '',
81 method => 'POST',
82 permissions => {
83 check => ['perm', '/access/realm', ['Realm.Allocate']],
84 },
85 description => "Add an authentication server.",
86 parameters => PVE::Auth::Plugin->createSchema(),
87 returns => { type => 'null' },
88 code => sub {
89 my ($param) = @_;
90
91 # always extract, add it with hook
92 my $password = extract_param($param, 'password');
93
94 PVE::Auth::Plugin::lock_domain_config(
95 sub {
96
97 my $cfg = cfs_read_file($domainconfigfile);
98 my $ids = $cfg->{ids};
99
100 my $realm = extract_param($param, 'realm');
101 my $type = $param->{type};
102
103 die "domain '$realm' already exists\n"
104 if $ids->{$realm};
105
106 die "unable to use reserved name '$realm'\n"
107 if ($realm eq 'pam' || $realm eq 'pve');
108
109 die "unable to create builtin type '$type'\n"
110 if ($type eq 'pam' || $type eq 'pve');
111
112 my $plugin = PVE::Auth::Plugin->lookup($type);
113 my $config = $plugin->check_config($realm, $param, 1, 1);
114
115 if ($config->{default}) {
116 foreach my $r (keys %$ids) {
117 delete $ids->{$r}->{default};
118 }
119 }
120
121 $ids->{$realm} = $config;
122
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
130 cfs_write_file($domainconfigfile, $cfg);
131 }, "add auth server failed");
132
133 return undef;
134 }});
135
136 __PACKAGE__->register_method ({
137 name => 'update',
138 path => '{realm}',
139 method => 'PUT',
140 permissions => {
141 check => ['perm', '/access/realm', ['Realm.Allocate']],
142 },
143 description => "Update authentication server settings.",
144 protected => 1,
145 parameters => PVE::Auth::Plugin->updateSchema(),
146 returns => { type => 'null' },
147 code => sub {
148 my ($param) = @_;
149
150 # always extract, update in hook
151 my $password = extract_param($param, 'password');
152
153 PVE::Auth::Plugin::lock_domain_config(
154 sub {
155
156 my $cfg = cfs_read_file($domainconfigfile);
157 my $ids = $cfg->{ids};
158
159 my $digest = extract_param($param, 'digest');
160 PVE::SectionConfig::assert_if_modified($cfg, $digest);
161
162 my $realm = extract_param($param, 'realm');
163
164 die "domain '$realm' does not exist\n"
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);
169
170 my $delete_pw = 0;
171 foreach my $opt (PVE::Tools::split_list($delete_str)) {
172 delete $ids->{$realm}->{$opt};
173 $delete_pw = 1 if $opt eq 'password';
174 }
175
176 my $plugin = PVE::Auth::Plugin->lookup($ids->{$realm}->{type});
177 my $config = $plugin->check_config($realm, $param, 0, 1);
178
179 if ($config->{default}) {
180 foreach my $r (keys %$ids) {
181 delete $ids->{$r}->{default};
182 }
183 }
184
185 foreach my $p (keys %$config) {
186 $ids->{$realm}->{$p} = $config->{$p};
187 }
188
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
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 ({
204 name => 'read',
205 path => '{realm}',
206 method => 'GET',
207 description => "Get auth server configuration.",
208 permissions => {
209 check => ['perm', '/access/realm', ['Realm.Allocate', 'Sys.Audit'], any => 1],
210 },
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};
224
225 my $data = $cfg->{ids}->{$realm};
226 die "domain '$realm' does not exist\n" if !$data;
227
228 $data->{digest} = $cfg->{digest};
229
230 return $data;
231 }});
232
233
234 __PACKAGE__->register_method ({
235 name => 'delete',
236 path => '{realm}',
237 method => 'DELETE',
238 permissions => {
239 check => ['perm', '/access/realm', ['Realm.Allocate']],
240 },
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
253 PVE::Auth::Plugin::lock_domain_config(
254 sub {
255
256 my $cfg = cfs_read_file($domainconfigfile);
257 my $ids = $cfg->{ids};
258 my $realm = $param->{realm};
259
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});
265
266 delete $ids->{$realm};
267
268 cfs_write_file($domainconfigfile, $cfg);
269 }, "delete auth server failed");
270
271 return undef;
272 }});
273
274 my $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;
305 } elsif ($opts->{'enable-new'}) {
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
327 my $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
369 my $parse_sync_opts = sub {
370 my ($param, $realmconfig) = @_;
371
372 my $sync_opts_fmt = PVE::JSONSchema::get_format('realm-sync-options');
373
374 my $cfg_defaults = {};
375 if (defined(my $cfg_opts = $realmconfig->{'sync-defaults-options'})) {
376 $cfg_defaults = PVE::JSONSchema::parse_property_string($sync_opts_fmt, $cfg_opts);
377 }
378
379 my $res = {};
380 for my $opt (sort keys %$sync_opts_fmt) {
381 my $fmt = $sync_opts_fmt->{$opt};
382
383 $res->{$opt} = $param->{$opt} // $cfg_defaults->{$opt} // $fmt->{default};
384
385 raise_param_exc({
386 "$opt" => 'Not passed as parameter and not defined in realm default sync options.'
387 }) if !defined($res->{$opt});
388 }
389 return $res;
390 };
391
392 __PACKAGE__->register_method ({
393 name => 'sync',
394 path => '{realm}/sync',
395 method => 'POST',
396 permissions => {
397 description => "'Realm.AllocateUser' on '/access/realm/<realm>' and "
398 ." 'User.Modify' permissions to '/access/groups/'.",
399 check => [ 'and',
400 [ 'userid-param', 'Realm.AllocateUser' ],
401 [ 'userid-group', ['User.Modify'] ],
402 ],
403 },
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.",
407 protected => 1,
408 parameters => {
409 additionalProperties => 0,
410 properties => get_standard_option('realm-sync-options', {
411 realm => get_standard_option('realm'),
412 'dry-run' => {
413 description => "If set, does not write anything.",
414 type => 'boolean',
415 optional => 1,
416 default => 0,
417 },
418 }),
419 },
420 returns => {
421 description => 'Worker Task-UPID',
422 type => 'string'
423 },
424 code => sub {
425 my ($param) = @_;
426
427 my $rpcenv = PVE::RPCEnvironment::get();
428 my $authuser = $rpcenv->get_user();
429
430 my $dry_run = extract_param($param, 'dry-run');
431 my $realm = $param->{realm};
432 my $cfg = cfs_read_file($domainconfigfile);
433 my $realmconfig = $cfg->{ids}->{$realm};
434
435 raise_param_exc({ 'realm' => 'Realm does not exist.' }) if !defined($realmconfig);
436 my $type = $realmconfig->{type};
437
438 if ($type ne 'ldap' && $type ne 'ad') {
439 die "Cannot sync realm type '$type'! Only LDAP/AD realms can be synced.\n";
440 }
441
442 my $opts = $parse_sync_opts->($param, $realmconfig); # can throw up
443
444 my $scope = $opts->{scope};
445 my $whatstring = $scope eq 'both' ? "users and groups" : $scope;
446
447 my $plugin = PVE::Auth::Plugin->lookup($type);
448
449 my $worker = sub {
450 print "(dry test run) " if $dry_run;
451 print "starting sync for realm $realm\n";
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);
457 }
458
459 PVE::AccessControl::lock_user_config(sub {
460 my $usercfg = cfs_read_file("user.cfg");
461 print "got data from server, updating $whatstring\n";
462
463 if ($scope eq 'users' || $scope eq 'both') {
464 $update_users->($usercfg, $realm, $synced_users, $opts);
465 }
466
467 if ($scope eq 'groups' || $scope eq 'both') {
468 $update_groups->($usercfg, $realm, $synced_groups, $opts);
469 }
470
471 if ($dry_run) {
472 print "\nNOTE: Dry test run, changes were NOT written to the configuration.\n";
473 return;
474 }
475 cfs_write_file("user.cfg", $usercfg);
476 print "successfully updated $whatstring configuration\n";
477 }, "syncing $whatstring failed");
478 };
479
480 my $workerid = !$dry_run ? 'auth-realm-sync' : 'auth-realm-sync-test';
481 return $rpcenv->fork_worker($workerid, $realm, $authuser, $worker);
482 }});
483
484 1;