use strict;
use warnings;
-use PVE::Exception qw(raise raise_perm_exc);
+use PVE::Exception qw(raise raise_perm_exc raise_param_exc);
use PVE::Cluster qw (cfs_read_file cfs_write_file);
-use PVE::Tools qw(split_list);
+use PVE::Tools qw(split_list extract_param);
use PVE::AccessControl;
use PVE::JSONSchema qw(get_standard_option register_standard_option);
+use PVE::TokenConfig;
use PVE::SafeSyslog;
optional => 1,
completion => \&PVE::AccessControl::complete_group,
});
+register_standard_option('token-subid', {
+ type => 'string',
+ pattern => $PVE::AccessControl::token_subid_regex,
+ description => 'User-specific token identifier.',
+});
+register_standard_option('token-expire', {
+ description => "API token expiration date (seconds since epoch). '0' means no expiration date.",
+ type => 'integer',
+ minimum => 0,
+ optional => 1,
+ default => 'same as user',
+});
+register_standard_option('token-privsep', {
+ description => "Restrict API token privileges with separate ACLs (default), or give full privileges of corresponding user.",
+ type => 'boolean',
+ optional => 1,
+ default => 1,
+});
+register_standard_option('token-comment', { type => 'string', optional => 1 });
+register_standard_option('token-info', {
+ type => 'object',
+ properties => {
+ expire => get_standard_option('token-expire'),
+ privsep => get_standard_option('token-privsep'),
+ comment => get_standard_option('token-comment'),
+ }
+});
+
+my $token_info_extend = sub {
+ my ($props) = @_;
+
+ my $obj = get_standard_option('token-info');
+ my $base_props = $obj->{properties};
+ $obj->{properties} = {};
+
+ foreach my $prop (keys %$base_props) {
+ $obj->{properties}->{$prop} = $base_props->{$prop};
+ }
+
+ foreach my $add_prop (keys %$props) {
+ $obj->{properties}->{$add_prop} = $props->{$add_prop};
+ }
+
+ return $obj;
+};
my $extract_user_data = sub {
my ($data, $full) = @_;
return $res if !$full;
$res->{groups} = $data->{groups} ? [ keys %{$data->{groups}} ] : [];
+ $res->{tokens} = $data->{tokens};
return $res;
};
type => 'boolean',
description => "Optional filter for enable property.",
optional => 1,
+ },
+ full => {
+ type => 'boolean',
+ description => "Include group and token information.",
+ optional => 1,
+ default => 0,
}
},
},
email => get_standard_option('user-email'),
comment => get_standard_option('user-comment'),
keys => get_standard_option('user-keys'),
+ groups => get_standard_option('group-list'),
+ tokens => {
+ type => 'array',
+ optional => 1,
+ items => $token_info_extend->({
+ tokenid => get_standard_option('token-subid'),
+ }),
+ }
},
},
links => [ { rel => 'child', href => "{userid}" } ],
my $allowed_users = $rpcenv->group_member_join([keys %$groups]);
foreach my $user (keys %{$usercfg->{users}}) {
-
if (!($canUserMod || $user eq $authuser)) {
next if !$allowed_users->{$user};
}
- my $entry = &$extract_user_data($usercfg->{users}->{$user});
+ my $entry = &$extract_user_data($usercfg->{users}->{$user}, $param->{full});
if (defined($param->{enabled})) {
next if $entry->{enable} && !$param->{enabled};
next if !$entry->{enable} && $param->{enabled};
}
+ $entry->{groups} = join(',', @{$entry->{groups}}) if $entry->{groups};
+ $entry->{tokens} = [ map { { tokenid => $_, %{$entry->{tokens}->{$_}} } } sort keys %{$entry->{tokens}} ]
+ if defined($entry->{tokens});
+
$entry->{userid} = $user;
push @$res, $entry;
}
code => sub {
my ($param) = @_;
- PVE::AccessControl::lock_user_config(
- sub {
+ PVE::AccessControl::lock_user_config(sub {
+ my ($username, $ruid, $realm) = PVE::AccessControl::verify_username($param->{userid});
- my ($username, $ruid, $realm) = PVE::AccessControl::verify_username($param->{userid});
-
- my $usercfg = cfs_read_file("user.cfg");
+ my $usercfg = cfs_read_file("user.cfg");
- die "user '$username' already exists\n"
- if $usercfg->{users}->{$username};
+ # ensure "user exists" check works for case insensitive realms
+ $username = PVE::AccessControl::lookup_username($username, 1);
+ die "user '$username' already exists\n" if $usercfg->{users}->{$username};
- PVE::AccessControl::domain_set_password($realm, $ruid, $param->{password})
- if defined($param->{password});
+ PVE::AccessControl::domain_set_password($realm, $ruid, $param->{password})
+ if defined($param->{password});
- my $enable = defined($param->{enable}) ? $param->{enable} : 1;
- $usercfg->{users}->{$username} = { enable => $enable };
- $usercfg->{users}->{$username}->{expire} = $param->{expire} if $param->{expire};
+ my $enable = defined($param->{enable}) ? $param->{enable} : 1;
+ $usercfg->{users}->{$username} = { enable => $enable };
+ $usercfg->{users}->{$username}->{expire} = $param->{expire} if $param->{expire};
- if ($param->{groups}) {
- foreach my $group (split_list($param->{groups})) {
- if ($usercfg->{groups}->{$group}) {
- PVE::AccessControl::add_user_group($username, $usercfg, $group);
- } else {
- die "no such group '$group'\n";
- }
+ if ($param->{groups}) {
+ foreach my $group (split_list($param->{groups})) {
+ if ($usercfg->{groups}->{$group}) {
+ PVE::AccessControl::add_user_group($username, $usercfg, $group);
+ } else {
+ die "no such group '$group'\n";
}
}
+ }
- $usercfg->{users}->{$username}->{firstname} = $param->{firstname} if $param->{firstname};
- $usercfg->{users}->{$username}->{lastname} = $param->{lastname} if $param->{lastname};
- $usercfg->{users}->{$username}->{email} = $param->{email} if $param->{email};
- $usercfg->{users}->{$username}->{comment} = $param->{comment} if $param->{comment};
- $usercfg->{users}->{$username}->{keys} = $param->{keys} if $param->{keys};
+ $usercfg->{users}->{$username}->{firstname} = $param->{firstname} if $param->{firstname};
+ $usercfg->{users}->{$username}->{lastname} = $param->{lastname} if $param->{lastname};
+ $usercfg->{users}->{$username}->{email} = $param->{email} if $param->{email};
+ $usercfg->{users}->{$username}->{comment} = $param->{comment} if $param->{comment};
+ $usercfg->{users}->{$username}->{keys} = $param->{keys} if $param->{keys};
- cfs_write_file("user.cfg", $usercfg);
- }, "create user failed");
+ cfs_write_file("user.cfg", $usercfg);
+ }, "create user failed");
return undef;
}});
email => get_standard_option('user-email'),
comment => get_standard_option('user-comment'),
keys => get_standard_option('user-keys'),
- groups => { type => 'array' },
+ groups => {
+ type => 'array',
+ optional => 1,
+ items => {
+ type => 'string',
+ format => 'pve-groupid',
+ },
+ },
+ tokens => {
+ optional => 1,
+ type => 'object',
+ },
},
type => "object"
},
$plugin->delete_user($cfg, $realm, $ruid);
}
+ # Remove TFA data before removing the user entry as the user entry tells us whether
+ # we need ot update priv/tfa.cfg.
+ PVE::AccessControl::user_set_tfa($userid, $realm, undef, undef, $usercfg, $domain_cfg);
+
delete $usercfg->{users}->{$userid};
PVE::AccessControl::delete_user_group($userid, $usercfg);
PVE::AccessControl::delete_user_acl($userid, $usercfg);
-
cfs_write_file("user.cfg", $usercfg);
}, "delete user failed");
return undef;
}});
+__PACKAGE__->register_method ({
+ name => 'read_user_tfa_type',
+ path => '{userid}/tfa',
+ method => 'GET',
+ protected => 1,
+ description => "Get user TFA types (Personal and Realm).",
+ permissions => {
+ check => [ 'or',
+ ['userid-param', 'self'],
+ ['userid-group', ['User.Modify', 'Sys.Audit']],
+ ],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ userid => get_standard_option('userid-completed'),
+ },
+ },
+ returns => {
+ additionalProperties => 0,
+ properties => {
+ realm => {
+ type => 'string',
+ enum => [qw(oath yubico)],
+ description => "The type of TFA the users realm has set, if any.",
+ optional => 1,
+ },
+ user => {
+ type => 'string',
+ enum => [qw(oath u2f)],
+ description => "The type of TFA the user has set, if any.",
+ optional => 1,
+ },
+ },
+ type => "object"
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my ($username, undef, $realm) = PVE::AccessControl::verify_username($param->{userid});
+
+
+ my $domain_cfg = cfs_read_file('domains.cfg');
+ my $realm_cfg = $domain_cfg->{ids}->{$realm};
+ die "auth domain '$realm' does not exist\n" if !$realm_cfg;
+
+ my $realm_tfa = {};
+ $realm_tfa = PVE::Auth::Plugin::parse_tfa_config($realm_cfg->{tfa})
+ if $realm_cfg->{tfa};
+
+ my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
+ my $tfa = $tfa_cfg->{users}->{$username};
+
+ my $res = {};
+ $res->{realm} = $realm_tfa->{type} if $realm_tfa->{type};
+ $res->{user} = $tfa->{type} if $tfa->{type};
+ return $res;
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'token_index',
+ path => '{userid}/token',
+ method => 'GET',
+ description => "Get user API tokens.",
+ permissions => {
+ check => ['or',
+ ['userid-param', 'self'],
+ ['perm', '/access/users/{userid}', ['User.Modify']],
+ ],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ userid => get_standard_option('userid-completed'),
+ },
+ },
+ returns => {
+ type => "array",
+ items => $token_info_extend->({
+ tokenid => get_standard_option('token-subid'),
+ }),
+ links => [ { rel => 'child', href => "{tokenid}" } ],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $userid = PVE::AccessControl::verify_username($param->{userid});
+ my $usercfg = cfs_read_file("user.cfg");
+
+ my $user = PVE::AccessControl::check_user_exist($usercfg, $userid);
+
+ my $tokens = $user->{tokens} // {};
+ return [ map { $tokens->{$_}->{tokenid} = $_; $tokens->{$_} } keys %$tokens];
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'read_token',
+ path => '{userid}/token/{tokenid}',
+ method => 'GET',
+ description => "Get specific API token information.",
+ permissions => {
+ check => ['or',
+ ['userid-param', 'self'],
+ ['perm', '/access/users/{userid}', ['User.Modify']],
+ ],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ userid => get_standard_option('userid-completed'),
+ tokenid => get_standard_option('token-subid'),
+ },
+ },
+ returns => get_standard_option('token-info'),
+ code => sub {
+ my ($param) = @_;
+
+ my $userid = PVE::AccessControl::verify_username($param->{userid});
+ my $tokenid = $param->{tokenid};
+
+ my $usercfg = cfs_read_file("user.cfg");
+
+ return PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid);
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'generate_token',
+ path => '{userid}/token/{tokenid}',
+ method => 'POST',
+ 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!",
+ protected => 1,
+ permissions => {
+ check => ['or',
+ ['userid-param', 'self'],
+ ['perm', '/access/users/{userid}', ['User.Modify']],
+ ],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ userid => get_standard_option('userid-completed'),
+ tokenid => get_standard_option('token-subid'),
+ expire => get_standard_option('token-expire'),
+ privsep => get_standard_option('token-privsep'),
+ comment => get_standard_option('token-comment'),
+ },
+ },
+ returns => {
+ additionalProperties => 0,
+ type => "object",
+ properties => {
+ info => get_standard_option('token-info'),
+ value => {
+ type => 'string',
+ description => 'API token value used for authentication.',
+ },
+ 'full-tokenid' => {
+ type => 'string',
+ format_description => '<userid>!<tokenid>',
+ description => 'The full token id.',
+ },
+ },
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $userid = PVE::AccessControl::verify_username(extract_param($param, 'userid'));
+ my $tokenid = extract_param($param, 'tokenid');
+
+ my $usercfg = cfs_read_file("user.cfg");
+
+ my $token = PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid, 1);
+ my ($full_tokenid, $value);
+
+ PVE::AccessControl::check_user_exist($usercfg, $userid);
+ raise_param_exc({ 'tokenid' => 'Token already exists.' }) if defined($token);
+
+ my $generate_and_add_token = sub {
+ $usercfg = cfs_read_file("user.cfg");
+ PVE::AccessControl::check_user_exist($usercfg, $userid);
+ die "Token already exists.\n" if defined(PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid, 1));
+
+ $full_tokenid = PVE::AccessControl::join_tokenid($userid, $tokenid);
+ $value = PVE::TokenConfig::generate_token($full_tokenid);
+
+ $token = {};
+ $token->{privsep} = defined($param->{privsep}) ? $param->{privsep} : 1;
+ $token->{expire} = $param->{expire} if defined($param->{expire});
+ $token->{comment} = $param->{comment} if $param->{comment};
+
+ $usercfg->{users}->{$userid}->{tokens}->{$tokenid} = $token;
+ cfs_write_file("user.cfg", $usercfg);
+ };
+
+ PVE::AccessControl::lock_user_config($generate_and_add_token, 'generating token failed');
+
+ return {
+ info => $token,
+ value => $value,
+ 'full-tokenid' => $full_tokenid,
+ };
+ }});
+
+
+__PACKAGE__->register_method ({
+ name => 'update_token_info',
+ path => '{userid}/token/{tokenid}',
+ method => 'PUT',
+ description => "Update API token for a specific user.",
+ protected => 1,
+ permissions => {
+ check => ['or',
+ ['userid-param', 'self'],
+ ['perm', '/access/users/{userid}', ['User.Modify']],
+ ],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ userid => get_standard_option('userid-completed'),
+ tokenid => get_standard_option('token-subid'),
+ expire => get_standard_option('token-expire'),
+ privsep => get_standard_option('token-privsep'),
+ comment => get_standard_option('token-comment'),
+ },
+ },
+ returns => get_standard_option('token-info', { description => "Updated token information." }),
+ code => sub {
+ my ($param) = @_;
+
+ my $userid = PVE::AccessControl::verify_username(extract_param($param, 'userid'));
+ my $tokenid = extract_param($param, 'tokenid');
+
+ my $usercfg = cfs_read_file("user.cfg");
+ my $token = PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid);
+
+ my $update_token = sub {
+ $usercfg = cfs_read_file("user.cfg");
+ $token = PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid);
+
+ my $full_tokenid = PVE::AccessControl::join_tokenid($userid, $tokenid);
+
+ $token->{privsep} = $param->{privsep} if defined($param->{privsep});
+ $token->{expire} = $param->{expire} if defined($param->{expire});
+ $token->{comment} = $param->{comment} if $param->{comment};
+
+ $usercfg->{users}->{$userid}->{tokens}->{$tokenid} = $token;
+ cfs_write_file("user.cfg", $usercfg);
+ };
+
+ PVE::AccessControl::lock_user_config($update_token, 'updating token info failed');
+
+ return $token;
+ }});
+
+
+__PACKAGE__->register_method ({
+ name => 'remove_token',
+ path => '{userid}/token/{tokenid}',
+ method => 'DELETE',
+ description => "Remove API token for a specific user.",
+ protected => 1,
+ permissions => {
+ check => ['or',
+ ['userid-param', 'self'],
+ ['perm', '/access/users/{userid}', ['User.Modify']],
+ ],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ userid => get_standard_option('userid-completed'),
+ tokenid => get_standard_option('token-subid'),
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $userid = PVE::AccessControl::verify_username(extract_param($param, 'userid'));
+ my $tokenid = extract_param($param, 'tokenid');
+
+ my $usercfg = cfs_read_file("user.cfg");
+ my $token = PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid);
+
+ my $update_token = sub {
+ $usercfg = cfs_read_file("user.cfg");
+
+ PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid);
+
+ my $full_tokenid = PVE::AccessControl::join_tokenid($userid, $tokenid);
+ PVE::TokenConfig::delete_token($full_tokenid);
+ delete $usercfg->{users}->{$userid}->{tokens}->{$tokenid};
+
+ cfs_write_file("user.cfg", $usercfg);
+ };
+
+ PVE::AccessControl::lock_user_config($update_token, 'deleting token failed');
+
+ return;
+ }});
1;