5
0
mirror of git://git.proxmox.com/git/pve-access-control.git synced 2025-01-03 01:17:55 +03:00

iimported from svn 'pve-access-control/trunk'

This commit is contained in:
Dietmar Maurer 2011-08-23 07:27:48 +02:00
commit 2c3a6c0aaa
32 changed files with 4594 additions and 0 deletions

457
ChangeLog Normal file
View File

@ -0,0 +1,457 @@
2011-08-15 Proxmox Support Team <support@proxmox.com>
* PVE/AccessControl.pm (parse_user_config): fix parser for files
without newline at eof
(parse_shadow_passwd): fix parser for files without newline at eof
(parse_domains): fix parser for files without newline at eof
2011-08-01 Proxmox Support Team <support@proxmox.com>
* PVE/AccessControl.pm (lock_*): remove $parent in calls to
cfs_lock_file()
2011-07-22 Proxmox Support Team <support@proxmox.com>
* PVE/API2/Domains.pm (create): use lower case: s/AD/ad/ and
s/LDAP/ldap/
* PVE/AccessControl.pm (write_domains): use lc($type)
2011-07-14 Proxmox Support Team <support@proxmox.com>
* control.in (Depends): remove depend on liburi-perl (code moved
to pve-common)
2011-07-05 Proxmox Support Team <support@proxmox.com>
* PVE/API2/User.pm (create_user): add -enable parameter
* PVE/API2/User.pm (update_user): use -enable instead of
-lock/-unlock
2011-06-27 Proxmox Support Team <support@proxmox.com>
* PVE/AccessControl.pm (normalize_path): allow '-' in path
2011-05-30 Proxmox Support Team <support@proxmox.com>
* PVE/AccessControl.pm (assemble_csrf_prevention_token): CSRF
token may not depend on cookie, because cookie can be updated from
other window.
2011-03-30 Proxmox Support Team <support@proxmox.com>
* PVE/API2/AccessControl.pm (create_ticket): also return user name
2011-03-24 Proxmox Support Team <support@proxmox.com>
* PVE/AccessControl.pm (verify_csrf_prevention_token): add CSRF
prevention code
2011-03-23 Proxmox Support Team <support@proxmox.com>
* PVE/RPCEnvironment.pm (active_workers): simple log rotation when
file is bigger that 50KB
2011-03-22 Proxmox Support Team <support@proxmox.com>
* PVE/RPCEnvironment.pm (set_result_count): a way to set the total
number of results - we use that for the ExtJS paging grid.
2011-03-21 Proxmox Support Team <support@proxmox.com>
* PVE/RPCEnvironment.pm (active_workers): immediately move finished
task to the index file.
2011-03-17 Proxmox Support Team <support@proxmox.com>
* PVE/RPCEnvironment.pm (active_workers): update/get worker list
2011-03-16 Proxmox Support Team <support@proxmox.com>
* PVE/RPCEnvironment.pm (fork_worker): add code to simulate running
in foreground (cli).
2011-02-24 Proxmox Support Team <support@proxmox.com>
* PVE/AccessControl.pm (roles): fix group permission propagation
* PVE/API2/ACL.pm: cleanup API - use '-users' and '-gropus'
instead of '-uglist'
2011-02-23 Proxmox Support Team <support@proxmox.com>
* PVE/API2/AccessControl.pm (create_ticket): moved code from REST.pm
2011-02-22 Proxmox Support Team <support@proxmox.com>
* PVE/AccessControl.pm: make 'domains.cfg' readable by www-data,
add 'default' attribute.
* PVE/AccessControl.pm: realm is now part of the username.
Example: 'userid@realm'
(valid_attributes): add 'domain, port, secure' attributes for AD.
(parse_domains): add attribute 'secure' (replace LDAPS type),
* PVE/AccessControl.pm (parse_user_config): add firstname/lastname
and email fields.
2011-02-21 Proxmox Support Team <support@proxmox.com>
* PVE/API2/Group.pm (update_group): implement modgroup (set
comment)
2011-02-18 Proxmox Support Team <support@proxmox.com>
* PVE/AccessControl.pm (create_roles): try to create a predefined
set of roles automatically.
2011-02-17 Proxmox Support Team <support@proxmox.com>
* PVE/API2/Domains.pm: new API to for domains.cfg
* PVE/AccessControl.pm (authenticate_user_domain): added a 'domid'
attribute to users. This references an entry in the domain
config. This is simpler than the previous domain search
algorithm.
* PVE/API2/User.pm: save domid, name, comment and expire time for
user entries.
* PVE/AccessControl.pm (authenticate_user): check for expired
accounts
* control.in (Depends): depend on liburi-perl (we use URI::Escape
to encode text in our config files).
* PVE/AccessControl.pm (enable_user, disable_user): removed
clumsy methods, not needed.
2011-02-16 Proxmox Support Team <support@proxmox.com>
* README (privileges): Changes set of privileges. We try to be as
simple as possible. We can refinen them in future.
* PVE/ACLCache.pm: deleted - moved code into RPCEnvironment.
2011-02-15 Proxmox Support Team <support@proxmox.com>
* PVE/AccessControl.pm (verify_username): restrict user names to
64 charachters. Add new priviledges Sys.PowerOff, Sys.Console and
Sys.Syslog
* PVE/ACLCache.pm: move code into new file.
* test/perm-test1.pl: modified to use new PVE::ACLCache class.
* PVE/AccessControl.pm: add new class PVE::ACLCache (speed up ACL
checks)
2011-01-27 Proxmox Support Team <support@proxmox.com>
* pveum (auth): remove auth method - we do not use it any
longer, comment out ability to pass password via environment
variable.
* PVE/AccessControl.pm (check_permissions): new helper to check
permissions.
2011-01-21 root <root@maui.maurer-it.com>
* PVE/AccessControl.pm: register a JSONSchema standard option for
'userid'.
* pveum: allow to pass passwords with environment variable
PVE_PW_TICKET
* pveum (auth): new method to verify credentials/privileges (used
by our kvm patches and vncterm)
2011-01-12 root <root@maui.maurer-it.com>
* PVE/AccessControl.pm: use new PVE::Cluster class and read data
from cluster filesystem (instead of local filesystem).
2011-01-11 root <root@maui.maurer-it.com>
* control.in (Depends): depend on new pve-cluster package
* PVE/AccessControl.pm (read_pubkey, read_privkey): inotify does
not work on the cluster filesystem, so I removed that code. Also
moved lock files to /var/lock/pve-manager (cluster filesystem does
not support locks - we need to do cluster wide locks later)
2010-09-14 Proxmox Support Team <support@proxmox.com>
* PVE/API2/AccessControl.pm: moved from pve-manager
* PVE/: create correct directory hierarchy
* Makefile (install): use 'verifyapi'
* pveum: add verifyapi
2010-08-25 Proxmox Support Team <support@proxmox.com>
* pveum: use new PVE::CLIHandler
2010-08-24 Proxmox Support Team <support@proxmox.com>
* pveum: use new PVE::RPCEnvironment
* *.pm: remove $conn parameter everywhere
2010-08-16 Proxmox Support Team <support@proxmox.com>
* AccessControl.pm (lock_user_config): add call to die, remove
@param - we do not need that here
(lock_shadow_config): add call to die, remove @param
* *.pm: remove $resp parameter everywhere.
* AccessControl.pm (verify_username): add test for username
length (at least 3 characters)
2010-08-13 Proxmox Support Team <support@proxmox.com>
* User.pm: use new 'format' property in schema
* ACL.pm: use new 'format' property in schema, remove redundant
calls to verify_XXX calls.
* Role.pm: use new 'format' property in schema, remove redundant
calls to verify_XXX calls.
* Group.pm: use new 'format' property in schema, remove redundant
calls to verify_XXX calls.
* AccessControl.pm (modify_acl): strict error checking - use 'die'
instead of 'warn', moved to ACL.pm
(verify_username): fix serious bug
2010-08-12 Proxmox Support Team <support@proxmox.com>
* Group.pm: use the new RESTHandler for API methods
* Role.pm: use the new RESTHandler for API methods
* AccessControl.pm (add_group): moved to Group.pm
(delete_group): moved to Group.pm
(delete_role): moved to Role.pm
(modify_role): moved to Role.pm
* User.pm: strict error checking - use 'die' instead of 'warn'
* User.pm (delete_user): raise error when user does not exist.
* Group.pm (delete_group): raise error when group does not exist.
* pveum: use the new
RESTHandler (PVE::API2::User->cli_handler()). That way we have
automatic command line argument parsing.
* User.pm: use the new RESTHandler for API methods. Those methods
are automatically exposed with the API Server (pve-manager), and
we can use them in the command line tools.
* AccessControl.pm (modify_user, delete_user): moved to User.pm
2010-08-10 Proxmox Support Team <support@proxmox.com>
* control.in (Depends): depend on libpve-common-perl
* AccessControl.pm: initialize Crypt::OpenSSL::RSA with
import_random_seed(), else I get a 'Segmentation fault' when
creating tickets ("pveum ticket <testuser>").
* AccessControl.pm: Moved utilities to new PVE::Tools
module (pve-common), use new PVE::INotify to read/write config files.
* AccessControl.pm (parse_domains): ignore case (always convert
type to lower case), fix bug from Seth and test for 'ldaps'.
(file_set_contents): use O_WRONLY|O_CREAT instead of 'w' - else
perm gets ignored.
2010-08-09 Seth Lauzon <seth.lauzon@gmail.com>
* AccessControl.pm (authenticate_user_ldap): changed the bind function
for LDAP to allow for secure connection
2010-07-21 Seth Lauzon <seth.lauzon@gmail.com>
* AccessControl.pm (parse_domains): require base_dn for LDAP domains
(valid_attributes): renamed from valid_params to maintain conformity
2010-07-19 Proxmox Support Team <support@proxmox.com>
* AccessControl.pm (authenticate_user_domain): always add timeout
after failed auth
(file_set_contents): correctly emit exception if print/close fails
2010-07-19 Seth Lauzon <seth.lauzon@gmail.com>
* AccessControl.pm: fixed timeout for ldap/AD errors and reduced to two seconds
* AccessControl.pm: modified LDAP authentication to a two step bind method
2010-07-16 Proxmox Support Team <support@proxmox.com>
* AccessControl.pm (authenticate_user_domain): catch special
case ($domain eq '')
(parse_domains): fix various bugs, allow spaces between domains,
skip duplicate parameters
2010-07-16 Seth Lauzon <seth.lauzon@gmail.com>
* AccessControl.pm (parse_domains): borrowed code from Storage.pm to make it
less fragile to syntax errors in the domains.cfg file
* AccessControl.pm: implemented LDAP authentication
* AccessControl.pm: added four second timeout on authentication failure for
user_authentication_ldap and user_authentication_ad
2010-07-14 Proxmox Support Team <support@proxmox.com>
* AccessControl.pm (ldap_bind): rename to authenticate_user_ad (AD
only)
(load_domains_config): return a reference to an array (not the
array itself)
(parse_config): return a reference to an array (not the array
itself)
(authenticate_user_domain): restructure code - this is no the
centralized interface for authenticationn
(authenticate_user_domain): add 'shadow' and 'PAM' default entries
if there is no configuration for them in domain.cfg
(authenticate_user_shadow): renamed from authenticate_user_pve
* control.in (Depends): add libnet-ldap-perl
2010-07-14 Seth Lauzon <seth.lauzon@gmail.com>A
* AccessControl.pm: implemented Active Directory authentication
2010-07-09 Seth Lauzon <seth.lauzon@gmail.com>
* AccessControl.pm (modify_acl): check if role exists
2010-07-08 Proxmox Support Team <support@proxmox.com>
* pveum (print_usage): improve usage text.
2010-07-08 Seth Lauzon <seth.lauzon@gmail.com>
* AccessControl.pm: modify/delete ACL functionality
* pveum (aclmod): Add/Modify ACL
(acldel): Delete ACL
2010-07-07 Proxmox Support Team <support@proxmox.com>
* AccessControl.pm: implemented shadowauthentication (add/modify/delete/verify)
with file locking (Seth)
(encrypt_pw): use SHA256 to crypt passwords
(save_shadow_config): change mode to 0600, store to /etc/pve/auth/shadow.cfg
(parse_shadow): simplify code - there is no need to trim strings. Instead check for
correct format.
* test/auth-test.pl: program for testing authentication methods (Seth)
* pveum (read_password): added confirm password
2010-07-05 Proxmox Support Team <support@proxmox.com>
* AccessControl.pm (modify_user): remove call to change_password()
- not neccessary at all (Seth)
* AccessControl.pm: cleanup - remove space in function calls(Seth)
2010-07-02 Proxmox Support Team <support@proxmox.com>
* AccessControl.pm (lock_user_config): renamed from lock_config,
because we will have more then one config file (auth.conf, shadow
password, ...)
(modify_user): check for exceptions after lock_user_config()
(delete_user): check for exceptions after lock_user_config(),
raise invalid characters exception
(delete_group): check for exceptions after lock_user_config(),
raise invalid characters exception
(modify_role): check for exceptions after lock_user_config()
(delete_role): check for exceptions after lock_user_config(),
raise invalid characters exception
(verify_username): add $noerr parameter, raise exeption if
user name contain invalid characters and $noerr is not set
(verify_groupname): add $noerr parameter, raise exeption if
group name contain invalid characters and $noerr is not set
(verify_rolename): add $noerr parameter, raise exeption if
role name contain invalid characters and $noerr is not set
2010-07-01 Proxmox Support Team <support@proxmox.com>
* AccessControl.pm: implemented file locking functionality for all
processes that make modifications to configuration file (Seth) -
code for lock_file() was copied from QemuServer.pm.
2010-06-29 Proxmox Support Team <support@proxmox.com>
* pveum: new roleadd/rolemod/roledel (Seth)
* AccessControl.pm (modify_role): create role and modify privileges (Seth)
* AccessControl.pm (delete_role): delete role functionality (Seth)
2010-06-28 Proxmox Support Team <support@proxmox.com>
* pveum: new groupadd/groupdel (patch from Seth)
* AccessControl.pm (add_user): moved functionality to modify_user and
removed subroutine (Seth)
* pveum: useradd command no longer requires a password and now uses
modify_user (Seth)
2010-06-25 Proxmox Support Team <support@proxmox.com>
* AccessControl.pm (modify_user): include patch from Seth
2010-06-24 Proxmox Support Team <support@proxmox.com>
* test/perm-test1.pl (check_permission): a first regression test
* test/user.cfg.ex1: add another example - for use by regression
tests
* test/dump-perm.pl: print permission as nice list, add ability to
specify usr.cfg file
2010-06-23 Proxmox Support Team <support@proxmox.com>
* pveum: implement some simple functions (add user, create ticket)
* pveum-pl: rename to pveum
* pveum.c: remove suexec code - we will use a daemon instead
* pvesh: removed (dead code)
* test/dump-perm.pl: simple script to dump permissions
* test/: created new directory for test skripts
* test/dump-users.pl: simple script to dump user table
2010-06-22 Proxmox Support Team <support@proxmox.com>
* AccessControl.pm (add_user): Updated "valid_privs" with new
permissions from readme (Seth)
2010-06-21 Proxmox Support Team <support@proxmox.com>
* copyright: change license to AGPL
2010-03-17 Proxmox Support Team <support@proxmox.com>
* pveum-pl: move all priviledged function to this file.
2009-07-09 Proxmox Support Team <support@proxmox.com>
* pveum: added dummy binary

