mirror of
https://git.proxmox.com/git/pve-access-control
synced 2025-07-24 18:59:01 +00:00
new plugin architecture for Auth modules
This commit is contained in:
parent
3030a17643
commit
5bb4e06a64
4
Makefile
4
Makefile
@ -1,8 +1,8 @@
|
||||
RELEASE=2.0
|
||||
RELEASE=2.1
|
||||
|
||||
VERSION=1.0
|
||||
PACKAGE=libpve-access-control
|
||||
PKGREL=21
|
||||
PKGREL=22
|
||||
|
||||
DESTDIR=
|
||||
PREFIX=/usr
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
|
@ -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
109
PVE/Auth/AD.pm
Executable 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
81
PVE/Auth/LDAP.pm
Executable 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
11
PVE/Auth/Makefile
Normal 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
73
PVE/Auth/PAM.pm
Executable 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
110
PVE/Auth/PVE.pm
Executable 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
193
PVE/Auth/Plugin.pm
Executable 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;
|
@ -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
37
TODO
@ -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>
|
||||
|
||||
...
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user