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,219 +14,257 @@ use PVE::RESTHandler;
use base qw(PVE::RESTHandler); use base qw(PVE::RESTHandler);
register_standard_option('acl-propagate', { register_standard_option(
description => "Allow to propagate (inherit) permissions.", 'acl-propagate',
type => 'boolean', {
optional => 1, description => "Allow to propagate (inherit) permissions.",
default => 1, type => 'boolean',
}); optional => 1,
register_standard_option('acl-path', { default => 1,
description => "Access control path", },
type => 'string', );
}); register_standard_option(
'acl-path',
{
description => "Access control path",
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 =>
user => 'all', "The returned list is restricted to objects where you have rights to modify permissions.",
user => 'all',
}, },
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => {}, properties => {},
}, },
returns => { returns => {
type => 'array', type => 'array',
items => { items => {
type => "object", type => "object",
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
propagate => get_standard_option('acl-propagate'), propagate => get_standard_option('acl-propagate'),
path => get_standard_option('acl-path'), path => get_standard_option('acl-path'),
type => { type => 'string', enum => ['user', 'group', 'token'] }, type => { type => 'string', enum => ['user', 'group', 'token'] },
ugid => { type => 'string' }, ugid => { type => 'string' },
roleid => { type => 'string' }, roleid => { type => 'string' },
}, },
}, },
}, },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get(); my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user(); my $authuser = $rpcenv->get_user();
my $res = []; my $res = [];
my $usercfg = $rpcenv->{user_cfg}; my $usercfg = $rpcenv->{user_cfg};
if (!$usercfg || !$usercfg->{acl_root}) { if (!$usercfg || !$usercfg->{acl_root}) {
return $res; return $res;
} }
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(
my ($path, $node) = @_; "/",
foreach my $type (qw(user group token)) { $root,
my $d = $node->{"${type}s"}; sub {
next if !$d; my ($path, $node) = @_;
next if !($audit || $rpcenv->check_perm_modify($authuser, $path, 1)); foreach my $type (qw(user group token)) {
foreach my $id (keys %$d) { my $d = $node->{"${type}s"};
foreach my $role (keys %{$d->{$id}}) { next if !$d;
my $propagate = $d->{$id}->{$role}; next if !($audit || $rpcenv->check_perm_modify($authuser, $path, 1));
push @$res, { foreach my $id (keys %$d) {
path => $path, foreach my $role (keys %{ $d->{$id} }) {
type => $type, my $propagate = $d->{$id}->{$role};
ugid => $id, push @$res,
roleid => $role, {
propagate => $propagate, path => $path,
}; type => $type,
} ugid => $id,
} roleid => $role,
} propagate => $propagate,
}); };
}
}
}
},
);
return $res; return $res;
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'update_acl', name => 'update_acl',
protected => 1, protected => 1,
path => '', path => '',
method => 'PUT', method => 'PUT',
permissions => { permissions => {
check => ['perm-modify', '{path}'], check => ['perm-modify', '{path}'],
}, },
description => "Update Access Control List (add or remove permissions).", description => "Update Access Control List (add or remove permissions).",
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
propagate => get_standard_option('acl-propagate'), propagate => get_standard_option('acl-propagate'),
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',
optional => 1, format => 'pve-userid-list',
}, optional => 1,
groups => { },
description => "List of groups.", groups => {
type => 'string', format => 'pve-groupid-list', description => "List of groups.",
optional => 1, type => 'string',
}, format => 'pve-groupid-list',
tokens => { optional => 1,
description => "List of API tokens.", },
type => 'string', format => 'pve-tokenid-list', tokens => {
optional => 1, description => "List of API tokens.",
}, type => 'string',
roles => { format => 'pve-tokenid-list',
description => "List of roles.", optional => 1,
type => 'string', format => 'pve-roleid-list', },
}, roles => {
delete => { description => "List of roles.",
description => "Remove permissions (instead of adding it).", type => 'string',
type => 'boolean', format => 'pve-roleid-list',
optional => 1, },
}, delete => {
}, description => "Remove permissions (instead of adding it).",
type => 'boolean',
optional => 1,
},
},
}, },
returns => { type => 'null' }, returns => { type => 'null' },
code => sub { code => sub {
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});
raise_param_exc({ path => "invalid ACL path '$param->{path}'" }) if !$path; raise_param_exc({ path => "invalid ACL path '$param->{path}'" }) if !$path;
if (!$param->{delete} && !PVE::AccessControl::check_path($path)) { if (!$param->{delete} && !PVE::AccessControl::check_path($path)) {
raise_param_exc({ path => "invalid ACL path '$param->{path}'" }); raise_param_exc({ path => "invalid ACL path '$param->{path}'" });
} }
PVE::AccessControl::lock_user_config( PVE::AccessControl::lock_user_config(
sub { sub {
my $cfg = cfs_read_file("user.cfg"); my $cfg = cfs_read_file("user.cfg");
my $rpcenv = PVE::RPCEnvironment::get(); my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user(); my $authuser = $rpcenv->get_user();
my $auth_user_privs = $rpcenv->permissions($authuser, $path); my $auth_user_privs = $rpcenv->permissions($authuser, $path);
my $propagate = 1; my $propagate = 1;
if (defined($param->{propagate})) { if (defined($param->{propagate})) {
$propagate = $param->{propagate} ? 1 : 0; $propagate = $param->{propagate} ? 1 : 0;
} }
my $node = PVE::AccessControl::find_acl_tree_node($cfg->{acl_root}, $path); my $node = PVE::AccessControl::find_acl_tree_node($cfg->{acl_root}, $path);
foreach my $role (split_list($param->{roles})) { foreach my $role (split_list($param->{roles})) {
die "role '$role' does not exist\n" die "role '$role' does not exist\n"
if !$cfg->{roles}->{$role}; if !$cfg->{roles}->{$role};
# permissions() returns set privs as key, and propagate bit as value! # permissions() returns set privs as key, and propagate bit as value!
if (!defined($auth_user_privs->{'Permissions.Modify'})) { if (!defined($auth_user_privs->{'Permissions.Modify'})) {
# 'perm-modify' allows /vms/* with VM.Allocate and similar restricted use cases # 'perm-modify' allows /vms/* with VM.Allocate and similar restricted use cases
# filter those to only allow handing out a subset of currently active privs # filter those to only allow handing out a subset of currently active privs
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})) {
die "group '$group' does not exist\n" die "group '$group' does not exist\n"
if !$cfg->{groups}->{$group}; if !$cfg->{groups}->{$group};
if ($param->{delete}) { if ($param->{delete}) {
delete($node->{groups}->{$group}->{$role}); delete($node->{groups}->{$group}->{$role});
} else { } else {
$node->{groups}->{$group}->{$role} = $propagate; $node->{groups}->{$group}->{$role} = $propagate;
} }
} }
foreach my $userid (split_list($param->{users})) { foreach my $userid (split_list($param->{users})) {
my $username = PVE::AccessControl::verify_username($userid); my $username = PVE::AccessControl::verify_username($userid);
die "user '$username' does not exist\n" die "user '$username' does not exist\n"
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;
} }
} }
foreach my $tokenid (split_list($param->{tokens})) { foreach my $tokenid (split_list($param->{tokens})) {
my ($username, $token) = PVE::AccessControl::split_tokenid($tokenid); my ($username, $token) = PVE::AccessControl::split_tokenid($tokenid);
PVE::AccessControl::check_token_exist($cfg, $username, $token); PVE::AccessControl::check_token_exist($cfg, $username, $token);
if ($param->{delete}) { if ($param->{delete}) {
delete $node->{tokens}->{$tokenid}->{$role}; delete $node->{tokens}->{$tokenid}->{$role};
} else { } else {
$node->{tokens}->{$tokenid}->{$role} = $propagate; $node->{tokens}->{$tokenid}->{$role} = $propagate;
} }
} }
} }
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,84 +32,84 @@ 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',
description => "Directory index.", description => "Directory index.",
permissions => { permissions => {
user => 'all', user => 'all',
}, },
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => {}, properties => {},
}, },
returns => { returns => {
type => 'array', type => 'array',
items => { items => {
type => "object", type => "object",
properties => { properties => {
subdir => { type => 'string' }, subdir => { type => 'string' },
}, },
}, },
links => [ { rel => 'child', href => "{subdir}" } ], links => [{ rel => 'child', href => "{subdir}" }],
}, },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
my $res = []; my $res = [];
my $ma = __PACKAGE__->method_attributes(); my $ma = __PACKAGE__->method_attributes();
foreach my $info (@$ma) { foreach my $info (@$ma) {
next if !$info->{subclass}; next if !$info->{subclass};
my $subpath = $info->{match_re}->[0]; my $subpath = $info->{match_re}->[0];
push @$res, { subdir => $subpath }; push @$res, { subdir => $subpath };
} }
push @$res, { subdir => 'ticket' }; push @$res, { subdir => 'ticket' };
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,274 +118,287 @@ 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))
# valid ticket && ($ticketuser eq $username)
) {
# 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) = @_;
die "TFA response should be in 'password', not 'otp' when 'tfa-challenge' is set\n" die "TFA response should be in 'password', not 'otp' when 'tfa-challenge' is set\n"
if defined($otp) && defined($tfa_challenge); if defined($otp) && defined($tfa_challenge);
my ($ticketuser, undef, $tfa_info); my ($ticketuser, undef, $tfa_info);
if (!defined($tfa_challenge)) { if (!defined($tfa_challenge)) {
# We only verify this ticket if we're not responding to a TFA challenge, as in that case # We only verify this ticket if we're not responding to a TFA challenge, as in that case
# it is a TFA-data ticket and will be verified by `authenticate_user`. # it is a TFA-data ticket and will be verified by `authenticate_user`.
($ticketuser, undef, $tfa_info) = PVE::AccessControl::verify_ticket($pw_or_ticket, 1); ($ticketuser, undef, $tfa_info) = PVE::AccessControl::verify_ticket($pw_or_ticket, 1);
} }
if (defined($ticketuser) && ($ticketuser eq 'root@pam' || $ticketuser eq $username)) { if (defined($ticketuser) && ($ticketuser eq 'root@pam' || $ticketuser eq $username)) {
if (defined($tfa_info)) { if (defined($tfa_info)) {
die "incomplete ticket\n"; die "incomplete ticket\n";
} }
# 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,
);
} }
my %extra; my %extra;
my $ticket_data = $username; my $ticket_data = $username;
my $aad; my $aad;
if (defined($tfa_info)) { if (defined($tfa_info)) {
$extra{NeedTFA} = 1; $extra{NeedTFA} = 1;
$ticket_data = "!tfa!$tfa_info"; $ticket_data = "!tfa!$tfa_info";
$aad = $username; $aad = $username;
} }
my $ticket = PVE::AccessControl::assemble_ticket($ticket_data, $aad); my $ticket = PVE::AccessControl::assemble_ticket($ticket_data, $aad);
my $csrftoken = PVE::AccessControl::assemble_csrf_prevention_token($username); my $csrftoken = PVE::AccessControl::assemble_csrf_prevention_token($username);
return { return {
ticket => $ticket, ticket => $ticket,
username => $username, username => $username,
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',
permissions => { user => 'world' }, permissions => { user => 'world' },
description => "Dummy. Useful for formatters which want to provide a login page.", description => "Dummy. Useful for formatters which want to provide a login page.",
parameters => { parameters => {
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
description => "Create or verify authentication ticket.", description => "Create or verify authentication ticket.",
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
username => { username => {
description => "User name", description => "User name",
type => 'string', type => 'string',
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>.", {
optional => 1, description =>
completion => \&PVE::AccessControl::complete_realm, "You can optionally pass the realm using this parameter. Normally"
}), . " the realm is simply added to the username <username>\@<realm>.",
password => { optional => 1,
description => "The secret password. This can also be a valid ticket.", completion => \&PVE::AccessControl::complete_realm,
type => 'string', },
}, ),
otp => { password => {
description => "One-time password for Two-factor authentication.", description => "The secret password. This can also be a valid ticket.",
type => 'string', type => 'string',
optional => 1, },
}, otp => {
path => { description => "One-time password for Two-factor authentication.",
description => "Verify ticket, and check if user have access 'privs' on 'path'", type => 'string',
type => 'string', optional => 1,
requires => 'privs', },
optional => 1, path => {
maxLength => 64, description => "Verify ticket, and check if user have access 'privs' on 'path'",
}, type => 'string',
privs => { requires => 'privs',
description => "Verify ticket, and check if user have access 'privs' on 'path'", optional => 1,
type => 'string' , format => 'pve-priv-list', maxLength => 64,
requires => 'path', },
optional => 1, privs => {
maxLength => 64, description => "Verify ticket, and check if user have access 'privs' on 'path'",
}, type => 'string',
'new-format' => { format => 'pve-priv-list',
type => 'boolean', requires => 'path',
description => 'This parameter is now ignored and assumed to be 1.', optional => 1,
optional => 1, maxLength => 64,
default => 1, },
}, 'new-format' => {
'tfa-challenge' => { type => 'boolean',
type => 'string', description => 'This parameter is now ignored and assumed to be 1.',
optional => 1,
default => 1,
},
'tfa-challenge' => {
type => 'string',
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) = @_;
my $username = $param->{username}; my $username = $param->{username};
$username .= "\@$param->{realm}" if $param->{realm}; $username .= "\@$param->{realm}" if $param->{realm};
$username = PVE::AccessControl::lookup_username($username); $username = PVE::AccessControl::lookup_username($username);
my $rpcenv = PVE::RPCEnvironment::get(); my $rpcenv = PVE::RPCEnvironment::get();
my $res; my $res;
eval { eval {
# test if user exists and is enabled # test if user exists and is enabled
$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,
} else { $username,
$res = create_ticket_do( $param->{password},
$rpcenv, $param->{otp},
$username, $param->{path},
$param->{password}, $param->{privs},
$param->{otp}, );
$param->{'tfa-challenge'}, } else {
); $res = create_ticket_do(
} $rpcenv,
}; $username,
if (my $err = $@) { $param->{password},
my $clientip = $rpcenv->get_client_ip() || ''; $param->{otp},
syslog('err', "authentication failure; rhost=$clientip user=$username msg=$err"); $param->{'tfa-challenge'},
# do not return any info to prevent user enumeration attacks );
die PVE::Exception->new("authentication failure\n", code => 401); }
} };
if (my $err = $@) {
my $clientip = $rpcenv->get_client_ip() || '';
syslog('err', "authentication failure; rhost=$clientip user=$username msg=$err");
# do not return any info to prevent user enumeration attacks
die PVE::Exception->new("authentication failure\n", code => 401);
}
$res->{cap} = $rpcenv->compute_api_permission($username) $res->{cap} = $rpcenv->compute_api_permission($username)
if !defined($res->{NeedTFA}); if !defined($res->{NeedTFA});
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};
} }
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.",
['userid-param', 'self'], check => [
[ 'and', 'or',
[ 'userid-param', 'Realm.AllocateUser'], ['userid-param', 'self'],
[ 'userid-group', ['User.Modify']] [
] 'and', ['userid-param', 'Realm.AllocateUser'],
], ['userid-group', ['User.Modify']],
],
],
}, },
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 change the regular user password allowtoken => 0, # we don't want tokens to change the regular user password
description => "Change user password.", description => "Change user password.",
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
userid => get_standard_option('userid-completed'), userid => get_standard_option('userid-completed'),
password => { password => {
description => "The new password.", description => "The new password.",
type => 'string', type => 'string',
minLength => 8, minLength => 8,
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 {
my ($param) = @_; my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get(); my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user(); my $authuser = $rpcenv->get_user();
my ($userid, $ruid, $realm) = $rpcenv->reauth_user_for_user_modification( my ($userid, $ruid, $realm) = $rpcenv->reauth_user_for_user_modification(
$authuser, $authuser,
$param->{userid}, $param->{userid},
$param->{'confirmation-password'}, $param->{'confirmation-password'},
'confirmation-password', 'confirmation-password',
); );
if ($authuser eq 'root@pam') { if ($authuser eq 'root@pam') {
# OK - root can change anything # OK - root can change anything
} else { } else {
if ($authuser eq $userid) { if ($authuser eq $userid) {
$rpcenv->check_user_enabled($userid); $rpcenv->check_user_enabled($userid);
# OK - each user can change their own password # OK - each user can change their own password
} else { } else {
# only root may change root password # only root may change root password
raise_perm_exc() if $userid eq 'root@pam'; raise_perm_exc() if $userid eq 'root@pam';
# do not allow to change system user passwords # do not allow to change system user passwords
raise_perm_exc() if $realm eq 'pam'; raise_perm_exc() if $realm eq 'pam';
} }
} }
PVE::AccessControl::domain_set_password($realm, $ruid, $param->{password}); PVE::AccessControl::domain_set_password($realm, $ruid, $param->{password});
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;
@ -408,12 +421,12 @@ sub get_u2f_instance {
# via the 'Host' header (in case a node has multiple hosts available). # via the 'Host' header (in case a node has multiple hosts available).
my $origin = $u2fconfig->{origin}; my $origin = $u2fconfig->{origin};
if (!defined($origin)) { if (!defined($origin)) {
$origin = $rpcenv->get_request_host(1); $origin = $rpcenv->get_request_host(1);
if ($origin) { if ($origin) {
$origin = "https://$origin"; $origin = "https://$origin";
} else { } else {
die "failed to figure out u2f origin\n"; die "failed to figure out u2f origin\n";
} }
} }
my $appid = $u2fconfig->{appid} // $origin; my $appid = $u2fconfig->{appid} // $origin;
@ -428,22 +441,22 @@ sub verify_user_tfa_config {
my ($type, $tfa_cfg, $value) = @_; my ($type, $tfa_cfg, $value) = @_;
if (!defined($type)) { if (!defined($type)) {
die "missing tfa 'type'\n"; die "missing tfa 'type'\n";
} }
if ($type ne 'oath') { if ($type ne 'oath') {
die "invalid type for custom tfa authentication\n"; die "invalid type for custom tfa authentication\n";
} }
my $secret = $tfa_cfg->{keys} my $secret = $tfa_cfg->{keys}
or die "missing TOTP secret\n"; or die "missing TOTP secret\n";
$tfa_cfg = $tfa_cfg->{config}; $tfa_cfg = $tfa_cfg->{config};
# Copy the hash to verify that we have no unexpected keys without modifying the original hash. # Copy the hash to verify that we have no unexpected keys without modifying the original hash.
$tfa_cfg = {%$tfa_cfg}; $tfa_cfg = {%$tfa_cfg};
# We can only verify 1 secret but oath_verify_otp allows multiple: # We can only verify 1 secret but oath_verify_otp allows multiple:
if (scalar(PVE::Tools::split_list($secret)) != 1) { if (scalar(PVE::Tools::split_list($secret)) != 1) {
die "only exactly one secret key allowed\n"; die "only exactly one secret key allowed\n";
} }
my $digits = delete($tfa_cfg->{digits}) // 6; my $digits = delete($tfa_cfg->{digits}) // 6;
@ -452,73 +465,77 @@ sub verify_user_tfa_config {
# my $algorithm = delete($tfa_cfg->{algorithm}) // 'sha1'; # my $algorithm = delete($tfa_cfg->{algorithm}) // 'sha1';
if (length(my $more = join(', ', keys %$tfa_cfg))) { if (length(my $more = join(', ', keys %$tfa_cfg))) {
die "unexpected tfa config keys: $more\n"; die "unexpected tfa config keys: $more\n";
} }
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"
user => 'all', . " have 'Sys.Audit' permission on /access.",
user => 'all',
}, },
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
userid => { userid => {
type => 'string', type => 'string',
description => "User ID or full API token ID", description => "User ID or full API token ID",
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(
description => "Only dump this specific path, not the whole tree.", 'acl-path',
optional => 1, {
}), description => "Only dump this specific path, not the whole tree.",
}, optional => 1,
},
),
},
}, },
returns => { returns => {
type => 'object', type => 'object',
description => 'Map of "path" => (Map of "privilege" => "propagate boolean").', description => 'Map of "path" => (Map of "privilege" => "propagate boolean").',
}, },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get(); my $rpcenv = PVE::RPCEnvironment::get();
my $authid = $rpcenv->get_user(); my $authid = $rpcenv->get_user();
my $userid = $param->{userid}; my $userid = $param->{userid};
$userid = $authid if !defined($userid); $userid = $authid if !defined($userid);
my ($user, $token) = PVE::AccessControl::split_tokenid($userid, 1); my ($user, $token) = PVE::AccessControl::split_tokenid($userid, 1);
my $check_self = $userid eq $authid; my $check_self = $userid eq $authid;
my $check_owned_token = defined($user) && $user eq $authid; my $check_owned_token = defined($user) && $user eq $authid;
if (!($check_self || $check_owned_token)) { if (!($check_self || $check_owned_token)) {
$rpcenv->check($rpcenv->get_user(), '/access', ['Sys.Audit']); $rpcenv->check($rpcenv->get_user(), '/access', ['Sys.Audit']);
} }
my $res; my $res;
if (my $path = $param->{path}) { if (my $path = $param->{path}) {
my $perms = $rpcenv->permissions($userid, $path); my $perms = $rpcenv->permissions($userid, $path);
if ($perms) { if ($perms) {
$res = { $path => $perms }; $res = { $path => $perms };
} else { } else {
$res = {}; $res = {};
} }
} else { } else {
$res = $rpcenv->get_effective_permissions($userid); $res = $rpcenv->get_effective_permissions($userid);
} }
return $res; return $res;
}}); },
});
1; 1;

View File

