X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=PVE%2FAPI2%2FUser.pm;h=05de57f37d4155078b30cfd646bb7457732a5940;hb=54d312f350a90cf2cb6c62cdc43009881e9c7cf3;hp=4c859dc2e66081055a3450528a9a52729081d1e6;hpb=af5d7da7f10abf98a83a74c2a498c24a75aeeed0;p=pve-access-control.git diff --git a/PVE/API2/User.pm b/PVE/API2/User.pm index 4c859dc..05de57f 100644 --- a/PVE/API2/User.pm +++ b/PVE/API2/User.pm @@ -2,11 +2,12 @@ package PVE::API2::User; 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; @@ -40,6 +41,51 @@ register_standard_option('group-list', { 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) = @_; @@ -53,6 +99,7 @@ my $extract_user_data = sub { return $res if !$full; $res->{groups} = $data->{groups} ? [ keys %{$data->{groups}} ] : []; + $res->{tokens} = $data->{tokens}; return $res; }; @@ -73,6 +120,12 @@ __PACKAGE__->register_method ({ type => 'boolean', description => "Optional filter for enable property.", optional => 1, + }, + full => { + type => 'boolean', + description => "Include group and token information.", + optional => 1, + default => 0, } }, }, @@ -89,6 +142,14 @@ __PACKAGE__->register_method ({ 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}" } ], @@ -108,18 +169,21 @@ __PACKAGE__->register_method ({ 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; } @@ -165,41 +229,40 @@ __PACKAGE__->register_method ({ 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; }}); @@ -228,7 +291,18 @@ __PACKAGE__->register_method ({ 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" }, @@ -355,15 +429,319 @@ __PACKAGE__->register_method ({ $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 => '!', + 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;