PACKAGE=libpve-access-control
+DEB=${PACKAGE}_${DEB_VERSION_UPSTREAM_REVISION}_all.deb
+DSC=${PACKAGE}_${DEB_VERSION_UPSTREAM_REVISION}.dsc
BUILDDIR ?= ${PACKAGE}-${DEB_VERSION_UPSTREAM}
-DESTDIR=
-PREFIX=/usr
-BINDIR=${PREFIX}/bin
-SBINDIR=${PREFIX}/sbin
-MANDIR=${PREFIX}/share/man
-DOCDIR=${PREFIX}/share/doc/${PACKAGE}
-MAN1DIR=${MANDIR}/man1/
-BASHCOMPLDIR=${PREFIX}/share/bash-completion/completions/
-ZSHCOMPLDIR=${PREFIX}/share/zsh/vendor-completions/
-
-export PERLDIR=${PREFIX}/share/perl5
-
GITVERSION:=$(shell git rev-parse HEAD)
-DEB=${PACKAGE}_${DEB_VERSION_UPSTREAM_REVISION}_all.deb
-DSC=${PACKAGE}_${DEB_VERSION_UPSTREAM_REVISION}.dsc
-
-# this requires package pve-doc-generator
-export NOVIEW=1
-include /usr/share/pve-doc-generator/pve-doc-generator.mk
-
all:
.PHONY: dinstall
dinstall: deb
dpkg -i ${DEB}
-pveum.bash-completion: PVE/CLI/pveum.pm
- perl -I. -T -e "use PVE::CLI::pveum; PVE::CLI::pveum->generate_bash_completions();" >$@.tmp
- mv $@.tmp $@
-
-pveum.zsh-completion: PVE/CLI/pveum.pm
- perl -I. -T -e "use PVE::CLI::pveum; PVE::CLI::pveum->generate_zsh_completions();" >$@.tmp
- mv $@.tmp $@
-
-.PHONY: install
-install: pveum.1 oathkeygen pveum.bash-completion pveum.zsh-completion
- install -d ${DESTDIR}${BINDIR}
- install -d ${DESTDIR}${SBINDIR}
- install -m 0755 pveum ${DESTDIR}${SBINDIR}
- install -m 0755 oathkeygen ${DESTDIR}${BINDIR}
- make -C PVE install
- install -d ${DESTDIR}/${MAN1DIR}
- install -d ${DESTDIR}/${DOCDIR}
- install -m 0644 pveum.1 ${DESTDIR}/${MAN1DIR}
- install -m 0644 -D pveum.bash-completion ${DESTDIR}${BASHCOMPLDIR}/pveum
- install -m 0644 -D pveum.zsh-completion ${DESTDIR}${ZSHCOMPLDIR}/_pveum
-
-.PHONY: test
-test:
- perl -I. ./pveum verifyapi
- perl -I. -T -e "use PVE::CLI::pveum; PVE::CLI::pveum->verify_api();"
- make -C test check
-
${BUILDDIR}:
rm -rf ${BUILDDIR}
- rsync -a * ${BUILDDIR}
+ cp -a src ${BUILDDIR}
+ cp -a debian ${BUILDDIR}/
echo "git clone git://git.proxmox.com/git/pve-access-control.git\\ngit checkout ${GITVERSION}" > ${BUILDDIR}/debian/SOURCE
.PHONY: deb
.PHONY: clean
clean:
- rm -rf ${BUILDDIR}
- make cleanup-docgen
- rm -rf *.deb *.buildinfo *.changes ${PACKAGE}*.tar.gz *.dsc
+ rm -rf ${BUILDDIR} *.deb *.buildinfo *.changes ${PACKAGE}*.tar.gz *.dsc
find . -name '*~' -exec rm {} ';'
.PHONY: distclean
+++ /dev/null
-package PVE::API2::ACL;
-
-use strict;
-use warnings;
-use PVE::Cluster qw (cfs_read_file cfs_write_file);
-use PVE::Tools qw(split_list);
-use PVE::AccessControl;
-use PVE::Exception qw(raise_param_exc);
-use PVE::JSONSchema qw(get_standard_option register_standard_option);
-
-use PVE::SafeSyslog;
-
-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',
-});
-
-__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',
- },
- parameters => {
- 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' },
- },
- },
- },
- code => sub {
- my ($param) = @_;
-
- my $rpcenv = PVE::RPCEnvironment::get();
- my $authuser = $rpcenv->get_user();
- my $res = [];
-
- my $usercfg = $rpcenv->{user_cfg};
- if (!$usercfg || !$usercfg->{acl}) {
- return $res;
- }
-
- my $audit = $rpcenv->check($authuser, '/access', ['Sys.Audit'], 1);
-
- my $acl = $usercfg->{acl};
- foreach my $path (keys %$acl) {
- foreach my $type (qw(user group token)) {
- my $d = $acl->{$path}->{"${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;
- }});
-
-__PACKAGE__->register_method ({
- name => 'update_acl',
- protected => 1,
- path => '',
- method => 'PUT',
- permissions => {
- 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,
- },
- },
- },
- returns => { type => 'null' },
- code => sub {
- my ($param) = @_;
-
- 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;
-
- 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");
-
- my $propagate = 1;
-
- if (defined($param->{propagate})) {
- $propagate = $param->{propagate} ? 1 : 0;
- }
-
- foreach my $role (split_list($param->{roles})) {
- die "role '$role' does not exist\n"
- if !$cfg->{roles}->{$role};
-
- foreach my $group (split_list($param->{groups})) {
-
- die "group '$group' does not exist\n"
- if !$cfg->{groups}->{$group};
-
- if ($param->{delete}) {
- delete($cfg->{acl}->{$path}->{groups}->{$group}->{$role});
- } else {
- $cfg->{acl}->{$path}->{groups}->{$group}->{$role} = $propagate;
- }
- }
-
- 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};
-
- if ($param->{delete}) {
- delete($cfg->{acl}->{$path}->{users}->{$username}->{$role});
- } else {
- $cfg->{acl}->{$path}->{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);
-
- if ($param->{delete}) {
- delete $cfg->{acl}->{$path}->{tokens}->{$tokenid}->{$role};
- } else {
- $cfg->{acl}->{$path}->{tokens}->{$tokenid}->{$role} = $propagate;
- }
- }
- }
-
- cfs_write_file("user.cfg", $cfg);
- }, "ACL update failed");
-
- return undef;
- }});
-
-1;
+++ /dev/null
-package PVE::API2::AccessControl;
-
-use strict;
-use warnings;
-
-use JSON;
-use MIME::Base64;
-
-use PVE::Exception qw(raise raise_perm_exc raise_param_exc);
-use PVE::SafeSyslog;
-use PVE::RPCEnvironment;
-use PVE::Cluster qw(cfs_read_file);
-use PVE::DataCenterConfig;
-use PVE::RESTHandler;
-use PVE::AccessControl;
-use PVE::JSONSchema qw(get_standard_option);
-use PVE::API2::Domains;
-use PVE::API2::User;
-use PVE::API2::Group;
-use PVE::API2::Role;
-use PVE::API2::ACL;
-use PVE::Auth::Plugin;
-use PVE::OTP;
-use PVE::Tools;
-
-my $u2f_available = 0;
-eval {
- require PVE::U2F;
- $u2f_available = 1;
-};
-
-use base qw(PVE::RESTHandler);
-
-__PACKAGE__->register_method ({
- subclass => "PVE::API2::User",
- path => 'users',
-});
-
-__PACKAGE__->register_method ({
- subclass => "PVE::API2::Group",
- path => 'groups',
-});
-
-__PACKAGE__->register_method ({
- subclass => "PVE::API2::Role",
- path => 'roles',
-});
-
-__PACKAGE__->register_method ({
- subclass => "PVE::API2::ACL",
- path => 'acl',
-});
-
-__PACKAGE__->register_method ({
- subclass => "PVE::API2::Domains",
- path => 'domains',
-});
-
-__PACKAGE__->register_method ({
- name => 'index',
- path => '',
- method => 'GET',
- description => "Directory index.",
- permissions => {
- user => 'all',
- },
- parameters => {
- additionalProperties => 0,
- properties => {},
- },
- returns => {
- type => 'array',
- items => {
- type => "object",
- properties => {
- subdir => { type => 'string' },
- },
- },
- links => [ { rel => 'child', href => "{subdir}" } ],
- },
- code => sub {
- my ($param) = @_;
-
- my $res = [];
-
- my $ma = __PACKAGE__->method_attributes();
-
- foreach my $info (@$ma) {
- next if !$info->{subclass};
-
- my $subpath = $info->{match_re}->[0];
-
- push @$res, { subdir => $subpath };
- }
-
- push @$res, { subdir => 'ticket' };
- push @$res, { subdir => 'password' };
-
- return $res;
- }});
-
-
-my $verify_auth = sub {
- my ($rpcenv, $username, $pw_or_ticket, $otp, $path, $privs) = @_;
-
- my $normpath = PVE::AccessControl::normalize_path($path);
-
- my $ticketuser;
- 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
- } else {
- $username = PVE::AccessControl::authenticate_user($username, $pw_or_ticket, $otp);
- }
-
- my $privlist = [ PVE::Tools::split_list($privs) ];
- if (!($normpath && scalar(@$privlist) && $rpcenv->check($username, $normpath, $privlist))) {
- die "no permission ($path, $privs)\n";
- }
-
- return { username => $username };
-};
-
-my $create_ticket = sub {
- my ($rpcenv, $username, $pw_or_ticket, $otp) = @_;
-
- my ($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
- } else {
- ($username, $tfa_info) = PVE::AccessControl::authenticate_user($username, $pw_or_ticket, $otp);
- }
-
- my %extra;
- my $ticket_data = $username;
- if (defined($tfa_info)) {
- $extra{NeedTFA} = 1;
- if ($tfa_info->{type} eq 'u2f') {
- my $u2finfo = $tfa_info->{data};
- my $u2f = get_u2f_instance($rpcenv, $u2finfo->@{qw(publicKey keyHandle)});
- my $challenge = $u2f->auth_challenge()
- or die "failed to get u2f challenge\n";
- $challenge = decode_json($challenge);
- $extra{U2FChallenge} = $challenge;
- $ticket_data = "u2f!$username!$challenge->{challenge}";
- } else {
- # General half-login / 'missing 2nd factor' ticket:
- $ticket_data = "tfa!$username";
- }
- }
-
- my $ticket = PVE::AccessControl::assemble_ticket($ticket_data);
- my $csrftoken = PVE::AccessControl::assemble_csrf_prevention_token($username);
-
- return {
- ticket => $ticket,
- username => $username,
- CSRFPreventionToken => $csrftoken,
- %extra,
- };
-};
-
-my $compute_api_permission = sub {
- my ($rpcenv, $authuser) = @_;
-
- my $usercfg = $rpcenv->{user_cfg};
-
- 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|SDN\./,
- };
- map { $res->{$_} = {} } keys %$priv_re_map;
-
- my $required_paths = ['/', '/nodes', '/access/groups', '/vms', '/storage', '/sdn'];
-
- my $checked_paths = {};
- foreach my $path (@$required_paths, keys %{$usercfg->{acl}}) {
- next if $checked_paths->{$path};
- $checked_paths->{$path} = 1;
-
- my $path_perm = $rpcenv->permissions($authuser, $path);
-
- my $toplevel = ($path =~ /^\/(\w+)/) ? $1 : 'dc';
- if ($toplevel eq 'pool') {
- foreach my $priv (keys %$path_perm) {
- 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 $priv !~ m/^($priv_regex)/;
- $res->{$toplevel}->{$priv} = 1;
- }
- }
- }
-
- return $res;
-};
-
-__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,
- },
- returns => { type => "null" },
- code => sub { return undef; }});
-
-__PACKAGE__->register_method ({
- name => 'create_ticket',
- path => 'ticket',
- method => 'POST',
- permissions => {
- 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>\@<relam>.",
- 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,
- },
- }
- },
- 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
- }
- },
- code => sub {
- my ($param) = @_;
-
- my $username = $param->{username};
- $username .= "\@$param->{realm}" if $param->{realm};
-
- $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);
-
- if ($param->{path} && $param->{privs}) {
- $res = &$verify_auth($rpcenv, $username, $param->{password}, $param->{otp},
- $param->{path}, $param->{privs});
- } else {
- $res = &$create_ticket($rpcenv, $username, $param->{password}, $param->{otp});
- }
- };
- 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} = &$compute_api_permission($rpcenv, $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};
- }
-
- PVE::Cluster::log_msg('info', 'root@pam', "successful auth for user '$username'");
-
- return $res;
- }});
-
-__PACKAGE__->register_method ({
- name => 'change_password',
- path => 'password',
- method => 'PUT',
- permissions => {
- description => "Each user is allowed to change his own password. A user can change the password of another user if he has '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.",
- 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 => 5,
- maxLength => 64,
- },
- }
- },
- returns => { type => "null" },
- code => sub {
- my ($param) = @_;
-
- my $rpcenv = PVE::RPCEnvironment::get();
- my $authuser = $rpcenv->get_user();
-
- my ($userid, $ruid, $realm) = PVE::AccessControl::verify_username($param->{userid});
-
- $rpcenv->check_user_exist($userid);
-
- 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 its 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::Cluster::log_msg('info', 'root@pam', "changed password for user '$userid'");
-
- return undef;
- }});
-
-sub get_u2f_config() {
- die "u2f support not available\n" if !$u2f_available;
-
- my $dc = cfs_read_file('datacenter.cfg');
- my $u2f = $dc->{u2f};
- die "u2f not configured in datacenter.cfg\n" if !$u2f;
- return $u2f;
-}
-
-sub get_u2f_instance {
- my ($rpcenv, $publicKey, $keyHandle) = @_;
-
- # We store the public key base64 encoded (as the api provides it in binary)
- $publicKey = decode_base64($publicKey) if defined($publicKey);
-
- my $u2fconfig = get_u2f_config();
- my $u2f = PVE::U2F->new();
-
- # 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";
- }
- }
-
- my $appid = $u2fconfig->{appid} // $origin;
- $u2f->set_appid($appid);
- $u2f->set_origin($origin);
- $u2f->set_publicKey($publicKey) if defined($publicKey);
- $u2f->set_keyHandle($keyHandle) if defined($keyHandle);
- return $u2f;
-}
-
-sub verify_user_tfa_config {
- my ($type, $tfa_cfg, $value) = @_;
-
- if (!defined($type)) {
- die "missing tfa 'type'\n";
- }
-
- if ($type ne 'oath') {
- die "invalid type for custom tfa authentication\n";
- }
-
- my $secret = $tfa_cfg->{keys}
- 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";
- }
-
- my $digits = delete($tfa_cfg->{digits}) // 6;
- my $step = delete($tfa_cfg->{step}) // 30;
- # Maybe also this?
- # my $algorithm = delete($tfa_cfg->{algorithm}) // 'sha1';
-
- if (length(my $more = join(', ', keys %$tfa_cfg))) {
- die "unexpected tfa config keys: $more\n";
- }
-
- PVE::OTP::oath_verify_otp($value, $secret, $step, $digits);
-}
-
-__PACKAGE__->register_method ({
- name => 'change_tfa',
- path => 'tfa',
- method => 'PUT',
- permissions => {
- description => 'A user can change their own u2f or totp token.',
- 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's TFA settings
- description => "Change user u2f authentication.",
- parameters => {
- additionalProperties => 0,
- properties => {
- userid => get_standard_option('userid', {
- completion => \&PVE::AccessControl::complete_username,
- }),
- password => {
- optional => 1, # Only required if not root@pam
- description => "The current password.",
- type => 'string',
- minLength => 5,
- maxLength => 64,
- },
- action => {
- description => 'The action to perform',
- type => 'string',
- enum => [qw(delete new confirm)],
- },
- response => {
- optional => 1,
- description =>
- 'Either the the response to the current u2f registration challenge,'
- .' or, when adding TOTP, the currently valid TOTP value.',
- type => 'string',
- },
- key => {
- optional => 1,
- description => 'When adding TOTP, the shared secret value.',
- type => 'string',
- format => 'pve-tfa-secret',
- },
- config => {
- optional => 1,
- description => 'A TFA configuration. This must currently be of type TOTP of not set at all.',
- type => 'string',
- format => 'pve-tfa-config',
- maxLength => 128,
- },
- }
- },
- returns => { type => 'object' },
- code => sub {
- my ($param) = @_;
-
- my $rpcenv = PVE::RPCEnvironment::get();
- my $authuser = $rpcenv->get_user();
-
- my $action = delete $param->{action};
- my $response = delete $param->{response};
- my $password = delete($param->{password}) // '';
- my $key = delete($param->{key});
- my $config = delete($param->{config});
-
- my ($userid, $ruid, $realm) = PVE::AccessControl::verify_username($param->{userid});
- $rpcenv->check_user_exist($userid);
-
- # Only root may modify root
- raise_perm_exc() if $userid eq 'root@pam' && $authuser ne 'root@pam';
-
- # Regular users need to confirm their password to change u2f settings.
- if ($authuser ne 'root@pam') {
- raise_param_exc({ 'password' => 'password is required to modify u2f data' })
- if !defined($password);
- my $domain_cfg = cfs_read_file('domains.cfg');
- my $cfg = $domain_cfg->{ids}->{$realm};
- die "auth domain '$realm' does not exist\n" if !$cfg;
- my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
- $plugin->authenticate_user($cfg, $realm, $ruid, $password);
- }
-
- if ($action eq 'delete') {
- PVE::AccessControl::user_set_tfa($userid, $realm, undef, undef);
- PVE::Cluster::log_msg('info', $authuser, "deleted u2f data for user '$userid'");
- } elsif ($action eq 'new') {
- if (defined($config)) {
- $config = PVE::Auth::Plugin::parse_tfa_config($config);
- my $type = delete($config->{type});
- my $tfa_cfg = {
- keys => $key,
- config => $config,
- };
- verify_user_tfa_config($type, $tfa_cfg, $response);
- PVE::AccessControl::user_set_tfa($userid, $realm, $type, $tfa_cfg);
- } else {
- # The default is U2F:
- my $u2f = get_u2f_instance($rpcenv);
- my $challenge = $u2f->registration_challenge()
- or raise("failed to get u2f challenge");
- $challenge = decode_json($challenge);
- PVE::AccessControl::user_set_tfa($userid, $realm, 'u2f', $challenge);
- return $challenge;
- }
- } elsif ($action eq 'confirm') {
- raise_param_exc({ 'response' => "confirm action requires the 'response' parameter to be set" })
- if !defined($response);
-
- my ($type, $u2fdata) = PVE::AccessControl::user_get_tfa($userid, $realm);
- raise("no u2f data available")
- if (!defined($type) || $type ne 'u2f');
-
- my $challenge = $u2fdata->{challenge}
- or raise("no active challenge");
-
- my $u2f = get_u2f_instance($rpcenv);
- $u2f->set_challenge($challenge);
- my ($keyHandle, $publicKey) = $u2f->registration_verify($response);
- PVE::AccessControl::user_set_tfa($userid, $realm, 'u2f', {
- keyHandle => $keyHandle,
- publicKey => $publicKey, # already base64 encoded
- });
- } else {
- die "invalid action: $action\n";
- }
-
- return {};
- }});
-
-__PACKAGE__->register_method({
- name => 'verify_tfa',
- path => 'tfa',
- method => 'POST',
- permissions => { user => 'all' },
- protected => 1, # else we can't access shadow files
- allowtoken => 0, # we don't want tokens to access TFA information
- description => 'Finish a u2f challenge.',
- parameters => {
- additionalProperties => 0,
- properties => {
- response => {
- type => 'string',
- description => 'The response to the current authentication challenge.',
- },
- }
- },
- returns => {
- type => 'object',
- properties => {
- ticket => { type => 'string' },
- # cap
- }
- },
- code => sub {
- my ($param) = @_;
-
- my $rpcenv = PVE::RPCEnvironment::get();
- my $authuser = $rpcenv->get_user();
- my ($username, undef, $realm) = PVE::AccessControl::verify_username($authuser);
-
- my ($tfa_type, $tfa_data) = PVE::AccessControl::user_get_tfa($username, $realm);
- if (!defined($tfa_type)) {
- raise('no u2f data available');
- }
-
- eval {
- if ($tfa_type eq 'u2f') {
- my $challenge = $rpcenv->get_u2f_challenge()
- or raise('no active challenge');
-
- my $keyHandle = $tfa_data->{keyHandle};
- my $publicKey = $tfa_data->{publicKey};
- raise("incomplete u2f setup")
- if !defined($keyHandle) || !defined($publicKey);
-
- my $u2f = get_u2f_instance($rpcenv, $publicKey, $keyHandle);
- $u2f->set_challenge($challenge);
-
- my ($counter, $present) = $u2f->auth_verify($param->{response});
- # Do we want to do anything with these?
- } else {
- # sanity check before handing off to the verification code:
- my $keys = $tfa_data->{keys} or die "missing tfa keys\n";
- my $config = $tfa_data->{config} or die "bad tfa entry\n";
- PVE::AccessControl::verify_one_time_pw($tfa_type, $authuser, $keys, $config, $param->{response});
- }
- };
- if (my $err = $@) {
- my $clientip = $rpcenv->get_client_ip() || '';
- syslog('err', "authentication verification failure; rhost=$clientip user=$authuser msg=$err");
- die PVE::Exception->new("authentication failure\n", code => 401);
- }
-
- return {
- ticket => PVE::AccessControl::assemble_ticket($authuser),
- cap => &$compute_api_permission($rpcenv, $authuser),
- }
- }});
-
-__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. A user can dump the permissions of another user 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,
- }),
- },
- },
- returns => {
- type => 'object',
- description => 'Map of "path" => (Map of "privilege" => "propagate boolean").',
- },
- code => sub {
- my ($param) = @_;
-
- my $rpcenv = PVE::RPCEnvironment::get();
-
- my $userid = $param->{userid};
- if (defined($userid)) {
- $rpcenv->check($rpcenv->get_user(), '/access', ['Sys.Audit']);
- } else {
- $userid = $rpcenv->get_user();
- }
-
- 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);
- }
-
- return $res;
- }});
-
-1;
+++ /dev/null
-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) = @_;
-
- # always extract, add it with hook
- my $password = extract_param($param, 'password');
-
- 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;
-
- my $opts = $plugin->options();
- if (defined($password) && !defined($opts->{password})) {
- $password = undef;
- warn "ignoring password parameter";
- }
- $plugin->on_add_hook($realm, $config, password => $password);
-
- 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) = @_;
-
- # always extract, update in hook
- my $password = extract_param($param, 'password');
-
- 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);
-
- 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 $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};
- }
-
- my $opts = $plugin->options();
- if ($delete_pw || defined($password)) {
- $plugin->on_update_hook($realm, $config, password => $password);
- } else {
- $plugin->on_update_hook($realm, $config);
- }
-
- 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 "authentication domain '$realm' does not exist\n" if !$ids->{$realm};
-
- my $plugin = PVE::Auth::Plugin->lookup($ids->{$realm}->{type});
-
- $plugin->on_delete_hook($realm, $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 $cfg_defaults = {};
- if (defined(my $cfg_opts = $realmconfig->{'sync-defaults-options'})) {
- $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};
-
- $res->{$opt} = $param->{$opt} // $cfg_defaults->{$opt} // $fmt->{default};
-
- raise_param_exc({
- "$opt" => 'Not passed as parameter and not defined in realm default sync options.'
- }) if !defined($res->{$opt});
- }
- return $res;
-};
-
-__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',
- [ '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'),
- 'dry-run' => {
- description => "If set, does not write anything.",
- type => 'boolean',
- optional => 1,
- default => 0,
- },
- }),
- },
- returns => {
- description => 'Worker Task-UPID',
- type => 'string'
- },
- code => sub {
- my ($param) = @_;
-
- 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};
-
- 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 "(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);
- }
-
- 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);
- }
-
- 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);
- }});
-
-1;
+++ /dev/null
-package PVE::API2::Group;
-
-use strict;
-use warnings;
-use PVE::Cluster qw (cfs_read_file cfs_write_file);
-use PVE::AccessControl;
-use PVE::SafeSyslog;
-use PVE::RESTHandler;
-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-comment', { type => 'string', optional => 1 });
-
-__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',
- },
- parameters => {
- 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}" } ],
- },
- code => sub {
- my ($param) = @_;
-
- my $res = [];
-
- 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'];
-
- 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;
- }});
-
-__PACKAGE__->register_method ({
- name => 'create_group',
- protected => 1,
- path => '',
- method => 'POST',
- permissions => {
- 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'),
- },
- },
- returns => { type => 'null' },
- code => sub {
- my ($param) = @_;
-
- PVE::AccessControl::lock_user_config(
- sub {
-
- my $usercfg = cfs_read_file("user.cfg");
-
- my $group = $param->{groupid};
-
- die "group '$group' already exists\n"
- if $usercfg->{groups}->{$group};
-
- $usercfg->{groups}->{$group} = { users => {} };
-
- $usercfg->{groups}->{$group}->{comment} = $param->{comment} if $param->{comment};
-
-
- cfs_write_file("user.cfg", $usercfg);
- }, "create group failed");
-
- return undef;
- }});
-
-__PACKAGE__->register_method ({
- name => 'update_group',
- protected => 1,
- path => '{groupid}',
- method => 'PUT',
- permissions => {
- 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'),
- },
- },
- returns => { type => 'null' },
- code => sub {
- my ($param) = @_;
-
- PVE::AccessControl::lock_user_config(
- sub {
-
- my $usercfg = cfs_read_file("user.cfg");
-
- my $group = $param->{groupid};
-
- my $data = $usercfg->{groups}->{$group};
-
- die "group '$group' does not exist\n"
- if !$data;
-
- $data->{comment} = $param->{comment} if defined($param->{comment});
-
- cfs_write_file("user.cfg", $usercfg);
- }, "update group failed");
-
- return undef;
- }});
-
-__PACKAGE__->register_method ({
- name => 'read_group',
- path => '{groupid}',
- method => 'GET',
- permissions => {
- check => ['perm', '/access/groups', ['Sys.Audit', 'Group.Allocate'], any => 1],
- },
- description => "Get group configuration.",
- parameters => {
- 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')
- },
- },
- },
- code => sub {
- my ($param) = @_;
-
- my $group = $param->{groupid};
-
- my $usercfg = cfs_read_file("user.cfg");
-
- my $data = $usercfg->{groups}->{$group};
-
- die "group '$group' does not exist\n" if !$data;
-
- my $members = $data->{users} ? [ keys %{$data->{users}} ] : [];
-
- my $res = { members => $members };
-
- $res->{comment} = $data->{comment} if defined($data->{comment});
-
- return $res;
- }});
-
-
-__PACKAGE__->register_method ({
- name => 'delete_group',
- protected => 1,
- path => '{groupid}',
- method => 'DELETE',
- permissions => {
- check => ['perm', '/access/groups', ['Group.Allocate']],
- },
- description => "Delete group.",
- parameters => {
- additionalProperties => 0,
- properties => {
- groupid => get_standard_option('group-id'),
- }
- },
- returns => { type => 'null' },
- code => sub {
- my ($param) = @_;
-
- PVE::AccessControl::lock_user_config(
- sub {
-
- my $usercfg = cfs_read_file("user.cfg");
-
- my $group = $param->{groupid};
-
- die "group '$group' does not exist\n"
- if !$usercfg->{groups}->{$group};
-
- delete ($usercfg->{groups}->{$group});
-
- PVE::AccessControl::delete_group_acl($group, $usercfg);
-
- cfs_write_file("user.cfg", $usercfg);
- }, "delete group failed");
-
- return undef;
- }});
-
-1;
+++ /dev/null
-
-API2_SOURCES= \
- AccessControl.pm \
- Domains.pm \
- ACL.pm \
- Role.pm \
- Group.pm \
- User.pm
-
-.PHONY: install
-install:
- for i in ${API2_SOURCES}; do install -D -m 0644 $$i ${DESTDIR}${PERLDIR}/PVE/API2/$$i; done
+++ /dev/null
-package PVE::API2::Role;
-
-use strict;
-use warnings;
-use PVE::Cluster qw (cfs_read_file cfs_write_file);
-use PVE::AccessControl;
-use PVE::JSONSchema qw(get_standard_option register_standard_option);
-
-use PVE::SafeSyslog;
-
-use PVE::RESTHandler;
-
-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,
-});
-
-__PACKAGE__->register_method ({
- name => 'index',
- path => '',
- method => 'GET',
- description => "Role index.",
- permissions => {
- user => 'all',
- },
- parameters => {
- 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}" } ],
- },
- code => sub {
- my ($param) = @_;
-
- my $res = [];
-
- 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),
- };
- }
-
- return $res;
-}});
-
-__PACKAGE__->register_method ({
- name => 'create_role',
- protected => 1,
- path => '',
- method => 'POST',
- permissions => {
- 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'),
- },
- },
- returns => { type => 'null' },
- code => sub {
- my ($param) = @_;
-
- PVE::AccessControl::lock_user_config(
- sub {
-
- my $usercfg = cfs_read_file("user.cfg");
-
- my $role = $param->{roleid};
-
- die "role '$role' already exists\n"
- if $usercfg->{roles}->{$role};
-
- $usercfg->{roles}->{$role} = {};
-
- PVE::AccessControl::add_role_privs($role, $usercfg, $param->{privs});
-
- cfs_write_file("user.cfg", $usercfg);
- }, "create role failed");
-
- return undef;
-}});
-
-__PACKAGE__->register_method ({
- name => 'update_role',
- protected => 1,
- path => '{roleid}',
- method => 'PUT',
- permissions => {
- 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' },
- },
- },
- returns => { type => 'null' },
- code => sub {
- my ($param) = @_;
-
- my $role = $param->{roleid};
-
- 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");
-
- die "role '$role' does not exist\n"
- if !$usercfg->{roles}->{$role};
-
- $usercfg->{roles}->{$role} = {} if !$param->{append};
-
- PVE::AccessControl::add_role_privs($role, $usercfg, $param->{privs});
-
- cfs_write_file("user.cfg", $usercfg);
- }, "update role failed");
-
- return undef;
-}});
-
-__PACKAGE__->register_method ({
- name => 'read_role',
- path => '{roleid}',
- method => 'GET',
- permissions => {
- user => 'all',
- },
- description => "Get role configuration.",
- parameters => {
- additionalProperties => 0,
- properties => {
- roleid => get_standard_option('role-id'),
- },
- },
- returns => {
- type => "object",
- additionalProperties => 0,
- properties => PVE::AccessControl::create_priv_properties(),
- },
- code => sub {
- my ($param) = @_;
-
- my $usercfg = cfs_read_file("user.cfg");
-
- my $role = $param->{roleid};
-
- my $data = $usercfg->{roles}->{$role};
-
- die "role '$role' does not exist\n" if !$data;
-
- return $data;
- }
-});
-
-__PACKAGE__->register_method ({
- name => 'delete_role',
- protected => 1,
- path => '{roleid}',
- method => 'DELETE',
- permissions => {
- check => ['perm', '/access', ['Sys.Modify']],
- },
- description => "Delete role.",
- parameters => {
- additionalProperties => 0,
- properties => {
- roleid => get_standard_option('role-id'),
- },
- },
- returns => { type => 'null' },
- code => sub {
- my ($param) = @_;
-
- my $role = $param->{roleid};
-
- 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");
-
- die "role '$role' does not exist\n"
- if !$usercfg->{roles}->{$role};
-
- delete ($usercfg->{roles}->{$role});
-
- # fixme: delete role from acl?
-
- cfs_write_file("user.cfg", $usercfg);
- }, "delete role failed");
-
- return undef;
- }
-});
-
-1;
+++ /dev/null
-package PVE::API2::User;
-
-use strict;
-use warnings;
-use PVE::Exception qw(raise raise_perm_exc raise_param_exc);
-use PVE::Cluster qw (cfs_read_file cfs_write_file);
-use PVE::Tools qw(split_list extract_param);
-use PVE::AccessControl;
-use PVE::JSONSchema qw(get_standard_option register_standard_option);
-use PVE::TokenConfig;
-
-use PVE::SafeSyslog;
-
-use PVE::RESTHandler;
-
-use base qw(PVE::RESTHandler);
-
-register_standard_option('user-enable', {
- description => "Enable the account (default). You can set this to '0' to disable the account",
- type => 'boolean',
- optional => 1,
- default => 1,
-});
-register_standard_option('user-expire', {
- description => "Account expiration date (seconds since epoch). '0' means no expiration date.",
- type => 'integer',
- minimum => 0,
- optional => 1,
-});
-register_standard_option('user-firstname', { type => 'string', optional => 1 });
-register_standard_option('user-lastname', { type => 'string', optional => 1 });
-register_standard_option('user-email', { type => 'string', optional => 1, format => 'email-opt' });
-register_standard_option('user-comment', { type => 'string', optional => 1 });
-register_standard_option('user-keys', {
- description => "Keys for two factor auth (yubico).",
- type => 'string',
- optional => 1,
-});
-register_standard_option('group-list', {
- type => 'string', format => 'pve-groupid-list',
- optional => 1,
- completion => \&PVE::AccessControl::complete_group,
-});
-register_standard_option('token-subid', {
- type => 'string',
- pattern => $PVE::AccessControl::token_subid_regex,
- description => 'User-specific token identifier.',
-});
-register_standard_option('token-expire', {
- description => "API token expiration date (seconds since epoch). '0' means no expiration date.",
- type => 'integer',
- minimum => 0,
- optional => 1,
- default => 'same as user',
-});
-register_standard_option('token-privsep', {
- description => "Restrict API token privileges with separate ACLs (default), or give full privileges of corresponding user.",
- type => 'boolean',
- optional => 1,
- default => 1,
-});
-register_standard_option('token-comment', { type => 'string', optional => 1 });
-register_standard_option('token-info', {
- type => 'object',
- properties => {
- expire => get_standard_option('token-expire'),
- privsep => get_standard_option('token-privsep'),
- comment => get_standard_option('token-comment'),
- }
-});
-
-my $token_info_extend = sub {
- my ($props) = @_;
-
- my $obj = get_standard_option('token-info');
- my $base_props = $obj->{properties};
- $obj->{properties} = {};
-
- foreach my $prop (keys %$base_props) {
- $obj->{properties}->{$prop} = $base_props->{$prop};
- }
-
- foreach my $add_prop (keys %$props) {
- $obj->{properties}->{$add_prop} = $props->{$add_prop};
- }
-
- return $obj;
-};
-
-my $extract_user_data = sub {
- my ($data, $full) = @_;
-
- my $res = {};
-
- foreach my $prop (qw(enable expire firstname lastname email comment keys)) {
- $res->{$prop} = $data->{$prop} if defined($data->{$prop});
- }
-
- return $res if !$full;
-
- $res->{groups} = $data->{groups} ? [ keys %{$data->{groups}} ] : [];
- $res->{tokens} = $data->{tokens};
-
- return $res;
-};
-
-__PACKAGE__->register_method ({
- name => 'index',
- path => '',
- method => 'GET',
- description => "User index.",
- permissions => {
- description => "The returned list is restricted to users where you have 'User.Modify' or 'Sys.Audit' permissions on '/access/groups' or on a group the user belongs too. But it always includes the current (authenticated) user.",
- user => 'all',
- },
- parameters => {
- additionalProperties => 0,
- properties => {
- enabled => {
- type => 'boolean',
- description => "Optional filter for enable property.",
- optional => 1,
- },
- full => {
- type => 'boolean',
- description => "Include group and token information.",
- optional => 1,
- default => 0,
- }
- },
- },
- returns => {
- type => 'array',
- items => {
- type => "object",
- properties => {
- userid => get_standard_option('userid-completed'),
- enable => get_standard_option('user-enable'),
- expire => get_standard_option('user-expire'),
- firstname => get_standard_option('user-firstname'),
- lastname => get_standard_option('user-lastname'),
- email => get_standard_option('user-email'),
- comment => get_standard_option('user-comment'),
- keys => get_standard_option('user-keys'),
- groups => get_standard_option('group-list'),
- tokens => {
- type => 'array',
- optional => 1,
- items => $token_info_extend->({
- tokenid => get_standard_option('token-subid'),
- }),
- }
- },
- },
- links => [ { rel => 'child', href => "{userid}" } ],
- },
- code => sub {
- my ($param) = @_;
-
- my $rpcenv = PVE::RPCEnvironment::get();
- my $usercfg = $rpcenv->{user_cfg};
- my $authuser = $rpcenv->get_user();
-
- my $res = [];
-
- my $privs = [ 'User.Modify', 'Sys.Audit' ];
- my $canUserMod = $rpcenv->check_any($authuser, "/access/groups", $privs, 1);
- my $groups = $rpcenv->filter_groups($authuser, $privs, 1);
- my $allowed_users = $rpcenv->group_member_join([keys %$groups]);
-
- foreach my $user (keys %{$usercfg->{users}}) {
- if (!($canUserMod || $user eq $authuser)) {
- next if !$allowed_users->{$user};
- }
-
- my $entry = &$extract_user_data($usercfg->{users}->{$user}, $param->{full});
-
- if (defined($param->{enabled})) {
- next if $entry->{enable} && !$param->{enabled};
- next if !$entry->{enable} && $param->{enabled};
- }
-
- $entry->{groups} = join(',', @{$entry->{groups}}) if $entry->{groups};
- $entry->{tokens} = [ map { { tokenid => $_, %{$entry->{tokens}->{$_}} } } sort keys %{$entry->{tokens}} ]
- if defined($entry->{tokens});
-
- $entry->{userid} = $user;
- push @$res, $entry;
- }
-
- return $res;
- }});
-
-__PACKAGE__->register_method ({
- name => 'create_user',
- protected => 1,
- path => '',
- method => 'POST',
- permissions => {
- description => "You need 'Realm.AllocateUser' on '/access/realm/<realm>' on the realm of user <userid>, and 'User.Modify' permissions to '/access/groups/<group>' for any group specified (or 'User.Modify' on '/access/groups' if you pass no groups.",
- check => [ 'and',
- [ 'userid-param', 'Realm.AllocateUser'],
- [ 'userid-group', ['User.Modify'], groups_param => 1],
- ],
- },
- description => "Create new user.",
- parameters => {
- additionalProperties => 0,
- properties => {
- userid => get_standard_option('userid-completed'),
- enable => get_standard_option('user-enable'),
- expire => get_standard_option('user-expire'),
- firstname => get_standard_option('user-firstname'),
- lastname => get_standard_option('user-lastname'),
- email => get_standard_option('user-email'),
- comment => get_standard_option('user-comment'),
- keys => get_standard_option('user-keys'),
- password => {
- description => "Initial password.",
- type => 'string',
- optional => 1,
- minLength => 5,
- maxLength => 64
- },
- groups => get_standard_option('group-list'),
- },
- },
- returns => { type => 'null' },
- code => sub {
- my ($param) = @_;
-
- PVE::AccessControl::lock_user_config(sub {
- my ($username, $ruid, $realm) = PVE::AccessControl::verify_username($param->{userid});
-
- my $usercfg = cfs_read_file("user.cfg");
-
- # ensure "user exists" check works for case insensitive realms
- $username = PVE::AccessControl::lookup_username($username, 1);
- die "user '$username' already exists\n" if $usercfg->{users}->{$username};
-
- PVE::AccessControl::domain_set_password($realm, $ruid, $param->{password})
- if defined($param->{password});
-
- my $enable = defined($param->{enable}) ? $param->{enable} : 1;
- $usercfg->{users}->{$username} = { enable => $enable };
- $usercfg->{users}->{$username}->{expire} = $param->{expire} if $param->{expire};
-
- if ($param->{groups}) {
- foreach my $group (split_list($param->{groups})) {
- if ($usercfg->{groups}->{$group}) {
- PVE::AccessControl::add_user_group($username, $usercfg, $group);
- } else {
- die "no such group '$group'\n";
- }
- }
- }
-
- $usercfg->{users}->{$username}->{firstname} = $param->{firstname} if $param->{firstname};
- $usercfg->{users}->{$username}->{lastname} = $param->{lastname} if $param->{lastname};
- $usercfg->{users}->{$username}->{email} = $param->{email} if $param->{email};
- $usercfg->{users}->{$username}->{comment} = $param->{comment} if $param->{comment};
- $usercfg->{users}->{$username}->{keys} = $param->{keys} if $param->{keys};
-
- cfs_write_file("user.cfg", $usercfg);
- }, "create user failed");
-
- return undef;
- }});
-
-__PACKAGE__->register_method ({
- name => 'read_user',
- path => '{userid}',
- method => 'GET',
- description => "Get user configuration.",
- permissions => {
- check => ['userid-group', ['User.Modify', 'Sys.Audit']],
- },
- parameters => {
- additionalProperties => 0,
- properties => {
- userid => get_standard_option('userid-completed'),
- },
- },
- returns => {
- additionalProperties => 0,
- properties => {
- enable => get_standard_option('user-enable'),
- expire => get_standard_option('user-expire'),
- firstname => get_standard_option('user-firstname'),
- lastname => get_standard_option('user-lastname'),
- email => get_standard_option('user-email'),
- comment => get_standard_option('user-comment'),
- keys => get_standard_option('user-keys'),
- groups => {
- type => 'array',
- optional => 1,
- items => {
- type => 'string',
- format => 'pve-groupid',
- },
- },
- tokens => {
- optional => 1,
- type => 'object',
- },
- },
- type => "object"
- },
- code => sub {
- my ($param) = @_;
-
- my ($username, undef, $domain) =
- PVE::AccessControl::verify_username($param->{userid});
-
- my $usercfg = cfs_read_file("user.cfg");
-
- my $data = PVE::AccessControl::check_user_exist($usercfg, $username);
-
- return &$extract_user_data($data, 1);
- }});
-
-__PACKAGE__->register_method ({
- name => 'update_user',
- protected => 1,
- path => '{userid}',
- method => 'PUT',
- permissions => {
- check => ['userid-group', ['User.Modify'], groups_param => 1 ],
- },
- description => "Update user configuration.",
- parameters => {
- additionalProperties => 0,
- properties => {
- userid => get_standard_option('userid-completed'),
- enable => get_standard_option('user-enable'),
- expire => get_standard_option('user-expire'),
- firstname => get_standard_option('user-firstname'),
- lastname => get_standard_option('user-lastname'),
- email => get_standard_option('user-email'),
- comment => get_standard_option('user-comment'),
- keys => get_standard_option('user-keys'),
- groups => get_standard_option('group-list'),
- append => {
- type => 'boolean',
- optional => 1,
- requires => 'groups',
- },
- },
- },
- returns => { type => 'null' },
- code => sub {
- my ($param) = @_;
-
- my ($username, $ruid, $realm) =
- PVE::AccessControl::verify_username($param->{userid});
-
- PVE::AccessControl::lock_user_config(
- sub {
-
- my $usercfg = cfs_read_file("user.cfg");
-
- PVE::AccessControl::check_user_exist($usercfg, $username);
-
- $usercfg->{users}->{$username}->{enable} = $param->{enable} if defined($param->{enable});
-
- $usercfg->{users}->{$username}->{expire} = $param->{expire} if defined($param->{expire});
-
- PVE::AccessControl::delete_user_group($username, $usercfg)
- if (!$param->{append} && defined($param->{groups}));
-
- if ($param->{groups}) {
- foreach my $group (split_list($param->{groups})) {
- if ($usercfg->{groups}->{$group}) {
- PVE::AccessControl::add_user_group($username, $usercfg, $group);
- } else {
- die "no such group '$group'\n";
- }
- }
- }
-
- $usercfg->{users}->{$username}->{firstname} = $param->{firstname} if defined($param->{firstname});
- $usercfg->{users}->{$username}->{lastname} = $param->{lastname} if defined($param->{lastname});
- $usercfg->{users}->{$username}->{email} = $param->{email} if defined($param->{email});
- $usercfg->{users}->{$username}->{comment} = $param->{comment} if defined($param->{comment});
- $usercfg->{users}->{$username}->{keys} = $param->{keys} if defined($param->{keys});
-
- cfs_write_file("user.cfg", $usercfg);
- }, "update user failed");
-
- return undef;
- }});
-
-__PACKAGE__->register_method ({
- name => 'delete_user',
- protected => 1,
- path => '{userid}',
- method => 'DELETE',
- description => "Delete user.",
- permissions => {
- check => [ 'and',
- [ 'userid-param', 'Realm.AllocateUser'],
- [ 'userid-group', ['User.Modify']],
- ],
- },
- parameters => {
- additionalProperties => 0,
- properties => {
- userid => get_standard_option('userid-completed'),
- }
- },
- returns => { type => 'null' },
- code => sub {
- my ($param) = @_;
-
- my $rpcenv = PVE::RPCEnvironment::get();
- my $authuser = $rpcenv->get_user();
-
- my ($userid, $ruid, $realm) =
- PVE::AccessControl::verify_username($param->{userid});
-
- PVE::AccessControl::lock_user_config(
- sub {
-
- my $usercfg = cfs_read_file("user.cfg");
-
- my $domain_cfg = cfs_read_file('domains.cfg');
- if (my $cfg = $domain_cfg->{ids}->{$realm}) {
- my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
- $plugin->delete_user($cfg, $realm, $ruid);
- }
-
- # Remove TFA data before removing the user entry as the user entry tells us whether
- # we need ot update priv/tfa.cfg.
- PVE::AccessControl::user_set_tfa($userid, $realm, undef, undef, $usercfg, $domain_cfg);
-
- delete $usercfg->{users}->{$userid};
-
- PVE::AccessControl::delete_user_group($userid, $usercfg);
- PVE::AccessControl::delete_user_acl($userid, $usercfg);
- cfs_write_file("user.cfg", $usercfg);
- }, "delete user failed");
-
- return undef;
- }});
-
-__PACKAGE__->register_method ({
- name => 'read_user_tfa_type',
- path => '{userid}/tfa',
- method => 'GET',
- protected => 1,
- description => "Get user TFA types (Personal and Realm).",
- permissions => {
- check => [ 'or',
- ['userid-param', 'self'],
- ['userid-group', ['User.Modify', 'Sys.Audit']],
- ],
- },
- parameters => {
- additionalProperties => 0,
- properties => {
- userid => get_standard_option('userid-completed'),
- },
- },
- returns => {
- additionalProperties => 0,
- properties => {
- realm => {
- type => 'string',
- enum => [qw(oath yubico)],
- description => "The type of TFA the users realm has set, if any.",
- optional => 1,
- },
- user => {
- type => 'string',
- enum => [qw(oath u2f)],
- description => "The type of TFA the user has set, if any.",
- optional => 1,
- },
- },
- type => "object"
- },
- code => sub {
- my ($param) = @_;
-
- my ($username, undef, $realm) = PVE::AccessControl::verify_username($param->{userid});
-
-
- 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_tfa = PVE::Auth::Plugin::parse_tfa_config($realm_cfg->{tfa})
- if $realm_cfg->{tfa};
-
- my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
- my $tfa = $tfa_cfg->{users}->{$username};
-
- my $res = {};
- $res->{realm} = $realm_tfa->{type} if $realm_tfa->{type};
- $res->{user} = $tfa->{type} if $tfa->{type};
- return $res;
- }});
-
-__PACKAGE__->register_method ({
- name => 'token_index',
- path => '{userid}/token',
- method => 'GET',
- description => "Get user API tokens.",
- permissions => {
- check => ['or',
- ['userid-param', 'self'],
- ['perm', '/access/users/{userid}', ['User.Modify']],
- ],
- },
- parameters => {
- additionalProperties => 0,
- properties => {
- userid => get_standard_option('userid-completed'),
- },
- },
- returns => {
- type => "array",
- items => $token_info_extend->({
- tokenid => get_standard_option('token-subid'),
- }),
- links => [ { rel => 'child', href => "{tokenid}" } ],
- },
- code => sub {
- my ($param) = @_;
-
- my $userid = PVE::AccessControl::verify_username($param->{userid});
- my $usercfg = cfs_read_file("user.cfg");
-
- my $user = PVE::AccessControl::check_user_exist($usercfg, $userid);
-
- my $tokens = $user->{tokens} // {};
- return [ map { $tokens->{$_}->{tokenid} = $_; $tokens->{$_} } keys %$tokens];
- }});
-
-__PACKAGE__->register_method ({
- name => 'read_token',
- path => '{userid}/token/{tokenid}',
- method => 'GET',
- description => "Get specific API token information.",
- permissions => {
- check => ['or',
- ['userid-param', 'self'],
- ['perm', '/access/users/{userid}', ['User.Modify']],
- ],
- },
- parameters => {
- additionalProperties => 0,
- properties => {
- userid => get_standard_option('userid-completed'),
- tokenid => get_standard_option('token-subid'),
- },
- },
- returns => get_standard_option('token-info'),
- code => sub {
- my ($param) = @_;
-
- my $userid = PVE::AccessControl::verify_username($param->{userid});
- my $tokenid = $param->{tokenid};
-
- my $usercfg = cfs_read_file("user.cfg");
-
- return PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid);
- }});
-
-__PACKAGE__->register_method ({
- name => 'generate_token',
- path => '{userid}/token/{tokenid}',
- method => 'POST',
- description => "Generate a new API token for a specific user. NOTE: returns API token value, which needs to be stored as it cannot be retrieved afterwards!",
- protected => 1,
- permissions => {
- check => ['or',
- ['userid-param', 'self'],
- ['perm', '/access/users/{userid}', ['User.Modify']],
- ],
- },
- parameters => {
- additionalProperties => 0,
- properties => {
- userid => get_standard_option('userid-completed'),
- tokenid => get_standard_option('token-subid'),
- expire => get_standard_option('token-expire'),
- privsep => get_standard_option('token-privsep'),
- comment => get_standard_option('token-comment'),
- },
- },
- returns => {
- additionalProperties => 0,
- type => "object",
- properties => {
- info => get_standard_option('token-info'),
- value => {
- type => 'string',
- description => 'API token value used for authentication.',
- },
- 'full-tokenid' => {
- type => 'string',
- format_description => '<userid>!<tokenid>',
- description => 'The full token id.',
- },
- },
- },
- code => sub {
- my ($param) = @_;
-
- my $userid = PVE::AccessControl::verify_username(extract_param($param, 'userid'));
- my $tokenid = extract_param($param, 'tokenid');
-
- my $usercfg = cfs_read_file("user.cfg");
-
- my $token = PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid, 1);
- my ($full_tokenid, $value);
-
- PVE::AccessControl::check_user_exist($usercfg, $userid);
- raise_param_exc({ 'tokenid' => 'Token already exists.' }) if defined($token);
-
- my $generate_and_add_token = sub {
- $usercfg = cfs_read_file("user.cfg");
- PVE::AccessControl::check_user_exist($usercfg, $userid);
- die "Token already exists.\n" if defined(PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid, 1));
-
- $full_tokenid = PVE::AccessControl::join_tokenid($userid, $tokenid);
- $value = PVE::TokenConfig::generate_token($full_tokenid);
-
- $token = {};
- $token->{privsep} = defined($param->{privsep}) ? $param->{privsep} : 1;
- $token->{expire} = $param->{expire} if defined($param->{expire});
- $token->{comment} = $param->{comment} if $param->{comment};
-
- $usercfg->{users}->{$userid}->{tokens}->{$tokenid} = $token;
- cfs_write_file("user.cfg", $usercfg);
- };
-
- PVE::AccessControl::lock_user_config($generate_and_add_token, 'generating token failed');
-
- return {
- info => $token,
- value => $value,
- 'full-tokenid' => $full_tokenid,
- };
- }});
-
-
-__PACKAGE__->register_method ({
- name => 'update_token_info',
- path => '{userid}/token/{tokenid}',
- method => 'PUT',
- description => "Update API token for a specific user.",
- protected => 1,
- permissions => {
- check => ['or',
- ['userid-param', 'self'],
- ['perm', '/access/users/{userid}', ['User.Modify']],
- ],
- },
- parameters => {
- additionalProperties => 0,
- properties => {
- userid => get_standard_option('userid-completed'),
- tokenid => get_standard_option('token-subid'),
- expire => get_standard_option('token-expire'),
- privsep => get_standard_option('token-privsep'),
- comment => get_standard_option('token-comment'),
- },
- },
- returns => get_standard_option('token-info', { description => "Updated token information." }),
- code => sub {
- my ($param) = @_;
-
- my $userid = PVE::AccessControl::verify_username(extract_param($param, 'userid'));
- my $tokenid = extract_param($param, 'tokenid');
-
- my $usercfg = cfs_read_file("user.cfg");
- my $token = PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid);
-
- my $update_token = sub {
- $usercfg = cfs_read_file("user.cfg");
- $token = PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid);
-
- my $full_tokenid = PVE::AccessControl::join_tokenid($userid, $tokenid);
-
- $token->{privsep} = $param->{privsep} if defined($param->{privsep});
- $token->{expire} = $param->{expire} if defined($param->{expire});
- $token->{comment} = $param->{comment} if $param->{comment};
-
- $usercfg->{users}->{$userid}->{tokens}->{$tokenid} = $token;
- cfs_write_file("user.cfg", $usercfg);
- };
-
- PVE::AccessControl::lock_user_config($update_token, 'updating token info failed');
-
- return $token;
- }});
-
-
-__PACKAGE__->register_method ({
- name => 'remove_token',
- path => '{userid}/token/{tokenid}',
- method => 'DELETE',
- description => "Remove API token for a specific user.",
- protected => 1,
- permissions => {
- check => ['or',
- ['userid-param', 'self'],
- ['perm', '/access/users/{userid}', ['User.Modify']],
- ],
- },
- parameters => {
- additionalProperties => 0,
- properties => {
- userid => get_standard_option('userid-completed'),
- tokenid => get_standard_option('token-subid'),
- },
- },
- returns => { type => 'null' },
- code => sub {
- my ($param) = @_;
-
- my $userid = PVE::AccessControl::verify_username(extract_param($param, 'userid'));
- my $tokenid = extract_param($param, 'tokenid');
-
- my $usercfg = cfs_read_file("user.cfg");
- my $token = PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid);
-
- my $update_token = sub {
- $usercfg = cfs_read_file("user.cfg");
-
- PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid);
-
- my $full_tokenid = PVE::AccessControl::join_tokenid($userid, $tokenid);
- PVE::TokenConfig::delete_token($full_tokenid);
- delete $usercfg->{users}->{$userid}->{tokens}->{$tokenid};
-
- cfs_write_file("user.cfg", $usercfg);
- };
-
- PVE::AccessControl::lock_user_config($update_token, 'deleting token failed');
-
- return;
- }});
-1;
+++ /dev/null
-package PVE::AccessControl;
-
-use strict;
-use warnings;
-use Encode;
-use Crypt::OpenSSL::Random;
-use Crypt::OpenSSL::RSA;
-use Net::SSLeay;
-use Net::IP;
-use MIME::Base64;
-use Digest::SHA;
-use IO::File;
-use File::stat;
-use JSON;
-
-use PVE::OTP;
-use PVE::Ticket;
-use PVE::Tools qw(run_command lock_file file_get_contents split_list safe_print);
-use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_write_file cfs_lock_file);
-use PVE::JSONSchema qw(register_standard_option get_standard_option);
-
-use PVE::Auth::Plugin;
-use PVE::Auth::AD;
-use PVE::Auth::LDAP;
-use PVE::Auth::PVE;
-use PVE::Auth::PAM;
-
-# load and initialize all plugins
-
-PVE::Auth::AD->register();
-PVE::Auth::LDAP->register();
-PVE::Auth::PVE->register();
-PVE::Auth::PAM->register();
-PVE::Auth::Plugin->init();
-
-# $authdir must be writable by root only!
-my $confdir = "/etc/pve";
-my $authdir = "$confdir/priv";
-
-my $pve_www_key_fn = "$confdir/pve-www.key";
-
-my $pve_auth_key_files = {
- priv => "$authdir/authkey.key",
- pub => "$confdir/authkey.pub",
- pubold => "$confdir/authkey.pub.old",
-};
-
-my $pve_auth_key_cache = {};
-
-my $ticket_lifetime = 3600 * 2; # 2 hours
-my $auth_graceperiod = 60 * 5; # 5 minutes
-my $authkey_lifetime = 3600 * 24; # rotate every 24 hours
-
-Crypt::OpenSSL::RSA->import_random_seed();
-
-cfs_register_file('user.cfg',
- \&parse_user_config,
- \&write_user_config);
-cfs_register_file('priv/tfa.cfg',
- \&parse_priv_tfa_config,
- \&write_priv_tfa_config);
-
-sub verify_username {
- PVE::Auth::Plugin::verify_username(@_);
-}
-
-sub pve_verify_realm {
- PVE::Auth::Plugin::pve_verify_realm(@_);
-}
-
-sub lock_user_config {
- my ($code, $errmsg) = @_;
-
- cfs_lock_file("user.cfg", undef, $code);
- if (my $err = $@) {
- $errmsg ? die "$errmsg: $err" : die $err;
- }
-}
-
-my $cache_read_key = sub {
- my ($type) = @_;
-
- my $path = $pve_auth_key_files->{$type};
-
- my $read_key_and_mtime = sub {
- my $fh = IO::File->new($path, "r");
-
- return undef if !defined($fh);
-
- my $st = stat($fh);
- my $pem = PVE::Tools::safe_read_from($fh, 0, 0, $path);
-
- close $fh;
-
- my $key;
- if ($type eq 'pub' || $type eq 'pubold') {
- $key = eval { Crypt::OpenSSL::RSA->new_public_key($pem); };
- } elsif ($type eq 'priv') {
- $key = eval { Crypt::OpenSSL::RSA->new_private_key($pem); };
- } else {
- die "Invalid authkey type '$type'\n";
- }
-
- return { key => $key, mtime => $st->mtime };
- };
-
- if (!defined($pve_auth_key_cache->{$type})) {
- $pve_auth_key_cache->{$type} = $read_key_and_mtime->();
- } else {
- my $st = stat($path);
- if (!$st || $st->mtime != $pve_auth_key_cache->{$type}->{mtime}) {
- $pve_auth_key_cache->{$type} = $read_key_and_mtime->();
- }
- }
-
- return $pve_auth_key_cache->{$type};
-};
-
-sub get_pubkey {
- my ($old) = @_;
-
- my $type = $old ? 'pubold' : 'pub';
-
- my $res = $cache_read_key->($type);
- return undef if !defined($res);
-
- return wantarray ? ($res->{key}, $res->{mtime}) : $res->{key};
-}
-
-sub get_privkey {
- my $res = $cache_read_key->('priv');
-
- if (!defined($res) || !check_authkey(1)) {
- rotate_authkey();
- $res = $cache_read_key->('priv');
- }
-
- return wantarray ? ($res->{key}, $res->{mtime}) : $res->{key};
-}
-
-sub check_authkey {
- my ($quiet) = @_;
-
- # skip check if non-quorate, as rotation is not possible anyway
- return 1 if !PVE::Cluster::check_cfs_quorum(1);
-
- my ($pub_key, $mtime) = get_pubkey();
- if (!$pub_key) {
- warn "auth key pair missing, generating new one..\n" if !$quiet;
- return 0;
- } else {
- my $now = time();
- if ($now - $mtime >= $authkey_lifetime) {
- warn "auth key pair too old, rotating..\n" if !$quiet;;
- return 0;
- } elsif ($mtime > $now + $auth_graceperiod) {
- # a nodes RTC had a time set in the future during key generation -> ticket
- # validity is clamped to 0+5 min grace period until now >= mtime again
- my (undef, $old_mtime) = get_pubkey(1);
- if ($old_mtime && $mtime >= $old_mtime && $mtime - $old_mtime < $ticket_lifetime) {
- warn "auth key pair generated in the future (key $mtime > host $now),"
- ." but old key still exists and in valid grace period so avoid automatic"
- ." fixup. Cluster time not in sync?\n" if !$quiet;
- return 1;
- }
- warn "auth key pair generated in the future (key $mtime > host $now), rotating..\n" if !$quiet;
- return 0;
- } else {
- warn "auth key new enough, skipping rotation\n" if !$quiet;;
- return 1;
- }
- }
-}
-
-sub rotate_authkey {
- return if $authkey_lifetime == 0;
-
- PVE::Cluster::cfs_lock_authkey(undef, sub {
- # re-check with lock to avoid double rotation in clusters
- return if check_authkey();
-
- my $old = get_pubkey();
- my $new = Crypt::OpenSSL::RSA->generate_key(2048);
-
- if ($old) {
- eval {
- my $pem = $old->get_public_key_x509_string();
- # mtime is used for caching and ticket age range calculation
- PVE::Tools::file_set_contents($pve_auth_key_files->{pubold}, $pem);
- };
- die "Failed to store old auth key: $@\n" if $@;
- }
-
- eval {
- my $pem = $new->get_public_key_x509_string();
- # mtime is used for caching and ticket age range calculation,
- # should be close to that of pubold above
- PVE::Tools::file_set_contents($pve_auth_key_files->{pub}, $pem);
- };
- if ($@) {
- if ($old) {
- warn "Failed to store new auth key - $@\n";
- warn "Reverting to previous auth key\n";
- eval {
- my $pem = $old->get_public_key_x509_string();
- PVE::Tools::file_set_contents($pve_auth_key_files->{pub}, $pem);
- };
- die "Failed to restore old auth key: $@\n" if $@;
- } else {
- die "Failed to store new auth key - $@\n";
- }
- }
-
- eval {
- my $pem = $new->get_private_key_string();
- PVE::Tools::file_set_contents($pve_auth_key_files->{priv}, $pem);
- };
- if ($@) {
- warn "Failed to store new auth key - $@\n";
- warn "Deleting auth key to force regeneration\n";
- unlink $pve_auth_key_files->{pub};
- unlink $pve_auth_key_files->{priv};
- }
- });
- die $@ if $@;
-}
-
-PVE::JSONSchema::register_standard_option('tokenid', {
- description => "API token identifier.",
- type => "string",
- format => "pve-tokenid",
-});
-
-our $token_subid_regex = $PVE::Auth::Plugin::realm_regex;
-
-# username@realm username realm tokenid
-our $token_full_regex = qr/((${PVE::Auth::Plugin::user_regex})\@(${PVE::Auth::Plugin::realm_regex}))!(${token_subid_regex})/;
-
-our $userid_or_token_regex = qr/^$PVE::Auth::Plugin::user_regex\@$PVE::Auth::Plugin::realm_regex(?:!$token_subid_regex)?$/;
-
-sub split_tokenid {
- my ($tokenid, $noerr) = @_;
-
- if ($tokenid =~ /^${token_full_regex}$/) {
- return ($1, $4);
- }
-
- die "'$tokenid' is not a valid token ID - not able to split into user and token parts\n" if !$noerr;
-
- return undef;
-}
-
-sub join_tokenid {
- my ($username, $tokensubid) = @_;
-
- my $joined = "${username}!${tokensubid}";
-
- return pve_verify_tokenid($joined);
-}
-
-PVE::JSONSchema::register_format('pve-tokenid', \&pve_verify_tokenid);
-sub pve_verify_tokenid {
- my ($tokenid, $noerr) = @_;
-
- if ($tokenid =~ /^${token_full_regex}$/) {
- return wantarray ? ($tokenid, $2, $3, $4) : $tokenid;
- }
-
- die "value '$tokenid' does not look like a valid token ID\n" if !$noerr;
-
- return undef;
-}
-
-
-my $csrf_prevention_secret;
-my $csrf_prevention_secret_legacy;
-my $get_csrfr_secret = sub {
- if (!$csrf_prevention_secret) {
- my $input = PVE::Tools::file_get_contents($pve_www_key_fn);
- $csrf_prevention_secret = Digest::SHA::hmac_sha256_base64($input);
- $csrf_prevention_secret_legacy = Digest::SHA::sha1_base64($input);
- }
- return $csrf_prevention_secret;
-};
-
-sub assemble_csrf_prevention_token {
- my ($username) = @_;
-
- my $secret = &$get_csrfr_secret();
-
- return PVE::Ticket::assemble_csrf_prevention_token ($secret, $username);
-}
-
-sub verify_csrf_prevention_token {
- my ($username, $token, $noerr) = @_;
-
- my $secret = $get_csrfr_secret->();
-
- # FIXME: remove with PVE 7 and/or refactor all into PVE::Ticket ?
- if ($token =~ m/^([A-Z0-9]{8}):(\S+)$/) {
- my $sig = $2;
- if (length($sig) == 27) {
- # the legacy secret got populated by above get_csrfr_secret call
- $secret = $csrf_prevention_secret_legacy;
- }
- }
-
- return PVE::Ticket::verify_csrf_prevention_token(
- $secret, $username, $token, -$auth_graceperiod, $ticket_lifetime, $noerr);
-}
-
-my $get_ticket_age_range = sub {
- my ($now, $mtime, $rotated) = @_;
-
- my $key_age = $now - $mtime;
- $key_age = 0 if $key_age < 0;
-
- my $min = -$auth_graceperiod;
- my $max = $ticket_lifetime;
-
- if ($rotated) {
- # ticket creation after rotation is not allowed
- $min = $key_age - $auth_graceperiod;
- } else {
- if ($key_age > $authkey_lifetime && $authkey_lifetime > 0) {
- if (PVE::Cluster::check_cfs_quorum(1)) {
- # key should have been rotated, clamp range accordingly
- $min = $key_age - $authkey_lifetime;
- } else {
- warn "Cluster not quorate - extending auth key lifetime!\n";
- }
- }
-
- $max = $key_age + $auth_graceperiod if $key_age < $ticket_lifetime;
- }
-
- return undef if $min > $ticket_lifetime;
- return ($min, $max);
-};
-
-sub assemble_ticket {
- my ($data) = @_;
-
- my $rsa_priv = get_privkey();
-
- return PVE::Ticket::assemble_rsa_ticket($rsa_priv, 'PVE', $data);
-}
-
-sub verify_ticket {
- my ($ticket, $noerr) = @_;
-
- my $now = time();
-
- my $check = sub {
- my ($old) = @_;
-
- my ($rsa_pub, $rsa_mtime) = get_pubkey($old);
- return undef if !$rsa_pub;
-
- my ($min, $max) = $get_ticket_age_range->($now, $rsa_mtime, $old);
- return undef if !defined($min);
-
- return PVE::Ticket::verify_rsa_ticket(
- $rsa_pub, 'PVE', $ticket, undef, $min, $max, 1);
- };
-
- my ($data, $age) = $check->();
-
- # check with old, rotated key if current key failed
- ($data, $age) = $check->(1) if !defined($data);
-
- my $auth_failure = sub {
- if ($noerr) {
- return undef;
- } else {
- # raise error via undef ticket
- PVE::Ticket::verify_rsa_ticket(undef, 'PVE');
- }
- };
-
- if (!defined($data)) {
- return $auth_failure->();
- }
-
- my ($username, $tfa_info);
- if ($data =~ m{^u2f!([^!]+)!([0-9a-zA-Z/.=_\-+]+)$}) {
- # Ticket for u2f-users:
- ($username, my $challenge) = ($1, $2);
- if ($challenge eq 'verified') {
- # u2f challenge was completed
- $challenge = undef;
- } elsif (!wantarray) {
- # The caller is not aware there could be an ongoing challenge,
- # so we treat this ticket as invalid:
- return $auth_failure->();
- }
- $tfa_info = {
- type => 'u2f',
- challenge => $challenge,
- };
- } elsif ($data =~ /^tfa!(.*)$/) {
- # TOTP and Yubico don't require a challenge so this is the generic
- # 'missing 2nd factor ticket'
- $username = $1;
- $tfa_info = { type => 'tfa' };
- } else {
- # Regular ticket (full access)
- $username = $data;
- }
-
- return undef if !PVE::Auth::Plugin::verify_username($username, $noerr);
-
- return wantarray ? ($username, $age, $tfa_info) : $username;
-}
-
-sub verify_token {
- my ($api_token) = @_;
-
- die "no API token specified\n" if !$api_token;
-
- my ($tokenid, $value);
- if ($api_token =~ /^(.*)=(.*)$/) {
- $tokenid = $1;
- $value = $2;
- } else {
- die "no tokenid specified\n";
- }
-
- my ($username, $token) = split_tokenid($tokenid);
-
- my $usercfg = cfs_read_file('user.cfg');
- check_user_enabled($usercfg, $username);
- check_token_exist($usercfg, $username, $token);
-
- my $ctime = time();
-
- my $user = $usercfg->{users}->{$username};
- die "account expired\n" if $user->{expire} && ($user->{expire} < $ctime);
-
- my $token_info = $user->{tokens}->{$token};
- die "token expired\n" if $token_info->{expire} && ($token_info->{expire} < $ctime);
-
- die "invalid token value!\n" if !PVE::Cluster::verify_token($tokenid, $value);
-
- return wantarray ? ($tokenid) : $tokenid;
-}
-
-
-# VNC tickets
-# - they do not contain the username in plain text
-# - they are restricted to a specific resource path (example: '/vms/100')
-sub assemble_vnc_ticket {
- my ($username, $path) = @_;
-
- my $rsa_priv = get_privkey();
-
- $path = normalize_path($path);
-
- my $secret_data = "$username:$path";
-
- return PVE::Ticket::assemble_rsa_ticket(
- $rsa_priv, 'PVEVNC', undef, $secret_data);
-}
-
-sub verify_vnc_ticket {
- my ($ticket, $username, $path, $noerr) = @_;
-
- my $secret_data = "$username:$path";
-
- my ($rsa_pub, $rsa_mtime) = get_pubkey();
- if (!$rsa_pub || (time() - $rsa_mtime > $authkey_lifetime && $authkey_lifetime > 0)) {
- if ($noerr) {
- return undef;
- } else {
- # raise error via undef ticket
- PVE::Ticket::verify_rsa_ticket($rsa_pub, 'PVEVNC');
- }
- }
-
- return PVE::Ticket::verify_rsa_ticket(
- $rsa_pub, 'PVEVNC', $ticket, $secret_data, -20, 40, $noerr);
-}
-
-sub assemble_spice_ticket {
- my ($username, $vmid, $node) = @_;
-
- my $secret = &$get_csrfr_secret();
-
- return PVE::Ticket::assemble_spice_ticket(
- $secret, $username, $vmid, $node);
-}
-
-sub verify_spice_connect_url {
- my ($connect_str) = @_;
-
- my $secret = &$get_csrfr_secret();
-
- return PVE::Ticket::verify_spice_connect_url($secret, $connect_str);
-}
-
-sub read_x509_subject_spice {
- my ($filename) = @_;
-
- # read x509 subject
- my $bio = Net::SSLeay::BIO_new_file($filename, 'r');
- die "Could not open $filename using OpenSSL\n"
- if !$bio;
-
- my $x509 = Net::SSLeay::PEM_read_bio_X509($bio);
- Net::SSLeay::BIO_free($bio);
-
- die "Could not parse X509 certificate in $filename\n"
- if !$x509;
-
- my $nameobj = Net::SSLeay::X509_get_subject_name($x509);
- my $subject = Net::SSLeay::X509_NAME_oneline($nameobj);
- Net::SSLeay::X509_free($x509);
-
- # remote-viewer wants comma as seperator (not '/')
- $subject =~ s!^/!!;
- $subject =~ s!/(\w+=)!,$1!g;
-
- return $subject;
-}
-
-# helper to generate SPICE remote-viewer configuration
-sub remote_viewer_config {
- my ($authuser, $vmid, $node, $proxy, $title, $port) = @_;
-
- if (!$proxy) {
- my $host = `hostname -f` || PVE::INotify::nodename();
- chomp $host;
- $proxy = $host;
- }
-
- my ($ticket, $proxyticket) = assemble_spice_ticket($authuser, $vmid, $node);
-
- my $filename = "/etc/pve/local/pve-ssl.pem";
- my $subject = read_x509_subject_spice($filename);
-
- my $cacert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192);
- $cacert =~ s/\n/\\n/g;
-
- $proxy = "[$proxy]" if Net::IP::ip_is_ipv6($proxy);
- my $config = {
- 'secure-attention' => "Ctrl+Alt+Ins",
- 'toggle-fullscreen' => "Shift+F11",
- 'release-cursor' => "Ctrl+Alt+R",
- type => 'spice',
- title => $title,
- host => $proxyticket, # this breaks tls hostname verification, so we need to use 'host-subject'
- proxy => "http://$proxy:3128",
- 'tls-port' => $port,
- 'host-subject' => $subject,
- ca => $cacert,
- password => $ticket,
- 'delete-this-file' => 1,
- };
-
- return ($ticket, $proxyticket, $config);
-}
-
-sub check_user_exist {
- my ($usercfg, $username, $noerr) = @_;
-
- $username = PVE::Auth::Plugin::verify_username($username, $noerr);
- return undef if !$username;
-
- return $usercfg->{users}->{$username} if $usercfg && $usercfg->{users}->{$username};
-
- die "no such user ('$username')\n" if !$noerr;
-
- return undef;
-}
-
-sub check_user_enabled {
- my ($usercfg, $username, $noerr) = @_;
-
- my $data = check_user_exist($usercfg, $username, $noerr);
- return undef if !$data;
-
- return 1 if $data->{enable};
-
- die "user '$username' is disabled\n" if !$noerr;
-
- return undef;
-}
-
-sub check_token_exist {
- my ($usercfg, $username, $tokenid, $noerr) = @_;
-
- my $user = check_user_exist($usercfg, $username, $noerr);
- return undef if !$user;
-
- return $user->{tokens}->{$tokenid}
- if defined($user->{tokens}) && $user->{tokens}->{$tokenid};
-
- die "no such token '$tokenid' for user '$username'\n" if !$noerr;
-
- return undef;
-}
-
-sub verify_one_time_pw {
- my ($type, $username, $keys, $tfa_cfg, $otp) = @_;
-
- die "missing one time password for two-factor authentication '$type'\n" if !$otp;
-
- # fixme: proxy support?
- my $proxy;
-
- if ($type eq 'yubico') {
- PVE::OTP::yubico_verify_otp($otp, $keys, $tfa_cfg->{url},
- $tfa_cfg->{id}, $tfa_cfg->{key}, $proxy);
- } elsif ($type eq 'oath') {
- PVE::OTP::oath_verify_otp($otp, $keys, $tfa_cfg->{step}, $tfa_cfg->{digits});
- } else {
- die "unknown tfa type '$type'\n";
- }
-}
-
-# password should be utf8 encoded
-# Note: some plugins delay/sleep if auth fails
-sub authenticate_user {
- my ($username, $password, $otp) = @_;
-
- die "no username specified\n" if !$username;
-
- my ($ruid, $realm);
-
- ($username, $ruid, $realm) = PVE::Auth::Plugin::verify_username($username);
-
- my $usercfg = cfs_read_file('user.cfg');
-
- check_user_enabled($usercfg, $username);
-
- my $ctime = time();
- my $expire = $usercfg->{users}->{$username}->{expire};
-
- die "account expired\n" if $expire && ($expire < $ctime);
-
- my $domain_cfg = cfs_read_file('domains.cfg');
-
- my $cfg = $domain_cfg->{ids}->{$realm};
- die "auth domain '$realm' does not exist\n" if !$cfg;
- my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
- $plugin->authenticate_user($cfg, $realm, $ruid, $password);
-
- my ($type, $tfa_data) = user_get_tfa($username, $realm);
- if ($type) {
- if ($type eq 'u2f') {
- # Note that if the user did not manage to complete the initial u2f registration
- # challenge we have a hash containing a 'challenge' entry in the user's tfa.cfg entry:
- $tfa_data = undef if exists $tfa_data->{challenge};
- } elsif (!defined($otp)) {
- # The user requires a 2nd factor but has not provided one. Return success but
- # don't clear $tfa_data.
- } else {
- my $keys = $tfa_data->{keys};
- my $tfa_cfg = $tfa_data->{config};
- verify_one_time_pw($type, $username, $keys, $tfa_cfg, $otp);
- $tfa_data = undef;
- }
-
- # Return the type along with the rest:
- if ($tfa_data) {
- $tfa_data = {
- type => $type,
- data => $tfa_data,
- };
- }
- }
-
- return wantarray ? ($username, $tfa_data) : $username;
-}
-
-sub domain_set_password {
- my ($realm, $username, $password) = @_;
-
- die "no auth domain specified" if !$realm;
-
- my $domain_cfg = cfs_read_file('domains.cfg');
-
- my $cfg = $domain_cfg->{ids}->{$realm};
- die "auth domain '$realm' does not exist\n" if !$cfg;
- my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
- $plugin->store_password($cfg, $realm, $username, $password);
-}
-
-sub add_user_group {
- my ($username, $usercfg, $group) = @_;
-
- $usercfg->{users}->{$username}->{groups}->{$group} = 1;
- $usercfg->{groups}->{$group}->{users}->{$username} = 1;
-}
-
-sub delete_user_group {
- my ($username, $usercfg) = @_;
-
- foreach my $group (keys %{$usercfg->{groups}}) {
-
- delete ($usercfg->{groups}->{$group}->{users}->{$username})
- if $usercfg->{groups}->{$group}->{users}->{$username};
- }
-}
-
-sub delete_user_acl {
- my ($username, $usercfg) = @_;
-
- foreach my $acl (keys %{$usercfg->{acl}}) {
-
- delete ($usercfg->{acl}->{$acl}->{users}->{$username})
- if $usercfg->{acl}->{$acl}->{users}->{$username};
- }
-}
-
-sub delete_group_acl {
- my ($group, $usercfg) = @_;
-
- foreach my $acl (keys %{$usercfg->{acl}}) {
-
- delete ($usercfg->{acl}->{$acl}->{groups}->{$group})
- if $usercfg->{acl}->{$acl}->{groups}->{$group};
- }
-}
-
-sub delete_pool_acl {
- my ($pool, $usercfg) = @_;
-
- my $path = "/pool/$pool";
-
- delete ($usercfg->{acl}->{$path})
-}
-
-# we automatically create some predefined roles by splitting privs
-# into 3 groups (per category)
-# root: only root is allowed to do that
-# admin: an administrator can to that
-# user: a normal user/customer can to that
-my $privgroups = {
- VM => {
- root => [],
- admin => [
- 'VM.Config.Disk',
- 'VM.Config.CPU',
- 'VM.Config.Memory',
- 'VM.Config.Network',
- 'VM.Config.HWType',
- 'VM.Config.Options', # covers all other things
- 'VM.Allocate',
- 'VM.Clone',
- 'VM.Migrate',
- 'VM.Monitor',
- 'VM.Snapshot',
- 'VM.Snapshot.Rollback',
- ],
- user => [
- 'VM.Config.CDROM', # change CDROM media
- 'VM.Config.Cloudinit',
- 'VM.Console',
- 'VM.Backup',
- 'VM.PowerMgmt',
- ],
- audit => [
- 'VM.Audit',
- ],
- },
- Sys => {
- root => [
- 'Sys.PowerMgmt',
- 'Sys.Modify', # edit/change node settings
- ],
- admin => [
- 'Permissions.Modify',
- 'Sys.Console',
- 'Sys.Syslog',
- ],
- user => [],
- audit => [
- 'Sys.Audit',
- ],
- },
- Datastore => {
- root => [],
- admin => [
- 'Datastore.Allocate',
- 'Datastore.AllocateTemplate',
- ],
- user => [
- 'Datastore.AllocateSpace',
- ],
- audit => [
- 'Datastore.Audit',
- ],
- },
- SDN => {
- root => [],
- admin => [
- 'SDN.Allocate',
- 'SDN.Audit',
- ],
- audit => [
- 'SDN.Audit',
- ],
- },
- User => {
- root => [
- 'Realm.Allocate',
- ],
- admin => [
- 'User.Modify',
- 'Group.Allocate', # edit/change group settings
- 'Realm.AllocateUser',
- ],
- user => [],
- audit => [],
- },
- Pool => {
- root => [],
- admin => [
- 'Pool.Allocate', # create/delete pools
- ],
- user => [],
- audit => [],
- },
-};
-
-my $valid_privs = {};
-
-my $special_roles = {
- 'NoAccess' => {}, # no privileges
- 'Administrator' => $valid_privs, # all privileges
-};
-
-sub create_roles {
-
- foreach my $cat (keys %$privgroups) {
- my $cd = $privgroups->{$cat};
- foreach my $p (@{$cd->{root}}, @{$cd->{admin}},
- @{$cd->{user}}, @{$cd->{audit}}) {
- $valid_privs->{$p} = 1;
- }
- foreach my $p (@{$cd->{admin}}, @{$cd->{user}}, @{$cd->{audit}}) {
-
- $special_roles->{"PVE${cat}Admin"}->{$p} = 1;
- $special_roles->{"PVEAdmin"}->{$p} = 1;
- }
- if (scalar(@{$cd->{user}})) {
- foreach my $p (@{$cd->{user}}, @{$cd->{audit}}) {
- $special_roles->{"PVE${cat}User"}->{$p} = 1;
- }
- }
- foreach my $p (@{$cd->{audit}}) {
- $special_roles->{"PVEAuditor"}->{$p} = 1;
- }
- }
-
- $special_roles->{"PVETemplateUser"} = { 'VM.Clone' => 1, 'VM.Audit' => 1 };
-};
-
-create_roles();
-
-sub create_priv_properties {
- my $properties = {};
- foreach my $priv (keys %$valid_privs) {
- $properties->{$priv} = {
- type => 'boolean',
- optional => 1,
- };
- }
- return $properties;
-}
-
-sub role_is_special {
- my ($role) = @_;
- return (exists $special_roles->{$role}) ? 1 : 0;
-}
-
-sub add_role_privs {
- my ($role, $usercfg, $privs) = @_;
-
- return if !$privs;
-
- die "role '$role' does not exist\n" if !$usercfg->{roles}->{$role};
-
- foreach my $priv (split_list($privs)) {
- if (defined ($valid_privs->{$priv})) {
- $usercfg->{roles}->{$role}->{$priv} = 1;
- } else {
- die "invalid privilege '$priv'\n";
- }
- }
-}
-
-sub lookup_username {
- my ($username, $noerr) = @_;
-
- $username =~ m!^(${PVE::Auth::Plugin::user_regex})\@(${PVE::Auth::Plugin::realm_regex})$!;
-
- my $realm = $2;
- my $domain_cfg = cfs_read_file("domains.cfg");
- my $casesensitive = $domain_cfg->{ids}->{$realm}->{'case-sensitive'} // 1;
- my $usercfg = cfs_read_file('user.cfg');
-
- if (!$casesensitive) {
- my @matches = grep { lc $username eq lc $_ } (keys %{$usercfg->{users}});
-
- die "ambiguous case insensitive match of username '$username', cannot safely grant access!\n"
- if scalar @matches > 1 && !$noerr;
-
- return $matches[0]
- }
-
- return $username;
-}
-
-sub normalize_path {
- my $path = shift;
-
- $path =~ s|/+|/|g;
-
- $path =~ s|/$||;
-
- $path = '/' if !$path;
-
- $path = "/$path" if $path !~ m|^/|;
-
- return undef if $path !~ m|^[[:alnum:]\.\-\_\/]+$|;
-
- return $path;
-}
-
-sub check_path {
- my ($path) = @_;
- return $path =~ m!^(
- /
- |/access
- |/access/groups
- |/access/realm
- |/nodes
- |/nodes/[[:alnum:]\.\-\_]+
- |/pool
- |/pool/[[:alnum:]\.\-\_]+
- |/sdn
- |/storage
- |/storage/[[:alnum:]\.\-\_]+
- |/vms
- |/vms/[1-9][0-9]{2,}
- )$!xs;
-}
-
-PVE::JSONSchema::register_format('pve-groupid', \&verify_groupname);
-sub verify_groupname {
- my ($groupname, $noerr) = @_;
-
- if ($groupname !~ m/^[A-Za-z0-9\.\-_]+$/) {
-
- die "group name '$groupname' contains invalid characters\n" if !$noerr;
-
- return undef;
- }
-
- return $groupname;
-}
-
-PVE::JSONSchema::register_format('pve-roleid', \&verify_rolename);
-sub verify_rolename {
- my ($rolename, $noerr) = @_;
-
- if ($rolename !~ m/^[A-Za-z0-9\.\-_]+$/) {
-
- die "role name '$rolename' contains invalid characters\n" if !$noerr;
-
- return undef;
- }
-
- return $rolename;
-}
-
-PVE::JSONSchema::register_format('pve-poolid', \&verify_poolname);
-sub verify_poolname {
- my ($poolname, $noerr) = @_;
-
- if ($poolname !~ m/^[A-Za-z0-9\.\-_]+$/) {
-
- die "pool name '$poolname' contains invalid characters\n" if !$noerr;
-
- return undef;
- }
-
- return $poolname;
-}
-
-PVE::JSONSchema::register_format('pve-priv', \&verify_privname);
-sub verify_privname {
- my ($priv, $noerr) = @_;
-
- if (!$valid_privs->{$priv}) {
- die "invalid privilege '$priv'\n" if !$noerr;
-
- return undef;
- }
-
- return $priv;
-}
-
-sub userconfig_force_defaults {
- my ($cfg) = @_;
-
- foreach my $r (keys %$special_roles) {
- $cfg->{roles}->{$r} = $special_roles->{$r};
- }
-
- # add root user if not exists
- if (!$cfg->{users}->{'root@pam'}) {
- $cfg->{users}->{'root@pam'}->{enable} = 1;
- }
-}
-
-sub parse_user_config {
- my ($filename, $raw) = @_;
-
- my $cfg = {};
-
- userconfig_force_defaults($cfg);
-
- $raw = '' if !defined($raw);
- while ($raw =~ /^\s*(.+?)\s*$/gm) {
- my $line = $1;
- my @data;
-
- foreach my $d (split (/:/, $line)) {
- $d =~ s/^\s+//;
- $d =~ s/\s+$//;
- push @data, $d
- }
-
- my $et = shift @data;
-
- if ($et eq 'user') {
- my ($user, $enable, $expire, $firstname, $lastname, $email, $comment, $keys) = @data;
-
- my (undef, undef, $realm) = PVE::Auth::Plugin::verify_username($user, 1);
- if (!$realm) {
- warn "user config - ignore user '$user' - invalid user name\n";
- next;
- }
-
- $enable = $enable ? 1 : 0;
-
- $expire = 0 if !$expire;
-
- if ($expire !~ m/^\d+$/) {
- warn "user config - ignore user '$user' - (illegal characters in expire '$expire')\n";
- next;
- }
- $expire = int($expire);
-
- #if (!verify_groupname ($group, 1)) {
- # warn "user config - ignore user '$user' - invalid characters in group name\n";
- # next;
- #}
-
- $cfg->{users}->{$user} = {
- enable => $enable,
- # group => $group,
- };
- $cfg->{users}->{$user}->{firstname} = PVE::Tools::decode_text($firstname) if $firstname;
- $cfg->{users}->{$user}->{lastname} = PVE::Tools::decode_text($lastname) if $lastname;
- $cfg->{users}->{$user}->{email} = $email;
- $cfg->{users}->{$user}->{comment} = PVE::Tools::decode_text($comment) if $comment;
- $cfg->{users}->{$user}->{expire} = $expire;
- # keys: allowed yubico key ids or oath secrets (base32 encoded)
- $cfg->{users}->{$user}->{keys} = $keys if $keys;
-
- #$cfg->{users}->{$user}->{groups}->{$group} = 1;
- #$cfg->{groups}->{$group}->{$user} = 1;
-
- } elsif ($et eq 'group') {
- my ($group, $userlist, $comment) = @data;
-
- if (!verify_groupname($group, 1)) {
- warn "user config - ignore group '$group' - invalid characters in group name\n";
- next;
- }
-
- # make sure to add the group (even if there are no members)
- $cfg->{groups}->{$group} = { users => {} } if !$cfg->{groups}->{$group};
-
- $cfg->{groups}->{$group}->{comment} = PVE::Tools::decode_text($comment) if $comment;
-
- foreach my $user (split_list($userlist)) {
-
- if (!PVE::Auth::Plugin::verify_username($user, 1)) {
- warn "user config - ignore invalid group member '$user'\n";
- next;
- }
-
- if ($cfg->{users}->{$user}) { # user exists
- $cfg->{users}->{$user}->{groups}->{$group} = 1;
- } else {
- warn "user config - ignore invalid group member '$user'\n";
- }
- $cfg->{groups}->{$group}->{users}->{$user} = 1;
- }
-
- } elsif ($et eq 'role') {
- my ($role, $privlist) = @data;
-
- if (!verify_rolename($role, 1)) {
- warn "user config - ignore role '$role' - invalid characters in role name\n";
- next;
- }
-
- # make sure to add the role (even if there are no privileges)
- $cfg->{roles}->{$role} = {} if !$cfg->{roles}->{$role};
-
- foreach my $priv (split_list($privlist)) {
- if (defined ($valid_privs->{$priv})) {
- $cfg->{roles}->{$role}->{$priv} = 1;
- } else {
- warn "user config - ignore invalid privilege '$priv'\n";
- }
- }
-
- } elsif ($et eq 'acl') {
- my ($propagate, $pathtxt, $uglist, $rolelist) = @data;
-
- $propagate = $propagate ? 1 : 0;
-
- if (my $path = normalize_path($pathtxt)) {
- foreach my $role (split_list($rolelist)) {
-
- if (!verify_rolename($role, 1)) {
- warn "user config - ignore invalid role name '$role' in acl\n";
- next;
- }
-
- if (!$cfg->{roles}->{$role}) {
- warn "user config - ignore invalid acl role '$role'\n";
- next;
- }
-
- foreach my $ug (split_list($uglist)) {
- my ($group) = $ug =~ m/^@(\S+)$/;
-
- if ($group && verify_groupname($group, 1)) {
- if (!$cfg->{groups}->{$group}) { # group does not exist
- warn "user config - ignore invalid acl group '$group'\n";
- }
- $cfg->{acl}->{$path}->{groups}->{$group}->{$role} = $propagate;
- } elsif (PVE::Auth::Plugin::verify_username($ug, 1)) {
- if (!$cfg->{users}->{$ug}) { # user does not exist
- warn "user config - ignore invalid acl member '$ug'\n";
- }
- $cfg->{acl}->{$path}->{users}->{$ug}->{$role} = $propagate;
- } elsif (my ($user, $token) = split_tokenid($ug, 1)) {
- if (check_token_exist($cfg, $user, $token, 1)) {
- $cfg->{acl}->{$path}->{tokens}->{$ug}->{$role} = $propagate;
- } else {
- warn "user config - ignore invalid acl token '$ug'\n";
- }
- } else {
- warn "user config - invalid user/group '$ug' in acl\n";
- }
- }
- }
- } else {
- warn "user config - ignore invalid path in acl '$pathtxt'\n";
- }
- } elsif ($et eq 'pool') {
- my ($pool, $comment, $vmlist, $storelist) = @data;
-
- if (!verify_poolname($pool, 1)) {
- warn "user config - ignore pool '$pool' - invalid characters in pool name\n";
- next;
- }
-
- # make sure to add the pool (even if there are no members)
- $cfg->{pools}->{$pool} = { vms => {}, storage => {} } if !$cfg->{pools}->{$pool};
-
- $cfg->{pools}->{$pool}->{comment} = PVE::Tools::decode_text($comment) if $comment;
-
- foreach my $vmid (split_list($vmlist)) {
- if ($vmid !~ m/^\d+$/) {
- warn "user config - ignore invalid vmid '$vmid' in pool '$pool'\n";
- next;
- }
- $vmid = int($vmid);
-
- if ($cfg->{vms}->{$vmid}) {
- warn "user config - ignore duplicate vmid '$vmid' in pool '$pool'\n";
- next;
- }
-
- $cfg->{pools}->{$pool}->{vms}->{$vmid} = 1;
-
- # record vmid ==> pool relation
- $cfg->{vms}->{$vmid} = $pool;
- }
-
- foreach my $storeid (split_list($storelist)) {
- if ($storeid !~ m/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i) {
- warn "user config - ignore invalid storage '$storeid' in pool '$pool'\n";
- next;
- }
- $cfg->{pools}->{$pool}->{storage}->{$storeid} = 1;
- }
- } elsif ($et eq 'token') {
- my ($tokenid, $expire, $privsep, $comment) = @data;
-
- my ($user, $token) = split_tokenid($tokenid, 1);
- if (!($user && $token)) {
- warn "user config - ignore invalid tokenid '$tokenid'\n";
- next;
- }
-
- $privsep = $privsep ? 1 : 0;
-
- $expire = 0 if !$expire;
-
- if ($expire !~ m/^\d+$/) {
- warn "user config - ignore token '$tokenid' - (illegal characters in expire '$expire')\n";
- next;
- }
- $expire = int($expire);
-
- if (my $user_cfg = $cfg->{users}->{$user}) { # user exists
- $user_cfg->{tokens}->{$token} = {} if !$user_cfg->{tokens}->{$token};
- my $token_cfg = $user_cfg->{tokens}->{$token};
- $token_cfg->{privsep} = $privsep;
- $token_cfg->{expire} = $expire;
- $token_cfg->{comment} = PVE::Tools::decode_text($comment) if $comment;
- } else {
- warn "user config - ignore token '$tokenid' - user does not exist\n";
- }
- } else {
- warn "user config - ignore config line: $line\n";
- }
- }
-
- userconfig_force_defaults($cfg);
-
- return $cfg;
-}
-
-sub write_user_config {
- my ($filename, $cfg) = @_;
-
- my $data = '';
-
- foreach my $user (sort keys %{$cfg->{users}}) {
- my $d = $cfg->{users}->{$user};
- my $firstname = $d->{firstname} ? PVE::Tools::encode_text($d->{firstname}) : '';
- my $lastname = $d->{lastname} ? PVE::Tools::encode_text($d->{lastname}) : '';
- my $email = $d->{email} || '';
- my $comment = $d->{comment} ? PVE::Tools::encode_text($d->{comment}) : '';
- my $expire = int($d->{expire} || 0);
- my $enable = $d->{enable} ? 1 : 0;
- my $keys = $d->{keys} ? $d->{keys} : '';
- $data .= "user:$user:$enable:$expire:$firstname:$lastname:$email:$comment:$keys:\n";
-
- my $user_tokens = $d->{tokens};
- foreach my $token (sort keys %$user_tokens) {
- my $td = $user_tokens->{$token};
- my $full_tokenid = join_tokenid($user, $token);
- my $comment = $td->{comment} ? PVE::Tools::encode_text($td->{comment}) : '';
- my $expire = int($td->{expire} || 0);
- my $privsep = $td->{privsep} ? 1 : 0;
- $data .= "token:$full_tokenid:$expire:$privsep:$comment:\n";
- }
- }
-
- $data .= "\n";
-
- foreach my $group (sort keys %{$cfg->{groups}}) {
- my $d = $cfg->{groups}->{$group};
- my $list = join (',', sort keys %{$d->{users}});
- my $comment = $d->{comment} ? PVE::Tools::encode_text($d->{comment}) : '';
- $data .= "group:$group:$list:$comment:\n";
- }
-
- $data .= "\n";
-
- foreach my $pool (sort keys %{$cfg->{pools}}) {
- my $d = $cfg->{pools}->{$pool};
- my $vmlist = join (',', sort keys %{$d->{vms}});
- my $storelist = join (',', sort keys %{$d->{storage}});
- my $comment = $d->{comment} ? PVE::Tools::encode_text($d->{comment}) : '';
- $data .= "pool:$pool:$comment:$vmlist:$storelist:\n";
- }
-
- $data .= "\n";
-
- foreach my $role (sort keys %{$cfg->{roles}}) {
- next if $special_roles->{$role};
-
- my $d = $cfg->{roles}->{$role};
- my $list = join (',', sort keys %$d);
- $data .= "role:$role:$list:\n";
- }
-
- $data .= "\n";
-
- my $collect_rolelist_members = sub {
- my ($acl_members, $result, $prefix, $exclude) = @_;
-
- foreach my $member (keys %$acl_members) {
- next if $exclude && $member eq $exclude;
-
- my $l0 = '';
- my $l1 = '';
- foreach my $role (sort keys %{$acl_members->{$member}}) {
- my $propagate = $acl_members->{$member}->{$role};
- if ($propagate) {
- $l1 .= ',' if $l1;
- $l1 .= $role;
- } else {
- $l0 .= ',' if $l0;
- $l0 .= $role;
- }
- }
- $result->{0}->{$l0}->{"${prefix}${member}"} = 1 if $l0;
- $result->{1}->{$l1}->{"${prefix}${member}"} = 1 if $l1;
- }
- };
-
- foreach my $path (sort keys %{$cfg->{acl}}) {
- my $d = $cfg->{acl}->{$path};
-
- my $rolelist_members = {};
-
- $collect_rolelist_members->($d->{'groups'}, $rolelist_members, '@');
-
- # no need to save 'root@pam', it is always 'Administrator'
- $collect_rolelist_members->($d->{'users'}, $rolelist_members, '', 'root@pam');
-
- $collect_rolelist_members->($d->{'tokens'}, $rolelist_members, '');
-
- foreach my $propagate (0,1) {
- my $filtered = $rolelist_members->{$propagate};
- foreach my $rolelist (sort keys %$filtered) {
- my $uglist = join (',', sort keys %{$filtered->{$rolelist}});
- $data .= "acl:$propagate:$path:$uglist:$rolelist:\n";
- }
-
- }
- }
-
- return $data;
-}
-
-# The TFA configuration in priv/tfa.cfg format contains one line per user of
-# the form:
-# USER:TYPE:DATA
-# DATA is a base64 encoded json string and its format depends on the type.
-sub parse_priv_tfa_config {
- my ($filename, $raw) = @_;
-
- my $users = {};
- my $cfg = { users => $users };
-
- $raw = '' if !defined($raw);
- while ($raw =~ /^\s*(.+?)\s*$/gm) {
- my $line = $1;
- my ($user, $type, $data) = split(/:/, $line, 3);
-
- my (undef, undef, $realm) = PVE::Auth::Plugin::verify_username($user, 1);
- if (!$realm) {
- warn "user tfa config - ignore user '$user' - invalid user name\n";
- next;
- }
-
- $data = decode_json(decode_base64($data));
-
- $users->{$user} = {
- type => $type,
- data => $data,
- };
- }
-
- return $cfg;
-}
-
-sub write_priv_tfa_config {
- my ($filename, $cfg) = @_;
-
- my $output = '';
-
- my $users = $cfg->{users};
- foreach my $user (sort keys %$users) {
- my $info = $users->{$user};
- next if !%$info; # skip empty entries
-
- $info = {%$info}; # copy to verify contents:
-
- my $type = delete $info->{type};
- my $data = delete $info->{data};
-
- if (my @keys = keys %$info) {
- die "invalid keys in TFA config for user $user: " . join(', ', @keys) . "\n";
- }
-
- $data = encode_base64(encode_json($data), '');
- $output .= "${user}:${type}:${data}\n";
- }
-
- return $output;
-}
-
-sub roles {
- my ($cfg, $user, $path) = @_;
-
- # NOTE: we do not consider pools here.
- # NOTE: for privsep tokens, this does not filter roles by those that the
- # corresponding user has.
- # Use $rpcenv->permission() for any actual permission checks!
-
- return 'Administrator' if $user eq 'root@pam'; # root can do anything
-
- if (pve_verify_tokenid($user, 1)) {
- my $tokenid = $user;
- my ($username, $token) = split_tokenid($tokenid);
-
- my $token_info = $cfg->{users}->{$username}->{tokens}->{$token};
- return () if !$token_info;
-
- my $user_roles = roles($cfg, $username, $path);
-
- # return full user privileges
- return $user_roles if !$token_info->{privsep};
- }
-
- my $roles = {};
-
- foreach my $p (sort keys %{$cfg->{acl}}) {
- my $final = ($path eq $p);
-
- next if !(($p eq '/') || $final || ($path =~ m|^$p/|));
-
- my $acl = $cfg->{acl}->{$p};
-
- #print "CHECKACL $path $p\n";
- #print "ACL $path = " . Dumper ($acl);
- if (my $ri = $acl->{tokens}->{$user}) {
- my $new;
- foreach my $role (keys %$ri) {
- my $propagate = $ri->{$role};
- if ($final || $propagate) {
- #print "APPLY ROLE $p $user $role\n";
- $new = {} if !$new;
- $new->{$role} = $propagate;
- }
- }
- if ($new) {
- $roles = $new; # overwrite previous settings
- next;
- }
- }
-
- if (my $ri = $acl->{users}->{$user}) {
- my $new;
- foreach my $role (keys %$ri) {
- my $propagate = $ri->{$role};
- if ($final || $propagate) {
- #print "APPLY ROLE $p $user $role\n";
- $new = {} if !$new;
- $new->{$role} = $propagate;
- }
- }
- if ($new) {
- $roles = $new; # overwrite previous settings
- next; # user privs always override group privs
- }
- }
-
- my $new;
- foreach my $g (keys %{$acl->{groups}}) {
- next if !$cfg->{groups}->{$g}->{users}->{$user};
- if (my $ri = $acl->{groups}->{$g}) {
- foreach my $role (keys %$ri) {
- my $propagate = $ri->{$role};
- if ($final || $propagate) {
- #print "APPLY ROLE $p \@$g $role\n";
- $new = {} if !$new;
- $new->{$role} = $propagate;
- }
- }
- }
- }
- if ($new) {
- $roles = $new; # overwrite previous settings
- next;
- }
- }
-
- return { 'NoAccess' => $roles->{NoAccess} } if defined ($roles->{NoAccess});
- #return () if defined ($roles->{NoAccess});
-
- #print "permission $user $path = " . Dumper ($roles);
-
- #print "roles $user $path = " . join (',', @ra) . "\n";
-
- return $roles;
-}
-
-sub remove_vm_access {
- my ($vmid) = @_;
- my $delVMaccessFn = sub {
- my $usercfg = cfs_read_file("user.cfg");
- my $modified;
-
- if (my $acl = $usercfg->{acl}->{"/vms/$vmid"}) {
- delete $usercfg->{acl}->{"/vms/$vmid"};
- $modified = 1;
- }
- if (my $pool = $usercfg->{vms}->{$vmid}) {
- if (my $data = $usercfg->{pools}->{$pool}) {
- delete $data->{vms}->{$vmid};
- delete $usercfg->{vms}->{$vmid};
- $modified = 1;
- }
- }
- cfs_write_file("user.cfg", $usercfg) if $modified;
- };
-
- lock_user_config($delVMaccessFn, "access permissions cleanup for VM $vmid failed");
-}
-
-sub remove_storage_access {
- my ($storeid) = @_;
-
- my $deleteStorageAccessFn = sub {
- my $usercfg = cfs_read_file("user.cfg");
- my $modified;
-
- if (my $storage = $usercfg->{acl}->{"/storage/$storeid"}) {
- delete $usercfg->{acl}->{"/storage/$storeid"};
- $modified = 1;
- }
- foreach my $pool (keys %{$usercfg->{pools}}) {
- delete $usercfg->{pools}->{$pool}->{storage}->{$storeid};
- $modified = 1;
- }
- cfs_write_file("user.cfg", $usercfg) if $modified;
- };
-
- lock_user_config($deleteStorageAccessFn,
- "access permissions cleanup for storage $storeid failed");
-}
-
-sub add_vm_to_pool {
- my ($vmid, $pool) = @_;
-
- my $addVMtoPoolFn = sub {
- my $usercfg = cfs_read_file("user.cfg");
- if (my $data = $usercfg->{pools}->{$pool}) {
- $data->{vms}->{$vmid} = 1;
- $usercfg->{vms}->{$vmid} = $pool;
- cfs_write_file("user.cfg", $usercfg);
- }
- };
-
- lock_user_config($addVMtoPoolFn, "can't add VM $vmid to pool '$pool'");
-}
-
-sub remove_vm_from_pool {
- my ($vmid) = @_;
-
- my $delVMfromPoolFn = sub {
- my $usercfg = cfs_read_file("user.cfg");
- if (my $pool = $usercfg->{vms}->{$vmid}) {
- if (my $data = $usercfg->{pools}->{$pool}) {
- delete $data->{vms}->{$vmid};
- delete $usercfg->{vms}->{$vmid};
- cfs_write_file("user.cfg", $usercfg);
- }
- }
- };
-
- lock_user_config($delVMfromPoolFn, "pool cleanup for VM $vmid failed");
-}
-
-my $USER_CONTROLLED_TFA_TYPES = {
- u2f => 1,
- oath => 1,
-};
-
-# Delete an entry by setting $data=undef in which case $type is ignored.
-# Otherwise both must be valid.
-sub user_set_tfa {
- my ($userid, $realm, $type, $data, $cached_usercfg, $cached_domaincfg) = @_;
-
- if (defined($data) && !defined($type)) {
- # This is an internal usage error and should not happen
- die "cannot set tfa data without a type\n";
- }
-
- my $user_cfg = $cached_usercfg || cfs_read_file('user.cfg');
- my $user = $user_cfg->{users}->{$userid}
- or die "user '$userid' not found\n";
-
- my $domain_cfg = $cached_domaincfg || 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};
- if (defined($realm_tfa)) {
- $realm_tfa = PVE::Auth::Plugin::parse_tfa_config($realm_tfa);
- # If the realm has a TFA setting, we're only allowed to use that.
- if (defined($data)) {
- my $required_type = $realm_tfa->{type};
- if ($required_type ne $type) {
- die "realm '$realm' only allows TFA of type '$required_type\n";
- }
-
- if (defined($data->{config})) {
- # XXX: Is it enough if the type matches? Or should the configuration also match?
- }
-
- # realm-configured tfa always uses a simple key list, so use the user.cfg
- $user->{keys} = $data->{keys};
- } else {
- die "realm '$realm' does not allow removing the 2nd factor\n";
- }
- } else {
- # Without a realm-enforced TFA setting the user can add a u2f or totp entry by themselves.
- # The 'yubico' type requires yubico server settings, which have to be configured on the
- # realm, so this is not supported here:
- die "domain '$realm' does not support TFA type '$type'\n"
- if defined($data) && !$USER_CONTROLLED_TFA_TYPES->{$type};
- }
-
- # Custom TFA entries are stored in priv/tfa.cfg as they can be more complet: u2f uses a
- # public key and a key handle, TOTP requires the usual totp settings...
-
- my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
- my $tfa = ($tfa_cfg->{users}->{$userid} //= {});
-
- if (defined($data)) {
- $tfa->{type} = $type;
- $tfa->{data} = $data;
- cfs_write_file('priv/tfa.cfg', $tfa_cfg);
-
- $user->{keys} = "x!$type";
- } else {
- delete $tfa_cfg->{users}->{$userid};
- cfs_write_file('priv/tfa.cfg', $tfa_cfg);
-
- delete $user->{keys};
- }
-
- cfs_write_file('user.cfg', $user_cfg);
-}
-
-sub user_get_tfa {
- my ($username, $realm) = @_;
-
- my $user_cfg = cfs_read_file('user.cfg');
- my $user = $user_cfg->{users}->{$username}
- or die "user '$username' not found\n";
-
- my $keys = $user->{keys};
-
- 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;
-
- if (!$keys) {
- return if !$realm_tfa;
- die "missing required 2nd keys\n";
- }
-
- # new style config starts with an 'x' and optionally contains a !<type> suffix
- if ($keys !~ /^x(?:!.*)?$/) {
- # old style config, find the type via the realm
- return if !$realm_tfa;
- return ($realm_tfa->{type}, {
- keys => $keys,
- config => $realm_tfa,
- });
- } else {
- my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
- my $tfa = $tfa_cfg->{users}->{$username};
- return if !$tfa; # should not happen (user.cfg wasn't cleaned up?)
-
- if ($realm_tfa) {
- # if the realm has a tfa setting we need to verify the type:
- die "auth domain '$realm' and user have mismatching TFA settings\n"
- if $realm_tfa && $realm_tfa->{type} ne $tfa->{type};
- }
-
- return ($tfa->{type}, $tfa->{data});
- }
-}
-
-# bash completion helpers
-
-register_standard_option('userid-completed',
- get_standard_option('userid', { completion => \&complete_username}),
-);
-
-sub complete_username {
-
- my $user_cfg = cfs_read_file('user.cfg');
-
- return [ keys %{$user_cfg->{users}} ];
-}
-
-sub complete_group {
-
- my $user_cfg = cfs_read_file('user.cfg');
-
- return [ keys %{$user_cfg->{groups}} ];
-}
-
-sub complete_realm {
-
- my $domain_cfg = cfs_read_file('domains.cfg');
-
- return [ keys %{$domain_cfg->{ids}} ];
-}
-
-1;
+++ /dev/null
-package PVE::Auth::AD;
-
-use strict;
-use warnings;
-use PVE::Auth::LDAP;
-use PVE::LDAP;
-
-use base qw(PVE::Auth::LDAP);
-
-sub type {
- return 'ad';
-}
-
-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'),
- };
-}
-
-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 },
- };
-}
-
-sub get_users {
- my ($class, $config, $realm) = @_;
-
- $config->{user_attr} //= 'sAMAccountName';
-
- return $class->SUPER::get_users($config, $realm);
-}
-
-sub authenticate_user {
- my ($class, $config, $realm, $username, $password) = @_;
-
- my $servers = [$config->{server1}];
- push @$servers, $config->{server2} if $config->{server2};
-
- my ($scheme, $port) = $class->get_scheme_and_port($config);
-
- 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;
- }
- }
- } elsif (defined($config->{verify})) {
- $ad_args{verify} = 'none';
- }
-
- if ($scheme ne 'ldap') {
- $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};
-
- PVE::LDAP::auth_user_dn($ldap, $username, $password);
-
- $ldap->unbind();
- return 1;
-}
-
-1;
+++ /dev/null
-package PVE::Auth::LDAP;
-
-use strict;
-use warnings;
-
-use PVE::Auth::Plugin;
-use PVE::JSONSchema;
-use PVE::LDAP;
-use PVE::Tools;
-
-use base qw(PVE::Auth::Plugin);
-
-sub type {
- return 'ldap';
-}
-
-sub properties {
- return {
- base_dn => {
- description => "LDAP base domain name",
- type => 'string',
- pattern => '\w+=[^,]+(,\s*\w+=[^,]+)*',
- 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',
- pattern => '\w+=[^,]+(,\s*\w+=[^,]+)*',
- 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',
- pattern => '\w+=[^,]+(,\s*\w+=[^,]+)*',
- 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,
- }
- };
-}
-
-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 },
- };
-}
-
-sub get_scheme_and_port {
- my ($class, $config) = @_;
-
- my $scheme = $config->{mode} // ($config->{secure} ? 'ldaps' : 'ldap');
-
- my $default_port = $scheme eq 'ldaps' ? 636 : 389;
- my $port = $config->{port} // $default_port;
-
- return ($scheme, $port);
-}
-
-sub connect_and_bind {
- my ($class, $config, $realm) = @_;
-
- my $servers = [$config->{server1}];
- push @$servers, $config->{server2} if $config->{server2};
-
- my ($scheme, $port) = $class->get_scheme_and_port($config);
-
- 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;
- }
- }
- } else {
- $ldap_args{verify} = 'none';
- }
-
- if ($scheme ne 'ldap') {
- $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 = 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";
- } else {
- PVE::LDAP::ldap_bind($ldap);
- }
-
- if (!$config->{base_dn}) {
- my $root = $ldap->root_dse(attrs => [ 'defaultNamingContext' ]);
- $config->{base_dn} = $root->get_value('defaultNamingContext');
- }
-
- return $ldap;
-}
-
-# returns:
-# {
-# 'username@realm' => {
-# 'attr1' => 'value1',
-# 'attr2' => 'value2',
-# ...
-# },
-# ...
-# }
-#
-# or in list context:
-# (
-# {
-# 'username@realm' => {
-# 'attr1' => 'value1',
-# 'attr2' => 'value2',
-# ...
-# },
-# ...
-# },
-# {
-# 'uid=username,dc=....' => 'username@realm',
-# ...
-# }
-# )
-# the map of dn->username is needed for group membership sync
-sub get_users {
- my ($class, $config, $realm) = @_;
-
- my $ldap = $class->connect_and_bind($config, $realm);
-
- 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',
- };
-
- foreach my $attr (PVE::Tools::split_list($config->{sync_attributes})) {
- my ($ours, $ldap) = ($attr =~ m/^\s*(\w+)=(.*)\s*$/);
- $ldap_attribute_map->{$ldap} = $ours;
- }
-
- my $filter = $config->{filter};
- my $basedn = $config->{base_dn};
-
- $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 $ret = {};
- my $dnmap = {};
-
- foreach my $user (@$users) {
- 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;
- }
-
- $ret->{$username} = {};
-
- foreach my $attr (keys %$user_attributes) {
- if (my $ours = $ldap_attribute_map->{$attr}) {
- $ret->{$username}->{$ours} = $user_attributes->{$attr}->[0];
- }
- }
-
- if (wantarray) {
- my $dn = $user->{dn};
- $dnmap->{$dn} = $username;
- }
- }
-
- return wantarray ? ($ret, $dnmap) : $ret;
-}
-
-# needs a map for dn -> username, we get this from the get_users call
-# otherwise we cannot determine the group membership
-sub get_groups {
- my ($class, $config, $realm, $dnmap) = @_;
-
- my $filter = $config->{group_filter};
- my $basedn = $config->{group_dn} // $config->{base_dn};
- my $attr = $config->{group_name_attr};
- $config->{group_classes} //= 'groupOfNames, group, univentionGroup, ipausergroup';
- my $classes = [PVE::Tools::split_list($config->{group_classes})];
-
- my $ldap = $class->connect_and_bind($config, $realm);
-
- my $groups = PVE::LDAP::query_groups($ldap, $basedn, $classes, $filter, $attr);
-
- my $ret = {};
-
- foreach my $group (@$groups) {
- 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;
- }
-
- $ret->{$name} = { users => {} };
- foreach my $member (@{$group->{members}}) {
- if (my $user = $dnmap->{$member}) {
- $ret->{$name}->{users}->{$user} = 1;
- }
- }
- }
- }
-
- return $ret;
-}
-
-sub authenticate_user {
- my ($class, $config, $realm, $username, $password) = @_;
-
- my $ldap = $class->connect_and_bind($config, $realm);
-
- 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();
- return 1;
-}
-
-my $ldap_pw_dir = "/etc/pve/priv/realm";
-
-sub ldap_cred_file_name {
- my ($realmid) = @_;
- return "${ldap_pw_dir}/${realmid}.pw";
-}
-
-sub get_cred_file {
- my ($realmid) = @_;
-
- my $cred_file = ldap_cred_file_name($realmid);
- if (-e $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";
- }
-
- return $cred_file;
-}
-
-sub ldap_set_credentials {
- my ($password, $realmid) = @_;
-
- my $cred_file = ldap_cred_file_name($realmid);
- mkdir $ldap_pw_dir;
-
- PVE::Tools::file_set_contents($cred_file, $password);
-
- return $cred_file;
-}
-
-sub ldap_get_credentials {
- my ($realmid) = @_;
-
- if (my $cred_file = get_cred_file($realmid)) {
- return PVE::Tools::file_read_firstline($cred_file);
- }
- return undef;
-}
-
-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";
- }
-}
-
-sub on_add_hook {
- my ($class, $realm, $config, %param) = @_;
-
- if (defined($param{password})) {
- ldap_set_credentials($param{password}, $realm);
- } else {
- ldap_delete_credentials($realm);
- }
-}
-
-sub on_update_hook {
- my ($class, $realm, $config, %param) = @_;
-
- return if !exists($param{password});
-
- if (defined($param{password})) {
- ldap_set_credentials($param{password}, $realm);
- } else {
- ldap_delete_credentials($realm);
- }
-}
-
-sub on_delete_hook {
- my ($class, $realm, $config) = @_;
-
- ldap_delete_credentials($realm);
-}
-
-1;
+++ /dev/null
-
-AUTH_SOURCES= \
- Plugin.pm \
- PVE.pm \
- PAM.pm \
- AD.pm \
- LDAP.pm
-
-.PHONY: install
-install:
- for i in ${AUTH_SOURCES}; do install -D -m 0644 $$i ${DESTDIR}${PERLDIR}/PVE/Auth/$$i; done
+++ /dev/null
-package PVE::Auth::PAM;
-
-use strict;
-use warnings;
-
-use PVE::Tools qw(run_command);
-use PVE::Auth::Plugin;
-use Authen::PAM qw(:constants);
-
-use base qw(PVE::Auth::Plugin);
-
-sub type {
- return 'pam';
-}
-
-sub options {
- return {
- default => { optional => 1 },
- comment => { optional => 1 },
- tfa => { optional => 1 },
- };
-}
-
-sub authenticate_user {
- my ($class, $config, $realm, $username, $password) = @_;
-
- # user (www-data) need to be able to read /etc/passwd /etc/shadow
- die "no password\n" if !$password;
-
- my $pamh = new Authen::PAM('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";
- }
-
- my $res;
-
- if (($res = $pamh->pam_authenticate(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
-
- return 1;
-}
-
-
-sub store_password {
- my ($class, $config, $realm, $username, $password) = @_;
-
- my $cmd = ['usermod'];
-
- my $epw = PVE::Tools::encrypt_pw($password);
-
- push @$cmd, '-p', $epw, $username;
-
- run_command($cmd, errmsg => 'change password failed');
-}
-
-1;
+++ /dev/null
-package PVE::Auth::PVE;
-
-use strict;
-use warnings;
-use Encode;
-
-use PVE::Tools;
-use PVE::Auth::Plugin;
-use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_write_file cfs_lock_file);
-
-use base qw(PVE::Auth::Plugin);
-
-my $shadowconfigfile = "priv/shadow.cfg";
-
-cfs_register_file($shadowconfigfile,
- \&parse_shadow_passwd,
- \&write_shadow_config);
-
-sub parse_shadow_passwd {
- my ($filename, $raw) = @_;
-
- my $shadow = {};
-
- return $shadow if !defined($raw);
-
- while ($raw =~ /^\s*(.+?)\s*$/gm) {
- my $line = $1;
-
- 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;
- }
-
- return $shadow;
-}
-
-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";
- }
-
- return $data
-}
-
-sub lock_shadow_config {
- my ($code, $errmsg) = @_;
-
- cfs_lock_file($shadowconfigfile, undef, $code);
- my $err = $@;
- if ($err) {
- $errmsg ? die "$errmsg: $err" : die $err;
- }
-}
-
-sub type {
- return 'pve';
-}
-
-sub options {
- return {
- default => { optional => 1 },
- comment => { optional => 1 },
- tfa => { optional => 1 },
- };
-}
-
-sub authenticate_user {
- my ($class, $config, $realm, $username, $password) = @_;
-
- die "no password\n" if !$password;
-
- 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});
- } else {
- die "no password set\n";
- }
-
- return 1;
-}
-
-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);
- });
-}
-
-sub delete_user {
- my ($class, $config, $realm, $username) = @_;
-
- lock_shadow_config(sub {
- my $shadow_cfg = cfs_read_file($shadowconfigfile);
-
- delete $shadow_cfg->{users}->{$username};
-
- cfs_write_file($shadowconfigfile, $shadow_cfg);
- });
-}
-
-1;
+++ /dev/null
-package PVE::Auth::Plugin;
-
-use strict;
-use warnings;
-
-use Digest::SHA;
-use Encode;
-
-use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_lock_file);
-use PVE::JSONSchema qw(get_standard_option);
-use PVE::SectionConfig;
-use PVE::Tools;
-
-use base qw(PVE::SectionConfig);
-
-my $domainconfigfile = "domains.cfg";
-
-cfs_register_file($domainconfigfile,
- sub { __PACKAGE__->parse_config(@_); },
- sub { __PACKAGE__->write_config(@_); });
-
-sub lock_domain_config {
- my ($code, $errmsg) = @_;
-
- cfs_lock_file($domainconfigfile, undef, $code);
- my $err = $@;
- if ($err) {
- $errmsg ? die "$errmsg: $err" : die $err;
- }
-}
-
-our $realm_regex = qr/[A-Za-z][A-Za-z0-9\.\-_]+/;
-our $user_regex = qr![^\s:/]+!;
-
-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 $realm;
-}
-
-PVE::JSONSchema::register_standard_option('realm', {
- description => "Authentication domain ID",
- type => 'string', format => 'pve-realm',
- maxLength => 32,
-});
-
-my $realm_sync_options_desc = {
- scope => {
- description => "Select what to sync.",
- type => 'string',
- enum => [qw(users groups both)],
- optional => '1',
- },
- full => {
- description => "If set, uses the LDAP Directory as source of truth,"
- ." deleting users or groups not returned from the sync. Otherwise"
- ." only syncs information which is not already present, and does not"
- ." deletes or modifies anything else.",
- type => 'boolean',
- optional => '1',
- },
- 'enable-new' => {
- description => "Enable newly synced users immediately.",
- type => 'boolean',
- default => '1',
- optional => '1',
- },
- purge => {
- description => "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;
- }
- if ($len > 64) {
- die "user name '$username' is too long ($len > 64)\n" if !$noerr;
- return undef;
- }
-
- # we only allow a limited set of characters
- # colon is not allowed, because we store usernames in
- # colon separated lists)!
- # 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;
- }
-
- die "value '$username' does not look like a valid user name\n" if !$noerr;
-
- return undef;
-}
-
-PVE::JSONSchema::register_standard_option('userid', {
- description => "User ID",
- type => 'string', format => 'pve-userid',
- maxLength => 64,
-});
-
-my $tfa_format = {
- type => {
- description => "The type of 2nd factor authentication.",
- format_description => 'TFATYPE',
- type => 'string',
- enum => [qw(yubico oath)],
- },
- id => {
- description => "Yubico API ID.",
- format_description => 'ID',
- type => 'string',
- optional => 1,
- },
- key => {
- description => "Yubico API Key.",
- format_description => 'KEY',
- type => 'string',
- optional => 1,
- },
- url => {
- description => "Yubico API URL.",
- format_description => 'URL',
- type => 'string',
- optional => 1,
- },
- digits => {
- description => "TOTP digits.",
- format_description => 'COUNT',
- type => 'integer',
- minimum => 6, maximum => 8,
- default => 6,
- optional => 1,
- },
- step => {
- description => "TOTP time period.",
- format_description => 'SECONDS',
- type => 'integer',
- minimum => 10,
- default => 30,
- optional => 1,
- },
-};
-
-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,
-});
-
-sub parse_tfa_config {
- my ($data) = @_;
-
- return PVE::JSONSchema::parse_property_string($tfa_format, $data);
-}
-
-my $defaultData = {
- propertyList => {
- type => { description => "Realm type." },
- realm => get_standard_option('realm'),
- },
-};
-
-sub private {
- return $defaultData;
-}
-
-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);
- }
- return undef;
-}
-
-sub parse_config {
- my ($class, $filename, $raw) = @_;
-
- 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;
- }
- }
-
- if ($data->{comment}) {
- $data->{comment} = PVE::Tools::decode_text($data->{comment});
- }
-
- }
-
- # add default domains
-
- $cfg->{ids}->{pve}->{type} = 'pve'; # force type
- $cfg->{ids}->{pve}->{comment} = "Proxmox VE authentication server"
- if !$cfg->{ids}->{pve}->{comment};
-
- $cfg->{ids}->{pam}->{type} = 'pam'; # force type
- $cfg->{ids}->{pam}->{plugin} = 'PVE::Auth::PAM';
- $cfg->{ids}->{pam}->{comment} = "Linux PAM standard authentication"
- 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});
- }
- }
-
- $class->SUPER::write_config($filename, $cfg);
-}
-
-sub authenticate_user {
- my ($class, $config, $realm, $username, $password) = @_;
-
- die "overwrite me";
-}
-
-sub store_password {
- my ($class, $config, $realm, $username, $password) = @_;
-
- my $type = $class->type();
-
- die "can't set password on auth type '$type'\n";
-}
-
-sub delete_user {
- my ($class, $config, $realm, $username) = @_;
-
- # do nothing by default
-}
-
-# called during addition of realm (before the new domain config got written)
-# `password` is moved to %param to avoid writing it out to the config
-# die to abort additon if there are (grave) problems
-# NOTE: runs in a domain config *locked* context
-sub on_add_hook {
- my ($class, $realm, $config, %param) = @_;
- # do nothing by default
-}
-
-# called during domain configuration update (before the updated domain config got
-# written). `password` is moved to %param to avoid writing it out to the config
-# die to abort the update if there are (grave) problems
-# NOTE: runs in a domain config *locked* context
-sub on_update_hook {
- my ($class, $realm, $config, %param) = @_;
- # do nothing by default
-}
-
-# called during deletion of realms (before the new domain config got written)
-# and if the activate check on addition fails, to cleanup all storage traces
-# which on_add_hook may have created.
-# die to abort deletion if there are (very grave) problems
-# NOTE: runs in a storage config *locked* context
-sub on_delete_hook {
- my ($class, $realm, $config) = @_;
- # do nothing by default
-}
-
-1;
+++ /dev/null
-SOURCES=pveum.pm
-
-.PHONY: install
-install: ${SOURCES}
- install -d -m 0755 ${DESTDIR}${PERLDIR}/PVE/CLI
- for i in ${SOURCES}; do install -D -m 0644 $$i ${DESTDIR}${PERLDIR}/PVE/CLI/$$i; done
-
-
-clean:
+++ /dev/null
-package PVE::CLI::pveum;
-
-use strict;
-use warnings;
-
-use PVE::AccessControl;
-use PVE::RPCEnvironment;
-use PVE::API2::User;
-use PVE::API2::Group;
-use PVE::API2::Role;
-use PVE::API2::ACL;
-use PVE::API2::AccessControl;
-use PVE::API2::Pool;
-use PVE::API2::Domains;
-use PVE::CLIFormatter;
-use PVE::CLIHandler;
-use PVE::JSONSchema qw(get_standard_option);
-use PVE::PTY;
-use PVE::RESTHandler;
-use PVE::Tools qw(extract_param);
-
-use base qw(PVE::CLIHandler);
-
-sub setup_environment {
- PVE::RPCEnvironment->setup_default_cli_env();
-}
-
-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: ');
- },
- }),
- ]
- };
-
- return $mapping->{$name};
-}
-
-my $print_api_result = sub {
- my ($data, $schema, $options) = @_;
- PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
-};
-
-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";
- } else {
- PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
- }
-};
-
-__PACKAGE__->register_method({
- name => 'token_permissions',
- path => 'token_permissions',
- 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,
- }),
- },
- },
- returns => {
- type => 'object',
- description => 'Hash of structure "path" => "privilege" => "propagate boolean".',
- },
- code => sub {
- my ($param) = @_;
-
- my $token_subid = extract_param($param, "tokenid");
- $param->{userid} = PVE::AccessControl::join_tokenid($param->{userid}, $token_subid);
-
- return PVE::API2::AccessControl->permissions($param);
- }});
-
-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 => [ 'PVE::API2::AccessControl', 'change_tfa', ['userid'], { action => 'delete', key => undef, config => undef, response => undef, }, ],
- },
- 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 ],
- remove => [ 'PVE::API2::User', 'remove_token', ['userid', 'tokenid'], {}, $print_api_result, $PVE::RESTHandler::standard_output_options ],
- 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],
- },
- 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],
- },
- 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],
- },
- 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],
- },
- 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";
- }],
-
- passwd => [ 'PVE::API2::AccessControl', 'change_password', ['userid'] ],
-
- useradd => { alias => 'user add' },
- usermod => { alias => 'user modify' },
- userdel => { alias => 'user delete' },
-
- groupadd => { alias => 'group add' },
- groupmod => { alias => 'group modify' },
- groupdel => { alias => 'group delete' },
-
- roleadd => { alias => 'role add' },
- rolemod => { alias => 'role modify' },
- roledel => { alias => 'role delete' },
-
- aclmod => { alias => 'acl modify' },
- acldel => { alias => 'acl delete' },
-};
-
-1;
+++ /dev/null
-
-
-.PHONY: install
-install:
- make -C Auth install
- install -D -m 0644 AccessControl.pm ${DESTDIR}${PERLDIR}/PVE/AccessControl.pm
- install -D -m 0644 RPCEnvironment.pm ${DESTDIR}${PERLDIR}/PVE/RPCEnvironment.pm
- install -D -m 0644 TokenConfig.pm ${DESTDIR}${PERLDIR}/PVE/TokenConfig.pm
- make -C API2 install
- make -C CLI install
+++ /dev/null
-package PVE::RPCEnvironment;
-
-use strict;
-use warnings;
-
-use PVE::RESTEnvironment;
-
-use PVE::Exception qw(raise raise_perm_exc);
-use PVE::SafeSyslog;
-use PVE::Tools;
-use PVE::INotify;
-use PVE::Cluster;
-use PVE::ProcFSTools;
-use PVE::AccessControl;
-
-use base qw(PVE::RESTEnvironment);
-
-# ACL cache
-
-my $compile_acl_path = sub {
- my ($self, $user, $path) = @_;
-
- my $cfg = $self->{user_cfg};
-
- return undef if !$cfg->{roles};
-
- die "internal error" if $user eq 'root@pam';
-
- my $cache = $self->{aclcache};
- $cache->{$user} = {} if !$cache->{$user};
- my $data = $cache->{$user};
-
- my ($username, undef) = PVE::AccessControl::split_tokenid($user, 1);
- die "internal error" if $username && $username ne 'root@pam' && !defined($cache->{$username});
-
- if (!$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;
- }
- }
- }
- }
-
- my $roles = PVE::AccessControl::roles($cfg, $user, $path);
-
- # apply roles inherited from pools
- # Note: assume we do not want to propagate those privs
- if ($data->{poolroles}->{$path}) {
- if (!defined($roles->{NoAccess})) {
- if ($data->{poolroles}->{$path}->{NoAccess}) {
- $roles = { 'NoAccess' => 0 };
- } else {
- foreach my $role (keys %{$data->{poolroles}->{$path}}) {
- $roles->{$role} = 0 if !defined($roles->{$role});
- }
- }
- }
- }
-
- $data->{roles}->{$path} = $roles;
-
- my $privs = {};
- foreach my $role (keys %$roles) {
- if (my $privset = $cfg->{roles}->{$role}) {
- foreach my $p (keys %$privset) {
- $privs->{$p} = $roles->{$role};
- }
- }
- }
-
- if ($username && $username ne 'root@pam') {
- # intersect user and token permissions
- my $user_privs = $cache->{$username}->{privs}->{$path};
- $privs = { map { $_ => $user_privs->{$_} && $privs->{$_} } keys %$privs };
- }
-
- $data->{privs}->{$path} = $privs;
-
- return $privs;
-};
-
-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'}} };
- }
-
- 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};
-
- return {} if !$token_info;
-
- # 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};
- } else {
- $user = PVE::AccessControl::verify_username($user, 1);
- return {} if !$user;
- }
-
- my $cache = $self->{aclcache};
- $cache->{$user} = {} if !$cache->{$user};
-
- my $acl = $cache->{$user};
-
- my $perm = $acl->{privs}->{$path};
- return $perm if $perm;
-
- return &$compile_acl_path($self, $user, $path);
-}
-
-sub get_effective_permissions {
- my ($self, $user) = @_;
-
- # default / top level paths
- my $paths = {
- '/' => 1,
- '/access' => 1,
- '/access/groups' => 1,
- '/nodes' => 1,
- '/pools' => 1,
- '/storage' => 1,
- '/vms' => 1,
- };
-
- my $cfg = $self->{user_cfg};
-
- # paths explicitly listed in ACLs
- foreach my $acl_path (keys %{$cfg->{acl}}) {
- $paths->{$acl_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;
- }
- }
-
- my $perms = {};
- foreach my $path (keys %$paths) {
- my $path_perms = $self->permissions($user, $path);
- # filter paths where user has NO permissions
- $perms->{$path} = $path_perms if %$path_perms;
- }
- return $perms;
-}
-
-sub check {
- my ($self, $user, $path, $privs, $noerr) = @_;
-
- 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");
- }
- };
-
- return 1;
-};
-
-sub check_any {
- my ($self, $user, $path, $privs, $noerr) = @_;
-
- my $perm = $self->permissions($user, $path);
-
- my $found = 0;
- foreach my $priv (@$privs) {
- 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);
- } else {
- return $self->check($username, $path, $privs, $noerr);
- }
-}
-
-sub check_user_enabled {
- my ($self, $user, $noerr) = @_;
-
- my $cfg = $self->{user_cfg};
- return PVE::AccessControl::check_user_enabled($cfg, $user, $noerr);
-}
-
-sub check_user_exist {
- my ($self, $user, $noerr) = @_;
-
- my $cfg = $self->{user_cfg};
- return PVE::AccessControl::check_user_exist($cfg, $user, $noerr);
-}
-
-sub check_pool_exist {
- my ($self, $pool, $noerr) = @_;
-
- my $cfg = $self->{user_cfg};
-
- return 1 if $cfg->{pools}->{$pool};
-
- return undef if $noerr;
-
- raise_perm_exc("pool '$pool' does not exist");
-}
-
-sub check_vm_perm {
- my ($self, $user, $vmid, $pool, $privs, $any, $noerr) = @_;
-
- my $cfg = $self->{user_cfg};
-
- if ($pool) {
- 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) = @_;
-
- my $cfg = $self->{user_cfg};
-
- return 0 if !$cfg->{groups}->{$group};
-
- return defined($cfg->{groups}->{$group}->{users}->{$user});
-}
-
-sub filter_groups {
- my ($self, $user, $privs, $any) = @_;
-
- 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};
- }
- }
-
- return $groups;
-}
-
-sub group_member_join {
- my ($self, $grouplist) = @_;
-
- my $users = {};
-
- 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;
- }
- }
-
- return $users;
-}
-
-sub check_perm_modify {
- my ($self, $username, $path, $noerr) = @_;
-
- return $self->check($username, '/access', [ 'Permissions.Modify' ], $noerr) if !$path;
-
- my $testperms = [ 'Permissions.Modify' ];
- if ($path =~ m|^/storage/.+$|) {
- push @$testperms, 'Datastore.Allocate';
- } elsif ($path =~ m|^/vms/.+$|) {
- push @$testperms, 'VM.Allocate';
- } elsif ($path =~ m|^/pool/.+$|) {
- push @$testperms, 'Pool.Allocate';
- }
-
- return $self->check_any($username, $path, $testperms, $noerr);
-}
-
-sub exec_api2_perm_check {
- my ($self, $check, $username, $param, $noerr) = @_;
-
- # syslog("info", "CHECK " . join(', ', @$check));
-
- my $ind = 0;
- my $test = $check->[$ind++];
- 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;
- } 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();
- } 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);
- $path = PVE::AccessControl::normalize_path($path);
- return $self->check_full($username, $path, $privs, $any, $noerr);
- } elsif ($test eq 'userid-group') {
- my $userid = $param->{userid};
- my ($t, $privs, %options) = @$check;
- return 0 if !$options{groups_param} && !$self->check_user_exist($userid, $noerr);
- if (!$self->check_any($username, "/access/groups", $privs, 1)) {
- my $groups = $self->filter_groups($username, $privs, 1);
- if ($options{groups_param}) {
- 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};
- }
- } else {
- 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";
- }
- } elsif ($test eq 'perm-modify') {
- my ($t, $tmplpath) = @$check;
- my $path = PVE::Tools::template_replace($tmplpath, $param);
- $path = PVE::AccessControl::normalize_path($path);
- return $self->check_perm_modify($username, $path, $noerr);
- } else {
- die "unknown permission test";
- }
-};
-
-sub check_api2_permissions {
- my ($self, $perm, $username, $param) = @_;
-
- return 1 if !$username && $perm->{user} && $perm->{user} eq 'world';
-
- raise_perm_exc("user != null") if !$username;
-
- return 1 if $username eq 'root@pam';
-
- raise_perm_exc('user != root@pam') if !$perm;
-
- return 1 if $perm->{user} && $perm->{user} eq 'all';
-
- return $self->exec_api2_perm_check($perm->{check}, $username, $param)
- if $perm->{check};
-
- raise_perm_exc();
-}
-
-sub log_cluster_msg {
- my ($self, $pri, $user, $msg) = @_;
-
- PVE::Cluster::log_msg($pri, $user, $msg);
-}
-
-sub broadcast_tasklist {
- my ($self, $tlist) = @_;
-
- PVE::Cluster::broadcast_tasklist($tlist);
-}
-
-# initialize environment - must be called once at program startup
-sub init {
- my ($class, $type, %params) = @_;
-
- $class = ref($class) || $class;
-
- my $self = $class->SUPER::init($type, %params);
-
- $self->{user_cfg} = {};
- $self->{aclcache} = {};
- $self->{aclversion} = undef;
-
- return $self;
-};
-
-
-# init_request - must be called before each RPC request
-sub init_request {
- my ($self, %params) = @_;
-
- PVE::Cluster::cfs_update();
-
- $self->{result_attributes} = {};
-
- 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'";
- }
- }
-
- 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;
- }
- }
- };
- if (my $err = $@) {
- $self->{user_cfg} = {};
- die "Unable to load access control list: $err";
- }
-}
-
-# hacks: to provide better backwards compatibiliy
-
-# old code uses PVE::RPCEnvironment::get();
-# new code should use PVE::RPCEnvironment->get();
-sub get {
- return PVE::RESTEnvironment->get();
-}
-
-# old code uses PVE::RPCEnvironment::is_worker();
-# new code should use PVE::RPCEnvironment->is_worker();
-sub is_worker {
- return PVE::RESTEnvironment->is_worker();
-}
-
-1;
+++ /dev/null
-package PVE::TokenConfig;
-
-use strict;
-use warnings;
-
-use UUID;
-
-use PVE::AccessControl;
-use PVE::Cluster;
-
-my $parse_token_cfg = sub {
- my ($filename, $raw) = @_;
-
- my $parsed = {};
- return $parsed if !defined($raw);
-
- my @lines = split(/\n/, $raw);
- foreach my $line (@lines) {
- next if $line =~ m/^\s*$/;
-
- if ($line =~ m/^(\S+) (\S+)$/) {
- if (PVE::AccessControl::pve_verify_tokenid($1, 1)) {
- $parsed->{$1} = $2;
- next;
- }
- }
-
- warn "skipping invalid token.cfg entry\n";
- }
-
- return $parsed;
-};
-
-my $write_token_cfg = sub {
- my ($filename, $data) = @_;
-
- my $raw = '';
- foreach my $tokenid (sort keys %$data) {
- $raw .= "$tokenid $data->{$tokenid}\n";
- }
-
- return $raw;
-};
-
-PVE::Cluster::cfs_register_file('priv/token.cfg', $parse_token_cfg, $write_token_cfg);
-
-sub generate_token {
- my ($tokenid) = @_;
-
- 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');
-
- $token_cfg->{$tokenid} = $uuid;
-
- PVE::Cluster::cfs_write_file('priv/token.cfg', $token_cfg);
-
- return $uuid;
- });
-
- die "$@\n" if defined($@);
-
- return $token_value;
-}
-
-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');
-
- delete $token_cfg->{$tokenid};
-
- PVE::Cluster::cfs_write_file('priv/token.cfg', $token_cfg);
- });
-
- die "$@\n" if defined($@);
-}
+++ /dev/null
-#!/usr/bin/perl
-
-use strict;
-use warnings;
-use MIME::Base32; #libmime-base32-perl
-
-my $test;
-open(RND, "/dev/urandom");
-sysread(RND, $test, 10) == 10 || die "read random data failed\n";
-print MIME::Base32::encode_rfc3548($test) . "\n";
-
+++ /dev/null
-#!/usr/bin/perl
-
-use strict;
-use warnings;
-
-use PVE::CLI::pveum;
-
-PVE::CLI::pveum->run_cli_handler();
--- /dev/null
+include /usr/share/dpkg/pkg-info.mk
+include /usr/share/dpkg/architecture.mk
+
+PACKAGE=libpve-access-control
+
+BUILDDIR ?= ${PACKAGE}-${DEB_VERSION_UPSTREAM}
+
+DESTDIR=
+PREFIX=/usr
+BINDIR=${PREFIX}/bin
+SBINDIR=${PREFIX}/sbin
+MANDIR=${PREFIX}/share/man
+DOCDIR=${PREFIX}/share/doc/${PACKAGE}
+MAN1DIR=${MANDIR}/man1/
+BASHCOMPLDIR=${PREFIX}/share/bash-completion/completions/
+ZSHCOMPLDIR=${PREFIX}/share/zsh/vendor-completions/
+
+export PERLDIR=${PREFIX}/share/perl5
+-include /usr/share/pve-doc-generator/pve-doc-generator.mk
+
+all:
+
+pveum.bash-completion: PVE/CLI/pveum.pm
+ perl -I. -T -e "use PVE::CLI::pveum; PVE::CLI::pveum->generate_bash_completions();" >$@.tmp
+ mv $@.tmp $@
+
+pveum.zsh-completion: PVE/CLI/pveum.pm
+ perl -I. -T -e "use PVE::CLI::pveum; PVE::CLI::pveum->generate_zsh_completions();" >$@.tmp
+ mv $@.tmp $@
+
+.PHONY: install
+install: pveum.1 oathkeygen pveum.bash-completion pveum.zsh-completion
+ install -d ${DESTDIR}${BINDIR}
+ install -d ${DESTDIR}${SBINDIR}
+ install -m 0755 pveum ${DESTDIR}${SBINDIR}
+ install -m 0755 oathkeygen ${DESTDIR}${BINDIR}
+ make -C PVE install
+ install -d ${DESTDIR}/${MAN1DIR}
+ install -d ${DESTDIR}/${DOCDIR}
+ install -m 0644 pveum.1 ${DESTDIR}/${MAN1DIR}
+ install -m 0644 -D pveum.bash-completion ${DESTDIR}${BASHCOMPLDIR}/pveum
+ install -m 0644 -D pveum.zsh-completion ${DESTDIR}${ZSHCOMPLDIR}/_pveum
+
+.PHONY: test
+test:
+ perl -I. ./pveum verifyapi
+ perl -I. -T -e "use PVE::CLI::pveum; PVE::CLI::pveum->verify_api();"
+ make -C test check
+
+.PHONY: clean distclean
+distclean: clean
+clean:
+ make cleanup-docgen
+ find . -name '*~' -exec rm {} ';'
--- /dev/null
+package PVE::API2::ACL;
+
+use strict;
+use warnings;
+use PVE::Cluster qw (cfs_read_file cfs_write_file);
+use PVE::Tools qw(split_list);
+use PVE::AccessControl;
+use PVE::Exception qw(raise_param_exc);
+use PVE::JSONSchema qw(get_standard_option register_standard_option);
+
+use PVE::SafeSyslog;
+
+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',
+});
+
+__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',
+ },
+ parameters => {
+ 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' },
+ },
+ },
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $authuser = $rpcenv->get_user();
+ my $res = [];
+
+ my $usercfg = $rpcenv->{user_cfg};
+ if (!$usercfg || !$usercfg->{acl}) {
+ return $res;
+ }
+
+ my $audit = $rpcenv->check($authuser, '/access', ['Sys.Audit'], 1);
+
+ my $acl = $usercfg->{acl};
+ foreach my $path (keys %$acl) {
+ foreach my $type (qw(user group token)) {
+ my $d = $acl->{$path}->{"${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;
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'update_acl',
+ protected => 1,
+ path => '',
+ method => 'PUT',
+ permissions => {
+ 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,
+ },
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ 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;
+
+ 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");
+
+ my $propagate = 1;
+
+ if (defined($param->{propagate})) {
+ $propagate = $param->{propagate} ? 1 : 0;
+ }
+
+ foreach my $role (split_list($param->{roles})) {
+ die "role '$role' does not exist\n"
+ if !$cfg->{roles}->{$role};
+
+ foreach my $group (split_list($param->{groups})) {
+
+ die "group '$group' does not exist\n"
+ if !$cfg->{groups}->{$group};
+
+ if ($param->{delete}) {
+ delete($cfg->{acl}->{$path}->{groups}->{$group}->{$role});
+ } else {
+ $cfg->{acl}->{$path}->{groups}->{$group}->{$role} = $propagate;
+ }
+ }
+
+ 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};
+
+ if ($param->{delete}) {
+ delete($cfg->{acl}->{$path}->{users}->{$username}->{$role});
+ } else {
+ $cfg->{acl}->{$path}->{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);
+
+ if ($param->{delete}) {
+ delete $cfg->{acl}->{$path}->{tokens}->{$tokenid}->{$role};
+ } else {
+ $cfg->{acl}->{$path}->{tokens}->{$tokenid}->{$role} = $propagate;
+ }
+ }
+ }
+
+ cfs_write_file("user.cfg", $cfg);
+ }, "ACL update failed");
+
+ return undef;
+ }});
+
+1;
--- /dev/null
+package PVE::API2::AccessControl;
+
+use strict;
+use warnings;
+
+use JSON;
+use MIME::Base64;
+
+use PVE::Exception qw(raise raise_perm_exc raise_param_exc);
+use PVE::SafeSyslog;
+use PVE::RPCEnvironment;
+use PVE::Cluster qw(cfs_read_file);
+use PVE::DataCenterConfig;
+use PVE::RESTHandler;
+use PVE::AccessControl;
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::API2::Domains;
+use PVE::API2::User;
+use PVE::API2::Group;
+use PVE::API2::Role;
+use PVE::API2::ACL;
+use PVE::Auth::Plugin;
+use PVE::OTP;
+use PVE::Tools;
+
+my $u2f_available = 0;
+eval {
+ require PVE::U2F;
+ $u2f_available = 1;
+};
+
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method ({
+ subclass => "PVE::API2::User",
+ path => 'users',
+});
+
+__PACKAGE__->register_method ({
+ subclass => "PVE::API2::Group",
+ path => 'groups',
+});
+
+__PACKAGE__->register_method ({
+ subclass => "PVE::API2::Role",
+ path => 'roles',
+});
+
+__PACKAGE__->register_method ({
+ subclass => "PVE::API2::ACL",
+ path => 'acl',
+});
+
+__PACKAGE__->register_method ({
+ subclass => "PVE::API2::Domains",
+ path => 'domains',
+});
+
+__PACKAGE__->register_method ({
+ name => 'index',
+ path => '',
+ method => 'GET',
+ description => "Directory index.",
+ permissions => {
+ user => 'all',
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {},
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {
+ subdir => { type => 'string' },
+ },
+ },
+ links => [ { rel => 'child', href => "{subdir}" } ],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $res = [];
+
+ my $ma = __PACKAGE__->method_attributes();
+
+ foreach my $info (@$ma) {
+ next if !$info->{subclass};
+
+ my $subpath = $info->{match_re}->[0];
+
+ push @$res, { subdir => $subpath };
+ }
+
+ push @$res, { subdir => 'ticket' };
+ push @$res, { subdir => 'password' };
+
+ return $res;
+ }});
+
+
+my $verify_auth = sub {
+ my ($rpcenv, $username, $pw_or_ticket, $otp, $path, $privs) = @_;
+
+ my $normpath = PVE::AccessControl::normalize_path($path);
+
+ my $ticketuser;
+ 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
+ } else {
+ $username = PVE::AccessControl::authenticate_user($username, $pw_or_ticket, $otp);
+ }
+
+ my $privlist = [ PVE::Tools::split_list($privs) ];
+ if (!($normpath && scalar(@$privlist) && $rpcenv->check($username, $normpath, $privlist))) {
+ die "no permission ($path, $privs)\n";
+ }
+
+ return { username => $username };
+};
+
+my $create_ticket = sub {
+ my ($rpcenv, $username, $pw_or_ticket, $otp) = @_;
+
+ my ($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
+ } else {
+ ($username, $tfa_info) = PVE::AccessControl::authenticate_user($username, $pw_or_ticket, $otp);
+ }
+
+ my %extra;
+ my $ticket_data = $username;
+ if (defined($tfa_info)) {
+ $extra{NeedTFA} = 1;
+ if ($tfa_info->{type} eq 'u2f') {
+ my $u2finfo = $tfa_info->{data};
+ my $u2f = get_u2f_instance($rpcenv, $u2finfo->@{qw(publicKey keyHandle)});
+ my $challenge = $u2f->auth_challenge()
+ or die "failed to get u2f challenge\n";
+ $challenge = decode_json($challenge);
+ $extra{U2FChallenge} = $challenge;
+ $ticket_data = "u2f!$username!$challenge->{challenge}";
+ } else {
+ # General half-login / 'missing 2nd factor' ticket:
+ $ticket_data = "tfa!$username";
+ }
+ }
+
+ my $ticket = PVE::AccessControl::assemble_ticket($ticket_data);
+ my $csrftoken = PVE::AccessControl::assemble_csrf_prevention_token($username);
+
+ return {
+ ticket => $ticket,
+ username => $username,
+ CSRFPreventionToken => $csrftoken,
+ %extra,
+ };
+};
+
+my $compute_api_permission = sub {
+ my ($rpcenv, $authuser) = @_;
+
+ my $usercfg = $rpcenv->{user_cfg};
+
+ 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|SDN\./,
+ };
+ map { $res->{$_} = {} } keys %$priv_re_map;
+
+ my $required_paths = ['/', '/nodes', '/access/groups', '/vms', '/storage', '/sdn'];
+
+ my $checked_paths = {};
+ foreach my $path (@$required_paths, keys %{$usercfg->{acl}}) {
+ next if $checked_paths->{$path};
+ $checked_paths->{$path} = 1;
+
+ my $path_perm = $rpcenv->permissions($authuser, $path);
+
+ my $toplevel = ($path =~ /^\/(\w+)/) ? $1 : 'dc';
+ if ($toplevel eq 'pool') {
+ foreach my $priv (keys %$path_perm) {
+ 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 $priv !~ m/^($priv_regex)/;
+ $res->{$toplevel}->{$priv} = 1;
+ }
+ }
+ }
+
+ return $res;
+};
+
+__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,
+ },
+ returns => { type => "null" },
+ code => sub { return undef; }});
+
+__PACKAGE__->register_method ({
+ name => 'create_ticket',
+ path => 'ticket',
+ method => 'POST',
+ permissions => {
+ 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>\@<relam>.",
+ 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,
+ },
+ }
+ },
+ 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
+ }
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $username = $param->{username};
+ $username .= "\@$param->{realm}" if $param->{realm};
+
+ $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);
+
+ if ($param->{path} && $param->{privs}) {
+ $res = &$verify_auth($rpcenv, $username, $param->{password}, $param->{otp},
+ $param->{path}, $param->{privs});
+ } else {
+ $res = &$create_ticket($rpcenv, $username, $param->{password}, $param->{otp});
+ }
+ };
+ 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} = &$compute_api_permission($rpcenv, $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};
+ }
+
+ PVE::Cluster::log_msg('info', 'root@pam', "successful auth for user '$username'");
+
+ return $res;
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'change_password',
+ path => 'password',
+ method => 'PUT',
+ permissions => {
+ description => "Each user is allowed to change his own password. A user can change the password of another user if he has '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.",
+ 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 => 5,
+ maxLength => 64,
+ },
+ }
+ },
+ returns => { type => "null" },
+ code => sub {
+ my ($param) = @_;
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $authuser = $rpcenv->get_user();
+
+ my ($userid, $ruid, $realm) = PVE::AccessControl::verify_username($param->{userid});
+
+ $rpcenv->check_user_exist($userid);
+
+ 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 its 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::Cluster::log_msg('info', 'root@pam', "changed password for user '$userid'");
+
+ return undef;
+ }});
+
+sub get_u2f_config() {
+ die "u2f support not available\n" if !$u2f_available;
+
+ my $dc = cfs_read_file('datacenter.cfg');
+ my $u2f = $dc->{u2f};
+ die "u2f not configured in datacenter.cfg\n" if !$u2f;
+ return $u2f;
+}
+
+sub get_u2f_instance {
+ my ($rpcenv, $publicKey, $keyHandle) = @_;
+
+ # We store the public key base64 encoded (as the api provides it in binary)
+ $publicKey = decode_base64($publicKey) if defined($publicKey);
+
+ my $u2fconfig = get_u2f_config();
+ my $u2f = PVE::U2F->new();
+
+ # 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";
+ }
+ }
+
+ my $appid = $u2fconfig->{appid} // $origin;
+ $u2f->set_appid($appid);
+ $u2f->set_origin($origin);
+ $u2f->set_publicKey($publicKey) if defined($publicKey);
+ $u2f->set_keyHandle($keyHandle) if defined($keyHandle);
+ return $u2f;
+}
+
+sub verify_user_tfa_config {
+ my ($type, $tfa_cfg, $value) = @_;
+
+ if (!defined($type)) {
+ die "missing tfa 'type'\n";
+ }
+
+ if ($type ne 'oath') {
+ die "invalid type for custom tfa authentication\n";
+ }
+
+ my $secret = $tfa_cfg->{keys}
+ 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";
+ }
+
+ my $digits = delete($tfa_cfg->{digits}) // 6;
+ my $step = delete($tfa_cfg->{step}) // 30;
+ # Maybe also this?
+ # my $algorithm = delete($tfa_cfg->{algorithm}) // 'sha1';
+
+ if (length(my $more = join(', ', keys %$tfa_cfg))) {
+ die "unexpected tfa config keys: $more\n";
+ }
+
+ PVE::OTP::oath_verify_otp($value, $secret, $step, $digits);
+}
+
+__PACKAGE__->register_method ({
+ name => 'change_tfa',
+ path => 'tfa',
+ method => 'PUT',
+ permissions => {
+ description => 'A user can change their own u2f or totp token.',
+ 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's TFA settings
+ description => "Change user u2f authentication.",
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ userid => get_standard_option('userid', {
+ completion => \&PVE::AccessControl::complete_username,
+ }),
+ password => {
+ optional => 1, # Only required if not root@pam
+ description => "The current password.",
+ type => 'string',
+ minLength => 5,
+ maxLength => 64,
+ },
+ action => {
+ description => 'The action to perform',
+ type => 'string',
+ enum => [qw(delete new confirm)],
+ },
+ response => {
+ optional => 1,
+ description =>
+ 'Either the the response to the current u2f registration challenge,'
+ .' or, when adding TOTP, the currently valid TOTP value.',
+ type => 'string',
+ },
+ key => {
+ optional => 1,
+ description => 'When adding TOTP, the shared secret value.',
+ type => 'string',
+ format => 'pve-tfa-secret',
+ },
+ config => {
+ optional => 1,
+ description => 'A TFA configuration. This must currently be of type TOTP of not set at all.',
+ type => 'string',
+ format => 'pve-tfa-config',
+ maxLength => 128,
+ },
+ }
+ },
+ returns => { type => 'object' },
+ code => sub {
+ my ($param) = @_;
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $authuser = $rpcenv->get_user();
+
+ my $action = delete $param->{action};
+ my $response = delete $param->{response};
+ my $password = delete($param->{password}) // '';
+ my $key = delete($param->{key});
+ my $config = delete($param->{config});
+
+ my ($userid, $ruid, $realm) = PVE::AccessControl::verify_username($param->{userid});
+ $rpcenv->check_user_exist($userid);
+
+ # Only root may modify root
+ raise_perm_exc() if $userid eq 'root@pam' && $authuser ne 'root@pam';
+
+ # Regular users need to confirm their password to change u2f settings.
+ if ($authuser ne 'root@pam') {
+ raise_param_exc({ 'password' => 'password is required to modify u2f data' })
+ if !defined($password);
+ my $domain_cfg = cfs_read_file('domains.cfg');
+ my $cfg = $domain_cfg->{ids}->{$realm};
+ die "auth domain '$realm' does not exist\n" if !$cfg;
+ my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
+ $plugin->authenticate_user($cfg, $realm, $ruid, $password);
+ }
+
+ if ($action eq 'delete') {
+ PVE::AccessControl::user_set_tfa($userid, $realm, undef, undef);
+ PVE::Cluster::log_msg('info', $authuser, "deleted u2f data for user '$userid'");
+ } elsif ($action eq 'new') {
+ if (defined($config)) {
+ $config = PVE::Auth::Plugin::parse_tfa_config($config);
+ my $type = delete($config->{type});
+ my $tfa_cfg = {
+ keys => $key,
+ config => $config,
+ };
+ verify_user_tfa_config($type, $tfa_cfg, $response);
+ PVE::AccessControl::user_set_tfa($userid, $realm, $type, $tfa_cfg);
+ } else {
+ # The default is U2F:
+ my $u2f = get_u2f_instance($rpcenv);
+ my $challenge = $u2f->registration_challenge()
+ or raise("failed to get u2f challenge");
+ $challenge = decode_json($challenge);
+ PVE::AccessControl::user_set_tfa($userid, $realm, 'u2f', $challenge);
+ return $challenge;
+ }
+ } elsif ($action eq 'confirm') {
+ raise_param_exc({ 'response' => "confirm action requires the 'response' parameter to be set" })
+ if !defined($response);
+
+ my ($type, $u2fdata) = PVE::AccessControl::user_get_tfa($userid, $realm);
+ raise("no u2f data available")
+ if (!defined($type) || $type ne 'u2f');
+
+ my $challenge = $u2fdata->{challenge}
+ or raise("no active challenge");
+
+ my $u2f = get_u2f_instance($rpcenv);
+ $u2f->set_challenge($challenge);
+ my ($keyHandle, $publicKey) = $u2f->registration_verify($response);
+ PVE::AccessControl::user_set_tfa($userid, $realm, 'u2f', {
+ keyHandle => $keyHandle,
+ publicKey => $publicKey, # already base64 encoded
+ });
+ } else {
+ die "invalid action: $action\n";
+ }
+
+ return {};
+ }});
+
+__PACKAGE__->register_method({
+ name => 'verify_tfa',
+ path => 'tfa',
+ method => 'POST',
+ permissions => { user => 'all' },
+ protected => 1, # else we can't access shadow files
+ allowtoken => 0, # we don't want tokens to access TFA information
+ description => 'Finish a u2f challenge.',
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ response => {
+ type => 'string',
+ description => 'The response to the current authentication challenge.',
+ },
+ }
+ },
+ returns => {
+ type => 'object',
+ properties => {
+ ticket => { type => 'string' },
+ # cap
+ }
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $authuser = $rpcenv->get_user();
+ my ($username, undef, $realm) = PVE::AccessControl::verify_username($authuser);
+
+ my ($tfa_type, $tfa_data) = PVE::AccessControl::user_get_tfa($username, $realm);
+ if (!defined($tfa_type)) {
+ raise('no u2f data available');
+ }
+
+ eval {
+ if ($tfa_type eq 'u2f') {
+ my $challenge = $rpcenv->get_u2f_challenge()
+ or raise('no active challenge');
+
+ my $keyHandle = $tfa_data->{keyHandle};
+ my $publicKey = $tfa_data->{publicKey};
+ raise("incomplete u2f setup")
+ if !defined($keyHandle) || !defined($publicKey);
+
+ my $u2f = get_u2f_instance($rpcenv, $publicKey, $keyHandle);
+ $u2f->set_challenge($challenge);
+
+ my ($counter, $present) = $u2f->auth_verify($param->{response});
+ # Do we want to do anything with these?
+ } else {
+ # sanity check before handing off to the verification code:
+ my $keys = $tfa_data->{keys} or die "missing tfa keys\n";
+ my $config = $tfa_data->{config} or die "bad tfa entry\n";
+ PVE::AccessControl::verify_one_time_pw($tfa_type, $authuser, $keys, $config, $param->{response});
+ }
+ };
+ if (my $err = $@) {
+ my $clientip = $rpcenv->get_client_ip() || '';
+ syslog('err', "authentication verification failure; rhost=$clientip user=$authuser msg=$err");
+ die PVE::Exception->new("authentication failure\n", code => 401);
+ }
+
+ return {
+ ticket => PVE::AccessControl::assemble_ticket($authuser),
+ cap => &$compute_api_permission($rpcenv, $authuser),
+ }
+ }});
+
+__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. A user can dump the permissions of another user 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,
+ }),
+ },
+ },
+ returns => {
+ type => 'object',
+ description => 'Map of "path" => (Map of "privilege" => "propagate boolean").',
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+
+ my $userid = $param->{userid};
+ if (defined($userid)) {
+ $rpcenv->check($rpcenv->get_user(), '/access', ['Sys.Audit']);
+ } else {
+ $userid = $rpcenv->get_user();
+ }
+
+ 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);
+ }
+
+ return $res;
+ }});
+
+1;
--- /dev/null
+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) = @_;
+
+ # always extract, add it with hook
+ my $password = extract_param($param, 'password');
+
+ 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;
+
+ my $opts = $plugin->options();
+ if (defined($password) && !defined($opts->{password})) {
+ $password = undef;
+ warn "ignoring password parameter";
+ }
+ $plugin->on_add_hook($realm, $config, password => $password);
+
+ 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) = @_;
+
+ # always extract, update in hook
+ my $password = extract_param($param, 'password');
+
+ 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);
+
+ 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 $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};
+ }
+
+ my $opts = $plugin->options();
+ if ($delete_pw || defined($password)) {
+ $plugin->on_update_hook($realm, $config, password => $password);
+ } else {
+ $plugin->on_update_hook($realm, $config);
+ }
+
+ 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 "authentication domain '$realm' does not exist\n" if !$ids->{$realm};
+
+ my $plugin = PVE::Auth::Plugin->lookup($ids->{$realm}->{type});
+
+ $plugin->on_delete_hook($realm, $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 $cfg_defaults = {};
+ if (defined(my $cfg_opts = $realmconfig->{'sync-defaults-options'})) {
+ $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};
+
+ $res->{$opt} = $param->{$opt} // $cfg_defaults->{$opt} // $fmt->{default};
+
+ raise_param_exc({
+ "$opt" => 'Not passed as parameter and not defined in realm default sync options.'
+ }) if !defined($res->{$opt});
+ }
+ return $res;
+};
+
+__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',
+ [ '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'),
+ 'dry-run' => {
+ description => "If set, does not write anything.",
+ type => 'boolean',
+ optional => 1,
+ default => 0,
+ },
+ }),
+ },
+ returns => {
+ description => 'Worker Task-UPID',
+ type => 'string'
+ },
+ code => sub {
+ my ($param) = @_;
+
+ 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};
+
+ 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 "(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);
+ }
+
+ 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);
+ }
+
+ 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);
+ }});
+
+1;
--- /dev/null
+package PVE::API2::Group;
+
+use strict;
+use warnings;
+use PVE::Cluster qw (cfs_read_file cfs_write_file);
+use PVE::AccessControl;
+use PVE::SafeSyslog;
+use PVE::RESTHandler;
+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-comment', { type => 'string', optional => 1 });
+
+__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',
+ },
+ parameters => {
+ 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}" } ],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $res = [];
+
+ 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'];
+
+ 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;
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'create_group',
+ protected => 1,
+ path => '',
+ method => 'POST',
+ permissions => {
+ 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'),
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ PVE::AccessControl::lock_user_config(
+ sub {
+
+ my $usercfg = cfs_read_file("user.cfg");
+
+ my $group = $param->{groupid};
+
+ die "group '$group' already exists\n"
+ if $usercfg->{groups}->{$group};
+
+ $usercfg->{groups}->{$group} = { users => {} };
+
+ $usercfg->{groups}->{$group}->{comment} = $param->{comment} if $param->{comment};
+
+
+ cfs_write_file("user.cfg", $usercfg);
+ }, "create group failed");
+
+ return undef;
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'update_group',
+ protected => 1,
+ path => '{groupid}',
+ method => 'PUT',
+ permissions => {
+ 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'),
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ PVE::AccessControl::lock_user_config(
+ sub {
+
+ my $usercfg = cfs_read_file("user.cfg");
+
+ my $group = $param->{groupid};
+
+ my $data = $usercfg->{groups}->{$group};
+
+ die "group '$group' does not exist\n"
+ if !$data;
+
+ $data->{comment} = $param->{comment} if defined($param->{comment});
+
+ cfs_write_file("user.cfg", $usercfg);
+ }, "update group failed");
+
+ return undef;
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'read_group',
+ path => '{groupid}',
+ method => 'GET',
+ permissions => {
+ check => ['perm', '/access/groups', ['Sys.Audit', 'Group.Allocate'], any => 1],
+ },
+ description => "Get group configuration.",
+ parameters => {
+ 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')
+ },
+ },
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $group = $param->{groupid};
+
+ my $usercfg = cfs_read_file("user.cfg");
+
+ my $data = $usercfg->{groups}->{$group};
+
+ die "group '$group' does not exist\n" if !$data;
+
+ my $members = $data->{users} ? [ keys %{$data->{users}} ] : [];
+
+ my $res = { members => $members };
+
+ $res->{comment} = $data->{comment} if defined($data->{comment});
+
+ return $res;
+ }});
+
+
+__PACKAGE__->register_method ({
+ name => 'delete_group',
+ protected => 1,
+ path => '{groupid}',
+ method => 'DELETE',
+ permissions => {
+ check => ['perm', '/access/groups', ['Group.Allocate']],
+ },
+ description => "Delete group.",
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ groupid => get_standard_option('group-id'),
+ }
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ PVE::AccessControl::lock_user_config(
+ sub {
+
+ my $usercfg = cfs_read_file("user.cfg");
+
+ my $group = $param->{groupid};
+
+ die "group '$group' does not exist\n"
+ if !$usercfg->{groups}->{$group};
+
+ delete ($usercfg->{groups}->{$group});
+
+ PVE::AccessControl::delete_group_acl($group, $usercfg);
+
+ cfs_write_file("user.cfg", $usercfg);
+ }, "delete group failed");
+
+ return undef;
+ }});
+
+1;
--- /dev/null
+
+API2_SOURCES= \
+ AccessControl.pm \
+ Domains.pm \
+ ACL.pm \
+ Role.pm \
+ Group.pm \
+ User.pm
+
+.PHONY: install
+install:
+ for i in ${API2_SOURCES}; do install -D -m 0644 $$i ${DESTDIR}${PERLDIR}/PVE/API2/$$i; done
--- /dev/null
+package PVE::API2::Role;
+
+use strict;
+use warnings;
+use PVE::Cluster qw (cfs_read_file cfs_write_file);
+use PVE::AccessControl;
+use PVE::JSONSchema qw(get_standard_option register_standard_option);
+
+use PVE::SafeSyslog;
+
+use PVE::RESTHandler;
+
+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,
+});
+
+__PACKAGE__->register_method ({
+ name => 'index',
+ path => '',
+ method => 'GET',
+ description => "Role index.",
+ permissions => {
+ user => 'all',
+ },
+ parameters => {
+ 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}" } ],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $res = [];
+
+ 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),
+ };
+ }
+
+ return $res;
+}});
+
+__PACKAGE__->register_method ({
+ name => 'create_role',
+ protected => 1,
+ path => '',
+ method => 'POST',
+ permissions => {
+ 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'),
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ PVE::AccessControl::lock_user_config(
+ sub {
+
+ my $usercfg = cfs_read_file("user.cfg");
+
+ my $role = $param->{roleid};
+
+ die "role '$role' already exists\n"
+ if $usercfg->{roles}->{$role};
+
+ $usercfg->{roles}->{$role} = {};
+
+ PVE::AccessControl::add_role_privs($role, $usercfg, $param->{privs});
+
+ cfs_write_file("user.cfg", $usercfg);
+ }, "create role failed");
+
+ return undef;
+}});
+
+__PACKAGE__->register_method ({
+ name => 'update_role',
+ protected => 1,
+ path => '{roleid}',
+ method => 'PUT',
+ permissions => {
+ 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' },
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $role = $param->{roleid};
+
+ 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");
+
+ die "role '$role' does not exist\n"
+ if !$usercfg->{roles}->{$role};
+
+ $usercfg->{roles}->{$role} = {} if !$param->{append};
+
+ PVE::AccessControl::add_role_privs($role, $usercfg, $param->{privs});
+
+ cfs_write_file("user.cfg", $usercfg);
+ }, "update role failed");
+
+ return undef;
+}});
+
+__PACKAGE__->register_method ({
+ name => 'read_role',
+ path => '{roleid}',
+ method => 'GET',
+ permissions => {
+ user => 'all',
+ },
+ description => "Get role configuration.",
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ roleid => get_standard_option('role-id'),
+ },
+ },
+ returns => {
+ type => "object",
+ additionalProperties => 0,
+ properties => PVE::AccessControl::create_priv_properties(),
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $usercfg = cfs_read_file("user.cfg");
+
+ my $role = $param->{roleid};
+
+ my $data = $usercfg->{roles}->{$role};
+
+ die "role '$role' does not exist\n" if !$data;
+
+ return $data;
+ }
+});
+
+__PACKAGE__->register_method ({
+ name => 'delete_role',
+ protected => 1,
+ path => '{roleid}',
+ method => 'DELETE',
+ permissions => {
+ check => ['perm', '/access', ['Sys.Modify']],
+ },
+ description => "Delete role.",
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ roleid => get_standard_option('role-id'),
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $role = $param->{roleid};
+
+ 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");
+
+ die "role '$role' does not exist\n"
+ if !$usercfg->{roles}->{$role};
+
+ delete ($usercfg->{roles}->{$role});
+
+ # fixme: delete role from acl?
+
+ cfs_write_file("user.cfg", $usercfg);
+ }, "delete role failed");
+
+ return undef;
+ }
+});
+
+1;
--- /dev/null
+package PVE::API2::User;
+
+use strict;
+use warnings;
+use PVE::Exception qw(raise raise_perm_exc raise_param_exc);
+use PVE::Cluster qw (cfs_read_file cfs_write_file);
+use PVE::Tools qw(split_list extract_param);
+use PVE::AccessControl;
+use PVE::JSONSchema qw(get_standard_option register_standard_option);
+use PVE::TokenConfig;
+
+use PVE::SafeSyslog;
+
+use PVE::RESTHandler;
+
+use base qw(PVE::RESTHandler);
+
+register_standard_option('user-enable', {
+ description => "Enable the account (default). You can set this to '0' to disable the account",
+ type => 'boolean',
+ optional => 1,
+ default => 1,
+});
+register_standard_option('user-expire', {
+ description => "Account expiration date (seconds since epoch). '0' means no expiration date.",
+ type => 'integer',
+ minimum => 0,
+ optional => 1,
+});
+register_standard_option('user-firstname', { type => 'string', optional => 1 });
+register_standard_option('user-lastname', { type => 'string', optional => 1 });
+register_standard_option('user-email', { type => 'string', optional => 1, format => 'email-opt' });
+register_standard_option('user-comment', { type => 'string', optional => 1 });
+register_standard_option('user-keys', {
+ description => "Keys for two factor auth (yubico).",
+ type => 'string',
+ optional => 1,
+});
+register_standard_option('group-list', {
+ type => 'string', format => 'pve-groupid-list',
+ optional => 1,
+ completion => \&PVE::AccessControl::complete_group,
+});
+register_standard_option('token-subid', {
+ type => 'string',
+ pattern => $PVE::AccessControl::token_subid_regex,
+ description => 'User-specific token identifier.',
+});
+register_standard_option('token-expire', {
+ description => "API token expiration date (seconds since epoch). '0' means no expiration date.",
+ type => 'integer',
+ minimum => 0,
+ optional => 1,
+ default => 'same as user',
+});
+register_standard_option('token-privsep', {
+ description => "Restrict API token privileges with separate ACLs (default), or give full privileges of corresponding user.",
+ type => 'boolean',
+ optional => 1,
+ default => 1,
+});
+register_standard_option('token-comment', { type => 'string', optional => 1 });
+register_standard_option('token-info', {
+ type => 'object',
+ properties => {
+ expire => get_standard_option('token-expire'),
+ privsep => get_standard_option('token-privsep'),
+ comment => get_standard_option('token-comment'),
+ }
+});
+
+my $token_info_extend = sub {
+ my ($props) = @_;
+
+ my $obj = get_standard_option('token-info');
+ my $base_props = $obj->{properties};
+ $obj->{properties} = {};
+
+ foreach my $prop (keys %$base_props) {
+ $obj->{properties}->{$prop} = $base_props->{$prop};
+ }
+
+ foreach my $add_prop (keys %$props) {
+ $obj->{properties}->{$add_prop} = $props->{$add_prop};
+ }
+
+ return $obj;
+};
+
+my $extract_user_data = sub {
+ my ($data, $full) = @_;
+
+ my $res = {};
+
+ foreach my $prop (qw(enable expire firstname lastname email comment keys)) {
+ $res->{$prop} = $data->{$prop} if defined($data->{$prop});
+ }
+
+ return $res if !$full;
+
+ $res->{groups} = $data->{groups} ? [ keys %{$data->{groups}} ] : [];
+ $res->{tokens} = $data->{tokens};
+
+ return $res;
+};
+
+__PACKAGE__->register_method ({
+ name => 'index',
+ path => '',
+ method => 'GET',
+ description => "User index.",
+ permissions => {
+ description => "The returned list is restricted to users where you have 'User.Modify' or 'Sys.Audit' permissions on '/access/groups' or on a group the user belongs too. But it always includes the current (authenticated) user.",
+ user => 'all',
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ enabled => {
+ type => 'boolean',
+ description => "Optional filter for enable property.",
+ optional => 1,
+ },
+ full => {
+ type => 'boolean',
+ description => "Include group and token information.",
+ optional => 1,
+ default => 0,
+ }
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {
+ userid => get_standard_option('userid-completed'),
+ enable => get_standard_option('user-enable'),
+ expire => get_standard_option('user-expire'),
+ firstname => get_standard_option('user-firstname'),
+ lastname => get_standard_option('user-lastname'),
+ email => get_standard_option('user-email'),
+ comment => get_standard_option('user-comment'),
+ keys => get_standard_option('user-keys'),
+ groups => get_standard_option('group-list'),
+ tokens => {
+ type => 'array',
+ optional => 1,
+ items => $token_info_extend->({
+ tokenid => get_standard_option('token-subid'),
+ }),
+ }
+ },
+ },
+ links => [ { rel => 'child', href => "{userid}" } ],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $usercfg = $rpcenv->{user_cfg};
+ my $authuser = $rpcenv->get_user();
+
+ my $res = [];
+
+ my $privs = [ 'User.Modify', 'Sys.Audit' ];
+ my $canUserMod = $rpcenv->check_any($authuser, "/access/groups", $privs, 1);
+ my $groups = $rpcenv->filter_groups($authuser, $privs, 1);
+ my $allowed_users = $rpcenv->group_member_join([keys %$groups]);
+
+ foreach my $user (keys %{$usercfg->{users}}) {
+ if (!($canUserMod || $user eq $authuser)) {
+ next if !$allowed_users->{$user};
+ }
+
+ my $entry = &$extract_user_data($usercfg->{users}->{$user}, $param->{full});
+
+ if (defined($param->{enabled})) {
+ next if $entry->{enable} && !$param->{enabled};
+ next if !$entry->{enable} && $param->{enabled};
+ }
+
+ $entry->{groups} = join(',', @{$entry->{groups}}) if $entry->{groups};
+ $entry->{tokens} = [ map { { tokenid => $_, %{$entry->{tokens}->{$_}} } } sort keys %{$entry->{tokens}} ]
+ if defined($entry->{tokens});
+
+ $entry->{userid} = $user;
+ push @$res, $entry;
+ }
+
+ return $res;
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'create_user',
+ protected => 1,
+ path => '',
+ method => 'POST',
+ permissions => {
+ description => "You need 'Realm.AllocateUser' on '/access/realm/<realm>' on the realm of user <userid>, and 'User.Modify' permissions to '/access/groups/<group>' for any group specified (or 'User.Modify' on '/access/groups' if you pass no groups.",
+ check => [ 'and',
+ [ 'userid-param', 'Realm.AllocateUser'],
+ [ 'userid-group', ['User.Modify'], groups_param => 1],
+ ],
+ },
+ description => "Create new user.",
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ userid => get_standard_option('userid-completed'),
+ enable => get_standard_option('user-enable'),
+ expire => get_standard_option('user-expire'),
+ firstname => get_standard_option('user-firstname'),
+ lastname => get_standard_option('user-lastname'),
+ email => get_standard_option('user-email'),
+ comment => get_standard_option('user-comment'),
+ keys => get_standard_option('user-keys'),
+ password => {
+ description => "Initial password.",
+ type => 'string',
+ optional => 1,
+ minLength => 5,
+ maxLength => 64
+ },
+ groups => get_standard_option('group-list'),
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ PVE::AccessControl::lock_user_config(sub {
+ my ($username, $ruid, $realm) = PVE::AccessControl::verify_username($param->{userid});
+
+ my $usercfg = cfs_read_file("user.cfg");
+
+ # ensure "user exists" check works for case insensitive realms
+ $username = PVE::AccessControl::lookup_username($username, 1);
+ die "user '$username' already exists\n" if $usercfg->{users}->{$username};
+
+ PVE::AccessControl::domain_set_password($realm, $ruid, $param->{password})
+ if defined($param->{password});
+
+ my $enable = defined($param->{enable}) ? $param->{enable} : 1;
+ $usercfg->{users}->{$username} = { enable => $enable };
+ $usercfg->{users}->{$username}->{expire} = $param->{expire} if $param->{expire};
+
+ if ($param->{groups}) {
+ foreach my $group (split_list($param->{groups})) {
+ if ($usercfg->{groups}->{$group}) {
+ PVE::AccessControl::add_user_group($username, $usercfg, $group);
+ } else {
+ die "no such group '$group'\n";
+ }
+ }
+ }
+
+ $usercfg->{users}->{$username}->{firstname} = $param->{firstname} if $param->{firstname};
+ $usercfg->{users}->{$username}->{lastname} = $param->{lastname} if $param->{lastname};
+ $usercfg->{users}->{$username}->{email} = $param->{email} if $param->{email};
+ $usercfg->{users}->{$username}->{comment} = $param->{comment} if $param->{comment};
+ $usercfg->{users}->{$username}->{keys} = $param->{keys} if $param->{keys};
+
+ cfs_write_file("user.cfg", $usercfg);
+ }, "create user failed");
+
+ return undef;
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'read_user',
+ path => '{userid}',
+ method => 'GET',
+ description => "Get user configuration.",
+ permissions => {
+ check => ['userid-group', ['User.Modify', 'Sys.Audit']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ userid => get_standard_option('userid-completed'),
+ },
+ },
+ returns => {
+ additionalProperties => 0,
+ properties => {
+ enable => get_standard_option('user-enable'),
+ expire => get_standard_option('user-expire'),
+ firstname => get_standard_option('user-firstname'),
+ lastname => get_standard_option('user-lastname'),
+ email => get_standard_option('user-email'),
+ comment => get_standard_option('user-comment'),
+ keys => get_standard_option('user-keys'),
+ groups => {
+ type => 'array',
+ optional => 1,
+ items => {
+ type => 'string',
+ format => 'pve-groupid',
+ },
+ },
+ tokens => {
+ optional => 1,
+ type => 'object',
+ },
+ },
+ type => "object"
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my ($username, undef, $domain) =
+ PVE::AccessControl::verify_username($param->{userid});
+
+ my $usercfg = cfs_read_file("user.cfg");
+
+ my $data = PVE::AccessControl::check_user_exist($usercfg, $username);
+
+ return &$extract_user_data($data, 1);
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'update_user',
+ protected => 1,
+ path => '{userid}',
+ method => 'PUT',
+ permissions => {
+ check => ['userid-group', ['User.Modify'], groups_param => 1 ],
+ },
+ description => "Update user configuration.",
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ userid => get_standard_option('userid-completed'),
+ enable => get_standard_option('user-enable'),
+ expire => get_standard_option('user-expire'),
+ firstname => get_standard_option('user-firstname'),
+ lastname => get_standard_option('user-lastname'),
+ email => get_standard_option('user-email'),
+ comment => get_standard_option('user-comment'),
+ keys => get_standard_option('user-keys'),
+ groups => get_standard_option('group-list'),
+ append => {
+ type => 'boolean',
+ optional => 1,
+ requires => 'groups',
+ },
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my ($username, $ruid, $realm) =
+ PVE::AccessControl::verify_username($param->{userid});
+
+ PVE::AccessControl::lock_user_config(
+ sub {
+
+ my $usercfg = cfs_read_file("user.cfg");
+
+ PVE::AccessControl::check_user_exist($usercfg, $username);
+
+ $usercfg->{users}->{$username}->{enable} = $param->{enable} if defined($param->{enable});
+
+ $usercfg->{users}->{$username}->{expire} = $param->{expire} if defined($param->{expire});
+
+ PVE::AccessControl::delete_user_group($username, $usercfg)
+ if (!$param->{append} && defined($param->{groups}));
+
+ if ($param->{groups}) {
+ foreach my $group (split_list($param->{groups})) {
+ if ($usercfg->{groups}->{$group}) {
+ PVE::AccessControl::add_user_group($username, $usercfg, $group);
+ } else {
+ die "no such group '$group'\n";
+ }
+ }
+ }
+
+ $usercfg->{users}->{$username}->{firstname} = $param->{firstname} if defined($param->{firstname});
+ $usercfg->{users}->{$username}->{lastname} = $param->{lastname} if defined($param->{lastname});
+ $usercfg->{users}->{$username}->{email} = $param->{email} if defined($param->{email});
+ $usercfg->{users}->{$username}->{comment} = $param->{comment} if defined($param->{comment});
+ $usercfg->{users}->{$username}->{keys} = $param->{keys} if defined($param->{keys});
+
+ cfs_write_file("user.cfg", $usercfg);
+ }, "update user failed");
+
+ return undef;
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'delete_user',
+ protected => 1,
+ path => '{userid}',
+ method => 'DELETE',
+ description => "Delete user.",
+ permissions => {
+ check => [ 'and',
+ [ 'userid-param', 'Realm.AllocateUser'],
+ [ 'userid-group', ['User.Modify']],
+ ],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ userid => get_standard_option('userid-completed'),
+ }
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $authuser = $rpcenv->get_user();
+
+ my ($userid, $ruid, $realm) =
+ PVE::AccessControl::verify_username($param->{userid});
+
+ PVE::AccessControl::lock_user_config(
+ sub {
+
+ my $usercfg = cfs_read_file("user.cfg");
+
+ my $domain_cfg = cfs_read_file('domains.cfg');
+ if (my $cfg = $domain_cfg->{ids}->{$realm}) {
+ my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
+ $plugin->delete_user($cfg, $realm, $ruid);
+ }
+
+ # Remove TFA data before removing the user entry as the user entry tells us whether
+ # we need ot update priv/tfa.cfg.
+ PVE::AccessControl::user_set_tfa($userid, $realm, undef, undef, $usercfg, $domain_cfg);
+
+ delete $usercfg->{users}->{$userid};
+
+ PVE::AccessControl::delete_user_group($userid, $usercfg);
+ PVE::AccessControl::delete_user_acl($userid, $usercfg);
+ cfs_write_file("user.cfg", $usercfg);
+ }, "delete user failed");
+
+ return undef;
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'read_user_tfa_type',
+ path => '{userid}/tfa',
+ method => 'GET',
+ protected => 1,
+ description => "Get user TFA types (Personal and Realm).",
+ permissions => {
+ check => [ 'or',
+ ['userid-param', 'self'],
+ ['userid-group', ['User.Modify', 'Sys.Audit']],
+ ],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ userid => get_standard_option('userid-completed'),
+ },
+ },
+ returns => {
+ additionalProperties => 0,
+ properties => {
+ realm => {
+ type => 'string',
+ enum => [qw(oath yubico)],
+ description => "The type of TFA the users realm has set, if any.",
+ optional => 1,
+ },
+ user => {
+ type => 'string',
+ enum => [qw(oath u2f)],
+ description => "The type of TFA the user has set, if any.",
+ optional => 1,
+ },
+ },
+ type => "object"
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my ($username, undef, $realm) = PVE::AccessControl::verify_username($param->{userid});
+
+
+ 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_tfa = PVE::Auth::Plugin::parse_tfa_config($realm_cfg->{tfa})
+ if $realm_cfg->{tfa};
+
+ my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
+ my $tfa = $tfa_cfg->{users}->{$username};
+
+ my $res = {};
+ $res->{realm} = $realm_tfa->{type} if $realm_tfa->{type};
+ $res->{user} = $tfa->{type} if $tfa->{type};
+ return $res;
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'token_index',
+ path => '{userid}/token',
+ method => 'GET',
+ description => "Get user API tokens.",
+ permissions => {
+ check => ['or',
+ ['userid-param', 'self'],
+ ['perm', '/access/users/{userid}', ['User.Modify']],
+ ],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ userid => get_standard_option('userid-completed'),
+ },
+ },
+ returns => {
+ type => "array",
+ items => $token_info_extend->({
+ tokenid => get_standard_option('token-subid'),
+ }),
+ links => [ { rel => 'child', href => "{tokenid}" } ],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $userid = PVE::AccessControl::verify_username($param->{userid});
+ my $usercfg = cfs_read_file("user.cfg");
+
+ my $user = PVE::AccessControl::check_user_exist($usercfg, $userid);
+
+ my $tokens = $user->{tokens} // {};
+ return [ map { $tokens->{$_}->{tokenid} = $_; $tokens->{$_} } keys %$tokens];
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'read_token',
+ path => '{userid}/token/{tokenid}',
+ method => 'GET',
+ description => "Get specific API token information.",
+ permissions => {
+ check => ['or',
+ ['userid-param', 'self'],
+ ['perm', '/access/users/{userid}', ['User.Modify']],
+ ],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ userid => get_standard_option('userid-completed'),
+ tokenid => get_standard_option('token-subid'),
+ },
+ },
+ returns => get_standard_option('token-info'),
+ code => sub {
+ my ($param) = @_;
+
+ my $userid = PVE::AccessControl::verify_username($param->{userid});
+ my $tokenid = $param->{tokenid};
+
+ my $usercfg = cfs_read_file("user.cfg");
+
+ return PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid);
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'generate_token',
+ path => '{userid}/token/{tokenid}',
+ method => 'POST',
+ description => "Generate a new API token for a specific user. NOTE: returns API token value, which needs to be stored as it cannot be retrieved afterwards!",
+ protected => 1,
+ permissions => {
+ check => ['or',
+ ['userid-param', 'self'],
+ ['perm', '/access/users/{userid}', ['User.Modify']],
+ ],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ userid => get_standard_option('userid-completed'),
+ tokenid => get_standard_option('token-subid'),
+ expire => get_standard_option('token-expire'),
+ privsep => get_standard_option('token-privsep'),
+ comment => get_standard_option('token-comment'),
+ },
+ },
+ returns => {
+ additionalProperties => 0,
+ type => "object",
+ properties => {
+ info => get_standard_option('token-info'),
+ value => {
+ type => 'string',
+ description => 'API token value used for authentication.',
+ },
+ 'full-tokenid' => {
+ type => 'string',
+ format_description => '<userid>!<tokenid>',
+ description => 'The full token id.',
+ },
+ },
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $userid = PVE::AccessControl::verify_username(extract_param($param, 'userid'));
+ my $tokenid = extract_param($param, 'tokenid');
+
+ my $usercfg = cfs_read_file("user.cfg");
+
+ my $token = PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid, 1);
+ my ($full_tokenid, $value);
+
+ PVE::AccessControl::check_user_exist($usercfg, $userid);
+ raise_param_exc({ 'tokenid' => 'Token already exists.' }) if defined($token);
+
+ my $generate_and_add_token = sub {
+ $usercfg = cfs_read_file("user.cfg");
+ PVE::AccessControl::check_user_exist($usercfg, $userid);
+ die "Token already exists.\n" if defined(PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid, 1));
+
+ $full_tokenid = PVE::AccessControl::join_tokenid($userid, $tokenid);
+ $value = PVE::TokenConfig::generate_token($full_tokenid);
+
+ $token = {};
+ $token->{privsep} = defined($param->{privsep}) ? $param->{privsep} : 1;
+ $token->{expire} = $param->{expire} if defined($param->{expire});
+ $token->{comment} = $param->{comment} if $param->{comment};
+
+ $usercfg->{users}->{$userid}->{tokens}->{$tokenid} = $token;
+ cfs_write_file("user.cfg", $usercfg);
+ };
+
+ PVE::AccessControl::lock_user_config($generate_and_add_token, 'generating token failed');
+
+ return {
+ info => $token,
+ value => $value,
+ 'full-tokenid' => $full_tokenid,
+ };
+ }});
+
+
+__PACKAGE__->register_method ({
+ name => 'update_token_info',
+ path => '{userid}/token/{tokenid}',
+ method => 'PUT',
+ description => "Update API token for a specific user.",
+ protected => 1,
+ permissions => {
+ check => ['or',
+ ['userid-param', 'self'],
+ ['perm', '/access/users/{userid}', ['User.Modify']],
+ ],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ userid => get_standard_option('userid-completed'),
+ tokenid => get_standard_option('token-subid'),
+ expire => get_standard_option('token-expire'),
+ privsep => get_standard_option('token-privsep'),
+ comment => get_standard_option('token-comment'),
+ },
+ },
+ returns => get_standard_option('token-info', { description => "Updated token information." }),
+ code => sub {
+ my ($param) = @_;
+
+ my $userid = PVE::AccessControl::verify_username(extract_param($param, 'userid'));
+ my $tokenid = extract_param($param, 'tokenid');
+
+ my $usercfg = cfs_read_file("user.cfg");
+ my $token = PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid);
+
+ my $update_token = sub {
+ $usercfg = cfs_read_file("user.cfg");
+ $token = PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid);
+
+ my $full_tokenid = PVE::AccessControl::join_tokenid($userid, $tokenid);
+
+ $token->{privsep} = $param->{privsep} if defined($param->{privsep});
+ $token->{expire} = $param->{expire} if defined($param->{expire});
+ $token->{comment} = $param->{comment} if $param->{comment};
+
+ $usercfg->{users}->{$userid}->{tokens}->{$tokenid} = $token;
+ cfs_write_file("user.cfg", $usercfg);
+ };
+
+ PVE::AccessControl::lock_user_config($update_token, 'updating token info failed');
+
+ return $token;
+ }});
+
+
+__PACKAGE__->register_method ({
+ name => 'remove_token',
+ path => '{userid}/token/{tokenid}',
+ method => 'DELETE',
+ description => "Remove API token for a specific user.",
+ protected => 1,
+ permissions => {
+ check => ['or',
+ ['userid-param', 'self'],
+ ['perm', '/access/users/{userid}', ['User.Modify']],
+ ],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ userid => get_standard_option('userid-completed'),
+ tokenid => get_standard_option('token-subid'),
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $userid = PVE::AccessControl::verify_username(extract_param($param, 'userid'));
+ my $tokenid = extract_param($param, 'tokenid');
+
+ my $usercfg = cfs_read_file("user.cfg");
+ my $token = PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid);
+
+ my $update_token = sub {
+ $usercfg = cfs_read_file("user.cfg");
+
+ PVE::AccessControl::check_token_exist($usercfg, $userid, $tokenid);
+
+ my $full_tokenid = PVE::AccessControl::join_tokenid($userid, $tokenid);
+ PVE::TokenConfig::delete_token($full_tokenid);
+ delete $usercfg->{users}->{$userid}->{tokens}->{$tokenid};
+
+ cfs_write_file("user.cfg", $usercfg);
+ };
+
+ PVE::AccessControl::lock_user_config($update_token, 'deleting token failed');
+
+ return;
+ }});
+1;
--- /dev/null
+package PVE::AccessControl;
+
+use strict;
+use warnings;
+use Encode;
+use Crypt::OpenSSL::Random;
+use Crypt::OpenSSL::RSA;
+use Net::SSLeay;
+use Net::IP;
+use MIME::Base64;
+use Digest::SHA;
+use IO::File;
+use File::stat;
+use JSON;
+
+use PVE::OTP;
+use PVE::Ticket;
+use PVE::Tools qw(run_command lock_file file_get_contents split_list safe_print);
+use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_write_file cfs_lock_file);
+use PVE::JSONSchema qw(register_standard_option get_standard_option);
+
+use PVE::Auth::Plugin;
+use PVE::Auth::AD;
+use PVE::Auth::LDAP;
+use PVE::Auth::PVE;
+use PVE::Auth::PAM;
+
+# load and initialize all plugins
+
+PVE::Auth::AD->register();
+PVE::Auth::LDAP->register();
+PVE::Auth::PVE->register();
+PVE::Auth::PAM->register();
+PVE::Auth::Plugin->init();
+
+# $authdir must be writable by root only!
+my $confdir = "/etc/pve";
+my $authdir = "$confdir/priv";
+
+my $pve_www_key_fn = "$confdir/pve-www.key";
+
+my $pve_auth_key_files = {
+ priv => "$authdir/authkey.key",
+ pub => "$confdir/authkey.pub",
+ pubold => "$confdir/authkey.pub.old",
+};
+
+my $pve_auth_key_cache = {};
+
+my $ticket_lifetime = 3600 * 2; # 2 hours
+my $auth_graceperiod = 60 * 5; # 5 minutes
+my $authkey_lifetime = 3600 * 24; # rotate every 24 hours
+
+Crypt::OpenSSL::RSA->import_random_seed();
+
+cfs_register_file('user.cfg',
+ \&parse_user_config,
+ \&write_user_config);
+cfs_register_file('priv/tfa.cfg',
+ \&parse_priv_tfa_config,
+ \&write_priv_tfa_config);
+
+sub verify_username {
+ PVE::Auth::Plugin::verify_username(@_);
+}
+
+sub pve_verify_realm {
+ PVE::Auth::Plugin::pve_verify_realm(@_);
+}
+
+sub lock_user_config {
+ my ($code, $errmsg) = @_;
+
+ cfs_lock_file("user.cfg", undef, $code);
+ if (my $err = $@) {
+ $errmsg ? die "$errmsg: $err" : die $err;
+ }
+}
+
+my $cache_read_key = sub {
+ my ($type) = @_;
+
+ my $path = $pve_auth_key_files->{$type};
+
+ my $read_key_and_mtime = sub {
+ my $fh = IO::File->new($path, "r");
+
+ return undef if !defined($fh);
+
+ my $st = stat($fh);
+ my $pem = PVE::Tools::safe_read_from($fh, 0, 0, $path);
+
+ close $fh;
+
+ my $key;
+ if ($type eq 'pub' || $type eq 'pubold') {
+ $key = eval { Crypt::OpenSSL::RSA->new_public_key($pem); };
+ } elsif ($type eq 'priv') {
+ $key = eval { Crypt::OpenSSL::RSA->new_private_key($pem); };
+ } else {
+ die "Invalid authkey type '$type'\n";
+ }
+
+ return { key => $key, mtime => $st->mtime };
+ };
+
+ if (!defined($pve_auth_key_cache->{$type})) {
+ $pve_auth_key_cache->{$type} = $read_key_and_mtime->();
+ } else {
+ my $st = stat($path);
+ if (!$st || $st->mtime != $pve_auth_key_cache->{$type}->{mtime}) {
+ $pve_auth_key_cache->{$type} = $read_key_and_mtime->();
+ }
+ }
+
+ return $pve_auth_key_cache->{$type};
+};
+
+sub get_pubkey {
+ my ($old) = @_;
+
+ my $type = $old ? 'pubold' : 'pub';
+
+ my $res = $cache_read_key->($type);
+ return undef if !defined($res);
+
+ return wantarray ? ($res->{key}, $res->{mtime}) : $res->{key};
+}
+
+sub get_privkey {
+ my $res = $cache_read_key->('priv');
+
+ if (!defined($res) || !check_authkey(1)) {
+ rotate_authkey();
+ $res = $cache_read_key->('priv');
+ }
+
+ return wantarray ? ($res->{key}, $res->{mtime}) : $res->{key};
+}
+
+sub check_authkey {
+ my ($quiet) = @_;
+
+ # skip check if non-quorate, as rotation is not possible anyway
+ return 1 if !PVE::Cluster::check_cfs_quorum(1);
+
+ my ($pub_key, $mtime) = get_pubkey();
+ if (!$pub_key) {
+ warn "auth key pair missing, generating new one..\n" if !$quiet;
+ return 0;
+ } else {
+ my $now = time();
+ if ($now - $mtime >= $authkey_lifetime) {
+ warn "auth key pair too old, rotating..\n" if !$quiet;;
+ return 0;
+ } elsif ($mtime > $now + $auth_graceperiod) {
+ # a nodes RTC had a time set in the future during key generation -> ticket
+ # validity is clamped to 0+5 min grace period until now >= mtime again
+ my (undef, $old_mtime) = get_pubkey(1);
+ if ($old_mtime && $mtime >= $old_mtime && $mtime - $old_mtime < $ticket_lifetime) {
+ warn "auth key pair generated in the future (key $mtime > host $now),"
+ ." but old key still exists and in valid grace period so avoid automatic"
+ ." fixup. Cluster time not in sync?\n" if !$quiet;
+ return 1;
+ }
+ warn "auth key pair generated in the future (key $mtime > host $now), rotating..\n" if !$quiet;
+ return 0;
+ } else {
+ warn "auth key new enough, skipping rotation\n" if !$quiet;;
+ return 1;
+ }
+ }
+}
+
+sub rotate_authkey {
+ return if $authkey_lifetime == 0;
+
+ PVE::Cluster::cfs_lock_authkey(undef, sub {
+ # re-check with lock to avoid double rotation in clusters
+ return if check_authkey();
+
+ my $old = get_pubkey();
+ my $new = Crypt::OpenSSL::RSA->generate_key(2048);
+
+ if ($old) {
+ eval {
+ my $pem = $old->get_public_key_x509_string();
+ # mtime is used for caching and ticket age range calculation
+ PVE::Tools::file_set_contents($pve_auth_key_files->{pubold}, $pem);
+ };
+ die "Failed to store old auth key: $@\n" if $@;
+ }
+
+ eval {
+ my $pem = $new->get_public_key_x509_string();
+ # mtime is used for caching and ticket age range calculation,
+ # should be close to that of pubold above
+ PVE::Tools::file_set_contents($pve_auth_key_files->{pub}, $pem);
+ };
+ if ($@) {
+ if ($old) {
+ warn "Failed to store new auth key - $@\n";
+ warn "Reverting to previous auth key\n";
+ eval {
+ my $pem = $old->get_public_key_x509_string();
+ PVE::Tools::file_set_contents($pve_auth_key_files->{pub}, $pem);
+ };
+ die "Failed to restore old auth key: $@\n" if $@;
+ } else {
+ die "Failed to store new auth key - $@\n";
+ }
+ }
+
+ eval {
+ my $pem = $new->get_private_key_string();
+ PVE::Tools::file_set_contents($pve_auth_key_files->{priv}, $pem);
+ };
+ if ($@) {
+ warn "Failed to store new auth key - $@\n";
+ warn "Deleting auth key to force regeneration\n";
+ unlink $pve_auth_key_files->{pub};
+ unlink $pve_auth_key_files->{priv};
+ }
+ });
+ die $@ if $@;
+}
+
+PVE::JSONSchema::register_standard_option('tokenid', {
+ description => "API token identifier.",
+ type => "string",
+ format => "pve-tokenid",
+});
+
+our $token_subid_regex = $PVE::Auth::Plugin::realm_regex;
+
+# username@realm username realm tokenid
+our $token_full_regex = qr/((${PVE::Auth::Plugin::user_regex})\@(${PVE::Auth::Plugin::realm_regex}))!(${token_subid_regex})/;
+
+our $userid_or_token_regex = qr/^$PVE::Auth::Plugin::user_regex\@$PVE::Auth::Plugin::realm_regex(?:!$token_subid_regex)?$/;
+
+sub split_tokenid {
+ my ($tokenid, $noerr) = @_;
+
+ if ($tokenid =~ /^${token_full_regex}$/) {
+ return ($1, $4);
+ }
+
+ die "'$tokenid' is not a valid token ID - not able to split into user and token parts\n" if !$noerr;
+
+ return undef;
+}
+
+sub join_tokenid {
+ my ($username, $tokensubid) = @_;
+
+ my $joined = "${username}!${tokensubid}";
+
+ return pve_verify_tokenid($joined);
+}
+
+PVE::JSONSchema::register_format('pve-tokenid', \&pve_verify_tokenid);
+sub pve_verify_tokenid {
+ my ($tokenid, $noerr) = @_;
+
+ if ($tokenid =~ /^${token_full_regex}$/) {
+ return wantarray ? ($tokenid, $2, $3, $4) : $tokenid;
+ }
+
+ die "value '$tokenid' does not look like a valid token ID\n" if !$noerr;
+
+ return undef;
+}
+
+
+my $csrf_prevention_secret;
+my $csrf_prevention_secret_legacy;
+my $get_csrfr_secret = sub {
+ if (!$csrf_prevention_secret) {
+ my $input = PVE::Tools::file_get_contents($pve_www_key_fn);
+ $csrf_prevention_secret = Digest::SHA::hmac_sha256_base64($input);
+ $csrf_prevention_secret_legacy = Digest::SHA::sha1_base64($input);
+ }
+ return $csrf_prevention_secret;
+};
+
+sub assemble_csrf_prevention_token {
+ my ($username) = @_;
+
+ my $secret = &$get_csrfr_secret();
+
+ return PVE::Ticket::assemble_csrf_prevention_token ($secret, $username);
+}
+
+sub verify_csrf_prevention_token {
+ my ($username, $token, $noerr) = @_;
+
+ my $secret = $get_csrfr_secret->();
+
+ # FIXME: remove with PVE 7 and/or refactor all into PVE::Ticket ?
+ if ($token =~ m/^([A-Z0-9]{8}):(\S+)$/) {
+ my $sig = $2;
+ if (length($sig) == 27) {
+ # the legacy secret got populated by above get_csrfr_secret call
+ $secret = $csrf_prevention_secret_legacy;
+ }
+ }
+
+ return PVE::Ticket::verify_csrf_prevention_token(
+ $secret, $username, $token, -$auth_graceperiod, $ticket_lifetime, $noerr);
+}
+
+my $get_ticket_age_range = sub {
+ my ($now, $mtime, $rotated) = @_;
+
+ my $key_age = $now - $mtime;
+ $key_age = 0 if $key_age < 0;
+
+ my $min = -$auth_graceperiod;
+ my $max = $ticket_lifetime;
+
+ if ($rotated) {
+ # ticket creation after rotation is not allowed
+ $min = $key_age - $auth_graceperiod;
+ } else {
+ if ($key_age > $authkey_lifetime && $authkey_lifetime > 0) {
+ if (PVE::Cluster::check_cfs_quorum(1)) {
+ # key should have been rotated, clamp range accordingly
+ $min = $key_age - $authkey_lifetime;
+ } else {
+ warn "Cluster not quorate - extending auth key lifetime!\n";
+ }
+ }
+
+ $max = $key_age + $auth_graceperiod if $key_age < $ticket_lifetime;
+ }
+
+ return undef if $min > $ticket_lifetime;
+ return ($min, $max);
+};
+
+sub assemble_ticket {
+ my ($data) = @_;
+
+ my $rsa_priv = get_privkey();
+
+ return PVE::Ticket::assemble_rsa_ticket($rsa_priv, 'PVE', $data);
+}
+
+sub verify_ticket {
+ my ($ticket, $noerr) = @_;
+
+ my $now = time();
+
+ my $check = sub {
+ my ($old) = @_;
+
+ my ($rsa_pub, $rsa_mtime) = get_pubkey($old);
+ return undef if !$rsa_pub;
+
+ my ($min, $max) = $get_ticket_age_range->($now, $rsa_mtime, $old);
+ return undef if !defined($min);
+
+ return PVE::Ticket::verify_rsa_ticket(
+ $rsa_pub, 'PVE', $ticket, undef, $min, $max, 1);
+ };
+
+ my ($data, $age) = $check->();
+
+ # check with old, rotated key if current key failed
+ ($data, $age) = $check->(1) if !defined($data);
+
+ my $auth_failure = sub {
+ if ($noerr) {
+ return undef;
+ } else {
+ # raise error via undef ticket
+ PVE::Ticket::verify_rsa_ticket(undef, 'PVE');
+ }
+ };
+
+ if (!defined($data)) {
+ return $auth_failure->();
+ }
+
+ my ($username, $tfa_info);
+ if ($data =~ m{^u2f!([^!]+)!([0-9a-zA-Z/.=_\-+]+)$}) {
+ # Ticket for u2f-users:
+ ($username, my $challenge) = ($1, $2);
+ if ($challenge eq 'verified') {
+ # u2f challenge was completed
+ $challenge = undef;
+ } elsif (!wantarray) {
+ # The caller is not aware there could be an ongoing challenge,
+ # so we treat this ticket as invalid:
+ return $auth_failure->();
+ }
+ $tfa_info = {
+ type => 'u2f',
+ challenge => $challenge,
+ };
+ } elsif ($data =~ /^tfa!(.*)$/) {
+ # TOTP and Yubico don't require a challenge so this is the generic
+ # 'missing 2nd factor ticket'
+ $username = $1;
+ $tfa_info = { type => 'tfa' };
+ } else {
+ # Regular ticket (full access)
+ $username = $data;
+ }
+
+ return undef if !PVE::Auth::Plugin::verify_username($username, $noerr);
+
+ return wantarray ? ($username, $age, $tfa_info) : $username;
+}
+
+sub verify_token {
+ my ($api_token) = @_;
+
+ die "no API token specified\n" if !$api_token;
+
+ my ($tokenid, $value);
+ if ($api_token =~ /^(.*)=(.*)$/) {
+ $tokenid = $1;
+ $value = $2;
+ } else {
+ die "no tokenid specified\n";
+ }
+
+ my ($username, $token) = split_tokenid($tokenid);
+
+ my $usercfg = cfs_read_file('user.cfg');
+ check_user_enabled($usercfg, $username);
+ check_token_exist($usercfg, $username, $token);
+
+ my $ctime = time();
+
+ my $user = $usercfg->{users}->{$username};
+ die "account expired\n" if $user->{expire} && ($user->{expire} < $ctime);
+
+ my $token_info = $user->{tokens}->{$token};
+ die "token expired\n" if $token_info->{expire} && ($token_info->{expire} < $ctime);
+
+ die "invalid token value!\n" if !PVE::Cluster::verify_token($tokenid, $value);
+
+ return wantarray ? ($tokenid) : $tokenid;
+}
+
+
+# VNC tickets
+# - they do not contain the username in plain text
+# - they are restricted to a specific resource path (example: '/vms/100')
+sub assemble_vnc_ticket {
+ my ($username, $path) = @_;
+
+ my $rsa_priv = get_privkey();
+
+ $path = normalize_path($path);
+
+ my $secret_data = "$username:$path";
+
+ return PVE::Ticket::assemble_rsa_ticket(
+ $rsa_priv, 'PVEVNC', undef, $secret_data);
+}
+
+sub verify_vnc_ticket {
+ my ($ticket, $username, $path, $noerr) = @_;
+
+ my $secret_data = "$username:$path";
+
+ my ($rsa_pub, $rsa_mtime) = get_pubkey();
+ if (!$rsa_pub || (time() - $rsa_mtime > $authkey_lifetime && $authkey_lifetime > 0)) {
+ if ($noerr) {
+ return undef;
+ } else {
+ # raise error via undef ticket
+ PVE::Ticket::verify_rsa_ticket($rsa_pub, 'PVEVNC');
+ }
+ }
+
+ return PVE::Ticket::verify_rsa_ticket(
+ $rsa_pub, 'PVEVNC', $ticket, $secret_data, -20, 40, $noerr);
+}
+
+sub assemble_spice_ticket {
+ my ($username, $vmid, $node) = @_;
+
+ my $secret = &$get_csrfr_secret();
+
+ return PVE::Ticket::assemble_spice_ticket(
+ $secret, $username, $vmid, $node);
+}
+
+sub verify_spice_connect_url {
+ my ($connect_str) = @_;
+
+ my $secret = &$get_csrfr_secret();
+
+ return PVE::Ticket::verify_spice_connect_url($secret, $connect_str);
+}
+
+sub read_x509_subject_spice {
+ my ($filename) = @_;
+
+ # read x509 subject
+ my $bio = Net::SSLeay::BIO_new_file($filename, 'r');
+ die "Could not open $filename using OpenSSL\n"
+ if !$bio;
+
+ my $x509 = Net::SSLeay::PEM_read_bio_X509($bio);
+ Net::SSLeay::BIO_free($bio);
+
+ die "Could not parse X509 certificate in $filename\n"
+ if !$x509;
+
+ my $nameobj = Net::SSLeay::X509_get_subject_name($x509);
+ my $subject = Net::SSLeay::X509_NAME_oneline($nameobj);
+ Net::SSLeay::X509_free($x509);
+
+ # remote-viewer wants comma as seperator (not '/')
+ $subject =~ s!^/!!;
+ $subject =~ s!/(\w+=)!,$1!g;
+
+ return $subject;
+}
+
+# helper to generate SPICE remote-viewer configuration
+sub remote_viewer_config {
+ my ($authuser, $vmid, $node, $proxy, $title, $port) = @_;
+
+ if (!$proxy) {
+ my $host = `hostname -f` || PVE::INotify::nodename();
+ chomp $host;
+ $proxy = $host;
+ }
+
+ my ($ticket, $proxyticket) = assemble_spice_ticket($authuser, $vmid, $node);
+
+ my $filename = "/etc/pve/local/pve-ssl.pem";
+ my $subject = read_x509_subject_spice($filename);
+
+ my $cacert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192);
+ $cacert =~ s/\n/\\n/g;
+
+ $proxy = "[$proxy]" if Net::IP::ip_is_ipv6($proxy);
+ my $config = {
+ 'secure-attention' => "Ctrl+Alt+Ins",
+ 'toggle-fullscreen' => "Shift+F11",
+ 'release-cursor' => "Ctrl+Alt+R",
+ type => 'spice',
+ title => $title,
+ host => $proxyticket, # this breaks tls hostname verification, so we need to use 'host-subject'
+ proxy => "http://$proxy:3128",
+ 'tls-port' => $port,
+ 'host-subject' => $subject,
+ ca => $cacert,
+ password => $ticket,
+ 'delete-this-file' => 1,
+ };
+
+ return ($ticket, $proxyticket, $config);
+}
+
+sub check_user_exist {
+ my ($usercfg, $username, $noerr) = @_;
+
+ $username = PVE::Auth::Plugin::verify_username($username, $noerr);
+ return undef if !$username;
+
+ return $usercfg->{users}->{$username} if $usercfg && $usercfg->{users}->{$username};
+
+ die "no such user ('$username')\n" if !$noerr;
+
+ return undef;
+}
+
+sub check_user_enabled {
+ my ($usercfg, $username, $noerr) = @_;
+
+ my $data = check_user_exist($usercfg, $username, $noerr);
+ return undef if !$data;
+
+ return 1 if $data->{enable};
+
+ die "user '$username' is disabled\n" if !$noerr;
+
+ return undef;
+}
+
+sub check_token_exist {
+ my ($usercfg, $username, $tokenid, $noerr) = @_;
+
+ my $user = check_user_exist($usercfg, $username, $noerr);
+ return undef if !$user;
+
+ return $user->{tokens}->{$tokenid}
+ if defined($user->{tokens}) && $user->{tokens}->{$tokenid};
+
+ die "no such token '$tokenid' for user '$username'\n" if !$noerr;
+
+ return undef;
+}
+
+sub verify_one_time_pw {
+ my ($type, $username, $keys, $tfa_cfg, $otp) = @_;
+
+ die "missing one time password for two-factor authentication '$type'\n" if !$otp;
+
+ # fixme: proxy support?
+ my $proxy;
+
+ if ($type eq 'yubico') {
+ PVE::OTP::yubico_verify_otp($otp, $keys, $tfa_cfg->{url},
+ $tfa_cfg->{id}, $tfa_cfg->{key}, $proxy);
+ } elsif ($type eq 'oath') {
+ PVE::OTP::oath_verify_otp($otp, $keys, $tfa_cfg->{step}, $tfa_cfg->{digits});
+ } else {
+ die "unknown tfa type '$type'\n";
+ }
+}
+
+# password should be utf8 encoded
+# Note: some plugins delay/sleep if auth fails
+sub authenticate_user {
+ my ($username, $password, $otp) = @_;
+
+ die "no username specified\n" if !$username;
+
+ my ($ruid, $realm);
+
+ ($username, $ruid, $realm) = PVE::Auth::Plugin::verify_username($username);
+
+ my $usercfg = cfs_read_file('user.cfg');
+
+ check_user_enabled($usercfg, $username);
+
+ my $ctime = time();
+ my $expire = $usercfg->{users}->{$username}->{expire};
+
+ die "account expired\n" if $expire && ($expire < $ctime);
+
+ my $domain_cfg = cfs_read_file('domains.cfg');
+
+ my $cfg = $domain_cfg->{ids}->{$realm};
+ die "auth domain '$realm' does not exist\n" if !$cfg;
+ my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
+ $plugin->authenticate_user($cfg, $realm, $ruid, $password);
+
+ my ($type, $tfa_data) = user_get_tfa($username, $realm);
+ if ($type) {
+ if ($type eq 'u2f') {
+ # Note that if the user did not manage to complete the initial u2f registration
+ # challenge we have a hash containing a 'challenge' entry in the user's tfa.cfg entry:
+ $tfa_data = undef if exists $tfa_data->{challenge};
+ } elsif (!defined($otp)) {
+ # The user requires a 2nd factor but has not provided one. Return success but
+ # don't clear $tfa_data.
+ } else {
+ my $keys = $tfa_data->{keys};
+ my $tfa_cfg = $tfa_data->{config};
+ verify_one_time_pw($type, $username, $keys, $tfa_cfg, $otp);
+ $tfa_data = undef;
+ }
+
+ # Return the type along with the rest:
+ if ($tfa_data) {
+ $tfa_data = {
+ type => $type,
+ data => $tfa_data,
+ };
+ }
+ }
+
+ return wantarray ? ($username, $tfa_data) : $username;
+}
+
+sub domain_set_password {
+ my ($realm, $username, $password) = @_;
+
+ die "no auth domain specified" if !$realm;
+
+ my $domain_cfg = cfs_read_file('domains.cfg');
+
+ my $cfg = $domain_cfg->{ids}->{$realm};
+ die "auth domain '$realm' does not exist\n" if !$cfg;
+ my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
+ $plugin->store_password($cfg, $realm, $username, $password);
+}
+
+sub add_user_group {
+ my ($username, $usercfg, $group) = @_;
+
+ $usercfg->{users}->{$username}->{groups}->{$group} = 1;
+ $usercfg->{groups}->{$group}->{users}->{$username} = 1;
+}
+
+sub delete_user_group {
+ my ($username, $usercfg) = @_;
+
+ foreach my $group (keys %{$usercfg->{groups}}) {
+
+ delete ($usercfg->{groups}->{$group}->{users}->{$username})
+ if $usercfg->{groups}->{$group}->{users}->{$username};
+ }
+}
+
+sub delete_user_acl {
+ my ($username, $usercfg) = @_;
+
+ foreach my $acl (keys %{$usercfg->{acl}}) {
+
+ delete ($usercfg->{acl}->{$acl}->{users}->{$username})
+ if $usercfg->{acl}->{$acl}->{users}->{$username};
+ }
+}
+
+sub delete_group_acl {
+ my ($group, $usercfg) = @_;
+
+ foreach my $acl (keys %{$usercfg->{acl}}) {
+
+ delete ($usercfg->{acl}->{$acl}->{groups}->{$group})
+ if $usercfg->{acl}->{$acl}->{groups}->{$group};
+ }
+}
+
+sub delete_pool_acl {
+ my ($pool, $usercfg) = @_;
+
+ my $path = "/pool/$pool";
+
+ delete ($usercfg->{acl}->{$path})
+}
+
+# we automatically create some predefined roles by splitting privs
+# into 3 groups (per category)
+# root: only root is allowed to do that
+# admin: an administrator can to that
+# user: a normal user/customer can to that
+my $privgroups = {
+ VM => {
+ root => [],
+ admin => [
+ 'VM.Config.Disk',
+ 'VM.Config.CPU',
+ 'VM.Config.Memory',
+ 'VM.Config.Network',
+ 'VM.Config.HWType',
+ 'VM.Config.Options', # covers all other things
+ 'VM.Allocate',
+ 'VM.Clone',
+ 'VM.Migrate',
+ 'VM.Monitor',
+ 'VM.Snapshot',
+ 'VM.Snapshot.Rollback',
+ ],
+ user => [
+ 'VM.Config.CDROM', # change CDROM media
+ 'VM.Config.Cloudinit',
+ 'VM.Console',
+ 'VM.Backup',
+ 'VM.PowerMgmt',
+ ],
+ audit => [
+ 'VM.Audit',
+ ],
+ },
+ Sys => {
+ root => [
+ 'Sys.PowerMgmt',
+ 'Sys.Modify', # edit/change node settings
+ ],
+ admin => [
+ 'Permissions.Modify',
+ 'Sys.Console',
+ 'Sys.Syslog',
+ ],
+ user => [],
+ audit => [
+ 'Sys.Audit',
+ ],
+ },
+ Datastore => {
+ root => [],
+ admin => [
+ 'Datastore.Allocate',
+ 'Datastore.AllocateTemplate',
+ ],
+ user => [
+ 'Datastore.AllocateSpace',
+ ],
+ audit => [
+ 'Datastore.Audit',
+ ],
+ },
+ SDN => {
+ root => [],
+ admin => [
+ 'SDN.Allocate',
+ 'SDN.Audit',
+ ],
+ audit => [
+ 'SDN.Audit',
+ ],
+ },
+ User => {
+ root => [
+ 'Realm.Allocate',
+ ],
+ admin => [
+ 'User.Modify',
+ 'Group.Allocate', # edit/change group settings
+ 'Realm.AllocateUser',
+ ],
+ user => [],
+ audit => [],
+ },
+ Pool => {
+ root => [],
+ admin => [
+ 'Pool.Allocate', # create/delete pools
+ ],
+ user => [],
+ audit => [],
+ },
+};
+
+my $valid_privs = {};
+
+my $special_roles = {
+ 'NoAccess' => {}, # no privileges
+ 'Administrator' => $valid_privs, # all privileges
+};
+
+sub create_roles {
+
+ foreach my $cat (keys %$privgroups) {
+ my $cd = $privgroups->{$cat};
+ foreach my $p (@{$cd->{root}}, @{$cd->{admin}},
+ @{$cd->{user}}, @{$cd->{audit}}) {
+ $valid_privs->{$p} = 1;
+ }
+ foreach my $p (@{$cd->{admin}}, @{$cd->{user}}, @{$cd->{audit}}) {
+
+ $special_roles->{"PVE${cat}Admin"}->{$p} = 1;
+ $special_roles->{"PVEAdmin"}->{$p} = 1;
+ }
+ if (scalar(@{$cd->{user}})) {
+ foreach my $p (@{$cd->{user}}, @{$cd->{audit}}) {
+ $special_roles->{"PVE${cat}User"}->{$p} = 1;
+ }
+ }
+ foreach my $p (@{$cd->{audit}}) {
+ $special_roles->{"PVEAuditor"}->{$p} = 1;
+ }
+ }
+
+ $special_roles->{"PVETemplateUser"} = { 'VM.Clone' => 1, 'VM.Audit' => 1 };
+};
+
+create_roles();
+
+sub create_priv_properties {
+ my $properties = {};
+ foreach my $priv (keys %$valid_privs) {
+ $properties->{$priv} = {
+ type => 'boolean',
+ optional => 1,
+ };
+ }
+ return $properties;
+}
+
+sub role_is_special {
+ my ($role) = @_;
+ return (exists $special_roles->{$role}) ? 1 : 0;
+}
+
+sub add_role_privs {
+ my ($role, $usercfg, $privs) = @_;
+
+ return if !$privs;
+
+ die "role '$role' does not exist\n" if !$usercfg->{roles}->{$role};
+
+ foreach my $priv (split_list($privs)) {
+ if (defined ($valid_privs->{$priv})) {
+ $usercfg->{roles}->{$role}->{$priv} = 1;
+ } else {
+ die "invalid privilege '$priv'\n";
+ }
+ }
+}
+
+sub lookup_username {
+ my ($username, $noerr) = @_;
+
+ $username =~ m!^(${PVE::Auth::Plugin::user_regex})\@(${PVE::Auth::Plugin::realm_regex})$!;
+
+ my $realm = $2;
+ my $domain_cfg = cfs_read_file("domains.cfg");
+ my $casesensitive = $domain_cfg->{ids}->{$realm}->{'case-sensitive'} // 1;
+ my $usercfg = cfs_read_file('user.cfg');
+
+ if (!$casesensitive) {
+ my @matches = grep { lc $username eq lc $_ } (keys %{$usercfg->{users}});
+
+ die "ambiguous case insensitive match of username '$username', cannot safely grant access!\n"
+ if scalar @matches > 1 && !$noerr;
+
+ return $matches[0]
+ }
+
+ return $username;
+}
+
+sub normalize_path {
+ my $path = shift;
+
+ $path =~ s|/+|/|g;
+
+ $path =~ s|/$||;
+
+ $path = '/' if !$path;
+
+ $path = "/$path" if $path !~ m|^/|;
+
+ return undef if $path !~ m|^[[:alnum:]\.\-\_\/]+$|;
+
+ return $path;
+}
+
+sub check_path {
+ my ($path) = @_;
+ return $path =~ m!^(
+ /
+ |/access
+ |/access/groups
+ |/access/realm
+ |/nodes
+ |/nodes/[[:alnum:]\.\-\_]+
+ |/pool
+ |/pool/[[:alnum:]\.\-\_]+
+ |/sdn
+ |/storage
+ |/storage/[[:alnum:]\.\-\_]+
+ |/vms
+ |/vms/[1-9][0-9]{2,}
+ )$!xs;
+}
+
+PVE::JSONSchema::register_format('pve-groupid', \&verify_groupname);
+sub verify_groupname {
+ my ($groupname, $noerr) = @_;
+
+ if ($groupname !~ m/^[A-Za-z0-9\.\-_]+$/) {
+
+ die "group name '$groupname' contains invalid characters\n" if !$noerr;
+
+ return undef;
+ }
+
+ return $groupname;
+}
+
+PVE::JSONSchema::register_format('pve-roleid', \&verify_rolename);
+sub verify_rolename {
+ my ($rolename, $noerr) = @_;
+
+ if ($rolename !~ m/^[A-Za-z0-9\.\-_]+$/) {
+
+ die "role name '$rolename' contains invalid characters\n" if !$noerr;
+
+ return undef;
+ }
+
+ return $rolename;
+}
+
+PVE::JSONSchema::register_format('pve-poolid', \&verify_poolname);
+sub verify_poolname {
+ my ($poolname, $noerr) = @_;
+
+ if ($poolname !~ m/^[A-Za-z0-9\.\-_]+$/) {
+
+ die "pool name '$poolname' contains invalid characters\n" if !$noerr;
+
+ return undef;
+ }
+
+ return $poolname;
+}
+
+PVE::JSONSchema::register_format('pve-priv', \&verify_privname);
+sub verify_privname {
+ my ($priv, $noerr) = @_;
+
+ if (!$valid_privs->{$priv}) {
+ die "invalid privilege '$priv'\n" if !$noerr;
+
+ return undef;
+ }
+
+ return $priv;
+}
+
+sub userconfig_force_defaults {
+ my ($cfg) = @_;
+
+ foreach my $r (keys %$special_roles) {
+ $cfg->{roles}->{$r} = $special_roles->{$r};
+ }
+
+ # add root user if not exists
+ if (!$cfg->{users}->{'root@pam'}) {
+ $cfg->{users}->{'root@pam'}->{enable} = 1;
+ }
+}
+
+sub parse_user_config {
+ my ($filename, $raw) = @_;
+
+ my $cfg = {};
+
+ userconfig_force_defaults($cfg);
+
+ $raw = '' if !defined($raw);
+ while ($raw =~ /^\s*(.+?)\s*$/gm) {
+ my $line = $1;
+ my @data;
+
+ foreach my $d (split (/:/, $line)) {
+ $d =~ s/^\s+//;
+ $d =~ s/\s+$//;
+ push @data, $d
+ }
+
+ my $et = shift @data;
+
+ if ($et eq 'user') {
+ my ($user, $enable, $expire, $firstname, $lastname, $email, $comment, $keys) = @data;
+
+ my (undef, undef, $realm) = PVE::Auth::Plugin::verify_username($user, 1);
+ if (!$realm) {
+ warn "user config - ignore user '$user' - invalid user name\n";
+ next;
+ }
+
+ $enable = $enable ? 1 : 0;
+
+ $expire = 0 if !$expire;
+
+ if ($expire !~ m/^\d+$/) {
+ warn "user config - ignore user '$user' - (illegal characters in expire '$expire')\n";
+ next;
+ }
+ $expire = int($expire);
+
+ #if (!verify_groupname ($group, 1)) {
+ # warn "user config - ignore user '$user' - invalid characters in group name\n";
+ # next;
+ #}
+
+ $cfg->{users}->{$user} = {
+ enable => $enable,
+ # group => $group,
+ };
+ $cfg->{users}->{$user}->{firstname} = PVE::Tools::decode_text($firstname) if $firstname;
+ $cfg->{users}->{$user}->{lastname} = PVE::Tools::decode_text($lastname) if $lastname;
+ $cfg->{users}->{$user}->{email} = $email;
+ $cfg->{users}->{$user}->{comment} = PVE::Tools::decode_text($comment) if $comment;
+ $cfg->{users}->{$user}->{expire} = $expire;
+ # keys: allowed yubico key ids or oath secrets (base32 encoded)
+ $cfg->{users}->{$user}->{keys} = $keys if $keys;
+
+ #$cfg->{users}->{$user}->{groups}->{$group} = 1;
+ #$cfg->{groups}->{$group}->{$user} = 1;
+
+ } elsif ($et eq 'group') {
+ my ($group, $userlist, $comment) = @data;
+
+ if (!verify_groupname($group, 1)) {
+ warn "user config - ignore group '$group' - invalid characters in group name\n";
+ next;
+ }
+
+ # make sure to add the group (even if there are no members)
+ $cfg->{groups}->{$group} = { users => {} } if !$cfg->{groups}->{$group};
+
+ $cfg->{groups}->{$group}->{comment} = PVE::Tools::decode_text($comment) if $comment;
+
+ foreach my $user (split_list($userlist)) {
+
+ if (!PVE::Auth::Plugin::verify_username($user, 1)) {
+ warn "user config - ignore invalid group member '$user'\n";
+ next;
+ }
+
+ if ($cfg->{users}->{$user}) { # user exists
+ $cfg->{users}->{$user}->{groups}->{$group} = 1;
+ } else {
+ warn "user config - ignore invalid group member '$user'\n";
+ }
+ $cfg->{groups}->{$group}->{users}->{$user} = 1;
+ }
+
+ } elsif ($et eq 'role') {
+ my ($role, $privlist) = @data;
+
+ if (!verify_rolename($role, 1)) {
+ warn "user config - ignore role '$role' - invalid characters in role name\n";
+ next;
+ }
+
+ # make sure to add the role (even if there are no privileges)
+ $cfg->{roles}->{$role} = {} if !$cfg->{roles}->{$role};
+
+ foreach my $priv (split_list($privlist)) {
+ if (defined ($valid_privs->{$priv})) {
+ $cfg->{roles}->{$role}->{$priv} = 1;
+ } else {
+ warn "user config - ignore invalid privilege '$priv'\n";
+ }
+ }
+
+ } elsif ($et eq 'acl') {
+ my ($propagate, $pathtxt, $uglist, $rolelist) = @data;
+
+ $propagate = $propagate ? 1 : 0;
+
+ if (my $path = normalize_path($pathtxt)) {
+ foreach my $role (split_list($rolelist)) {
+
+ if (!verify_rolename($role, 1)) {
+ warn "user config - ignore invalid role name '$role' in acl\n";
+ next;
+ }
+
+ if (!$cfg->{roles}->{$role}) {
+ warn "user config - ignore invalid acl role '$role'\n";
+ next;
+ }
+
+ foreach my $ug (split_list($uglist)) {
+ my ($group) = $ug =~ m/^@(\S+)$/;
+
+ if ($group && verify_groupname($group, 1)) {
+ if (!$cfg->{groups}->{$group}) { # group does not exist
+ warn "user config - ignore invalid acl group '$group'\n";
+ }
+ $cfg->{acl}->{$path}->{groups}->{$group}->{$role} = $propagate;
+ } elsif (PVE::Auth::Plugin::verify_username($ug, 1)) {
+ if (!$cfg->{users}->{$ug}) { # user does not exist
+ warn "user config - ignore invalid acl member '$ug'\n";
+ }
+ $cfg->{acl}->{$path}->{users}->{$ug}->{$role} = $propagate;
+ } elsif (my ($user, $token) = split_tokenid($ug, 1)) {
+ if (check_token_exist($cfg, $user, $token, 1)) {
+ $cfg->{acl}->{$path}->{tokens}->{$ug}->{$role} = $propagate;
+ } else {
+ warn "user config - ignore invalid acl token '$ug'\n";
+ }
+ } else {
+ warn "user config - invalid user/group '$ug' in acl\n";
+ }
+ }
+ }
+ } else {
+ warn "user config - ignore invalid path in acl '$pathtxt'\n";
+ }
+ } elsif ($et eq 'pool') {
+ my ($pool, $comment, $vmlist, $storelist) = @data;
+
+ if (!verify_poolname($pool, 1)) {
+ warn "user config - ignore pool '$pool' - invalid characters in pool name\n";
+ next;
+ }
+
+ # make sure to add the pool (even if there are no members)
+ $cfg->{pools}->{$pool} = { vms => {}, storage => {} } if !$cfg->{pools}->{$pool};
+
+ $cfg->{pools}->{$pool}->{comment} = PVE::Tools::decode_text($comment) if $comment;
+
+ foreach my $vmid (split_list($vmlist)) {
+ if ($vmid !~ m/^\d+$/) {
+ warn "user config - ignore invalid vmid '$vmid' in pool '$pool'\n";
+ next;
+ }
+ $vmid = int($vmid);
+
+ if ($cfg->{vms}->{$vmid}) {
+ warn "user config - ignore duplicate vmid '$vmid' in pool '$pool'\n";
+ next;
+ }
+
+ $cfg->{pools}->{$pool}->{vms}->{$vmid} = 1;
+
+ # record vmid ==> pool relation
+ $cfg->{vms}->{$vmid} = $pool;
+ }
+
+ foreach my $storeid (split_list($storelist)) {
+ if ($storeid !~ m/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i) {
+ warn "user config - ignore invalid storage '$storeid' in pool '$pool'\n";
+ next;
+ }
+ $cfg->{pools}->{$pool}->{storage}->{$storeid} = 1;
+ }
+ } elsif ($et eq 'token') {
+ my ($tokenid, $expire, $privsep, $comment) = @data;
+
+ my ($user, $token) = split_tokenid($tokenid, 1);
+ if (!($user && $token)) {
+ warn "user config - ignore invalid tokenid '$tokenid'\n";
+ next;
+ }
+
+ $privsep = $privsep ? 1 : 0;
+
+ $expire = 0 if !$expire;
+
+ if ($expire !~ m/^\d+$/) {
+ warn "user config - ignore token '$tokenid' - (illegal characters in expire '$expire')\n";
+ next;
+ }
+ $expire = int($expire);
+
+ if (my $user_cfg = $cfg->{users}->{$user}) { # user exists
+ $user_cfg->{tokens}->{$token} = {} if !$user_cfg->{tokens}->{$token};
+ my $token_cfg = $user_cfg->{tokens}->{$token};
+ $token_cfg->{privsep} = $privsep;
+ $token_cfg->{expire} = $expire;
+ $token_cfg->{comment} = PVE::Tools::decode_text($comment) if $comment;
+ } else {
+ warn "user config - ignore token '$tokenid' - user does not exist\n";
+ }
+ } else {
+ warn "user config - ignore config line: $line\n";
+ }
+ }
+
+ userconfig_force_defaults($cfg);
+
+ return $cfg;
+}
+
+sub write_user_config {
+ my ($filename, $cfg) = @_;
+
+ my $data = '';
+
+ foreach my $user (sort keys %{$cfg->{users}}) {
+ my $d = $cfg->{users}->{$user};
+ my $firstname = $d->{firstname} ? PVE::Tools::encode_text($d->{firstname}) : '';
+ my $lastname = $d->{lastname} ? PVE::Tools::encode_text($d->{lastname}) : '';
+ my $email = $d->{email} || '';
+ my $comment = $d->{comment} ? PVE::Tools::encode_text($d->{comment}) : '';
+ my $expire = int($d->{expire} || 0);
+ my $enable = $d->{enable} ? 1 : 0;
+ my $keys = $d->{keys} ? $d->{keys} : '';
+ $data .= "user:$user:$enable:$expire:$firstname:$lastname:$email:$comment:$keys:\n";
+
+ my $user_tokens = $d->{tokens};
+ foreach my $token (sort keys %$user_tokens) {
+ my $td = $user_tokens->{$token};
+ my $full_tokenid = join_tokenid($user, $token);
+ my $comment = $td->{comment} ? PVE::Tools::encode_text($td->{comment}) : '';
+ my $expire = int($td->{expire} || 0);
+ my $privsep = $td->{privsep} ? 1 : 0;
+ $data .= "token:$full_tokenid:$expire:$privsep:$comment:\n";
+ }
+ }
+
+ $data .= "\n";
+
+ foreach my $group (sort keys %{$cfg->{groups}}) {
+ my $d = $cfg->{groups}->{$group};
+ my $list = join (',', sort keys %{$d->{users}});
+ my $comment = $d->{comment} ? PVE::Tools::encode_text($d->{comment}) : '';
+ $data .= "group:$group:$list:$comment:\n";
+ }
+
+ $data .= "\n";
+
+ foreach my $pool (sort keys %{$cfg->{pools}}) {
+ my $d = $cfg->{pools}->{$pool};
+ my $vmlist = join (',', sort keys %{$d->{vms}});
+ my $storelist = join (',', sort keys %{$d->{storage}});
+ my $comment = $d->{comment} ? PVE::Tools::encode_text($d->{comment}) : '';
+ $data .= "pool:$pool:$comment:$vmlist:$storelist:\n";
+ }
+
+ $data .= "\n";
+
+ foreach my $role (sort keys %{$cfg->{roles}}) {
+ next if $special_roles->{$role};
+
+ my $d = $cfg->{roles}->{$role};
+ my $list = join (',', sort keys %$d);
+ $data .= "role:$role:$list:\n";
+ }
+
+ $data .= "\n";
+
+ my $collect_rolelist_members = sub {
+ my ($acl_members, $result, $prefix, $exclude) = @_;
+
+ foreach my $member (keys %$acl_members) {
+ next if $exclude && $member eq $exclude;
+
+ my $l0 = '';
+ my $l1 = '';
+ foreach my $role (sort keys %{$acl_members->{$member}}) {
+ my $propagate = $acl_members->{$member}->{$role};
+ if ($propagate) {
+ $l1 .= ',' if $l1;
+ $l1 .= $role;
+ } else {
+ $l0 .= ',' if $l0;
+ $l0 .= $role;
+ }
+ }
+ $result->{0}->{$l0}->{"${prefix}${member}"} = 1 if $l0;
+ $result->{1}->{$l1}->{"${prefix}${member}"} = 1 if $l1;
+ }
+ };
+
+ foreach my $path (sort keys %{$cfg->{acl}}) {
+ my $d = $cfg->{acl}->{$path};
+
+ my $rolelist_members = {};
+
+ $collect_rolelist_members->($d->{'groups'}, $rolelist_members, '@');
+
+ # no need to save 'root@pam', it is always 'Administrator'
+ $collect_rolelist_members->($d->{'users'}, $rolelist_members, '', 'root@pam');
+
+ $collect_rolelist_members->($d->{'tokens'}, $rolelist_members, '');
+
+ foreach my $propagate (0,1) {
+ my $filtered = $rolelist_members->{$propagate};
+ foreach my $rolelist (sort keys %$filtered) {
+ my $uglist = join (',', sort keys %{$filtered->{$rolelist}});
+ $data .= "acl:$propagate:$path:$uglist:$rolelist:\n";
+ }
+
+ }
+ }
+
+ return $data;
+}
+
+# The TFA configuration in priv/tfa.cfg format contains one line per user of
+# the form:
+# USER:TYPE:DATA
+# DATA is a base64 encoded json string and its format depends on the type.
+sub parse_priv_tfa_config {
+ my ($filename, $raw) = @_;
+
+ my $users = {};
+ my $cfg = { users => $users };
+
+ $raw = '' if !defined($raw);
+ while ($raw =~ /^\s*(.+?)\s*$/gm) {
+ my $line = $1;
+ my ($user, $type, $data) = split(/:/, $line, 3);
+
+ my (undef, undef, $realm) = PVE::Auth::Plugin::verify_username($user, 1);
+ if (!$realm) {
+ warn "user tfa config - ignore user '$user' - invalid user name\n";
+ next;
+ }
+
+ $data = decode_json(decode_base64($data));
+
+ $users->{$user} = {
+ type => $type,
+ data => $data,
+ };
+ }
+
+ return $cfg;
+}
+
+sub write_priv_tfa_config {
+ my ($filename, $cfg) = @_;
+
+ my $output = '';
+
+ my $users = $cfg->{users};
+ foreach my $user (sort keys %$users) {
+ my $info = $users->{$user};
+ next if !%$info; # skip empty entries
+
+ $info = {%$info}; # copy to verify contents:
+
+ my $type = delete $info->{type};
+ my $data = delete $info->{data};
+
+ if (my @keys = keys %$info) {
+ die "invalid keys in TFA config for user $user: " . join(', ', @keys) . "\n";
+ }
+
+ $data = encode_base64(encode_json($data), '');
+ $output .= "${user}:${type}:${data}\n";
+ }
+
+ return $output;
+}
+
+sub roles {
+ my ($cfg, $user, $path) = @_;
+
+ # NOTE: we do not consider pools here.
+ # NOTE: for privsep tokens, this does not filter roles by those that the
+ # corresponding user has.
+ # Use $rpcenv->permission() for any actual permission checks!
+
+ return 'Administrator' if $user eq 'root@pam'; # root can do anything
+
+ if (pve_verify_tokenid($user, 1)) {
+ my $tokenid = $user;
+ my ($username, $token) = split_tokenid($tokenid);
+
+ my $token_info = $cfg->{users}->{$username}->{tokens}->{$token};
+ return () if !$token_info;
+
+ my $user_roles = roles($cfg, $username, $path);
+
+ # return full user privileges
+ return $user_roles if !$token_info->{privsep};
+ }
+
+ my $roles = {};
+
+ foreach my $p (sort keys %{$cfg->{acl}}) {
+ my $final = ($path eq $p);
+
+ next if !(($p eq '/') || $final || ($path =~ m|^$p/|));
+
+ my $acl = $cfg->{acl}->{$p};
+
+ #print "CHECKACL $path $p\n";
+ #print "ACL $path = " . Dumper ($acl);
+ if (my $ri = $acl->{tokens}->{$user}) {
+ my $new;
+ foreach my $role (keys %$ri) {
+ my $propagate = $ri->{$role};
+ if ($final || $propagate) {
+ #print "APPLY ROLE $p $user $role\n";
+ $new = {} if !$new;
+ $new->{$role} = $propagate;
+ }
+ }
+ if ($new) {
+ $roles = $new; # overwrite previous settings
+ next;
+ }
+ }
+
+ if (my $ri = $acl->{users}->{$user}) {
+ my $new;
+ foreach my $role (keys %$ri) {
+ my $propagate = $ri->{$role};
+ if ($final || $propagate) {
+ #print "APPLY ROLE $p $user $role\n";
+ $new = {} if !$new;
+ $new->{$role} = $propagate;
+ }
+ }
+ if ($new) {
+ $roles = $new; # overwrite previous settings
+ next; # user privs always override group privs
+ }
+ }
+
+ my $new;
+ foreach my $g (keys %{$acl->{groups}}) {
+ next if !$cfg->{groups}->{$g}->{users}->{$user};
+ if (my $ri = $acl->{groups}->{$g}) {
+ foreach my $role (keys %$ri) {
+ my $propagate = $ri->{$role};
+ if ($final || $propagate) {
+ #print "APPLY ROLE $p \@$g $role\n";
+ $new = {} if !$new;
+ $new->{$role} = $propagate;
+ }
+ }
+ }
+ }
+ if ($new) {
+ $roles = $new; # overwrite previous settings
+ next;
+ }
+ }
+
+ return { 'NoAccess' => $roles->{NoAccess} } if defined ($roles->{NoAccess});
+ #return () if defined ($roles->{NoAccess});
+
+ #print "permission $user $path = " . Dumper ($roles);
+
+ #print "roles $user $path = " . join (',', @ra) . "\n";
+
+ return $roles;
+}
+
+sub remove_vm_access {
+ my ($vmid) = @_;
+ my $delVMaccessFn = sub {
+ my $usercfg = cfs_read_file("user.cfg");
+ my $modified;
+
+ if (my $acl = $usercfg->{acl}->{"/vms/$vmid"}) {
+ delete $usercfg->{acl}->{"/vms/$vmid"};
+ $modified = 1;
+ }
+ if (my $pool = $usercfg->{vms}->{$vmid}) {
+ if (my $data = $usercfg->{pools}->{$pool}) {
+ delete $data->{vms}->{$vmid};
+ delete $usercfg->{vms}->{$vmid};
+ $modified = 1;
+ }
+ }
+ cfs_write_file("user.cfg", $usercfg) if $modified;
+ };
+
+ lock_user_config($delVMaccessFn, "access permissions cleanup for VM $vmid failed");
+}
+
+sub remove_storage_access {
+ my ($storeid) = @_;
+
+ my $deleteStorageAccessFn = sub {
+ my $usercfg = cfs_read_file("user.cfg");
+ my $modified;
+
+ if (my $storage = $usercfg->{acl}->{"/storage/$storeid"}) {
+ delete $usercfg->{acl}->{"/storage/$storeid"};
+ $modified = 1;
+ }
+ foreach my $pool (keys %{$usercfg->{pools}}) {
+ delete $usercfg->{pools}->{$pool}->{storage}->{$storeid};
+ $modified = 1;
+ }
+ cfs_write_file("user.cfg", $usercfg) if $modified;
+ };
+
+ lock_user_config($deleteStorageAccessFn,
+ "access permissions cleanup for storage $storeid failed");
+}
+
+sub add_vm_to_pool {
+ my ($vmid, $pool) = @_;
+
+ my $addVMtoPoolFn = sub {
+ my $usercfg = cfs_read_file("user.cfg");
+ if (my $data = $usercfg->{pools}->{$pool}) {
+ $data->{vms}->{$vmid} = 1;
+ $usercfg->{vms}->{$vmid} = $pool;
+ cfs_write_file("user.cfg", $usercfg);
+ }
+ };
+
+ lock_user_config($addVMtoPoolFn, "can't add VM $vmid to pool '$pool'");
+}
+
+sub remove_vm_from_pool {
+ my ($vmid) = @_;
+
+ my $delVMfromPoolFn = sub {
+ my $usercfg = cfs_read_file("user.cfg");
+ if (my $pool = $usercfg->{vms}->{$vmid}) {
+ if (my $data = $usercfg->{pools}->{$pool}) {
+ delete $data->{vms}->{$vmid};
+ delete $usercfg->{vms}->{$vmid};
+ cfs_write_file("user.cfg", $usercfg);
+ }
+ }
+ };
+
+ lock_user_config($delVMfromPoolFn, "pool cleanup for VM $vmid failed");
+}
+
+my $USER_CONTROLLED_TFA_TYPES = {
+ u2f => 1,
+ oath => 1,
+};
+
+# Delete an entry by setting $data=undef in which case $type is ignored.
+# Otherwise both must be valid.
+sub user_set_tfa {
+ my ($userid, $realm, $type, $data, $cached_usercfg, $cached_domaincfg) = @_;
+
+ if (defined($data) && !defined($type)) {
+ # This is an internal usage error and should not happen
+ die "cannot set tfa data without a type\n";
+ }
+
+ my $user_cfg = $cached_usercfg || cfs_read_file('user.cfg');
+ my $user = $user_cfg->{users}->{$userid}
+ or die "user '$userid' not found\n";
+
+ my $domain_cfg = $cached_domaincfg || 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};
+ if (defined($realm_tfa)) {
+ $realm_tfa = PVE::Auth::Plugin::parse_tfa_config($realm_tfa);
+ # If the realm has a TFA setting, we're only allowed to use that.
+ if (defined($data)) {
+ my $required_type = $realm_tfa->{type};
+ if ($required_type ne $type) {
+ die "realm '$realm' only allows TFA of type '$required_type\n";
+ }
+
+ if (defined($data->{config})) {
+ # XXX: Is it enough if the type matches? Or should the configuration also match?
+ }
+
+ # realm-configured tfa always uses a simple key list, so use the user.cfg
+ $user->{keys} = $data->{keys};
+ } else {
+ die "realm '$realm' does not allow removing the 2nd factor\n";
+ }
+ } else {
+ # Without a realm-enforced TFA setting the user can add a u2f or totp entry by themselves.
+ # The 'yubico' type requires yubico server settings, which have to be configured on the
+ # realm, so this is not supported here:
+ die "domain '$realm' does not support TFA type '$type'\n"
+ if defined($data) && !$USER_CONTROLLED_TFA_TYPES->{$type};
+ }
+
+ # Custom TFA entries are stored in priv/tfa.cfg as they can be more complet: u2f uses a
+ # public key and a key handle, TOTP requires the usual totp settings...
+
+ my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
+ my $tfa = ($tfa_cfg->{users}->{$userid} //= {});
+
+ if (defined($data)) {
+ $tfa->{type} = $type;
+ $tfa->{data} = $data;
+ cfs_write_file('priv/tfa.cfg', $tfa_cfg);
+
+ $user->{keys} = "x!$type";
+ } else {
+ delete $tfa_cfg->{users}->{$userid};
+ cfs_write_file('priv/tfa.cfg', $tfa_cfg);
+
+ delete $user->{keys};
+ }
+
+ cfs_write_file('user.cfg', $user_cfg);
+}
+
+sub user_get_tfa {
+ my ($username, $realm) = @_;
+
+ my $user_cfg = cfs_read_file('user.cfg');
+ my $user = $user_cfg->{users}->{$username}
+ or die "user '$username' not found\n";
+
+ my $keys = $user->{keys};
+
+ 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;
+
+ if (!$keys) {
+ return if !$realm_tfa;
+ die "missing required 2nd keys\n";
+ }
+
+ # new style config starts with an 'x' and optionally contains a !<type> suffix
+ if ($keys !~ /^x(?:!.*)?$/) {
+ # old style config, find the type via the realm
+ return if !$realm_tfa;
+ return ($realm_tfa->{type}, {
+ keys => $keys,
+ config => $realm_tfa,
+ });
+ } else {
+ my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
+ my $tfa = $tfa_cfg->{users}->{$username};
+ return if !$tfa; # should not happen (user.cfg wasn't cleaned up?)
+
+ if ($realm_tfa) {
+ # if the realm has a tfa setting we need to verify the type:
+ die "auth domain '$realm' and user have mismatching TFA settings\n"
+ if $realm_tfa && $realm_tfa->{type} ne $tfa->{type};
+ }
+
+ return ($tfa->{type}, $tfa->{data});
+ }
+}
+
+# bash completion helpers
+
+register_standard_option('userid-completed',
+ get_standard_option('userid', { completion => \&complete_username}),
+);
+
+sub complete_username {
+
+ my $user_cfg = cfs_read_file('user.cfg');
+
+ return [ keys %{$user_cfg->{users}} ];
+}
+
+sub complete_group {
+
+ my $user_cfg = cfs_read_file('user.cfg');
+
+ return [ keys %{$user_cfg->{groups}} ];
+}
+
+sub complete_realm {
+
+ my $domain_cfg = cfs_read_file('domains.cfg');
+
+ return [ keys %{$domain_cfg->{ids}} ];
+}
+
+1;
--- /dev/null
+package PVE::Auth::AD;
+
+use strict;
+use warnings;
+use PVE::Auth::LDAP;
+use PVE::LDAP;
+
+use base qw(PVE::Auth::LDAP);
+
+sub type {
+ return 'ad';
+}
+
+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'),
+ };
+}
+
+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 },
+ };
+}
+
+sub get_users {
+ my ($class, $config, $realm) = @_;
+
+ $config->{user_attr} //= 'sAMAccountName';
+
+ return $class->SUPER::get_users($config, $realm);
+}
+
+sub authenticate_user {
+ my ($class, $config, $realm, $username, $password) = @_;
+
+ my $servers = [$config->{server1}];
+ push @$servers, $config->{server2} if $config->{server2};
+
+ my ($scheme, $port) = $class->get_scheme_and_port($config);
+
+ 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;
+ }
+ }
+ } elsif (defined($config->{verify})) {
+ $ad_args{verify} = 'none';
+ }
+
+ if ($scheme ne 'ldap') {
+ $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};
+
+ PVE::LDAP::auth_user_dn($ldap, $username, $password);
+
+ $ldap->unbind();
+ return 1;
+}
+
+1;
--- /dev/null
+package PVE::Auth::LDAP;
+
+use strict;
+use warnings;
+
+use PVE::Auth::Plugin;
+use PVE::JSONSchema;
+use PVE::LDAP;
+use PVE::Tools;
+
+use base qw(PVE::Auth::Plugin);
+
+sub type {
+ return 'ldap';
+}
+
+sub properties {
+ return {
+ base_dn => {
+ description => "LDAP base domain name",
+ type => 'string',
+ pattern => '\w+=[^,]+(,\s*\w+=[^,]+)*',
+ 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',
+ pattern => '\w+=[^,]+(,\s*\w+=[^,]+)*',
+ 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',
+ pattern => '\w+=[^,]+(,\s*\w+=[^,]+)*',
+ 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,
+ }
+ };
+}
+
+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 },
+ };
+}
+
+sub get_scheme_and_port {
+ my ($class, $config) = @_;
+
+ my $scheme = $config->{mode} // ($config->{secure} ? 'ldaps' : 'ldap');
+
+ my $default_port = $scheme eq 'ldaps' ? 636 : 389;
+ my $port = $config->{port} // $default_port;
+
+ return ($scheme, $port);
+}
+
+sub connect_and_bind {
+ my ($class, $config, $realm) = @_;
+
+ my $servers = [$config->{server1}];
+ push @$servers, $config->{server2} if $config->{server2};
+
+ my ($scheme, $port) = $class->get_scheme_and_port($config);
+
+ 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;
+ }
+ }
+ } else {
+ $ldap_args{verify} = 'none';
+ }
+
+ if ($scheme ne 'ldap') {
+ $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 = 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";
+ } else {
+ PVE::LDAP::ldap_bind($ldap);
+ }
+
+ if (!$config->{base_dn}) {
+ my $root = $ldap->root_dse(attrs => [ 'defaultNamingContext' ]);
+ $config->{base_dn} = $root->get_value('defaultNamingContext');
+ }
+
+ return $ldap;
+}
+
+# returns:
+# {
+# 'username@realm' => {
+# 'attr1' => 'value1',
+# 'attr2' => 'value2',
+# ...
+# },
+# ...
+# }
+#
+# or in list context:
+# (
+# {
+# 'username@realm' => {
+# 'attr1' => 'value1',
+# 'attr2' => 'value2',
+# ...
+# },
+# ...
+# },
+# {
+# 'uid=username,dc=....' => 'username@realm',
+# ...
+# }
+# )
+# the map of dn->username is needed for group membership sync
+sub get_users {
+ my ($class, $config, $realm) = @_;
+
+ my $ldap = $class->connect_and_bind($config, $realm);
+
+ 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',
+ };
+
+ foreach my $attr (PVE::Tools::split_list($config->{sync_attributes})) {
+ my ($ours, $ldap) = ($attr =~ m/^\s*(\w+)=(.*)\s*$/);
+ $ldap_attribute_map->{$ldap} = $ours;
+ }
+
+ my $filter = $config->{filter};
+ my $basedn = $config->{base_dn};
+
+ $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 $ret = {};
+ my $dnmap = {};
+
+ foreach my $user (@$users) {
+ 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;
+ }
+
+ $ret->{$username} = {};
+
+ foreach my $attr (keys %$user_attributes) {
+ if (my $ours = $ldap_attribute_map->{$attr}) {
+ $ret->{$username}->{$ours} = $user_attributes->{$attr}->[0];
+ }
+ }
+
+ if (wantarray) {
+ my $dn = $user->{dn};
+ $dnmap->{$dn} = $username;
+ }
+ }
+
+ return wantarray ? ($ret, $dnmap) : $ret;
+}
+
+# needs a map for dn -> username, we get this from the get_users call
+# otherwise we cannot determine the group membership
+sub get_groups {
+ my ($class, $config, $realm, $dnmap) = @_;
+
+ my $filter = $config->{group_filter};
+ my $basedn = $config->{group_dn} // $config->{base_dn};
+ my $attr = $config->{group_name_attr};
+ $config->{group_classes} //= 'groupOfNames, group, univentionGroup, ipausergroup';
+ my $classes = [PVE::Tools::split_list($config->{group_classes})];
+
+ my $ldap = $class->connect_and_bind($config, $realm);
+
+ my $groups = PVE::LDAP::query_groups($ldap, $basedn, $classes, $filter, $attr);
+
+ my $ret = {};
+
+ foreach my $group (@$groups) {
+ 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;
+ }
+
+ $ret->{$name} = { users => {} };
+ foreach my $member (@{$group->{members}}) {
+ if (my $user = $dnmap->{$member}) {
+ $ret->{$name}->{users}->{$user} = 1;
+ }
+ }
+ }
+ }
+
+ return $ret;
+}
+
+sub authenticate_user {
+ my ($class, $config, $realm, $username, $password) = @_;
+
+ my $ldap = $class->connect_and_bind($config, $realm);
+
+ 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();
+ return 1;
+}
+
+my $ldap_pw_dir = "/etc/pve/priv/realm";
+
+sub ldap_cred_file_name {
+ my ($realmid) = @_;
+ return "${ldap_pw_dir}/${realmid}.pw";
+}
+
+sub get_cred_file {
+ my ($realmid) = @_;
+
+ my $cred_file = ldap_cred_file_name($realmid);
+ if (-e $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";
+ }
+
+ return $cred_file;
+}
+
+sub ldap_set_credentials {
+ my ($password, $realmid) = @_;
+
+ my $cred_file = ldap_cred_file_name($realmid);
+ mkdir $ldap_pw_dir;
+
+ PVE::Tools::file_set_contents($cred_file, $password);
+
+ return $cred_file;
+}
+
+sub ldap_get_credentials {
+ my ($realmid) = @_;
+
+ if (my $cred_file = get_cred_file($realmid)) {
+ return PVE::Tools::file_read_firstline($cred_file);
+ }
+ return undef;
+}
+
+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";
+ }
+}
+
+sub on_add_hook {
+ my ($class, $realm, $config, %param) = @_;
+
+ if (defined($param{password})) {
+ ldap_set_credentials($param{password}, $realm);
+ } else {
+ ldap_delete_credentials($realm);
+ }
+}
+
+sub on_update_hook {
+ my ($class, $realm, $config, %param) = @_;
+
+ return if !exists($param{password});
+
+ if (defined($param{password})) {
+ ldap_set_credentials($param{password}, $realm);
+ } else {
+ ldap_delete_credentials($realm);
+ }
+}
+
+sub on_delete_hook {
+ my ($class, $realm, $config) = @_;
+
+ ldap_delete_credentials($realm);
+}
+
+1;
--- /dev/null
+
+AUTH_SOURCES= \
+ Plugin.pm \
+ PVE.pm \
+ PAM.pm \
+ AD.pm \
+ LDAP.pm
+
+.PHONY: install
+install:
+ for i in ${AUTH_SOURCES}; do install -D -m 0644 $$i ${DESTDIR}${PERLDIR}/PVE/Auth/$$i; done
--- /dev/null
+package PVE::Auth::PAM;
+
+use strict;
+use warnings;
+
+use PVE::Tools qw(run_command);
+use PVE::Auth::Plugin;
+use Authen::PAM qw(:constants);
+
+use base qw(PVE::Auth::Plugin);
+
+sub type {
+ return 'pam';
+}
+
+sub options {
+ return {
+ default => { optional => 1 },
+ comment => { optional => 1 },
+ tfa => { optional => 1 },
+ };
+}
+
+sub authenticate_user {
+ my ($class, $config, $realm, $username, $password) = @_;
+
+ # user (www-data) need to be able to read /etc/passwd /etc/shadow
+ die "no password\n" if !$password;
+
+ my $pamh = new Authen::PAM('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";
+ }
+
+ my $res;
+
+ if (($res = $pamh->pam_authenticate(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
+
+ return 1;
+}
+
+
+sub store_password {
+ my ($class, $config, $realm, $username, $password) = @_;
+
+ my $cmd = ['usermod'];
+
+ my $epw = PVE::Tools::encrypt_pw($password);
+
+ push @$cmd, '-p', $epw, $username;
+
+ run_command($cmd, errmsg => 'change password failed');
+}
+
+1;
--- /dev/null
+package PVE::Auth::PVE;
+
+use strict;
+use warnings;
+use Encode;
+
+use PVE::Tools;
+use PVE::Auth::Plugin;
+use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_write_file cfs_lock_file);
+
+use base qw(PVE::Auth::Plugin);
+
+my $shadowconfigfile = "priv/shadow.cfg";
+
+cfs_register_file($shadowconfigfile,
+ \&parse_shadow_passwd,
+ \&write_shadow_config);
+
+sub parse_shadow_passwd {
+ my ($filename, $raw) = @_;
+
+ my $shadow = {};
+
+ return $shadow if !defined($raw);
+
+ while ($raw =~ /^\s*(.+?)\s*$/gm) {
+ my $line = $1;
+
+ 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;
+ }
+
+ return $shadow;
+}
+
+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";
+ }
+
+ return $data
+}
+
+sub lock_shadow_config {
+ my ($code, $errmsg) = @_;
+
+ cfs_lock_file($shadowconfigfile, undef, $code);
+ my $err = $@;
+ if ($err) {
+ $errmsg ? die "$errmsg: $err" : die $err;
+ }
+}
+
+sub type {
+ return 'pve';
+}
+
+sub options {
+ return {
+ default => { optional => 1 },
+ comment => { optional => 1 },
+ tfa => { optional => 1 },
+ };
+}
+
+sub authenticate_user {
+ my ($class, $config, $realm, $username, $password) = @_;
+
+ die "no password\n" if !$password;
+
+ 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});
+ } else {
+ die "no password set\n";
+ }
+
+ return 1;
+}
+
+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);
+ });
+}
+
+sub delete_user {
+ my ($class, $config, $realm, $username) = @_;
+
+ lock_shadow_config(sub {
+ my $shadow_cfg = cfs_read_file($shadowconfigfile);
+
+ delete $shadow_cfg->{users}->{$username};
+
+ cfs_write_file($shadowconfigfile, $shadow_cfg);
+ });
+}
+
+1;
--- /dev/null
+package PVE::Auth::Plugin;
+
+use strict;
+use warnings;
+
+use Digest::SHA;
+use Encode;
+
+use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_lock_file);
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::SectionConfig;
+use PVE::Tools;
+
+use base qw(PVE::SectionConfig);
+
+my $domainconfigfile = "domains.cfg";
+
+cfs_register_file($domainconfigfile,
+ sub { __PACKAGE__->parse_config(@_); },
+ sub { __PACKAGE__->write_config(@_); });
+
+sub lock_domain_config {
+ my ($code, $errmsg) = @_;
+
+ cfs_lock_file($domainconfigfile, undef, $code);
+ my $err = $@;
+ if ($err) {
+ $errmsg ? die "$errmsg: $err" : die $err;
+ }
+}
+
+our $realm_regex = qr/[A-Za-z][A-Za-z0-9\.\-_]+/;
+our $user_regex = qr![^\s:/]+!;
+
+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 $realm;
+}
+
+PVE::JSONSchema::register_standard_option('realm', {
+ description => "Authentication domain ID",
+ type => 'string', format => 'pve-realm',
+ maxLength => 32,
+});
+
+my $realm_sync_options_desc = {
+ scope => {
+ description => "Select what to sync.",
+ type => 'string',
+ enum => [qw(users groups both)],
+ optional => '1',
+ },
+ full => {
+ description => "If set, uses the LDAP Directory as source of truth,"
+ ." deleting users or groups not returned from the sync. Otherwise"
+ ." only syncs information which is not already present, and does not"
+ ." deletes or modifies anything else.",
+ type => 'boolean',
+ optional => '1',
+ },
+ 'enable-new' => {
+ description => "Enable newly synced users immediately.",
+ type => 'boolean',
+ default => '1',
+ optional => '1',
+ },
+ purge => {
+ description => "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;
+ }
+ if ($len > 64) {
+ die "user name '$username' is too long ($len > 64)\n" if !$noerr;
+ return undef;
+ }
+
+ # we only allow a limited set of characters
+ # colon is not allowed, because we store usernames in
+ # colon separated lists)!
+ # 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;
+ }
+
+ die "value '$username' does not look like a valid user name\n" if !$noerr;
+
+ return undef;
+}
+
+PVE::JSONSchema::register_standard_option('userid', {
+ description => "User ID",
+ type => 'string', format => 'pve-userid',
+ maxLength => 64,
+});
+
+my $tfa_format = {
+ type => {
+ description => "The type of 2nd factor authentication.",
+ format_description => 'TFATYPE',
+ type => 'string',
+ enum => [qw(yubico oath)],
+ },
+ id => {
+ description => "Yubico API ID.",
+ format_description => 'ID',
+ type => 'string',
+ optional => 1,
+ },
+ key => {
+ description => "Yubico API Key.",
+ format_description => 'KEY',
+ type => 'string',
+ optional => 1,
+ },
+ url => {
+ description => "Yubico API URL.",
+ format_description => 'URL',
+ type => 'string',
+ optional => 1,
+ },
+ digits => {
+ description => "TOTP digits.",
+ format_description => 'COUNT',
+ type => 'integer',
+ minimum => 6, maximum => 8,
+ default => 6,
+ optional => 1,
+ },
+ step => {
+ description => "TOTP time period.",
+ format_description => 'SECONDS',
+ type => 'integer',
+ minimum => 10,
+ default => 30,
+ optional => 1,
+ },
+};
+
+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,
+});
+
+sub parse_tfa_config {
+ my ($data) = @_;
+
+ return PVE::JSONSchema::parse_property_string($tfa_format, $data);
+}
+
+my $defaultData = {
+ propertyList => {
+ type => { description => "Realm type." },
+ realm => get_standard_option('realm'),
+ },
+};
+
+sub private {
+ return $defaultData;
+}
+
+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);
+ }
+ return undef;
+}
+
+sub parse_config {
+ my ($class, $filename, $raw) = @_;
+
+ 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;
+ }
+ }
+
+ if ($data->{comment}) {
+ $data->{comment} = PVE::Tools::decode_text($data->{comment});
+ }
+
+ }
+
+ # add default domains
+
+ $cfg->{ids}->{pve}->{type} = 'pve'; # force type
+ $cfg->{ids}->{pve}->{comment} = "Proxmox VE authentication server"
+ if !$cfg->{ids}->{pve}->{comment};
+
+ $cfg->{ids}->{pam}->{type} = 'pam'; # force type
+ $cfg->{ids}->{pam}->{plugin} = 'PVE::Auth::PAM';
+ $cfg->{ids}->{pam}->{comment} = "Linux PAM standard authentication"
+ 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});
+ }
+ }
+
+ $class->SUPER::write_config($filename, $cfg);
+}
+
+sub authenticate_user {
+ my ($class, $config, $realm, $username, $password) = @_;
+
+ die "overwrite me";
+}
+
+sub store_password {
+ my ($class, $config, $realm, $username, $password) = @_;
+
+ my $type = $class->type();
+
+ die "can't set password on auth type '$type'\n";
+}
+
+sub delete_user {
+ my ($class, $config, $realm, $username) = @_;
+
+ # do nothing by default
+}
+
+# called during addition of realm (before the new domain config got written)
+# `password` is moved to %param to avoid writing it out to the config
+# die to abort additon if there are (grave) problems
+# NOTE: runs in a domain config *locked* context
+sub on_add_hook {
+ my ($class, $realm, $config, %param) = @_;
+ # do nothing by default
+}
+
+# called during domain configuration update (before the updated domain config got
+# written). `password` is moved to %param to avoid writing it out to the config
+# die to abort the update if there are (grave) problems
+# NOTE: runs in a domain config *locked* context
+sub on_update_hook {
+ my ($class, $realm, $config, %param) = @_;
+ # do nothing by default
+}
+
+# called during deletion of realms (before the new domain config got written)
+# and if the activate check on addition fails, to cleanup all storage traces
+# which on_add_hook may have created.
+# die to abort deletion if there are (very grave) problems
+# NOTE: runs in a storage config *locked* context
+sub on_delete_hook {
+ my ($class, $realm, $config) = @_;
+ # do nothing by default
+}
+
+1;
--- /dev/null
+SOURCES=pveum.pm
+
+.PHONY: install
+install: ${SOURCES}
+ install -d -m 0755 ${DESTDIR}${PERLDIR}/PVE/CLI
+ for i in ${SOURCES}; do install -D -m 0644 $$i ${DESTDIR}${PERLDIR}/PVE/CLI/$$i; done
+
+
+clean:
--- /dev/null
+package PVE::CLI::pveum;
+
+use strict;
+use warnings;
+
+use PVE::AccessControl;
+use PVE::RPCEnvironment;
+use PVE::API2::User;
+use PVE::API2::Group;
+use PVE::API2::Role;
+use PVE::API2::ACL;
+use PVE::API2::AccessControl;
+use PVE::API2::Pool;
+use PVE::API2::Domains;
+use PVE::CLIFormatter;
+use PVE::CLIHandler;
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::PTY;
+use PVE::RESTHandler;
+use PVE::Tools qw(extract_param);
+
+use base qw(PVE::CLIHandler);
+
+sub setup_environment {
+ PVE::RPCEnvironment->setup_default_cli_env();
+}
+
+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: ');
+ },
+ }),
+ ]
+ };
+
+ return $mapping->{$name};
+}
+
+my $print_api_result = sub {
+ my ($data, $schema, $options) = @_;
+ PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
+};
+
+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";
+ } else {
+ PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
+ }
+};
+
+__PACKAGE__->register_method({
+ name => 'token_permissions',
+ path => 'token_permissions',
+ 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,
+ }),
+ },
+ },
+ returns => {
+ type => 'object',
+ description => 'Hash of structure "path" => "privilege" => "propagate boolean".',
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $token_subid = extract_param($param, "tokenid");
+ $param->{userid} = PVE::AccessControl::join_tokenid($param->{userid}, $token_subid);
+
+ return PVE::API2::AccessControl->permissions($param);
+ }});
+
+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 => [ 'PVE::API2::AccessControl', 'change_tfa', ['userid'], { action => 'delete', key => undef, config => undef, response => undef, }, ],
+ },
+ 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 ],
+ remove => [ 'PVE::API2::User', 'remove_token', ['userid', 'tokenid'], {}, $print_api_result, $PVE::RESTHandler::standard_output_options ],
+ 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],
+ },
+ 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],
+ },
+ 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],
+ },
+ 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],
+ },
+ 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";
+ }],
+
+ passwd => [ 'PVE::API2::AccessControl', 'change_password', ['userid'] ],
+
+ useradd => { alias => 'user add' },
+ usermod => { alias => 'user modify' },
+ userdel => { alias => 'user delete' },
+
+ groupadd => { alias => 'group add' },
+ groupmod => { alias => 'group modify' },
+ groupdel => { alias => 'group delete' },
+
+ roleadd => { alias => 'role add' },
+ rolemod => { alias => 'role modify' },
+ roledel => { alias => 'role delete' },
+
+ aclmod => { alias => 'acl modify' },
+ acldel => { alias => 'acl delete' },
+};
+
+1;
--- /dev/null
+
+
+.PHONY: install
+install:
+ make -C Auth install
+ install -D -m 0644 AccessControl.pm ${DESTDIR}${PERLDIR}/PVE/AccessControl.pm
+ install -D -m 0644 RPCEnvironment.pm ${DESTDIR}${PERLDIR}/PVE/RPCEnvironment.pm
+ install -D -m 0644 TokenConfig.pm ${DESTDIR}${PERLDIR}/PVE/TokenConfig.pm
+ make -C API2 install
+ make -C CLI install
--- /dev/null
+package PVE::RPCEnvironment;
+
+use strict;
+use warnings;
+
+use PVE::RESTEnvironment;
+
+use PVE::Exception qw(raise raise_perm_exc);
+use PVE::SafeSyslog;
+use PVE::Tools;
+use PVE::INotify;
+use PVE::Cluster;
+use PVE::ProcFSTools;
+use PVE::AccessControl;
+
+use base qw(PVE::RESTEnvironment);
+
+# ACL cache
+
+my $compile_acl_path = sub {
+ my ($self, $user, $path) = @_;
+
+ my $cfg = $self->{user_cfg};
+
+ return undef if !$cfg->{roles};
+
+ die "internal error" if $user eq 'root@pam';
+
+ my $cache = $self->{aclcache};
+ $cache->{$user} = {} if !$cache->{$user};
+ my $data = $cache->{$user};
+
+ my ($username, undef) = PVE::AccessControl::split_tokenid($user, 1);
+ die "internal error" if $username && $username ne 'root@pam' && !defined($cache->{$username});
+
+ if (!$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;
+ }
+ }
+ }
+ }
+
+ my $roles = PVE::AccessControl::roles($cfg, $user, $path);
+
+ # apply roles inherited from pools
+ # Note: assume we do not want to propagate those privs
+ if ($data->{poolroles}->{$path}) {
+ if (!defined($roles->{NoAccess})) {
+ if ($data->{poolroles}->{$path}->{NoAccess}) {
+ $roles = { 'NoAccess' => 0 };
+ } else {
+ foreach my $role (keys %{$data->{poolroles}->{$path}}) {
+ $roles->{$role} = 0 if !defined($roles->{$role});
+ }
+ }
+ }
+ }
+
+ $data->{roles}->{$path} = $roles;
+
+ my $privs = {};
+ foreach my $role (keys %$roles) {
+ if (my $privset = $cfg->{roles}->{$role}) {
+ foreach my $p (keys %$privset) {
+ $privs->{$p} = $roles->{$role};
+ }
+ }
+ }
+
+ if ($username && $username ne 'root@pam') {
+ # intersect user and token permissions
+ my $user_privs = $cache->{$username}->{privs}->{$path};
+ $privs = { map { $_ => $user_privs->{$_} && $privs->{$_} } keys %$privs };
+ }
+
+ $data->{privs}->{$path} = $privs;
+
+ return $privs;
+};
+
+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'}} };
+ }
+
+ 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};
+
+ return {} if !$token_info;
+
+ # 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};
+ } else {
+ $user = PVE::AccessControl::verify_username($user, 1);
+ return {} if !$user;
+ }
+
+ my $cache = $self->{aclcache};
+ $cache->{$user} = {} if !$cache->{$user};
+
+ my $acl = $cache->{$user};
+
+ my $perm = $acl->{privs}->{$path};
+ return $perm if $perm;
+
+ return &$compile_acl_path($self, $user, $path);
+}
+
+sub get_effective_permissions {
+ my ($self, $user) = @_;
+
+ # default / top level paths
+ my $paths = {
+ '/' => 1,
+ '/access' => 1,
+ '/access/groups' => 1,
+ '/nodes' => 1,
+ '/pools' => 1,
+ '/storage' => 1,
+ '/vms' => 1,
+ };
+
+ my $cfg = $self->{user_cfg};
+
+ # paths explicitly listed in ACLs
+ foreach my $acl_path (keys %{$cfg->{acl}}) {
+ $paths->{$acl_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;
+ }
+ }
+
+ my $perms = {};
+ foreach my $path (keys %$paths) {
+ my $path_perms = $self->permissions($user, $path);
+ # filter paths where user has NO permissions
+ $perms->{$path} = $path_perms if %$path_perms;
+ }
+ return $perms;
+}
+
+sub check {
+ my ($self, $user, $path, $privs, $noerr) = @_;
+
+ 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");
+ }
+ };
+
+ return 1;
+};
+
+sub check_any {
+ my ($self, $user, $path, $privs, $noerr) = @_;
+
+ my $perm = $self->permissions($user, $path);
+
+ my $found = 0;
+ foreach my $priv (@$privs) {
+ 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);
+ } else {
+ return $self->check($username, $path, $privs, $noerr);
+ }
+}
+
+sub check_user_enabled {
+ my ($self, $user, $noerr) = @_;
+
+ my $cfg = $self->{user_cfg};
+ return PVE::AccessControl::check_user_enabled($cfg, $user, $noerr);
+}
+
+sub check_user_exist {
+ my ($self, $user, $noerr) = @_;
+
+ my $cfg = $self->{user_cfg};
+ return PVE::AccessControl::check_user_exist($cfg, $user, $noerr);
+}
+
+sub check_pool_exist {
+ my ($self, $pool, $noerr) = @_;
+
+ my $cfg = $self->{user_cfg};
+
+ return 1 if $cfg->{pools}->{$pool};
+
+ return undef if $noerr;
+
+ raise_perm_exc("pool '$pool' does not exist");
+}
+
+sub check_vm_perm {
+ my ($self, $user, $vmid, $pool, $privs, $any, $noerr) = @_;
+
+ my $cfg = $self->{user_cfg};
+
+ if ($pool) {
+ 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) = @_;
+
+ my $cfg = $self->{user_cfg};
+
+ return 0 if !$cfg->{groups}->{$group};
+
+ return defined($cfg->{groups}->{$group}->{users}->{$user});
+}
+
+sub filter_groups {
+ my ($self, $user, $privs, $any) = @_;
+
+ 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};
+ }
+ }
+
+ return $groups;
+}
+
+sub group_member_join {
+ my ($self, $grouplist) = @_;
+
+ my $users = {};
+
+ 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;
+ }
+ }
+
+ return $users;
+}
+
+sub check_perm_modify {
+ my ($self, $username, $path, $noerr) = @_;
+
+ return $self->check($username, '/access', [ 'Permissions.Modify' ], $noerr) if !$path;
+
+ my $testperms = [ 'Permissions.Modify' ];
+ if ($path =~ m|^/storage/.+$|) {
+ push @$testperms, 'Datastore.Allocate';
+ } elsif ($path =~ m|^/vms/.+$|) {
+ push @$testperms, 'VM.Allocate';
+ } elsif ($path =~ m|^/pool/.+$|) {
+ push @$testperms, 'Pool.Allocate';
+ }
+
+ return $self->check_any($username, $path, $testperms, $noerr);
+}
+
+sub exec_api2_perm_check {
+ my ($self, $check, $username, $param, $noerr) = @_;
+
+ # syslog("info", "CHECK " . join(', ', @$check));
+
+ my $ind = 0;
+ my $test = $check->[$ind++];
+ 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;
+ } 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();
+ } 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);
+ $path = PVE::AccessControl::normalize_path($path);
+ return $self->check_full($username, $path, $privs, $any, $noerr);
+ } elsif ($test eq 'userid-group') {
+ my $userid = $param->{userid};
+ my ($t, $privs, %options) = @$check;
+ return 0 if !$options{groups_param} && !$self->check_user_exist($userid, $noerr);
+ if (!$self->check_any($username, "/access/groups", $privs, 1)) {
+ my $groups = $self->filter_groups($username, $privs, 1);
+ if ($options{groups_param}) {
+ 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};
+ }
+ } else {
+ 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";
+ }
+ } elsif ($test eq 'perm-modify') {
+ my ($t, $tmplpath) = @$check;
+ my $path = PVE::Tools::template_replace($tmplpath, $param);
+ $path = PVE::AccessControl::normalize_path($path);
+ return $self->check_perm_modify($username, $path, $noerr);
+ } else {
+ die "unknown permission test";
+ }
+};
+
+sub check_api2_permissions {
+ my ($self, $perm, $username, $param) = @_;
+
+ return 1 if !$username && $perm->{user} && $perm->{user} eq 'world';
+
+ raise_perm_exc("user != null") if !$username;
+
+ return 1 if $username eq 'root@pam';
+
+ raise_perm_exc('user != root@pam') if !$perm;
+
+ return 1 if $perm->{user} && $perm->{user} eq 'all';
+
+ return $self->exec_api2_perm_check($perm->{check}, $username, $param)
+ if $perm->{check};
+
+ raise_perm_exc();
+}
+
+sub log_cluster_msg {
+ my ($self, $pri, $user, $msg) = @_;
+
+ PVE::Cluster::log_msg($pri, $user, $msg);
+}
+
+sub broadcast_tasklist {
+ my ($self, $tlist) = @_;
+
+ PVE::Cluster::broadcast_tasklist($tlist);
+}
+
+# initialize environment - must be called once at program startup
+sub init {
+ my ($class, $type, %params) = @_;
+
+ $class = ref($class) || $class;
+
+ my $self = $class->SUPER::init($type, %params);
+
+ $self->{user_cfg} = {};
+ $self->{aclcache} = {};
+ $self->{aclversion} = undef;
+
+ return $self;
+};
+
+
+# init_request - must be called before each RPC request
+sub init_request {
+ my ($self, %params) = @_;
+
+ PVE::Cluster::cfs_update();
+
+ $self->{result_attributes} = {};
+
+ 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'";
+ }
+ }
+
+ 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;
+ }
+ }
+ };
+ if (my $err = $@) {
+ $self->{user_cfg} = {};
+ die "Unable to load access control list: $err";
+ }
+}
+
+# hacks: to provide better backwards compatibiliy
+
+# old code uses PVE::RPCEnvironment::get();
+# new code should use PVE::RPCEnvironment->get();
+sub get {
+ return PVE::RESTEnvironment->get();
+}
+
+# old code uses PVE::RPCEnvironment::is_worker();
+# new code should use PVE::RPCEnvironment->is_worker();
+sub is_worker {
+ return PVE::RESTEnvironment->is_worker();
+}
+
+1;
--- /dev/null
+package PVE::TokenConfig;
+
+use strict;
+use warnings;
+
+use UUID;
+
+use PVE::AccessControl;
+use PVE::Cluster;
+
+my $parse_token_cfg = sub {
+ my ($filename, $raw) = @_;
+
+ my $parsed = {};
+ return $parsed if !defined($raw);
+
+ my @lines = split(/\n/, $raw);
+ foreach my $line (@lines) {
+ next if $line =~ m/^\s*$/;
+
+ if ($line =~ m/^(\S+) (\S+)$/) {
+ if (PVE::AccessControl::pve_verify_tokenid($1, 1)) {
+ $parsed->{$1} = $2;
+ next;
+ }
+ }
+
+ warn "skipping invalid token.cfg entry\n";
+ }
+
+ return $parsed;
+};
+
+my $write_token_cfg = sub {
+ my ($filename, $data) = @_;
+
+ my $raw = '';
+ foreach my $tokenid (sort keys %$data) {
+ $raw .= "$tokenid $data->{$tokenid}\n";
+ }
+
+ return $raw;
+};
+
+PVE::Cluster::cfs_register_file('priv/token.cfg', $parse_token_cfg, $write_token_cfg);
+
+sub generate_token {
+ my ($tokenid) = @_;
+
+ 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');
+
+ $token_cfg->{$tokenid} = $uuid;
+
+ PVE::Cluster::cfs_write_file('priv/token.cfg', $token_cfg);
+
+ return $uuid;
+ });
+
+ die "$@\n" if defined($@);
+
+ return $token_value;
+}
+
+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');
+
+ delete $token_cfg->{$tokenid};
+
+ PVE::Cluster::cfs_write_file('priv/token.cfg', $token_cfg);
+ });
+
+ die "$@\n" if defined($@);
+}
--- /dev/null
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use MIME::Base32; #libmime-base32-perl
+
+my $test;
+open(RND, "/dev/urandom");
+sysread(RND, $test, 10) == 10 || die "read random data failed\n";
+print MIME::Base32::encode_rfc3548($test) . "\n";
+
--- /dev/null
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use PVE::CLI::pveum;
+
+PVE::CLI::pveum->run_cli_handler();
--- /dev/null
+
+all:
+
+.PHONY: check
+check:
+ perl -I.. parser_writer.pl
+ perl -I.. perm-test1.pl
+ perl -I.. perm-test2.pl
+ perl -I.. perm-test3.pl
+ perl -I.. perm-test4.pl
+ perl -I.. perm-test5.pl
+ perl -I.. perm-test6.pl
+ perl -I.. perm-test7.pl
+ perl -I.. perm-test8.pl
--- /dev/null
+#!/usr/bin/perl -w
+
+use strict;
+use PVE::PTY;
+use PVE::AccessControl;
+
+my $username = shift;
+die "Username missing" if !$username;
+
+my $password = PVE::PTY::read_password('password: ');
+PVE::AccessControl::authenticate_user($username,$password);
+
+print "Authentication Successful!!\n";
+
+exit (0);
--- /dev/null
+#!/usr/bin/perl -w
+
+use strict;
+use PVE::RPCEnvironment;
+use Getopt::Long;
+use Data::Dumper;
+
+# example:
+# dump-perm.pl -f myuser.cfg root /
+
+my $opt_file;
+if (!GetOptions ("file=s" => \$opt_file)) {
+ exit (-1);
+}
+
+my $username = shift;
+my $path = shift;
+
+if (!($username && $path)) {
+ print "usage: $0 <username> <path>\n";
+ exit (-1);
+}
+
+my $cfg;
+
+my $rpcenv = PVE::RPCEnvironment->init('cli');
+if ($opt_file) {
+ $rpcenv->init_request(userconfig => $opt_file);
+} else {
+ $rpcenv->init_request();
+}
+
+my $perm = $rpcenv->permissions($username, $path);
+
+print "permission for user '$username' on '$path':\n";
+print join(',', keys %$perm) . "\n";
+
+exit (0);
--- /dev/null
+#!/usr/bin/perl -w
+
+use strict;
+use PVE::AccessControl;
+use Data::Dumper;
+
+my $cfg;
+
+$cfg = PVE::AccessControl::load_user_config();
+
+print Dumper($cfg) . "\n";
+
+exit (0);
--- /dev/null
+#!/usr/bin/perl -w
+
+use strict;
+
+use Test::More;
+use PVE::AccessControl;
+
+use Storable qw(dclone);
+
+PVE::AccessControl::create_roles();
+my $default_user_cfg = {};
+PVE::AccessControl::userconfig_force_defaults($default_user_cfg);
+
+my $add_default_user_properties = sub {
+ my ($user) = @_;
+
+ $user->{enable} = 1 if !defined($user->{enable});
+ $user->{expire} = 0 if !defined($user->{expire});
+ $user->{email} = undef if !defined($user->{email});
+
+ return $user;
+};
+
+sub default_roles {
+ my $roles = dclone($default_user_cfg->{roles});
+ return $roles;
+}
+
+sub default_roles_with {
+ my ($extra_roles) = @_;
+
+ my $roles = default_roles();
+
+ foreach my $r (@$extra_roles) {
+ my $role = dclone($r);
+ my $roleid = delete $role->{id};
+ $roles->{$roleid} = $role;
+ }
+
+ return $roles;
+}
+
+sub default_users {
+ my $users = dclone($default_user_cfg->{users});
+ return { map { $_ => $add_default_user_properties->($users->{$_}); } keys %$users};
+}
+
+sub default_users_with {
+ my ($extra_users) = @_;
+
+ my $users = default_users();
+
+ foreach my $u (@$extra_users) {
+ my $user = dclone($u);
+ my $userid = delete $user->{id};
+ $users->{$userid} = $add_default_user_properties->($user);
+ }
+
+ return $users;
+}
+
+sub default_groups {
+ return {};
+}
+
+sub default_groups_with {
+ my ($extra_groups) = @_;
+
+ my $groups = default_groups();
+
+ foreach my $g (@$extra_groups) {
+ my $group = dclone($g);
+ my $groupid = delete $group->{id};
+ $groups->{$groupid} = $group;
+ }
+
+ return $groups;
+}
+
+sub default_pools {
+ return {};
+}
+
+sub default_pools_with {
+ my ($extra_pools) = @_;
+
+ my $pools = default_pools();
+
+ foreach my $p (@$extra_pools) {
+ my $pool = dclone($p);
+ my $poolid = delete $pool->{id};
+ $pools->{$poolid} = $pool;
+ }
+
+ return $pools;
+}
+
+sub default_pool_vms_with {
+ my ($extra_pools) = @_;
+
+ my $vms = {};
+ foreach my $pool (@$extra_pools) {
+ foreach my $vmid (keys %{$pool->{vms}}) {
+ $vms->{$vmid} = $pool->{id};
+ }
+ }
+ return $vms;
+}
+
+sub default_acls {
+ return {};
+}
+
+# note: does not support merging paths!
+sub default_acls_with {
+ my ($extra_acls) = @_;
+
+ my $acls = default_acls();
+
+ foreach my $a (@$extra_acls) {
+ my $acl = dclone($a);
+ my $path = delete $acl->{path};
+ $acls->{$path} = $acl;
+ }
+
+ return $acls;
+}
+
+my $default_cfg = {
+ test_pam => {
+ 'id' => 'test@pam',
+ 'enable' => 1,
+ 'expire' => 0,
+ 'email' => undef,
+ },
+ test2_pam => {
+ 'id' => 'test2@pam',
+ 'enable' => 1,
+ 'expire' => 0,
+ 'email' => undef,
+ },
+ test_pam_with_group => {
+ 'id' => 'test@pam',
+ 'enable' => 1,
+ 'expire' => 0,
+ 'email' => undef,
+ 'groups' => { 'testgroup' => 1 },
+ },
+ test2_pam_with_group => {
+ 'id' => 'test2@pam',
+ 'enable' => 1,
+ 'expire' => 0,
+ 'email' => undef,
+ 'groups' => { 'testgroup' => 1 },
+ },
+ test3_pam => {
+ 'id' => 'test3@pam',
+ 'enable' => 1,
+ 'expire' => 0,
+ 'email' => undef,
+ 'groups' => { 'another' => 1 },
+ },
+ test_pam_with_token => {
+ 'id' => 'test@pam',
+ 'enable' => 1,
+ 'expire' => 0,
+ 'email' => undef,
+ 'tokens' => {
+ 'full' => {
+ 'privsep' => 0,
+ 'expire' => 0,
+ },
+ },
+ },
+ test_pam2_with_token => {
+ 'id' => 'test2@pam',
+ 'enable' => 1,
+ 'expire' => 0,
+ 'email' => undef,
+ 'tokens' => {
+ 'full' => {
+ 'privsep' => 0,
+ 'expire' => 0,
+ },
+ 'privsep' => {
+ 'privsep' => 1,
+ 'expire' => 0,
+ },
+ 'expired' => {
+ 'privsep' => 0,
+ 'expire' => 1,
+ },
+ },
+ },
+ test_group_empty => {
+ 'id' => 'testgroup',
+ users => {},
+ },
+ test_group_single_member => {
+ 'id' => 'testgroup',
+ 'users' => {
+ 'test@pam' => 1,
+ },
+ },
+ test_group_members => {
+ 'id' => 'testgroup',
+ 'users' => {
+ 'test@pam' => 1,
+ 'test2@pam' => 1,
+ },
+ },
+ test_group_second => {
+ 'id' => 'another',
+ users => {
+ 'test3@pam' => 1,
+ },
+ },
+ test_role_single_priv => {
+ 'id' => 'testrolesingle',
+ 'VM.Allocate' => 1,
+ },
+ test_role_privs => {
+ 'id' => 'testrole',
+ 'VM.Allocate' => 1,
+ 'Datastore.Audit' => 1,
+ },
+ test_pool_empty => {
+ 'id' => 'testpool',
+ vms => {},
+ storage => {},
+ },
+ test_pool_members => {
+ 'id' => 'testpool',
+ vms => { 123 => 1, 1234 => 1},
+ storage => { 'local' => 1, 'local-zfs' => 1},
+ },
+ test_pool_duplicate_vms => {
+ 'id' => 'test_duplicate_vms',
+ vms => {},
+ storage => {},
+ },
+ test_pool_duplicate_storages => {
+ 'id' => 'test_duplicate_storages',
+ vms => {},
+ storage => { 'local' => 1, 'local-zfs' => 1},
+ },
+ acl_simple_user => {
+ 'path' => '/',
+ users => {
+ 'test@pam' => {
+ 'PVEVMAdmin' => 1,
+ },
+ },
+ },
+ acl_complex_users => {
+ 'path' => '/storage',
+ users => {
+ 'test2@pam' => {
+ 'PVEDatastoreUser' => 1,
+ },
+ 'test@pam' => {
+ 'PVEDatastoreAdmin' => 1,
+ },
+ },
+ },
+ acl_complex_missing_user => {
+ 'path' => '/storage',
+ users => {
+ 'test2@pam' => {
+ 'PVEDatastoreUser' => 1,
+ },
+ 'test@pam' => {
+ 'PVEDatastoreAdmin' => 1,
+ },
+ },
+ },
+ acl_simple_token => {
+ 'path' => '/',
+ tokens => {
+ 'test@pam!full' => {
+ 'PVEVMAdmin' => 1,
+ },
+ },
+ },
+ acl_complex_tokens => {
+ 'path' => '/storage',
+ tokens => {
+ 'test2@pam!privsep' => {
+ 'PVEDatastoreUser' => 1,
+ },
+ 'test2@pam!expired' => {
+ 'PVEDatastoreAdmin' => 1,
+ },
+ 'test@pam!full' => {
+ 'PVEDatastoreAdmin' => 1,
+ },
+ },
+ },
+ acl_complex_missing_token => {
+ 'path' => '/storage',
+ tokens => {
+ 'test2@pam!expired' => {
+ 'PVEDatastoreAdmin' => 1,
+ },
+ 'test2@pam!privsep' => {
+ 'PVEDatastoreUser' => 1,
+ },
+ },
+ },
+ acl_simple_group => {
+ 'path' => '/',
+ groups => {
+ 'testgroup' => {
+ 'PVEVMAdmin' => 1,
+ },
+ },
+ },
+ acl_complex_groups => {
+ 'path' => '/storage',
+ groups => {
+ 'testgroup' => {
+ 'PVEDatastoreAdmin' => 1,
+ },
+ 'another' => {
+ 'PVEDatastoreUser' => 1,
+ },
+ },
+ },
+ acl_simple_group_noprop => {
+ 'path' => '/',
+ groups => {
+ 'testgroup' => {
+ 'PVEVMAdmin' => 0,
+ },
+ },
+ },
+ acl_complex_groups_noprop => {
+ 'path' => '/storage',
+ groups => {
+ 'testgroup' => {
+ 'PVEDatastoreAdmin' => 0,
+ },
+ 'another' => {
+ 'PVEDatastoreUser' => 0,
+ },
+ },
+ },
+ acl_complex_missing_group => {
+ 'path' => '/storage',
+ groups => {
+ 'testgroup' => {
+ 'PVEDatastoreAdmin' => 1,
+ },
+ 'another' => {
+ 'PVEDatastoreUser' => 1,
+ },
+ },
+ },
+ acl_missing_role => {
+ 'path' => '/storage',
+ users => {
+ 'test@pam' => {
+ 'MissingRole' => 1,
+ },
+ },
+ },
+};
+
+$default_cfg->{'acl_complex_mixed_root'} = {
+ 'path' => '/',
+ users => $default_cfg->{'acl_simple_user'}->{users},
+ groups => $default_cfg->{'acl_simple_group'}->{groups},
+};
+
+$default_cfg->{'acl_complex_mixed_storage'} = {
+ 'path' => '/storage',
+ users => $default_cfg->{'acl_complex_users'}->{users},
+ groups => $default_cfg->{'acl_complex_groups'}->{groups},
+};
+
+$default_cfg->{'acl_complex_mixed_root_noprop'} = {
+ 'path' => '/',
+ users => $default_cfg->{'acl_simple_user'}->{users},
+ groups => $default_cfg->{'acl_simple_group_noprop'}->{groups},
+};
+
+$default_cfg->{'acl_complex_mixed_storage_noprop'} = {
+ 'path' => '/storage',
+ users => $default_cfg->{'acl_complex_users'}->{users},
+ groups => $default_cfg->{'acl_complex_groups_noprop'}->{groups},
+};
+
+my $default_raw = {
+ users => {
+ 'root@pam' => 'user:root@pam:1:0::::::',
+ 'test_pam' => 'user:test@pam:1:0::::::',
+ 'test2_pam' => 'user:test2@pam:1:0::::::',
+ 'test3_pam' => 'user:test3@pam:1:0::::::',
+ },
+ groups => {
+ 'test_group_empty' => 'group:testgroup:::',
+ 'test_group_single_member' => 'group:testgroup:test@pam::',
+ 'test_group_members' => 'group:testgroup:test2@pam,test@pam::',
+ 'test_group_members_out_of_order' => 'group:testgroup:test@pam,test2@pam::',
+ 'test_group_second' => 'group:another:test3@pam::',
+ },
+ tokens => {
+ 'test_token_simple' => 'token:test@pam!full:0:0::',
+ 'test_token_multi_full' => 'token:test2@pam!full:0:0::',
+ 'test_token_multi_privsep' => 'token:test2@pam!privsep:0:1::',
+ 'test_token_multi_expired' => 'token:test2@pam!expired:1:0::',
+ },
+ roles => {
+ 'test_role_single_priv' => 'role:testrolesingle:VM.Allocate:',
+ 'test_role_privs' => 'role:testrole:Datastore.Audit,VM.Allocate:',
+ 'test_role_privs_out_of_order' => 'role:testrole:VM.Allocate,Datastore.Audit:',
+ 'test_role_privs_duplicate' => 'role:testrole:VM.Allocate,Datastore.Audit,VM.Allocate:',
+ 'test_role_privs_invalid' => 'role:testrole:VM.Invalid,Datastore.Audit,VM.Allocate:',
+ },
+ pools => {
+ 'test_pool_empty' => 'pool:testpool::::',
+ 'test_pool_invalid' => 'pool:testpool::non-numeric:inval!d:',
+ 'test_pool_members' => 'pool:testpool::123,1234:local,local-zfs:',
+ 'test_pool_duplicate_vms' => 'pool:test_duplicate_vms::123,1234::',
+ 'test_pool_duplicate_vms_expected' => 'pool:test_duplicate_vms::::',
+ 'test_pool_duplicate_storages' => 'pool:test_duplicate_storages:::local,local-zfs:',
+ },
+ acl => {
+ 'acl_simple_user' => 'acl:1:/:test@pam:PVEVMAdmin:',
+ 'acl_complex_users_1' => 'acl:1:/storage:test@pam:PVEDatastoreAdmin:',
+ 'acl_complex_users_2' => 'acl:1:/storage:test2@pam:PVEDatastoreUser:',
+ 'acl_simple_token' => 'acl:1:/:test@pam!full:PVEVMAdmin:',
+ 'acl_complex_tokens_1' => 'acl:1:/storage:test2@pam!expired,test@pam!full:PVEDatastoreAdmin:',
+ 'acl_complex_tokens_2' => 'acl:1:/storage:test2@pam!privsep:PVEDatastoreUser:',
+ 'acl_complex_tokens_1_missing' => 'acl:1:/storage:test2@pam!expired:PVEDatastoreAdmin:',
+ 'acl_simple_group' => 'acl:1:/:@testgroup:PVEVMAdmin:',
+ 'acl_complex_groups_1' => 'acl:1:/storage:@testgroup:PVEDatastoreAdmin:',
+ 'acl_complex_groups_2' => 'acl:1:/storage:@another:PVEDatastoreUser:',
+ 'acl_simple_group_noprop' => 'acl:0:/:@testgroup:PVEVMAdmin:',
+ 'acl_complex_groups_1_noprop' => 'acl:0:/storage:@testgroup:PVEDatastoreAdmin:',
+ 'acl_complex_groups_2_noprop' => 'acl:0:/storage:@another:PVEDatastoreUser:',
+ 'acl_complex_mixed_1' => 'acl:1:/:@testgroup,test@pam:PVEVMAdmin:',
+ 'acl_complex_mixed_2' => 'acl:1:/storage:@testgroup,test@pam:PVEDatastoreAdmin:',
+ 'acl_complex_mixed_3' => 'acl:1:/storage:@another,test2@pam:PVEDatastoreUser:',
+ 'acl_missing_role' => 'acl:1:/storage:test@pam:MissingRole:',
+ },
+};
+
+my $tests = [
+ {
+ name => "empty_config",
+ config => {},
+ expected_config => {
+ users => { 'root@pam' => { enable => 1 } },
+ roles => default_roles(),
+ },
+ raw => "",
+ expected_raw => "\n\n\n\n",
+ },
+ {
+ name => "default_config",
+ config => {
+ users => default_users(),
+ roles => default_roles(),
+ },
+ raw => $default_raw->{users}->{'root@pam'}."\n\n\n\n\n",
+ },
+ {
+ name => "group_empty",
+ config => {
+ users => default_users(),
+ roles => default_roles(),
+ groups => default_groups_with([$default_cfg->{'test_group_empty'}]),
+ },
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n\n".
+ $default_raw->{groups}->{'test_group_empty'}."\n\n".
+ "\n\n",
+ },
+ {
+ name => "group_inexisting_member",
+ config => {
+ users => default_users(),
+ roles => default_roles(),
+ groups => default_groups_with([$default_cfg->{'test_group_empty'}]),
+ },
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n\n".
+ "group:testgroup:does_not_exist::".
+ "\n\n\n\n",
+ expected_raw => "".
+ $default_raw->{users}->{'root@pam'}."\n\n".
+ $default_raw->{groups}->{'test_group_empty'}."\n\n".
+ "\n\n",
+ },
+ {
+ name => "group_invalid_member",
+ expected_config => {
+ users => default_users(),
+ roles => default_roles(),
+ },
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n\n".
+ 'group:inval!d:root@pam:'.
+ "\n\n",
+ },
+ {
+ name => "group_with_one_member",
+ config => {
+ users => default_users_with([$default_cfg->{test_pam_with_group}]),
+ roles => default_roles(),
+ groups => default_groups_with([$default_cfg->{'test_group_single_member'}]),
+ },
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n".
+ $default_raw->{users}->{'test_pam'}."\n\n".
+ $default_raw->{groups}->{'test_group_single_member'}."\n\n".
+ "\n\n",
+ },
+ {
+ name => "group_with_members",
+ config => {
+ users => default_users_with([$default_cfg->{test_pam_with_group}, $default_cfg->{test2_pam_with_group}]),
+ roles => default_roles(),
+ groups => default_groups_with([$default_cfg->{'test_group_members'}]),
+ },
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n".
+ $default_raw->{users}->{'test2_pam'}."\n".
+ $default_raw->{users}->{'test_pam'}."\n\n".
+ $default_raw->{groups}->{'test_group_members'}."\n\n".
+ "\n\n",
+ },
+ {
+ name => "token_simple",
+ config => {
+ users => default_users_with([$default_cfg->{test_pam_with_token}]),
+ roles => default_roles(),
+ },
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n".
+ $default_raw->{users}->{'test_pam'}."\n".
+ $default_raw->{tokens}->{'test_token_simple'}."\n\n\n\n\n",
+ },
+ {
+ name => "token_multi",
+ config => {
+ users => default_users_with([$default_cfg->{test_pam_with_token}, $default_cfg->{test_pam2_with_token}]),
+ roles => default_roles(),
+ },
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n".
+ $default_raw->{users}->{'test2_pam'}."\n".
+ $default_raw->{tokens}->{'test_token_multi_expired'}."\n".
+ $default_raw->{tokens}->{'test_token_multi_full'}."\n".
+ $default_raw->{tokens}->{'test_token_multi_privsep'}."\n".
+ $default_raw->{users}->{'test_pam'}."\n".
+ $default_raw->{tokens}->{'test_token_simple'}."\n".
+ "\n\n\n\n",
+ },
+ {
+ name => "custom_role_with_single_priv",
+ config => {
+ users => default_users(),
+ roles => default_roles_with([$default_cfg->{test_role_single_priv}]),
+ },
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n\n\n\n".
+ $default_raw->{roles}->{'test_role_single_priv'}."\n\n",
+ },
+ {
+ name => "custom_role_with_privs",
+ config => {
+ users => default_users(),
+ roles => default_roles_with([$default_cfg->{test_role_privs}]),
+ },
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n\n\n\n".
+ $default_raw->{roles}->{'test_role_privs'}."\n\n",
+ },
+ {
+ name => "custom_role_with_duplicate_privs",
+ config => {
+ users => default_users(),
+ roles => default_roles_with([$default_cfg->{test_role_privs}]),
+ },
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n\n\n\n".
+ $default_raw->{roles}->{'test_role_privs_duplicate'}."\n\n",
+ expected_raw => "".
+ $default_raw->{users}->{'root@pam'}."\n\n\n\n".
+ $default_raw->{roles}->{'test_role_privs'}."\n\n",
+ },
+ {
+ name => "custom_role_with_invalid_priv",
+ config => {
+ users => default_users(),
+ roles => default_roles_with([$default_cfg->{test_role_privs}]),
+ },
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n\n\n\n".
+ $default_raw->{roles}->{'test_role_privs_invalid'}."\n\n",
+ expected_raw => "".
+ $default_raw->{users}->{'root@pam'}."\n\n\n\n".
+ $default_raw->{roles}->{'test_role_privs'}."\n\n",
+ },
+ {
+ name => "pool_empty",
+ config => {
+ users => default_users(),
+ roles => default_roles(),
+ pools => default_pools_with([$default_cfg->{test_pool_empty}]),
+ },
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n\n\n".
+ $default_raw->{pools}->{'test_pool_empty'}."\n\n\n",
+ },
+ {
+ name => "pool_invalid",
+ config => {
+ users => default_users(),
+ roles => default_roles(),
+ pools => default_pools_with([$default_cfg->{test_pool_empty}]),
+ },
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n\n\n".
+ $default_raw->{pools}->{'test_pool_invalid'}."\n\n\n",
+ expected_raw => "".
+ $default_raw->{users}->{'root@pam'}."\n\n\n".
+ $default_raw->{pools}->{'test_pool_empty'}."\n\n\n",
+ },
+ {
+ name => "pool_members",
+ config => {
+ users => default_users(),
+ roles => default_roles(),
+ pools => default_pools_with([$default_cfg->{test_pool_members}]),
+ vms => default_pool_vms_with([$default_cfg->{test_pool_members}]),
+ },
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n\n\n".
+ $default_raw->{pools}->{'test_pool_members'}."\n\n\n",
+ },
+ {
+ name => "pool_duplicate_members",
+ config => {
+ users => default_users(),
+ roles => default_roles(),
+ pools => default_pools_with([$default_cfg->{test_pool_members}, $default_cfg->{test_pool_duplicate_vms}, $default_cfg->{test_pool_duplicate_storages}]),
+ vms => default_pool_vms_with([$default_cfg->{test_pool_members}]),
+ },
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n\n\n".
+ $default_raw->{pools}->{'test_pool_members'}."\n".
+ $default_raw->{pools}->{'test_pool_duplicate_vms'}."\n".
+ $default_raw->{pools}->{'test_pool_duplicate_storages'}."\n",
+ expected_raw => "".
+ $default_raw->{users}->{'root@pam'}."\n\n\n".
+ $default_raw->{pools}->{'test_pool_duplicate_storages'}."\n".
+ $default_raw->{pools}->{'test_pool_duplicate_vms_expected'}."\n".
+ $default_raw->{pools}->{'test_pool_members'}."\n\n\n",
+ },
+ {
+ name => "acl_simple_user",
+ config => {
+ users => default_users_with([$default_cfg->{test_pam}]),
+ roles => default_roles(),
+ acl => default_acls_with([$default_cfg->{acl_simple_user}]),
+ },
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n".
+ $default_raw->{users}->{'test_pam'}."\n\n\n\n\n".
+ $default_raw->{acl}->{'acl_simple_user'}."\n",
+ },
+ {
+ name => "acl_complex_users",
+ config => {
+ users => default_users_with([$default_cfg->{test_pam}, $default_cfg->{'test2_pam'}]),
+ roles => default_roles(),
+ acl => default_acls_with([$default_cfg->{acl_simple_user}, $default_cfg->{acl_complex_users}]),
+ },
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n".
+ $default_raw->{users}->{'test2_pam'}."\n".
+ $default_raw->{users}->{'test_pam'}."\n\n\n\n\n".
+ $default_raw->{acl}->{'acl_simple_user'}."\n".
+ $default_raw->{acl}->{'acl_complex_users_1'}."\n".
+ $default_raw->{acl}->{'acl_complex_users_2'}."\n",
+ },
+ {
+ name => "acl_complex_missing_user",
+ config => {
+ users => default_users_with([$default_cfg->{test2_pam}]),
+ roles => default_roles(),
+ acl => default_acls_with([$default_cfg->{acl_simple_user}, $default_cfg->{acl_complex_missing_user}]),
+ },
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n".
+ $default_raw->{users}->{'test2_pam'}."\n\n\n\n\n".
+ $default_raw->{acl}->{'acl_simple_user'}."\n".
+ $default_raw->{acl}->{'acl_complex_users_1'}."\n".
+ $default_raw->{acl}->{'acl_complex_users_2'}."\n",
+ },
+ {
+ name => "acl_simple_group",
+ config => {
+ users => default_users_with([$default_cfg->{test_pam_with_group}]),
+ groups => default_groups_with([$default_cfg->{'test_group_single_member'}]),
+ roles => default_roles(),
+ acl => default_acls_with([$default_cfg->{acl_simple_group}]),
+ },
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n".
+ $default_raw->{users}->{'test_pam'}."\n\n".
+ $default_raw->{groups}->{'test_group_single_member'}."\n\n\n\n".
+ $default_raw->{acl}->{'acl_simple_group'}."\n",
+ },
+ {
+ name => "acl_complex_groups",
+ config => {
+ users => default_users_with([$default_cfg->{test_pam_with_group}, $default_cfg->{'test2_pam_with_group'}, $default_cfg->{'test3_pam'}]),
+ groups => default_groups_with([$default_cfg->{'test_group_members'}, $default_cfg->{'test_group_second'}]),
+ roles => default_roles(),
+ acl => default_acls_with([$default_cfg->{acl_simple_group}, $default_cfg->{acl_complex_groups}]),
+ },
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n".
+ $default_raw->{users}->{'test2_pam'}."\n".
+ $default_raw->{users}->{'test3_pam'}."\n".
+ $default_raw->{users}->{'test_pam'}."\n\n".
+ $default_raw->{groups}->{'test_group_second'}."\n".
+ $default_raw->{groups}->{'test_group_members'}."\n\n\n\n".
+ $default_raw->{acl}->{'acl_simple_group'}."\n".
+ $default_raw->{acl}->{'acl_complex_groups_1'}."\n".
+ $default_raw->{acl}->{'acl_complex_groups_2'}."\n",
+ },
+ {
+ name => "acl_complex_missing_group",
+ config => {
+ users => default_users_with([$default_cfg->{test_pam}, $default_cfg->{'test2_pam'}, $default_cfg->{'test3_pam'}]),
+ groups => default_groups_with([$default_cfg->{'test_group_second'}]),
+ roles => default_roles(),
+ acl => default_acls_with([$default_cfg->{acl_simple_group}, $default_cfg->{acl_complex_missing_group}]),
+ },
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n".
+ $default_raw->{users}->{'test2_pam'}."\n".
+ $default_raw->{users}->{'test3_pam'}."\n".
+ $default_raw->{users}->{'test_pam'}."\n\n".
+ $default_raw->{groups}->{'test_group_second'}."\n".
+ $default_raw->{acl}->{'acl_simple_group'}."\n".
+ $default_raw->{acl}->{'acl_complex_groups_1'}."\n".
+ $default_raw->{acl}->{'acl_complex_groups_2'}."\n",
+ expected_raw => "".
+ $default_raw->{users}->{'root@pam'}."\n".
+ $default_raw->{users}->{'test2_pam'}."\n".
+ $default_raw->{users}->{'test3_pam'}."\n".
+ $default_raw->{users}->{'test_pam'}."\n\n".
+ $default_raw->{groups}->{'test_group_second'}."\n\n\n\n".
+ $default_raw->{acl}->{'acl_simple_group'}."\n".
+ $default_raw->{acl}->{'acl_complex_groups_1'}."\n".
+ $default_raw->{acl}->{'acl_complex_groups_2'}."\n",
+ },
+ {
+ name => "acl_simple_token",
+ config => {
+ users => default_users_with([$default_cfg->{test_pam_with_token}]),
+ roles => default_roles(),
+ acl => default_acls_with([$default_cfg->{acl_simple_token}]),
+ },
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n".
+ $default_raw->{users}->{'test_pam'}."\n".
+ $default_raw->{tokens}->{'test_token_simple'}."\n\n\n\n\n".
+ $default_raw->{acl}->{'acl_simple_token'}."\n",
+ },
+ {
+ name => "acl_complex_tokens",
+ config => {
+ users => default_users_with([$default_cfg->{test_pam_with_token}, $default_cfg->{'test_pam2_with_token'}]),
+ roles => default_roles(),
+ acl => default_acls_with([$default_cfg->{acl_simple_token}, $default_cfg->{acl_complex_tokens}]),
+ },
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n".
+ $default_raw->{users}->{'test2_pam'}."\n".
+ $default_raw->{tokens}->{'test_token_multi_expired'}."\n".
+ $default_raw->{tokens}->{'test_token_multi_full'}."\n".
+ $default_raw->{tokens}->{'test_token_multi_privsep'}."\n".
+ $default_raw->{users}->{'test_pam'}."\n".
+ $default_raw->{tokens}->{'test_token_simple'}."\n\n\n\n\n".
+ $default_raw->{acl}->{'acl_simple_token'}."\n".
+ $default_raw->{acl}->{'acl_complex_tokens_1'}."\n".
+ $default_raw->{acl}->{'acl_complex_tokens_2'}."\n",
+ },
+ {
+ name => "acl_complex_missing_token",
+ config => {
+ users => default_users_with([$default_cfg->{test_pam}, $default_cfg->{test_pam2_with_token}]),
+ roles => default_roles(),
+ acl => default_acls_with([$default_cfg->{acl_complex_missing_token}]),
+ },
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n".
+ $default_raw->{users}->{'test2_pam'}."\n".
+ $default_raw->{tokens}->{'test_token_multi_expired'}."\n".
+ $default_raw->{tokens}->{'test_token_multi_full'}."\n".
+ $default_raw->{tokens}->{'test_token_multi_privsep'}."\n".
+ $default_raw->{users}->{'test_pam'}."\n".
+ $default_raw->{acl}->{'acl_simple_token'}."\n".
+ $default_raw->{acl}->{'acl_complex_tokens_1'}."\n".
+ $default_raw->{acl}->{'acl_complex_tokens_2'}."\n",
+ expected_raw => "".
+ $default_raw->{users}->{'root@pam'}."\n".
+ $default_raw->{users}->{'test2_pam'}."\n".
+ $default_raw->{tokens}->{'test_token_multi_expired'}."\n".
+ $default_raw->{tokens}->{'test_token_multi_full'}."\n".
+ $default_raw->{tokens}->{'test_token_multi_privsep'}."\n".
+ $default_raw->{users}->{'test_pam'}."\n\n\n\n\n".
+ $default_raw->{acl}->{'acl_complex_tokens_1_missing'}."\n".
+ $default_raw->{acl}->{'acl_complex_tokens_2'}."\n",
+ },
+ {
+ name => "acl_missing_role",
+ config => {
+ users => default_users_with([$default_cfg->{test_pam}]),
+ roles => default_roles(),
+ acl => default_acls_with([$default_cfg->{acl_simple_user}]),
+ },
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n".
+ $default_raw->{users}->{'test_pam'}."\n\n\n\n\n".
+ $default_raw->{acl}->{'acl_simple_user'}."\n".
+ $default_raw->{acl}->{'acl_missing_role'}."\n",
+ expected_raw => "".
+ $default_raw->{users}->{'root@pam'}."\n".
+ $default_raw->{users}->{'test_pam'}."\n\n\n\n\n".
+ $default_raw->{acl}->{'acl_simple_user'}."\n",
+ },
+ {
+ name => "acl_complex_mixed",
+ config => {
+ users => default_users_with([$default_cfg->{test_pam_with_group}, $default_cfg->{'test2_pam_with_group'}, $default_cfg->{'test3_pam'}]),
+ groups => default_groups_with([$default_cfg->{'test_group_members'}, $default_cfg->{'test_group_second'}]),
+ roles => default_roles(),
+ acl => default_acls_with([
+ $default_cfg->{acl_complex_mixed_root},
+ $default_cfg->{acl_complex_mixed_storage},
+ ]),
+ },
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n".
+ $default_raw->{users}->{'test2_pam'}."\n".
+ $default_raw->{users}->{'test3_pam'}."\n".
+ $default_raw->{users}->{'test_pam'}."\n\n".
+ $default_raw->{groups}->{'test_group_second'}."\n".
+ $default_raw->{groups}->{'test_group_members'}."\n\n\n\n".
+ $default_raw->{acl}->{'acl_simple_group'}."\n".
+ $default_raw->{acl}->{'acl_complex_groups_1'}."\n".
+ $default_raw->{acl}->{'acl_complex_groups_2'}."\n".
+ $default_raw->{acl}->{'acl_simple_user'}."\n".
+ $default_raw->{acl}->{'acl_complex_users_1'}."\n".
+ $default_raw->{acl}->{'acl_complex_users_2'}."\n",
+ expected_raw => "".
+ $default_raw->{users}->{'root@pam'}."\n".
+ $default_raw->{users}->{'test2_pam'}."\n".
+ $default_raw->{users}->{'test3_pam'}."\n".
+ $default_raw->{users}->{'test_pam'}."\n\n".
+ $default_raw->{groups}->{'test_group_second'}."\n".
+ $default_raw->{groups}->{'test_group_members'}."\n\n\n\n".
+ $default_raw->{acl}->{'acl_complex_mixed_1'}."\n".
+ $default_raw->{acl}->{'acl_complex_mixed_2'}."\n".
+ $default_raw->{acl}->{'acl_complex_mixed_3'}."\n",
+ },
+ {
+ name => "acl_complex_mixed_prop_noprop_no_merge_sort_by_path",
+ config => {
+ users => default_users_with([$default_cfg->{test_pam_with_group}, $default_cfg->{'test2_pam_with_group'}, $default_cfg->{'test3_pam'}]),
+ groups => default_groups_with([$default_cfg->{'test_group_members'}, $default_cfg->{'test_group_second'}]),
+ roles => default_roles(),
+ acl => default_acls_with([
+ $default_cfg->{acl_complex_mixed_root_noprop},
+ $default_cfg->{acl_complex_mixed_storage_noprop},
+ ]),
+ },
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n".
+ $default_raw->{users}->{'test2_pam'}."\n".
+ $default_raw->{users}->{'test3_pam'}."\n".
+ $default_raw->{users}->{'test_pam'}."\n\n".
+ $default_raw->{groups}->{'test_group_second'}."\n".
+ $default_raw->{groups}->{'test_group_members'}."\n\n\n\n".
+ $default_raw->{acl}->{'acl_simple_group_noprop'}."\n".
+ $default_raw->{acl}->{'acl_simple_user'}."\n".
+ $default_raw->{acl}->{'acl_complex_groups_1_noprop'}."\n".
+ $default_raw->{acl}->{'acl_complex_groups_2_noprop'}."\n".
+ $default_raw->{acl}->{'acl_complex_users_1'}."\n".
+ $default_raw->{acl}->{'acl_complex_users_2'}."\n",
+ },
+ {
+ name => "sort_roles_and_privs",
+ raw => "".
+ $default_raw->{users}->{'root@pam'}."\n".
+ $default_raw->{roles}->{'test_role_single_priv'}."\n\n".
+ $default_raw->{roles}->{'test_role_privs_out_of_order'}."\n\n",
+ expected_raw => "".
+ $default_raw->{users}->{'root@pam'}."\n\n\n\n".
+ $default_raw->{roles}->{'test_role_privs'}."\n".
+ $default_raw->{roles}->{'test_role_single_priv'}."\n\n",
+ },
+ {
+ name => "sort_users_and_group_members",
+ raw => "".
+ $default_raw->{users}->{'test2_pam'}."\n".
+ $default_raw->{users}->{'root@pam'}."\n".
+ $default_raw->{users}->{'test_pam'}."\n\n".
+ $default_raw->{groups}->{'test_group_members_out_of_order'}."\n\n".
+ "\n\n",
+ expected_raw => "".
+ $default_raw->{users}->{'root@pam'}."\n".
+ $default_raw->{users}->{'test2_pam'}."\n".
+ $default_raw->{users}->{'test_pam'}."\n\n".
+ $default_raw->{groups}->{'test_group_members'}."\n\n".
+ "\n\n",
+ },
+ {
+ name => "sort_user_groups_and_acls",
+ raw => "".
+ $default_raw->{users}->{'test2_pam'}."\n".
+ $default_raw->{users}->{'root@pam'}."\n".
+ $default_raw->{users}->{'test_pam'}."\n\n".
+ $default_raw->{users}->{'test3_pam'}."\n".
+ $default_raw->{groups}->{'test_group_members_out_of_order'}."\n\n\n\n".
+ $default_raw->{groups}->{'test_group_second'}."\n".
+ $default_raw->{acl}->{'acl_simple_user'}."\n".
+ $default_raw->{acl}->{'acl_simple_group'}."\n".
+ $default_raw->{acl}->{'acl_complex_users_1'}."\n".
+ $default_raw->{acl}->{'acl_complex_users_2'}."\n".
+ $default_raw->{acl}->{'acl_complex_groups_1'}."\n".
+ $default_raw->{acl}->{'acl_complex_groups_2'}."\n",
+ expected_raw => "".
+ $default_raw->{users}->{'root@pam'}."\n".
+ $default_raw->{users}->{'test2_pam'}."\n".
+ $default_raw->{users}->{'test3_pam'}."\n".
+ $default_raw->{users}->{'test_pam'}."\n\n".
+ $default_raw->{groups}->{'test_group_second'}."\n".
+ $default_raw->{groups}->{'test_group_members'}."\n\n\n\n".
+ $default_raw->{acl}->{'acl_complex_mixed_1'}."\n".
+ $default_raw->{acl}->{'acl_complex_mixed_2'}."\n".
+ $default_raw->{acl}->{'acl_complex_mixed_3'}."\n",
+ },
+ {
+ name => 'default_values',
+ config => {
+ users => {
+ 'root@pam' => {
+ enable => 0,
+ expire => 0,
+ email => undef,
+ },
+ 'test@pam' => {
+ enable => 0,
+ expire => 0,
+ email => undef,
+ tokens => {
+ 'test' => {
+ expire => 0,
+ privsep => 0,
+ },
+ },
+ },
+ },
+ roles => default_roles_with([{ id => 'testrole' }]),
+ groups => default_groups_with([$default_cfg->{test_group_empty}]),
+ pools => default_pools_with([$default_cfg->{test_pool_empty}]),
+ },
+ raw => "".
+ 'user:root@pam'."\n".
+ 'user:test@pam'."\n".
+ 'token:test@pam!test'."\n\n".
+ 'group:testgroup'."\n\n".
+ 'pool:testpool'."\n\n".
+ 'role:testrole'."\n\n".
+ 'acl::/:',
+ expected_raw => "".
+ 'user:root@pam:0:0::::::'."\n".
+ 'user:test@pam:0:0::::::'."\n".
+ 'token:test@pam!test:0:0::'."\n\n".
+ 'group:testgroup:::'."\n\n".
+ 'pool:testpool::::'."\n\n".
+ 'role:testrole::'."\n\n",
+ },
+];
+
+
+my $number_of_tests_run = 0;
+foreach my $t (@$tests) {
+ my $expected_config = $t->{expected_config} // $t->{config};
+ my $expected_raw = $t->{expected_raw} // $t->{raw};
+ if (defined($t->{raw})) {
+ my $parsed = PVE::AccessControl::parse_user_config($t->{name}, $t->{raw});
+ if (defined($expected_config)) {
+ is_deeply($parsed, $expected_config, "$t->{name}_parse");
+ $number_of_tests_run++;
+ }
+ if (defined($t->{expected_raw}) && !defined($t->{config})) {
+ is(PVE::AccessControl::write_user_config($t->{name}, $parsed), $t->{expected_raw}, "$t->{name}_rewrite");
+ $number_of_tests_run++;
+ }
+
+ }
+ if (defined($t->{config})) {
+ my $written = PVE::AccessControl::write_user_config($t->{name}, $t->{config});
+ if (defined($expected_raw)) {
+ is($written, $expected_raw, "$t->{name}_write");
+ $number_of_tests_run++;
+ }
+ if (defined($t->{expected_config}) && !defined($t->{raw})) {
+ is_deeply(PVE::AccessControl::parse_user_config($t->{name}, $t->{written}), $t->{expected_config}, "$t->{name}_reparse");
+ $number_of_tests_run++;
+ }
+ }
+};
+
+done_testing( $number_of_tests_run);
--- /dev/null
+#!/usr/bin/perl -w
+
+use strict;
+use PVE::Tools;
+use PVE::AccessControl;
+use PVE::RPCEnvironment;
+use Getopt::Long;
+
+my $rpcenv = PVE::RPCEnvironment->init('cli');
+
+my $cfgfn = "test1.cfg";
+$rpcenv->init_request(userconfig => $cfgfn);
+
+sub check_roles {
+ my ($user, $path, $expected_result) = @_;
+
+ my $roles = PVE::AccessControl::roles($rpcenv->{user_cfg}, $user, $path);
+ my $res = join(',', sort keys %$roles);
+
+ die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
+ if $res ne $expected_result;
+
+ print "ROLES:$path:$user:$res\n";
+}
+
+sub check_permission {
+ my ($user, $path, $expected_result) = @_;
+
+ my $perm = $rpcenv->permissions($user, $path);
+ my $res = join(',', sort keys %$perm);
+
+ die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
+ 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;
+
+ print "PERM:$path:$user:$res\n";
+}
+
+check_roles('max@pve', '/', '');
+check_roles('max@pve', '/vms', 'vm_admin');
+
+#user permissions overrides group permissions
+check_roles('max@pve', '/vms/100', 'customer');
+check_roles('max@pve', '/vms/101', 'vm_admin');
+
+check_permission('max@pve', '/', '');
+check_permission('max@pve', '/vms', 'Permissions.Modify,VM.Allocate,VM.Audit,VM.Console');
+check_permission('max@pve', '/vms/100', 'VM.Audit,VM.PowerMgmt');
+
+check_permission('alex@pve', '/vms', '');
+check_permission('alex@pve', '/vms/100', 'VM.Audit,VM.PowerMgmt');
+
+
+check_roles('max@pve', '/vms/200', 'storage_manager');
+check_roles('joe@pve', '/vms/200', 'vm_admin');
+check_roles('sue@pve', '/vms/200', 'NoAccess');
+
+print "all tests passed\n";
+
+exit (0);
--- /dev/null
+#!/usr/bin/perl -w
+
+use strict;
+use PVE::Tools;
+use PVE::AccessControl;
+use PVE::RPCEnvironment;
+use Getopt::Long;
+
+my $rpcenv = PVE::RPCEnvironment->init('cli');
+
+my $cfgfn = "test2.cfg";
+$rpcenv->init_request(userconfig => $cfgfn);
+
+sub check_roles {
+ my ($user, $path, $expected_result) = @_;
+
+ my $roles = PVE::AccessControl::roles($rpcenv->{user_cfg}, $user, $path);
+ my $res = join(',', sort keys %$roles);
+
+ die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
+ if $res ne $expected_result;
+
+ print "ROLES:$path:$user:$res\n";
+}
+
+# inherit multiple group permissions
+
+check_roles('User1@pve', '/', '');
+check_roles('User2@pve', '/', '');
+
+check_roles('User1@pve', '/vms', 'Role1,Role2');
+check_roles('User2@pve', '/vms', '');
+
+check_roles('User1@pve', '/vms/100', 'Role1,Role2');
+check_roles('User2@pve', '/vms', '');
+
+print "all tests passed\n";
+
+exit (0);
--- /dev/null
+#!/usr/bin/perl -w
+
+use strict;
+use PVE::Tools;
+use PVE::AccessControl;
+use PVE::RPCEnvironment;
+use Getopt::Long;
+
+my $rpcenv = PVE::RPCEnvironment->init('cli');
+
+my $cfgfn = "test3.cfg";
+$rpcenv->init_request(userconfig => $cfgfn);
+
+sub check_roles {
+ my ($user, $path, $expected_result) = @_;
+
+ my $roles = PVE::AccessControl::roles($rpcenv->{user_cfg}, $user, $path);
+ my $res = join(',', sort keys %$roles);
+
+ die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
+ if $res ne $expected_result;
+
+ print "ROLES:$path:$user:$res\n";
+}
+
+check_roles('User1@pve', '', '');
+check_roles('User2@pve', '', '');
+
+check_roles('User1@pve', '/vms/300', 'Role1');
+check_roles('User1@pve', '/vms/200', 'Role2');
+
+print "all tests passed\n";
+
+exit (0);
--- /dev/null
+#!/usr/bin/perl -w
+
+use strict;
+use PVE::Tools;
+use PVE::AccessControl;
+use PVE::RPCEnvironment;
+use Getopt::Long;
+
+my $rpcenv = PVE::RPCEnvironment->init('cli');
+
+my $cfgfn = "test4.cfg";
+$rpcenv->init_request(userconfig => $cfgfn);
+
+sub check_roles {
+ my ($user, $path, $expected_result) = @_;
+
+ my $roles = PVE::AccessControl::roles($rpcenv->{user_cfg}, $user, $path);
+ my $res = join(',', sort keys %$roles);
+
+ die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
+ 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);
--- /dev/null
+#!/usr/bin/perl -w
+
+use strict;
+use PVE::Tools;
+use PVE::AccessControl;
+use PVE::RPCEnvironment;
+use Getopt::Long;
+
+my $rpcenv = PVE::RPCEnvironment->init('cli');
+
+my $cfgfn = "test5.cfg";
+$rpcenv->init_request(userconfig => $cfgfn);
+
+sub check_roles {
+ my ($user, $path, $expected_result) = @_;
+
+ my $roles = PVE::AccessControl::roles($rpcenv->{user_cfg}, $user, $path);
+ my $res = join(',', sort keys %$roles);
+
+ die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
+ 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');
+check_roles('User1@pve', '/vms/100/a/b', 'Role2');
+check_roles('User1@pve', '/vms/100/a/b/c', 'Role2');
+check_roles('User1@pve', '/vms/200', 'Role1');
+
+check_roles('User2@pve', '/kvm', 'Role2');
+check_roles('User2@pve', '/kvm/vms', 'Role1');
+check_roles('User2@pve', '/kvm/vms/100', '');
+check_roles('User2@pve', '/kvm/vms/100/a', 'Role3');
+check_roles('User2@pve', '/kvm/vms/100/a/b', '');
+
+print "all tests passed\n";
+
+exit (0);
--- /dev/null
+#!/usr/bin/perl -w
+
+use strict;
+use PVE::Tools;
+use PVE::AccessControl;
+use PVE::RPCEnvironment;
+use Getopt::Long;
+
+my $rpcenv = PVE::RPCEnvironment->init('cli');
+
+my $cfgfn = "test6.cfg";
+$rpcenv->init_request(userconfig => $cfgfn);
+
+sub check_roles {
+ my ($user, $path, $expected_result) = @_;
+
+ my $roles = PVE::AccessControl::roles($rpcenv->{user_cfg}, $user, $path);
+ my $res = join(',', sort keys %$roles);
+
+ die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
+ if $res ne $expected_result;
+
+ print "ROLES:$path:$user:$res\n";
+}
+
+sub check_permissions {
+ my ($user, $path, $expected_result) = @_;
+
+ my $perm = $rpcenv->permissions($user, $path);
+ my $res = join(',', sort keys %$perm);
+
+ die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
+ 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;
+
+ print "PERM:$path:$user:$res\n";
+}
+
+check_roles('User1@pve', '', '');
+check_roles('User2@pve', '', '');
+check_roles('User3@pve', '', '');
+check_roles('User4@pve', '', '');
+
+check_roles('User1@pve', '/vms', 'RoleTEST1');
+check_roles('User2@pve', '/vms', 'RoleTEST1');
+check_roles('User3@pve', '/vms', 'NoAccess');
+check_roles('User4@pve', '/vms', '');
+
+check_roles('User1@pve', '/vms/100', 'RoleTEST1');
+check_roles('User2@pve', '/vms/100', 'RoleTEST1');
+check_roles('User3@pve', '/vms/100', 'NoAccess');
+check_roles('User4@pve', '/vms/100', '');
+
+check_roles('User1@pve', '/vms/300', 'RoleTEST1');
+check_roles('User2@pve', '/vms/300', 'RoleTEST1');
+check_roles('User3@pve', '/vms/300', 'NoAccess');
+check_roles('User4@pve', '/vms/300', 'RoleTEST1');
+
+check_permissions('User1@pve', '/vms/500', 'VM.Console,VM.PowerMgmt');
+check_permissions('User2@pve', '/vms/500', 'VM.Console,VM.PowerMgmt');
+# without pool
+check_roles('User3@pve', '/vms/500', 'NoAccess');
+# with pool
+check_permissions('User3@pve', '/vms/500', '');
+# without pool
+check_roles('User4@pve', '/vms/500', '');
+# with pool
+check_permissions('User4@pve', '/vms/500', '');
+
+
+check_permissions('User1@pve', '/vms/600', 'VM.Console');
+check_permissions('User2@pve', '/vms/600', 'VM.Console');
+check_permissions('User3@pve', '/vms/600', '');
+check_permissions('User4@pve', '/vms/600', 'VM.Console');
+
+check_permissions('User1@pve', '/storage/store1', 'VM.Console,VM.PowerMgmt');
+check_permissions('User2@pve', '/storage/store1', 'VM.PowerMgmt');
+check_permissions('User3@pve', '/storage/store1', 'VM.PowerMgmt');
+check_permissions('User4@pve', '/storage/store1', 'VM.Console');
+
+check_permissions('User1@pve', '/storage/store2', 'VM.PowerMgmt');
+check_permissions('User2@pve', '/storage/store2', 'VM.PowerMgmt');
+check_permissions('User3@pve', '/storage/store2', 'VM.PowerMgmt');
+check_permissions('User4@pve', '/storage/store2', '');
+
+print "all tests passed\n";
+
+exit (0);
--- /dev/null
+#!/usr/bin/perl -w
+
+use strict;
+use PVE::Tools;
+use PVE::AccessControl;
+use PVE::RPCEnvironment;
+use Getopt::Long;
+
+my $rpcenv = PVE::RPCEnvironment->init('cli');
+
+my $cfgfn = "test7.cfg";
+$rpcenv->init_request(userconfig => $cfgfn);
+
+sub check_roles {
+ my ($user, $path, $expected_result) = @_;
+
+ my $roles = PVE::AccessControl::roles($rpcenv->{user_cfg}, $user, $path);
+ my $res = join(',', sort keys %$roles);
+
+ die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
+ if $res ne $expected_result;
+
+ print "ROLES:$path:$user:$res\n";
+}
+
+sub check_permissions {
+ my ($user, $path, $expected_result) = @_;
+
+ my $perm = $rpcenv->permissions($user, $path);
+ my $res = join(',', sort keys %$perm);
+
+ die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
+ 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;
+
+ print "PERM:$path:$user:$res\n";
+}
+
+check_roles('User1@pve', '/vms', 'Role1');
+check_roles('User1@pve', '/vms/200', 'Role1');
+
+# no pool
+check_roles('User1@pve', '/vms/100', 'Role1');
+# with pool
+check_permissions('User1@pve', '/vms/100', '');
+
+print "all tests passed\n";
+
+exit (0);
--- /dev/null
+#!/usr/bin/perl -w
+
+use strict;
+use PVE::Tools;
+use PVE::AccessControl;
+use PVE::RPCEnvironment;
+
+my $rpcenv = PVE::RPCEnvironment->init('cli');
+
+my $cfgfn = "test8.cfg";
+$rpcenv->init_request(userconfig => $cfgfn);
+
+sub check_roles {
+ my ($user, $path, $expected_result) = @_;
+
+ my $roles = PVE::AccessControl::roles($rpcenv->{user_cfg}, $user, $path);
+ my $res = join(',', sort keys %$roles);
+
+ die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
+ if $res ne $expected_result;
+
+ print "ROLES:$path:$user:$res\n";
+}
+
+sub check_permission {
+ my ($user, $path, $expected_result) = @_;
+
+ my $perm = $rpcenv->permissions($user, $path);
+ my $res = join(',', sort keys %$perm);
+
+ die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
+ 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;
+
+ print "PERM:$path:$user:$res\n";
+}
+
+check_roles('max@pve', '/', '');
+check_roles('max@pve', '/vms', 'vm_admin');
+
+#user permissions overrides group permissions
+check_roles('max@pve', '/vms/100', 'customer');
+check_roles('max@pve', '/vms/101', 'vm_admin');
+
+check_permission('max@pve', '/', '');
+check_permission('max@pve', '/vms', 'Permissions.Modify,VM.Allocate,VM.Audit,VM.Console');
+check_permission('max@pve', '/vms/100', 'VM.Audit,VM.PowerMgmt');
+
+check_permission('alex@pve', '/vms', '');
+check_permission('alex@pve', '/vms/100', 'VM.Audit,VM.PowerMgmt');
+
+check_roles('max@pve', '/vms/200', 'storage_manager');
+check_roles('joe@pve', '/vms/200', 'vm_admin');
+check_roles('sue@pve', '/vms/200', 'NoAccess');
+
+check_roles('carol@pam', '/vms/200', 'NoAccess');
+check_roles('carol@pam!token', '/vms/200', 'NoAccess');
+check_roles('max@pve!token', '/vms/200', 'storage_manager');
+check_roles('max@pve!token2', '/vms/200', 'customer');
+
+print "all tests passed\n";
+
+exit (0);
+
--- /dev/null
+user:joe@pve:1:
+user:max@pve:1:
+user:alex@pve:1:
+user:sue@pve:1:
+user:carol@pam:1:
+
+group:testgroup1:joe@pve,max@pve,sue@pve:
+group:testgroup2:alex@pve,carol@pam,sue@pve:
+group:testgroup3:max@pve:
+
+role:storage_manager:Datastore.AllocateSpace,Datastore.Audit:
+role:customer:VM.Audit,VM.PowerMgmt:
+role:vm_admin:VM.Audit,VM.Allocate,Permissions.Modify,VM.Console:
+
+acl:1:/vms:@testgroup1:vm_admin:
+acl:1:/vms/100/:alex@pve,max@pve:customer:
+acl:1:/storage/nfs1:@testgroup2:storage_manager:
+acl:1:/users:max@pve:Administrator:
+
+acl:1:/vms/200:@testgroup3:storage_manager:
+acl:1:/vms/200:@testgroup2:NoAccess:
+
--- /dev/null
+user:User1@pve:1:
+user:User2@pve:1:
+
+group:GroupA:User1@pve:
+group:GroupB:User1@pve:
+
+role:Role1:VM.PowerMgmt:
+role:Role2:VM.Console:
+
+acl:1:/vms:@GroupA:Role1:
+acl:1:/vms:@GroupB:Role2:
--- /dev/null
+user:User1@pve:1:
+user:User2@pve:1:
+
+group:GroupA:User1@pve:
+group:GroupB:User1@pve:
+
+role:Role1:VM.PowerMgmt:
+role:Role2:VM.Console:
+
+acl:1:/vms:@GroupA:Role1:
+acl:1:/vms/200:@GroupB:Role2:
--- /dev/null
+user:User1@pve:1:
+user:User2@pve:1:
+
+group:GroupA:User1@pve,User2@pve:
+group:GroupB:User1@pve,User2@pve:
+
+role:Role1:VM.PowerMgmt:
+role:Role2:VM.Console:
+
+acl:1:/vms:@GroupA:Role1:
+acl:1:/vms:User2@pve:NoAccess:
--- /dev/null
+user:User1@pve:1:
+user:User2@pve:1:
+
+group:GroupA:User1@pve,User2@pve:
+group:GroupB:User1@pve,User2@pve:
+
+role:Role1:VM.PowerMgmt:
+role:Role2:VM.Console:
+role:Role3:VM.Console:
+
+acl:1:/vms:User1@pve:Role1:
+acl:1:/vms/100/a/b:User1@pve:Role2:
+
+acl:0:/kvm:User2@pve:Role2:
+acl:0:/kvm/vms:User2@pve:Role1:
+acl:0:/kvm/vms/100/a:User2@pve:Role3:
--- /dev/null
+user:User1@pve:1:
+user:User2@pve:1:
+user:User3@pve:1:
+user:User4@pve:1:
+
+group:DEVEL:User1@pve,User2@pve,User3@pve:
+group:MARKETING:User1@pve,User4@pve:
+
+role:RoleDEVEL:VM.PowerMgmt:
+role:RoleMARKETING:VM.Console:
+role:RoleTEST1:VM.Console:
+
+acl:1:/pool/devel:@DEVEL:RoleDEVEL:
+acl:1:/pool/marketing:@MARKETING:RoleMARKETING:
+
+acl:1:/vms:@DEVEL:RoleTEST1:
+acl:1:/vms:User3@pve:NoAccess:
+acl:1:/vms/300:@MARKETING:RoleTEST1:
+
+pool:devel:MITS development:500,501,502:store1 store2:
+pool:marketing:MITS marketing:600:store1:
--- /dev/null
+user:User1@pve:1:
+user:User2@pve:1:
+
+group:GroupA:User1@pve,User2@pve:
+group:GroupB:User1@pve,User2@pve:
+
+role:Role1:VM.PowerMgmt:
+role:Role2:VM.Console:
+role:Role3:VM.Console:
+
+acl:1:/pool/devel:User1@pve:NoAccess:
+
+acl:1:/vms:User1@pve:Role1:
+
+pool:devel:Development:100:store1:
--- /dev/null
+user:joe@pve:1:
+user:max@pve:1:
+token:max@pve!token::0:
+token:max@pve!token2::1:
+user:alex@pve:1:
+user:sue@pve:1:
+user:carol@pam:1:
+token:carol@pam!token:
+
+group:testgroup1:joe@pve,max@pve,sue@pve:
+group:testgroup2:alex@pve,carol@pam,sue@pve:
+group:testgroup3:max@pve:
+
+role:storage_manager:Datastore.AllocateSpace,Datastore.Audit:
+role:customer:VM.Audit,VM.PowerMgmt:
+role:vm_admin:VM.Audit,VM.Allocate,Permissions.Modify,VM.Console:
+
+acl:1:/vms:@testgroup1:vm_admin:
+acl:1:/vms/100/:alex@pve,max@pve:customer:
+acl:1:/storage/nfs1:@testgroup2:storage_manager:
+acl:1:/users:max@pve:Administrator:
+
+acl:1:/vms/200:@testgroup3:storage_manager:
+acl:1:/vms/200:@testgroup2:NoAccess:
+
+acl:1:/vms/200:carol@pam!token:vm_admin
+acl:1:/vms/200:max@pve!token:storage_manager
+acl:1:/vms/200:max@pve!token2:customer
+++ /dev/null
-
-all:
-
-.PHONY: check
-check:
- perl -I.. parser_writer.pl
- perl -I.. perm-test1.pl
- perl -I.. perm-test2.pl
- perl -I.. perm-test3.pl
- perl -I.. perm-test4.pl
- perl -I.. perm-test5.pl
- perl -I.. perm-test6.pl
- perl -I.. perm-test7.pl
- perl -I.. perm-test8.pl
+++ /dev/null
-#!/usr/bin/perl -w
-
-use strict;
-use PVE::PTY;
-use PVE::AccessControl;
-
-my $username = shift;
-die "Username missing" if !$username;
-
-my $password = PVE::PTY::read_password('password: ');
-PVE::AccessControl::authenticate_user($username,$password);
-
-print "Authentication Successful!!\n";
-
-exit (0);
+++ /dev/null
-#!/usr/bin/perl -w
-
-use strict;
-use PVE::RPCEnvironment;
-use Getopt::Long;
-use Data::Dumper;
-
-# example:
-# dump-perm.pl -f myuser.cfg root /
-
-my $opt_file;
-if (!GetOptions ("file=s" => \$opt_file)) {
- exit (-1);
-}
-
-my $username = shift;
-my $path = shift;
-
-if (!($username && $path)) {
- print "usage: $0 <username> <path>\n";
- exit (-1);
-}
-
-my $cfg;
-
-my $rpcenv = PVE::RPCEnvironment->init('cli');
-if ($opt_file) {
- $rpcenv->init_request(userconfig => $opt_file);
-} else {
- $rpcenv->init_request();
-}
-
-my $perm = $rpcenv->permissions($username, $path);
-
-print "permission for user '$username' on '$path':\n";
-print join(',', keys %$perm) . "\n";
-
-exit (0);
+++ /dev/null
-#!/usr/bin/perl -w
-
-use strict;
-use PVE::AccessControl;
-use Data::Dumper;
-
-my $cfg;
-
-$cfg = PVE::AccessControl::load_user_config();
-
-print Dumper($cfg) . "\n";
-
-exit (0);
+++ /dev/null
-#!/usr/bin/perl -w
-
-use strict;
-
-use Test::More;
-use PVE::AccessControl;
-
-use Storable qw(dclone);
-
-PVE::AccessControl::create_roles();
-my $default_user_cfg = {};
-PVE::AccessControl::userconfig_force_defaults($default_user_cfg);
-
-my $add_default_user_properties = sub {
- my ($user) = @_;
-
- $user->{enable} = 1 if !defined($user->{enable});
- $user->{expire} = 0 if !defined($user->{expire});
- $user->{email} = undef if !defined($user->{email});
-
- return $user;
-};
-
-sub default_roles {
- my $roles = dclone($default_user_cfg->{roles});
- return $roles;
-}
-
-sub default_roles_with {
- my ($extra_roles) = @_;
-
- my $roles = default_roles();
-
- foreach my $r (@$extra_roles) {
- my $role = dclone($r);
- my $roleid = delete $role->{id};
- $roles->{$roleid} = $role;
- }
-
- return $roles;
-}
-
-sub default_users {
- my $users = dclone($default_user_cfg->{users});
- return { map { $_ => $add_default_user_properties->($users->{$_}); } keys %$users};
-}
-
-sub default_users_with {
- my ($extra_users) = @_;
-
- my $users = default_users();
-
- foreach my $u (@$extra_users) {
- my $user = dclone($u);
- my $userid = delete $user->{id};
- $users->{$userid} = $add_default_user_properties->($user);
- }
-
- return $users;
-}
-
-sub default_groups {
- return {};
-}
-
-sub default_groups_with {
- my ($extra_groups) = @_;
-
- my $groups = default_groups();
-
- foreach my $g (@$extra_groups) {
- my $group = dclone($g);
- my $groupid = delete $group->{id};
- $groups->{$groupid} = $group;
- }
-
- return $groups;
-}
-
-sub default_pools {
- return {};
-}
-
-sub default_pools_with {
- my ($extra_pools) = @_;
-
- my $pools = default_pools();
-
- foreach my $p (@$extra_pools) {
- my $pool = dclone($p);
- my $poolid = delete $pool->{id};
- $pools->{$poolid} = $pool;
- }
-
- return $pools;
-}
-
-sub default_pool_vms_with {
- my ($extra_pools) = @_;
-
- my $vms = {};
- foreach my $pool (@$extra_pools) {
- foreach my $vmid (keys %{$pool->{vms}}) {
- $vms->{$vmid} = $pool->{id};
- }
- }
- return $vms;
-}
-
-sub default_acls {
- return {};
-}
-
-# note: does not support merging paths!
-sub default_acls_with {
- my ($extra_acls) = @_;
-
- my $acls = default_acls();
-
- foreach my $a (@$extra_acls) {
- my $acl = dclone($a);
- my $path = delete $acl->{path};
- $acls->{$path} = $acl;
- }
-
- return $acls;
-}
-
-my $default_cfg = {
- test_pam => {
- 'id' => 'test@pam',
- 'enable' => 1,
- 'expire' => 0,
- 'email' => undef,
- },
- test2_pam => {
- 'id' => 'test2@pam',
- 'enable' => 1,
- 'expire' => 0,
- 'email' => undef,
- },
- test_pam_with_group => {
- 'id' => 'test@pam',
- 'enable' => 1,
- 'expire' => 0,
- 'email' => undef,
- 'groups' => { 'testgroup' => 1 },
- },
- test2_pam_with_group => {
- 'id' => 'test2@pam',
- 'enable' => 1,
- 'expire' => 0,
- 'email' => undef,
- 'groups' => { 'testgroup' => 1 },
- },
- test3_pam => {
- 'id' => 'test3@pam',
- 'enable' => 1,
- 'expire' => 0,
- 'email' => undef,
- 'groups' => { 'another' => 1 },
- },
- test_pam_with_token => {
- 'id' => 'test@pam',
- 'enable' => 1,
- 'expire' => 0,
- 'email' => undef,
- 'tokens' => {
- 'full' => {
- 'privsep' => 0,
- 'expire' => 0,
- },
- },
- },
- test_pam2_with_token => {
- 'id' => 'test2@pam',
- 'enable' => 1,
- 'expire' => 0,
- 'email' => undef,
- 'tokens' => {
- 'full' => {
- 'privsep' => 0,
- 'expire' => 0,
- },
- 'privsep' => {
- 'privsep' => 1,
- 'expire' => 0,
- },
- 'expired' => {
- 'privsep' => 0,
- 'expire' => 1,
- },
- },
- },
- test_group_empty => {
- 'id' => 'testgroup',
- users => {},
- },
- test_group_single_member => {
- 'id' => 'testgroup',
- 'users' => {
- 'test@pam' => 1,
- },
- },
- test_group_members => {
- 'id' => 'testgroup',
- 'users' => {
- 'test@pam' => 1,
- 'test2@pam' => 1,
- },
- },
- test_group_second => {
- 'id' => 'another',
- users => {
- 'test3@pam' => 1,
- },
- },
- test_role_single_priv => {
- 'id' => 'testrolesingle',
- 'VM.Allocate' => 1,
- },
- test_role_privs => {
- 'id' => 'testrole',
- 'VM.Allocate' => 1,
- 'Datastore.Audit' => 1,
- },
- test_pool_empty => {
- 'id' => 'testpool',
- vms => {},
- storage => {},
- },
- test_pool_members => {
- 'id' => 'testpool',
- vms => { 123 => 1, 1234 => 1},
- storage => { 'local' => 1, 'local-zfs' => 1},
- },
- test_pool_duplicate_vms => {
- 'id' => 'test_duplicate_vms',
- vms => {},
- storage => {},
- },
- test_pool_duplicate_storages => {
- 'id' => 'test_duplicate_storages',
- vms => {},
- storage => { 'local' => 1, 'local-zfs' => 1},
- },
- acl_simple_user => {
- 'path' => '/',
- users => {
- 'test@pam' => {
- 'PVEVMAdmin' => 1,
- },
- },
- },
- acl_complex_users => {
- 'path' => '/storage',
- users => {
- 'test2@pam' => {
- 'PVEDatastoreUser' => 1,
- },
- 'test@pam' => {
- 'PVEDatastoreAdmin' => 1,
- },
- },
- },
- acl_complex_missing_user => {
- 'path' => '/storage',
- users => {
- 'test2@pam' => {
- 'PVEDatastoreUser' => 1,
- },
- 'test@pam' => {
- 'PVEDatastoreAdmin' => 1,
- },
- },
- },
- acl_simple_token => {
- 'path' => '/',
- tokens => {
- 'test@pam!full' => {
- 'PVEVMAdmin' => 1,
- },
- },
- },
- acl_complex_tokens => {
- 'path' => '/storage',
- tokens => {
- 'test2@pam!privsep' => {
- 'PVEDatastoreUser' => 1,
- },
- 'test2@pam!expired' => {
- 'PVEDatastoreAdmin' => 1,
- },
- 'test@pam!full' => {
- 'PVEDatastoreAdmin' => 1,
- },
- },
- },
- acl_complex_missing_token => {
- 'path' => '/storage',
- tokens => {
- 'test2@pam!expired' => {
- 'PVEDatastoreAdmin' => 1,
- },
- 'test2@pam!privsep' => {
- 'PVEDatastoreUser' => 1,
- },
- },
- },
- acl_simple_group => {
- 'path' => '/',
- groups => {
- 'testgroup' => {
- 'PVEVMAdmin' => 1,
- },
- },
- },
- acl_complex_groups => {
- 'path' => '/storage',
- groups => {
- 'testgroup' => {
- 'PVEDatastoreAdmin' => 1,
- },
- 'another' => {
- 'PVEDatastoreUser' => 1,
- },
- },
- },
- acl_simple_group_noprop => {
- 'path' => '/',
- groups => {
- 'testgroup' => {
- 'PVEVMAdmin' => 0,
- },
- },
- },
- acl_complex_groups_noprop => {
- 'path' => '/storage',
- groups => {
- 'testgroup' => {
- 'PVEDatastoreAdmin' => 0,
- },
- 'another' => {
- 'PVEDatastoreUser' => 0,
- },
- },
- },
- acl_complex_missing_group => {
- 'path' => '/storage',
- groups => {
- 'testgroup' => {
- 'PVEDatastoreAdmin' => 1,
- },
- 'another' => {
- 'PVEDatastoreUser' => 1,
- },
- },
- },
- acl_missing_role => {
- 'path' => '/storage',
- users => {
- 'test@pam' => {
- 'MissingRole' => 1,
- },
- },
- },
-};
-
-$default_cfg->{'acl_complex_mixed_root'} = {
- 'path' => '/',
- users => $default_cfg->{'acl_simple_user'}->{users},
- groups => $default_cfg->{'acl_simple_group'}->{groups},
-};
-
-$default_cfg->{'acl_complex_mixed_storage'} = {
- 'path' => '/storage',
- users => $default_cfg->{'acl_complex_users'}->{users},
- groups => $default_cfg->{'acl_complex_groups'}->{groups},
-};
-
-$default_cfg->{'acl_complex_mixed_root_noprop'} = {
- 'path' => '/',
- users => $default_cfg->{'acl_simple_user'}->{users},
- groups => $default_cfg->{'acl_simple_group_noprop'}->{groups},
-};
-
-$default_cfg->{'acl_complex_mixed_storage_noprop'} = {
- 'path' => '/storage',
- users => $default_cfg->{'acl_complex_users'}->{users},
- groups => $default_cfg->{'acl_complex_groups_noprop'}->{groups},
-};
-
-my $default_raw = {
- users => {
- 'root@pam' => 'user:root@pam:1:0::::::',
- 'test_pam' => 'user:test@pam:1:0::::::',
- 'test2_pam' => 'user:test2@pam:1:0::::::',
- 'test3_pam' => 'user:test3@pam:1:0::::::',
- },
- groups => {
- 'test_group_empty' => 'group:testgroup:::',
- 'test_group_single_member' => 'group:testgroup:test@pam::',
- 'test_group_members' => 'group:testgroup:test2@pam,test@pam::',
- 'test_group_members_out_of_order' => 'group:testgroup:test@pam,test2@pam::',
- 'test_group_second' => 'group:another:test3@pam::',
- },
- tokens => {
- 'test_token_simple' => 'token:test@pam!full:0:0::',
- 'test_token_multi_full' => 'token:test2@pam!full:0:0::',
- 'test_token_multi_privsep' => 'token:test2@pam!privsep:0:1::',
- 'test_token_multi_expired' => 'token:test2@pam!expired:1:0::',
- },
- roles => {
- 'test_role_single_priv' => 'role:testrolesingle:VM.Allocate:',
- 'test_role_privs' => 'role:testrole:Datastore.Audit,VM.Allocate:',
- 'test_role_privs_out_of_order' => 'role:testrole:VM.Allocate,Datastore.Audit:',
- 'test_role_privs_duplicate' => 'role:testrole:VM.Allocate,Datastore.Audit,VM.Allocate:',
- 'test_role_privs_invalid' => 'role:testrole:VM.Invalid,Datastore.Audit,VM.Allocate:',
- },
- pools => {
- 'test_pool_empty' => 'pool:testpool::::',
- 'test_pool_invalid' => 'pool:testpool::non-numeric:inval!d:',
- 'test_pool_members' => 'pool:testpool::123,1234:local,local-zfs:',
- 'test_pool_duplicate_vms' => 'pool:test_duplicate_vms::123,1234::',
- 'test_pool_duplicate_vms_expected' => 'pool:test_duplicate_vms::::',
- 'test_pool_duplicate_storages' => 'pool:test_duplicate_storages:::local,local-zfs:',
- },
- acl => {
- 'acl_simple_user' => 'acl:1:/:test@pam:PVEVMAdmin:',
- 'acl_complex_users_1' => 'acl:1:/storage:test@pam:PVEDatastoreAdmin:',
- 'acl_complex_users_2' => 'acl:1:/storage:test2@pam:PVEDatastoreUser:',
- 'acl_simple_token' => 'acl:1:/:test@pam!full:PVEVMAdmin:',
- 'acl_complex_tokens_1' => 'acl:1:/storage:test2@pam!expired,test@pam!full:PVEDatastoreAdmin:',
- 'acl_complex_tokens_2' => 'acl:1:/storage:test2@pam!privsep:PVEDatastoreUser:',
- 'acl_complex_tokens_1_missing' => 'acl:1:/storage:test2@pam!expired:PVEDatastoreAdmin:',
- 'acl_simple_group' => 'acl:1:/:@testgroup:PVEVMAdmin:',
- 'acl_complex_groups_1' => 'acl:1:/storage:@testgroup:PVEDatastoreAdmin:',
- 'acl_complex_groups_2' => 'acl:1:/storage:@another:PVEDatastoreUser:',
- 'acl_simple_group_noprop' => 'acl:0:/:@testgroup:PVEVMAdmin:',
- 'acl_complex_groups_1_noprop' => 'acl:0:/storage:@testgroup:PVEDatastoreAdmin:',
- 'acl_complex_groups_2_noprop' => 'acl:0:/storage:@another:PVEDatastoreUser:',
- 'acl_complex_mixed_1' => 'acl:1:/:@testgroup,test@pam:PVEVMAdmin:',
- 'acl_complex_mixed_2' => 'acl:1:/storage:@testgroup,test@pam:PVEDatastoreAdmin:',
- 'acl_complex_mixed_3' => 'acl:1:/storage:@another,test2@pam:PVEDatastoreUser:',
- 'acl_missing_role' => 'acl:1:/storage:test@pam:MissingRole:',
- },
-};
-
-my $tests = [
- {
- name => "empty_config",
- config => {},
- expected_config => {
- users => { 'root@pam' => { enable => 1 } },
- roles => default_roles(),
- },
- raw => "",
- expected_raw => "\n\n\n\n",
- },
- {
- name => "default_config",
- config => {
- users => default_users(),
- roles => default_roles(),
- },
- raw => $default_raw->{users}->{'root@pam'}."\n\n\n\n\n",
- },
- {
- name => "group_empty",
- config => {
- users => default_users(),
- roles => default_roles(),
- groups => default_groups_with([$default_cfg->{'test_group_empty'}]),
- },
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n\n".
- $default_raw->{groups}->{'test_group_empty'}."\n\n".
- "\n\n",
- },
- {
- name => "group_inexisting_member",
- config => {
- users => default_users(),
- roles => default_roles(),
- groups => default_groups_with([$default_cfg->{'test_group_empty'}]),
- },
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n\n".
- "group:testgroup:does_not_exist::".
- "\n\n\n\n",
- expected_raw => "".
- $default_raw->{users}->{'root@pam'}."\n\n".
- $default_raw->{groups}->{'test_group_empty'}."\n\n".
- "\n\n",
- },
- {
- name => "group_invalid_member",
- expected_config => {
- users => default_users(),
- roles => default_roles(),
- },
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n\n".
- 'group:inval!d:root@pam:'.
- "\n\n",
- },
- {
- name => "group_with_one_member",
- config => {
- users => default_users_with([$default_cfg->{test_pam_with_group}]),
- roles => default_roles(),
- groups => default_groups_with([$default_cfg->{'test_group_single_member'}]),
- },
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n".
- $default_raw->{users}->{'test_pam'}."\n\n".
- $default_raw->{groups}->{'test_group_single_member'}."\n\n".
- "\n\n",
- },
- {
- name => "group_with_members",
- config => {
- users => default_users_with([$default_cfg->{test_pam_with_group}, $default_cfg->{test2_pam_with_group}]),
- roles => default_roles(),
- groups => default_groups_with([$default_cfg->{'test_group_members'}]),
- },
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n".
- $default_raw->{users}->{'test2_pam'}."\n".
- $default_raw->{users}->{'test_pam'}."\n\n".
- $default_raw->{groups}->{'test_group_members'}."\n\n".
- "\n\n",
- },
- {
- name => "token_simple",
- config => {
- users => default_users_with([$default_cfg->{test_pam_with_token}]),
- roles => default_roles(),
- },
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n".
- $default_raw->{users}->{'test_pam'}."\n".
- $default_raw->{tokens}->{'test_token_simple'}."\n\n\n\n\n",
- },
- {
- name => "token_multi",
- config => {
- users => default_users_with([$default_cfg->{test_pam_with_token}, $default_cfg->{test_pam2_with_token}]),
- roles => default_roles(),
- },
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n".
- $default_raw->{users}->{'test2_pam'}."\n".
- $default_raw->{tokens}->{'test_token_multi_expired'}."\n".
- $default_raw->{tokens}->{'test_token_multi_full'}."\n".
- $default_raw->{tokens}->{'test_token_multi_privsep'}."\n".
- $default_raw->{users}->{'test_pam'}."\n".
- $default_raw->{tokens}->{'test_token_simple'}."\n".
- "\n\n\n\n",
- },
- {
- name => "custom_role_with_single_priv",
- config => {
- users => default_users(),
- roles => default_roles_with([$default_cfg->{test_role_single_priv}]),
- },
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n\n\n\n".
- $default_raw->{roles}->{'test_role_single_priv'}."\n\n",
- },
- {
- name => "custom_role_with_privs",
- config => {
- users => default_users(),
- roles => default_roles_with([$default_cfg->{test_role_privs}]),
- },
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n\n\n\n".
- $default_raw->{roles}->{'test_role_privs'}."\n\n",
- },
- {
- name => "custom_role_with_duplicate_privs",
- config => {
- users => default_users(),
- roles => default_roles_with([$default_cfg->{test_role_privs}]),
- },
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n\n\n\n".
- $default_raw->{roles}->{'test_role_privs_duplicate'}."\n\n",
- expected_raw => "".
- $default_raw->{users}->{'root@pam'}."\n\n\n\n".
- $default_raw->{roles}->{'test_role_privs'}."\n\n",
- },
- {
- name => "custom_role_with_invalid_priv",
- config => {
- users => default_users(),
- roles => default_roles_with([$default_cfg->{test_role_privs}]),
- },
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n\n\n\n".
- $default_raw->{roles}->{'test_role_privs_invalid'}."\n\n",
- expected_raw => "".
- $default_raw->{users}->{'root@pam'}."\n\n\n\n".
- $default_raw->{roles}->{'test_role_privs'}."\n\n",
- },
- {
- name => "pool_empty",
- config => {
- users => default_users(),
- roles => default_roles(),
- pools => default_pools_with([$default_cfg->{test_pool_empty}]),
- },
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n\n\n".
- $default_raw->{pools}->{'test_pool_empty'}."\n\n\n",
- },
- {
- name => "pool_invalid",
- config => {
- users => default_users(),
- roles => default_roles(),
- pools => default_pools_with([$default_cfg->{test_pool_empty}]),
- },
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n\n\n".
- $default_raw->{pools}->{'test_pool_invalid'}."\n\n\n",
- expected_raw => "".
- $default_raw->{users}->{'root@pam'}."\n\n\n".
- $default_raw->{pools}->{'test_pool_empty'}."\n\n\n",
- },
- {
- name => "pool_members",
- config => {
- users => default_users(),
- roles => default_roles(),
- pools => default_pools_with([$default_cfg->{test_pool_members}]),
- vms => default_pool_vms_with([$default_cfg->{test_pool_members}]),
- },
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n\n\n".
- $default_raw->{pools}->{'test_pool_members'}."\n\n\n",
- },
- {
- name => "pool_duplicate_members",
- config => {
- users => default_users(),
- roles => default_roles(),
- pools => default_pools_with([$default_cfg->{test_pool_members}, $default_cfg->{test_pool_duplicate_vms}, $default_cfg->{test_pool_duplicate_storages}]),
- vms => default_pool_vms_with([$default_cfg->{test_pool_members}]),
- },
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n\n\n".
- $default_raw->{pools}->{'test_pool_members'}."\n".
- $default_raw->{pools}->{'test_pool_duplicate_vms'}."\n".
- $default_raw->{pools}->{'test_pool_duplicate_storages'}."\n",
- expected_raw => "".
- $default_raw->{users}->{'root@pam'}."\n\n\n".
- $default_raw->{pools}->{'test_pool_duplicate_storages'}."\n".
- $default_raw->{pools}->{'test_pool_duplicate_vms_expected'}."\n".
- $default_raw->{pools}->{'test_pool_members'}."\n\n\n",
- },
- {
- name => "acl_simple_user",
- config => {
- users => default_users_with([$default_cfg->{test_pam}]),
- roles => default_roles(),
- acl => default_acls_with([$default_cfg->{acl_simple_user}]),
- },
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n".
- $default_raw->{users}->{'test_pam'}."\n\n\n\n\n".
- $default_raw->{acl}->{'acl_simple_user'}."\n",
- },
- {
- name => "acl_complex_users",
- config => {
- users => default_users_with([$default_cfg->{test_pam}, $default_cfg->{'test2_pam'}]),
- roles => default_roles(),
- acl => default_acls_with([$default_cfg->{acl_simple_user}, $default_cfg->{acl_complex_users}]),
- },
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n".
- $default_raw->{users}->{'test2_pam'}."\n".
- $default_raw->{users}->{'test_pam'}."\n\n\n\n\n".
- $default_raw->{acl}->{'acl_simple_user'}."\n".
- $default_raw->{acl}->{'acl_complex_users_1'}."\n".
- $default_raw->{acl}->{'acl_complex_users_2'}."\n",
- },
- {
- name => "acl_complex_missing_user",
- config => {
- users => default_users_with([$default_cfg->{test2_pam}]),
- roles => default_roles(),
- acl => default_acls_with([$default_cfg->{acl_simple_user}, $default_cfg->{acl_complex_missing_user}]),
- },
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n".
- $default_raw->{users}->{'test2_pam'}."\n\n\n\n\n".
- $default_raw->{acl}->{'acl_simple_user'}."\n".
- $default_raw->{acl}->{'acl_complex_users_1'}."\n".
- $default_raw->{acl}->{'acl_complex_users_2'}."\n",
- },
- {
- name => "acl_simple_group",
- config => {
- users => default_users_with([$default_cfg->{test_pam_with_group}]),
- groups => default_groups_with([$default_cfg->{'test_group_single_member'}]),
- roles => default_roles(),
- acl => default_acls_with([$default_cfg->{acl_simple_group}]),
- },
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n".
- $default_raw->{users}->{'test_pam'}."\n\n".
- $default_raw->{groups}->{'test_group_single_member'}."\n\n\n\n".
- $default_raw->{acl}->{'acl_simple_group'}."\n",
- },
- {
- name => "acl_complex_groups",
- config => {
- users => default_users_with([$default_cfg->{test_pam_with_group}, $default_cfg->{'test2_pam_with_group'}, $default_cfg->{'test3_pam'}]),
- groups => default_groups_with([$default_cfg->{'test_group_members'}, $default_cfg->{'test_group_second'}]),
- roles => default_roles(),
- acl => default_acls_with([$default_cfg->{acl_simple_group}, $default_cfg->{acl_complex_groups}]),
- },
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n".
- $default_raw->{users}->{'test2_pam'}."\n".
- $default_raw->{users}->{'test3_pam'}."\n".
- $default_raw->{users}->{'test_pam'}."\n\n".
- $default_raw->{groups}->{'test_group_second'}."\n".
- $default_raw->{groups}->{'test_group_members'}."\n\n\n\n".
- $default_raw->{acl}->{'acl_simple_group'}."\n".
- $default_raw->{acl}->{'acl_complex_groups_1'}."\n".
- $default_raw->{acl}->{'acl_complex_groups_2'}."\n",
- },
- {
- name => "acl_complex_missing_group",
- config => {
- users => default_users_with([$default_cfg->{test_pam}, $default_cfg->{'test2_pam'}, $default_cfg->{'test3_pam'}]),
- groups => default_groups_with([$default_cfg->{'test_group_second'}]),
- roles => default_roles(),
- acl => default_acls_with([$default_cfg->{acl_simple_group}, $default_cfg->{acl_complex_missing_group}]),
- },
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n".
- $default_raw->{users}->{'test2_pam'}."\n".
- $default_raw->{users}->{'test3_pam'}."\n".
- $default_raw->{users}->{'test_pam'}."\n\n".
- $default_raw->{groups}->{'test_group_second'}."\n".
- $default_raw->{acl}->{'acl_simple_group'}."\n".
- $default_raw->{acl}->{'acl_complex_groups_1'}."\n".
- $default_raw->{acl}->{'acl_complex_groups_2'}."\n",
- expected_raw => "".
- $default_raw->{users}->{'root@pam'}."\n".
- $default_raw->{users}->{'test2_pam'}."\n".
- $default_raw->{users}->{'test3_pam'}."\n".
- $default_raw->{users}->{'test_pam'}."\n\n".
- $default_raw->{groups}->{'test_group_second'}."\n\n\n\n".
- $default_raw->{acl}->{'acl_simple_group'}."\n".
- $default_raw->{acl}->{'acl_complex_groups_1'}."\n".
- $default_raw->{acl}->{'acl_complex_groups_2'}."\n",
- },
- {
- name => "acl_simple_token",
- config => {
- users => default_users_with([$default_cfg->{test_pam_with_token}]),
- roles => default_roles(),
- acl => default_acls_with([$default_cfg->{acl_simple_token}]),
- },
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n".
- $default_raw->{users}->{'test_pam'}."\n".
- $default_raw->{tokens}->{'test_token_simple'}."\n\n\n\n\n".
- $default_raw->{acl}->{'acl_simple_token'}."\n",
- },
- {
- name => "acl_complex_tokens",
- config => {
- users => default_users_with([$default_cfg->{test_pam_with_token}, $default_cfg->{'test_pam2_with_token'}]),
- roles => default_roles(),
- acl => default_acls_with([$default_cfg->{acl_simple_token}, $default_cfg->{acl_complex_tokens}]),
- },
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n".
- $default_raw->{users}->{'test2_pam'}."\n".
- $default_raw->{tokens}->{'test_token_multi_expired'}."\n".
- $default_raw->{tokens}->{'test_token_multi_full'}."\n".
- $default_raw->{tokens}->{'test_token_multi_privsep'}."\n".
- $default_raw->{users}->{'test_pam'}."\n".
- $default_raw->{tokens}->{'test_token_simple'}."\n\n\n\n\n".
- $default_raw->{acl}->{'acl_simple_token'}."\n".
- $default_raw->{acl}->{'acl_complex_tokens_1'}."\n".
- $default_raw->{acl}->{'acl_complex_tokens_2'}."\n",
- },
- {
- name => "acl_complex_missing_token",
- config => {
- users => default_users_with([$default_cfg->{test_pam}, $default_cfg->{test_pam2_with_token}]),
- roles => default_roles(),
- acl => default_acls_with([$default_cfg->{acl_complex_missing_token}]),
- },
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n".
- $default_raw->{users}->{'test2_pam'}."\n".
- $default_raw->{tokens}->{'test_token_multi_expired'}."\n".
- $default_raw->{tokens}->{'test_token_multi_full'}."\n".
- $default_raw->{tokens}->{'test_token_multi_privsep'}."\n".
- $default_raw->{users}->{'test_pam'}."\n".
- $default_raw->{acl}->{'acl_simple_token'}."\n".
- $default_raw->{acl}->{'acl_complex_tokens_1'}."\n".
- $default_raw->{acl}->{'acl_complex_tokens_2'}."\n",
- expected_raw => "".
- $default_raw->{users}->{'root@pam'}."\n".
- $default_raw->{users}->{'test2_pam'}."\n".
- $default_raw->{tokens}->{'test_token_multi_expired'}."\n".
- $default_raw->{tokens}->{'test_token_multi_full'}."\n".
- $default_raw->{tokens}->{'test_token_multi_privsep'}."\n".
- $default_raw->{users}->{'test_pam'}."\n\n\n\n\n".
- $default_raw->{acl}->{'acl_complex_tokens_1_missing'}."\n".
- $default_raw->{acl}->{'acl_complex_tokens_2'}."\n",
- },
- {
- name => "acl_missing_role",
- config => {
- users => default_users_with([$default_cfg->{test_pam}]),
- roles => default_roles(),
- acl => default_acls_with([$default_cfg->{acl_simple_user}]),
- },
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n".
- $default_raw->{users}->{'test_pam'}."\n\n\n\n\n".
- $default_raw->{acl}->{'acl_simple_user'}."\n".
- $default_raw->{acl}->{'acl_missing_role'}."\n",
- expected_raw => "".
- $default_raw->{users}->{'root@pam'}."\n".
- $default_raw->{users}->{'test_pam'}."\n\n\n\n\n".
- $default_raw->{acl}->{'acl_simple_user'}."\n",
- },
- {
- name => "acl_complex_mixed",
- config => {
- users => default_users_with([$default_cfg->{test_pam_with_group}, $default_cfg->{'test2_pam_with_group'}, $default_cfg->{'test3_pam'}]),
- groups => default_groups_with([$default_cfg->{'test_group_members'}, $default_cfg->{'test_group_second'}]),
- roles => default_roles(),
- acl => default_acls_with([
- $default_cfg->{acl_complex_mixed_root},
- $default_cfg->{acl_complex_mixed_storage},
- ]),
- },
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n".
- $default_raw->{users}->{'test2_pam'}."\n".
- $default_raw->{users}->{'test3_pam'}."\n".
- $default_raw->{users}->{'test_pam'}."\n\n".
- $default_raw->{groups}->{'test_group_second'}."\n".
- $default_raw->{groups}->{'test_group_members'}."\n\n\n\n".
- $default_raw->{acl}->{'acl_simple_group'}."\n".
- $default_raw->{acl}->{'acl_complex_groups_1'}."\n".
- $default_raw->{acl}->{'acl_complex_groups_2'}."\n".
- $default_raw->{acl}->{'acl_simple_user'}."\n".
- $default_raw->{acl}->{'acl_complex_users_1'}."\n".
- $default_raw->{acl}->{'acl_complex_users_2'}."\n",
- expected_raw => "".
- $default_raw->{users}->{'root@pam'}."\n".
- $default_raw->{users}->{'test2_pam'}."\n".
- $default_raw->{users}->{'test3_pam'}."\n".
- $default_raw->{users}->{'test_pam'}."\n\n".
- $default_raw->{groups}->{'test_group_second'}."\n".
- $default_raw->{groups}->{'test_group_members'}."\n\n\n\n".
- $default_raw->{acl}->{'acl_complex_mixed_1'}."\n".
- $default_raw->{acl}->{'acl_complex_mixed_2'}."\n".
- $default_raw->{acl}->{'acl_complex_mixed_3'}."\n",
- },
- {
- name => "acl_complex_mixed_prop_noprop_no_merge_sort_by_path",
- config => {
- users => default_users_with([$default_cfg->{test_pam_with_group}, $default_cfg->{'test2_pam_with_group'}, $default_cfg->{'test3_pam'}]),
- groups => default_groups_with([$default_cfg->{'test_group_members'}, $default_cfg->{'test_group_second'}]),
- roles => default_roles(),
- acl => default_acls_with([
- $default_cfg->{acl_complex_mixed_root_noprop},
- $default_cfg->{acl_complex_mixed_storage_noprop},
- ]),
- },
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n".
- $default_raw->{users}->{'test2_pam'}."\n".
- $default_raw->{users}->{'test3_pam'}."\n".
- $default_raw->{users}->{'test_pam'}."\n\n".
- $default_raw->{groups}->{'test_group_second'}."\n".
- $default_raw->{groups}->{'test_group_members'}."\n\n\n\n".
- $default_raw->{acl}->{'acl_simple_group_noprop'}."\n".
- $default_raw->{acl}->{'acl_simple_user'}."\n".
- $default_raw->{acl}->{'acl_complex_groups_1_noprop'}."\n".
- $default_raw->{acl}->{'acl_complex_groups_2_noprop'}."\n".
- $default_raw->{acl}->{'acl_complex_users_1'}."\n".
- $default_raw->{acl}->{'acl_complex_users_2'}."\n",
- },
- {
- name => "sort_roles_and_privs",
- raw => "".
- $default_raw->{users}->{'root@pam'}."\n".
- $default_raw->{roles}->{'test_role_single_priv'}."\n\n".
- $default_raw->{roles}->{'test_role_privs_out_of_order'}."\n\n",
- expected_raw => "".
- $default_raw->{users}->{'root@pam'}."\n\n\n\n".
- $default_raw->{roles}->{'test_role_privs'}."\n".
- $default_raw->{roles}->{'test_role_single_priv'}."\n\n",
- },
- {
- name => "sort_users_and_group_members",
- raw => "".
- $default_raw->{users}->{'test2_pam'}."\n".
- $default_raw->{users}->{'root@pam'}."\n".
- $default_raw->{users}->{'test_pam'}."\n\n".
- $default_raw->{groups}->{'test_group_members_out_of_order'}."\n\n".
- "\n\n",
- expected_raw => "".
- $default_raw->{users}->{'root@pam'}."\n".
- $default_raw->{users}->{'test2_pam'}."\n".
- $default_raw->{users}->{'test_pam'}."\n\n".
- $default_raw->{groups}->{'test_group_members'}."\n\n".
- "\n\n",
- },
- {
- name => "sort_user_groups_and_acls",
- raw => "".
- $default_raw->{users}->{'test2_pam'}."\n".
- $default_raw->{users}->{'root@pam'}."\n".
- $default_raw->{users}->{'test_pam'}."\n\n".
- $default_raw->{users}->{'test3_pam'}."\n".
- $default_raw->{groups}->{'test_group_members_out_of_order'}."\n\n\n\n".
- $default_raw->{groups}->{'test_group_second'}."\n".
- $default_raw->{acl}->{'acl_simple_user'}."\n".
- $default_raw->{acl}->{'acl_simple_group'}."\n".
- $default_raw->{acl}->{'acl_complex_users_1'}."\n".
- $default_raw->{acl}->{'acl_complex_users_2'}."\n".
- $default_raw->{acl}->{'acl_complex_groups_1'}."\n".
- $default_raw->{acl}->{'acl_complex_groups_2'}."\n",
- expected_raw => "".
- $default_raw->{users}->{'root@pam'}."\n".
- $default_raw->{users}->{'test2_pam'}."\n".
- $default_raw->{users}->{'test3_pam'}."\n".
- $default_raw->{users}->{'test_pam'}."\n\n".
- $default_raw->{groups}->{'test_group_second'}."\n".
- $default_raw->{groups}->{'test_group_members'}."\n\n\n\n".
- $default_raw->{acl}->{'acl_complex_mixed_1'}."\n".
- $default_raw->{acl}->{'acl_complex_mixed_2'}."\n".
- $default_raw->{acl}->{'acl_complex_mixed_3'}."\n",
- },
- {
- name => 'default_values',
- config => {
- users => {
- 'root@pam' => {
- enable => 0,
- expire => 0,
- email => undef,
- },
- 'test@pam' => {
- enable => 0,
- expire => 0,
- email => undef,
- tokens => {
- 'test' => {
- expire => 0,
- privsep => 0,
- },
- },
- },
- },
- roles => default_roles_with([{ id => 'testrole' }]),
- groups => default_groups_with([$default_cfg->{test_group_empty}]),
- pools => default_pools_with([$default_cfg->{test_pool_empty}]),
- },
- raw => "".
- 'user:root@pam'."\n".
- 'user:test@pam'."\n".
- 'token:test@pam!test'."\n\n".
- 'group:testgroup'."\n\n".
- 'pool:testpool'."\n\n".
- 'role:testrole'."\n\n".
- 'acl::/:',
- expected_raw => "".
- 'user:root@pam:0:0::::::'."\n".
- 'user:test@pam:0:0::::::'."\n".
- 'token:test@pam!test:0:0::'."\n\n".
- 'group:testgroup:::'."\n\n".
- 'pool:testpool::::'."\n\n".
- 'role:testrole::'."\n\n",
- },
-];
-
-
-my $number_of_tests_run = 0;
-foreach my $t (@$tests) {
- my $expected_config = $t->{expected_config} // $t->{config};
- my $expected_raw = $t->{expected_raw} // $t->{raw};
- if (defined($t->{raw})) {
- my $parsed = PVE::AccessControl::parse_user_config($t->{name}, $t->{raw});
- if (defined($expected_config)) {
- is_deeply($parsed, $expected_config, "$t->{name}_parse");
- $number_of_tests_run++;
- }
- if (defined($t->{expected_raw}) && !defined($t->{config})) {
- is(PVE::AccessControl::write_user_config($t->{name}, $parsed), $t->{expected_raw}, "$t->{name}_rewrite");
- $number_of_tests_run++;
- }
-
- }
- if (defined($t->{config})) {
- my $written = PVE::AccessControl::write_user_config($t->{name}, $t->{config});
- if (defined($expected_raw)) {
- is($written, $expected_raw, "$t->{name}_write");
- $number_of_tests_run++;
- }
- if (defined($t->{expected_config}) && !defined($t->{raw})) {
- is_deeply(PVE::AccessControl::parse_user_config($t->{name}, $t->{written}), $t->{expected_config}, "$t->{name}_reparse");
- $number_of_tests_run++;
- }
- }
-};
-
-done_testing( $number_of_tests_run);
+++ /dev/null
-#!/usr/bin/perl -w
-
-use strict;
-use PVE::Tools;
-use PVE::AccessControl;
-use PVE::RPCEnvironment;
-use Getopt::Long;
-
-my $rpcenv = PVE::RPCEnvironment->init('cli');
-
-my $cfgfn = "test1.cfg";
-$rpcenv->init_request(userconfig => $cfgfn);
-
-sub check_roles {
- my ($user, $path, $expected_result) = @_;
-
- my $roles = PVE::AccessControl::roles($rpcenv->{user_cfg}, $user, $path);
- my $res = join(',', sort keys %$roles);
-
- die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
- if $res ne $expected_result;
-
- print "ROLES:$path:$user:$res\n";
-}
-
-sub check_permission {
- my ($user, $path, $expected_result) = @_;
-
- my $perm = $rpcenv->permissions($user, $path);
- my $res = join(',', sort keys %$perm);
-
- die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
- 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;
-
- print "PERM:$path:$user:$res\n";
-}
-
-check_roles('max@pve', '/', '');
-check_roles('max@pve', '/vms', 'vm_admin');
-
-#user permissions overrides group permissions
-check_roles('max@pve', '/vms/100', 'customer');
-check_roles('max@pve', '/vms/101', 'vm_admin');
-
-check_permission('max@pve', '/', '');
-check_permission('max@pve', '/vms', 'Permissions.Modify,VM.Allocate,VM.Audit,VM.Console');
-check_permission('max@pve', '/vms/100', 'VM.Audit,VM.PowerMgmt');
-
-check_permission('alex@pve', '/vms', '');
-check_permission('alex@pve', '/vms/100', 'VM.Audit,VM.PowerMgmt');
-
-
-check_roles('max@pve', '/vms/200', 'storage_manager');
-check_roles('joe@pve', '/vms/200', 'vm_admin');
-check_roles('sue@pve', '/vms/200', 'NoAccess');
-
-print "all tests passed\n";
-
-exit (0);
+++ /dev/null
-#!/usr/bin/perl -w
-
-use strict;
-use PVE::Tools;
-use PVE::AccessControl;
-use PVE::RPCEnvironment;
-use Getopt::Long;
-
-my $rpcenv = PVE::RPCEnvironment->init('cli');
-
-my $cfgfn = "test2.cfg";
-$rpcenv->init_request(userconfig => $cfgfn);
-
-sub check_roles {
- my ($user, $path, $expected_result) = @_;
-
- my $roles = PVE::AccessControl::roles($rpcenv->{user_cfg}, $user, $path);
- my $res = join(',', sort keys %$roles);
-
- die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
- if $res ne $expected_result;
-
- print "ROLES:$path:$user:$res\n";
-}
-
-# inherit multiple group permissions
-
-check_roles('User1@pve', '/', '');
-check_roles('User2@pve', '/', '');
-
-check_roles('User1@pve', '/vms', 'Role1,Role2');
-check_roles('User2@pve', '/vms', '');
-
-check_roles('User1@pve', '/vms/100', 'Role1,Role2');
-check_roles('User2@pve', '/vms', '');
-
-print "all tests passed\n";
-
-exit (0);
+++ /dev/null
-#!/usr/bin/perl -w
-
-use strict;
-use PVE::Tools;
-use PVE::AccessControl;
-use PVE::RPCEnvironment;
-use Getopt::Long;
-
-my $rpcenv = PVE::RPCEnvironment->init('cli');
-
-my $cfgfn = "test3.cfg";
-$rpcenv->init_request(userconfig => $cfgfn);
-
-sub check_roles {
- my ($user, $path, $expected_result) = @_;
-
- my $roles = PVE::AccessControl::roles($rpcenv->{user_cfg}, $user, $path);
- my $res = join(',', sort keys %$roles);
-
- die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
- if $res ne $expected_result;
-
- print "ROLES:$path:$user:$res\n";
-}
-
-check_roles('User1@pve', '', '');
-check_roles('User2@pve', '', '');
-
-check_roles('User1@pve', '/vms/300', 'Role1');
-check_roles('User1@pve', '/vms/200', 'Role2');
-
-print "all tests passed\n";
-
-exit (0);
+++ /dev/null
-#!/usr/bin/perl -w
-
-use strict;
-use PVE::Tools;
-use PVE::AccessControl;
-use PVE::RPCEnvironment;
-use Getopt::Long;
-
-my $rpcenv = PVE::RPCEnvironment->init('cli');
-
-my $cfgfn = "test4.cfg";
-$rpcenv->init_request(userconfig => $cfgfn);
-
-sub check_roles {
- my ($user, $path, $expected_result) = @_;
-
- my $roles = PVE::AccessControl::roles($rpcenv->{user_cfg}, $user, $path);
- my $res = join(',', sort keys %$roles);
-
- die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
- 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);
+++ /dev/null
-#!/usr/bin/perl -w
-
-use strict;
-use PVE::Tools;
-use PVE::AccessControl;
-use PVE::RPCEnvironment;
-use Getopt::Long;
-
-my $rpcenv = PVE::RPCEnvironment->init('cli');
-
-my $cfgfn = "test5.cfg";
-$rpcenv->init_request(userconfig => $cfgfn);
-
-sub check_roles {
- my ($user, $path, $expected_result) = @_;
-
- my $roles = PVE::AccessControl::roles($rpcenv->{user_cfg}, $user, $path);
- my $res = join(',', sort keys %$roles);
-
- die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
- 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');
-check_roles('User1@pve', '/vms/100/a/b', 'Role2');
-check_roles('User1@pve', '/vms/100/a/b/c', 'Role2');
-check_roles('User1@pve', '/vms/200', 'Role1');
-
-check_roles('User2@pve', '/kvm', 'Role2');
-check_roles('User2@pve', '/kvm/vms', 'Role1');
-check_roles('User2@pve', '/kvm/vms/100', '');
-check_roles('User2@pve', '/kvm/vms/100/a', 'Role3');
-check_roles('User2@pve', '/kvm/vms/100/a/b', '');
-
-print "all tests passed\n";
-
-exit (0);
+++ /dev/null
-#!/usr/bin/perl -w
-
-use strict;
-use PVE::Tools;
-use PVE::AccessControl;
-use PVE::RPCEnvironment;
-use Getopt::Long;
-
-my $rpcenv = PVE::RPCEnvironment->init('cli');
-
-my $cfgfn = "test6.cfg";
-$rpcenv->init_request(userconfig => $cfgfn);
-
-sub check_roles {
- my ($user, $path, $expected_result) = @_;
-
- my $roles = PVE::AccessControl::roles($rpcenv->{user_cfg}, $user, $path);
- my $res = join(',', sort keys %$roles);
-
- die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
- if $res ne $expected_result;
-
- print "ROLES:$path:$user:$res\n";
-}
-
-sub check_permissions {
- my ($user, $path, $expected_result) = @_;
-
- my $perm = $rpcenv->permissions($user, $path);
- my $res = join(',', sort keys %$perm);
-
- die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
- 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;
-
- print "PERM:$path:$user:$res\n";
-}
-
-check_roles('User1@pve', '', '');
-check_roles('User2@pve', '', '');
-check_roles('User3@pve', '', '');
-check_roles('User4@pve', '', '');
-
-check_roles('User1@pve', '/vms', 'RoleTEST1');
-check_roles('User2@pve', '/vms', 'RoleTEST1');
-check_roles('User3@pve', '/vms', 'NoAccess');
-check_roles('User4@pve', '/vms', '');
-
-check_roles('User1@pve', '/vms/100', 'RoleTEST1');
-check_roles('User2@pve', '/vms/100', 'RoleTEST1');
-check_roles('User3@pve', '/vms/100', 'NoAccess');
-check_roles('User4@pve', '/vms/100', '');
-
-check_roles('User1@pve', '/vms/300', 'RoleTEST1');
-check_roles('User2@pve', '/vms/300', 'RoleTEST1');
-check_roles('User3@pve', '/vms/300', 'NoAccess');
-check_roles('User4@pve', '/vms/300', 'RoleTEST1');
-
-check_permissions('User1@pve', '/vms/500', 'VM.Console,VM.PowerMgmt');
-check_permissions('User2@pve', '/vms/500', 'VM.Console,VM.PowerMgmt');
-# without pool
-check_roles('User3@pve', '/vms/500', 'NoAccess');
-# with pool
-check_permissions('User3@pve', '/vms/500', '');
-# without pool
-check_roles('User4@pve', '/vms/500', '');
-# with pool
-check_permissions('User4@pve', '/vms/500', '');
-
-
-check_permissions('User1@pve', '/vms/600', 'VM.Console');
-check_permissions('User2@pve', '/vms/600', 'VM.Console');
-check_permissions('User3@pve', '/vms/600', '');
-check_permissions('User4@pve', '/vms/600', 'VM.Console');
-
-check_permissions('User1@pve', '/storage/store1', 'VM.Console,VM.PowerMgmt');
-check_permissions('User2@pve', '/storage/store1', 'VM.PowerMgmt');
-check_permissions('User3@pve', '/storage/store1', 'VM.PowerMgmt');
-check_permissions('User4@pve', '/storage/store1', 'VM.Console');
-
-check_permissions('User1@pve', '/storage/store2', 'VM.PowerMgmt');
-check_permissions('User2@pve', '/storage/store2', 'VM.PowerMgmt');
-check_permissions('User3@pve', '/storage/store2', 'VM.PowerMgmt');
-check_permissions('User4@pve', '/storage/store2', '');
-
-print "all tests passed\n";
-
-exit (0);
+++ /dev/null
-#!/usr/bin/perl -w
-
-use strict;
-use PVE::Tools;
-use PVE::AccessControl;
-use PVE::RPCEnvironment;
-use Getopt::Long;
-
-my $rpcenv = PVE::RPCEnvironment->init('cli');
-
-my $cfgfn = "test7.cfg";
-$rpcenv->init_request(userconfig => $cfgfn);
-
-sub check_roles {
- my ($user, $path, $expected_result) = @_;
-
- my $roles = PVE::AccessControl::roles($rpcenv->{user_cfg}, $user, $path);
- my $res = join(',', sort keys %$roles);
-
- die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
- if $res ne $expected_result;
-
- print "ROLES:$path:$user:$res\n";
-}
-
-sub check_permissions {
- my ($user, $path, $expected_result) = @_;
-
- my $perm = $rpcenv->permissions($user, $path);
- my $res = join(',', sort keys %$perm);
-
- die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
- 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;
-
- print "PERM:$path:$user:$res\n";
-}
-
-check_roles('User1@pve', '/vms', 'Role1');
-check_roles('User1@pve', '/vms/200', 'Role1');
-
-# no pool
-check_roles('User1@pve', '/vms/100', 'Role1');
-# with pool
-check_permissions('User1@pve', '/vms/100', '');
-
-print "all tests passed\n";
-
-exit (0);
+++ /dev/null
-#!/usr/bin/perl -w
-
-use strict;
-use PVE::Tools;
-use PVE::AccessControl;
-use PVE::RPCEnvironment;
-
-my $rpcenv = PVE::RPCEnvironment->init('cli');
-
-my $cfgfn = "test8.cfg";
-$rpcenv->init_request(userconfig => $cfgfn);
-
-sub check_roles {
- my ($user, $path, $expected_result) = @_;
-
- my $roles = PVE::AccessControl::roles($rpcenv->{user_cfg}, $user, $path);
- my $res = join(',', sort keys %$roles);
-
- die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
- if $res ne $expected_result;
-
- print "ROLES:$path:$user:$res\n";
-}
-
-sub check_permission {
- my ($user, $path, $expected_result) = @_;
-
- my $perm = $rpcenv->permissions($user, $path);
- my $res = join(',', sort keys %$perm);
-
- die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
- 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;
-
- print "PERM:$path:$user:$res\n";
-}
-
-check_roles('max@pve', '/', '');
-check_roles('max@pve', '/vms', 'vm_admin');
-
-#user permissions overrides group permissions
-check_roles('max@pve', '/vms/100', 'customer');
-check_roles('max@pve', '/vms/101', 'vm_admin');
-
-check_permission('max@pve', '/', '');
-check_permission('max@pve', '/vms', 'Permissions.Modify,VM.Allocate,VM.Audit,VM.Console');
-check_permission('max@pve', '/vms/100', 'VM.Audit,VM.PowerMgmt');
-
-check_permission('alex@pve', '/vms', '');
-check_permission('alex@pve', '/vms/100', 'VM.Audit,VM.PowerMgmt');
-
-check_roles('max@pve', '/vms/200', 'storage_manager');
-check_roles('joe@pve', '/vms/200', 'vm_admin');
-check_roles('sue@pve', '/vms/200', 'NoAccess');
-
-check_roles('carol@pam', '/vms/200', 'NoAccess');
-check_roles('carol@pam!token', '/vms/200', 'NoAccess');
-check_roles('max@pve!token', '/vms/200', 'storage_manager');
-check_roles('max@pve!token2', '/vms/200', 'customer');
-
-print "all tests passed\n";
-
-exit (0);
-
+++ /dev/null
-user:joe@pve:1:
-user:max@pve:1:
-user:alex@pve:1:
-user:sue@pve:1:
-user:carol@pam:1:
-
-group:testgroup1:joe@pve,max@pve,sue@pve:
-group:testgroup2:alex@pve,carol@pam,sue@pve:
-group:testgroup3:max@pve:
-
-role:storage_manager:Datastore.AllocateSpace,Datastore.Audit:
-role:customer:VM.Audit,VM.PowerMgmt:
-role:vm_admin:VM.Audit,VM.Allocate,Permissions.Modify,VM.Console:
-
-acl:1:/vms:@testgroup1:vm_admin:
-acl:1:/vms/100/:alex@pve,max@pve:customer:
-acl:1:/storage/nfs1:@testgroup2:storage_manager:
-acl:1:/users:max@pve:Administrator:
-
-acl:1:/vms/200:@testgroup3:storage_manager:
-acl:1:/vms/200:@testgroup2:NoAccess:
-
+++ /dev/null
-user:User1@pve:1:
-user:User2@pve:1:
-
-group:GroupA:User1@pve:
-group:GroupB:User1@pve:
-
-role:Role1:VM.PowerMgmt:
-role:Role2:VM.Console:
-
-acl:1:/vms:@GroupA:Role1:
-acl:1:/vms:@GroupB:Role2:
+++ /dev/null
-user:User1@pve:1:
-user:User2@pve:1:
-
-group:GroupA:User1@pve:
-group:GroupB:User1@pve:
-
-role:Role1:VM.PowerMgmt:
-role:Role2:VM.Console:
-
-acl:1:/vms:@GroupA:Role1:
-acl:1:/vms/200:@GroupB:Role2:
+++ /dev/null
-user:User1@pve:1:
-user:User2@pve:1:
-
-group:GroupA:User1@pve,User2@pve:
-group:GroupB:User1@pve,User2@pve:
-
-role:Role1:VM.PowerMgmt:
-role:Role2:VM.Console:
-
-acl:1:/vms:@GroupA:Role1:
-acl:1:/vms:User2@pve:NoAccess:
+++ /dev/null
-user:User1@pve:1:
-user:User2@pve:1:
-
-group:GroupA:User1@pve,User2@pve:
-group:GroupB:User1@pve,User2@pve:
-
-role:Role1:VM.PowerMgmt:
-role:Role2:VM.Console:
-role:Role3:VM.Console:
-
-acl:1:/vms:User1@pve:Role1:
-acl:1:/vms/100/a/b:User1@pve:Role2:
-
-acl:0:/kvm:User2@pve:Role2:
-acl:0:/kvm/vms:User2@pve:Role1:
-acl:0:/kvm/vms/100/a:User2@pve:Role3:
+++ /dev/null
-user:User1@pve:1:
-user:User2@pve:1:
-user:User3@pve:1:
-user:User4@pve:1:
-
-group:DEVEL:User1@pve,User2@pve,User3@pve:
-group:MARKETING:User1@pve,User4@pve:
-
-role:RoleDEVEL:VM.PowerMgmt:
-role:RoleMARKETING:VM.Console:
-role:RoleTEST1:VM.Console:
-
-acl:1:/pool/devel:@DEVEL:RoleDEVEL:
-acl:1:/pool/marketing:@MARKETING:RoleMARKETING:
-
-acl:1:/vms:@DEVEL:RoleTEST1:
-acl:1:/vms:User3@pve:NoAccess:
-acl:1:/vms/300:@MARKETING:RoleTEST1:
-
-pool:devel:MITS development:500,501,502:store1 store2:
-pool:marketing:MITS marketing:600:store1:
+++ /dev/null
-user:User1@pve:1:
-user:User2@pve:1:
-
-group:GroupA:User1@pve,User2@pve:
-group:GroupB:User1@pve,User2@pve:
-
-role:Role1:VM.PowerMgmt:
-role:Role2:VM.Console:
-role:Role3:VM.Console:
-
-acl:1:/pool/devel:User1@pve:NoAccess:
-
-acl:1:/vms:User1@pve:Role1:
-
-pool:devel:Development:100:store1:
+++ /dev/null
-user:joe@pve:1:
-user:max@pve:1:
-token:max@pve!token::0:
-token:max@pve!token2::1:
-user:alex@pve:1:
-user:sue@pve:1:
-user:carol@pam:1:
-token:carol@pam!token:
-
-group:testgroup1:joe@pve,max@pve,sue@pve:
-group:testgroup2:alex@pve,carol@pam,sue@pve:
-group:testgroup3:max@pve:
-
-role:storage_manager:Datastore.AllocateSpace,Datastore.Audit:
-role:customer:VM.Audit,VM.PowerMgmt:
-role:vm_admin:VM.Audit,VM.Allocate,Permissions.Modify,VM.Console:
-
-acl:1:/vms:@testgroup1:vm_admin:
-acl:1:/vms/100/:alex@pve,max@pve:customer:
-acl:1:/storage/nfs1:@testgroup2:storage_manager:
-acl:1:/users:max@pve:Administrator:
-
-acl:1:/vms/200:@testgroup3:storage_manager:
-acl:1:/vms/200:@testgroup2:NoAccess:
-
-acl:1:/vms/200:carol@pam!token:vm_admin
-acl:1:/vms/200:max@pve!token:storage_manager
-acl:1:/vms/200:max@pve!token2:customer