@ -23,15 +23,15 @@ my $map_remove_vanished = sub {
my ($opt, $delete_deprecated) = @_; my ($opt, $delete_deprecated) = @_;
if (!defined($opt->{'remove-vanished'}) && ($opt->{full} || $opt->{purge})) { if (!defined($opt->{'remove-vanished'}) && ($opt->{full} || $opt->{purge})) {
my $props = []; my $props = [];
push @$props, 'entry', 'properties' if $opt->{full}; push @$props, 'entry', 'properties' if $opt->{full};
push @$props, 'acl' if $opt->{purge}; push @$props, 'acl' if $opt->{purge};
$opt->{'remove-vanished'} = join(';', @$props); $opt->{'remove-vanished'} = join(';', @$props);
} }
if ($delete_deprecated) { if ($delete_deprecated) {
delete $opt->{full}; delete $opt->{full};
delete $opt->{purge}; delete $opt->{purge};
} }
return $opt; return $opt;
@ -48,315 +48,336 @@ 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 =>
user => 'world', "Anyone can access that, because we need that list for the login box (before the user is authenticated).",
user => 'world',
}, },
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => {}, properties => {},
}, },
returns => { returns => {
type => 'array', type => 'array',
items => { items => {
type => "object", type => "object",
properties => { properties => {
realm => { type => 'string' }, realm => { type => 'string' },
type => { type => 'string' }, type => { type => 'string' },
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 =>
type => 'string', "A comment. The GUI use this text when you select a domain (Realm) on the login window.",
optional => 1, type => 'string',
}, optional => 1,
}, },
}, },
links => [ { rel => 'child', href => "{realm}" } ], },
links => [{ rel => 'child', href => "{realm}" }],
}, },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
my $res = []; my $res = [];
my $cfg = cfs_read_file($domainconfigfile); my $cfg = cfs_read_file($domainconfigfile);
my $ids = $cfg->{ids}; my $ids = $cfg->{ids};
foreach my $realm (keys %$ids) { foreach my $realm (keys %$ids) {
my $d = $ids->{$realm}; my $d = $ids->{$realm};
my $entry = { realm => $realm, type => $d->{type} }; my $entry = { realm => $realm, type => $d->{type} };
$entry->{comment} = $d->{comment} if $d->{comment}; $entry->{comment} = $d->{comment} if $d->{comment};
$entry->{default} = 1 if $d->{default}; $entry->{default} = 1 if $d->{default};
if ($d->{tfa} && (my $tfa_cfg = PVE::Auth::Plugin::parse_tfa_config($d->{tfa}))) { if ($d->{tfa} && (my $tfa_cfg = PVE::Auth::Plugin::parse_tfa_config($d->{tfa}))) {
$entry->{tfa} = $tfa_cfg->{type}; $entry->{tfa} = $tfa_cfg->{type};
} }
push @$res, $entry; push @$res, $entry;
} }
return $res; return $res;
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'create', name => 'create',
protected => 1, protected => 1,
path => '', path => '',
method => 'POST', method => 'POST',
permissions => { permissions => {
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(
'check-connection' => { 0,
description => 'Check bind connection to the server.', {
type => 'boolean', 'check-connection' => {
optional => 1, description => 'Check bind connection to the server.',
default => 0, type => 'boolean',
}, optional => 1,
}), default => 0,
},
},
),
returns => { type => 'null' }, returns => { type => 'null' },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
# always extract, add it with hook # always extract, add it with hook
my $password = extract_param($param, 'password'); my $password = extract_param($param, 'password');
PVE::Auth::Plugin::lock_domain_config( PVE::Auth::Plugin::lock_domain_config(
sub { sub {
my $cfg = cfs_read_file($domainconfigfile); my $cfg = cfs_read_file($domainconfigfile);
my $ids = $cfg->{ids}; my $ids = $cfg->{ids};
my $realm = extract_param($param, 'realm'); my $realm = extract_param($param, 'realm');
my $type = $param->{type}; my $type = $param->{type};
my $check_connection = extract_param($param, 'check-connection'); my $check_connection = extract_param($param, 'check-connection');
die "domain '$realm' already exists\n" die "domain '$realm' already exists\n"
if $ids->{$realm}; if $ids->{$realm};
die "unable to use reserved name '$realm'\n" die "unable to use reserved name '$realm'\n"
if ($realm eq 'pam' || $realm eq 'pve'); if ($realm eq 'pam' || $realm eq 'pve');
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
if defined($check_connection) && !($type eq 'ldap' || $type eq 'ad'); "'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 ($type eq 'ad' || $type eq 'ldap') { if ($type eq 'ad' || $type eq 'ldap') {
$map_sync_default_options->($param, 1); $map_sync_default_options->($param, 1);
} }
my $plugin = PVE::Auth::Plugin->lookup($type); my $plugin = PVE::Auth::Plugin->lookup($type);
my $config = $plugin->check_config($realm, $param, 1, 1); my $config = $plugin->check_config($realm, $param, 1, 1);
if ($config->{default}) { if ($config->{default}) {
foreach my $r (keys %$ids) { foreach my $r (keys %$ids) {
delete $ids->{$r}->{default}; delete $ids->{$r}->{default};
} }
} }
$ids->{$realm} = $config; $ids->{$realm} = $config;
my $opts = $plugin->options(); my $opts = $plugin->options();
if (defined($password) && !defined($opts->{password})) { if (defined($password) && !defined($opts->{password})) {
$password = undef; $password = undef;
warn "ignoring password parameter"; warn "ignoring password parameter";
} }
$plugin->on_add_hook($realm, $config, password => $password); $plugin->on_add_hook($realm, $config, password => $password);
# Only for LDAP/AD, implied through the existence of the 'check-connection' param # Only for LDAP/AD, implied through the existence of the 'check-connection' param
$plugin->check_connection($realm, $config, password => $password) $plugin->check_connection($realm, $config, password => $password)
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',
permissions => { permissions => {
check => ['perm', '/access/realm', ['Realm.Allocate']], check => ['perm', '/access/realm', ['Realm.Allocate']],
}, },
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(
'check-connection' => { 0,
description => 'Check bind connection to the server.', {
type => 'boolean', 'check-connection' => {
optional => 1, description => 'Check bind connection to the server.',
default => 0, type => 'boolean',
}, optional => 1,
}), default => 0,
},
},
),
returns => { type => 'null' }, returns => { type => 'null' },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
# always extract, update in hook # always extract, update in hook
my $password = extract_param($param, 'password'); my $password = extract_param($param, 'password');
PVE::Auth::Plugin::lock_domain_config( PVE::Auth::Plugin::lock_domain_config(
sub { sub {
my $cfg = cfs_read_file($domainconfigfile); my $cfg = cfs_read_file($domainconfigfile);
my $ids = $cfg->{ids}; my $ids = $cfg->{ids};
my $digest = extract_param($param, 'digest'); my $digest = extract_param($param, 'digest');
PVE::SectionConfig::assert_if_modified($cfg, $digest); PVE::SectionConfig::assert_if_modified($cfg, $digest);
my $realm = extract_param($param, 'realm'); my $realm = extract_param($param, 'realm');
my $type = $ids->{$realm}->{type}; my $type = $ids->{$realm}->{type};
my $check_connection = extract_param($param, 'check-connection'); my $check_connection = extract_param($param, 'check-connection');
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
if defined($check_connection) && !($type eq 'ldap' || $type eq 'ad'); "'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');
my $delete_str = extract_param($param, 'delete'); my $delete_str = extract_param($param, 'delete');
die "no options specified\n" die "no options specified\n"
if !$delete_str && !scalar(keys %$param) && !defined($password); if !$delete_str && !scalar(keys %$param) && !defined($password);
my $delete_pw = 0; my $delete_pw = 0;
foreach my $opt (PVE::Tools::split_list($delete_str)) { foreach my $opt (PVE::Tools::split_list($delete_str)) {
delete $ids->{$realm}->{$opt}; delete $ids->{$realm}->{$opt};
$delete_pw = 1 if $opt eq 'password'; $delete_pw = 1 if $opt eq 'password';
} }
if ($type eq 'ad' || $type eq 'ldap') { if ($type eq 'ad' || $type eq 'ldap') {
$map_sync_default_options->($param, 1); $map_sync_default_options->($param, 1);
} }
my $plugin = PVE::Auth::Plugin->lookup($type); my $plugin = PVE::Auth::Plugin->lookup($type);
my $config = $plugin->check_config($realm, $param, 0, 1); my $config = $plugin->check_config($realm, $param, 0, 1);
if ($config->{default}) { if ($config->{default}) {
foreach my $r (keys %$ids) { foreach my $r (keys %$ids) {
delete $ids->{$r}->{default}; delete $ids->{$r}->{default};
} }
} }
foreach my $p (keys %$config) { foreach my $p (keys %$config) {
$ids->{$realm}->{$p} = $config->{$p}; $ids->{$realm}->{$p} = $config->{$p};
} }
my $opts = $plugin->options(); my $opts = $plugin->options();
if ($delete_pw || defined($password)) { if ($delete_pw || defined($password)) {
$plugin->on_update_hook($realm, $config, password => $password); $plugin->on_update_hook($realm, $config, password => $password);
} else { } else {
$plugin->on_update_hook($realm, $config); $plugin->on_update_hook($realm, $config);
} }
# Only for LDAP/AD, implied through the existence of the 'check-connection' param # Only for LDAP/AD, implied through the existence of the 'check-connection' param
$plugin->check_connection($realm, $ids->{$realm}, password => $password) $plugin->check_connection($realm, $ids->{$realm}, password => $password)
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',
description => "Get auth server configuration.", description => "Get auth server configuration.",
permissions => { permissions => {
check => ['perm', '/access/realm', ['Realm.Allocate', 'Sys.Audit'], any => 1], check => ['perm', '/access/realm', ['Realm.Allocate', 'Sys.Audit'], any => 1],
}, },
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
realm => get_standard_option('realm'), realm => get_standard_option('realm'),
}, },
}, },
returns => {}, returns => {},
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
my $cfg = cfs_read_file($domainconfigfile); my $cfg = cfs_read_file($domainconfigfile);
my $realm = $param->{realm}; my $realm = $param->{realm};
my $data = $cfg->{ids}->{$realm}; my $data = $cfg->{ids}->{$realm};
die "domain '$realm' does not exist\n" if !$data; die "domain '$realm' does not exist\n" if !$data;
my $type = $data->{type}; my $type = $data->{type};
if ($type eq 'ad' || $type eq 'ldap') { if ($type eq 'ad' || $type eq 'ldap') {
$map_sync_default_options->($data); $map_sync_default_options->($data);
} }
$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',
permissions => { permissions => {
check => ['perm', '/access/realm', ['Realm.Allocate']], check => ['perm', '/access/realm', ['Realm.Allocate']],
}, },
description => "Delete an authentication server.", description => "Delete an authentication server.",
protected => 1, protected => 1,
parameters => { parameters => {
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 {
my ($param) = @_; my ($param) = @_;
PVE::Auth::Plugin::lock_domain_config( PVE::Auth::Plugin::lock_domain_config(
sub { sub {
my $cfg = cfs_read_file($domainconfigfile); my $cfg = cfs_read_file($domainconfigfile);
my $ids = $cfg->{ids}; my $ids = $cfg->{ids};
my $realm = $param->{realm}; my $realm = $param->{realm};
die "authentication domain '$realm' does not exist\n" if !$ids->{$realm}; die "authentication domain '$realm' does not exist\n" if !$ids->{$realm};
my $plugin = PVE::Auth::Plugin->lookup($ids->{$realm}->{type}); my $plugin = PVE::Auth::Plugin->lookup($ids->{$realm}->{type});
$plugin->on_delete_hook($realm, $ids->{$realm}); $plugin->on_delete_hook($realm, $ids->{$realm});
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) = @_;
if (defined(my $vanished = $opts->{'remove-vanished'})) { if (defined(my $vanished = $opts->{'remove-vanished'})) {
print "syncing users (remove-vanished opts: $vanished)\n"; print "syncing users (remove-vanished opts: $vanished)\n";
} else { } else {
print "syncing users\n"; print "syncing users\n";
} }
$usercfg->{users} = {} if !defined($usercfg->{users}); $usercfg->{users} = {} if !defined($usercfg->{users});
@ -365,43 +386,43 @@ my $update_users = sub {
print "deleting outdated existing users first\n" if $to_remove->{entry}; print "deleting outdated existing users first\n" if $to_remove->{entry};
foreach my $userid (sort keys %$users) { foreach my $userid (sort keys %$users) {
next if $userid !~ m/\@$realm$/; next if $userid !~ m/\@$realm$/;
next if defined($synced_users->{$userid}); next if defined($synced_users->{$userid});
if ($to_remove->{entry}) { if ($to_remove->{entry}) {
print "remove user '$userid'\n"; print "remove user '$userid'\n";
delete $users->{$userid}; delete $users->{$userid};
} }
if ($to_remove->{acl}) { if ($to_remove->{acl}) {
print "purge users '$userid' ACL entries\n"; print "purge users '$userid' ACL entries\n";
PVE::AccessControl::delete_user_acl($userid, $usercfg); PVE::AccessControl::delete_user_acl($userid, $usercfg);
} }
} }
foreach my $userid (sort keys %$synced_users) { foreach my $userid (sort keys %$synced_users) {
my $synced_user = $synced_users->{$userid} // {}; my $synced_user = $synced_users->{$userid} // {};
my $olduser = $users->{$userid}; my $olduser = $users->{$userid};
if ($to_remove->{properties} || !defined($olduser)) { if ($to_remove->{properties} || !defined($olduser)) {
# we use the synced user, but want to keep some properties on update # we use the synced user, but want to keep some properties on update
if (defined($olduser)) { if (defined($olduser)) {
print "overwriting user '$userid'\n"; print "overwriting user '$userid'\n";
} else { } else {
$olduser = {}; $olduser = {};
print "adding user '$userid'\n"; print "adding user '$userid'\n";
} }
my $user = $users->{$userid} = $synced_user; my $user = $users->{$userid} = $synced_user;
my $enabled = $olduser->{enable} // $opts->{'enable-new'}; my $enabled = $olduser->{enable} // $opts->{'enable-new'};
$user->{enable} = $enabled if defined($enabled); $user->{enable} = $enabled if defined($enabled);
$user->{tokens} = $olduser->{tokens} if defined($olduser->{tokens}); $user->{tokens} = $olduser->{tokens} if defined($olduser->{tokens});
} else { } else {
foreach my $attr (keys %$synced_user) { foreach my $attr (keys %$synced_user) {
$olduser->{$attr} = $synced_user->{$attr}; $olduser->{$attr} = $synced_user->{$attr};
} }
print "updating user '$userid'\n"; print "updating user '$userid'\n";
} }
} }
}; };
@ -409,9 +430,9 @@ my $update_groups = sub {
my ($usercfg, $realm, $synced_groups, $opts) = @_; my ($usercfg, $realm, $synced_groups, $opts) = @_;
if (defined(my $vanished = $opts->{'remove-vanished'})) { if (defined(my $vanished = $opts->{'remove-vanished'})) {
print "syncing groups (remove-vanished opts: $vanished)\n"; print "syncing groups (remove-vanished opts: $vanished)\n";
} else { } else {
print "syncing groups\n"; print "syncing groups\n";
} }
$usercfg->{groups} = {} if !defined($usercfg->{groups}); $usercfg->{groups} = {} if !defined($usercfg->{groups});
@ -420,36 +441,36 @@ my $update_groups = sub {
print "deleting outdated existing groups first\n" if $to_remove->{entry}; print "deleting outdated existing groups first\n" if $to_remove->{entry};
foreach my $groupid (sort keys %$groups) { foreach my $groupid (sort keys %$groups) {
next if $groupid !~ m/\-$realm$/; next if $groupid !~ m/\-$realm$/;
next if defined($synced_groups->{$groupid}); next if defined($synced_groups->{$groupid});
if ($to_remove->{entry}) { if ($to_remove->{entry}) {
print "remove group '$groupid'\n"; print "remove group '$groupid'\n";
delete $groups->{$groupid}; delete $groups->{$groupid};
} }
if ($to_remove->{acl}) { if ($to_remove->{acl}) {
print "purge groups '$groupid' ACL entries\n"; print "purge groups '$groupid' ACL entries\n";
PVE::AccessControl::delete_group_acl($groupid, $usercfg); PVE::AccessControl::delete_group_acl($groupid, $usercfg);
} }
} }
foreach my $groupid (sort keys %$synced_groups) { foreach my $groupid (sort keys %$synced_groups) {
my $synced_group = $synced_groups->{$groupid}; my $synced_group = $synced_groups->{$groupid};
my $oldgroup = $groups->{$groupid}; my $oldgroup = $groups->{$groupid};
if ($to_remove->{properties} || !defined($oldgroup)) { if ($to_remove->{properties} || !defined($oldgroup)) {
if (defined($oldgroup)) { if (defined($oldgroup)) {
print "overwriting group '$groupid'\n"; print "overwriting group '$groupid'\n";
} else { } else {
print "adding group '$groupid'\n"; print "adding group '$groupid'\n";
} }
$groups->{$groupid} = $synced_group; $groups->{$groupid} = $synced_group;
} else { } else {
foreach my $attr (keys %$synced_group) { foreach my $attr (keys %$synced_group) {
$oldgroup->{$attr} = $synced_group->{$attr}; $oldgroup->{$attr} = $synced_group->{$attr};
} }
print "updating group '$groupid'\n"; print "updating group '$groupid'\n";
} }
} }
}; };
@ -460,116 +481,126 @@ my $parse_sync_opts = sub {
my $cfg_defaults = {}; my $cfg_defaults = {};
if (defined(my $cfg_opts = $realmconfig->{'sync-defaults-options'})) { if (defined(my $cfg_opts = $realmconfig->{'sync-defaults-options'})) {
$cfg_defaults = PVE::JSONSchema::parse_property_string($sync_opts_fmt, $cfg_opts); $cfg_defaults = PVE::JSONSchema::parse_property_string($sync_opts_fmt, $cfg_opts);
} }
my $res = {}; my $res = {};
for my $opt (sort keys %$sync_opts_fmt) { for my $opt (sort keys %$sync_opts_fmt) {
my $fmt = $sync_opts_fmt->{$opt}; my $fmt = $sync_opts_fmt->{$opt};
$res->{$opt} = $param->{$opt} // $cfg_defaults->{$opt} // $fmt->{default}; $res->{$opt} = $param->{$opt} // $cfg_defaults->{$opt} // $fmt->{default};
} }
$map_remove_vanished->($res, 1); $map_remove_vanished->($res, 1);
# 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 => [
['perm', '/access/realm/{realm}', ['Realm.AllocateUser']], 'and',
['perm', '/access/groups', ['User.Modify']], ['perm', '/access/realm/{realm}', ['Realm.AllocateUser']],
], ['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 => get_standard_option('realm'), 'realm-sync-options',
'dry-run' => { {
description => "If set, does not write anything.", realm => get_standard_option('realm'),
type => 'boolean', 'dry-run' => {
optional => 1, description => "If set, does not write anything.",
default => 0, type => 'boolean',
}, optional => 1,
}), default => 0,
},
},
),
}, },
returns => { returns => {
description => 'Worker Task-UPID', description => 'Worker Task-UPID',
type => 'string' type => 'string',
}, },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get(); my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user(); my $authuser = $rpcenv->get_user();
my $dry_run = extract_param($param, 'dry-run'); my $dry_run = extract_param($param, 'dry-run');
my $realm = $param->{realm}; my $realm = $param->{realm};
my $cfg = cfs_read_file($domainconfigfile); my $cfg = cfs_read_file($domainconfigfile);
my $realmconfig = $cfg->{ids}->{$realm}; my $realmconfig = $cfg->{ids}->{$realm};
raise_param_exc({ 'realm' => 'Realm does not exist.' }) if !defined($realmconfig); raise_param_exc({ 'realm' => 'Realm does not exist.' }) if !defined($realmconfig);
my $type = $realmconfig->{type}; my $type = $realmconfig->{type};
if ($type ne 'ldap' && $type ne 'ad') { if ($type ne 'ldap' && $type ne 'ad') {
die "Cannot sync realm type '$type'! Only LDAP/AD realms can be synced.\n"; die "Cannot sync realm type '$type'! Only LDAP/AD realms can be synced.\n";
} }
my $opts = $parse_sync_opts->($param, $realmconfig); # can throw up my $opts = $parse_sync_opts->($param, $realmconfig); # can throw up
my $scope = $opts->{scope}; my $scope = $opts->{scope};
my $whatstring = $scope eq 'both' ? "users and groups" : $scope; my $whatstring = $scope eq 'both' ? "users and groups" : $scope;
my $plugin = PVE::Auth::Plugin->lookup($type); my $plugin = PVE::Auth::Plugin->lookup($type);
my $worker = sub { my $worker = sub {
print "(dry test run) " if $dry_run; print "(dry test run) " if $dry_run;
print "starting sync for realm $realm\n"; print "starting sync for realm $realm\n";
my ($synced_users, $dnmap) = $plugin->get_users($realmconfig, $realm); my ($synced_users, $dnmap) = $plugin->get_users($realmconfig, $realm);
my $synced_groups = {}; my $synced_groups = {};
if ($scope eq 'groups' || $scope eq 'both') { if ($scope eq 'groups' || $scope eq 'both') {
$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(
my $usercfg = cfs_read_file("user.cfg"); sub {
print "got data from server, updating $whatstring\n"; my $usercfg = cfs_read_file("user.cfg");
print "got data from server, updating $whatstring\n";
if ($scope eq 'users' || $scope eq 'both') { if ($scope eq 'users' || $scope eq 'both') {
$update_users->($usercfg, $realm, $synced_users, $opts); $update_users->($usercfg, $realm, $synced_users, $opts);
} }
if ($scope eq 'groups' || $scope eq 'both') { if ($scope eq 'groups' || $scope eq 'both') {
$update_groups->($usercfg, $realm, $synced_groups, $opts); $update_groups->($usercfg, $realm, $synced_groups, $opts);
} }
if ($dry_run) { if ($dry_run) {
print "\nNOTE: Dry test run, changes were NOT written to the configuration.\n"; print
return; "\nNOTE: Dry test run, changes were NOT written to the configuration.\n";
} return;
cfs_write_file("user.cfg", $usercfg); }
print "successfully updated $whatstring configuration\n"; cfs_write_file("user.cfg", $usercfg);
}, "syncing $whatstring failed"); print "successfully updated $whatstring configuration\n";
}; },
"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,231 +10,246 @@ 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(
type => 'string', 'group-id',
format => 'pve-groupid', {
completion => \&PVE::AccessControl::complete_group, type => 'string',
}); format => 'pve-groupid',
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 =>
user => 'all', "The returned list is restricted to groups where you have 'User.Modify', 'Sys.Audit' or 'Group.Allocate' permissions on /access/groups/<group>.",
user => 'all',
}, },
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => {}, properties => {},
}, },
returns => { returns => {
type => 'array', type => 'array',
items => { items => {
type => "object", type => "object",
properties => { properties => {
groupid => get_standard_option('group-id'), groupid => get_standard_option('group-id'),
comment => get_standard_option('group-comment'), comment => get_standard_option('group-comment'),
users => { users => {
type => 'string', type => 'string',
format => 'pve-userid-list', format => 'pve-userid-list',
optional => 1, optional => 1,
description => 'list of users which form this group', description => 'list of users which form this group',
}, },
}, },
}, },
links => [ { rel => 'child', href => "{groupid}" } ], links => [{ rel => 'child', href => "{groupid}" }],
}, },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
my $res = [];
my $rpcenv = PVE::RPCEnvironment::get(); my $res = [];
my $usercfg = cfs_read_file("user.cfg");
my $authuser = $rpcenv->get_user();
my $privs = [ 'User.Modify', 'Sys.Audit', 'Group.Allocate']; my $rpcenv = PVE::RPCEnvironment::get();
my $usercfg = cfs_read_file("user.cfg");
my $authuser = $rpcenv->get_user();
foreach my $group (keys %{$usercfg->{groups}}) { my $privs = ['User.Modify', 'Sys.Audit', 'Group.Allocate'];
next if !$rpcenv->check_any($authuser, "/access/groups/$group", $privs, 1);
my $data = $usercfg->{groups}->{$group};
my $entry = { groupid => $group };
$entry->{comment} = $data->{comment} if defined($data->{comment});
$entry->{users} = join (',', sort keys %{$data->{users}}) if defined($data->{users});
push @$res, $entry;
}
return $res; foreach my $group (keys %{ $usercfg->{groups} }) {
}}); next if !$rpcenv->check_any($authuser, "/access/groups/$group", $privs, 1);
my $data = $usercfg->{groups}->{$group};
my $entry = { groupid => $group };
$entry->{comment} = $data->{comment} if defined($data->{comment});
$entry->{users} = join(',', sort keys %{ $data->{users} })
if defined($data->{users});
push @$res, $entry;
}
__PACKAGE__->register_method ({ return $res;
name => 'create_group', },
});
__PACKAGE__->register_method({
name => 'create_group',
protected => 1, protected => 1,
path => '', path => '',
method => 'POST', method => 'POST',
permissions => { permissions => {
check => ['perm', '/access/groups', ['Group.Allocate']], check => ['perm', '/access/groups', ['Group.Allocate']],
}, },
description => "Create new group.", description => "Create new group.",
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
groupid => get_standard_option('group-id'), groupid => get_standard_option('group-id'),
comment => get_standard_option('group-comment'), comment => get_standard_option('group-comment'),
}, },
}, },
returns => { type => 'null' }, returns => { type => 'null' },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
PVE::AccessControl::lock_user_config( PVE::AccessControl::lock_user_config(
sub { sub {
my $usercfg = cfs_read_file("user.cfg");
my $group = $param->{groupid}; my $usercfg = cfs_read_file("user.cfg");
die "group '$group' already exists\n"
if $usercfg->{groups}->{$group};
$usercfg->{groups}->{$group} = { users => {} }; my $group = $param->{groupid};
$usercfg->{groups}->{$group}->{comment} = $param->{comment} if $param->{comment}; die "group '$group' already exists\n"
if $usercfg->{groups}->{$group};
$usercfg->{groups}->{$group} = { users => {} };
cfs_write_file("user.cfg", $usercfg);
}, "create group failed");
return undef; $usercfg->{groups}->{$group}->{comment} = $param->{comment}
}}); if $param->{comment};
__PACKAGE__->register_method ({ cfs_write_file("user.cfg", $usercfg);
name => 'update_group', },
"create group failed",
);
return undef;
},
});
__PACKAGE__->register_method({
name => 'update_group',
protected => 1, protected => 1,
path => '{groupid}', path => '{groupid}',
method => 'PUT', method => 'PUT',
permissions => { permissions => {
check => ['perm', '/access/groups', ['Group.Allocate']], check => ['perm', '/access/groups', ['Group.Allocate']],
}, },
description => "Update group data.", description => "Update group data.",
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
groupid => get_standard_option('group-id'), groupid => get_standard_option('group-id'),
comment => get_standard_option('group-comment'), comment => get_standard_option('group-comment'),
}, },
}, },
returns => { type => 'null' }, returns => { type => 'null' },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
PVE::AccessControl::lock_user_config( PVE::AccessControl::lock_user_config(
sub { sub {
my $usercfg = cfs_read_file("user.cfg");
my $group = $param->{groupid}; my $usercfg = cfs_read_file("user.cfg");
my $data = $usercfg->{groups}->{$group};
die "group '$group' does not exist\n" my $group = $param->{groupid};
if !$data;
$data->{comment} = $param->{comment} if defined($param->{comment}); my $data = $usercfg->{groups}->{$group};
cfs_write_file("user.cfg", $usercfg);
}, "update group failed");
return undef; die "group '$group' does not exist\n"
}}); if !$data;
__PACKAGE__->register_method ({ $data->{comment} = $param->{comment} if defined($param->{comment});
name => 'read_group',
path => '{groupid}', cfs_write_file("user.cfg", $usercfg);
},
"update group failed",
);
return undef;
},
});
__PACKAGE__->register_method({
name => 'read_group',
path => '{groupid}',
method => 'GET', method => 'GET',
permissions => { permissions => {
check => ['perm', '/access/groups', ['Sys.Audit', 'Group.Allocate'], any => 1], check => ['perm', '/access/groups', ['Sys.Audit', 'Group.Allocate'], any => 1],
}, },
description => "Get group configuration.", description => "Get group configuration.",
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
groupid => get_standard_option('group-id'), groupid => get_standard_option('group-id'),
}, },
}, },
returns => { returns => {
type => "object", type => "object",
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
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'),
}, },
}, },
}, },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
my $group = $param->{groupid}; my $group = $param->{groupid};
my $usercfg = cfs_read_file("user.cfg"); my $usercfg = cfs_read_file("user.cfg");
my $data = $usercfg->{groups}->{$group};
die "group '$group' does not exist\n" if !$data; my $data = $usercfg->{groups}->{$group};
my $members = $data->{users} ? [ keys %{$data->{users}} ] : []; die "group '$group' does not exist\n" if !$data;
my $res = { members => $members }; my $members = $data->{users} ? [keys %{ $data->{users} }] : [];
$res->{comment} = $data->{comment} if defined($data->{comment}); my $res = { members => $members };
return $res; $res->{comment} = $data->{comment} if defined($data->{comment});
}});
return $res;
},
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'delete_group', name => 'delete_group',
protected => 1, protected => 1,
path => '{groupid}', path => '{groupid}',
method => 'DELETE', method => 'DELETE',
permissions => { permissions => {
check => ['perm', '/access/groups', ['Group.Allocate']], check => ['perm', '/access/groups', ['Group.Allocate']],
}, },
description => "Delete group.", description => "Delete group.",
parameters => { parameters => {
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 {
my ($param) = @_; my ($param) = @_;
PVE::AccessControl::lock_user_config( PVE::AccessControl::lock_user_config(
sub { sub {
my $usercfg = cfs_read_file("user.cfg"); my $usercfg = cfs_read_file("user.cfg");
my $group = $param->{groupid}; my $group = $param->{groupid};
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});
PVE::AccessControl::delete_group_acl($group, $usercfg); delete($usercfg->{groups}->{$group});
cfs_write_file("user.cfg", $usercfg); PVE::AccessControl::delete_group_acl($group, $usercfg);
}, "delete group failed");
cfs_write_file("user.cfg", $usercfg);
return undef; },
}}); "delete group failed",
);
return undef;
},
});
1; 1;