68
Makefile Normal file
View File

@ -0,0 +1,68 @@
RELEASE=2.0
VERSION=1.0
PACKAGE=libpve-access-control
PKGREL=1
DESTDIR=
PREFIX=/usr
BINDIR=${PREFIX}/bin
SBINDIR=${PREFIX}/sbin
MANDIR=${PREFIX}/share/man
DOCDIR=${PREFIX}/share/doc
MAN1DIR=${MANDIR}/man1/
export PERLDIR=${PREFIX}/share/perl5
ARCH:=$(shell dpkg-architecture -qDEB_BUILD_ARCH)
DEB=${PACKAGE}_${VERSION}-${PKGREL}_${ARCH}.deb
all: ${DEB}
.PHONY: dinstall
dinstall: deb
dpkg -i ${DEB}
.PHONY: install
install:
install -d ${DESTDIR}${BINDIR}
install -d ${DESTDIR}${SBINDIR}
install -m 0755 pveum ${DESTDIR}${SBINDIR}
make -C PVE install
perl -I. ./pveum verifyapi
install -d ${DESTDIR}/usr/share/man/man1
pod2man -n pveum -s 1 -r "proxmox 2.0" -c "Proxmox Documentation" <pveum | gzip -9 > ${DESTDIR}/usr/share/man/man1/pveum.1.gz
.PHONY: deb ${DEB}
deb ${DEB}:
rm -rf debian
mkdir debian
make DESTDIR=${CURDIR}/debian install
install -d -m 0755 debian/DEBIAN
sed -e s/@@VERSION@@/${VERSION}/ -e s/@@PKGRELEASE@@/${PKGREL}/ -e s/@@ARCH@@/${ARCH}/ <control.in >debian/DEBIAN/control
install -D -m 0644 copyright debian/${DOCDIR}/${PACKAGE}/copyright
install -m 0644 changelog.Debian debian/${DOCDIR}/${PACKAGE}/
gzip -9 debian/${DOCDIR}/${PACKAGE}/changelog.Debian
install -m 0644 ChangeLog debian/${DOCDIR}/${PACKAGE}/changelog
gzip -9 debian/${DOCDIR}/${PACKAGE}/changelog
dpkg-deb --build debian
mv debian.deb ${DEB}
#rm -rf debian
lintian ${DEB}
.PHONY: upload
upload: ${DEB}
umount /pve/${RELEASE}; mount /pve/${RELEASE} -o rw
mkdir -p /pve/${RELEASE}/extra
rm -f /pve/${RELEASE}/extra/${PACKAGE}_*.deb
rm -f /pve/${RELEASE}/extra/Packages*
cp ${DEB} /pve/${RELEASE}/extra
cd /pve/${RELEASE}/extra; dpkg-scanpackages . /dev/null > Packages; gzip -9c Packages > Packages.gz
umount /pve/${RELEASE}; mount /pve/${RELEASE} -o ro
.PHONY: clean
clean:
rm -rf debian *~ *.deb ${PACKAGE}-*.tar.gz
find . -name '*~' -exec rm {} ';'
.PHONY: distclean
distclean: clean

170
PVE/API2/ACL.pm Normal file
View File

