new plugin architecture for Auth modules

This commit is contained in:
Dietmar Maurer 2012-05-16 07:22:25 +02:00
parent 3030a17643
commit 5bb4e06a64
13 changed files with 690 additions and 689 deletions

View File

@ -1,8 +1,8 @@
RELEASE=2.0
RELEASE=2.1
VERSION=1.0
PACKAGE=libpve-access-control
PKGREL=21
PKGREL=22
DESTDIR=
PREFIX=/usr

View File

@ -2,12 +2,14 @@ package PVE::API2::Domains;
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";
@ -43,9 +45,10 @@ __PACKAGE__->register_method ({
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};
@ -64,101 +67,39 @@ __PACKAGE__->register_method ({
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};
my $plugin = PVE::Auth::Plugin->lookup($type);
my $config = $plugin->check_config($realm, $param, 1, 1);
if ($config->{default}) {
foreach my $r (keys %$ids) {
delete $ids->{$r}->{default};
}
}
foreach my $p (keys %$param) {
next if $p eq 'realm';
$cfg->{$realm}->{$p} = $param->{$p} if $param->{$p};
}
# port 0 ==> use default
# server2 == '' ===> delete server2
for my $p (qw(port server2)) {
if (defined($param->{$p}) && !$param->{$p}) {
delete $cfg->{$realm}->{$p};
}
}
$ids->{$realm} = $config;
cfs_write_file($domainconfigfile, $cfg);
}, "add auth server failed");
@ -175,92 +116,46 @@ __PACKAGE__->register_method ({
},
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};
if (defined($param->{secure})) {
$cfg->{$realm}->{secure} = $param->{secure} ? 1 : 0;
my $delete_str = extract_param($param, 'delete');
die "no options specified\n" if !$delete_str && !scalar(keys %$param);
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);
@ -292,9 +187,11 @@ __PACKAGE__->register_method ({
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;
}});
@ -318,16 +215,17 @@ __PACKAGE__->register_method ({
code => sub {
my ($param) = @_;
PVE::AccessControl::lock_user_config(
PVE::Auth::Plugin::lock_domain_config(
sub {
my $cfg = cfs_read_file($domainconfigfile);
my $ids = $cfg->{ids};
my $realm = $param->{realm};
die "domain '$realm' does not exist\n" if !$cfg->{$realm};
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");

View File

@ -330,9 +330,13 @@ __PACKAGE__->register_method ({
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);

View File

@ -6,22 +6,31 @@ use Crypt::OpenSSL::Random;
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
@ -32,41 +41,20 @@ cfs_register_file('user.cfg',
\&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 verify_username {
PVE::Auth::Plugin::verify_username(@_);
}
sub pve_verify_realm {
PVE::Auth::Plugin::pve_verify_realm(@_);
}
sub lock_user_config {
my ($code, $errmsg) = @_;
cfs_lock_file("user.cfg", undef, $code);
my $err = $@;
if ($err) {
$errmsg ? die "$errmsg: $err" : die $err;
}
}
sub lock_domain_config {
my ($code, $errmsg) = @_;
cfs_lock_file($domainconfigfile, undef, $code);
my $err = $@;
if ($err) {
$errmsg ? die "$errmsg: $err" : die $err;
}
}
sub lock_shadow_config {
my ($code, $errmsg) = @_;
cfs_lock_file($shadowconfigfile, undef, $code);
my $err = $@;
if ($err) {
if (my $err = $@) {
$errmsg ? die "$errmsg: $err" : die $err;
}
}
@ -163,7 +151,7 @@ sub verify_ticket {
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;
}
@ -225,159 +213,10 @@ sub verify_vnc_ticket {
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};
@ -408,9 +247,9 @@ sub authenticate_user {
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');
@ -428,64 +267,33 @@ sub authenticate_user {
die "account expired\n"
}
authenticate_user_domain($realm, $userid, $password);
my $domain_cfg = cfs_read_file('domains.cfg');
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;
}
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;
run_command($cmd, errmsg => 'change password failed');
}
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 {
@ -652,26 +460,6 @@ sub create_roles {
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) = @_;
@ -704,58 +492,6 @@ sub normalize_path {
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 {
@ -850,7 +586,7 @@ sub parse_user_config {
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;
@ -899,7 +635,7 @@ sub parse_user_config {
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;
}
@ -950,7 +686,7 @@ sub parse_user_config {
} 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 {
@ -1012,191 +748,6 @@ sub parse_user_config {
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) = @_;
@ -1370,7 +921,7 @@ sub roles {
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);

109
PVE/Auth/AD.pm Executable file
View File

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

81
PVE/Auth/LDAP.pm Executable file
View File

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

11
PVE/Auth/Makefile Normal file
View File

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

73
PVE/Auth/PAM.pm Executable file
View File

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

110
PVE/Auth/PVE.pm Executable file
View File

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

193
PVE/Auth/Plugin.pm Executable file
View File

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

View File

@ -2,6 +2,7 @@
.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

37
TODO
View File

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

View File

@ -1,3 +1,10 @@
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