-RELEASE=2.0
+RELEASE=2.1
VERSION=1.0
PACKAGE=libpve-access-control
-PKGREL=21
+PKGREL=22
DESTDIR=
PREFIX=/usr
use strict;
use warnings;
+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";
my $res = [];
my $cfg = cfs_read_file($domainconfigfile);
-
- foreach my $realm (keys %$cfg) {
- my $d = $cfg->{$realm};
+ my $ids = $cfg->{ids};
+
+ foreach my $realm (keys %$ids) {
+ my $d = $ids->{$realm};
my $entry = { realm => $realm, type => $d->{type} };
$entry->{comment} = $d->{comment} if $d->{comment};
$entry->{default} = 1 if $d->{default};
check => ['perm', '/access/realm', ['Realm.Allocate']],
},
description => "Add an authentication server.",
- parameters => {
- additionalProperties => 0,
- properties => {
- realm => get_standard_option('realm'),
- type => {
- description => "Server type.",
- type => 'string',
- enum => [ 'ad', 'ldap' ],
- },
- server1 => {
- description => "Server IP address (or DNS name)",
- type => 'string',
- },
- server2 => {
- description => "Fallback Server IP address (or DNS name)",
- type => 'string',
- optional => 1,
- },
- secure => {
- description => "Use secure LDAPS protocol.",
- type => 'boolean',
- optional => 1,
- },
- default => {
- description => "Use this as default realm",
- type => 'boolean',
- optional => 1,
- },
- comment => {
- type => 'string',
- optional => 1,
- },
- port => {
- description => "Server port. Use '0' if you want to use default settings'",
- type => 'integer',
- minimum => 0,
- maximum => 65535,
- optional => 1,
- },
- domain => {
- description => "AD domain name",
- type => 'string',
- optional => 1,
- },
- base_dn => {
- description => "LDAP base domain name",
- type => 'string',
- optional => 1,
- },
- user_attr => {
- description => "LDAP user attribute name",
- type => 'string',
- optional => 1,
- },
- },
- },
+ parameters => PVE::Auth::Plugin->createSchema(),
returns => { type => 'null' },
code => sub {
my ($param) = @_;
- PVE::AccessControl::lock_domain_config(
+ PVE::Auth::Plugin::lock_domain_config(
sub {
my $cfg = cfs_read_file($domainconfigfile);
+ my $ids = $cfg->{ids};
- my $realm = $param->{realm};
+ my $realm = extract_param($param, 'realm');
+ my $type = $param->{type};
die "domain '$realm' already exists\n"
- if $cfg->{$realm};
+ if $ids->{$realm};
die "unable to use reserved name '$realm'\n"
if ($realm eq 'pam' || $realm eq 'pve');
- if (defined($param->{secure})) {
- $cfg->{$realm}->{secure} = $param->{secure} ? 1 : 0;
- }
+ die "unable to create builtin type '$type'\n"
+ if ($type eq 'pam' || $type eq 'pve');
- 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} if $param->{$p};
- }
+ my $plugin = PVE::Auth::Plugin->lookup($type);
+ my $config = $plugin->check_config($realm, $param, 1, 1);
- # port 0 ==> use default
- # server2 == '' ===> delete server2
- for my $p (qw(port server2)) {
- if (defined($param->{$p}) && !$param->{$p}) {
- delete $cfg->{$realm}->{$p};
+ if ($config->{default}) {
+ foreach my $r (keys %$ids) {
+ delete $ids->{$r}->{default};
}
}
+ $ids->{$realm} = $config;
+
cfs_write_file($domainconfigfile, $cfg);
}, "add auth server failed");
},
description => "Update authentication server settings.",
protected => 1,
- parameters => {
- additionalProperties => 0,
- properties => {
- realm => get_standard_option('realm'),
- server1 => {
- description => "Server IP address (or DNS name)",
- type => 'string',
- optional => 1,
- },
- server2 => {
- description => "Fallback Server IP address (or DNS name)",
- type => 'string',
- optional => 1,
- },
- secure => {
- description => "Use secure LDAPS protocol.",
- type => 'boolean',
- optional => 1,
- },
- default => {
- description => "Use this as default realm",
- type => 'boolean',
- optional => 1,
- },
- comment => {
- type => 'string',
- optional => 1,
- },
- port => {
- description => "Server port. Use '0' if you want to use default settings'",
- type => 'integer',
- minimum => 0,
- maximum => 65535,
- optional => 1,
- },
- domain => {
- description => "AD domain name",
- type => 'string',
- optional => 1,
- },
- base_dn => {
- description => "LDAP base domain name",
- type => 'string',
- optional => 1,
- },
- user_attr => {
- description => "LDAP user attribute name",
- type => 'string',
- optional => 1,
- },
- },
- },
+ parameters => PVE::Auth::Plugin->updateSchema(),
returns => { type => 'null' },
code => sub {
my ($param) = @_;
- PVE::AccessControl::lock_domain_config(
+ PVE::Auth::Plugin::lock_domain_config(
sub {
my $cfg = cfs_read_file($domainconfigfile);
+ my $ids = $cfg->{ids};
- my $realm = $param->{realm};
- delete $param->{realm};
+ my $digest = extract_param($param, 'digest');
+ PVE::SectionConfig::assert_if_modified($cfg, $digest);
+
+ my $realm = extract_param($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 !$ids->{$realm};
+
+ my $delete_str = extract_param($param, 'delete');
+ die "no options specified\n" if !$delete_str && !scalar(keys %$param);
- if (defined($param->{secure})) {
- $cfg->{$realm}->{secure} = $param->{secure} ? 1 : 0;
+ foreach my $opt (PVE::Tools::split_list($delete_str)) {
+ delete $ids->{$realm}->{$opt};
}
+
+ my $plugin = PVE::Auth::Plugin->lookup($ids->{$realm}->{type});
+ my $config = $plugin->check_config($realm, $param, 0, 1);
- if ($param->{default}) {
- foreach my $r (keys %$cfg) {
- delete $cfg->{$r}->{default};
+ if ($config->{default}) {
+ foreach my $r (keys %$ids) {
+ delete $ids->{$r}->{default};
}
}
- foreach my $p (keys %$param) {
- if ($param->{$p}) {
- $cfg->{$realm}->{$p} = $param->{$p};
- } else {
- delete $cfg->{$realm}->{$p};
- }
+ foreach my $p (keys %$config) {
+ $ids->{$realm}->{$p} = $config->{$p};
}
cfs_write_file($domainconfigfile, $cfg);
my $realm = $param->{realm};
- my $data = $cfg->{$realm};
+ my $data = $cfg->{ids}->{$realm};
die "domain '$realm' does not exist\n" if !$data;
+ $data->{digest} = $cfg->{digest};
+
return $data;
}});
code => sub {
my ($param) = @_;
- PVE::AccessControl::lock_user_config(
+ PVE::Auth::Plugin::lock_domain_config(
sub {
my $cfg = cfs_read_file($domainconfigfile);
+ my $ids = $cfg->{ids};
my $realm = $param->{realm};
- die "domain '$realm' does not exist\n" if !$cfg->{$realm};
+ die "domain '$realm' does not exist\n" if !$ids->{$realm};
- delete $cfg->{$realm};
+ delete $ids->{$realm};
cfs_write_file($domainconfigfile, $cfg);
}, "delete auth server failed");
my $usercfg = cfs_read_file("user.cfg");
- delete ($usercfg->{users}->{$userid});
+ 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);
+ }
- PVE::AccessControl::delete_shadow_password($ruid) if $realm eq 'pve';
+ delete $usercfg->{users}->{$userid};
PVE::AccessControl::delete_user_group($userid, $usercfg);
PVE::AccessControl::delete_user_acl($userid, $usercfg);
use Crypt::OpenSSL::RSA;
use MIME::Base64;
use Digest::SHA;
-use Authen::PAM qw(:constants);
-use Net::LDAP;
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;
-use Encode;
+
+use PVE::Auth::Plugin;
+use PVE::Auth::AD;
+use PVE::Auth::LDAP;
+use PVE::Auth::PVE;
+use PVE::Auth::PAM;
use Data::Dumper; # fixme: remove
+# 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 $authprivkeyfn = "$authdir/authkey.key";
my $authpubkeyfn = "$confdir/authkey.pub";
-my $shadowconfigfile = "priv/shadow.cfg";
-my $domainconfigfile = "domains.cfg";
my $pve_www_key_fn = "$confdir/pve-www.key";
my $ticket_lifetime = 3600*2; # 2 hours
\&parse_user_config,
\&write_user_config);
-cfs_register_file($shadowconfigfile,
- \&parse_shadow_passwd,
- \&write_shadow_config);
-cfs_register_file($domainconfigfile,
- \&parse_domains,
- \&write_domains);
-
-
-sub lock_user_config {
- my ($code, $errmsg) = @_;
-
- cfs_lock_file("user.cfg", undef, $code);
- my $err = $@;
- if ($err) {
- $errmsg ? die "$errmsg: $err" : die $err;
- }
+sub verify_username {
+ PVE::Auth::Plugin::verify_username(@_);
}
-sub lock_domain_config {
- my ($code, $errmsg) = @_;
-
- cfs_lock_file($domainconfigfile, undef, $code);
- my $err = $@;
- if ($err) {
- $errmsg ? die "$errmsg: $err" : die $err;
- }
+sub pve_verify_realm {
+ PVE::Auth::Plugin::pve_verify_realm(@_);
}
-sub lock_shadow_config {
+sub lock_user_config {
my ($code, $errmsg) = @_;
- cfs_lock_file($shadowconfigfile, undef, $code);
- my $err = $@;
- if ($err) {
+ cfs_lock_file("user.cfg", undef, $code);
+ if (my $err = $@) {
$errmsg ? die "$errmsg: $err" : die $err;
}
}
my $age = time() - $ttime;
- if (verify_username($username, 1) &&
+ if (PVE::Auth::Plugin::verify_username($username, 1) &&
($age > -300) && ($age < $ticket_lifetime)) {
return wantarray ? ($username, $age) : $username;
}
return undef;
}
-
-sub authenticate_user_shadow {
- my ($userid, $password) = @_;
-
- die "no password\n" if !$password;
-
- my $shadow_cfg = cfs_read_file($shadowconfigfile);
-
- if ($shadow_cfg->{users}->{$userid}) {
- my $encpw = crypt($password, $shadow_cfg->{users}->{$userid}->{shadow});
- die "invalid credentials\n" if ($encpw ne $shadow_cfg->{users}->{$userid}->{shadow});
- } else {
- die "no password set\n";
- }
-}
-
-sub authenticate_user_pam {
- my ($userid, $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 ('common-auth', $userid, 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
-}
-
-sub authenticate_user_ad {
-
- my ($entry, $server, $userid, $password) = @_;
-
- my $default_port = $entry->{secure} ? 636: 389;
- my $port = $entry->{port} ? $entry->{port} : $default_port;
- my $scheme = $entry->{secure} ? 'ldaps' : 'ldap';
- my $conn_string = "$scheme://${server}:$port";
-
- my $ldap = Net::LDAP->new($server) || die "$@\n";
-
- $userid = "$userid\@$entry->{domain}"
- if $userid !~ m/@/ && $entry->{domain};
-
- my $res = $ldap->bind($userid, password => $password);
-
- my $code = $res->code();
- my $err = $res->error;
-
- $ldap->unbind();
-
- die "$err\n" if ($code);
-}
-
-sub authenticate_user_ldap {
-
- my ($entry, $server, $userid, $password) = @_;
-
- my $default_port = $entry->{secure} ? 636: 389;
- my $port = $entry->{port} ? $entry->{port} : $default_port;
- my $scheme = $entry->{secure} ? 'ldaps' : 'ldap';
- my $conn_string = "$scheme://${server}:$port";
-
- my $ldap = Net::LDAP->new($conn_string, verify => 'none') || die "$@\n";
- my $search = $entry->{user_attr} . "=" . $userid;
- my $result = $ldap->search( base => "$entry->{base_dn}",
- scope => "sub",
- filter => "$search",
- attrs => ['dn']
- );
- die "no entries returned\n" if !$result->entries;
- my @entries = $result->entries;
- my $res = $ldap->bind($entries[0]->dn, password => $password);
-
- my $code = $res->code();
- my $err = $res->error;
-
- $ldap->unbind();
-
- die "$err\n" if ($code);
-}
-
-sub authenticate_user_domain {
- my ($realm, $userid, $password) = @_;
-
- my $domain_cfg = cfs_read_file($domainconfigfile);
-
- die "no auth domain specified" if !$realm;
-
- if ($realm eq 'pam') {
- authenticate_user_pam($userid, $password);
- return;
- }
-
- eval {
- if ($realm eq 'pve') {
- authenticate_user_shadow($userid, $password);
- } else {
-
- my $cfg = $domain_cfg->{$realm};
- die "auth domain '$realm' does not exists\n" if !$cfg;
-
- if ($cfg->{type} eq 'ad') {
- eval { authenticate_user_ad($cfg, $cfg->{server1}, $userid, $password); };
- my $err = $@;
- return if !$err;
- die $err if !$cfg->{server2};
- authenticate_user_ad($cfg, $cfg->{server2}, $userid, $password);
- } elsif ($cfg->{type} eq 'ldap') {
- eval { authenticate_user_ldap($cfg, $cfg->{server1}, $userid, $password); };
- my $err = $@;
- return if !$err;
- die $err if !$cfg->{server2};
- authenticate_user_ldap($cfg, $cfg->{server2}, $userid, $password);
- } else {
- die "unknown auth type '$cfg->{type}'\n";
- }
- }
- };
- if (my $err = $@) {
- sleep(2); # timeout after failed auth
- die $err;
- }
-}
-
sub check_user_exist {
my ($usercfg, $username, $noerr) = @_;
- $username = verify_username($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 username specified\n" if !$username;
- my ($userid, $realm);
+ my ($ruid, $realm);
- ($username, $userid, $realm) = verify_username($username);
+ ($username, $ruid, $realm) = PVE::Auth::Plugin::verify_username($username);
my $usercfg = cfs_read_file('user.cfg');
die "account expired\n"
}
- authenticate_user_domain($realm, $userid, $password);
+ my $domain_cfg = cfs_read_file('domains.cfg');
- return $username;
-}
-
-sub delete_shadow_password {
- my ($userid) = @_;
-
- lock_shadow_config(sub {
- my $shadow_cfg = cfs_read_file($shadowconfigfile);
- delete ($shadow_cfg->{users}->{$userid})
- if $shadow_cfg->{users}->{$userid};
- cfs_write_file($shadowconfigfile, $shadow_cfg);
- });
-}
-
-sub store_shadow_password {
- my ($userid, $password) = @_;
-
- lock_shadow_config(sub {
- my $shadow_cfg = cfs_read_file($shadowconfigfile);
- $shadow_cfg->{users}->{$userid}->{shadow} = encrypt_pw($password);
- cfs_write_file($shadowconfigfile, $shadow_cfg);
- });
-}
-
-sub encrypt_pw {
- my ($pw) = @_;
-
- my $time = substr (Digest::SHA::sha1_base64 (time), 0, 8);
- return crypt (encode("utf8", $pw), "\$5\$$time\$");
-}
-
-sub store_pam_password {
- my ($userid, $password) = @_;
-
- my $cmd = ['usermod'];
-
- my $epw = encrypt_pw($password);
- push @$cmd, '-p', $epw;
-
- push @$cmd, $userid;
+ eval {
+ my $cfg = $domain_cfg->{ids}->{$realm};
+ die "auth domain '$realm' does not exists\n" if !$cfg;
+ my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
+ $plugin->authenticate_user($cfg, $realm, $ruid, $password);
+ };
+ if (my $err = $@) {
+ sleep(2); # timeout after failed auth
+ die $err;
+ }
- run_command($cmd, errmsg => 'change password failed');
+ return $username;
}
sub domain_set_password {
- my ($realm, $userid, $password) = @_;
+ my ($realm, $username, $password) = @_;
die "no auth domain specified" if !$realm;
- if ($realm eq 'pam') {
- store_pam_password($userid, $password);
- } elsif ($realm eq 'pve') {
- store_shadow_password($userid, $password);
- } else {
- die "can't set password on auth domain '$realm'\n";
- }
+ my $domain_cfg = cfs_read_file('domains.cfg');
+
+ my $cfg = $domain_cfg->{ids}->{$realm};
+ die "auth domain '$realm' does not exists\n" if !$cfg;
+ my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
+ $plugin->store_password($cfg, $realm, $username, $password);
}
sub add_user_group {
create_roles();
-my $valid_attributes = {
- ad => {
- server1 => '[\w\d]+(.[\w\d]+)*',
- server2 => '[\w\d]+(.[\w\d]+)*',
- domain => '\S+',
- port => '\d+',
- secure => '',
- comment => '.*',
- },
- ldap => {
- server1 => '[\w\d]+(.[\w\d]+)*',
- server2 => '[\w\d]+(.[\w\d]+)*',
- base_dn => '\w+=[^,]+(,\s*\w+=[^,]+)*',
- user_attr => '\S{2,}',
- secure => '',
- port => '\d+',
- comment => '.*',
- }
-};
-
sub add_role_privs {
my ($role, $usercfg, $privs) = @_;
return $path;
}
-my $realm_regex = qr/[A-Za-z][A-Za-z0-9\.\-_]+/;
-
-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_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!^([^\s:/]+)\@(${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,
-});
-
-PVE::JSONSchema::register_standard_option('realm', {
- description => "Authentication domain ID",
- type => 'string', format => 'pve-realm',
- maxLength => 32,
-});
PVE::JSONSchema::register_format('pve-groupid', \&verify_groupname);
sub verify_groupname {
if ($et eq 'user') {
my ($user, $enable, $expire, $firstname, $lastname, $email, $comment) = @data;
- my (undef, undef, $realm) = verify_username($user, 1);
+ my (undef, undef, $realm) = PVE::Auth::Plugin::verify_username($user, 1);
if (!$realm) {
warn "user config - ignore user '$user' - invalid user name\n";
next;
foreach my $user (split_list($userlist)) {
- if (!verify_username($user, 1)) {
+ if (!PVE::Auth::Plugin::verify_username($user, 1)) {
warn "user config - ignore invalid group member '$user'\n";
next;
}
} else {
warn "user config - ignore invalid acl group '$group'\n";
}
- } elsif (verify_username($ug, 1)) {
+ } elsif (PVE::Auth::Plugin::verify_username($ug, 1)) {
if ($cfg->{users}->{$ug}) { # user exists
$cfg->{acl}->{$path}->{users}->{$ug}->{$role} = $propagate;
} else {
return $cfg;
}
-sub parse_shadow_passwd {
- my ($filename, $raw) = @_;
-
- my $shadow = {};
-
- while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
- my $line = $1;
-
- next if $line =~ m/^\s*$/; # skip empty lines
-
- 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_domains {
- my ($filename, $cfg) = @_;
-
- my $data = '';
-
- my $wrote_default;
-
- foreach my $realm (sort keys %$cfg) {
- my $entry = $cfg->{$realm};
- my $type = lc($entry->{type});
-
- next if !$type;
-
- next if ($type eq 'pam') || ($type eq 'pve');
-
- my $formats = $valid_attributes->{$type};
- next if !$formats;
-
- $data .= "$type: $realm\n";
-
- foreach my $k (sort keys %$entry) {
- next if $k eq 'type';
- my $v = $entry->{$k};
- if ($k eq 'default') {
- $data .= "\t$k\n" if $v && !$wrote_default;
- $wrote_default = 1;
- } elsif (defined($formats->{$k})) {
- if (!$formats->{$k}) {
- $data .= "\t$k\n" if $v;
- } elsif ($v =~ m/^$formats->{$k}$/) {
- $v = PVE::Tools::encode_text($v) if $k eq 'comment';
- $data .= "\t$k $v\n";
- } else {
- die "invalid value '$v' for attribute '$k'\n";
- }
- } else {
- die "invalid attribute '$k' - not supported\n";
- }
- }
-
- $data .= "\n";
- }
-
- return $data;
-}
-
-sub parse_domains {
- my ($filename, $raw) = @_;
-
- my $cfg = {};
-
- my $default;
-
- while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
- my $line = $1;
-
- next if $line =~ m/^\#/; # skip comment lines
- next if $line =~ m/^\s*$/; # skip empty lines
-
- if ($line =~ m/^(\S+):\s*(\S+)\s*$/) {
- my $realm = $2;
- my $type = lc($1);
-
- my $ignore = 0;
- my $entry;
-
- my $formats = $valid_attributes->{$type};
- if (!$formats) {
- $ignore = 1;
- warn "ignoring domain '$realm' - (unsupported authentication type '$type')\n";
- } elsif (!pve_verify_realm($realm, 1)) {
- $ignore = 1;
- warn "ignoring domain '$realm' - (illegal characters)\n";
- } else {
- $entry = { type => $type };
- }
-
- while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
- $line = $1;
-
- next if $line =~ m/^\#/; #skip comment lines
- last if $line =~ m/^\s*$/;
-
- next if $ignore; # skip
-
- if ($line =~ m/^\s+(default)\s*$/) {
- $default = $realm if !$default;
- } elsif ($line =~ m/^\s+(\S+)(\s+(.*\S))?\s*$/) {
- my ($k, $v) = (lc($1), $3);
- if (defined($formats->{$k})) {
- if (!$formats->{$k} && !defined($v)) {
- $entry->{$k} = 1;
- } elsif ($formats->{$k} && $v =~ m/^$formats->{$k}$/) {
- if (!defined($entry->{$k})) {
- $v = PVE::Tools::decode_text($v) if $k eq 'comment';
- $entry->{$k} = $v;
- } else {
- warn "ignoring duplicate attribute '$k $v'\n";
- }
- } else {
- warn "ignoring value '$v' for attribute '$k' - invalid format\n";
- }
- } else {
- warn "ignoring attribute '$k' - not supported\n";
- }
- } else {
- warn "ignore config line: $line\n";
- }
- }
-
- if ($entry->{server2} && !$entry->{server1}) {
- $entry->{server1} = $entry->{server2};
- delete $entry->{server2};
- }
-
- if ($ignore) {
- # do nothing
- } elsif (!$entry->{server1}) {
- warn "ignoring domain '$realm' - missing server attribute\n";
- } elsif (($entry->{type} eq "ldap") && !$entry->{user_attr}) {
- warn "ignoring domain '$realm' - missing user attribute\n";
- } elsif (($entry->{type} eq "ldap") && !$entry->{base_dn}) {
- warn "ignoring domain '$realm' - missing base_dn attribute\n";
- } elsif (($entry->{type} eq "ad") && !$entry->{domain}) {
- warn "ignoring domain '$realm' - missing domain attribute\n";
- } else {
- $cfg->{$realm} = $entry;
- }
-
- } else {
- warn "ignore config line: $line\n";
- }
- }
-
- $cfg->{$default}->{default} = 1 if $default;
-
- # add default domains
-
- $cfg->{pve} = {
- type => 'builtin',
- comment => "Proxmox VE authentication server",
- };
-
- $cfg->{pam} = {
- type => 'builtin',
- comment => "Linux PAM standard authentication",
- };
-
- return $cfg;
-}
-
-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 write_user_config {
my ($filename, $cfg) = @_;
sub permission {
my ($cfg, $user, $path) = @_;
- $user = verify_username($user, 1);
+ $user = PVE::Auth::Plugin::verify_username($user, 1);
return {} if !$user;
my @ra = roles($cfg, $user, $path);
--- /dev/null
+package PVE::Auth::AD;
+
+use strict;
+use warnings;
+use PVE::Auth::Plugin;
+use Net::LDAP;
+
+use base qw(PVE::Auth::Plugin);
+
+sub type {
+ return 'ad';
+}
+
+sub properties {
+ return {
+ server1 => {
+ description => "Server IP address (or DNS name)",
+ type => 'string',
+ pattern => '[\w\d]+(.[\w\d]+)*',
+ maxLength => 256,
+ },
+ server2 => {
+ description => "Fallback Server IP address (or DNS name)",
+ type => 'string',
+ optional => 1,
+ pattern => '[\w\d]+(.[\w\d]+)*',
+ maxLength => 256,
+ },
+ secure => {
+ description => "Use secure LDAPS protocol.",
+ type => 'boolean',
+ 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,
+ },
+ };
+}
+
+sub options {
+ return {
+ server1 => {},
+ server2 => { optional => 1 },
+ domain => {},
+ port => { optional => 1 },
+ secure => { optional => 1 },
+ default => { optional => 1 },,
+ comment => { optional => 1 },
+ };
+}
+
+my $authenticate_user_ad = sub {
+ my ($config, $server, $username, $password) = @_;
+
+ my $default_port = $config->{secure} ? 636: 389;
+ my $port = $config->{port} ? $config->{port} : $default_port;
+ my $scheme = $config->{secure} ? 'ldaps' : 'ldap';
+ my $conn_string = "$scheme://${server}:$port";
+
+ my $ldap = Net::LDAP->new($server) || die "$@\n";
+
+ $username = "$username\@$config->{domain}"
+ if $username !~ m/@/ && $config->{domain};
+
+ my $res = $ldap->bind($username, password => $password);
+
+ my $code = $res->code();
+ my $err = $res->error;
+
+ $ldap->unbind();
+
+ die "$err\n" if ($code);
+};
+
+sub authenticate_user {
+ my ($class, $config, $realm, $username, $password) = @_;
+
+ eval { &$authenticate_user_ad($config, $config->{server1}, $username, $password); };
+ my $err = $@;
+ return 1 if !$err;
+ die $err if !$config->{server2};
+ &$authenticate_user_ad($config, $config->{server2}, $username, $password);
+ return 1;
+}
+
+1;
--- /dev/null
+package PVE::Auth::LDAP;
+
+use strict;
+use PVE::Auth::Plugin;
+use Net::LDAP;
+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,
+ },
+ };
+}
+
+sub options {
+ return {
+ server1 => {},
+ server2 => { optional => 1 },
+ base_dn => {},
+ user_attr => {},
+ port => { optional => 1 },
+ secure => { optional => 1 },
+ default => { optional => 1 },
+ comment => { optional => 1 },
+ };
+}
+
+my $authenticate_user_ldap = sub {
+ my ($config, $server, $username, $password) = @_;
+
+ my $default_port = $config->{secure} ? 636: 389;
+ my $port = $config->{port} ? $config->{port} : $default_port;
+ my $scheme = $config->{secure} ? 'ldaps' : 'ldap';
+ my $conn_string = "$scheme://${server}:$port";
+
+ my $ldap = Net::LDAP->new($conn_string, verify => 'none') || die "$@\n";
+ my $search = $config->{user_attr} . "=" . $username;
+ my $result = $ldap->search( base => "$config->{base_dn}",
+ scope => "sub",
+ filter => "$search",
+ attrs => ['dn']
+ );
+ die "no entries returned\n" if !$result->entries;
+ my @entries = $result->entries;
+ my $res = $ldap->bind($entries[0]->dn, password => $password);
+
+ my $code = $res->code();
+ my $err = $res->error;
+
+ $ldap->unbind();
+
+ die "$err\n" if ($code);
+};
+
+sub authenticate_user {
+ my ($class, $config, $realm, $username, $password) = @_;
+
+ eval { &$authenticate_user_ldap($config, $config->{server1}, $username, $password); };
+ my $err = $@;
+ return 1 if !$err;
+ die $err if !$config->{server2};
+ &$authenticate_user_ldap($config, $config->{server2}, $username, $password);
+}
+
+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 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 },
+ };
+}
+
+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('common-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::Auth::Plugin::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 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 = {};
+
+ while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
+ my $line = $1;
+
+ next if $line =~ m/^\s*$/; # skip empty lines
+
+ 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 defaults {
+ return {
+ default => { optional => 1 },
+ comment => { 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($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::Auth::Plugin::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 Encode;
+use Digest::SHA;
+use PVE::Tools;
+use PVE::SectionConfig;
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_lock_file);
+
+use Data::Dumper;
+
+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;
+ }
+}
+
+my $realm_regex = qr/[A-Za-z][A-Za-z0-9\.\-_]+/;
+
+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,
+});
+
+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!^([^\s:/]+)\@(${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,
+});
+
+sub encrypt_pw {
+ my ($pw) = @_;
+
+ my $time = substr(Digest::SHA::sha1_base64 (time), 0, 8);
+ return crypt(encode("utf8", $pw), "\$5\$$time\$");
+}
+
+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',
+ comment => "Proxmox VE authentication server",
+ };
+
+ $cfg->{ids}->{pam} = {
+ type => 'pam',
+ plugin => 'PVE::Auth::PAM',
+ comment => "Linux PAM standard authentication",
+ };
+
+ return $cfg;
+};
+
+sub write_config {
+ my ($class, $filename, $cfg) = @_;
+
+ delete $cfg->{ids}->{pve};
+ delete $cfg->{ids}->{pam};
+
+ 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
+}
+
+1;
.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
make -C API2 install
\ No newline at end of file
+++ /dev/null
-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>
-
-...
-
+libpve-access-control (1.0-22) unstable; urgency=low
+
+ * new plugin architecture for Auth modules, minor API change for Auth
+ domains (new 'delete' parameter)
+
+ -- Proxmox Support Team <support@proxmox.com> Wed, 16 May 2012 07:21:44 +0200
+
libpve-access-control (1.0-21) unstable; urgency=low
* do not allow user names including slash