]> git.proxmox.com Git - pve-access-control.git/blobdiff - src/PVE/RPCEnvironment.pm
bump version to 8.1.4
[pve-access-control.git] / src / PVE / RPCEnvironment.pm
index 8aae0940e5157cb7380c29fa801aaeb1517b4891..e6683532c1f506aaaf688665fcdef50ba80e319a 100644 (file)
@@ -5,7 +5,7 @@ use warnings;
 
 use PVE::AccessControl;
 use PVE::Cluster;
-use PVE::Exception qw(raise raise_perm_exc);
+use PVE::Exception qw(raise raise_param_exc raise_perm_exc);
 use PVE::INotify;
 use PVE::ProcFSTools;
 use PVE::RESTEnvironment;
@@ -23,15 +23,18 @@ my $compile_acl_path = sub {
 
     return undef if !$cfg->{roles};
 
+    # permissions() has an early return for this case
     die "internal error" if $user eq 'root@pam';
 
     my $cache = $self->{aclcache};
     $cache->{$user} = {} if !$cache->{$user};
     my $data = $cache->{$user};
 
+    # permissions() will always prime the cache for the owning user
     my ($username, undef) = PVE::AccessControl::split_tokenid($user, 1);
     die "internal error" if $username && $username ne 'root@pam' && !defined($cache->{$username});
 
+    # resolve and cache roles of the current user/token for all pool ACL paths
     if (!$data->{poolroles}) {
        $data->{poolroles} = {};
 
@@ -52,44 +55,83 @@ my $compile_acl_path = sub {
        }
     }
 
+    # get roles of current user/token on checked path - this already handles
+    # propagation and NoAccess along the path
+    #
+    # hash mapping role name to propagation flag value, a key being defined
+    # means the role is set
     my $roles = PVE::AccessControl::roles($cfg, $user, $path);
 
     # apply roles inherited from pools
-    # Note: assume we do not want to propagate those privs
     if ($data->{poolroles}->{$path}) {
+       # NoAccess must not be trumped by pool ACLs
        if (!defined($roles->{NoAccess})) {
            if ($data->{poolroles}->{$path}->{NoAccess}) {
+               # but pool ACL NoAccess trumps regular ACL
                $roles = { 'NoAccess' => 0 };
            } else {
                foreach my $role (keys %{$data->{poolroles}->{$path}}) {
+                   # only use role from pool ACL if regular ACL didn't already
+                   # set it, and never set propagation for pool-derived ACLs
                    $roles->{$role} = 0 if !defined($roles->{$role});
                }
            }
        }
     }
 
+    # cache roles
     $data->{roles}->{$path} = $roles;
 
+    # derive privs from set roles - hash mapping privilege name to propagation
+    # flag value, a key being defined means the priv is set
     my $privs = {};
     foreach my $role (keys %$roles) {
        if (my $privset = $cfg->{roles}->{$role}) {
            foreach my $p (keys %$privset) {
-               $privs->{$p} = $roles->{$role};
+               # set priv '$p' to propagated iff any of the set roles
+               # containing it have the propagated flag set
+               $privs->{$p} ||= $roles->{$role};
            }
        }
     }
 
+    # intersect user and token permissions
     if ($username && $username ne 'root@pam') {
-       # intersect user and token permissions
+       # map of set privs to their propagation flag value, for the owning user
        my $user_privs = $cache->{$username}->{privs}->{$path};
-       $privs = { map { $_ => $user_privs->{$_} && $privs->{$_} } keys %$privs };
+       # list of privs set both for token and owning user
+       my $filtered_privs = [ grep { defined($user_privs->{$_}) } keys %$privs ];
+       # intersection of privs using filtered list, combining both propagation
+       # flags
+       $privs = { map { $_ => $user_privs->{$_} && $privs->{$_} } @$filtered_privs };
     }
 
+    foreach my $priv (keys %$privs) {
+       # safeguard, this should never happen anyway
+       delete $privs->{$priv} if !defined($privs->{$priv});
+    }
+
+    # cache privs
     $data->{privs}->{$path} = $privs;
 
     return $privs;
 };
 