@ -0,0 +1,170 @@
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::SafeSyslog;
use Data::Dumper; # fixme: remove
use PVE::RESTHandler;
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
name => 'read_acl',
path => '',
method => 'GET',
description => "Get Access Control List (ACLs).",
parameters => {
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
items => {
type => "object",
additionalProperties => 0,
properties => {
path => { type => 'string' },
type => { type => 'string', enum => ['user', 'group'] },
ugid => { type => 'string' },
roleid => { type => 'string' },
propagate => { type => 'boolean' },
},
},
},
code => sub {
my ($param) = @_;
my $res = [];
my $usercfg = cfs_read_file("user.cfg");
if (!$usercfg || !$usercfg->{acl}) {
return {};
}
my $acl = $usercfg->{acl};
foreach my $path (keys %$acl) {
foreach my $type (qw(users groups)) {
my $d = $acl->{$path}->{$type};
next if !$d;
foreach my $id (keys %$d) {
foreach my $role (keys %{$d->{$id}}) {
my $propagate = $d->{$id}->{$role};
push @$res, {
path => $path,
type => $type eq 'groups' ? 'group' : 'user',
ugid => $id,
roleid => $role,
propagate => $propagate,
};
}
}
}
}
return $res;
}});
__PACKAGE__->register_method ({
name => 'update_acl',
protected => 1,
path => '',
method => 'PUT',
description => "Update Access Control List (add or remove permissions).",
parameters => {
additionalProperties => 0,
properties => {
path => {
description => "Access control path",
type => 'string',
},
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,
},
roles => {
description => "List of roles.",
type => 'string', format => 'pve-roleid-list',
},
propagate => {
description => "Allow to propagate (inherit) permissions.",
type => 'boolean',
optional => 1,
},
delete => {
description => "Remove permissions (instead of adding it).",
type => 'boolean',
optional => 1,
},
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
if (!($param->{users} || $param->{groups})) {
raise_param_exc({
users => "either 'users' or 'groups' is required.",
groups => "either 'users' or 'groups' is required." });
}
my $path = PVE::AccessControl::normalize_path($param->{path});
raise_param_exc({ path => "invalid ACL path '$param->{path}'" }) if !$path;
PVE::AccessControl::lock_user_config(
sub {
my $cfg = cfs_read_file("user.cfg");
my $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;
}
}
}
cfs_write_file("user.cfg", $cfg);
}, "ACL update failed");
return undef;
}});
1;

175
PVE/API2/AccessControl.pm Normal file
View File

@ -0,0 +1,175 @@
package PVE::API2::AccessControl;
use strict;
use warnings;
use PVE::SafeSyslog;
use PVE::RPCEnvironment;
use PVE::Cluster;
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 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.",
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' };
return $res;
}});
__PACKAGE__->register_method ({
name => 'create_ticket',
path => 'ticket',
method => 'POST',
permissions => { user => 'world' },
protected => 1, # else we can't access shadow files
description => "Create authentication ticket.",
parameters => {
additionalProperties => 0,
properties => {
username => {
description => "User name",
type => 'string',
maxLength => 64,
},
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}),
password => {
description => "The secret password. This can also be a valid ticket.",
type => 'string',
},
path => {
description => "Only create ticket if user have access 'privs' on 'path'",
type => 'string',
requires => 'privs',
optional => 1,
maxLength => 64,
},
privs => {
description => "Only create ticket if user have access 'privs' on 'path'",
type => 'string' , format => 'pve-priv-list',
requires => 'path',
optional => 1,
maxLength => 64,
},
}
},
returns => {
type => "object",
properties => {
ticket => { type => 'string' },
username => { type => 'string' },
CSRFPreventionToken => { type => 'string' },
}
},
code => sub {
my ($param) = @_;
my $username = $param->{username};
$username .= "\@$param->{realm}" if $param->{realm};
my $rpcenv = PVE::RPCEnvironment::get();
my $clientip = $rpcenv->get_client_ip() || '';
my $ticket;
my $token;
eval {
if ($param->{path} && $param->{privs}) {
my $privs = [ PVE::Tools::split_list($param->{privs}) ];
my $path = PVE::AccessControl::normalize_path($param->{path});
if (!($path && scalar(@$privs) && $rpcenv->check($username, $path, $privs))) {
die "no permission ($param->{path}, $param->{privs})\n";
}
}
my $tmp;
if (($tmp = PVE::AccessControl::verify_ticket($param->{password}, 1)) &&
($tmp eq $username)) {
# got valid ticket
} else {
$username = PVE::AccessControl::authenticate_user($username, $param->{password});
}
$ticket = PVE::AccessControl::assemble_ticket($username);
$token = PVE::AccessControl::assemble_csrf_prevention_token($username);
};
if (my $err = $@) {
syslog('err', "authentication failure; rhost=$clientip user=$username msg=$err");
die $err;
}
PVE::Cluster::log_msg('info', 'root@pam', "successful auth for user '$username'");
return {
ticket => $ticket,
username => $username,
CSRFPreventionToken => $token,
};
}});
1;

304
PVE/API2/Domains.pm Normal file
View File

