[pve-devel] [PATCH v3 access-control 04/20] API token: add REs, helpers, parsing + writing

Fabian Grünbichler f.gruenbichler at proxmox.com
Tue Jan 21 13:54:02 CET 2020


token definitions/references in user.cfg always use the full form of the
token id, consisting of:

USER at REALM!TOKENID

token definitions are represented by their own lines prefixed with
'token', which need to come after the corresponding user definition, but
before any ACLs referencing them.

parsed representation in a user config hash is inside a new 'tokens'
element of the corresponding user object, using the unique-per-user
token id as key.

only token metadata is stored inside user.cfg / accessible via the
parsed user config hash. the actual token values will be stored
root-readable only in a separate (shadow) file.

'comment' and 'expire' have the same semantics as for users.

'privsep' determines whether an API token gets the full privileges of
the corresponding user, or just the intersection of privileges of the
corresponding user and those of the API token itself.

Signed-off-by: Fabian Grünbichler <f.gruenbichler at proxmox.com>
---

Notes:
    v1->v2:
    - remove 'enable' boolean for tokens
    
    I am a bit unsure how to differentiate in a clean way between:
    A full userid/tokenid (username at realm OR username at real!token)
    B user (username at realm)
    C tokenid (username at realm!token)
    D token/tokensubid/tokenid-per-user (just the part after !)
    
    I am not sure whether it makes much sense to replace all the existing naming
    where B becomes A with the introduction of tokens. it might make sense to have
    some specific variable naming for those few places where we explicitly handle
    the difference (A goes in, we check if it's B or C and do different stuff in
    either case), as well as for cleanly separating between C and D. applies to
    patches after this as well..
    
    recommendations/input welcome ;)

 PVE/AccessControl.pm | 88 ++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 88 insertions(+)

diff --git a/PVE/AccessControl.pm b/PVE/AccessControl.pm
index 1c7b551..b293291 100644
--- a/PVE/AccessControl.pm
+++ b/PVE/AccessControl.pm
@@ -211,6 +211,47 @@ sub rotate_authkey {
     die $@ if $@;
 }
 
+our $token_subid_regex = $PVE::Auth::Plugin::realm_regex;
+
+# username at 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 {
@@ -1000,6 +1041,12 @@ sub parse_user_config {
 			    } else {
 				warn "user config - ignore invalid acl member '$ug'\n";
 			    }
+			} elsif (my ($user, $token) = split_tokenid($ug, 1)) {
+			    if ($cfg->{users}->{$user}->{tokens}->{$token}) { # token exists
+				$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";
 			}
@@ -1046,6 +1093,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";
 	}
@@ -1071,6 +1146,16 @@ 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";
@@ -1137,12 +1222,15 @@ sub write_user_config {
 	# no need to save 'root at pam', it is always 'Administrator'
 	$collect_rolelist_members->($d->{'users'}, $rolelist_members, '', 'root at 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";
 	    }
+
 	}
     }
 
-- 
2.20.1





More information about the pve-devel mailing list