View File

@ -21,97 +21,100 @@ my $get_cluster_last_run = sub {
die "error on getting state for '$jobid': $@\n" if $@; die "error on getting state for '$jobid': $@\n" if $@;
if (my $upid = $state->{upid}) { if (my $upid = $state->{upid}) {
if (my $decoded = PVE::Tools::upid_decode($upid)) { if (my $decoded = PVE::Tools::upid_decode($upid)) {
return $decoded->{starttime}; return $decoded->{starttime};
} }
} else { } else {
return $state->{time}; return $state->{time};
} }
return undef; return undef;
}; };
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'syncjob_index', name => 'syncjob_index',
path => '', path => '',
method => 'GET', method => 'GET',
description => "List configured realm-sync-jobs.", description => "List configured realm-sync-jobs.",
permissions => { permissions => {
check => ['perm', '/', ['Sys.Audit']], check => ['perm', '/', ['Sys.Audit']],
}, },
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => {}, properties => {},
}, },
returns => { returns => {
type => 'array', type => 'array',
items => { items => {
type => "object", type => "object",
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.",
type => 'boolean', type => 'boolean',
}, },
comment => { comment => {
description => "A comment for the job.", description => "A comment for the job.",
type => 'string', type => 'string',
optional => 1, optional => 1,
}, },
schedule => { schedule => {
description => "The configured sync schedule.", description => "The configured sync schedule.",
type => 'string', type => 'string',
}, },
realm => get_standard_option('realm'), realm => get_standard_option('realm'),
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 =>
type => 'integer', "Last execution time of the job in seconds since the beginning of the UNIX epoch",
optional => 1, type => 'integer',
}, optional => 1,
'next-run' => { },
description => "Next planned execution time of the job in seconds since the beginning of the UNIX epoch.", 'next-run' => {
type => 'integer', description =>
optional => 1, "Next planned execution time of the job in seconds since the beginning of the UNIX epoch.",
}, type => 'integer',
}, optional => 1,
}, },
links => [ { rel => 'child', href => "{id}" } ], },
},
links => [{ rel => 'child', href => "{id}" }],
}, },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get(); my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user(); my $user = $rpcenv->get_user();
my $jobs_data = cfs_read_file('jobs.cfg'); my $jobs_data = cfs_read_file('jobs.cfg');
my $order = $jobs_data->{order}; my $order = $jobs_data->{order};
my $jobs = $jobs_data->{ids}; my $jobs = $jobs_data->{ids};
my $res = []; my $res = [];
for my $jobid (sort { $order->{$a} <=> $order->{$b} } keys %$jobs) { for my $jobid (sort { $order->{$a} <=> $order->{$b} } keys %$jobs) {
my $job = $jobs->{$jobid}; my $job = $jobs->{$jobid};
next if $job->{type} ne 'realm-sync'; next if $job->{type} ne 'realm-sync';
$job->{id} = $jobid; $job->{id} = $jobid;
if (my $schedule = $job->{schedule}) { if (my $schedule = $job->{schedule}) {
$job->{'last-run'} = eval { $get_cluster_last_run->($jobid) }; $job->{'last-run'} = eval { $get_cluster_last_run->($jobid) };
my $last_run = $job->{'last-run'} // time(); # current time as fallback my $last_run = $job->{'last-run'} // time(); # current time as fallback
my $calendar_event = Proxmox::RS::CalendarEvent->new($schedule); my $calendar_event = Proxmox::RS::CalendarEvent->new($schedule);
my $next_run = $calendar_event->compute_next_event($last_run); my $next_run = $calendar_event->compute_next_event($last_run);
$job->{'next-run'} = $next_run if defined($next_run); $job->{'next-run'} = $next_run if defined($next_run);
} }
push @$res, $job; push @$res, $job;
} }
return $res; return $res;
}}); },
});
__PACKAGE__->register_method({ __PACKAGE__->register_method({
name => 'read_job', name => 'read_job',
@ -119,31 +122,32 @@ __PACKAGE__->register_method({
method => 'GET', method => 'GET',
description => "Read realm-sync job definition.", description => "Read realm-sync job definition.",
permissions => { permissions => {
check => ['perm', '/', ['Sys.Audit']], check => ['perm', '/', ['Sys.Audit']],
}, },
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
id => { id => {
type => 'string', type => 'string',
format => 'pve-configid', format => 'pve-configid',
}, },
}, },
}, },
returns => { returns => {
type => 'object', type => 'object',
}, },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
my $jobs = cfs_read_file('jobs.cfg'); my $jobs = cfs_read_file('jobs.cfg');
my $id = $param->{id}; my $id = $param->{id};
my $job = $jobs->{ids}->{$id}; my $job = $jobs->{ids}->{$id};
return $job if $job && $job->{type} eq 'realm-sync'; return $job if $job && $job->{type} eq 'realm-sync';
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',
@ -152,47 +156,53 @@ __PACKAGE__->register_method({
protected => 1, protected => 1,
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 => [
['perm', '/access/realm/{realm}', ['Realm.AllocateUser']], 'and',
['perm', '/access/groups', ['User.Modify']], ['perm', '/access/realm/{realm}', ['Realm.AllocateUser']],
], ['perm', '/access/groups', ['User.Modify']],
],
}, },
parameters => PVE::Jobs::RealmSync->createSchema(), parameters => PVE::Jobs::RealmSync->createSchema(),
returns => { type => 'null' }, returns => { type => 'null' },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
my $id = extract_param($param, 'id'); my $id = extract_param($param, 'id');
cfs_lock_file('jobs.cfg', undef, sub { cfs_lock_file(
my $data = cfs_read_file('jobs.cfg'); 'jobs.cfg',
undef,
sub {
my $data = cfs_read_file('jobs.cfg');
die "Job '$id' already exists\n" die "Job '$id' already exists\n"
if $data->{ids}->{$id}; if $data->{ids}->{$id};
my $plugin = PVE::Job::Registry->lookup('realm-sync'); my $plugin = PVE::Job::Registry->lookup('realm-sync');
my $opts = $plugin->check_config($id, $param, 1, 1); my $opts = $plugin->check_config($id, $param, 1, 1);
my $realm = $opts->{realm}; my $realm = $opts->{realm};
my $cfg = cfs_read_file('domains.cfg'); my $cfg = cfs_read_file('domains.cfg');
raise_param_exc({ realm => "No such realm '$realm'" }) raise_param_exc({ realm => "No such realm '$realm'" })
if !defined($cfg->{ids}->{$realm}); if !defined($cfg->{ids}->{$realm});
my $realm_type = $cfg->{ids}->{$realm}->{type}; my $realm_type = $cfg->{ids}->{$realm}->{type};
raise_param_exc({ realm => "Only LDAP/AD realms can be synced." }) raise_param_exc({ realm => "Only LDAP/AD realms can be synced." })
if $realm_type ne 'ldap' && $realm_type ne 'ad'; if $realm_type ne 'ldap' && $realm_type ne 'ad';
$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',
@ -201,45 +211,50 @@ __PACKAGE__->register_method({
protected => 1, protected => 1,
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 => [
['perm', '/access/realm/{realm}', ['Realm.AllocateUser']], 'and',
['perm', '/access/groups', ['User.Modify']], ['perm', '/access/realm/{realm}', ['Realm.AllocateUser']],
], ['perm', '/access/groups', ['User.Modify']],
],
}, },
parameters => PVE::Jobs::RealmSync->updateSchema(), parameters => PVE::Jobs::RealmSync->updateSchema(),
returns => { type => 'null' }, returns => { type => 'null' },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
my $id = extract_param($param, 'id'); my $id = extract_param($param, 'id');
my $delete = extract_param($param, 'delete'); my $delete = extract_param($param, 'delete');
$delete = [PVE::Tools::split_list($delete)] if $delete; $delete = [PVE::Tools::split_list($delete)] if $delete;
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(
my $jobs = cfs_read_file('jobs.cfg'); 'jobs.cfg',
undef,
sub {
my $jobs = cfs_read_file('jobs.cfg');
my $plugin = PVE::Job::Registry->lookup('realm-sync'); my $plugin = PVE::Job::Registry->lookup('realm-sync');
my $opts = $plugin->check_config($id, $param, 0, 1); my $opts = $plugin->check_config($id, $param, 0, 1);
my $job = $jobs->{ids}->{$id}; my $job = $jobs->{ids}->{$id};
die "no such realm-sync job\n" if !$job || $job->{type} ne 'realm-sync'; die "no such realm-sync job\n" if !$job || $job->{type} ne 'realm-sync';
my $options = $plugin->options(); my $options = $plugin->options();
PVE::SectionConfig::delete_from_config($job, $options, $opts, $delete); PVE::SectionConfig::delete_from_config($job, $options, $opts, $delete);
$job->{$_} = $param->{$_} for keys $param->%*; $job->{$_} = $param->{$_} for keys $param->%*;
cfs_write_file('jobs.cfg', $jobs); cfs_write_file('jobs.cfg', $jobs);
return;
});
die "$@" if ($@);
}});
return;
},
);
die "$@" if ($@);
},
});
__PACKAGE__->register_method({ __PACKAGE__->register_method({
name => 'delete_job', name => 'delete_job',
@ -247,38 +262,46 @@ __PACKAGE__->register_method({
method => 'DELETE', method => 'DELETE',
description => "Delete realm-sync job definition.", description => "Delete realm-sync job definition.",
permissions => { permissions => {
check => ['perm', '/', ['Sys.Modify']], check => ['perm', '/', ['Sys.Modify']],
}, },
protected => 1, protected => 1,
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
id => { id => {
type => 'string', type => 'string',
format => 'pve-configid', format => 'pve-configid',
}, },
}, },
}, },
returns => { type => 'null' }, returns => { type => 'null' },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
my $id = $param->{id}; my $id = $param->{id};
cfs_lock_file('jobs.cfg', undef, sub { cfs_lock_file(
my $jobs = cfs_read_file('jobs.cfg'); 'jobs.cfg',
undef,
sub {
my $jobs = cfs_read_file('jobs.cfg');
if (!defined($jobs->{ids}->{$id}) || $jobs->{ids}->{$id}->{type} ne 'realm-sync') { if (
raise_param_exc({ id => "No such job '$id'" }); !defined($jobs->{ids}->{$id})
} || $jobs->{ids}->{$id}->{type} ne 'realm-sync'
delete $jobs->{ids}->{$id}; ) {
raise_param_exc({ id => "No such job '$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

@ -33,304 +33,327 @@ my $lookup_openid_auth = sub {
die "wrong realm type ($config->{type} != openid)\n" if $config->{type} ne "openid"; die "wrong realm type ($config->{type} != openid)\n" if $config->{type} ne "openid";
my $openid_config = { my $openid_config = {
issuer_url => $config->{'issuer-url'}, issuer_url => $config->{'issuer-url'},
client_id => $config->{'client-id'}, client_id => $config->{'client-id'},
client_key => $config->{'client-key'}, client_key => $config->{'client-key'},
}; };
$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',
description => "Directory index.", description => "Directory index.",
permissions => { permissions => {
user => 'all', user => 'all',
}, },
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => {}, properties => {},
}, },
returns => { returns => {
type => 'array', type => 'array',
items => { items => {
type => "object", type => "object",
properties => { properties => {
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',
protected => 1, protected => 1,
description => "Get the OpenId Authorization Url for the specified realm.", description => "Get the OpenId Authorization Url for the specified realm.",
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
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 =>
type => 'string', "Redirection Url. The client should set this to the used server url (location.origin).",
maxLength => 255, type => 'string',
}, maxLength => 255,
}, },
},
}, },
returns => { returns => {
type => "string", type => "string",
description => "Redirection URL.", description => "Redirection URL.",
}, },
permissions => { user => 'world' }, permissions => { user => 'world' },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
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 = extract_param($param, 'realm'); my $realm = extract_param($param, 'realm');
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',
protected => 1, protected => 1,
description => " Verify OpenID authorization code and create a ticket.", description => " Verify OpenID authorization code and create a ticket.",
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
'state' => { 'state' => {
description => "OpenId state.", description => "OpenId state.",
type => 'string', type => 'string',
maxLength => 1024, maxLength => 1024,
}, },
code => { code => {
description => "OpenId authorization code.", description => "OpenId authorization code.",
type => 'string', type => 'string',
maxLength => 4096, maxLength => 4096,
}, },
'redirect-url' => { 'redirect-url' => {
description => "Redirection Url. The client should set this to the used server url (location.origin).", description =>
type => 'string', "Redirection Url. The client should set this to the used server url (location.origin).",
maxLength => 255, type => 'string',
}, maxLength => 255,
}, },
},
}, },
returns => { returns => {
properties => { properties => {
username => { type => 'string' }, username => { type => 'string' },
ticket => { type => 'string' }, ticket => { type => 'string' },
CSRFPreventionToken => { type => 'string' }, CSRFPreventionToken => { type => 'string' },
cap => { type => 'object' }, # computed api permissions cap => { type => 'object' }, # computed api permissions
clustername => { type => 'string', optional => 1 }, clustername => { type => 'string', optional => 1 },
}, },
}, },
permissions => { user => 'world' }, permissions => { user => 'world' },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get(); my $rpcenv = PVE::RPCEnvironment::get();
my $res; my $res;
eval { eval {
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');
my ($config, $openid) = $lookup_openid_auth->($realm, $redirect_url); my ($config, $openid) = $lookup_openid_auth->($realm, $redirect_url);
my $info = $openid->verify_authorization_code( my $info = $openid->verify_authorization_code(
$param->{code}, $param->{code},
$private_auth_state, $private_auth_state,
$config->{'query-userinfo'} // 1, $config->{'query-userinfo'} // 1,
); );
my $subject = $info->{'sub'}; my $subject = $info->{'sub'};
my $unique_name; my $unique_name;
my $user_attr = $config->{'username-claim'} // 'sub'; my $user_attr = $config->{'username-claim'} // 'sub';
if (defined($info->{$user_attr})) { if (defined($info->{$user_attr})) {
$unique_name = $info->{$user_attr}; $unique_name = $info->{$user_attr};
} elsif ($user_attr eq 'subject') { # stay compat with old versions } elsif ($user_attr eq 'subject') { # stay compat with old versions
$unique_name = $subject; $unique_name = $subject;
} elsif ($user_attr eq 'username') { # stay compat with old versions } elsif ($user_attr eq 'username') { # stay compat with old versions
my $username = $info->{'preferred_username'}; my $username = $info->{'preferred_username'};
die "missing claim 'preferred_username'\n" if !defined($username); die "missing claim 'preferred_username'\n" if !defined($username);
$unique_name = $username; $unique_name = $username;
} else { } else {
# neither the attr nor fallback are defined in info.. # neither the attr nor fallback are defined in info..
die "missing configured claim '$user_attr' in returned info object\n"; die "missing configured claim '$user_attr' in returned info object\n";
} }
my $username = "${unique_name}\@${realm}"; my $username = "${unique_name}\@${realm}";
# first, check if $username respects our naming conventions # first, check if $username respects our naming conventions
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(
my $usercfg = cfs_read_file("user.cfg"); sub {
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'})) {
$entry->{email} = $email; $entry->{email} = $email;
} }
if (defined(my $given_name = $info->{'given_name'})) { if (defined(my $given_name = $info->{'given_name'})) {
$entry->{firstname} = $given_name; $entry->{firstname} = $given_name;
} }
if (defined(my $family_name = $info->{'family_name'})) { if (defined(my $family_name = $info->{'family_name'})) {
$entry->{lastname} = $family_name; $entry->{lastname} = $family_name;
} }
$usercfg->{users}->{$username} = $entry; $usercfg->{users}->{$username} = $entry;
cfs_write_file("user.cfg", $usercfg); cfs_write_file("user.cfg", $usercfg);
}, "autocreate openid user failed"); },
} else { "autocreate openid user failed",
# test if user exists and is enabled );
$rpcenv->check_user_enabled($username); } else {
} # test if user exists and is enabled
$rpcenv->check_user_enabled($username);
}
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(
my $usercfg = cfs_read_file("user.cfg"); sub {
my $usercfg = cfs_read_file("user.cfg");
my $oidc_groups; my $oidc_groups;
for my $group (@$groups_list) { for my $group (@$groups_list) {
if (PVE::AccessControl::verify_groupname($group, 1)) { if (PVE::AccessControl::verify_groupname($group, 1)) {
# add realm name as suffix to group # add realm name as suffix to group
$oidc_groups->{"$group-$realm"} = 1; $oidc_groups->{"$group-$realm"} = 1;
} else { } else {
# 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",
); );
} }
} }
# 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'}) {
# populate all groups in claim # populate all groups in claim
$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) { }
# log a messages about created groups here if ($groups_to_create) {
my $groups_to_create_string = join(', ', sort keys %$groups_to_create); # log a messages about created groups here
syslog( my $groups_to_create_string =
'info', join(', ', sort keys %$groups_to_create);
"groups created automatically from openid claim: $groups_to_create_string" syslog(
); 'info',
} "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" );
); }
}
if (keys %$groups_intersect) { if (keys %$groups_intersect) {
# ensure user is a member of the groups # ensure user is a member of the groups
for my $group (keys %$groups_intersect) { for my $group (keys %$groups_intersect) {
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 =
syslog( join(', ', sort keys %$groups_intersect);
'info', syslog(
"openid user '$username' added to groups: $groups_intersect_string" 'info',
); "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"); },
} else { "openid group mapping failed",
syslog('err', "openid groups list is not an array; groups will not be updated"); );
} } else {
} else { syslog(
syslog('err', "openid groups claim '$groups_claim' is not found in claims"); 'err',
} "openid groups list is not an array; groups will not be updated",
} );
}
} else {
syslog('err', "openid groups claim '$groups_claim' is not found in claims");
}
}
my $ticket = PVE::AccessControl::assemble_ticket($username); my $ticket = PVE::AccessControl::assemble_ticket($username);
my $csrftoken = PVE::AccessControl::assemble_csrf_prevention_token($username); my $csrftoken = PVE::AccessControl::assemble_csrf_prevention_token($username);
my $cap = $rpcenv->compute_api_permission($username); my $cap = $rpcenv->compute_api_permission($username);
$res = { $res = {
ticket => $ticket, ticket => $ticket,
username => $username, username => $username,
CSRFPreventionToken => $csrftoken, CSRFPreventionToken => $csrftoken,
cap => $cap, cap => $cap,
}; };
my $clinfo = PVE::Cluster::get_clinfo(); my $clinfo = PVE::Cluster::get_clinfo();
if ($clinfo->{cluster}->{name} && $rpcenv->check($username, '/', ['Sys.Audit'], 1)) { if (
$res->{clustername} = $clinfo->{cluster}->{name}; $clinfo->{cluster}->{name} && $rpcenv->check($username, '/', ['Sys.Audit'], 1)
} ) {
}; $res->{clustername} = $clinfo->{cluster}->{name};
if (my $err = $@) { }
my $clientip = $rpcenv->get_client_ip() || ''; };
syslog('err', "openid authentication failure; rhost=$clientip msg=$err"); if (my $err = $@) {
# do not return any info to prevent user enumeration attacks my $clientip = $rpcenv->get_client_ip() || '';
die PVE::Exception->new("authentication failure\n", code => 401); syslog('err', "openid authentication failure; rhost=$clientip msg=$err");
} # do not return any info to prevent user enumeration attacks
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,215 +10,235 @@ 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(
type => 'string', 'role-id',
format => 'pve-roleid', {
}); type => 'string',
register_standard_option('role-privs', { format => 'pve-roleid',
type => 'string' , },
format => 'pve-priv-list', );
optional => 1, register_standard_option(
}); 'role-privs',
{
type => 'string',
format => 'pve-priv-list',
optional => 1,
},
);
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'index', name => 'index',
path => '', path => '',
method => 'GET', method => 'GET',
description => "Role index.", description => "Role index.",
permissions => { permissions => {
user => 'all', user => 'all',
}, },
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => {}, properties => {},
}, },
returns => { returns => {
type => 'array', type => 'array',
items => { items => {
type => "object", type => "object",
properties => { properties => {
roleid => get_standard_option('role-id'), roleid => get_standard_option('role-id'),
privs => get_standard_option('role-privs'), privs => get_standard_option('role-privs'),
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) = @_;
my $res = []; my $res = [];
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, {
privs => $privs, roleid => $role,
special => PVE::AccessControl::role_is_special($role), privs => $privs,
}; special => PVE::AccessControl::role_is_special($role),
} };
}
return $res; return $res;
}}); },
});
__PACKAGE__->register_method ({ __PACKAGE__->register_method({
name => 'create_role', name => 'create_role',
protected => 1, protected => 1,
path => '', path => '',
method => 'POST', method => 'POST',
permissions => { permissions => {
check => ['perm', '/access', ['Sys.Modify']], check => ['perm', '/access', ['Sys.Modify']],
}, },
description => "Create new role.", description => "Create new role.",
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
roleid => get_standard_option('role-id'), roleid => get_standard_option('role-id'),
privs => get_standard_option('role-privs'), privs => get_standard_option('role-privs'),
}, },
}, },
returns => { type => 'null' }, returns => { type => 'null' },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
my $role = $param->{roleid}; my $role = $param->{roleid};
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(
my $usercfg = cfs_read_file("user.cfg"); sub {
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};
$usercfg->{roles}->{$role} = {}; $usercfg->{roles}->{$role} = {};
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}',
method => 'PUT', method => 'PUT',
permissions => { permissions => {
check => ['perm', '/access', ['Sys.Modify']], check => ['perm', '/access', ['Sys.Modify']],
}, },
description => "Update an existing role.", description => "Update an existing role.",
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
roleid => get_standard_option('role-id'), roleid => get_standard_option('role-id'),
privs => get_standard_option('role-privs'), privs => get_standard_option('role-privs'),
append => { type => 'boolean', optional => 1, requires => 'privs' }, append => { type => 'boolean', optional => 1, requires => 'privs' },
}, },
}, },
returns => { type => 'null' }, returns => { type => 'null' },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
my $role = $param->{roleid}; my $role = $param->{roleid};
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(
my $usercfg = cfs_read_file("user.cfg"); sub {
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};
$usercfg->{roles}->{$role} = {} if !$param->{append}; $usercfg->{roles}->{$role} = {} if !$param->{append};
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',
permissions => { permissions => {
user => 'all', user => 'all',
}, },
description => "Get role configuration.", description => "Get role configuration.",
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
roleid => get_standard_option('role-id'), roleid => get_standard_option('role-id'),
}, },
}, },
returns => { returns => {
type => "object", type => "object",
additionalProperties => 0, additionalProperties => 0,
properties => PVE::AccessControl::create_priv_properties(), properties => PVE::AccessControl::create_priv_properties(),
}, },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
my $usercfg = cfs_read_file("user.cfg"); my $usercfg = cfs_read_file("user.cfg");
my $role = $param->{roleid}; my $role = $param->{roleid};
my $data = $usercfg->{roles}->{$role}; my $data = $usercfg->{roles}->{$role};
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}',
method => 'DELETE', method => 'DELETE',
permissions => { permissions => {
check => ['perm', '/access', ['Sys.Modify']], check => ['perm', '/access', ['Sys.Modify']],
}, },
description => "Delete role.", description => "Delete role.",
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
roleid => get_standard_option('role-id'), roleid => get_standard_option('role-id'),
}, },
}, },
returns => { type => 'null' }, returns => { type => 'null' },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
my $role = $param->{roleid}; my $role = $param->{roleid};
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(
my $usercfg = cfs_read_file("user.cfg"); sub {
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 = {
@ -34,22 +34,22 @@ my $TFA_TYPE_SCHEMA = {
my %TFA_INFO_PROPERTIES = ( my %TFA_INFO_PROPERTIES = (
id => { id => {
type => 'string', type => 'string',
description => 'The id used to reference this entry.', description => 'The id used to reference this entry.',
}, },
description => { description => {
type => 'string', type => 'string',
description => 'User chosen description for this entry.', description => 'User chosen description for this entry.',
}, },
created => { created => {
type => 'integer', type => 'integer',
description => 'Creation time of this entry as unix epoch.', description => 'Creation time of this entry as unix epoch.',
}, },
enable => { enable => {
type => 'boolean', type => 'boolean',
description => 'Whether this TFA entry is currently enabled.', description => 'Whether this TFA entry is currently enabled.',
optional => 1, optional => 1,
default => 1, default => 1,
}, },
); );
@ -57,8 +57,8 @@ my $TYPED_TFA_ENTRY_SCHEMA = {
type => 'object', type => 'object',
description => 'TFA Entry.', description => 'TFA Entry.',
properties => { properties => {
type => $TFA_TYPE_SCHEMA, type => $TFA_TYPE_SCHEMA,
%TFA_INFO_PROPERTIES, %TFA_INFO_PROPERTIES,
}, },
}; };
@ -70,28 +70,28 @@ my $TFA_ID_SCHEMA = {
my $TFA_UPDATE_INFO_SCHEMA = { my $TFA_UPDATE_INFO_SCHEMA = {
type => 'object', type => 'object',
properties => { properties => {
id => { id => {
type => 'string', type => 'string',
description => 'The id of a newly added TFA entry.', description => 'The id of a newly added TFA entry.',
}, },
challenge => { challenge => {
type => 'string', type => 'string',
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,290 +100,304 @@ 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(
my $user_cfg = cfs_read_file('user.cfg'); sub {
my $user = $user_cfg->{users}->{$userid}; my $user_cfg = cfs_read_file('user.cfg');
my $keys = $user->{keys}; my $user = $user_cfg->{users}->{$userid};
# When enabling, we convert old-old keys, my $keys = $user->{keys};
# When disabling, we shouldn't actually have old keys anymore, so if they are there, # When enabling, we convert old-old keys,
# they'll be removed. # When disabling, we shouldn't actually have old keys anymore, so if they are there,
if ($tfa_cfg && $keys && $keys !~ /^x(?:!.*)?$/) { # they'll be removed.
my $domain_cfg = cfs_read_file('domains.cfg'); if ($tfa_cfg && $keys && $keys !~ /^x(?:!.*)?$/) {
my $realm_cfg = $domain_cfg->{ids}->{$realm}; my $domain_cfg = cfs_read_file('domains.cfg');
die "auth domain '$realm' does not exist\n" if !$realm_cfg; my $realm_cfg = $domain_cfg->{ids}->{$realm};
die "auth domain '$realm' does not exist\n" if !$realm_cfg;
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; );
cfs_write_file("user.cfg", $user_cfg); }
}, "enabling TFA for the user failed"); $user->{keys} = $tfa_cfg ? 'x' : undef;
cfs_write_file("user.cfg", $user_cfg);
},
"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
description => 'List TFA configurations of users.', description => 'List TFA configurations of users.',
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
userid => get_standard_option('userid', { userid => get_standard_option(
completion => \&PVE::AccessControl::complete_username, 'userid',
}), {
} 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
description => 'Fetch a requested TFA entry if present.', description => 'Fetch a requested TFA entry if present.',
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
userid => get_standard_option('userid', { userid => get_standard_option(
completion => \&PVE::AccessControl::complete_username, 'userid',
}), {
id => $TFA_ID_SCHEMA, completion => \&PVE::AccessControl::complete_username,
} },
),
id => $TFA_ID_SCHEMA,
},
}, },
returns => $TYPED_TFA_ENTRY_SCHEMA, returns => $TYPED_TFA_ENTRY_SCHEMA,
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');
my $id = $param->{id}; my $id = $param->{id};
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
allowtoken => 0, # we don't want tokens to change the regular user's TFA settings allowtoken => 0, # we don't want tokens to change the regular user's TFA settings
description => 'Delete a TFA entry by ID.', description => 'Delete a TFA entry by ID.',
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
userid => get_standard_option('userid', { userid => get_standard_option(
completion => \&PVE::AccessControl::complete_username, 'userid',
}), {
id => $TFA_ID_SCHEMA, completion => \&PVE::AccessControl::complete_username,
password => $OPTIONAL_PASSWORD_SCHEMA, },
} ),
id => $TFA_ID_SCHEMA,
password => $OPTIONAL_PASSWORD_SCHEMA,
},
}, },
returns => { type => 'null' }, returns => { type => 'null' },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
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 {
my $tfa_cfg = cfs_read_file('priv/tfa.cfg'); my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
my $has_entries_left = $tfa_cfg->api_delete_tfa($userid, $param->{id}); my $has_entries_left = $tfa_cfg->api_delete_tfa($userid, $param->{id});
cfs_write_file('priv/tfa.cfg', $tfa_cfg); cfs_write_file('priv/tfa.cfg', $tfa_cfg);
return $has_entries_left; return $has_entries_left;
}); });
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',
permissions => { permissions => {
description => "Returns all or just the logged-in user, depending on privileges.", description => "Returns all or just the logged-in user, depending on privileges.",
user => 'all', user => 'all',
}, },
protected => 1, # else we can't access shadow files protected => 1, # else we can't access shadow files
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.",
type => 'array', type => 'array',
items => { items => {
type => 'object', type => 'object',
properties => { properties => {
userid => { userid => {
type => 'string', type => 'string',
description => 'User this entry belongs to.', description => 'User this entry belongs to.',
}, },
entries => { entries => {
type => 'array', type => 'array',
items => $TYPED_TFA_ENTRY_SCHEMA, items => $TYPED_TFA_ENTRY_SCHEMA,
}, },
'totp-locked' => { 'totp-locked' => {
type => 'boolean', type => 'boolean',
optional => 1, optional => 1,
description => 'True if the user is currently locked out of TOTP factors.', description => 'True if the user is currently locked out of TOTP factors.',
}, },
'tfa-locked-until' => { 'tfa-locked-until' => {
type => 'integer', type => 'integer',
optional => 1, optional => 1,
description => description =>
'Contains a timestamp until when a user is locked out of 2nd factors.', 'Contains a timestamp until when a user is locked out of 2nd factors.',
}, },
}, },
}, },
links => [ { rel => 'child', href => "{userid}" } ], links => [{ rel => 'child', href => "{userid}" }],
}, },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get(); my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user(); my $authuser = $rpcenv->get_user();
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;
} }
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]);
return [ return [
grep { grep {
my $userid = $_->{userid}; my $userid = $_->{userid};
$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
allowtoken => 0, # we don't want tokens to change the regular user's TFA settings allowtoken => 0, # we don't want tokens to change the regular user's TFA settings
description => 'Add a TFA entry for a user.', description => 'Add a TFA entry for a user.',
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
userid => get_standard_option('userid', { userid => get_standard_option(
completion => \&PVE::AccessControl::complete_username, 'userid',
}), {
completion => \&PVE::AccessControl::complete_username,
},
),
type => $TFA_TYPE_SCHEMA, type => $TFA_TYPE_SCHEMA,
description => { description => {
type => 'string', type => 'string',
description => 'A description to distinguish multiple entries from one another', description => 'A description to distinguish multiple entries from one another',
maxLength => 255, maxLength => 255,
optional => 1, optional => 1,
}, },
totp => { totp => {
type => 'string', type => 'string',
description => "A totp URI.", description => "A totp URI.",
optional => 1, optional => 1,
}, },
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 =>
description => 'When responding to a u2f challenge: the original challenge string', 'When responding to a u2f challenge: the original challenge string',
optional => 1, optional => 1,
}, },
password => $OPTIONAL_PASSWORD_SCHEMA, password => $OPTIONAL_PASSWORD_SCHEMA,
}, },
}, },
returns => $TFA_UPDATE_INFO_SCHEMA, returns => $TFA_UPDATE_INFO_SCHEMA,
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
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};
my $value = delete $param->{value}; my $value = delete $param->{value};
if ($type eq 'yubico') { if ($type eq 'yubico') {
$value = validate_yubico_otp($userid, $realm, $value); $value = validate_yubico_otp($userid, $realm, $value);
} }
return PVE::AccessControl::lock_tfa_config(sub { return PVE::AccessControl::lock_tfa_config(sub {
my $tfa_cfg = cfs_read_file('priv/tfa.cfg'); my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
set_user_tfa_enabled($userid, $realm, $tfa_cfg); set_user_tfa_enabled($userid, $realm, $tfa_cfg);
PVE::AccessControl::configure_u2f_and_wa($tfa_cfg); PVE::AccessControl::configure_u2f_and_wa($tfa_cfg);
my $response = $tfa_cfg->api_add_tfa_entry( my $response = $tfa_cfg->api_add_tfa_entry(
$userid, $userid,
$param->{description}, $param->{description},
$param->{totp}, $param->{totp},
$value, $value,
$param->{challenge}, $param->{challenge},
$type, $type,
); );
cfs_write_file('priv/tfa.cfg', $tfa_cfg); cfs_write_file('priv/tfa.cfg', $tfa_cfg);
return $response; return $response;
}); });
}}); },
});
sub validate_yubico_otp : prototype($$$) { sub validate_yubico_otp : prototype($$$) {
my ($userid, $realm, $value) = @_; my ($userid, $realm, $value) = @_;
@ -394,11 +408,11 @@ sub validate_yubico_otp : prototype($$$) {
my $realm_tfa = $realm_cfg->{tfa}; my $realm_tfa = $realm_cfg->{tfa};
die "no yubico otp configuration available for realm $realm\n" die "no yubico otp configuration available for realm $realm\n"
if !$realm_tfa; if !$realm_tfa;
$realm_tfa = PVE::Auth::Plugin::parse_tfa_config($realm_tfa); $realm_tfa = PVE::Auth::Plugin::parse_tfa_config($realm_tfa);
die "realm is not setup for Yubico OTP\n" die "realm is not setup for Yubico OTP\n"
if !$realm_tfa || $realm_tfa->{type} ne 'yubico'; if !$realm_tfa || $realm_tfa->{type} ne 'yubico';
my $public_key = substr($value, 0, 12); my $public_key = substr($value, 0, 12);
@ -407,64 +421,62 @@ 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
allowtoken => 0, # we don't want tokens to change the regular user's TFA settings allowtoken => 0, # we don't want tokens to change the regular user's TFA settings
description => 'Add a TFA entry for a user.', description => 'Add a TFA entry for a user.',
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
userid => get_standard_option('userid', { userid => get_standard_option(
completion => \&PVE::AccessControl::complete_username, 'userid',
}), {
id => $TFA_ID_SCHEMA, completion => \&PVE::AccessControl::complete_username,
description => { },
type => 'string', ),
description => 'A description to distinguish multiple entries from one another', id => $TFA_ID_SCHEMA,
maxLength => 255, description => {
optional => 1, type => 'string',
}, description => 'A description to distinguish multiple entries from one another',
enable => { maxLength => 255,
type => 'boolean', optional => 1,
description => 'Whether the entry should be enabled for login.', },
optional => 1, enable => {
}, type => 'boolean',
password => $OPTIONAL_PASSWORD_SCHEMA, description => 'Whether the entry should be enabled for login.',
}, optional => 1,
},
password => $OPTIONAL_PASSWORD_SCHEMA,
},
}, },
returns => { type => 'null' }, returns => { type => 'null' },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
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;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -13,88 +13,89 @@ sub type {
sub properties { sub properties {
return { return {
server1 => { server1 => {
description => "Server IP address (or DNS name)", description => "Server IP address (or DNS name)",
type => 'string', type => 'string',
format => 'address', format => 'address',
maxLength => 256, maxLength => 256,
}, },
server2 => { server2 => {
description => "Fallback Server IP address (or DNS name)", description => "Fallback Server IP address (or DNS name)",
type => 'string', type => 'string',
optional => 1, optional => 1,
format => 'address', format => 'address',
maxLength => 256, maxLength => 256,
}, },
secure => { secure => {
description => "Use secure LDAPS protocol. DEPRECATED: use 'mode' instead.", description => "Use secure LDAPS protocol. DEPRECATED: use 'mode' instead.",
type => 'boolean', type => 'boolean',
optional => 1, optional => 1,
}, },
sslversion => { sslversion => {
description => "LDAPS TLS/SSL version. It's not recommended to use version older than 1.2!", description =>
type => 'string', "LDAPS TLS/SSL version. It's not recommended to use version older than 1.2!",
enum => [qw(tlsv1 tlsv1_1 tlsv1_2 tlsv1_3)], type => 'string',
optional => 1, enum => [qw(tlsv1 tlsv1_1 tlsv1_2 tlsv1_3)],
}, optional => 1,
default => { },
description => "Use this as default realm", default => {
type => 'boolean', description => "Use this as default realm",
optional => 1, type => 'boolean',
}, optional => 1,
comment => { },
description => "Description.", comment => {
type => 'string', description => "Description.",
optional => 1, type => 'string',
maxLength => 4096, optional => 1,
}, maxLength => 4096,
port => { },
description => "Server port.", port => {
type => 'integer', description => "Server port.",
minimum => 1, type => 'integer',
maximum => 65535, minimum => 1,
optional => 1, maximum => 65535,
}, optional => 1,
domain => { },
description => "AD domain name", domain => {
type => 'string', description => "AD domain name",
pattern => '\S+', type => 'string',
optional => 1, pattern => '\S+',
maxLength => 256, optional => 1,
}, maxLength => 256,
tfa => PVE::JSONSchema::get_standard_option('tfa'), },
tfa => PVE::JSONSchema::get_standard_option('tfa'),
}; };
} }
sub options { sub options {
return { return {
server1 => {}, server1 => {},
server2 => { optional => 1 }, server2 => { optional => 1 },
domain => {}, domain => {},
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 },
capath => { optional => 1 }, capath => { optional => 1 },
cert => { optional => 1 }, cert => { optional => 1 },
certkey => { optional => 1 }, certkey => { optional => 1 },
base_dn => { optional => 1 }, base_dn => { optional => 1 },
bind_dn => { optional => 1 }, bind_dn => { optional => 1 },
password => { optional => 1 }, password => { optional => 1 },
user_attr => { optional => 1 }, user_attr => { optional => 1 },
filter => { optional => 1 }, filter => { optional => 1 },
sync_attributes => { optional => 1 }, sync_attributes => { optional => 1 },
user_classes => { optional => 1 }, user_classes => { optional => 1 },
group_dn => { optional => 1 }, group_dn => { optional => 1 },
group_name_attr => { optional => 1 }, group_name_attr => { optional => 1 },
group_filter => { optional => 1 }, group_filter => { optional => 1 },
group_classes => { optional => 1 }, group_classes => { optional => 1 },
'sync-defaults-options' => { optional => 1 }, 'sync-defaults-options' => { optional => 1 },
mode => { optional => 1 }, mode => { optional => 1 },
'case-sensitive' => { optional => 1 }, 'case-sensitive' => { optional => 1 },
}; };
} }
@ -116,28 +117,28 @@ sub authenticate_user {
my %ad_args; my %ad_args;
if ($config->{verify}) { if ($config->{verify}) {
$ad_args{verify} = 'require'; $ad_args{verify} = 'require';
$ad_args{clientcert} = $config->{cert} if $config->{cert}; $ad_args{clientcert} = $config->{cert} if $config->{cert};
$ad_args{clientkey} = $config->{certkey} if $config->{certkey}; $ad_args{clientkey} = $config->{certkey} if $config->{certkey};
if (defined(my $capath = $config->{capath})) { if (defined(my $capath = $config->{capath})) {
if (-d $capath) { if (-d $capath) {
$ad_args{capath} = $capath; $ad_args{capath} = $capath;
} else { } else {
$ad_args{cafile} = $capath; $ad_args{cafile} = $capath;
} }
} }
} elsif (defined($config->{verify})) { } elsif (defined($config->{verify})) {
$ad_args{verify} = 'none'; $ad_args{verify} = 'none';
} }
if ($scheme ne 'ldap') { if ($scheme ne 'ldap') {
$ad_args{sslversion} = $config->{sslversion} // 'tlsv1_2'; $ad_args{sslversion} = $config->{sslversion} // 'tlsv1_2';
} }
my $ldap = PVE::LDAP::ldap_connect($servers, $scheme, $port, \%ad_args); my $ldap = PVE::LDAP::ldap_connect($servers, $scheme, $port, \%ad_args);
$username = "$username\@$config->{domain}" $username = "$username\@$config->{domain}"
if $username !~ m/@/ && $config->{domain}; if $username !~ m/@/ && $config->{domain};
PVE::LDAP::auth_user_dn($ldap, $username, $password); PVE::LDAP::auth_user_dn($ldap, $username, $password);

View File

@ -16,153 +16,154 @@ sub type {
sub properties { sub properties {
return { return {
base_dn => { base_dn => {
description => "LDAP base domain name", description => "LDAP base domain name",
type => 'string', type => 'string',
optional => 1, optional => 1,
maxLength => 256, maxLength => 256,
}, },
user_attr => { user_attr => {
description => "LDAP user attribute name", description => "LDAP user attribute name",
type => 'string', type => 'string',
pattern => '\S{2,}', pattern => '\S{2,}',
optional => 1, optional => 1,
maxLength => 256, maxLength => 256,
}, },
bind_dn => { bind_dn => {
description => "LDAP bind domain name", description => "LDAP bind domain name",
type => 'string', type => 'string',
optional => 1, optional => 1,
maxLength => 256, maxLength => 256,
}, },
password => { password => {
description => "LDAP bind password. Will be stored in '/etc/pve/priv/realm/<REALM>.pw'.", description =>
type => 'string', "LDAP bind password. Will be stored in '/etc/pve/priv/realm/<REALM>.pw'.",
optional => 1, type => 'string',
}, optional => 1,
verify => { },
description => "Verify the server's SSL certificate", verify => {
type => 'boolean', description => "Verify the server's SSL certificate",
optional => 1, type => 'boolean',
default => 0, optional => 1,
}, default => 0,
capath => { },
description => "Path to the CA certificate store", capath => {
type => 'string', description => "Path to the CA certificate store",
optional => 1, type => 'string',
default => '/etc/ssl/certs', optional => 1,
}, default => '/etc/ssl/certs',
cert => { },
description => "Path to the client certificate", cert => {
type => 'string', description => "Path to the client certificate",
optional => 1, type => 'string',
}, optional => 1,
certkey => { },
description => "Path to the client certificate key", certkey => {
type => 'string', description => "Path to the client certificate key",
optional => 1, type => 'string',
}, optional => 1,
filter => { },
description => "LDAP filter for user sync.", filter => {
type => 'string', description => "LDAP filter for user sync.",
optional => 1, type => 'string',
maxLength => 2048, optional => 1,
}, maxLength => 2048,
sync_attributes => { },
description => "Comma separated list of key=value pairs for specifying" sync_attributes => {
." which LDAP attributes map to which PVE user field. For example," description => "Comma separated list of key=value pairs for specifying"
." to map the LDAP attribute 'mail' to PVEs 'email', write " . " which LDAP attributes map to which PVE user field. For example,"
." 'email=mail'. By default, each PVE user field is represented " . " to map the LDAP attribute 'mail' to PVEs 'email', write "
." by an LDAP attribute of the same name.", . " 'email=mail'. By default, each PVE user field is represented "
optional => 1, . " by an LDAP attribute of the same name.",
type => 'string', optional => 1,
pattern => '\w+=[^,]+(,\s*\w+=[^,]+)*', type => 'string',
}, pattern => '\w+=[^,]+(,\s*\w+=[^,]+)*',
user_classes => { },
description => "The objectclasses for users.", user_classes => {
type => 'string', description => "The objectclasses for users.",
default => 'inetorgperson, posixaccount, person, user', type => 'string',
format => 'ldap-simple-attr-list', default => 'inetorgperson, posixaccount, person, user',
optional => 1, format => 'ldap-simple-attr-list',
}, optional => 1,
group_dn => { },
description => "LDAP base domain name for group sync. If not set, the" group_dn => {
." base_dn will be used.", description => "LDAP base domain name for group sync. If not set, the"
type => 'string', . " base_dn will be used.",
optional => 1, type => 'string',
maxLength => 256, optional => 1,
}, maxLength => 256,
group_name_attr => { },
description => "LDAP attribute representing a groups name. If not set" group_name_attr => {
." or found, the first value of the DN will be used as name.", description => "LDAP attribute representing a groups name. If not set"
type => 'string', . " or found, the first value of the DN will be used as name.",
format => 'ldap-simple-attr', type => 'string',
optional => 1, format => 'ldap-simple-attr',
maxLength => 256, optional => 1,
}, maxLength => 256,
group_filter => { },
description => "LDAP filter for group sync.", group_filter => {
type => 'string', description => "LDAP filter for group sync.",
optional => 1, type => 'string',
maxLength => 2048, optional => 1,
}, maxLength => 2048,
group_classes => { },
description => "The objectclasses for groups.", group_classes => {
type => 'string', description => "The objectclasses for groups.",
default => 'groupOfNames, group, univentionGroup, ipausergroup', type => 'string',
format => 'ldap-simple-attr-list', default => 'groupOfNames, group, univentionGroup, ipausergroup',
optional => 1, format => 'ldap-simple-attr-list',
}, optional => 1,
'sync-defaults-options' => { },
description => "The default options for behavior of synchronizations.", 'sync-defaults-options' => {
type => 'string', description => "The default options for behavior of synchronizations.",
format => 'realm-sync-options', type => 'string',
optional => 1, format => 'realm-sync-options',
}, optional => 1,
mode => { },
description => "LDAP protocol mode.", mode => {
type => 'string', description => "LDAP protocol mode.",
enum => [ 'ldap', 'ldaps', 'ldap+starttls'], type => 'string',
optional => 1, enum => ['ldap', 'ldaps', 'ldap+starttls'],
default => 'ldap', optional => 1,
}, default => 'ldap',
},
'case-sensitive' => { 'case-sensitive' => {
description => "username is case-sensitive", description => "username is case-sensitive",
type => 'boolean', type => 'boolean',
optional => 1, optional => 1,
default => 1, default => 1,
}, },
}; };
} }
sub options { sub options {
return { return {
server1 => {}, server1 => {},
server2 => { optional => 1 }, server2 => { optional => 1 },
base_dn => {}, base_dn => {},
bind_dn => { optional => 1 }, bind_dn => { optional => 1 },
password => { optional => 1 }, password => { optional => 1 },
user_attr => {}, user_attr => {},
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 },
capath => { optional => 1 }, capath => { optional => 1 },
cert => { optional => 1 }, cert => { optional => 1 },
certkey => { optional => 1 }, certkey => { optional => 1 },
filter => { optional => 1 }, filter => { optional => 1 },
sync_attributes => { optional => 1 }, sync_attributes => { optional => 1 },
user_classes => { optional => 1 }, user_classes => { optional => 1 },
group_dn => { optional => 1 }, group_dn => { optional => 1 },
group_name_attr => { optional => 1 }, group_name_attr => { optional => 1 },
group_filter => { optional => 1 }, group_filter => { optional => 1 },
group_classes => { optional => 1 }, group_classes => { optional => 1 },
'sync-defaults-options' => { optional => 1 }, 'sync-defaults-options' => { optional => 1 },
mode => { optional => 1 }, mode => { optional => 1 },
'case-sensitive' => { optional => 1 }, 'case-sensitive' => { optional => 1 },
}; };
} }
@ -171,17 +172,17 @@ my sub verify_sync_attribute_value {
# The attribute does not include the realm, so can't use PVE::Auth::Plugin::verify_username # The attribute does not include the realm, so can't use PVE::Auth::Plugin::verify_username
if ($attr eq 'username') { if ($attr eq 'username') {
die "value '$value' does not look like a valid user name\n" die "value '$value' does not look like a valid user name\n"
if $value !~ m/${PVE::Auth::Plugin::user_regex}/; if $value !~ m/${PVE::Auth::Plugin::user_regex}/;
return; return;
} }
return if $attr eq 'enable'; # for backwards compat, don't parse/validate return if $attr eq 'enable'; # for backwards compat, don't parse/validate
if (my $schema = PVE::JSONSchema::get_standard_option("user-$attr")) { if (my $schema = PVE::JSONSchema::get_standard_option("user-$attr")) {
PVE::JSONSchema::validate($value, $schema, "invalid value '$value'\n"); PVE::JSONSchema::validate($value, $schema, "invalid value '$value'\n");
} else { } else {
die "internal error: no schema for attribute '$attr' with value '$value' available!\n"; die "internal error: no schema for attribute '$attr' with value '$value' available!\n";
} }
} }
@ -206,40 +207,40 @@ sub connect_and_bind {
my %ldap_args; my %ldap_args;
if ($config->{verify}) { if ($config->{verify}) {
$ldap_args{verify} = 'require'; $ldap_args{verify} = 'require';
$ldap_args{clientcert} = $config->{cert} if $config->{cert}; $ldap_args{clientcert} = $config->{cert} if $config->{cert};
$ldap_args{clientkey} = $config->{certkey} if $config->{certkey}; $ldap_args{clientkey} = $config->{certkey} if $config->{certkey};
if (defined(my $capath = $config->{capath})) { if (defined(my $capath = $config->{capath})) {
if (-d $capath) { if (-d $capath) {
$ldap_args{capath} = $capath; $ldap_args{capath} = $capath;
} else { } else {
$ldap_args{cafile} = $capath; $ldap_args{cafile} = $capath;
} }
} }
} else { } else {
$ldap_args{verify} = 'none'; $ldap_args{verify} = 'none';
} }
if ($scheme ne 'ldap') { if ($scheme ne 'ldap') {
$ldap_args{sslversion} = $config->{sslversion} || 'tlsv1_2'; $ldap_args{sslversion} = $config->{sslversion} || 'tlsv1_2';
} }
my $ldap = PVE::LDAP::ldap_connect($servers, $scheme, $port, \%ldap_args); my $ldap = PVE::LDAP::ldap_connect($servers, $scheme, $port, \%ldap_args);
if ($config->{bind_dn}) { if ($config->{bind_dn}) {
my $bind_dn = $config->{bind_dn}; my $bind_dn = $config->{bind_dn};
my $bind_pass = $param->{password} || ldap_get_credentials($realm); my $bind_pass = $param->{password} || ldap_get_credentials($realm);
die "missing password for realm $realm\n" if !defined($bind_pass); die "missing password for realm $realm\n" if !defined($bind_pass);
PVE::LDAP::ldap_bind($ldap, $bind_dn, $bind_pass); PVE::LDAP::ldap_bind($ldap, $bind_dn, $bind_pass);
} elsif ($config->{cert} && $config->{certkey}) { } elsif ($config->{cert} && $config->{certkey}) {
warn "skipping anonymous bind with clientcert\n"; warn "skipping anonymous bind with clientcert\n";
} else { } else {
PVE::LDAP::ldap_bind($ldap); PVE::LDAP::ldap_bind($ldap);
} }
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');
} }
return $ldap; return $ldap;
@ -278,26 +279,26 @@ sub get_users {
my $user_name_attr = $config->{user_attr} // 'uid'; my $user_name_attr = $config->{user_attr} // 'uid';
my $ldap_attribute_map = { my $ldap_attribute_map = {
$user_name_attr => 'username', $user_name_attr => 'username',
enable => 'enable', enable => 'enable',
expire => 'expire', expire => 'expire',
firstname => 'firstname', firstname => 'firstname',
lastname => 'lastname', lastname => 'lastname',
email => 'email', email => 'email',
comment => 'comment', comment => 'comment',
keys => 'keys', keys => 'keys',
# NOTE: also ensure verify_sync_attribute_value can handle any new/changed attribute name # NOTE: also ensure verify_sync_attribute_value can handle any new/changed attribute name
}; };
# build on the fly as this is small and only called once per realm in a ldap-sync anyway # build on the fly as this is small and only called once per realm in a ldap-sync anyway
my $valid_sync_attributes = { map { $_ => 1 } values $ldap_attribute_map->%* }; my $valid_sync_attributes = { map { $_ => 1 } values $ldap_attribute_map->%* };
foreach my $attr (PVE::Tools::split_list($config->{sync_attributes})) { foreach my $attr (PVE::Tools::split_list($config->{sync_attributes})) {
my ($ours, $ldap) = ($attr =~ m/^\s*(\w+)=(.*)\s*$/); my ($ours, $ldap) = ($attr =~ m/^\s*(\w+)=(.*)\s*$/);
if (!$valid_sync_attributes->{$ours}) { if (!$valid_sync_attributes->{$ours}) {
warn "skipping bad 'sync_attributes' entry '$ours' is not a valid target attribute\n"; warn "skipping bad 'sync_attributes' entry '$ours' is not a valid target attribute\n";
next; next;
} }
$ldap_attribute_map->{$ldap} = $ours; $ldap_attribute_map->{$ldap} = $ours;
} }
my $filter = $config->{filter}; my $filter = $config->{filter};
@ -306,41 +307,42 @@ 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 = {};
foreach my $user (@$users) { foreach my $user (@$users) {
my $user_attributes = $user->{attributes}; my $user_attributes = $user->{attributes};
my $userid = $user_attributes->{$user_name_attr}->[0]; my $userid = $user_attributes->{$user_name_attr}->[0];
my $username = "$userid\@$realm"; my $username = "$userid\@$realm";
# we cannot sync usernames that do not meet our criteria # we cannot sync usernames that do not meet our criteria
eval { PVE::Auth::Plugin::verify_username($username) }; eval { PVE::Auth::Plugin::verify_username($username) };
if (my $err = $@) { if (my $err = $@) {
warn "$err"; warn "$err";
next; next;
} }
$ret->{$username} = {}; $ret->{$username} = {};
foreach my $attr (keys %$user_attributes) { foreach my $attr (keys %$user_attributes) {
if (my $ours = $ldap_attribute_map->{$attr}) { if (my $ours = $ldap_attribute_map->{$attr}) {
my $value = $user_attributes->{$attr}->[0]; my $value = $user_attributes->{$attr}->[0];
eval { verify_sync_attribute_value($ours, $value) }; eval { verify_sync_attribute_value($ours, $value) };
if (my $err = $@) { if (my $err = $@) {
warn "skipping attribute mapping '$attr'->'$ours' for user '$username' - $err"; warn "skipping attribute mapping '$attr'->'$ours' for user '$username' - $err";
next; next;
} }
$ret->{$username}->{$ours} = $value; $ret->{$username}->{$ours} = $value;
} }
} }
if (wantarray) { if (wantarray) {
my $dn = $user->{dn}; my $dn = $user->{dn};
$dnmap->{lc($dn)} = $username; $dnmap->{ lc($dn) } = $username;
} }
} }
return wantarray ? ($ret, $dnmap) : $ret; return wantarray ? ($ret, $dnmap) : $ret;
@ -364,27 +366,27 @@ sub get_groups {
my $ret = {}; my $ret = {};
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) {
$name .= "-$realm"; $name .= "-$realm";
# we cannot sync groups that do not meet our criteria # we cannot sync groups that do not meet our criteria
eval { PVE::AccessControl::verify_groupname($name) }; eval { PVE::AccessControl::verify_groupname($name) };
if (my $err = $@) { if (my $err = $@) {
warn "$err"; warn "$err";
next; next;
} }
$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;
} }
} }
} }
} }
return $ret; return $ret;
@ -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();
@ -414,10 +417,10 @@ sub get_cred_file {
my $cred_file = ldap_cred_file_name($realmid); my $cred_file = ldap_cred_file_name($realmid);
if (-e $cred_file) { if (-e $cred_file) {
return $cred_file; return $cred_file;
} elsif (-e "/etc/pve/priv/ldap/${realmid}.pw") { } elsif (-e "/etc/pve/priv/ldap/${realmid}.pw") {
# FIXME: remove fallback with 7.0 by doing a rename on upgrade from 6.x # FIXME: remove fallback with 7.0 by doing a rename on upgrade from 6.x
return "/etc/pve/priv/ldap/${realmid}.pw"; return "/etc/pve/priv/ldap/${realmid}.pw";
} }
return $cred_file; return $cred_file;
@ -438,7 +441,7 @@ sub ldap_get_credentials {
my ($realmid) = @_; my ($realmid) = @_;
if (my $cred_file = get_cred_file($realmid)) { if (my $cred_file = get_cred_file($realmid)) {
return PVE::Tools::file_read_firstline($cred_file); return PVE::Tools::file_read_firstline($cred_file);
} }
return undef; return undef;
} }
@ -447,8 +450,8 @@ 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";
} }
} }
@ -456,9 +459,9 @@ sub on_add_hook {
my ($class, $realm, $config, %param) = @_; my ($class, $realm, $config, %param) = @_;
if (defined($param{password})) { if (defined($param{password})) {
ldap_set_credentials($param{password}, $realm); ldap_set_credentials($param{password}, $realm);
} else { } else {
ldap_delete_credentials($realm); ldap_delete_credentials($realm);
} }
} }
@ -468,9 +471,9 @@ sub on_update_hook {
return if !exists($param{password}); return if !exists($param{password});
if (defined($param{password})) { if (defined($param{password})) {
ldap_set_credentials($param{password}, $realm); ldap_set_credentials($param{password}, $realm);
} else { } else {
ldap_delete_credentials($realm); ldap_delete_credentials($realm);
} }
} }