@ -0,0 +1,304 @@
package PVE::API2::Domains;
use strict;
use warnings;
use PVE::Cluster qw (cfs_read_file cfs_write_file);
use PVE::AccessControl;
use PVE::JSONSchema qw(get_standard_option);
use PVE::SafeSyslog;
use Data::Dumper; # fixme: remove
use PVE::RESTHandler;
my $domainconfigfile = "domains.cfg";
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
name => 'index',
path => '',
method => 'GET',
description => "Authentication domain index.",
permissions => { user => 'world' },
parameters => {
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
realm => { type => 'string' },
comment => { type => 'string', optional => 1 },
},
},
links => [ { rel => 'child', href => "{realm}" } ],
},
code => sub {
my ($param) = @_;
my $res = [];
my $cfg = cfs_read_file($domainconfigfile);
foreach my $realm (keys %$cfg) {
my $d = $cfg->{$realm};
my $entry = { realm => $realm, type => $d->{type} };
$entry->{comment} = $d->{comment} if $d->{comment};
$entry->{default} = 1 if $d->{default};
push @$res, $entry;
}
return $res;
}});
__PACKAGE__->register_method ({
name => 'create',
protected => 1,
path => '',
method => 'POST',
description => "Add an authentication server.",
parameters => {
additionalProperties => 0,
properties => {
realm => get_standard_option('realm'),
type => {
description => "Server type.",
type => 'string',
enum => [ 'ad', 'ldap' ],
},
server1 => {
description => "Server IP address (or DNS name)",
type => 'string',
},
server2 => {
description => "Fallback Server IP address (or DNS name)",
type => 'string',
optional => 1,
},
secure => {
description => "Use secure LDAPS protocol.",
type => 'boolean',
optional => 1,
},
default => {
description => "Use this as default realm",
type => 'boolean',
optional => 1,
},
comment => {
type => 'string',
optional => 1,
},
port => {
description => "Server port",
type => 'integer',
minimum => 1,
maximum => 65535,
optional => 1,
},
base_dn => {
description => "LDAP base domain name",
type => 'string',
optional => 1,
},
user_attr => {
description => "LDAP user attribute name",
type => 'string',
optional => 1,
},
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
PVE::AccessControl::lock_domain_config(
sub {
my $cfg = cfs_read_file($domainconfigfile);
my $realm = $param->{realm};
die "domain '$realm' already exists\n"
if $cfg->{$realm};
die "unable to use reserved name '$realm'\n"
if ($realm eq 'pam' || $realm eq 'pve');
if (defined($param->{secure})) {
$cfg->{$realm}->{secure} = $param->{secure} ? 1 : 0;
}
if ($param->{default}) {
foreach my $r (keys %$cfg) {
delete $cfg->{$r}->{default};
}
}
foreach my $p (keys %$param) {
next if $p eq 'realm';
$cfg->{$realm}->{$p} = $param->{$p};
}
cfs_write_file($domainconfigfile, $cfg);
}, "add auth server failed");
return undef;
}});
__PACKAGE__->register_method ({
name => 'update',
path => '{realm}',
method => 'PUT',
description => "Update authentication server settings.",
protected => 1,
parameters => {
additionalProperties => 0,
properties => {
realm => get_standard_option('realm'),
server1 => {
description => "Server IP address (or DNS name)",
type => 'string',
optional => 1,
},
server2 => {
description => "Fallback Server IP address (or DNS name)",
type => 'string',
optional => 1,
},
secure => {
description => "Use secure LDAPS protocol.",
type => 'boolean',
optional => 1,
},
default => {
description => "Use this as default realm",
type => 'boolean',
optional => 1,
},
comment => {
type => 'string',
optional => 1,
},
port => {
description => "Server port",
type => 'integer',
minimum => 1,
maximum => 65535,
optional => 1,
},
base_dn => {
description => "LDAP base domain name",
type => 'string',
optional => 1,
},
user_attr => {
description => "LDAP user attribute name",
type => 'string',
optional => 1,
},
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
PVE::AccessControl::lock_domain_config(
sub {
my $cfg = cfs_read_file($domainconfigfile);
my $realm = $param->{realm};
delete $param->{realm};
die "unable to modify bultin domain '$realm'\n"
if ($realm eq 'pam' || $realm eq 'pve');
die "domain '$realm' does not exist\n"
if !$cfg->{$realm};
if (defined($param->{secure})) {
$cfg->{$realm}->{secure} = $param->{secure} ? 1 : 0;
}
if ($param->{default}) {
foreach my $r (keys %$cfg) {
delete $cfg->{$r}->{default};
}
}
foreach my $p (keys %$param) {
$cfg->{$realm}->{$p} = $param->{$p};
}
cfs_write_file($domainconfigfile, $cfg);
}, "update auth server failed");
return undef;
}});
# fixme: return format!
__PACKAGE__->register_method ({
name => 'read',
path => '{realm}',
method => 'GET',
description => "Get auth server configuration.",
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->{$realm};
die "domain '$realm' does not exist\n" if !$data;
return $data;
}});
__PACKAGE__->register_method ({
name => 'delete',
path => '{realm}',
method => 'DELETE',
description => "Delete an authentication server.",
protected => 1,
parameters => {
additionalProperties => 0,
properties => {
realm => get_standard_option('realm'),
}
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
PVE::AccessControl::lock_user_config(
sub {
my $cfg = cfs_read_file($domainconfigfile);
my $realm = $param->{realm};
die "domain '$realm' does not exist\n" if !$cfg->{$realm};
delete $cfg->{$realm};
cfs_write_file($domainconfigfile, $cfg);
}, "delete auth server failed");
return undef;
}});
1;

206
PVE/API2/Group.pm Normal file
View File

@ -0,0 +1,206 @@
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 Data::Dumper; # fixme: remove
use PVE::RESTHandler;
use base qw(PVE::RESTHandler);
my $extract_group_data = sub {
my ($data, $full) = @_;
my $res = {};
$res->{comment} = $data->{comment} if defined($data->{comment});
return $res if !$full;
$res->{users} = $data->{users} ? [ keys %{$data->{users}} ] : [];
return $res;
};
# fixme: index should return more/all attributes?
__PACKAGE__->register_method ({
name => 'index',
path => '',
method => 'GET',
description => "Group index.",
parameters => {
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
groupid => { type => 'string' },
},
},
links => [ { rel => 'child', href => "{groupid}" } ],
},
code => sub {
my ($param) = @_;
my $res = [];
my $usercfg = cfs_read_file("user.cfg");
foreach my $group (keys %{$usercfg->{groups}}) {
my $entry = &$extract_group_data($usercfg->{groups}->{$group});
$entry->{groupid} = $group;
push @$res, $entry;
}
return $res;
}});
__PACKAGE__->register_method ({
name => 'create_group',
protected => 1,
path => '',
method => 'POST',
description => "Create new group.",
parameters => {
additionalProperties => 0,
properties => {
groupid => { type => 'string', format => 'pve-groupid' },
comment => { type => 'string', optional => 1 },
},
},
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',
description => "Update group data.",
parameters => {
additionalProperties => 0,
properties => {
# fixme: set/delete members
groupid => { type => 'string', format => 'pve-groupid' },
comment => { type => 'string', optional => 1 },
},
},
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 $param->{comment};
cfs_write_file("user.cfg", $usercfg);
}, "create group failed");
return undef;
}});
# fixme: return format!
__PACKAGE__->register_method ({
name => 'read_group',
path => '{groupid}',
method => 'GET',
description => "Get group configuration.",
parameters => {
additionalProperties => 0,
properties => {
groupid => { type => 'string', format => 'pve-groupid' },
},
},
returns => {},
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;
return &$extract_group_data($data, 1);
}});
__PACKAGE__->register_method ({
name => 'delete_group',
protected => 1,
path => '{groupid}',
method => 'DELETE',
description => "Delete group.",
parameters => {
additionalProperties => 0,
properties => {
groupid => { type => 'string' , format => 'pve-groupid' },
}
},
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;

12
PVE/API2/Makefile Normal file
View File

@ -0,0 +1,12 @@
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

193
PVE/API2/Role.pm Normal file
View File

@ -0,0 +1,193 @@
package PVE::API2::Role;
use strict;
use warnings;
use PVE::Cluster qw (cfs_read_file cfs_write_file);
use PVE::AccessControl;
use PVE::SafeSyslog;
use Data::Dumper; # fixme: remove
use PVE::RESTHandler;
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
name => 'index',
path => '',
method => 'GET',
description => "Role index.",
parameters => {
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
roleid => { type => 'string' },
},
},
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 };
}
return $res;
}});
__PACKAGE__->register_method ({
name => 'create_role',
protected => 1,
path => '',
method => 'POST',
description => "Create new role.",
parameters => {
additionalProperties => 0,
properties => {
roleid => { type => 'string', format => 'pve-roleid' },
privs => { type => 'string' , format => 'pve-priv-list', optional => 1 },
},
},
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',
description => "Create new role.",
parameters => {
additionalProperties => 0,
properties => {
roleid => { type => 'string', format => 'pve-roleid' },
privs => { type => 'string' , format => 'pve-priv-list' },
append => {
type => 'boolean',
optional => 1,
requires => 'privs',
},
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
PVE::AccessControl::lock_user_config(
sub {
my $role = $param->{roleid};
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;
}});
# fixme: return format!
__PACKAGE__->register_method ({
name => 'read_role',
path => '{roleid}',
method => 'GET',
description => "Get role configuration.",
parameters => {
additionalProperties => 0,
properties => {
roleid => { type => 'string' , format => 'pve-roleid' },
},
},
returns => {},
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',
description => "Delete role.",
parameters => {
additionalProperties => 0,
properties => {
roleid => { type => 'string', format => 'pve-roleid' },
}
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
PVE::AccessControl::lock_user_config(
sub {
my $role = $param->{roleid};
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;

300
PVE/API2/User.pm Normal file
View File

@ -0,0 +1,300 @@
package PVE::API2::User;
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::JSONSchema qw(get_standard_option);
use PVE::SafeSyslog;
use Data::Dumper; # fixme: remove
use PVE::RESTHandler;
use base qw(PVE::RESTHandler);
my $extract_user_data = sub {
my ($data, $full) = @_;
my $res = {};
foreach my $prop (qw(enable expire firstname lastname email comment)) {
$res->{$prop} = $data->{$prop} if defined($data->{$prop});
}
return $res if !$full;
$res->{groups} = $data->{groups} ? [ keys %{$data->{groups}} ] : [];
return $res;
};
__PACKAGE__->register_method ({
name => 'index',
path => '',
method => 'GET',
description => "User index.",
parameters => {
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
userid => { type => 'string' },
},
},
links => [ { rel => 'child', href => "{userid}" } ],
},
code => sub {
my ($param) = @_;
my $res = [];
my $usercfg = cfs_read_file("user.cfg");
foreach my $user (keys %{$usercfg->{users}}) {
next if $user eq 'root';
my $entry = &$extract_user_data($usercfg->{users}->{$user});
$entry->{userid} = $user;
push @$res, $entry;
}
return $res;
}});
__PACKAGE__->register_method ({
name => 'create_user',
protected => 1,
path => '',
method => 'POST',
description => "Create new user.",
parameters => {
additionalProperties => 0,
properties => {
userid => get_standard_option('userid'),
password => { type => 'string', optional => 1 },
groups => { type => 'string', optional => 1, format => 'pve-groupid-list'},
firstname => { type => 'string', optional => 1 },
lastname => { type => 'string', optional => 1 },
email => { type => 'string', optional => 1, format => 'email-opt' },
comment => { type => 'string', optional => 1 },
expire => {
description => "Account expiration date (seconds since epoch). '0' means no expiration date.",
type => 'integer',
minimum => 0,
optional => 1,
},
enable => {
description => "Enable the account (default). You can set this to '0' to disable the accout",
type => 'boolean',
optional => 1,
default => 1,
},
},
},
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");
die "user '$username' already exists\n"
if $usercfg->{users}->{$username};
PVE::AccessControl::domain_set_password($realm, $ruid, $param->{password})
if $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};
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.",
parameters => {
additionalProperties => 0,
properties => {
userid => get_standard_option('userid'),
},
},
returns => {
additionalProperties => 0,
properties => {
enable => { type => 'boolean' },
expire => { type => 'integer', optional => 1 },
firstname => { type => 'string', optional => 1 },
lastname => { type => 'string', optional => 1 },
email => { type => 'string', optional => 1 },
comment => { type => 'string', optional => 1 },
groups => { type => 'array' },
}
},
code => sub {
my ($param) = @_;
my ($username, undef, $domain) =
PVE::AccessControl::verify_username($param->{userid});
my $usercfg = cfs_read_file("user.cfg");
my $data = $usercfg->{users}->{$username};
die "user '$username' does not exist\n" if !$data;
return &$extract_user_data($data, 1);
}});
__PACKAGE__->register_method ({
name => 'update_user',
protected => 1,
path => '{userid}',
method => 'PUT',
description => "Update user configuration.",
parameters => {
additionalProperties => 0,
properties => {
userid => get_standard_option('userid'),
password => { type => 'string', optional => 1 },
groups => { type => 'string', optional => 1, format => 'pve-groupid-list' },
append => {
type => 'boolean',
optional => 1,
requires => 'groups',
},
enable => {
description => "Enable/disable the account.",
type => 'boolean',
optional => 1,
},
firstname => { type => 'string', optional => 1 },
lastname => { type => 'string', optional => 1 },
email => { type => 'string', optional => 1, format => 'email-opt' },
comment => { type => 'string', optional => 1 },
expire => {
description => "Account expiration date (seconds since epoch). '0' means no expiration date.",
type => 'integer',
minimum => 0,
optional => 1
},
},
},
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");
die "user '$username' does not exist\n"
if !$usercfg->{users}->{$username};
PVE::AccessControl::domain_set_password($realm, $ruid, $param->{password})
if $param->{password};
$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} && $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});
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.",
parameters => {
additionalProperties => 0,
properties => {
userid => get_standard_option('userid'),
}
},
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");
die "user '$username' does not exist\n"
if !$usercfg->{users}->{$username};
delete ($usercfg->{users}->{$username});
PVE::AccessControl::delete_shadow_password($ruid) if $realm eq 'pve';
PVE::AccessControl::delete_user_group($username, $usercfg);
PVE::AccessControl::delete_user_acl($username, $usercfg);
cfs_write_file("user.cfg", $usercfg);
}, "delete user failed");
return undef;
}});
1;

1235
PVE/AccessControl.pm Normal file

File diff suppressed because it is too large Load Diff

7
PVE/Makefile Normal file
View File

@ -0,0 +1,7 @@
.PHONY: install
install:
install -D -m 0644 AccessControl.pm ${DESTDIR}${PERLDIR}/PVE/AccessControl.pm
install -D -m 0644 RPCEnvironment.pm ${DESTDIR}${PERLDIR}/PVE/RPCEnvironment.pm
make -C API2 install

672
PVE/RPCEnvironment.pm Normal file
View File

