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 {
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')
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) = @_;
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);
'Datastore.Audit',
],
},
+ SDN => {
+ root => [],
+ admin => [
+ 'SDN.Allocate',
+ 'SDN.Audit',
+ ],
+ audit => [
+ 'SDN.Audit',
+ ],
+ },
User => {
root => [
'Realm.Allocate',
if ($cfg->{users}->{$user}) { # user exists
$cfg->{users}->{$user}->{groups}->{$group} = 1;
- $cfg->{groups}->{$group}->{users}->{$user} = 1;
} else {
warn "user config - ignore invalid group member '$user'\n";
}
+ $cfg->{groups}->{$group}->{users}->{$user} = 1;
}
} elsif ($et eq 'role') {
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";
}
}
next;
}
+ if (!$cfg->{roles}->{$role}) {
+ warn "user config - ignore invalid acl role '$role'\n";
+ next;
+ }
+
foreach my $ug (split_list($uglist)) {
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 {
+ if (!$cfg->{groups}->{$group}) { # group does not exist
warn "user config - ignore invalid acl group '$group'\n";
}
+ $cfg->{acl}->{$path}->{groups}->{$group}->{$role} = $propagate;
} elsif (PVE::Auth::Plugin::verify_username($ug, 1)) {
- if ($cfg->{users}->{$ug}) { # user exists
- $cfg->{acl}->{$path}->{users}->{$ug}->{$role} = $propagate;
- } else {
+ if (!$cfg->{users}->{$ug}) { # user does not exist
warn "user config - ignore invalid acl member '$ug'\n";
}
+ $cfg->{acl}->{$path}->{users}->{$ug}->{$role} = $propagate;
+ } 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";
}
}
$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";
}
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 (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";
}
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";
}
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;
$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 (',', sort keys %{$ra->{0}->{$rolelist}});
- $data .= "acl:0:$path:$uglist:$rolelist:\n";
- }
- foreach my $rolelist (sort keys %{$ra->{1}}) {
- my $uglist = join (',', sort keys %{$ra->{1}->{$rolelist}});
- $data .= "acl:1:$path:$uglist:$rolelist:\n";
}
}
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);
#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;
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
}
}
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});
-
- #print "permission $user $path = " . Dumper ($perm);
+ return { 'NoAccess' => $roles->{NoAccess} } if defined ($roles->{NoAccess});
+ #return () if defined ($roles->{NoAccess});
- 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 {