View File

@ -18,98 +18,99 @@ sub type {
sub properties { sub properties {
return { return {
"issuer-url" => { "issuer-url" => {
description => "OpenID Issuer Url", description => "OpenID Issuer Url",
type => 'string', type => 'string',
maxLength => 256, maxLength => 256,
}, },
"client-id" => { "client-id" => {
description => "OpenID Client ID", description => "OpenID Client ID",
type => 'string', type => 'string',
maxLength => 256, maxLength => 256,
}, },
"client-key" => { "client-key" => {
description => "OpenID Client Key", description => "OpenID Client Key",
type => 'string', type => 'string',
optional => 1, optional => 1,
maxLength => 256, maxLength => 256,
}, },
autocreate => { autocreate => {
description => "Automatically create users if they do not exist.", description => "Automatically create users if they do not exist.",
optional => 1, optional => 1,
type => 'boolean', type => 'boolean',
default => 0, default => 0,
}, },
"username-claim" => { "username-claim" => {
description => "OpenID claim used to generate the unique username.", description => "OpenID claim used to generate the unique username.",
type => 'string', type => 'string',
optional => 1, optional => 1,
}, },
"groups-claim" => { "groups-claim" => {
description => "OpenID claim used to retrieve groups with.", description => "OpenID claim used to retrieve groups with.",
type => 'string', type => 'string',
pattern => $openid_claim_regex, pattern => $openid_claim_regex,
maxLength => 256, maxLength => 256,
optional => 1, optional => 1,
}, },
"groups-autocreate" => { "groups-autocreate" => {
description => "Automatically create groups if they do not exist.", description => "Automatically create groups if they do not exist.",
optional => 1, optional => 1,
type => 'boolean', type => 'boolean',
default => 0, default => 0,
}, },
"groups-overwrite" => { "groups-overwrite" => {
description => "All groups will be overwritten for the user on login.", description => "All groups will be overwritten for the user on login.",
type => 'boolean', type => 'boolean',
default => 0, default => 0,
optional => 1, optional => 1,
}, },
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"
type => 'string', . "Authorization Server is being requested to use for the Auth Request.",
pattern => '^[^\x00-\x1F\x7F <>#"]*$', # Prohibit characters not allowed in URI RFC 2396. type => 'string',
optional => 1, pattern => '^[^\x00-\x1F\x7F <>#"]*$', # Prohibit characters not allowed in URI RFC 2396.
}, optional => 1,
"query-userinfo" => { },
description => "Enables querying the userinfo endpoint for claims values.", "query-userinfo" => {
type => 'boolean', description => "Enables querying the userinfo endpoint for claims values.",
default => 1, type => 'boolean',
optional => 1, default => 1,
}, optional => 1,
}; },
};
} }
sub options { sub options {
return { return {
"issuer-url" => {}, "issuer-url" => {},
"client-id" => {}, "client-id" => {},
"client-key" => { optional => 1 }, "client-key" => { optional => 1 },
autocreate => { optional => 1 }, autocreate => { optional => 1 },
"username-claim" => { optional => 1, fixed => 1 }, "username-claim" => { optional => 1, fixed => 1 },
"groups-claim" => { optional => 1 }, "groups-claim" => { optional => 1 },
"groups-autocreate" => { optional => 1 }, "groups-autocreate" => { optional => 1 },
"groups-overwrite" => { optional => 1 }, "groups-overwrite" => { optional => 1 },
prompt => { optional => 1 }, prompt => { optional => 1 },
scopes => { optional => 1 }, scopes => { optional => 1 },
"acr-values" => { optional => 1 }, "acr-values" => { optional => 1 },
default => { optional => 1 }, default => { optional => 1 },
comment => { optional => 1 }, comment => { optional => 1 },
"query-userinfo" => { optional => 1 }, "query-userinfo" => { 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

@ -15,9 +15,9 @@ sub type {
sub options { sub options {
return { return {
default => { optional => 1 }, default => { optional => 1 },
comment => { optional => 1 }, comment => { optional => 1 },
tfa => { optional => 1 }, tfa => { optional => 1 },
}; };
} }
@ -27,38 +27,42 @@ 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(
my @res; 'proxmox-ve-auth',
while(@_) { $username,
my $msg_type = shift; sub {
my $msg = shift; my @res;
push @res, (0, $password); while (@_) {
} my $msg_type = shift;
push @res, 0; my $msg = shift;
return @res; push @res, (0, $password);
}); }
push @res, 0;
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";
} }
if (my $rpcenv = PVE::RPCEnvironment::get()) { if (my $rpcenv = PVE::RPCEnvironment::get()) {
if (my $ip = $rpcenv->get_client_ip()) { if (my $ip = $rpcenv->get_client_ip()) {
$pamh->pam_set_item(PAM_RHOST(), $ip); $pamh->pam_set_item(PAM_RHOST(), $ip);
} }
} }
my $res; my $res;
if (($res = $pamh->pam_authenticate(0)) != PAM_SUCCESS) { if (($res = $pamh->pam_authenticate(0)) != PAM_SUCCESS) {
my $err = $pamh->pam_strerror($res); my $err = $pamh->pam_strerror($res);
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";
} }
$pamh = 0; # call destructor $pamh = 0; # call destructor
@ -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) = @_;
@ -24,15 +22,15 @@ sub parse_shadow_passwd {
return $shadow if !defined($raw); return $shadow if !defined($raw);
while ($raw =~ /^\s*(.+?)\s*$/gm) { while ($raw =~ /^\s*(.+?)\s*$/gm) {
my $line = $1; my $line = $1;
if ($line !~ m/^\S+:\S+:$/) { if ($line !~ m/^\S+:\S+:$/) {
warn "pve shadow password: ignore invalid line $.\n"; warn "pve shadow password: ignore invalid line $.\n";
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;
} }
return $shadow; return $shadow;
@ -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 {
@ -56,7 +54,7 @@ sub lock_shadow_config {
cfs_lock_file($shadowconfigfile, undef, $code); cfs_lock_file($shadowconfigfile, undef, $code);
my $err = $@; my $err = $@;
if ($err) { if ($err) {
$errmsg ? die "$errmsg: $err" : die $err; $errmsg ? die "$errmsg: $err" : die $err;
} }
} }
@ -66,9 +64,9 @@ sub type {
sub options { sub options {
return { return {
default => { optional => 1 }, default => { optional => 1 },
comment => { optional => 1 }, comment => { optional => 1 },
tfa => { optional => 1 }, tfa => { optional => 1 },
}; };
} }
@ -78,13 +76,13 @@ sub authenticate_user {
die "no password\n" if !$password; die "no password\n" if !$password;
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";
} }
return 1; return 1;
@ -94,23 +92,23 @@ sub store_password {
my ($class, $config, $realm, $username, $password) = @_; my ($class, $config, $realm, $username, $password) = @_;
lock_shadow_config(sub { lock_shadow_config(sub {
my $shadow_cfg = cfs_read_file($shadowconfigfile); my $shadow_cfg = cfs_read_file($shadowconfigfile);
my $epw = PVE::Tools::encrypt_pw($password); my $epw = PVE::Tools::encrypt_pw($password);
$shadow_cfg->{users}->{$username}->{shadow} = $epw; $shadow_cfg->{users}->{$username}->{shadow} = $epw;
cfs_write_file($shadowconfigfile, $shadow_cfg); cfs_write_file($shadowconfigfile, $shadow_cfg);
}); });
} }
sub delete_user { sub delete_user {
my ($class, $config, $realm, $username) = @_; my ($class, $config, $realm, $username) = @_;
lock_shadow_config(sub { lock_shadow_config(sub {
my $shadow_cfg = cfs_read_file($shadowconfigfile); my $shadow_cfg = cfs_read_file($shadowconfigfile);
delete $shadow_cfg->{users}->{$username}; delete $shadow_cfg->{users}->{$username};
cfs_write_file($shadowconfigfile, $shadow_cfg); cfs_write_file($shadowconfigfile, $shadow_cfg);
}); });
} }
1; 1;

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(
sub { __PACKAGE__->parse_config(@_); }, $domainconfigfile,
sub { __PACKAGE__->write_config(@_); }); sub { __PACKAGE__->parse_config(@_); },
sub { __PACKAGE__->write_config(@_); },
);
sub lock_domain_config { sub lock_domain_config {
my ($code, $errmsg) = @_; my ($code, $errmsg) = @_;
@ -25,7 +27,7 @@ sub lock_domain_config {
cfs_lock_file($domainconfigfile, undef, $code); cfs_lock_file($domainconfigfile, undef, $code);
my $err = $@; my $err = $@;
if ($err) { if ($err) {
$errmsg ? die "$errmsg: $err" : die $err; $errmsg ? die "$errmsg: $err" : die $err;
} }
} }
@ -34,87 +36,100 @@ 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) = @_;
if ($realm !~ m/^${realm_regex}$/) { if ($realm !~ m/^${realm_regex}$/) {
return undef if $noerr; return undef if $noerr;
die "value does not look like a valid realm\n"; die "value does not look like a valid realm\n";
} }
return $realm; return $realm;
} }
PVE::JSONSchema::register_standard_option('realm', { PVE::JSONSchema::register_standard_option(
description => "Authentication domain ID", 'realm',
type => 'string', format => 'pve-realm', {
maxLength => 32, description => "Authentication domain ID",
}); type => 'string',
format => 'pve-realm',
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(
description => "Select what to sync.", 'sync-scope',
type => 'string', {
enum => [qw(users groups both)], description => "Select what to sync.",
optional => '1', type => 'string',
}); enum => [qw(users groups both)],
optional => '1',
},
);
PVE::JSONSchema::register_standard_option('sync-remove-vanished', { PVE::JSONSchema::register_standard_option(
description => "A semicolon-separated list of things to remove when they or the user" 'sync-remove-vanished',
." vanishes during a sync. The following values are possible: 'entry' removes the" {
." user/group when not returned from the sync. 'properties' removes the set" description => "A semicolon-separated list of things to remove when they or the user"
." properties on existing user/group that do not appear in the source (even custom ones)." . " vanishes during a sync. The following values are possible: 'entry' removes the"
." 'acl' removes acls when the user/group is not returned from the sync." . " user/group when not returned from the sync. 'properties' removes the set"
." Instead of a list it also can be 'none' (the default).", . " properties on existing user/group that do not appear in the source (even custom ones)."
type => 'string', . " 'acl' removes acls when the user/group is not returned from the sync."
default => 'none', . " Instead of a list it also can be 'none' (the default).",
typetext => "([acl];[properties];[entry])|none", type => 'string',
pattern => "(?:(?:$remove_options\;)*$remove_options)|none", default => 'none',
optional => '1', typetext => "([acl];[properties];[entry])|none",
}); pattern => "(?:(?:$remove_options\;)*$remove_options)|none",
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"
type => 'boolean', . " delete or modify anything else.",
optional => '1', type => 'boolean',
optional => '1',
}, },
'enable-new' => { 'enable-new' => {
description => "Enable newly synced users immediately.", description => "Enable newly synced users immediately.",
type => 'boolean', type => 'boolean',
default => '1', default => '1',
optional => '1', optional => '1',
}, },
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',
}, },
}; };
PVE::JSONSchema::register_standard_option('realm-sync-options', $realm_sync_options_desc); PVE::JSONSchema::register_standard_option('realm-sync-options', $realm_sync_options_desc);
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) = @_;
$username = '' if !$username; $username = '' if !$username;
my $len = length($username); my $len = length($username);
if ($len < 3) { if ($len < 3) {
die "user name '$username' is too short\n" if !$noerr; die "user name '$username' is too short\n" if !$noerr;
return undef; return undef;
} }
if ($len > 64) { if ($len > 64) {
die "user name '$username' is too long ($len > 64)\n" if !$noerr; die "user name '$username' is too long ($len > 64)\n" if !$noerr;
return undef; return undef;
} }
# we only allow a limited set of characters # we only allow a limited set of characters
@ -123,7 +138,7 @@ sub verify_username {
# slash is not allowed because it is used as pve API delimiter # slash is not allowed because it is used as pve API delimiter
# also see "man useradd" # also see "man useradd"
if ($username =~ m!^(${user_regex})\@(${realm_regex})$!) { if ($username =~ m!^(${user_regex})\@(${realm_regex})$!) {
return wantarray ? ($username, $1, $2) : $username; return wantarray ? ($username, $1, $2) : $username;
} }
die "value '$username' does not look like a valid user name\n" if !$noerr; die "value '$username' does not look like a valid user name\n" if !$noerr;
@ -131,11 +146,15 @@ sub verify_username {
return undef; return undef;
} }
PVE::JSONSchema::register_standard_option('userid', { PVE::JSONSchema::register_standard_option(
description => "Full User ID, in the `name\@realm` format.", 'userid',
type => 'string', format => 'pve-userid', {
maxLength => 64, description => "Full User ID, in the `name\@realm` format.",
}); type => 'string',
format => 'pve-userid',
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(
description => "Use Two-factor authentication.", 'tfa',
type => 'string', format => 'pve-tfa-config', {
optional => 1, description => "Use Two-factor authentication.",
maxLength => 128, type => 'string',
}); format => 'pve-tfa-config',
optional => 1,
maxLength => 128,
},
);
sub parse_tfa_config { sub parse_tfa_config {
my ($data) = @_; my ($data) = @_;
@ -197,8 +221,8 @@ sub parse_tfa_config {
my $defaultData = { my $defaultData = {
propertyList => { propertyList => {
type => { description => "Realm type." }, type => { description => "Realm type." },
realm => get_standard_option('realm'), realm => get_standard_option('realm'),
}, },
}; };
@ -210,12 +234,12 @@ sub parse_section_header {
my ($class, $line) = @_; my ($class, $line) = @_;
if ($line =~ m/^(\S+):\s*(\S+)\s*$/) { if ($line =~ m/^(\S+):\s*(\S+)\s*$/) {
my ($type, $realm) = (lc($1), $2); my ($type, $realm) = (lc($1), $2);
my $errmsg = undef; # set if you want to skip whole section my $errmsg = undef; # set if you want to skip whole section
eval { pve_verify_realm($realm); }; eval { pve_verify_realm($realm); };
$errmsg = $@ if $@; $errmsg = $@ if $@;
my $config = {}; # to return additional attributes my $config = {}; # to return additional attributes
return ($type, $realm, $errmsg, $config); return ($type, $realm, $errmsg, $config);
} }
return undef; return undef;
} }
@ -226,20 +250,20 @@ 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}) {
if ($default) { if ($default) {
delete $data->{default}; delete $data->{default};
} else { } else {
$default = $realm; $default = $realm;
} }
} }
if ($data->{comment}) { if ($data->{comment}) {
$data->{comment} = PVE::Tools::decode_text($data->{comment}); $data->{comment} = PVE::Tools::decode_text($data->{comment});
} }
} }
@ -247,24 +271,24 @@ sub parse_config {
$cfg->{ids}->{pve}->{type} = 'pve'; # force type $cfg->{ids}->{pve}->{type} = 'pve'; # force type
$cfg->{ids}->{pve}->{comment} = "Proxmox VE authentication server" $cfg->{ids}->{pve}->{comment} = "Proxmox VE authentication server"
if !$cfg->{ids}->{pve}->{comment}; if !$cfg->{ids}->{pve}->{comment};
$cfg->{ids}->{pam}->{type} = 'pam'; # force type $cfg->{ids}->{pam}->{type} = 'pam'; # force type
$cfg->{ids}->{pam}->{plugin} = 'PVE::Auth::PAM'; $cfg->{ids}->{pam}->{plugin} = 'PVE::Auth::PAM';
$cfg->{ids}->{pam}->{comment} = "Linux PAM standard authentication" $cfg->{ids}->{pam}->{comment} = "Linux PAM standard authentication"
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});
} }
} }
$class->SUPER::write_config($filename, $cfg); $class->SUPER::write_config($filename, $cfg);

