]> git.proxmox.com Git - pve-access-control.git/blobdiff - PVE/AccessControl.pm
user.cfg: sort entries alphabetically in each section
[pve-access-control.git] / PVE / AccessControl.pm
index 0c59334e9075598dfe259d9f5e45378434a71340..3e52c5f5389ae6b5c7d3216a3f014535b37119b8 100644 (file)
@@ -47,9 +47,8 @@ my $pve_auth_key_files = {
 
 my $pve_auth_key_cache = {};
 
-my $ticket_lifetime = 3600*2; # 2 hours
-# TODO: set to 24h for PVE 6.0
-my $authkey_lifetime = 3600*0; # rotation disabled
+my $ticket_lifetime = 3600 * 2; # 2 hours
+my $authkey_lifetime = 3600 * 24; # rotate every 24 hours
 
 Crypt::OpenSSL::RSA->import_random_seed();
 
@@ -167,18 +166,21 @@ sub rotate_authkey {
        return if check_authkey();
 
        my $old = get_pubkey();
+       my $new = Crypt::OpenSSL::RSA->generate_key(2048);
 
        if ($old) {
            eval {
                my $pem = $old->get_public_key_x509_string();
+               # mtime is used for caching and ticket age range calculation
                PVE::Tools::file_set_contents($pve_auth_key_files->{pubold}, $pem);
            };
            die "Failed to store old auth key: $@\n" if $@;
        }
 
-       my $new = Crypt::OpenSSL::RSA->generate_key(2048);
        eval {
            my $pem = $new->get_public_key_x509_string();
+           # mtime is used for caching and ticket age range calculation,
+           # should be close to that of pubold above
            PVE::Tools::file_set_contents($pve_auth_key_files->{pub}, $pem);
        };
        if ($@) {
@@ -210,10 +212,12 @@ sub rotate_authkey {
 }
 
 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::sha1_base64($input);
+       $csrf_prevention_secret = Digest::SHA::hmac_sha256_base64($input);
+       $csrf_prevention_secret_legacy = Digest::SHA::sha1_base64($input);
     }
     return $csrf_prevention_secret;
 };
@@ -229,7 +233,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);
@@ -284,7 +297,7 @@ sub verify_ticket {
        return undef if !$rsa_pub;
 
        my ($min, $max) = $get_ticket_age_range->($now, $rsa_mtime, $old);
-       return undef if !$min;
+       return undef if !defined($min);
 
        return PVE::Ticket::verify_rsa_ticket(
            $rsa_pub, 'PVE', $ticket, undef, $min, $max, 1);
@@ -308,10 +321,10 @@ sub verify_ticket {
        return $auth_failure->();
     }
 
-    my ($username, $challenge);
+    my ($username, $tfa_info);
     if ($data =~ m{^u2f!([^!]+)!([0-9a-zA-Z/.=_\-+]+)$}) {
        # Ticket for u2f-users:
-       ($username, $challenge) = ($1, $2);
+       ($username, my $challenge) = ($1, $2);
        if ($challenge eq 'verified') {
            # u2f challenge was completed
            $challenge = undef;
@@ -320,6 +333,15 @@ sub verify_ticket {
            # so we treat this ticket as invalid:
            return $auth_failure->();
        }
+       $tfa_info = {
+           type => 'u2f',
+           challenge => $challenge,
+       };
+    } elsif ($data =~ /^tfa!(.*)$/) {
+       # TOTP and Yubico don't require a challenge so this is the generic
+       # 'missing 2nd factor ticket'
+       $username = $1;
+       $tfa_info = { type => 'tfa' };
     } else {
        # Regular ticket (full access)
        $username = $data;
@@ -327,7 +349,7 @@ sub verify_ticket {
 
     return undef if !PVE::Auth::Plugin::verify_username($username, $noerr);
 
-    return wantarray ? ($username, $age, $challenge) : $username;
+    return wantarray ? ($username, $age, $tfa_info) : $username;
 }
 
 # VNC tickets
@@ -515,22 +537,32 @@ sub authenticate_user {
     my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
     $plugin->authenticate_user($cfg, $realm, $ruid, $password);
 
-    my $u2f;
-
     my ($type, $tfa_data) = user_get_tfa($username, $realm);
     if ($type) {
        if ($type eq 'u2f') {
            # Note that if the user did not manage to complete the initial u2f registration
            # challenge we have a hash containing a 'challenge' entry in the user's tfa.cfg entry:
-           $u2f = $tfa_data if !exists $tfa_data->{challenge};
+           $tfa_data = undef if exists $tfa_data->{challenge};
+       } elsif (!defined($otp)) {
+           # The user requires a 2nd factor but has not provided one. Return success but
+           # don't clear $tfa_data.
        } else {
            my $keys = $tfa_data->{keys};
            my $tfa_cfg = $tfa_data->{config};
            verify_one_time_pw($type, $username, $keys, $tfa_cfg, $otp);
+           $tfa_data = undef;
+       }
+
+       # Return the type along with the rest:
+       if ($tfa_data) {
+           $tfa_data = {
+               type => $type,
+               data => $tfa_data,
+           };
        }
     }
 
-    return wantarray ? ($username, $u2f) : $username;
+    return wantarray ? ($username, $tfa_data) : $username;
 }
 
 sub domain_set_password {
@@ -942,8 +974,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 {
@@ -1016,7 +1049,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}) : '';
@@ -1030,7 +1063,7 @@ sub write_user_config {
 
     $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 $comment = $d->{comment} ? PVE::Tools::encode_text($d->{comment}) : '';
@@ -1039,7 +1072,7 @@ sub write_user_config {
 
     $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}});
@@ -1049,7 +1082,7 @@ sub write_user_config {
 
     $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};
@@ -1354,7 +1387,7 @@ sub remove_vm_from_pool {
     lock_user_config($delVMfromPoolFn, "pool cleanup for VM $vmid failed");
 }
 
-my $CUSTOM_TFA_TYPES = {
+my $USER_CONTROLLED_TFA_TYPES = {
     u2f => 1,
     oath => 1,
 };
@@ -1401,7 +1434,7 @@ sub user_set_tfa {
        # The 'yubico' type requires yubico server settings, which have to be configured on the
        # realm, so this is not supported here:
        die "domain '$realm' does not support TFA type '$type'\n"
-           if defined($data) && !$CUSTOM_TFA_TYPES->{$type};
+           if defined($data) && !$USER_CONTROLLED_TFA_TYPES->{$type};
     }
 
     # Custom TFA entries are stored in priv/tfa.cfg as they can be more complet: u2f uses a
@@ -1415,7 +1448,7 @@ sub user_set_tfa {
        $tfa->{data} = $data;
        cfs_write_file('priv/tfa.cfg', $tfa_cfg);
 
-       $user->{keys} = 'x';
+       $user->{keys} = "x!$type";
     } else {
        delete $tfa_cfg->{users}->{$userid};
        cfs_write_file('priv/tfa.cfg', $tfa_cfg);
@@ -1434,7 +1467,6 @@ sub user_get_tfa {
        or die "user '$username' not found\n";
 
     my $keys = $user->{keys};
-    return if !$keys;
 
     my $domain_cfg = cfs_read_file('domains.cfg');
     my $realm_cfg = $domain_cfg->{ids}->{$realm};
@@ -1444,7 +1476,13 @@ sub user_get_tfa {
     $realm_tfa = PVE::Auth::Plugin::parse_tfa_config($realm_tfa)
        if $realm_tfa;
 
-    if ($keys ne 'x') {
+    if (!$keys) {
+       return if !$realm_tfa;
+       die "missing required 2nd keys\n";
+    }
+
+    # new style config starts with an 'x' and optionally contains a !<type> suffix
+    if ($keys !~ /^x(?:!.*)?$/) {
        # old style config, find the type via the realm
        return if !$realm_tfa;
        return ($realm_tfa->{type}, {