]> git.proxmox.com Git - pve-access-control.git/blob - src/PVE/API2/User.pm
api: users: code-style cleanup and sort when iterating users
[pve-access-control.git] / src / PVE / API2 / User.pm
1 package PVE::API2::User;
2
3 use strict;
4 use warnings;
5
6 use PVE::Exception qw(raise raise_perm_exc raise_param_exc);
7 use PVE::Cluster qw (cfs_read_file cfs_write_file);
8 use PVE::Tools qw(split_list extract_param);
9 use PVE::JSONSchema qw(get_standard_option register_standard_option);
10 use PVE::SafeSyslog;
11
12 use PVE::AccessControl;
13 use PVE::TokenConfig;
14
15 use PVE::RESTHandler;
16
17 use base qw(PVE::RESTHandler);
18
19 register_standard_option('user-enable', {
20 description => "Enable the account (default). You can set this to '0' to disable the account",
21 type => 'boolean',
22 optional => 1,
23 default => 1,
24 });
25 register_standard_option('user-expire', {
26 description => "Account expiration date (seconds since epoch). '0' means no expiration date.",
27 type => 'integer',
28 minimum => 0,
29 optional => 1,
30 });
31 register_standard_option('user-firstname', { type => 'string', optional => 1 });
32 register_standard_option('user-lastname', { type => 'string', optional => 1 });
33 register_standard_option('user-email', { type => 'string', optional => 1, format => 'email-opt' });
34 register_standard_option('user-comment', { type => 'string', optional => 1 });
35 register_standard_option('user-keys', {
36 description => "Keys for two factor auth (yubico).",
37 type => 'string',
38 optional => 1,
39 });
40 register_standard_option('group-list', {
41 type => 'string', format => 'pve-groupid-list',
42 optional => 1,
43 completion => \&PVE::AccessControl::complete_group,
44 });
45 register_standard_option('token-subid', {
46 type => 'string',
47 pattern => $PVE::AccessControl::token_subid_regex,
48 description => 'User-specific token identifier.',
49 });
50 register_standard_option('token-expire', {
51 description => "API token expiration date (seconds since epoch). '0' means no expiration date.",
52 type => 'integer',
53 minimum => 0,
54 optional => 1,
55 default => 'same as user',
56 });
57 register_standard_option('token-privsep', {
58 description => "Restrict API token privileges with separate ACLs (default), or give full privileges of corresponding user.",
59 type => 'boolean',
60 optional => 1,
61 default => 1,
62 });
63 register_standard_option('token-comment', { type => 'string', optional => 1 });
64 register_standard_option('token-info', {
65 type => 'object',
66 properties => {
67 expire => get_standard_option('token-expire'),
68 privsep => get_standard_option('token-privsep'),
69 comment => get_standard_option('token-comment'),
70 }
71 });
72
73 my $token_info_extend = sub {
74 my ($props) = @_;
75
76 my $obj = get_standard_option('token-info');
77 my $base_props = $obj->{properties};
78 $obj->{properties} = {};
79
80 foreach my $prop (keys %$base_props) {
81 $obj->{properties}->{$prop} = $base_props->{$prop};
82 }
83
84 foreach my $add_prop (keys %$props) {
85 $obj->{properties}->{$add_prop} = $props->{$add_prop};
86 }
87
88 return $obj;
89 };
90
91 my $extract_user_data = sub {
92 my ($data, $full) = @_;
93
94 my $res = {};
95
96 foreach my $prop (qw(enable expire firstname lastname email comment keys)) {
97 $res->{$prop} = $data->{$prop} if defined($data->{$prop});
98 }
99
100 return $res if !$full;
101
102 $res->{groups} = $data->{groups} ? [ keys %{$data->{groups}} ] : [];
103 $res->{tokens} = $data->{tokens};
104
105 return $res;
106 };
107
108 __PACKAGE__->register_method ({
109 name => 'index',
110 path => '',
111 method => 'GET',
112 description => "User index.",
113 permissions => {
114 description => "The returned list is restricted to users where you have 'User.Modify' or 'Sys.Audit' permissions on '/access/groups' or on a group the user belongs too. But it always includes the current (authenticated) user.",
115 user => 'all',
116 },
117 parameters => {
118 additionalProperties => 0,
119 properties => {
120 enabled => {
121 type => 'boolean',
122 description => "Optional filter for enable property.",
123 optional => 1,
124 },
125 full => {
126 type => 'boolean',
127 description => "Include group and token information.",
128 optional => 1,
129 default => 0,
130 }
131 },
132 },
133 returns => {
134 type => 'array',
135 items => {
136 type => "object",
137 properties => {
138 userid => get_standard_option('userid-completed'),
139 enable => get_standard_option('user-enable'),
140 expire => get_standard_option('user-expire'),
141 firstname => get_standard_option('user-firstname'),
142 lastname => get_standard_option('user-lastname'),
143 email => get_standard_option('user-email'),
144 comment => get_standard_option('user-comment'),
145 keys => get_standard_option('user-keys'),
146 groups => get_standard_option('group-list'),
147 tokens => {
148 type => 'array',
149 optional => 1,
150 items => $token_info_extend->({
151 tokenid => get_standard_option('token-subid'),
152 }),
153 },
154 'realm-type' => {
155 type => 'string', format => 'pve-realm',
156 description => 'The type of the users realm',
157 optional => 1, # it should always be there, but we use conditional code below, so..
158 },
159 },
160 },
161 links => [ { rel => 'child', href => "{userid}" } ],
162 },
163 code => sub {
164 my ($param) = @_;
165
166 my $rpcenv = PVE::RPCEnvironment::get();
167 my $usercfg = $rpcenv->{user_cfg};
168 my $authuser = $rpcenv->get_user();
169
170 my $domainscfg = cfs_read_file('domains.cfg');
171 my $domainids = $domainscfg->{ids};
172
173 my $res = [];
174
175 my $privs = [ 'User.Modify', 'Sys.Audit' ];
176 my $canUserMod = $rpcenv->check_any($authuser, "/access/groups", $privs, 1);
177 my $groups = $rpcenv->filter_groups($authuser, $privs, 1);
178 my $allowed_users = $rpcenv->group_member_join([keys %$groups]);
179
180 foreach my $user (sort keys %{$usercfg->{users}}) {
181 if (!($canUserMod || $user eq $authuser)) {
182 next if !$allowed_users->{$user};
183 }
184
185 my $entry = $extract_user_data->($usercfg->{users}->{$user}, $param->{full});
186
187 if (defined($param->{enabled})) {
188 next if $entry->{enable} && !$param->{enabled};
189 next if !$entry->{enable} && $param->{enabled};
190 }
191
192 $entry->{groups} = join(',', @{$entry->{groups}}) if $entry->{groups};
193
194 if (defined(my $tokens = $entry->{tokens})) {
195 $entry->{tokens} = [
196 map { { tokenid => $_, %{$tokens->{$_}} } } sort keys %$tokens
197 ];
198 }
199
200 my (undef, undef, $realm) = PVE::AccessControl::verify_username($user, 1);
201 if (defined($realm) && exists($domainids->{$realm})) {
202 $entry->{'realm-type'} = $domainids->{$realm}->{type};
203 }
204
205 $entry->{userid} = $user;
206
207 push @$res, $entry;
208 }
209
210 return $res;
211 }});
212
213 __PACKAGE__->register_method ({
214 name => 'create_user',
215 protected => 1,
216 path => '',
217 method => 'POST',
218 permissions => {
219 description => "You need 'Realm.AllocateUser' on '/access/realm/<realm>' on the realm of user <userid>, and 'User.Modify' permissions to '/access/groups/<group>' for any group specified (or 'User.Modify' on '/access/groups' if you pass no groups.",
220 check => [ 'and',
221 [ 'userid-param', 'Realm.AllocateUser'],
222 [ 'userid-group', ['User.Modify'], groups_param => 1],
223 ],
224 },
225 description => "Create new user.",
226 parameters => {
227 additionalProperties => 0,
228 properties => {
229 userid => get_standard_option('userid-completed'),
230 enable => get_standard_option('user-enable'),
231 expire => get_standard_option('user-expire'),
232 firstname => get_standard_option('user-firstname'),
233 lastname => get_standard_option('user-lastname'),
234 email => get_standard_option('user-email'),
235 comment => get_standard_option('user-comment'),
236 keys => get_standard_option('user-keys'),
237 password => {
238 description => "Initial password.",
239 type => 'string',
240 optional => 1,
241 minLength => 5,
242 maxLength => 64
243 },
244 groups => get_standard_option('group-list'),
245 },
246 },
247 returns => { type => 'null' },
248 code => sub {
249 my ($param) = @_;
250
251 PVE::AccessControl::lock_user_config(sub {
252 my ($username, $ruid, $realm) = PVE::AccessControl::verify_username($param->{userid});
253
254 my $usercfg = cfs_read_file("user.cfg");
255
256 # ensure "user exists" check works for case insensitive realms
257 $username = PVE::AccessControl::lookup_username($username, 1);
258 die "user '$username' already exists\n" if $usercfg->{users}->{$username};
259
260 PVE::AccessControl::domain_set_password($realm, $ruid, $param->{password})
261 if defined($param->{password});
262
263 my $enable = defined($param->{enable}) ? $param->{enable} : 1;
264 $usercfg->{users}->{$username} = { enable => $enable };
265 $usercfg->{users}->{$username}->{expire} = $param->{expire} if $param->{expire};
266
267 if ($param->{groups}) {
268 foreach my $group (split_list($param->{groups})) {
269 if ($usercfg->{groups}->{$group}) {
270 PVE::AccessControl::add_user_group($username, $usercfg, $group);
271 } else {
272 die "no such group '$group'\n";
273 }
274 }
275 }
276
277 $usercfg->{users}->{$username}->{firstname} = $param->{firstname} if $param->{firstname};
278 $usercfg->{users}->{$username}->{lastname} = $param->{lastname} if $param->{lastname};
279 $usercfg->{users}->{$username}->{email} = $param->{email} if $param->{email};
280 $usercfg->{users}->{$username}->{comment} = $param->{comment} if $param->{comment};
281 $usercfg->{users}->{$username}->{keys} = $param->{keys} if $param->{keys};
282
283 cfs_write_file("user.cfg", $usercfg);
284 }, "create user failed");
285
286 return undef;
287 }});
288
289 __PACKAGE__->register_method ({
290 name => 'read_user',
291 path => '{userid}',
292 method => 'GET',
293 description => "Get user configuration.",
294 permissions => {
295 check => ['userid-group', ['User.Modify', 'Sys.Audit']],
296 },
297 parameters => {
298 additionalProperties => 0,
299 properties => {
300 userid => get_standard_option('userid-completed'),
301 },
302 },
303 returns => {
304 additionalProperties => 0,
305 properties => {
306 enable => get_standard_option('user-enable'),
307 expire => get_standard_option('user-expire'),
308 firstname => get_standard_option('user-firstname'),
309 lastname => get_standard_option('user-lastname'),
310 email => get_standard_option('user-email'),
311 comment => get_standard_option('user-comment'),
312 keys => get_standard_option('user-keys'),
313 groups => {
314 type => 'array',
315 optional => 1,
316 items => {
317 type => 'string',
318 format => 'pve-groupid',
319 },
320 },
321 tokens => {
322 optional => 1,
323 type => 'object',
324 },
325 },
326 type => "object"
327 },
328 code => sub {
329 my ($param) = @_;
330
331 my ($username, undef, $domain) =
332 PVE::AccessControl::verify_username($param->{userid});
333
334 my $usercfg = cfs_read_file("user.cfg");
335
336 my $data = PVE::AccessControl::check_user_exist($usercfg, $username);
337
338 return &$extract_user_data($data, 1);
339 }});
340
341 __PACKAGE__->register_method ({
342 name => 'update_user',
343 protected => 1,
344 path => '{userid}',
345 method => 'PUT',
346 permissions => {
347 check => ['userid-group', ['User.Modify'], groups_param => 1 ],
348 },
349 description => "Update user configuration.",
350 parameters => {
351 additionalProperties => 0,
352 properties => {
353 userid => get_standard_option('userid-completed'),
354 enable => get_standard_option('user-enable'),
355 expire => get_standard_option('user-expire'),
356 firstname => get_standard_option('user-firstname'),
357 lastname => get_standard_option('user-lastname'),
358 email => get_standard_option('user-email'),
359 comment => get_standard_option('user-comment'),
360 keys => get_standard_option('user-keys'),
361 groups => get_standard_option('group-list'),
362 append => {
363 type => 'boolean',
364 optional => 1,
365 requires => 'groups',
366 },
367 },
368 },
369 returns => { type => 'null' },
370 code => sub {
371 my ($param) = @_;
372
373 my ($username, $ruid, $realm) =
374 PVE::AccessControl::verify_username($param->{userid});
375
376 PVE::AccessControl::lock_user_config(
377 sub {
378
379 my $usercfg = cfs_read_file("user.cfg");
380
381 PVE::AccessControl::check_user_exist($usercfg, $username);
382
383 $usercfg->{users}->{$username}->{enable} = $param->{enable} if defined($param->{enable});
384
385 $usercfg->{users}->{$username}->{expire} = $param->{expire} if defined($param->{expire});
386
387 PVE::AccessControl::delete_user_group($username, $usercfg)
388 if (!$param->{append} && defined($param->{groups}));
389
390 if ($param->{groups}) {
391 foreach my $group (split_list($param->{groups})) {
392 if ($usercfg->{groups}->{$group}) {
393 PVE::AccessControl::add_user_group($username, $usercfg, $group);
394 } else {
395 die "no such group '$group'\n";
396 }
397 }
398 }
399
400 $usercfg->{users}->{$username}->{firstname} = $param->{firstname} if defined($param->{firstname});
401 $usercfg->{users}->{$username}->{lastname} = $param->{lastname} if defined($param->{lastname});
402 $usercfg->{users}->{$username}->{email} = $param->{email} if defined($param->{email});
403 $usercfg->{users}->{$username}->{comment} = $param->{comment} if defined($param->{comment});
404 $usercfg->{users}->{$username}->{keys} = $param->{keys} if defined($param->{keys});
405
406 cfs_write_file("user.cfg", $usercfg);
407 }, "update user failed");
408
409 return undef;
410 }});
411
412 __PACKAGE__->register_method ({
413 name => 'delete_user',
414 protected => 1,
415 path => '{userid}',
416 method => 'DELETE',
417 description => "Delete user.",
418 permissions => {
419 check => [ 'and',
420 [ 'userid-param', 'Realm.AllocateUser'],
421 [ 'userid-group', ['User.Modify']],
422 ],
423 },
424 parameters => {
425 additionalProperties => 0,
426 properties => {
427 userid => get_standard_option('userid-completed'),
428 }
429 },
430 returns => { type => 'null' },
431 code => sub {
432 my ($param) = @_;
433
434 my $rpcenv = PVE::RPCEnvironment::get();
435 my $authuser = $rpcenv->get_user();
436
437 my ($userid, $ruid, $realm) =
438 PVE::AccessControl::verify_username($param->{userid});
439
440 PVE::AccessControl::lock_user_config(
441 sub {
442
443 my $usercfg = cfs_read_file("user.cfg");
444
445 my $domain_cfg = cfs_read_file('domains.cfg');
446 if (my $cfg = $domain_cfg->{ids}->{$realm}) {
447 my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
448 $plugin->delete_user($cfg, $realm, $ruid);
449 }
450
451 # Remove TFA data before removing the user entry as the user entry tells us whether
452 # we need ot update priv/tfa.cfg.
453 PVE::AccessControl::user_set_tfa($userid, $realm, undef, undef, $usercfg, $domain_cfg);
454
455 delete $usercfg->{users}->{$userid};
456
457 PVE::AccessControl::delete_user_group($userid, $usercfg);
458 PVE::AccessControl::delete_user_acl($userid, $usercfg);
459 cfs_write_file("user.cfg", $usercfg);
460 }, "delete user failed");
461
462 return undef;
463 }});
464
465 __PACKAGE__->register_method ({
466 name => 'read_user_tfa_type',
467 path => '{userid}/tfa',
468 method => 'GET',
469 protected => 1,
470 description => "Get user TFA types (Personal and Realm).",
471 permissions => {
472 check => [ 'or',
473 ['userid-param', 'self'],
474 ['userid-group', ['User.Modify', 'Sys.Audit']],
475 ],
476 },
477 parameters => {
478 additionalProperties => 0,
479 properties => {
480 userid => get_standard_option('userid-completed'),
481 },
482 },
483 returns => {
484 additionalProperties => 0,
485 properties => {
486 realm => {
487 type => 'string',
488 enum => [qw(oath yubico)],
489 description => "The type of TFA the users realm has set, if any.",
490 optional => 1,
491 },
492 user => {
493 type => 'string',
494 enum => [qw(oath u2f)],
495 description => "The type of TFA the user has set, if any.",
496 optional => 1,
497 },
498 },
499 type => "object"
500 },
501 code => sub {
502 my ($param) = @_;
503
504 my ($username, undef, $realm) = PVE::AccessControl::verify_username($param->{userid});
505
506
507 my $domain_cfg = cfs_read_file('domains.cfg');
508 my $realm_cfg = $domain_cfg->{ids}->{$realm};
509 die "auth domain '$realm' does not exist\n" if !$realm_cfg;
510
511 my $realm_tfa = {};
512 $realm_tfa = PVE::Auth::Plugin::parse_tfa_config($realm_cfg->{tfa})
513 if $realm_cfg->{tfa};
514
515 my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
516 my $tfa = $tfa_cfg->{users}->{$username};
517
518 my $res = {};
519 $res->{realm} = $realm_tfa->{type} if $realm_tfa->{type};
520 $res->{user} = $tfa->{type} if $tfa->{type};
521 return $res;
522 }});
523
524 __PACKAGE__->register_method ({
525 name => 'token_index',
526 path => '{userid}/token',
527 method => 'GET',
528 description => "Get user API tokens.",
529 permissions => {
530 check => ['or',
531 ['userid-param', 'self'],
532 ['perm', '/access/users/{userid}', ['User.Modify']],
533 ],
534 },
535 parameters => {
536 additionalProperties => 0,
537 properties => {
538 userid => get_standard_option('userid-completed'),
539 },
540 },
541 returns => {
542 type => "array",
543 items => $token_info_extend->({
544 tokenid => get_standard_option('token-subid'),
545 }),
546 links => [ { rel => 'child', href => "{tokenid}" } ],
547 },
548 code => sub {
549 my ($param) = @_;
550
551 my $userid = PVE::AccessControl::verify_username($param->{userid});
552 my $usercfg = cfs_read_file("user.cfg");
553
554 my $user = PVE::AccessControl::check_user_exist($usercfg, $userid);
555
556 my $tokens = $user->{tokens} // {};
557 return [ map { $tokens->{$_}->{tokenid} = $_; $tokens->{$_} } keys %$tokens];
558 }});
559
560 __PACKAGE__->register_method ({
561 name => 'read_token',
562 path => '{userid}/token/{tokenid}',
563 method => 'GET',
564 description => "Get specific API token information.",
565 permissions => {
566 check => ['or',
567 ['userid-param', 'self'],
568 ['perm', '/access/users/{userid}', ['User.Modify']],
569 ],
570 },
571 parameters => {
572 additionalProperties => 0,
573 properties => {
574 userid => get_standard_option('userid-completed'),
575 tokenid => get_standard_option('token-subid'),
576 },
577 },
578 returns => get_standard_option('token-info'),
579 code => sub {
580 my ($param) = @_;
581
582 my $userid = PVE::AccessControl::verify_username($param->{userid});
583 my $tokenid = $param->{tokenid};
584
585 my $usercfg = cfs_read_file("user.cfg");
586
587 return PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid);
588 }});
589
590 __PACKAGE__->register_method ({
591 name => 'generate_token',
592 path => '{userid}/token/{tokenid}',
593 method => 'POST',
594 description => "Generate a new API token for a specific user. NOTE: returns API token value, which needs to be stored as it cannot be retrieved afterwards!",
595 protected => 1,
596 permissions => {
597 check => ['or',
598 ['userid-param', 'self'],
599 ['perm', '/access/users/{userid}', ['User.Modify']],
600 ],
601 },
602 parameters => {
603 additionalProperties => 0,
604 properties => {
605 userid => get_standard_option('userid-completed'),
606 tokenid => get_standard_option('token-subid'),
607 expire => get_standard_option('token-expire'),
608 privsep => get_standard_option('token-privsep'),
609 comment => get_standard_option('token-comment'),
610 },
611 },
612 returns => {
613 additionalProperties => 0,
614 type => "object",
615 properties => {
616 info => get_standard_option('token-info'),
617 value => {
618 type => 'string',
619 description => 'API token value used for authentication.',
620 },
621 'full-tokenid' => {
622 type => 'string',
623 format_description => '<userid>!<tokenid>',
624 description => 'The full token id.',
625 },
626 },
627 },
628 code => sub {
629 my ($param) = @_;
630
631 my $userid = PVE::AccessControl::verify_username(extract_param($param, 'userid'));
632 my $tokenid = extract_param($param, 'tokenid');
633
634 my $usercfg = cfs_read_file("user.cfg");
635
636 my $token = PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid, 1);
637 my ($full_tokenid, $value);
638
639 PVE::AccessControl::check_user_exist($usercfg, $userid);
640 raise_param_exc({ 'tokenid' => 'Token already exists.' }) if defined($token);
641
642 my $generate_and_add_token = sub {
643 $usercfg = cfs_read_file("user.cfg");
644 PVE::AccessControl::check_user_exist($usercfg, $userid);
645 die "Token already exists.\n" if defined(PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid, 1));
646
647 $full_tokenid = PVE::AccessControl::join_tokenid($userid, $tokenid);
648 $value = PVE::TokenConfig::generate_token($full_tokenid);
649
650 $token = {};
651 $token->{privsep} = defined($param->{privsep}) ? $param->{privsep} : 1;
652 $token->{expire} = $param->{expire} if defined($param->{expire});
653 $token->{comment} = $param->{comment} if $param->{comment};
654
655 $usercfg->{users}->{$userid}->{tokens}->{$tokenid} = $token;
656 cfs_write_file("user.cfg", $usercfg);
657 };
658
659 PVE::AccessControl::lock_user_config($generate_and_add_token, 'generating token failed');
660
661 return {
662 info => $token,
663 value => $value,
664 'full-tokenid' => $full_tokenid,
665 };
666 }});
667
668
669 __PACKAGE__->register_method ({
670 name => 'update_token_info',
671 path => '{userid}/token/{tokenid}',
672 method => 'PUT',
673 description => "Update API token for a specific user.",
674 protected => 1,
675 permissions => {
676 check => ['or',
677 ['userid-param', 'self'],
678 ['perm', '/access/users/{userid}', ['User.Modify']],
679 ],
680 },
681 parameters => {
682 additionalProperties => 0,
683 properties => {
684 userid => get_standard_option('userid-completed'),
685 tokenid => get_standard_option('token-subid'),
686 expire => get_standard_option('token-expire'),
687 privsep => get_standard_option('token-privsep'),
688 comment => get_standard_option('token-comment'),
689 },
690 },
691 returns => get_standard_option('token-info', { description => "Updated token information." }),
692 code => sub {
693 my ($param) = @_;
694
695 my $userid = PVE::AccessControl::verify_username(extract_param($param, 'userid'));
696 my $tokenid = extract_param($param, 'tokenid');
697
698 my $usercfg = cfs_read_file("user.cfg");
699 my $token = PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid);
700
701 my $update_token = sub {
702 $usercfg = cfs_read_file("user.cfg");
703 $token = PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid);
704
705 my $full_tokenid = PVE::AccessControl::join_tokenid($userid, $tokenid);
706
707 $token->{privsep} = $param->{privsep} if defined($param->{privsep});
708 $token->{expire} = $param->{expire} if defined($param->{expire});
709 $token->{comment} = $param->{comment} if $param->{comment};
710
711 $usercfg->{users}->{$userid}->{tokens}->{$tokenid} = $token;
712 cfs_write_file("user.cfg", $usercfg);
713 };
714
715 PVE::AccessControl::lock_user_config($update_token, 'updating token info failed');
716
717 return $token;
718 }});
719
720
721 __PACKAGE__->register_method ({
722 name => 'remove_token',
723 path => '{userid}/token/{tokenid}',
724 method => 'DELETE',
725 description => "Remove API token for a specific user.",
726 protected => 1,
727 permissions => {
728 check => ['or',
729 ['userid-param', 'self'],
730 ['perm', '/access/users/{userid}', ['User.Modify']],
731 ],
732 },
733 parameters => {
734 additionalProperties => 0,
735 properties => {
736 userid => get_standard_option('userid-completed'),
737 tokenid => get_standard_option('token-subid'),
738 },
739 },
740 returns => { type => 'null' },
741 code => sub {
742 my ($param) = @_;
743
744 my $userid = PVE::AccessControl::verify_username(extract_param($param, 'userid'));
745 my $tokenid = extract_param($param, 'tokenid');
746
747 my $usercfg = cfs_read_file("user.cfg");
748 my $token = PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid);
749
750 my $update_token = sub {
751 $usercfg = cfs_read_file("user.cfg");
752
753 PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid);
754
755 my $full_tokenid = PVE::AccessControl::join_tokenid($userid, $tokenid);
756 PVE::TokenConfig::delete_token($full_tokenid);
757 delete $usercfg->{users}->{$userid}->{tokens}->{$tokenid};
758
759 cfs_write_file("user.cfg", $usercfg);
760 };
761
762 PVE::AccessControl::lock_user_config($update_token, 'deleting token failed');
763
764 return;
765 }});
766 1;