View File

@ -30,17 +30,20 @@ sub param_mapping {
my ($name) = @_; my ($name) = @_;
my $mapping = { my $mapping = {
'change_password' => [ 'change_password' => [
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(
func => sub { 'pve-password',
# do not accept values given on cmdline {
return PVE::PTY::read_password('Enter password: '); func => sub {
}, # do not accept values given on cmdline
}), return PVE::PTY::read_password('Enter password: ');
] },
},
),
],
}; };
return $mapping->{$name}; return $mapping->{$name};
@ -55,31 +58,31 @@ my $print_perm_result = sub {
my ($data, $schema, $options) = @_; my ($data, $schema, $options) = @_;
if (!defined($options->{'output-format'}) || $options->{'output-format'} eq 'text') { if (!defined($options->{'output-format'}) || $options->{'output-format'} eq 'text') {
my $table_schema = { my $table_schema = {
type => 'array', type => 'array',
items => { items => {
type => 'object', type => 'object',
properties => { properties => {
'path' => { type => 'string', title => 'ACL path' }, 'path' => { type => 'string', title => 'ACL path' },
'permissions' => { type => 'string', title => 'Permissions' }, 'permissions' => { type => 'string', title => 'Permissions' },
}, },
}, },
}; };
my $table_data = []; my $table_data = [];
foreach my $path (sort keys %$data) { foreach my $path (sort keys %$data) {
my $value = ''; my $value = '';
my $curr = $data->{$path}; my $curr = $data->{$path};
foreach my $perm (sort keys %$curr) { foreach my $perm (sort keys %$curr) {
$value .= "\n" if $value; $value .= "\n" if $value;
$value .= $perm; $value .= $perm;
$value .= " (*)" if $curr->{$perm}; $value .= " (*)" if $curr->{$perm};
} }
push @$table_data, { path => $path, permissions => $value }; push @$table_data, { path => $path, permissions => $value };
} }
PVE::CLIFormatter::print_api_result($table_data, $table_schema, undef, $options); PVE::CLIFormatter::print_api_result($table_data, $table_schema, undef, $options);
print "Permissions marked with '(*)' have the 'propagate' flag set.\n"; print "Permissions marked with '(*)' have the 'propagate' flag set.\n";
} else { } else {
PVE::CLIFormatter::print_api_result($data, $schema, undef, $options); PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
} }
}; };
@ -89,28 +92,32 @@ __PACKAGE__->register_method({
method => 'GET', method => 'GET',
description => 'Retrieve effective permissions of given token.', description => 'Retrieve effective permissions of given token.',
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
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(
description => "Only dump this specific path, not the whole tree.", 'acl-path',
optional => 1, {
}), description => "Only dump this specific path, not the whole tree.",
}, optional => 1,
},
),
},
}, },
returns => { returns => {
type => 'object', type => 'object',
description => 'Hash of structure "path" => "privilege" => "propagate boolean".', description => 'Hash of structure "path" => "privilege" => "propagate boolean".',
}, },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
my $token_subid = extract_param($param, "tokenid"); my $token_subid = extract_param($param, "tokenid");
$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',
@ -118,34 +125,35 @@ __PACKAGE__->register_method({
method => 'PUT', method => 'PUT',
description => 'Delete TFA entries from a user.', description => 'Delete TFA entries from a user.',
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
userid => get_standard_option('userid'), userid => get_standard_option('userid'),
id => { id => {
description => "The TFA ID, if none provided, all TFA entries will be deleted.", description => "The TFA ID, if none provided, all TFA entries will be deleted.",
type => 'string', type => 'string',
optional => 1, optional => 1,
}, },
}, },
}, },
returns => { type => 'null' }, returns => { type => 'null' },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
my $userid = extract_param($param, "userid"); my $userid = extract_param($param, "userid");
my $tfa_id = extract_param($param, "id"); my $tfa_id = extract_param($param, "id");
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');
if (defined($tfa_id)) { if (defined($tfa_id)) {
$tfa_cfg->api_delete_tfa($userid, $tfa_id); $tfa_cfg->api_delete_tfa($userid, $tfa_id);
} else { } else {
$tfa_cfg->remove_user($userid); $tfa_cfg->remove_user($userid);
} }
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',
@ -153,97 +161,181 @@ __PACKAGE__->register_method({
method => 'GET', method => 'GET',
description => "List TFA entries.", description => "List TFA entries.",
parameters => { parameters => {
additionalProperties => 0, additionalProperties => 0,
properties => { properties => {
userid => get_standard_option('userid', { optional => 1 }), userid => get_standard_option('userid', { optional => 1 }),
}, },
}, },
returns => { type => 'null' }, returns => { type => 'null' },
code => sub { code => sub {
my ($param) = @_; my ($param) = @_;
my $userid = extract_param($param, "userid"); my $userid = extract_param($param, "userid");
my sub format_tfa_entries : prototype($;$) { my sub format_tfa_entries : prototype($;$) {
my ($entries, $indent) = @_; my ($entries, $indent) = @_;
$indent //= ''; $indent //= '';
my $nl = ''; my $nl = '';
for my $entry (@$entries) { for my $entry (@$entries) {
my ($id, $ty, $desc) = ($entry->@{qw/id type description/}); my ($id, $ty, $desc) = ($entry->@{qw/id type description/});
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)) {
format_tfa_entries($tfa_cfg->api_list_user_tfa($userid)); format_tfa_entries($tfa_cfg->api_list_user_tfa($userid));
} else { } else {
my $result = $tfa_cfg->api_list_tfa('', 1); my $result = $tfa_cfg->api_list_tfa('', 1);
my $nl = ''; my $nl = '';
for my $entry (sort { $a->{userid} cmp $b->{userid} } @$result) { for my $entry (sort { $a->{userid} cmp $b->{userid} } @$result) {
print "${nl}$entry->{userid}:\n"; print "${nl}$entry->{userid}:\n";
format_tfa_entries($entry->{entries}, ' '); format_tfa_entries($entry->{entries}, ' ');
$nl = "\n"; $nl = "\n";
} }
} }
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',
tfa => { 'index',
delete => [ __PACKAGE__, 'delete_tfa', ['userid'] ], [],
list => [ __PACKAGE__, 'list_tfa', ['userid'] ], {},
unlock => [ 'PVE::API2::User', 'unlock_tfa', ['userid'] ], $print_api_result,
}, $PVE::RESTHandler::standard_output_options,
token => { ],
add => [ 'PVE::API2::User', 'generate_token', ['userid', 'tokenid'], {}, $print_api_result, $PVE::RESTHandler::standard_output_options ], permissions => [
modify => [ 'PVE::API2::User', 'update_token_info', ['userid', 'tokenid'], {}, $print_api_result, $PVE::RESTHandler::standard_output_options ], 'PVE::API2::AccessControl',
delete => [ 'PVE::API2::User', 'remove_token', ['userid', 'tokenid'], {}, $print_api_result, $PVE::RESTHandler::standard_output_options ], 'permissions',
remove => { alias => 'delete' }, ['userid'],
list => [ '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], $print_perm_result,
} $PVE::RESTHandler::standard_output_options,
],
tfa => {
delete => [__PACKAGE__, 'delete_tfa', ['userid']],
list => [__PACKAGE__, 'list_tfa', ['userid']],
unlock => ['PVE::API2::User', 'unlock_tfa', ['userid']],
},
token => {
add => [
'PVE::API2::User',
'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' },
list => [
'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 => [
my ($res) = @_; 'PVE::API2::AccessControl',
print "$res->{ticket}\n"; 'create_ticket',
}], ['username'],
undef,
sub {
my ($res) = @_;
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

@ -23,7 +23,7 @@ sub type {
} }
my $props = get_standard_option('realm-sync-options', { my $props = get_standard_option('realm-sync-options', {
realm => get_standard_option('realm'), realm => get_standard_option('realm'),
}); });
sub properties { sub properties {
@ -32,20 +32,20 @@ sub properties {
sub options { sub options {
my $options = { my $options = {
enabled => { optional => 1 }, enabled => { optional => 1 },
schedule => {}, schedule => {},
comment => { optional => 1 }, comment => { optional => 1 },
scope => {}, scope => {},
}; };
for my $opt (keys %$props) { for my $opt (keys %$props) {
next if defined($options->{$opt}); next if defined($options->{$opt});
# ignore legacy props from realm-sync schema # ignore legacy props from realm-sync schema
next if $opt eq 'full' || $opt eq 'purge'; next if $opt eq 'full' || $opt eq 'purge';
if ($props->{$opt}->{optional}) { if ($props->{$opt}->{optional}) {
$options->{$opt} = { optional => 1 }; $options->{$opt} = { optional => 1 };
} else { } else {
$options->{$opt} = {}; $options->{$opt} = {};
} }
} }
$options->{realm}->{fixed} = 1; $options->{realm}->{fixed} = 1;
@ -69,8 +69,8 @@ sub createSchema {
my $opts = $class->options(); my $opts = $class->options();
for my $opt (keys $schema->{properties}->%*) { for my $opt (keys $schema->{properties}->%*) {
next if defined($opts->{$opt}) || $opt eq 'id'; next if defined($opts->{$opt}) || $opt eq 'id';
delete $schema->{properties}->{$opt}; delete $schema->{properties}->{$opt};
} }
return $schema; return $schema;
@ -82,9 +82,9 @@ sub updateSchema {
my $opts = $class->options(); my $opts = $class->options();
for my $opt (keys $schema->{properties}->%*) { for my $opt (keys $schema->{properties}->%*) {
next if defined($opts->{$opt}); next if defined($opts->{$opt});
next if $opt eq 'id' || $opt eq 'delete'; next if $opt eq 'id' || $opt eq 'delete';
delete $schema->{properties}->{$opt}; delete $schema->{properties}->{$opt};
} }
return $schema; return $schema;
@ -111,9 +111,9 @@ sub save_state {
my $statefile = "$statedir/realm-sync-$id.json"; my $statefile = "$statedir/realm-sync-$id.json";
if (defined($state)) { if (defined($state)) {
PVE::Tools::file_set_contents($statefile, encode_json($state)); PVE::Tools::file_set_contents($statefile, encode_json($state));
} else { } else {
unlink $statefile or $! == ENOENT or die "could not delete state for $id - $!\n"; unlink $statefile or $! == ENOENT or die "could not delete state for $id - $!\n";
} }
return undef; return undef;
@ -123,7 +123,7 @@ sub run {
my ($class, $conf, $id, $schedule) = @_; my ($class, $conf, $id, $schedule) = @_;
for my $opt (keys %$conf) { for my $opt (keys %$conf) {
delete $conf->{$opt} if !defined($props->{$opt}); delete $conf->{$opt} if !defined($props->{$opt});
} }
my $realm = $conf->{realm}; my $realm = $conf->{realm};
@ -133,69 +133,88 @@ 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(
my $members = PVE::Cluster::get_members(); 'realm-sync',
undef,
sub {
my $members = PVE::Cluster::get_members();
my $state = get_state($id); my $state = get_state($id);
my $last_node = $state->{node} // $nodename; my $last_node = $state->{node} // $nodename;
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 =
return 0 if !defined($next_sync) || $now < $next_sync; # not yet its (next) turn PVE::CalendarEvent::compute_next_event($cal_spec, $parsed->{starttime});
} return 0 if !defined($next_sync) || $now < $next_sync; # not yet its (next) turn
# check if still running and node is online }
my $tasks = PVE::Cluster::get_tasklist(); # check if still running and node is online
for my $task (@$tasks) { my $tasks = PVE::Cluster::get_tasklist();
next if $task->{upid} ne $last_upid; for my $task (@$tasks) {
last if defined($task->{endtime}); # it's already finished next if $task->{upid} ne $last_upid;
last if !$last_node_online; # it's not finished and the node is offline last if defined($task->{endtime}); # it's already finished
return 0; # not finished and online last if !$last_node_online; # it's not finished and the node is offline
} return 0; # not finished and 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 } elsif (defined($last_time) && ($last_time + 60) > $now && $last_node_online) {
return 0; # another node started this job in the last 60 seconds and is still online
} return 0;
}
# any of the following conditions should be true here: # any of the following conditions should be true here:
# * it was started on another node but that node is offline now # * it was started on another node but that node is offline now
# * 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(
node => $nodename, $id,
time => $now, {
}); node => $nodename,
return 1; time => $now,
}); },
);
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(
if ($err && !$upid) { 'realm-sync',
save_state($id, { undef,
node => $nodename, sub {
time => $now, if ($err && !$upid) {
error => $err, save_state(
}); $id,
die "$err\n"; {
} node => $nodename,
time => $now,
error => $err,
},
);
die "$err\n";
}
save_state($id, { save_state(
node => $nodename, $id,
upid => $upid, {
}); node => $nodename,
}); upid => $upid,
die $@ if $@; },
return $upid; );
},
);
die $@ if $@;
return $upid;
} }
return "OK"; # all other cases should not run the sync on this node return "OK"; # all other cases should not run the sync on this node

View File

@ -32,27 +32,28 @@ 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;
} }
} }
} }
} }
# get roles of current user/token on checked path - this already handles # get roles of current user/token on checked path - this already handles
@ -64,19 +65,19 @@ my $compile_acl_path = sub {
# apply roles inherited from pools # apply roles inherited from pools
if ($data->{poolroles}->{$path}) { if ($data->{poolroles}->{$path}) {
# NoAccess must not be trumped by pool ACLs # NoAccess must not be trumped by pool ACLs
if (!defined($roles->{NoAccess})) { if (!defined($roles->{NoAccess})) {
if ($data->{poolroles}->{$path}->{NoAccess}) { if ($data->{poolroles}->{$path}->{NoAccess}) {
# 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});
} }
} }
} }
} }
# cache roles # cache roles
@ -86,29 +87,29 @@ my $compile_acl_path = sub {
# flag value, a key being defined means the priv is set # flag value, a key being defined means the priv is set
my $privs = {}; my $privs = {};
foreach my $role (keys %$roles) { foreach my $role (keys %$roles) {
if (my $privset = $cfg->{roles}->{$role}) { if (my $privset = $cfg->{roles}->{$role}) {
foreach my $p (keys %$privset) { foreach my $p (keys %$privset) {
# set priv '$p' to propagated iff any of the set roles # set priv '$p' to propagated iff any of the set roles
# containing it have the propagated flag set # containing it have the propagated flag set
$privs->{$p} ||= $roles->{$role}; $privs->{$p} ||= $roles->{$role};
} }
} }
} }
# intersect user and token permissions # intersect user and token permissions
if ($username && $username ne 'root@pam') { if ($username && $username ne 'root@pam') {
# 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 };
} }
foreach my $priv (keys %$privs) { foreach my $priv (keys %$privs) {
# safeguard, this should never happen anyway # safeguard, this should never happen anyway
delete $privs->{$priv} if !defined($privs->{$priv}); delete $privs->{$priv} if !defined($privs->{$priv});
} }
# cache privs # cache privs
@ -136,31 +137,31 @@ sub permissions {
my ($self, $user, $path) = @_; my ($self, $user, $path) = @_;
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)) {
# this shouldn't happen! # this shouldn't happen!
warn "internal error: ACL check called for undefined ACL path!\n"; warn "internal error: ACL check called for undefined ACL path!\n";
return {}; return {};
} }
if (PVE::AccessControl::pve_verify_tokenid($user, 1)) { if (PVE::AccessControl::pve_verify_tokenid($user, 1)) {
my ($username, $token) = PVE::AccessControl::split_tokenid($user); my ($username, $token) = PVE::AccessControl::split_tokenid($user);
my $cfg = $self->{user_cfg}; my $cfg = $self->{user_cfg};
my $token_info = $cfg->{users}->{$username}->{tokens}->{$token}; my $token_info = $cfg->{users}->{$username}->{tokens}->{$token};
return {} if !$token_info; return {} if !$token_info;
# ensure cache for user is populated # ensure cache for user is populated
my $user_perms = $self->permissions($username, $path); my $user_perms = $self->permissions($username, $path);
# return user privs for non-privsep tokens # return user privs for non-privsep tokens
return $user_perms if !$token_info->{privsep}; return $user_perms if !$token_info->{privsep};
} else { } else {
$user = PVE::AccessControl::verify_username($user, 1); $user = PVE::AccessControl::verify_username($user, 1);
return {} if !$user; return {} if !$user;
} }
my $cache = $self->{aclcache}; my $cache = $self->{aclcache};
@ -181,53 +182,57 @@ sub compute_api_permission {
my $res = {}; my $res = {};
my $priv_re_map = { my $priv_re_map = {
vms => qr/VM\.|Permissions\.Modify/, vms => qr/VM\.|Permissions\.Modify/,
access => qr/(User|Group)\.|Permissions\.Modify/, access => qr/(User|Group)\.|Permissions\.Modify/,
storage => qr/Datastore\.|Permissions\.Modify/, storage => qr/Datastore\.|Permissions\.Modify/,
nodes => qr/Sys\.|Permissions\.Modify/, nodes => qr/Sys\.|Permissions\.Modify/,
sdn => qr/SDN\.|Permissions\.Modify/, sdn => qr/SDN\.|Permissions\.Modify/,
dc => qr/Sys\.Audit|Sys\.Modify|SDN\./, dc => qr/Sys\.Audit|Sys\.Modify|SDN\./,
mapping => qr/Mapping\.|Permissions.Modify/, mapping => qr/Mapping\.|Permissions.Modify/,
}; };
map { $res->{$_} = {} } keys %$priv_re_map; map { $res->{$_} = {} } keys %$priv_re_map;
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(
my ($path, $node) = @_; "/",
push @$defined_paths, $path; $usercfg->{acl_root},
}); sub {
my ($path, $node) = @_;
push @$defined_paths, $path;
},
);
my $checked_paths = {}; my $checked_paths = {};
foreach my $path (@$required_paths, @$defined_paths) { foreach my $path (@$required_paths, @$defined_paths) {
next if $checked_paths->{$path}; next if $checked_paths->{$path};
$checked_paths->{$path} = 1; $checked_paths->{$path} = 1;
my $path_perm = $self->permissions($authuser, $path); my $path_perm = $self->permissions($authuser, $path);
my $toplevel = ($path =~ /^\/(\w+)/) ? $1 : 'dc'; my $toplevel = ($path =~ /^\/(\w+)/) ? $1 : 'dc';
if ($toplevel eq 'pool') { if ($toplevel eq 'pool') {
foreach my $priv (keys %$path_perm) { foreach my $priv (keys %$path_perm) {
next if !defined($path_perm->{$priv}); next if !defined($path_perm->{$priv});
if ($priv =~ m/^VM\./) { if ($priv =~ m/^VM\./) {
$res->{vms}->{$priv} = 1; $res->{vms}->{$priv} = 1;
} elsif ($priv =~ m/^Datastore\./) { } elsif ($priv =~ m/^Datastore\./) {
$res->{storage}->{$priv} = 1; $res->{storage}->{$priv} = 1;
} elsif ($priv eq 'Permissions.Modify') { } elsif ($priv eq 'Permissions.Modify') {
$res->{storage}->{$priv} = 1; $res->{storage}->{$priv} = 1;
$res->{vms}->{$priv} = 1; $res->{vms}->{$priv} = 1;
} }
} }
} else { } else {
my $priv_regex = $priv_re_map->{$toplevel} // next; my $priv_regex = $priv_re_map->{$toplevel} // next;
foreach my $priv (keys %$path_perm) { foreach my $priv (keys %$path_perm) {
next if !defined($path_perm->{$priv}); next if !defined($path_perm->{$priv});
next if $priv !~ m/^($priv_regex)/; next if $priv !~ m/^($priv_regex)/;
$res->{$toplevel}->{$priv} = 1; $res->{$toplevel}->{$priv} = 1;
} }
} }
} }
return $res; return $res;
@ -238,43 +243,47 @@ sub get_effective_permissions {
# default / top level paths # default / top level paths
my $paths = { my $paths = {
'/' => 1, '/' => 1,
'/access' => 1, '/access' => 1,
'/access/groups' => 1, '/access/groups' => 1,
'/nodes' => 1, '/nodes' => 1,
'/pool' => 1, '/pool' => 1,
'/sdn' => 1, '/sdn' => 1,
'/storage' => 1, '/storage' => 1,
'/vms' => 1, '/vms' => 1,
}; };
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(
my ($path, $node) = @_; "/",
$paths->{$path} = 1; $cfg->{acl_root},
}); sub {
my ($path, $node) = @_;
$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;
} }
} }
my $perms = {}; my $perms = {};
foreach my $path (keys %$paths) { foreach my $path (keys %$paths) {
my $path_perms = $self->permissions($user, $path); my $path_perms = $self->permissions($user, $path);
foreach my $priv (keys %$path_perms) { foreach my $priv (keys %$path_perms) {
delete $path_perms->{$priv} if !defined($path_perms->{$priv}); delete $path_perms->{$priv} if !defined($path_perms->{$priv});
} }
# filter paths where user has NO permissions # filter paths where user has NO permissions
$perms->{$path} = $path_perms if %$path_perms; $perms->{$path} = $path_perms if %$path_perms;
} }
return $perms; return $perms;
} }
@ -285,15 +294,15 @@ sub check {
my $perm = $self->permissions($user, $path); my $perm = $self->permissions($user, $path);
foreach my $priv (@$privs) { foreach my $priv (@$privs) {
PVE::AccessControl::verify_privname($priv); PVE::AccessControl::verify_privname($priv);
if (!defined($perm->{$priv})) { if (!defined($perm->{$priv})) {
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) = @_;
@ -302,26 +311,26 @@ sub check_any {
my $found = 0; my $found = 0;
foreach my $priv (@$privs) { foreach my $priv (@$privs) {
PVE::AccessControl::verify_privname($priv); PVE::AccessControl::verify_privname($priv);
if (defined($perm->{$priv})) { if (defined($perm->{$priv})) {
$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) = @_;
if ($any) { if ($any) {
return $self->check_any($username, $path, $privs, $noerr); return $self->check_any($username, $path, $privs, $noerr);
} else { } else {
return $self->check($username, $path, $privs, $noerr); return $self->check($username, $path, $privs, $noerr);
} }
} }
@ -336,12 +345,12 @@ sub check_sdn_bridge {
my $cfg = $self->{user_cfg}; my $cfg = $self->{user_cfg};
my $bridge_acl = PVE::AccessControl::find_acl_tree_node($cfg->{acl_root}, $path); my $bridge_acl = PVE::AccessControl::find_acl_tree_node($cfg->{acl_root}, $path);
if ($bridge_acl) { if ($bridge_acl) {
# check access to VLANs # check access to VLANs
my $vlans = $bridge_acl->{children}; my $vlans = $bridge_acl->{children};
for my $vlan (keys %$vlans) { for my $vlan (keys %$vlans) {
my $vlanpath = "$path/$vlan"; my $vlanpath = "$path/$vlan";
return 1 if $self->check_any($username, $vlanpath, $privs, 1); return 1 if $self->check_any($username, $vlanpath, $privs, 1);
} }
} }
# repeat check, but fatal # repeat check, but fatal
@ -382,10 +391,10 @@ sub check_vm_perm {
my $cfg = $self->{user_cfg}; my $cfg = $self->{user_cfg};
if ($pool) { if ($pool) {
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,11 +412,11 @@ 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};
} }
} }
return $groups; return $groups;
@ -420,11 +429,11 @@ sub group_member_join {
my $cfg = $self->{user_cfg}; my $cfg = $self->{user_cfg};
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;
} }
} }
return $users; return $users;
@ -433,15 +442,15 @@ 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/.+$|) {
push @$testperms, 'VM.Allocate'; push @$testperms, 'VM.Allocate';
} elsif ($path =~ m|^/pool/.+$|) { } elsif ($path =~ m|^/pool/.+$|) {
push @$testperms, 'Pool.Allocate'; push @$testperms, 'Pool.Allocate';
} }
return $self->check_any($username, $path, $testperms, $noerr); return $self->check_any($username, $path, $testperms, $noerr);
@ -457,85 +466,85 @@ sub exec_api2_perm_check {
die "no permission test specified" if !$test; die "no permission test specified" if !$test;
if ($test eq 'and') { if ($test eq 'and') {
while (my $subcheck = $check->[$ind++]) { while (my $subcheck = $check->[$ind++]) {
$self->exec_api2_perm_check($subcheck, $username, $param); $self->exec_api2_perm_check($subcheck, $username, $param);
} }
return 1; return 1;
} elsif ($test eq 'or') { } elsif ($test eq 'or') {
while (my $subcheck = $check->[$ind++]) { while (my $subcheck = $check->[$ind++]) {
return 1 if $self->exec_api2_perm_check($subcheck, $username, $param, 1); return 1 if $self->exec_api2_perm_check($subcheck, $username, $param, 1);
} }
return 0 if $noerr; return 0 if $noerr;
raise_perm_exc(); raise_perm_exc();
} elsif ($test eq 'perm') { } elsif ($test eq 'perm') {
my ($t, $tmplpath, $privs, %options) = @$check; my ($t, $tmplpath, $privs, %options) = @$check;
my $any = $options{any}; my $any = $options{any};
die "missing parameters" if !($tmplpath && $privs); die "missing parameters" if !($tmplpath && $privs);
my $require_param = $options{require_param}; my $require_param = $options{require_param};
if ($require_param && !defined($param->{$require_param})) { if ($require_param && !defined($param->{$require_param})) {
return 0 if $noerr; return 0 if $noerr;
raise_perm_exc(); raise_perm_exc();
} }
my $path = PVE::Tools::template_replace($tmplpath, $param); my $path = PVE::Tools::template_replace($tmplpath, $param);
my $normpath = PVE::AccessControl::normalize_path($path); my $normpath = PVE::AccessControl::normalize_path($path);
warn "Failed to normalize '$path'\n" if !defined($normpath) && defined($path); warn "Failed to normalize '$path'\n" if !defined($normpath) && defined($path);
return $self->check_full($username, $normpath, $privs, $any, $noerr); return $self->check_full($username, $normpath, $privs, $any, $noerr);
} elsif ($test eq 'userid-group') { } elsif ($test eq 'userid-group') {
my $userid = $param->{userid}; my $userid = $param->{userid};
my ($t, $privs, %options) = @$check; my ($t, $privs, %options) = @$check;
my $check_existing_user = !$options{groups_param} || $options{groups_param} ne 'create'; my $check_existing_user = !$options{groups_param} || $options{groups_param} ne 'create';
return 0 if $check_existing_user && !$self->check_user_exist($userid, $noerr); return 0 if $check_existing_user && !$self->check_user_exist($userid, $noerr);
# check permission for ALL groups (and thus ALL users) # check permission for ALL groups (and thus ALL users)
if (!$self->check_any($username, "/access/groups", $privs, 1)) { if (!$self->check_any($username, "/access/groups", $privs, 1)) {
# list of groups $username has any of $privs on # list of groups $username has any of $privs on
my $groups = $self->filter_groups($username, $privs, 1); my $groups = $self->filter_groups($username, $privs, 1);
if ($options{groups_param}) { if ($options{groups_param}) {
# does $username have any of $privs on all new/updated/.. groups? # does $username have any of $privs on all new/updated/.. groups?
my @group_param = PVE::Tools::split_list($param->{groups}); my @group_param = PVE::Tools::split_list($param->{groups});
raise_perm_exc("/access/groups, " . join("|", @$privs)) if !scalar(@group_param); raise_perm_exc("/access/groups, " . join("|", @$privs)) if !scalar(@group_param);
foreach my $pg (@group_param) { foreach my $pg (@group_param) {
raise_perm_exc("/access/groups/$pg, " . join("|", @$privs)) raise_perm_exc("/access/groups/$pg, " . join("|", @$privs))
if !$groups->{$pg}; if !$groups->{$pg};
} }
} }
if ($check_existing_user) { if ($check_existing_user) {
# does $username have any of $privs on any existing group of $userid # does $username have any of $privs on any existing group of $userid
my $allowed_users = $self->group_member_join([keys %$groups]); my $allowed_users = $self->group_member_join([keys %$groups]);
if (!$allowed_users->{$userid}) { if (!$allowed_users->{$userid}) {
return 0 if $noerr; return 0 if $noerr;
raise_perm_exc(); raise_perm_exc();
} }
} }
} }
return 1; return 1;
} elsif ($test eq 'userid-param') { } elsif ($test eq 'userid-param') {
my ($userid, undef, $realm) = PVE::AccessControl::verify_username($param->{userid}); my ($userid, undef, $realm) = PVE::AccessControl::verify_username($param->{userid});
my ($t, $subtest) = @$check; my ($t, $subtest) = @$check;
die "missing parameters" if !$subtest; die "missing parameters" if !$subtest;
if ($subtest eq 'self') { if ($subtest eq 'self') {
return 0 if !$self->check_user_exist($userid, $noerr); return 0 if !$self->check_user_exist($userid, $noerr);
return 1 if $username eq $userid; return 1 if $username eq $userid;
return 0 if $noerr; return 0 if $noerr;
raise_perm_exc(); raise_perm_exc();
} elsif ($subtest eq 'Realm.AllocateUser') { } elsif ($subtest eq 'Realm.AllocateUser') {
my $path = "/access/realm/$realm"; my $path = "/access/realm/$realm";
return $self->check($username, $path, ['Realm.AllocateUser'], $noerr); return $self->check($username, $path, ['Realm.AllocateUser'], $noerr);
} else { } else {
die "unknown userid-param test"; die "unknown userid-param test";
} }
} elsif ($test eq 'perm-modify') { } elsif ($test eq 'perm-modify') {
my ($t, $tmplpath) = @$check; my ($t, $tmplpath) = @$check;
my $path = PVE::Tools::template_replace($tmplpath, $param); my $path = PVE::Tools::template_replace($tmplpath, $param);
$path = PVE::AccessControl::normalize_path($path); $path = PVE::AccessControl::normalize_path($path);
return 0 if !defined($path); # should already die in API2::ACL return 0 if !defined($path); # should already die in API2::ACL
return $self->check_perm_modify($username, $path, $noerr); return $self->check_perm_modify($username, $path, $noerr);
} 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) = @_;
@ -551,7 +560,7 @@ sub check_api2_permissions {
return 1 if $perm->{user} && $perm->{user} eq 'all'; return 1 if $perm->{user} && $perm->{user} eq 'all';
return $self->exec_api2_perm_check($perm->{check}, $username, $param) return $self->exec_api2_perm_check($perm->{check}, $username, $param)
if $perm->{check}; if $perm->{check};
raise_perm_exc(); raise_perm_exc();
} }
@ -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 {
@ -594,32 +602,36 @@ sub init_request {
my $userconfig; # we use this for regression tests my $userconfig; # we use this for regression tests
foreach my $p (keys %params) { foreach my $p (keys %params) {
if ($p eq 'userconfig') { if ($p eq 'userconfig') {
$userconfig = $params{$p}; $userconfig = $params{$p};
} else { } else {
die "unknown parameter '$p'"; die "unknown parameter '$p'";
} }
} }
eval { eval {
$self->{aclcache} = {}; $self->{aclcache} = {};
if ($userconfig) { if ($userconfig) {
my $ucdata = PVE::Tools::file_get_contents($userconfig); my $ucdata = PVE::Tools::file_get_contents($userconfig);
my $cfg = PVE::AccessControl::parse_user_config($userconfig, $ucdata); my $cfg = PVE::AccessControl::parse_user_config($userconfig, $ucdata);
$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}
$self->{aclversion} = $ucvers; || !defined($self->{aclversion})
my $cfg = PVE::Cluster::cfs_read_file('user.cfg'); || !defined($ucvers)
$self->{user_cfg} = $cfg; || ($ucvers ne $self->{aclversion})
} ) {
} $self->{aclversion} = $ucvers;
my $cfg = PVE::Cluster::cfs_read_file('user.cfg');
$self->{user_cfg} = $cfg;
}
}
}; };
if (my $err = $@) { if (my $err = $@) {
$self->{user_cfg} = {}; $self->{user_cfg} = {};
die "Unable to load access control list: $err"; die "Unable to load access control list: $err";
} }
} }
@ -654,17 +666,17 @@ sub reauth_user_for_user_modification : prototype($$$$;$) {
# Regular users need to confirm their password to change TFA settings. # Regular users need to confirm their password to change TFA settings.
if ($authuser ne 'root@pam') { if ($authuser ne 'root@pam') {
raise_param_exc({ $param_name => 'password is required to modify user' }) raise_param_exc({ $param_name => 'password is required to modify user' })
if !defined($password); if !defined($password);
($authuser, my $auth_username, my $auth_realm) = ($authuser, my $auth_username, my $auth_realm) =
PVE::AccessControl::verify_username($authuser); PVE::AccessControl::verify_username($authuser);
my $domain_cfg = PVE::Cluster::cfs_read_file('domains.cfg'); my $domain_cfg = PVE::Cluster::cfs_read_file('domains.cfg');
my $cfg = $domain_cfg->{ids}->{$auth_realm}; my $cfg = $domain_cfg->{ids}->{$auth_realm};
die "auth domain '$auth_realm' does not exist\n" if !$cfg; die "auth domain '$auth_realm' does not exist\n" if !$cfg;
my $plugin = PVE::Auth::Plugin->lookup($cfg->{type}); my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
$plugin->authenticate_user($cfg, $auth_realm, $auth_username, $password); $plugin->authenticate_user($cfg, $auth_realm, $auth_username, $password);
} }
return wantarray ? ($userid, $ruid, $realm) : $userid; return wantarray ? ($userid, $ruid, $realm) : $userid;

View File

@ -16,16 +16,16 @@ my $parse_token_cfg = sub {
my @lines = split(/\n/, $raw); my @lines = split(/\n/, $raw);
foreach my $line (@lines) { foreach my $line (@lines) {
next if $line =~ m/^\s*$/; next if $line =~ m/^\s*$/;
if ($line =~ m/^(\S+) (\S+)$/) { if ($line =~ m/^(\S+) (\S+)$/) {
if (PVE::AccessControl::pve_verify_tokenid($1, 1)) { if (PVE::AccessControl::pve_verify_tokenid($1, 1)) {
$parsed->{$1} = $2; $parsed->{$1} = $2;
next; next;
} }
} }
warn "skipping invalid token.cfg entry\n"; warn "skipping invalid token.cfg entry\n";
} }
return $parsed; return $parsed;
@ -36,7 +36,7 @@ my $write_token_cfg = sub {
my $raw = ''; my $raw = '';
foreach my $tokenid (sort keys %$data) { foreach my $tokenid (sort keys %$data) {
$raw .= "$tokenid $data->{$tokenid}\n"; $raw .= "$tokenid $data->{$tokenid}\n";
} }
return $raw; return $raw;
@ -49,16 +49,20 @@ 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(
my $uuid = UUID::uuid(); 'priv/token.cfg',
my $token_cfg = PVE::Cluster::cfs_read_file('priv/token.cfg'); 10,
sub {
my $uuid = UUID::uuid();
my $token_cfg = PVE::Cluster::cfs_read_file('priv/token.cfg');
$token_cfg->{$tokenid} = $uuid; $token_cfg->{$tokenid} = $uuid;
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(
my $token_cfg = PVE::Cluster::cfs_read_file('priv/token.cfg'); 'priv/token.cfg',
10,
sub {
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

@ -30,134 +30,138 @@ my $stranger_privsep_perms = $rpcenv->get_effective_permissions('stranger@pve!pr
my $stranger_user_tests = [ my $stranger_user_tests = [
{ {
description => 'get stranger\'s perms without passing the user\'s userid', description => 'get stranger\'s perms without passing the user\'s userid',
rpcuser => 'stranger@pve', rpcuser => 'stranger@pve',
params => {}, params => {},
result => $stranger_perms, result => $stranger_perms,
}, },
{ {
description => 'get stranger\'s perms with passing the user\'s userid', description => 'get stranger\'s perms with passing the user\'s userid',
rpcuser => 'stranger@pve', rpcuser => 'stranger@pve',
params => { params => {
userid => 'stranger@pve', userid => 'stranger@pve',
}, },
result => $stranger_perms, result => $stranger_perms,
}, },
{ {
description => 'get stranger-owned non-priv-sep\'d token\'s perms from stranger user', description => 'get stranger-owned non-priv-sep\'d token\'s perms from stranger user',
rpcuser => 'stranger@pve', rpcuser => 'stranger@pve',
params => { params => {
userid => 'stranger@pve!noprivsep', userid => 'stranger@pve!noprivsep',
}, },
result => $stranger_perms, result => $stranger_perms,
}, },
{ {
description => 'get stranger-owned priv-sep\'d token\'s perms from stranger user', description => 'get stranger-owned priv-sep\'d token\'s perms from stranger user',
rpcuser => 'stranger@pve', rpcuser => 'stranger@pve',
params => { params => {
userid => 'stranger@pve!privsep', userid => 'stranger@pve!privsep',
}, },
result => $stranger_privsep_perms, result => $stranger_privsep_perms,
}, },
{ {
description => 'get auditor\'s perms from stranger user', description => 'get auditor\'s perms from stranger user',
should_fail => 1, should_fail => 1,
rpcuser => 'stranger@pve', rpcuser => 'stranger@pve',
params => { params => {
userid => 'auditor@pam', userid => 'auditor@pam',
}, },
}, },
{ {
description => 'get auditor-owned token\'s perms from stranger user', description => 'get auditor-owned token\'s perms from stranger user',
should_fail => 1, should_fail => 1,
rpcuser => 'stranger@pve', rpcuser => 'stranger@pve',
params => { params => {
userid => 'auditor@pam!noprivsep', userid => 'auditor@pam!noprivsep',
}, },
}, },
]; ];
my $stranger_nonprivsep_tests = [ my $stranger_nonprivsep_tests = [
{ {
description => 'get stranger-owned non-priv-sep\'d token\'s perms without passing the token', description =>
rpcuser => 'stranger@pve!noprivsep', 'get stranger-owned non-priv-sep\'d token\'s perms without passing the token',
params => {}, rpcuser => 'stranger@pve!noprivsep',
result => $stranger_perms, params => {},
result => $stranger_perms,
}, },
{ {
description => 'get stranger-owned non-priv-sep\'d token\'s perms with passing the token', description =>
rpcuser => 'stranger@pve!noprivsep', 'get stranger-owned non-priv-sep\'d token\'s perms with passing the token',
params => { rpcuser => 'stranger@pve!noprivsep',
userid => 'stranger@pve!noprivsep', params => {
}, userid => 'stranger@pve!noprivsep',
result => $stranger_perms, },
result => $stranger_perms,
}, },
{ {
description => 'get stranger\'s perms from stranger-owned non-priv-sep\'d token', description => 'get stranger\'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 => 'stranger@pve', userid => 'stranger@pve',
}, },
}, },
{ {
description => 'get stranger-owned priv-sep\'d token\'s perms ' description => 'get stranger-owned priv-sep\'d token\'s perms '
. 'from stranger-owned non-priv-sep\'d token', . '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 => 'stranger@pve!privsep', userid => 'stranger@pve!privsep',
}, },
}, },
{ {
description => 'get auditor-owned token\'s perms from stranger-owned non-priv-sep\'d token', description =>
should_fail => 1, 'get auditor-owned token\'s perms from stranger-owned non-priv-sep\'d token',
rpcuser => 'stranger@pve!noprivsep', should_fail => 1,
params => { rpcuser => 'stranger@pve!noprivsep',
userid => 'auditor@pam!noprivsep', params => {
} 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 =>
rpcuser => 'stranger@pve!privsep', 'get stranger-owned priv-sep\'d token\'s perms without passing the token',
params => {}, rpcuser => 'stranger@pve!privsep',
result => $stranger_privsep_perms, params => {},
result => $stranger_privsep_perms,
}, },
{ {
description => 'get stranger-owned priv-sep\'d token\'s perms with passing the token', description => 'get stranger-owned priv-sep\'d token\'s perms with passing the token',
rpcuser => 'stranger@pve!privsep', rpcuser => 'stranger@pve!privsep',
params => { params => {
userid => 'stranger@pve!privsep', userid => 'stranger@pve!privsep',
}, },
result => $stranger_privsep_perms, result => $stranger_privsep_perms,
}, },
{ {
description => 'get stranger\'s perms from stranger-owned priv-sep\'d token', description => 'get stranger\'s perms from stranger-owned priv-sep\'d token',
should_fail => 1, should_fail => 1,
rpcuser => 'stranger@pve!privsep', rpcuser => 'stranger@pve!privsep',
params => { params => {
userid => 'stranger@pve', userid => 'stranger@pve',
}, },
}, },
{ {
description => 'get stranger-owned non-priv-sep\'d token\'s perms ' description => 'get stranger-owned non-priv-sep\'d token\'s perms '
. 'from stranger-owned priv-sep\'d token', . 'from stranger-owned priv-sep\'d token',
should_fail => 1, should_fail => 1,
rpcuser => 'stranger@pve!privsep', rpcuser => 'stranger@pve!privsep',
params => { params => {
userid => 'stranger@pve!noprivsep', userid => 'stranger@pve!noprivsep',
}, },
}, },
{ {
description => 'get auditor-owned token\'s perms from stranger-owned priv-sep\'d token', description => 'get auditor-owned token\'s perms from stranger-owned priv-sep\'d token',
should_fail => 1, should_fail => 1,
rpcuser => 'stranger@pve!privsep', rpcuser => 'stranger@pve!privsep',
params => { params => {
userid => 'auditor@pam!noprivsep', userid => 'auditor@pam!noprivsep',
} },
}, },
]; ];
@ -167,134 +171,137 @@ my $auditor_privsep_perms = $rpcenv->get_effective_permissions('auditor@pam!priv
my $auditor_user_tests = [ my $auditor_user_tests = [
{ {
description => 'get auditor\'s perms without passing the user\'s userid', description => 'get auditor\'s perms without passing the user\'s userid',
rpcuser => 'auditor@pam', rpcuser => 'auditor@pam',
params => {}, params => {},
result => $auditor_perms, result => $auditor_perms,
}, },
{ {
description => 'get auditor\'s perms with passing the user\'s userid', description => 'get auditor\'s perms with passing the user\'s userid',
rpcuser => 'auditor@pam', rpcuser => 'auditor@pam',
params => { params => {
userid => 'auditor@pam', userid => 'auditor@pam',
}, },
result => $auditor_perms, result => $auditor_perms,
}, },
{ {
description => 'get auditor-owned non-priv-sep\'d token\'s perms from auditor user', description => 'get auditor-owned non-priv-sep\'d token\'s perms from auditor user',
rpcuser => 'auditor@pam', rpcuser => 'auditor@pam',
params => { params => {
userid => 'auditor@pam!noprivsep', userid => 'auditor@pam!noprivsep',
}, },
result => $auditor_perms, result => $auditor_perms,
}, },
{ {
description => 'get auditor-owned priv-sep\'d token\'s perms from auditor user', description => 'get auditor-owned priv-sep\'d token\'s perms from auditor user',
rpcuser => 'auditor@pam', rpcuser => 'auditor@pam',
params => { params => {
userid => 'auditor@pam!privsep', userid => 'auditor@pam!privsep',
}, },
result => $auditor_privsep_perms, result => $auditor_privsep_perms,
}, },
{ {
description => 'get stranger\'s perms from auditor user', description => 'get stranger\'s perms from auditor user',
rpcuser => 'auditor@pam', rpcuser => 'auditor@pam',
params => { params => {
userid => 'stranger@pve', userid => 'stranger@pve',
}, },
result => $stranger_perms, result => $stranger_perms,
}, },
{ {
description => 'get stranger-owned token\'s perms from auditor user', description => 'get stranger-owned token\'s perms from auditor user',
rpcuser => 'auditor@pam', rpcuser => 'auditor@pam',
params => { params => {
userid => 'stranger@pve!noprivsep', userid => 'stranger@pve!noprivsep',
}, },
result => $stranger_perms, result => $stranger_perms,
}, },
]; ];
my $auditor_nonprivsep_tests = [ my $auditor_nonprivsep_tests = [
{ {
description => 'get auditor-owned non-priv-sep\'d token\'s perms without passing the token', description =>
rpcuser => 'auditor@pam!noprivsep', 'get auditor-owned non-priv-sep\'d token\'s perms without passing the token',
params => {}, rpcuser => 'auditor@pam!noprivsep',
result => $auditor_perms, params => {},
result => $auditor_perms,
}, },
{ {
description => 'get auditor-owned non-priv-sep\'d token\'s perms with passing the token', description =>
rpcuser => 'auditor@pam!noprivsep', 'get auditor-owned non-priv-sep\'d token\'s perms with passing the token',
params => { rpcuser => 'auditor@pam!noprivsep',
userid => 'auditor@pam!noprivsep', params => {
}, userid => 'auditor@pam!noprivsep',
result => $auditor_perms, },
result => $auditor_perms,
}, },
{ {
description => 'get auditor\'s perms from auditor-owned non-priv-sep\'d token', description => 'get auditor\'s perms from auditor-owned non-priv-sep\'d token',
rpcuser => 'auditor@pam!noprivsep', rpcuser => 'auditor@pam!noprivsep',
params => { params => {
userid => 'auditor@pam', userid => 'auditor@pam',
}, },
result => $auditor_perms, result => $auditor_perms,
}, },
{ {
description => 'get auditor-owned priv-sep\'d token\'s perms ' description => 'get auditor-owned priv-sep\'d token\'s perms '
. 'from auditor-owned non-priv-sep\'d token', . 'from auditor-owned non-priv-sep\'d token',
rpcuser => 'auditor@pam!noprivsep', rpcuser => 'auditor@pam!noprivsep',
params => { params => {
userid => 'auditor@pam!privsep', userid => 'auditor@pam!privsep',
}, },
result => $auditor_privsep_perms, result => $auditor_privsep_perms,
}, },
{ {
description => 'get stranger-owned token\'s perms from auditor-owned non-priv-sep\'d token', description =>
rpcuser => 'auditor@pam!noprivsep', 'get stranger-owned token\'s perms from auditor-owned non-priv-sep\'d token',
params => { rpcuser => 'auditor@pam!noprivsep',
userid => 'stranger@pve!noprivsep', params => {
}, userid => 'stranger@pve!noprivsep',
result => $stranger_perms, },
result => $stranger_perms,
}, },
]; ];
my $auditor_privsep_tests = [ my $auditor_privsep_tests = [
{ {
description => 'get auditor-owned priv-sep\'d token\'s perms without passing the token', description => 'get auditor-owned priv-sep\'d token\'s perms without passing the token',
rpcuser => 'auditor@pam!privsep', rpcuser => 'auditor@pam!privsep',
params => {}, params => {},
result => $auditor_privsep_perms, result => $auditor_privsep_perms,
}, },
{ {
description => 'get auditor-owned priv-sep\'d token\'s perms with passing the token', description => 'get auditor-owned priv-sep\'d token\'s perms with passing the token',
rpcuser => 'auditor@pam!privsep', rpcuser => 'auditor@pam!privsep',
params => { params => {
userid => 'auditor@pam!privsep', userid => 'auditor@pam!privsep',
}, },
result => $auditor_privsep_perms, result => $auditor_privsep_perms,
}, },
{ {
description => 'get auditor\'s perms from auditor-owned priv-sep\'d token', description => 'get auditor\'s perms from auditor-owned priv-sep\'d token',
should_fail => 1, should_fail => 1,
rpcuser => 'auditor@pam!privsep', rpcuser => 'auditor@pam!privsep',
params => { params => {
userid => 'auditor@pam', userid => 'auditor@pam',
}, },
}, },
{ {
description => 'get auditor-owned non-priv-sep\'d token\'s perms ' description => 'get auditor-owned non-priv-sep\'d token\'s perms '
. 'from auditor-owned priv-sep\'d token', . 'from auditor-owned priv-sep\'d token',
should_fail => 1, should_fail => 1,
rpcuser => 'auditor@pam!privsep', rpcuser => 'auditor@pam!privsep',
params => { params => {
userid => 'auditor@pam!noprivsep', userid => 'auditor@pam!noprivsep',
}, },
}, },
{ {
description => 'get stranger-owned token\'s perms from auditor-owned priv-sep\'d token', description => 'get stranger-owned token\'s perms from auditor-owned priv-sep\'d token',
should_fail => 1, should_fail => 1,
rpcuser => 'auditor@pam!privsep', rpcuser => 'auditor@pam!privsep',
params => { params => {
userid => 'stranger@pve!noprivsep', userid => 'stranger@pve!noprivsep',
}, },
}, },
]; ];
@ -315,10 +322,10 @@ for my $case ($tests->@*) {
my $result = eval { $handler->handle($handler_info, $case->{params}) }; my $result = eval { $handler->handle($handler_info, $case->{params}) };
if ($@) { if ($@) {
my $should_fail = exists($case->{should_fail}) ? $case->{should_fail} : 0; my $should_fail = exists($case->{should_fail}) ? $case->{should_fail} : 0;
is(defined($@), $should_fail, "should fail: $case->{description}") || diag explain $@; is(defined($@), $should_fail, "should fail: $case->{description}") || diag explain $@;
} else { } else {
is_deeply($result, $case->{result}, $case->{description}); is_deeply($result, $case->{result}, $case->{description});
} }
} }

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

@ -8,20 +8,20 @@ use Getopt::Long;
use PVE::RPCEnvironment; use PVE::RPCEnvironment;
# example: # example:
# 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;
my $path = shift; 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);

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,7 @@ sub check_roles {
my $res = join(',', sort keys %$roles); my $res = join(',', sort keys %$roles);
die "unexpected result\nneed '${expected_result}'\ngot '$res'\n" die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
if $res ne $expected_result; if $res ne $expected_result;
print "ROLES:$path:$user:$res\n"; print "ROLES:$path:$user:$res\n";
} }
@ -34,12 +34,12 @@ sub check_permission {
my $res = join(',', sort keys %$perm); my $res = join(',', sort keys %$perm);
die "unexpected result\nneed '${expected_result}'\ngot '$res'\n" die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
if $res ne $expected_result; if $res ne $expected_result;
$perm = $rpcenv->permissions($user, $path); $perm = $rpcenv->permissions($user, $path);
$res = join(',', sort keys %$perm); $res = join(',', sort keys %$perm);
die "unexpected result (compiled)\nneed '${expected_result}'\ngot '$res'\n" die "unexpected result (compiled)\nneed '${expected_result}'\ngot '$res'\n"
if $res ne $expected_result; if $res ne $expected_result;
print "PERM:$path:$user:$res\n"; print "PERM:$path:$user:$res\n";
} }
@ -63,27 +63,27 @@ check_permission(
'alex@pve', 'alex@pve',
'/vms/300', '/vms/300',
'' # 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(
'alex@pve', 'alex@pve',
'/vms/400', '/vms/400',
'' # sorted, comma-separated expected privilege string, loosely grouped by prefix '' # sorted, comma-separated expected privilege string, loosely grouped by prefix
. 'Datastore.Allocate,Datastore.AllocateSpace,Datastore.AllocateTemplate,Datastore.Audit,' . 'Datastore.Allocate,Datastore.AllocateSpace,Datastore.AllocateTemplate,Datastore.Audit,'
. 'Group.Allocate,' . 'Group.Allocate,'
. 'Mapping.Audit,Mapping.Modify,Mapping.Use,' . 'Mapping.Audit,Mapping.Modify,Mapping.Use,'
. 'Permissions.Modify,' . 'Permissions.Modify,'
. 'Pool.Allocate,Pool.Audit,' . 'Pool.Allocate,Pool.Audit,'
. 'Realm.Allocate,Realm.AllocateUser,' . 'Realm.Allocate,Realm.AllocateUser,'
. 'SDN.Allocate,SDN.Audit,SDN.Use,' . 'SDN.Allocate,SDN.Audit,SDN.Use,'
. 'Sys.AccessNetwork,Sys.Audit,Sys.Console,Sys.Incoming,Sys.Modify,Sys.PowerMgmt,Sys.Syslog,' . 'Sys.AccessNetwork,Sys.Audit,Sys.Console,Sys.Incoming,Sys.Modify,Sys.PowerMgmt,Sys.Syslog,'
. 'User.Modify,' . 'User.Modify,'
. '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',
); );
check_roles('max@pve', '/vms/200', 'storage_manager'); check_roles('max@pve', '/vms/200', 'storage_manager');
@ -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

@ -22,7 +22,7 @@ sub check_roles {
my $res = join(',', sort keys %$roles); my $res = join(',', sort keys %$roles);
die "unexpected result\nneed '${expected_result}'\ngot '$res'\n" die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
if $res ne $expected_result; if $res ne $expected_result;
print "ROLES:$path:$user:$res\n"; print "ROLES:$path:$user:$res\n";
} }
@ -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

