# 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;
}
configure_u2f_and_wa($tfa_cfg);
- my $must_save = 0;
+ my ($result, $tfa_done);
if (defined($tfa_challenge)) {
+ $tfa_done = 1;
$tfa_challenge = verify_ticket($tfa_challenge, 0, $username);
- $must_save = $tfa_cfg->authentication_verify($username, $tfa_challenge, $tfa_response);
+ $result = $tfa_cfg->authentication_verify2($username, $tfa_challenge, $tfa_response);
$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)) {
- $must_save = $tfa_cfg->authentication_verify($username, $tfa_challenge, $tfa_response);
+ $tfa_done = 1;
+ $result = $tfa_cfg->authentication_verify2($username, $tfa_challenge, $tfa_response);
} else {
die "no such challenge\n";
}
}
}
- if ($must_save) {
- cfs_write_file('priv/tfa.cfg', $tfa_cfg);
+ if ($tfa_done) {
+ if (!$result) {
+ # authentication_verify2 somehow returned undef - should be unreachable
+ die "2nd factor failed\n";
+ }
+
+ if ($result->{'needs-saving'}) {
+ cfs_write_file('priv/tfa.cfg', $tfa_cfg);
+ }
+ 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;
'Sys.Incoming', # incoming storage/guest migrations
],
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
}
}
+ # 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 };
};
|/pool
|/pool/[[:alnum:]\.\-\_]+
|/sdn
+ |/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 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