@ -0,0 +1,672 @@
package PVE::RPCEnvironment;
use strict;
use warnings;
use POSIX ":sys_wait_h";
use IO::File;
use Fcntl qw(:flock);
use PVE::SafeSyslog;
use PVE::Tools;
use PVE::INotify;
use PVE::Cluster;
use PVE::ProcFSTools;
use PVE::AccessControl;
# we use this singleton class to pass RPC related environment values
my $pve_env;
# save $SIG{CHLD} handler implementation.
# simply set $SIG{CHLD} = $worker_reaper;
# and register forked processes with &$register_worker(pid)
# Note: using $SIG{CHLD} = 'IGNORE' or $SIG{CHLD} = sub { wait (); } or ...
# has serious side effects, because perls built in system() and open()
# functions can't get the correct exit status of a child. So we cant use
# that (also see perlipc)
my $WORKER_PIDS;
my $log_task_result = sub {
my ($upid, $user, $status) = @_;
my $msg = 'successful';
my $pri = 'info';
if ($status != 0) {
my $ec = $status >> 8;
my $ic = $status & 255;
$msg = $ec ? "failed ($ec)" : "interrupted ($ic)";
$pri = 'err';
}
my $tlist = active_workers($upid);
PVE::Cluster::broadcast_tasklist($tlist);
my $task;
foreach my $t (@$tlist) {
if ($t->{upid} eq $upid) {
$task = $t;
last;
}
}
if ($task && $task->{status}) {
$msg = $task->{status};
}
PVE::Cluster::log_msg($pri, $user, "end task $upid $msg");
};
my $worker_reaper = sub {
local $!; local $?;
foreach my $pid (keys %$WORKER_PIDS) {
my $waitpid = waitpid ($pid, WNOHANG);
if (defined($waitpid) && ($waitpid == $pid)) {
my $info = $WORKER_PIDS->{$pid};
if ($info && $info->{upid} && $info->{user}) {
&$log_task_result($info->{upid}, $info->{user}, $?);
}
delete ($WORKER_PIDS->{$pid});
}
}
};
my $register_worker = sub {
my ($pid, $user, $upid) = @_;
return if !$pid;
# do not register if already finished
my $waitpid = waitpid ($pid, WNOHANG);
if (defined($waitpid) && ($waitpid == $pid)) {
delete ($WORKER_PIDS->{$pid});
return;
}
$WORKER_PIDS->{$pid} = {
user => $user,
upid => $upid,
};
};
# ACL cache
my $compile_acl = sub {
my ($self, $user) = @_;
my $res = {};
my $cfg = $self->{user_cfg};
return undef if !$cfg->{roles};
if ($user eq 'root@pam') { # root can do anything
return {'/' => $cfg->{roles}->{'Administrator'}};
}
foreach my $path (sort keys %{$cfg->{acl}}) {
my @ra = PVE::AccessControl::roles($cfg, $user, $path);
my $privs = {};
foreach my $role (@ra) {
if (my $privset = $cfg->{roles}->{$role}) {
foreach my $p (keys %$privset) {
$privs->{$p} = 1;
}
}
}
$res->{$path} = $privs;
}
return $res;
};
sub permissions {
my ($self, $user, $path) = @_;
$user = PVE::AccessControl::verify_username($user, 1);
return {} if !$user;
my $cache = $self->{aclcache};
my $acl = $cache->{$user};
if (!$acl) {
if (!($acl = &$compile_acl($self, $user))) {
return {};
}
$cache->{$user} = $acl;
}
my $perm;
if (!($perm = $acl->{$path})) {
$perm = {};
foreach my $p (sort keys %$acl) {
my $final = ($path eq $p);
next if !(($p eq '/') || $final || ($path =~ m|^$p/|));
$perm = $acl->{$p};
}
$acl->{$path} = $perm;
}
return $perm;
}
sub check {
my ($self, $user, $path, $privs) = @_;
my $perm = $self->permissions($user, $path);
foreach my $priv (@$privs) {
return undef if !$perm->{$priv};
};
return 1;
};
sub user_enabled {
my ($self, $user) = @_;
my $cfg = $self->{user_cfg};
return PVE::AccessControl::user_enabled($cfg, $user);
}
# initialize environment - must be called once at program startup
sub init {
my ($class, $type, %params) = @_;
$class = ref($class) || $class;
die "already initialized" if $pve_env;
die "unknown environment type" if !$type || $type !~ m/^(cli|pub|priv)$/;
$SIG{CHLD} = $worker_reaper;
# environment types
# cli ... command started fron command line
# pub ... access from public server (apache)
# priv ... access from private server (pvedaemon)
my $self = {
user_cfg => {},
aclcache => {},
aclversion => undef,
type => $type,
};
bless $self, $class;
foreach my $p (keys %params) {
if ($p eq 'atfork') {
$self->{$p} = $params{$p};
} else {
die "unknown option '$p'";
}
}
$pve_env = $self;
my ($sysname, $nodename) = POSIX::uname();
$nodename =~ s/\..*$//; # strip domain part, if any
$self->{nodename} = $nodename;
return $self;
};
# get the singleton
sub get {
die "not initialized" if !$pve_env;
return $pve_env;
}
# init_request - must be called before each RPC request
sub init_request {
my ($self, %params) = @_;
PVE::Cluster::cfs_update();
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";
}
}
sub set_client_ip {
my ($self, $ip) = @_;
$self->{client_ip} = $ip;
}
sub get_client_ip {
my ($self) = @_;
return $self->{client_ip};
}
sub set_result_count {
my ($self, $count) = @_;
$self->{result_count} = $count;
}
sub get_result_count {
my ($self) = @_;
return $self->{result_count};
}
sub set_language {
my ($self, $lang) = @_;
# fixme: initialize I18N
$self->{language} = $lang;
}
sub get_language {
my ($self) = @_;
return $self->{language};
}
sub set_user {
my ($self, $user) = @_;
# fixme: get ACLs
$self->{user} = $user;
}
sub get_user {
my ($self) = @_;
die "user name not set\n" if !$self->{user};
return $self->{user};
}
# read/update list of active workers
# we move all finished tasks to the archive index,
# but keep aktive and most recent task in the active file.
sub active_workers {
my ($new_upid) = @_;
my $lkfn = "/var/log/pve/tasks/.active.lock";
my $timeout = 10;
my $code = sub {
my $tasklist = PVE::INotify::read_file('active');
my @ta;
my $tlist = [];
my $thash = {}; # only list task once
my $check_task = sub {
my ($task) = @_;
my $pstart = PVE::ProcFSTools::read_proc_starttime($task->{pid});
if ($pstart && ($pstart == $task->{pstart})) {
push @$tlist, $task;
} else {
delete $task->{pid};
push @ta, $task;
}
delete $task->{pstart};
};
foreach my $task (@$tasklist) {
my $upid = $task->{upid};
next if $thash->{$upid};
$thash->{$upid} = $task;
&$check_task($task);
}
if ($new_upid && !(my $task = $thash->{$new_upid})) {
$task = PVE::Tools::upid_decode($new_upid);
$task->{upid} = $new_upid;
$thash->{$new_upid} = $task;
&$check_task($task);
}
@ta = sort { $b->{starttime} cmp $a->{starttime} } @ta;
my $save = defined($new_upid);
foreach my $task (@ta) {
next if $task->{endtime};
$task->{endtime} = time();
$task->{status} = PVE::Tools::upid_read_status($task->{upid});
$save = 1;
}
my $archive = '';
my @arlist = ();
foreach my $task (@ta) {
if (!$task->{saved}) {
$archive .= sprintf("$task->{upid} %08X $task->{status}\n", $task->{endtime});
$save = 1;
push @arlist, $task;
$task->{saved} = 1;
}
}
if ($archive) {
my $size = 0;
my $filename = "/var/log/pve/tasks/index";
eval {
my $fh = IO::File->new($filename, '>>', 0644) ||
die "unable to open file '$filename' - $!\n";
PVE::Tools::safe_print($filename, $fh, $archive);
$size = -s $fh;
close($fh) ||
die "unable to close file '$filename' - $!\n";
};
my $err = $@;
if ($err) {
syslog('err', $err);
foreach my $task (@arlist) { # mark as not saved
$task->{saved} = 0;
}
}
my $maxsize = 50000; # about 1000 entries
if ($size > $maxsize) {
rename($filename, "$filename.1");
}
}
# we try to reduce the amount of data
# list all running tasks and task and a few others
# try to limit to 25 tasks
my $ctime = time();
my $max = 25 - scalar(@$tlist);
foreach my $task (@ta) {
last if $max <= 0;
push @$tlist, $task;
$max--;
}
PVE::INotify::write_file('active', $tlist) if $save;
return $tlist;
};
my $res = PVE::Tools::lock_file($lkfn, $timeout, $code);
die $@ if $@;
return $res;
}
# start long running workers
# STDIN is redirected to /dev/null
# STDOUT,STDERR are redirected to the filename returned by upid_decode
# NOTE: we simulate running in foreground if ($self->{type} eq 'cli')
sub fork_worker {
my ($self, $dtype, $id, $user, $function) = @_;
$dtype = 'unknown' if !defined ($dtype);
$id = '' if !defined ($id);
$user = 'root@pve' if !defined ($user);
my $sync = $self->{type} eq 'cli' ? 1 : 0;
local $SIG{INT} =
local $SIG{QUIT} =
local $SIG{PIPE} =
local $SIG{TERM} = 'IGNORE';
my $starttime = time ();
my @psync = POSIX::pipe();
my @csync = POSIX::pipe();
my $node = $self->{nodename};
my $cpid = fork();
die "unable to fork worker - $!" if !defined($cpid);
my $workerpuid = $cpid ? $cpid : $$;
my $pstart = PVE::ProcFSTools::read_proc_starttime($workerpuid) ||
die "unable to read process start time";
my $upid = PVE::Tools::upid_encode ({
node => $node, pid => $workerpuid, pstart => $pstart,
starttime => $starttime, type => $dtype, id => $id, user => $user });
my $outfh;
if (!$cpid) { # child
$0 = "task $upid";
$SIG{INT} = $SIG{QUIT} = $SIG{TERM} = sub { die "received interrupt\n"; };
$SIG{CHLD} = $SIG{PIPE} = 'DEFAULT';
# set sess/process group - we want to be able to kill the
# whole process group
POSIX::setsid();
POSIX::close ($psync[0]);
POSIX::close ($csync[1]);
$outfh = $sync ? $psync[1] : undef;
eval {
PVE::INotify::inotify_close();
if (my $atfork = $self->{atfork}) {
&$atfork();
}
# same algorythm as used inside SA
# STDIN = /dev/null
my $fd = fileno (STDIN);
close STDIN;
POSIX::close(0) if $fd != 0;
die "unable to redirect STDIN - $!"
if !open(STDIN, "</dev/null");
$outfh = PVE::Tools::upid_open($upid) if !$sync;
# redirect STDOUT
$fd = fileno(STDOUT);
close STDOUT;
POSIX::close (1) if $fd != 1;
die "unable to redirect STDOUT - $!"
if !open(STDOUT, ">&", $outfh);
STDOUT->autoflush (1);
# redirect STDERR to STDOUT
$fd = fileno (STDERR);
close STDERR;
POSIX::close(2) if $fd != 2;
die "unable to redirect STDERR - $!"
if !open(STDERR, ">&1");
STDERR->autoflush(1);
};
if (my $err = $@) {
my $msg = "ERROR: $err";
POSIX::write($psync[1], $msg, length ($msg));
POSIX::close($psync[1]);
POSIX::_exit(1);
kill('KILL', $$);
}
# sync with parent (signal that we are read)
if ($sync) {
print "$upid\n";
} else {
POSIX::write($psync[1], $upid, length ($upid));
POSIX::close($psync[1]);
}
my $readbuf = '';
# sync with parent (wait until parent is ready)
POSIX::read($csync[0], $readbuf, 4096);
die "parent setup error\n" if $readbuf ne 'OK';
eval { &$function($upid); };
my $err = $@;
if ($err) {
chomp $err;
$err =~ s/\n/ /mg;
syslog('err', $err);
print STDERR "TASK ERROR: $err\n";
POSIX::_exit(-1);
} else {
print STDERR "TASK OK\n";
POSIX::_exit (0);
}
kill('KILL', $$);
}
# parent
POSIX::close ($psync[1]);
POSIX::close ($csync[0]);
my $readbuf = '';
# sync with child (wait until child starts)
POSIX::read($psync[0], $readbuf, 4096);
if (!$sync) {
POSIX::close($psync[0]);
&$register_worker($cpid, $user, $upid);
} else {
chomp $readbuf;
}
eval {
die "got no worker upid - start worker failed\n" if !$readbuf;
if ($readbuf =~ m/^ERROR:\s*(.+)$/m) {
die "starting worker failed: $1\n";
}
if ($readbuf ne $upid) {
die "got strange worker upid ('$readbuf' != '$upid') - start worker failed\n";
}
if ($sync) {
$outfh = PVE::Tools::upid_open($upid);
}
};
my $err = $@;
if (!$err) {
my $msg = 'OK';
POSIX::write($csync[1], $msg, length ($msg));
POSIX::close($csync[1]);
} else {
POSIX::close($csync[1]);
kill (9, $cpid); # make sure it gets killed
die $err;
}
PVE::Cluster::log_msg('info', $user, "starting task $upid");
my $tlist = active_workers($upid);
PVE::Cluster::broadcast_tasklist($tlist);
my $res = 0;
if ($sync) {
my $count;
my $outbuf = '';
eval {
local $SIG{INT} =
local $SIG{QUIT} =
local $SIG{TERM} = sub { die "got interrupt\n"; };
local $SIG{PIPE} = sub { die "broken pipe\n"; };
while (($count = POSIX::read($psync[0], $readbuf, 4096)) && ($count > 0)) {
$outbuf .= $readbuf;
while ($outbuf =~ s/^(([^\010\r\n]*)(\r|\n|(\010)+|\r\n))//s) {
my $line = $1;
my $data = $2;
if ($data =~ m/^TASK OK$/) {
# skip
} elsif ($data =~ m/^TASK ERROR: (.+)$/) {
print STDERR "$1\n";
} else {
print $line;
}
if ($outfh) {
print $outfh $line;
}
}
}
};
my $err = $@;
POSIX::close($psync[0]);
if ($outbuf) { # just to be sure
print $outbuf;
if ($outfh) {
print $outfh $outbuf;
}
}
if ($err) {
$err =~ s/\n/ /mg;
print STDERR "$err\n";
if ($outfh) {
print $outfh "TASK ERROR: $err\n";
}
kill (15, $cpid);
} else {
kill (9, $cpid); # make sure it gets killed
}
close($outfh);
waitpid ($cpid, 0);
$res = $?;
&$log_task_result($upid, $user, $res);
}
return wantarray ? ($upid, $res) : $upid;
}
1;