@ -22,7 +22,7 @@ sub check_roles {
my $res = join(',', sort keys %$roles); my $res = join(',', sort keys %$roles);
die "unexpected result\nneed '${expected_result}'\ngot '$res'\n" die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
if $res ne $expected_result; if $res ne $expected_result;
print "ROLES:$path:$user:$res\n"; print "ROLES:$path:$user:$res\n";
} }
@ -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

@ -22,15 +22,14 @@ sub check_roles {
my $res = join(',', sort keys %$roles); my $res = join(',', sort keys %$roles);
die "unexpected result\nneed '${expected_result}'\ngot '$res'\n" die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
if $res ne $expected_result; if $res ne $expected_result;
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

@ -22,12 +22,11 @@ sub check_roles {
my $res = join(',', sort keys %$roles); my $res = join(',', sort keys %$roles);
die "unexpected result\nneed '${expected_result}'\ngot '$res'\n" die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
if $res ne $expected_result; if $res ne $expected_result;
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

@ -22,7 +22,7 @@ sub check_roles {
my $res = join(',', sort keys %$roles); my $res = join(',', sort keys %$roles);
die "unexpected result\nneed '${expected_result}'\ngot '$res'\n" die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
if $res ne $expected_result; if $res ne $expected_result;
print "ROLES:$path:$user:$res\n"; print "ROLES:$path:$user:$res\n";
} }
@ -34,12 +34,12 @@ sub check_permissions {
my $res = join(',', sort keys %$perm); my $res = join(',', sort keys %$perm);
die "unexpected result\nneed '${expected_result}'\ngot '$res'\n" die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
if $res ne $expected_result; if $res ne $expected_result;
$perm = $rpcenv->permissions($user, $path); $perm = $rpcenv->permissions($user, $path);
$res = join(',', sort keys %$perm); $res = join(',', sort keys %$perm);
die "unexpected result (compiled)\nneed '${expected_result}'\ngot '$res'\n" die "unexpected result (compiled)\nneed '${expected_result}'\ngot '$res'\n"
if $res ne $expected_result; if $res ne $expected_result;
print "PERM:$path:$user:$res\n"; print "PERM:$path:$user:$res\n";
} }
@ -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

