auto-format code using perltidy with Proxmox style guide

using the new top-level `make tidy` target, which calls perltidy via
our wrapper to enforce the desired style as closely as possible.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
This commit is contained in:
Thomas Lamprecht 2025-06-01 14:02:38 +02:00
parent 124e7a199b
commit 9590c6bdfe
34 changed files with 6005 additions and 5472 deletions

View File

@ -14,24 +14,31 @@ use PVE::RESTHandler;
use base qw(PVE::RESTHandler); use base qw(PVE::RESTHandler);
register_standard_option('acl-propagate', { register_standard_option(
'acl-propagate',
{
description => "Allow to propagate (inherit) permissions.", description => "Allow to propagate (inherit) permissions.",
type => 'boolean', type => 'boolean',
optional => 1, optional => 1,
default => 1, default => 1,
}); },
register_standard_option('acl-path', { );
register_standard_option(
'acl-path',
{
description => "Access control path", description => "Access control path",
type => 'string', type => 'string',
}); },
);
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'read_acl', name => 'read_acl',
path => '', path => '',
method => 'GET', method => 'GET',
description => "Get Access Control List (ACLs).", description => "Get Access Control List (ACLs).",
permissions => { permissions => {
description => "The returned list is restricted to objects where you have rights to modify permissions.", description =>
"The returned list is restricted to objects where you have rights to modify permissions.",
user => 'all', user => 'all',
}, },
parameters => { parameters => {
@ -67,16 +74,20 @@ __PACKAGE__->register_method ({
my $audit = $rpcenv->check($authuser, '/access', ['Sys.Audit'], 1); my $audit = $rpcenv->check($authuser, '/access', ['Sys.Audit'], 1);
my $root = $usercfg->{acl_root}; my $root = $usercfg->{acl_root};
PVE::AccessControl::iterate_acl_tree("/", $root, sub { PVE::AccessControl::iterate_acl_tree(
"/",
$root,
sub {
my ($path, $node) = @_; my ($path, $node) = @_;
foreach my $type (qw(user group token)) { foreach my $type (qw(user group token)) {
my $d = $node->{"${type}s"}; my $d = $node->{"${type}s"};
next if !$d; next if !$d;
next if !($audit || $rpcenv->check_perm_modify($authuser, $path, 1)); next if !($audit || $rpcenv->check_perm_modify($authuser, $path, 1));
foreach my $id (keys %$d) { foreach my $id (keys %$d) {
foreach my $role (keys %{$d->{$id}}) { foreach my $role (keys %{ $d->{$id} }) {
my $propagate = $d->{$id}->{$role}; my $propagate = $d->{$id}->{$role};
push @$res, { push @$res,
{
path => $path, path => $path,
type => $type, type => $type,
ugid => $id, ugid => $id,
@ -86,12 +97,14 @@ __PACKAGE__->register_method ({
} }
} }
} }
}); },
);
return $res; return $res;
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'update_acl', name => 'update_acl',
protected => 1, protected => 1,
path => '', path => '',
@ -107,22 +120,26 @@ __PACKAGE__->register_method ({
path => get_standard_option('acl-path'), path => get_standard_option('acl-path'),
users => { users => {
description => "List of users.", description => "List of users.",
type => 'string', format => 'pve-userid-list', type => 'string',
format => 'pve-userid-list',
optional => 1, optional => 1,
}, },
groups => { groups => {
description => "List of groups.", description => "List of groups.",
type => 'string', format => 'pve-groupid-list', type => 'string',
format => 'pve-groupid-list',
optional => 1, optional => 1,
}, },
tokens => { tokens => {
description => "List of API tokens.", description => "List of API tokens.",
type => 'string', format => 'pve-tokenid-list', type => 'string',
format => 'pve-tokenid-list',
optional => 1, optional => 1,
}, },
roles => { roles => {
description => "List of roles.", description => "List of roles.",
type => 'string', format => 'pve-roleid-list', type => 'string',
format => 'pve-roleid-list',
}, },
delete => { delete => {
description => "Remove permissions (instead of adding it).", description => "Remove permissions (instead of adding it).",
@ -136,7 +153,10 @@ __PACKAGE__->register_method ({
my ($param) = @_; my ($param) = @_;
if (!($param->{users} || $param->{groups} || $param->{tokens})) { if (!($param->{users} || $param->{groups} || $param->{tokens})) {
raise_param_exc({ map { $_ => "either 'users', 'groups' or 'tokens' is required." } qw(users groups tokens) }); raise_param_exc({
map { $_ => "either 'users', 'groups' or 'tokens' is required." }
qw(users groups tokens)
});
} }
my $path = PVE::AccessControl::normalize_path($param->{path}); my $path = PVE::AccessControl::normalize_path($param->{path});
@ -173,17 +193,32 @@ __PACKAGE__->register_method ({
my $role_privs = $cfg->{roles}->{$role}; my $role_privs = $cfg->{roles}->{$role};
my $verb = $param->{delete} ? 'remove' : 'add'; my $verb = $param->{delete} ? 'remove' : 'add';
foreach my $priv (keys $role_privs->%*) { foreach my $priv (keys $role_privs->%*) {
raise_param_exc({ role => "Cannot $verb role '$role' - requires 'Permissions.Modify' or superset of privileges." }) raise_param_exc(
if !defined($auth_user_privs->{$priv}); {
role =>
"Cannot $verb role '$role' - requires 'Permissions.Modify' or superset of privileges.",
},
) if !defined($auth_user_privs->{$priv});
# propagation is only potentially problematic for adding ACLs, not removing.. # propagation is only potentially problematic for adding ACLs, not removing..
raise_param_exc({ role => "Cannot $verb role '$role' with propagation - requires 'Permissions.Modify' or propagated superset of privileges." }) raise_param_exc(
if $propagate && $auth_user_privs->{$priv} != $propagate && !$param->{delete}; {
role =>
"Cannot $verb role '$role' with propagation - requires 'Permissions.Modify' or propagated superset of privileges.",
},
)
if $propagate
&& $auth_user_privs->{$priv} != $propagate
&& !$param->{delete};
} }
# NoAccess has no privs, needs an explicit check # NoAccess has no privs, needs an explicit check
raise_param_exc({ role => "Cannot $verb role '$role' - requires 'Permissions.Modify'"}) raise_param_exc(
if $role eq 'NoAccess'; {
role =>
"Cannot $verb role '$role' - requires 'Permissions.Modify'",
},
) if $role eq 'NoAccess';
} }
foreach my $group (split_list($param->{groups})) { foreach my $group (split_list($param->{groups})) {
@ -205,7 +240,7 @@ __PACKAGE__->register_method ({
if !$cfg->{users}->{$username}; if !$cfg->{users}->{$username};
if ($param->{delete}) { if ($param->{delete}) {
delete ($node->{users}->{$username}->{$role}); delete($node->{users}->{$username}->{$role});
} else { } else {
$node->{users}->{$username}->{$role} = $propagate; $node->{users}->{$username}->{$role} = $propagate;
} }
@ -224,9 +259,12 @@ __PACKAGE__->register_method ({
} }
cfs_write_file("user.cfg", $cfg); cfs_write_file("user.cfg", $cfg);
}, "ACL update failed"); },
"ACL update failed",
);
return undef; return undef;
}}); },
});
1; 1;

View File

@ -32,42 +32,42 @@ eval {
use base qw(PVE::RESTHandler); use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
subclass => "PVE::API2::User", subclass => "PVE::API2::User",
path => 'users', path => 'users',
}); });
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
subclass => "PVE::API2::Group", subclass => "PVE::API2::Group",
path => 'groups', path => 'groups',
}); });
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
subclass => "PVE::API2::Role", subclass => "PVE::API2::Role",
path => 'roles', path => 'roles',
}); });
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
subclass => "PVE::API2::ACL", subclass => "PVE::API2::ACL",
path => 'acl', path => 'acl',
}); });
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
subclass => "PVE::API2::Domains", subclass => "PVE::API2::Domains",
path => 'domains', path => 'domains',
}); });
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
subclass => "PVE::API2::OpenId", subclass => "PVE::API2::OpenId",
path => 'openid', path => 'openid',
}); });
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
subclass => "PVE::API2::TFA", subclass => "PVE::API2::TFA",
path => 'tfa', path => 'tfa',
}); });
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'index', name => 'index',
path => '', path => '',
method => 'GET', method => 'GET',
@ -87,7 +87,7 @@ __PACKAGE__->register_method ({
subdir => { type => 'string' }, subdir => { type => 'string' },
}, },
}, },
links => [ { rel => 'child', href => "{subdir}" } ], links => [{ rel => 'child', href => "{subdir}" }],
}, },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
@ -108,8 +108,8 @@ __PACKAGE__->register_method ({
push @$res, { subdir => 'password' }; push @$res, { subdir => 'password' };
return $res; return $res;
}}); },
});
my sub verify_auth : prototype($$$$$$) { my sub verify_auth : prototype($$$$$$) {
my ($rpcenv, $username, $pw_or_ticket, $otp, $path, $privs) = @_; my ($rpcenv, $username, $pw_or_ticket, $otp, $path, $privs) = @_;
@ -118,26 +118,26 @@ my sub verify_auth : prototype($$$$$$) {
die "invalid path - $path\n" if defined($path) && !defined($normpath); die "invalid path - $path\n" if defined($path) && !defined($normpath);
my $ticketuser; my $ticketuser;
if (($ticketuser = PVE::AccessControl::verify_ticket($pw_or_ticket, 1)) && if (
($ticketuser eq $username)) { ($ticketuser = PVE::AccessControl::verify_ticket($pw_or_ticket, 1))
&& ($ticketuser eq $username)
) {
# valid ticket # valid ticket
} elsif (PVE::AccessControl::verify_vnc_ticket($pw_or_ticket, $username, $normpath, 1)) { } elsif (PVE::AccessControl::verify_vnc_ticket($pw_or_ticket, $username, $normpath, 1)) {
# valid vnc ticket # valid vnc ticket
} else { } else {
$username = PVE::AccessControl::authenticate_user( $username = PVE::AccessControl::authenticate_user(
$username, $username, $pw_or_ticket, $otp,
$pw_or_ticket,
$otp,
); );
} }
my $privlist = [ PVE::Tools::split_list($privs) ]; my $privlist = [PVE::Tools::split_list($privs)];
if (!($normpath && scalar(@$privlist) && $rpcenv->check($username, $normpath, $privlist))) { if (!($normpath && scalar(@$privlist) && $rpcenv->check($username, $normpath, $privlist))) {
die "no permission ($path, $privs)\n"; die "no permission ($path, $privs)\n";
} }
return { username => $username }; return { username => $username };
}; }
my sub create_ticket_do : prototype($$$$$) { my sub create_ticket_do : prototype($$$$$) {
my ($rpcenv, $username, $pw_or_ticket, $otp, $tfa_challenge) = @_; my ($rpcenv, $username, $pw_or_ticket, $otp, $tfa_challenge) = @_;
@ -160,10 +160,7 @@ my sub create_ticket_do : prototype($$$$$) {
# valid ticket. Note: root@pam can create tickets for other users # valid ticket. Note: root@pam can create tickets for other users
} else { } else {
($username, $tfa_info) = PVE::AccessControl::authenticate_user( ($username, $tfa_info) = PVE::AccessControl::authenticate_user(
$username, $username, $pw_or_ticket, $otp, $tfa_challenge,
$pw_or_ticket,
$otp,
$tfa_challenge,
); );
} }
@ -185,9 +182,9 @@ my sub create_ticket_do : prototype($$$$$) {
CSRFPreventionToken => $csrftoken, CSRFPreventionToken => $csrftoken,
%extra, %extra,
}; };
}; }
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'get_ticket', name => 'get_ticket',
path => 'ticket', path => 'ticket',
method => 'GET', method => 'GET',
@ -197,15 +194,16 @@ __PACKAGE__->register_method ({
additionalProperties => 0, additionalProperties => 0,
}, },
returns => { type => "null" }, returns => { type => "null" },
code => sub { return undef; }}); code => sub { return undef; },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'create_ticket', name => 'create_ticket',
path => 'ticket', path => 'ticket',
method => 'POST', method => 'POST',
permissions => { permissions => {
description => "You need to pass valid credientials.", description => "You need to pass valid credientials.",
user => 'world' user => 'world',
}, },
protected => 1, # else we can't access shadow files protected => 1, # else we can't access shadow files
allowtoken => 0, # we don't want tokens to create tickets allowtoken => 0, # we don't want tokens to create tickets
@ -219,12 +217,16 @@ __PACKAGE__->register_method ({
maxLength => 64, maxLength => 64,
completion => \&PVE::AccessControl::complete_username, completion => \&PVE::AccessControl::complete_username,
}, },
realm => get_standard_option('realm', { realm => get_standard_option(
description => "You can optionally pass the realm using this parameter. Normally" 'realm',
." the realm is simply added to the username <username>\@<realm>.", {
description =>
"You can optionally pass the realm using this parameter. Normally"
. " the realm is simply added to the username <username>\@<realm>.",
optional => 1, optional => 1,
completion => \&PVE::AccessControl::complete_realm, completion => \&PVE::AccessControl::complete_realm,
}), },
),
password => { password => {
description => "The secret password. This can also be a valid ticket.", description => "The secret password. This can also be a valid ticket.",
type => 'string', type => 'string',
@ -243,7 +245,8 @@ __PACKAGE__->register_method ({
}, },
privs => { privs => {
description => "Verify ticket, and check if user have access 'privs' on 'path'", description => "Verify ticket, and check if user have access 'privs' on 'path'",
type => 'string' , format => 'pve-priv-list', type => 'string',
format => 'pve-priv-list',
requires => 'path', requires => 'path',
optional => 1, optional => 1,
maxLength => 64, maxLength => 64,
@ -259,17 +262,17 @@ __PACKAGE__->register_method ({
description => "The signed TFA challenge string the user wants to respond to.", description => "The signed TFA challenge string the user wants to respond to.",
optional => 1, optional => 1,
}, },
} },
}, },
returns => { returns => {
type => "object", type => "object",
properties => { properties => {
username => { type => 'string' }, username => { type => 'string' },
ticket => { type => 'string', optional => 1}, ticket => { type => 'string', optional => 1 },
CSRFPreventionToken => { type => 'string', optional => 1 }, CSRFPreventionToken => { type => 'string', optional => 1 },
clustername => { type => 'string', optional => 1 }, clustername => { type => 'string', optional => 1 },
# cap => computed api permissions, unless there's a u2f challenge # cap => computed api permissions, unless there's a u2f challenge
} },
}, },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
@ -286,8 +289,14 @@ __PACKAGE__->register_method ({
$rpcenv->check_user_enabled($username); $rpcenv->check_user_enabled($username);
if ($param->{path} && $param->{privs}) { if ($param->{path} && $param->{privs}) {
$res = verify_auth($rpcenv, $username, $param->{password}, $param->{otp}, $res = verify_auth(
$param->{path}, $param->{privs}); $rpcenv,
$username,
$param->{password},
$param->{otp},
$param->{path},
$param->{privs},
);
} else { } else {
$res = create_ticket_do( $res = create_ticket_do(
$rpcenv, $rpcenv,
@ -316,24 +325,27 @@ __PACKAGE__->register_method ({
PVE::Cluster::log_msg('info', 'root@pam', "successful auth for user '$username'"); PVE::Cluster::log_msg('info', 'root@pam', "successful auth for user '$username'");
return $res; return $res;
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'change_password', name => 'change_password',
path => 'password', path => 'password',
method => 'PUT', method => 'PUT',
permissions => { permissions => {
description => "Each user is allowed to change their own password. A user can change the" description =>
." password of another user if they have 'Realm.AllocateUser' (on the realm of user" "Each user is allowed to change their own password. A user can change the"
." <userid>) and 'User.Modify' permission on /access/groups/<group> on a group where" . " password of another user if they have 'Realm.AllocateUser' (on the realm of user"
." user <userid> is member of. For the PAM realm, a password change does not take " . " <userid>) and 'User.Modify' permission on /access/groups/<group> on a group where"
." effect cluster-wide, but only applies to the local node.", . " user <userid> is member of. For the PAM realm, a password change does not take "
check => [ 'or', . " effect cluster-wide, but only applies to the local node.",
check => [
'or',
['userid-param', 'self'], ['userid-param', 'self'],
[ 'and', [
[ 'userid-param', 'Realm.AllocateUser'], 'and', ['userid-param', 'Realm.AllocateUser'],
[ 'userid-group', ['User.Modify']] ['userid-group', ['User.Modify']],
] ],
], ],
}, },
protected => 1, # else we can't access shadow files protected => 1, # else we can't access shadow files
@ -350,7 +362,7 @@ __PACKAGE__->register_method ({
maxLength => 64, maxLength => 64,
}, },
'confirmation-password' => $PVE::API2::TFA::OPTIONAL_PASSWORD_SCHEMA, 'confirmation-password' => $PVE::API2::TFA::OPTIONAL_PASSWORD_SCHEMA,
} },
}, },
returns => { type => "null" }, returns => { type => "null" },
code => sub { code => sub {
@ -385,7 +397,8 @@ __PACKAGE__->register_method ({
PVE::Cluster::log_msg('info', 'root@pam', "changed password for user '$userid'"); PVE::Cluster::log_msg('info', 'root@pam', "changed password for user '$userid'");
return undef; return undef;
}}); },
});
sub get_u2f_config() { sub get_u2f_config() {
die "u2f support not available\n" if !$u2f_available; die "u2f support not available\n" if !$u2f_available;
@ -458,16 +471,16 @@ sub verify_user_tfa_config {
PVE::OTP::oath_verify_otp($value, $secret, $step, $digits); PVE::OTP::oath_verify_otp($value, $secret, $step, $digits);
} }
__PACKAGE__->register_method({ __PACKAGE__->register_method({
name => 'permissions', name => 'permissions',
path => 'permissions', path => 'permissions',
method => 'GET', method => 'GET',
description => 'Retrieve effective permissions of given user/token.', description => 'Retrieve effective permissions of given user/token.',
permissions => { permissions => {
description => "Each user/token is allowed to dump their own permissions (or that of owned" description =>
." tokens). A user can dump the permissions of another user or their tokens if they" "Each user/token is allowed to dump their own permissions (or that of owned"
." have 'Sys.Audit' permission on /access.", . " tokens). A user can dump the permissions of another user or their tokens if they"
. " have 'Sys.Audit' permission on /access.",
user => 'all', user => 'all',
}, },
parameters => { parameters => {
@ -479,10 +492,13 @@ __PACKAGE__->register_method({
pattern => $PVE::AccessControl::userid_or_token_regex, pattern => $PVE::AccessControl::userid_or_token_regex,
optional => 1, optional => 1,
}, },
path => get_standard_option('acl-path', { path => get_standard_option(
'acl-path',
{
description => "Only dump this specific path, not the whole tree.", description => "Only dump this specific path, not the whole tree.",
optional => 1, optional => 1,
}), },
),
}, },
}, },
returns => { returns => {
@ -519,6 +535,7 @@ __PACKAGE__->register_method({
} }
return $res; return $res;
}}); },
});
1; 1;

View File

@ -48,16 +48,18 @@ my $map_sync_default_options = sub {
my $new_opt = $map_remove_vanished->($old_opt, $delete_deprecated); my $new_opt = $map_remove_vanished->($old_opt, $delete_deprecated);
$cfg->{'sync-defaults-options'} = PVE::JSONSchema::print_property_string($new_opt, $sync_opts_fmt); $cfg->{'sync-defaults-options'} =
PVE::JSONSchema::print_property_string($new_opt, $sync_opts_fmt);
}; };
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'index', name => 'index',
path => '', path => '',
method => 'GET', method => 'GET',
description => "Authentication domain index.", description => "Authentication domain index.",
permissions => { permissions => {
description => "Anyone can access that, because we need that list for the login box (before the user is authenticated).", description =>
"Anyone can access that, because we need that list for the login box (before the user is authenticated).",
user => 'world', user => 'world',
}, },
parameters => { parameters => {
@ -74,17 +76,18 @@ __PACKAGE__->register_method ({
tfa => { tfa => {
description => "Two-factor authentication provider.", description => "Two-factor authentication provider.",
type => 'string', type => 'string',
enum => [ 'yubico', 'oath' ], enum => ['yubico', 'oath'],
optional => 1, optional => 1,
}, },
comment => { comment => {
description => "A comment. The GUI use this text when you select a domain (Realm) on the login window.", description =>
"A comment. The GUI use this text when you select a domain (Realm) on the login window.",
type => 'string', type => 'string',
optional => 1, optional => 1,
}, },
}, },
}, },
links => [ { rel => 'child', href => "{realm}" } ], links => [{ rel => 'child', href => "{realm}" }],
}, },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
@ -106,9 +109,10 @@ __PACKAGE__->register_method ({
} }
return $res; return $res;
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'create', name => 'create',
protected => 1, protected => 1,
path => '', path => '',
@ -117,14 +121,17 @@ __PACKAGE__->register_method ({
check => ['perm', '/access/realm', ['Realm.Allocate']], check => ['perm', '/access/realm', ['Realm.Allocate']],
}, },
description => "Add an authentication server.", description => "Add an authentication server.",
parameters => PVE::Auth::Plugin->createSchema(0, { parameters => PVE::Auth::Plugin->createSchema(
0,
{
'check-connection' => { 'check-connection' => {
description => 'Check bind connection to the server.', description => 'Check bind connection to the server.',
type => 'boolean', type => 'boolean',
optional => 1, optional => 1,
default => 0, default => 0,
}, },
}), },
),
returns => { type => 'null' }, returns => { type => 'null' },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
@ -151,7 +158,8 @@ __PACKAGE__->register_method ({
die "unable to create builtin type '$type'\n" die "unable to create builtin type '$type'\n"
if ($type eq 'pam' || $type eq 'pve'); if ($type eq 'pam' || $type eq 'pve');
die "'check-connection' parameter can only be set for realms of type 'ldap' or 'ad'\n" die
"'check-connection' parameter can only be set for realms of type 'ldap' or 'ad'\n"
if defined($check_connection) && !($type eq 'ldap' || $type eq 'ad'); if defined($check_connection) && !($type eq 'ldap' || $type eq 'ad');
if ($type eq 'ad' || $type eq 'ldap') { if ($type eq 'ad' || $type eq 'ldap') {
@ -181,12 +189,15 @@ __PACKAGE__->register_method ({
if $check_connection; if $check_connection;
cfs_write_file($domainconfigfile, $cfg); cfs_write_file($domainconfigfile, $cfg);
}, "add auth server failed"); },
"add auth server failed",
);
return undef; return undef;
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'update', name => 'update',
path => '{realm}', path => '{realm}',
method => 'PUT', method => 'PUT',
@ -195,14 +206,17 @@ __PACKAGE__->register_method ({
}, },
description => "Update authentication server settings.", description => "Update authentication server settings.",
protected => 1, protected => 1,
parameters => PVE::Auth::Plugin->updateSchema(0, { parameters => PVE::Auth::Plugin->updateSchema(
0,
{
'check-connection' => { 'check-connection' => {
description => 'Check bind connection to the server.', description => 'Check bind connection to the server.',
type => 'boolean', type => 'boolean',
optional => 1, optional => 1,
default => 0, default => 0,
}, },
}), },
),
returns => { type => 'null' }, returns => { type => 'null' },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
@ -226,7 +240,8 @@ __PACKAGE__->register_method ({
die "domain '$realm' does not exist\n" die "domain '$realm' does not exist\n"
if !$ids->{$realm}; if !$ids->{$realm};
die "'check-connection' parameter can only be set for realms of type 'ldap' or 'ad'\n" die
"'check-connection' parameter can only be set for realms of type 'ldap' or 'ad'\n"
if defined($check_connection) && !($type eq 'ldap' || $type eq 'ad'); if defined($check_connection) && !($type eq 'ldap' || $type eq 'ad');
my $delete_str = extract_param($param, 'delete'); my $delete_str = extract_param($param, 'delete');
@ -268,13 +283,16 @@ __PACKAGE__->register_method ({
if $check_connection; if $check_connection;
cfs_write_file($domainconfigfile, $cfg); cfs_write_file($domainconfigfile, $cfg);
}, "update auth server failed"); },
"update auth server failed",
);
return undef; return undef;
}}); },
});
# fixme: return format! # fixme: return format!
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'read', name => 'read',
path => '{realm}', path => '{realm}',
method => 'GET', method => 'GET',
@ -307,10 +325,10 @@ __PACKAGE__->register_method ({
$data->{digest} = $cfg->{digest}; $data->{digest} = $cfg->{digest};
return $data; return $data;
}}); },
});
__PACKAGE__->register_method({
__PACKAGE__->register_method ({
name => 'delete', name => 'delete',
path => '{realm}', path => '{realm}',
method => 'DELETE', method => 'DELETE',
@ -323,7 +341,7 @@ __PACKAGE__->register_method ({
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
realm => get_standard_option('realm'), realm => get_standard_option('realm'),
} },
}, },
returns => { type => 'null' }, returns => { type => 'null' },
code => sub { code => sub {
@ -345,10 +363,13 @@ __PACKAGE__->register_method ({
delete $ids->{$realm}; delete $ids->{$realm};
cfs_write_file($domainconfigfile, $cfg); cfs_write_file($domainconfigfile, $cfg);
}, "delete auth server failed"); },
"delete auth server failed",
);
return undef; return undef;
}}); },
});
my $update_users = sub { my $update_users = sub {
my ($usercfg, $realm, $synced_users, $opts) = @_; my ($usercfg, $realm, $synced_users, $opts) = @_;
@ -474,31 +495,35 @@ my $parse_sync_opts = sub {
# only scope has no implicit value # only scope has no implicit value
raise_param_exc({ raise_param_exc({
"scope" => 'Not passed as parameter and not defined in realm default sync options.' "scope" => 'Not passed as parameter and not defined in realm default sync options.',
}) if !defined($res->{scope}); })
if !defined($res->{scope});
return $res; return $res;
}; };
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'sync', name => 'sync',
path => '{realm}/sync', path => '{realm}/sync',
method => 'POST', method => 'POST',
permissions => { permissions => {
description => "'Realm.AllocateUser' on '/access/realm/<realm>' and " description => "'Realm.AllocateUser' on '/access/realm/<realm>' and "
." 'User.Modify' permissions to '/access/groups/'.", . " 'User.Modify' permissions to '/access/groups/'.",
check => [ 'and', check => [
'and',
['perm', '/access/realm/{realm}', ['Realm.AllocateUser']], ['perm', '/access/realm/{realm}', ['Realm.AllocateUser']],
['perm', '/access/groups', ['User.Modify']], ['perm', '/access/groups', ['User.Modify']],
], ],
}, },
description => "Syncs users and/or groups from the configured LDAP to user.cfg." description => "Syncs users and/or groups from the configured LDAP to user.cfg."
." NOTE: Synced groups will have the name 'name-\$realm', so make sure" . " NOTE: Synced groups will have the name 'name-\$realm', so make sure"
." those groups do not exist to prevent overwriting.", . " those groups do not exist to prevent overwriting.",
protected => 1, protected => 1,
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => get_standard_option('realm-sync-options', { properties => get_standard_option(
'realm-sync-options',
{
realm => get_standard_option('realm'), realm => get_standard_option('realm'),
'dry-run' => { 'dry-run' => {
description => "If set, does not write anything.", description => "If set, does not write anything.",
@ -506,11 +531,12 @@ __PACKAGE__->register_method ({
optional => 1, optional => 1,
default => 0, default => 0,
}, },
}), },
),
}, },
returns => { returns => {
description => 'Worker Task-UPID', description => 'Worker Task-UPID',
type => 'string' type => 'string',
}, },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
@ -547,7 +573,8 @@ __PACKAGE__->register_method ({
$synced_groups = $plugin->get_groups($realmconfig, $realm, $dnmap); $synced_groups = $plugin->get_groups($realmconfig, $realm, $dnmap);
} }
PVE::AccessControl::lock_user_config(sub { PVE::AccessControl::lock_user_config(
sub {
my $usercfg = cfs_read_file("user.cfg"); my $usercfg = cfs_read_file("user.cfg");
print "got data from server, updating $whatstring\n"; print "got data from server, updating $whatstring\n";
@ -560,16 +587,20 @@ __PACKAGE__->register_method ({
} }
if ($dry_run) { if ($dry_run) {
print "\nNOTE: Dry test run, changes were NOT written to the configuration.\n"; print
"\nNOTE: Dry test run, changes were NOT written to the configuration.\n";
return; return;
} }
cfs_write_file("user.cfg", $usercfg); cfs_write_file("user.cfg", $usercfg);
print "successfully updated $whatstring configuration\n"; print "successfully updated $whatstring configuration\n";
}, "syncing $whatstring failed"); },
"syncing $whatstring failed",
);
}; };
my $workerid = !$dry_run ? 'auth-realm-sync' : 'auth-realm-sync-test'; my $workerid = !$dry_run ? 'auth-realm-sync' : 'auth-realm-sync-test';
return $rpcenv->fork_worker($workerid, $realm, $authuser, $worker); return $rpcenv->fork_worker($workerid, $realm, $authuser, $worker);
}}); },
});
1; 1;

View File

