use JSON;
use MIME::Base64;
-use PVE::Exception qw(raise raise_perm_exc);
+use PVE::Exception qw(raise raise_perm_exc raise_param_exc);
use PVE::SafeSyslog;
use PVE::RPCEnvironment;
use PVE::Cluster qw(cfs_read_file);
-use PVE::Corosync;
+use PVE::DataCenterConfig;
use PVE::RESTHandler;
use PVE::AccessControl;
use PVE::JSONSchema qw(get_standard_option);
use PVE::API2::Group;
use PVE::API2::Role;
use PVE::API2::ACL;
+use PVE::Auth::Plugin;
use PVE::OTP;
use PVE::Tools;
my $create_ticket = sub {
my ($rpcenv, $username, $pw_or_ticket, $otp) = @_;
- my ($ticketuser, $u2fdata);
- if (($ticketuser = PVE::AccessControl::verify_ticket($pw_or_ticket, 1)) &&
- ($ticketuser eq 'root@pam' || $ticketuser eq $username)) {
+ my ($ticketuser, undef, $tfa_info) = PVE::AccessControl::verify_ticket($pw_or_ticket, 1);
+ if (defined($ticketuser) && ($ticketuser eq 'root@pam' || $ticketuser eq $username)) {
+ if (defined($tfa_info)) {
+ die "incomplete ticket\n";
+ }
# valid ticket. Note: root@pam can create tickets for other users
} else {
- ($username, $u2fdata) = PVE::AccessControl::authenticate_user($username, $pw_or_ticket, $otp);
+ ($username, $tfa_info) = PVE::AccessControl::authenticate_user($username, $pw_or_ticket, $otp);
}
my %extra;
my $ticket_data = $username;
- if (defined($u2fdata)) {
- my $u2f = get_u2f_instance($rpcenv, $u2fdata->@{qw(publicKey keyHandle)});
- my $challenge = $u2f->auth_challenge()
- or die "failed to get u2f challenge\n";
- $challenge = decode_json($challenge);
- $extra{U2FChallenge} = $challenge;
- $ticket_data = "u2f!$username!$challenge->{challenge}";
+ if (defined($tfa_info)) {
+ $extra{NeedTFA} = 1;
+ if ($tfa_info->{type} eq 'u2f') {
+ my $u2finfo = $tfa_info->{data};
+ my $u2f = get_u2f_instance($rpcenv, $u2finfo->@{qw(publicKey keyHandle)});
+ my $challenge = $u2f->auth_challenge()
+ or die "failed to get u2f challenge\n";
+ $challenge = decode_json($challenge);
+ $extra{U2FChallenge} = $challenge;
+ $ticket_data = "u2f!$username!$challenge->{challenge}";
+ } else {
+ # General half-login / 'missing 2nd factor' ticket:
+ $ticket_data = "tfa!$username";
+ }
}
my $ticket = PVE::AccessControl::assemble_ticket($ticket_data);
user => 'world'
},
protected => 1, # else we can't access shadow files
+ allowtoken => 0, # we don't want tokens to create tickets
description => "Create or verify authentication ticket.",
parameters => {
additionalProperties => 0,
}
$res->{cap} = &$compute_api_permission($rpcenv, $username)
- if !defined($res->{U2FChallenge});
-
- if (PVE::Corosync::check_conf_exists(1)) {
- if ($rpcenv->check($username, '/', ['Sys.Audit'], 1)) {
- eval {
- my $conf = cfs_read_file('corosync.conf');
- my $totem = PVE::Corosync::totem_config($conf);
- if ($totem->{cluster_name}) {
- $res->{clustername} = $totem->{cluster_name};
- }
- };
- warn "$@\n" if $@;
- }
+ if !defined($res->{NeedTFA});
+
+ my $clinfo = PVE::Cluster::get_clinfo();
+ if ($clinfo->{cluster}->{name} && $rpcenv->check($username, '/', ['Sys.Audit'], 1)) {
+ $res->{clustername} = $clinfo->{cluster}->{name};
}
PVE::Cluster::log_msg('info', 'root@pam', "successful auth for user '$username'");
],
},
protected => 1, # else we can't access shadow files
+ allowtoken => 0, # we don't want tokens to change the regular user password
description => "Change user password.",
parameters => {
additionalProperties => 0,
my $dc = cfs_read_file('datacenter.cfg');
my $u2f = $dc->{u2f};
die "u2f not configured in datacenter.cfg\n" if !$u2f;
- $u2f = PVE::JSONSchema::parse_property_string($PVE::Cluster::u2f_format, $u2f);
return $u2f;
}
],
},
protected => 1, # else we can't access shadow files
+ allowtoken => 0, # we don't want tokens to change the regular user's TFA settings
description => "Change user u2f authentication.",
parameters => {
additionalProperties => 0,
optional => 1,
description => 'When adding TOTP, the shared secret value.',
type => 'string',
- # This is what pve-common's PVE::OTP::oath_verify_otp accepts.
- # Should we move this to pve-common's JSONSchema as a named format?
- pattern => qr/[A-Z2-7=]{16}|[A-Fa-f0-9]{40}/,
+ format => 'pve-tfa-secret',
},
config => {
optional => 1,
# Regular users need to confirm their password to change u2f settings.
if ($authuser ne 'root@pam') {
- raise_param_exc('password' => 'password is required to modify u2f data')
+ raise_param_exc({ 'password' => 'password is required to modify u2f data' })
if !defined($password);
my $domain_cfg = cfs_read_file('domains.cfg');
my $cfg = $domain_cfg->{ids}->{$realm};
- die "auth domain '$realm' does not exists\n" if !$cfg;
+ die "auth domain '$realm' does not exist\n" if !$cfg;
my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
$plugin->authenticate_user($cfg, $realm, $ruid, $password);
}
return $challenge;
}
} elsif ($action eq 'confirm') {
- raise_param_exc('response' => "confirm action requires the 'response' parameter to be set")
+ raise_param_exc({ 'response' => "confirm action requires the 'response' parameter to be set" })
if !defined($response);
my ($type, $u2fdata) = PVE::AccessControl::user_get_tfa($userid, $realm);
my ($keyHandle, $publicKey) = $u2f->registration_verify($response);
PVE::AccessControl::user_set_tfa($userid, $realm, 'u2f', {
keyHandle => $keyHandle,
- publicKey => encode_base64($publicKey, ''),
+ publicKey => $publicKey, # already base64 encoded
});
} else {
die "invalid action: $action\n";
method => 'POST',
permissions => { user => 'all' },
protected => 1, # else we can't access shadow files
+ allowtoken => 0, # we don't want tokens to access TFA information
description => 'Finish a u2f challenge.',
parameters => {
additionalProperties => 0,
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
- my $challenge = $rpcenv->get_u2f_challenge()
- or raise('no active challenge');
my $authuser = $rpcenv->get_user();
my ($username, undef, $realm) = PVE::AccessControl::verify_username($authuser);
- my ($tfa_type, $u2fdata) = PVE::AccessControl::user_get_tfa($username, $realm);
- if (!defined($tfa_type) || $tfa_type ne 'u2f') {
+ my ($tfa_type, $tfa_data) = PVE::AccessControl::user_get_tfa($username, $realm);
+ if (!defined($tfa_type)) {
raise('no u2f data available');
}
- my $keyHandle = $u2fdata->{keyHandle};
- my $publicKey = $u2fdata->{publicKey};
- raise("incomplete u2f setup")
- if !defined($keyHandle) || !defined($publicKey);
+ eval {
+ if ($tfa_type eq 'u2f') {
+ my $challenge = $rpcenv->get_u2f_challenge()
+ or raise('no active challenge');
- my $u2f = get_u2f_instance($rpcenv, $publicKey, $keyHandle);
- $u2f->set_challenge($challenge);
+ my $keyHandle = $tfa_data->{keyHandle};
+ my $publicKey = $tfa_data->{publicKey};
+ raise("incomplete u2f setup")
+ if !defined($keyHandle) || !defined($publicKey);
- eval {
- my ($counter, $present) = $u2f->auth_verify($param->{response});
- # Do we want to do anything with these?
+ my $u2f = get_u2f_instance($rpcenv, $publicKey, $keyHandle);
+ $u2f->set_challenge($challenge);
+
+ my ($counter, $present) = $u2f->auth_verify($param->{response});
+ # Do we want to do anything with these?
+ } else {
+ # sanity check before handing off to the verification code:
+ my $keys = $tfa_data->{keys} or die "missing tfa keys\n";
+ my $config = $tfa_data->{config} or die "bad tfa entry\n";
+ PVE::AccessControl::verify_one_time_pw($tfa_type, $authuser, $keys, $config, $param->{response});
+ }
};
if (my $err = $@) {
my $clientip = $rpcenv->get_client_ip() || '';
die PVE::Exception->new("authentication failure\n", code => 401);
}
- # create a new ticket for the user:
- my $ticket_data = "u2f!$authuser!verified";
return {
- ticket => PVE::AccessControl::assemble_ticket($ticket_data),
+ ticket => PVE::AccessControl::assemble_ticket($authuser),
cap => &$compute_api_permission($rpcenv, $authuser),
}
}});
+__PACKAGE__->register_method({
+ name => 'permissions',
+ path => 'permissions',
+ method => 'GET',
+ description => 'Retrieve effective permissions of given user/token.',
+ permissions => {
+ description => "Each user/token is allowed to dump their own permissions. A user can dump the permissions of another user if they have 'Sys.Audit' permission on /access.",
+ user => 'all',
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ userid => {
+ type => 'string',
+ description => "User ID or full API token ID",
+ pattern => $PVE::AccessControl::userid_or_token_regex,
+ optional => 1,
+ },
+ path => get_standard_option('acl-path', {
+ description => "Only dump this specific path, not the whole tree.",
+ optional => 1,
+ }),
+ },
+ },
+ returns => {
+ type => 'object',
+ description => 'Map of "path" => (Map of "privilege" => "propagate boolean").',
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+
+ my $userid = $param->{userid};
+ if (defined($userid)) {
+ $rpcenv->check($rpcenv->get_user(), '/access', ['Sys.Audit']);
+ } else {
+ $userid = $rpcenv->get_user();
+ }
+
+ my $res;
+
+ if (my $path = $param->{path}) {
+ my $perms = $rpcenv->permissions($userid, $path);
+ if ($perms) {
+ $res = { $path => $perms };
+ } else {
+ $res = {};
+ }
+ } else {
+ $res = $rpcenv->get_effective_permissions($userid);
+ }
+
+ return $res;
+ }});
+
1;