@ -22,7 +22,7 @@ sub check_roles {
my $res = join(',', sort keys %$roles); my $res = join(',', sort keys %$roles);
die "unexpected result\nneed '${expected_result}'\ngot '$res'\n" die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
if $res ne $expected_result; if $res ne $expected_result;
print "ROLES:$path:$user:$res\n"; print "ROLES:$path:$user:$res\n";
} }
@ -34,12 +34,12 @@ sub check_permissions {
my $res = join(',', sort keys %$perm); my $res = join(',', sort keys %$perm);
die "unexpected result\nneed '${expected_result}'\ngot '$res'\n" die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
if $res ne $expected_result; if $res ne $expected_result;
$perm = $rpcenv->permissions($user, $path); $perm = $rpcenv->permissions($user, $path);
$res = join(',', sort keys %$perm); $res = join(',', sort keys %$perm);
die "unexpected result (compiled)\nneed '${expected_result}'\ngot '$res'\n" die "unexpected result (compiled)\nneed '${expected_result}'\ngot '$res'\n"
if $res ne $expected_result; if $res ne $expected_result;
print "PERM:$path:$user:$res\n"; print "PERM:$path:$user:$res\n";
} }
@ -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

@ -20,7 +20,7 @@ sub check_roles {
my $res = join(',', sort keys %$roles); my $res = join(',', sort keys %$roles);
die "unexpected result\nneed '${expected_result}'\ngot '$res'\n" die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
if $res ne $expected_result; if $res ne $expected_result;
print "ROLES:$path:$user:$res\n"; print "ROLES:$path:$user:$res\n";
} }
@ -32,12 +32,12 @@ sub check_permission {
my $res = join(',', sort keys %$perm); my $res = join(',', sort keys %$perm);
die "unexpected result\nneed '${expected_result}'\ngot '$res'\n" die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
if $res ne $expected_result; if $res ne $expected_result;
$perm = $rpcenv->permissions($user, $path); $perm = $rpcenv->permissions($user, $path);
$res = join(',', sort keys %$perm); $res = join(',', sort keys %$perm);
die "unexpected result (compiled)\nneed '${expected_result}'\ngot '$res'\n" die "unexpected result (compiled)\nneed '${expected_result}'\ngot '$res'\n"
if $res ne $expected_result; if $res ne $expected_result;
print "PERM:$path:$user:$res\n"; print "PERM:$path:$user:$res\n";
} }
@ -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

