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,
+ p