X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=PVE%2FAccessControl.pm;h=a3990de08273220f6e2450d023965292c821096d;hb=084c149a48efcbca465f75a3a719efca180e0292;hp=e3f90eefbf6256ec3f37f033d30af31f848a245b;hpb=51e6f56d257d823664fd3a68d8a164e41c949a66;p=pve-access-control.git diff --git a/PVE/AccessControl.pm b/PVE/AccessControl.pm index e3f90ee..a3990de 100644 --- a/PVE/AccessControl.pm +++ b/PVE/AccessControl.pm @@ -211,11 +211,60 @@ sub rotate_authkey { die $@ if $@; } +PVE::JSONSchema::register_standard_option('tokenid', { + description => "API token identifier.", + type => "string", + format => "pve-tokenid", +}); + +our $token_subid_regex = $PVE::Auth::Plugin::realm_regex; + +# username@realm username realm tokenid +our $token_full_regex = qr/((${PVE::Auth::Plugin::user_regex})\@(${PVE::Auth::Plugin::realm_regex}))!(${token_subid_regex})/; + +our $userid_or_token_regex = qr/^$PVE::Auth::Plugin::user_regex\@$PVE::Auth::Plugin::realm_regex(?:!$token_subid_regex)?$/; + +sub split_tokenid { + my ($tokenid, $noerr) = @_; + + if ($tokenid =~ /^${token_full_regex}$/) { + return ($1, $4); + } + + die "'$tokenid' is not a valid token ID - not able to split into user and token parts\n" if !$noerr; + + return undef; +} + +sub join_tokenid { + my ($username, $tokensubid) = @_; + + my $joined = "${username}!${tokensubid}"; + + return pve_verify_tokenid($joined); +} + +PVE::JSONSchema::register_format('pve-tokenid', \&pve_verify_tokenid); +sub pve_verify_tokenid { + my ($tokenid, $noerr) = @_; + + if ($tokenid =~ /^${token_full_regex}$/) { + return wantarray ? ($tokenid, $2, $3, $4) : $tokenid; + } + + die "value '$tokenid' does not look like a valid token ID\n" if !$noerr; + + return undef; +} + + my $csrf_prevention_secret; +my $csrf_prevention_secret_legacy; my $get_csrfr_secret = sub { if (!$csrf_prevention_secret) { my $input = PVE::Tools::file_get_contents($pve_www_key_fn); $csrf_prevention_secret = Digest::SHA::hmac_sha256_base64($input); + $csrf_prevention_secret_legacy = Digest::SHA::sha1_base64($input); } return $csrf_prevention_secret; }; @@ -231,7 +280,16 @@ sub assemble_csrf_prevention_token { sub verify_csrf_prevention_token { my ($username, $token, $noerr) = @_; - my $secret = &$get_csrfr_secret(); + my $secret = $get_csrfr_secret->(); + + # FIXME: remove with PVE 7 and/or refactor all into PVE::Ticket ? + if ($token =~ m/^([A-Z0-9]{8}):(\S+)$/) { + my $sig = $2; + if (length($sig) == 27) { + # the legacy secret got populated by above get_csrfr_secret call + $secret = $csrf_prevention_secret_legacy; + } + } return PVE::Ticket::verify_csrf_prevention_token( $secret, $username, $token, -300, $ticket_lifetime, $noerr); @@ -341,6 +399,39 @@ sub verify_ticket { return wantarray ? ($username, $age, $tfa_info) : $username; } +sub verify_token { + my ($api_token) = @_; + + die "no API token specified\n" if !$api_token; + + my ($tokenid, $value); + if ($api_token =~ /^(.*)=(.*)$/) { + $tokenid = $1; + $value = $2; + } else { + die "no tokenid specified\n"; + } + + my ($username, $token) = split_tokenid($tokenid); + + my $usercfg = cfs_read_file('user.cfg'); + check_user_enabled($usercfg, $username); + check_token_exist($usercfg, $username, $token); + + my $ctime = time(); + + my $user = $usercfg->{users}->{$username}; + die "account expired\n" if $user->{expire} && ($user->{expire} < $ctime); + + my $token_info = $user->{tokens}->{$token}; + die "token expired\n" if $token_info->{expire} && ($token_info->{expire} < $ctime); + + die "invalid token value!\n" if !PVE::Cluster::verify_token($tokenid, $value); + + return wantarray ? ($tokenid) : $tokenid; +} + + # VNC tickets # - they do not contain the username in plain text # - they are restricted to a specific resource path (example: '/vms/100') @@ -481,6 +572,20 @@ sub check_user_enabled { return undef; } +sub check_token_exist { + my ($usercfg, $username, $tokenid, $noerr) = @_; + + my $user = check_user_exist($usercfg, $username, $noerr); + return undef if !$user; + + return $user->{tokens}->{$tokenid} + if defined($user->{tokens}) && $user->{tokens}->{$tokenid}; + + die "no such token '$tokenid' for user '$username'\n" if !$noerr; + + return undef; +} + sub verify_one_time_pw { my ($type, $username, $keys, $tfa_cfg, $otp) = @_; @@ -522,7 +627,7 @@ sub authenticate_user { 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); @@ -672,6 +777,16 @@ my $privgroups = { 'Datastore.Audit', ], }, + SDN => { + root => [], + admin => [ + 'SDN.Allocate', + 'SDN.Audit', + ], + audit => [ + 'SDN.Audit', + ], + }, User => { root => [ 'Realm.Allocate', @@ -947,13 +1062,15 @@ sub parse_user_config { if (defined ($valid_privs->{$priv})) { $cfg->{roles}->{$role}->{$priv} = 1; } else { - warn "user config - ignore invalid priviledge '$priv'\n"; + warn "user config - ignore invalid privilege '$priv'\n"; } } } elsif ($et eq 'acl') { my ($propagate, $pathtxt, $uglist, $rolelist) = @data; + $propagate = $propagate ? 1 : 0; + if (my $path = normalize_path($pathtxt)) { foreach my $role (split_list($rolelist)) { @@ -963,8 +1080,9 @@ sub parse_user_config { } foreach my $ug (split_list($uglist)) { - if ($ug =~ m/^@(\S+)$/) { - my $group = $1; + my ($group) = $ug =~ m/^@(\S+)$/; + + if ($group && verify_groupname($group, 1)) { if ($cfg->{groups}->{$group}) { # group exists $cfg->{acl}->{$path}->{groups}->{$group}->{$role} = $propagate; } else { @@ -976,6 +1094,12 @@ sub parse_user_config { } else { warn "user config - ignore invalid acl member '$ug'\n"; } + } elsif (my ($user, $token) = split_tokenid($ug, 1)) { + if (check_token_exist($cfg, $user, $token, 1)) { + $cfg->{acl}->{$path}->{tokens}->{$ug}->{$role} = $propagate; + } else { + warn "user config - ignore invalid acl token '$ug'\n"; + } } else { warn "user config - invalid user/group '$ug' in acl\n"; } @@ -1022,6 +1146,34 @@ sub parse_user_config { } $cfg->{pools}->{$pool}->{storage}->{$storeid} = 1; } + } elsif ($et eq 'token') { + my ($tokenid, $expire, $privsep, $comment) = @data; + + my ($user, $token) = split_tokenid($tokenid, 1); + if (!($user && $token)) { + warn "user config - ignore invalid tokenid '$tokenid'\n"; + next; + } + + $privsep = $privsep ? 1 : 0; + + $expire = 0 if !$expire; + + if ($expire !~ m/^\d+$/) { + warn "user config - ignore token '$tokenid' - (illegal characters in expire '$expire')\n"; + next; + } + $expire = int($expire); + + if (my $user_cfg = $cfg->{users}->{$user}) { # user exists + $user_cfg->{tokens}->{$token} = {} if !$user_cfg->{tokens}->{$token}; + my $token_cfg = $user_cfg->{tokens}->{$token}; + $token_cfg->{privsep} = $privsep; + $token_cfg->{expire} = $expire; + $token_cfg->{comment} = PVE::Tools::decode_text($comment) if $comment; + } else { + warn "user config - ignore token '$tokenid' - user does not exist\n"; + } } else { warn "user config - ignore config line: $line\n"; } @@ -1037,7 +1189,7 @@ sub write_user_config { my $data = ''; - foreach my $user (keys %{$cfg->{users}}) { + foreach my $user (sort keys %{$cfg->{users}}) { my $d = $cfg->{users}->{$user}; my $firstname = $d->{firstname} ? PVE::Tools::encode_text($d->{firstname}) : ''; my $lastname = $d->{lastname} ? PVE::Tools::encode_text($d->{lastname}) : ''; @@ -1047,49 +1199,59 @@ sub write_user_config { my $enable = $d->{enable} ? 1 : 0; my $keys = $d->{keys} ? $d->{keys} : ''; $data .= "user:$user:$enable:$expire:$firstname:$lastname:$email:$comment:$keys:\n"; + + my $user_tokens = $d->{tokens}; + foreach my $token (sort keys %$user_tokens) { + my $td = $user_tokens->{$token}; + my $full_tokenid = join_tokenid($user, $token); + my $comment = $td->{comment} ? PVE::Tools::encode_text($td->{comment}) : ''; + my $expire = int($td->{expire} || 0); + my $privsep = $td->{privsep} ? 1 : 0; + $data .= "token:$full_tokenid:$expire:$privsep:$comment:\n"; + } } $data .= "\n"; - foreach my $group (keys %{$cfg->{groups}}) { + foreach my $group (sort keys %{$cfg->{groups}}) { my $d = $cfg->{groups}->{$group}; - my $list = join (',', keys %{$d->{users}}); + my $list = join (',', sort keys %{$d->{users}}); my $comment = $d->{comment} ? PVE::Tools::encode_text($d->{comment}) : ''; $data .= "group:$group:$list:$comment:\n"; } $data .= "\n"; - foreach my $pool (keys %{$cfg->{pools}}) { + foreach my $pool (sort keys %{$cfg->{pools}}) { my $d = $cfg->{pools}->{$pool}; - my $vmlist = join (',', keys %{$d->{vms}}); - my $storelist = join (',', keys %{$d->{storage}}); + my $vmlist = join (',', sort keys %{$d->{vms}}); + my $storelist = join (',', sort keys %{$d->{storage}}); my $comment = $d->{comment} ? PVE::Tools::encode_text($d->{comment}) : ''; $data .= "pool:$pool:$comment:$vmlist:$storelist:\n"; } $data .= "\n"; - foreach my $role (keys %{$cfg->{roles}}) { + foreach my $role (sort keys %{$cfg->{roles}}) { next if $special_roles->{$role}; my $d = $cfg->{roles}->{$role}; - my $list = join (',', keys %$d); + my $list = join (',', sort keys %$d); $data .= "role:$role:$list:\n"; } $data .= "\n"; - foreach my $path (sort keys %{$cfg->{acl}}) { - my $d = $cfg->{acl}->{$path}; + my $collect_rolelist_members = sub { + my ($acl_members, $result, $prefix, $exclude) = @_; - my $ra = {}; + foreach my $member (keys %$acl_members) { + next if $exclude && $member eq $exclude; - foreach my $group (keys %{$d->{groups}}) { my $l0 = ''; my $l1 = ''; - foreach my $role (sort keys %{$d->{groups}->{$group}}) { - my $propagate = $d->{groups}->{$group}->{$role}; + foreach my $role (sort keys %{$acl_members->{$member}}) { + my $propagate = $acl_members->{$member}->{$role}; if ($propagate) { $l1 .= ',' if $l1; $l1 .= $role; @@ -1098,37 +1260,30 @@ sub write_user_config { $l0 .= $role; } } - $ra->{0}->{$l0}->{"\@$group"} = 1 if $l0; - $ra->{1}->{$l1}->{"\@$group"} = 1 if $l1; + $result->{0}->{$l0}->{"${prefix}${member}"} = 1 if $l0; + $result->{1}->{$l1}->{"${prefix}${member}"} = 1 if $l1; } + }; - foreach my $user (keys %{$d->{users}}) { - # no need to save, because root is always 'Administrator' - next if $user eq 'root@pam'; + foreach my $path (sort keys %{$cfg->{acl}}) { + my $d = $cfg->{acl}->{$path}; - my $l0 = ''; - my $l1 = ''; - foreach my $role (sort keys %{$d->{users}->{$user}}) { - my $propagate = $d->{users}->{$user}->{$role}; - if ($propagate) { - $l1 .= ',' if $l1; - $l1 .= $role; - } else { - $l0 .= ',' if $l0; - $l0 .= $role; - } + my $rolelist_members = {}; + + $collect_rolelist_members->($d->{'groups'}, $rolelist_members, '@'); + + # no need to save 'root@pam', it is always 'Administrator' + $collect_rolelist_members->($d->{'users'}, $rolelist_members, '', 'root@pam'); + + $collect_rolelist_members->($d->{'tokens'}, $rolelist_members, ''); + + foreach my $propagate (0,1) { + my $filtered = $rolelist_members->{$propagate}; + foreach my $rolelist (sort keys %$filtered) { + my $uglist = join (',', sort keys %{$filtered->{$rolelist}}); + $data .= "acl:$propagate:$path:$uglist:$rolelist:\n"; } - $ra->{0}->{$l0}->{$user} = 1 if $l0; - $ra->{1}->{$l1}->{$user} = 1 if $l1; - } - foreach my $rolelist (sort keys %{$ra->{0}}) { - my $uglist = join (',', keys %{$ra->{0}->{$rolelist}}); - $data .= "acl:0:$path:$uglist:$rolelist:\n"; - } - foreach my $rolelist (sort keys %{$ra->{1}}) { - my $uglist = join (',', keys %{$ra->{1}->{$rolelist}}); - $data .= "acl:1:$path:$uglist:$rolelist:\n"; } } @@ -1197,11 +1352,26 @@ sub roles { my ($cfg, $user, $path) = @_; # NOTE: we do not consider pools here. - # You need to use $rpcenv->roles() instead if you want that. + # NOTE: for privsep tokens, this does not filter roles by those that the + # corresponding user has. + # Use $rpcenv->permission() for any actual permission checks! return 'Administrator' if $user eq 'root@pam'; # root can do anything - my $perm = {}; + if (pve_verify_tokenid($user, 1)) { + my $tokenid = $user; + my ($username, $token) = split_tokenid($tokenid); + + my $token_info = $cfg->{users}->{$username}->{tokens}->{$token}; + return () if !$token_info; + + my $user_roles = roles($cfg, $username, $path); + + # return full user privileges + return $user_roles if !$token_info->{privsep}; + } + + my $roles = {}; foreach my $p (sort keys %{$cfg->{acl}}) { my $final = ($path eq $p); @@ -1212,6 +1382,21 @@ sub roles { #print "CHECKACL $path $p\n"; #print "ACL $path = " . Dumper ($acl); + if (my $ri = $acl->{tokens}->{$user}) { + my $new; + foreach my $role (keys %$ri) { + my $propagate = $ri->{$role}; + if ($final || $propagate) { + #print "APPLY ROLE $p $user $role\n"; + $new = {} if !$new; + $new->{$role} = $propagate; + } + } + if ($new) { + $roles = $new; # overwrite previous settings + next; + } + } if (my $ri = $acl->{users}->{$user}) { my $new; @@ -1220,11 +1405,11 @@ sub roles { if ($final || $propagate) { #print "APPLY ROLE $p $user $role\n"; $new = {} if !$new; - $new->{$role} = 1; + $new->{$role} = $propagate; } } if ($new) { - $perm = $new; # overwrite previous settings + $roles = $new; # overwrite previous settings next; # user privs always override group privs } } @@ -1238,64 +1423,25 @@ sub roles { if ($final || $propagate) { #print "APPLY ROLE $p \@$g $role\n"; $new = {} if !$new; - $new->{$role} = 1; + $new->{$role} = $propagate; } } } } if ($new) { - $perm = $new; # overwrite previous settings + $roles = $new; # overwrite previous settings next; } } - return ('NoAccess') if defined ($perm->{NoAccess}); - #return () if defined ($perm->{NoAccess}); + return { 'NoAccess' => $roles->{NoAccess} } if defined ($roles->{NoAccess}); + #return () if defined ($roles->{NoAccess}); - #print "permission $user $path = " . Dumper ($perm); - - my @ra = keys %$perm; + #print "permission $user $path = " . Dumper ($roles); #print "roles $user $path = " . join (',', @ra) . "\n"; - return @ra; -} - -sub permission { - my ($cfg, $user, $path) = @_; - - $user = PVE::Auth::Plugin::verify_username($user, 1); - return {} if !$user; - - my @ra = roles($cfg, $user, $path); - - my $privs = {}; - - foreach my $role (@ra) { - if (my $privset = $cfg->{roles}->{$role}) { - foreach my $p (keys %$privset) { - $privs->{$p} = 1; - } - } - } - - #print "priviledges $user $path = " . Dumper ($privs); - - return $privs; -} - -sub check_permissions { - my ($username, $path, $privlist) = @_; - - $path = normalize_path($path); - my $usercfg = cfs_read_file('user.cfg'); - my $perm = permission($usercfg, $username, $path); - - foreach my $priv (split_list($privlist)) { - return undef if !$perm->{$priv}; - }; - - return 1; + return $roles; } sub remove_vm_access {