X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=PVE%2FAPI2%2FAccessControl.pm;h=a77694b7561d64f080db9c4c26e22d6ffd7b0f1d;hb=eb41d2005159da16a8b9f50a5f972fdb029ee09e;hp=2caa4afac9897cc31130826900df37ab6b7aef0b;hpb=f25628d3efbf04553f45d2fdbb9819102ea75464;p=pve-access-control.git diff --git a/PVE/API2/AccessControl.pm b/PVE/API2/AccessControl.pm index 2caa4af..a77694b 100644 --- a/PVE/API2/AccessControl.pm +++ b/PVE/API2/AccessControl.pm @@ -6,11 +6,11 @@ use warnings; use JSON; use MIME::Base64; -use PVE::Exception qw(raise raise_perm_exc); +use PVE::Exception qw(raise raise_perm_exc raise_param_exc); use PVE::SafeSyslog; use PVE::RPCEnvironment; use PVE::Cluster qw(cfs_read_file); -use PVE::Corosync; +use PVE::DataCenterConfig; use PVE::RESTHandler; use PVE::AccessControl; use PVE::JSONSchema qw(get_standard_option); @@ -19,6 +19,7 @@ use PVE::API2::User; use PVE::API2::Group; use PVE::API2::Role; use PVE::API2::ACL; +use PVE::Auth::Plugin; use PVE::OTP; use PVE::Tools; @@ -31,36 +32,36 @@ eval { use base qw(PVE::RESTHandler); __PACKAGE__->register_method ({ - subclass => "PVE::API2::User", + subclass => "PVE::API2::User", path => 'users', }); __PACKAGE__->register_method ({ - subclass => "PVE::API2::Group", + subclass => "PVE::API2::Group", path => 'groups', }); __PACKAGE__->register_method ({ - subclass => "PVE::API2::Role", + subclass => "PVE::API2::Role", path => 'roles', }); __PACKAGE__->register_method ({ - subclass => "PVE::API2::ACL", + subclass => "PVE::API2::ACL", path => 'acl', }); __PACKAGE__->register_method ({ - subclass => "PVE::API2::Domains", + subclass => "PVE::API2::Domains", path => 'domains', }); __PACKAGE__->register_method ({ - name => 'index', - path => '', + name => 'index', + path => '', method => 'GET', description => "Directory index.", - permissions => { + permissions => { user => 'all', }, parameters => { @@ -79,7 +80,7 @@ __PACKAGE__->register_method ({ }, code => sub { my ($param) = @_; - + my $res = []; my $ma = __PACKAGE__->method_attributes(); @@ -175,11 +176,12 @@ my $compute_api_permission = sub { access => qr/(User|Group)\.|Permissions\.Modify/, storage => qr/Datastore\.|Permissions\.Modify/, nodes => qr/Sys\.|Permissions\.Modify/, - dc => qr/Sys\.Audit/, + sdn => qr/SDN\.|Permissions\.Modify/, + dc => qr/Sys\.Audit|SDN\./, }; map { $res->{$_} = {} } keys %$priv_re_map; - my $required_paths = ['/', '/nodes', '/access/groups', '/vms', '/storage']; + my $required_paths = ['/', '/nodes', '/access/groups', '/vms', '/storage', '/sdn']; my $checked_paths = {}; foreach my $path (@$required_paths, keys %{$usercfg->{acl}}) { @@ -213,8 +215,8 @@ my $compute_api_permission = sub { }; __PACKAGE__->register_method ({ - name => 'get_ticket', - path => 'ticket', + name => 'get_ticket', + path => 'ticket', method => 'GET', permissions => { user => 'world' }, description => "Dummy. Useful for formatters which want to provide a login page.", @@ -223,16 +225,17 @@ __PACKAGE__->register_method ({ }, returns => { type => "null" }, code => sub { return undef; }}); - + __PACKAGE__->register_method ({ - name => 'create_ticket', - path => 'ticket', + name => 'create_ticket', + path => 'ticket', method => 'POST', - permissions => { + permissions => { description => "You need to pass valid credientials.", - user => 'world' + user => 'world' }, protected => 1, # else we can't access shadow files + allowtoken => 0, # we don't want tokens to create tickets description => "Create or verify authentication ticket.", parameters => { additionalProperties => 0, @@ -248,7 +251,7 @@ __PACKAGE__->register_method ({ optional => 1, completion => \&PVE::AccessControl::complete_realm, }), - password => { + password => { description => "The secret password. This can also be a valid ticket.", type => 'string', }, @@ -264,7 +267,7 @@ __PACKAGE__->register_method ({ optional => 1, maxLength => 64, }, - privs => { + privs => { description => "Verify ticket, and check if user have access 'privs' on 'path'", type => 'string' , format => 'pve-priv-list', requires => 'path', @@ -285,10 +288,11 @@ __PACKAGE__->register_method ({ }, code => sub { my ($param) = @_; - + my $username = $param->{username}; $username .= "\@$param->{realm}" if $param->{realm}; + $username = PVE::AccessControl::lookup_username($username); my $rpcenv = PVE::RPCEnvironment::get(); my $res; @@ -313,17 +317,9 @@ __PACKAGE__->register_method ({ $res->{cap} = &$compute_api_permission($rpcenv, $username) if !defined($res->{NeedTFA}); - if (PVE::Corosync::check_conf_exists(1)) { - if ($rpcenv->check($username, '/', ['Sys.Audit'], 1)) { - eval { - my $conf = cfs_read_file('corosync.conf'); - my $totem = PVE::Corosync::totem_config($conf); - if ($totem->{cluster_name}) { - $res->{clustername} = $totem->{cluster_name}; - } - }; - warn "$@\n" if $@; - } + my $clinfo = PVE::Cluster::get_clinfo(); + if ($clinfo->{cluster}->{name} && $rpcenv->check($username, '/', ['Sys.Audit'], 1)) { + $res->{clustername} = $clinfo->{cluster}->{name}; } PVE::Cluster::log_msg('info', 'root@pam', "successful auth for user '$username'"); @@ -333,11 +329,11 @@ __PACKAGE__->register_method ({ __PACKAGE__->register_method ({ name => 'change_password', - path => 'password', + path => 'password', method => 'PUT', - permissions => { + permissions => { description => "Each user is allowed to change his own password. A user can change the password of another user if he has 'Realm.AllocateUser' (on the realm of user ) and 'User.Modify' permission on /access/groups/ on a group where user is member of.", - check => [ 'or', + check => [ 'or', ['userid-param', 'self'], [ 'and', [ 'userid-param', 'Realm.AllocateUser'], @@ -346,15 +342,16 @@ __PACKAGE__->register_method ({ ], }, protected => 1, # else we can't access shadow files + allowtoken => 0, # we don't want tokens to change the regular user password description => "Change user password.", parameters => { additionalProperties => 0, properties => { userid => get_standard_option('userid-completed'), - password => { + password => { description => "The new password.", type => 'string', - minLength => 5, + minLength => 5, maxLength => 64, }, } @@ -397,7 +394,6 @@ sub get_u2f_config() { my $dc = cfs_read_file('datacenter.cfg'); my $u2f = $dc->{u2f}; die "u2f not configured in datacenter.cfg\n" if !$u2f; - $u2f = PVE::JSONSchema::parse_property_string($PVE::Cluster::u2f_format, $u2f); return $u2f; } @@ -478,6 +474,7 @@ __PACKAGE__->register_method ({ ], }, protected => 1, # else we can't access shadow files + allowtoken => 0, # we don't want tokens to change the regular user's TFA settings description => "Change user u2f authentication.", parameters => { additionalProperties => 0, @@ -508,9 +505,7 @@ __PACKAGE__->register_method ({ optional => 1, description => 'When adding TOTP, the shared secret value.', type => 'string', - # This is what pve-common's PVE::OTP::oath_verify_otp accepts. - # Should we move this to pve-common's JSONSchema as a named format? - pattern => qr/[A-Z2-7=]{16}|[A-Fa-f0-9]{40}/, + format => 'pve-tfa-secret', }, config => { optional => 1, @@ -542,11 +537,11 @@ __PACKAGE__->register_method ({ # Regular users need to confirm their password to change u2f settings. if ($authuser ne 'root@pam') { - raise_param_exc('password' => 'password is required to modify u2f data') + raise_param_exc({ 'password' => 'password is required to modify u2f data' }) if !defined($password); my $domain_cfg = cfs_read_file('domains.cfg'); my $cfg = $domain_cfg->{ids}->{$realm}; - die "auth domain '$realm' does not exists\n" if !$cfg; + die "auth domain '$realm' does not exist\n" if !$cfg; my $plugin = PVE::Auth::Plugin->lookup($cfg->{type}); $plugin->authenticate_user($cfg, $realm, $ruid, $password); } @@ -574,7 +569,7 @@ __PACKAGE__->register_method ({ return $challenge; } } elsif ($action eq 'confirm') { - raise_param_exc('response' => "confirm action requires the 'response' parameter to be set") + raise_param_exc({ 'response' => "confirm action requires the 'response' parameter to be set" }) if !defined($response); my ($type, $u2fdata) = PVE::AccessControl::user_get_tfa($userid, $realm); @@ -589,7 +584,7 @@ __PACKAGE__->register_method ({ my ($keyHandle, $publicKey) = $u2f->registration_verify($response); PVE::AccessControl::user_set_tfa($userid, $realm, 'u2f', { keyHandle => $keyHandle, - publicKey => encode_base64($publicKey, ''), + publicKey => $publicKey, # already base64 encoded }); } else { die "invalid action: $action\n"; @@ -604,6 +599,7 @@ __PACKAGE__->register_method({ method => 'POST', permissions => { user => 'all' }, protected => 1, # else we can't access shadow files + allowtoken => 0, # we don't want tokens to access TFA information description => 'Finish a u2f challenge.', parameters => { additionalProperties => 0, @@ -667,4 +663,60 @@ __PACKAGE__->register_method({ } }}); +__PACKAGE__->register_method({ + name => 'permissions', + path => 'permissions', + method => 'GET', + description => 'Retrieve effective permissions of given user/token.', + permissions => { + description => "Each user/token is allowed to dump their own permissions. A user can dump the permissions of another user if they have 'Sys.Audit' permission on /access.", + user => 'all', + }, + parameters => { + additionalProperties => 0, + properties => { + userid => { + type => 'string', + description => "User ID or full API token ID", + pattern => $PVE::AccessControl::userid_or_token_regex, + optional => 1, + }, + path => get_standard_option('acl-path', { + description => "Only dump this specific path, not the whole tree.", + optional => 1, + }), + }, + }, + returns => { + type => 'object', + description => 'Map of "path" => (Map of "privilege" => "propagate boolean").', + }, + code => sub { + my ($param) = @_; + + my $rpcenv = PVE::RPCEnvironment::get(); + + my $userid = $param->{userid}; + if (defined($userid)) { + $rpcenv->check($rpcenv->get_user(), '/access', ['Sys.Audit']); + } else { + $userid = $rpcenv->get_user(); + } + + my $res; + + if (my $path = $param->{path}) { + my $perms = $rpcenv->permissions($userid, $path); + if ($perms) { + $res = { $path => $perms }; + } else { + $res = {}; + } + } else { + $res = $rpcenv->get_effective_permissions($userid); + } + + return $res; + }}); + 1;