mirror of
https://git.proxmox.com/git/pve-access-control
synced 2025-07-15 22:59:51 +00:00

this api call syncs the users and groups from LDAP/AD to the user.cfg it also implements a 'full' mode where we first delete all users/groups from the config and sync them again the parameter 'enable' controls if newly synced users are 'enabled' (if no sync parameter handles that) the parameter 'purge' controls if ACLs get removed for users/groups that do not exists anymore after also add this command to pveum Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
431 lines
11 KiB
Perl
431 lines
11 KiB
Perl
package PVE::API2::Domains;
|
|
|
|
use strict;
|
|
use warnings;
|
|
use PVE::Exception qw(raise_param_exc);
|
|
use PVE::Tools qw(extract_param);
|
|
use PVE::Cluster qw (cfs_read_file cfs_write_file);
|
|
use PVE::AccessControl;
|
|
use PVE::JSONSchema qw(get_standard_option);
|
|
|
|
use PVE::SafeSyslog;
|
|
use PVE::RESTHandler;
|
|
use PVE::Auth::Plugin;
|
|
|
|
my $domainconfigfile = "domains.cfg";
|
|
|
|
use base qw(PVE::RESTHandler);
|
|
|
|
__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',
|
|
},
|
|
parameters => {
|
|
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}" } ],
|
|
},
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
my $res = [];
|
|
|
|
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;
|
|
}
|
|
|
|
return $res;
|
|
}});
|
|
|
|
__PACKAGE__->register_method ({
|
|
name => 'create',
|
|
protected => 1,
|
|
path => '',
|
|
method => 'POST',
|
|
permissions => {
|
|
check => ['perm', '/access/realm', ['Realm.Allocate']],
|
|
},
|
|
description => "Add an authentication server.",
|
|
parameters => PVE::Auth::Plugin->createSchema(),
|
|
returns => { type => 'null' },
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
PVE::Auth::Plugin::lock_domain_config(
|
|
sub {
|
|
|
|
my $cfg = cfs_read_file($domainconfigfile);
|
|
my $ids = $cfg->{ids};
|
|
|
|
my $realm = extract_param($param, 'realm');
|
|
my $type = $param->{type};
|
|
|
|
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 create builtin type '$type'\n"
|
|
if ($type eq 'pam' || $type eq 'pve');
|
|
|
|
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};
|
|
}
|
|
}
|
|
|
|
$ids->{$realm} = $config;
|
|
|
|
cfs_write_file($domainconfigfile, $cfg);
|
|
}, "add auth server failed");
|
|
|
|
return undef;
|
|
}});
|
|
|
|
__PACKAGE__->register_method ({
|
|
name => 'update',
|
|
path => '{realm}',
|
|
method => 'PUT',
|
|
permissions => {
|
|
check => ['perm', '/access/realm', ['Realm.Allocate']],
|
|
},
|
|
description => "Update authentication server settings.",
|
|
protected => 1,
|
|
parameters => PVE::Auth::Plugin->updateSchema(),
|
|
returns => { type => 'null' },
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
PVE::Auth::Plugin::lock_domain_config(
|
|
sub {
|
|
|
|
my $cfg = cfs_read_file($domainconfigfile);
|
|
my $ids = $cfg->{ids};
|
|
|
|
my $digest = extract_param($param, 'digest');
|
|
PVE::SectionConfig::assert_if_modified($cfg, $digest);
|
|
|
|
my $realm = extract_param($param, 'realm');
|
|
|
|
die "domain '$realm' does not exist\n"
|
|
if !$ids->{$realm};
|
|
|
|
my $delete_str = extract_param($param, 'delete');
|
|
die "no options specified\n" if !$delete_str && !scalar(keys %$param);
|
|
|
|
foreach my $opt (PVE::Tools::split_list($delete_str)) {
|
|
delete $ids->{$realm}->{$opt};
|
|
}
|
|
|
|
my $plugin = PVE::Auth::Plugin->lookup($ids->{$realm}->{type});
|
|
my $config = $plugin->check_config($realm, $param, 0, 1);
|
|
|
|
if ($config->{default}) {
|
|
foreach my $r (keys %$ids) {
|
|
delete $ids->{$r}->{default};
|
|
}
|
|
}
|
|
|
|
foreach my $p (keys %$config) {
|
|
$ids->{$realm}->{$p} = $config->{$p};
|
|
}
|
|
|
|
cfs_write_file($domainconfigfile, $cfg);
|
|
}, "update auth server failed");
|
|
|
|
return undef;
|
|
}});
|
|
|
|
# fixme: return format!
|
|
__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],
|
|
},
|
|
parameters => {
|
|
additionalProperties => 0,
|
|
properties => {
|
|
realm => get_standard_option('realm'),
|
|
},
|
|
},
|
|
returns => {},
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
my $cfg = cfs_read_file($domainconfigfile);
|
|
|
|
my $realm = $param->{realm};
|
|
|
|
my $data = $cfg->{ids}->{$realm};
|
|
die "domain '$realm' does not exist\n" if !$data;
|
|
|
|
$data->{digest} = $cfg->{digest};
|
|
|
|
return $data;
|
|
}});
|
|
|
|
|
|
__PACKAGE__->register_method ({
|
|
name => 'delete',
|
|
path => '{realm}',
|
|
method => 'DELETE',
|
|
permissions => {
|
|
check => ['perm', '/access/realm', ['Realm.Allocate']],
|
|
},
|
|
description => "Delete an authentication server.",
|
|
protected => 1,
|
|
parameters => {
|
|
additionalProperties => 0,
|
|
properties => {
|
|
realm => get_standard_option('realm'),
|
|
}
|
|
},
|
|
returns => { type => 'null' },
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
PVE::Auth::Plugin::lock_domain_config(
|
|
sub {
|
|
|
|
my $cfg = cfs_read_file($domainconfigfile);
|
|
my $ids = $cfg->{ids};
|
|
|
|
my $realm = $param->{realm};
|
|
|
|
die "domain '$realm' does not exist\n" if !$ids->{$realm};
|
|
|
|
delete $ids->{$realm};
|
|
|
|
cfs_write_file($domainconfigfile, $cfg);
|
|
}, "delete auth server failed");
|
|
|
|
return undef;
|
|
}});
|
|
|
|
__PACKAGE__->register_method ({
|
|
name => 'sync',
|
|
path => '{realm}/sync',
|
|
method => 'POST',
|
|
permissions => {
|
|
description => "You need 'Realm.AllocateUser' on '/access/realm/<realm>' on the realm and 'User.Modify' permissions to '/access/groups/'.",
|
|
check => [ 'and',
|
|
[ 'userid-param', 'Realm.AllocateUser'],
|
|
[ 'userid-group', ['User.Modify']],
|
|
],
|
|
},
|
|
description => "Syncs users and/or groups from LDAP to user.cfg. ".
|
|
"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 => {
|
|
realm => get_standard_option('realm'),
|
|
scope => {
|
|
description => "Select what to sync.",
|
|
type => 'string',
|
|
enum => [qw(users groups both)],
|
|
},
|
|
full => {
|
|
description => "If set, uses the LDAP Directory as source of truth, ".
|
|
"deleting all information not contained there. ".
|
|
"Otherwise only syncs information set explicitly.",
|
|
type => 'boolean',
|
|
},
|
|
enable => {
|
|
description => "Enable newly synced users.",
|
|
type => 'boolean',
|
|
},
|
|
purge => {
|
|
description => "Remove ACLs for users/groups that were removed from the config.",
|
|
type => 'boolean',
|
|
},
|
|
}
|
|
},
|
|
returns => { type => 'string' },
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
my $rpcenv = PVE::RPCEnvironment::get();
|
|
my $authuser = $rpcenv->get_user();
|
|
|
|
|
|
my $realm = $param->{realm};
|
|
my $cfg = cfs_read_file($domainconfigfile);
|
|
my $ids = $cfg->{ids};
|
|
|
|
raise_param_exc({ 'realm' => 'Realm does not exist.' }) if !defined($ids->{$realm});
|
|
my $type = $ids->{$realm}->{type};
|
|
|
|
if ($type ne 'ldap' && $type ne 'ad') {
|
|
die "Only LDAP/AD realms can be synced.\n";
|
|
}
|
|
|
|
my $scope = $param->{scope};
|
|
my $sync_users;
|
|
my $sync_groups;
|
|
|
|
my $errorstring = "syncing ";
|
|
if ($scope eq 'users') {
|
|
$errorstring .= "users ";
|
|
$sync_users = 1;
|
|
} elsif ($scope eq 'groups') {
|
|
$errorstring .= "groups ";
|
|
$sync_groups = 1;
|
|
} elsif ($scope eq 'both') {
|
|
$errorstring .= "users and groups ";
|
|
$sync_users = $sync_groups = 1;
|
|
}
|
|
$errorstring .= "failed.";
|
|
|
|
my $plugin = PVE::Auth::Plugin->lookup($ids->{$realm}->{type});
|
|
|
|
my $realmdata = $ids->{$realm};
|
|
my $users = {};
|
|
my $groups = {};
|
|
|
|
|
|
my $worker = sub {
|
|
print "starting sync for $realm\n";
|
|
if ($sync_groups) {
|
|
my $dnmap = {};
|
|
($users, $dnmap) = $plugin->get_users($realmdata, $realm);
|
|
$groups = $plugin->get_groups($realmdata, $realm, $dnmap);
|
|
} else {
|
|
$users = $plugin->get_users($realmdata, $realm);
|
|
}
|
|
|
|
PVE::AccessControl::lock_user_config(
|
|
sub {
|
|
my $usercfg = cfs_read_file("user.cfg");
|
|
print "got data from server, modifying users/groups\n";
|
|
|
|
if ($sync_users) {
|
|
print "syncing users\n";
|
|
my $oldusers = $usercfg->{users};
|
|
|
|
my $oldtokens = {};
|
|
my $oldenabled = {};
|
|
|
|
if ($param->{full}) {
|
|
print "full sync, deleting existing users first\n";
|
|
foreach my $userid (keys %$oldusers) {
|
|
next if $userid !~ m/\@$realm$/;
|
|
# we save the old tokens
|
|
$oldtokens->{$userid} = $oldusers->{$userid}->{tokens};
|
|
$oldenabled->{$userid} = $oldusers->{$userid}->{enable} // 0;
|
|
delete $oldusers->{$userid};
|
|
PVE::AccessControl::delete_user_acl($userid, $usercfg)
|
|
if $param->{purge} && !$users->{$userid};
|
|
print "removed user '$userid'\n";
|
|
}
|
|
}
|
|
|
|
foreach my $userid (keys %$users) {
|
|
my $user = $users->{$userid};
|
|
if (!defined($oldusers->{$userid})) {
|
|
$oldusers->{$userid} = $user;
|
|
|
|
if (defined($oldenabled->{$userid})) {
|
|
$oldusers->{$userid}->{enable} = $oldenabled->{$userid};
|
|
} elsif ($param->{enable}) {
|
|
$oldusers->{$userid}->{enable} = 1;
|
|
}
|
|
|
|
if (defined($oldtokens->{$userid})) {
|
|
$oldusers->{$userid}->{tokens} = $oldtokens->{$userid};
|
|
}
|
|
|
|
print "added user '$userid'\n";
|
|
} else {
|
|
my $olduser = $oldusers->{$userid};
|
|
foreach my $attr (keys %$user) {
|
|
$olduser->{$attr} = $user->{$attr};
|
|
}
|
|
print "updated user '$userid'\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($sync_groups) {
|
|
print "syncing groups\n";
|
|
my $oldgroups = $usercfg->{groups};
|
|
|
|
if ($param->{full}) {
|
|
print "full sync, deleting existing groups first\n";
|
|
foreach my $groupid (keys %$oldgroups) {
|
|
next if $groupid !~ m/\-$realm$/;
|
|
delete $oldgroups->{$groupid};
|
|
PVE::AccessControl::delete_group_acl($groupid, $usercfg)
|
|
if $param->{purge} && !$groups->{$groupid};
|
|
print "removed group '$groupid'\n";
|
|
}
|
|
}
|
|
|
|
foreach my $groupid (keys %$groups) {
|
|
my $group = $groups->{$groupid};
|
|
if (!defined($oldgroups->{$groupid})) {
|
|
$oldgroups->{$groupid} = $group;
|
|
print "added group '$groupid'\n";
|
|
} else {
|
|
my $oldgroup = $oldgroups->{$groupid};
|
|
foreach my $attr (keys %$group) {
|
|
$oldgroup->{$attr} = $group->{$attr};
|
|
}
|
|
print "updated group '$groupid'\n";
|
|
}
|
|
}
|
|
}
|
|
cfs_write_file("user.cfg", $usercfg);
|
|
print "updated user.cfg\n";
|
|
}, $errorstring);
|
|
};
|
|
|
|
return $rpcenv->fork_worker('ldapsync', $realm, $authuser, $worker);
|
|
}});
|
|
|
|
1;
|