]> git.proxmox.com Git - pve-access-control.git/blob - PVE/API2/Domains.pm
split and sort some module use
[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 PVE::Auth::Plugin::lock_domain_config(
92 sub {
93
94 my $cfg = cfs_read_file($domainconfigfile);
95 my $ids = $cfg->{ids};
96
97 my $realm = extract_param($param, 'realm');
98 my $type = $param->{type};
99
100 die "domain '$realm' already exists\n"
101 if $ids->{$realm};
102
103 die "unable to use reserved name '$realm'\n"
104 if ($realm eq 'pam' || $realm eq 'pve');
105
106 die "unable to create builtin type '$type'\n"
107 if ($type eq 'pam' || $type eq 'pve');
108
109 my $plugin = PVE::Auth::Plugin->lookup($type);
110 my $config = $plugin->check_config($realm, $param, 1, 1);
111
112 if ($config->{default}) {
113 foreach my $r (keys %$ids) {
114 delete $ids->{$r}->{default};
115 }
116 }
117
118 $ids->{$realm} = $config;
119
120 cfs_write_file($domainconfigfile, $cfg);
121 }, "add auth server failed");
122
123 return undef;
124 }});
125
126 __PACKAGE__->register_method ({
127 name => 'update',
128 path => '{realm}',
129 method => 'PUT',
130 permissions => {
131 check => ['perm', '/access/realm', ['Realm.Allocate']],
132 },
133 description => "Update authentication server settings.",
134 protected => 1,
135 parameters => PVE::Auth::Plugin->updateSchema(),
136 returns => { type => 'null' },
137 code => sub {
138 my ($param) = @_;
139
140 PVE::Auth::Plugin::lock_domain_config(
141 sub {
142
143 my $cfg = cfs_read_file($domainconfigfile);
144 my $ids = $cfg->{ids};
145
146 my $digest = extract_param($param, 'digest');
147 PVE::SectionConfig::assert_if_modified($cfg, $digest);
148
149 my $realm = extract_param($param, 'realm');
150
151 die "domain '$realm' does not exist\n"
152 if !$ids->{$realm};
153
154 my $delete_str = extract_param($param, 'delete');
155 die "no options specified\n" if !$delete_str && !scalar(keys %$param);
156
157 foreach my $opt (PVE::Tools::split_list($delete_str)) {
158 delete $ids->{$realm}->{$opt};
159 }
160
161 my $plugin = PVE::Auth::Plugin->lookup($ids->{$realm}->{type});
162 my $config = $plugin->check_config($realm, $param, 0, 1);
163
164 if ($config->{default}) {
165 foreach my $r (keys %$ids) {
166 delete $ids->{$r}->{default};
167 }
168 }
169
170 foreach my $p (keys %$config) {
171 $ids->{$realm}->{$p} = $config->{$p};
172 }
173
174 cfs_write_file($domainconfigfile, $cfg);
175 }, "update auth server failed");
176
177 return undef;
178 }});
179
180 # fixme: return format!
181 __PACKAGE__->register_method ({
182 name => 'read',
183 path => '{realm}',
184 method => 'GET',
185 description => "Get auth server configuration.",
186 permissions => {
187 check => ['perm', '/access/realm', ['Realm.Allocate', 'Sys.Audit'], any => 1],
188 },
189 parameters => {
190 additionalProperties => 0,
191 properties => {
192 realm => get_standard_option('realm'),
193 },
194 },
195 returns => {},
196 code => sub {
197 my ($param) = @_;
198
199 my $cfg = cfs_read_file($domainconfigfile);
200
201 my $realm = $param->{realm};
202
203 my $data = $cfg->{ids}->{$realm};
204 die "domain '$realm' does not exist\n" if !$data;
205
206 $data->{digest} = $cfg->{digest};
207
208 return $data;
209 }});
210
211
212 __PACKAGE__->register_method ({
213 name => 'delete',
214 path => '{realm}',
215 method => 'DELETE',
216 permissions => {
217 check => ['perm', '/access/realm', ['Realm.Allocate']],
218 },
219 description => "Delete an authentication server.",
220 protected => 1,
221 parameters => {
222 additionalProperties => 0,
223 properties => {
224 realm => get_standard_option('realm'),
225 }
226 },
227 returns => { type => 'null' },
228 code => sub {
229 my ($param) = @_;
230
231 PVE::Auth::Plugin::lock_domain_config(
232 sub {
233
234 my $cfg = cfs_read_file($domainconfigfile);
235 my $ids = $cfg->{ids};
236
237 my $realm = $param->{realm};
238
239 die "domain '$realm' does not exist\n" if !$ids->{$realm};
240
241 delete $ids->{$realm};
242
243 cfs_write_file($domainconfigfile, $cfg);
244 }, "delete auth server failed");
245
246 return undef;
247 }});
248
249 my $update_users = sub {
250 my ($usercfg, $realm, $synced_users, $opts) = @_;
251
252 print "syncing users\n";
253 $usercfg->{users} = {} if !defined($usercfg->{users});
254 my $users = $usercfg->{users};
255
256 my $oldusers = {};
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$/;
261
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";
268 }
269 }
270 }
271
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;
276
277 my $olduser = $oldusers->{$userid} // {};
278 if (defined(my $enabled = $olduser->{enable})) {
279 $user->{enable} = $enabled;
280 } elsif ($opts->{'enable-new'}) {
281 $user->{enable} = 1;
282 }
283
284 if (defined($olduser->{tokens})) {
285 $user->{tokens} = $olduser->{tokens};
286 }
287 if (defined($oldusers->{$userid})) {
288 print "updated user '$userid'\n";
289 } else {
290 print "added user '$userid'\n";
291 }
292 } else {
293 my $olduser = $users->{$userid};
294 foreach my $attr (keys %$synced_user) {
295 $olduser->{$attr} = $synced_user->{$attr};
296 }
297 print "updated user '$userid'\n";
298 }
299 }
300 };
301
302 my $update_groups = sub {
303 my ($usercfg, $realm, $synced_groups, $opts) = @_;
304
305 print "syncing groups\n";
306 $usercfg->{groups} = {} if !defined($usercfg->{groups});
307 my $groups = $usercfg->{groups};
308 my $oldgroups = {};
309
310 if ($opts->{full}) {
311 print "full sync, deleting outdated existing groups first\n";
312 foreach my $groupid (sort keys %$groups) {
313 next if $groupid !~ m/\-$realm$/;
314
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";
321 }
322 }
323 }
324
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";
331 } else {
332 print "added group '$groupid'\n";
333 }
334 } else {
335 my $group = $groups->{$groupid};
336 foreach my $attr (keys %$synced_group) {
337 $group->{$attr} = $synced_group->{$attr};
338 }
339 print "updated group '$groupid'\n";
340 }
341 }
342 };
343
344 my $parse_sync_opts = sub {
345 my ($param, $realmconfig) = @_;
346
347 my $sync_opts_fmt = PVE::JSONSchema::get_format('realm-sync-options');
348
349 my $res = {};
350 if (defined(my $cfg_opts = $realmconfig->{'sync-defaults-options'})) {
351 $res = PVE::JSONSchema::parse_property_string($sync_opts_fmt, $cfg_opts);
352 }
353
354 for my $opt (sort keys %$sync_opts_fmt) {
355 my $fmt = $sync_opts_fmt->{$opt};
356
357 if (exists $param->{$opt}) {
358 $res->{$opt} = $param->{$opt};
359 } elsif (!exists $res->{$opt}) {
360 raise_param_exc({
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};
364 }
365 }
366 return $res;
367 };
368
369 __PACKAGE__->register_method ({
370 name => 'sync',
371 path => '{realm}/sync',
372 method => 'POST',
373 permissions => {
374 description => "'Realm.AllocateUser' on '/access/realm/<realm>' and "
375 ." 'User.Modify' permissions to '/access/groups/'.",
376 check => [ 'and',
377 [ 'userid-param', 'Realm.AllocateUser' ],
378 [ 'userid-group', ['User.Modify'] ],
379 ],
380 },
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.",
384 protected => 1,
385 parameters => {
386 additionalProperties => 0,
387 properties => get_standard_option('realm-sync-options', {
388 realm => get_standard_option('realm'),
389 })
390 },
391 returns => {
392 description => 'Worker Task-UPID',
393 type => 'string'
394 },
395 code => sub {
396 my ($param) = @_;
397
398 my $rpcenv = PVE::RPCEnvironment::get();
399 my $authuser = $rpcenv->get_user();
400
401 my $realm = $param->{realm};
402 my $cfg = cfs_read_file($domainconfigfile);
403 my $realmconfig = $cfg->{ids}->{$realm};
404
405 raise_param_exc({ 'realm' => 'Realm does not exist.' }) if !defined($realmconfig);
406 my $type = $realmconfig->{type};
407
408 if ($type ne 'ldap' && $type ne 'ad') {
409 die "Cannot sync realm type '$type'! Only LDAP/AD realms can be synced.\n";
410 }
411
412 my $opts = $parse_sync_opts->($param, $realmconfig); # can throw up
413
414 my $scope = $opts->{scope};
415 my $whatstring = $scope eq 'both' ? "users and groups" : $scope;
416
417 my $plugin = PVE::Auth::Plugin->lookup($type);
418
419 my $worker = sub {
420 print "starting sync for realm $realm\n";
421
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);
426 }
427
428 PVE::AccessControl::lock_user_config(sub {
429 my $usercfg = cfs_read_file("user.cfg");
430 print "got data from server, updating $whatstring\n";
431
432 if ($scope eq 'users' || $scope eq 'both') {
433 $update_users->($usercfg, $realm, $synced_users, $opts);
434 }
435
436 if ($scope eq 'groups' || $scope eq 'both') {
437 $update_groups->($usercfg, $realm, $synced_groups, $opts);
438 }
439
440 cfs_write_file("user.cfg", $usercfg);
441 print "successfully updated $whatstring configuration\n";
442 }, "syncing $whatstring failed");
443 };
444
445 return $rpcenv->fork_worker('auth-realm-sync', $realm, $authuser, $worker);
446 }});
447
448 1;