]> git.proxmox.com Git - pve-access-control.git/blobdiff - src/PVE/AccessControl.pm
fix #5335: sort ACL entries in user.cfg
[pve-access-control.git] / src / PVE / AccessControl.pm
index 89b7d9045551fb6f4f78d7b9b3453865ac550ce6..47f2d38b09c7f267e74978de506cb47cbdf8ae41 100644 (file)
@@ -16,6 +16,7 @@ use JSON;
 use Scalar::Util 'weaken';
 use URI::Escape;
 
+use PVE::Exception qw(raise_perm_exc raise_param_exc);
 use PVE::OTP;
 use PVE::Ticket;
 use PVE::Tools qw(run_command lock_file file_get_contents split_list safe_print);
@@ -717,8 +718,8 @@ sub verify_one_time_pw {
 
 # password should be utf8 encoded
 # Note: some plugins delay/sleep if auth fails
-sub authenticate_user : prototype($$$$;$) {
-    my ($username, $password, $otp, $new_format, $tfa_challenge) = @_;
+sub authenticate_user : prototype($$$;$) {
+    my ($username, $password, $otp, $tfa_challenge) = @_;
 
     die "no username specified\n" if !$username;
 
@@ -744,52 +745,18 @@ sub authenticate_user : prototype($$$$;$) {
 
     $plugin->authenticate_user($cfg, $realm, $ruid, $password);
 
-    if ($new_format) {
-       # This is the first factor with an optional immediate 2nd factor for TOTP:
-       my $tfa_challenge = authenticate_2nd_new($username, $realm, $otp, undef);
-       return wantarray ? ($username, $tfa_challenge) : $username;
-    } else {
-       return authenticate_2nd_old($username, $realm, $otp);
-    }
-}
-
-sub authenticate_2nd_old : prototype($$$) {
-    my ($username, $realm, $otp) = @_;
-
-    my ($type, $tfa_data) = user_get_tfa($username, $realm, 0);
-    if ($type) {
-       if ($type eq 'incompatible') {
-           die "old login api disabled, user has incompatible TFA entries\n";
-       } elsif ($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:
-           $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, $tfa_data) : $username;
+    # This is the first factor with an optional immediate 2nd factor for TOTP:
+    $tfa_challenge = authenticate_2nd_new($username, $realm, $otp, undef);
+    return wantarray ? ($username, $tfa_challenge) : $username;
 }
 
 sub authenticate_2nd_new_do : prototype($$$$) {
     my ($username, $realm, $tfa_response, $tfa_challenge) = @_;
-    my ($tfa_cfg, $realm_tfa) = user_get_tfa($username, $realm, 1);
+    my ($tfa_cfg, $realm_tfa) = user_get_tfa($username, $realm);
 
+    # FIXME: `$tfa_cfg` is now usually never undef - use cheap check for
+    # whether the user has *any* entries here instead whe it is available in
+    # pve-rs
     if (!defined($tfa_cfg)) {
        return undef;
     }
@@ -842,6 +809,10 @@ sub authenticate_2nd_new_do : prototype($$$$) {
        $tfa_challenge = undef;
     } else {
        $tfa_challenge = $tfa_cfg->authentication_challenge($username);
+
+       die "missing required 2nd keys\n"
+           if $realm_tfa && !defined($tfa_challenge);
+
        if (defined($tfa_response)) {
            if (defined($tfa_challenge)) {
                $tfa_done = 1;
@@ -858,26 +829,20 @@ sub authenticate_2nd_new_do : prototype($$$$) {
            die "2nd factor failed\n";
        }
 
-       # FIXME: Remove this case when enabling the ones below!
-       if (!$result->{result}) {
-           die "2nd factor failed\n";
-       }
-
        if ($result->{'needs-saving'}) {
            cfs_write_file('priv/tfa.cfg', $tfa_cfg);
        }
-       # FIXME: Switch to the code below to use the updated `priv/tfa.cfg` format!
-       #if ($result->{'totp-limit-reached'}) {
-       #    # FIXME: send mail to the user (or admin/root if no email configured)
-       #    die "failed 2nd factor: TOTP limit reached, locked\n";
-       #}
-       #if ($result->{'tfa-limit-reached'}) {
-       #    # FIXME: send mail to the user (or admin/root if no email configured)
-       #    die "failed 1nd factor: TFA limit reached, user locked out\n";
-       #}
-       #if (!$result->{result}) {
-       #    die "failed 2nd factor\n";
-       #}
+       if ($result->{'totp-limit-reached'}) {
+           # FIXME: send mail to the user (or admin/root if no email configured)
+           die "failed 2nd factor: TOTP limit reached, locked\n";
+       }
+       if ($result->{'tfa-limit-reached'}) {
+           # FIXME: send mail to the user (or admin/root if no email configured)
+           die "failed 1nd factor: TFA limit reached, user locked out\n";
+       }
+       if (!$result->{result}) {
+           die "failed 2nd factor\n";
+       }
     }
 
     return $tfa_challenge;
@@ -986,7 +951,7 @@ sub iterate_acl_tree {
 
     my $children = $node->{children};
 
-    foreach my $child (keys %$children) {
+    foreach my $child (sort keys %$children) {
        iterate_acl_tree("$path/$child", $children->{$child}, $code);
     }
 }
@@ -1101,9 +1066,9 @@ my $privgroups = {
            'Sys.PowerMgmt',
            'Sys.Modify', # edit/change node settings
            'Sys.Incoming', # incoming storage/guest migrations
+           'Sys.AccessNetwork', # for, e.g., downloading ISOs from any URL
        ],
        admin => [
-           'Permissions.Modify',
            'Sys.Console',
            'Sys.Syslog',
        ],
@@ -1131,6 +1096,9 @@ my $privgroups = {
            'SDN.Allocate',
            'SDN.Audit',
        ],
+       user => [
+           'SDN.Use',
+       ],
        audit => [
            'SDN.Audit',
        ],
@@ -1159,9 +1127,23 @@ my $privgroups = {
            'Pool.Audit',
        ],
     },
+    Mapping => {
+       root => [],
+       admin => [
+           'Mapping.Modify',
+       ],
+       user => [
+           'Mapping.Use',
+       ],
+       audit => [
+           'Mapping.Audit',
+       ],
+    },
 };
 
-my $valid_privs = {};
+my $valid_privs = {
+    'Permissions.Modify' => 1, # not contained in a group
+};
 
 my $special_roles = {
     'NoAccess' => {}, # no privileges
@@ -1170,27 +1152,32 @@ my $special_roles = {
 
 sub create_roles {
 
-    foreach my $cat (keys %$privgroups) {
+    for my $cat (keys %$privgroups) {
        my $cd = $privgroups->{$cat};
-       foreach my $p (@{$cd->{root}}, @{$cd->{admin}},
-                      @{$cd->{user}}, @{$cd->{audit}}) {
-           $valid_privs->{$p} = 1;
+       # create map to easily check if a privilege is valid
+       for my $priv (@{$cd->{root}}, @{$cd->{admin}}, @{$cd->{user}}, @{$cd->{audit}}) {
+           $valid_privs->{$priv} = 1;
        }
-       foreach my $p (@{$cd->{admin}}, @{$cd->{user}}, @{$cd->{audit}}) {
-
-           $special_roles->{"PVE${cat}Admin"}->{$p} = 1;
-           $special_roles->{"PVEAdmin"}->{$p} = 1;
+       # create grouped admin roles and PVEAdmin
+       for my $priv (@{$cd->{admin}}, @{$cd->{user}}, @{$cd->{audit}}) {
+           $special_roles->{"PVE${cat}Admin"}->{$priv} = 1;
+           $special_roles->{"PVEAdmin"}->{$priv} = 1;
        }
+       # create grouped user and audit roles
        if (scalar(@{$cd->{user}})) {
-           foreach my $p (@{$cd->{user}}, @{$cd->{audit}}) {
-               $special_roles->{"PVE${cat}User"}->{$p} = 1;
+           for my $priv (@{$cd->{user}}, @{$cd->{audit}}) {
+               $special_roles->{"PVE${cat}User"}->{$priv} = 1;
            }
        }
-       foreach my $p (@{$cd->{audit}}) {
-           $special_roles->{"PVEAuditor"}->{$p} = 1;
+       for my $priv (@{$cd->{audit}}) {
+           $special_roles->{"PVEAuditor"}->{$priv} = 1;
        }
     }
 
+    # remove Mapping.Modify from PVEAdmin, only Administrator, root@pam and
+    # PVEMappingAdmin should be able to use that for now
+    delete $special_roles->{"PVEAdmin"}->{"Mapping.Modify"};
+
     $special_roles->{"PVETemplateUser"} = { 'VM.Clone' => 1, 'VM.Audit' => 1 };
 };
 
@@ -1280,14 +1267,25 @@ sub check_path {
        |/nodes
        |/nodes/[[:alnum:]\.\-\_]+
        |/pool
-       |/pool/[[:alnum:]\.\-\_]+
+       |/pool/[A-Za-z0-9\.\-_]+(?:/[A-Za-z0-9\.\-_]+){0,2}
        |/sdn
+       |/sdn/controllers
+       |/sdn/controllers/[[:alnum:]\_\-]+
+       |/sdn/dns
+       |/sdn/dns/[[:alnum:]]+
+       |/sdn/ipams
+       |/sdn/ipams/[[:alnum:]]+
+       |/sdn/zones
        |/sdn/zones/[[:alnum:]\.\-\_]+
-       |/sdn/vnets/[[:alnum:]\.\-\_]+
+       |/sdn/zones/[[:alnum:]\.\-\_]+/[[:alnum:]\.\-\_]+
+       |/sdn/zones/[[:alnum:]\.\-\_]+/[[:alnum:]\.\-\_]+/[1-9][0-9]{0,3}
        |/storage
        |/storage/[[:alnum:]\.\-\_]+
        |/vms
        |/vms/[1-9][0-9]{2,}
+       |/mapping
+       |/mapping/[[:alnum:]\.\-\_]+
+       |/mapping/[[:alnum:]\.\-\_]+/[[:alnum:]\.\-\_]+
     )$!xs;
 }
 
@@ -1323,8 +1321,14 @@ PVE::JSONSchema::register_format('pve-poolid', \&verify_poolname);
 sub verify_poolname {
     my ($poolname, $noerr) = @_;
 
-    if ($poolname !~ m/^[A-Za-z0-9\.\-_]+$/) {
+    if (split("/", $poolname) > 3) {
+       die "pool name '$poolname' nested too deeply (max levels = 3)\n" if !$noerr;
+
+       return undef;
+    }
 
+    # also adapt check_path above if changed!
+    if ($poolname !~ m!^[A-Za-z0-9\.\-_]+(?:/[A-Za-z0-9\.\-_]+){0,2}$!) {
        die "pool name '$poolname' contains invalid characters\n" if !$noerr;
 
        return undef;
@@ -1528,7 +1532,21 @@ sub parse_user_config {
            }
 
            # make sure to add the pool (even if there are no members)
-           $cfg->{pools}->{$pool} = { vms => {}, storage => {} } if !$cfg->{pools}->{$pool};
+           $cfg->{pools}->{$pool} = { vms => {}, storage => {}, pools => {} }
+               if !$cfg->{pools}->{$pool};
+
+           if ($pool =~ m!/!) {
+               my $curr = $pool;
+               while ($curr =~ m!^(.+)/[^/]+$!) {
+                   # ensure nested pool info is correctly recorded
+                   my $parent = $1;
+                   $cfg->{pools}->{$curr}->{parent} = $parent;
+                   $cfg->{pools}->{$parent} = { vms => {}, storage => {}, pools => {} }
+                       if !$cfg->{pools}->{$parent};
+                   $cfg->{pools}->{$parent}->{pools}->{$curr} = 1;
+                   $curr = $parent;
+               }
+           }
 
            $cfg->{pools}->{$pool}->{comment} = PVE::Tools::decode_text($comment) if $comment;
 
@@ -1724,8 +1742,6 @@ sub parse_priv_tfa_config {
 sub write_priv_tfa_config {
     my ($filename, $cfg) = @_;
 
-    assert_new_tfa_config_available();
-
     return $cfg->write();
 }
 
@@ -1921,39 +1937,9 @@ my $USER_CONTROLLED_TFA_TYPES = {
     oath => 1,
 };
 
-sub assert_new_tfa_config_available() {
-    PVE::Cluster::cfs_update();
-    my $version_info = PVE::Cluster::get_node_kv('version-info');
-    die "cannot update tfa config, please make sure all cluster nodes are up to date\n"
-       if !$version_info;
-    my $members = PVE::Cluster::get_members() or return; # get_members returns undef on no cluster
-    my $old = '';
-    foreach my $node (keys $members->%*) {
-       my $info = $version_info->{$node};
-       if (!$info) {
-           $old .= "  cluster node '$node' is too old, did not broadcast its version info\n";
-           next;
-       }
-       $info = from_json($info);
-       my $ver = $info->{version};
-       if ($ver !~ /^(\d+\.\d+)-(\d+)/) {
-           $old .= "  cluster node '$node' provided an invalid version string: '$ver'\n";
-           next;
-       }
-       my ($maj, $rel) = ($1, $2);
-       if (!($maj > 7.0 || ($maj == 7.0 && $rel >= 15))) {
-           $old .= "  cluster node '$node' is too old ($ver < 7.0-15)\n";
-           next;
-       }
-    }
-    die "cannot update tfa config, following nodes are not up to date:\n$old" if length($old);
-}
-
 sub user_remove_tfa : prototype($) {
     my ($userid) = @_;
 
-    assert_new_tfa_config_available();
-
     my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
     $tfa_cfg->remove_user($userid);
     cfs_write_file('priv/tfa.cfg', $tfa_cfg);
@@ -2032,7 +2018,7 @@ sub add_old_keys_to_realm_tfa : prototype($$$$) {
 }
 
 sub user_get_tfa : prototype($$$) {
-    my ($username, $realm, $new_format) = @_;
+    my ($username, $realm) = @_;
 
     my $user_cfg = cfs_read_file('user.cfg');
     my $user = $user_cfg->{users}->{$username}
@@ -2048,40 +2034,12 @@ sub user_get_tfa : prototype($$$) {
     $realm_tfa = PVE::Auth::Plugin::parse_tfa_config($realm_tfa)
        if $realm_tfa;
 
-    if (!$keys) {
-       return if !$realm_tfa;
-       die "missing required 2nd keys\n";
-    }
-
-    if ($new_format) {
-       my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
-       if (defined($keys) && $keys !~ /^x(?:!.*)$/) {
-           add_old_keys_to_realm_tfa($username, $tfa_cfg, $realm_tfa, $keys);
-       }
-       return ($tfa_cfg, $realm_tfa);
+    my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
+    if (defined($keys) && $keys !~ /^x(?:!.*)$/) {
+       add_old_keys_to_realm_tfa($username, $tfa_cfg, $realm_tfa, $keys);
     }
 
-    # 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}, {
-           keys => $keys,
-           config => $realm_tfa,
-       });
-    } else {
-       my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
-       my $tfa = $tfa_cfg->{users}->{$username};
-       return if !$tfa; # should not happen (user.cfg wasn't cleaned up?)
-
-       if ($realm_tfa) {
-           # if the realm has a tfa setting we need to verify the type:
-           die "auth domain '$realm' and user have mismatching TFA settings\n"
-               if $realm_tfa && $realm_tfa->{type} ne $tfa->{type};
-       }
-
-       return ($tfa->{type}, $tfa->{data});
-    }
+    return ($tfa_cfg, $realm_tfa);
 }
 
 # bash completion helpers