273
README Normal file
View File

@ -0,0 +1,273 @@
User Management and Access Control
==================================
Proxmox VE implements an easy but flexible way to manage users. A
powerful Access Control algorithm is used to grant permissions to
individual users or group of users.
Best Practices:
Use groups in ACLs (not individual users).
User Authentication
===================
Proxmox VE can use different authentication servers. Those
servers are listed in '/etc/pve/priv/domain.cfg', indexed by a unique
ID (called 'authentication domain' or 'realm').
User names need to be unique. We create unique names by adding the
'realm' to the user ID: <userid>@<realm>
File format 'domain.cfg'
----example domains.cfg ------------------
# an active directory server
AD: mycompany
server1 10.10.10.1
server2 10.10.10.2
...
# an LDAP server
LDAP: example.com
server1 10.10.10.2
....
------------------------------------------
There are 2 special authentication domains name 'pve' and 'pam':
* pve: stores paswords to "/etc/pve/priv/shadow.cfg" (SHA256 crypt);
* pam: use unix 'pam'
Proposed user database fields:
==============================
users:
login_name: email address (user@domain)
enable: 1 = TRUE, 0 = FALSE
expire: <integer> (account expiration date)
domid: reference to authentication domain
firstname: user first name
lastname: user last name
email: user's email address
comment: arbitrary comment
special user root: The root user has full administrative privileges
group:
group_name: the name of the group
user_list: list of login names
comment: a more verbose description
privileges:
defines rights required to execute actions or read
information.
VM.Allocate: create/remove new VM to server inventory
VM.Migrate: migrate VM to alternate server on cluster
VM.PowerMgmt: power management (start, stop, reset, shutdown, ...)
VM.Console: console access to VM
VM.Audit: view VM config
VM.Modify: modify VM config
Datastore.Allocate: create/remove/modify a data store.
Datastore.AllocateSpace: allocate space on a datastore
Datastore.Audit: view/browse a datastore
Permissions.Modify: modify access permissions
Sys.PowerMgmt: Node power management (start, stop, reset, shutdown, ...)
Sys.Console: console access to Node
Sys.Syslog: view Syslog
Sys.Audit: view node status/config
We may need to refine those in future - the following privs
are just examples:
VM.Create: create new VM to server inventory
VM.Remove: remove VM from inventory
VM.MemoryModify: modify memory associated with VM
VM.AddNewDisk: add new disk to VM
VM.AddExistingDisk: add an existing disk to VM
VM.DiskModify: modify disk space for associated VM
VM.UseRawDevice: associate a raw device with VM
VM.PowerOn: power on VM
VM.PowerOff: power off VM
VM.ConfigureCD: assign a device/image file to VM
VM.CpuModify: modify number of CPUs associated with VM
VM.CpuCyclesModify: modify CPU cycles for VM
VM.NetworkAdd: add network device to VM
VM.NetworkConfigure: configure network device associated with VM
VM.NetworkRemove: remove network device from VM
Network.AssignNetwork: assign system networks
role:
defines a sets of priviledges
predefined roles:
administrator: full administrative privileges
read_only: read only
no_access: no privileges
We store the following attribute for roles:
role_name: the name of the group
description: a more verbose description
privileges: list of privileges
permission:
Assign roles to users or groups.
ACL and Objects:
================
An access control list (ACL) is a list of permissions attached to an object. The list specifies who or what is allowed to access the object and what operations are allowed to be performed on the object.
Object: A Virtual machine, Network (bridge, venet), Hosts, Host Memory, Storage, ...
We can identify our objects by an unique (file system like) path, which also defines a tree like hierarchy relation. ACL can be inherited. Permissions are inherited if the propagate flag is set on the parent. Child permissions always overwrite inherited permissions. User permission takes precedence over all group permissions. If multiple group permission apply the resulting role is the union of all those group priviledges.
There is at most one object permission per user or group
We store the following attributes for ACLs:
propagate: propagate permissions down in the hierarchy
path: path to uniquely identify the object
user_or_group: ID of user or group (group ID start with @)
role: list of role IDs.
User Database:
To keep it simple, we suggest to use a single text file, which is replicated to all cluster nodes.
Also, we can store ACLs inside this file.
Here is a short example how such file could look like:
-----User/Group/Role Database example--------
user:joe@example.com:$1$nd91DtDy$mJtzWJAN2AAABKij0JgMy1/:Joe Average:Just a comment:
user:max@example.com:$1$nd91DtDy$LANSNJAN2AAABKidhfgMy3/:Max Mustermann:Another comment:
user:edward@example.com:$1$nd91DtDy$LANSNAAAAAAABKidhfgMy3/:Edward Example:Example VM Manager:
group:admin:Internal Administrator Group:root:
group:audit:Read only accounts used for audit::
group:customers:Our Customers:joe@example.com,max@example.com:
role:vm_user:Virtual Machine User:VM.ConfigureCD,VM.Console:
role:vm_manager:Virtual Machine Manager:VM.ConfigureCD,VM.Console,VM.AddNewDisk,VM.PowerOn,VM.PowerOff:
role:vm_operator:Virtual Machine Operator:VM.Create,VM.ConfigureCD,VM.Console,VM.AddNewDisk,VM.PowerOn,VM.PowerOff:
role:ds_consumer:DataStore Consumer:Datastore.AllocateSpace:
role:nw_consumer:Network Consumer:Network.AssignNetwork:
# group admin can do anything
acl:0:/:@admin:Administrator:
# group audit can view anything
acl:1:/:@audit:read_only:
# user max can manage all qemu/kvm machines
acl:1:/vm/qemu:max@example.com:vm_manager:
# user joe can use openvz vm 230
acl:1:/vm/openvz/230:joe@example.com:vm_user:
# user Edward can create openvz VMs using vmbr0 and store0
acl:1:/vm/openvz:edward@example.com:vm_operator:
acl:1:/network/vmbr0:edward@example.com:ds_consumer:
acl:1:/storage/store0:edward@example.com:nw_consumer:
---------------------------------------------
Basic model RBAC -> http://en.wikipedia.org/wiki/Role-based_access_control
# Subject: A person or automated agent
subject:joe@example.com:
subject:max@example.com:
# Role: Job function or title which defines an authority level
role:vm_user:Virtual Machine User:
role:admin:Administrator:
# Subject Assignment: Subject -> Role(s)
SA:vm_user:joe@example.com,max@example.com:
SA:admin:joe@example.com:
# Permissions: An approval of a mode of access to a resource
# Permission Assignment: Role -> Permissions (set of allowed operation)
perm:vm_user:VM.ConfigureCD,VM.Console:
perm:admin:VM.ConfigureCD,VM.Console,VM.Create:
---------------------------------------------
We can merge 'perm' into the 'role' table, because it is
a 1 -> 1 mapping
subject:joe@example.com:
subject:max@example.com:
role:vm_user:Virtual Machine User:VM.ConfigureCD,VM.Console:
role:admin:Administrator:VM.ConfigureCD,VM.Console,VM.Create:
SA:vm_user:joe@example.com,max@example.com:
SA:admin:joe@example.com:
-----------------------------------------------
We can have different subject assignment for different objects.
subject:joe@example.com:
subject:max@example.com:
role:vm_user:Virtual Machine User:VM.ConfigureCD,VM.Console:
role:admin:Administrator:VM.ConfigureCD,VM.Console,VM.Create:
# joe is 'admin' for openvz VMs, but 'vm_user' for qemu VMs
SA:/vm/openvz:admin:joe@example.com:
SA:/vm/qemu:vm_user:joe@example.com,max@example.com:
-----------------------------------------------
Let us use more convenient names.
Use 'user' instead of 'subject'.
Use 'acl' instead of 'SA'.
user:joe@example.com:
user:max@example.com:
role:vm_user:Virtual Machine User:VM.ConfigureCD,VM.Console:
role:admin:Administrator:VM.ConfigureCD,VM.Console,VM.Create:
# joe is 'admin' for openvz VMs, but 'vm_user' for qemu VMs
acl:/vm/openvz:admin:joe@example.com:
acl:/vm/qemu:vm_user:joe@example.com,max@example.com:
-----------------------------------------------
Finally introduce groups to group users. ACL can then
use 'users' or 'groups'.
user:joe@example.com:
user:max@example.com:
group:customers:Our Customers:joe@example.com,max@example.com:
role:vm_user:Virtual Machine User:VM.ConfigureCD,VM.Console:
role:admin:Administrator:VM.ConfigureCD,VM.Console,VM.Create:
acl:/vm/openvz:admin:joe@example.com:
acl:/vm/qemu:vm_user:@customers:
-----------------------------------------------