@ -10,21 +10,25 @@ use PVE::JSONSchema qw(get_standard_option register_standard_option);
use base qw(PVE::RESTHandler); use base qw(PVE::RESTHandler);
register_standard_option('group-id', { register_standard_option(
'group-id',
{
type => 'string', type => 'string',
format => 'pve-groupid', format => 'pve-groupid',
completion => \&PVE::AccessControl::complete_group, completion => \&PVE::AccessControl::complete_group,
}); },
);
register_standard_option('group-comment', { type => 'string', optional => 1 }); register_standard_option('group-comment', { type => 'string', optional => 1 });
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'index', name => 'index',
path => '', path => '',
method => 'GET', method => 'GET',
description => "Group index.", description => "Group index.",
permissions => { permissions => {
description => "The returned list is restricted to groups where you have 'User.Modify', 'Sys.Audit' or 'Group.Allocate' permissions on /access/groups/<group>.", description =>
"The returned list is restricted to groups where you have 'User.Modify', 'Sys.Audit' or 'Group.Allocate' permissions on /access/groups/<group>.",
user => 'all', user => 'all',
}, },
parameters => { parameters => {
@ -46,7 +50,7 @@ __PACKAGE__->register_method ({
}, },
}, },
}, },
links => [ { rel => 'child', href => "{groupid}" } ], links => [{ rel => 'child', href => "{groupid}" }],
}, },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
@ -57,21 +61,23 @@ __PACKAGE__->register_method ({
my $usercfg = cfs_read_file("user.cfg"); my $usercfg = cfs_read_file("user.cfg");
my $authuser = $rpcenv->get_user(); my $authuser = $rpcenv->get_user();
my $privs = [ 'User.Modify', 'Sys.Audit', 'Group.Allocate']; my $privs = ['User.Modify', 'Sys.Audit', 'Group.Allocate'];
foreach my $group (keys %{$usercfg->{groups}}) { foreach my $group (keys %{ $usercfg->{groups} }) {
next if !$rpcenv->check_any($authuser, "/access/groups/$group", $privs, 1); next if !$rpcenv->check_any($authuser, "/access/groups/$group", $privs, 1);
my $data = $usercfg->{groups}->{$group}; my $data = $usercfg->{groups}->{$group};
my $entry = { groupid => $group }; my $entry = { groupid => $group };
$entry->{comment} = $data->{comment} if defined($data->{comment}); $entry->{comment} = $data->{comment} if defined($data->{comment});
$entry->{users} = join (',', sort keys %{$data->{users}}) if defined($data->{users}); $entry->{users} = join(',', sort keys %{ $data->{users} })
if defined($data->{users});
push @$res, $entry; push @$res, $entry;
} }
return $res; return $res;
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'create_group', name => 'create_group',
protected => 1, protected => 1,
path => '', path => '',
@ -103,16 +109,19 @@ __PACKAGE__->register_method ({
$usercfg->{groups}->{$group} = { users => {} }; $usercfg->{groups}->{$group} = { users => {} };
$usercfg->{groups}->{$group}->{comment} = $param->{comment} if $param->{comment}; $usercfg->{groups}->{$group}->{comment} = $param->{comment}
if $param->{comment};
cfs_write_file("user.cfg", $usercfg); cfs_write_file("user.cfg", $usercfg);
}, "create group failed"); },
"create group failed",
);
return undef; return undef;
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'update_group', name => 'update_group',
protected => 1, protected => 1,
path => '{groupid}', path => '{groupid}',
@ -147,12 +156,15 @@ __PACKAGE__->register_method ({
$data->{comment} = $param->{comment} if defined($param->{comment}); $data->{comment} = $param->{comment} if defined($param->{comment});
cfs_write_file("user.cfg", $usercfg); cfs_write_file("user.cfg", $usercfg);
}, "update group failed"); },
"update group failed",
);
return undef; return undef;
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'read_group', name => 'read_group',
path => '{groupid}', path => '{groupid}',
method => 'GET', method => 'GET',
@ -173,7 +185,7 @@ __PACKAGE__->register_method ({
comment => get_standard_option('group-comment'), comment => get_standard_option('group-comment'),
members => { members => {
type => 'array', type => 'array',
items => get_standard_option('userid-completed') items => get_standard_option('userid-completed'),
}, },
}, },
}, },
@ -188,17 +200,17 @@ __PACKAGE__->register_method ({
die "group '$group' does not exist\n" if !$data; die "group '$group' does not exist\n" if !$data;
my $members = $data->{users} ? [ keys %{$data->{users}} ] : []; my $members = $data->{users} ? [keys %{ $data->{users} }] : [];
my $res = { members => $members }; my $res = { members => $members };
$res->{comment} = $data->{comment} if defined($data->{comment}); $res->{comment} = $data->{comment} if defined($data->{comment});
return $res; return $res;
}}); },
});
__PACKAGE__->register_method({
__PACKAGE__->register_method ({
name => 'delete_group', name => 'delete_group',
protected => 1, protected => 1,
path => '{groupid}', path => '{groupid}',
@ -211,7 +223,7 @@ __PACKAGE__->register_method ({
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
groupid => get_standard_option('group-id'), groupid => get_standard_option('group-id'),
} },
}, },
returns => { type => 'null' }, returns => { type => 'null' },
code => sub { code => sub {
@ -227,14 +239,17 @@ __PACKAGE__->register_method ({
die "group '$group' does not exist\n" die "group '$group' does not exist\n"
if !$usercfg->{groups}->{$group}; if !$usercfg->{groups}->{$group};
delete ($usercfg->{groups}->{$group}); delete($usercfg->{groups}->{$group});
PVE::AccessControl::delete_group_acl($group, $usercfg); PVE::AccessControl::delete_group_acl($group, $usercfg);
cfs_write_file("user.cfg", $usercfg); cfs_write_file("user.cfg", $usercfg);
}, "delete group failed"); },
"delete group failed",
);
return undef; return undef;
}}); },
});
1; 1;

View File

