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);
# 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;
$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;
}
$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;
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;
my $children = $node->{children};
- foreach my $child (keys %$children) {
+ foreach my $child (sort keys %$children) {
iterate_acl_tree("$path/$child", $children->{$child}, $code);
}
}
'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',
],
'SDN.Allocate',
'SDN.Audit',
],
+ user => [
+ 'SDN.Use',
+ ],
audit => [
'SDN.Audit',
],
'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
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 };
};
|/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;
}
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;
}
# 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;
sub write_priv_tfa_config {
my ($filename, $cfg) = @_;
- assert_new_tfa_config_available();
-
return $cfg->write();
}
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);
}
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}
$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