@ -12,69 +12,69 @@ use PVE::API2::Domains;
my $domainscfg = { 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,
'keys' => 'some', 'keys' => 'some',
}, },
'user2@syncedrealm' => { 'user2@syncedrealm' => {
username => 'user2', username => 'user2',
enable => 1, enable => 1,
}, },
'user3@syncedrealm' => { 'user3@syncedrealm' => {
username => 'user3', username => 'user3',
enable => 1, enable => 1,
}, },
}, },
groups => { groups => {
'group1-syncedrealm' => { users => {}, }, 'group1-syncedrealm' => { users => {} },
'group2-syncedrealm' => { users => {}, }, 'group2-syncedrealm' => { users => {} },
}, },
acl_root => { acl_root => {
users => { users => {
'user3@syncedrealm' => {}, 'user3@syncedrealm' => {},
}, },
groups => {}, groups => {},
}, },
}; };
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',
}, },
], ],
groups => [ groups => [
{ {
dn => 'dc=group1,dc=syncedrealm', dn => 'dc=group1,dc=syncedrealm',
members => [ members => [
'uid=user1,dc=syncedrealm', 'uid=user1,dc=syncedrealm',
], ],
}, },
{ {
dn => 'dc=group3,dc=syncedrealm', dn => 'dc=group3,dc=syncedrealm',
members => [ members => [
'uid=nonexisting,dc=syncedrealm', 'uid=nonexisting,dc=syncedrealm',
], ],
} },
], ],
}; };
@ -83,24 +83,24 @@ 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); }
if ($filename eq 'user.cfg') { return dclone($initialusercfg); } if ($filename eq 'user.cfg') { return dclone($initialusercfg); }
die "unexpected cfs_read_file"; die "unexpected cfs_read_file";
}, },
cfs_write_file => sub { cfs_write_file => sub {
my ($filename, $data) = @_; my ($filename, $data) = @_;
if ($filename eq 'user.cfg') { if ($filename eq 'user.cfg') {
$returned_user_cfg = $data; $returned_user_cfg = $data;
return; return;
} }
die "unexpected cfs_read_file"; die "unexpected cfs_read_file";
}, },
cfs_lock_file => sub { cfs_lock_file => sub {
my ($filename, $timeout, $code) = @_; my ($filename, $timeout, $code) = @_;
return $code->(); return $code->();
}, },
); );
@ -120,21 +120,21 @@ $pve_rpcenvironment->mock(
get => sub { return bless {}, 'PVE::RPCEnvironment'; }, get => sub { return bless {}, 'PVE::RPCEnvironment'; },
get_user => sub { return 'root@pam'; }, get_user => sub { return 'root@pam'; },
fork_worker => sub { fork_worker => sub {
my ($class, $workertype, $id, $user, $code) = @_; my ($class, $workertype, $id, $user, $code) = @_;
return $code->(); return $code->();
}, },
); );
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};
}, },
query_groups => sub { query_groups => sub {
return $sync_response->{groups}; return $sync_response->{groups};
}, },
); );
@ -145,205 +145,205 @@ $pve_auth_ldap->mock(
my $tests = [ my $tests = [
[ [
"non-full without purge", "non-full without purge",
{ {
realm => 'syncedrealm', realm => 'syncedrealm',
scope => 'both', scope => 'both',
}, },
{ {
users => { users => {
'root@pam' => { username => 'root', }, 'root@pam' => { username => 'root' },
'user1@syncedrealm' => { 'user1@syncedrealm' => {
username => 'user1', username => 'user1',
enable => 1, enable => 1,
'keys' => 'some', 'keys' => 'some',
}, },
'user2@syncedrealm' => { 'user2@syncedrealm' => {
username => 'user2', username => 'user2',
enable => 1, enable => 1,
}, },
'user3@syncedrealm' => { 'user3@syncedrealm' => {
username => 'user3', username => 'user3',
enable => 1, enable => 1,
}, },
'user4@syncedrealm' => { 'user4@syncedrealm' => {
username => 'user4', username => 'user4',
enable => 1, enable => 1,
}, },
}, },
groups => { groups => {
'group1-syncedrealm' => { 'group1-syncedrealm' => {
users => { users => {
'user1@syncedrealm' => 1, 'user1@syncedrealm' => 1,
}, },
}, },
'group2-syncedrealm' => { users => {}, }, 'group2-syncedrealm' => { users => {} },
'group3-syncedrealm' => { users => {}, }, 'group3-syncedrealm' => { users => {} },
}, },
acl_root => { acl_root => {
users => { users => {
'user3@syncedrealm' => {}, 'user3@syncedrealm' => {},
}, },
groups => {}, groups => {},
}, },
}, },
], ],
[ [
"full without purge", "full without purge",
{ {
realm => 'syncedrealm', realm => 'syncedrealm',
'remove-vanished' => 'entry;properties', 'remove-vanished' => 'entry;properties',
scope => 'both', scope => 'both',
}, },
{ {
users => { users => {
'root@pam' => { username => 'root', }, 'root@pam' => { username => 'root' },
'user1@syncedrealm' => { 'user1@syncedrealm' => {
username => 'user1', username => 'user1',
enable => 1, enable => 1,
}, },
'user2@syncedrealm' => { 'user2@syncedrealm' => {
username => 'user2', username => 'user2',
enable => 1, enable => 1,
}, },
'user4@syncedrealm' => { 'user4@syncedrealm' => {
username => 'user4', username => 'user4',
enable => 1, enable => 1,
}, },
}, },
groups => { groups => {
'group1-syncedrealm' => { 'group1-syncedrealm' => {
users => { users => {
'user1@syncedrealm' => 1, 'user1@syncedrealm' => 1,
}, },
}, },
'group3-syncedrealm' => { users => {}, } 'group3-syncedrealm' => { users => {} },
}, },
acl_root => { acl_root => {
users => { users => {
'user3@syncedrealm' => {}, 'user3@syncedrealm' => {},
}, },
groups => {}, groups => {},
}, },
}, },
], ],
[ [
"non-full with purge", "non-full with purge",
{ {
realm => 'syncedrealm', realm => 'syncedrealm',
'remove-vanished' => 'acl', 'remove-vanished' => 'acl',
scope => 'both', scope => 'both',
}, },
{ {
users => { users => {
'root@pam' => { username => 'root', }, 'root@pam' => { username => 'root' },
'user1@syncedrealm' => { 'user1@syncedrealm' => {
username => 'user1', username => 'user1',
enable => 1, enable => 1,
'keys' => 'some', 'keys' => 'some',
}, },
'user2@syncedrealm' => { 'user2@syncedrealm' => {
username => 'user2', username => 'user2',
enable => 1, enable => 1,
}, },
'user3@syncedrealm' => { 'user3@syncedrealm' => {
username => 'user3', username => 'user3',
enable => 1, enable => 1,
}, },
'user4@syncedrealm' => { 'user4@syncedrealm' => {
username => 'user4', username => 'user4',
enable => 1, enable => 1,
}, },
}, },
groups => { groups => {
'group1-syncedrealm' => { 'group1-syncedrealm' => {
users => { users => {
'user1@syncedrealm' => 1, 'user1@syncedrealm' => 1,
}, },
}, },
'group2-syncedrealm' => { users => {}, }, 'group2-syncedrealm' => { users => {} },
'group3-syncedrealm' => { users => {}, }, 'group3-syncedrealm' => { users => {} },
}, },
acl_root => { acl_root => {
users => {}, users => {},
groups => {}, groups => {},
}, },
}, },
], ],
[ [
"full with purge", "full with purge",
{ {
realm => 'syncedrealm', realm => 'syncedrealm',
'remove-vanished' => 'acl;entry;properties', 'remove-vanished' => 'acl;entry;properties',
scope => 'both', scope => 'both',
}, },
{ {
users => { users => {
'root@pam' => { username => 'root', }, 'root@pam' => { username => 'root' },
'user1@syncedrealm' => { 'user1@syncedrealm' => {
username => 'user1', username => 'user1',
enable => 1, enable => 1,
}, },
'user2@syncedrealm' => { 'user2@syncedrealm' => {
username => 'user2', username => 'user2',
enable => 1, enable => 1,
}, },
'user4@syncedrealm' => { 'user4@syncedrealm' => {
username => 'user4', username => 'user4',
enable => 1, enable => 1,
}, },
}, },
groups => { groups => {
'group1-syncedrealm' => { 'group1-syncedrealm' => {
users => { users => {
'user1@syncedrealm' => 1, 'user1@syncedrealm' => 1,
}, },
}, },
'group3-syncedrealm' => { users => {}, }, 'group3-syncedrealm' => { users => {} },
}, },
acl_root => { acl_root => {
users => {}, users => {},
groups => {}, groups => {},
}, },
}, },
], ],
[ [
"don't delete properties, but users and acls", "don't delete properties, but users and acls",
{ {
realm => 'syncedrealm', realm => 'syncedrealm',
'remove-vanished' => 'acl;entry', 'remove-vanished' => 'acl;entry',
scope => 'both', scope => 'both',
}, },
{ {
users => { users => {
'root@pam' => { username => 'root', }, 'root@pam' => { username => 'root' },
'user1@syncedrealm' => { 'user1@syncedrealm' => {
username => 'user1', username => 'user1',
enable => 1, enable => 1,
'keys' => 'some', 'keys' => 'some',
}, },
'user2@syncedrealm' => { 'user2@syncedrealm' => {
username => 'user2', username => 'user2',
enable => 1, enable => 1,
}, },
'user4@syncedrealm' => { 'user4@syncedrealm' => {
username => 'user4', username => 'user4',
enable => 1, enable => 1,
}, },
}, },
groups => { groups => {
'group1-syncedrealm' => { 'group1-syncedrealm' => {
users => { users => {
'user1@syncedrealm' => 1, 'user1@syncedrealm' => 1,
}, },
}, },
'group3-syncedrealm' => { users => {}, }, 'group3-syncedrealm' => { users => {} },
}, },
acl_root => { acl_root => {
users => {}, users => {},
groups => {}, groups => {},
}, },
}, },
], ],
]; ];