X-Git-Url: https://git.proxmox.com/?p=pve-access-control.git;a=blobdiff_plain;f=PVE%2FAPI2%2FDomains.pm;h=8ae1db08d4a58facc7ddcbbbb193a1362da5a836;hp=117ef3c92fd58a5f020349507b0789aee7c0e102;hb=b49abe2d256689c87a48a3a19696929356ff33c1;hpb=af4a8a8522118cfd15b0c58f81d748a9184a0ef8 diff --git a/PVE/API2/Domains.pm b/PVE/API2/Domains.pm index 117ef3c..8ae1db0 100644 --- a/PVE/API2/Domains.pm +++ b/PVE/API2/Domains.pm @@ -2,26 +2,30 @@ 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 Data::Dumper; # fixme: remove - use PVE::RESTHandler; +use PVE::Auth::Plugin; my $domainconfigfile = "domains.cfg"; use base qw(PVE::RESTHandler); __PACKAGE__->register_method ({ - name => 'index', - path => '', + name => 'index', + path => '', method => 'GET', description => "Authentication domain index.", - permissions => { user => 'world' }, + 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 => {}, @@ -32,23 +36,38 @@ __PACKAGE__->register_method ({ type => "object", properties => { realm => { type => 'string' }, - comment => { type => 'string', optional => 1 }, + 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); - - foreach my $realm (keys %$cfg) { - my $d = $cfg->{$realm}; + 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; } @@ -56,103 +75,47 @@ __PACKAGE__->register_method ({ }}); __PACKAGE__->register_method ({ - name => 'create', + name => 'create', protected => 1, - path => '', + path => '', method => 'POST', - description => "Add an authentication server.", - parameters => { - additionalProperties => 0, - properties => { - realm => get_standard_option('realm'), - type => { - description => "Server type.", - type => 'string', - enum => [ 'ad', 'ldap' ], - }, - server1 => { - description => "Server IP address (or DNS name)", - type => 'string', - }, - server2 => { - description => "Fallback Server IP address (or DNS name)", - type => 'string', - optional => 1, - }, - secure => { - description => "Use secure LDAPS protocol.", - type => 'boolean', - optional => 1, - }, - default => { - description => "Use this as default realm", - type => 'boolean', - optional => 1, - }, - comment => { - type => 'string', - optional => 1, - }, - port => { - description => "Server port. Use '0' if you want to use default settings'", - type => 'integer', - minimum => 0, - maximum => 65535, - optional => 1, - }, - domain => { - description => "AD domain name", - type => 'string', - optional => 1, - }, - base_dn => { - description => "LDAP base domain name", - type => 'string', - optional => 1, - }, - user_attr => { - description => "LDAP user attribute name", - type => 'string', - optional => 1, - }, - }, + 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::AccessControl::lock_domain_config( + PVE::Auth::Plugin::lock_domain_config( sub { - + my $cfg = cfs_read_file($domainconfigfile); + my $ids = $cfg->{ids}; - my $realm = $param->{realm}; - - die "domain '$realm' already exists\n" - if $cfg->{$realm}; + 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'); - if (defined($param->{secure})) { - $cfg->{$realm}->{secure} = $param->{secure} ? 1 : 0; - } + 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 ($param->{default}) { - foreach my $r (keys %$cfg) { - delete $cfg->{$r}->{default}; + if ($config->{default}) { + foreach my $r (keys %$ids) { + delete $ids->{$r}->{default}; } } - foreach my $p (keys %$param) { - next if $p eq 'realm'; - $cfg->{$realm}->{$p} = $param->{$p}; - } - - # port 0 ==> use default - if (defined($param->{port}) && !$param->{port}) { - delete $cfg->{$realm}->{port}; - } + $ids->{$realm} = $config; cfs_write_file($domainconfigfile, $cfg); }, "add auth server failed"); @@ -161,98 +124,51 @@ __PACKAGE__->register_method ({ }}); __PACKAGE__->register_method ({ - name => 'update', - path => '{realm}', + name => 'update', + path => '{realm}', method => 'PUT', + permissions => { + check => ['perm', '/access/realm', ['Realm.Allocate']], + }, description => "Update authentication server settings.", protected => 1, - parameters => { - additionalProperties => 0, - properties => { - realm => get_standard_option('realm'), - server1 => { - description => "Server IP address (or DNS name)", - type => 'string', - optional => 1, - }, - server2 => { - description => "Fallback Server IP address (or DNS name)", - type => 'string', - optional => 1, - }, - secure => { - description => "Use secure LDAPS protocol.", - type => 'boolean', - optional => 1, - }, - default => { - description => "Use this as default realm", - type => 'boolean', - optional => 1, - }, - comment => { - type => 'string', - optional => 1, - }, - port => { - description => "Server port. Use '0' if you want to use default settings'", - type => 'integer', - minimum => 0, - maximum => 65535, - optional => 1, - }, - domain => { - description => "AD domain name", - type => 'string', - optional => 1, - }, - base_dn => { - description => "LDAP base domain name", - type => 'string', - optional => 1, - }, - user_attr => { - description => "LDAP user attribute name", - type => 'string', - optional => 1, - }, - }, - }, + parameters => PVE::Auth::Plugin->updateSchema(), returns => { type => 'null' }, code => sub { my ($param) = @_; - PVE::AccessControl::lock_domain_config( + PVE::Auth::Plugin::lock_domain_config( sub { - + my $cfg = cfs_read_file($domainconfigfile); + my $ids = $cfg->{ids}; - my $realm = $param->{realm}; - delete $param->{realm}; + my $digest = extract_param($param, 'digest'); + PVE::SectionConfig::assert_if_modified($cfg, $digest); - die "unable to modify bultin domain '$realm'\n" - if ($realm eq 'pam' || $realm eq 'pve'); + my $realm = extract_param($param, 'realm'); - die "domain '$realm' does not exist\n" - if !$cfg->{$realm}; + die "domain '$realm' does not exist\n" + if !$ids->{$realm}; - if (defined($param->{secure})) { - $cfg->{$realm}->{secure} = $param->{secure} ? 1 : 0; - } + my $delete_str = extract_param($param, 'delete'); + die "no options specified\n" if !$delete_str && !scalar(keys %$param); - if ($param->{default}) { - foreach my $r (keys %$cfg) { - delete $cfg->{$r}->{default}; - } + foreach my $opt (PVE::Tools::split_list($delete_str)) { + delete $ids->{$realm}->{$opt}; } - foreach my $p (keys %$param) { - $cfg->{$realm}->{$p} = $param->{$p}; + 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}; + } } - # port 0 ==> use default - if (defined($param->{port}) && !$param->{port}) { - delete $cfg->{$realm}->{port}; + foreach my $p (keys %$config) { + $ids->{$realm}->{$p} = $config->{$p}; } cfs_write_file($domainconfigfile, $cfg); @@ -263,10 +179,13 @@ __PACKAGE__->register_method ({ # fixme: return format! __PACKAGE__->register_method ({ - name => 'read', - path => '{realm}', + 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 => { @@ -280,18 +199,23 @@ __PACKAGE__->register_method ({ my $cfg = cfs_read_file($domainconfigfile); my $realm = $param->{realm}; - - my $data = $cfg->{$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}', + name => 'delete', + path => '{realm}', method => 'DELETE', + permissions => { + check => ['perm', '/access/realm', ['Realm.Allocate']], + }, description => "Delete an authentication server.", protected => 1, parameters => { @@ -304,21 +228,221 @@ __PACKAGE__->register_method ({ code => sub { my ($param) = @_; - PVE::AccessControl::lock_user_config( + 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 !$cfg->{$realm}; - delete $cfg->{$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; }}); +my $update_users = sub { + my ($usercfg, $realm, $synced_users, $opts) = @_; + + print "syncing users\n"; + $usercfg->{users} = {} if !defined($usercfg->{users}); + my $users = $usercfg->{users}; + + my $oldusers = {}; + if ($opts->{'full'}) { + print "full sync, deleting outdated existing users first\n"; + foreach my $userid (sort keys %$users) { + next if $userid !~ m/\@$realm$/; + + $oldusers->{$userid} = delete $users->{$userid}; + if ($opts->{'purge'} && !$synced_users->{$userid}) { + PVE::AccessControl::delete_user_acl($userid, $usercfg); + print "purged user '$userid' and all its ACL entries\n"; + } elsif (!defined($synced_users->{$userid})) { + print "remove user '$userid'\n"; + } + } + } + + foreach my $userid (sort keys %$synced_users) { + my $synced_user = $synced_users->{$userid} // {}; + if (!defined($users->{$userid})) { + my $user = $users->{$userid} = $synced_user; + + my $olduser = $oldusers->{$userid} // {}; + if (defined(my $enabled = $olduser->{enable})) { + $user->{enable} = $enabled; + } elsif ($opts->{'enable-new'}) { + $user->{enable} = 1; + } + + if (defined($olduser->{tokens})) { + $user->{tokens} = $olduser->{tokens}; + } + if (defined($oldusers->{$userid})) { + print "updated user '$userid'\n"; + } else { + print "added user '$userid'\n"; + } + } else { + my $olduser = $users->{$userid}; + foreach my $attr (keys %$synced_user) { + $olduser->{$attr} = $synced_user->{$attr}; + } + print "updated user '$userid'\n"; + } + } +}; + +my $update_groups = sub { + my ($usercfg, $realm, $synced_groups, $opts) = @_; + + print "syncing groups\n"; + $usercfg->{groups} = {} if !defined($usercfg->{groups}); + my $groups = $usercfg->{groups}; + my $oldgroups = {}; + + if ($opts->{full}) { + print "full sync, deleting outdated existing groups first\n"; + foreach my $groupid (sort keys %$groups) { + next if $groupid !~ m/\-$realm$/; + + my $oldgroups->{$groupid} = delete $groups->{$groupid}; + if ($opts->{purge} && !$synced_groups->{$groupid}) { + print "purged group '$groupid' and all its ACL entries\n"; + PVE::AccessControl::delete_group_acl($groupid, $usercfg) + } elsif (!defined($synced_groups->{$groupid})) { + print "removed group '$groupid'\n"; + } + } + } + + foreach my $groupid (sort keys %$synced_groups) { + my $synced_group = $synced_groups->{$groupid}; + if (!defined($groups->{$groupid})) { + $groups->{$groupid} = $synced_group; + if (defined($oldgroups->{$groupid})) { + print "updated group '$groupid'\n"; + } else { + print "added group '$groupid'\n"; + } + } else { + my $group = $groups->{$groupid}; + foreach my $attr (keys %$synced_group) { + $group->{$attr} = $synced_group->{$attr}; + } + print "updated group '$groupid'\n"; + } + } +}; + +my $parse_sync_opts = sub { + my ($param, $realmconfig) = @_; + + my $sync_opts_fmt = PVE::JSONSchema::get_format('realm-sync-options'); + + my $res = {}; + if (defined(my $cfg_opts = $realmconfig->{'sync-defaults-options'})) { + $res = PVE::JSONSchema::parse_property_string($sync_opts_fmt, $cfg_opts); + } + + for my $opt (sort keys %$sync_opts_fmt) { + my $fmt = $sync_opts_fmt->{$opt}; + + if (exists $param->{$opt}) { + $res->{$opt} = $param->{$opt}; + } elsif (!exists $res->{$opt}) { + raise_param_exc({ + "$opt" => 'Not passed as parameter and not defined in realm default sync options.' + }) if !$fmt->{optional}; + $res->{$opt} = $fmt->{default} if exists $fmt->{default}; + } + } + return $res; +}; + +__PACKAGE__->register_method ({ + name => 'sync', + path => '{realm}/sync', + method => 'POST', + permissions => { + description => "'Realm.AllocateUser' on '/access/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 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.", + protected => 1, + parameters => { + additionalProperties => 0, + properties => get_standard_option('realm-sync-options', { + realm => get_standard_option('realm'), + }) + }, + returns => { + description => 'Worker Task-UPID', + 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 $realmconfig = $cfg->{ids}->{$realm}; + + 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"; + } + + my $opts = $parse_sync_opts->($param, $realmconfig); # can throw up + + my $scope = $opts->{scope}; + my $whatstring = $scope eq 'both' ? "users and groups" : $scope; + + my $plugin = PVE::Auth::Plugin->lookup($type); + + my $worker = sub { + 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); + } + + 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 'groups' || $scope eq 'both') { + $update_groups->($usercfg, $realm, $synced_groups, $opts); + } + + cfs_write_file("user.cfg", $usercfg); + print "successfully updated $whatstring configuration\n"; + }, "syncing $whatstring failed"); + }; + + return $rpcenv->fork_worker('auth-realm-sync', $realm, $authuser, $worker); + }}); + 1;