+# this is the method used by permission check helpers below
+#
+# returned value is a hash mapping all set privileges on $path to their
+# respective propagation flag. the propagation flag is informational only -
+# actual propagation is handled in PVE::AccessControl::roles(). to determine
+# whether a privilege is set, check for definedness in the returned hash.
+#
+# compiled ACLs are cached, so repeated checks for the same path and user are
+# almost free.
+#
+# if $user is a tokenid, permissions are calculated depending on the
+# privilege-separation flag value:
+# - non-priv-separated: permissions for owning user are returned
+# - priv-separated: permissions for owning user are calculated and intersected
+#   with those of token
 sub permissions {
     my ($self, $user, $path) = @_;
 
@@ -98,6 +140,12 @@ sub permissions {
        return { map { $_ => 1 } keys %{$cfg->{roles}->{'Administrator'}} };
     }
 
+    if (!defined($path)) {
+       # this shouldn't happen!
+       warn "internal error: ACL check called for undefined ACL path!\n";
+       return {};
+    }
+
     if (PVE::AccessControl::pve_verify_tokenid($user, 1)) {
        my ($username, $token) = PVE::AccessControl::split_tokenid($user);
        my $cfg = $self->{user_cfg};
@@ -138,14 +186,20 @@ sub compute_api_permission {
        storage => qr/Datastore\.|Permissions\.Modify/,
        nodes => qr/Sys\.|Permissions\.Modify/,
        sdn => qr/SDN\.|Permissions\.Modify/,
-       dc => qr/Sys\.Audit|SDN\./,
+       dc => qr/Sys\.Audit|Sys\.Modify|SDN\./,
+       mapping => qr/Mapping\.|Permissions.Modify/,
     };
     map { $res->{$_} = {} } keys %$priv_re_map;
 
-    my $required_paths = ['/', '/nodes', '/access/groups', '/vms', '/storage', '/sdn'];
+    my $required_paths = ['/', '/nodes', '/access/groups', '/vms', '/storage', '/sdn', '/mapping'];
+    my $defined_paths = [];
+    PVE::AccessControl::iterate_acl_tree("/", $usercfg->{acl_root}, sub {
+       my ($path, $node) = @_;
+       push @$defined_paths, $path;
+    });
 
     my $checked_paths = {};
-    foreach my $path (@$required_paths, keys %{$usercfg->{acl}}) {
+    foreach my $path (@$required_paths, @$defined_paths) {
        next if $checked_paths->{$path};
        $checked_paths->{$path} = 1;
 
@@ -154,6 +208,8 @@ sub compute_api_permission {
        my $toplevel = ($path =~ /^\/(\w+)/) ? $1 : 'dc';
        if ($toplevel eq 'pool') {
            foreach my $priv (keys %$path_perm) {
+               next if !defined($path_perm->{$priv});
+
                if ($priv =~ m/^VM\./) {
                    $res->{vms}->{$priv} = 1;
                } elsif ($priv =~ m/^Datastore\./) {
@@ -166,6 +222,8 @@ sub compute_api_permission {
        } else {
            my $priv_regex = $priv_re_map->{$toplevel} // next;
            foreach my $priv (keys %$path_perm) {
+               next if !defined($path_perm->{$priv});
+
                next if $priv !~ m/^($priv_regex)/;
                $res->{$toplevel}->{$priv} = 1;
            }
@@ -184,7 +242,8 @@ sub get_effective_permissions {
        '/access' => 1,
        '/access/groups' => 1,
        '/nodes' => 1,
-       '/pools' => 1,
+       '/pool' => 1,
+       '/sdn' => 1,
        '/storage' => 1,
        '/vms' => 1,
     };
@@ -192,9 +251,10 @@ sub get_effective_permissions {
     my $cfg = $self->{user_cfg};
 
     # paths explicitly listed in ACLs
-    foreach my $acl_path (keys %{$cfg->{acl}}) {
-       $paths->{$acl_path} = 1;
-    }
+    PVE::AccessControl::iterate_acl_tree("/", $cfg->{acl_root}, sub {
+       my ($path, $node) = @_;
+       $paths->{$path} = 1;
+    });
 
     # paths referenced by pool definitions
     foreach my $pool (keys %{$cfg->{pools}}) {
@@ -210,6 +270,9 @@ sub get_effective_permissions {
     my $perms = {};
     foreach my $path (keys %$paths) {
        my $path_perms = $self->permissions($user, $path);
+       foreach my $priv (keys %$path_perms) {
+           delete $path_perms->{$priv} if !defined($path_perms->{$priv});
+       }
        # filter paths where user has NO permissions
        $perms->{$path} = $path_perms if %$path_perms;
     }
@@ -262,6 +325,31 @@ sub check_full {
     }
 }
 
+# check for any fashion of access to vnet/bridge
+sub check_sdn_bridge {
+    my ($self, $username, $zone, $bridge, $privs, $noerr) = @_;
+
+    my $path = "/sdn/zones/$zone/$bridge";
+    # check access to bridge itself
+    return 1 if $self->check_any($username, $path, $privs, 1);
+
+    my $cfg = $self->{user_cfg};
+    my $bridge_acl = PVE::AccessControl::find_acl_tree_node($cfg->{acl_root}, $path);
+    if ($bridge_acl) {
+       # check access to VLANs
+       my $vlans = $bridge_acl->{children};
+       for my $vlan (keys %$vlans) {
+           my $vlanpath = "$path/$vlan";
+           return 1 if $self->check_any($username, $vlanpath, $privs, 1);
+       }
+    }
+
+    # repeat check, but fatal
+    $self->check_any($username, $path, $privs, 0) if !$noerr;
+
+    return;
+}
+
 sub check_user_enabled {
     my ($self, $user, $noerr) = @_;
 
@@ -389,22 +477,32 @@ sub exec_api2_perm_check {
            raise_perm_exc();
        }
        my $path = PVE::Tools::template_replace($tmplpath, $param);
-       $path = PVE::AccessControl::normalize_path($path);
-       return $self->check_full($username, $path, $privs, $any, $noerr);
+       my $normpath = PVE::AccessControl::normalize_path($path);
+       warn "Failed to normalize '$path'\n" if !defined($normpath) && defined($path);
+
+       return $self->check_full($username, $normpath, $privs, $any, $noerr);
     } elsif ($test eq 'userid-group') {
        my $userid = $param->{userid};
        my ($t, $privs, %options) = @$check;
-       return 0 if !$options{groups_param} && !$self->check_user_exist($userid, $noerr);
+
+       my $check_existing_user = !$options{groups_param} || $options{groups_param} ne 'create';
+       return 0 if $check_existing_user && !$self->check_user_exist($userid, $noerr);
+
+       # check permission for ALL groups (and thus ALL users)
        if (!$self->check_any($username, "/access/groups", $privs, 1)) {
+           # list of groups $username has any of $privs on
            my $groups = $self->filter_groups($username, $privs, 1);
            if ($options{groups_param}) {
+               # does $username have any of $privs on all new/updated/.. groups?
                my @group_param = PVE::Tools::split_list($param->{groups});
                raise_perm_exc("/access/groups, " . join("|", @$privs)) if !scalar(@group_param);
                foreach my $pg (@group_param) {
                    raise_perm_exc("/access/groups/$pg, " . join("|", @$privs))
                        if !$groups->{$pg};
                }
-           } else {
+           }
+           if ($check_existing_user) {
+               # does $username have any of $privs on any existing group of $userid
                my $allowed_users = $self->group_member_join([keys %$groups]);
                if (!$allowed_users->{$userid}) {
                    return 0 if $noerr;
@@ -428,12 +526,13 @@ sub exec_api2_perm_check {
        } else {
            die "unknown userid-param test";
        }
-     } elsif ($test eq 'perm-modify') {
+    } elsif ($test eq 'perm-modify') {
        my ($t, $tmplpath) = @$check;
        my $path = PVE::Tools::template_replace($tmplpath, $param);
        $path = PVE::AccessControl::normalize_path($path);
+       return 0 if !defined($path); # should already die in API2::ACL
        return $self->check_perm_modify($username, $path, $noerr);
-   } else {
+    } else {
        die "unknown permission test";
     }
 };
@@ -524,7 +623,7 @@ sub init_request {
     }
 }
 
-# hacks: to provide better backwards compatibiliy
+# hacks: to provide better backwards compatibility
 
 # old code uses PVE::RPCEnvironment::get();
 # new code should use PVE::RPCEnvironment->get();
@@ -538,4 +637,37 @@ sub is_worker {
     return PVE::RESTEnvironment->is_worker();
 }
 
+# Permission helper for TFA and password API endpoints modifying users.
+# Only root may modify root, regular users need to specify their password.
+#
+# Returns the same as `verify_username` in list context (userid, ruid, realm),
+# or just the userid in scalar context.
+sub reauth_user_for_user_modification : prototype($$$$;$) {
+    my ($rpcenv, $authuser, $userid, $password, $param_name) = @_;
+
+    $param_name //= 'password';
+
+    ($userid, my $ruid, my $realm) = PVE::AccessControl::verify_username($userid);
+    $rpcenv->check_user_exist($userid);
+
+    raise_perm_exc() if $userid eq 'root@pam' && $authuser ne 'root@pam';
+
+    # Regular users need to confirm their password to change TFA settings.
+    if ($authuser ne 'root@pam') {
+       raise_param_exc({ $param_name => 'password is required to modify user' })
+           if !defined($password);
+
+       ($authuser, my $auth_username, my $auth_realm) =
+           PVE::AccessControl::verify_username($authuser);
+
+       my $domain_cfg = PVE::Cluster::cfs_read_file('domains.cfg');
+       my $cfg = $domain_cfg->{ids}->{$auth_realm};
+       die "auth domain '$auth_realm' does not exist\n" if !$cfg;
+       my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
+       $plugin->authenticate_user($cfg, $auth_realm, $auth_username, $password);
+    }
+
+    return wantarray ? ($userid, $ruid, $realm) : $userid;
+}
+
 1;