37
TODO Normal file
View File

@ -0,0 +1,37 @@
TODO: pve-access-control
------------------------
Seth?: Implement API Class to manage the domains.cfg file
(AuthDomains.pm)
pveum api:
Is it worth to emulate the useradd/usermod interface? We initially
done that because we thought users are common with that.
But now it would be possible to expose a 'REST' like interface - like
the one we use with pvesh.
pveum (get|set|create|delete) <path> [OPTIONS]
useradd: pveum create users/<username> [OPTIONS]
usermod: pveum set users/<username> [OPTIONS]
userdel: pveum delete users/<username>
list: pveum get users
data: pveum get users/<username>
groupadd: pveum create groups/<groupname> [OPTIONS]
groupmod: pveum set groups/<groupname> [OPTIONS]
groupdel: pveum delete groups/<groupname>
list: pveum get groups
data: pveum get groups/<groupname>
roleadd: pveum create roles/<rolename> [OPTIONS]
rolemod: pveum set roles/<rolename> [OPTIONS]
roledel: pveum delete roles/<rolename>
list: pveum get roles
data: pveum get roles/<rolename>
...

14
changelog.Debian Normal file
View File

@ -0,0 +1,14 @@
libpve-access-control (1.0-1) unstable; urgency=low
* allow '-' in permission paths
* bump version to 1.0
-- Proxmox Support Team <support@proxmox.com> Mon, 27 Jun 2011 13:51:48 +0200
libpve-access-control (0.1) unstable; urgency=low
* first dummy package - no functionality
-- Proxmox Support Team <support@proxmox.com> Thu, 09 Jul 2009 16:03:00 +0200

10
control.in Normal file
View File

@ -0,0 +1,10 @@
Package: libpve-access-control
Version: @@VERSION@@-@@PKGRELEASE@@
Section: perl
Priority: optional
Architecture: @@ARCH@@
Depends: libc6 (>= 2.3), perl (>= 5.6.0-16), libcrypt-openssl-rsa-perl, libcrypt-openssl-random-perl, libjson-xs-perl, libjson-perl, libterm-readline-gnu-perl,libnet-ldap-perl, libpve-common-perl, pve-cluster
Maintainer: Proxmox Support Team <support@proxmox.com>
Description: Proxmox VE access control library
This package contains the role based user management and access
control function used by Proxmox VE.

16
copyright Normal file
View File

@ -0,0 +1,16 @@
Copyright (C) 2010 Proxmox Server Solutions GmbH
This software is written by Proxmox Server Solutions GmbH <support@proxmox.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

102
pveum Executable file
View File

@ -0,0 +1,102 @@
#!/usr/bin/perl -w
use strict;
use Getopt::Long;
use PVE::Tools qw(run_command);
use PVE::Cluster;
use PVE::SafeSyslog;
use PVE::AccessControl;
use File::Path qw(make_path remove_tree);
use Term::ReadLine;
use PVE::INotify;
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::JSONSchema qw(get_standard_option);
use PVE::CLIHandler;
use base qw(PVE::CLIHandler);
use Data::Dumper; # fixme: remove
$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin';
initlog('pveum');
#fixme: logging?
die "please run as root\n" if $> != 0;
PVE::INotify::inotify_init();
my $rpcenv = PVE::RPCEnvironment->init('cli');
$rpcenv->init_request();
$rpcenv->set_language($ENV{LANG});
$rpcenv->set_user('root@pam');
# autmatically generate the private key if it does not already exists
PVE::Cluster::gen_auth_key();
my $read_password = sub {
# return $ENV{PVE_PW_TICKET} if defined($ENV{PVE_PW_TICKET});
my $term = new Term::ReadLine ('pveum');
my $attribs = $term->Attribs;
$attribs->{redisplay_function} = $attribs->{shadow_redisplay};
my $input = $term->readline('Enter new password: ');
my $conf = $term->readline('Retype new password: ');
die "Passwords do not match.\n" if ($input ne $conf);
return $input;
};
my $cmddef = {
ticket => [ 'PVE::API2::AccessControl', 'create_ticket', ['username'], undef,
sub {
my ($res) = @_;
print "$res->{ticket}\n";
}],
useradd => [ 'PVE::API2::User', 'create_user', ['userid'] ],
usermod => [ 'PVE::API2::User', 'update_user', ['userid'] ],
userdel => [ 'PVE::API2::User', 'delete_user', ['userid'] ],
groupadd => [ 'PVE::API2::Group', 'create_group', ['groupid'] ],
groupmod => [ 'PVE::API2::Group', 'update_group', ['groupid'] ],
groupdel => [ 'PVE::API2::Group', 'delete_group', ['groupid'] ],
roleadd => [ 'PVE::API2::Role', 'create_role', ['roleid'] ],
rolemod => [ 'PVE::API2::Role', 'update_role', ['roleid'] ],
roledel => [ 'PVE::API2::Role', 'delete_role', ['roleid'] ],
aclmod => [ 'PVE::API2::ACL', 'update_acl', ['path', 'roles'], { delete => 0 }],
acldel => [ 'PVE::API2::ACL', 'update_acl', ['path', 'roles'], { delete => 1 }],
};
my $cmd = shift;
if ($cmd && $cmd eq 'verifyapi') {
PVE::RESTHandler::validate_method_schemas();
exit 0;
}
PVE::CLIHandler::handle_cmd($cmddef, "pveum", $cmd, \@ARGV, $read_password);
exit 0;
__END__
=head1 NAME
pveum - PVE User Manager
=head1 SYNOPSIS
pveum <COMMAND> [OPTIONS]
=head1 DESCRIPTION
no description available

22
test.pl Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/perl -w
use strict;
use PVE::AccessControl;
# create ticket using username and password
#my $ticket = PVE::AccessControl::create_ticket(undef, $username, $password);
# create ticket using ident auth
my $login = getpwuid($<);
my $username = ($< == 0) ? 'root' : "$login\@localhost";
my $ticket = PVE::AccessControl::create_ticket(undef, $username);
print "got ticket using ident auth: $ticket\n";
for (my $i = 0; $i < 1; $i++) {
$ticket = PVE::AccessControl::create_ticket($ticket, $username);
print "renewed ticket: $ticket\n";
}
my $user = 'testuser';
PVE::AccessControl::add_user($ticket, $user, 'testpw');

