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