@ -31,7 +31,7 @@ my $get_cluster_last_run = sub {
return undef; return undef;
}; };
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'syncjob_index', name => 'syncjob_index',
path => '', path => '',
method => 'GET', method => 'GET',
@ -50,7 +50,7 @@ __PACKAGE__->register_method ({
properties => { properties => {
id => { id => {
description => "The ID of the entry.", description => "The ID of the entry.",
type => 'string' type => 'string',
}, },
enabled => { enabled => {
description => "If the job is enabled or not.", description => "If the job is enabled or not.",
@ -69,18 +69,20 @@ __PACKAGE__->register_method ({
scope => get_standard_option('sync-scope'), scope => get_standard_option('sync-scope'),
'remove-vanished' => get_standard_option('sync-remove-vanished'), 'remove-vanished' => get_standard_option('sync-remove-vanished'),
'last-run' => { 'last-run' => {
description => "Last execution time of the job in seconds since the beginning of the UNIX epoch", description =>
"Last execution time of the job in seconds since the beginning of the UNIX epoch",
type => 'integer', type => 'integer',
optional => 1, optional => 1,
}, },
'next-run' => { 'next-run' => {
description => "Next planned execution time of the job in seconds since the beginning of the UNIX epoch.", description =>
"Next planned execution time of the job in seconds since the beginning of the UNIX epoch.",
type => 'integer', type => 'integer',
optional => 1, optional => 1,
}, },
}, },
}, },
links => [ { rel => 'child', href => "{id}" } ], links => [{ rel => 'child', href => "{id}" }],
}, },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
@ -111,7 +113,8 @@ __PACKAGE__->register_method ({
} }
return $res; return $res;
}}); },
});
__PACKAGE__->register_method({ __PACKAGE__->register_method({
name => 'read_job', name => 'read_job',
@ -143,7 +146,8 @@ __PACKAGE__->register_method({
raise_param_exc({ id => "No such job '$id'" }); raise_param_exc({ id => "No such job '$id'" });
}}); },
});
__PACKAGE__->register_method({ __PACKAGE__->register_method({
name => 'create_job', name => 'create_job',
@ -153,8 +157,9 @@ __PACKAGE__->register_method({
description => "Create new realm-sync job.", description => "Create new realm-sync job.",
permissions => { permissions => {
description => "'Realm.AllocateUser' on '/access/realm/<realm>' and " description => "'Realm.AllocateUser' on '/access/realm/<realm>' and "
."'User.Modify' permissions to '/access/groups/'.", . "'User.Modify' permissions to '/access/groups/'.",
check => [ 'and', check => [
'and',
['perm', '/access/realm/{realm}', ['Realm.AllocateUser']], ['perm', '/access/realm/{realm}', ['Realm.AllocateUser']],
['perm', '/access/groups', ['User.Modify']], ['perm', '/access/groups', ['User.Modify']],
], ],
@ -166,7 +171,10 @@ __PACKAGE__->register_method({
my $id = extract_param($param, 'id'); my $id = extract_param($param, 'id');
cfs_lock_file('jobs.cfg', undef, sub { cfs_lock_file(
'jobs.cfg',
undef,
sub {
my $data = cfs_read_file('jobs.cfg'); my $data = cfs_read_file('jobs.cfg');
die "Job '$id' already exists\n" die "Job '$id' already exists\n"
@ -188,11 +196,13 @@ __PACKAGE__->register_method({
$data->{ids}->{$id} = $opts; $data->{ids}->{$id} = $opts;
cfs_write_file('jobs.cfg', $data); cfs_write_file('jobs.cfg', $data);
}); },
);
die "$@" if ($@); die "$@" if ($@);
return undef; return undef;
}}); },
});
__PACKAGE__->register_method({ __PACKAGE__->register_method({
name => 'update_job', name => 'update_job',
@ -202,8 +212,9 @@ __PACKAGE__->register_method({
description => "Update realm-sync job definition.", description => "Update realm-sync job definition.",
permissions => { permissions => {
description => "'Realm.AllocateUser' on '/access/realm/<realm>' and 'User.Modify'" description => "'Realm.AllocateUser' on '/access/realm/<realm>' and 'User.Modify'"
." permissions to '/access/groups/'.", . " permissions to '/access/groups/'.",
check => [ 'and', check => [
'and',
['perm', '/access/realm/{realm}', ['Realm.AllocateUser']], ['perm', '/access/realm/{realm}', ['Realm.AllocateUser']],
['perm', '/access/groups', ['User.Modify']], ['perm', '/access/groups', ['User.Modify']],
], ],
@ -219,7 +230,10 @@ __PACKAGE__->register_method({
die "no job options specified\n" if !scalar(keys %$param); die "no job options specified\n" if !scalar(keys %$param);
cfs_lock_file('jobs.cfg', undef, sub { cfs_lock_file(
'jobs.cfg',
undef,
sub {
my $jobs = cfs_read_file('jobs.cfg'); my $jobs = cfs_read_file('jobs.cfg');
my $plugin = PVE::Job::Registry->lookup('realm-sync'); my $plugin = PVE::Job::Registry->lookup('realm-sync');
@ -236,10 +250,11 @@ __PACKAGE__->register_method({
cfs_write_file('jobs.cfg', $jobs); cfs_write_file('jobs.cfg', $jobs);
return; return;
}); },
);
die "$@" if ($@); die "$@" if ($@);
}}); },
});
__PACKAGE__->register_method({ __PACKAGE__->register_method({
name => 'delete_job', name => 'delete_job',
@ -265,20 +280,28 @@ __PACKAGE__->register_method({
my $id = $param->{id}; my $id = $param->{id};
cfs_lock_file('jobs.cfg', undef, sub { cfs_lock_file(
'jobs.cfg',
undef,
sub {
my $jobs = cfs_read_file('jobs.cfg'); my $jobs = cfs_read_file('jobs.cfg');
if (!defined($jobs->{ids}->{$id}) || $jobs->{ids}->{$id}->{type} ne 'realm-sync') { if (
!defined($jobs->{ids}->{$id})
|| $jobs->{ids}->{$id}->{type} ne 'realm-sync'
) {
raise_param_exc({ id => "No such job '$id'" }); raise_param_exc({ id => "No such job '$id'" });
} }
delete $jobs->{ids}->{$id}; delete $jobs->{ids}->{$id};
cfs_write_file('jobs.cfg', $jobs); cfs_write_file('jobs.cfg', $jobs);
PVE::Jobs::RealmSync::save_state($id, undef); PVE::Jobs::RealmSync::save_state($id, undef);
}); },
);
die "$@" if $@; die "$@" if $@;
return undef; return undef;
}}); },
});
1; 1;

View File

@ -40,17 +40,17 @@ my $lookup_openid_auth = sub {
$openid_config->{prompt} = $config->{'prompt'} if defined($config->{'prompt'}); $openid_config->{prompt} = $config->{'prompt'} if defined($config->{'prompt'});
my $scopes = $config->{'scopes'} // 'email profile'; my $scopes = $config->{'scopes'} // 'email profile';
$openid_config->{scopes} = [ PVE::Tools::split_list($scopes) ]; $openid_config->{scopes} = [PVE::Tools::split_list($scopes)];
if (defined(my $acr = $config->{'acr-values'})) { if (defined(my $acr = $config->{'acr-values'})) {
$openid_config->{acr_values} = [ PVE::Tools::split_list($acr) ]; $openid_config->{acr_values} = [PVE::Tools::split_list($acr)];
} }
my $openid = PVE::RS::OpenId->discover($openid_config, $redirect_url); my $openid = PVE::RS::OpenId->discover($openid_config, $redirect_url);
return ($config, $openid); return ($config, $openid);
}; };
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'index', name => 'index',
path => '', path => '',
method => 'GET', method => 'GET',
@ -70,18 +70,18 @@ __PACKAGE__->register_method ({
subdir => { type => 'string' }, subdir => { type => 'string' },
}, },
}, },
links => [ { rel => 'child', href => "{subdir}" } ], links => [{ rel => 'child', href => "{subdir}" }],
}, },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
return [ return [
{ subdir => 'auth-url' }, { subdir => 'auth-url' }, { subdir => 'login' },
{ subdir => 'login' },
]; ];
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'auth_url', name => 'auth_url',
path => 'auth-url', path => 'auth-url',
method => 'POST', method => 'POST',
@ -92,7 +92,8 @@ __PACKAGE__->register_method ({
properties => { properties => {
realm => get_standard_option('realm'), realm => get_standard_option('realm'),
'redirect-url' => { 'redirect-url' => {
description => "Redirection Url. The client should set this to the used server url (location.origin).", description =>
"Redirection Url. The client should set this to the used server url (location.origin).",
type => 'string', type => 'string',
maxLength => 255, maxLength => 255,
}, },
@ -113,12 +114,13 @@ __PACKAGE__->register_method ({
my $redirect_url = extract_param($param, 'redirect-url'); my $redirect_url = extract_param($param, 'redirect-url');
my ($config, $openid) = $lookup_openid_auth->($realm, $redirect_url); my ($config, $openid) = $lookup_openid_auth->($realm, $redirect_url);
my $url = $openid->authorize_url($openid_state_path , $realm); my $url = $openid->authorize_url($openid_state_path, $realm);
return $url; return $url;
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'login', name => 'login',
path => 'login', path => 'login',
method => 'POST', method => 'POST',
@ -138,7 +140,8 @@ __PACKAGE__->register_method ({
maxLength => 4096, maxLength => 4096,
}, },
'redirect-url' => { 'redirect-url' => {
description => "Redirection Url. The client should set this to the used server url (location.origin).", description =>
"Redirection Url. The client should set this to the used server url (location.origin).",
type => 'string', type => 'string',
maxLength => 255, maxLength => 255,
}, },
@ -164,8 +167,8 @@ __PACKAGE__->register_method ({
my $dcconf = PVE::Cluster::cfs_read_file('datacenter.cfg'); my $dcconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
local $ENV{all_proxy} = $dcconf->{http_proxy} if exists $dcconf->{http_proxy}; local $ENV{all_proxy} = $dcconf->{http_proxy} if exists $dcconf->{http_proxy};
my ($realm, $private_auth_state) = PVE::RS::OpenId::verify_public_auth_state( my ($realm, $private_auth_state) =
$openid_state_path, $param->{'state'}); PVE::RS::OpenId::verify_public_auth_state($openid_state_path, $param->{'state'});
my $redirect_url = extract_param($param, 'redirect-url'); my $redirect_url = extract_param($param, 'redirect-url');
@ -200,10 +203,12 @@ __PACKAGE__->register_method ({
PVE::Auth::Plugin::verify_username($username); PVE::Auth::Plugin::verify_username($username);
if ($config->{'autocreate'} && !$rpcenv->check_user_exist($username, 1)) { if ($config->{'autocreate'} && !$rpcenv->check_user_exist($username, 1)) {
PVE::AccessControl::lock_user_config(sub { PVE::AccessControl::lock_user_config(
sub {
my $usercfg = cfs_read_file("user.cfg"); my $usercfg = cfs_read_file("user.cfg");
die "user '$username' already exists\n" if $usercfg->{users}->{$username}; die "user '$username' already exists\n"
if $usercfg->{users}->{$username};
my $entry = { enable => 1 }; my $entry = { enable => 1 };
if (defined(my $email = $info->{'email'})) { if (defined(my $email = $info->{'email'})) {
@ -219,7 +224,9 @@ __PACKAGE__->register_method ({
$usercfg->{users}->{$username} = $entry; $usercfg->{users}->{$username} = $entry;
cfs_write_file("user.cfg", $usercfg); cfs_write_file("user.cfg", $usercfg);
}, "autocreate openid user failed"); },
"autocreate openid user failed",
);
} else { } else {
# test if user exists and is enabled # test if user exists and is enabled
$rpcenv->check_user_enabled($username); $rpcenv->check_user_enabled($username);
@ -228,7 +235,8 @@ __PACKAGE__->register_method ({
if (defined(my $groups_claim = $config->{'groups-claim'})) { if (defined(my $groups_claim = $config->{'groups-claim'})) {
if (defined(my $groups_list = $info->{$groups_claim})) { if (defined(my $groups_list = $info->{$groups_claim})) {
if (ref($groups_list) eq 'ARRAY') { if (ref($groups_list) eq 'ARRAY') {
PVE::AccessControl::lock_user_config(sub { PVE::AccessControl::lock_user_config(
sub {
my $usercfg = cfs_read_file("user.cfg"); my $usercfg = cfs_read_file("user.cfg");
my $oidc_groups; my $oidc_groups;
@ -240,7 +248,7 @@ __PACKAGE__->register_method ({
# ignore any groups in the list that have invalid characters # ignore any groups in the list that have invalid characters
syslog( syslog(
'warn', 'warn',
"openid group '$group' contains invalid characters" "openid group '$group' contains invalid characters",
); );
} }
} }
@ -248,7 +256,8 @@ __PACKAGE__->register_method ({
# get groups that exist in OIDC and PVE # get groups that exist in OIDC and PVE
my $groups_intersect; my $groups_intersect;
for my $group (keys %$oidc_groups) { for my $group (keys %$oidc_groups) {
$groups_intersect->{$group} = 1 if $usercfg->{groups}->{$group}; $groups_intersect->{$group} = 1
if $usercfg->{groups}->{$group};
} }
if ($config->{'groups-autocreate'}) { if ($config->{'groups-autocreate'}) {
@ -256,27 +265,28 @@ __PACKAGE__->register_method ({
$groups_intersect = $oidc_groups; $groups_intersect = $oidc_groups;
my $groups_to_create; my $groups_to_create;
for my $group (keys %$oidc_groups) { for my $group (keys %$oidc_groups) {
$groups_to_create->{$group} = 1 if !$usercfg->{groups}->{$group}; $groups_to_create->{$group} = 1
if !$usercfg->{groups}->{$group};
} }
if ($groups_to_create) { if ($groups_to_create) {
# log a messages about created groups here # log a messages about created groups here
my $groups_to_create_string = join(', ', sort keys %$groups_to_create); my $groups_to_create_string =
join(', ', sort keys %$groups_to_create);
syslog( syslog(
'info', 'info',
"groups created automatically from openid claim: $groups_to_create_string" "groups created automatically from openid claim: $groups_to_create_string",
); );
} }
} }
# if groups should be overwritten, delete all the users groups first # if groups should be overwritten, delete all the users groups first
if ($config->{'groups-overwrite'} ) { if ($config->{'groups-overwrite'}) {
PVE::AccessControl::delete_user_group( PVE::AccessControl::delete_user_group(
$username, $username, $usercfg,
$usercfg,
); );
syslog( syslog(
'info', 'info',
"openid overwrite groups enabled; user '$username' removed from all groups" "openid overwrite groups enabled; user '$username' removed from all groups",
); );
} }
@ -286,21 +296,27 @@ __PACKAGE__->register_method ({
PVE::AccessControl::add_user_group( PVE::AccessControl::add_user_group(
$username, $username,
$usercfg, $usercfg,
$group $group,
); );
} }
my $groups_intersect_string = join(', ', sort keys %$groups_intersect); my $groups_intersect_string =
join(', ', sort keys %$groups_intersect);
syslog( syslog(
'info', 'info',
"openid user '$username' added to groups: $groups_intersect_string" "openid user '$username' added to groups: $groups_intersect_string",
); );
} }
cfs_write_file("user.cfg", $usercfg); cfs_write_file("user.cfg", $usercfg);
}, "openid group mapping failed"); },
"openid group mapping failed",
);
} else { } else {
syslog('err', "openid groups list is not an array; groups will not be updated"); syslog(
'err',
"openid groups list is not an array; groups will not be updated",
);
} }
} else { } else {
syslog('err', "openid groups claim '$groups_claim' is not found in claims"); syslog('err', "openid groups claim '$groups_claim' is not found in claims");
@ -319,7 +335,9 @@ __PACKAGE__->register_method ({
}; };
my $clinfo = PVE::Cluster::get_clinfo(); my $clinfo = PVE::Cluster::get_clinfo();
if ($clinfo->{cluster}->{name} && $rpcenv->check($username, '/', ['Sys.Audit'], 1)) { if (
$clinfo->{cluster}->{name} && $rpcenv->check($username, '/', ['Sys.Audit'], 1)
) {
$res->{clustername} = $clinfo->{cluster}->{name}; $res->{clustername} = $clinfo->{cluster}->{name};
} }
}; };
@ -330,7 +348,12 @@ __PACKAGE__->register_method ({
die PVE::Exception->new("authentication failure\n", code => 401); die PVE::Exception->new("authentication failure\n", code => 401);
} }
PVE::Cluster::log_msg('info', 'root@pam', "successful openid auth for user '$res->{username}'"); PVE::Cluster::log_msg(
'info',
'root@pam',
"successful openid auth for user '$res->{username}'",
);
return $res; return $res;
}}); },
});

View File

@ -10,17 +10,23 @@ use PVE::JSONSchema qw(get_standard_option register_standard_option);
use base qw(PVE::RESTHandler); use base qw(PVE::RESTHandler);
register_standard_option('role-id', { register_standard_option(
'role-id',
{
type => 'string', type => 'string',
format => 'pve-roleid', format => 'pve-roleid',
}); },
register_standard_option('role-privs', { );
type => 'string' , register_standard_option(
'role-privs',
{
type => 'string',
format => 'pve-priv-list', format => 'pve-priv-list',
optional => 1, optional => 1,
}); },
);
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'index', name => 'index',
path => '', path => '',
method => 'GET', method => 'GET',
@ -42,7 +48,7 @@ __PACKAGE__->register_method ({
special => { type => 'boolean', optional => 1, default => 0 }, special => { type => 'boolean', optional => 1, default => 0 },
}, },
}, },
links => [ { rel => 'child', href => "{roleid}" } ], links => [{ rel => 'child', href => "{roleid}" }],
}, },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
@ -51,9 +57,10 @@ __PACKAGE__->register_method ({
my $usercfg = cfs_read_file("user.cfg"); my $usercfg = cfs_read_file("user.cfg");
foreach my $role (keys %{$usercfg->{roles}}) { foreach my $role (keys %{ $usercfg->{roles} }) {
my $privs = join(',', sort keys %{$usercfg->{roles}->{$role}}); my $privs = join(',', sort keys %{ $usercfg->{roles}->{$role} });
push @$res, { push @$res,
{
roleid => $role, roleid => $role,
privs => $privs, privs => $privs,
special => PVE::AccessControl::role_is_special($role), special => PVE::AccessControl::role_is_special($role),
@ -61,9 +68,10 @@ __PACKAGE__->register_method ({
} }
return $res; return $res;
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'create_role', name => 'create_role',
protected => 1, protected => 1,
path => '', path => '',
@ -87,11 +95,13 @@ __PACKAGE__->register_method ({
if ($role =~ /^PVE/i) { if ($role =~ /^PVE/i) {
raise_param_exc({ raise_param_exc({
roleid => "cannot use role ID starting with the (case-insensitive) 'PVE' namespace", roleid =>
"cannot use role ID starting with the (case-insensitive) 'PVE' namespace",
}); });
} }
PVE::AccessControl::lock_user_config(sub { PVE::AccessControl::lock_user_config(
sub {
my $usercfg = cfs_read_file("user.cfg"); my $usercfg = cfs_read_file("user.cfg");
die "role '$role' already exists\n" if $usercfg->{roles}->{$role}; die "role '$role' already exists\n" if $usercfg->{roles}->{$role};
@ -101,12 +111,15 @@ __PACKAGE__->register_method ({
PVE::AccessControl::add_role_privs($role, $usercfg, $param->{privs}); PVE::AccessControl::add_role_privs($role, $usercfg, $param->{privs});
cfs_write_file("user.cfg", $usercfg); cfs_write_file("user.cfg", $usercfg);
}, "create role failed"); },
"create role failed",
);
return undef; return undef;
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'update_role', name => 'update_role',
protected => 1, protected => 1,
path => '{roleid}', path => '{roleid}',
@ -132,7 +145,8 @@ __PACKAGE__->register_method ({
die "auto-generated role '$role' cannot be modified\n" die "auto-generated role '$role' cannot be modified\n"
if PVE::AccessControl::role_is_special($role); if PVE::AccessControl::role_is_special($role);
PVE::AccessControl::lock_user_config(sub { PVE::AccessControl::lock_user_config(
sub {
my $usercfg = cfs_read_file("user.cfg"); my $usercfg = cfs_read_file("user.cfg");
die "role '$role' does not exist\n" if !$usercfg->{roles}->{$role}; die "role '$role' does not exist\n" if !$usercfg->{roles}->{$role};
@ -142,12 +156,15 @@ __PACKAGE__->register_method ({
PVE::AccessControl::add_role_privs($role, $usercfg, $param->{privs}); PVE::AccessControl::add_role_privs($role, $usercfg, $param->{privs});
cfs_write_file("user.cfg", $usercfg); cfs_write_file("user.cfg", $usercfg);
}, "update role failed"); },
"update role failed",
);
return undef; return undef;
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'read_role', name => 'read_role',
path => '{roleid}', path => '{roleid}',
method => 'GET', method => 'GET',
@ -178,10 +195,10 @@ __PACKAGE__->register_method ({
die "role '$role' does not exist\n" if !$data; die "role '$role' does not exist\n" if !$data;
return $data; return $data;
} },
}); });
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'delete_role', name => 'delete_role',
protected => 1, protected => 1,
path => '{roleid}', path => '{roleid}',
@ -205,20 +222,23 @@ __PACKAGE__->register_method ({
die "auto-generated role '$role' cannot be deleted\n" die "auto-generated role '$role' cannot be deleted\n"
if PVE::AccessControl::role_is_special($role); if PVE::AccessControl::role_is_special($role);
PVE::AccessControl::lock_user_config(sub { PVE::AccessControl::lock_user_config(
sub {
my $usercfg = cfs_read_file("user.cfg"); my $usercfg = cfs_read_file("user.cfg");
die "role '$role' does not exist\n" if !$usercfg->{roles}->{$role}; die "role '$role' does not exist\n" if !$usercfg->{roles}->{$role};
delete ($usercfg->{roles}->{$role}); delete($usercfg->{roles}->{$role});
# fixme: delete role from acl? # fixme: delete role from acl?
cfs_write_file("user.cfg", $usercfg); cfs_write_file("user.cfg", $usercfg);
}, "delete role failed"); },
"delete role failed",
);
return undef; return undef;
} },
}); });
1; 1;

View File

@ -23,7 +23,7 @@ our $OPTIONAL_PASSWORD_SCHEMA = {
type => 'string', type => 'string',
optional => 1, # Only required if not root@pam optional => 1, # Only required if not root@pam
minLength => 5, minLength => 5,
maxLength => 64 maxLength => 64,
}; };
my $TFA_TYPE_SCHEMA = { my $TFA_TYPE_SCHEMA = {
@ -79,17 +79,17 @@ my $TFA_UPDATE_INFO_SCHEMA = {
optional => 1, optional => 1,
description => description =>
'When adding u2f entries, this contains a challenge the user must respond to in order' 'When adding u2f entries, this contains a challenge the user must respond to in order'
.' to finish the registration.' . ' to finish the registration.',
}, },
recovery => { recovery => {
type => 'array', type => 'array',
optional => 1, optional => 1,
description => description =>
'When adding recovery codes, this contains the list of codes to be displayed to' 'When adding recovery codes, this contains the list of codes to be displayed to'
.' the user', . ' the user',
items => { items => {
type => 'string', type => 'string',
description => 'A recovery entry.' description => 'A recovery entry.',
}, },
}, },
}, },
@ -100,7 +100,8 @@ my $TFA_UPDATE_INFO_SCHEMA = {
my sub set_user_tfa_enabled : prototype($$$) { my sub set_user_tfa_enabled : prototype($$$) {
my ($userid, $realm, $tfa_cfg) = @_; my ($userid, $realm, $tfa_cfg) = @_;
PVE::AccessControl::lock_user_config(sub { PVE::AccessControl::lock_user_config(
sub {
my $user_cfg = cfs_read_file('user.cfg'); my $user_cfg = cfs_read_file('user.cfg');
my $user = $user_cfg->{users}->{$userid}; my $user = $user_cfg->{users}->{$userid};
my $keys = $user->{keys}; my $keys = $user->{keys};
@ -115,21 +116,24 @@ my sub set_user_tfa_enabled : prototype($$$) {
my $realm_tfa = $realm_cfg->{tfa}; my $realm_tfa = $realm_cfg->{tfa};
$realm_tfa = PVE::Auth::Plugin::parse_tfa_config($realm_tfa) if $realm_tfa; $realm_tfa = PVE::Auth::Plugin::parse_tfa_config($realm_tfa) if $realm_tfa;
PVE::AccessControl::add_old_keys_to_realm_tfa($userid, $tfa_cfg, $realm_tfa, $keys); PVE::AccessControl::add_old_keys_to_realm_tfa(
$userid, $tfa_cfg, $realm_tfa, $keys,
);
} }
$user->{keys} = $tfa_cfg ? 'x' : undef; $user->{keys} = $tfa_cfg ? 'x' : undef;
cfs_write_file("user.cfg", $user_cfg); cfs_write_file("user.cfg", $user_cfg);
}, "enabling TFA for the user failed"); },
"enabling TFA for the user failed",
);
} }
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'list_user_tfa', name => 'list_user_tfa',
path => '{userid}', path => '{userid}',
method => 'GET', method => 'GET',
permissions => { permissions => {
check => [ 'or', check => [
['userid-param', 'self'], 'or', ['userid-param', 'self'], ['userid-group', ['User.Modify', 'Sys.Audit']],
['userid-group', ['User.Modify', 'Sys.Audit']],
], ],
}, },
protected => 1, # else we can't access shadow files protected => 1, # else we can't access shadow files
@ -137,31 +141,34 @@ __PACKAGE__->register_method ({
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
userid => get_standard_option('userid', { userid => get_standard_option(
'userid',
{
completion => \&PVE::AccessControl::complete_username, completion => \&PVE::AccessControl::complete_username,
}), },
} ),
},
}, },
returns => { returns => {
description => "A list of the user's TFA entries.", description => "A list of the user's TFA entries.",
type => 'array', type => 'array',
items => $TYPED_TFA_ENTRY_SCHEMA, items => $TYPED_TFA_ENTRY_SCHEMA,
links => [ { rel => 'child', href => "{id}" } ], links => [{ rel => 'child', href => "{id}" }],
}, },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
my $tfa_cfg = cfs_read_file('priv/tfa.cfg'); my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
return $tfa_cfg->api_list_user_tfa($param->{userid}); return $tfa_cfg->api_list_user_tfa($param->{userid});
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'get_tfa_entry', name => 'get_tfa_entry',
path => '{userid}/{id}', path => '{userid}/{id}',
method => 'GET', method => 'GET',
permissions => { permissions => {
check => [ 'or', check => [
['userid-param', 'self'], 'or', ['userid-param', 'self'], ['userid-group', ['User.Modify', 'Sys.Audit']],
['userid-group', ['User.Modify', 'Sys.Audit']],
], ],
}, },
protected => 1, # else we can't access shadow files protected => 1, # else we can't access shadow files
@ -169,11 +176,14 @@ __PACKAGE__->register_method ({
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
userid => get_standard_option('userid', { userid => get_standard_option(
'userid',
{
completion => \&PVE::AccessControl::complete_username, completion => \&PVE::AccessControl::complete_username,
}), },
),
id => $TFA_ID_SCHEMA, id => $TFA_ID_SCHEMA,
} },
}, },
returns => $TYPED_TFA_ENTRY_SCHEMA, returns => $TYPED_TFA_ENTRY_SCHEMA,
code => sub { code => sub {
@ -183,16 +193,16 @@ __PACKAGE__->register_method ({
my $entry = $tfa_cfg->api_get_tfa_entry($param->{userid}, $id); my $entry = $tfa_cfg->api_get_tfa_entry($param->{userid}, $id);
raise("No such tfa entry '$id'", code => HTTP::Status::HTTP_NOT_FOUND) if !$entry; raise("No such tfa entry '$id'", code => HTTP::Status::HTTP_NOT_FOUND) if !$entry;
return $entry; return $entry;
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'delete_tfa', name => 'delete_tfa',
path => '{userid}/{id}', path => '{userid}/{id}',
method => 'DELETE', method => 'DELETE',
permissions => { permissions => {
check => [ 'or', check => [
['userid-param', 'self'], 'or', ['userid-param', 'self'], ['userid-group', ['User.Modify']],
['userid-group', ['User.Modify']],
], ],
}, },
protected => 1, # else we can't access shadow files protected => 1, # else we can't access shadow files
@ -201,12 +211,15 @@ __PACKAGE__->register_method ({
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
userid => get_standard_option('userid', { userid => get_standard_option(
'userid',
{
completion => \&PVE::AccessControl::complete_username, completion => \&PVE::AccessControl::complete_username,
}), },
),
id => $TFA_ID_SCHEMA, id => $TFA_ID_SCHEMA,
password => $OPTIONAL_PASSWORD_SCHEMA, password => $OPTIONAL_PASSWORD_SCHEMA,
} },
}, },
returns => { type => 'null' }, returns => { type => 'null' },
code => sub { code => sub {
@ -215,9 +228,7 @@ __PACKAGE__->register_method ({
my $rpcenv = PVE::RPCEnvironment::get(); my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user(); my $authuser = $rpcenv->get_user();
my $userid = $rpcenv->reauth_user_for_user_modification( my $userid = $rpcenv->reauth_user_for_user_modification(
$authuser, $authuser, $param->{userid}, $param->{password},
$param->{userid},
$param->{password},
); );
my $has_entries_left = PVE::AccessControl::lock_tfa_config(sub { my $has_entries_left = PVE::AccessControl::lock_tfa_config(sub {
@ -229,9 +240,10 @@ __PACKAGE__->register_method ({
if (!$has_entries_left) { if (!$has_entries_left) {
set_user_tfa_enabled($userid, undef, undef); set_user_tfa_enabled($userid, undef, undef);
} }
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'list_tfa', name => 'list_tfa',
path => '', path => '',
method => 'GET', method => 'GET',
@ -243,7 +255,7 @@ __PACKAGE__->register_method ({
description => 'List TFA configurations of users.', description => 'List TFA configurations of users.',
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => {} properties => {},
}, },
returns => { returns => {
description => "The list tuples of user and TFA entries.", description => "The list tuples of user and TFA entries.",
@ -272,7 +284,7 @@ __PACKAGE__->register_method ({
}, },
}, },
}, },
links => [ { rel => 'child', href => "{userid}" } ], links => [{ rel => 'child', href => "{userid}" }],
}, },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
@ -283,7 +295,7 @@ __PACKAGE__->register_method ({
my $tfa_cfg = cfs_read_file('priv/tfa.cfg'); my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
my $entries = $tfa_cfg->api_list_tfa($authuser, 1); my $entries = $tfa_cfg->api_list_tfa($authuser, 1);
my $privs = [ 'User.Modify', 'Sys.Audit' ]; my $privs = ['User.Modify', 'Sys.Audit'];
if ($rpcenv->check_any($authuser, "/access/groups", $privs, 1)) { if ($rpcenv->check_any($authuser, "/access/groups", $privs, 1)) {
# can modify all # can modify all
return $entries; return $entries;
@ -297,16 +309,16 @@ __PACKAGE__->register_method ({
$userid eq $authuser || $allowed_users->{$userid} $userid eq $authuser || $allowed_users->{$userid}
} $entries->@* } $entries->@*
]; ];
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'add_tfa_entry', name => 'add_tfa_entry',
path => '{userid}', path => '{userid}',
method => 'POST', method => 'POST',
permissions => { permissions => {
check => [ 'or', check => [
['userid-param', 'self'], 'or', ['userid-param', 'self'], ['userid-group', ['User.Modify']],
['userid-group', ['User.Modify']],
], ],
}, },
protected => 1, # else we can't access shadow files protected => 1, # else we can't access shadow files
@ -315,9 +327,12 @@ __PACKAGE__->register_method ({
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
userid => get_standard_option('userid', { userid => get_standard_option(
'userid',
{
completion => \&PVE::AccessControl::complete_username, completion => \&PVE::AccessControl::complete_username,
}), },
),
type => $TFA_TYPE_SCHEMA, type => $TFA_TYPE_SCHEMA,
description => { description => {
type => 'string', type => 'string',
@ -332,14 +347,14 @@ __PACKAGE__->register_method ({
}, },
value => { value => {
type => 'string', type => 'string',
description => description => 'The current value for the provided totp URI, or a Webauthn/U2F'
'The current value for the provided totp URI, or a Webauthn/U2F' . ' challenge response',
.' challenge response',
optional => 1, optional => 1,
}, },
challenge => { challenge => {
type => 'string', type => 'string',
description => 'When responding to a u2f challenge: the original challenge string', description =>
'When responding to a u2f challenge: the original challenge string',
optional => 1, optional => 1,
}, },
password => $OPTIONAL_PASSWORD_SCHEMA, password => $OPTIONAL_PASSWORD_SCHEMA,
@ -352,9 +367,7 @@ __PACKAGE__->register_method ({
my $rpcenv = PVE::RPCEnvironment::get(); my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user(); my $authuser = $rpcenv->get_user();
my ($userid, undef, $realm) = $rpcenv->reauth_user_for_user_modification( my ($userid, undef, $realm) = $rpcenv->reauth_user_for_user_modification(
$authuser, $authuser, $param->{userid}, $param->{password},
$param->{userid},
$param->{password},
); );
my $type = delete $param->{type}; my $type = delete $param->{type};
@ -383,7 +396,8 @@ __PACKAGE__->register_method ({
return $response; return $response;
}); });
}}); },
});
sub validate_yubico_otp : prototype($$$) { sub validate_yubico_otp : prototype($$$) {
my ($userid, $realm, $value) = @_; my ($userid, $realm, $value) = @_;
@ -407,14 +421,13 @@ sub validate_yubico_otp : prototype($$$) {
return $public_key; return $public_key;
} }
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'update_tfa_entry', name => 'update_tfa_entry',
path => '{userid}/{id}', path => '{userid}/{id}',
method => 'PUT', method => 'PUT',
permissions => { permissions => {
check => [ 'or', check => [
['userid-param', 'self'], 'or', ['userid-param', 'self'], ['userid-group', ['User.Modify']],
['userid-group', ['User.Modify']],
], ],
}, },
protected => 1, # else we can't access shadow files protected => 1, # else we can't access shadow files
@ -423,9 +436,12 @@ __PACKAGE__->register_method ({
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
userid => get_standard_option('userid', { userid => get_standard_option(
'userid',
{
completion => \&PVE::AccessControl::complete_username, completion => \&PVE::AccessControl::complete_username,
}), },
),
id => $TFA_ID_SCHEMA, id => $TFA_ID_SCHEMA,
description => { description => {
type => 'string', type => 'string',
@ -448,23 +464,19 @@ __PACKAGE__->register_method ({
my $rpcenv = PVE::RPCEnvironment::get(); my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user(); my $authuser = $rpcenv->get_user();
my $userid = $rpcenv->reauth_user_for_user_modification( my $userid = $rpcenv->reauth_user_for_user_modification(
$authuser, $authuser, $param->{userid}, $param->{password},
$param->{userid},
$param->{password},
); );
PVE::AccessControl::lock_tfa_config(sub { PVE::AccessControl::lock_tfa_config(sub {
my $tfa_cfg = cfs_read_file('priv/tfa.cfg'); my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
$tfa_cfg->api_update_tfa_entry( $tfa_cfg->api_update_tfa_entry(
$userid, $userid, $param->{id}, $param->{description}, $param->{enable},
$param->{id},
$param->{description},
$param->{enable},
); );
cfs_write_file('priv/tfa.cfg', $tfa_cfg); cfs_write_file('priv/tfa.cfg', $tfa_cfg);
}); });
}}); },
});
1; 1;

View File

@ -17,69 +17,104 @@ use PVE::RESTHandler;
use base qw(PVE::RESTHandler); use base qw(PVE::RESTHandler);
register_standard_option('user-enable', { register_standard_option(
description => "Enable the account (default). You can set this to '0' to disable the account", 'user-enable',
{
description =>
"Enable the account (default). You can set this to '0' to disable the account",
type => 'boolean', type => 'boolean',
optional => 1, optional => 1,
default => 1, default => 1,
}); },
register_standard_option('user-expire', { );
description => "Account expiration date (seconds since epoch). '0' means no expiration date.", register_standard_option(
'user-expire',
{
description =>
"Account expiration date (seconds since epoch). '0' means no expiration date.",
type => 'integer', type => 'integer',
minimum => 0, minimum => 0,
optional => 1, optional => 1,
}); },
register_standard_option('user-firstname', { type => 'string', optional => 1, maxLength => 1024, }); );
register_standard_option('user-lastname', { type => 'string', optional => 1, maxLength => 1024, }); register_standard_option('user-firstname', { type => 'string', optional => 1, maxLength => 1024 });
register_standard_option('user-email', { register_standard_option('user-lastname', { type => 'string', optional => 1, maxLength => 1024 });
register_standard_option(
'user-email',
{
type => 'string', type => 'string',
optional => 1, optional => 1,
format => 'email-opt', format => 'email-opt',
maxLength => 254, # 256 including punctuation and separator is the max path as per RFC 5321 maxLength => 254, # 256 including punctuation and separator is the max path as per RFC 5321
}); },
register_standard_option('user-comment', { );
register_standard_option(
'user-comment',
{
type => 'string', type => 'string',
optional => 1, optional => 1,
maxLength => 2048, maxLength => 2048,
}); },
register_standard_option('user-keys', { );
register_standard_option(
'user-keys',
{
description => "Keys for two factor auth (yubico).", description => "Keys for two factor auth (yubico).",
type => 'string', type => 'string',
pattern => '[0-9a-zA-Z!=]{0,4096}', pattern => '[0-9a-zA-Z!=]{0,4096}',
optional => 1, optional => 1,
}); },
register_standard_option('group-list', { );
type => 'string', format => 'pve-groupid-list', register_standard_option(
'group-list',
{
type => 'string',
format => 'pve-groupid-list',
optional => 1, optional => 1,
completion => \&PVE::AccessControl::complete_group, completion => \&PVE::AccessControl::complete_group,
}); },
register_standard_option('token-subid', { );
register_standard_option(
'token-subid',
{
type => 'string', type => 'string',
pattern => $PVE::AccessControl::token_subid_regex, pattern => $PVE::AccessControl::token_subid_regex,
description => 'User-specific token identifier.', description => 'User-specific token identifier.',
}); },
register_standard_option('token-expire', { );
description => "API token expiration date (seconds since epoch). '0' means no expiration date.", register_standard_option(
'token-expire',
{
description =>
"API token expiration date (seconds since epoch). '0' means no expiration date.",
type => 'integer', type => 'integer',
minimum => 0, minimum => 0,
optional => 1, optional => 1,
default => 'same as user', default => 'same as user',
}); },
register_standard_option('token-privsep', { );
description => "Restrict API token privileges with separate ACLs (default), or give full privileges of corresponding user.", register_standard_option(
'token-privsep',
{
description =>
"Restrict API token privileges with separate ACLs (default), or give full privileges of corresponding user.",
type => 'boolean', type => 'boolean',
optional => 1, optional => 1,
default => 1, default => 1,
}); },
);
register_standard_option('token-comment', { type => 'string', optional => 1 }); register_standard_option('token-comment', { type => 'string', optional => 1 });
register_standard_option('token-info', { register_standard_option(
'token-info',
{
type => 'object', type => 'object',
properties => { properties => {
expire => get_standard_option('token-expire'), expire => get_standard_option('token-expire'),
privsep => get_standard_option('token-privsep'), privsep => get_standard_option('token-privsep'),
comment => get_standard_option('token-comment'), comment => get_standard_option('token-comment'),
} },
}); },
);
my $token_info_extend = sub { my $token_info_extend = sub {
my ($props) = @_; my ($props) = @_;
@ -110,19 +145,20 @@ my $extract_user_data = sub {
return $res if !$full; return $res if !$full;
$res->{groups} = $data->{groups} ? [ sort keys %{$data->{groups}} ] : []; $res->{groups} = $data->{groups} ? [sort keys %{ $data->{groups} }] : [];
$res->{tokens} = $data->{tokens}; $res->{tokens} = $data->{tokens};
return $res; return $res;
}; };
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'index', name => 'index',
path => '', path => '',
method => 'GET', method => 'GET',
description => "User index.", description => "User index.",
permissions => { permissions => {
description => "The returned list is restricted to users where you have 'User.Modify' or 'Sys.Audit' permissions on '/access/groups' or on a group the user belongs too. But it always includes the current (authenticated) user.", description =>
"The returned list is restricted to users where you have 'User.Modify' or 'Sys.Audit' permissions on '/access/groups' or on a group the user belongs too. But it always includes the current (authenticated) user.",
user => 'all', user => 'all',
}, },
protected => 1, # to access priv/tfa.cfg protected => 1, # to access priv/tfa.cfg
@ -139,7 +175,7 @@ __PACKAGE__->register_method ({
description => "Include group and token information.", description => "Include group and token information.",
optional => 1, optional => 1,
default => 0, default => 0,
} },
}, },
}, },
returns => { returns => {
@ -164,7 +200,8 @@ __PACKAGE__->register_method ({
}), }),
}, },
'realm-type' => { 'realm-type' => {
type => 'string', format => 'pve-realm', type => 'string',
format => 'pve-realm',
description => 'The type of the users realm', description => 'The type of the users realm',
optional => 1, # it should always be there, but we use conditional code below, so.. optional => 1, # it should always be there, but we use conditional code below, so..
}, },
@ -181,7 +218,7 @@ __PACKAGE__->register_method ({
}, },
}, },
}, },
links => [ { rel => 'child', href => "{userid}" } ], links => [{ rel => 'child', href => "{userid}" }],
}, },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
@ -195,14 +232,14 @@ __PACKAGE__->register_method ({
my $res = []; my $res = [];
my $privs = [ 'User.Modify', 'Sys.Audit' ]; my $privs = ['User.Modify', 'Sys.Audit'];
my $canUserMod = $rpcenv->check_any($authuser, "/access/groups", $privs, 1); my $canUserMod = $rpcenv->check_any($authuser, "/access/groups", $privs, 1);
my $groups = $rpcenv->filter_groups($authuser, $privs, 1); my $groups = $rpcenv->filter_groups($authuser, $privs, 1);
my $allowed_users = $rpcenv->group_member_join([keys %$groups]); my $allowed_users = $rpcenv->group_member_join([keys %$groups]);
my $tfa_cfg = cfs_read_file('priv/tfa.cfg'); my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
foreach my $user (sort keys %{$usercfg->{users}}) { foreach my $user (sort keys %{ $usercfg->{users} }) {
if (!($canUserMod || $user eq $authuser)) { if (!($canUserMod || $user eq $authuser)) {
next if !$allowed_users->{$user}; next if !$allowed_users->{$user};
} }
@ -214,17 +251,17 @@ __PACKAGE__->register_method ({
next if !$entry->{enable} && $param->{enabled}; next if !$entry->{enable} && $param->{enabled};
} }
$entry->{groups} = join(',', @{$entry->{groups}}) if $entry->{groups}; $entry->{groups} = join(',', @{ $entry->{groups} }) if $entry->{groups};
if (defined(my $tokens = $entry->{tokens})) { if (defined(my $tokens = $entry->{tokens})) {
$entry->{tokens} = [ $entry->{tokens} =
map { { tokenid => $_, %{$tokens->{$_}} } } sort keys %$tokens [map { { tokenid => $_, %{ $tokens->{$_} } } } sort keys %$tokens];
];
} }
if ($user =~ /($PVE::Auth::Plugin::realm_regex)$/) { if ($user =~ /($PVE::Auth::Plugin::realm_regex)$/) {
my $realm = $1; my $realm = $1;
$entry->{'realm-type'} = $domainids->{$realm}->{type} if exists $domainids->{$realm}; $entry->{'realm-type'} = $domainids->{$realm}->{type}
if exists $domainids->{$realm};
} }
$entry->{userid} = $user; $entry->{userid} = $user;
@ -241,19 +278,21 @@ __PACKAGE__->register_method ({
} }
return $res; return $res;
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'create_user', name => 'create_user',
protected => 1, protected => 1,
path => '', path => '',
method => 'POST', method => 'POST',
permissions => { permissions => {
description => "You need 'Realm.AllocateUser' on '/access/realm/<realm>' on the realm of user <userid>, and 'User.Modify' permissions to '/access/groups/<group>' for any group specified (or 'User.Modify' on '/access/groups' if you pass no groups.", description =>
"You need 'Realm.AllocateUser' on '/access/realm/<realm>' on the realm of user <userid>, and 'User.Modify' permissions to '/access/groups/<group>' for any group specified (or 'User.Modify' on '/access/groups' if you pass no groups.",
check => [ check => [
'and', 'and',
[ 'userid-param', 'Realm.AllocateUser'], ['userid-param', 'Realm.AllocateUser'],
[ 'userid-group', ['User.Modify'], groups_param => 'create'], ['userid-group', ['User.Modify'], groups_param => 'create'],
], ],
}, },
description => "Create new user.", description => "Create new user.",
@ -273,7 +312,7 @@ __PACKAGE__->register_method ({
type => 'string', type => 'string',
optional => 1, optional => 1,
minLength => 8, minLength => 8,
maxLength => 64 maxLength => 64,
}, },
groups => get_standard_option('group-list'), groups => get_standard_option('group-list'),
}, },
@ -282,8 +321,10 @@ __PACKAGE__->register_method ({
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
PVE::AccessControl::lock_user_config(sub { PVE::AccessControl::lock_user_config(
my ($username, $ruid, $realm) = PVE::AccessControl::verify_username($param->{userid}); sub {
my ($username, $ruid, $realm) =
PVE::AccessControl::verify_username($param->{userid});
my $usercfg = cfs_read_file("user.cfg"); my $usercfg = cfs_read_file("user.cfg");
@ -308,19 +349,25 @@ __PACKAGE__->register_method ({
} }
} }
$usercfg->{users}->{$username}->{firstname} = $param->{firstname} if $param->{firstname}; $usercfg->{users}->{$username}->{firstname} = $param->{firstname}
$usercfg->{users}->{$username}->{lastname} = $param->{lastname} if $param->{lastname}; if $param->{firstname};
$usercfg->{users}->{$username}->{lastname} = $param->{lastname}
if $param->{lastname};
$usercfg->{users}->{$username}->{email} = $param->{email} if $param->{email}; $usercfg->{users}->{$username}->{email} = $param->{email} if $param->{email};
$usercfg->{users}->{$username}->{comment} = $param->{comment} if $param->{comment}; $usercfg->{users}->{$username}->{comment} = $param->{comment}
if $param->{comment};
$usercfg->{users}->{$username}->{keys} = $param->{keys} if $param->{keys}; $usercfg->{users}->{$username}->{keys} = $param->{keys} if $param->{keys};
cfs_write_file("user.cfg", $usercfg); cfs_write_file("user.cfg", $usercfg);
}, "create user failed"); },
"create user failed",
);
return undef; return undef;
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'read_user', name => 'read_user',
path => '{userid}', path => '{userid}',
method => 'GET', method => 'GET',
@ -358,7 +405,7 @@ __PACKAGE__->register_method ({
additionalProperties => get_standard_option('token-info'), additionalProperties => get_standard_option('token-info'),
}, },
}, },
type => "object" type => "object",
}, },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
@ -370,15 +417,16 @@ __PACKAGE__->register_method ({
my $data = PVE::AccessControl::check_user_exist($usercfg, $username); my $data = PVE::AccessControl::check_user_exist($usercfg, $username);
return &$extract_user_data($data, 1); return &$extract_user_data($data, 1);
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'update_user', name => 'update_user',
protected => 1, protected => 1,
path => '{userid}', path => '{userid}',
method => 'PUT', method => 'PUT',
permissions => { permissions => {
check => ['userid-group', ['User.Modify'], groups_param => 'update' ], check => ['userid-group', ['User.Modify'], groups_param => 'update'],
}, },
description => "Update user configuration.", description => "Update user configuration.",
parameters => { parameters => {
@ -406,13 +454,16 @@ __PACKAGE__->register_method ({
my ($username, $ruid, $realm) = PVE::AccessControl::verify_username($param->{userid}); my ($username, $ruid, $realm) = PVE::AccessControl::verify_username($param->{userid});
PVE::AccessControl::lock_user_config(sub { PVE::AccessControl::lock_user_config(
sub {
my $usercfg = cfs_read_file("user.cfg"); my $usercfg = cfs_read_file("user.cfg");
PVE::AccessControl::check_user_exist($usercfg, $username); PVE::AccessControl::check_user_exist($usercfg, $username);
$usercfg->{users}->{$username}->{enable} = $param->{enable} if defined($param->{enable}); $usercfg->{users}->{$username}->{enable} = $param->{enable}
$usercfg->{users}->{$username}->{expire} = $param->{expire} if defined($param->{expire}); if defined($param->{enable});
$usercfg->{users}->{$username}->{expire} = $param->{expire}
if defined($param->{expire});
PVE::AccessControl::delete_user_group($username, $usercfg) PVE::AccessControl::delete_user_group($username, $usercfg)
if (!$param->{append} && defined($param->{groups})); if (!$param->{append} && defined($param->{groups}));
@ -427,35 +478,42 @@ __PACKAGE__->register_method ({
} }
} }
$usercfg->{users}->{$username}->{firstname} = $param->{firstname} if defined($param->{firstname}); $usercfg->{users}->{$username}->{firstname} = $param->{firstname}
$usercfg->{users}->{$username}->{lastname} = $param->{lastname} if defined($param->{lastname}); if defined($param->{firstname});
$usercfg->{users}->{$username}->{email} = $param->{email} if defined($param->{email}); $usercfg->{users}->{$username}->{lastname} = $param->{lastname}
$usercfg->{users}->{$username}->{comment} = $param->{comment} if defined($param->{comment}); if defined($param->{lastname});
$usercfg->{users}->{$username}->{keys} = $param->{keys} if defined($param->{keys}); $usercfg->{users}->{$username}->{email} = $param->{email}
if defined($param->{email});
$usercfg->{users}->{$username}->{comment} = $param->{comment}
if defined($param->{comment});
$usercfg->{users}->{$username}->{keys} = $param->{keys}
if defined($param->{keys});
cfs_write_file("user.cfg", $usercfg); cfs_write_file("user.cfg", $usercfg);
}, "update user failed"); },
"update user failed",
);
return undef; return undef;
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'delete_user', name => 'delete_user',
protected => 1, protected => 1,
path => '{userid}', path => '{userid}',
method => 'DELETE', method => 'DELETE',
description => "Delete user.", description => "Delete user.",
permissions => { permissions => {
check => [ 'and', check => [
[ 'userid-param', 'Realm.AllocateUser'], 'and', ['userid-param', 'Realm.AllocateUser'], ['userid-group', ['User.Modify']],
[ 'userid-group', ['User.Modify']],
], ],
}, },
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
userid => get_standard_option('userid-completed'), userid => get_standard_option('userid-completed'),
} },
}, },
returns => { type => 'null' }, returns => { type => 'null' },
code => sub { code => sub {
@ -466,7 +524,8 @@ __PACKAGE__->register_method ({
my ($userid, $ruid, $realm) = PVE::AccessControl::verify_username($param->{userid}); my ($userid, $ruid, $realm) = PVE::AccessControl::verify_username($param->{userid});
PVE::AccessControl::lock_user_config(sub { PVE::AccessControl::lock_user_config(
sub {
my $usercfg = cfs_read_file("user.cfg"); my $usercfg = cfs_read_file("user.cfg");
# NOTE: disable the user first (transaction like), so if (e.g.) we fail in the middle of # NOTE: disable the user first (transaction like), so if (e.g.) we fail in the middle of
@ -497,21 +556,23 @@ __PACKAGE__->register_method ({
cfs_write_file("user.cfg", $usercfg); cfs_write_file("user.cfg", $usercfg);
}; };
die "$@$partial_deletion\n" if $@; die "$@$partial_deletion\n" if $@;
}, "delete user failed"); },
"delete user failed",
);
return undef; return undef;
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'read_user_tfa_type', name => 'read_user_tfa_type',
path => '{userid}/tfa', path => '{userid}/tfa',
method => 'GET', method => 'GET',
protected => 1, protected => 1,
description => "Get user TFA types (Personal and Realm).", description => "Get user TFA types (Personal and Realm).",
permissions => { permissions => {
check => [ 'or', check => [
['userid-param', 'self'], 'or', ['userid-param', 'self'], ['userid-group', ['User.Modify', 'Sys.Audit']],
['userid-group', ['User.Modify', 'Sys.Audit']],
], ],
}, },
parameters => { parameters => {
@ -538,15 +599,13 @@ __PACKAGE__->register_method ({
user => { user => {
type => 'string', type => 'string',
enum => [qw(oath u2f)], enum => [qw(oath u2f)],
description => description => "The type of TFA the user has set, if any."
"The type of TFA the user has set, if any."
. " Only set if 'multiple' was not passed.", . " Only set if 'multiple' was not passed.",
optional => 1, optional => 1,
}, },
types => { types => {
type => 'array', type => 'array',
description => description => "Array of the user configured TFA types, if any."
"Array of the user configured TFA types, if any."
. " Only available if 'multiple' was not passed.", . " Only available if 'multiple' was not passed.",
optional => 1, optional => 1,
items => { items => {
@ -556,7 +615,7 @@ __PACKAGE__->register_method ({
}, },
}, },
}, },
type => "object" type => "object",
}, },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
@ -569,7 +628,8 @@ __PACKAGE__->register_method ({
my $res = {}; my $res = {};
my $realm_tfa = {}; my $realm_tfa = {};
$realm_tfa = PVE::Auth::Plugin::parse_tfa_config($realm_cfg->{tfa}) if $realm_cfg->{tfa}; $realm_tfa = PVE::Auth::Plugin::parse_tfa_config($realm_cfg->{tfa})
if $realm_cfg->{tfa};
$res->{realm} = $realm_tfa->{type} if $realm_tfa->{type}; $res->{realm} = $realm_tfa->{type} if $realm_tfa->{type};
my $tfa_cfg = cfs_read_file('priv/tfa.cfg'); my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
@ -586,16 +646,17 @@ __PACKAGE__->register_method ({
$res->{user} = $tfa->{type} if $tfa->{type}; $res->{user} = $tfa->{type} if $tfa->{type};
} }
return $res; return $res;
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'unlock_tfa', name => 'unlock_tfa',
path => '{userid}/unlock-tfa', path => '{userid}/unlock-tfa',
method => 'PUT', method => 'PUT',
protected => 1, protected => 1,
description => "Unlock a user's TFA authentication.", description => "Unlock a user's TFA authentication.",
permissions => { permissions => {
check => [ 'userid-group', ['User.Modify']], check => ['userid-group', ['User.Modify']],
}, },
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
@ -618,18 +679,17 @@ __PACKAGE__->register_method ({
}); });
return $user_was_locked; return $user_was_locked;
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'token_index', name => 'token_index',
path => '{userid}/token', path => '{userid}/token',
method => 'GET', method => 'GET',
description => "Get user API tokens.", description => "Get user API tokens.",
permissions => { permissions => {
check => [ check => [
'or', 'or', ['userid-param', 'self'], ['userid-group', ['User.Modify']],
['userid-param', 'self'],
['userid-group', ['User.Modify']],
], ],
}, },
parameters => { parameters => {
@ -643,7 +703,7 @@ __PACKAGE__->register_method ({
items => $token_info_extend->({ items => $token_info_extend->({
tokenid => get_standard_option('token-subid'), tokenid => get_standard_option('token-subid'),
}), }),
links => [ { rel => 'child', href => "{tokenid}" } ], links => [{ rel => 'child', href => "{tokenid}" }],
}, },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
@ -654,19 +714,18 @@ __PACKAGE__->register_method ({
my $user = PVE::AccessControl::check_user_exist($usercfg, $userid); my $user = PVE::AccessControl::check_user_exist($usercfg, $userid);
my $tokens = $user->{tokens} // {}; my $tokens = $user->{tokens} // {};
return [ map { $tokens->{$_}->{tokenid} = $_; $tokens->{$_} } keys %$tokens]; return [map { $tokens->{$_}->{tokenid} = $_; $tokens->{$_} } keys %$tokens];
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'read_token', name => 'read_token',
path => '{userid}/token/{tokenid}', path => '{userid}/token/{tokenid}',
method => 'GET', method => 'GET',
description => "Get specific API token information.", description => "Get specific API token information.",
permissions => { permissions => {
check => [ check => [
'or', 'or', ['userid-param', 'self'], ['userid-group', ['User.Modify']],
['userid-param', 'self'],
['userid-group', ['User.Modify']],
], ],
}, },
parameters => { parameters => {
@ -686,19 +745,19 @@ __PACKAGE__->register_method ({
my $usercfg = cfs_read_file("user.cfg"); my $usercfg = cfs_read_file("user.cfg");
return PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid); return PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid);
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'generate_token', name => 'generate_token',
path => '{userid}/token/{tokenid}', path => '{userid}/token/{tokenid}',
method => 'POST', method => 'POST',
description => "Generate a new API token for a specific user. NOTE: returns API token value, which needs to be stored as it cannot be retrieved afterwards!", description =>
"Generate a new API token for a specific user. NOTE: returns API token value, which needs to be stored as it cannot be retrieved afterwards!",
protected => 1, protected => 1,
permissions => { permissions => {
check => [ check => [
'or', 'or', ['userid-param', 'self'], ['userid-group', ['User.Modify']],
['userid-param', 'self'],
['userid-group', ['User.Modify']],
], ],
}, },
parameters => { parameters => {
@ -744,7 +803,8 @@ __PACKAGE__->register_method ({
my $generate_and_add_token = sub { my $generate_and_add_token = sub {
$usercfg = cfs_read_file("user.cfg"); $usercfg = cfs_read_file("user.cfg");
PVE::AccessControl::check_user_exist($usercfg, $userid); PVE::AccessControl::check_user_exist($usercfg, $userid);
die "Token already exists.\n" if defined(PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid, 1)); die "Token already exists.\n"
if defined(PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid, 1));
$full_tokenid = PVE::AccessControl::join_tokenid($userid, $tokenid); $full_tokenid = PVE::AccessControl::join_tokenid($userid, $tokenid);
$value = PVE::TokenConfig::generate_token($full_tokenid); $value = PVE::TokenConfig::generate_token($full_tokenid);
@ -758,17 +818,18 @@ __PACKAGE__->register_method ({
cfs_write_file("user.cfg", $usercfg); cfs_write_file("user.cfg", $usercfg);
}; };
PVE::AccessControl::lock_user_config($generate_and_add_token, 'generating token failed'); PVE::AccessControl::lock_user_config($generate_and_add_token,
'generating token failed');
return { return {
info => $token, info => $token,
value => $value, value => $value,
'full-tokenid' => $full_tokenid, 'full-tokenid' => $full_tokenid,
}; };
}}); },
});
__PACKAGE__->register_method({
__PACKAGE__->register_method ({
name => 'update_token_info', name => 'update_token_info',
path => '{userid}/token/{tokenid}', path => '{userid}/token/{tokenid}',
method => 'PUT', method => 'PUT',
@ -776,9 +837,7 @@ __PACKAGE__->register_method ({
protected => 1, protected => 1,
permissions => { permissions => {
check => [ check => [
'or', 'or', ['userid-param', 'self'], ['userid-group', ['User.Modify']],
['userid-param', 'self'],
['userid-group', ['User.Modify']],
], ],
}, },
parameters => { parameters => {
@ -791,7 +850,8 @@ __PACKAGE__->register_method ({
comment => get_standard_option('token-comment'), comment => get_standard_option('token-comment'),
}, },
}, },
returns => get_standard_option('token-info', { description => "Updated token information." }), returns =>
get_standard_option('token-info', { description => "Updated token information." }),
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
@ -801,7 +861,8 @@ __PACKAGE__->register_method ({
my $usercfg = cfs_read_file("user.cfg"); my $usercfg = cfs_read_file("user.cfg");
my $token = PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid); my $token = PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid);
PVE::AccessControl::lock_user_config(sub { PVE::AccessControl::lock_user_config(
sub {
$usercfg = cfs_read_file("user.cfg"); $usercfg = cfs_read_file("user.cfg");
$token = PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid); $token = PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid);
@ -813,13 +874,15 @@ __PACKAGE__->register_method ({
$usercfg->{users}->{$userid}->{tokens}->{$tokenid} = $token; $usercfg->{users}->{$userid}->{tokens}->{$tokenid} = $token;
cfs_write_file("user.cfg", $usercfg); cfs_write_file("user.cfg", $usercfg);
}, 'updating token info failed'); },
'updating token info failed',
);
return $token; return $token;
}}); },
});
__PACKAGE__->register_method({
__PACKAGE__->register_method ({
name => 'remove_token', name => 'remove_token',
path => '{userid}/token/{tokenid}', path => '{userid}/token/{tokenid}',
method => 'DELETE', method => 'DELETE',
@ -827,9 +890,7 @@ __PACKAGE__->register_method ({
protected => 1, protected => 1,
permissions => { permissions => {
check => [ check => [
'or', 'or', ['userid-param', 'self'], ['userid-group', ['User.Modify']],
['userid-param', 'self'],
['userid-group', ['User.Modify']],
], ],
}, },
parameters => { parameters => {
@ -849,7 +910,8 @@ __PACKAGE__->register_method ({
my $usercfg = cfs_read_file("user.cfg"); my $usercfg = cfs_read_file("user.cfg");
my $token = PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid); my $token = PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid);
PVE::AccessControl::lock_user_config(sub { PVE::AccessControl::lock_user_config(
sub {
$usercfg = cfs_read_file("user.cfg"); $usercfg = cfs_read_file("user.cfg");
PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid); PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid);
@ -859,8 +921,11 @@ __PACKAGE__->register_method ({
delete $usercfg->{users}->{$userid}->{tokens}->{$tokenid}; delete $usercfg->{users}->{$userid}->{tokens}->{$tokenid};
cfs_write_file("user.cfg", $usercfg); cfs_write_file("user.cfg", $usercfg);
}, 'deleting token failed'); },
'deleting token failed',
);
return; return;
}}); },
});
1; 1;

View File

@ -77,6 +77,7 @@ sub pve_verify_realm {
# 2) user config # 2) user config
# If we permit the other way round, too, we might end up deadlocking! # If we permit the other way round, too, we might end up deadlocking!
my $user_config_locked; my $user_config_locked;
sub lock_user_config { sub lock_user_config {
my ($code, $errmsg) = @_; my ($code, $errmsg) = @_;
@ -179,7 +180,7 @@ sub check_authkey {
} else { } else {
my $now = time(); my $now = time();
if ($now - $mtime >= $authkey_lifetime) { if ($now - $mtime >= $authkey_lifetime) {
warn "auth key pair too old, rotating..\n" if !$quiet;; warn "auth key pair too old, rotating..\n" if !$quiet;
return 0; return 0;
} elsif ($mtime > $now + $auth_graceperiod) { } elsif ($mtime > $now + $auth_graceperiod) {
# a nodes RTC had a time set in the future during key generation -> ticket # a nodes RTC had a time set in the future during key generation -> ticket
@ -187,14 +188,16 @@ sub check_authkey {
my (undef, $old_mtime) = get_pubkey(1); my (undef, $old_mtime) = get_pubkey(1);
if ($old_mtime && $mtime >= $old_mtime && $mtime - $old_mtime < $ticket_lifetime) { if ($old_mtime && $mtime >= $old_mtime && $mtime - $old_mtime < $ticket_lifetime) {
warn "auth key pair generated in the future (key $mtime > host $now)," warn "auth key pair generated in the future (key $mtime > host $now),"
." but old key still exists and in valid grace period so avoid automatic" . " but old key still exists and in valid grace period so avoid automatic"
." fixup. Cluster time not in sync?\n" if !$quiet; . " fixup. Cluster time not in sync?\n"
if !$quiet;
return 1; return 1;
} }
warn "auth key pair generated in the future (key $mtime > host $now), rotating..\n" if !$quiet; warn "auth key pair generated in the future (key $mtime > host $now), rotating..\n"
if !$quiet;
return 0; return 0;
} else { } else {
warn "auth key new enough, skipping rotation\n" if !$quiet;; warn "auth key new enough, skipping rotation\n" if !$quiet;
return 1; return 1;
} }
} }
@ -203,7 +206,9 @@ sub check_authkey {
sub rotate_authkey { sub rotate_authkey {
return if $authkey_lifetime == 0; return if $authkey_lifetime == 0;
PVE::Cluster::cfs_lock_authkey(undef, sub { PVE::Cluster::cfs_lock_authkey(
undef,
sub {
# stat() calls might be answered from the kernel page cache for up to # stat() calls might be answered from the kernel page cache for up to
# 1s, so this special dance is needed to avoid a double rotation in # 1s, so this special dance is needed to avoid a double rotation in
# clusters *despite* the cfs_lock context.. # clusters *despite* the cfs_lock context..
@ -258,22 +263,28 @@ sub rotate_authkey {
unlink $pve_auth_key_files->{pub}; unlink $pve_auth_key_files->{pub};
unlink $pve_auth_key_files->{priv}; unlink $pve_auth_key_files->{priv};
} }
}); },
);
die $@ if $@; die $@ if $@;
} }
PVE::JSONSchema::register_standard_option('tokenid', { PVE::JSONSchema::register_standard_option(
'tokenid',
{
description => "API token identifier.", description => "API token identifier.",
type => "string", type => "string",
format => "pve-tokenid", format => "pve-tokenid",
}); },
);
our $token_subid_regex = $PVE::Auth::Plugin::realm_regex; our $token_subid_regex = $PVE::Auth::Plugin::realm_regex;
# username@realm username realm tokenid # username@realm username realm tokenid
our $token_full_regex = qr/((${PVE::Auth::Plugin::user_regex})\@(${PVE::Auth::Plugin::realm_regex}))!(${token_subid_regex})/; our $token_full_regex =
qr/((${PVE::Auth::Plugin::user_regex})\@(${PVE::Auth::Plugin::realm_regex}))!(${token_subid_regex})/;
our $userid_or_token_regex = qr/^$PVE::Auth::Plugin::user_regex\@$PVE::Auth::Plugin::realm_regex(?:!$token_subid_regex)?$/; our $userid_or_token_regex =
qr/^$PVE::Auth::Plugin::user_regex\@$PVE::Auth::Plugin::realm_regex(?:!$token_subid_regex)?$/;
sub split_tokenid { sub split_tokenid {
my ($tokenid, $noerr) = @_; my ($tokenid, $noerr) = @_;
@ -282,7 +293,8 @@ sub split_tokenid {
return ($1, $4); return ($1, $4);
} }
die "'$tokenid' is not a valid token ID - not able to split into user and token parts\n" if !$noerr; die "'$tokenid' is not a valid token ID - not able to split into user and token parts\n"
if !$noerr;
return undef; return undef;
} }
@ -296,6 +308,7 @@ sub join_tokenid {
} }
PVE::JSONSchema::register_format('pve-tokenid', \&pve_verify_tokenid); PVE::JSONSchema::register_format('pve-tokenid', \&pve_verify_tokenid);
sub pve_verify_tokenid { sub pve_verify_tokenid {
my ($tokenid, $noerr) = @_; my ($tokenid, $noerr) = @_;
@ -308,7 +321,6 @@ sub pve_verify_tokenid {
return undef; return undef;
} }
my $csrf_prevention_secret; my $csrf_prevention_secret;
my $csrf_prevention_secret_legacy; my $csrf_prevention_secret_legacy;
my $get_csrfr_secret = sub { my $get_csrfr_secret = sub {
@ -325,7 +337,7 @@ sub assemble_csrf_prevention_token {
my $secret = &$get_csrfr_secret(); my $secret = &$get_csrfr_secret();
return PVE::Ticket::assemble_csrf_prevention_token ($secret, $username); return PVE::Ticket::assemble_csrf_prevention_token($secret, $username);
} }
sub verify_csrf_prevention_token { sub verify_csrf_prevention_token {
@ -343,7 +355,8 @@ sub verify_csrf_prevention_token {
} }
return PVE::Ticket::verify_csrf_prevention_token( return PVE::Ticket::verify_csrf_prevention_token(
$secret, $username, $token, -$auth_graceperiod, $ticket_lifetime, $noerr); $secret, $username, $token, -$auth_graceperiod, $ticket_lifetime, $noerr,
);
} }
my $get_ticket_age_range = sub { my $get_ticket_age_range = sub {
@ -404,8 +417,8 @@ sub verify_ticket : prototype($;$$) {
my ($min, $max) = $get_ticket_age_range->($now, $rsa_mtime, $old); my ($min, $max) = $get_ticket_age_range->($now, $rsa_mtime, $old);
return undef if !defined($min); return undef if !defined($min);
return PVE::Ticket::verify_rsa_ticket( return PVE::Ticket::verify_rsa_ticket($rsa_pub, 'PVE', $ticket, $tfa_ticket_aad, $min,
$rsa_pub, 'PVE', $ticket, $tfa_ticket_aad, $min, $max, 1); $max, 1);
}; };
my ($data, $age) = $check->(); my ($data, $age) = $check->();
@ -494,7 +507,8 @@ sub verify_token {
my $token_info = $user->{tokens}->{$token}; my $token_info = $user->{tokens}->{$token};
my $ctime = time(); my $ctime = time();
die "token '$token' access expired\n" if $token_info->{expire} && ($token_info->{expire} < $ctime); die "token '$token' access expired\n"
if $token_info->{expire} && ($token_info->{expire} < $ctime);
die "invalid token value!\n" if !PVE::Cluster::verify_token($tokenid, $value); die "invalid token value!\n" if !PVE::Cluster::verify_token($tokenid, $value);
@ -512,8 +526,7 @@ my $assemble_short_lived_ticket = sub {
my $secret_data = "$username:$path"; my $secret_data = "$username:$path";
return PVE::Ticket::assemble_rsa_ticket( return PVE::Ticket::assemble_rsa_ticket($rsa_priv, $prefix, undef, $secret_data);
$rsa_priv, $prefix, undef, $secret_data);
}; };
my $verify_short_lived_ticket = sub { my $verify_short_lived_ticket = sub {
@ -535,8 +548,8 @@ my $verify_short_lived_ticket = sub {
} }
} }
return PVE::Ticket::verify_rsa_ticket( return PVE::Ticket::verify_rsa_ticket($rsa_pub, $prefix, $ticket, $secret_data, -20, 40,
$rsa_pub, $prefix, $ticket, $secret_data, -20, 40, $noerr); $noerr);
}; };
# VNC tickets # VNC tickets
@ -574,8 +587,7 @@ sub assemble_spice_ticket {
my $secret = &$get_csrfr_secret(); my $secret = &$get_csrfr_secret();
return PVE::Ticket::assemble_spice_ticket( return PVE::Ticket::assemble_spice_ticket($secret, $username, $vmid, $node);
$secret, $username, $vmid, $node);
} }
sub verify_spice_connect_url { sub verify_spice_connect_url {
@ -707,8 +719,9 @@ sub verify_one_time_pw {
my $proxy; my $proxy;
if ($type eq 'yubico') { if ($type eq 'yubico') {
PVE::OTP::yubico_verify_otp($otp, $keys, $tfa_cfg->{url}, PVE::OTP::yubico_verify_otp(
$tfa_cfg->{id}, $tfa_cfg->{key}, $proxy); $otp, $keys, $tfa_cfg->{url}, $tfa_cfg->{id}, $tfa_cfg->{key}, $proxy,
);
} elsif ($type eq 'oath') { } elsif ($type eq 'oath') {
PVE::OTP::oath_verify_otp($otp, $keys, $tfa_cfg->{step}, $tfa_cfg->{digits}); PVE::OTP::oath_verify_otp($otp, $keys, $tfa_cfg->{step}, $tfa_cfg->{digits});
} else { } else {
@ -816,7 +829,8 @@ sub authenticate_2nd_new_do : prototype($$$$) {
if (defined($tfa_response)) { if (defined($tfa_response)) {
if (defined($tfa_challenge)) { if (defined($tfa_challenge)) {
$tfa_done = 1; $tfa_done = 1;
$result = $tfa_cfg->authentication_verify2($username, $tfa_challenge, $tfa_response); $result =
$tfa_cfg->authentication_verify2($username, $tfa_challenge, $tfa_response);
} else { } else {
die "no such challenge\n"; die "no such challenge\n";
} }
@ -960,7 +974,7 @@ sub iterate_acl_tree {
sub find_acl_tree_node { sub find_acl_tree_node {
my ($root, $path) = @_; my ($root, $path) = @_;
my $split_path = [ split("/", $path) ]; my $split_path = [split("/", $path)];
if (!$split_path) { if (!$split_path) {
return $root; return $root;
@ -989,9 +1003,9 @@ sub add_user_group {
sub delete_user_group { sub delete_user_group {
my ($username, $usercfg) = @_; my ($username, $usercfg) = @_;
foreach my $group (keys %{$usercfg->{groups}}) { foreach my $group (keys %{ $usercfg->{groups} }) {
delete ($usercfg->{groups}->{$group}->{users}->{$username}) delete($usercfg->{groups}->{$group}->{users}->{$username})
if $usercfg->{groups}->{$group}->{users}->{$username}; if $usercfg->{groups}->{$group}->{users}->{$username};
} }
} }
@ -1002,7 +1016,7 @@ sub delete_user_acl {
my $code = sub { my $code = sub {
my ($path, $acl_node) = @_; my ($path, $acl_node) = @_;
delete ($acl_node->{users}->{$username}) delete($acl_node->{users}->{$username})
if $acl_node->{users}->{$username}; if $acl_node->{users}->{$username};
}; };
@ -1015,7 +1029,7 @@ sub delete_group_acl {
my $code = sub { my $code = sub {
my ($path, $acl_node) = @_; my ($path, $acl_node) = @_;
delete ($acl_node->{groups}->{$group}) delete($acl_node->{groups}->{$group})
if $acl_node->{groups}->{$group}; if $acl_node->{groups}->{$group};
}; };
@ -1025,7 +1039,7 @@ sub delete_group_acl {
sub delete_pool_acl { sub delete_pool_acl {
my ($pool, $usercfg) = @_; my ($pool, $usercfg) = @_;
delete ($usercfg->{acl_root}->{children}->{pool}->{children}->{$pool}); delete($usercfg->{acl_root}->{children}->{pool}->{children}->{$pool});
} }
# we automatically create some predefined roles by splitting privs # we automatically create some predefined roles by splitting privs
@ -1069,8 +1083,7 @@ my $privgroups = {
'Sys.AccessNetwork', # for, e.g., downloading ISOs from any URL 'Sys.AccessNetwork', # for, e.g., downloading ISOs from any URL
], ],
admin => [ admin => [
'Sys.Console', 'Sys.Console', 'Sys.Syslog',
'Sys.Syslog',
], ],
user => [], user => [],
audit => [ audit => [
@ -1080,8 +1093,7 @@ my $privgroups = {
Datastore => { Datastore => {
root => [], root => [],
admin => [ admin => [
'Datastore.Allocate', 'Datastore.Allocate', 'Datastore.AllocateTemplate',
'Datastore.AllocateTemplate',
], ],
user => [ user => [
'Datastore.AllocateSpace', 'Datastore.AllocateSpace',
@ -1093,8 +1105,7 @@ my $privgroups = {
SDN => { SDN => {
root => [], root => [],
admin => [ admin => [
'SDN.Allocate', 'SDN.Allocate', 'SDN.Audit',
'SDN.Audit',
], ],
user => [ user => [
'SDN.Use', 'SDN.Use',
@ -1155,21 +1166,21 @@ sub create_roles {
for my $cat (keys %$privgroups) { for my $cat (keys %$privgroups) {
my $cd = $privgroups->{$cat}; my $cd = $privgroups->{$cat};
# create map to easily check if a privilege is valid # create map to easily check if a privilege is valid
for my $priv (@{$cd->{root}}, @{$cd->{admin}}, @{$cd->{user}}, @{$cd->{audit}}) { for my $priv (@{ $cd->{root} }, @{ $cd->{admin} }, @{ $cd->{user} }, @{ $cd->{audit} }) {
$valid_privs->{$priv} = 1; $valid_privs->{$priv} = 1;
} }
# create grouped admin roles and PVEAdmin # create grouped admin roles and PVEAdmin
for my $priv (@{$cd->{admin}}, @{$cd->{user}}, @{$cd->{audit}}) { for my $priv (@{ $cd->{admin} }, @{ $cd->{user} }, @{ $cd->{audit} }) {
$special_roles->{"PVE${cat}Admin"}->{$priv} = 1; $special_roles->{"PVE${cat}Admin"}->{$priv} = 1;
$special_roles->{"PVEAdmin"}->{$priv} = 1; $special_roles->{"PVEAdmin"}->{$priv} = 1;
} }
# create grouped user and audit roles # create grouped user and audit roles
if (scalar(@{$cd->{user}})) { if (scalar(@{ $cd->{user} })) {
for my $priv (@{$cd->{user}}, @{$cd->{audit}}) { for my $priv (@{ $cd->{user} }, @{ $cd->{audit} }) {
$special_roles->{"PVE${cat}User"}->{$priv} = 1; $special_roles->{"PVE${cat}User"}->{$priv} = 1;
} }
} }
for my $priv (@{$cd->{audit}}) { for my $priv (@{ $cd->{audit} }) {
$special_roles->{"PVEAuditor"}->{$priv} = 1; $special_roles->{"PVEAuditor"}->{$priv} = 1;
} }
} }
@ -1179,7 +1190,7 @@ sub create_roles {
delete $special_roles->{"PVEAdmin"}->{"Mapping.Modify"}; delete $special_roles->{"PVEAdmin"}->{"Mapping.Modify"};
$special_roles->{"PVETemplateUser"} = { 'VM.Clone' => 1, 'VM.Audit' => 1 }; $special_roles->{"PVETemplateUser"} = { 'VM.Clone' => 1, 'VM.Audit' => 1 };
}; }
create_roles(); create_roles();
@ -1207,7 +1218,7 @@ sub add_role_privs {
die "role '$role' does not exist\n" if !$usercfg->{roles}->{$role}; die "role '$role' does not exist\n" if !$usercfg->{roles}->{$role};
foreach my $priv (split_list($privs)) { foreach my $priv (split_list($privs)) {
if (defined ($valid_privs->{$priv})) { if (defined($valid_privs->{$priv})) {
$usercfg->{roles}->{$role}->{$priv} = 1; $usercfg->{roles}->{$role}->{$priv} = 1;
} else { } else {
die "invalid privilege '$priv'\n"; die "invalid privilege '$priv'\n";
@ -1226,9 +1237,10 @@ sub lookup_username {
if (!$casesensitive) { if (!$casesensitive) {
my $usercfg = cfs_read_file('user.cfg'); my $usercfg = cfs_read_file('user.cfg');
my @matches = grep { lc $username eq lc $_ } (keys %{$usercfg->{users}}); my @matches = grep { lc $username eq lc $_ } (keys %{ $usercfg->{users} });
die "ambiguous case insensitive match of username '$username', cannot safely grant access!\n" die
"ambiguous case insensitive match of username '$username', cannot safely grant access!\n"
if scalar @matches > 1 && !$noerr; if scalar @matches > 1 && !$noerr;
return $matches[0] if defined($matches[0]); return $matches[0] if defined($matches[0]);
@ -1290,6 +1302,7 @@ sub check_path {
} }
PVE::JSONSchema::register_format('pve-groupid', \&verify_groupname); PVE::JSONSchema::register_format('pve-groupid', \&verify_groupname);
sub verify_groupname { sub verify_groupname {
my ($groupname, $noerr) = @_; my ($groupname, $noerr) = @_;
@ -1304,6 +1317,7 @@ sub verify_groupname {
} }
PVE::JSONSchema::register_format('pve-roleid', \&verify_rolename); PVE::JSONSchema::register_format('pve-roleid', \&verify_rolename);
sub verify_rolename { sub verify_rolename {
my ($rolename, $noerr) = @_; my ($rolename, $noerr) = @_;
@ -1318,6 +1332,7 @@ sub verify_rolename {
} }
PVE::JSONSchema::register_format('pve-poolid', \&verify_poolname); PVE::JSONSchema::register_format('pve-poolid', \&verify_poolname);
sub verify_poolname { sub verify_poolname {
my ($poolname, $noerr) = @_; my ($poolname, $noerr) = @_;
@ -1338,6 +1353,7 @@ sub verify_poolname {
} }
PVE::JSONSchema::register_format('pve-priv', \&verify_privname); PVE::JSONSchema::register_format('pve-priv', \&verify_privname);
sub verify_privname { sub verify_privname {
my ($priv, $noerr) = @_; my ($priv, $noerr) = @_;
@ -1380,10 +1396,10 @@ sub parse_user_config {
my $line = $1; my $line = $1;
my @data; my @data;
foreach my $d (split (/:/, $line)) { foreach my $d (split(/:/, $line)) {
$d =~ s/^\s+//; $d =~ s/^\s+//;
$d =~ s/\s+$//; $d =~ s/\s+$//;
push @data, $d push @data, $d;
} }
my $et = shift @data; my $et = shift @data;
@ -1402,7 +1418,8 @@ sub parse_user_config {
$expire = 0 if !$expire; $expire = 0 if !$expire;
if ($expire !~ m/^\d+$/) { if ($expire !~ m/^\d+$/) {
warn "user config - ignore user '$user' - (illegal characters in expire '$expire')\n"; warn
"user config - ignore user '$user' - (illegal characters in expire '$expire')\n";
next; next;
} }
$expire = int($expire); $expire = int($expire);
@ -1467,7 +1484,7 @@ sub parse_user_config {
$cfg->{roles}->{$role} = {} if !$cfg->{roles}->{$role}; $cfg->{roles}->{$role} = {} if !$cfg->{roles}->{$role};
foreach my $priv (split_list($privlist)) { foreach my $priv (split_list($privlist)) {
if (defined ($valid_privs->{$priv})) { if (defined($valid_privs->{$priv})) {
$cfg->{roles}->{$role}->{$priv} = 1; $cfg->{roles}->{$role}->{$priv} = 1;
} else { } else {
warn "user config - ignore invalid privilege '$priv'\n"; warn "user config - ignore invalid privilege '$priv'\n";
@ -1510,7 +1527,8 @@ sub parse_user_config {
$acl_node->{users}->{$ug}->{$role} = $propagate; $acl_node->{users}->{$ug}->{$role} = $propagate;
} elsif (my ($user, $token) = split_tokenid($ug, 1)) { } elsif (my ($user, $token) = split_tokenid($ug, 1)) {
if (check_token_exist($cfg, $user, $token, 1)) { if (check_token_exist($cfg, $user, $token, 1)) {
$acl_node = find_acl_tree_node($cfg->{acl_root}, $path) if !$acl_node; $acl_node = find_acl_tree_node($cfg->{acl_root}, $path)
if !$acl_node;
$acl_node->{tokens}->{$ug}->{$role} = $propagate; $acl_node->{tokens}->{$ug}->{$role} = $propagate;
} else { } else {
warn "user config - ignore invalid acl token '$ug'\n"; warn "user config - ignore invalid acl token '$ug'\n";
@ -1589,7 +1607,8 @@ sub parse_user_config {
$expire = 0 if !$expire; $expire = 0 if !$expire;
if ($expire !~ m/^\d+$/) { if ($expire !~ m/^\d+$/) {
warn "user config - ignore token '$tokenid' - (illegal characters in expire '$expire')\n"; warn
"user config - ignore token '$tokenid' - (illegal characters in expire '$expire')\n";
next; next;
} }
$expire = int($expire); $expire = int($expire);
@ -1618,7 +1637,7 @@ sub write_user_config {
my $data = ''; my $data = '';
foreach my $user (sort keys %{$cfg->{users}}) { foreach my $user (sort keys %{ $cfg->{users} }) {
my $d = $cfg->{users}->{$user}; my $d = $cfg->{users}->{$user};
my $firstname = $d->{firstname} ? PVE::Tools::encode_text($d->{firstname}) : ''; my $firstname = $d->{firstname} ? PVE::Tools::encode_text($d->{firstname}) : '';
my $lastname = $d->{lastname} ? PVE::Tools::encode_text($d->{lastname}) : ''; my $lastname = $d->{lastname} ? PVE::Tools::encode_text($d->{lastname}) : '';
@ -1642,30 +1661,30 @@ sub write_user_config {
$data .= "\n"; $data .= "\n";
foreach my $group (sort keys %{$cfg->{groups}}) { foreach my $group (sort keys %{ $cfg->{groups} }) {
my $d = $cfg->{groups}->{$group}; my $d = $cfg->{groups}->{$group};
my $list = join (',', sort keys %{$d->{users}}); my $list = join(',', sort keys %{ $d->{users} });
my $comment = $d->{comment} ? PVE::Tools::encode_text($d->{comment}) : ''; my $comment = $d->{comment} ? PVE::Tools::encode_text($d->{comment}) : '';
$data .= "group:$group:$list:$comment:\n"; $data .= "group:$group:$list:$comment:\n";
} }
$data .= "\n"; $data .= "\n";
foreach my $pool (sort keys %{$cfg->{pools}}) { foreach my $pool (sort keys %{ $cfg->{pools} }) {
my $d = $cfg->{pools}->{$pool}; my $d = $cfg->{pools}->{$pool};
my $vmlist = join (',', sort keys %{$d->{vms}}); my $vmlist = join(',', sort keys %{ $d->{vms} });
my $storelist = join (',', sort keys %{$d->{storage}}); my $storelist = join(',', sort keys %{ $d->{storage} });
my $comment = $d->{comment} ? PVE::Tools::encode_text($d->{comment}) : ''; my $comment = $d->{comment} ? PVE::Tools::encode_text($d->{comment}) : '';
$data .= "pool:$pool:$comment:$vmlist:$storelist:\n"; $data .= "pool:$pool:$comment:$vmlist:$storelist:\n";
} }
$data .= "\n"; $data .= "\n";
foreach my $role (sort keys %{$cfg->{roles}}) { foreach my $role (sort keys %{ $cfg->{roles} }) {
next if $special_roles->{$role}; next if $special_roles->{$role};
my $d = $cfg->{roles}->{$role}; my $d = $cfg->{roles}->{$role};
my $list = join (',', sort keys %$d); my $list = join(',', sort keys %$d);
$data .= "role:$role:$list:\n"; $data .= "role:$role:$list:\n";
} }
@ -1679,7 +1698,7 @@ sub write_user_config {
my $l0 = ''; my $l0 = '';
my $l1 = ''; my $l1 = '';
foreach my $role (sort keys %{$acl_members->{$member}}) { foreach my $role (sort keys %{ $acl_members->{$member} }) {
my $propagate = $acl_members->{$member}->{$role}; my $propagate = $acl_members->{$member}->{$role};
if ($propagate) { if ($propagate) {
$l1 .= ',' if $l1; $l1 .= ',' if $l1;
@ -1694,7 +1713,10 @@ sub write_user_config {
} }
}; };
iterate_acl_tree("/", $cfg->{acl_root}, sub { iterate_acl_tree(
"/",
$cfg->{acl_root},
sub {
my ($path, $d) = @_; my ($path, $d) = @_;
my $rolelist_members = {}; my $rolelist_members = {};
@ -1706,15 +1728,16 @@ sub write_user_config {
$collect_rolelist_members->($d->{'tokens'}, $rolelist_members, ''); $collect_rolelist_members->($d->{'tokens'}, $rolelist_members, '');
foreach my $propagate (0,1) { foreach my $propagate (0, 1) {
my $filtered = $rolelist_members->{$propagate}; my $filtered = $rolelist_members->{$propagate};
foreach my $rolelist (sort keys %$filtered) { foreach my $rolelist (sort keys %$filtered) {
my $uglist = join (',', sort keys %{$filtered->{$rolelist}}); my $uglist = join(',', sort keys %{ $filtered->{$rolelist} });
$data .= "acl:$propagate:$path:$uglist:$rolelist:\n"; $data .= "acl:$propagate:$path:$uglist:$rolelist:\n";
} }
} }
}); },
);
return $data; return $data;
} }
@ -1776,9 +1799,9 @@ sub roles {
my $roles = {}; my $roles = {};
my $split = [ split("/", $path) ]; my $split = [split("/", $path)];
if ($path eq '/') { if ($path eq '/') {
$split = [ '' ]; $split = [''];
} }
my $acl = $cfg->{acl_root}; my $acl = $cfg->{acl_root};
@ -1826,7 +1849,7 @@ sub roles {
} }
my $new; my $new;
foreach my $g (keys %{$acl->{groups}}) { foreach my $g (keys %{ $acl->{groups} }) {
next if !$cfg->{groups}->{$g}->{users}->{$user}; next if !$cfg->{groups}->{$g}->{users}->{$user};
if (my $ri = $acl->{groups}->{$g}) { if (my $ri = $acl->{groups}->{$g}) {
foreach my $role (keys %$ri) { foreach my $role (keys %$ri) {
@ -1845,7 +1868,7 @@ sub roles {
} }
} }
return { 'NoAccess' => $roles->{NoAccess} } if defined ($roles->{NoAccess}); return { 'NoAccess' => $roles->{NoAccess} } if defined($roles->{NoAccess});
#return () if defined ($roles->{NoAccess}); #return () if defined ($roles->{NoAccess});
#print "permission $user $path = " . Dumper ($roles); #print "permission $user $path = " . Dumper ($roles);
@ -1889,15 +1912,17 @@ sub remove_storage_access {
delete $usercfg->{acl_root}->{children}->{storage}->{children}->{$storeid}; delete $usercfg->{acl_root}->{children}->{storage}->{children}->{$storeid};
$modified = 1; $modified = 1;
} }
foreach my $pool (keys %{$usercfg->{pools}}) { foreach my $pool (keys %{ $usercfg->{pools} }) {
delete $usercfg->{pools}->{$pool}->{storage}->{$storeid}; delete $usercfg->{pools}->{$pool}->{storage}->{$storeid};
$modified = 1; $modified = 1;
} }
cfs_write_file("user.cfg", $usercfg) if $modified; cfs_write_file("user.cfg", $usercfg) if $modified;
}; };
lock_user_config($deleteStorageAccessFn, lock_user_config(
"access permissions cleanup for storage $storeid failed"); $deleteStorageAccessFn,
"access permissions cleanup for storage $storeid failed",
);
} }
sub add_vm_to_pool { sub add_vm_to_pool {
@ -2044,29 +2069,30 @@ sub user_get_tfa : prototype($$$) {
# bash completion helpers # bash completion helpers
register_standard_option('userid-completed', register_standard_option(
get_standard_option('userid', { completion => \&complete_username}), 'userid-completed',
get_standard_option('userid', { completion => \&complete_username }),
); );
sub complete_username { sub complete_username {
my $user_cfg = cfs_read_file('user.cfg'); my $user_cfg = cfs_read_file('user.cfg');
return [ keys %{$user_cfg->{users}} ]; return [keys %{ $user_cfg->{users} }];
} }
sub complete_group { sub complete_group {
my $user_cfg = cfs_read_file('user.cfg'); my $user_cfg = cfs_read_file('user.cfg');
return [ keys %{$user_cfg->{groups}} ]; return [keys %{ $user_cfg->{groups} }];
} }
sub complete_realm { sub complete_realm {
my $domain_cfg = cfs_read_file('domains.cfg'); my $domain_cfg = cfs_read_file('domains.cfg');
return [ keys %{$domain_cfg->{ids}} ]; return [keys %{ $domain_cfg->{ids} }];
} }
1; 1;

View File

@ -32,7 +32,8 @@ sub properties {
optional => 1, optional => 1,
}, },
sslversion => { sslversion => {
description => "LDAPS TLS/SSL version. It's not recommended to use version older than 1.2!", description =>
"LDAPS TLS/SSL version. It's not recommended to use version older than 1.2!",
type => 'string', type => 'string',
enum => [qw(tlsv1 tlsv1_1 tlsv1_2 tlsv1_3)], enum => [qw(tlsv1 tlsv1_1 tlsv1_2 tlsv1_3)],
optional => 1, optional => 1,
@ -74,7 +75,7 @@ sub options {
port => { optional => 1 }, port => { optional => 1 },
secure => { optional => 1 }, secure => { optional => 1 },
sslversion => { optional => 1 }, sslversion => { optional => 1 },
default => { optional => 1 },, default => { optional => 1 },
comment => { optional => 1 }, comment => { optional => 1 },
tfa => { optional => 1 }, tfa => { optional => 1 },
verify => { optional => 1 }, verify => { optional => 1 },

View File

@ -36,7 +36,8 @@ sub properties {
maxLength => 256, maxLength => 256,
}, },
password => { password => {
description => "LDAP bind password. Will be stored in '/etc/pve/priv/realm/<REALM>.pw'.", description =>
"LDAP bind password. Will be stored in '/etc/pve/priv/realm/<REALM>.pw'.",
type => 'string', type => 'string',
optional => 1, optional => 1,
}, },
@ -70,10 +71,10 @@ sub properties {
}, },
sync_attributes => { sync_attributes => {
description => "Comma separated list of key=value pairs for specifying" description => "Comma separated list of key=value pairs for specifying"
." which LDAP attributes map to which PVE user field. For example," . " which LDAP attributes map to which PVE user field. For example,"
." to map the LDAP attribute 'mail' to PVEs 'email', write " . " to map the LDAP attribute 'mail' to PVEs 'email', write "
." 'email=mail'. By default, each PVE user field is represented " . " 'email=mail'. By default, each PVE user field is represented "
." by an LDAP attribute of the same name.", . " by an LDAP attribute of the same name.",
optional => 1, optional => 1,
type => 'string', type => 'string',
pattern => '\w+=[^,]+(,\s*\w+=[^,]+)*', pattern => '\w+=[^,]+(,\s*\w+=[^,]+)*',
@ -87,14 +88,14 @@ sub properties {
}, },
group_dn => { group_dn => {
description => "LDAP base domain name for group sync. If not set, the" description => "LDAP base domain name for group sync. If not set, the"
." base_dn will be used.", . " base_dn will be used.",
type => 'string', type => 'string',
optional => 1, optional => 1,
maxLength => 256, maxLength => 256,
}, },
group_name_attr => { group_name_attr => {
description => "LDAP attribute representing a groups name. If not set" description => "LDAP attribute representing a groups name. If not set"
." or found, the first value of the DN will be used as name.", . " or found, the first value of the DN will be used as name.",
type => 'string', type => 'string',
format => 'ldap-simple-attr', format => 'ldap-simple-attr',
optional => 1, optional => 1,
@ -122,7 +123,7 @@ sub properties {
mode => { mode => {
description => "LDAP protocol mode.", description => "LDAP protocol mode.",
type => 'string', type => 'string',
enum => [ 'ldap', 'ldaps', 'ldap+starttls'], enum => ['ldap', 'ldaps', 'ldap+starttls'],
optional => 1, optional => 1,
default => 'ldap', default => 'ldap',
}, },
@ -238,7 +239,7 @@ sub connect_and_bind {
} }
if (!$config->{base_dn}) { if (!$config->{base_dn}) {
my $root = $ldap->root_dse(attrs => [ 'defaultNamingContext' ]); my $root = $ldap->root_dse(attrs => ['defaultNamingContext']);
$config->{base_dn} = $root->get_value('defaultNamingContext'); $config->{base_dn} = $root->get_value('defaultNamingContext');
} }
@ -306,7 +307,8 @@ sub get_users {
$config->{user_classes} //= 'inetorgperson, posixaccount, person, user'; $config->{user_classes} //= 'inetorgperson, posixaccount, person, user';
my $classes = [PVE::Tools::split_list($config->{user_classes})]; my $classes = [PVE::Tools::split_list($config->{user_classes})];
my $users = PVE::LDAP::query_users($ldap, $filter, [keys %$ldap_attribute_map], $basedn, $classes); my $users =
PVE::LDAP::query_users($ldap, $filter, [keys %$ldap_attribute_map], $basedn, $classes);
my $ret = {}; my $ret = {};
my $dnmap = {}; my $dnmap = {};
@ -339,7 +341,7 @@ sub get_users {
if (wantarray) { if (wantarray) {
my $dn = $user->{dn}; my $dn = $user->{dn};
$dnmap->{lc($dn)} = $username; $dnmap->{ lc($dn) } = $username;
} }
} }
@ -365,7 +367,7 @@ sub get_groups {
foreach my $group (@$groups) { foreach my $group (@$groups) {
my $name = $group->{name}; my $name = $group->{name};
if (!$name && $group->{dn} =~ m/^[^=]+=([^,]+),/){ if (!$name && $group->{dn} =~ m/^[^=]+=([^,]+),/) {
$name = PVE::Tools::trim($1); $name = PVE::Tools::trim($1);
} }
if ($name) { if ($name) {
@ -379,8 +381,8 @@ sub get_groups {
} }
$ret->{$name} = { users => {} }; $ret->{$name} = { users => {} };
foreach my $member (@{$group->{members}}) { foreach my $member (@{ $group->{members} }) {
if (my $user = $dnmap->{lc($member)}) { if (my $user = $dnmap->{ lc($member) }) {
$ret->{$name}->{users}->{$user} = 1; $ret->{$name}->{users}->{$user} = 1;
} }
} }
@ -395,7 +397,8 @@ sub authenticate_user {
my $ldap = $class->connect_and_bind($config, $realm); my $ldap = $class->connect_and_bind($config, $realm);
my $user_dn = PVE::LDAP::get_user_dn($ldap, $username, $config->{user_attr}, $config->{base_dn}); my $user_dn =
PVE::LDAP::get_user_dn($ldap, $username, $config->{user_attr}, $config->{base_dn});
PVE::LDAP::auth_user_dn($ldap, $user_dn, $password); PVE::LDAP::auth_user_dn($ldap, $user_dn, $password);
$ldap->unbind(); $ldap->unbind();
@ -447,7 +450,7 @@ sub ldap_delete_credentials {
my ($realmid) = @_; my ($realmid) = @_;
if (my $cred_file = get_cred_file($realmid)) { if (my $cred_file = get_cred_file($realmid)) {
return if ! -e $cred_file; # nothing to do return if !-e $cred_file; # nothing to do
unlink($cred_file) or warn "removing LDAP credentials '$cred_file' failed: $!\n"; unlink($cred_file) or warn "removing LDAP credentials '$cred_file' failed: $!\n";
} }
} }

View File

@ -66,21 +66,22 @@ sub properties {
}, },
prompt => { prompt => {
description => "Specifies whether the Authorization Server prompts the End-User for" description => "Specifies whether the Authorization Server prompts the End-User for"
." reauthentication and consent.", . " reauthentication and consent.",
type => 'string', type => 'string',
pattern => '(?:none|login|consent|select_account|\S+)', # \S+ is the extension variant pattern => '(?:none|login|consent|select_account|\S+)', # \S+ is the extension variant
optional => 1, optional => 1,
}, },
scopes => { scopes => {
description => "Specifies the scopes (user details) that should be authorized and" description => "Specifies the scopes (user details) that should be authorized and"
." returned, for example 'email' or 'profile'.", . " returned, for example 'email' or 'profile'.",
type => 'string', # format => 'some-safe-id-list', # FIXME: TODO type => 'string', # format => 'some-safe-id-list', # FIXME: TODO
default => "email profile", default => "email profile",
optional => 1, optional => 1,
}, },
'acr-values' => { 'acr-values' => {
description => "Specifies the Authentication Context Class Reference values that the" description =>
."Authorization Server is being requested to use for the Auth Request.", "Specifies the Authentication Context Class Reference values that the"
. "Authorization Server is being requested to use for the Auth Request.",
type => 'string', type => 'string',
pattern => '^[^\x00-\x1F\x7F <>#"]*$', # Prohibit characters not allowed in URI RFC 2396. pattern => '^[^\x00-\x1F\x7F <>#"]*$', # Prohibit characters not allowed in URI RFC 2396.
optional => 1, optional => 1,
@ -119,5 +120,4 @@ sub authenticate_user {
die "OpenID realm does not allow password verification.\n"; die "OpenID realm does not allow password verification.\n";
} }
1; 1;

View File

@ -27,18 +27,22 @@ sub authenticate_user {
# user (www-data) need to be able to read /etc/passwd /etc/shadow # user (www-data) need to be able to read /etc/passwd /etc/shadow
die "no password\n" if !$password; die "no password\n" if !$password;
my $pamh = Authen::PAM->new('proxmox-ve-auth', $username, sub { my $pamh = Authen::PAM->new(
'proxmox-ve-auth',
$username,
sub {
my @res; my @res;
while(@_) { while (@_) {
my $msg_type = shift; my $msg_type = shift;
my $msg = shift; my $msg = shift;
push @res, (0, $password); push @res, (0, $password);
} }
push @res, 0; push @res, 0;
return @res; return @res;
}); },
);
if (!ref ($pamh)) { if (!ref($pamh)) {
my $err = $pamh->pam_strerror($pamh); my $err = $pamh->pam_strerror($pamh);
die "error during PAM init: $err"; die "error during PAM init: $err";
} }
@ -56,7 +60,7 @@ sub authenticate_user {
die "$err\n"; die "$err\n";
} }
if (($res = $pamh->pam_acct_mgmt (0)) != PAM_SUCCESS) { if (($res = $pamh->pam_acct_mgmt(0)) != PAM_SUCCESS) {
my $err = $pamh->pam_strerror($res); my $err = $pamh->pam_strerror($res);
die "$err\n"; die "$err\n";
} }
@ -66,7 +70,6 @@ sub authenticate_user {
return 1; return 1;
} }
sub store_password { sub store_password {
my ($class, $config, $realm, $username, $password) = @_; my ($class, $config, $realm, $username, $password) = @_;

View File

@ -12,9 +12,7 @@ use base qw(PVE::Auth::Plugin);
my $shadowconfigfile = "priv/shadow.cfg"; my $shadowconfigfile = "priv/shadow.cfg";
cfs_register_file($shadowconfigfile, cfs_register_file($shadowconfigfile, \&parse_shadow_passwd, \&write_shadow_config);
\&parse_shadow_passwd,
\&write_shadow_config);
sub parse_shadow_passwd { sub parse_shadow_passwd {
my ($filename, $raw) = @_; my ($filename, $raw) = @_;
@ -31,7 +29,7 @@ sub parse_shadow_passwd {
next; next;
} }
my ($userid, $crypt_pass) = split (/:/, $line); my ($userid, $crypt_pass) = split(/:/, $line);
$shadow->{users}->{$userid}->{shadow} = $crypt_pass; $shadow->{users}->{$userid}->{shadow} = $crypt_pass;
} }
@ -42,12 +40,12 @@ sub write_shadow_config {
my ($filename, $cfg) = @_; my ($filename, $cfg) = @_;
my $data = ''; my $data = '';
foreach my $userid (keys %{$cfg->{users}}) { foreach my $userid (keys %{ $cfg->{users} }) {
my $crypt_pass = $cfg->{users}->{$userid}->{shadow}; my $crypt_pass = $cfg->{users}->{$userid}->{shadow};
$data .= "$userid:$crypt_pass:\n"; $data .= "$userid:$crypt_pass:\n";
} }
return $data return $data;
} }
sub lock_shadow_config { sub lock_shadow_config {
@ -80,8 +78,8 @@ sub authenticate_user {
my $shadow_cfg = cfs_read_file($shadowconfigfile); my $shadow_cfg = cfs_read_file($shadowconfigfile);
if ($shadow_cfg->{users}->{$username}) { if ($shadow_cfg->{users}->{$username}) {
my $encpw = crypt(Encode::encode('utf8', $password), my $encpw =
$shadow_cfg->{users}->{$username}->{shadow}); crypt(Encode::encode('utf8', $password), $shadow_cfg->{users}->{$username}->{shadow});
die "invalid credentials\n" if ($encpw ne $shadow_cfg->{users}->{$username}->{shadow}); die "invalid credentials\n" if ($encpw ne $shadow_cfg->{users}->{$username}->{shadow});
} else { } else {
die "no password set\n"; die "no password set\n";

View File

@ -15,9 +15,11 @@ use base qw(PVE::SectionConfig);
my $domainconfigfile = "domains.cfg"; my $domainconfigfile = "domains.cfg";
cfs_register_file($domainconfigfile, cfs_register_file(
$domainconfigfile,
sub { __PACKAGE__->parse_config(@_); }, sub { __PACKAGE__->parse_config(@_); },
sub { __PACKAGE__->write_config(@_); }); sub { __PACKAGE__->write_config(@_); },
);
sub lock_domain_config { sub lock_domain_config {
my ($code, $errmsg) = @_; my ($code, $errmsg) = @_;
@ -34,6 +36,7 @@ our $user_regex = qr![^\s:/]+!;
our $groupname_regex_chars = qr/A-Za-z0-9\.\-_/; our $groupname_regex_chars = qr/A-Za-z0-9\.\-_/;
PVE::JSONSchema::register_format('pve-realm', \&pve_verify_realm); PVE::JSONSchema::register_format('pve-realm', \&pve_verify_realm);
sub pve_verify_realm { sub pve_verify_realm {
my ($realm, $noerr) = @_; my ($realm, $noerr) = @_;
@ -44,45 +47,56 @@ sub pve_verify_realm {
return $realm; return $realm;
} }
PVE::JSONSchema::register_standard_option('realm', { PVE::JSONSchema::register_standard_option(
'realm',
{
description => "Authentication domain ID", description => "Authentication domain ID",
type => 'string', format => 'pve-realm', type => 'string',
format => 'pve-realm',
maxLength => 32, maxLength => 32,
}); },
);
my $remove_options = "(?:acl|properties|entry)"; my $remove_options = "(?:acl|properties|entry)";
PVE::JSONSchema::register_standard_option('sync-scope', { PVE::JSONSchema::register_standard_option(
'sync-scope',
{
description => "Select what to sync.", description => "Select what to sync.",
type => 'string', type => 'string',
enum => [qw(users groups both)], enum => [qw(users groups both)],
optional => '1', optional => '1',
}); },
);
PVE::JSONSchema::register_standard_option('sync-remove-vanished', { PVE::JSONSchema::register_standard_option(
'sync-remove-vanished',
{
description => "A semicolon-separated list of things to remove when they or the user" description => "A semicolon-separated list of things to remove when they or the user"
." vanishes during a sync. The following values are possible: 'entry' removes the" . " vanishes during a sync. The following values are possible: 'entry' removes the"
." user/group when not returned from the sync. 'properties' removes the set" . " user/group when not returned from the sync. 'properties' removes the set"
." properties on existing user/group that do not appear in the source (even custom ones)." . " properties on existing user/group that do not appear in the source (even custom ones)."
." 'acl' removes acls when the user/group is not returned from the sync." . " 'acl' removes acls when the user/group is not returned from the sync."
." Instead of a list it also can be 'none' (the default).", . " Instead of a list it also can be 'none' (the default).",
type => 'string', type => 'string',
default => 'none', default => 'none',
typetext => "([acl];[properties];[entry])|none", typetext => "([acl];[properties];[entry])|none",
pattern => "(?:(?:$remove_options\;)*$remove_options)|none", pattern => "(?:(?:$remove_options\;)*$remove_options)|none",
optional => '1', optional => '1',
}); },
);
my $realm_sync_options_desc = { my $realm_sync_options_desc = {
scope => get_standard_option('sync-scope'), scope => get_standard_option('sync-scope'),
'remove-vanished' => get_standard_option('sync-remove-vanished'), 'remove-vanished' => get_standard_option('sync-remove-vanished'),
# TODO check/rewrite in pve7to8, and remove with 8.0 # TODO check/rewrite in pve7to8, and remove with 8.0
full => { full => {
description => "DEPRECATED: use 'remove-vanished' instead. If set, uses the LDAP Directory as source of truth," description =>
." deleting users or groups not returned from the sync and removing" "DEPRECATED: use 'remove-vanished' instead. If set, uses the LDAP Directory as source of truth,"
." all locally modified properties of synced users. If not set," . " deleting users or groups not returned from the sync and removing"
." only syncs information which is present in the synced data, and does not" . " all locally modified properties of synced users. If not set,"
." delete or modify anything else.", . " only syncs information which is present in the synced data, and does not"
. " delete or modify anything else.",
type => 'boolean', type => 'boolean',
optional => '1', optional => '1',
}, },
@ -94,7 +108,7 @@ my $realm_sync_options_desc = {
}, },
purge => { purge => {
description => "DEPRECATED: use 'remove-vanished' instead. Remove ACLs for users or" description => "DEPRECATED: use 'remove-vanished' instead. Remove ACLs for users or"
." groups which were removed from the config during a sync.", . " groups which were removed from the config during a sync.",
type => 'boolean', type => 'boolean',
optional => '1', optional => '1',
}, },
@ -103,6 +117,7 @@ PVE::JSONSchema::register_standard_option('realm-sync-options', $realm_sync_opti
PVE::JSONSchema::register_format('realm-sync-options', $realm_sync_options_desc); PVE::JSONSchema::register_format('realm-sync-options', $realm_sync_options_desc);
PVE::JSONSchema::register_format('pve-userid', \&verify_username); PVE::JSONSchema::register_format('pve-userid', \&verify_username);
sub verify_username { sub verify_username {
my ($username, $noerr) = @_; my ($username, $noerr) = @_;
@ -131,11 +146,15 @@ sub verify_username {
return undef; return undef;
} }
PVE::JSONSchema::register_standard_option('userid', { PVE::JSONSchema::register_standard_option(
'userid',
{
description => "Full User ID, in the `name\@realm` format.", description => "Full User ID, in the `name\@realm` format.",
type => 'string', format => 'pve-userid', type => 'string',
format => 'pve-userid',
maxLength => 64, maxLength => 64,
}); },
);
my $tfa_format = { my $tfa_format = {
type => { type => {
@ -166,7 +185,8 @@ my $tfa_format = {
description => "TOTP digits.", description => "TOTP digits.",
format_description => 'COUNT', format_description => 'COUNT',
type => 'integer', type => 'integer',
minimum => 6, maximum => 8, minimum => 6,
maximum => 8,
default => 6, default => 6,
optional => 1, optional => 1,
}, },
@ -182,12 +202,16 @@ my $tfa_format = {
PVE::JSONSchema::register_format('pve-tfa-config', $tfa_format); PVE::JSONSchema::register_format('pve-tfa-config', $tfa_format);
PVE::JSONSchema::register_standard_option('tfa', { PVE::JSONSchema::register_standard_option(
'tfa',
{
description => "Use Two-factor authentication.", description => "Use Two-factor authentication.",
type => 'string', format => 'pve-tfa-config', type => 'string',
format => 'pve-tfa-config',
optional => 1, optional => 1,
maxLength => 128, maxLength => 128,
}); },
);
sub parse_tfa_config { sub parse_tfa_config {
my ($data) = @_; my ($data) = @_;
@ -226,7 +250,7 @@ sub parse_config {
my $cfg = $class->SUPER::parse_config($filename, $raw); my $cfg = $class->SUPER::parse_config($filename, $raw);
my $default; my $default;
foreach my $realm (keys %{$cfg->{ids}}) { foreach my $realm (keys %{ $cfg->{ids} }) {
my $data = $cfg->{ids}->{$realm}; my $data = $cfg->{ids}->{$realm};
# make sure there is only one default marker # make sure there is only one default marker
if ($data->{default}) { if ($data->{default}) {
@ -255,12 +279,12 @@ sub parse_config {
if !$cfg->{ids}->{pam}->{comment}; if !$cfg->{ids}->{pam}->{comment};
return $cfg; return $cfg;
}; }
sub write_config { sub write_config {
my ($class, $filename, $cfg) = @_; my ($class, $filename, $cfg) = @_;
foreach my $realm (keys %{$cfg->{ids}}) { foreach my $realm (keys %{ $cfg->{ids} }) {
my $data = $cfg->{ids}->{$realm}; my $data = $cfg->{ids}->{$realm};
if ($data->{comment}) { if ($data->{comment}) {
$data->{comment} = PVE::Tools::encode_text($data->{comment}); $data->{comment} = PVE::Tools::encode_text($data->{comment});

View File

@ -34,13 +34,16 @@ sub param_mapping {
PVE::CLIHandler::get_standard_mapping('pve-password'), PVE::CLIHandler::get_standard_mapping('pve-password'),
], ],
'create_ticket' => [ 'create_ticket' => [
PVE::CLIHandler::get_standard_mapping('pve-password', { PVE::CLIHandler::get_standard_mapping(
'pve-password',
{
func => sub { func => sub {
# do not accept values given on cmdline # do not accept values given on cmdline
return PVE::PTY::read_password('Enter password: '); return PVE::PTY::read_password('Enter password: ');
}, },
}), },
] ),
],
}; };
return $mapping->{$name}; return $mapping->{$name};
@ -93,10 +96,13 @@ __PACKAGE__->register_method({
properties => { properties => {
userid => get_standard_option('userid'), userid => get_standard_option('userid'),
tokenid => get_standard_option('token-subid'), tokenid => get_standard_option('token-subid'),
path => get_standard_option('acl-path', { path => get_standard_option(
'acl-path',
{
description => "Only dump this specific path, not the whole tree.", description => "Only dump this specific path, not the whole tree.",
optional => 1, optional => 1,
}), },
),
}, },
}, },
returns => { returns => {
@ -110,7 +116,8 @@ __PACKAGE__->register_method({
$param->{userid} = PVE::AccessControl::join_tokenid($param->{userid}, $token_subid); $param->{userid} = PVE::AccessControl::join_tokenid($param->{userid}, $token_subid);
return PVE::API2::AccessControl->permissions($param); return PVE::API2::AccessControl->permissions($param);
}}); },
});
__PACKAGE__->register_method({ __PACKAGE__->register_method({
name => 'delete_tfa', name => 'delete_tfa',
@ -145,7 +152,8 @@ __PACKAGE__->register_method({
cfs_write_file('priv/tfa.cfg', $tfa_cfg); cfs_write_file('priv/tfa.cfg', $tfa_cfg);
}); });
return; return;
}}); },
});
__PACKAGE__->register_method({ __PACKAGE__->register_method({
name => 'list_tfa', name => 'list_tfa',
@ -175,7 +183,7 @@ __PACKAGE__->register_method({
printf("${nl}${indent}%-9s %s\n${indent} %s\n", "$ty:", $id, $desc // ''); printf("${nl}${indent}%-9s %s\n${indent} %s\n", "$ty:", $id, $desc // '');
$nl = "\n"; $nl = "\n";
} }
}; }
my $tfa_cfg = cfs_read_file('priv/tfa.cfg'); my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
if (defined($userid)) { if (defined($userid)) {
@ -190,60 +198,144 @@ __PACKAGE__->register_method({
} }
} }
return; return;
}}); },
});
our $cmddef = { our $cmddef = {
user => { user => {
add => [ 'PVE::API2::User', 'create_user', ['userid'] ], add => ['PVE::API2::User', 'create_user', ['userid']],
modify => [ 'PVE::API2::User', 'update_user', ['userid'] ], modify => ['PVE::API2::User', 'update_user', ['userid']],
delete => [ 'PVE::API2::User', 'delete_user', ['userid'] ], delete => ['PVE::API2::User', 'delete_user', ['userid']],
list => [ 'PVE::API2::User', 'index', [], {}, $print_api_result, $PVE::RESTHandler::standard_output_options], list => [
permissions => [ 'PVE::API2::AccessControl', 'permissions', ['userid'], {}, $print_perm_result, $PVE::RESTHandler::standard_output_options], 'PVE::API2::User',
'index',
[],
{},
$print_api_result,
$PVE::RESTHandler::standard_output_options,
],
permissions => [
'PVE::API2::AccessControl',
'permissions',
['userid'],
{},
$print_perm_result,
$PVE::RESTHandler::standard_output_options,
],
tfa => { tfa => {
delete => [ __PACKAGE__, 'delete_tfa', ['userid'] ], delete => [__PACKAGE__, 'delete_tfa', ['userid']],
list => [ __PACKAGE__, 'list_tfa', ['userid'] ], list => [__PACKAGE__, 'list_tfa', ['userid']],
unlock => [ 'PVE::API2::User', 'unlock_tfa', ['userid'] ], unlock => ['PVE::API2::User', 'unlock_tfa', ['userid']],
}, },
token => { token => {
add => [ 'PVE::API2::User', 'generate_token', ['userid', 'tokenid'], {}, $print_api_result, $PVE::RESTHandler::standard_output_options ], add => [
modify => [ 'PVE::API2::User', 'update_token_info', ['userid', 'tokenid'], {}, $print_api_result, $PVE::RESTHandler::standard_output_options ], 'PVE::API2::User',
delete => [ 'PVE::API2::User', 'remove_token', ['userid', 'tokenid'], {}, $print_api_result, $PVE::RESTHandler::standard_output_options ], 'generate_token',
['userid', 'tokenid'],
{},
$print_api_result,
$PVE::RESTHandler::standard_output_options,
],
modify => [
'PVE::API2::User',
'update_token_info',
['userid', 'tokenid'],
{},
$print_api_result,
$PVE::RESTHandler::standard_output_options,
],
delete => [
'PVE::API2::User',
'remove_token',
['userid', 'tokenid'],
{},
$print_api_result,
$PVE::RESTHandler::standard_output_options,
],
remove => { alias => 'delete' }, remove => { alias => 'delete' },
list => [ 'PVE::API2::User', 'token_index', ['userid'], {}, $print_api_result, $PVE::RESTHandler::standard_output_options], list => [
permissions => [ __PACKAGE__, 'token_permissions', ['userid', 'tokenid'], {}, $print_perm_result, $PVE::RESTHandler::standard_output_options], 'PVE::API2::User',
} 'token_index',
['userid'],
{},
$print_api_result,
$PVE::RESTHandler::standard_output_options,
],
permissions => [
__PACKAGE__,
'token_permissions',
['userid', 'tokenid'],
{},
$print_perm_result,
$PVE::RESTHandler::standard_output_options,
],
},
}, },
group => { group => {
add => [ 'PVE::API2::Group', 'create_group', ['groupid'] ], add => ['PVE::API2::Group', 'create_group', ['groupid']],
modify => [ 'PVE::API2::Group', 'update_group', ['groupid'] ], modify => ['PVE::API2::Group', 'update_group', ['groupid']],
delete => [ 'PVE::API2::Group', 'delete_group', ['groupid'] ], delete => ['PVE::API2::Group', 'delete_group', ['groupid']],
list => [ 'PVE::API2::Group', 'index', [], {}, $print_api_result, $PVE::RESTHandler::standard_output_options], list => [
'PVE::API2::Group',
'index',
[],
{},
$print_api_result,
$PVE::RESTHandler::standard_output_options,
],
}, },
role => { role => {
add => [ 'PVE::API2::Role', 'create_role', ['roleid'] ], add => ['PVE::API2::Role', 'create_role', ['roleid']],
modify => [ 'PVE::API2::Role', 'update_role', ['roleid'] ], modify => ['PVE::API2::Role', 'update_role', ['roleid']],
delete => [ 'PVE::API2::Role', 'delete_role', ['roleid'] ], delete => ['PVE::API2::Role', 'delete_role', ['roleid']],
list => [ 'PVE::API2::Role', 'index', [], {}, $print_api_result, $PVE::RESTHandler::standard_output_options], list => [
'PVE::API2::Role',
'index',
[],
{},
$print_api_result,
$PVE::RESTHandler::standard_output_options,
],
}, },
acl => { acl => {
modify => [ 'PVE::API2::ACL', 'update_acl', ['path'], { delete => 0 }], modify => ['PVE::API2::ACL', 'update_acl', ['path'], { delete => 0 }],
delete => [ 'PVE::API2::ACL', 'update_acl', ['path'], { delete => 1 }], delete => ['PVE::API2::ACL', 'update_acl', ['path'], { delete => 1 }],
list => [ 'PVE::API2::ACL', 'read_acl', [], {}, $print_api_result, $PVE::RESTHandler::standard_output_options], list => [
'PVE::API2::ACL',
'read_acl',
[],
{},
$print_api_result,
$PVE::RESTHandler::standard_output_options,
],
}, },
realm => { realm => {
add => [ 'PVE::API2::Domains', 'create', ['realm'] ], add => ['PVE::API2::Domains', 'create', ['realm']],
modify => [ 'PVE::API2::Domains', 'update', ['realm'] ], modify => ['PVE::API2::Domains', 'update', ['realm']],
delete => [ 'PVE::API2::Domains', 'delete', ['realm'] ], delete => ['PVE::API2::Domains', 'delete', ['realm']],
list => [ 'PVE::API2::Domains', 'index', [], {}, $print_api_result, $PVE::RESTHandler::standard_output_options], list => [
sync => [ 'PVE::API2::Domains', 'sync', ['realm'], ], 'PVE::API2::Domains',
'index',
[],
{},
$print_api_result,
$PVE::RESTHandler::standard_output_options,
],
sync => ['PVE::API2::Domains', 'sync', ['realm']],
}, },
ticket => [ 'PVE::API2::AccessControl', 'create_ticket', ['username'], undef, sub { ticket => [
'PVE::API2::AccessControl',
'create_ticket',
['username'],
undef,
sub {
my ($res) = @_; my ($res) = @_;
print "$res->{ticket}\n"; print "$res->{ticket}\n";
}], },
],
passwd => [ 'PVE::API2::AccessControl', 'change_password', ['userid'] ], passwd => ['PVE::API2::AccessControl', 'change_password', ['userid']],
useradd => { alias => 'user add' }, useradd => { alias => 'user add' },
usermod => { alias => 'user modify' }, usermod => { alias => 'user modify' },
@ -272,10 +364,17 @@ eval {
if ($have_pool_api) { if ($have_pool_api) {
$cmddef->{pool} = { $cmddef->{pool} = {
add => [ 'PVE::API2::Pool', 'create_pool', ['poolid'] ], add => ['PVE::API2::Pool', 'create_pool', ['poolid']],
modify => [ 'PVE::API2::Pool', 'update_pool', ['poolid'] ], modify => ['PVE::API2::Pool', 'update_pool', ['poolid']],
delete => [ 'PVE::API2::Pool', 'delete_pool', ['poolid'] ], delete => ['PVE::API2::Pool', 'delete_pool', ['poolid']],
list => [ 'PVE::API2::Pool', 'index', [], {}, $print_api_result, $PVE::RESTHandler::standard_output_options], list => [
'PVE::API2::Pool',
'index',
[],
{},
$print_api_result,
$PVE::RESTHandler::standard_output_options,
],
}; };
} }

View File

@ -133,7 +133,10 @@ sub run {
my $nodename = PVE::INotify::nodename(); my $nodename = PVE::INotify::nodename();
# check statefile in pmxcfs if we should start # check statefile in pmxcfs if we should start
my $shouldrun = PVE::Cluster::cfs_lock_domain('realm-sync', undef, sub { my $shouldrun = PVE::Cluster::cfs_lock_domain(
'realm-sync',
undef,
sub {
my $members = PVE::Cluster::get_members(); my $members = PVE::Cluster::get_members();
my $state = get_state($id); my $state = get_state($id);
@ -141,13 +144,15 @@ sub run {
my $last_upid = $state->{upid}; my $last_upid = $state->{upid};
my $last_time = $state->{time}; my $last_time = $state->{time};
my $last_node_online = $last_node eq $nodename || ($members->{$last_node} // {})->{online}; my $last_node_online =
$last_node eq $nodename || ($members->{$last_node} // {})->{online};
if (defined($last_upid)) { if (defined($last_upid)) {
# first check if the next run is scheduled # first check if the next run is scheduled
if (my $parsed = PVE::Tools::upid_decode($last_upid, 1)) { if (my $parsed = PVE::Tools::upid_decode($last_upid, 1)) {
my $cal_spec = PVE::CalendarEvent::parse_calendar_event($schedule); my $cal_spec = PVE::CalendarEvent::parse_calendar_event($schedule);
my $next_sync = PVE::CalendarEvent::compute_next_event($cal_spec, $parsed->{starttime}); my $next_sync =
PVE::CalendarEvent::compute_next_event($cal_spec, $parsed->{starttime});
return 0 if !defined($next_sync) || $now < $next_sync; # not yet its (next) turn return 0 if !defined($next_sync) || $now < $next_sync; # not yet its (next) turn
} }
# check if still running and node is online # check if still running and node is online
@ -158,7 +163,7 @@ sub run {
last if !$last_node_online; # it's not finished and the node is offline last if !$last_node_online; # it's not finished and the node is offline
return 0; # not finished and online return 0; # not finished and online
} }
} elsif (defined($last_time) && ($last_time+60) > $now && $last_node_online) { } elsif (defined($last_time) && ($last_time + 60) > $now && $last_node_online) {
# another node started this job in the last 60 seconds and is still online # another node started this job in the last 60 seconds and is still online
return 0; return 0;
} }
@ -168,32 +173,46 @@ sub run {
# * it was started but either too long ago, or with an error # * it was started but either too long ago, or with an error
# * the started task finished # * the started task finished
save_state($id, { save_state(
$id,
{
node => $nodename, node => $nodename,
time => $now, time => $now,
}); },
);
return 1; return 1;
}); },
);
die $@ if $@; die $@ if $@;
if ($shouldrun) { if ($shouldrun) {
my $upid = eval { PVE::API2::Domains->sync($conf) }; my $upid = eval { PVE::API2::Domains->sync($conf) };
my $err = $@; my $err = $@;
PVE::Cluster::cfs_lock_domain('realm-sync', undef, sub { PVE::Cluster::cfs_lock_domain(
'realm-sync',
undef,
sub {
if ($err && !$upid) { if ($err && !$upid) {
save_state($id, { save_state(
$id,
{
node => $nodename, node => $nodename,
time => $now, time => $now,
error => $err, error => $err,
}); },
);
die "$err\n"; die "$err\n";
} }
save_state($id, { save_state(
$id,
{
node => $nodename, node => $nodename,
upid => $upid, upid => $upid,
}); },
}); );
},
);
die $@ if $@; die $@ if $@;
return $upid; return $upid;
} }

View File

@ -32,22 +32,23 @@ my $compile_acl_path = sub {
# permissions() will always prime the cache for the owning user # permissions() will always prime the cache for the owning user
my ($username, undef) = PVE::AccessControl::split_tokenid($user, 1); my ($username, undef) = PVE::AccessControl::split_tokenid($user, 1);
die "internal error" if $username && $username ne 'root@pam' && !defined($cache->{$username}); die "internal error"
if $username && $username ne 'root@pam' && !defined($cache->{$username});
# resolve and cache roles of the current user/token for all pool ACL paths # resolve and cache roles of the current user/token for all pool ACL paths
if (!$data->{poolroles}) { if (!$data->{poolroles}) {
$data->{poolroles} = {}; $data->{poolroles} = {};
foreach my $pool (keys %{$cfg->{pools}}) { foreach my $pool (keys %{ $cfg->{pools} }) {
my $d = $cfg->{pools}->{$pool}; my $d = $cfg->{pools}->{$pool};
my $pool_roles = PVE::AccessControl::roles($cfg, $user, "/pool/$pool"); # pool roles my $pool_roles = PVE::AccessControl::roles($cfg, $user, "/pool/$pool"); # pool roles
next if !scalar(keys %$pool_roles); next if !scalar(keys %$pool_roles);
foreach my $vmid (keys %{$d->{vms}}) { foreach my $vmid (keys %{ $d->{vms} }) {
for my $role (keys %$pool_roles) { for my $role (keys %$pool_roles) {
$data->{poolroles}->{"/vms/$vmid"}->{$role} = 1; $data->{poolroles}->{"/vms/$vmid"}->{$role} = 1;
} }
} }
foreach my $storeid (keys %{$d->{storage}}) { foreach my $storeid (keys %{ $d->{storage} }) {
for my $role (keys %$pool_roles) { for my $role (keys %$pool_roles) {
$data->{poolroles}->{"/storage/$storeid"}->{$role} = 1; $data->{poolroles}->{"/storage/$storeid"}->{$role} = 1;
} }
@ -70,7 +71,7 @@ my $compile_acl_path = sub {
# but pool ACL NoAccess trumps regular ACL # but pool ACL NoAccess trumps regular ACL
$roles = { 'NoAccess' => 0 }; $roles = { 'NoAccess' => 0 };
} else { } else {
foreach my $role (keys %{$data->{poolroles}->{$path}}) { foreach my $role (keys %{ $data->{poolroles}->{$path} }) {
# only use role from pool ACL if regular ACL didn't already # only use role from pool ACL if regular ACL didn't already
# set it, and never set propagation for pool-derived ACLs # set it, and never set propagation for pool-derived ACLs
$roles->{$role} = 0 if !defined($roles->{$role}); $roles->{$role} = 0 if !defined($roles->{$role});
@ -100,7 +101,7 @@ my $compile_acl_path = sub {
# map of set privs to their propagation flag value, for the owning user # map of set privs to their propagation flag value, for the owning user
my $user_privs = $cache->{$username}->{privs}->{$path}; my $user_privs = $cache->{$username}->{privs}->{$path};
# list of privs set both for token and owning user # list of privs set both for token and owning user
my $filtered_privs = [ grep { defined($user_privs->{$_}) } keys %$privs ]; my $filtered_privs = [grep { defined($user_privs->{$_}) } keys %$privs];
# intersection of privs using filtered list, combining both propagation # intersection of privs using filtered list, combining both propagation
# flags # flags
$privs = { map { $_ => $user_privs->{$_} && $privs->{$_} } @$filtered_privs }; $privs = { map { $_ => $user_privs->{$_} && $privs->{$_} } @$filtered_privs };
@ -137,7 +138,7 @@ sub permissions {
if ($user eq 'root@pam') { # root can do anything if ($user eq 'root@pam') { # root can do anything
my $cfg = $self->{user_cfg}; my $cfg = $self->{user_cfg};
return { map { $_ => 1 } keys %{$cfg->{roles}->{'Administrator'}} }; return { map { $_ => 1 } keys %{ $cfg->{roles}->{'Administrator'} } };
} }
if (!defined($path)) { if (!defined($path)) {
@ -193,10 +194,14 @@ sub compute_api_permission {
my $required_paths = ['/', '/nodes', '/access/groups', '/vms', '/storage', '/sdn', '/mapping']; my $required_paths = ['/', '/nodes', '/access/groups', '/vms', '/storage', '/sdn', '/mapping'];
my $defined_paths = []; my $defined_paths = [];
PVE::AccessControl::iterate_acl_tree("/", $usercfg->{acl_root}, sub { PVE::AccessControl::iterate_acl_tree(
"/",
$usercfg->{acl_root},
sub {
my ($path, $node) = @_; my ($path, $node) = @_;
push @$defined_paths, $path; push @$defined_paths, $path;
}); },
);
my $checked_paths = {}; my $checked_paths = {};
foreach my $path (@$required_paths, @$defined_paths) { foreach my $path (@$required_paths, @$defined_paths) {
@ -251,18 +256,22 @@ sub get_effective_permissions {
my $cfg = $self->{user_cfg}; my $cfg = $self->{user_cfg};
# paths explicitly listed in ACLs # paths explicitly listed in ACLs
PVE::AccessControl::iterate_acl_tree("/", $cfg->{acl_root}, sub { PVE::AccessControl::iterate_acl_tree(
"/",
$cfg->{acl_root},
sub {
my ($path, $node) = @_; my ($path, $node) = @_;
$paths->{$path} = 1; $paths->{$path} = 1;
}); },
);
# paths referenced by pool definitions # paths referenced by pool definitions
foreach my $pool (keys %{$cfg->{pools}}) { foreach my $pool (keys %{ $cfg->{pools} }) {
my $d = $cfg->{pools}->{$pool}; my $d = $cfg->{pools}->{$pool};
foreach my $vmid (keys %{$d->{vms}}) { foreach my $vmid (keys %{ $d->{vms} }) {
$paths->{"/vms/$vmid"} = 1; $paths->{"/vms/$vmid"} = 1;
} }
foreach my $storeid (keys %{$d->{storage}}) { foreach my $storeid (keys %{ $d->{storage} }) {
$paths->{"/storage/$storeid"} = 1; $paths->{"/storage/$storeid"} = 1;
} }
} }
@ -290,10 +299,10 @@ sub check {
return undef if $noerr; return undef if $noerr;
raise_perm_exc("$path, $priv"); raise_perm_exc("$path, $priv");
} }
}; }
return 1; return 1;
}; }
sub check_any { sub check_any {
my ($self, $user, $path, $privs, $noerr) = @_; my ($self, $user, $path, $privs, $noerr) = @_;
@ -307,14 +316,14 @@ sub check_any {
$found = 1; $found = 1;
last; last;
} }
}; }
return 1 if $found; return 1 if $found;
return undef if $noerr; return undef if $noerr;
raise_perm_exc("$path, " . join("|", @$privs)); raise_perm_exc("$path, " . join("|", @$privs));
}; }
sub check_full { sub check_full {
my ($self, $username, $path, $privs, $any, $noerr) = @_; my ($self, $username, $path, $privs, $any, $noerr) = @_;
@ -385,7 +394,7 @@ sub check_vm_perm {
return if $self->check_full($user, "/pool/$pool", $privs, $any, 1); return if $self->check_full($user, "/pool/$pool", $privs, $any, 1);
} }
return $self->check_full($user, "/vms/$vmid", $privs, $any, $noerr); return $self->check_full($user, "/vms/$vmid", $privs, $any, $noerr);
}; }
sub is_group_member { sub is_group_member {
my ($self, $group, $user) = @_; my ($self, $group, $user) = @_;
@ -403,7 +412,7 @@ sub filter_groups {
my $cfg = $self->{user_cfg}; my $cfg = $self->{user_cfg};
my $groups = {}; my $groups = {};
foreach my $group (keys %{$cfg->{groups}}) { foreach my $group (keys %{ $cfg->{groups} }) {
my $path = "/access/groups/$group"; my $path = "/access/groups/$group";
if ($self->check_full($user, $path, $privs, $any, 1)) { if ($self->check_full($user, $path, $privs, $any, 1)) {
$groups->{$group} = $cfg->{groups}->{$group}; $groups->{$group} = $cfg->{groups}->{$group};
@ -422,7 +431,7 @@ sub group_member_join {
foreach my $group (@$grouplist) { foreach my $group (@$grouplist) {
my $data = $cfg->{groups}->{$group}; my $data = $cfg->{groups}->{$group};
next if !$data; next if !$data;
foreach my $user (keys %{$data->{users}}) { foreach my $user (keys %{ $data->{users} }) {
$users->{$user} = 1; $users->{$user} = 1;
} }
} }
@ -433,9 +442,9 @@ sub group_member_join {
sub check_perm_modify { sub check_perm_modify {
my ($self, $username, $path, $noerr) = @_; my ($self, $username, $path, $noerr) = @_;
return $self->check($username, '/access', [ 'Permissions.Modify' ], $noerr) if !$path; return $self->check($username, '/access', ['Permissions.Modify'], $noerr) if !$path;
my $testperms = [ 'Permissions.Modify' ]; my $testperms = ['Permissions.Modify'];
if ($path =~ m|^/storage/.+$|) { if ($path =~ m|^/storage/.+$|) {
push @$testperms, 'Datastore.Allocate'; push @$testperms, 'Datastore.Allocate';
} elsif ($path =~ m|^/vms/.+$|) { } elsif ($path =~ m|^/vms/.+$|) {
@ -535,7 +544,7 @@ sub exec_api2_perm_check {
} else { } else {
die "unknown permission test"; die "unknown permission test";
} }
}; }
sub check_api2_permissions { sub check_api2_permissions {
my ($self, $perm, $username, $param) = @_; my ($self, $perm, $username, $param) = @_;
@ -581,8 +590,7 @@ sub init {
$self->{aclversion} = undef; $self->{aclversion} = undef;
return $self; return $self;
}; }
# init_request - must be called before each RPC request # init_request - must be called before each RPC request
sub init_request { sub init_request {
@ -609,8 +617,12 @@ sub init_request {
$self->{user_cfg} = $cfg; $self->{user_cfg} = $cfg;
} else { } else {
my $ucvers = PVE::Cluster::cfs_file_version('user.cfg'); my $ucvers = PVE::Cluster::cfs_file_version('user.cfg');
if (!$self->{aclcache} || !defined($self->{aclversion}) || if (
!defined($ucvers) || ($ucvers ne $self->{aclversion})) { !$self->{aclcache}
|| !defined($self->{aclversion})
|| !defined($ucvers)
|| ($ucvers ne $self->{aclversion})
) {
$self->{aclversion} = $ucvers; $self->{aclversion} = $ucvers;
my $cfg = PVE::Cluster::cfs_read_file('user.cfg'); my $cfg = PVE::Cluster::cfs_read_file('user.cfg');
$self->{user_cfg} = $cfg; $self->{user_cfg} = $cfg;

View File

@ -49,7 +49,10 @@ sub generate_token {
PVE::AccessControl::pve_verify_tokenid($tokenid); PVE::AccessControl::pve_verify_tokenid($tokenid);
my $token_value = PVE::Cluster::cfs_lock_file('priv/token.cfg', 10, sub { my $token_value = PVE::Cluster::cfs_lock_file(
'priv/token.cfg',
10,
sub {
my $uuid = UUID::uuid(); my $uuid = UUID::uuid();
my $token_cfg = PVE::Cluster::cfs_read_file('priv/token.cfg'); my $token_cfg = PVE::Cluster::cfs_read_file('priv/token.cfg');
@ -58,7 +61,8 @@ sub generate_token {
PVE::Cluster::cfs_write_file('priv/token.cfg', $token_cfg); PVE::Cluster::cfs_write_file('priv/token.cfg', $token_cfg);
return $uuid; return $uuid;
}); },
);
die "$@\n" if defined($@); die "$@\n" if defined($@);
@ -68,13 +72,17 @@ sub generate_token {
sub delete_token { sub delete_token {
my ($tokenid) = @_; my ($tokenid) = @_;
PVE::Cluster::cfs_lock_file('priv/token.cfg', 10, sub { PVE::Cluster::cfs_lock_file(
'priv/token.cfg',
10,
sub {
my $token_cfg = PVE::Cluster::cfs_read_file('priv/token.cfg'); my $token_cfg = PVE::Cluster::cfs_read_file('priv/token.cfg');
delete $token_cfg->{$tokenid}; delete $token_cfg->{$tokenid};
PVE::Cluster::cfs_write_file('priv/token.cfg', $token_cfg); PVE::Cluster::cfs_write_file('priv/token.cfg', $token_cfg);
}); },
);
die "$@\n" if defined($@); die "$@\n" if defined($@);
} }

View File

@ -79,13 +79,15 @@ my $stranger_user_tests = [
my $stranger_nonprivsep_tests = [ my $stranger_nonprivsep_tests = [
{ {
description => 'get stranger-owned non-priv-sep\'d token\'s perms without passing the token', description =>
'get stranger-owned non-priv-sep\'d token\'s perms without passing the token',
rpcuser => 'stranger@pve!noprivsep', rpcuser => 'stranger@pve!noprivsep',
params => {}, params => {},
result => $stranger_perms, result => $stranger_perms,
}, },
{ {
description => 'get stranger-owned non-priv-sep\'d token\'s perms with passing the token', description =>
'get stranger-owned non-priv-sep\'d token\'s perms with passing the token',
rpcuser => 'stranger@pve!noprivsep', rpcuser => 'stranger@pve!noprivsep',
params => { params => {
userid => 'stranger@pve!noprivsep', userid => 'stranger@pve!noprivsep',
@ -110,18 +112,20 @@ my $stranger_nonprivsep_tests = [
}, },
}, },
{ {
description => 'get auditor-owned token\'s perms from stranger-owned non-priv-sep\'d token', description =>
'get auditor-owned token\'s perms from stranger-owned non-priv-sep\'d token',
should_fail => 1, should_fail => 1,
rpcuser => 'stranger@pve!noprivsep', rpcuser => 'stranger@pve!noprivsep',
params => { params => {
userid => 'auditor@pam!noprivsep', userid => 'auditor@pam!noprivsep',
} },
}, },
]; ];
my $stranger_privsep_tests = [ my $stranger_privsep_tests = [
{ {
description => 'get stranger-owned priv-sep\'d token\'s perms without passing the token', description =>
'get stranger-owned priv-sep\'d token\'s perms without passing the token',
rpcuser => 'stranger@pve!privsep', rpcuser => 'stranger@pve!privsep',
params => {}, params => {},
result => $stranger_privsep_perms, result => $stranger_privsep_perms,
@ -157,7 +161,7 @@ my $stranger_privsep_tests = [
rpcuser => 'stranger@pve!privsep', rpcuser => 'stranger@pve!privsep',
params => { params => {
userid => 'auditor@pam!noprivsep', userid => 'auditor@pam!noprivsep',
} },
}, },
]; ];
@ -216,13 +220,15 @@ my $auditor_user_tests = [
my $auditor_nonprivsep_tests = [ my $auditor_nonprivsep_tests = [
{ {
description => 'get auditor-owned non-priv-sep\'d token\'s perms without passing the token', description =>
'get auditor-owned non-priv-sep\'d token\'s perms without passing the token',
rpcuser => 'auditor@pam!noprivsep', rpcuser => 'auditor@pam!noprivsep',
params => {}, params => {},
result => $auditor_perms, result => $auditor_perms,
}, },
{ {
description => 'get auditor-owned non-priv-sep\'d token\'s perms with passing the token', description =>
'get auditor-owned non-priv-sep\'d token\'s perms with passing the token',
rpcuser => 'auditor@pam!noprivsep', rpcuser => 'auditor@pam!noprivsep',
params => { params => {
userid => 'auditor@pam!noprivsep', userid => 'auditor@pam!noprivsep',
@ -247,7 +253,8 @@ my $auditor_nonprivsep_tests = [
result => $auditor_privsep_perms, result => $auditor_privsep_perms,
}, },
{ {
description => 'get stranger-owned token\'s perms from auditor-owned non-priv-sep\'d token', description =>
'get stranger-owned token\'s perms from auditor-owned non-priv-sep\'d token',
rpcuser => 'auditor@pam!noprivsep', rpcuser => 'auditor@pam!noprivsep',
params => { params => {
userid => 'stranger@pve!noprivsep', userid => 'stranger@pve!noprivsep',

View File

@ -11,8 +11,8 @@ my $username = shift;
die "Username missing" if !$username; die "Username missing" if !$username;
my $password = PVE::PTY::read_password('password: '); my $password = PVE::PTY::read_password('password: ');
PVE::AccessControl::authenticate_user($username,$password); PVE::AccessControl::authenticate_user($username, $password);
print "Authentication Successful!!\n"; print "Authentication Successful!!\n";
exit (0); exit(0);

View File

@ -12,8 +12,8 @@ use PVE::RPCEnvironment;
# dump-perm.pl -f myuser.cfg root / # dump-perm.pl -f myuser.cfg root /
my $opt_file; my $opt_file;
if (!GetOptions ("file=s" => \$opt_file)) { if (!GetOptions("file=s" => \$opt_file)) {
exit (-1); exit(-1);
} }
my $username = shift; my $username = shift;
@ -21,7 +21,7 @@ my $path = shift;
if (!($username && $path)) { if (!($username && $path)) {
print "usage: $0 <username> <path>\n"; print "usage: $0 <username> <path>\n";
exit (-1); exit(-1);
} }
my $cfg; my $cfg;
@ -38,4 +38,4 @@ my $perm = $rpcenv->permissions($username, $path);
print "permission for user '$username' on '$path':\n"; print "permission for user '$username' on '$path':\n";
print join(',', keys %$perm) . "\n"; print join(',', keys %$perm) . "\n";
exit (0); exit(0);

View File

@ -13,4 +13,4 @@ $cfg = PVE::AccessControl::load_user_config();
print Dumper($cfg) . "\n"; print Dumper($cfg) . "\n";
exit (0); exit(0);

View File

@ -43,7 +43,7 @@ sub default_roles_with {
sub default_users { sub default_users {
my $users = dclone($default_user_cfg->{users}); my $users = dclone($default_user_cfg->{users});
return { map { $_ => $add_default_user_properties->($users->{$_}); } keys %$users}; return { map { $_ => $add_default_user_properties->($users->{$_}); } keys %$users };
} }
sub default_users_with { sub default_users_with {
@ -101,7 +101,7 @@ sub default_pool_vms_with {
my $vms = {}; my $vms = {};
foreach my $pool (@$extra_pools) { foreach my $pool (@$extra_pools) {
foreach my $vmid (keys %{$pool->{vms}}) { foreach my $vmid (keys %{ $pool->{vms} }) {
$vms->{$vmid} = $pool->{id}; $vms->{$vmid} = $pool->{id};
} }
} }
@ -121,7 +121,7 @@ sub default_acls_with {
foreach my $a (@$extra_acls) { foreach my $a (@$extra_acls) {
my $acl = dclone($a); my $acl = dclone($a);
my $path = delete $acl->{path}; my $path = delete $acl->{path};
my $split_path = [ split("/", $path) ]; my $split_path = [split("/", $path)];
my $node = $acls; my $node = $acls;
for my $p (@$split_path) { for my $p (@$split_path) {
next if !$p; next if !$p;
@ -129,7 +129,7 @@ sub default_acls_with {
$node->{children}->{$p} = {} if !$node->{children}->{$p}; $node->{children}->{$p} = {} if !$node->{children}->{$p};
$node = $node->{children}->{$p}; $node = $node->{children}->{$p};
} }
%$node = ( %$acl ); %$node = (%$acl);
} }
return $acls; return $acls;
@ -241,8 +241,8 @@ my $default_cfg = {
}, },
test_pool_members => { test_pool_members => {
'id' => 'testpool', 'id' => 'testpool',
vms => { 123 => 1, 1234 => 1}, vms => { 123 => 1, 1234 => 1 },
storage => { 'local' => 1, 'local-zfs' => 1}, storage => { 'local' => 1, 'local-zfs' => 1 },
pools => {}, pools => {},
}, },
test_pool_duplicate_vms => { test_pool_duplicate_vms => {
@ -254,7 +254,7 @@ my $default_cfg = {
test_pool_duplicate_storages => { test_pool_duplicate_storages => {
'id' => 'test_duplicate_storages', 'id' => 'test_duplicate_storages',
vms => {}, vms => {},
storage => { 'local' => 1, 'local-zfs' => 1}, storage => { 'local' => 1, 'local-zfs' => 1 },
pools => {}, pools => {},
}, },
acl_simple_user => { acl_simple_user => {
@ -443,7 +443,8 @@ my $default_raw = {
'acl_complex_users_1' => 'acl:1:/storage:test@pam:PVEDatastoreAdmin:', 'acl_complex_users_1' => 'acl:1:/storage:test@pam:PVEDatastoreAdmin:',
'acl_complex_users_2' => 'acl:1:/storage:test2@pam:PVEDatastoreUser:', 'acl_complex_users_2' => 'acl:1:/storage:test2@pam:PVEDatastoreUser:',
'acl_simple_token' => 'acl:1:/:test@pam!full:PVEVMAdmin:', 'acl_simple_token' => 'acl:1:/:test@pam!full:PVEVMAdmin:',
'acl_complex_tokens_1' => 'acl:1:/storage:test2@pam!expired,test@pam!full:PVEDatastoreAdmin:', 'acl_complex_tokens_1' =>
'acl:1:/storage:test2@pam!expired,test@pam!full:PVEDatastoreAdmin:',
'acl_complex_tokens_2' => 'acl:1:/storage:test2@pam!privsep:PVEDatastoreUser:', 'acl_complex_tokens_2' => 'acl:1:/storage:test2@pam!privsep:PVEDatastoreUser:',
'acl_complex_tokens_1_missing' => 'acl:1:/storage:test2@pam!expired:PVEDatastoreAdmin:', 'acl_complex_tokens_1_missing' => 'acl:1:/storage:test2@pam!expired:PVEDatastoreAdmin:',
'acl_simple_group' => 'acl:1:/:@testgroup:PVEVMAdmin:', 'acl_simple_group' => 'acl:1:/:@testgroup:PVEVMAdmin:',
@ -478,7 +479,7 @@ my $tests = [
users => default_users(), users => default_users(),
roles => default_roles(), roles => default_roles(),
}, },
raw => $default_raw->{users}->{'root@pam'}."\n\n\n\n\n", raw => $default_raw->{users}->{'root@pam'} . "\n\n\n\n\n",
}, },
{ {
name => "group_empty", name => "group_empty",
@ -488,10 +489,9 @@ my $tests = [
roles => default_roles(), roles => default_roles(),
groups => default_groups_with([$default_cfg->{'test_group_empty'}]), groups => default_groups_with([$default_cfg->{'test_group_empty'}]),
}, },
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n\n". . $default_raw->{users}->{'root@pam'} . "\n\n"
$default_raw->{groups}->{'test_group_empty'}."\n\n". . $default_raw->{groups}->{'test_group_empty'} . "\n\n" . "\n\n",
"\n\n",
}, },
{ {
name => "group_inexisting_member", name => "group_inexisting_member",
@ -501,14 +501,13 @@ my $tests = [
roles => default_roles(), roles => default_roles(),
groups => default_groups_with([$default_cfg->{'test_group_empty'}]), groups => default_groups_with([$default_cfg->{'test_group_empty'}]),
}, },
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n\n". . $default_raw->{users}->{'root@pam'} . "\n\n"
"group:testgroup:does_not_exist::". . "group:testgroup:does_not_exist::"
"\n\n\n\n", . "\n\n\n\n",
expected_raw => "". expected_raw => ""
$default_raw->{users}->{'root@pam'}."\n\n". . $default_raw->{users}->{'root@pam'} . "\n\n"
$default_raw->{groups}->{'test_group_empty'}."\n\n". . $default_raw->{groups}->{'test_group_empty'} . "\n\n" . "\n\n",
"\n\n",
}, },
{ {
name => "group_invalid_member", name => "group_invalid_member",
@ -517,10 +516,9 @@ my $tests = [
users => default_users(), users => default_users(),
roles => default_roles(), roles => default_roles(),
}, },
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n\n". . $default_raw->{users}->{'root@pam'} . "\n\n"
'group:inval!d:root@pam:'. . 'group:inval!d:root@pam:' . "\n\n",
"\n\n",
}, },
{ {
name => "group_with_one_member", name => "group_with_one_member",
@ -530,26 +528,26 @@ my $tests = [
roles => default_roles(), roles => default_roles(),
groups => default_groups_with([$default_cfg->{'test_group_single_member'}]), groups => default_groups_with([$default_cfg->{'test_group_single_member'}]),
}, },
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n". . $default_raw->{users}->{'root@pam'} . "\n"
$default_raw->{users}->{'test_pam'}."\n\n". . $default_raw->{users}->{'test_pam'} . "\n\n"
$default_raw->{groups}->{'test_group_single_member'}."\n\n". . $default_raw->{groups}->{'test_group_single_member'} . "\n\n" . "\n\n",
"\n\n",
}, },
{ {
name => "group_with_members", name => "group_with_members",
config => { config => {
acl_root => default_acls(), acl_root => default_acls(),
users => default_users_with([$default_cfg->{test_pam_with_group}, $default_cfg->{test2_pam_with_group}]), users => default_users_with(
[$default_cfg->{test_pam_with_group}, $default_cfg->{test2_pam_with_group}]
),
roles => default_roles(), roles => default_roles(),
groups => default_groups_with([$default_cfg->{'test_group_members'}]), groups => default_groups_with([$default_cfg->{'test_group_members'}]),
}, },
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n". . $default_raw->{users}->{'root@pam'} . "\n"
$default_raw->{users}->{'test2_pam'}."\n". . $default_raw->{users}->{'test2_pam'} . "\n"
$default_raw->{users}->{'test_pam'}."\n\n". . $default_raw->{users}->{'test_pam'} . "\n\n"
$default_raw->{groups}->{'test_group_members'}."\n\n". . $default_raw->{groups}->{'test_group_members'} . "\n\n" . "\n\n",
"\n\n",
}, },
{ {
name => "token_simple", name => "token_simple",
@ -558,27 +556,30 @@ my $tests = [
users => default_users_with([$default_cfg->{test_pam_with_token}]), users => default_users_with([$default_cfg->{test_pam_with_token}]),
roles => default_roles(), roles => default_roles(),
}, },
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n". . $default_raw->{users}->{'root@pam'} . "\n"
$default_raw->{users}->{'test_pam'}."\n". . $default_raw->{users}->{'test_pam'} . "\n"
$default_raw->{tokens}->{'test_token_simple'}."\n\n\n\n\n", . $default_raw->{tokens}->{'test_token_simple'}
. "\n\n\n\n\n",
}, },
{ {
name => "token_multi", name => "token_multi",
config => { config => {
acl_root => default_acls(), acl_root => default_acls(),
users => default_users_with([$default_cfg->{test_pam_with_token}, $default_cfg->{test_pam2_with_token}]), users => default_users_with(
[$default_cfg->{test_pam_with_token}, $default_cfg->{test_pam2_with_token}]
),
roles => default_roles(), roles => default_roles(),
}, },
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n". . $default_raw->{users}->{'root@pam'} . "\n"
$default_raw->{users}->{'test2_pam'}."\n". . $default_raw->{users}->{'test2_pam'} . "\n"
$default_raw->{tokens}->{'test_token_multi_expired'}."\n". . $default_raw->{tokens}->{'test_token_multi_expired'} . "\n"
$default_raw->{tokens}->{'test_token_multi_full'}."\n". . $default_raw->{tokens}->{'test_token_multi_full'} . "\n"
$default_raw->{tokens}->{'test_token_multi_privsep'}."\n". . $default_raw->{tokens}->{'test_token_multi_privsep'} . "\n"
$default_raw->{users}->{'test_pam'}."\n". . $default_raw->{users}->{'test_pam'} . "\n"
$default_raw->{tokens}->{'test_token_simple'}."\n". . $default_raw->{tokens}->{'test_token_simple'} . "\n"
"\n\n\n\n", . "\n\n\n\n",
}, },
{ {
name => "custom_role_with_single_priv", name => "custom_role_with_single_priv",
@ -587,9 +588,10 @@ my $tests = [
users => default_users(), users => default_users(),
roles => default_roles_with([$default_cfg->{test_role_single_priv}]), roles => default_roles_with([$default_cfg->{test_role_single_priv}]),
}, },
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n\n\n\n". . $default_raw->{users}->{'root@pam'}
$default_raw->{roles}->{'test_role_single_priv'}."\n\n", . "\n\n\n\n"
. $default_raw->{roles}->{'test_role_single_priv'} . "\n\n",
}, },
{ {
name => "custom_role_with_privs", name => "custom_role_with_privs",
@ -598,9 +600,10 @@ my $tests = [
users => default_users(), users => default_users(),
roles => default_roles_with([$default_cfg->{test_role_privs}]), roles => default_roles_with([$default_cfg->{test_role_privs}]),
}, },
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n\n\n\n". . $default_raw->{users}->{'root@pam'}
$default_raw->{roles}->{'test_role_privs'}."\n\n", . "\n\n\n\n"
. $default_raw->{roles}->{'test_role_privs'} . "\n\n",
}, },
{ {
name => "custom_role_with_duplicate_privs", name => "custom_role_with_duplicate_privs",
@ -609,12 +612,14 @@ my $tests = [
users => default_users(), users => default_users(),
roles => default_roles_with([$default_cfg->{test_role_privs}]), roles => default_roles_with([$default_cfg->{test_role_privs}]),
}, },
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n\n\n\n". . $default_raw->{users}->{'root@pam'}
$default_raw->{roles}->{'test_role_privs_duplicate'}."\n\n", . "\n\n\n\n"
expected_raw => "". . $default_raw->{roles}->{'test_role_privs_duplicate'} . "\n\n",
$default_raw->{users}->{'root@pam'}."\n\n\n\n". expected_raw => ""
$default_raw->{roles}->{'test_role_privs'}."\n\n", . $default_raw->{users}->{'root@pam'}
. "\n\n\n\n"
. $default_raw->{roles}->{'test_role_privs'} . "\n\n",
}, },
{ {
name => "custom_role_with_invalid_priv", name => "custom_role_with_invalid_priv",
@ -623,12 +628,14 @@ my $tests = [
users => default_users(), users => default_users(),
roles => default_roles_with([$default_cfg->{test_role_privs}]), roles => default_roles_with([$default_cfg->{test_role_privs}]),
}, },
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n\n\n\n". . $default_raw->{users}->{'root@pam'}
$default_raw->{roles}->{'test_role_privs_invalid'}."\n\n", . "\n\n\n\n"
expected_raw => "". . $default_raw->{roles}->{'test_role_privs_invalid'} . "\n\n",
$default_raw->{users}->{'root@pam'}."\n\n\n\n". expected_raw => ""
$default_raw->{roles}->{'test_role_privs'}."\n\n", . $default_raw->{users}->{'root@pam'}
. "\n\n\n\n"
. $default_raw->{roles}->{'test_role_privs'} . "\n\n",
}, },
{ {
name => "pool_empty", name => "pool_empty",
@ -638,9 +645,11 @@ my $tests = [
roles => default_roles(), roles => default_roles(),
pools => default_pools_with([$default_cfg->{test_pool_empty}]), pools => default_pools_with([$default_cfg->{test_pool_empty}]),
}, },
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n\n\n". . $default_raw->{users}->{'root@pam'}
$default_raw->{pools}->{'test_pool_empty'}."\n\n\n", . "\n\n\n"
. $default_raw->{pools}->{'test_pool_empty'}
. "\n\n\n",
}, },
{ {
name => "pool_invalid", name => "pool_invalid",
@ -650,12 +659,16 @@ my $tests = [
roles => default_roles(), roles => default_roles(),
pools => default_pools_with([$default_cfg->{test_pool_empty}]), pools => default_pools_with([$default_cfg->{test_pool_empty}]),
}, },
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n\n\n". . $default_raw->{users}->{'root@pam'}
$default_raw->{pools}->{'test_pool_invalid'}."\n\n\n", . "\n\n\n"
expected_raw => "". . $default_raw->{pools}->{'test_pool_invalid'}
$default_raw->{users}->{'root@pam'}."\n\n\n". . "\n\n\n",
$default_raw->{pools}->{'test_pool_empty'}."\n\n\n", expected_raw => ""
. $default_raw->{users}->{'root@pam'}
. "\n\n\n"
. $default_raw->{pools}->{'test_pool_empty'}
. "\n\n\n",
}, },
{ {
name => "pool_members", name => "pool_members",
@ -666,9 +679,11 @@ my $tests = [
pools => default_pools_with([$default_cfg->{test_pool_members}]), pools => default_pools_with([$default_cfg->{test_pool_members}]),
vms => default_pool_vms_with([$default_cfg->{test_pool_members}]), vms => default_pool_vms_with([$default_cfg->{test_pool_members}]),
}, },
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n\n\n". . $default_raw->{users}->{'root@pam'}
$default_raw->{pools}->{'test_pool_members'}."\n\n\n", . "\n\n\n"
. $default_raw->{pools}->{'test_pool_members'}
. "\n\n\n",
}, },
{ {
name => "pool_duplicate_members", name => "pool_duplicate_members",
@ -676,19 +691,28 @@ my $tests = [
acl_root => default_acls(), acl_root => default_acls(),
users => default_users(), users => default_users(),
roles => default_roles(), roles => default_roles(),
pools => default_pools_with([$default_cfg->{test_pool_members}, $default_cfg->{test_pool_duplicate_vms}, $default_cfg->{test_pool_duplicate_storages}]), pools => default_pools_with(
[
$default_cfg->{test_pool_members},
$default_cfg->{test_pool_duplicate_vms},
$default_cfg->{test_pool_duplicate_storages},
],
),
vms => default_pool_vms_with([$default_cfg->{test_pool_members}]), vms => default_pool_vms_with([$default_cfg->{test_pool_members}]),
}, },
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n\n\n". . $default_raw->{users}->{'root@pam'}
$default_raw->{pools}->{'test_pool_members'}."\n". . "\n\n\n"
$default_raw->{pools}->{'test_pool_duplicate_vms'}."\n". . $default_raw->{pools}->{'test_pool_members'} . "\n"
$default_raw->{pools}->{'test_pool_duplicate_storages'}."\n", . $default_raw->{pools}->{'test_pool_duplicate_vms'} . "\n"
expected_raw => "". . $default_raw->{pools}->{'test_pool_duplicate_storages'} . "\n",
$default_raw->{users}->{'root@pam'}."\n\n\n". expected_raw => ""
$default_raw->{pools}->{'test_pool_duplicate_storages'}."\n". . $default_raw->{users}->{'root@pam'}
$default_raw->{pools}->{'test_pool_duplicate_vms_expected'}."\n". . "\n\n\n"
$default_raw->{pools}->{'test_pool_members'}."\n\n\n", . $default_raw->{pools}->{'test_pool_duplicate_storages'} . "\n"
. $default_raw->{pools}->{'test_pool_duplicate_vms_expected'} . "\n"
. $default_raw->{pools}->{'test_pool_members'}
. "\n\n\n",
}, },
{ {
name => "acl_simple_user", name => "acl_simple_user",
@ -697,39 +721,47 @@ my $tests = [
roles => default_roles(), roles => default_roles(),
acl_root => default_acls_with([$default_cfg->{acl_simple_user}]), acl_root => default_acls_with([$default_cfg->{acl_simple_user}]),
}, },
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n". . $default_raw->{users}->{'root@pam'} . "\n"
$default_raw->{users}->{'test_pam'}."\n\n\n\n\n". . $default_raw->{users}->{'test_pam'}
$default_raw->{acl}->{'acl_simple_user'}."\n", . "\n\n\n\n\n"
. $default_raw->{acl}->{'acl_simple_user'} . "\n",
}, },
{ {
name => "acl_complex_users", name => "acl_complex_users",
config => { config => {
users => default_users_with([$default_cfg->{test_pam}, $default_cfg->{'test2_pam'}]), users =>
default_users_with([$default_cfg->{test_pam}, $default_cfg->{'test2_pam'}]),
roles => default_roles(), roles => default_roles(),
acl_root => default_acls_with([$default_cfg->{acl_simple_user}, $default_cfg->{acl_complex_users}]), acl_root => default_acls_with(
[$default_cfg->{acl_simple_user}, $default_cfg->{acl_complex_users}]
),
}, },
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n". . $default_raw->{users}->{'root@pam'} . "\n"
$default_raw->{users}->{'test2_pam'}."\n". . $default_raw->{users}->{'test2_pam'} . "\n"
$default_raw->{users}->{'test_pam'}."\n\n\n\n\n". . $default_raw->{users}->{'test_pam'}
$default_raw->{acl}->{'acl_simple_user'}."\n". . "\n\n\n\n\n"
$default_raw->{acl}->{'acl_complex_users_1'}."\n". . $default_raw->{acl}->{'acl_simple_user'} . "\n"
$default_raw->{acl}->{'acl_complex_users_2'}."\n", . $default_raw->{acl}->{'acl_complex_users_1'} . "\n"
. $default_raw->{acl}->{'acl_complex_users_2'} . "\n",
}, },
{ {
name => "acl_complex_missing_user", name => "acl_complex_missing_user",
config => { config => {
users => default_users_with([$default_cfg->{test2_pam}]), users => default_users_with([$default_cfg->{test2_pam}]),
roles => default_roles(), roles => default_roles(),
acl_root => default_acls_with([$default_cfg->{acl_simple_user}, $default_cfg->{acl_complex_missing_user}]), acl_root => default_acls_with(
[$default_cfg->{acl_simple_user}, $default_cfg->{acl_complex_missing_user}]
),
}, },
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n". . $default_raw->{users}->{'root@pam'} . "\n"
$default_raw->{users}->{'test2_pam'}."\n\n\n\n\n". . $default_raw->{users}->{'test2_pam'}
$default_raw->{acl}->{'acl_simple_user'}."\n". . "\n\n\n\n\n"
$default_raw->{acl}->{'acl_complex_users_1'}."\n". . $default_raw->{acl}->{'acl_simple_user'} . "\n"
$default_raw->{acl}->{'acl_complex_users_2'}."\n", . $default_raw->{acl}->{'acl_complex_users_1'} . "\n"
. $default_raw->{acl}->{'acl_complex_users_2'} . "\n",
}, },
{ {
name => "acl_simple_group", name => "acl_simple_group",
@ -739,57 +771,78 @@ my $tests = [
roles => default_roles(), roles => default_roles(),
acl_root => default_acls_with([$default_cfg->{acl_simple_group}]), acl_root => default_acls_with([$default_cfg->{acl_simple_group}]),
}, },
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n". . $default_raw->{users}->{'root@pam'} . "\n"
$default_raw->{users}->{'test_pam'}."\n\n". . $default_raw->{users}->{'test_pam'} . "\n\n"
$default_raw->{groups}->{'test_group_single_member'}."\n\n\n\n". . $default_raw->{groups}->{'test_group_single_member'}
$default_raw->{acl}->{'acl_simple_group'}."\n", . "\n\n\n\n"
. $default_raw->{acl}->{'acl_simple_group'} . "\n",
}, },
{ {
name => "acl_complex_groups", name => "acl_complex_groups",
config => { config => {
users => default_users_with([$default_cfg->{test_pam_with_group}, $default_cfg->{'test2_pam_with_group'}, $default_cfg->{'test3_pam'}]), users => default_users_with(
groups => default_groups_with([$default_cfg->{'test_group_members'}, $default_cfg->{'test_group_second'}]), [
$default_cfg->{test_pam_with_group},
$default_cfg->{'test2_pam_with_group'},
$default_cfg->{'test3_pam'},
],
),
groups => default_groups_with(
[$default_cfg->{'test_group_members'}, $default_cfg->{'test_group_second'}]
),
roles => default_roles(), roles => default_roles(),
acl_root => default_acls_with([$default_cfg->{acl_simple_group}, $default_cfg->{acl_complex_groups}]), acl_root => default_acls_with(
[$default_cfg->{acl_simple_group}, $default_cfg->{acl_complex_groups}]
),
}, },
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n". . $default_raw->{users}->{'root@pam'} . "\n"
$default_raw->{users}->{'test2_pam'}."\n". . $default_raw->{users}->{'test2_pam'} . "\n"
$default_raw->{users}->{'test3_pam'}."\n". . $default_raw->{users}->{'test3_pam'} . "\n"
$default_raw->{users}->{'test_pam'}."\n\n". . $default_raw->{users}->{'test_pam'} . "\n\n"
$default_raw->{groups}->{'test_group_second'}."\n". . $default_raw->{groups}->{'test_group_second'} . "\n"
$default_raw->{groups}->{'test_group_members'}."\n\n\n\n". . $default_raw->{groups}->{'test_group_members'}
$default_raw->{acl}->{'acl_simple_group'}."\n". . "\n\n\n\n"
$default_raw->{acl}->{'acl_complex_groups_1'}."\n". . $default_raw->{acl}->{'acl_simple_group'} . "\n"
$default_raw->{acl}->{'acl_complex_groups_2'}."\n", . $default_raw->{acl}->{'acl_complex_groups_1'} . "\n"
. $default_raw->{acl}->{'acl_complex_groups_2'} . "\n",
}, },
{ {
name => "acl_complex_missing_group", name => "acl_complex_missing_group",
config => { config => {
users => default_users_with([$default_cfg->{test_pam}, $default_cfg->{'test2_pam'}, $default_cfg->{'test3_pam'}]), users => default_users_with(
[
$default_cfg->{test_pam},
$default_cfg->{'test2_pam'},
$default_cfg->{'test3_pam'},
],
),
groups => default_groups_with([$default_cfg->{'test_group_second'}]), groups => default_groups_with([$default_cfg->{'test_group_second'}]),
roles => default_roles(), roles => default_roles(),
acl_root => default_acls_with([$default_cfg->{acl_simple_group}, $default_cfg->{acl_complex_missing_group}]), acl_root => default_acls_with(
[$default_cfg->{acl_simple_group}, $default_cfg->{acl_complex_missing_group}]
),
}, },
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n". . $default_raw->{users}->{'root@pam'} . "\n"
$default_raw->{users}->{'test2_pam'}."\n". . $default_raw->{users}->{'test2_pam'} . "\n"
$default_raw->{users}->{'test3_pam'}."\n". . $default_raw->{users}->{'test3_pam'} . "\n"
$default_raw->{users}->{'test_pam'}."\n\n". . $default_raw->{users}->{'test_pam'} . "\n\n"
$default_raw->{groups}->{'test_group_second'}."\n". . $default_raw->{groups}->{'test_group_second'} . "\n"
$default_raw->{acl}->{'acl_simple_group'}."\n". . $default_raw->{acl}->{'acl_simple_group'} . "\n"
$default_raw->{acl}->{'acl_complex_groups_1'}."\n". . $default_raw->{acl}->{'acl_complex_groups_1'} . "\n"
$default_raw->{acl}->{'acl_complex_groups_2'}."\n", . $default_raw->{acl}->{'acl_complex_groups_2'} . "\n",
expected_raw => "". expected_raw => ""
$default_raw->{users}->{'root@pam'}."\n". . $default_raw->{users}->{'root@pam'} . "\n"
$default_raw->{users}->{'test2_pam'}."\n". . $default_raw->{users}->{'test2_pam'} . "\n"
$default_raw->{users}->{'test3_pam'}."\n". . $default_raw->{users}->{'test3_pam'} . "\n"
$default_raw->{users}->{'test_pam'}."\n\n". . $default_raw->{users}->{'test_pam'} . "\n\n"
$default_raw->{groups}->{'test_group_second'}."\n\n\n\n". . $default_raw->{groups}->{'test_group_second'}
$default_raw->{acl}->{'acl_simple_group'}."\n". . "\n\n\n\n"
$default_raw->{acl}->{'acl_complex_groups_1'}."\n". . $default_raw->{acl}->{'acl_simple_group'} . "\n"
$default_raw->{acl}->{'acl_complex_groups_2'}."\n", . $default_raw->{acl}->{'acl_complex_groups_1'} . "\n"
. $default_raw->{acl}->{'acl_complex_groups_2'} . "\n",
}, },
{ {
name => "acl_simple_token", name => "acl_simple_token",
@ -798,57 +851,66 @@ my $tests = [
roles => default_roles(), roles => default_roles(),
acl_root => default_acls_with([$default_cfg->{acl_simple_token}]), acl_root => default_acls_with([$default_cfg->{acl_simple_token}]),
}, },
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n". . $default_raw->{users}->{'root@pam'} . "\n"
$default_raw->{users}->{'test_pam'}."\n". . $default_raw->{users}->{'test_pam'} . "\n"
$default_raw->{tokens}->{'test_token_simple'}."\n\n\n\n\n". . $default_raw->{tokens}->{'test_token_simple'}
$default_raw->{acl}->{'acl_simple_token'}."\n", . "\n\n\n\n\n"
. $default_raw->{acl}->{'acl_simple_token'} . "\n",
}, },
{ {
name => "acl_complex_tokens", name => "acl_complex_tokens",
config => { config => {
users => default_users_with([$default_cfg->{test_pam_with_token}, $default_cfg->{'test_pam2_with_token'}]), users => default_users_with(
[$default_cfg->{test_pam_with_token}, $default_cfg->{'test_pam2_with_token'}]
),
roles => default_roles(), roles => default_roles(),
acl_root => default_acls_with([$default_cfg->{acl_simple_token}, $default_cfg->{acl_complex_tokens}]), acl_root => default_acls_with(
[$default_cfg->{acl_simple_token}, $default_cfg->{acl_complex_tokens}]
),
}, },
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n". . $default_raw->{users}->{'root@pam'} . "\n"
$default_raw->{users}->{'test2_pam'}."\n". . $default_raw->{users}->{'test2_pam'} . "\n"
$default_raw->{tokens}->{'test_token_multi_expired'}."\n". . $default_raw->{tokens}->{'test_token_multi_expired'} . "\n"
$default_raw->{tokens}->{'test_token_multi_full'}."\n". . $default_raw->{tokens}->{'test_token_multi_full'} . "\n"
$default_raw->{tokens}->{'test_token_multi_privsep'}."\n". . $default_raw->{tokens}->{'test_token_multi_privsep'} . "\n"
$default_raw->{users}->{'test_pam'}."\n". . $default_raw->{users}->{'test_pam'} . "\n"
$default_raw->{tokens}->{'test_token_simple'}."\n\n\n\n\n". . $default_raw->{tokens}->{'test_token_simple'}
$default_raw->{acl}->{'acl_simple_token'}."\n". . "\n\n\n\n\n"
$default_raw->{acl}->{'acl_complex_tokens_1'}."\n". . $default_raw->{acl}->{'acl_simple_token'} . "\n"
$default_raw->{acl}->{'acl_complex_tokens_2'}."\n", . $default_raw->{acl}->{'acl_complex_tokens_1'} . "\n"
. $default_raw->{acl}->{'acl_complex_tokens_2'} . "\n",
}, },
{ {
name => "acl_complex_missing_token", name => "acl_complex_missing_token",
config => { config => {
users => default_users_with([$default_cfg->{test_pam}, $default_cfg->{test_pam2_with_token}]), users => default_users_with(
[$default_cfg->{test_pam}, $default_cfg->{test_pam2_with_token}]
),
roles => default_roles(), roles => default_roles(),
acl_root => default_acls_with([$default_cfg->{acl_complex_missing_token}]), acl_root => default_acls_with([$default_cfg->{acl_complex_missing_token}]),
}, },
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n". . $default_raw->{users}->{'root@pam'} . "\n"
$default_raw->{users}->{'test2_pam'}."\n". . $default_raw->{users}->{'test2_pam'} . "\n"
$default_raw->{tokens}->{'test_token_multi_expired'}."\n". . $default_raw->{tokens}->{'test_token_multi_expired'} . "\n"
$default_raw->{tokens}->{'test_token_multi_full'}."\n". . $default_raw->{tokens}->{'test_token_multi_full'} . "\n"
$default_raw->{tokens}->{'test_token_multi_privsep'}."\n". . $default_raw->{tokens}->{'test_token_multi_privsep'} . "\n"
$default_raw->{users}->{'test_pam'}."\n". . $default_raw->{users}->{'test_pam'} . "\n"
$default_raw->{acl}->{'acl_simple_token'}."\n". . $default_raw->{acl}->{'acl_simple_token'} . "\n"
$default_raw->{acl}->{'acl_complex_tokens_1'}."\n". . $default_raw->{acl}->{'acl_complex_tokens_1'} . "\n"
$default_raw->{acl}->{'acl_complex_tokens_2'}."\n", . $default_raw->{acl}->{'acl_complex_tokens_2'} . "\n",
expected_raw => "". expected_raw => ""
$default_raw->{users}->{'root@pam'}."\n". . $default_raw->{users}->{'root@pam'} . "\n"
$default_raw->{users}->{'test2_pam'}."\n". . $default_raw->{users}->{'test2_pam'} . "\n"
$default_raw->{tokens}->{'test_token_multi_expired'}."\n". . $default_raw->{tokens}->{'test_token_multi_expired'} . "\n"
$default_raw->{tokens}->{'test_token_multi_full'}."\n". . $default_raw->{tokens}->{'test_token_multi_full'} . "\n"
$default_raw->{tokens}->{'test_token_multi_privsep'}."\n". . $default_raw->{tokens}->{'test_token_multi_privsep'} . "\n"
$default_raw->{users}->{'test_pam'}."\n\n\n\n\n". . $default_raw->{users}->{'test_pam'}
$default_raw->{acl}->{'acl_complex_tokens_1_missing'}."\n". . "\n\n\n\n\n"
$default_raw->{acl}->{'acl_complex_tokens_2'}."\n", . $default_raw->{acl}->{'acl_complex_tokens_1_missing'} . "\n"
. $default_raw->{acl}->{'acl_complex_tokens_2'} . "\n",
}, },
{ {
name => "acl_missing_role", name => "acl_missing_role",
@ -857,127 +919,149 @@ my $tests = [
roles => default_roles(), roles => default_roles(),
acl_root => default_acls_with([$default_cfg->{acl_simple_user}]), acl_root => default_acls_with([$default_cfg->{acl_simple_user}]),
}, },
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n". . $default_raw->{users}->{'root@pam'} . "\n"
$default_raw->{users}->{'test_pam'}."\n\n\n\n\n". . $default_raw->{users}->{'test_pam'}
$default_raw->{acl}->{'acl_simple_user'}."\n". . "\n\n\n\n\n"
$default_raw->{acl}->{'acl_missing_role'}."\n", . $default_raw->{acl}->{'acl_simple_user'} . "\n"
expected_raw => "". . $default_raw->{acl}->{'acl_missing_role'} . "\n",
$default_raw->{users}->{'root@pam'}."\n". expected_raw => ""
$default_raw->{users}->{'test_pam'}."\n\n\n\n\n". . $default_raw->{users}->{'root@pam'} . "\n"
$default_raw->{acl}->{'acl_simple_user'}."\n", . $default_raw->{users}->{'test_pam'}
. "\n\n\n\n\n"
. $default_raw->{acl}->{'acl_simple_user'} . "\n",
}, },
{ {
name => "acl_complex_mixed", name => "acl_complex_mixed",
config => { config => {
users => default_users_with([$default_cfg->{test_pam_with_group}, $default_cfg->{'test2_pam_with_group'}, $default_cfg->{'test3_pam'}]), users => default_users_with(
groups => default_groups_with([$default_cfg->{'test_group_members'}, $default_cfg->{'test_group_second'}]), [
$default_cfg->{test_pam_with_group},
$default_cfg->{'test2_pam_with_group'},
$default_cfg->{'test3_pam'},
],
),
groups => default_groups_with(
[$default_cfg->{'test_group_members'}, $default_cfg->{'test_group_second'}]
),
roles => default_roles(), roles => default_roles(),
acl_root => default_acls_with([ acl_root => default_acls_with([
$default_cfg->{acl_complex_mixed_root}, $default_cfg->{acl_complex_mixed_root},
$default_cfg->{acl_complex_mixed_storage}, $default_cfg->{acl_complex_mixed_storage},
]), ]),
}, },
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n". . $default_raw->{users}->{'root@pam'} . "\n"
$default_raw->{users}->{'test2_pam'}."\n". . $default_raw->{users}->{'test2_pam'} . "\n"
$default_raw->{users}->{'test3_pam'}."\n". . $default_raw->{users}->{'test3_pam'} . "\n"
$default_raw->{users}->{'test_pam'}."\n\n". . $default_raw->{users}->{'test_pam'} . "\n\n"
$default_raw->{groups}->{'test_group_second'}."\n". . $default_raw->{groups}->{'test_group_second'} . "\n"
$default_raw->{groups}->{'test_group_members'}."\n\n\n\n". . $default_raw->{groups}->{'test_group_members'}
$default_raw->{acl}->{'acl_simple_group'}."\n". . "\n\n\n\n"
$default_raw->{acl}->{'acl_complex_groups_1'}."\n". . $default_raw->{acl}->{'acl_simple_group'} . "\n"
$default_raw->{acl}->{'acl_complex_groups_2'}."\n". . $default_raw->{acl}->{'acl_complex_groups_1'} . "\n"
$default_raw->{acl}->{'acl_simple_user'}."\n". . $default_raw->{acl}->{'acl_complex_groups_2'} . "\n"
$default_raw->{acl}->{'acl_complex_users_1'}."\n". . $default_raw->{acl}->{'acl_simple_user'} . "\n"
$default_raw->{acl}->{'acl_complex_users_2'}."\n", . $default_raw->{acl}->{'acl_complex_users_1'} . "\n"
expected_raw => "". . $default_raw->{acl}->{'acl_complex_users_2'} . "\n",
$default_raw->{users}->{'root@pam'}."\n". expected_raw => ""
$default_raw->{users}->{'test2_pam'}."\n". . $default_raw->{users}->{'root@pam'} . "\n"
$default_raw->{users}->{'test3_pam'}."\n". . $default_raw->{users}->{'test2_pam'} . "\n"
$default_raw->{users}->{'test_pam'}."\n\n". . $default_raw->{users}->{'test3_pam'} . "\n"
$default_raw->{groups}->{'test_group_second'}."\n". . $default_raw->{users}->{'test_pam'} . "\n\n"
$default_raw->{groups}->{'test_group_members'}."\n\n\n\n". . $default_raw->{groups}->{'test_group_second'} . "\n"
$default_raw->{acl}->{'acl_complex_mixed_1'}."\n". . $default_raw->{groups}->{'test_group_members'}
$default_raw->{acl}->{'acl_complex_mixed_2'}."\n". . "\n\n\n\n"
$default_raw->{acl}->{'acl_complex_mixed_3'}."\n", . $default_raw->{acl}->{'acl_complex_mixed_1'} . "\n"
. $default_raw->{acl}->{'acl_complex_mixed_2'} . "\n"
. $default_raw->{acl}->{'acl_complex_mixed_3'} . "\n",
}, },
{ {
name => "acl_complex_mixed_prop_noprop_no_merge_sort_by_path", name => "acl_complex_mixed_prop_noprop_no_merge_sort_by_path",
config => { config => {
users => default_users_with([$default_cfg->{test_pam_with_group}, $default_cfg->{'test2_pam_with_group'}, $default_cfg->{'test3_pam'}]), users => default_users_with(
groups => default_groups_with([$default_cfg->{'test_group_members'}, $default_cfg->{'test_group_second'}]), [
$default_cfg->{test_pam_with_group},
$default_cfg->{'test2_pam_with_group'},
$default_cfg->{'test3_pam'},
],
),
groups => default_groups_with(
[$default_cfg->{'test_group_members'}, $default_cfg->{'test_group_second'}]
),
roles => default_roles(), roles => default_roles(),
acl_root => default_acls_with([ acl_root => default_acls_with([
$default_cfg->{acl_complex_mixed_root_noprop}, $default_cfg->{acl_complex_mixed_root_noprop},
$default_cfg->{acl_complex_mixed_storage_noprop}, $default_cfg->{acl_complex_mixed_storage_noprop},
]), ]),
}, },
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n". . $default_raw->{users}->{'root@pam'} . "\n"
$default_raw->{users}->{'test2_pam'}."\n". . $default_raw->{users}->{'test2_pam'} . "\n"
$default_raw->{users}->{'test3_pam'}."\n". . $default_raw->{users}->{'test3_pam'} . "\n"
$default_raw->{users}->{'test_pam'}."\n\n". . $default_raw->{users}->{'test_pam'} . "\n\n"
$default_raw->{groups}->{'test_group_second'}."\n". . $default_raw->{groups}->{'test_group_second'} . "\n"
$default_raw->{groups}->{'test_group_members'}."\n\n\n\n". . $default_raw->{groups}->{'test_group_members'}
$default_raw->{acl}->{'acl_simple_group_noprop'}."\n". . "\n\n\n\n"
$default_raw->{acl}->{'acl_simple_user'}."\n". . $default_raw->{acl}->{'acl_simple_group_noprop'} . "\n"
$default_raw->{acl}->{'acl_complex_groups_1_noprop'}."\n". . $default_raw->{acl}->{'acl_simple_user'} . "\n"
$default_raw->{acl}->{'acl_complex_groups_2_noprop'}."\n". . $default_raw->{acl}->{'acl_complex_groups_1_noprop'} . "\n"
$default_raw->{acl}->{'acl_complex_users_1'}."\n". . $default_raw->{acl}->{'acl_complex_groups_2_noprop'} . "\n"
$default_raw->{acl}->{'acl_complex_users_2'}."\n", . $default_raw->{acl}->{'acl_complex_users_1'} . "\n"
. $default_raw->{acl}->{'acl_complex_users_2'} . "\n",
}, },
{ {
name => "sort_roles_and_privs", name => "sort_roles_and_privs",
raw => "". raw => ""
$default_raw->{users}->{'root@pam'}."\n". . $default_raw->{users}->{'root@pam'} . "\n"
$default_raw->{roles}->{'test_role_single_priv'}."\n\n". . $default_raw->{roles}->{'test_role_single_priv'} . "\n\n"
$default_raw->{roles}->{'test_role_privs_out_of_order'}."\n\n", . $default_raw->{roles}->{'test_role_privs_out_of_order'} . "\n\n",
expected_raw => "". expected_raw => ""
$default_raw->{users}->{'root@pam'}."\n\n\n\n". . $default_raw->{users}->{'root@pam'}
$default_raw->{roles}->{'test_role_privs'}."\n". . "\n\n\n\n"
$default_raw->{roles}->{'test_role_single_priv'}."\n\n", . $default_raw->{roles}->{'test_role_privs'} . "\n"
. $default_raw->{roles}->{'test_role_single_priv'} . "\n\n",
}, },
{ {
name => "sort_users_and_group_members", name => "sort_users_and_group_members",
raw => "". raw => ""
$default_raw->{users}->{'test2_pam'}."\n". . $default_raw->{users}->{'test2_pam'} . "\n"
$default_raw->{users}->{'root@pam'}."\n". . $default_raw->{users}->{'root@pam'} . "\n"
$default_raw->{users}->{'test_pam'}."\n\n". . $default_raw->{users}->{'test_pam'} . "\n\n"
$default_raw->{groups}->{'test_group_members_out_of_order'}."\n\n". . $default_raw->{groups}->{'test_group_members_out_of_order'} . "\n\n" . "\n\n",
"\n\n", expected_raw => ""
expected_raw => "". . $default_raw->{users}->{'root@pam'} . "\n"
$default_raw->{users}->{'root@pam'}."\n". . $default_raw->{users}->{'test2_pam'} . "\n"
$default_raw->{users}->{'test2_pam'}."\n". . $default_raw->{users}->{'test_pam'} . "\n\n"
$default_raw->{users}->{'test_pam'}."\n\n". . $default_raw->{groups}->{'test_group_members'} . "\n\n" . "\n\n",
$default_raw->{groups}->{'test_group_members'}."\n\n".
"\n\n",
}, },
{ {
name => "sort_user_groups_and_acls", name => "sort_user_groups_and_acls",
raw => "". raw => ""
$default_raw->{users}->{'test2_pam'}."\n". . $default_raw->{users}->{'test2_pam'} . "\n"
$default_raw->{users}->{'root@pam'}."\n". . $default_raw->{users}->{'root@pam'} . "\n"
$default_raw->{users}->{'test_pam'}."\n\n". . $default_raw->{users}->{'test_pam'} . "\n\n"
$default_raw->{users}->{'test3_pam'}."\n". . $default_raw->{users}->{'test3_pam'} . "\n"
$default_raw->{groups}->{'test_group_members_out_of_order'}."\n\n\n\n". . $default_raw->{groups}->{'test_group_members_out_of_order'}
$default_raw->{groups}->{'test_group_second'}."\n". . "\n\n\n\n"
$default_raw->{acl}->{'acl_simple_user'}."\n". . $default_raw->{groups}->{'test_group_second'} . "\n"
$default_raw->{acl}->{'acl_simple_group'}."\n". . $default_raw->{acl}->{'acl_simple_user'} . "\n"
$default_raw->{acl}->{'acl_complex_users_1'}."\n". . $default_raw->{acl}->{'acl_simple_group'} . "\n"
$default_raw->{acl}->{'acl_complex_users_2'}."\n". . $default_raw->{acl}->{'acl_complex_users_1'} . "\n"
$default_raw->{acl}->{'acl_complex_groups_1'}."\n". . $default_raw->{acl}->{'acl_complex_users_2'} . "\n"
$default_raw->{acl}->{'acl_complex_groups_2'}."\n", . $default_raw->{acl}->{'acl_complex_groups_1'} . "\n"
expected_raw => "". . $default_raw->{acl}->{'acl_complex_groups_2'} . "\n",
$default_raw->{users}->{'root@pam'}."\n". expected_raw => ""
$default_raw->{users}->{'test2_pam'}."\n". . $default_raw->{users}->{'root@pam'} . "\n"
$default_raw->{users}->{'test3_pam'}."\n". . $default_raw->{users}->{'test2_pam'} . "\n"
$default_raw->{users}->{'test_pam'}."\n\n". . $default_raw->{users}->{'test3_pam'} . "\n"
$default_raw->{groups}->{'test_group_second'}."\n". . $default_raw->{users}->{'test_pam'} . "\n\n"
$default_raw->{groups}->{'test_group_members'}."\n\n\n\n". . $default_raw->{groups}->{'test_group_second'} . "\n"
$default_raw->{acl}->{'acl_complex_mixed_1'}."\n". . $default_raw->{groups}->{'test_group_members'}
$default_raw->{acl}->{'acl_complex_mixed_2'}."\n". . "\n\n\n\n"
$default_raw->{acl}->{'acl_complex_mixed_3'}."\n", . $default_raw->{acl}->{'acl_complex_mixed_1'} . "\n"
. $default_raw->{acl}->{'acl_complex_mixed_2'} . "\n"
. $default_raw->{acl}->{'acl_complex_mixed_3'} . "\n",
}, },
{ {
name => 'default_values', name => 'default_values',
@ -1005,25 +1089,24 @@ my $tests = [
pools => default_pools_with([$default_cfg->{test_pool_empty}]), pools => default_pools_with([$default_cfg->{test_pool_empty}]),
acl_root => {}, acl_root => {},
}, },
raw => "". raw => ""
'user:root@pam'."\n". . 'user:root@pam' . "\n"
'user:test@pam'."\n". . 'user:test@pam' . "\n"
'token:test@pam!test'."\n\n". . 'token:test@pam!test' . "\n\n"
'group:testgroup'."\n\n". . 'group:testgroup' . "\n\n"
'pool:testpool'."\n\n". . 'pool:testpool' . "\n\n"
'role:testrole'."\n\n". . 'role:testrole' . "\n\n"
'acl::/:', . 'acl::/:',
expected_raw => "". expected_raw => ""
'user:root@pam:0:0::::::'."\n". . 'user:root@pam:0:0::::::' . "\n"
'user:test@pam:0:0::::::'."\n". . 'user:test@pam:0:0::::::' . "\n"
'token:test@pam!test:0:0::'."\n\n". . 'token:test@pam!test:0:0::' . "\n\n"
'group:testgroup:::'."\n\n". . 'group:testgroup:::' . "\n\n"
'pool:testpool::::'."\n\n". . 'pool:testpool::::' . "\n\n"
'role:testrole::'."\n\n", . 'role:testrole::' . "\n\n",
}, },
]; ];
my $number_of_tests_run = 0; my $number_of_tests_run = 0;
foreach my $t (@$tests) { foreach my $t (@$tests) {
my $expected_config = $t->{expected_config} // $t->{config}; my $expected_config = $t->{expected_config} // $t->{config};
@ -1035,7 +1118,11 @@ foreach my $t (@$tests) {
$number_of_tests_run++; $number_of_tests_run++;
} }
if (defined($t->{expected_raw}) && !defined($t->{config})) { if (defined($t->{expected_raw}) && !defined($t->{config})) {
is(PVE::AccessControl::write_user_config($t->{name}, $parsed), $t->{expected_raw}, "$t->{name}_rewrite"); is(
PVE::AccessControl::write_user_config($t->{name}, $parsed),
$t->{expected_raw},
"$t->{name}_rewrite",
);
$number_of_tests_run++; $number_of_tests_run++;
} }
@ -1047,10 +1134,14 @@ foreach my $t (@$tests) {
$number_of_tests_run++; $number_of_tests_run++;
} }
if (defined($t->{expected_config}) && !defined($t->{raw})) { if (defined($t->{expected_config}) && !defined($t->{raw})) {
is_deeply(PVE::AccessControl::parse_user_config($t->{name}, $t->{written}), $t->{expected_config}, "$t->{name}_reparse"); is_deeply(
PVE::AccessControl::parse_user_config($t->{name}, $t->{written}),
$t->{expected_config},
"$t->{name}_reparse",
);
$number_of_tests_run++; $number_of_tests_run++;
} }
} }
}; }
done_testing( $number_of_tests_run); done_testing($number_of_tests_run);

View File

@ -65,7 +65,7 @@ check_permission(
'' # sorted, comma-separated expected privilege string '' # sorted, comma-separated expected privilege string
. 'VM.Allocate,VM.Audit,VM.Backup,VM.Clone,VM.Config.CDROM,VM.Config.CPU,VM.Config.Cloudinit,' . 'VM.Allocate,VM.Audit,VM.Backup,VM.Clone,VM.Config.CDROM,VM.Config.CPU,VM.Config.Cloudinit,'
. 'VM.Config.Disk,VM.Config.HWType,VM.Config.Memory,VM.Config.Network,VM.Config.Options,' . 'VM.Config.Disk,VM.Config.HWType,VM.Config.Memory,VM.Config.Network,VM.Config.Options,'
. 'VM.Console,VM.Migrate,VM.Monitor,VM.PowerMgmt,VM.Snapshot,VM.Snapshot.Rollback' . 'VM.Console,VM.Migrate,VM.Monitor,VM.PowerMgmt,VM.Snapshot,VM.Snapshot.Rollback',
); );
# Administrator -> Permissions.Modify! # Administrator -> Permissions.Modify!
check_permission( check_permission(
@ -92,4 +92,4 @@ check_roles('sue@pve', '/vms/200', 'NoAccess');
print "all tests passed\n"; print "all tests passed\n";
exit (0); exit(0);

View File

@ -40,4 +40,4 @@ check_roles('User2@pve', '/vms', '');
print "all tests passed\n"; print "all tests passed\n";
exit (0); exit(0);

View File

@ -35,4 +35,4 @@ check_roles('User1@pve', '/vms/200', 'Role2');
print "all tests passed\n"; print "all tests passed\n";
exit (0); exit(0);

View File

@ -27,10 +27,9 @@ sub check_roles {
print "ROLES:$path:$user:$res\n"; print "ROLES:$path:$user:$res\n";
} }
check_roles('User1@pve', '/vms/300', 'Role1'); check_roles('User1@pve', '/vms/300', 'Role1');
check_roles('User2@pve', '/vms/300', 'NoAccess'); check_roles('User2@pve', '/vms/300', 'NoAccess');
print "all tests passed\n"; print "all tests passed\n";
exit (0); exit(0);

View File

@ -27,7 +27,6 @@ sub check_roles {
print "ROLES:$path:$user:$res\n"; print "ROLES:$path:$user:$res\n";
} }
check_roles('User1@pve', '/vms', 'Role1'); check_roles('User1@pve', '/vms', 'Role1');
check_roles('User1@pve', '/vms/100', 'Role1'); check_roles('User1@pve', '/vms/100', 'Role1');
check_roles('User1@pve', '/vms/100/a', 'Role1'); check_roles('User1@pve', '/vms/100/a', 'Role1');
@ -43,4 +42,4 @@ check_roles('User2@pve', '/kvm/vms/100/a/b', '');
print "all tests passed\n"; print "all tests passed\n";
exit (0); exit(0);

View File

@ -109,4 +109,4 @@ check_permissions('User4@pve', '/storage/store2', '');
print "all tests passed\n"; print "all tests passed\n";
exit (0); exit(0);

View File

@ -54,4 +54,4 @@ check_permissions('User1@pve', '/vms/100', '');
print "all tests passed\n"; print "all tests passed\n";
exit (0); exit(0);

View File

@ -70,5 +70,5 @@ check_permission('max@pve!token2', '/vms/300', 'VM.Allocate,VM.Audit,VM.Console,
print "all tests passed\n"; print "all tests passed\n";
exit (0); exit(0);

View File

@ -14,13 +14,13 @@ my $domainscfg = {
ids => { ids => {
"pam" => { type => 'pam' }, "pam" => { type => 'pam' },
"pve" => { type => 'pve' }, "pve" => { type => 'pve' },
"syncedrealm" => { type => 'ldap' } "syncedrealm" => { type => 'ldap' },
}, },
}; };
my $initialusercfg = { my $initialusercfg = {
users => { users => {
'root@pam' => { username => 'root', }, 'root@pam' => { username => 'root' },
'user1@syncedrealm' => { 'user1@syncedrealm' => {
username => 'user1', username => 'user1',
enable => 1, enable => 1,
@ -36,8 +36,8 @@ my $initialusercfg = {
}, },
}, },
groups => { groups => {
'group1-syncedrealm' => { users => {}, }, 'group1-syncedrealm' => { users => {} },
'group2-syncedrealm' => { users => {}, }, 'group2-syncedrealm' => { users => {} },
}, },
acl_root => { acl_root => {
users => { users => {
@ -50,15 +50,15 @@ my $initialusercfg = {
my $sync_response = { my $sync_response = {
user => [ user => [
{ {
attributes => { 'uid' => ['user1'], }, attributes => { 'uid' => ['user1'] },
dn => 'uid=user1,dc=syncedrealm', dn => 'uid=user1,dc=syncedrealm',
}, },
{ {
attributes => { 'uid' => ['user2'], }, attributes => { 'uid' => ['user2'] },
dn => 'uid=user2,dc=syncedrealm', dn => 'uid=user2,dc=syncedrealm',
}, },
{ {
attributes => { 'uid' => ['user4'], }, attributes => { 'uid' => ['user4'] },
dn => 'uid=user4,dc=syncedrealm', dn => 'uid=user4,dc=syncedrealm',
}, },
], ],
@ -74,7 +74,7 @@ my $sync_response = {
members => [ members => [
'uid=nonexisting,dc=syncedrealm', 'uid=nonexisting,dc=syncedrealm',
], ],
} },
], ],
}; };
@ -83,7 +83,7 @@ my $returned_user_cfg = {};
# mocking all cluster and ldap operations # mocking all cluster and ldap operations
my $pve_cluster_module = Test::MockModule->new('PVE::Cluster'); my $pve_cluster_module = Test::MockModule->new('PVE::Cluster');
$pve_cluster_module->mock( $pve_cluster_module->mock(
cfs_update => sub {}, cfs_update => sub { },
cfs_read_file => sub { cfs_read_file => sub {
my ($filename) = @_; my ($filename) = @_;
if ($filename eq 'domains.cfg') { return dclone($domainscfg); } if ($filename eq 'domains.cfg') { return dclone($domainscfg); }
@ -129,7 +129,7 @@ $pve_rpcenvironment->mock(
my $pve_ldap_module = Test::MockModule->new('PVE::LDAP'); my $pve_ldap_module = Test::MockModule->new('PVE::LDAP');
$pve_ldap_module->mock( $pve_ldap_module->mock(
ldap_connect => sub { return {}; }, ldap_connect => sub { return {}; },
ldap_bind => sub {}, ldap_bind => sub { },
query_users => sub { query_users => sub {
return $sync_response->{user}; return $sync_response->{user};
}, },
@ -152,7 +152,7 @@ my $tests = [
}, },
{ {
users => { users => {
'root@pam' => { username => 'root', }, 'root@pam' => { username => 'root' },
'user1@syncedrealm' => { 'user1@syncedrealm' => {
username => 'user1', username => 'user1',
enable => 1, enable => 1,
@ -177,8 +177,8 @@ my $tests = [
'user1@syncedrealm' => 1, 'user1@syncedrealm' => 1,
}, },
}, },
'group2-syncedrealm' => { users => {}, }, 'group2-syncedrealm' => { users => {} },
'group3-syncedrealm' => { users => {}, }, 'group3-syncedrealm' => { users => {} },
}, },
acl_root => { acl_root => {
users => { users => {
@ -197,7 +197,7 @@ my $tests = [
}, },
{ {
users => { users => {
'root@pam' => { username => 'root', }, 'root@pam' => { username => 'root' },
'user1@syncedrealm' => { 'user1@syncedrealm' => {
username => 'user1', username => 'user1',
enable => 1, enable => 1,
@ -217,7 +217,7 @@ my $tests = [
'user1@syncedrealm' => 1, 'user1@syncedrealm' => 1,
}, },
}, },
'group3-syncedrealm' => { users => {}, } 'group3-syncedrealm' => { users => {} },
}, },
acl_root => { acl_root => {
users => { users => {
@ -236,7 +236,7 @@ my $tests = [
}, },
{ {
users => { users => {
'root@pam' => { username => 'root', }, 'root@pam' => { username => 'root' },
'user1@syncedrealm' => { 'user1@syncedrealm' => {
username => 'user1', username => 'user1',
enable => 1, enable => 1,
@ -261,8 +261,8 @@ my $tests = [
'user1@syncedrealm' => 1, 'user1@syncedrealm' => 1,
}, },
}, },
'group2-syncedrealm' => { users => {}, }, 'group2-syncedrealm' => { users => {} },
'group3-syncedrealm' => { users => {}, }, 'group3-syncedrealm' => { users => {} },
}, },
acl_root => { acl_root => {
users => {}, users => {},
@ -279,7 +279,7 @@ my $tests = [
}, },
{ {
users => { users => {
'root@pam' => { username => 'root', }, 'root@pam' => { username => 'root' },
'user1@syncedrealm' => { 'user1@syncedrealm' => {
username => 'user1', username => 'user1',
enable => 1, enable => 1,
@ -299,7 +299,7 @@ my $tests = [
'user1@syncedrealm' => 1, 'user1@syncedrealm' => 1,
}, },
}, },
'group3-syncedrealm' => { users => {}, }, 'group3-syncedrealm' => { users => {} },
}, },
acl_root => { acl_root => {
users => {}, users => {},
@ -316,7 +316,7 @@ my $tests = [
}, },
{ {
users => { users => {
'root@pam' => { username => 'root', }, 'root@pam' => { username => 'root' },
'user1@syncedrealm' => { 'user1@syncedrealm' => {
username => 'user1', username => 'user1',
enable => 1, enable => 1,
@ -337,7 +337,7 @@ my $tests = [
'user1@syncedrealm' => 1, 'user1@syncedrealm' => 1,
}, },
}, },
'group3-syncedrealm' => { users => {}, }, 'group3-syncedrealm' => { users => {} },
}, },
acl_root => { acl_root => {
users => {}, users => {},