mirror of
https://git.proxmox.com/git/pve-access-control
synced 2025-10-04 08:21:57 +00:00
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:
parent
124e7a199b
commit
9590c6bdfe
@ -14,219 +14,257 @@ use PVE::RESTHandler;
|
||||
|
||||
use base qw(PVE::RESTHandler);
|
||||
|
||||
register_standard_option('acl-propagate', {
|
||||
description => "Allow to propagate (inherit) permissions.",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
default => 1,
|
||||
});
|
||||
register_standard_option('acl-path', {
|
||||
description => "Access control path",
|
||||
type => 'string',
|
||||
});
|
||||
register_standard_option(
|
||||
'acl-propagate',
|
||||
{
|
||||
description => "Allow to propagate (inherit) permissions.",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
default => 1,
|
||||
},
|
||||
);
|
||||
register_standard_option(
|
||||
'acl-path',
|
||||
{
|
||||
description => "Access control path",
|
||||
type => 'string',
|
||||
},
|
||||
);
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'read_acl',
|
||||
path => '',
|
||||
method => 'GET',
|
||||
description => "Get Access Control List (ACLs).",
|
||||
permissions => {
|
||||
description => "The returned list is restricted to objects where you have rights to modify permissions.",
|
||||
user => 'all',
|
||||
description =>
|
||||
"The returned list is restricted to objects where you have rights to modify permissions.",
|
||||
user => 'all',
|
||||
},
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {},
|
||||
additionalProperties => 0,
|
||||
properties => {},
|
||||
},
|
||||
returns => {
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
propagate => get_standard_option('acl-propagate'),
|
||||
path => get_standard_option('acl-path'),
|
||||
type => { type => 'string', enum => ['user', 'group', 'token'] },
|
||||
ugid => { type => 'string' },
|
||||
roleid => { type => 'string' },
|
||||
},
|
||||
},
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
propagate => get_standard_option('acl-propagate'),
|
||||
path => get_standard_option('acl-path'),
|
||||
type => { type => 'string', enum => ['user', 'group', 'token'] },
|
||||
ugid => { type => 'string' },
|
||||
roleid => { type => 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $authuser = $rpcenv->get_user();
|
||||
my $res = [];
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $authuser = $rpcenv->get_user();
|
||||
my $res = [];
|
||||
|
||||
my $usercfg = $rpcenv->{user_cfg};
|
||||
if (!$usercfg || !$usercfg->{acl_root}) {
|
||||
return $res;
|
||||
}
|
||||
my $usercfg = $rpcenv->{user_cfg};
|
||||
if (!$usercfg || !$usercfg->{acl_root}) {
|
||||
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};
|
||||
PVE::AccessControl::iterate_acl_tree("/", $root, sub {
|
||||
my ($path, $node) = @_;
|
||||
foreach my $type (qw(user group token)) {
|
||||
my $d = $node->{"${type}s"};
|
||||
next if !$d;
|
||||
next if !($audit || $rpcenv->check_perm_modify($authuser, $path, 1));
|
||||
foreach my $id (keys %$d) {
|
||||
foreach my $role (keys %{$d->{$id}}) {
|
||||
my $propagate = $d->{$id}->{$role};
|
||||
push @$res, {
|
||||
path => $path,
|
||||
type => $type,
|
||||
ugid => $id,
|
||||
roleid => $role,
|
||||
propagate => $propagate,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
my $root = $usercfg->{acl_root};
|
||||
PVE::AccessControl::iterate_acl_tree(
|
||||
"/",
|
||||
$root,
|
||||
sub {
|
||||
my ($path, $node) = @_;
|
||||
foreach my $type (qw(user group token)) {
|
||||
my $d = $node->{"${type}s"};
|
||||
next if !$d;
|
||||
next if !($audit || $rpcenv->check_perm_modify($authuser, $path, 1));
|
||||
foreach my $id (keys %$d) {
|
||||
foreach my $role (keys %{ $d->{$id} }) {
|
||||
my $propagate = $d->{$id}->{$role};
|
||||
push @$res,
|
||||
{
|
||||
path => $path,
|
||||
type => $type,
|
||||
ugid => $id,
|
||||
roleid => $role,
|
||||
propagate => $propagate,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return $res;
|
||||
}});
|
||||
return $res;
|
||||
},
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'update_acl',
|
||||
protected => 1,
|
||||
path => '',
|
||||
method => 'PUT',
|
||||
permissions => {
|
||||
check => ['perm-modify', '{path}'],
|
||||
check => ['perm-modify', '{path}'],
|
||||
},
|
||||
description => "Update Access Control List (add or remove permissions).",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
propagate => get_standard_option('acl-propagate'),
|
||||
path => get_standard_option('acl-path'),
|
||||
users => {
|
||||
description => "List of users.",
|
||||
type => 'string', format => 'pve-userid-list',
|
||||
optional => 1,
|
||||
},
|
||||
groups => {
|
||||
description => "List of groups.",
|
||||
type => 'string', format => 'pve-groupid-list',
|
||||
optional => 1,
|
||||
},
|
||||
tokens => {
|
||||
description => "List of API tokens.",
|
||||
type => 'string', format => 'pve-tokenid-list',
|
||||
optional => 1,
|
||||
},
|
||||
roles => {
|
||||
description => "List of roles.",
|
||||
type => 'string', format => 'pve-roleid-list',
|
||||
},
|
||||
delete => {
|
||||
description => "Remove permissions (instead of adding it).",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
},
|
||||
},
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
propagate => get_standard_option('acl-propagate'),
|
||||
path => get_standard_option('acl-path'),
|
||||
users => {
|
||||
description => "List of users.",
|
||||
type => 'string',
|
||||
format => 'pve-userid-list',
|
||||
optional => 1,
|
||||
},
|
||||
groups => {
|
||||
description => "List of groups.",
|
||||
type => 'string',
|
||||
format => 'pve-groupid-list',
|
||||
optional => 1,
|
||||
},
|
||||
tokens => {
|
||||
description => "List of API tokens.",
|
||||
type => 'string',
|
||||
format => 'pve-tokenid-list',
|
||||
optional => 1,
|
||||
},
|
||||
roles => {
|
||||
description => "List of roles.",
|
||||
type => 'string',
|
||||
format => 'pve-roleid-list',
|
||||
},
|
||||
delete => {
|
||||
description => "Remove permissions (instead of adding it).",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => { type => 'null' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
if (!($param->{users} || $param->{groups} || $param->{tokens})) {
|
||||
raise_param_exc({ map { $_ => "either 'users', 'groups' or 'tokens' is required." } qw(users groups tokens) });
|
||||
}
|
||||
if (!($param->{users} || $param->{groups} || $param->{tokens})) {
|
||||
raise_param_exc({
|
||||
map { $_ => "either 'users', 'groups' or 'tokens' is required." }
|
||||
qw(users groups tokens)
|
||||
});
|
||||
}
|
||||
|
||||
my $path = PVE::AccessControl::normalize_path($param->{path});
|
||||
raise_param_exc({ path => "invalid ACL path '$param->{path}'" }) if !$path;
|
||||
my $path = PVE::AccessControl::normalize_path($param->{path});
|
||||
raise_param_exc({ path => "invalid ACL path '$param->{path}'" }) if !$path;
|
||||
|
||||
if (!$param->{delete} && !PVE::AccessControl::check_path($path)) {
|
||||
raise_param_exc({ path => "invalid ACL path '$param->{path}'" });
|
||||
}
|
||||
if (!$param->{delete} && !PVE::AccessControl::check_path($path)) {
|
||||
raise_param_exc({ path => "invalid ACL path '$param->{path}'" });
|
||||
}
|
||||
|
||||
PVE::AccessControl::lock_user_config(
|
||||
sub {
|
||||
my $cfg = cfs_read_file("user.cfg");
|
||||
PVE::AccessControl::lock_user_config(
|
||||
sub {
|
||||
my $cfg = cfs_read_file("user.cfg");
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $authuser = $rpcenv->get_user();
|
||||
my $auth_user_privs = $rpcenv->permissions($authuser, $path);
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $authuser = $rpcenv->get_user();
|
||||
my $auth_user_privs = $rpcenv->permissions($authuser, $path);
|
||||
|
||||
my $propagate = 1;
|
||||
my $propagate = 1;
|
||||
|
||||
if (defined($param->{propagate})) {
|
||||
$propagate = $param->{propagate} ? 1 : 0;
|
||||
}
|
||||
if (defined($param->{propagate})) {
|
||||
$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})) {
|
||||
die "role '$role' does not exist\n"
|
||||
if !$cfg->{roles}->{$role};
|
||||
foreach my $role (split_list($param->{roles})) {
|
||||
die "role '$role' does not exist\n"
|
||||
if !$cfg->{roles}->{$role};
|
||||
|
||||
# permissions() returns set privs as key, and propagate bit as value!
|
||||
if (!defined($auth_user_privs->{'Permissions.Modify'})) {
|
||||
# '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
|
||||
my $role_privs = $cfg->{roles}->{$role};
|
||||
my $verb = $param->{delete} ? 'remove' : 'add';
|
||||
foreach my $priv (keys $role_privs->%*) {
|
||||
raise_param_exc({ role => "Cannot $verb role '$role' - requires 'Permissions.Modify' or superset of privileges." })
|
||||
if !defined($auth_user_privs->{$priv});
|
||||
# permissions() returns set privs as key, and propagate bit as value!
|
||||
if (!defined($auth_user_privs->{'Permissions.Modify'})) {
|
||||
# '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
|
||||
my $role_privs = $cfg->{roles}->{$role};
|
||||
my $verb = $param->{delete} ? 'remove' : 'add';
|
||||
foreach my $priv (keys $role_privs->%*) {
|
||||
raise_param_exc(
|
||||
{
|
||||
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..
|
||||
raise_param_exc({ role => "Cannot $verb role '$role' with propagation - requires 'Permissions.Modify' or propagated superset of privileges." })
|
||||
if $propagate && $auth_user_privs->{$priv} != $propagate && !$param->{delete};
|
||||
}
|
||||
# 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.",
|
||||
},
|
||||
)
|
||||
if $propagate
|
||||
&& $auth_user_privs->{$priv} != $propagate
|
||||
&& !$param->{delete};
|
||||
}
|
||||
|
||||
# NoAccess has no privs, needs an explicit check
|
||||
raise_param_exc({ role => "Cannot $verb role '$role' - requires 'Permissions.Modify'"})
|
||||
if $role eq 'NoAccess';
|
||||
}
|
||||
# NoAccess has no privs, needs an explicit check
|
||||
raise_param_exc(
|
||||
{
|
||||
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"
|
||||
if !$cfg->{groups}->{$group};
|
||||
die "group '$group' does not exist\n"
|
||||
if !$cfg->{groups}->{$group};
|
||||
|
||||
if ($param->{delete}) {
|
||||
delete($node->{groups}->{$group}->{$role});
|
||||
} else {
|
||||
$node->{groups}->{$group}->{$role} = $propagate;
|
||||
}
|
||||
}
|
||||
if ($param->{delete}) {
|
||||
delete($node->{groups}->{$group}->{$role});
|
||||
} else {
|
||||
$node->{groups}->{$group}->{$role} = $propagate;
|
||||
}
|
||||
}
|
||||
|
||||
foreach my $userid (split_list($param->{users})) {
|
||||
my $username = PVE::AccessControl::verify_username($userid);
|
||||
foreach my $userid (split_list($param->{users})) {
|
||||
my $username = PVE::AccessControl::verify_username($userid);
|
||||
|
||||
die "user '$username' does not exist\n"
|
||||
if !$cfg->{users}->{$username};
|
||||
die "user '$username' does not exist\n"
|
||||
if !$cfg->{users}->{$username};
|
||||
|
||||
if ($param->{delete}) {
|
||||
delete ($node->{users}->{$username}->{$role});
|
||||
} else {
|
||||
$node->{users}->{$username}->{$role} = $propagate;
|
||||
}
|
||||
}
|
||||
if ($param->{delete}) {
|
||||
delete($node->{users}->{$username}->{$role});
|
||||
} else {
|
||||
$node->{users}->{$username}->{$role} = $propagate;
|
||||
}
|
||||
}
|
||||
|
||||
foreach my $tokenid (split_list($param->{tokens})) {
|
||||
my ($username, $token) = PVE::AccessControl::split_tokenid($tokenid);
|
||||
PVE::AccessControl::check_token_exist($cfg, $username, $token);
|
||||
foreach my $tokenid (split_list($param->{tokens})) {
|
||||
my ($username, $token) = PVE::AccessControl::split_tokenid($tokenid);
|
||||
PVE::AccessControl::check_token_exist($cfg, $username, $token);
|
||||
|
||||
if ($param->{delete}) {
|
||||
delete $node->{tokens}->{$tokenid}->{$role};
|
||||
} else {
|
||||
$node->{tokens}->{$tokenid}->{$role} = $propagate;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($param->{delete}) {
|
||||
delete $node->{tokens}->{$tokenid}->{$role};
|
||||
} else {
|
||||
$node->{tokens}->{$tokenid}->{$role} = $propagate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cfs_write_file("user.cfg", $cfg);
|
||||
}, "ACL update failed");
|
||||
cfs_write_file("user.cfg", $cfg);
|
||||
},
|
||||
"ACL update failed",
|
||||
);
|
||||
|
||||
return undef;
|
||||
}});
|
||||
return undef;
|
||||
},
|
||||
});
|
||||
|
||||
1;
|
||||
|
@ -32,84 +32,84 @@ eval {
|
||||
|
||||
use base qw(PVE::RESTHandler);
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
subclass => "PVE::API2::User",
|
||||
path => 'users',
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
subclass => "PVE::API2::Group",
|
||||
path => 'groups',
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
subclass => "PVE::API2::Role",
|
||||
path => 'roles',
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
subclass => "PVE::API2::ACL",
|
||||
path => 'acl',
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
subclass => "PVE::API2::Domains",
|
||||
path => 'domains',
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
subclass => "PVE::API2::OpenId",
|
||||
path => 'openid',
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
subclass => "PVE::API2::TFA",
|
||||
path => 'tfa',
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'index',
|
||||
path => '',
|
||||
method => 'GET',
|
||||
description => "Directory index.",
|
||||
permissions => {
|
||||
user => 'all',
|
||||
user => 'all',
|
||||
},
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {},
|
||||
additionalProperties => 0,
|
||||
properties => {},
|
||||
},
|
||||
returns => {
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
properties => {
|
||||
subdir => { type => 'string' },
|
||||
},
|
||||
},
|
||||
links => [ { rel => 'child', href => "{subdir}" } ],
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
properties => {
|
||||
subdir => { type => 'string' },
|
||||
},
|
||||
},
|
||||
links => [{ rel => 'child', href => "{subdir}" }],
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
my $res = [];
|
||||
my $res = [];
|
||||
|
||||
my $ma = __PACKAGE__->method_attributes();
|
||||
my $ma = __PACKAGE__->method_attributes();
|
||||
|
||||
foreach my $info (@$ma) {
|
||||
next if !$info->{subclass};
|
||||
foreach my $info (@$ma) {
|
||||
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 => 'password' };
|
||||
|
||||
return $res;
|
||||
}});
|
||||
push @$res, { subdir => 'ticket' };
|
||||
push @$res, { subdir => 'password' };
|
||||
|
||||
return $res;
|
||||
},
|
||||
});
|
||||
|
||||
my sub verify_auth : prototype($$$$$$) {
|
||||
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);
|
||||
|
||||
my $ticketuser;
|
||||
if (($ticketuser = PVE::AccessControl::verify_ticket($pw_or_ticket, 1)) &&
|
||||
($ticketuser eq $username)) {
|
||||
# valid ticket
|
||||
if (
|
||||
($ticketuser = PVE::AccessControl::verify_ticket($pw_or_ticket, 1))
|
||||
&& ($ticketuser eq $username)
|
||||
) {
|
||||
# valid ticket
|
||||
} elsif (PVE::AccessControl::verify_vnc_ticket($pw_or_ticket, $username, $normpath, 1)) {
|
||||
# valid vnc ticket
|
||||
# valid vnc ticket
|
||||
} else {
|
||||
$username = PVE::AccessControl::authenticate_user(
|
||||
$username,
|
||||
$pw_or_ticket,
|
||||
$otp,
|
||||
);
|
||||
$username = PVE::AccessControl::authenticate_user(
|
||||
$username, $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))) {
|
||||
die "no permission ($path, $privs)\n";
|
||||
die "no permission ($path, $privs)\n";
|
||||
}
|
||||
|
||||
return { username => $username };
|
||||
};
|
||||
}
|
||||
|
||||
my sub create_ticket_do : prototype($$$$$) {
|
||||
my ($rpcenv, $username, $pw_or_ticket, $otp, $tfa_challenge) = @_;
|
||||
|
||||
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);
|
||||
if (!defined($tfa_challenge)) {
|
||||
# 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`.
|
||||
# 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`.
|
||||
|
||||
($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($tfa_info)) {
|
||||
die "incomplete ticket\n";
|
||||
}
|
||||
# valid ticket. Note: root@pam can create tickets for other users
|
||||
if (defined($tfa_info)) {
|
||||
die "incomplete ticket\n";
|
||||
}
|
||||
# valid ticket. Note: root@pam can create tickets for other users
|
||||
} else {
|
||||
($username, $tfa_info) = PVE::AccessControl::authenticate_user(
|
||||
$username,
|
||||
$pw_or_ticket,
|
||||
$otp,
|
||||
$tfa_challenge,
|
||||
);
|
||||
($username, $tfa_info) = PVE::AccessControl::authenticate_user(
|
||||
$username, $pw_or_ticket, $otp, $tfa_challenge,
|
||||
);
|
||||
}
|
||||
|
||||
my %extra;
|
||||
my $ticket_data = $username;
|
||||
my $aad;
|
||||
if (defined($tfa_info)) {
|
||||
$extra{NeedTFA} = 1;
|
||||
$ticket_data = "!tfa!$tfa_info";
|
||||
$aad = $username;
|
||||
$extra{NeedTFA} = 1;
|
||||
$ticket_data = "!tfa!$tfa_info";
|
||||
$aad = $username;
|
||||
}
|
||||
|
||||
my $ticket = PVE::AccessControl::assemble_ticket($ticket_data, $aad);
|
||||
my $csrftoken = PVE::AccessControl::assemble_csrf_prevention_token($username);
|
||||
|
||||
return {
|
||||
ticket => $ticket,
|
||||
username => $username,
|
||||
CSRFPreventionToken => $csrftoken,
|
||||
%extra,
|
||||
ticket => $ticket,
|
||||
username => $username,
|
||||
CSRFPreventionToken => $csrftoken,
|
||||
%extra,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'get_ticket',
|
||||
path => 'ticket',
|
||||
method => 'GET',
|
||||
permissions => { user => 'world' },
|
||||
description => "Dummy. Useful for formatters which want to provide a login page.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
additionalProperties => 0,
|
||||
},
|
||||
returns => { type => "null" },
|
||||
code => sub { return undef; }});
|
||||
code => sub { return undef; },
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'create_ticket',
|
||||
path => 'ticket',
|
||||
method => 'POST',
|
||||
permissions => {
|
||||
description => "You need to pass valid credientials.",
|
||||
user => 'world'
|
||||
description => "You need to pass valid credientials.",
|
||||
user => 'world',
|
||||
},
|
||||
protected => 1, # else we can't access shadow files
|
||||
allowtoken => 0, # we don't want tokens to create tickets
|
||||
description => "Create or verify authentication ticket.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
username => {
|
||||
description => "User name",
|
||||
type => 'string',
|
||||
maxLength => 64,
|
||||
completion => \&PVE::AccessControl::complete_username,
|
||||
},
|
||||
realm => get_standard_option('realm', {
|
||||
description => "You can optionally pass the realm using this parameter. Normally"
|
||||
." the realm is simply added to the username <username>\@<realm>.",
|
||||
optional => 1,
|
||||
completion => \&PVE::AccessControl::complete_realm,
|
||||
}),
|
||||
password => {
|
||||
description => "The secret password. This can also be a valid ticket.",
|
||||
type => 'string',
|
||||
},
|
||||
otp => {
|
||||
description => "One-time password for Two-factor authentication.",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
},
|
||||
path => {
|
||||
description => "Verify ticket, and check if user have access 'privs' on 'path'",
|
||||
type => 'string',
|
||||
requires => 'privs',
|
||||
optional => 1,
|
||||
maxLength => 64,
|
||||
},
|
||||
privs => {
|
||||
description => "Verify ticket, and check if user have access 'privs' on 'path'",
|
||||
type => 'string' , format => 'pve-priv-list',
|
||||
requires => 'path',
|
||||
optional => 1,
|
||||
maxLength => 64,
|
||||
},
|
||||
'new-format' => {
|
||||
type => 'boolean',
|
||||
description => 'This parameter is now ignored and assumed to be 1.',
|
||||
optional => 1,
|
||||
default => 1,
|
||||
},
|
||||
'tfa-challenge' => {
|
||||
type => 'string',
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
username => {
|
||||
description => "User name",
|
||||
type => 'string',
|
||||
maxLength => 64,
|
||||
completion => \&PVE::AccessControl::complete_username,
|
||||
},
|
||||
realm => get_standard_option(
|
||||
'realm',
|
||||
{
|
||||
description =>
|
||||
"You can optionally pass the realm using this parameter. Normally"
|
||||
. " the realm is simply added to the username <username>\@<realm>.",
|
||||
optional => 1,
|
||||
completion => \&PVE::AccessControl::complete_realm,
|
||||
},
|
||||
),
|
||||
password => {
|
||||
description => "The secret password. This can also be a valid ticket.",
|
||||
type => 'string',
|
||||
},
|
||||
otp => {
|
||||
description => "One-time password for Two-factor authentication.",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
},
|
||||
path => {
|
||||
description => "Verify ticket, and check if user have access 'privs' on 'path'",
|
||||
type => 'string',
|
||||
requires => 'privs',
|
||||
optional => 1,
|
||||
maxLength => 64,
|
||||
},
|
||||
privs => {
|
||||
description => "Verify ticket, and check if user have access 'privs' on 'path'",
|
||||
type => 'string',
|
||||
format => 'pve-priv-list',
|
||||
requires => 'path',
|
||||
optional => 1,
|
||||
maxLength => 64,
|
||||
},
|
||||
'new-format' => {
|
||||
type => 'boolean',
|
||||
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.",
|
||||
optional => 1,
|
||||
},
|
||||
}
|
||||
optional => 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => "object",
|
||||
properties => {
|
||||
username => { type => 'string' },
|
||||
ticket => { type => 'string', optional => 1},
|
||||
CSRFPreventionToken => { type => 'string', optional => 1 },
|
||||
clustername => { type => 'string', optional => 1 },
|
||||
# cap => computed api permissions, unless there's a u2f challenge
|
||||
}
|
||||
type => "object",
|
||||
properties => {
|
||||
username => { type => 'string' },
|
||||
ticket => { type => 'string', optional => 1 },
|
||||
CSRFPreventionToken => { type => 'string', optional => 1 },
|
||||
clustername => { type => 'string', optional => 1 },
|
||||
# cap => computed api permissions, unless there's a u2f challenge
|
||||
},
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
my $username = $param->{username};
|
||||
$username .= "\@$param->{realm}" if $param->{realm};
|
||||
my $username = $param->{username};
|
||||
$username .= "\@$param->{realm}" if $param->{realm};
|
||||
|
||||
$username = PVE::AccessControl::lookup_username($username);
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
$username = PVE::AccessControl::lookup_username($username);
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
|
||||
my $res;
|
||||
eval {
|
||||
# test if user exists and is enabled
|
||||
$rpcenv->check_user_enabled($username);
|
||||
my $res;
|
||||
eval {
|
||||
# test if user exists and is enabled
|
||||
$rpcenv->check_user_enabled($username);
|
||||
|
||||
if ($param->{path} && $param->{privs}) {
|
||||
$res = verify_auth($rpcenv, $username, $param->{password}, $param->{otp},
|
||||
$param->{path}, $param->{privs});
|
||||
} else {
|
||||
$res = create_ticket_do(
|
||||
$rpcenv,
|
||||
$username,
|
||||
$param->{password},
|
||||
$param->{otp},
|
||||
$param->{'tfa-challenge'},
|
||||
);
|
||||
}
|
||||
};
|
||||
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);
|
||||
}
|
||||
if ($param->{path} && $param->{privs}) {
|
||||
$res = verify_auth(
|
||||
$rpcenv,
|
||||
$username,
|
||||
$param->{password},
|
||||
$param->{otp},
|
||||
$param->{path},
|
||||
$param->{privs},
|
||||
);
|
||||
} else {
|
||||
$res = create_ticket_do(
|
||||
$rpcenv,
|
||||
$username,
|
||||
$param->{password},
|
||||
$param->{otp},
|
||||
$param->{'tfa-challenge'},
|
||||
);
|
||||
}
|
||||
};
|
||||
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)
|
||||
if !defined($res->{NeedTFA});
|
||||
$res->{cap} = $rpcenv->compute_api_permission($username)
|
||||
if !defined($res->{NeedTFA});
|
||||
|
||||
my $clinfo = PVE::Cluster::get_clinfo();
|
||||
if ($clinfo->{cluster}->{name} && $rpcenv->check($username, '/', ['Sys.Audit'], 1)) {
|
||||
$res->{clustername} = $clinfo->{cluster}->{name};
|
||||
}
|
||||
my $clinfo = PVE::Cluster::get_clinfo();
|
||||
if ($clinfo->{cluster}->{name} && $rpcenv->check($username, '/', ['Sys.Audit'], 1)) {
|
||||
$res->{clustername} = $clinfo->{cluster}->{name};
|
||||
}
|
||||
|
||||
PVE::Cluster::log_msg('info', 'root@pam', "successful auth for user '$username'");
|
||||
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',
|
||||
path => 'password',
|
||||
method => 'PUT',
|
||||
permissions => {
|
||||
description => "Each user is allowed to change their own password. A user can change the"
|
||||
." password of another user if they have 'Realm.AllocateUser' (on the realm of user"
|
||||
." <userid>) and 'User.Modify' permission on /access/groups/<group> on a group where"
|
||||
." user <userid> is member of. For the PAM realm, a password change does not take "
|
||||
." effect cluster-wide, but only applies to the local node.",
|
||||
check => [ 'or',
|
||||
['userid-param', 'self'],
|
||||
[ 'and',
|
||||
[ 'userid-param', 'Realm.AllocateUser'],
|
||||
[ 'userid-group', ['User.Modify']]
|
||||
]
|
||||
],
|
||||
description =>
|
||||
"Each user is allowed to change their own password. A user can change the"
|
||||
. " password of another user if they have 'Realm.AllocateUser' (on the realm of user"
|
||||
. " <userid>) and 'User.Modify' permission on /access/groups/<group> on a group where"
|
||||
. " user <userid> is member of. For the PAM realm, a password change does not take "
|
||||
. " effect cluster-wide, but only applies to the local node.",
|
||||
check => [
|
||||
'or',
|
||||
['userid-param', 'self'],
|
||||
[
|
||||
'and', ['userid-param', 'Realm.AllocateUser'],
|
||||
['userid-group', ['User.Modify']],
|
||||
],
|
||||
],
|
||||
},
|
||||
protected => 1, # else we can't access shadow files
|
||||
allowtoken => 0, # we don't want tokens to change the regular user password
|
||||
description => "Change user password.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
userid => get_standard_option('userid-completed'),
|
||||
password => {
|
||||
description => "The new password.",
|
||||
type => 'string',
|
||||
minLength => 8,
|
||||
maxLength => 64,
|
||||
},
|
||||
'confirmation-password' => $PVE::API2::TFA::OPTIONAL_PASSWORD_SCHEMA,
|
||||
}
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
userid => get_standard_option('userid-completed'),
|
||||
password => {
|
||||
description => "The new password.",
|
||||
type => 'string',
|
||||
minLength => 8,
|
||||
maxLength => 64,
|
||||
},
|
||||
'confirmation-password' => $PVE::API2::TFA::OPTIONAL_PASSWORD_SCHEMA,
|
||||
},
|
||||
},
|
||||
returns => { type => "null" },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $authuser = $rpcenv->get_user();
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $authuser = $rpcenv->get_user();
|
||||
|
||||
my ($userid, $ruid, $realm) = $rpcenv->reauth_user_for_user_modification(
|
||||
$authuser,
|
||||
$param->{userid},
|
||||
$param->{'confirmation-password'},
|
||||
'confirmation-password',
|
||||
);
|
||||
my ($userid, $ruid, $realm) = $rpcenv->reauth_user_for_user_modification(
|
||||
$authuser,
|
||||
$param->{userid},
|
||||
$param->{'confirmation-password'},
|
||||
'confirmation-password',
|
||||
);
|
||||
|
||||
if ($authuser eq 'root@pam') {
|
||||
# OK - root can change anything
|
||||
} else {
|
||||
if ($authuser eq $userid) {
|
||||
$rpcenv->check_user_enabled($userid);
|
||||
# OK - each user can change their own password
|
||||
} else {
|
||||
# only root may change root password
|
||||
raise_perm_exc() if $userid eq 'root@pam';
|
||||
# do not allow to change system user passwords
|
||||
raise_perm_exc() if $realm eq 'pam';
|
||||
}
|
||||
}
|
||||
if ($authuser eq 'root@pam') {
|
||||
# OK - root can change anything
|
||||
} else {
|
||||
if ($authuser eq $userid) {
|
||||
$rpcenv->check_user_enabled($userid);
|
||||
# OK - each user can change their own password
|
||||
} else {
|
||||
# only root may change root password
|
||||
raise_perm_exc() if $userid eq 'root@pam';
|
||||
# do not allow to change system user passwords
|
||||
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() {
|
||||
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).
|
||||
my $origin = $u2fconfig->{origin};
|
||||
if (!defined($origin)) {
|
||||
$origin = $rpcenv->get_request_host(1);
|
||||
if ($origin) {
|
||||
$origin = "https://$origin";
|
||||
} else {
|
||||
die "failed to figure out u2f origin\n";
|
||||
}
|
||||
$origin = $rpcenv->get_request_host(1);
|
||||
if ($origin) {
|
||||
$origin = "https://$origin";
|
||||
} else {
|
||||
die "failed to figure out u2f origin\n";
|
||||
}
|
||||
}
|
||||
|
||||
my $appid = $u2fconfig->{appid} // $origin;
|
||||
@ -428,22 +441,22 @@ sub verify_user_tfa_config {
|
||||
my ($type, $tfa_cfg, $value) = @_;
|
||||
|
||||
if (!defined($type)) {
|
||||
die "missing tfa 'type'\n";
|
||||
die "missing tfa 'type'\n";
|
||||
}
|
||||
|
||||
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}
|
||||
or die "missing TOTP secret\n";
|
||||
or die "missing TOTP secret\n";
|
||||
$tfa_cfg = $tfa_cfg->{config};
|
||||
# Copy the hash to verify that we have no unexpected keys without modifying the original hash.
|
||||
$tfa_cfg = {%$tfa_cfg};
|
||||
|
||||
# We can only verify 1 secret but oath_verify_otp allows multiple:
|
||||
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;
|
||||
@ -452,73 +465,77 @@ sub verify_user_tfa_config {
|
||||
# my $algorithm = delete($tfa_cfg->{algorithm}) // 'sha1';
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
__PACKAGE__->register_method({
|
||||
name => 'permissions',
|
||||
path => 'permissions',
|
||||
method => 'GET',
|
||||
description => 'Retrieve effective permissions of given user/token.',
|
||||
permissions => {
|
||||
description => "Each user/token is allowed to dump their own permissions (or that of owned"
|
||||
." tokens). A user can dump the permissions of another user or their tokens if they"
|
||||
." have 'Sys.Audit' permission on /access.",
|
||||
user => 'all',
|
||||
description =>
|
||||
"Each user/token is allowed to dump their own permissions (or that of owned"
|
||||
. " tokens). A user can dump the permissions of another user or their tokens if they"
|
||||
. " have 'Sys.Audit' permission on /access.",
|
||||
user => 'all',
|
||||
},
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
userid => {
|
||||
type => 'string',
|
||||
description => "User ID or full API token ID",
|
||||
pattern => $PVE::AccessControl::userid_or_token_regex,
|
||||
optional => 1,
|
||||
},
|
||||
path => get_standard_option('acl-path', {
|
||||
description => "Only dump this specific path, not the whole tree.",
|
||||
optional => 1,
|
||||
}),
|
||||
},
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
userid => {
|
||||
type => 'string',
|
||||
description => "User ID or full API token ID",
|
||||
pattern => $PVE::AccessControl::userid_or_token_regex,
|
||||
optional => 1,
|
||||
},
|
||||
path => get_standard_option(
|
||||
'acl-path',
|
||||
{
|
||||
description => "Only dump this specific path, not the whole tree.",
|
||||
optional => 1,
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => 'object',
|
||||
description => 'Map of "path" => (Map of "privilege" => "propagate boolean").',
|
||||
type => 'object',
|
||||
description => 'Map of "path" => (Map of "privilege" => "propagate boolean").',
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $authid = $rpcenv->get_user();
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $authid = $rpcenv->get_user();
|
||||
|
||||
my $userid = $param->{userid};
|
||||
$userid = $authid if !defined($userid);
|
||||
my $userid = $param->{userid};
|
||||
$userid = $authid if !defined($userid);
|
||||
|
||||
my ($user, $token) = PVE::AccessControl::split_tokenid($userid, 1);
|
||||
my $check_self = $userid eq $authid;
|
||||
my $check_owned_token = defined($user) && $user eq $authid;
|
||||
my ($user, $token) = PVE::AccessControl::split_tokenid($userid, 1);
|
||||
my $check_self = $userid eq $authid;
|
||||
my $check_owned_token = defined($user) && $user eq $authid;
|
||||
|
||||
if (!($check_self || $check_owned_token)) {
|
||||
$rpcenv->check($rpcenv->get_user(), '/access', ['Sys.Audit']);
|
||||
}
|
||||
my $res;
|
||||
if (!($check_self || $check_owned_token)) {
|
||||
$rpcenv->check($rpcenv->get_user(), '/access', ['Sys.Audit']);
|
||||
}
|
||||
my $res;
|
||||
|
||||
if (my $path = $param->{path}) {
|
||||
my $perms = $rpcenv->permissions($userid, $path);
|
||||
if ($perms) {
|
||||
$res = { $path => $perms };
|
||||
} else {
|
||||
$res = {};
|
||||
}
|
||||
} else {
|
||||
$res = $rpcenv->get_effective_permissions($userid);
|
||||
}
|
||||
if (my $path = $param->{path}) {
|
||||
my $perms = $rpcenv->permissions($userid, $path);
|
||||
if ($perms) {
|
||||
$res = { $path => $perms };
|
||||
} else {
|
||||
$res = {};
|
||||
}
|
||||
} else {
|
||||
$res = $rpcenv->get_effective_permissions($userid);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}});
|
||||
return $res;
|
||||
},
|
||||
});
|
||||
|
||||
1;
|
||||
|
@ -23,15 +23,15 @@ my $map_remove_vanished = sub {
|
||||
my ($opt, $delete_deprecated) = @_;
|
||||
|
||||
if (!defined($opt->{'remove-vanished'}) && ($opt->{full} || $opt->{purge})) {
|
||||
my $props = [];
|
||||
push @$props, 'entry', 'properties' if $opt->{full};
|
||||
push @$props, 'acl' if $opt->{purge};
|
||||
$opt->{'remove-vanished'} = join(';', @$props);
|
||||
my $props = [];
|
||||
push @$props, 'entry', 'properties' if $opt->{full};
|
||||
push @$props, 'acl' if $opt->{purge};
|
||||
$opt->{'remove-vanished'} = join(';', @$props);
|
||||
}
|
||||
|
||||
if ($delete_deprecated) {
|
||||
delete $opt->{full};
|
||||
delete $opt->{purge};
|
||||
delete $opt->{full};
|
||||
delete $opt->{purge};
|
||||
}
|
||||
|
||||
return $opt;
|
||||
@ -48,315 +48,336 @@ my $map_sync_default_options = sub {
|
||||
|
||||
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',
|
||||
path => '',
|
||||
method => 'GET',
|
||||
description => "Authentication domain index.",
|
||||
permissions => {
|
||||
description => "Anyone can access that, because we need that list for the login box (before the user is authenticated).",
|
||||
user => 'world',
|
||||
description =>
|
||||
"Anyone can access that, because we need that list for the login box (before the user is authenticated).",
|
||||
user => 'world',
|
||||
},
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {},
|
||||
additionalProperties => 0,
|
||||
properties => {},
|
||||
},
|
||||
returns => {
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
properties => {
|
||||
realm => { type => 'string' },
|
||||
type => { type => 'string' },
|
||||
tfa => {
|
||||
description => "Two-factor authentication provider.",
|
||||
type => 'string',
|
||||
enum => [ 'yubico', 'oath' ],
|
||||
optional => 1,
|
||||
},
|
||||
comment => {
|
||||
description => "A comment. The GUI use this text when you select a domain (Realm) on the login window.",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
links => [ { rel => 'child', href => "{realm}" } ],
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
properties => {
|
||||
realm => { type => 'string' },
|
||||
type => { type => 'string' },
|
||||
tfa => {
|
||||
description => "Two-factor authentication provider.",
|
||||
type => 'string',
|
||||
enum => ['yubico', 'oath'],
|
||||
optional => 1,
|
||||
},
|
||||
comment => {
|
||||
description =>
|
||||
"A comment. The GUI use this text when you select a domain (Realm) on the login window.",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
links => [{ rel => 'child', href => "{realm}" }],
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
my $res = [];
|
||||
my $res = [];
|
||||
|
||||
my $cfg = cfs_read_file($domainconfigfile);
|
||||
my $ids = $cfg->{ids};
|
||||
my $cfg = cfs_read_file($domainconfigfile);
|
||||
my $ids = $cfg->{ids};
|
||||
|
||||
foreach my $realm (keys %$ids) {
|
||||
my $d = $ids->{$realm};
|
||||
my $entry = { realm => $realm, type => $d->{type} };
|
||||
$entry->{comment} = $d->{comment} if $d->{comment};
|
||||
$entry->{default} = 1 if $d->{default};
|
||||
if ($d->{tfa} && (my $tfa_cfg = PVE::Auth::Plugin::parse_tfa_config($d->{tfa}))) {
|
||||
$entry->{tfa} = $tfa_cfg->{type};
|
||||
}
|
||||
push @$res, $entry;
|
||||
}
|
||||
foreach my $realm (keys %$ids) {
|
||||
my $d = $ids->{$realm};
|
||||
my $entry = { realm => $realm, type => $d->{type} };
|
||||
$entry->{comment} = $d->{comment} if $d->{comment};
|
||||
$entry->{default} = 1 if $d->{default};
|
||||
if ($d->{tfa} && (my $tfa_cfg = PVE::Auth::Plugin::parse_tfa_config($d->{tfa}))) {
|
||||
$entry->{tfa} = $tfa_cfg->{type};
|
||||
}
|
||||
push @$res, $entry;
|
||||
}
|
||||
|
||||
return $res;
|
||||
}});
|
||||
return $res;
|
||||
},
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'create',
|
||||
protected => 1,
|
||||
path => '',
|
||||
method => 'POST',
|
||||
permissions => {
|
||||
check => ['perm', '/access/realm', ['Realm.Allocate']],
|
||||
check => ['perm', '/access/realm', ['Realm.Allocate']],
|
||||
},
|
||||
description => "Add an authentication server.",
|
||||
parameters => PVE::Auth::Plugin->createSchema(0, {
|
||||
'check-connection' => {
|
||||
description => 'Check bind connection to the server.',
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
default => 0,
|
||||
},
|
||||
}),
|
||||
parameters => PVE::Auth::Plugin->createSchema(
|
||||
0,
|
||||
{
|
||||
'check-connection' => {
|
||||
description => 'Check bind connection to the server.',
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
default => 0,
|
||||
},
|
||||
},
|
||||
),
|
||||
returns => { type => 'null' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
# always extract, add it with hook
|
||||
my $password = extract_param($param, 'password');
|
||||
# always extract, add it with hook
|
||||
my $password = extract_param($param, 'password');
|
||||
|
||||
PVE::Auth::Plugin::lock_domain_config(
|
||||
sub {
|
||||
PVE::Auth::Plugin::lock_domain_config(
|
||||
sub {
|
||||
|
||||
my $cfg = cfs_read_file($domainconfigfile);
|
||||
my $ids = $cfg->{ids};
|
||||
my $cfg = cfs_read_file($domainconfigfile);
|
||||
my $ids = $cfg->{ids};
|
||||
|
||||
my $realm = extract_param($param, 'realm');
|
||||
my $type = $param->{type};
|
||||
my $check_connection = extract_param($param, 'check-connection');
|
||||
my $realm = extract_param($param, 'realm');
|
||||
my $type = $param->{type};
|
||||
my $check_connection = extract_param($param, 'check-connection');
|
||||
|
||||
die "domain '$realm' already exists\n"
|
||||
if $ids->{$realm};
|
||||
die "domain '$realm' already exists\n"
|
||||
if $ids->{$realm};
|
||||
|
||||
die "unable to use reserved name '$realm'\n"
|
||||
if ($realm eq 'pam' || $realm eq 'pve');
|
||||
die "unable to use reserved name '$realm'\n"
|
||||
if ($realm eq 'pam' || $realm eq 'pve');
|
||||
|
||||
die "unable to create builtin type '$type'\n"
|
||||
if ($type eq 'pam' || $type eq 'pve');
|
||||
die "unable to create builtin type '$type'\n"
|
||||
if ($type eq 'pam' || $type eq 'pve');
|
||||
|
||||
die "'check-connection' parameter can only be set for realms of type 'ldap' or 'ad'\n"
|
||||
if defined($check_connection) && !($type eq 'ldap' || $type eq 'ad');
|
||||
die
|
||||
"'check-connection' parameter can only be set for realms of type 'ldap' or 'ad'\n"
|
||||
if defined($check_connection) && !($type eq 'ldap' || $type eq 'ad');
|
||||
|
||||
if ($type eq 'ad' || $type eq 'ldap') {
|
||||
$map_sync_default_options->($param, 1);
|
||||
}
|
||||
if ($type eq 'ad' || $type eq 'ldap') {
|
||||
$map_sync_default_options->($param, 1);
|
||||
}
|
||||
|
||||
my $plugin = PVE::Auth::Plugin->lookup($type);
|
||||
my $config = $plugin->check_config($realm, $param, 1, 1);
|
||||
my $plugin = PVE::Auth::Plugin->lookup($type);
|
||||
my $config = $plugin->check_config($realm, $param, 1, 1);
|
||||
|
||||
if ($config->{default}) {
|
||||
foreach my $r (keys %$ids) {
|
||||
delete $ids->{$r}->{default};
|
||||
}
|
||||
}
|
||||
if ($config->{default}) {
|
||||
foreach my $r (keys %$ids) {
|
||||
delete $ids->{$r}->{default};
|
||||
}
|
||||
}
|
||||
|
||||
$ids->{$realm} = $config;
|
||||
$ids->{$realm} = $config;
|
||||
|
||||
my $opts = $plugin->options();
|
||||
if (defined($password) && !defined($opts->{password})) {
|
||||
$password = undef;
|
||||
warn "ignoring password parameter";
|
||||
}
|
||||
$plugin->on_add_hook($realm, $config, password => $password);
|
||||
my $opts = $plugin->options();
|
||||
if (defined($password) && !defined($opts->{password})) {
|
||||
$password = undef;
|
||||
warn "ignoring password parameter";
|
||||
}
|
||||
$plugin->on_add_hook($realm, $config, password => $password);
|
||||
|
||||
# Only for LDAP/AD, implied through the existence of the 'check-connection' param
|
||||
$plugin->check_connection($realm, $config, password => $password)
|
||||
if $check_connection;
|
||||
# Only for LDAP/AD, implied through the existence of the 'check-connection' param
|
||||
$plugin->check_connection($realm, $config, password => $password)
|
||||
if $check_connection;
|
||||
|
||||
cfs_write_file($domainconfigfile, $cfg);
|
||||
}, "add auth server failed");
|
||||
cfs_write_file($domainconfigfile, $cfg);
|
||||
},
|
||||
"add auth server failed",
|
||||
);
|
||||
|
||||
return undef;
|
||||
}});
|
||||
return undef;
|
||||
},
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'update',
|
||||
path => '{realm}',
|
||||
method => 'PUT',
|
||||
permissions => {
|
||||
check => ['perm', '/access/realm', ['Realm.Allocate']],
|
||||
check => ['perm', '/access/realm', ['Realm.Allocate']],
|
||||
},
|
||||
description => "Update authentication server settings.",
|
||||
protected => 1,
|
||||
parameters => PVE::Auth::Plugin->updateSchema(0, {
|
||||
'check-connection' => {
|
||||
description => 'Check bind connection to the server.',
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
default => 0,
|
||||
},
|
||||
}),
|
||||
parameters => PVE::Auth::Plugin->updateSchema(
|
||||
0,
|
||||
{
|
||||
'check-connection' => {
|
||||
description => 'Check bind connection to the server.',
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
default => 0,
|
||||
},
|
||||
},
|
||||
),
|
||||
returns => { type => 'null' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
# always extract, update in hook
|
||||
my $password = extract_param($param, 'password');
|
||||
# always extract, update in hook
|
||||
my $password = extract_param($param, 'password');
|
||||
|
||||
PVE::Auth::Plugin::lock_domain_config(
|
||||
sub {
|
||||
PVE::Auth::Plugin::lock_domain_config(
|
||||
sub {
|
||||
|
||||
my $cfg = cfs_read_file($domainconfigfile);
|
||||
my $ids = $cfg->{ids};
|
||||
my $cfg = cfs_read_file($domainconfigfile);
|
||||
my $ids = $cfg->{ids};
|
||||
|
||||
my $digest = extract_param($param, 'digest');
|
||||
PVE::SectionConfig::assert_if_modified($cfg, $digest);
|
||||
my $digest = extract_param($param, 'digest');
|
||||
PVE::SectionConfig::assert_if_modified($cfg, $digest);
|
||||
|
||||
my $realm = extract_param($param, 'realm');
|
||||
my $type = $ids->{$realm}->{type};
|
||||
my $check_connection = extract_param($param, 'check-connection');
|
||||
my $realm = extract_param($param, 'realm');
|
||||
my $type = $ids->{$realm}->{type};
|
||||
my $check_connection = extract_param($param, 'check-connection');
|
||||
|
||||
die "domain '$realm' does not exist\n"
|
||||
if !$ids->{$realm};
|
||||
die "domain '$realm' does not exist\n"
|
||||
if !$ids->{$realm};
|
||||
|
||||
die "'check-connection' parameter can only be set for realms of type 'ldap' or 'ad'\n"
|
||||
if defined($check_connection) && !($type eq 'ldap' || $type eq 'ad');
|
||||
die
|
||||
"'check-connection' parameter can only be set for realms of type 'ldap' or 'ad'\n"
|
||||
if defined($check_connection) && !($type eq 'ldap' || $type eq 'ad');
|
||||
|
||||
my $delete_str = extract_param($param, 'delete');
|
||||
die "no options specified\n"
|
||||
if !$delete_str && !scalar(keys %$param) && !defined($password);
|
||||
my $delete_str = extract_param($param, 'delete');
|
||||
die "no options specified\n"
|
||||
if !$delete_str && !scalar(keys %$param) && !defined($password);
|
||||
|
||||
my $delete_pw = 0;
|
||||
foreach my $opt (PVE::Tools::split_list($delete_str)) {
|
||||
delete $ids->{$realm}->{$opt};
|
||||
$delete_pw = 1 if $opt eq 'password';
|
||||
}
|
||||
my $delete_pw = 0;
|
||||
foreach my $opt (PVE::Tools::split_list($delete_str)) {
|
||||
delete $ids->{$realm}->{$opt};
|
||||
$delete_pw = 1 if $opt eq 'password';
|
||||
}
|
||||
|
||||
if ($type eq 'ad' || $type eq 'ldap') {
|
||||
$map_sync_default_options->($param, 1);
|
||||
}
|
||||
if ($type eq 'ad' || $type eq 'ldap') {
|
||||
$map_sync_default_options->($param, 1);
|
||||
}
|
||||
|
||||
my $plugin = PVE::Auth::Plugin->lookup($type);
|
||||
my $config = $plugin->check_config($realm, $param, 0, 1);
|
||||
my $plugin = PVE::Auth::Plugin->lookup($type);
|
||||
my $config = $plugin->check_config($realm, $param, 0, 1);
|
||||
|
||||
if ($config->{default}) {
|
||||
foreach my $r (keys %$ids) {
|
||||
delete $ids->{$r}->{default};
|
||||
}
|
||||
}
|
||||
if ($config->{default}) {
|
||||
foreach my $r (keys %$ids) {
|
||||
delete $ids->{$r}->{default};
|
||||
}
|
||||
}
|
||||
|
||||
foreach my $p (keys %$config) {
|
||||
$ids->{$realm}->{$p} = $config->{$p};
|
||||
}
|
||||
foreach my $p (keys %$config) {
|
||||
$ids->{$realm}->{$p} = $config->{$p};
|
||||
}
|
||||
|
||||
my $opts = $plugin->options();
|
||||
if ($delete_pw || defined($password)) {
|
||||
$plugin->on_update_hook($realm, $config, password => $password);
|
||||
} else {
|
||||
$plugin->on_update_hook($realm, $config);
|
||||
}
|
||||
my $opts = $plugin->options();
|
||||
if ($delete_pw || defined($password)) {
|
||||
$plugin->on_update_hook($realm, $config, password => $password);
|
||||
} else {
|
||||
$plugin->on_update_hook($realm, $config);
|
||||
}
|
||||
|
||||
# Only for LDAP/AD, implied through the existence of the 'check-connection' param
|
||||
$plugin->check_connection($realm, $ids->{$realm}, password => $password)
|
||||
if $check_connection;
|
||||
# Only for LDAP/AD, implied through the existence of the 'check-connection' param
|
||||
$plugin->check_connection($realm, $ids->{$realm}, password => $password)
|
||||
if $check_connection;
|
||||
|
||||
cfs_write_file($domainconfigfile, $cfg);
|
||||
}, "update auth server failed");
|
||||
cfs_write_file($domainconfigfile, $cfg);
|
||||
},
|
||||
"update auth server failed",
|
||||
);
|
||||
|
||||
return undef;
|
||||
}});
|
||||
return undef;
|
||||
},
|
||||
});
|
||||
|
||||
# fixme: return format!
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'read',
|
||||
path => '{realm}',
|
||||
method => 'GET',
|
||||
description => "Get auth server configuration.",
|
||||
permissions => {
|
||||
check => ['perm', '/access/realm', ['Realm.Allocate', 'Sys.Audit'], any => 1],
|
||||
check => ['perm', '/access/realm', ['Realm.Allocate', 'Sys.Audit'], any => 1],
|
||||
},
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
realm => get_standard_option('realm'),
|
||||
},
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
realm => get_standard_option('realm'),
|
||||
},
|
||||
},
|
||||
returns => {},
|
||||
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};
|
||||
die "domain '$realm' does not exist\n" if !$data;
|
||||
my $data = $cfg->{ids}->{$realm};
|
||||
die "domain '$realm' does not exist\n" if !$data;
|
||||
|
||||
my $type = $data->{type};
|
||||
if ($type eq 'ad' || $type eq 'ldap') {
|
||||
$map_sync_default_options->($data);
|
||||
}
|
||||
my $type = $data->{type};
|
||||
if ($type eq 'ad' || $type eq 'ldap') {
|
||||
$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',
|
||||
path => '{realm}',
|
||||
method => 'DELETE',
|
||||
permissions => {
|
||||
check => ['perm', '/access/realm', ['Realm.Allocate']],
|
||||
check => ['perm', '/access/realm', ['Realm.Allocate']],
|
||||
},
|
||||
description => "Delete an authentication server.",
|
||||
protected => 1,
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
realm => get_standard_option('realm'),
|
||||
}
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
realm => get_standard_option('realm'),
|
||||
},
|
||||
},
|
||||
returns => { type => 'null' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
PVE::Auth::Plugin::lock_domain_config(
|
||||
sub {
|
||||
PVE::Auth::Plugin::lock_domain_config(
|
||||
sub {
|
||||
|
||||
my $cfg = cfs_read_file($domainconfigfile);
|
||||
my $ids = $cfg->{ids};
|
||||
my $realm = $param->{realm};
|
||||
my $cfg = cfs_read_file($domainconfigfile);
|
||||
my $ids = $cfg->{ids};
|
||||
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);
|
||||
}, "delete auth server failed");
|
||||
cfs_write_file($domainconfigfile, $cfg);
|
||||
},
|
||||
"delete auth server failed",
|
||||
);
|
||||
|
||||
return undef;
|
||||
}});
|
||||
return undef;
|
||||
},
|
||||
});
|
||||
|
||||
my $update_users = sub {
|
||||
my ($usercfg, $realm, $synced_users, $opts) = @_;
|
||||
|
||||
if (defined(my $vanished = $opts->{'remove-vanished'})) {
|
||||
print "syncing users (remove-vanished opts: $vanished)\n";
|
||||
print "syncing users (remove-vanished opts: $vanished)\n";
|
||||
} else {
|
||||
print "syncing users\n";
|
||||
print "syncing users\n";
|
||||
}
|
||||
|
||||
$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};
|
||||
foreach my $userid (sort keys %$users) {
|
||||
next if $userid !~ m/\@$realm$/;
|
||||
next if defined($synced_users->{$userid});
|
||||
next if $userid !~ m/\@$realm$/;
|
||||
next if defined($synced_users->{$userid});
|
||||
|
||||
if ($to_remove->{entry}) {
|
||||
print "remove user '$userid'\n";
|
||||
delete $users->{$userid};
|
||||
}
|
||||
if ($to_remove->{entry}) {
|
||||
print "remove user '$userid'\n";
|
||||
delete $users->{$userid};
|
||||
}
|
||||
|
||||
if ($to_remove->{acl}) {
|
||||
print "purge users '$userid' ACL entries\n";
|
||||
PVE::AccessControl::delete_user_acl($userid, $usercfg);
|
||||
}
|
||||
if ($to_remove->{acl}) {
|
||||
print "purge users '$userid' ACL entries\n";
|
||||
PVE::AccessControl::delete_user_acl($userid, $usercfg);
|
||||
}
|
||||
}
|
||||
|
||||
foreach my $userid (sort keys %$synced_users) {
|
||||
my $synced_user = $synced_users->{$userid} // {};
|
||||
my $olduser = $users->{$userid};
|
||||
if ($to_remove->{properties} || !defined($olduser)) {
|
||||
# we use the synced user, but want to keep some properties on update
|
||||
if (defined($olduser)) {
|
||||
print "overwriting user '$userid'\n";
|
||||
} else {
|
||||
$olduser = {};
|
||||
print "adding user '$userid'\n";
|
||||
}
|
||||
my $user = $users->{$userid} = $synced_user;
|
||||
my $synced_user = $synced_users->{$userid} // {};
|
||||
my $olduser = $users->{$userid};
|
||||
if ($to_remove->{properties} || !defined($olduser)) {
|
||||
# we use the synced user, but want to keep some properties on update
|
||||
if (defined($olduser)) {
|
||||
print "overwriting user '$userid'\n";
|
||||
} else {
|
||||
$olduser = {};
|
||||
print "adding user '$userid'\n";
|
||||
}
|
||||
my $user = $users->{$userid} = $synced_user;
|
||||
|
||||
my $enabled = $olduser->{enable} // $opts->{'enable-new'};
|
||||
$user->{enable} = $enabled if defined($enabled);
|
||||
$user->{tokens} = $olduser->{tokens} if defined($olduser->{tokens});
|
||||
my $enabled = $olduser->{enable} // $opts->{'enable-new'};
|
||||
$user->{enable} = $enabled if defined($enabled);
|
||||
$user->{tokens} = $olduser->{tokens} if defined($olduser->{tokens});
|
||||
|
||||
} else {
|
||||
foreach my $attr (keys %$synced_user) {
|
||||
$olduser->{$attr} = $synced_user->{$attr};
|
||||
}
|
||||
print "updating user '$userid'\n";
|
||||
}
|
||||
} else {
|
||||
foreach my $attr (keys %$synced_user) {
|
||||
$olduser->{$attr} = $synced_user->{$attr};
|
||||
}
|
||||
print "updating user '$userid'\n";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -409,9 +430,9 @@ my $update_groups = sub {
|
||||
my ($usercfg, $realm, $synced_groups, $opts) = @_;
|
||||
|
||||
if (defined(my $vanished = $opts->{'remove-vanished'})) {
|
||||
print "syncing groups (remove-vanished opts: $vanished)\n";
|
||||
print "syncing groups (remove-vanished opts: $vanished)\n";
|
||||
} else {
|
||||
print "syncing groups\n";
|
||||
print "syncing groups\n";
|
||||
}
|
||||
|
||||
$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};
|
||||
foreach my $groupid (sort keys %$groups) {
|
||||
next if $groupid !~ m/\-$realm$/;
|
||||
next if defined($synced_groups->{$groupid});
|
||||
next if $groupid !~ m/\-$realm$/;
|
||||
next if defined($synced_groups->{$groupid});
|
||||
|
||||
if ($to_remove->{entry}) {
|
||||
print "remove group '$groupid'\n";
|
||||
delete $groups->{$groupid};
|
||||
}
|
||||
if ($to_remove->{entry}) {
|
||||
print "remove group '$groupid'\n";
|
||||
delete $groups->{$groupid};
|
||||
}
|
||||
|
||||
if ($to_remove->{acl}) {
|
||||
print "purge groups '$groupid' ACL entries\n";
|
||||
PVE::AccessControl::delete_group_acl($groupid, $usercfg);
|
||||
}
|
||||
if ($to_remove->{acl}) {
|
||||
print "purge groups '$groupid' ACL entries\n";
|
||||
PVE::AccessControl::delete_group_acl($groupid, $usercfg);
|
||||
}
|
||||
}
|
||||
|
||||
foreach my $groupid (sort keys %$synced_groups) {
|
||||
my $synced_group = $synced_groups->{$groupid};
|
||||
my $oldgroup = $groups->{$groupid};
|
||||
if ($to_remove->{properties} || !defined($oldgroup)) {
|
||||
if (defined($oldgroup)) {
|
||||
print "overwriting group '$groupid'\n";
|
||||
} else {
|
||||
print "adding group '$groupid'\n";
|
||||
}
|
||||
$groups->{$groupid} = $synced_group;
|
||||
} else {
|
||||
foreach my $attr (keys %$synced_group) {
|
||||
$oldgroup->{$attr} = $synced_group->{$attr};
|
||||
}
|
||||
print "updating group '$groupid'\n";
|
||||
}
|
||||
my $synced_group = $synced_groups->{$groupid};
|
||||
my $oldgroup = $groups->{$groupid};
|
||||
if ($to_remove->{properties} || !defined($oldgroup)) {
|
||||
if (defined($oldgroup)) {
|
||||
print "overwriting group '$groupid'\n";
|
||||
} else {
|
||||
print "adding group '$groupid'\n";
|
||||
}
|
||||
$groups->{$groupid} = $synced_group;
|
||||
} else {
|
||||
foreach my $attr (keys %$synced_group) {
|
||||
$oldgroup->{$attr} = $synced_group->{$attr};
|
||||
}
|
||||
print "updating group '$groupid'\n";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -460,116 +481,126 @@ my $parse_sync_opts = sub {
|
||||
|
||||
my $cfg_defaults = {};
|
||||
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 = {};
|
||||
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);
|
||||
|
||||
# only scope has no implicit value
|
||||
raise_param_exc({
|
||||
"scope" => 'Not passed as parameter and not defined in realm default sync options.'
|
||||
}) if !defined($res->{scope});
|
||||
"scope" => 'Not passed as parameter and not defined in realm default sync options.',
|
||||
})
|
||||
if !defined($res->{scope});
|
||||
|
||||
return $res;
|
||||
};
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'sync',
|
||||
path => '{realm}/sync',
|
||||
method => 'POST',
|
||||
permissions => {
|
||||
description => "'Realm.AllocateUser' on '/access/realm/<realm>' and "
|
||||
." 'User.Modify' permissions to '/access/groups/'.",
|
||||
check => [ 'and',
|
||||
['perm', '/access/realm/{realm}', ['Realm.AllocateUser']],
|
||||
['perm', '/access/groups', ['User.Modify']],
|
||||
],
|
||||
description => "'Realm.AllocateUser' on '/access/realm/<realm>' and "
|
||||
. " 'User.Modify' permissions to '/access/groups/'.",
|
||||
check => [
|
||||
'and',
|
||||
['perm', '/access/realm/{realm}', ['Realm.AllocateUser']],
|
||||
['perm', '/access/groups', ['User.Modify']],
|
||||
],
|
||||
},
|
||||
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"
|
||||
." those groups do not exist to prevent overwriting.",
|
||||
. " NOTE: Synced groups will have the name 'name-\$realm', so make sure"
|
||||
. " those groups do not exist to prevent overwriting.",
|
||||
protected => 1,
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => get_standard_option('realm-sync-options', {
|
||||
realm => get_standard_option('realm'),
|
||||
'dry-run' => {
|
||||
description => "If set, does not write anything.",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
default => 0,
|
||||
},
|
||||
}),
|
||||
additionalProperties => 0,
|
||||
properties => get_standard_option(
|
||||
'realm-sync-options',
|
||||
{
|
||||
realm => get_standard_option('realm'),
|
||||
'dry-run' => {
|
||||
description => "If set, does not write anything.",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
default => 0,
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
returns => {
|
||||
description => 'Worker Task-UPID',
|
||||
type => 'string'
|
||||
description => 'Worker Task-UPID',
|
||||
type => 'string',
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $authuser = $rpcenv->get_user();
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $authuser = $rpcenv->get_user();
|
||||
|
||||
my $dry_run = extract_param($param, 'dry-run');
|
||||
my $realm = $param->{realm};
|
||||
my $cfg = cfs_read_file($domainconfigfile);
|
||||
my $realmconfig = $cfg->{ids}->{$realm};
|
||||
my $dry_run = extract_param($param, 'dry-run');
|
||||
my $realm = $param->{realm};
|
||||
my $cfg = cfs_read_file($domainconfigfile);
|
||||
my $realmconfig = $cfg->{ids}->{$realm};
|
||||
|
||||
raise_param_exc({ 'realm' => 'Realm does not exist.' }) if !defined($realmconfig);
|
||||
my $type = $realmconfig->{type};
|
||||
raise_param_exc({ 'realm' => 'Realm does not exist.' }) if !defined($realmconfig);
|
||||
my $type = $realmconfig->{type};
|
||||
|
||||
if ($type ne 'ldap' && $type ne 'ad') {
|
||||
die "Cannot sync realm type '$type'! Only LDAP/AD realms can be synced.\n";
|
||||
}
|
||||
if ($type ne 'ldap' && $type ne 'ad') {
|
||||
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 $whatstring = $scope eq 'both' ? "users and groups" : $scope;
|
||||
my $scope = $opts->{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 {
|
||||
print "(dry test run) " if $dry_run;
|
||||
print "starting sync for realm $realm\n";
|
||||
my $worker = sub {
|
||||
print "(dry test run) " if $dry_run;
|
||||
print "starting sync for realm $realm\n";
|
||||
|
||||
my ($synced_users, $dnmap) = $plugin->get_users($realmconfig, $realm);
|
||||
my $synced_groups = {};
|
||||
if ($scope eq 'groups' || $scope eq 'both') {
|
||||
$synced_groups = $plugin->get_groups($realmconfig, $realm, $dnmap);
|
||||
}
|
||||
my ($synced_users, $dnmap) = $plugin->get_users($realmconfig, $realm);
|
||||
my $synced_groups = {};
|
||||
if ($scope eq 'groups' || $scope eq 'both') {
|
||||
$synced_groups = $plugin->get_groups($realmconfig, $realm, $dnmap);
|
||||
}
|
||||
|
||||
PVE::AccessControl::lock_user_config(sub {
|
||||
my $usercfg = cfs_read_file("user.cfg");
|
||||
print "got data from server, updating $whatstring\n";
|
||||
PVE::AccessControl::lock_user_config(
|
||||
sub {
|
||||
my $usercfg = cfs_read_file("user.cfg");
|
||||
print "got data from server, updating $whatstring\n";
|
||||
|
||||
if ($scope eq 'users' || $scope eq 'both') {
|
||||
$update_users->($usercfg, $realm, $synced_users, $opts);
|
||||
}
|
||||
if ($scope eq 'users' || $scope eq 'both') {
|
||||
$update_users->($usercfg, $realm, $synced_users, $opts);
|
||||
}
|
||||
|
||||
if ($scope eq 'groups' || $scope eq 'both') {
|
||||
$update_groups->($usercfg, $realm, $synced_groups, $opts);
|
||||
}
|
||||
if ($scope eq 'groups' || $scope eq 'both') {
|
||||
$update_groups->($usercfg, $realm, $synced_groups, $opts);
|
||||
}
|
||||
|
||||
if ($dry_run) {
|
||||
print "\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";
|
||||
}, "syncing $whatstring failed");
|
||||
};
|
||||
if ($dry_run) {
|
||||
print
|
||||
"\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";
|
||||
},
|
||||
"syncing $whatstring failed",
|
||||
);
|
||||
};
|
||||
|
||||
my $workerid = !$dry_run ? 'auth-realm-sync' : 'auth-realm-sync-test';
|
||||
return $rpcenv->fork_worker($workerid, $realm, $authuser, $worker);
|
||||
}});
|
||||
my $workerid = !$dry_run ? 'auth-realm-sync' : 'auth-realm-sync-test';
|
||||
return $rpcenv->fork_worker($workerid, $realm, $authuser, $worker);
|
||||
},
|
||||
});
|
||||
|
||||
1;
|
||||
|
@ -10,231 +10,246 @@ use PVE::JSONSchema qw(get_standard_option register_standard_option);
|
||||
|
||||
use base qw(PVE::RESTHandler);
|
||||
|
||||
register_standard_option('group-id', {
|
||||
type => 'string',
|
||||
format => 'pve-groupid',
|
||||
completion => \&PVE::AccessControl::complete_group,
|
||||
});
|
||||
register_standard_option(
|
||||
'group-id',
|
||||
{
|
||||
type => 'string',
|
||||
format => 'pve-groupid',
|
||||
completion => \&PVE::AccessControl::complete_group,
|
||||
},
|
||||
);
|
||||
|
||||
register_standard_option('group-comment', { type => 'string', optional => 1 });
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'index',
|
||||
path => '',
|
||||
method => 'GET',
|
||||
description => "Group index.",
|
||||
permissions => {
|
||||
description => "The returned list is restricted to groups where you have 'User.Modify', 'Sys.Audit' or 'Group.Allocate' permissions on /access/groups/<group>.",
|
||||
user => 'all',
|
||||
description =>
|
||||
"The returned list is restricted to groups where you have 'User.Modify', 'Sys.Audit' or 'Group.Allocate' permissions on /access/groups/<group>.",
|
||||
user => 'all',
|
||||
},
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {},
|
||||
additionalProperties => 0,
|
||||
properties => {},
|
||||
},
|
||||
returns => {
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
properties => {
|
||||
groupid => get_standard_option('group-id'),
|
||||
comment => get_standard_option('group-comment'),
|
||||
users => {
|
||||
type => 'string',
|
||||
format => 'pve-userid-list',
|
||||
optional => 1,
|
||||
description => 'list of users which form this group',
|
||||
},
|
||||
},
|
||||
},
|
||||
links => [ { rel => 'child', href => "{groupid}" } ],
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
properties => {
|
||||
groupid => get_standard_option('group-id'),
|
||||
comment => get_standard_option('group-comment'),
|
||||
users => {
|
||||
type => 'string',
|
||||
format => 'pve-userid-list',
|
||||
optional => 1,
|
||||
description => 'list of users which form this group',
|
||||
},
|
||||
},
|
||||
},
|
||||
links => [{ rel => 'child', href => "{groupid}" }],
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
my $res = [];
|
||||
my $res = [];
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $usercfg = cfs_read_file("user.cfg");
|
||||
my $authuser = $rpcenv->get_user();
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $usercfg = cfs_read_file("user.cfg");
|
||||
my $authuser = $rpcenv->get_user();
|
||||
|
||||
my $privs = [ 'User.Modify', 'Sys.Audit', 'Group.Allocate'];
|
||||
my $privs = ['User.Modify', 'Sys.Audit', 'Group.Allocate'];
|
||||
|
||||
foreach my $group (keys %{$usercfg->{groups}}) {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
return $res;
|
||||
}});
|
||||
return $res;
|
||||
},
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'create_group',
|
||||
protected => 1,
|
||||
path => '',
|
||||
method => 'POST',
|
||||
permissions => {
|
||||
check => ['perm', '/access/groups', ['Group.Allocate']],
|
||||
check => ['perm', '/access/groups', ['Group.Allocate']],
|
||||
},
|
||||
description => "Create new group.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
groupid => get_standard_option('group-id'),
|
||||
comment => get_standard_option('group-comment'),
|
||||
},
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
groupid => get_standard_option('group-id'),
|
||||
comment => get_standard_option('group-comment'),
|
||||
},
|
||||
},
|
||||
returns => { type => 'null' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
PVE::AccessControl::lock_user_config(
|
||||
sub {
|
||||
PVE::AccessControl::lock_user_config(
|
||||
sub {
|
||||
|
||||
my $usercfg = cfs_read_file("user.cfg");
|
||||
my $usercfg = cfs_read_file("user.cfg");
|
||||
|
||||
my $group = $param->{groupid};
|
||||
my $group = $param->{groupid};
|
||||
|
||||
die "group '$group' already exists\n"
|
||||
if $usercfg->{groups}->{$group};
|
||||
die "group '$group' already exists\n"
|
||||
if $usercfg->{groups}->{$group};
|
||||
|
||||
$usercfg->{groups}->{$group} = { users => {} };
|
||||
$usercfg->{groups}->{$group} = { users => {} };
|
||||
|
||||
$usercfg->{groups}->{$group}->{comment} = $param->{comment} if $param->{comment};
|
||||
$usercfg->{groups}->{$group}->{comment} = $param->{comment}
|
||||
if $param->{comment};
|
||||
|
||||
cfs_write_file("user.cfg", $usercfg);
|
||||
},
|
||||
"create group failed",
|
||||
);
|
||||
|
||||
cfs_write_file("user.cfg", $usercfg);
|
||||
}, "create group failed");
|
||||
return undef;
|
||||
},
|
||||
});
|
||||
|
||||
return undef;
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'update_group',
|
||||
protected => 1,
|
||||
path => '{groupid}',
|
||||
method => 'PUT',
|
||||
permissions => {
|
||||
check => ['perm', '/access/groups', ['Group.Allocate']],
|
||||
check => ['perm', '/access/groups', ['Group.Allocate']],
|
||||
},
|
||||
description => "Update group data.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
groupid => get_standard_option('group-id'),
|
||||
comment => get_standard_option('group-comment'),
|
||||
},
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
groupid => get_standard_option('group-id'),
|
||||
comment => get_standard_option('group-comment'),
|
||||
},
|
||||
},
|
||||
returns => { type => 'null' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
PVE::AccessControl::lock_user_config(
|
||||
sub {
|
||||
PVE::AccessControl::lock_user_config(
|
||||
sub {
|
||||
|
||||
my $usercfg = cfs_read_file("user.cfg");
|
||||
my $usercfg = cfs_read_file("user.cfg");
|
||||
|
||||
my $group = $param->{groupid};
|
||||
my $group = $param->{groupid};
|
||||
|
||||
my $data = $usercfg->{groups}->{$group};
|
||||
my $data = $usercfg->{groups}->{$group};
|
||||
|
||||
die "group '$group' does not exist\n"
|
||||
if !$data;
|
||||
die "group '$group' does not exist\n"
|
||||
if !$data;
|
||||
|
||||
$data->{comment} = $param->{comment} if defined($param->{comment});
|
||||
$data->{comment} = $param->{comment} if defined($param->{comment});
|
||||
|
||||
cfs_write_file("user.cfg", $usercfg);
|
||||
}, "update group failed");
|
||||
cfs_write_file("user.cfg", $usercfg);
|
||||
},
|
||||
"update group failed",
|
||||
);
|
||||
|
||||
return undef;
|
||||
}});
|
||||
return undef;
|
||||
},
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'read_group',
|
||||
path => '{groupid}',
|
||||
method => 'GET',
|
||||
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.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
groupid => get_standard_option('group-id'),
|
||||
},
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
groupid => get_standard_option('group-id'),
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => "object",
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
comment => get_standard_option('group-comment'),
|
||||
members => {
|
||||
type => 'array',
|
||||
items => get_standard_option('userid-completed')
|
||||
},
|
||||
},
|
||||
type => "object",
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
comment => get_standard_option('group-comment'),
|
||||
members => {
|
||||
type => 'array',
|
||||
items => get_standard_option('userid-completed'),
|
||||
},
|
||||
},
|
||||
},
|
||||
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};
|
||||
my $data = $usercfg->{groups}->{$group};
|
||||
|
||||
die "group '$group' does not exist\n" if !$data;
|
||||
die "group '$group' does not exist\n" if !$data;
|
||||
|
||||
my $members = $data->{users} ? [ keys %{$data->{users}} ] : [];
|
||||
my $members = $data->{users} ? [keys %{ $data->{users} }] : [];
|
||||
|
||||
my $res = { members => $members };
|
||||
my $res = { members => $members };
|
||||
|
||||
$res->{comment} = $data->{comment} if defined($data->{comment});
|
||||
$res->{comment} = $data->{comment} if defined($data->{comment});
|
||||
|
||||
return $res;
|
||||
}});
|
||||
return $res;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'delete_group',
|
||||
protected => 1,
|
||||
path => '{groupid}',
|
||||
method => 'DELETE',
|
||||
permissions => {
|
||||
check => ['perm', '/access/groups', ['Group.Allocate']],
|
||||
check => ['perm', '/access/groups', ['Group.Allocate']],
|
||||
},
|
||||
description => "Delete group.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
groupid => get_standard_option('group-id'),
|
||||
}
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
groupid => get_standard_option('group-id'),
|
||||
},
|
||||
},
|
||||
returns => { type => 'null' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
PVE::AccessControl::lock_user_config(
|
||||
sub {
|
||||
PVE::AccessControl::lock_user_config(
|
||||
sub {
|
||||
|
||||
my $usercfg = cfs_read_file("user.cfg");
|
||||
my $usercfg = cfs_read_file("user.cfg");
|
||||
|
||||
my $group = $param->{groupid};
|
||||
my $group = $param->{groupid};
|
||||
|
||||
die "group '$group' does not exist\n"
|
||||
if !$usercfg->{groups}->{$group};
|
||||
die "group '$group' does not exist\n"
|
||||
if !$usercfg->{groups}->{$group};
|
||||
|
||||
delete ($usercfg->{groups}->{$group});
|
||||
delete($usercfg->{groups}->{$group});
|
||||
|
||||
PVE::AccessControl::delete_group_acl($group, $usercfg);
|
||||
PVE::AccessControl::delete_group_acl($group, $usercfg);
|
||||
|
||||
cfs_write_file("user.cfg", $usercfg);
|
||||
}, "delete group failed");
|
||||
cfs_write_file("user.cfg", $usercfg);
|
||||
},
|
||||
"delete group failed",
|
||||
);
|
||||
|
||||
return undef;
|
||||
}});
|
||||
return undef;
|
||||
},
|
||||
});
|
||||
|
||||
1;
|
||||
|
@ -21,97 +21,100 @@ my $get_cluster_last_run = sub {
|
||||
die "error on getting state for '$jobid': $@\n" if $@;
|
||||
|
||||
if (my $upid = $state->{upid}) {
|
||||
if (my $decoded = PVE::Tools::upid_decode($upid)) {
|
||||
return $decoded->{starttime};
|
||||
}
|
||||
if (my $decoded = PVE::Tools::upid_decode($upid)) {
|
||||
return $decoded->{starttime};
|
||||
}
|
||||
} else {
|
||||
return $state->{time};
|
||||
return $state->{time};
|
||||
}
|
||||
|
||||
return undef;
|
||||
};
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'syncjob_index',
|
||||
path => '',
|
||||
method => 'GET',
|
||||
description => "List configured realm-sync-jobs.",
|
||||
permissions => {
|
||||
check => ['perm', '/', ['Sys.Audit']],
|
||||
check => ['perm', '/', ['Sys.Audit']],
|
||||
},
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {},
|
||||
additionalProperties => 0,
|
||||
properties => {},
|
||||
},
|
||||
returns => {
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
properties => {
|
||||
id => {
|
||||
description => "The ID of the entry.",
|
||||
type => 'string'
|
||||
},
|
||||
enabled => {
|
||||
description => "If the job is enabled or not.",
|
||||
type => 'boolean',
|
||||
},
|
||||
comment => {
|
||||
description => "A comment for the job.",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
},
|
||||
schedule => {
|
||||
description => "The configured sync schedule.",
|
||||
type => 'string',
|
||||
},
|
||||
realm => get_standard_option('realm'),
|
||||
scope => get_standard_option('sync-scope'),
|
||||
'remove-vanished' => get_standard_option('sync-remove-vanished'),
|
||||
'last-run' => {
|
||||
description => "Last execution time of the job in seconds since the beginning of the UNIX epoch",
|
||||
type => 'integer',
|
||||
optional => 1,
|
||||
},
|
||||
'next-run' => {
|
||||
description => "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}" } ],
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
properties => {
|
||||
id => {
|
||||
description => "The ID of the entry.",
|
||||
type => 'string',
|
||||
},
|
||||
enabled => {
|
||||
description => "If the job is enabled or not.",
|
||||
type => 'boolean',
|
||||
},
|
||||
comment => {
|
||||
description => "A comment for the job.",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
},
|
||||
schedule => {
|
||||
description => "The configured sync schedule.",
|
||||
type => 'string',
|
||||
},
|
||||
realm => get_standard_option('realm'),
|
||||
scope => get_standard_option('sync-scope'),
|
||||
'remove-vanished' => get_standard_option('sync-remove-vanished'),
|
||||
'last-run' => {
|
||||
description =>
|
||||
"Last execution time of the job in seconds since the beginning of the UNIX epoch",
|
||||
type => 'integer',
|
||||
optional => 1,
|
||||
},
|
||||
'next-run' => {
|
||||
description =>
|
||||
"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}" }],
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $user = $rpcenv->get_user();
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $user = $rpcenv->get_user();
|
||||
|
||||
my $jobs_data = cfs_read_file('jobs.cfg');
|
||||
my $order = $jobs_data->{order};
|
||||
my $jobs = $jobs_data->{ids};
|
||||
my $jobs_data = cfs_read_file('jobs.cfg');
|
||||
my $order = $jobs_data->{order};
|
||||
my $jobs = $jobs_data->{ids};
|
||||
|
||||
my $res = [];
|
||||
for my $jobid (sort { $order->{$a} <=> $order->{$b} } keys %$jobs) {
|
||||
my $job = $jobs->{$jobid};
|
||||
next if $job->{type} ne 'realm-sync';
|
||||
my $res = [];
|
||||
for my $jobid (sort { $order->{$a} <=> $order->{$b} } keys %$jobs) {
|
||||
my $job = $jobs->{$jobid};
|
||||
next if $job->{type} ne 'realm-sync';
|
||||
|
||||
$job->{id} = $jobid;
|
||||
if (my $schedule = $job->{schedule}) {
|
||||
$job->{'last-run'} = eval { $get_cluster_last_run->($jobid) };
|
||||
my $last_run = $job->{'last-run'} // time(); # current time as fallback
|
||||
$job->{id} = $jobid;
|
||||
if (my $schedule = $job->{schedule}) {
|
||||
$job->{'last-run'} = eval { $get_cluster_last_run->($jobid) };
|
||||
my $last_run = $job->{'last-run'} // time(); # current time as fallback
|
||||
|
||||
my $calendar_event = Proxmox::RS::CalendarEvent->new($schedule);
|
||||
my $next_run = $calendar_event->compute_next_event($last_run);
|
||||
$job->{'next-run'} = $next_run if defined($next_run);
|
||||
}
|
||||
my $calendar_event = Proxmox::RS::CalendarEvent->new($schedule);
|
||||
my $next_run = $calendar_event->compute_next_event($last_run);
|
||||
$job->{'next-run'} = $next_run if defined($next_run);
|
||||
}
|
||||
|
||||
push @$res, $job;
|
||||
}
|
||||
push @$res, $job;
|
||||
}
|
||||
|
||||
return $res;
|
||||
}});
|
||||
return $res;
|
||||
},
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method({
|
||||
name => 'read_job',
|
||||
@ -119,31 +122,32 @@ __PACKAGE__->register_method({
|
||||
method => 'GET',
|
||||
description => "Read realm-sync job definition.",
|
||||
permissions => {
|
||||
check => ['perm', '/', ['Sys.Audit']],
|
||||
check => ['perm', '/', ['Sys.Audit']],
|
||||
},
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
id => {
|
||||
type => 'string',
|
||||
format => 'pve-configid',
|
||||
},
|
||||
},
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
id => {
|
||||
type => 'string',
|
||||
format => 'pve-configid',
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => 'object',
|
||||
type => 'object',
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
my $jobs = cfs_read_file('jobs.cfg');
|
||||
my $id = $param->{id};
|
||||
my $job = $jobs->{ids}->{$id};
|
||||
return $job if $job && $job->{type} eq 'realm-sync';
|
||||
my $jobs = cfs_read_file('jobs.cfg');
|
||||
my $id = $param->{id};
|
||||
my $job = $jobs->{ids}->{$id};
|
||||
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({
|
||||
name => 'create_job',
|
||||
@ -152,47 +156,53 @@ __PACKAGE__->register_method({
|
||||
protected => 1,
|
||||
description => "Create new realm-sync job.",
|
||||
permissions => {
|
||||
description => "'Realm.AllocateUser' on '/access/realm/<realm>' and "
|
||||
."'User.Modify' permissions to '/access/groups/'.",
|
||||
check => [ 'and',
|
||||
['perm', '/access/realm/{realm}', ['Realm.AllocateUser']],
|
||||
['perm', '/access/groups', ['User.Modify']],
|
||||
],
|
||||
description => "'Realm.AllocateUser' on '/access/realm/<realm>' and "
|
||||
. "'User.Modify' permissions to '/access/groups/'.",
|
||||
check => [
|
||||
'and',
|
||||
['perm', '/access/realm/{realm}', ['Realm.AllocateUser']],
|
||||
['perm', '/access/groups', ['User.Modify']],
|
||||
],
|
||||
},
|
||||
parameters => PVE::Jobs::RealmSync->createSchema(),
|
||||
returns => { type => 'null' },
|
||||
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 {
|
||||
my $data = cfs_read_file('jobs.cfg');
|
||||
cfs_lock_file(
|
||||
'jobs.cfg',
|
||||
undef,
|
||||
sub {
|
||||
my $data = cfs_read_file('jobs.cfg');
|
||||
|
||||
die "Job '$id' already exists\n"
|
||||
if $data->{ids}->{$id};
|
||||
die "Job '$id' already exists\n"
|
||||
if $data->{ids}->{$id};
|
||||
|
||||
my $plugin = PVE::Job::Registry->lookup('realm-sync');
|
||||
my $opts = $plugin->check_config($id, $param, 1, 1);
|
||||
my $plugin = PVE::Job::Registry->lookup('realm-sync');
|
||||
my $opts = $plugin->check_config($id, $param, 1, 1);
|
||||
|
||||
my $realm = $opts->{realm};
|
||||
my $cfg = cfs_read_file('domains.cfg');
|
||||
my $realm = $opts->{realm};
|
||||
my $cfg = cfs_read_file('domains.cfg');
|
||||
|
||||
raise_param_exc({ realm => "No such realm '$realm'" })
|
||||
if !defined($cfg->{ids}->{$realm});
|
||||
raise_param_exc({ realm => "No such realm '$realm'" })
|
||||
if !defined($cfg->{ids}->{$realm});
|
||||
|
||||
my $realm_type = $cfg->{ids}->{$realm}->{type};
|
||||
raise_param_exc({ realm => "Only LDAP/AD realms can be synced." })
|
||||
if $realm_type ne 'ldap' && $realm_type ne 'ad';
|
||||
my $realm_type = $cfg->{ids}->{$realm}->{type};
|
||||
raise_param_exc({ realm => "Only LDAP/AD realms can be synced." })
|
||||
if $realm_type ne 'ldap' && $realm_type ne 'ad';
|
||||
|
||||
$data->{ids}->{$id} = $opts;
|
||||
$data->{ids}->{$id} = $opts;
|
||||
|
||||
cfs_write_file('jobs.cfg', $data);
|
||||
});
|
||||
die "$@" if ($@);
|
||||
cfs_write_file('jobs.cfg', $data);
|
||||
},
|
||||
);
|
||||
die "$@" if ($@);
|
||||
|
||||
return undef;
|
||||
}});
|
||||
return undef;
|
||||
},
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method({
|
||||
name => 'update_job',
|
||||
@ -201,45 +211,50 @@ __PACKAGE__->register_method({
|
||||
protected => 1,
|
||||
description => "Update realm-sync job definition.",
|
||||
permissions => {
|
||||
description => "'Realm.AllocateUser' on '/access/realm/<realm>' and 'User.Modify'"
|
||||
." permissions to '/access/groups/'.",
|
||||
check => [ 'and',
|
||||
['perm', '/access/realm/{realm}', ['Realm.AllocateUser']],
|
||||
['perm', '/access/groups', ['User.Modify']],
|
||||
],
|
||||
description => "'Realm.AllocateUser' on '/access/realm/<realm>' and 'User.Modify'"
|
||||
. " permissions to '/access/groups/'.",
|
||||
check => [
|
||||
'and',
|
||||
['perm', '/access/realm/{realm}', ['Realm.AllocateUser']],
|
||||
['perm', '/access/groups', ['User.Modify']],
|
||||
],
|
||||
},
|
||||
parameters => PVE::Jobs::RealmSync->updateSchema(),
|
||||
returns => { type => 'null' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
my $id = extract_param($param, 'id');
|
||||
my $delete = extract_param($param, 'delete');
|
||||
$delete = [PVE::Tools::split_list($delete)] if $delete;
|
||||
my $id = extract_param($param, 'id');
|
||||
my $delete = extract_param($param, '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 {
|
||||
my $jobs = cfs_read_file('jobs.cfg');
|
||||
cfs_lock_file(
|
||||
'jobs.cfg',
|
||||
undef,
|
||||
sub {
|
||||
my $jobs = cfs_read_file('jobs.cfg');
|
||||
|
||||
my $plugin = PVE::Job::Registry->lookup('realm-sync');
|
||||
my $opts = $plugin->check_config($id, $param, 0, 1);
|
||||
my $plugin = PVE::Job::Registry->lookup('realm-sync');
|
||||
my $opts = $plugin->check_config($id, $param, 0, 1);
|
||||
|
||||
my $job = $jobs->{ids}->{$id};
|
||||
die "no such realm-sync job\n" if !$job || $job->{type} ne 'realm-sync';
|
||||
my $job = $jobs->{ids}->{$id};
|
||||
die "no such realm-sync job\n" if !$job || $job->{type} ne 'realm-sync';
|
||||
|
||||
my $options = $plugin->options();
|
||||
PVE::SectionConfig::delete_from_config($job, $options, $opts, $delete);
|
||||
my $options = $plugin->options();
|
||||
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);
|
||||
|
||||
return;
|
||||
});
|
||||
die "$@" if ($@);
|
||||
}});
|
||||
cfs_write_file('jobs.cfg', $jobs);
|
||||
|
||||
return;
|
||||
},
|
||||
);
|
||||
die "$@" if ($@);
|
||||
},
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method({
|
||||
name => 'delete_job',
|
||||
@ -247,38 +262,46 @@ __PACKAGE__->register_method({
|
||||
method => 'DELETE',
|
||||
description => "Delete realm-sync job definition.",
|
||||
permissions => {
|
||||
check => ['perm', '/', ['Sys.Modify']],
|
||||
check => ['perm', '/', ['Sys.Modify']],
|
||||
},
|
||||
protected => 1,
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
id => {
|
||||
type => 'string',
|
||||
format => 'pve-configid',
|
||||
},
|
||||
},
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
id => {
|
||||
type => 'string',
|
||||
format => 'pve-configid',
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => { type => 'null' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
my $id = $param->{id};
|
||||
my $id = $param->{id};
|
||||
|
||||
cfs_lock_file('jobs.cfg', undef, sub {
|
||||
my $jobs = cfs_read_file('jobs.cfg');
|
||||
cfs_lock_file(
|
||||
'jobs.cfg',
|
||||
undef,
|
||||
sub {
|
||||
my $jobs = cfs_read_file('jobs.cfg');
|
||||
|
||||
if (!defined($jobs->{ids}->{$id}) || $jobs->{ids}->{$id}->{type} ne 'realm-sync') {
|
||||
raise_param_exc({ id => "No such job '$id'" });
|
||||
}
|
||||
delete $jobs->{ids}->{$id};
|
||||
if (
|
||||
!defined($jobs->{ids}->{$id})
|
||||
|| $jobs->{ids}->{$id}->{type} ne 'realm-sync'
|
||||
) {
|
||||
raise_param_exc({ id => "No such job '$id'" });
|
||||
}
|
||||
delete $jobs->{ids}->{$id};
|
||||
|
||||
cfs_write_file('jobs.cfg', $jobs);
|
||||
PVE::Jobs::RealmSync::save_state($id, undef);
|
||||
});
|
||||
die "$@" if $@;
|
||||
cfs_write_file('jobs.cfg', $jobs);
|
||||
PVE::Jobs::RealmSync::save_state($id, undef);
|
||||
},
|
||||
);
|
||||
die "$@" if $@;
|
||||
|
||||
return undef;
|
||||
}});
|
||||
return undef;
|
||||
},
|
||||
});
|
||||
|
||||
1;
|
||||
|
@ -33,304 +33,327 @@ my $lookup_openid_auth = sub {
|
||||
die "wrong realm type ($config->{type} != openid)\n" if $config->{type} ne "openid";
|
||||
|
||||
my $openid_config = {
|
||||
issuer_url => $config->{'issuer-url'},
|
||||
client_id => $config->{'client-id'},
|
||||
client_key => $config->{'client-key'},
|
||||
issuer_url => $config->{'issuer-url'},
|
||||
client_id => $config->{'client-id'},
|
||||
client_key => $config->{'client-key'},
|
||||
};
|
||||
$openid_config->{prompt} = $config->{'prompt'} if defined($config->{'prompt'});
|
||||
|
||||
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'})) {
|
||||
$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);
|
||||
return ($config, $openid);
|
||||
};
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'index',
|
||||
path => '',
|
||||
method => 'GET',
|
||||
description => "Directory index.",
|
||||
permissions => {
|
||||
user => 'all',
|
||||
user => 'all',
|
||||
},
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {},
|
||||
additionalProperties => 0,
|
||||
properties => {},
|
||||
},
|
||||
returns => {
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
properties => {
|
||||
subdir => { type => 'string' },
|
||||
},
|
||||
},
|
||||
links => [ { rel => 'child', href => "{subdir}" } ],
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
properties => {
|
||||
subdir => { type => 'string' },
|
||||
},
|
||||
},
|
||||
links => [{ rel => 'child', href => "{subdir}" }],
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
return [
|
||||
{ subdir => 'auth-url' },
|
||||
{ subdir => 'login' },
|
||||
];
|
||||
}});
|
||||
return [
|
||||
{ subdir => 'auth-url' }, { subdir => 'login' },
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'auth_url',
|
||||
path => 'auth-url',
|
||||
method => 'POST',
|
||||
protected => 1,
|
||||
description => "Get the OpenId Authorization Url for the specified realm.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
realm => get_standard_option('realm'),
|
||||
'redirect-url' => {
|
||||
description => "Redirection Url. The client should set this to the used server url (location.origin).",
|
||||
type => 'string',
|
||||
maxLength => 255,
|
||||
},
|
||||
},
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
realm => get_standard_option('realm'),
|
||||
'redirect-url' => {
|
||||
description =>
|
||||
"Redirection Url. The client should set this to the used server url (location.origin).",
|
||||
type => 'string',
|
||||
maxLength => 255,
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => "string",
|
||||
description => "Redirection URL.",
|
||||
type => "string",
|
||||
description => "Redirection URL.",
|
||||
},
|
||||
permissions => { user => 'world' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
my $dcconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
|
||||
local $ENV{all_proxy} = $dcconf->{http_proxy} if exists $dcconf->{http_proxy};
|
||||
my $dcconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
|
||||
local $ENV{all_proxy} = $dcconf->{http_proxy} if exists $dcconf->{http_proxy};
|
||||
|
||||
my $realm = extract_param($param, 'realm');
|
||||
my $redirect_url = extract_param($param, 'redirect-url');
|
||||
my $realm = extract_param($param, 'realm');
|
||||
my $redirect_url = extract_param($param, 'redirect-url');
|
||||
|
||||
my ($config, $openid) = $lookup_openid_auth->($realm, $redirect_url);
|
||||
my $url = $openid->authorize_url($openid_state_path , $realm);
|
||||
my ($config, $openid) = $lookup_openid_auth->($realm, $redirect_url);
|
||||
my $url = $openid->authorize_url($openid_state_path, $realm);
|
||||
|
||||
return $url;
|
||||
}});
|
||||
return $url;
|
||||
},
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'login',
|
||||
path => 'login',
|
||||
method => 'POST',
|
||||
protected => 1,
|
||||
description => " Verify OpenID authorization code and create a ticket.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
'state' => {
|
||||
description => "OpenId state.",
|
||||
type => 'string',
|
||||
maxLength => 1024,
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
'state' => {
|
||||
description => "OpenId state.",
|
||||
type => 'string',
|
||||
maxLength => 1024,
|
||||
},
|
||||
code => {
|
||||
description => "OpenId authorization code.",
|
||||
type => 'string',
|
||||
maxLength => 4096,
|
||||
code => {
|
||||
description => "OpenId authorization code.",
|
||||
type => 'string',
|
||||
maxLength => 4096,
|
||||
},
|
||||
'redirect-url' => {
|
||||
description => "Redirection Url. The client should set this to the used server url (location.origin).",
|
||||
type => 'string',
|
||||
maxLength => 255,
|
||||
},
|
||||
},
|
||||
'redirect-url' => {
|
||||
description =>
|
||||
"Redirection Url. The client should set this to the used server url (location.origin).",
|
||||
type => 'string',
|
||||
maxLength => 255,
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
properties => {
|
||||
username => { type => 'string' },
|
||||
ticket => { type => 'string' },
|
||||
CSRFPreventionToken => { type => 'string' },
|
||||
cap => { type => 'object' }, # computed api permissions
|
||||
clustername => { type => 'string', optional => 1 },
|
||||
},
|
||||
properties => {
|
||||
username => { type => 'string' },
|
||||
ticket => { type => 'string' },
|
||||
CSRFPreventionToken => { type => 'string' },
|
||||
cap => { type => 'object' }, # computed api permissions
|
||||
clustername => { type => 'string', optional => 1 },
|
||||
},
|
||||
},
|
||||
permissions => { user => 'world' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
|
||||
my $res;
|
||||
eval {
|
||||
my $dcconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
|
||||
local $ENV{all_proxy} = $dcconf->{http_proxy} if exists $dcconf->{http_proxy};
|
||||
my $res;
|
||||
eval {
|
||||
my $dcconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
|
||||
local $ENV{all_proxy} = $dcconf->{http_proxy} if exists $dcconf->{http_proxy};
|
||||
|
||||
my ($realm, $private_auth_state) = PVE::RS::OpenId::verify_public_auth_state(
|
||||
$openid_state_path, $param->{'state'});
|
||||
my ($realm, $private_auth_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(
|
||||
$param->{code},
|
||||
$private_auth_state,
|
||||
$config->{'query-userinfo'} // 1,
|
||||
);
|
||||
my $subject = $info->{'sub'};
|
||||
my $info = $openid->verify_authorization_code(
|
||||
$param->{code},
|
||||
$private_auth_state,
|
||||
$config->{'query-userinfo'} // 1,
|
||||
);
|
||||
my $subject = $info->{'sub'};
|
||||
|
||||
my $unique_name;
|
||||
my $unique_name;
|
||||
|
||||
my $user_attr = $config->{'username-claim'} // 'sub';
|
||||
if (defined($info->{$user_attr})) {
|
||||
$unique_name = $info->{$user_attr};
|
||||
} elsif ($user_attr eq 'subject') { # stay compat with old versions
|
||||
$unique_name = $subject;
|
||||
} elsif ($user_attr eq 'username') { # stay compat with old versions
|
||||
my $username = $info->{'preferred_username'};
|
||||
die "missing claim 'preferred_username'\n" if !defined($username);
|
||||
$unique_name = $username;
|
||||
} else {
|
||||
# neither the attr nor fallback are defined in info..
|
||||
die "missing configured claim '$user_attr' in returned info object\n";
|
||||
}
|
||||
my $user_attr = $config->{'username-claim'} // 'sub';
|
||||
if (defined($info->{$user_attr})) {
|
||||
$unique_name = $info->{$user_attr};
|
||||
} elsif ($user_attr eq 'subject') { # stay compat with old versions
|
||||
$unique_name = $subject;
|
||||
} elsif ($user_attr eq 'username') { # stay compat with old versions
|
||||
my $username = $info->{'preferred_username'};
|
||||
die "missing claim 'preferred_username'\n" if !defined($username);
|
||||
$unique_name = $username;
|
||||
} else {
|
||||
# neither the attr nor fallback are defined in info..
|
||||
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
|
||||
PVE::Auth::Plugin::verify_username($username);
|
||||
# first, check if $username respects our naming conventions
|
||||
PVE::Auth::Plugin::verify_username($username);
|
||||
|
||||
if ($config->{'autocreate'} && !$rpcenv->check_user_exist($username, 1)) {
|
||||
PVE::AccessControl::lock_user_config(sub {
|
||||
my $usercfg = cfs_read_file("user.cfg");
|
||||
if ($config->{'autocreate'} && !$rpcenv->check_user_exist($username, 1)) {
|
||||
PVE::AccessControl::lock_user_config(
|
||||
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 };
|
||||
if (defined(my $email = $info->{'email'})) {
|
||||
$entry->{email} = $email;
|
||||
}
|
||||
if (defined(my $given_name = $info->{'given_name'})) {
|
||||
$entry->{firstname} = $given_name;
|
||||
}
|
||||
if (defined(my $family_name = $info->{'family_name'})) {
|
||||
$entry->{lastname} = $family_name;
|
||||
}
|
||||
my $entry = { enable => 1 };
|
||||
if (defined(my $email = $info->{'email'})) {
|
||||
$entry->{email} = $email;
|
||||
}
|
||||
if (defined(my $given_name = $info->{'given_name'})) {
|
||||
$entry->{firstname} = $given_name;
|
||||
}
|
||||
if (defined(my $family_name = $info->{'family_name'})) {
|
||||
$entry->{lastname} = $family_name;
|
||||
}
|
||||
|
||||
$usercfg->{users}->{$username} = $entry;
|
||||
$usercfg->{users}->{$username} = $entry;
|
||||
|
||||
cfs_write_file("user.cfg", $usercfg);
|
||||
}, "autocreate openid user failed");
|
||||
} else {
|
||||
# test if user exists and is enabled
|
||||
$rpcenv->check_user_enabled($username);
|
||||
}
|
||||
cfs_write_file("user.cfg", $usercfg);
|
||||
},
|
||||
"autocreate openid user failed",
|
||||
);
|
||||
} 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_list = $info->{$groups_claim})) {
|
||||
if (ref($groups_list) eq 'ARRAY') {
|
||||
PVE::AccessControl::lock_user_config(sub {
|
||||
my $usercfg = cfs_read_file("user.cfg");
|
||||
if (defined(my $groups_claim = $config->{'groups-claim'})) {
|
||||
if (defined(my $groups_list = $info->{$groups_claim})) {
|
||||
if (ref($groups_list) eq 'ARRAY') {
|
||||
PVE::AccessControl::lock_user_config(
|
||||
sub {
|
||||
my $usercfg = cfs_read_file("user.cfg");
|
||||
|
||||
my $oidc_groups;
|
||||
for my $group (@$groups_list) {
|
||||
if (PVE::AccessControl::verify_groupname($group, 1)) {
|
||||
# add realm name as suffix to group
|
||||
$oidc_groups->{"$group-$realm"} = 1;
|
||||
} else {
|
||||
# ignore any groups in the list that have invalid characters
|
||||
syslog(
|
||||
'warn',
|
||||
"openid group '$group' contains invalid characters"
|
||||
);
|
||||
}
|
||||
}
|
||||
my $oidc_groups;
|
||||
for my $group (@$groups_list) {
|
||||
if (PVE::AccessControl::verify_groupname($group, 1)) {
|
||||
# add realm name as suffix to group
|
||||
$oidc_groups->{"$group-$realm"} = 1;
|
||||
} else {
|
||||
# ignore any groups in the list that have invalid characters
|
||||
syslog(
|
||||
'warn',
|
||||
"openid group '$group' contains invalid characters",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
# get groups that exist in OIDC and PVE
|
||||
my $groups_intersect;
|
||||
for my $group (keys %$oidc_groups) {
|
||||
$groups_intersect->{$group} = 1 if $usercfg->{groups}->{$group};
|
||||
}
|
||||
# get groups that exist in OIDC and PVE
|
||||
my $groups_intersect;
|
||||
for my $group (keys %$oidc_groups) {
|
||||
$groups_intersect->{$group} = 1
|
||||
if $usercfg->{groups}->{$group};
|
||||
}
|
||||
|
||||
if ($config->{'groups-autocreate'}) {
|
||||
# populate all groups in claim
|
||||
$groups_intersect = $oidc_groups;
|
||||
my $groups_to_create;
|
||||
for my $group (keys %$oidc_groups) {
|
||||
$groups_to_create->{$group} = 1 if !$usercfg->{groups}->{$group};
|
||||
}
|
||||
if ($groups_to_create) {
|
||||
# log a messages about created groups here
|
||||
my $groups_to_create_string = join(', ', sort keys %$groups_to_create);
|
||||
syslog(
|
||||
'info',
|
||||
"groups created automatically from openid claim: $groups_to_create_string"
|
||||
);
|
||||
}
|
||||
}
|
||||
if ($config->{'groups-autocreate'}) {
|
||||
# populate all groups in claim
|
||||
$groups_intersect = $oidc_groups;
|
||||
my $groups_to_create;
|
||||
for my $group (keys %$oidc_groups) {
|
||||
$groups_to_create->{$group} = 1
|
||||
if !$usercfg->{groups}->{$group};
|
||||
}
|
||||
if ($groups_to_create) {
|
||||
# log a messages about created groups here
|
||||
my $groups_to_create_string =
|
||||
join(', ', sort keys %$groups_to_create);
|
||||
syslog(
|
||||
'info',
|
||||
"groups created automatically from openid claim: $groups_to_create_string",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
# if groups should be overwritten, delete all the users groups first
|
||||
if ($config->{'groups-overwrite'} ) {
|
||||
PVE::AccessControl::delete_user_group(
|
||||
$username,
|
||||
$usercfg,
|
||||
);
|
||||
syslog(
|
||||
'info',
|
||||
"openid overwrite groups enabled; user '$username' removed from all groups"
|
||||
);
|
||||
}
|
||||
# if groups should be overwritten, delete all the users groups first
|
||||
if ($config->{'groups-overwrite'}) {
|
||||
PVE::AccessControl::delete_user_group(
|
||||
$username, $usercfg,
|
||||
);
|
||||
syslog(
|
||||
'info',
|
||||
"openid overwrite groups enabled; user '$username' removed from all groups",
|
||||
);
|
||||
}
|
||||
|
||||
if (keys %$groups_intersect) {
|
||||
# ensure user is a member of the groups
|
||||
for my $group (keys %$groups_intersect) {
|
||||
PVE::AccessControl::add_user_group(
|
||||
$username,
|
||||
$usercfg,
|
||||
$group
|
||||
);
|
||||
}
|
||||
if (keys %$groups_intersect) {
|
||||
# ensure user is a member of the groups
|
||||
for my $group (keys %$groups_intersect) {
|
||||
PVE::AccessControl::add_user_group(
|
||||
$username,
|
||||
$usercfg,
|
||||
$group,
|
||||
);
|
||||
}
|
||||
|
||||
my $groups_intersect_string = join(', ', sort keys %$groups_intersect);
|
||||
syslog(
|
||||
'info',
|
||||
"openid user '$username' added to groups: $groups_intersect_string"
|
||||
);
|
||||
}
|
||||
my $groups_intersect_string =
|
||||
join(', ', sort keys %$groups_intersect);
|
||||
syslog(
|
||||
'info',
|
||||
"openid user '$username' added to groups: $groups_intersect_string",
|
||||
);
|
||||
}
|
||||
|
||||
cfs_write_file("user.cfg", $usercfg);
|
||||
}, "openid group mapping failed");
|
||||
} else {
|
||||
syslog('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");
|
||||
}
|
||||
}
|
||||
cfs_write_file("user.cfg", $usercfg);
|
||||
},
|
||||
"openid group mapping failed",
|
||||
);
|
||||
} else {
|
||||
syslog(
|
||||
'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 $csrftoken = PVE::AccessControl::assemble_csrf_prevention_token($username);
|
||||
my $cap = $rpcenv->compute_api_permission($username);
|
||||
my $ticket = PVE::AccessControl::assemble_ticket($username);
|
||||
my $csrftoken = PVE::AccessControl::assemble_csrf_prevention_token($username);
|
||||
my $cap = $rpcenv->compute_api_permission($username);
|
||||
|
||||
$res = {
|
||||
ticket => $ticket,
|
||||
username => $username,
|
||||
CSRFPreventionToken => $csrftoken,
|
||||
cap => $cap,
|
||||
};
|
||||
$res = {
|
||||
ticket => $ticket,
|
||||
username => $username,
|
||||
CSRFPreventionToken => $csrftoken,
|
||||
cap => $cap,
|
||||
};
|
||||
|
||||
my $clinfo = PVE::Cluster::get_clinfo();
|
||||
if ($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");
|
||||
# do not return any info to prevent user enumeration attacks
|
||||
die PVE::Exception->new("authentication failure\n", code => 401);
|
||||
}
|
||||
my $clinfo = PVE::Cluster::get_clinfo();
|
||||
if (
|
||||
$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");
|
||||
# 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;
|
||||
},
|
||||
});
|
||||
|
@ -10,215 +10,235 @@ use PVE::JSONSchema qw(get_standard_option register_standard_option);
|
||||
|
||||
use base qw(PVE::RESTHandler);
|
||||
|
||||
register_standard_option('role-id', {
|
||||
type => 'string',
|
||||
format => 'pve-roleid',
|
||||
});
|
||||
register_standard_option('role-privs', {
|
||||
type => 'string' ,
|
||||
format => 'pve-priv-list',
|
||||
optional => 1,
|
||||
});
|
||||
register_standard_option(
|
||||
'role-id',
|
||||
{
|
||||
type => 'string',
|
||||
format => 'pve-roleid',
|
||||
},
|
||||
);
|
||||
register_standard_option(
|
||||
'role-privs',
|
||||
{
|
||||
type => 'string',
|
||||
format => 'pve-priv-list',
|
||||
optional => 1,
|
||||
},
|
||||
);
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'index',
|
||||
path => '',
|
||||
method => 'GET',
|
||||
description => "Role index.",
|
||||
permissions => {
|
||||
user => 'all',
|
||||
user => 'all',
|
||||
},
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {},
|
||||
additionalProperties => 0,
|
||||
properties => {},
|
||||
},
|
||||
returns => {
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
properties => {
|
||||
roleid => get_standard_option('role-id'),
|
||||
privs => get_standard_option('role-privs'),
|
||||
special => { type => 'boolean', optional => 1, default => 0 },
|
||||
},
|
||||
},
|
||||
links => [ { rel => 'child', href => "{roleid}" } ],
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
properties => {
|
||||
roleid => get_standard_option('role-id'),
|
||||
privs => get_standard_option('role-privs'),
|
||||
special => { type => 'boolean', optional => 1, default => 0 },
|
||||
},
|
||||
},
|
||||
links => [{ rel => 'child', href => "{roleid}" }],
|
||||
},
|
||||
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}}) {
|
||||
my $privs = join(',', sort keys %{$usercfg->{roles}->{$role}});
|
||||
push @$res, {
|
||||
roleid => $role,
|
||||
privs => $privs,
|
||||
special => PVE::AccessControl::role_is_special($role),
|
||||
};
|
||||
}
|
||||
foreach my $role (keys %{ $usercfg->{roles} }) {
|
||||
my $privs = join(',', sort keys %{ $usercfg->{roles}->{$role} });
|
||||
push @$res,
|
||||
{
|
||||
roleid => $role,
|
||||
privs => $privs,
|
||||
special => PVE::AccessControl::role_is_special($role),
|
||||
};
|
||||
}
|
||||
|
||||
return $res;
|
||||
}});
|
||||
return $res;
|
||||
},
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'create_role',
|
||||
protected => 1,
|
||||
path => '',
|
||||
method => 'POST',
|
||||
permissions => {
|
||||
check => ['perm', '/access', ['Sys.Modify']],
|
||||
check => ['perm', '/access', ['Sys.Modify']],
|
||||
},
|
||||
description => "Create new role.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
roleid => get_standard_option('role-id'),
|
||||
privs => get_standard_option('role-privs'),
|
||||
},
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
roleid => get_standard_option('role-id'),
|
||||
privs => get_standard_option('role-privs'),
|
||||
},
|
||||
},
|
||||
returns => { type => 'null' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
my $role = $param->{roleid};
|
||||
my $role = $param->{roleid};
|
||||
|
||||
if ($role =~ /^PVE/i) {
|
||||
raise_param_exc({
|
||||
roleid => "cannot use role ID starting with the (case-insensitive) 'PVE' namespace",
|
||||
});
|
||||
}
|
||||
if ($role =~ /^PVE/i) {
|
||||
raise_param_exc({
|
||||
roleid =>
|
||||
"cannot use role ID starting with the (case-insensitive) 'PVE' namespace",
|
||||
});
|
||||
}
|
||||
|
||||
PVE::AccessControl::lock_user_config(sub {
|
||||
my $usercfg = cfs_read_file("user.cfg");
|
||||
PVE::AccessControl::lock_user_config(
|
||||
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);
|
||||
}, "create role failed");
|
||||
cfs_write_file("user.cfg", $usercfg);
|
||||
},
|
||||
"create role failed",
|
||||
);
|
||||
|
||||
return undef;
|
||||
}});
|
||||
return undef;
|
||||
},
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'update_role',
|
||||
protected => 1,
|
||||
path => '{roleid}',
|
||||
method => 'PUT',
|
||||
permissions => {
|
||||
check => ['perm', '/access', ['Sys.Modify']],
|
||||
check => ['perm', '/access', ['Sys.Modify']],
|
||||
},
|
||||
description => "Update an existing role.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
roleid => get_standard_option('role-id'),
|
||||
privs => get_standard_option('role-privs'),
|
||||
append => { type => 'boolean', optional => 1, requires => 'privs' },
|
||||
},
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
roleid => get_standard_option('role-id'),
|
||||
privs => get_standard_option('role-privs'),
|
||||
append => { type => 'boolean', optional => 1, requires => 'privs' },
|
||||
},
|
||||
},
|
||||
returns => { type => 'null' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
my $role = $param->{roleid};
|
||||
my $role = $param->{roleid};
|
||||
|
||||
die "auto-generated role '$role' cannot be modified\n"
|
||||
if PVE::AccessControl::role_is_special($role);
|
||||
die "auto-generated role '$role' cannot be modified\n"
|
||||
if PVE::AccessControl::role_is_special($role);
|
||||
|
||||
PVE::AccessControl::lock_user_config(sub {
|
||||
my $usercfg = cfs_read_file("user.cfg");
|
||||
PVE::AccessControl::lock_user_config(
|
||||
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);
|
||||
}, "update role failed");
|
||||
cfs_write_file("user.cfg", $usercfg);
|
||||
},
|
||||
"update role failed",
|
||||
);
|
||||
|
||||
return undef;
|
||||
}});
|
||||
return undef;
|
||||
},
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'read_role',
|
||||
path => '{roleid}',
|
||||
method => 'GET',
|
||||
permissions => {
|
||||
user => 'all',
|
||||
user => 'all',
|
||||
},
|
||||
description => "Get role configuration.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
roleid => get_standard_option('role-id'),
|
||||
},
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
roleid => get_standard_option('role-id'),
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => "object",
|
||||
additionalProperties => 0,
|
||||
properties => PVE::AccessControl::create_priv_properties(),
|
||||
type => "object",
|
||||
additionalProperties => 0,
|
||||
properties => PVE::AccessControl::create_priv_properties(),
|
||||
},
|
||||
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',
|
||||
protected => 1,
|
||||
path => '{roleid}',
|
||||
method => 'DELETE',
|
||||
permissions => {
|
||||
check => ['perm', '/access', ['Sys.Modify']],
|
||||
check => ['perm', '/access', ['Sys.Modify']],
|
||||
},
|
||||
description => "Delete role.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
roleid => get_standard_option('role-id'),
|
||||
},
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
roleid => get_standard_option('role-id'),
|
||||
},
|
||||
},
|
||||
returns => { type => 'null' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
my $role = $param->{roleid};
|
||||
my $role = $param->{roleid};
|
||||
|
||||
die "auto-generated role '$role' cannot be deleted\n"
|
||||
if PVE::AccessControl::role_is_special($role);
|
||||
die "auto-generated role '$role' cannot be deleted\n"
|
||||
if PVE::AccessControl::role_is_special($role);
|
||||
|
||||
PVE::AccessControl::lock_user_config(sub {
|
||||
my $usercfg = cfs_read_file("user.cfg");
|
||||
PVE::AccessControl::lock_user_config(
|
||||
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);
|
||||
}, "delete role failed");
|
||||
cfs_write_file("user.cfg", $usercfg);
|
||||
},
|
||||
"delete role failed",
|
||||
);
|
||||
|
||||
return undef;
|
||||
}
|
||||
return undef;
|
||||
},
|
||||
});
|
||||
|
||||
1;
|
||||
|
@ -23,7 +23,7 @@ our $OPTIONAL_PASSWORD_SCHEMA = {
|
||||
type => 'string',
|
||||
optional => 1, # Only required if not root@pam
|
||||
minLength => 5,
|
||||
maxLength => 64
|
||||
maxLength => 64,
|
||||
};
|
||||
|
||||
my $TFA_TYPE_SCHEMA = {
|
||||
@ -34,22 +34,22 @@ my $TFA_TYPE_SCHEMA = {
|
||||
|
||||
my %TFA_INFO_PROPERTIES = (
|
||||
id => {
|
||||
type => 'string',
|
||||
description => 'The id used to reference this entry.',
|
||||
type => 'string',
|
||||
description => 'The id used to reference this entry.',
|
||||
},
|
||||
description => {
|
||||
type => 'string',
|
||||
description => 'User chosen description for this entry.',
|
||||
type => 'string',
|
||||
description => 'User chosen description for this entry.',
|
||||
},
|
||||
created => {
|
||||
type => 'integer',
|
||||
description => 'Creation time of this entry as unix epoch.',
|
||||
type => 'integer',
|
||||
description => 'Creation time of this entry as unix epoch.',
|
||||
},
|
||||
enable => {
|
||||
type => 'boolean',
|
||||
description => 'Whether this TFA entry is currently enabled.',
|
||||
optional => 1,
|
||||
default => 1,
|
||||
type => 'boolean',
|
||||
description => 'Whether this TFA entry is currently enabled.',
|
||||
optional => 1,
|
||||
default => 1,
|
||||
},
|
||||
);
|
||||
|
||||
@ -57,8 +57,8 @@ my $TYPED_TFA_ENTRY_SCHEMA = {
|
||||
type => 'object',
|
||||
description => 'TFA Entry.',
|
||||
properties => {
|
||||
type => $TFA_TYPE_SCHEMA,
|
||||
%TFA_INFO_PROPERTIES,
|
||||
type => $TFA_TYPE_SCHEMA,
|
||||
%TFA_INFO_PROPERTIES,
|
||||
},
|
||||
};
|
||||
|
||||
@ -70,28 +70,28 @@ my $TFA_ID_SCHEMA = {
|
||||
my $TFA_UPDATE_INFO_SCHEMA = {
|
||||
type => 'object',
|
||||
properties => {
|
||||
id => {
|
||||
type => 'string',
|
||||
description => 'The id of a newly added TFA entry.',
|
||||
},
|
||||
challenge => {
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
description =>
|
||||
'When adding u2f entries, this contains a challenge the user must respond to in order'
|
||||
.' to finish the registration.'
|
||||
},
|
||||
recovery => {
|
||||
type => 'array',
|
||||
optional => 1,
|
||||
description =>
|
||||
'When adding recovery codes, this contains the list of codes to be displayed to'
|
||||
.' the user',
|
||||
items => {
|
||||
type => 'string',
|
||||
description => 'A recovery entry.'
|
||||
},
|
||||
},
|
||||
id => {
|
||||
type => 'string',
|
||||
description => 'The id of a newly added TFA entry.',
|
||||
},
|
||||
challenge => {
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
description =>
|
||||
'When adding u2f entries, this contains a challenge the user must respond to in order'
|
||||
. ' to finish the registration.',
|
||||
},
|
||||
recovery => {
|
||||
type => 'array',
|
||||
optional => 1,
|
||||
description =>
|
||||
'When adding recovery codes, this contains the list of codes to be displayed to'
|
||||
. ' the user',
|
||||
items => {
|
||||
type => 'string',
|
||||
description => 'A recovery entry.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -100,290 +100,304 @@ my $TFA_UPDATE_INFO_SCHEMA = {
|
||||
my sub set_user_tfa_enabled : prototype($$$) {
|
||||
my ($userid, $realm, $tfa_cfg) = @_;
|
||||
|
||||
PVE::AccessControl::lock_user_config(sub {
|
||||
my $user_cfg = cfs_read_file('user.cfg');
|
||||
my $user = $user_cfg->{users}->{$userid};
|
||||
my $keys = $user->{keys};
|
||||
# When enabling, we convert old-old keys,
|
||||
# When disabling, we shouldn't actually have old keys anymore, so if they are there,
|
||||
# they'll be removed.
|
||||
if ($tfa_cfg && $keys && $keys !~ /^x(?:!.*)?$/) {
|
||||
my $domain_cfg = cfs_read_file('domains.cfg');
|
||||
my $realm_cfg = $domain_cfg->{ids}->{$realm};
|
||||
die "auth domain '$realm' does not exist\n" if !$realm_cfg;
|
||||
PVE::AccessControl::lock_user_config(
|
||||
sub {
|
||||
my $user_cfg = cfs_read_file('user.cfg');
|
||||
my $user = $user_cfg->{users}->{$userid};
|
||||
my $keys = $user->{keys};
|
||||
# When enabling, we convert old-old keys,
|
||||
# When disabling, we shouldn't actually have old keys anymore, so if they are there,
|
||||
# they'll be removed.
|
||||
if ($tfa_cfg && $keys && $keys !~ /^x(?:!.*)?$/) {
|
||||
my $domain_cfg = cfs_read_file('domains.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};
|
||||
$realm_tfa = PVE::Auth::Plugin::parse_tfa_config($realm_tfa) if $realm_tfa;
|
||||
my $realm_tfa = $realm_cfg->{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);
|
||||
}
|
||||
$user->{keys} = $tfa_cfg ? 'x' : undef;
|
||||
cfs_write_file("user.cfg", $user_cfg);
|
||||
}, "enabling TFA for the user failed");
|
||||
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",
|
||||
);
|
||||
}
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'list_user_tfa',
|
||||
path => '{userid}',
|
||||
method => 'GET',
|
||||
permissions => {
|
||||
check => [ 'or',
|
||||
['userid-param', 'self'],
|
||||
['userid-group', ['User.Modify', 'Sys.Audit']],
|
||||
],
|
||||
check => [
|
||||
'or', ['userid-param', 'self'], ['userid-group', ['User.Modify', 'Sys.Audit']],
|
||||
],
|
||||
},
|
||||
protected => 1, # else we can't access shadow files
|
||||
description => 'List TFA configurations of users.',
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
userid => get_standard_option('userid', {
|
||||
completion => \&PVE::AccessControl::complete_username,
|
||||
}),
|
||||
}
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
userid => get_standard_option(
|
||||
'userid',
|
||||
{
|
||||
completion => \&PVE::AccessControl::complete_username,
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
description => "A list of the user's TFA entries.",
|
||||
type => 'array',
|
||||
items => $TYPED_TFA_ENTRY_SCHEMA,
|
||||
links => [ { rel => 'child', href => "{id}" } ],
|
||||
description => "A list of the user's TFA entries.",
|
||||
type => 'array',
|
||||
items => $TYPED_TFA_ENTRY_SCHEMA,
|
||||
links => [{ rel => 'child', href => "{id}" }],
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
|
||||
return $tfa_cfg->api_list_user_tfa($param->{userid});
|
||||
}});
|
||||
my ($param) = @_;
|
||||
my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
|
||||
return $tfa_cfg->api_list_user_tfa($param->{userid});
|
||||
},
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'get_tfa_entry',
|
||||
path => '{userid}/{id}',
|
||||
method => 'GET',
|
||||
permissions => {
|
||||
check => [ 'or',
|
||||
['userid-param', 'self'],
|
||||
['userid-group', ['User.Modify', 'Sys.Audit']],
|
||||
],
|
||||
check => [
|
||||
'or', ['userid-param', 'self'], ['userid-group', ['User.Modify', 'Sys.Audit']],
|
||||
],
|
||||
},
|
||||
protected => 1, # else we can't access shadow files
|
||||
description => 'Fetch a requested TFA entry if present.',
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
userid => get_standard_option('userid', {
|
||||
completion => \&PVE::AccessControl::complete_username,
|
||||
}),
|
||||
id => $TFA_ID_SCHEMA,
|
||||
}
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
userid => get_standard_option(
|
||||
'userid',
|
||||
{
|
||||
completion => \&PVE::AccessControl::complete_username,
|
||||
},
|
||||
),
|
||||
id => $TFA_ID_SCHEMA,
|
||||
},
|
||||
},
|
||||
returns => $TYPED_TFA_ENTRY_SCHEMA,
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
|
||||
my $id = $param->{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;
|
||||
return $entry;
|
||||
}});
|
||||
my ($param) = @_;
|
||||
my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
|
||||
my $id = $param->{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;
|
||||
return $entry;
|
||||
},
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'delete_tfa',
|
||||
path => '{userid}/{id}',
|
||||
method => 'DELETE',
|
||||
permissions => {
|
||||
check => [ 'or',
|
||||
['userid-param', 'self'],
|
||||
['userid-group', ['User.Modify']],
|
||||
],
|
||||
check => [
|
||||
'or', ['userid-param', 'self'], ['userid-group', ['User.Modify']],
|
||||
],
|
||||
},
|
||||
protected => 1, # else we can't access shadow files
|
||||
allowtoken => 0, # we don't want tokens to change the regular user's TFA settings
|
||||
description => 'Delete a TFA entry by ID.',
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
userid => get_standard_option('userid', {
|
||||
completion => \&PVE::AccessControl::complete_username,
|
||||
}),
|
||||
id => $TFA_ID_SCHEMA,
|
||||
password => $OPTIONAL_PASSWORD_SCHEMA,
|
||||
}
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
userid => get_standard_option(
|
||||
'userid',
|
||||
{
|
||||
completion => \&PVE::AccessControl::complete_username,
|
||||
},
|
||||
),
|
||||
id => $TFA_ID_SCHEMA,
|
||||
password => $OPTIONAL_PASSWORD_SCHEMA,
|
||||
},
|
||||
},
|
||||
returns => { type => 'null' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $authuser = $rpcenv->get_user();
|
||||
my $userid = $rpcenv->reauth_user_for_user_modification(
|
||||
$authuser,
|
||||
$param->{userid},
|
||||
$param->{password},
|
||||
);
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $authuser = $rpcenv->get_user();
|
||||
my $userid = $rpcenv->reauth_user_for_user_modification(
|
||||
$authuser, $param->{userid}, $param->{password},
|
||||
);
|
||||
|
||||
my $has_entries_left = PVE::AccessControl::lock_tfa_config(sub {
|
||||
my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
|
||||
my $has_entries_left = $tfa_cfg->api_delete_tfa($userid, $param->{id});
|
||||
cfs_write_file('priv/tfa.cfg', $tfa_cfg);
|
||||
return $has_entries_left;
|
||||
});
|
||||
if (!$has_entries_left) {
|
||||
set_user_tfa_enabled($userid, undef, undef);
|
||||
}
|
||||
}});
|
||||
my $has_entries_left = PVE::AccessControl::lock_tfa_config(sub {
|
||||
my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
|
||||
my $has_entries_left = $tfa_cfg->api_delete_tfa($userid, $param->{id});
|
||||
cfs_write_file('priv/tfa.cfg', $tfa_cfg);
|
||||
return $has_entries_left;
|
||||
});
|
||||
if (!$has_entries_left) {
|
||||
set_user_tfa_enabled($userid, undef, undef);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'list_tfa',
|
||||
path => '',
|
||||
method => 'GET',
|
||||
permissions => {
|
||||
description => "Returns all or just the logged-in user, depending on privileges.",
|
||||
user => 'all',
|
||||
description => "Returns all or just the logged-in user, depending on privileges.",
|
||||
user => 'all',
|
||||
},
|
||||
protected => 1, # else we can't access shadow files
|
||||
description => 'List TFA configurations of users.',
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {}
|
||||
additionalProperties => 0,
|
||||
properties => {},
|
||||
},
|
||||
returns => {
|
||||
description => "The list tuples of user and TFA entries.",
|
||||
type => 'array',
|
||||
items => {
|
||||
type => 'object',
|
||||
properties => {
|
||||
userid => {
|
||||
type => 'string',
|
||||
description => 'User this entry belongs to.',
|
||||
},
|
||||
entries => {
|
||||
type => 'array',
|
||||
items => $TYPED_TFA_ENTRY_SCHEMA,
|
||||
},
|
||||
'totp-locked' => {
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
description => 'True if the user is currently locked out of TOTP factors.',
|
||||
},
|
||||
'tfa-locked-until' => {
|
||||
type => 'integer',
|
||||
optional => 1,
|
||||
description =>
|
||||
'Contains a timestamp until when a user is locked out of 2nd factors.',
|
||||
},
|
||||
},
|
||||
},
|
||||
links => [ { rel => 'child', href => "{userid}" } ],
|
||||
description => "The list tuples of user and TFA entries.",
|
||||
type => 'array',
|
||||
items => {
|
||||
type => 'object',
|
||||
properties => {
|
||||
userid => {
|
||||
type => 'string',
|
||||
description => 'User this entry belongs to.',
|
||||
},
|
||||
entries => {
|
||||
type => 'array',
|
||||
items => $TYPED_TFA_ENTRY_SCHEMA,
|
||||
},
|
||||
'totp-locked' => {
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
description => 'True if the user is currently locked out of TOTP factors.',
|
||||
},
|
||||
'tfa-locked-until' => {
|
||||
type => 'integer',
|
||||
optional => 1,
|
||||
description =>
|
||||
'Contains a timestamp until when a user is locked out of 2nd factors.',
|
||||
},
|
||||
},
|
||||
},
|
||||
links => [{ rel => 'child', href => "{userid}" }],
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $authuser = $rpcenv->get_user();
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $authuser = $rpcenv->get_user();
|
||||
|
||||
my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
|
||||
my $entries = $tfa_cfg->api_list_tfa($authuser, 1);
|
||||
my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
|
||||
my $entries = $tfa_cfg->api_list_tfa($authuser, 1);
|
||||
|
||||
my $privs = [ 'User.Modify', 'Sys.Audit' ];
|
||||
if ($rpcenv->check_any($authuser, "/access/groups", $privs, 1)) {
|
||||
# can modify all
|
||||
return $entries;
|
||||
}
|
||||
my $privs = ['User.Modify', 'Sys.Audit'];
|
||||
if ($rpcenv->check_any($authuser, "/access/groups", $privs, 1)) {
|
||||
# can modify all
|
||||
return $entries;
|
||||
}
|
||||
|
||||
my $groups = $rpcenv->filter_groups($authuser, $privs, 1);
|
||||
my $allowed_users = $rpcenv->group_member_join([keys %$groups]);
|
||||
return [
|
||||
grep {
|
||||
my $userid = $_->{userid};
|
||||
$userid eq $authuser || $allowed_users->{$userid}
|
||||
} $entries->@*
|
||||
];
|
||||
}});
|
||||
my $groups = $rpcenv->filter_groups($authuser, $privs, 1);
|
||||
my $allowed_users = $rpcenv->group_member_join([keys %$groups]);
|
||||
return [
|
||||
grep {
|
||||
my $userid = $_->{userid};
|
||||
$userid eq $authuser || $allowed_users->{$userid}
|
||||
} $entries->@*
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'add_tfa_entry',
|
||||
path => '{userid}',
|
||||
method => 'POST',
|
||||
permissions => {
|
||||
check => [ 'or',
|
||||
['userid-param', 'self'],
|
||||
['userid-group', ['User.Modify']],
|
||||
],
|
||||
check => [
|
||||
'or', ['userid-param', 'self'], ['userid-group', ['User.Modify']],
|
||||
],
|
||||
},
|
||||
protected => 1, # else we can't access shadow files
|
||||
allowtoken => 0, # we don't want tokens to change the regular user's TFA settings
|
||||
description => 'Add a TFA entry for a user.',
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
userid => get_standard_option('userid', {
|
||||
completion => \&PVE::AccessControl::complete_username,
|
||||
}),
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
userid => get_standard_option(
|
||||
'userid',
|
||||
{
|
||||
completion => \&PVE::AccessControl::complete_username,
|
||||
},
|
||||
),
|
||||
type => $TFA_TYPE_SCHEMA,
|
||||
description => {
|
||||
type => 'string',
|
||||
description => 'A description to distinguish multiple entries from one another',
|
||||
maxLength => 255,
|
||||
optional => 1,
|
||||
},
|
||||
totp => {
|
||||
type => 'string',
|
||||
description => "A totp URI.",
|
||||
optional => 1,
|
||||
},
|
||||
value => {
|
||||
type => 'string',
|
||||
description =>
|
||||
'The current value for the provided totp URI, or a Webauthn/U2F'
|
||||
.' challenge response',
|
||||
optional => 1,
|
||||
},
|
||||
challenge => {
|
||||
type => 'string',
|
||||
description => 'When responding to a u2f challenge: the original challenge string',
|
||||
optional => 1,
|
||||
},
|
||||
password => $OPTIONAL_PASSWORD_SCHEMA,
|
||||
},
|
||||
description => {
|
||||
type => 'string',
|
||||
description => 'A description to distinguish multiple entries from one another',
|
||||
maxLength => 255,
|
||||
optional => 1,
|
||||
},
|
||||
totp => {
|
||||
type => 'string',
|
||||
description => "A totp URI.",
|
||||
optional => 1,
|
||||
},
|
||||
value => {
|
||||
type => 'string',
|
||||
description => 'The current value for the provided totp URI, or a Webauthn/U2F'
|
||||
. ' challenge response',
|
||||
optional => 1,
|
||||
},
|
||||
challenge => {
|
||||
type => 'string',
|
||||
description =>
|
||||
'When responding to a u2f challenge: the original challenge string',
|
||||
optional => 1,
|
||||
},
|
||||
password => $OPTIONAL_PASSWORD_SCHEMA,
|
||||
},
|
||||
},
|
||||
returns => $TFA_UPDATE_INFO_SCHEMA,
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $authuser = $rpcenv->get_user();
|
||||
my ($userid, undef, $realm) = $rpcenv->reauth_user_for_user_modification(
|
||||
$authuser,
|
||||
$param->{userid},
|
||||
$param->{password},
|
||||
);
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $authuser = $rpcenv->get_user();
|
||||
my ($userid, undef, $realm) = $rpcenv->reauth_user_for_user_modification(
|
||||
$authuser, $param->{userid}, $param->{password},
|
||||
);
|
||||
|
||||
my $type = delete $param->{type};
|
||||
my $value = delete $param->{value};
|
||||
if ($type eq 'yubico') {
|
||||
$value = validate_yubico_otp($userid, $realm, $value);
|
||||
}
|
||||
my $type = delete $param->{type};
|
||||
my $value = delete $param->{value};
|
||||
if ($type eq 'yubico') {
|
||||
$value = validate_yubico_otp($userid, $realm, $value);
|
||||
}
|
||||
|
||||
return PVE::AccessControl::lock_tfa_config(sub {
|
||||
my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
|
||||
return PVE::AccessControl::lock_tfa_config(sub {
|
||||
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(
|
||||
$userid,
|
||||
$param->{description},
|
||||
$param->{totp},
|
||||
$value,
|
||||
$param->{challenge},
|
||||
$type,
|
||||
);
|
||||
my $response = $tfa_cfg->api_add_tfa_entry(
|
||||
$userid,
|
||||
$param->{description},
|
||||
$param->{totp},
|
||||
$value,
|
||||
$param->{challenge},
|
||||
$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($$$) {
|
||||
my ($userid, $realm, $value) = @_;
|
||||
@ -394,11 +408,11 @@ sub validate_yubico_otp : prototype($$$) {
|
||||
|
||||
my $realm_tfa = $realm_cfg->{tfa};
|
||||
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);
|
||||
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);
|
||||
|
||||
@ -407,64 +421,62 @@ sub validate_yubico_otp : prototype($$$) {
|
||||
return $public_key;
|
||||
}
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
__PACKAGE__->register_method({
|
||||
name => 'update_tfa_entry',
|
||||
path => '{userid}/{id}',
|
||||
method => 'PUT',
|
||||
permissions => {
|
||||
check => [ 'or',
|
||||
['userid-param', 'self'],
|
||||
['userid-group', ['User.Modify']],
|
||||
],
|
||||
check => [
|
||||
'or', ['userid-param', 'self'], ['userid-group', ['User.Modify']],
|
||||
],
|
||||
},
|
||||
protected => 1, # else we can't access shadow files
|
||||
allowtoken => 0, # we don't want tokens to change the regular user's TFA settings
|
||||
description => 'Add a TFA entry for a user.',
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
userid => get_standard_option('userid', {
|
||||
completion => \&PVE::AccessControl::complete_username,
|
||||
}),
|
||||
id => $TFA_ID_SCHEMA,
|
||||
description => {
|
||||
type => 'string',
|
||||
description => 'A description to distinguish multiple entries from one another',
|
||||
maxLength => 255,
|
||||
optional => 1,
|
||||
},
|
||||
enable => {
|
||||
type => 'boolean',
|
||||
description => 'Whether the entry should be enabled for login.',
|
||||
optional => 1,
|
||||
},
|
||||
password => $OPTIONAL_PASSWORD_SCHEMA,
|
||||
},
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
userid => get_standard_option(
|
||||
'userid',
|
||||
{
|
||||
completion => \&PVE::AccessControl::complete_username,
|
||||
},
|
||||
),
|
||||
id => $TFA_ID_SCHEMA,
|
||||
description => {
|
||||
type => 'string',
|
||||
description => 'A description to distinguish multiple entries from one another',
|
||||
maxLength => 255,
|
||||
optional => 1,
|
||||
},
|
||||
enable => {
|
||||
type => 'boolean',
|
||||
description => 'Whether the entry should be enabled for login.',
|
||||
optional => 1,
|
||||
},
|
||||
password => $OPTIONAL_PASSWORD_SCHEMA,
|
||||
},
|
||||
},
|
||||
returns => { type => 'null' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $authuser = $rpcenv->get_user();
|
||||
my $userid = $rpcenv->reauth_user_for_user_modification(
|
||||
$authuser,
|
||||
$param->{userid},
|
||||
$param->{password},
|
||||
);
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $authuser = $rpcenv->get_user();
|
||||
my $userid = $rpcenv->reauth_user_for_user_modification(
|
||||
$authuser, $param->{userid}, $param->{password},
|
||||
);
|
||||
|
||||
PVE::AccessControl::lock_tfa_config(sub {
|
||||
my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
|
||||
PVE::AccessControl::lock_tfa_config(sub {
|
||||
my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
|
||||
|
||||
$tfa_cfg->api_update_tfa_entry(
|
||||
$userid,
|
||||
$param->{id},
|
||||
$param->{description},
|
||||
$param->{enable},
|
||||
);
|
||||
$tfa_cfg->api_update_tfa_entry(
|
||||
$userid, $param->{id}, $param->{description}, $param->{enable},
|
||||
);
|
||||
|
||||
cfs_write_file('priv/tfa.cfg', $tfa_cfg);
|
||||
});
|
||||
}});
|
||||
cfs_write_file('priv/tfa.cfg', $tfa_cfg);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
1;
|
||||
|
1237
src/PVE/API2/User.pm
1237
src/PVE/API2/User.pm
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -13,88 +13,89 @@ sub type {
|
||||
|
||||
sub properties {
|
||||
return {
|
||||
server1 => {
|
||||
description => "Server IP address (or DNS name)",
|
||||
type => 'string',
|
||||
format => 'address',
|
||||
maxLength => 256,
|
||||
},
|
||||
server2 => {
|
||||
description => "Fallback Server IP address (or DNS name)",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
format => 'address',
|
||||
maxLength => 256,
|
||||
},
|
||||
secure => {
|
||||
description => "Use secure LDAPS protocol. DEPRECATED: use 'mode' instead.",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
},
|
||||
sslversion => {
|
||||
description => "LDAPS TLS/SSL version. It's not recommended to use version older than 1.2!",
|
||||
type => 'string',
|
||||
enum => [qw(tlsv1 tlsv1_1 tlsv1_2 tlsv1_3)],
|
||||
optional => 1,
|
||||
},
|
||||
default => {
|
||||
description => "Use this as default realm",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
},
|
||||
comment => {
|
||||
description => "Description.",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
maxLength => 4096,
|
||||
},
|
||||
port => {
|
||||
description => "Server port.",
|
||||
type => 'integer',
|
||||
minimum => 1,
|
||||
maximum => 65535,
|
||||
optional => 1,
|
||||
},
|
||||
domain => {
|
||||
description => "AD domain name",
|
||||
type => 'string',
|
||||
pattern => '\S+',
|
||||
optional => 1,
|
||||
maxLength => 256,
|
||||
},
|
||||
tfa => PVE::JSONSchema::get_standard_option('tfa'),
|
||||
server1 => {
|
||||
description => "Server IP address (or DNS name)",
|
||||
type => 'string',
|
||||
format => 'address',
|
||||
maxLength => 256,
|
||||
},
|
||||
server2 => {
|
||||
description => "Fallback Server IP address (or DNS name)",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
format => 'address',
|
||||
maxLength => 256,
|
||||
},
|
||||
secure => {
|
||||
description => "Use secure LDAPS protocol. DEPRECATED: use 'mode' instead.",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
},
|
||||
sslversion => {
|
||||
description =>
|
||||
"LDAPS TLS/SSL version. It's not recommended to use version older than 1.2!",
|
||||
type => 'string',
|
||||
enum => [qw(tlsv1 tlsv1_1 tlsv1_2 tlsv1_3)],
|
||||
optional => 1,
|
||||
},
|
||||
default => {
|
||||
description => "Use this as default realm",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
},
|
||||
comment => {
|
||||
description => "Description.",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
maxLength => 4096,
|
||||
},
|
||||
port => {
|
||||
description => "Server port.",
|
||||
type => 'integer',
|
||||
minimum => 1,
|
||||
maximum => 65535,
|
||||
optional => 1,
|
||||
},
|
||||
domain => {
|
||||
description => "AD domain name",
|
||||
type => 'string',
|
||||
pattern => '\S+',
|
||||
optional => 1,
|
||||
maxLength => 256,
|
||||
},
|
||||
tfa => PVE::JSONSchema::get_standard_option('tfa'),
|
||||
};
|
||||
}
|
||||
|
||||
sub options {
|
||||
return {
|
||||
server1 => {},
|
||||
server2 => { optional => 1 },
|
||||
domain => {},
|
||||
port => { optional => 1 },
|
||||
secure => { optional => 1 },
|
||||
sslversion => { optional => 1 },
|
||||
default => { optional => 1 },,
|
||||
comment => { optional => 1 },
|
||||
tfa => { optional => 1 },
|
||||
verify => { optional => 1 },
|
||||
capath => { optional => 1 },
|
||||
cert => { optional => 1 },
|
||||
certkey => { optional => 1 },
|
||||
base_dn => { optional => 1 },
|
||||
bind_dn => { optional => 1 },
|
||||
password => { optional => 1 },
|
||||
user_attr => { optional => 1 },
|
||||
filter => { optional => 1 },
|
||||
sync_attributes => { optional => 1 },
|
||||
user_classes => { optional => 1 },
|
||||
group_dn => { optional => 1 },
|
||||
group_name_attr => { optional => 1 },
|
||||
group_filter => { optional => 1 },
|
||||
group_classes => { optional => 1 },
|
||||
'sync-defaults-options' => { optional => 1 },
|
||||
mode => { optional => 1 },
|
||||
'case-sensitive' => { optional => 1 },
|
||||
server1 => {},
|
||||
server2 => { optional => 1 },
|
||||
domain => {},
|
||||
port => { optional => 1 },
|
||||
secure => { optional => 1 },
|
||||
sslversion => { optional => 1 },
|
||||
default => { optional => 1 },
|
||||
comment => { optional => 1 },
|
||||
tfa => { optional => 1 },
|
||||
verify => { optional => 1 },
|
||||
capath => { optional => 1 },
|
||||
cert => { optional => 1 },
|
||||
certkey => { optional => 1 },
|
||||
base_dn => { optional => 1 },
|
||||
bind_dn => { optional => 1 },
|
||||
password => { optional => 1 },
|
||||
user_attr => { optional => 1 },
|
||||
filter => { optional => 1 },
|
||||
sync_attributes => { optional => 1 },
|
||||
user_classes => { optional => 1 },
|
||||
group_dn => { optional => 1 },
|
||||
group_name_attr => { optional => 1 },
|
||||
group_filter => { optional => 1 },
|
||||
group_classes => { optional => 1 },
|
||||
'sync-defaults-options' => { optional => 1 },
|
||||
mode => { optional => 1 },
|
||||
'case-sensitive' => { optional => 1 },
|
||||
};
|
||||
}
|
||||
|
||||
@ -116,28 +117,28 @@ sub authenticate_user {
|
||||
|
||||
my %ad_args;
|
||||
if ($config->{verify}) {
|
||||
$ad_args{verify} = 'require';
|
||||
$ad_args{clientcert} = $config->{cert} if $config->{cert};
|
||||
$ad_args{clientkey} = $config->{certkey} if $config->{certkey};
|
||||
if (defined(my $capath = $config->{capath})) {
|
||||
if (-d $capath) {
|
||||
$ad_args{capath} = $capath;
|
||||
} else {
|
||||
$ad_args{cafile} = $capath;
|
||||
}
|
||||
}
|
||||
$ad_args{verify} = 'require';
|
||||
$ad_args{clientcert} = $config->{cert} if $config->{cert};
|
||||
$ad_args{clientkey} = $config->{certkey} if $config->{certkey};
|
||||
if (defined(my $capath = $config->{capath})) {
|
||||
if (-d $capath) {
|
||||
$ad_args{capath} = $capath;
|
||||
} else {
|
||||
$ad_args{cafile} = $capath;
|
||||
}
|
||||
}
|
||||
} elsif (defined($config->{verify})) {
|
||||
$ad_args{verify} = 'none';
|
||||
$ad_args{verify} = 'none';
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
$username = "$username\@$config->{domain}"
|
||||
if $username !~ m/@/ && $config->{domain};
|
||||
if $username !~ m/@/ && $config->{domain};
|
||||
|
||||
PVE::LDAP::auth_user_dn($ldap, $username, $password);
|
||||
|
||||
|
@ -16,153 +16,154 @@ sub type {
|
||||
|
||||
sub properties {
|
||||
return {
|
||||
base_dn => {
|
||||
description => "LDAP base domain name",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
maxLength => 256,
|
||||
},
|
||||
user_attr => {
|
||||
description => "LDAP user attribute name",
|
||||
type => 'string',
|
||||
pattern => '\S{2,}',
|
||||
optional => 1,
|
||||
maxLength => 256,
|
||||
},
|
||||
bind_dn => {
|
||||
description => "LDAP bind domain name",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
maxLength => 256,
|
||||
},
|
||||
password => {
|
||||
description => "LDAP bind password. Will be stored in '/etc/pve/priv/realm/<REALM>.pw'.",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
},
|
||||
verify => {
|
||||
description => "Verify the server's SSL certificate",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
default => 0,
|
||||
},
|
||||
capath => {
|
||||
description => "Path to the CA certificate store",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
default => '/etc/ssl/certs',
|
||||
},
|
||||
cert => {
|
||||
description => "Path to the client certificate",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
},
|
||||
certkey => {
|
||||
description => "Path to the client certificate key",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
},
|
||||
filter => {
|
||||
description => "LDAP filter for user sync.",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
maxLength => 2048,
|
||||
},
|
||||
sync_attributes => {
|
||||
description => "Comma separated list of key=value pairs for specifying"
|
||||
." which LDAP attributes map to which PVE user field. For example,"
|
||||
." to map the LDAP attribute 'mail' to PVEs 'email', write "
|
||||
." 'email=mail'. By default, each PVE user field is represented "
|
||||
." by an LDAP attribute of the same name.",
|
||||
optional => 1,
|
||||
type => 'string',
|
||||
pattern => '\w+=[^,]+(,\s*\w+=[^,]+)*',
|
||||
},
|
||||
user_classes => {
|
||||
description => "The objectclasses for users.",
|
||||
type => 'string',
|
||||
default => 'inetorgperson, posixaccount, person, user',
|
||||
format => 'ldap-simple-attr-list',
|
||||
optional => 1,
|
||||
},
|
||||
group_dn => {
|
||||
description => "LDAP base domain name for group sync. If not set, the"
|
||||
." base_dn will be used.",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
maxLength => 256,
|
||||
},
|
||||
group_name_attr => {
|
||||
description => "LDAP attribute representing a groups name. If not set"
|
||||
." or found, the first value of the DN will be used as name.",
|
||||
type => 'string',
|
||||
format => 'ldap-simple-attr',
|
||||
optional => 1,
|
||||
maxLength => 256,
|
||||
},
|
||||
group_filter => {
|
||||
description => "LDAP filter for group sync.",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
maxLength => 2048,
|
||||
},
|
||||
group_classes => {
|
||||
description => "The objectclasses for groups.",
|
||||
type => 'string',
|
||||
default => 'groupOfNames, group, univentionGroup, ipausergroup',
|
||||
format => 'ldap-simple-attr-list',
|
||||
optional => 1,
|
||||
},
|
||||
'sync-defaults-options' => {
|
||||
description => "The default options for behavior of synchronizations.",
|
||||
type => 'string',
|
||||
format => 'realm-sync-options',
|
||||
optional => 1,
|
||||
},
|
||||
mode => {
|
||||
description => "LDAP protocol mode.",
|
||||
type => 'string',
|
||||
enum => [ 'ldap', 'ldaps', 'ldap+starttls'],
|
||||
optional => 1,
|
||||
default => 'ldap',
|
||||
},
|
||||
base_dn => {
|
||||
description => "LDAP base domain name",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
maxLength => 256,
|
||||
},
|
||||
user_attr => {
|
||||
description => "LDAP user attribute name",
|
||||
type => 'string',
|
||||
pattern => '\S{2,}',
|
||||
optional => 1,
|
||||
maxLength => 256,
|
||||
},
|
||||
bind_dn => {
|
||||
description => "LDAP bind domain name",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
maxLength => 256,
|
||||
},
|
||||
password => {
|
||||
description =>
|
||||
"LDAP bind password. Will be stored in '/etc/pve/priv/realm/<REALM>.pw'.",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
},
|
||||
verify => {
|
||||
description => "Verify the server's SSL certificate",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
default => 0,
|
||||
},
|
||||
capath => {
|
||||
description => "Path to the CA certificate store",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
default => '/etc/ssl/certs',
|
||||
},
|
||||
cert => {
|
||||
description => "Path to the client certificate",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
},
|
||||
certkey => {
|
||||
description => "Path to the client certificate key",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
},
|
||||
filter => {
|
||||
description => "LDAP filter for user sync.",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
maxLength => 2048,
|
||||
},
|
||||
sync_attributes => {
|
||||
description => "Comma separated list of key=value pairs for specifying"
|
||||
. " which LDAP attributes map to which PVE user field. For example,"
|
||||
. " to map the LDAP attribute 'mail' to PVEs 'email', write "
|
||||
. " 'email=mail'. By default, each PVE user field is represented "
|
||||
. " by an LDAP attribute of the same name.",
|
||||
optional => 1,
|
||||
type => 'string',
|
||||
pattern => '\w+=[^,]+(,\s*\w+=[^,]+)*',
|
||||
},
|
||||
user_classes => {
|
||||
description => "The objectclasses for users.",
|
||||
type => 'string',
|
||||
default => 'inetorgperson, posixaccount, person, user',
|
||||
format => 'ldap-simple-attr-list',
|
||||
optional => 1,
|
||||
},
|
||||
group_dn => {
|
||||
description => "LDAP base domain name for group sync. If not set, the"
|
||||
. " base_dn will be used.",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
maxLength => 256,
|
||||
},
|
||||
group_name_attr => {
|
||||
description => "LDAP attribute representing a groups name. If not set"
|
||||
. " or found, the first value of the DN will be used as name.",
|
||||
type => 'string',
|
||||
format => 'ldap-simple-attr',
|
||||
optional => 1,
|
||||
maxLength => 256,
|
||||
},
|
||||
group_filter => {
|
||||
description => "LDAP filter for group sync.",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
maxLength => 2048,
|
||||
},
|
||||
group_classes => {
|
||||
description => "The objectclasses for groups.",
|
||||
type => 'string',
|
||||
default => 'groupOfNames, group, univentionGroup, ipausergroup',
|
||||
format => 'ldap-simple-attr-list',
|
||||
optional => 1,
|
||||
},
|
||||
'sync-defaults-options' => {
|
||||
description => "The default options for behavior of synchronizations.",
|
||||
type => 'string',
|
||||
format => 'realm-sync-options',
|
||||
optional => 1,
|
||||
},
|
||||
mode => {
|
||||
description => "LDAP protocol mode.",
|
||||
type => 'string',
|
||||
enum => ['ldap', 'ldaps', 'ldap+starttls'],
|
||||
optional => 1,
|
||||
default => 'ldap',
|
||||
},
|
||||
'case-sensitive' => {
|
||||
description => "username is case-sensitive",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
default => 1,
|
||||
},
|
||||
description => "username is case-sensitive",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
default => 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
sub options {
|
||||
return {
|
||||
server1 => {},
|
||||
server2 => { optional => 1 },
|
||||
base_dn => {},
|
||||
bind_dn => { optional => 1 },
|
||||
password => { optional => 1 },
|
||||
user_attr => {},
|
||||
port => { optional => 1 },
|
||||
secure => { optional => 1 },
|
||||
sslversion => { optional => 1 },
|
||||
default => { optional => 1 },
|
||||
comment => { optional => 1 },
|
||||
tfa => { optional => 1 },
|
||||
verify => { optional => 1 },
|
||||
capath => { optional => 1 },
|
||||
cert => { optional => 1 },
|
||||
certkey => { optional => 1 },
|
||||
filter => { optional => 1 },
|
||||
sync_attributes => { optional => 1 },
|
||||
user_classes => { optional => 1 },
|
||||
group_dn => { optional => 1 },
|
||||
group_name_attr => { optional => 1 },
|
||||
group_filter => { optional => 1 },
|
||||
group_classes => { optional => 1 },
|
||||
'sync-defaults-options' => { optional => 1 },
|
||||
mode => { optional => 1 },
|
||||
'case-sensitive' => { optional => 1 },
|
||||
server1 => {},
|
||||
server2 => { optional => 1 },
|
||||
base_dn => {},
|
||||
bind_dn => { optional => 1 },
|
||||
password => { optional => 1 },
|
||||
user_attr => {},
|
||||
port => { optional => 1 },
|
||||
secure => { optional => 1 },
|
||||
sslversion => { optional => 1 },
|
||||
default => { optional => 1 },
|
||||
comment => { optional => 1 },
|
||||
tfa => { optional => 1 },
|
||||
verify => { optional => 1 },
|
||||
capath => { optional => 1 },
|
||||
cert => { optional => 1 },
|
||||
certkey => { optional => 1 },
|
||||
filter => { optional => 1 },
|
||||
sync_attributes => { optional => 1 },
|
||||
user_classes => { optional => 1 },
|
||||
group_dn => { optional => 1 },
|
||||
group_name_attr => { optional => 1 },
|
||||
group_filter => { optional => 1 },
|
||||
group_classes => { optional => 1 },
|
||||
'sync-defaults-options' => { optional => 1 },
|
||||
mode => { 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
|
||||
if ($attr eq 'username') {
|
||||
die "value '$value' does not look like a valid user name\n"
|
||||
if $value !~ m/${PVE::Auth::Plugin::user_regex}/;
|
||||
return;
|
||||
die "value '$value' does not look like a valid user name\n"
|
||||
if $value !~ m/${PVE::Auth::Plugin::user_regex}/;
|
||||
return;
|
||||
}
|
||||
|
||||
return if $attr eq 'enable'; # for backwards compat, don't parse/validate
|
||||
|
||||
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 {
|
||||
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;
|
||||
if ($config->{verify}) {
|
||||
$ldap_args{verify} = 'require';
|
||||
$ldap_args{clientcert} = $config->{cert} if $config->{cert};
|
||||
$ldap_args{clientkey} = $config->{certkey} if $config->{certkey};
|
||||
if (defined(my $capath = $config->{capath})) {
|
||||
if (-d $capath) {
|
||||
$ldap_args{capath} = $capath;
|
||||
} else {
|
||||
$ldap_args{cafile} = $capath;
|
||||
}
|
||||
}
|
||||
$ldap_args{verify} = 'require';
|
||||
$ldap_args{clientcert} = $config->{cert} if $config->{cert};
|
||||
$ldap_args{clientkey} = $config->{certkey} if $config->{certkey};
|
||||
if (defined(my $capath = $config->{capath})) {
|
||||
if (-d $capath) {
|
||||
$ldap_args{capath} = $capath;
|
||||
} else {
|
||||
$ldap_args{cafile} = $capath;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$ldap_args{verify} = 'none';
|
||||
$ldap_args{verify} = 'none';
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if ($config->{bind_dn}) {
|
||||
my $bind_dn = $config->{bind_dn};
|
||||
my $bind_pass = $param->{password} || ldap_get_credentials($realm);
|
||||
die "missing password for realm $realm\n" if !defined($bind_pass);
|
||||
PVE::LDAP::ldap_bind($ldap, $bind_dn, $bind_pass);
|
||||
my $bind_dn = $config->{bind_dn};
|
||||
my $bind_pass = $param->{password} || ldap_get_credentials($realm);
|
||||
die "missing password for realm $realm\n" if !defined($bind_pass);
|
||||
PVE::LDAP::ldap_bind($ldap, $bind_dn, $bind_pass);
|
||||
} elsif ($config->{cert} && $config->{certkey}) {
|
||||
warn "skipping anonymous bind with clientcert\n";
|
||||
warn "skipping anonymous bind with clientcert\n";
|
||||
} else {
|
||||
PVE::LDAP::ldap_bind($ldap);
|
||||
PVE::LDAP::ldap_bind($ldap);
|
||||
}
|
||||
|
||||
if (!$config->{base_dn}) {
|
||||
my $root = $ldap->root_dse(attrs => [ 'defaultNamingContext' ]);
|
||||
$config->{base_dn} = $root->get_value('defaultNamingContext');
|
||||
my $root = $ldap->root_dse(attrs => ['defaultNamingContext']);
|
||||
$config->{base_dn} = $root->get_value('defaultNamingContext');
|
||||
}
|
||||
|
||||
return $ldap;
|
||||
@ -278,26 +279,26 @@ sub get_users {
|
||||
|
||||
my $user_name_attr = $config->{user_attr} // 'uid';
|
||||
my $ldap_attribute_map = {
|
||||
$user_name_attr => 'username',
|
||||
enable => 'enable',
|
||||
expire => 'expire',
|
||||
firstname => 'firstname',
|
||||
lastname => 'lastname',
|
||||
email => 'email',
|
||||
comment => 'comment',
|
||||
keys => 'keys',
|
||||
# NOTE: also ensure verify_sync_attribute_value can handle any new/changed attribute name
|
||||
$user_name_attr => 'username',
|
||||
enable => 'enable',
|
||||
expire => 'expire',
|
||||
firstname => 'firstname',
|
||||
lastname => 'lastname',
|
||||
email => 'email',
|
||||
comment => 'comment',
|
||||
keys => 'keys',
|
||||
# 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
|
||||
my $valid_sync_attributes = { map { $_ => 1 } values $ldap_attribute_map->%* };
|
||||
|
||||
foreach my $attr (PVE::Tools::split_list($config->{sync_attributes})) {
|
||||
my ($ours, $ldap) = ($attr =~ m/^\s*(\w+)=(.*)\s*$/);
|
||||
if (!$valid_sync_attributes->{$ours}) {
|
||||
warn "skipping bad 'sync_attributes' entry – '$ours' is not a valid target attribute\n";
|
||||
next;
|
||||
}
|
||||
$ldap_attribute_map->{$ldap} = $ours;
|
||||
my ($ours, $ldap) = ($attr =~ m/^\s*(\w+)=(.*)\s*$/);
|
||||
if (!$valid_sync_attributes->{$ours}) {
|
||||
warn "skipping bad 'sync_attributes' entry – '$ours' is not a valid target attribute\n";
|
||||
next;
|
||||
}
|
||||
$ldap_attribute_map->{$ldap} = $ours;
|
||||
}
|
||||
|
||||
my $filter = $config->{filter};
|
||||
@ -306,41 +307,42 @@ sub get_users {
|
||||
$config->{user_classes} //= 'inetorgperson, posixaccount, person, user';
|
||||
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 $dnmap = {};
|
||||
|
||||
foreach my $user (@$users) {
|
||||
my $user_attributes = $user->{attributes};
|
||||
my $userid = $user_attributes->{$user_name_attr}->[0];
|
||||
my $username = "$userid\@$realm";
|
||||
my $user_attributes = $user->{attributes};
|
||||
my $userid = $user_attributes->{$user_name_attr}->[0];
|
||||
my $username = "$userid\@$realm";
|
||||
|
||||
# we cannot sync usernames that do not meet our criteria
|
||||
eval { PVE::Auth::Plugin::verify_username($username) };
|
||||
if (my $err = $@) {
|
||||
warn "$err";
|
||||
next;
|
||||
}
|
||||
# we cannot sync usernames that do not meet our criteria
|
||||
eval { PVE::Auth::Plugin::verify_username($username) };
|
||||
if (my $err = $@) {
|
||||
warn "$err";
|
||||
next;
|
||||
}
|
||||
|
||||
$ret->{$username} = {};
|
||||
$ret->{$username} = {};
|
||||
|
||||
foreach my $attr (keys %$user_attributes) {
|
||||
if (my $ours = $ldap_attribute_map->{$attr}) {
|
||||
my $value = $user_attributes->{$attr}->[0];
|
||||
eval { verify_sync_attribute_value($ours, $value) };
|
||||
if (my $err = $@) {
|
||||
warn "skipping attribute mapping '$attr'->'$ours' for user '$username' - $err";
|
||||
next;
|
||||
}
|
||||
$ret->{$username}->{$ours} = $value;
|
||||
}
|
||||
}
|
||||
foreach my $attr (keys %$user_attributes) {
|
||||
if (my $ours = $ldap_attribute_map->{$attr}) {
|
||||
my $value = $user_attributes->{$attr}->[0];
|
||||
eval { verify_sync_attribute_value($ours, $value) };
|
||||
if (my $err = $@) {
|
||||
warn "skipping attribute mapping '$attr'->'$ours' for user '$username' - $err";
|
||||
next;
|
||||
}
|
||||
$ret->{$username}->{$ours} = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if (wantarray) {
|
||||
my $dn = $user->{dn};
|
||||
$dnmap->{lc($dn)} = $username;
|
||||
}
|
||||
if (wantarray) {
|
||||
my $dn = $user->{dn};
|
||||
$dnmap->{ lc($dn) } = $username;
|
||||
}
|
||||
}
|
||||
|
||||
return wantarray ? ($ret, $dnmap) : $ret;
|
||||
@ -364,27 +366,27 @@ sub get_groups {
|
||||
my $ret = {};
|
||||
|
||||
foreach my $group (@$groups) {
|
||||
my $name = $group->{name};
|
||||
if (!$name && $group->{dn} =~ m/^[^=]+=([^,]+),/){
|
||||
$name = PVE::Tools::trim($1);
|
||||
}
|
||||
if ($name) {
|
||||
$name .= "-$realm";
|
||||
my $name = $group->{name};
|
||||
if (!$name && $group->{dn} =~ m/^[^=]+=([^,]+),/) {
|
||||
$name = PVE::Tools::trim($1);
|
||||
}
|
||||
if ($name) {
|
||||
$name .= "-$realm";
|
||||
|
||||
# we cannot sync groups that do not meet our criteria
|
||||
eval { PVE::AccessControl::verify_groupname($name) };
|
||||
if (my $err = $@) {
|
||||
warn "$err";
|
||||
next;
|
||||
}
|
||||
# we cannot sync groups that do not meet our criteria
|
||||
eval { PVE::AccessControl::verify_groupname($name) };
|
||||
if (my $err = $@) {
|
||||
warn "$err";
|
||||
next;
|
||||
}
|
||||
|
||||
$ret->{$name} = { users => {} };
|
||||
foreach my $member (@{$group->{members}}) {
|
||||
if (my $user = $dnmap->{lc($member)}) {
|
||||
$ret->{$name}->{users}->{$user} = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
$ret->{$name} = { users => {} };
|
||||
foreach my $member (@{ $group->{members} }) {
|
||||
if (my $user = $dnmap->{ lc($member) }) {
|
||||
$ret->{$name}->{users}->{$user} = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $ret;
|
||||
@ -395,7 +397,8 @@ sub authenticate_user {
|
||||
|
||||
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);
|
||||
|
||||
$ldap->unbind();
|
||||
@ -414,10 +417,10 @@ sub get_cred_file {
|
||||
|
||||
my $cred_file = ldap_cred_file_name($realmid);
|
||||
if (-e $cred_file) {
|
||||
return $cred_file;
|
||||
return $cred_file;
|
||||
} elsif (-e "/etc/pve/priv/ldap/${realmid}.pw") {
|
||||
# FIXME: remove fallback with 7.0 by doing a rename on upgrade from 6.x
|
||||
return "/etc/pve/priv/ldap/${realmid}.pw";
|
||||
# FIXME: remove fallback with 7.0 by doing a rename on upgrade from 6.x
|
||||
return "/etc/pve/priv/ldap/${realmid}.pw";
|
||||
}
|
||||
|
||||
return $cred_file;
|
||||
@ -438,7 +441,7 @@ sub ldap_get_credentials {
|
||||
my ($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;
|
||||
}
|
||||
@ -447,8 +450,8 @@ sub ldap_delete_credentials {
|
||||
my ($realmid) = @_;
|
||||
|
||||
if (my $cred_file = get_cred_file($realmid)) {
|
||||
return if ! -e $cred_file; # nothing to do
|
||||
unlink($cred_file) or warn "removing LDAP credentials '$cred_file' failed: $!\n";
|
||||
return if !-e $cred_file; # nothing to do
|
||||
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) = @_;
|
||||
|
||||
if (defined($param{password})) {
|
||||
ldap_set_credentials($param{password}, $realm);
|
||||
ldap_set_credentials($param{password}, $realm);
|
||||
} else {
|
||||
ldap_delete_credentials($realm);
|
||||
ldap_delete_credentials($realm);
|
||||
}
|
||||
}
|
||||
|
||||
@ -468,9 +471,9 @@ sub on_update_hook {
|
||||
return if !exists($param{password});
|
||||
|
||||
if (defined($param{password})) {
|
||||
ldap_set_credentials($param{password}, $realm);
|
||||
ldap_set_credentials($param{password}, $realm);
|
||||
} else {
|
||||
ldap_delete_credentials($realm);
|
||||
ldap_delete_credentials($realm);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,98 +18,99 @@ sub type {
|
||||
|
||||
sub properties {
|
||||
return {
|
||||
"issuer-url" => {
|
||||
description => "OpenID Issuer Url",
|
||||
type => 'string',
|
||||
maxLength => 256,
|
||||
},
|
||||
"client-id" => {
|
||||
description => "OpenID Client ID",
|
||||
type => 'string',
|
||||
maxLength => 256,
|
||||
},
|
||||
"client-key" => {
|
||||
description => "OpenID Client Key",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
maxLength => 256,
|
||||
},
|
||||
autocreate => {
|
||||
description => "Automatically create users if they do not exist.",
|
||||
optional => 1,
|
||||
type => 'boolean',
|
||||
default => 0,
|
||||
},
|
||||
"username-claim" => {
|
||||
description => "OpenID claim used to generate the unique username.",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
},
|
||||
"groups-claim" => {
|
||||
description => "OpenID claim used to retrieve groups with.",
|
||||
type => 'string',
|
||||
pattern => $openid_claim_regex,
|
||||
maxLength => 256,
|
||||
optional => 1,
|
||||
},
|
||||
"groups-autocreate" => {
|
||||
description => "Automatically create groups if they do not exist.",
|
||||
optional => 1,
|
||||
type => 'boolean',
|
||||
default => 0,
|
||||
},
|
||||
"groups-overwrite" => {
|
||||
description => "All groups will be overwritten for the user on login.",
|
||||
type => 'boolean',
|
||||
default => 0,
|
||||
optional => 1,
|
||||
},
|
||||
prompt => {
|
||||
description => "Specifies whether the Authorization Server prompts the End-User for"
|
||||
." reauthentication and consent.",
|
||||
type => 'string',
|
||||
pattern => '(?:none|login|consent|select_account|\S+)', # \S+ is the extension variant
|
||||
optional => 1,
|
||||
},
|
||||
scopes => {
|
||||
description => "Specifies the scopes (user details) that should be authorized and"
|
||||
." returned, for example 'email' or 'profile'.",
|
||||
type => 'string', # format => 'some-safe-id-list', # FIXME: TODO
|
||||
default => "email profile",
|
||||
optional => 1,
|
||||
},
|
||||
'acr-values' => {
|
||||
description => "Specifies the Authentication Context Class Reference values that the"
|
||||
."Authorization Server is being requested to use for the Auth Request.",
|
||||
type => 'string',
|
||||
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.",
|
||||
type => 'boolean',
|
||||
default => 1,
|
||||
optional => 1,
|
||||
},
|
||||
};
|
||||
"issuer-url" => {
|
||||
description => "OpenID Issuer Url",
|
||||
type => 'string',
|
||||
maxLength => 256,
|
||||
},
|
||||
"client-id" => {
|
||||
description => "OpenID Client ID",
|
||||
type => 'string',
|
||||
maxLength => 256,
|
||||
},
|
||||
"client-key" => {
|
||||
description => "OpenID Client Key",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
maxLength => 256,
|
||||
},
|
||||
autocreate => {
|
||||
description => "Automatically create users if they do not exist.",
|
||||
optional => 1,
|
||||
type => 'boolean',
|
||||
default => 0,
|
||||
},
|
||||
"username-claim" => {
|
||||
description => "OpenID claim used to generate the unique username.",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
},
|
||||
"groups-claim" => {
|
||||
description => "OpenID claim used to retrieve groups with.",
|
||||
type => 'string',
|
||||
pattern => $openid_claim_regex,
|
||||
maxLength => 256,
|
||||
optional => 1,
|
||||
},
|
||||
"groups-autocreate" => {
|
||||
description => "Automatically create groups if they do not exist.",
|
||||
optional => 1,
|
||||
type => 'boolean',
|
||||
default => 0,
|
||||
},
|
||||
"groups-overwrite" => {
|
||||
description => "All groups will be overwritten for the user on login.",
|
||||
type => 'boolean',
|
||||
default => 0,
|
||||
optional => 1,
|
||||
},
|
||||
prompt => {
|
||||
description => "Specifies whether the Authorization Server prompts the End-User for"
|
||||
. " reauthentication and consent.",
|
||||
type => 'string',
|
||||
pattern => '(?:none|login|consent|select_account|\S+)', # \S+ is the extension variant
|
||||
optional => 1,
|
||||
},
|
||||
scopes => {
|
||||
description => "Specifies the scopes (user details) that should be authorized and"
|
||||
. " returned, for example 'email' or 'profile'.",
|
||||
type => 'string', # format => 'some-safe-id-list', # FIXME: TODO
|
||||
default => "email profile",
|
||||
optional => 1,
|
||||
},
|
||||
'acr-values' => {
|
||||
description =>
|
||||
"Specifies the Authentication Context Class Reference values that the"
|
||||
. "Authorization Server is being requested to use for the Auth Request.",
|
||||
type => 'string',
|
||||
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.",
|
||||
type => 'boolean',
|
||||
default => 1,
|
||||
optional => 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
sub options {
|
||||
return {
|
||||
"issuer-url" => {},
|
||||
"client-id" => {},
|
||||
"client-key" => { optional => 1 },
|
||||
autocreate => { optional => 1 },
|
||||
"username-claim" => { optional => 1, fixed => 1 },
|
||||
"groups-claim" => { optional => 1 },
|
||||
"groups-autocreate" => { optional => 1 },
|
||||
"groups-overwrite" => { optional => 1 },
|
||||
prompt => { optional => 1 },
|
||||
scopes => { optional => 1 },
|
||||
"acr-values" => { optional => 1 },
|
||||
default => { optional => 1 },
|
||||
comment => { optional => 1 },
|
||||
"query-userinfo" => { optional => 1 },
|
||||
"issuer-url" => {},
|
||||
"client-id" => {},
|
||||
"client-key" => { optional => 1 },
|
||||
autocreate => { optional => 1 },
|
||||
"username-claim" => { optional => 1, fixed => 1 },
|
||||
"groups-claim" => { optional => 1 },
|
||||
"groups-autocreate" => { optional => 1 },
|
||||
"groups-overwrite" => { optional => 1 },
|
||||
prompt => { optional => 1 },
|
||||
scopes => { optional => 1 },
|
||||
"acr-values" => { optional => 1 },
|
||||
default => { optional => 1 },
|
||||
comment => { optional => 1 },
|
||||
"query-userinfo" => { optional => 1 },
|
||||
};
|
||||
}
|
||||
|
||||
@ -119,5 +120,4 @@ sub authenticate_user {
|
||||
die "OpenID realm does not allow password verification.\n";
|
||||
}
|
||||
|
||||
|
||||
1;
|
||||
|
@ -15,9 +15,9 @@ sub type {
|
||||
|
||||
sub options {
|
||||
return {
|
||||
default => { optional => 1 },
|
||||
comment => { optional => 1 },
|
||||
tfa => { optional => 1 },
|
||||
default => { optional => 1 },
|
||||
comment => { 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
|
||||
die "no password\n" if !$password;
|
||||
|
||||
my $pamh = Authen::PAM->new('proxmox-ve-auth', $username, sub {
|
||||
my @res;
|
||||
while(@_) {
|
||||
my $msg_type = shift;
|
||||
my $msg = shift;
|
||||
push @res, (0, $password);
|
||||
}
|
||||
push @res, 0;
|
||||
return @res;
|
||||
});
|
||||
my $pamh = Authen::PAM->new(
|
||||
'proxmox-ve-auth',
|
||||
$username,
|
||||
sub {
|
||||
my @res;
|
||||
while (@_) {
|
||||
my $msg_type = shift;
|
||||
my $msg = shift;
|
||||
push @res, (0, $password);
|
||||
}
|
||||
push @res, 0;
|
||||
return @res;
|
||||
},
|
||||
);
|
||||
|
||||
if (!ref ($pamh)) {
|
||||
my $err = $pamh->pam_strerror($pamh);
|
||||
die "error during PAM init: $err";
|
||||
if (!ref($pamh)) {
|
||||
my $err = $pamh->pam_strerror($pamh);
|
||||
die "error during PAM init: $err";
|
||||
}
|
||||
|
||||
if (my $rpcenv = PVE::RPCEnvironment::get()) {
|
||||
if (my $ip = $rpcenv->get_client_ip()) {
|
||||
$pamh->pam_set_item(PAM_RHOST(), $ip);
|
||||
}
|
||||
if (my $ip = $rpcenv->get_client_ip()) {
|
||||
$pamh->pam_set_item(PAM_RHOST(), $ip);
|
||||
}
|
||||
}
|
||||
|
||||
my $res;
|
||||
|
||||
if (($res = $pamh->pam_authenticate(0)) != PAM_SUCCESS) {
|
||||
my $err = $pamh->pam_strerror($res);
|
||||
die "$err\n";
|
||||
my $err = $pamh->pam_strerror($res);
|
||||
die "$err\n";
|
||||
}
|
||||
|
||||
if (($res = $pamh->pam_acct_mgmt (0)) != PAM_SUCCESS) {
|
||||
my $err = $pamh->pam_strerror($res);
|
||||
die "$err\n";
|
||||
if (($res = $pamh->pam_acct_mgmt(0)) != PAM_SUCCESS) {
|
||||
my $err = $pamh->pam_strerror($res);
|
||||
die "$err\n";
|
||||
}
|
||||
|
||||
$pamh = 0; # call destructor
|
||||
@ -66,7 +70,6 @@ sub authenticate_user {
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
sub store_password {
|
||||
my ($class, $config, $realm, $username, $password) = @_;
|
||||
|
||||
|
@ -12,9 +12,7 @@ use base qw(PVE::Auth::Plugin);
|
||||
|
||||
my $shadowconfigfile = "priv/shadow.cfg";
|
||||
|
||||
cfs_register_file($shadowconfigfile,
|
||||
\&parse_shadow_passwd,
|
||||
\&write_shadow_config);
|
||||
cfs_register_file($shadowconfigfile, \&parse_shadow_passwd, \&write_shadow_config);
|
||||
|
||||
sub parse_shadow_passwd {
|
||||
my ($filename, $raw) = @_;
|
||||
@ -24,15 +22,15 @@ sub parse_shadow_passwd {
|
||||
return $shadow if !defined($raw);
|
||||
|
||||
while ($raw =~ /^\s*(.+?)\s*$/gm) {
|
||||
my $line = $1;
|
||||
my $line = $1;
|
||||
|
||||
if ($line !~ m/^\S+:\S+:$/) {
|
||||
warn "pve shadow password: ignore invalid line $.\n";
|
||||
next;
|
||||
}
|
||||
if ($line !~ m/^\S+:\S+:$/) {
|
||||
warn "pve shadow password: ignore invalid line $.\n";
|
||||
next;
|
||||
}
|
||||
|
||||
my ($userid, $crypt_pass) = split (/:/, $line);
|
||||
$shadow->{users}->{$userid}->{shadow} = $crypt_pass;
|
||||
my ($userid, $crypt_pass) = split(/:/, $line);
|
||||
$shadow->{users}->{$userid}->{shadow} = $crypt_pass;
|
||||
}
|
||||
|
||||
return $shadow;
|
||||
@ -42,12 +40,12 @@ sub write_shadow_config {
|
||||
my ($filename, $cfg) = @_;
|
||||
|
||||
my $data = '';
|
||||
foreach my $userid (keys %{$cfg->{users}}) {
|
||||
my $crypt_pass = $cfg->{users}->{$userid}->{shadow};
|
||||
$data .= "$userid:$crypt_pass:\n";
|
||||
foreach my $userid (keys %{ $cfg->{users} }) {
|
||||
my $crypt_pass = $cfg->{users}->{$userid}->{shadow};
|
||||
$data .= "$userid:$crypt_pass:\n";
|
||||
}
|
||||
|
||||
return $data
|
||||
return $data;
|
||||
}
|
||||
|
||||
sub lock_shadow_config {
|
||||
@ -56,7 +54,7 @@ sub lock_shadow_config {
|
||||
cfs_lock_file($shadowconfigfile, undef, $code);
|
||||
my $err = $@;
|
||||
if ($err) {
|
||||
$errmsg ? die "$errmsg: $err" : die $err;
|
||||
$errmsg ? die "$errmsg: $err" : die $err;
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,9 +64,9 @@ sub type {
|
||||
|
||||
sub options {
|
||||
return {
|
||||
default => { optional => 1 },
|
||||
comment => { optional => 1 },
|
||||
tfa => { optional => 1 },
|
||||
default => { optional => 1 },
|
||||
comment => { optional => 1 },
|
||||
tfa => { optional => 1 },
|
||||
};
|
||||
}
|
||||
|
||||
@ -80,11 +78,11 @@ sub authenticate_user {
|
||||
my $shadow_cfg = cfs_read_file($shadowconfigfile);
|
||||
|
||||
if ($shadow_cfg->{users}->{$username}) {
|
||||
my $encpw = crypt(Encode::encode('utf8', $password),
|
||||
$shadow_cfg->{users}->{$username}->{shadow});
|
||||
die "invalid credentials\n" if ($encpw ne $shadow_cfg->{users}->{$username}->{shadow});
|
||||
my $encpw =
|
||||
crypt(Encode::encode('utf8', $password), $shadow_cfg->{users}->{$username}->{shadow});
|
||||
die "invalid credentials\n" if ($encpw ne $shadow_cfg->{users}->{$username}->{shadow});
|
||||
} else {
|
||||
die "no password set\n";
|
||||
die "no password set\n";
|
||||
}
|
||||
|
||||
return 1;
|
||||
@ -94,10 +92,10 @@ sub store_password {
|
||||
my ($class, $config, $realm, $username, $password) = @_;
|
||||
|
||||
lock_shadow_config(sub {
|
||||
my $shadow_cfg = cfs_read_file($shadowconfigfile);
|
||||
my $epw = PVE::Tools::encrypt_pw($password);
|
||||
$shadow_cfg->{users}->{$username}->{shadow} = $epw;
|
||||
cfs_write_file($shadowconfigfile, $shadow_cfg);
|
||||
my $shadow_cfg = cfs_read_file($shadowconfigfile);
|
||||
my $epw = PVE::Tools::encrypt_pw($password);
|
||||
$shadow_cfg->{users}->{$username}->{shadow} = $epw;
|
||||
cfs_write_file($shadowconfigfile, $shadow_cfg);
|
||||
});
|
||||
}
|
||||
|
||||
@ -105,12 +103,12 @@ sub delete_user {
|
||||
my ($class, $config, $realm, $username) = @_;
|
||||
|
||||
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;
|
||||
|
@ -15,9 +15,11 @@ use base qw(PVE::SectionConfig);
|
||||
|
||||
my $domainconfigfile = "domains.cfg";
|
||||
|
||||
cfs_register_file($domainconfigfile,
|
||||
sub { __PACKAGE__->parse_config(@_); },
|
||||
sub { __PACKAGE__->write_config(@_); });
|
||||
cfs_register_file(
|
||||
$domainconfigfile,
|
||||
sub { __PACKAGE__->parse_config(@_); },
|
||||
sub { __PACKAGE__->write_config(@_); },
|
||||
);
|
||||
|
||||
sub lock_domain_config {
|
||||
my ($code, $errmsg) = @_;
|
||||
@ -25,7 +27,7 @@ sub lock_domain_config {
|
||||
cfs_lock_file($domainconfigfile, undef, $code);
|
||||
my $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\.\-_/;
|
||||
|
||||
PVE::JSONSchema::register_format('pve-realm', \&pve_verify_realm);
|
||||
|
||||
sub pve_verify_realm {
|
||||
my ($realm, $noerr) = @_;
|
||||
|
||||
if ($realm !~ m/^${realm_regex}$/) {
|
||||
return undef if $noerr;
|
||||
die "value does not look like a valid realm\n";
|
||||
return undef if $noerr;
|
||||
die "value does not look like a valid realm\n";
|
||||
}
|
||||
return $realm;
|
||||
}
|
||||
|
||||
PVE::JSONSchema::register_standard_option('realm', {
|
||||
description => "Authentication domain ID",
|
||||
type => 'string', format => 'pve-realm',
|
||||
maxLength => 32,
|
||||
});
|
||||
PVE::JSONSchema::register_standard_option(
|
||||
'realm',
|
||||
{
|
||||
description => "Authentication domain ID",
|
||||
type => 'string',
|
||||
format => 'pve-realm',
|
||||
maxLength => 32,
|
||||
},
|
||||
);
|
||||
|
||||
my $remove_options = "(?:acl|properties|entry)";
|
||||
|
||||
PVE::JSONSchema::register_standard_option('sync-scope', {
|
||||
description => "Select what to sync.",
|
||||
type => 'string',
|
||||
enum => [qw(users groups both)],
|
||||
optional => '1',
|
||||
});
|
||||
PVE::JSONSchema::register_standard_option(
|
||||
'sync-scope',
|
||||
{
|
||||
description => "Select what to sync.",
|
||||
type => 'string',
|
||||
enum => [qw(users groups both)],
|
||||
optional => '1',
|
||||
},
|
||||
);
|
||||
|
||||
PVE::JSONSchema::register_standard_option('sync-remove-vanished', {
|
||||
description => "A semicolon-separated list of things to remove when they or the user"
|
||||
." vanishes during a sync. The following values are possible: 'entry' removes the"
|
||||
." user/group when not returned from the sync. 'properties' removes the set"
|
||||
." properties on existing user/group that do not appear in the source (even custom ones)."
|
||||
." 'acl' removes acls when the user/group is not returned from the sync."
|
||||
." Instead of a list it also can be 'none' (the default).",
|
||||
type => 'string',
|
||||
default => 'none',
|
||||
typetext => "([acl];[properties];[entry])|none",
|
||||
pattern => "(?:(?:$remove_options\;)*$remove_options)|none",
|
||||
optional => '1',
|
||||
});
|
||||
PVE::JSONSchema::register_standard_option(
|
||||
'sync-remove-vanished',
|
||||
{
|
||||
description => "A semicolon-separated list of things to remove when they or the user"
|
||||
. " vanishes during a sync. The following values are possible: 'entry' removes the"
|
||||
. " user/group when not returned from the sync. 'properties' removes the set"
|
||||
. " properties on existing user/group that do not appear in the source (even custom ones)."
|
||||
. " 'acl' removes acls when the user/group is not returned from the sync."
|
||||
. " Instead of a list it also can be 'none' (the default).",
|
||||
type => 'string',
|
||||
default => 'none',
|
||||
typetext => "([acl];[properties];[entry])|none",
|
||||
pattern => "(?:(?:$remove_options\;)*$remove_options)|none",
|
||||
optional => '1',
|
||||
},
|
||||
);
|
||||
|
||||
my $realm_sync_options_desc = {
|
||||
scope => get_standard_option('sync-scope'),
|
||||
'remove-vanished' => get_standard_option('sync-remove-vanished'),
|
||||
# TODO check/rewrite in pve7to8, and remove with 8.0
|
||||
full => {
|
||||
description => "DEPRECATED: use 'remove-vanished' instead. If set, uses the LDAP Directory as source of truth,"
|
||||
." deleting users or groups not returned from the sync and removing"
|
||||
." all locally modified properties of synced users. If not set,"
|
||||
." only syncs information which is present in the synced data, and does not"
|
||||
." delete or modify anything else.",
|
||||
type => 'boolean',
|
||||
optional => '1',
|
||||
description =>
|
||||
"DEPRECATED: use 'remove-vanished' instead. If set, uses the LDAP Directory as source of truth,"
|
||||
. " deleting users or groups not returned from the sync and removing"
|
||||
. " all locally modified properties of synced users. If not set,"
|
||||
. " only syncs information which is present in the synced data, and does not"
|
||||
. " delete or modify anything else.",
|
||||
type => 'boolean',
|
||||
optional => '1',
|
||||
},
|
||||
'enable-new' => {
|
||||
description => "Enable newly synced users immediately.",
|
||||
type => 'boolean',
|
||||
default => '1',
|
||||
optional => '1',
|
||||
description => "Enable newly synced users immediately.",
|
||||
type => 'boolean',
|
||||
default => '1',
|
||||
optional => '1',
|
||||
},
|
||||
purge => {
|
||||
description => "DEPRECATED: use 'remove-vanished' instead. Remove ACLs for users or"
|
||||
." groups which were removed from the config during a sync.",
|
||||
type => 'boolean',
|
||||
optional => '1',
|
||||
description => "DEPRECATED: use 'remove-vanished' instead. Remove ACLs for users or"
|
||||
. " groups which were removed from the config during a sync.",
|
||||
type => 'boolean',
|
||||
optional => '1',
|
||||
},
|
||||
};
|
||||
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('pve-userid', \&verify_username);
|
||||
|
||||
sub verify_username {
|
||||
my ($username, $noerr) = @_;
|
||||
|
||||
$username = '' if !$username;
|
||||
my $len = length($username);
|
||||
if ($len < 3) {
|
||||
die "user name '$username' is too short\n" if !$noerr;
|
||||
return undef;
|
||||
die "user name '$username' is too short\n" if !$noerr;
|
||||
return undef;
|
||||
}
|
||||
if ($len > 64) {
|
||||
die "user name '$username' is too long ($len > 64)\n" if !$noerr;
|
||||
return undef;
|
||||
die "user name '$username' is too long ($len > 64)\n" if !$noerr;
|
||||
return undef;
|
||||
}
|
||||
|
||||
# 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
|
||||
# also see "man useradd"
|
||||
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;
|
||||
@ -131,11 +146,15 @@ sub verify_username {
|
||||
return undef;
|
||||
}
|
||||
|
||||
PVE::JSONSchema::register_standard_option('userid', {
|
||||
description => "Full User ID, in the `name\@realm` format.",
|
||||
type => 'string', format => 'pve-userid',
|
||||
maxLength => 64,
|
||||
});
|
||||
PVE::JSONSchema::register_standard_option(
|
||||
'userid',
|
||||
{
|
||||
description => "Full User ID, in the `name\@realm` format.",
|
||||
type => 'string',
|
||||
format => 'pve-userid',
|
||||
maxLength => 64,
|
||||
},
|
||||
);
|
||||
|
||||
my $tfa_format = {
|
||||
type => {
|
||||
@ -166,7 +185,8 @@ my $tfa_format = {
|
||||
description => "TOTP digits.",
|
||||
format_description => 'COUNT',
|
||||
type => 'integer',
|
||||
minimum => 6, maximum => 8,
|
||||
minimum => 6,
|
||||
maximum => 8,
|
||||
default => 6,
|
||||
optional => 1,
|
||||
},
|
||||
@ -182,12 +202,16 @@ my $tfa_format = {
|
||||
|
||||
PVE::JSONSchema::register_format('pve-tfa-config', $tfa_format);
|
||||
|
||||
PVE::JSONSchema::register_standard_option('tfa', {
|
||||
description => "Use Two-factor authentication.",
|
||||
type => 'string', format => 'pve-tfa-config',
|
||||
optional => 1,
|
||||
maxLength => 128,
|
||||
});
|
||||
PVE::JSONSchema::register_standard_option(
|
||||
'tfa',
|
||||
{
|
||||
description => "Use Two-factor authentication.",
|
||||
type => 'string',
|
||||
format => 'pve-tfa-config',
|
||||
optional => 1,
|
||||
maxLength => 128,
|
||||
},
|
||||
);
|
||||
|
||||
sub parse_tfa_config {
|
||||
my ($data) = @_;
|
||||
@ -197,8 +221,8 @@ sub parse_tfa_config {
|
||||
|
||||
my $defaultData = {
|
||||
propertyList => {
|
||||
type => { description => "Realm type." },
|
||||
realm => get_standard_option('realm'),
|
||||
type => { description => "Realm type." },
|
||||
realm => get_standard_option('realm'),
|
||||
},
|
||||
};
|
||||
|
||||
@ -210,12 +234,12 @@ sub parse_section_header {
|
||||
my ($class, $line) = @_;
|
||||
|
||||
if ($line =~ m/^(\S+):\s*(\S+)\s*$/) {
|
||||
my ($type, $realm) = (lc($1), $2);
|
||||
my $errmsg = undef; # set if you want to skip whole section
|
||||
eval { pve_verify_realm($realm); };
|
||||
$errmsg = $@ if $@;
|
||||
my $config = {}; # to return additional attributes
|
||||
return ($type, $realm, $errmsg, $config);
|
||||
my ($type, $realm) = (lc($1), $2);
|
||||
my $errmsg = undef; # set if you want to skip whole section
|
||||
eval { pve_verify_realm($realm); };
|
||||
$errmsg = $@ if $@;
|
||||
my $config = {}; # to return additional attributes
|
||||
return ($type, $realm, $errmsg, $config);
|
||||
}
|
||||
return undef;
|
||||
}
|
||||
@ -226,20 +250,20 @@ sub parse_config {
|
||||
my $cfg = $class->SUPER::parse_config($filename, $raw);
|
||||
|
||||
my $default;
|
||||
foreach my $realm (keys %{$cfg->{ids}}) {
|
||||
my $data = $cfg->{ids}->{$realm};
|
||||
# make sure there is only one default marker
|
||||
if ($data->{default}) {
|
||||
if ($default) {
|
||||
delete $data->{default};
|
||||
} else {
|
||||
$default = $realm;
|
||||
}
|
||||
}
|
||||
foreach my $realm (keys %{ $cfg->{ids} }) {
|
||||
my $data = $cfg->{ids}->{$realm};
|
||||
# make sure there is only one default marker
|
||||
if ($data->{default}) {
|
||||
if ($default) {
|
||||
delete $data->{default};
|
||||
} else {
|
||||
$default = $realm;
|
||||
}
|
||||
}
|
||||
|
||||
if ($data->{comment}) {
|
||||
$data->{comment} = PVE::Tools::decode_text($data->{comment});
|
||||
}
|
||||
if ($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}->{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}->{plugin} = 'PVE::Auth::PAM';
|
||||
$cfg->{ids}->{pam}->{plugin} = 'PVE::Auth::PAM';
|
||||
$cfg->{ids}->{pam}->{comment} = "Linux PAM standard authentication"
|
||||
if !$cfg->{ids}->{pam}->{comment};
|
||||
if !$cfg->{ids}->{pam}->{comment};
|
||||
|
||||
return $cfg;
|
||||
};
|
||||
}
|
||||
|
||||
sub write_config {
|
||||
my ($class, $filename, $cfg) = @_;
|
||||
|
||||
foreach my $realm (keys %{$cfg->{ids}}) {
|
||||
my $data = $cfg->{ids}->{$realm};
|
||||
if ($data->{comment}) {
|
||||
$data->{comment} = PVE::Tools::encode_text($data->{comment});
|
||||
}
|
||||
foreach my $realm (keys %{ $cfg->{ids} }) {
|
||||
my $data = $cfg->{ids}->{$realm};
|
||||
if ($data->{comment}) {
|
||||
$data->{comment} = PVE::Tools::encode_text($data->{comment});
|
||||
}
|
||||
}
|
||||
|
||||
$class->SUPER::write_config($filename, $cfg);
|
||||
|
@ -30,17 +30,20 @@ sub param_mapping {
|
||||
my ($name) = @_;
|
||||
|
||||
my $mapping = {
|
||||
'change_password' => [
|
||||
PVE::CLIHandler::get_standard_mapping('pve-password'),
|
||||
],
|
||||
'create_ticket' => [
|
||||
PVE::CLIHandler::get_standard_mapping('pve-password', {
|
||||
func => sub {
|
||||
# do not accept values given on cmdline
|
||||
return PVE::PTY::read_password('Enter password: ');
|
||||
},
|
||||
}),
|
||||
]
|
||||
'change_password' => [
|
||||
PVE::CLIHandler::get_standard_mapping('pve-password'),
|
||||
],
|
||||
'create_ticket' => [
|
||||
PVE::CLIHandler::get_standard_mapping(
|
||||
'pve-password',
|
||||
{
|
||||
func => sub {
|
||||
# do not accept values given on cmdline
|
||||
return PVE::PTY::read_password('Enter password: ');
|
||||
},
|
||||
},
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
return $mapping->{$name};
|
||||
@ -55,31 +58,31 @@ my $print_perm_result = sub {
|
||||
my ($data, $schema, $options) = @_;
|
||||
|
||||
if (!defined($options->{'output-format'}) || $options->{'output-format'} eq 'text') {
|
||||
my $table_schema = {
|
||||
type => 'array',
|
||||
items => {
|
||||
type => 'object',
|
||||
properties => {
|
||||
'path' => { type => 'string', title => 'ACL path' },
|
||||
'permissions' => { type => 'string', title => 'Permissions' },
|
||||
},
|
||||
},
|
||||
};
|
||||
my $table_data = [];
|
||||
foreach my $path (sort keys %$data) {
|
||||
my $value = '';
|
||||
my $curr = $data->{$path};
|
||||
foreach my $perm (sort keys %$curr) {
|
||||
$value .= "\n" if $value;
|
||||
$value .= $perm;
|
||||
$value .= " (*)" if $curr->{$perm};
|
||||
}
|
||||
push @$table_data, { path => $path, permissions => $value };
|
||||
}
|
||||
PVE::CLIFormatter::print_api_result($table_data, $table_schema, undef, $options);
|
||||
print "Permissions marked with '(*)' have the 'propagate' flag set.\n";
|
||||
my $table_schema = {
|
||||
type => 'array',
|
||||
items => {
|
||||
type => 'object',
|
||||
properties => {
|
||||
'path' => { type => 'string', title => 'ACL path' },
|
||||
'permissions' => { type => 'string', title => 'Permissions' },
|
||||
},
|
||||
},
|
||||
};
|
||||
my $table_data = [];
|
||||
foreach my $path (sort keys %$data) {
|
||||
my $value = '';
|
||||
my $curr = $data->{$path};
|
||||
foreach my $perm (sort keys %$curr) {
|
||||
$value .= "\n" if $value;
|
||||
$value .= $perm;
|
||||
$value .= " (*)" if $curr->{$perm};
|
||||
}
|
||||
push @$table_data, { path => $path, permissions => $value };
|
||||
}
|
||||
PVE::CLIFormatter::print_api_result($table_data, $table_schema, undef, $options);
|
||||
print "Permissions marked with '(*)' have the 'propagate' flag set.\n";
|
||||
} 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',
|
||||
description => 'Retrieve effective permissions of given token.',
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
userid => get_standard_option('userid'),
|
||||
tokenid => get_standard_option('token-subid'),
|
||||
path => get_standard_option('acl-path', {
|
||||
description => "Only dump this specific path, not the whole tree.",
|
||||
optional => 1,
|
||||
}),
|
||||
},
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
userid => get_standard_option('userid'),
|
||||
tokenid => get_standard_option('token-subid'),
|
||||
path => get_standard_option(
|
||||
'acl-path',
|
||||
{
|
||||
description => "Only dump this specific path, not the whole tree.",
|
||||
optional => 1,
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => 'object',
|
||||
description => 'Hash of structure "path" => "privilege" => "propagate boolean".',
|
||||
type => 'object',
|
||||
description => 'Hash of structure "path" => "privilege" => "propagate boolean".',
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
my $token_subid = extract_param($param, "tokenid");
|
||||
$param->{userid} = PVE::AccessControl::join_tokenid($param->{userid}, $token_subid);
|
||||
my $token_subid = extract_param($param, "tokenid");
|
||||
$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({
|
||||
name => 'delete_tfa',
|
||||
@ -118,34 +125,35 @@ __PACKAGE__->register_method({
|
||||
method => 'PUT',
|
||||
description => 'Delete TFA entries from a user.',
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
userid => get_standard_option('userid'),
|
||||
id => {
|
||||
description => "The TFA ID, if none provided, all TFA entries will be deleted.",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
},
|
||||
},
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
userid => get_standard_option('userid'),
|
||||
id => {
|
||||
description => "The TFA ID, if none provided, all TFA entries will be deleted.",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => { type => 'null' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
my ($param) = @_;
|
||||
|
||||
my $userid = extract_param($param, "userid");
|
||||
my $tfa_id = extract_param($param, "id");
|
||||
my $userid = extract_param($param, "userid");
|
||||
my $tfa_id = extract_param($param, "id");
|
||||
|
||||
PVE::AccessControl::lock_tfa_config(sub {
|
||||
my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
|
||||
if (defined($tfa_id)) {
|
||||
$tfa_cfg->api_delete_tfa($userid, $tfa_id);
|
||||
} else {
|
||||
$tfa_cfg->remove_user($userid);
|
||||
}
|
||||
cfs_write_file('priv/tfa.cfg', $tfa_cfg);
|
||||
});
|
||||
return;
|
||||
}});
|
||||
PVE::AccessControl::lock_tfa_config(sub {
|
||||
my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
|
||||
if (defined($tfa_id)) {
|
||||
$tfa_cfg->api_delete_tfa($userid, $tfa_id);
|
||||
} else {
|
||||
$tfa_cfg->remove_user($userid);
|
||||
}
|
||||
cfs_write_file('priv/tfa.cfg', $tfa_cfg);
|
||||
});
|
||||
return;
|
||||
},
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method({
|
||||
name => 'list_tfa',
|
||||
@ -153,97 +161,181 @@ __PACKAGE__->register_method({
|
||||
method => 'GET',
|
||||
description => "List TFA entries.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
userid => get_standard_option('userid', { optional => 1 }),
|
||||
},
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
userid => get_standard_option('userid', { optional => 1 }),
|
||||
},
|
||||
},
|
||||
returns => { type => 'null' },
|
||||
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 ($entries, $indent) = @_;
|
||||
my sub format_tfa_entries : prototype($;$) {
|
||||
my ($entries, $indent) = @_;
|
||||
|
||||
$indent //= '';
|
||||
$indent //= '';
|
||||
|
||||
my $nl = '';
|
||||
for my $entry (@$entries) {
|
||||
my ($id, $ty, $desc) = ($entry->@{qw/id type description/});
|
||||
printf("${nl}${indent}%-9s %s\n${indent} %s\n", "$ty:", $id, $desc // '');
|
||||
$nl = "\n";
|
||||
}
|
||||
};
|
||||
my $nl = '';
|
||||
for my $entry (@$entries) {
|
||||
my ($id, $ty, $desc) = ($entry->@{qw/id type description/});
|
||||
printf("${nl}${indent}%-9s %s\n${indent} %s\n", "$ty:", $id, $desc // '');
|
||||
$nl = "\n";
|
||||
}
|
||||
}
|
||||
|
||||
my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
|
||||
if (defined($userid)) {
|
||||
format_tfa_entries($tfa_cfg->api_list_user_tfa($userid));
|
||||
} else {
|
||||
my $result = $tfa_cfg->api_list_tfa('', 1);
|
||||
my $nl = '';
|
||||
for my $entry (sort { $a->{userid} cmp $b->{userid} } @$result) {
|
||||
print "${nl}$entry->{userid}:\n";
|
||||
format_tfa_entries($entry->{entries}, ' ');
|
||||
$nl = "\n";
|
||||
}
|
||||
}
|
||||
return;
|
||||
}});
|
||||
my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
|
||||
if (defined($userid)) {
|
||||
format_tfa_entries($tfa_cfg->api_list_user_tfa($userid));
|
||||
} else {
|
||||
my $result = $tfa_cfg->api_list_tfa('', 1);
|
||||
my $nl = '';
|
||||
for my $entry (sort { $a->{userid} cmp $b->{userid} } @$result) {
|
||||
print "${nl}$entry->{userid}:\n";
|
||||
format_tfa_entries($entry->{entries}, ' ');
|
||||
$nl = "\n";
|
||||
}
|
||||
}
|
||||
return;
|
||||
},
|
||||
});
|
||||
|
||||
our $cmddef = {
|
||||
user => {
|
||||
add => [ 'PVE::API2::User', 'create_user', ['userid'] ],
|
||||
modify => [ 'PVE::API2::User', 'update_user', ['userid'] ],
|
||||
delete => [ 'PVE::API2::User', 'delete_user', ['userid'] ],
|
||||
list => [ 'PVE::API2::User', 'index', [], {}, $print_api_result, $PVE::RESTHandler::standard_output_options],
|
||||
permissions => [ 'PVE::API2::AccessControl', 'permissions', ['userid'], {}, $print_perm_result, $PVE::RESTHandler::standard_output_options],
|
||||
tfa => {
|
||||
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],
|
||||
}
|
||||
add => ['PVE::API2::User', 'create_user', ['userid']],
|
||||
modify => ['PVE::API2::User', 'update_user', ['userid']],
|
||||
delete => ['PVE::API2::User', 'delete_user', ['userid']],
|
||||
list => [
|
||||
'PVE::API2::User',
|
||||
'index',
|
||||
[],
|
||||
{},
|
||||
$print_api_result,
|
||||
$PVE::RESTHandler::standard_output_options,
|
||||
],
|
||||
permissions => [
|
||||
'PVE::API2::AccessControl',
|
||||
'permissions',
|
||||
['userid'],
|
||||
{},
|
||||
$print_perm_result,
|
||||
$PVE::RESTHandler::standard_output_options,
|
||||
],
|
||||
tfa => {
|
||||
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 => {
|
||||
add => [ 'PVE::API2::Group', 'create_group', ['groupid'] ],
|
||||
modify => [ 'PVE::API2::Group', 'update_group', ['groupid'] ],
|
||||
delete => [ 'PVE::API2::Group', 'delete_group', ['groupid'] ],
|
||||
list => [ 'PVE::API2::Group', 'index', [], {}, $print_api_result, $PVE::RESTHandler::standard_output_options],
|
||||
add => ['PVE::API2::Group', 'create_group', ['groupid']],
|
||||
modify => ['PVE::API2::Group', 'update_group', ['groupid']],
|
||||
delete => ['PVE::API2::Group', 'delete_group', ['groupid']],
|
||||
list => [
|
||||
'PVE::API2::Group',
|
||||
'index',
|
||||
[],
|
||||
{},
|
||||
$print_api_result,
|
||||
$PVE::RESTHandler::standard_output_options,
|
||||
],
|
||||
},
|
||||
role => {
|
||||
add => [ 'PVE::API2::Role', 'create_role', ['roleid'] ],
|
||||
modify => [ 'PVE::API2::Role', 'update_role', ['roleid'] ],
|
||||
delete => [ 'PVE::API2::Role', 'delete_role', ['roleid'] ],
|
||||
list => [ 'PVE::API2::Role', 'index', [], {}, $print_api_result, $PVE::RESTHandler::standard_output_options],
|
||||
add => ['PVE::API2::Role', 'create_role', ['roleid']],
|
||||
modify => ['PVE::API2::Role', 'update_role', ['roleid']],
|
||||
delete => ['PVE::API2::Role', 'delete_role', ['roleid']],
|
||||
list => [
|
||||
'PVE::API2::Role',
|
||||
'index',
|
||||
[],
|
||||
{},
|
||||
$print_api_result,
|
||||
$PVE::RESTHandler::standard_output_options,
|
||||
],
|
||||
},
|
||||
acl => {
|
||||
modify => [ 'PVE::API2::ACL', 'update_acl', ['path'], { delete => 0 }],
|
||||
delete => [ 'PVE::API2::ACL', 'update_acl', ['path'], { delete => 1 }],
|
||||
list => [ 'PVE::API2::ACL', 'read_acl', [], {}, $print_api_result, $PVE::RESTHandler::standard_output_options],
|
||||
modify => ['PVE::API2::ACL', 'update_acl', ['path'], { delete => 0 }],
|
||||
delete => ['PVE::API2::ACL', 'update_acl', ['path'], { delete => 1 }],
|
||||
list => [
|
||||
'PVE::API2::ACL',
|
||||
'read_acl',
|
||||
[],
|
||||
{},
|
||||
$print_api_result,
|
||||
$PVE::RESTHandler::standard_output_options,
|
||||
],
|
||||
},
|
||||
realm => {
|
||||
add => [ 'PVE::API2::Domains', 'create', ['realm'] ],
|
||||
modify => [ 'PVE::API2::Domains', 'update', ['realm'] ],
|
||||
delete => [ 'PVE::API2::Domains', 'delete', ['realm'] ],
|
||||
list => [ 'PVE::API2::Domains', 'index', [], {}, $print_api_result, $PVE::RESTHandler::standard_output_options],
|
||||
sync => [ 'PVE::API2::Domains', 'sync', ['realm'], ],
|
||||
add => ['PVE::API2::Domains', 'create', ['realm']],
|
||||
modify => ['PVE::API2::Domains', 'update', ['realm']],
|
||||
delete => ['PVE::API2::Domains', 'delete', ['realm']],
|
||||
list => [
|
||||
'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 {
|
||||
my ($res) = @_;
|
||||
print "$res->{ticket}\n";
|
||||
}],
|
||||
ticket => [
|
||||
'PVE::API2::AccessControl',
|
||||
'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' },
|
||||
usermod => { alias => 'user modify' },
|
||||
@ -272,10 +364,17 @@ eval {
|
||||
|
||||
if ($have_pool_api) {
|
||||
$cmddef->{pool} = {
|
||||
add => [ 'PVE::API2::Pool', 'create_pool', ['poolid'] ],
|
||||
modify => [ 'PVE::API2::Pool', 'update_pool', ['poolid'] ],
|
||||
delete => [ 'PVE::API2::Pool', 'delete_pool', ['poolid'] ],
|
||||
list => [ 'PVE::API2::Pool', 'index', [], {}, $print_api_result, $PVE::RESTHandler::standard_output_options],
|
||||
add => ['PVE::API2::Pool', 'create_pool', ['poolid']],
|
||||
modify => ['PVE::API2::Pool', 'update_pool', ['poolid']],
|
||||
delete => ['PVE::API2::Pool', 'delete_pool', ['poolid']],
|
||||
list => [
|
||||
'PVE::API2::Pool',
|
||||
'index',
|
||||
[],
|
||||
{},
|
||||
$print_api_result,
|
||||
$PVE::RESTHandler::standard_output_options,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,7 @@ sub type {
|
||||
}
|
||||
|
||||
my $props = get_standard_option('realm-sync-options', {
|
||||
realm => get_standard_option('realm'),
|
||||
realm => get_standard_option('realm'),
|
||||
});
|
||||
|
||||
sub properties {
|
||||
@ -32,20 +32,20 @@ sub properties {
|
||||
|
||||
sub options {
|
||||
my $options = {
|
||||
enabled => { optional => 1 },
|
||||
schedule => {},
|
||||
comment => { optional => 1 },
|
||||
scope => {},
|
||||
enabled => { optional => 1 },
|
||||
schedule => {},
|
||||
comment => { optional => 1 },
|
||||
scope => {},
|
||||
};
|
||||
for my $opt (keys %$props) {
|
||||
next if defined($options->{$opt});
|
||||
# ignore legacy props from realm-sync schema
|
||||
next if $opt eq 'full' || $opt eq 'purge';
|
||||
if ($props->{$opt}->{optional}) {
|
||||
$options->{$opt} = { optional => 1 };
|
||||
} else {
|
||||
$options->{$opt} = {};
|
||||
}
|
||||
next if defined($options->{$opt});
|
||||
# ignore legacy props from realm-sync schema
|
||||
next if $opt eq 'full' || $opt eq 'purge';
|
||||
if ($props->{$opt}->{optional}) {
|
||||
$options->{$opt} = { optional => 1 };
|
||||
} else {
|
||||
$options->{$opt} = {};
|
||||
}
|
||||
}
|
||||
$options->{realm}->{fixed} = 1;
|
||||
|
||||
@ -69,8 +69,8 @@ sub createSchema {
|
||||
|
||||
my $opts = $class->options();
|
||||
for my $opt (keys $schema->{properties}->%*) {
|
||||
next if defined($opts->{$opt}) || $opt eq 'id';
|
||||
delete $schema->{properties}->{$opt};
|
||||
next if defined($opts->{$opt}) || $opt eq 'id';
|
||||
delete $schema->{properties}->{$opt};
|
||||
}
|
||||
|
||||
return $schema;
|
||||
@ -82,9 +82,9 @@ sub updateSchema {
|
||||
|
||||
my $opts = $class->options();
|
||||
for my $opt (keys $schema->{properties}->%*) {
|
||||
next if defined($opts->{$opt});
|
||||
next if $opt eq 'id' || $opt eq 'delete';
|
||||
delete $schema->{properties}->{$opt};
|
||||
next if defined($opts->{$opt});
|
||||
next if $opt eq 'id' || $opt eq 'delete';
|
||||
delete $schema->{properties}->{$opt};
|
||||
}
|
||||
|
||||
return $schema;
|
||||
@ -111,9 +111,9 @@ sub save_state {
|
||||
my $statefile = "$statedir/realm-sync-$id.json";
|
||||
|
||||
if (defined($state)) {
|
||||
PVE::Tools::file_set_contents($statefile, encode_json($state));
|
||||
PVE::Tools::file_set_contents($statefile, encode_json($state));
|
||||
} 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;
|
||||
@ -123,7 +123,7 @@ sub run {
|
||||
my ($class, $conf, $id, $schedule) = @_;
|
||||
|
||||
for my $opt (keys %$conf) {
|
||||
delete $conf->{$opt} if !defined($props->{$opt});
|
||||
delete $conf->{$opt} if !defined($props->{$opt});
|
||||
}
|
||||
|
||||
my $realm = $conf->{realm};
|
||||
@ -133,69 +133,88 @@ sub run {
|
||||
my $nodename = PVE::INotify::nodename();
|
||||
|
||||
# check statefile in pmxcfs if we should start
|
||||
my $shouldrun = PVE::Cluster::cfs_lock_domain('realm-sync', undef, sub {
|
||||
my $members = PVE::Cluster::get_members();
|
||||
my $shouldrun = PVE::Cluster::cfs_lock_domain(
|
||||
'realm-sync',
|
||||
undef,
|
||||
sub {
|
||||
my $members = PVE::Cluster::get_members();
|
||||
|
||||
my $state = get_state($id);
|
||||
my $last_node = $state->{node} // $nodename;
|
||||
my $last_upid = $state->{upid};
|
||||
my $last_time = $state->{time};
|
||||
my $state = get_state($id);
|
||||
my $last_node = $state->{node} // $nodename;
|
||||
my $last_upid = $state->{upid};
|
||||
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)) {
|
||||
# first check if the next run is scheduled
|
||||
if (my $parsed = PVE::Tools::upid_decode($last_upid, 1)) {
|
||||
my $cal_spec = PVE::CalendarEvent::parse_calendar_event($schedule);
|
||||
my $next_sync = PVE::CalendarEvent::compute_next_event($cal_spec, $parsed->{starttime});
|
||||
return 0 if !defined($next_sync) || $now < $next_sync; # not yet its (next) turn
|
||||
}
|
||||
# check if still running and node is online
|
||||
my $tasks = PVE::Cluster::get_tasklist();
|
||||
for my $task (@$tasks) {
|
||||
next if $task->{upid} ne $last_upid;
|
||||
last if defined($task->{endtime}); # it's already finished
|
||||
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
|
||||
return 0;
|
||||
}
|
||||
if (defined($last_upid)) {
|
||||
# first check if the next run is scheduled
|
||||
if (my $parsed = PVE::Tools::upid_decode($last_upid, 1)) {
|
||||
my $cal_spec = PVE::CalendarEvent::parse_calendar_event($schedule);
|
||||
my $next_sync =
|
||||
PVE::CalendarEvent::compute_next_event($cal_spec, $parsed->{starttime});
|
||||
return 0 if !defined($next_sync) || $now < $next_sync; # not yet its (next) turn
|
||||
}
|
||||
# check if still running and node is online
|
||||
my $tasks = PVE::Cluster::get_tasklist();
|
||||
for my $task (@$tasks) {
|
||||
next if $task->{upid} ne $last_upid;
|
||||
last if defined($task->{endtime}); # it's already finished
|
||||
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
|
||||
return 0;
|
||||
}
|
||||
|
||||
# any of the following conditions should be true here:
|
||||
# * it was started on another node but that node is offline now
|
||||
# * it was started but either too long ago, or with an error
|
||||
# * the started task finished
|
||||
# any of the following conditions should be true here:
|
||||
# * it was started on another node but that node is offline now
|
||||
# * it was started but either too long ago, or with an error
|
||||
# * the started task finished
|
||||
|
||||
save_state($id, {
|
||||
node => $nodename,
|
||||
time => $now,
|
||||
});
|
||||
return 1;
|
||||
});
|
||||
save_state(
|
||||
$id,
|
||||
{
|
||||
node => $nodename,
|
||||
time => $now,
|
||||
},
|
||||
);
|
||||
return 1;
|
||||
},
|
||||
);
|
||||
die $@ if $@;
|
||||
|
||||
if ($shouldrun) {
|
||||
my $upid = eval { PVE::API2::Domains->sync($conf) };
|
||||
my $err = $@;
|
||||
PVE::Cluster::cfs_lock_domain('realm-sync', undef, sub {
|
||||
if ($err && !$upid) {
|
||||
save_state($id, {
|
||||
node => $nodename,
|
||||
time => $now,
|
||||
error => $err,
|
||||
});
|
||||
die "$err\n";
|
||||
}
|
||||
my $upid = eval { PVE::API2::Domains->sync($conf) };
|
||||
my $err = $@;
|
||||
PVE::Cluster::cfs_lock_domain(
|
||||
'realm-sync',
|
||||
undef,
|
||||
sub {
|
||||
if ($err && !$upid) {
|
||||
save_state(
|
||||
$id,
|
||||
{
|
||||
node => $nodename,
|
||||
time => $now,
|
||||
error => $err,
|
||||
},
|
||||
);
|
||||
die "$err\n";
|
||||
}
|
||||
|
||||
save_state($id, {
|
||||
node => $nodename,
|
||||
upid => $upid,
|
||||
});
|
||||
});
|
||||
die $@ if $@;
|
||||
return $upid;
|
||||
save_state(
|
||||
$id,
|
||||
{
|
||||
node => $nodename,
|
||||
upid => $upid,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
die $@ if $@;
|
||||
return $upid;
|
||||
}
|
||||
|
||||
return "OK"; # all other cases should not run the sync on this node
|
||||
|
@ -32,27 +32,28 @@ my $compile_acl_path = sub {
|
||||
|
||||
# permissions() will always prime the cache for the owning user
|
||||
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
|
||||
if (!$data->{poolroles}) {
|
||||
$data->{poolroles} = {};
|
||||
$data->{poolroles} = {};
|
||||
|
||||
foreach my $pool (keys %{$cfg->{pools}}) {
|
||||
my $d = $cfg->{pools}->{$pool};
|
||||
my $pool_roles = PVE::AccessControl::roles($cfg, $user, "/pool/$pool"); # pool roles
|
||||
next if !scalar(keys %$pool_roles);
|
||||
foreach my $vmid (keys %{$d->{vms}}) {
|
||||
for my $role (keys %$pool_roles) {
|
||||
$data->{poolroles}->{"/vms/$vmid"}->{$role} = 1;
|
||||
}
|
||||
}
|
||||
foreach my $storeid (keys %{$d->{storage}}) {
|
||||
for my $role (keys %$pool_roles) {
|
||||
$data->{poolroles}->{"/storage/$storeid"}->{$role} = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach my $pool (keys %{ $cfg->{pools} }) {
|
||||
my $d = $cfg->{pools}->{$pool};
|
||||
my $pool_roles = PVE::AccessControl::roles($cfg, $user, "/pool/$pool"); # pool roles
|
||||
next if !scalar(keys %$pool_roles);
|
||||
foreach my $vmid (keys %{ $d->{vms} }) {
|
||||
for my $role (keys %$pool_roles) {
|
||||
$data->{poolroles}->{"/vms/$vmid"}->{$role} = 1;
|
||||
}
|
||||
}
|
||||
foreach my $storeid (keys %{ $d->{storage} }) {
|
||||
for my $role (keys %$pool_roles) {
|
||||
$data->{poolroles}->{"/storage/$storeid"}->{$role} = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 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
|
||||
if ($data->{poolroles}->{$path}) {
|
||||
# NoAccess must not be trumped by pool ACLs
|
||||
if (!defined($roles->{NoAccess})) {
|
||||
if ($data->{poolroles}->{$path}->{NoAccess}) {
|
||||
# but pool ACL NoAccess trumps regular ACL
|
||||
$roles = { 'NoAccess' => 0 };
|
||||
} else {
|
||||
foreach my $role (keys %{$data->{poolroles}->{$path}}) {
|
||||
# only use role from pool ACL if regular ACL didn't already
|
||||
# set it, and never set propagation for pool-derived ACLs
|
||||
$roles->{$role} = 0 if !defined($roles->{$role});
|
||||
}
|
||||
}
|
||||
}
|
||||
# NoAccess must not be trumped by pool ACLs
|
||||
if (!defined($roles->{NoAccess})) {
|
||||
if ($data->{poolroles}->{$path}->{NoAccess}) {
|
||||
# but pool ACL NoAccess trumps regular ACL
|
||||
$roles = { 'NoAccess' => 0 };
|
||||
} else {
|
||||
foreach my $role (keys %{ $data->{poolroles}->{$path} }) {
|
||||
# only use role from pool ACL if regular ACL didn't already
|
||||
# set it, and never set propagation for pool-derived ACLs
|
||||
$roles->{$role} = 0 if !defined($roles->{$role});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# cache roles
|
||||
@ -86,29 +87,29 @@ my $compile_acl_path = sub {
|
||||
# flag value, a key being defined means the priv is set
|
||||
my $privs = {};
|
||||
foreach my $role (keys %$roles) {
|
||||
if (my $privset = $cfg->{roles}->{$role}) {
|
||||
foreach my $p (keys %$privset) {
|
||||
# set priv '$p' to propagated iff any of the set roles
|
||||
# containing it have the propagated flag set
|
||||
$privs->{$p} ||= $roles->{$role};
|
||||
}
|
||||
}
|
||||
if (my $privset = $cfg->{roles}->{$role}) {
|
||||
foreach my $p (keys %$privset) {
|
||||
# set priv '$p' to propagated iff any of the set roles
|
||||
# containing it have the propagated flag set
|
||||
$privs->{$p} ||= $roles->{$role};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# intersect user and token permissions
|
||||
if ($username && $username ne 'root@pam') {
|
||||
# map of set privs to their propagation flag value, for the owning user
|
||||
my $user_privs = $cache->{$username}->{privs}->{$path};
|
||||
# list of privs set both for token and owning user
|
||||
my $filtered_privs = [ grep { defined($user_privs->{$_}) } keys %$privs ];
|
||||
# intersection of privs using filtered list, combining both propagation
|
||||
# flags
|
||||
$privs = { map { $_ => $user_privs->{$_} && $privs->{$_} } @$filtered_privs };
|
||||
# map of set privs to their propagation flag value, for the owning user
|
||||
my $user_privs = $cache->{$username}->{privs}->{$path};
|
||||
# list of privs set both for token and owning user
|
||||
my $filtered_privs = [grep { defined($user_privs->{$_}) } keys %$privs];
|
||||
# intersection of privs using filtered list, combining both propagation
|
||||
# flags
|
||||
$privs = { map { $_ => $user_privs->{$_} && $privs->{$_} } @$filtered_privs };
|
||||
}
|
||||
|
||||
foreach my $priv (keys %$privs) {
|
||||
# safeguard, this should never happen anyway
|
||||
delete $privs->{$priv} if !defined($privs->{$priv});
|
||||
# safeguard, this should never happen anyway
|
||||
delete $privs->{$priv} if !defined($privs->{$priv});
|
||||
}
|
||||
|
||||
# cache privs
|
||||
@ -136,31 +137,31 @@ sub permissions {
|
||||
my ($self, $user, $path) = @_;
|
||||
|
||||
if ($user eq 'root@pam') { # root can do anything
|
||||
my $cfg = $self->{user_cfg};
|
||||
return { map { $_ => 1 } keys %{$cfg->{roles}->{'Administrator'}} };
|
||||
my $cfg = $self->{user_cfg};
|
||||
return { map { $_ => 1 } keys %{ $cfg->{roles}->{'Administrator'} } };
|
||||
}
|
||||
|
||||
if (!defined($path)) {
|
||||
# this shouldn't happen!
|
||||
warn "internal error: ACL check called for undefined ACL path!\n";
|
||||
return {};
|
||||
# this shouldn't happen!
|
||||
warn "internal error: ACL check called for undefined ACL path!\n";
|
||||
return {};
|
||||
}
|
||||
|
||||
if (PVE::AccessControl::pve_verify_tokenid($user, 1)) {
|
||||
my ($username, $token) = PVE::AccessControl::split_tokenid($user);
|
||||
my $cfg = $self->{user_cfg};
|
||||
my $token_info = $cfg->{users}->{$username}->{tokens}->{$token};
|
||||
my ($username, $token) = PVE::AccessControl::split_tokenid($user);
|
||||
my $cfg = $self->{user_cfg};
|
||||
my $token_info = $cfg->{users}->{$username}->{tokens}->{$token};
|
||||
|
||||
return {} if !$token_info;
|
||||
return {} if !$token_info;
|
||||
|
||||
# ensure cache for user is populated
|
||||
my $user_perms = $self->permissions($username, $path);
|
||||
# ensure cache for user is populated
|
||||
my $user_perms = $self->permissions($username, $path);
|
||||
|
||||
# return user privs for non-privsep tokens
|
||||
return $user_perms if !$token_info->{privsep};
|
||||
# return user privs for non-privsep tokens
|
||||
return $user_perms if !$token_info->{privsep};
|
||||
} else {
|
||||
$user = PVE::AccessControl::verify_username($user, 1);
|
||||
return {} if !$user;
|
||||
$user = PVE::AccessControl::verify_username($user, 1);
|
||||
return {} if !$user;
|
||||
}
|
||||
|
||||
my $cache = $self->{aclcache};
|
||||
@ -181,53 +182,57 @@ sub compute_api_permission {
|
||||
|
||||
my $res = {};
|
||||
my $priv_re_map = {
|
||||
vms => qr/VM\.|Permissions\.Modify/,
|
||||
access => qr/(User|Group)\.|Permissions\.Modify/,
|
||||
storage => qr/Datastore\.|Permissions\.Modify/,
|
||||
nodes => qr/Sys\.|Permissions\.Modify/,
|
||||
sdn => qr/SDN\.|Permissions\.Modify/,
|
||||
dc => qr/Sys\.Audit|Sys\.Modify|SDN\./,
|
||||
mapping => qr/Mapping\.|Permissions.Modify/,
|
||||
vms => qr/VM\.|Permissions\.Modify/,
|
||||
access => qr/(User|Group)\.|Permissions\.Modify/,
|
||||
storage => qr/Datastore\.|Permissions\.Modify/,
|
||||
nodes => qr/Sys\.|Permissions\.Modify/,
|
||||
sdn => qr/SDN\.|Permissions\.Modify/,
|
||||
dc => qr/Sys\.Audit|Sys\.Modify|SDN\./,
|
||||
mapping => qr/Mapping\.|Permissions.Modify/,
|
||||
};
|
||||
map { $res->{$_} = {} } keys %$priv_re_map;
|
||||
|
||||
my $required_paths = ['/', '/nodes', '/access/groups', '/vms', '/storage', '/sdn', '/mapping'];
|
||||
my $defined_paths = [];
|
||||
PVE::AccessControl::iterate_acl_tree("/", $usercfg->{acl_root}, sub {
|
||||
my ($path, $node) = @_;
|
||||
push @$defined_paths, $path;
|
||||
});
|
||||
PVE::AccessControl::iterate_acl_tree(
|
||||
"/",
|
||||
$usercfg->{acl_root},
|
||||
sub {
|
||||
my ($path, $node) = @_;
|
||||
push @$defined_paths, $path;
|
||||
},
|
||||
);
|
||||
|
||||
my $checked_paths = {};
|
||||
foreach my $path (@$required_paths, @$defined_paths) {
|
||||
next if $checked_paths->{$path};
|
||||
$checked_paths->{$path} = 1;
|
||||
next if $checked_paths->{$path};
|
||||
$checked_paths->{$path} = 1;
|
||||
|
||||
my $path_perm = $self->permissions($authuser, $path);
|
||||
my $path_perm = $self->permissions($authuser, $path);
|
||||
|
||||
my $toplevel = ($path =~ /^\/(\w+)/) ? $1 : 'dc';
|
||||
if ($toplevel eq 'pool') {
|
||||
foreach my $priv (keys %$path_perm) {
|
||||
next if !defined($path_perm->{$priv});
|
||||
my $toplevel = ($path =~ /^\/(\w+)/) ? $1 : 'dc';
|
||||
if ($toplevel eq 'pool') {
|
||||
foreach my $priv (keys %$path_perm) {
|
||||
next if !defined($path_perm->{$priv});
|
||||
|
||||
if ($priv =~ m/^VM\./) {
|
||||
$res->{vms}->{$priv} = 1;
|
||||
} elsif ($priv =~ m/^Datastore\./) {
|
||||
$res->{storage}->{$priv} = 1;
|
||||
} elsif ($priv eq 'Permissions.Modify') {
|
||||
$res->{storage}->{$priv} = 1;
|
||||
$res->{vms}->{$priv} = 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
my $priv_regex = $priv_re_map->{$toplevel} // next;
|
||||
foreach my $priv (keys %$path_perm) {
|
||||
next if !defined($path_perm->{$priv});
|
||||
if ($priv =~ m/^VM\./) {
|
||||
$res->{vms}->{$priv} = 1;
|
||||
} elsif ($priv =~ m/^Datastore\./) {
|
||||
$res->{storage}->{$priv} = 1;
|
||||
} elsif ($priv eq 'Permissions.Modify') {
|
||||
$res->{storage}->{$priv} = 1;
|
||||
$res->{vms}->{$priv} = 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
my $priv_regex = $priv_re_map->{$toplevel} // next;
|
||||
foreach my $priv (keys %$path_perm) {
|
||||
next if !defined($path_perm->{$priv});
|
||||
|
||||
next if $priv !~ m/^($priv_regex)/;
|
||||
$res->{$toplevel}->{$priv} = 1;
|
||||
}
|
||||
}
|
||||
next if $priv !~ m/^($priv_regex)/;
|
||||
$res->{$toplevel}->{$priv} = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $res;
|
||||
@ -238,43 +243,47 @@ sub get_effective_permissions {
|
||||
|
||||
# default / top level paths
|
||||
my $paths = {
|
||||
'/' => 1,
|
||||
'/access' => 1,
|
||||
'/access/groups' => 1,
|
||||
'/nodes' => 1,
|
||||
'/pool' => 1,
|
||||
'/sdn' => 1,
|
||||
'/storage' => 1,
|
||||
'/vms' => 1,
|
||||
'/' => 1,
|
||||
'/access' => 1,
|
||||
'/access/groups' => 1,
|
||||
'/nodes' => 1,
|
||||
'/pool' => 1,
|
||||
'/sdn' => 1,
|
||||
'/storage' => 1,
|
||||
'/vms' => 1,
|
||||
};
|
||||
|
||||
my $cfg = $self->{user_cfg};
|
||||
|
||||
# paths explicitly listed in ACLs
|
||||
PVE::AccessControl::iterate_acl_tree("/", $cfg->{acl_root}, sub {
|
||||
my ($path, $node) = @_;
|
||||
$paths->{$path} = 1;
|
||||
});
|
||||
PVE::AccessControl::iterate_acl_tree(
|
||||
"/",
|
||||
$cfg->{acl_root},
|
||||
sub {
|
||||
my ($path, $node) = @_;
|
||||
$paths->{$path} = 1;
|
||||
},
|
||||
);
|
||||
|
||||
# paths referenced by pool definitions
|
||||
foreach my $pool (keys %{$cfg->{pools}}) {
|
||||
my $d = $cfg->{pools}->{$pool};
|
||||
foreach my $vmid (keys %{$d->{vms}}) {
|
||||
$paths->{"/vms/$vmid"} = 1;
|
||||
}
|
||||
foreach my $storeid (keys %{$d->{storage}}) {
|
||||
$paths->{"/storage/$storeid"} = 1;
|
||||
}
|
||||
foreach my $pool (keys %{ $cfg->{pools} }) {
|
||||
my $d = $cfg->{pools}->{$pool};
|
||||
foreach my $vmid (keys %{ $d->{vms} }) {
|
||||
$paths->{"/vms/$vmid"} = 1;
|
||||
}
|
||||
foreach my $storeid (keys %{ $d->{storage} }) {
|
||||
$paths->{"/storage/$storeid"} = 1;
|
||||
}
|
||||
}
|
||||
|
||||
my $perms = {};
|
||||
foreach my $path (keys %$paths) {
|
||||
my $path_perms = $self->permissions($user, $path);
|
||||
foreach my $priv (keys %$path_perms) {
|
||||
delete $path_perms->{$priv} if !defined($path_perms->{$priv});
|
||||
}
|
||||
# filter paths where user has NO permissions
|
||||
$perms->{$path} = $path_perms if %$path_perms;
|
||||
my $path_perms = $self->permissions($user, $path);
|
||||
foreach my $priv (keys %$path_perms) {
|
||||
delete $path_perms->{$priv} if !defined($path_perms->{$priv});
|
||||
}
|
||||
# filter paths where user has NO permissions
|
||||
$perms->{$path} = $path_perms if %$path_perms;
|
||||
}
|
||||
return $perms;
|
||||
}
|
||||
@ -285,15 +294,15 @@ sub check {
|
||||
my $perm = $self->permissions($user, $path);
|
||||
|
||||
foreach my $priv (@$privs) {
|
||||
PVE::AccessControl::verify_privname($priv);
|
||||
if (!defined($perm->{$priv})) {
|
||||
return undef if $noerr;
|
||||
raise_perm_exc("$path, $priv");
|
||||
}
|
||||
};
|
||||
PVE::AccessControl::verify_privname($priv);
|
||||
if (!defined($perm->{$priv})) {
|
||||
return undef if $noerr;
|
||||
raise_perm_exc("$path, $priv");
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
};
|
||||
}
|
||||
|
||||
sub check_any {
|
||||
my ($self, $user, $path, $privs, $noerr) = @_;
|
||||
@ -302,26 +311,26 @@ sub check_any {
|
||||
|
||||
my $found = 0;
|
||||
foreach my $priv (@$privs) {
|
||||
PVE::AccessControl::verify_privname($priv);
|
||||
if (defined($perm->{$priv})) {
|
||||
$found = 1;
|
||||
last;
|
||||
}
|
||||
};
|
||||
PVE::AccessControl::verify_privname($priv);
|
||||
if (defined($perm->{$priv})) {
|
||||
$found = 1;
|
||||
last;
|
||||
}
|
||||
}
|
||||
|
||||
return 1 if $found;
|
||||
|
||||
return undef if $noerr;
|
||||
|
||||
raise_perm_exc("$path, " . join("|", @$privs));
|
||||
};
|
||||
}
|
||||
|
||||
sub check_full {
|
||||
my ($self, $username, $path, $privs, $any, $noerr) = @_;
|
||||
if ($any) {
|
||||
return $self->check_any($username, $path, $privs, $noerr);
|
||||
return $self->check_any($username, $path, $privs, $noerr);
|
||||
} 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 $bridge_acl = PVE::AccessControl::find_acl_tree_node($cfg->{acl_root}, $path);
|
||||
if ($bridge_acl) {
|
||||
# check access to VLANs
|
||||
my $vlans = $bridge_acl->{children};
|
||||
for my $vlan (keys %$vlans) {
|
||||
my $vlanpath = "$path/$vlan";
|
||||
return 1 if $self->check_any($username, $vlanpath, $privs, 1);
|
||||
}
|
||||
# check access to VLANs
|
||||
my $vlans = $bridge_acl->{children};
|
||||
for my $vlan (keys %$vlans) {
|
||||
my $vlanpath = "$path/$vlan";
|
||||
return 1 if $self->check_any($username, $vlanpath, $privs, 1);
|
||||
}
|
||||
}
|
||||
|
||||
# repeat check, but fatal
|
||||
@ -382,10 +391,10 @@ sub check_vm_perm {
|
||||
my $cfg = $self->{user_cfg};
|
||||
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
sub is_group_member {
|
||||
my ($self, $group, $user) = @_;
|
||||
@ -403,11 +412,11 @@ sub filter_groups {
|
||||
my $cfg = $self->{user_cfg};
|
||||
|
||||
my $groups = {};
|
||||
foreach my $group (keys %{$cfg->{groups}}) {
|
||||
my $path = "/access/groups/$group";
|
||||
if ($self->check_full($user, $path, $privs, $any, 1)) {
|
||||
$groups->{$group} = $cfg->{groups}->{$group};
|
||||
}
|
||||
foreach my $group (keys %{ $cfg->{groups} }) {
|
||||
my $path = "/access/groups/$group";
|
||||
if ($self->check_full($user, $path, $privs, $any, 1)) {
|
||||
$groups->{$group} = $cfg->{groups}->{$group};
|
||||
}
|
||||
}
|
||||
|
||||
return $groups;
|
||||
@ -420,11 +429,11 @@ sub group_member_join {
|
||||
|
||||
my $cfg = $self->{user_cfg};
|
||||
foreach my $group (@$grouplist) {
|
||||
my $data = $cfg->{groups}->{$group};
|
||||
next if !$data;
|
||||
foreach my $user (keys %{$data->{users}}) {
|
||||
$users->{$user} = 1;
|
||||
}
|
||||
my $data = $cfg->{groups}->{$group};
|
||||
next if !$data;
|
||||
foreach my $user (keys %{ $data->{users} }) {
|
||||
$users->{$user} = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return $users;
|
||||
@ -433,15 +442,15 @@ sub group_member_join {
|
||||
sub check_perm_modify {
|
||||
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/.+$|) {
|
||||
push @$testperms, 'Datastore.Allocate';
|
||||
push @$testperms, 'Datastore.Allocate';
|
||||
} elsif ($path =~ m|^/vms/.+$|) {
|
||||
push @$testperms, 'VM.Allocate';
|
||||
push @$testperms, 'VM.Allocate';
|
||||
} elsif ($path =~ m|^/pool/.+$|) {
|
||||
push @$testperms, 'Pool.Allocate';
|
||||
push @$testperms, 'Pool.Allocate';
|
||||
}
|
||||
|
||||
return $self->check_any($username, $path, $testperms, $noerr);
|
||||
@ -457,85 +466,85 @@ sub exec_api2_perm_check {
|
||||
die "no permission test specified" if !$test;
|
||||
|
||||
if ($test eq 'and') {
|
||||
while (my $subcheck = $check->[$ind++]) {
|
||||
$self->exec_api2_perm_check($subcheck, $username, $param);
|
||||
}
|
||||
return 1;
|
||||
while (my $subcheck = $check->[$ind++]) {
|
||||
$self->exec_api2_perm_check($subcheck, $username, $param);
|
||||
}
|
||||
return 1;
|
||||
} elsif ($test eq 'or') {
|
||||
while (my $subcheck = $check->[$ind++]) {
|
||||
return 1 if $self->exec_api2_perm_check($subcheck, $username, $param, 1);
|
||||
}
|
||||
return 0 if $noerr;
|
||||
raise_perm_exc();
|
||||
while (my $subcheck = $check->[$ind++]) {
|
||||
return 1 if $self->exec_api2_perm_check($subcheck, $username, $param, 1);
|
||||
}
|
||||
return 0 if $noerr;
|
||||
raise_perm_exc();
|
||||
} elsif ($test eq 'perm') {
|
||||
my ($t, $tmplpath, $privs, %options) = @$check;
|
||||
my $any = $options{any};
|
||||
die "missing parameters" if !($tmplpath && $privs);
|
||||
my $require_param = $options{require_param};
|
||||
if ($require_param && !defined($param->{$require_param})) {
|
||||
return 0 if $noerr;
|
||||
raise_perm_exc();
|
||||
}
|
||||
my $path = PVE::Tools::template_replace($tmplpath, $param);
|
||||
my $normpath = PVE::AccessControl::normalize_path($path);
|
||||
warn "Failed to normalize '$path'\n" if !defined($normpath) && defined($path);
|
||||
my ($t, $tmplpath, $privs, %options) = @$check;
|
||||
my $any = $options{any};
|
||||
die "missing parameters" if !($tmplpath && $privs);
|
||||
my $require_param = $options{require_param};
|
||||
if ($require_param && !defined($param->{$require_param})) {
|
||||
return 0 if $noerr;
|
||||
raise_perm_exc();
|
||||
}
|
||||
my $path = PVE::Tools::template_replace($tmplpath, $param);
|
||||
my $normpath = PVE::AccessControl::normalize_path($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') {
|
||||
my $userid = $param->{userid};
|
||||
my ($t, $privs, %options) = @$check;
|
||||
my $userid = $param->{userid};
|
||||
my ($t, $privs, %options) = @$check;
|
||||
|
||||
my $check_existing_user = !$options{groups_param} || $options{groups_param} ne 'create';
|
||||
return 0 if $check_existing_user && !$self->check_user_exist($userid, $noerr);
|
||||
my $check_existing_user = !$options{groups_param} || $options{groups_param} ne 'create';
|
||||
return 0 if $check_existing_user && !$self->check_user_exist($userid, $noerr);
|
||||
|
||||
# check permission for ALL groups (and thus ALL users)
|
||||
if (!$self->check_any($username, "/access/groups", $privs, 1)) {
|
||||
# list of groups $username has any of $privs on
|
||||
my $groups = $self->filter_groups($username, $privs, 1);
|
||||
if ($options{groups_param}) {
|
||||
# does $username have any of $privs on all new/updated/.. groups?
|
||||
my @group_param = PVE::Tools::split_list($param->{groups});
|
||||
raise_perm_exc("/access/groups, " . join("|", @$privs)) if !scalar(@group_param);
|
||||
foreach my $pg (@group_param) {
|
||||
raise_perm_exc("/access/groups/$pg, " . join("|", @$privs))
|
||||
if !$groups->{$pg};
|
||||
}
|
||||
}
|
||||
if ($check_existing_user) {
|
||||
# does $username have any of $privs on any existing group of $userid
|
||||
my $allowed_users = $self->group_member_join([keys %$groups]);
|
||||
if (!$allowed_users->{$userid}) {
|
||||
return 0 if $noerr;
|
||||
raise_perm_exc();
|
||||
}
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
# check permission for ALL groups (and thus ALL users)
|
||||
if (!$self->check_any($username, "/access/groups", $privs, 1)) {
|
||||
# list of groups $username has any of $privs on
|
||||
my $groups = $self->filter_groups($username, $privs, 1);
|
||||
if ($options{groups_param}) {
|
||||
# does $username have any of $privs on all new/updated/.. groups?
|
||||
my @group_param = PVE::Tools::split_list($param->{groups});
|
||||
raise_perm_exc("/access/groups, " . join("|", @$privs)) if !scalar(@group_param);
|
||||
foreach my $pg (@group_param) {
|
||||
raise_perm_exc("/access/groups/$pg, " . join("|", @$privs))
|
||||
if !$groups->{$pg};
|
||||
}
|
||||
}
|
||||
if ($check_existing_user) {
|
||||
# does $username have any of $privs on any existing group of $userid
|
||||
my $allowed_users = $self->group_member_join([keys %$groups]);
|
||||
if (!$allowed_users->{$userid}) {
|
||||
return 0 if $noerr;
|
||||
raise_perm_exc();
|
||||
}
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
} elsif ($test eq 'userid-param') {
|
||||
my ($userid, undef, $realm) = PVE::AccessControl::verify_username($param->{userid});
|
||||
my ($t, $subtest) = @$check;
|
||||
die "missing parameters" if !$subtest;
|
||||
if ($subtest eq 'self') {
|
||||
return 0 if !$self->check_user_exist($userid, $noerr);
|
||||
return 1 if $username eq $userid;
|
||||
return 0 if $noerr;
|
||||
raise_perm_exc();
|
||||
} elsif ($subtest eq 'Realm.AllocateUser') {
|
||||
my $path = "/access/realm/$realm";
|
||||
return $self->check($username, $path, ['Realm.AllocateUser'], $noerr);
|
||||
} else {
|
||||
die "unknown userid-param test";
|
||||
}
|
||||
my ($userid, undef, $realm) = PVE::AccessControl::verify_username($param->{userid});
|
||||
my ($t, $subtest) = @$check;
|
||||
die "missing parameters" if !$subtest;
|
||||
if ($subtest eq 'self') {
|
||||
return 0 if !$self->check_user_exist($userid, $noerr);
|
||||
return 1 if $username eq $userid;
|
||||
return 0 if $noerr;
|
||||
raise_perm_exc();
|
||||
} elsif ($subtest eq 'Realm.AllocateUser') {
|
||||
my $path = "/access/realm/$realm";
|
||||
return $self->check($username, $path, ['Realm.AllocateUser'], $noerr);
|
||||
} else {
|
||||
die "unknown userid-param test";
|
||||
}
|
||||
} elsif ($test eq 'perm-modify') {
|
||||
my ($t, $tmplpath) = @$check;
|
||||
my $path = PVE::Tools::template_replace($tmplpath, $param);
|
||||
$path = PVE::AccessControl::normalize_path($path);
|
||||
return 0 if !defined($path); # should already die in API2::ACL
|
||||
return $self->check_perm_modify($username, $path, $noerr);
|
||||
my ($t, $tmplpath) = @$check;
|
||||
my $path = PVE::Tools::template_replace($tmplpath, $param);
|
||||
$path = PVE::AccessControl::normalize_path($path);
|
||||
return 0 if !defined($path); # should already die in API2::ACL
|
||||
return $self->check_perm_modify($username, $path, $noerr);
|
||||
} else {
|
||||
die "unknown permission test";
|
||||
die "unknown permission test";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
sub check_api2_permissions {
|
||||
my ($self, $perm, $username, $param) = @_;
|
||||
@ -551,7 +560,7 @@ sub check_api2_permissions {
|
||||
return 1 if $perm->{user} && $perm->{user} eq 'all';
|
||||
|
||||
return $self->exec_api2_perm_check($perm->{check}, $username, $param)
|
||||
if $perm->{check};
|
||||
if $perm->{check};
|
||||
|
||||
raise_perm_exc();
|
||||
}
|
||||
@ -581,8 +590,7 @@ sub init {
|
||||
$self->{aclversion} = undef;
|
||||
|
||||
return $self;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
# init_request - must be called before each RPC request
|
||||
sub init_request {
|
||||
@ -594,32 +602,36 @@ sub init_request {
|
||||
|
||||
my $userconfig; # we use this for regression tests
|
||||
foreach my $p (keys %params) {
|
||||
if ($p eq 'userconfig') {
|
||||
$userconfig = $params{$p};
|
||||
} else {
|
||||
die "unknown parameter '$p'";
|
||||
}
|
||||
if ($p eq 'userconfig') {
|
||||
$userconfig = $params{$p};
|
||||
} else {
|
||||
die "unknown parameter '$p'";
|
||||
}
|
||||
}
|
||||
|
||||
eval {
|
||||
$self->{aclcache} = {};
|
||||
if ($userconfig) {
|
||||
my $ucdata = PVE::Tools::file_get_contents($userconfig);
|
||||
my $cfg = PVE::AccessControl::parse_user_config($userconfig, $ucdata);
|
||||
$self->{user_cfg} = $cfg;
|
||||
} else {
|
||||
my $ucvers = PVE::Cluster::cfs_file_version('user.cfg');
|
||||
if (!$self->{aclcache} || !defined($self->{aclversion}) ||
|
||||
!defined($ucvers) || ($ucvers ne $self->{aclversion})) {
|
||||
$self->{aclversion} = $ucvers;
|
||||
my $cfg = PVE::Cluster::cfs_read_file('user.cfg');
|
||||
$self->{user_cfg} = $cfg;
|
||||
}
|
||||
}
|
||||
$self->{aclcache} = {};
|
||||
if ($userconfig) {
|
||||
my $ucdata = PVE::Tools::file_get_contents($userconfig);
|
||||
my $cfg = PVE::AccessControl::parse_user_config($userconfig, $ucdata);
|
||||
$self->{user_cfg} = $cfg;
|
||||
} else {
|
||||
my $ucvers = PVE::Cluster::cfs_file_version('user.cfg');
|
||||
if (
|
||||
!$self->{aclcache}
|
||||
|| !defined($self->{aclversion})
|
||||
|| !defined($ucvers)
|
||||
|| ($ucvers ne $self->{aclversion})
|
||||
) {
|
||||
$self->{aclversion} = $ucvers;
|
||||
my $cfg = PVE::Cluster::cfs_read_file('user.cfg');
|
||||
$self->{user_cfg} = $cfg;
|
||||
}
|
||||
}
|
||||
};
|
||||
if (my $err = $@) {
|
||||
$self->{user_cfg} = {};
|
||||
die "Unable to load access control list: $err";
|
||||
$self->{user_cfg} = {};
|
||||
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.
|
||||
if ($authuser ne 'root@pam') {
|
||||
raise_param_exc({ $param_name => 'password is required to modify user' })
|
||||
if !defined($password);
|
||||
raise_param_exc({ $param_name => 'password is required to modify user' })
|
||||
if !defined($password);
|
||||
|
||||
($authuser, my $auth_username, my $auth_realm) =
|
||||
PVE::AccessControl::verify_username($authuser);
|
||||
($authuser, my $auth_username, my $auth_realm) =
|
||||
PVE::AccessControl::verify_username($authuser);
|
||||
|
||||
my $domain_cfg = PVE::Cluster::cfs_read_file('domains.cfg');
|
||||
my $cfg = $domain_cfg->{ids}->{$auth_realm};
|
||||
die "auth domain '$auth_realm' does not exist\n" if !$cfg;
|
||||
my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
|
||||
$plugin->authenticate_user($cfg, $auth_realm, $auth_username, $password);
|
||||
my $domain_cfg = PVE::Cluster::cfs_read_file('domains.cfg');
|
||||
my $cfg = $domain_cfg->{ids}->{$auth_realm};
|
||||
die "auth domain '$auth_realm' does not exist\n" if !$cfg;
|
||||
my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
|
||||
$plugin->authenticate_user($cfg, $auth_realm, $auth_username, $password);
|
||||
}
|
||||
|
||||
return wantarray ? ($userid, $ruid, $realm) : $userid;
|
||||
|
@ -16,16 +16,16 @@ my $parse_token_cfg = sub {
|
||||
|
||||
my @lines = split(/\n/, $raw);
|
||||
foreach my $line (@lines) {
|
||||
next if $line =~ m/^\s*$/;
|
||||
next if $line =~ m/^\s*$/;
|
||||
|
||||
if ($line =~ m/^(\S+) (\S+)$/) {
|
||||
if (PVE::AccessControl::pve_verify_tokenid($1, 1)) {
|
||||
$parsed->{$1} = $2;
|
||||
next;
|
||||
}
|
||||
}
|
||||
if ($line =~ m/^(\S+) (\S+)$/) {
|
||||
if (PVE::AccessControl::pve_verify_tokenid($1, 1)) {
|
||||
$parsed->{$1} = $2;
|
||||
next;
|
||||
}
|
||||
}
|
||||
|
||||
warn "skipping invalid token.cfg entry\n";
|
||||
warn "skipping invalid token.cfg entry\n";
|
||||
}
|
||||
|
||||
return $parsed;
|
||||
@ -36,7 +36,7 @@ my $write_token_cfg = sub {
|
||||
|
||||
my $raw = '';
|
||||
foreach my $tokenid (sort keys %$data) {
|
||||
$raw .= "$tokenid $data->{$tokenid}\n";
|
||||
$raw .= "$tokenid $data->{$tokenid}\n";
|
||||
}
|
||||
|
||||
return $raw;
|
||||
@ -49,16 +49,20 @@ sub generate_token {
|
||||
|
||||
PVE::AccessControl::pve_verify_tokenid($tokenid);
|
||||
|
||||
my $token_value = PVE::Cluster::cfs_lock_file('priv/token.cfg', 10, sub {
|
||||
my $uuid = UUID::uuid();
|
||||
my $token_cfg = PVE::Cluster::cfs_read_file('priv/token.cfg');
|
||||
my $token_value = PVE::Cluster::cfs_lock_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($@);
|
||||
|
||||
@ -68,13 +72,17 @@ sub generate_token {
|
||||
sub delete_token {
|
||||
my ($tokenid) = @_;
|
||||
|
||||
PVE::Cluster::cfs_lock_file('priv/token.cfg', 10, sub {
|
||||
my $token_cfg = PVE::Cluster::cfs_read_file('priv/token.cfg');
|
||||
PVE::Cluster::cfs_lock_file(
|
||||
'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($@);
|
||||
}
|
||||
|
@ -30,134 +30,138 @@ my $stranger_privsep_perms = $rpcenv->get_effective_permissions('stranger@pve!pr
|
||||
|
||||
my $stranger_user_tests = [
|
||||
{
|
||||
description => 'get stranger\'s perms without passing the user\'s userid',
|
||||
rpcuser => 'stranger@pve',
|
||||
params => {},
|
||||
result => $stranger_perms,
|
||||
description => 'get stranger\'s perms without passing the user\'s userid',
|
||||
rpcuser => 'stranger@pve',
|
||||
params => {},
|
||||
result => $stranger_perms,
|
||||
},
|
||||
{
|
||||
description => 'get stranger\'s perms with passing the user\'s userid',
|
||||
rpcuser => 'stranger@pve',
|
||||
params => {
|
||||
userid => 'stranger@pve',
|
||||
},
|
||||
result => $stranger_perms,
|
||||
description => 'get stranger\'s perms with passing the user\'s userid',
|
||||
rpcuser => 'stranger@pve',
|
||||
params => {
|
||||
userid => 'stranger@pve',
|
||||
},
|
||||
result => $stranger_perms,
|
||||
},
|
||||
{
|
||||
description => 'get stranger-owned non-priv-sep\'d token\'s perms from stranger user',
|
||||
rpcuser => 'stranger@pve',
|
||||
params => {
|
||||
userid => 'stranger@pve!noprivsep',
|
||||
},
|
||||
result => $stranger_perms,
|
||||
description => 'get stranger-owned non-priv-sep\'d token\'s perms from stranger user',
|
||||
rpcuser => 'stranger@pve',
|
||||
params => {
|
||||
userid => 'stranger@pve!noprivsep',
|
||||
},
|
||||
result => $stranger_perms,
|
||||
},
|
||||
{
|
||||
description => 'get stranger-owned priv-sep\'d token\'s perms from stranger user',
|
||||
rpcuser => 'stranger@pve',
|
||||
params => {
|
||||
userid => 'stranger@pve!privsep',
|
||||
},
|
||||
result => $stranger_privsep_perms,
|
||||
description => 'get stranger-owned priv-sep\'d token\'s perms from stranger user',
|
||||
rpcuser => 'stranger@pve',
|
||||
params => {
|
||||
userid => 'stranger@pve!privsep',
|
||||
},
|
||||
result => $stranger_privsep_perms,
|
||||
},
|
||||
{
|
||||
description => 'get auditor\'s perms from stranger user',
|
||||
should_fail => 1,
|
||||
rpcuser => 'stranger@pve',
|
||||
params => {
|
||||
userid => 'auditor@pam',
|
||||
},
|
||||
description => 'get auditor\'s perms from stranger user',
|
||||
should_fail => 1,
|
||||
rpcuser => 'stranger@pve',
|
||||
params => {
|
||||
userid => 'auditor@pam',
|
||||
},
|
||||
},
|
||||
{
|
||||
description => 'get auditor-owned token\'s perms from stranger user',
|
||||
should_fail => 1,
|
||||
rpcuser => 'stranger@pve',
|
||||
params => {
|
||||
userid => 'auditor@pam!noprivsep',
|
||||
},
|
||||
description => 'get auditor-owned token\'s perms from stranger user',
|
||||
should_fail => 1,
|
||||
rpcuser => 'stranger@pve',
|
||||
params => {
|
||||
userid => 'auditor@pam!noprivsep',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
my $stranger_nonprivsep_tests = [
|
||||
{
|
||||
description => 'get stranger-owned non-priv-sep\'d token\'s perms without passing the token',
|
||||
rpcuser => 'stranger@pve!noprivsep',
|
||||
params => {},
|
||||
result => $stranger_perms,
|
||||
description =>
|
||||
'get stranger-owned non-priv-sep\'d token\'s perms without passing the token',
|
||||
rpcuser => 'stranger@pve!noprivsep',
|
||||
params => {},
|
||||
result => $stranger_perms,
|
||||
},
|
||||
{
|
||||
description => 'get stranger-owned non-priv-sep\'d token\'s perms with passing the token',
|
||||
rpcuser => 'stranger@pve!noprivsep',
|
||||
params => {
|
||||
userid => 'stranger@pve!noprivsep',
|
||||
},
|
||||
result => $stranger_perms,
|
||||
description =>
|
||||
'get stranger-owned non-priv-sep\'d token\'s perms with passing the token',
|
||||
rpcuser => 'stranger@pve!noprivsep',
|
||||
params => {
|
||||
userid => 'stranger@pve!noprivsep',
|
||||
},
|
||||
result => $stranger_perms,
|
||||
},
|
||||
{
|
||||
description => 'get stranger\'s perms from stranger-owned non-priv-sep\'d token',
|
||||
should_fail => 1,
|
||||
rpcuser => 'stranger@pve!noprivsep',
|
||||
params => {
|
||||
userid => 'stranger@pve',
|
||||
},
|
||||
description => 'get stranger\'s perms from stranger-owned non-priv-sep\'d token',
|
||||
should_fail => 1,
|
||||
rpcuser => 'stranger@pve!noprivsep',
|
||||
params => {
|
||||
userid => 'stranger@pve',
|
||||
},
|
||||
},
|
||||
{
|
||||
description => 'get stranger-owned priv-sep\'d token\'s perms '
|
||||
. 'from stranger-owned non-priv-sep\'d token',
|
||||
should_fail => 1,
|
||||
rpcuser => 'stranger@pve!noprivsep',
|
||||
params => {
|
||||
userid => 'stranger@pve!privsep',
|
||||
},
|
||||
description => 'get stranger-owned priv-sep\'d token\'s perms '
|
||||
. 'from stranger-owned non-priv-sep\'d token',
|
||||
should_fail => 1,
|
||||
rpcuser => 'stranger@pve!noprivsep',
|
||||
params => {
|
||||
userid => 'stranger@pve!privsep',
|
||||
},
|
||||
},
|
||||
{
|
||||
description => 'get auditor-owned token\'s perms from stranger-owned non-priv-sep\'d token',
|
||||
should_fail => 1,
|
||||
rpcuser => 'stranger@pve!noprivsep',
|
||||
params => {
|
||||
userid => 'auditor@pam!noprivsep',
|
||||
}
|
||||
description =>
|
||||
'get auditor-owned token\'s perms from stranger-owned non-priv-sep\'d token',
|
||||
should_fail => 1,
|
||||
rpcuser => 'stranger@pve!noprivsep',
|
||||
params => {
|
||||
userid => 'auditor@pam!noprivsep',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
my $stranger_privsep_tests = [
|
||||
{
|
||||
description => 'get stranger-owned priv-sep\'d token\'s perms without passing the token',
|
||||
rpcuser => 'stranger@pve!privsep',
|
||||
params => {},
|
||||
result => $stranger_privsep_perms,
|
||||
description =>
|
||||
'get stranger-owned priv-sep\'d token\'s perms without passing the token',
|
||||
rpcuser => 'stranger@pve!privsep',
|
||||
params => {},
|
||||
result => $stranger_privsep_perms,
|
||||
},
|
||||
{
|
||||
description => 'get stranger-owned priv-sep\'d token\'s perms with passing the token',
|
||||
rpcuser => 'stranger@pve!privsep',
|
||||
params => {
|
||||
userid => 'stranger@pve!privsep',
|
||||
},
|
||||
result => $stranger_privsep_perms,
|
||||
description => 'get stranger-owned priv-sep\'d token\'s perms with passing the token',
|
||||
rpcuser => 'stranger@pve!privsep',
|
||||
params => {
|
||||
userid => 'stranger@pve!privsep',
|
||||
},
|
||||
result => $stranger_privsep_perms,
|
||||
},
|
||||
{
|
||||
description => 'get stranger\'s perms from stranger-owned priv-sep\'d token',
|
||||
should_fail => 1,
|
||||
rpcuser => 'stranger@pve!privsep',
|
||||
params => {
|
||||
userid => 'stranger@pve',
|
||||
},
|
||||
description => 'get stranger\'s perms from stranger-owned priv-sep\'d token',
|
||||
should_fail => 1,
|
||||
rpcuser => 'stranger@pve!privsep',
|
||||
params => {
|
||||
userid => 'stranger@pve',
|
||||
},
|
||||
},
|
||||
{
|
||||
description => 'get stranger-owned non-priv-sep\'d token\'s perms '
|
||||
. 'from stranger-owned priv-sep\'d token',
|
||||
should_fail => 1,
|
||||
rpcuser => 'stranger@pve!privsep',
|
||||
params => {
|
||||
userid => 'stranger@pve!noprivsep',
|
||||
},
|
||||
description => 'get stranger-owned non-priv-sep\'d token\'s perms '
|
||||
. 'from stranger-owned priv-sep\'d token',
|
||||
should_fail => 1,
|
||||
rpcuser => 'stranger@pve!privsep',
|
||||
params => {
|
||||
userid => 'stranger@pve!noprivsep',
|
||||
},
|
||||
},
|
||||
{
|
||||
description => 'get auditor-owned token\'s perms from stranger-owned priv-sep\'d token',
|
||||
should_fail => 1,
|
||||
rpcuser => 'stranger@pve!privsep',
|
||||
params => {
|
||||
userid => 'auditor@pam!noprivsep',
|
||||
}
|
||||
description => 'get auditor-owned token\'s perms from stranger-owned priv-sep\'d token',
|
||||
should_fail => 1,
|
||||
rpcuser => 'stranger@pve!privsep',
|
||||
params => {
|
||||
userid => 'auditor@pam!noprivsep',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@ -167,134 +171,137 @@ my $auditor_privsep_perms = $rpcenv->get_effective_permissions('auditor@pam!priv
|
||||
|
||||
my $auditor_user_tests = [
|
||||
{
|
||||
description => 'get auditor\'s perms without passing the user\'s userid',
|
||||
rpcuser => 'auditor@pam',
|
||||
params => {},
|
||||
result => $auditor_perms,
|
||||
description => 'get auditor\'s perms without passing the user\'s userid',
|
||||
rpcuser => 'auditor@pam',
|
||||
params => {},
|
||||
result => $auditor_perms,
|
||||
},
|
||||
{
|
||||
description => 'get auditor\'s perms with passing the user\'s userid',
|
||||
rpcuser => 'auditor@pam',
|
||||
params => {
|
||||
userid => 'auditor@pam',
|
||||
},
|
||||
result => $auditor_perms,
|
||||
description => 'get auditor\'s perms with passing the user\'s userid',
|
||||
rpcuser => 'auditor@pam',
|
||||
params => {
|
||||
userid => 'auditor@pam',
|
||||
},
|
||||
result => $auditor_perms,
|
||||
},
|
||||
{
|
||||
description => 'get auditor-owned non-priv-sep\'d token\'s perms from auditor user',
|
||||
rpcuser => 'auditor@pam',
|
||||
params => {
|
||||
userid => 'auditor@pam!noprivsep',
|
||||
},
|
||||
result => $auditor_perms,
|
||||
description => 'get auditor-owned non-priv-sep\'d token\'s perms from auditor user',
|
||||
rpcuser => 'auditor@pam',
|
||||
params => {
|
||||
userid => 'auditor@pam!noprivsep',
|
||||
},
|
||||
result => $auditor_perms,
|
||||
},
|
||||
{
|
||||
description => 'get auditor-owned priv-sep\'d token\'s perms from auditor user',
|
||||
rpcuser => 'auditor@pam',
|
||||
params => {
|
||||
userid => 'auditor@pam!privsep',
|
||||
},
|
||||
result => $auditor_privsep_perms,
|
||||
description => 'get auditor-owned priv-sep\'d token\'s perms from auditor user',
|
||||
rpcuser => 'auditor@pam',
|
||||
params => {
|
||||
userid => 'auditor@pam!privsep',
|
||||
},
|
||||
result => $auditor_privsep_perms,
|
||||
},
|
||||
{
|
||||
description => 'get stranger\'s perms from auditor user',
|
||||
rpcuser => 'auditor@pam',
|
||||
params => {
|
||||
userid => 'stranger@pve',
|
||||
},
|
||||
result => $stranger_perms,
|
||||
description => 'get stranger\'s perms from auditor user',
|
||||
rpcuser => 'auditor@pam',
|
||||
params => {
|
||||
userid => 'stranger@pve',
|
||||
},
|
||||
result => $stranger_perms,
|
||||
},
|
||||
{
|
||||
description => 'get stranger-owned token\'s perms from auditor user',
|
||||
rpcuser => 'auditor@pam',
|
||||
params => {
|
||||
userid => 'stranger@pve!noprivsep',
|
||||
},
|
||||
result => $stranger_perms,
|
||||
description => 'get stranger-owned token\'s perms from auditor user',
|
||||
rpcuser => 'auditor@pam',
|
||||
params => {
|
||||
userid => 'stranger@pve!noprivsep',
|
||||
},
|
||||
result => $stranger_perms,
|
||||
},
|
||||
];
|
||||
|
||||
my $auditor_nonprivsep_tests = [
|
||||
{
|
||||
description => 'get auditor-owned non-priv-sep\'d token\'s perms without passing the token',
|
||||
rpcuser => 'auditor@pam!noprivsep',
|
||||
params => {},
|
||||
result => $auditor_perms,
|
||||
description =>
|
||||
'get auditor-owned non-priv-sep\'d token\'s perms without passing the token',
|
||||
rpcuser => 'auditor@pam!noprivsep',
|
||||
params => {},
|
||||
result => $auditor_perms,
|
||||
},
|
||||
{
|
||||
description => 'get auditor-owned non-priv-sep\'d token\'s perms with passing the token',
|
||||
rpcuser => 'auditor@pam!noprivsep',
|
||||
params => {
|
||||
userid => 'auditor@pam!noprivsep',
|
||||
},
|
||||
result => $auditor_perms,
|
||||
description =>
|
||||
'get auditor-owned non-priv-sep\'d token\'s perms with passing the token',
|
||||
rpcuser => 'auditor@pam!noprivsep',
|
||||
params => {
|
||||
userid => 'auditor@pam!noprivsep',
|
||||
},
|
||||
result => $auditor_perms,
|
||||
},
|
||||
{
|
||||
description => 'get auditor\'s perms from auditor-owned non-priv-sep\'d token',
|
||||
rpcuser => 'auditor@pam!noprivsep',
|
||||
params => {
|
||||
userid => 'auditor@pam',
|
||||
},
|
||||
result => $auditor_perms,
|
||||
description => 'get auditor\'s perms from auditor-owned non-priv-sep\'d token',
|
||||
rpcuser => 'auditor@pam!noprivsep',
|
||||
params => {
|
||||
userid => 'auditor@pam',
|
||||
},
|
||||
result => $auditor_perms,
|
||||
},
|
||||
{
|
||||
description => 'get auditor-owned priv-sep\'d token\'s perms '
|
||||
. 'from auditor-owned non-priv-sep\'d token',
|
||||
rpcuser => 'auditor@pam!noprivsep',
|
||||
params => {
|
||||
userid => 'auditor@pam!privsep',
|
||||
},
|
||||
result => $auditor_privsep_perms,
|
||||
description => 'get auditor-owned priv-sep\'d token\'s perms '
|
||||
. 'from auditor-owned non-priv-sep\'d token',
|
||||
rpcuser => 'auditor@pam!noprivsep',
|
||||
params => {
|
||||
userid => 'auditor@pam!privsep',
|
||||
},
|
||||
result => $auditor_privsep_perms,
|
||||
},
|
||||
{
|
||||
description => 'get stranger-owned token\'s perms from auditor-owned non-priv-sep\'d token',
|
||||
rpcuser => 'auditor@pam!noprivsep',
|
||||
params => {
|
||||
userid => 'stranger@pve!noprivsep',
|
||||
},
|
||||
result => $stranger_perms,
|
||||
description =>
|
||||
'get stranger-owned token\'s perms from auditor-owned non-priv-sep\'d token',
|
||||
rpcuser => 'auditor@pam!noprivsep',
|
||||
params => {
|
||||
userid => 'stranger@pve!noprivsep',
|
||||
},
|
||||
result => $stranger_perms,
|
||||
},
|
||||
];
|
||||
|
||||
my $auditor_privsep_tests = [
|
||||
{
|
||||
description => 'get auditor-owned priv-sep\'d token\'s perms without passing the token',
|
||||
rpcuser => 'auditor@pam!privsep',
|
||||
params => {},
|
||||
result => $auditor_privsep_perms,
|
||||
description => 'get auditor-owned priv-sep\'d token\'s perms without passing the token',
|
||||
rpcuser => 'auditor@pam!privsep',
|
||||
params => {},
|
||||
result => $auditor_privsep_perms,
|
||||
},
|
||||
{
|
||||
description => 'get auditor-owned priv-sep\'d token\'s perms with passing the token',
|
||||
rpcuser => 'auditor@pam!privsep',
|
||||
params => {
|
||||
userid => 'auditor@pam!privsep',
|
||||
},
|
||||
result => $auditor_privsep_perms,
|
||||
description => 'get auditor-owned priv-sep\'d token\'s perms with passing the token',
|
||||
rpcuser => 'auditor@pam!privsep',
|
||||
params => {
|
||||
userid => 'auditor@pam!privsep',
|
||||
},
|
||||
result => $auditor_privsep_perms,
|
||||
},
|
||||
{
|
||||
description => 'get auditor\'s perms from auditor-owned priv-sep\'d token',
|
||||
should_fail => 1,
|
||||
rpcuser => 'auditor@pam!privsep',
|
||||
params => {
|
||||
userid => 'auditor@pam',
|
||||
},
|
||||
description => 'get auditor\'s perms from auditor-owned priv-sep\'d token',
|
||||
should_fail => 1,
|
||||
rpcuser => 'auditor@pam!privsep',
|
||||
params => {
|
||||
userid => 'auditor@pam',
|
||||
},
|
||||
},
|
||||
{
|
||||
description => 'get auditor-owned non-priv-sep\'d token\'s perms '
|
||||
. 'from auditor-owned priv-sep\'d token',
|
||||
should_fail => 1,
|
||||
rpcuser => 'auditor@pam!privsep',
|
||||
params => {
|
||||
userid => 'auditor@pam!noprivsep',
|
||||
},
|
||||
description => 'get auditor-owned non-priv-sep\'d token\'s perms '
|
||||
. 'from auditor-owned priv-sep\'d token',
|
||||
should_fail => 1,
|
||||
rpcuser => 'auditor@pam!privsep',
|
||||
params => {
|
||||
userid => 'auditor@pam!noprivsep',
|
||||
},
|
||||
},
|
||||
{
|
||||
description => 'get stranger-owned token\'s perms from auditor-owned priv-sep\'d token',
|
||||
should_fail => 1,
|
||||
rpcuser => 'auditor@pam!privsep',
|
||||
params => {
|
||||
userid => 'stranger@pve!noprivsep',
|
||||
},
|
||||
description => 'get stranger-owned token\'s perms from auditor-owned priv-sep\'d token',
|
||||
should_fail => 1,
|
||||
rpcuser => 'auditor@pam!privsep',
|
||||
params => {
|
||||
userid => 'stranger@pve!noprivsep',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@ -315,10 +322,10 @@ for my $case ($tests->@*) {
|
||||
my $result = eval { $handler->handle($handler_info, $case->{params}) };
|
||||
|
||||
if ($@) {
|
||||
my $should_fail = exists($case->{should_fail}) ? $case->{should_fail} : 0;
|
||||
is(defined($@), $should_fail, "should fail: $case->{description}") || diag explain $@;
|
||||
my $should_fail = exists($case->{should_fail}) ? $case->{should_fail} : 0;
|
||||
is(defined($@), $should_fail, "should fail: $case->{description}") || diag explain $@;
|
||||
} else {
|
||||
is_deeply($result, $case->{result}, $case->{description});
|
||||
is_deeply($result, $case->{result}, $case->{description});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,8 +11,8 @@ my $username = shift;
|
||||
die "Username missing" if !$username;
|
||||
|
||||
my $password = PVE::PTY::read_password('password: ');
|
||||
PVE::AccessControl::authenticate_user($username,$password);
|
||||
PVE::AccessControl::authenticate_user($username, $password);
|
||||
|
||||
print "Authentication Successful!!\n";
|
||||
|
||||
exit (0);
|
||||
exit(0);
|
||||
|
@ -12,8 +12,8 @@ use PVE::RPCEnvironment;
|
||||
# dump-perm.pl -f myuser.cfg root /
|
||||
|
||||
my $opt_file;
|
||||
if (!GetOptions ("file=s" => \$opt_file)) {
|
||||
exit (-1);
|
||||
if (!GetOptions("file=s" => \$opt_file)) {
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
my $username = shift;
|
||||
@ -21,7 +21,7 @@ my $path = shift;
|
||||
|
||||
if (!($username && $path)) {
|
||||
print "usage: $0 <username> <path>\n";
|
||||
exit (-1);
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
my $cfg;
|
||||
@ -38,4 +38,4 @@ my $perm = $rpcenv->permissions($username, $path);
|
||||
print "permission for user '$username' on '$path':\n";
|
||||
print join(',', keys %$perm) . "\n";
|
||||
|
||||
exit (0);
|
||||
exit(0);
|
||||
|
@ -13,4 +13,4 @@ $cfg = PVE::AccessControl::load_user_config();
|
||||
|
||||
print Dumper($cfg) . "\n";
|
||||
|
||||
exit (0);
|
||||
exit(0);
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -22,7 +22,7 @@ sub check_roles {
|
||||
my $res = join(',', sort keys %$roles);
|
||||
|
||||
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";
|
||||
}
|
||||
@ -34,12 +34,12 @@ sub check_permission {
|
||||
my $res = join(',', sort keys %$perm);
|
||||
|
||||
die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
|
||||
if $res ne $expected_result;
|
||||
if $res ne $expected_result;
|
||||
|
||||
$perm = $rpcenv->permissions($user, $path);
|
||||
$res = join(',', sort keys %$perm);
|
||||
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";
|
||||
}
|
||||
@ -63,27 +63,27 @@ check_permission(
|
||||
'alex@pve',
|
||||
'/vms/300',
|
||||
'' # sorted, comma-separated expected privilege string
|
||||
. '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.Console,VM.Migrate,VM.Monitor,VM.PowerMgmt,VM.Snapshot,VM.Snapshot.Rollback'
|
||||
. '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.Console,VM.Migrate,VM.Monitor,VM.PowerMgmt,VM.Snapshot,VM.Snapshot.Rollback',
|
||||
);
|
||||
# Administrator -> Permissions.Modify!
|
||||
check_permission(
|
||||
'alex@pve',
|
||||
'/vms/400',
|
||||
'' # sorted, comma-separated expected privilege string, loosely grouped by prefix
|
||||
. 'Datastore.Allocate,Datastore.AllocateSpace,Datastore.AllocateTemplate,Datastore.Audit,'
|
||||
. 'Group.Allocate,'
|
||||
. 'Mapping.Audit,Mapping.Modify,Mapping.Use,'
|
||||
. 'Permissions.Modify,'
|
||||
. 'Pool.Allocate,Pool.Audit,'
|
||||
. 'Realm.Allocate,Realm.AllocateUser,'
|
||||
. 'SDN.Allocate,SDN.Audit,SDN.Use,'
|
||||
. 'Sys.AccessNetwork,Sys.Audit,Sys.Console,Sys.Incoming,Sys.Modify,Sys.PowerMgmt,Sys.Syslog,'
|
||||
. 'User.Modify,'
|
||||
. '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.Console,VM.Migrate,VM.Monitor,VM.PowerMgmt,VM.Snapshot,VM.Snapshot.Rollback',
|
||||
. 'Datastore.Allocate,Datastore.AllocateSpace,Datastore.AllocateTemplate,Datastore.Audit,'
|
||||
. 'Group.Allocate,'
|
||||
. 'Mapping.Audit,Mapping.Modify,Mapping.Use,'
|
||||
. 'Permissions.Modify,'
|
||||
. 'Pool.Allocate,Pool.Audit,'
|
||||
. 'Realm.Allocate,Realm.AllocateUser,'
|
||||
. 'SDN.Allocate,SDN.Audit,SDN.Use,'
|
||||
. 'Sys.AccessNetwork,Sys.Audit,Sys.Console,Sys.Incoming,Sys.Modify,Sys.PowerMgmt,Sys.Syslog,'
|
||||
. 'User.Modify,'
|
||||
. '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.Console,VM.Migrate,VM.Monitor,VM.PowerMgmt,VM.Snapshot,VM.Snapshot.Rollback',
|
||||
);
|
||||
|
||||
check_roles('max@pve', '/vms/200', 'storage_manager');
|
||||
@ -92,4 +92,4 @@ check_roles('sue@pve', '/vms/200', 'NoAccess');
|
||||
|
||||
print "all tests passed\n";
|
||||
|
||||
exit (0);
|
||||
exit(0);
|
||||
|
@ -22,7 +22,7 @@ sub check_roles {
|
||||
my $res = join(',', sort keys %$roles);
|
||||
|
||||
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";
|
||||
}
|
||||
@ -40,4 +40,4 @@ check_roles('User2@pve', '/vms', '');
|
||||
|
||||
print "all tests passed\n";
|
||||
|
||||
exit (0);
|
||||
exit(0);
|
||||
|
@ -22,7 +22,7 @@ sub check_roles {
|
||||
my $res = join(',', sort keys %$roles);
|
||||
|
||||
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";
|
||||
}
|
||||
@ -35,4 +35,4 @@ check_roles('User1@pve', '/vms/200', 'Role2');
|
||||
|
||||
print "all tests passed\n";
|
||||
|
||||
exit (0);
|
||||
exit(0);
|
||||
|
@ -22,15 +22,14 @@ sub check_roles {
|
||||
my $res = join(',', sort keys %$roles);
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
|
||||
check_roles('User1@pve', '/vms/300', 'Role1');
|
||||
check_roles('User2@pve', '/vms/300', 'NoAccess');
|
||||
|
||||
print "all tests passed\n";
|
||||
|
||||
exit (0);
|
||||
exit(0);
|
||||
|
@ -22,12 +22,11 @@ sub check_roles {
|
||||
my $res = join(',', sort keys %$roles);
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
|
||||
check_roles('User1@pve', '/vms', 'Role1');
|
||||
check_roles('User1@pve', '/vms/100', '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";
|
||||
|
||||
exit (0);
|
||||
exit(0);
|
||||
|
@ -22,7 +22,7 @@ sub check_roles {
|
||||
my $res = join(',', sort keys %$roles);
|
||||
|
||||
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";
|
||||
}
|
||||
@ -34,12 +34,12 @@ sub check_permissions {
|
||||
my $res = join(',', sort keys %$perm);
|
||||
|
||||
die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
|
||||
if $res ne $expected_result;
|
||||
if $res ne $expected_result;
|
||||
|
||||
$perm = $rpcenv->permissions($user, $path);
|
||||
$res = join(',', sort keys %$perm);
|
||||
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";
|
||||
}
|
||||
@ -109,4 +109,4 @@ check_permissions('User4@pve', '/storage/store2', '');
|
||||
|
||||
print "all tests passed\n";
|
||||
|
||||
exit (0);
|
||||
exit(0);
|
||||
|
@ -22,7 +22,7 @@ sub check_roles {
|
||||
my $res = join(',', sort keys %$roles);
|
||||
|
||||
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";
|
||||
}
|
||||
@ -34,12 +34,12 @@ sub check_permissions {
|
||||
my $res = join(',', sort keys %$perm);
|
||||
|
||||
die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
|
||||
if $res ne $expected_result;
|
||||
if $res ne $expected_result;
|
||||
|
||||
$perm = $rpcenv->permissions($user, $path);
|
||||
$res = join(',', sort keys %$perm);
|
||||
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";
|
||||
}
|
||||
@ -54,4 +54,4 @@ check_permissions('User1@pve', '/vms/100', '');
|
||||
|
||||
print "all tests passed\n";
|
||||
|
||||
exit (0);
|
||||
exit(0);
|
||||
|
@ -20,7 +20,7 @@ sub check_roles {
|
||||
my $res = join(',', sort keys %$roles);
|
||||
|
||||
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";
|
||||
}
|
||||
@ -32,12 +32,12 @@ sub check_permission {
|
||||
my $res = join(',', sort keys %$perm);
|
||||
|
||||
die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
|
||||
if $res ne $expected_result;
|
||||
if $res ne $expected_result;
|
||||
|
||||
$perm = $rpcenv->permissions($user, $path);
|
||||
$res = join(',', sort keys %$perm);
|
||||
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";
|
||||
}
|
||||
@ -70,5 +70,5 @@ check_permission('max@pve!token2', '/vms/300', 'VM.Allocate,VM.Audit,VM.Console,
|
||||
|
||||
print "all tests passed\n";
|
||||
|
||||
exit (0);
|
||||
exit(0);
|
||||
|
||||
|
@ -12,69 +12,69 @@ use PVE::API2::Domains;
|
||||
|
||||
my $domainscfg = {
|
||||
ids => {
|
||||
"pam" => { type => 'pam' },
|
||||
"pve" => { type => 'pve' },
|
||||
"syncedrealm" => { type => 'ldap' }
|
||||
"pam" => { type => 'pam' },
|
||||
"pve" => { type => 'pve' },
|
||||
"syncedrealm" => { type => 'ldap' },
|
||||
},
|
||||
};
|
||||
|
||||
my $initialusercfg = {
|
||||
users => {
|
||||
'root@pam' => { username => 'root', },
|
||||
'user1@syncedrealm' => {
|
||||
username => 'user1',
|
||||
enable => 1,
|
||||
'keys' => 'some',
|
||||
},
|
||||
'user2@syncedrealm' => {
|
||||
username => 'user2',
|
||||
enable => 1,
|
||||
},
|
||||
'user3@syncedrealm' => {
|
||||
username => 'user3',
|
||||
enable => 1,
|
||||
},
|
||||
'root@pam' => { username => 'root' },
|
||||
'user1@syncedrealm' => {
|
||||
username => 'user1',
|
||||
enable => 1,
|
||||
'keys' => 'some',
|
||||
},
|
||||
'user2@syncedrealm' => {
|
||||
username => 'user2',
|
||||
enable => 1,
|
||||
},
|
||||
'user3@syncedrealm' => {
|
||||
username => 'user3',
|
||||
enable => 1,
|
||||
},
|
||||
},
|
||||
groups => {
|
||||
'group1-syncedrealm' => { users => {}, },
|
||||
'group2-syncedrealm' => { users => {}, },
|
||||
'group1-syncedrealm' => { users => {} },
|
||||
'group2-syncedrealm' => { users => {} },
|
||||
},
|
||||
acl_root => {
|
||||
users => {
|
||||
'user3@syncedrealm' => {},
|
||||
},
|
||||
groups => {},
|
||||
users => {
|
||||
'user3@syncedrealm' => {},
|
||||
},
|
||||
groups => {},
|
||||
},
|
||||
};
|
||||
|
||||
my $sync_response = {
|
||||
user => [
|
||||
{
|
||||
attributes => { 'uid' => ['user1'], },
|
||||
dn => 'uid=user1,dc=syncedrealm',
|
||||
},
|
||||
{
|
||||
attributes => { 'uid' => ['user2'], },
|
||||
dn => 'uid=user2,dc=syncedrealm',
|
||||
},
|
||||
{
|
||||
attributes => { 'uid' => ['user4'], },
|
||||
dn => 'uid=user4,dc=syncedrealm',
|
||||
},
|
||||
{
|
||||
attributes => { 'uid' => ['user1'] },
|
||||
dn => 'uid=user1,dc=syncedrealm',
|
||||
},
|
||||
{
|
||||
attributes => { 'uid' => ['user2'] },
|
||||
dn => 'uid=user2,dc=syncedrealm',
|
||||
},
|
||||
{
|
||||
attributes => { 'uid' => ['user4'] },
|
||||
dn => 'uid=user4,dc=syncedrealm',
|
||||
},
|
||||
],
|
||||
groups => [
|
||||
{
|
||||
dn => 'dc=group1,dc=syncedrealm',
|
||||
members => [
|
||||
'uid=user1,dc=syncedrealm',
|
||||
],
|
||||
},
|
||||
{
|
||||
dn => 'dc=group3,dc=syncedrealm',
|
||||
members => [
|
||||
'uid=nonexisting,dc=syncedrealm',
|
||||
],
|
||||
}
|
||||
{
|
||||
dn => 'dc=group1,dc=syncedrealm',
|
||||
members => [
|
||||
'uid=user1,dc=syncedrealm',
|
||||
],
|
||||
},
|
||||
{
|
||||
dn => 'dc=group3,dc=syncedrealm',
|
||||
members => [
|
||||
'uid=nonexisting,dc=syncedrealm',
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@ -83,24 +83,24 @@ my $returned_user_cfg = {};
|
||||
# mocking all cluster and ldap operations
|
||||
my $pve_cluster_module = Test::MockModule->new('PVE::Cluster');
|
||||
$pve_cluster_module->mock(
|
||||
cfs_update => sub {},
|
||||
cfs_update => sub { },
|
||||
cfs_read_file => sub {
|
||||
my ($filename) = @_;
|
||||
if ($filename eq 'domains.cfg') { return dclone($domainscfg); }
|
||||
if ($filename eq 'user.cfg') { return dclone($initialusercfg); }
|
||||
die "unexpected cfs_read_file";
|
||||
my ($filename) = @_;
|
||||
if ($filename eq 'domains.cfg') { return dclone($domainscfg); }
|
||||
if ($filename eq 'user.cfg') { return dclone($initialusercfg); }
|
||||
die "unexpected cfs_read_file";
|
||||
},
|
||||
cfs_write_file => sub {
|
||||
my ($filename, $data) = @_;
|
||||
if ($filename eq 'user.cfg') {
|
||||
$returned_user_cfg = $data;
|
||||
return;
|
||||
}
|
||||
die "unexpected cfs_read_file";
|
||||
my ($filename, $data) = @_;
|
||||
if ($filename eq 'user.cfg') {
|
||||
$returned_user_cfg = $data;
|
||||
return;
|
||||
}
|
||||
die "unexpected cfs_read_file";
|
||||
},
|
||||
cfs_lock_file => sub {
|
||||
my ($filename, $timeout, $code) = @_;
|
||||
return $code->();
|
||||
my ($filename, $timeout, $code) = @_;
|
||||
return $code->();
|
||||
},
|
||||
);
|
||||
|
||||
@ -120,21 +120,21 @@ $pve_rpcenvironment->mock(
|
||||
get => sub { return bless {}, 'PVE::RPCEnvironment'; },
|
||||
get_user => sub { return 'root@pam'; },
|
||||
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');
|
||||
$pve_ldap_module->mock(
|
||||
ldap_connect => sub { return {}; },
|
||||
ldap_bind => sub {},
|
||||
ldap_bind => sub { },
|
||||
query_users => sub {
|
||||
return $sync_response->{user};
|
||||
return $sync_response->{user};
|
||||
},
|
||||
query_groups => sub {
|
||||
return $sync_response->{groups};
|
||||
return $sync_response->{groups};
|
||||
},
|
||||
);
|
||||
|
||||
@ -145,205 +145,205 @@ $pve_auth_ldap->mock(
|
||||
|
||||
my $tests = [
|
||||
[
|
||||
"non-full without purge",
|
||||
{
|
||||
realm => 'syncedrealm',
|
||||
scope => 'both',
|
||||
},
|
||||
{
|
||||
users => {
|
||||
'root@pam' => { username => 'root', },
|
||||
'user1@syncedrealm' => {
|
||||
username => 'user1',
|
||||
enable => 1,
|
||||
'keys' => 'some',
|
||||
},
|
||||
'user2@syncedrealm' => {
|
||||
username => 'user2',
|
||||
enable => 1,
|
||||
},
|
||||
'user3@syncedrealm' => {
|
||||
username => 'user3',
|
||||
enable => 1,
|
||||
},
|
||||
'user4@syncedrealm' => {
|
||||
username => 'user4',
|
||||
enable => 1,
|
||||
},
|
||||
},
|
||||
groups => {
|
||||
'group1-syncedrealm' => {
|
||||
users => {
|
||||
'user1@syncedrealm' => 1,
|
||||
},
|
||||
},
|
||||
'group2-syncedrealm' => { users => {}, },
|
||||
'group3-syncedrealm' => { users => {}, },
|
||||
},
|
||||
acl_root => {
|
||||
users => {
|
||||
'user3@syncedrealm' => {},
|
||||
},
|
||||
groups => {},
|
||||
},
|
||||
},
|
||||
"non-full without purge",
|
||||
{
|
||||
realm => 'syncedrealm',
|
||||
scope => 'both',
|
||||
},
|
||||
{
|
||||
users => {
|
||||
'root@pam' => { username => 'root' },
|
||||
'user1@syncedrealm' => {
|
||||
username => 'user1',
|
||||
enable => 1,
|
||||
'keys' => 'some',
|
||||
},
|
||||
'user2@syncedrealm' => {
|
||||
username => 'user2',
|
||||
enable => 1,
|
||||
},
|
||||
'user3@syncedrealm' => {
|
||||
username => 'user3',
|
||||
enable => 1,
|
||||
},
|
||||
'user4@syncedrealm' => {
|
||||
username => 'user4',
|
||||
enable => 1,
|
||||
},
|
||||
},
|
||||
groups => {
|
||||
'group1-syncedrealm' => {
|
||||
users => {
|
||||
'user1@syncedrealm' => 1,
|
||||
},
|
||||
},
|
||||
'group2-syncedrealm' => { users => {} },
|
||||
'group3-syncedrealm' => { users => {} },
|
||||
},
|
||||
acl_root => {
|
||||
users => {
|
||||
'user3@syncedrealm' => {},
|
||||
},
|
||||
groups => {},
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"full without purge",
|
||||
{
|
||||
realm => 'syncedrealm',
|
||||
'remove-vanished' => 'entry;properties',
|
||||
scope => 'both',
|
||||
},
|
||||
{
|
||||
users => {
|
||||
'root@pam' => { username => 'root', },
|
||||
'user1@syncedrealm' => {
|
||||
username => 'user1',
|
||||
enable => 1,
|
||||
},
|
||||
'user2@syncedrealm' => {
|
||||
username => 'user2',
|
||||
enable => 1,
|
||||
},
|
||||
'user4@syncedrealm' => {
|
||||
username => 'user4',
|
||||
enable => 1,
|
||||
},
|
||||
},
|
||||
groups => {
|
||||
'group1-syncedrealm' => {
|
||||
users => {
|
||||
'user1@syncedrealm' => 1,
|
||||
},
|
||||
},
|
||||
'group3-syncedrealm' => { users => {}, }
|
||||
},
|
||||
acl_root => {
|
||||
users => {
|
||||
'user3@syncedrealm' => {},
|
||||
},
|
||||
groups => {},
|
||||
},
|
||||
},
|
||||
"full without purge",
|
||||
{
|
||||
realm => 'syncedrealm',
|
||||
'remove-vanished' => 'entry;properties',
|
||||
scope => 'both',
|
||||
},
|
||||
{
|
||||
users => {
|
||||
'root@pam' => { username => 'root' },
|
||||
'user1@syncedrealm' => {
|
||||
username => 'user1',
|
||||
enable => 1,
|
||||
},
|
||||
'user2@syncedrealm' => {
|
||||
username => 'user2',
|
||||
enable => 1,
|
||||
},
|
||||
'user4@syncedrealm' => {
|
||||
username => 'user4',
|
||||
enable => 1,
|
||||
},
|
||||
},
|
||||
groups => {
|
||||
'group1-syncedrealm' => {
|
||||
users => {
|
||||
'user1@syncedrealm' => 1,
|
||||
},
|
||||
},
|
||||
'group3-syncedrealm' => { users => {} },
|
||||
},
|
||||
acl_root => {
|
||||
users => {
|
||||
'user3@syncedrealm' => {},
|
||||
},
|
||||
groups => {},
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"non-full with purge",
|
||||
{
|
||||
realm => 'syncedrealm',
|
||||
'remove-vanished' => 'acl',
|
||||
scope => 'both',
|
||||
},
|
||||
{
|
||||
users => {
|
||||
'root@pam' => { username => 'root', },
|
||||
'user1@syncedrealm' => {
|
||||
username => 'user1',
|
||||
enable => 1,
|
||||
'keys' => 'some',
|
||||
},
|
||||
'user2@syncedrealm' => {
|
||||
username => 'user2',
|
||||
enable => 1,
|
||||
},
|
||||
'user3@syncedrealm' => {
|
||||
username => 'user3',
|
||||
enable => 1,
|
||||
},
|
||||
'user4@syncedrealm' => {
|
||||
username => 'user4',
|
||||
enable => 1,
|
||||
},
|
||||
},
|
||||
groups => {
|
||||
'group1-syncedrealm' => {
|
||||
users => {
|
||||
'user1@syncedrealm' => 1,
|
||||
},
|
||||
},
|
||||
'group2-syncedrealm' => { users => {}, },
|
||||
'group3-syncedrealm' => { users => {}, },
|
||||
},
|
||||
acl_root => {
|
||||
users => {},
|
||||
groups => {},
|
||||
},
|
||||
},
|
||||
"non-full with purge",
|
||||
{
|
||||
realm => 'syncedrealm',
|
||||
'remove-vanished' => 'acl',
|
||||
scope => 'both',
|
||||
},
|
||||
{
|
||||
users => {
|
||||
'root@pam' => { username => 'root' },
|
||||
'user1@syncedrealm' => {
|
||||
username => 'user1',
|
||||
enable => 1,
|
||||
'keys' => 'some',
|
||||
},
|
||||
'user2@syncedrealm' => {
|
||||
username => 'user2',
|
||||
enable => 1,
|
||||
},
|
||||
'user3@syncedrealm' => {
|
||||
username => 'user3',
|
||||
enable => 1,
|
||||
},
|
||||
'user4@syncedrealm' => {
|
||||
username => 'user4',
|
||||
enable => 1,
|
||||
},
|
||||
},
|
||||
groups => {
|
||||
'group1-syncedrealm' => {
|
||||
users => {
|
||||
'user1@syncedrealm' => 1,
|
||||
},
|
||||
},
|
||||
'group2-syncedrealm' => { users => {} },
|
||||
'group3-syncedrealm' => { users => {} },
|
||||
},
|
||||
acl_root => {
|
||||
users => {},
|
||||
groups => {},
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"full with purge",
|
||||
{
|
||||
realm => 'syncedrealm',
|
||||
'remove-vanished' => 'acl;entry;properties',
|
||||
scope => 'both',
|
||||
},
|
||||
{
|
||||
users => {
|
||||
'root@pam' => { username => 'root', },
|
||||
'user1@syncedrealm' => {
|
||||
username => 'user1',
|
||||
enable => 1,
|
||||
},
|
||||
'user2@syncedrealm' => {
|
||||
username => 'user2',
|
||||
enable => 1,
|
||||
},
|
||||
'user4@syncedrealm' => {
|
||||
username => 'user4',
|
||||
enable => 1,
|
||||
},
|
||||
},
|
||||
groups => {
|
||||
'group1-syncedrealm' => {
|
||||
users => {
|
||||
'user1@syncedrealm' => 1,
|
||||
},
|
||||
},
|
||||
'group3-syncedrealm' => { users => {}, },
|
||||
},
|
||||
acl_root => {
|
||||
users => {},
|
||||
groups => {},
|
||||
},
|
||||
},
|
||||
"full with purge",
|
||||
{
|
||||
realm => 'syncedrealm',
|
||||
'remove-vanished' => 'acl;entry;properties',
|
||||
scope => 'both',
|
||||
},
|
||||
{
|
||||
users => {
|
||||
'root@pam' => { username => 'root' },
|
||||
'user1@syncedrealm' => {
|
||||
username => 'user1',
|
||||
enable => 1,
|
||||
},
|
||||
'user2@syncedrealm' => {
|
||||
username => 'user2',
|
||||
enable => 1,
|
||||
},
|
||||
'user4@syncedrealm' => {
|
||||
username => 'user4',
|
||||
enable => 1,
|
||||
},
|
||||
},
|
||||
groups => {
|
||||
'group1-syncedrealm' => {
|
||||
users => {
|
||||
'user1@syncedrealm' => 1,
|
||||
},
|
||||
},
|
||||
'group3-syncedrealm' => { users => {} },
|
||||
},
|
||||
acl_root => {
|
||||
users => {},
|
||||
groups => {},
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"don't delete properties, but users and acls",
|
||||
{
|
||||
realm => 'syncedrealm',
|
||||
'remove-vanished' => 'acl;entry',
|
||||
scope => 'both',
|
||||
},
|
||||
{
|
||||
users => {
|
||||
'root@pam' => { username => 'root', },
|
||||
'user1@syncedrealm' => {
|
||||
username => 'user1',
|
||||
enable => 1,
|
||||
'keys' => 'some',
|
||||
},
|
||||
'user2@syncedrealm' => {
|
||||
username => 'user2',
|
||||
enable => 1,
|
||||
},
|
||||
'user4@syncedrealm' => {
|
||||
username => 'user4',
|
||||
enable => 1,
|
||||
},
|
||||
},
|
||||
groups => {
|
||||
'group1-syncedrealm' => {
|
||||
users => {
|
||||
'user1@syncedrealm' => 1,
|
||||
},
|
||||
},
|
||||
'group3-syncedrealm' => { users => {}, },
|
||||
},
|
||||
acl_root => {
|
||||
users => {},
|
||||
groups => {},
|
||||
},
|
||||
},
|
||||
"don't delete properties, but users and acls",
|
||||
{
|
||||
realm => 'syncedrealm',
|
||||
'remove-vanished' => 'acl;entry',
|
||||
scope => 'both',
|
||||
},
|
||||
{
|
||||
users => {
|
||||
'root@pam' => { username => 'root' },
|
||||
'user1@syncedrealm' => {
|
||||
username => 'user1',
|
||||
enable => 1,
|
||||
'keys' => 'some',
|
||||
},
|
||||
'user2@syncedrealm' => {
|
||||
username => 'user2',
|
||||
enable => 1,
|
||||
},
|
||||
'user4@syncedrealm' => {
|
||||
username => 'user4',
|
||||
enable => 1,
|
||||
},
|
||||
},
|
||||
groups => {
|
||||
'group1-syncedrealm' => {
|
||||
users => {
|
||||
'user1@syncedrealm' => 1,
|
||||
},
|
||||
},
|
||||
'group3-syncedrealm' => { users => {} },
|
||||
},
|
||||
acl_root => {
|
||||
users => {},
|
||||
groups => {},
|
||||
},
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user