10
test/Makefile Normal file
View File

@ -0,0 +1,10 @@
all:
.PHONY: check
check:
perl -I.. perm-test1.pl
perl -I.. perm-test2.pl
perl -I.. perm-test3.pl
perl -I.. perm-test4.pl

23
test/auth-test.pl Normal file
View File

@ -0,0 +1,23 @@
#!/usr/bin/perl -w
use strict;
use Term::ReadLine;
use PVE::AccessControl;
my $username = shift;
die "Username missing" if !$username;
sub read_password {
my $term = new Term::ReadLine ('pveum');
my $attribs = $term->Attribs;
$attribs->{redisplay_function} = $attribs->{shadow_redisplay};
my $input = $term->readline('password: ');
return $input;
}
my $password = read_password();
PVE::AccessControl::authenticate_user($username,$password);
print "Authentication Successful!!\n";
exit (0);

42
test/dump-perm.pl Executable file
View File

@ -0,0 +1,42 @@
#!/usr/bin/perl -w
use strict;
use PVE::AccessControl;
use Getopt::Long;
use Data::Dumper;
# example:
# dump-perm.pl -f myuser.cfg root /
my $opt_file;
if (!GetOptions ("file=s" => \$opt_file)) {
exit (-1);
}
my $username = shift;
my $path = shift;
if (!($username && $path)) {
print "usage: $0 <username> <path>\n";
exit (-1);
}
my $cfg;
if ($opt_file) {
my $fh = IO::File->new ($opt_file, 'r') ||
die "can't open file $opt_file - $!\n";
$cfg = PVE::AccessControl::parse_config ($opt_file, $fh);
$fh->close();
} else {
$cfg = PVE::AccessControl::load_user_config();
}
my $perm = PVE::AccessControl::permission($cfg, $username, $path);
print "permission for user '$username' on '$path':\n";
print join(',', keys %$perm) . "\n";
exit (0);

13
test/dump-users.pl Executable file
View File

@ -0,0 +1,13 @@
#!/usr/bin/perl -w
use strict;
use PVE::AccessControl;
use Data::Dumper;
my $cfg;
$cfg = PVE::AccessControl::load_user_config();
print Dumper($cfg) . "\n";
exit (0);

65
test/perm-test1.pl Executable file
View File

@ -0,0 +1,65 @@
#!/usr/bin/perl -w
use strict;
use PVE::Tools;
use PVE::AccessControl;
use PVE::RPCEnvironment;
use Getopt::Long;
my $rpcenv = PVE::RPCEnvironment->init('cli');
my $cfgfn = "user.cfg.ex1";
$rpcenv->init_request(userconfig => $cfgfn);
sub check_roles {
my ($user, $path, $expected_result) = @_;
my @ra = PVE::AccessControl::roles($rpcenv->{user_cfg}, $user, $path);
my $res = join(',', sort @ra);
die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
if $res ne $expected_result;
print "ROLES:$path:$user:$res\n";
}
sub check_permission {
my ($user, $path, $expected_result) = @_;
my $perm = PVE::AccessControl::permission($rpcenv->{user_cfg}, $user, $path);
my $res = join(',', sort keys %$perm);
die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
if $res ne $expected_result;
$perm = $rpcenv->permissions($user, $path);
$res = join(',', sort keys %$perm);
die "unexpected result (compiled)\nneed '${expected_result}'\ngot '$res'\n"
if $res ne $expected_result;
print "PERM:$path:$user:$res\n";
}
check_roles('max@pve', '/', '');
check_roles('max@pve', '/vms', 'vm_admin');
#user permissions overrides group permissions
check_roles('max@pve', '/vms/100', 'customer');
check_roles('max@pve', '/vms/101', 'vm_admin');
check_permission('max@pve', '/', '');
check_permission('max@pve', '/vms', 'Permissions.Modify,VM.Allocate,VM.Audit,VM.Console');
check_permission('max@pve', '/vms/100', 'VM.Audit,VM.PowerMgmt');
check_permission('alex@pve', '/vms', '');
check_permission('alex@pve', '/vms/100', 'VM.Audit,VM.PowerMgmt');
check_roles('max@pve', '/vms/200', 'storage_manager');
check_roles('joe@pve', '/vms/200', 'vm_admin');
check_roles('sue@pve', '/vms/200', '');
print "all tests passed\n";
exit (0);

39
test/perm-test2.pl Executable file
View File

@ -0,0 +1,39 @@
#!/usr/bin/perl -w
use strict;
use PVE::Tools;
use PVE::AccessControl;
use PVE::RPCEnvironment;
use Getopt::Long;
my $rpcenv = PVE::RPCEnvironment->init('cli');
my $cfgfn = "test2.cfg";
$rpcenv->init_request(userconfig => $cfgfn);
sub check_roles {
my ($user, $path, $expected_result) = @_;
my @ra = PVE::AccessControl::roles($rpcenv->{user_cfg}, $user, $path);
my $res = join(',', sort @ra);
die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
if $res ne $expected_result;
print "ROLES:$path:$user:$res\n";
}
# inherit multiple group permissions
check_roles('User1@pve', '/', '');
check_roles('User2@pve', '/', '');
check_roles('User1@pve', '/vms', 'Role1,Role2');
check_roles('User2@pve', '/vms', '');
check_roles('User1@pve', '/vms/100', 'Role1,Role2');
check_roles('User2@pve', '/vms', '');
print "all tests passed\n";
exit (0);

34
test/perm-test3.pl Executable file
View File

@ -0,0 +1,34 @@
#!/usr/bin/perl -w
use strict;
use PVE::Tools;
use PVE::AccessControl;
use PVE::RPCEnvironment;
use Getopt::Long;
my $rpcenv = PVE::RPCEnvironment->init('cli');
my $cfgfn = "test3.cfg";
$rpcenv->init_request(userconfig => $cfgfn);
sub check_roles {
my ($user, $path, $expected_result) = @_;
my @ra = PVE::AccessControl::roles($rpcenv->{user_cfg}, $user, $path);
my $res = join(',', sort @ra);
die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
if $res ne $expected_result;
print "ROLES:$path:$user:$res\n";
}
check_roles('User1@pve', '', '');
check_roles('User2@pve', '', '');
check_roles('User1@pve', '/vms/300', 'Role1');
check_roles('User1@pve', '/vms/200', 'Role2');
print "all tests passed\n";
exit (0);

32
test/perm-test4.pl Executable file
View File

@ -0,0 +1,32 @@
#!/usr/bin/perl -w
use strict;
use PVE::Tools;
use PVE::AccessControl;
use PVE::RPCEnvironment;
use Getopt::Long;
my $rpcenv = PVE::RPCEnvironment->init('cli');
my $cfgfn = "test4.cfg";
$rpcenv->init_request(userconfig => $cfgfn);
sub check_roles {
my ($user, $path, $expected_result) = @_;
my @ra = PVE::AccessControl::roles($rpcenv->{user_cfg}, $user, $path);
my $res = join(',', sort @ra);
die "unexpected result\nneed '${expected_result}'\ngot '$res'\n"
if $res ne $expected_result;
print "ROLES:$path:$user:$res\n";
}
check_roles('User1@pve', '/vms/300', 'Role1');
check_roles('User2@pve', '/vms/300', '');
print "all tests passed\n";
exit (0);

11
test/test2.cfg Normal file
View File

@ -0,0 +1,11 @@
user:User1@pve:1:
user:User2@pve:1:
group:GroupA:User1@pve:
group:GroupB:User1@pve:
role:Role1:VM.PowerMgmt:
role:Role2:VM.Console:
acl:1:/vms:@GroupA:Role1:
acl:1:/vms:@GroupB:Role2:

11
test/test3.cfg Normal file
View File

@ -0,0 +1,11 @@
user:User1@pve:1:
user:User2@pve:1:
group:GroupA:User1@pve:
group:GroupB:User1@pve:
role:Role1:VM.PowerMgmt:
role:Role2:VM.Console:
acl:1:/vms:@GroupA:Role1:
acl:1:/vms/200:@GroupB:Role2:

11
test/test4.cfg Normal file
View File

@ -0,0 +1,11 @@
user:User1@pve:1:
user:User2@pve:1:
group:GroupA:User1@pve,User2@pve:
group:GroupB:User1@pve,User2@pve:
role:Role1:VM.PowerMgmt:
role:Role2:VM.Console:
acl:1:/vms:@GroupA:Role1:
acl:1:/vms:User2@pve:NoAccess:

22
test/user.cfg.ex1 Normal file
View File

@ -0,0 +1,22 @@
user:joe@pve:1:
user:max@pve:1:
user:alex@pve:1:
user:sue@pve:1:
user:carol@pam:1:
group:testgroup1:joe@pve,max@pve,sue@pve:
group:testgroup2:alex@pve,carol@pam,sue@pve:
group:testgroup3:max@pve:
role:storage_manager:Datastore.AllocateSpace,Datastore.Audit:
role:customer:VM.Audit,VM.PowerMgmt:
role:vm_admin:VM.Audit,VM.Allocate,Permissions.Modify,VM.Console:
acl:1:/vms:@testgroup1:vm_admin:
acl:1:/vms/100/:alex@pve,max@pve:customer:
acl:1:/storage/nfs1:@testgroup2:storage_manager:
acl:1:/users:max@pve:Administrator:
acl:1:/vms/200:@testgroup3:storage_manager:
acl:1:/vms/200:@testgroup2:NoAccess:

8
user.cfg.ex Normal file
View File

@ -0,0 +1,8 @@
user:joe@localhost:1:
group:testgroup:joe@localhost:
role:admin:VM.ConfigureCD,VM.Create,Permissions.Modify,VM.Console:
acl:0:/users:@testgroup,joe@localhost:Administrator: