]> git.proxmox.com Git - pve-access-control.git/blobdiff - PVE/API2/AccessControl.pm
pveum: add permissions sub-commands
[pve-access-control.git] / PVE / API2 / AccessControl.pm
index f3366968ce0f17aa8147045dc3f82b6bc6010cb1..5b63d2bcc55e323b8836358b100d64bca07b59f8 100644 (file)
@@ -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;
 
@@ -125,23 +126,32 @@ my $verify_auth = sub {
 my $create_ticket = sub {
     my ($rpcenv, $username, $pw_or_ticket, $otp) = @_;
 
-    my ($ticketuser, $u2fdata);
-    if (($ticketuser = PVE::AccessControl::verify_ticket($pw_or_ticket, 1)) &&
-       ($ticketuser eq 'root@pam' || $ticketuser eq $username)) {
+    my ($ticketuser, undef, $tfa_info) = PVE::AccessControl::verify_ticket($pw_or_ticket, 1);
+    if (defined($ticketuser) && ($ticketuser eq 'root@pam' || $ticketuser eq $username)) {
+       if (defined($tfa_info)) {
+           die "incomplete ticket\n";
+       }
        # valid ticket. Note: root@pam can create tickets for other users
     } else {
-       ($username, $u2fdata) = PVE::AccessControl::authenticate_user($username, $pw_or_ticket, $otp);
+       ($username, $tfa_info) = PVE::AccessControl::authenticate_user($username, $pw_or_ticket, $otp);
     }
 
     my %extra;
     my $ticket_data = $username;
-    if (defined($u2fdata)) {
-       my $u2f = get_u2f_instance($rpcenv, $u2fdata->@{qw(publicKey keyHandle)});
-       my $challenge = $u2f->auth_challenge()
-           or die "failed to get u2f challenge\n";
-       $challenge = decode_json($challenge);
-       $extra{U2FChallenge} = $challenge;
-       $ticket_data = "u2f!$username!$challenge->{challenge}";
+    if (defined($tfa_info)) {
+       $extra{NeedTFA} = 1;
+       if ($tfa_info->{type} eq 'u2f') {
+           my $u2finfo = $tfa_info->{data};
+           my $u2f = get_u2f_instance($rpcenv, $u2finfo->@{qw(publicKey keyHandle)});
+           my $challenge = $u2f->auth_challenge()
+               or die "failed to get u2f challenge\n";
+           $challenge = decode_json($challenge);
+           $extra{U2FChallenge} = $challenge;
+           $ticket_data = "u2f!$username!$challenge->{challenge}";
+       } else {
+           # General half-login / 'missing 2nd factor' ticket:
+           $ticket_data = "tfa!$username";
+       }
     }
 
     my $ticket = PVE::AccessControl::assemble_ticket($ticket_data);
@@ -224,6 +234,7 @@ __PACKAGE__->register_method ({
        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,
@@ -302,19 +313,11 @@ __PACKAGE__->register_method ({
        }
 
        $res->{cap} = &$compute_api_permission($rpcenv, $username)
-           if !defined($res->{U2FChallenge});
-
-       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 $@;
-           }
+           if !defined($res->{NeedTFA});
+
+       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'");
@@ -337,6 +340,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 password
     description => "Change user password.",
     parameters => {
        additionalProperties => 0,
@@ -388,7 +392,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;
 }
 
@@ -469,6 +472,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,
@@ -499,9 +503,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,
@@ -533,11 +535,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);
        }
@@ -565,7 +567,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);
@@ -580,7 +582,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";
@@ -595,6 +597,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,
@@ -616,27 +619,35 @@ __PACKAGE__->register_method({
        my ($param) = @_;
 
        my $rpcenv = PVE::RPCEnvironment::get();
-       my $challenge = $rpcenv->get_u2f_challenge()
-          or raise('no active challenge');
        my $authuser = $rpcenv->get_user();
        my ($username, undef, $realm) = PVE::AccessControl::verify_username($authuser);
 
-       my ($tfa_type, $u2fdata) = PVE::AccessControl::user_get_tfa($username, $realm);
-       if (!defined($tfa_type) || $tfa_type ne 'u2f') {
+       my ($tfa_type, $tfa_data) = PVE::AccessControl::user_get_tfa($username, $realm);
+       if (!defined($tfa_type)) {
            raise('no u2f data available');
        }
 
-       my $keyHandle = $u2fdata->{keyHandle};
-       my $publicKey = $u2fdata->{publicKey};
-       raise("incomplete u2f setup")
-           if !defined($keyHandle) || !defined($publicKey);
+       eval {
+           if ($tfa_type eq 'u2f') {
+               my $challenge = $rpcenv->get_u2f_challenge()
+                  or raise('no active challenge');
 
-       my $u2f = get_u2f_instance($rpcenv, $publicKey, $keyHandle);
-       $u2f->set_challenge($challenge);
+               my $keyHandle = $tfa_data->{keyHandle};
+               my $publicKey = $tfa_data->{publicKey};
+               raise("incomplete u2f setup")
+                   if !defined($keyHandle) || !defined($publicKey);
 
-       eval {
-           my ($counter, $present) = $u2f->auth_verify($param->{response});
-           # Do we want to do anything with these?
+               my $u2f = get_u2f_instance($rpcenv, $publicKey, $keyHandle);
+               $u2f->set_challenge($challenge);
+
+               my ($counter, $present) = $u2f->auth_verify($param->{response});
+               # Do we want to do anything with these?
+           } else {
+               # sanity check before handing off to the verification code:
+               my $keys = $tfa_data->{keys} or die "missing tfa keys\n";
+               my $config = $tfa_data->{config} or die "bad tfa entry\n";
+               PVE::AccessControl::verify_one_time_pw($tfa_type, $authuser, $keys, $config, $param->{response});
+           }
        };
        if (my $err = $@) {
            my $clientip = $rpcenv->get_client_ip() || '';
@@ -644,12 +655,66 @@ __PACKAGE__->register_method({
            die PVE::Exception->new("authentication failure\n", code => 401);
        }
 
-       # create a new ticket for the user:
-       my $ticket_data = "u2f!$authuser!verified";
        return {
-           ticket => PVE::AccessControl::assemble_ticket($ticket_data),
+           ticket => PVE::AccessControl::assemble_ticket($authuser),
            cap => &$compute_api_permission($rpcenv, $authuser),
        }
     }});
 
+__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;