PMG/Unpack.pm \
PMG/RuleCache.pm \
PMG/Statistic.pm \
+ PMG/UserConfig.pm \
PMG/LDAPConfig.pm \
PMG/LDAPSet.pm \
PMG/LDAPCache.pm \
PMG/API2/Tasks.pm \
PMG/API2/LDAP.pm \
PMG/API2/Domains.pm \
+ PMG/API2/Users.pm \
PMG/API2/Transport.pm \
PMG/API2/MyNetworks.pm \
PMG/API2/Config.pm \
use PVE::RESTHandler;
use PVE::JSONSchema qw(get_standard_option);
+use PMG::Utils;
+use PMG::UserConfig;
use PMG::AccessControl;
use Data::Dumper;
my $rpcenv = PVE::RESTEnvironment::get();
- my $usercfg = { users => { 'root@pam' => { enable => 1 } }};
-
my $res;
eval {
- # test if user exists and is enabled
- PMG::AccessControl::check_user_enabled($usercfg, $username);
+ PMG::AccessControl::check_user_enabled($username);
$res = &$create_ticket($rpcenv, $username, $param->{password}, $param->{otp});
};
if (my $err = $@) {
my $rpcenv = PVE::RESTEnvironment::get();
my $authuser = $rpcenv->get_user();
- my ($userid, $ruid, $realm) = PMG::AccessControl::verify_username($param->{userid});
-
- my $usercfg = {}; # fixme;
-
- PMG::AccessControl::check_user_exist($usercfg, $userid);
+ my ($userid, $ruid, $realm) = PMG::Utils::verify_username($param->{userid});
if ($authuser eq 'root@pam') {
# OK - root can change anything
} else {
if ($authuser eq $userid) {
- PMG::AccessControl::check_user_enabled($usercfg, $userid);
- # OK - each user can change its own password
+ # OK - each enable user can change its own password
+ PMG::AccessControl::check_user_enabled($userid);
} else {
raise_perm_exc();
}
}
- PMP::AccessControl::domain_set_password($realm, $ruid, $param->{password});
+ PMG::AccessControl::domain_set_password($realm, $ruid, $param->{password});
syslog('info', "changed password for user '$userid'");
use PMG::API2::ClusterConfig;
use PMG::API2::MyNetworks;
use PMG::API2::SMTPWhitelist;
+use PMG::API2::Users;
use base qw(PVE::RESTHandler);
my $section_type_enum = PMG::Config::Base->lookup_types();
+__PACKAGE__->register_method ({
+ subclass => "PMG::API2::Users",
+ path => 'users',
+});
+
__PACKAGE__->register_method ({
subclass => "PMG::API2::RuleDB",
path => 'ruledb',
push @$res, { section => 'ldap' };
push @$res, { section => 'mynetworks' };
+ push @$res, { section => 'users' };
push @$res, { section => 'domains' };
push @$res, { section => 'cluster' };
push @$res, { section => 'ruledb' };
--- /dev/null
+package PMG::API2::Users;
+
+use strict;
+use warnings;
+use Data::Dumper;
+
+use PVE::SafeSyslog;
+use PVE::Tools qw(extract_param);
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::RESTHandler;
+use PVE::INotify;
+
+use PMG::UserConfig;
+
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method ({
+ name => 'index',
+ path => '',
+ method => 'GET',
+ description => "List users.",
+ proxyto => 'master',
+ parameters => {
+ additionalProperties => 0,
+ properties => {},
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {
+ userid => { type => 'string'},
+ enable => { type => 'boolean'},
+ role => { type => 'string'},
+ comment => { type => 'string', optional => 1},
+ },
+ },
+ links => [ { rel => 'child', href => "{userid}" } ],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $cfg = PMG::UserConfig->new();
+
+ my $res = [];
+
+ foreach my $userid (sort keys %$cfg) {
+ push @$res, $cfg->{$userid};
+ }
+
+ return $res;
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'create',
+ path => '',
+ method => 'POST',
+ proxyto => 'master',
+ protected => 1,
+ description => "Creat new user",
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ userid => get_standard_option('username'),
+ password => {
+ description => "Initial password.",
+ type => 'string',
+ optional => 1,
+ minLength => 5,
+ maxLength => 64
+ },
+ comment => {
+ description => "Comment.",
+ type => 'string',
+ optional => 1,
+ },
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $code = sub {
+
+ my $cfg = PMG::UserConfig->new();
+
+ die "User '$param->{userid}' already exists\n"
+ if $cfg->{$param->{userid}};
+
+ die "fixme";
+
+ $cfg->write();
+ };
+
+ PMG::UserConfig::lock_config($code, "create user failed");
+
+ return undef;
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'read',
+ path => '{userid}',
+ method => 'GET',
+ description => "Read User data.",
+ proxyto => 'master',
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ userid => get_standard_option('username'),
+ },
+ },
+ returns => {
+ type => "object",
+ properties => {},
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $cfg = PMG::UserConfig->new();
+
+ return $cfg->lookup_user_data($param->{userid});
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'write',
+ path => '{userid}',
+ method => 'PUT',
+ description => "Update user data.",
+ protected => 1,
+ proxyto => 'master',
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ userid => get_standard_option('username'),
+ comment => {
+ description => "Comment.",
+ type => 'string',
+ optional => 1,
+ },
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $code = sub {
+
+ my $cfg = PMG::UserConfig->new();
+
+ my $data = $cfg->lookup_user_data($param->{userid});
+
+ die "fixme";
+ #$data->{comment} = $param->{comment};
+
+ $cfg->write();
+ };
+
+ PMG::UserConfig::lock_config($code, "update user failed");
+
+ return undef;
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'delete',
+ path => '{userid}',
+ method => 'DELETE',
+ description => "Delete a user.",
+ protected => 1,
+ proxyto => 'master',
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ userid => get_standard_option('username'),
+ }
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $code = sub {
+
+ my $cfg = PMG::UserConfig->new();
+
+ $cfg->lookup_user_data($param->{userid}); # user exists?
+
+ delete $cfg->{$param->{userid}};
+
+ $cfg->write();
+ };
+
+ PMG::UserConfig::lock_config($code, "delete user failed");
+
+ return undef;
+ }});
+
+1;
use PVE::JSONSchema qw(get_standard_option);
-my $realm_regex = qr/[A-Za-z][A-Za-z0-9\.\-_]+/;
-
-PVE::JSONSchema::register_format('pmg-realm', \&verify_realm);
-sub 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 => 'pmg-realm',
- maxLength => 32,
-});
-
-PVE::JSONSchema::register_format('pmg-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 => 'pmg-userid',
- maxLength => 64,
-});
+use PMG::UserConfig;
sub normalize_path {
my $path = shift;
my ($ruid, $realm);
- ($username, $ruid, $realm) = verify_username($username);
+ ($username, $ruid, $realm) = PMG::Utils::verify_username($username);
if ($realm eq 'pam') {
- is_valid_user_utf8($ruid, $password);
+ die "invalid pam user (only root allowed)\n" if $ruid ne 'root';
+ authenticate_pam_user($ruid, $password);
return $username;
}
+ if ($realm eq 'pmg') {
+ my $usercfg = PMG::UserConfig->new();
+ $usercfg->authenticate_user($ruid, $password);
+ return $username;
+ }
+
die "no such realm '$realm'\n";
}
sub domain_set_password {
- my ($realm, $username, $password) = @_;
+ my ($realm, $ruid, $password) = @_;
die "no auth domain specified" if !$realm;
- die "not implemented";
-}
+ if ($realm eq 'pam') {
+ die "invalid pam user (only root allowed)\n" if $ruid ne 'root';
-sub check_user_exist {
- my ($usercfg, $username, $noerr) = @_;
+ my $cmd = ['usermod'];
- $username = verify_username($username, $noerr);
- return undef if !$username;
+ my $epw = PMG::Utils::encrypt_pw($password);
- return $usercfg->{users}->{$username} if $usercfg && $usercfg->{users}->{$username};
+ push @$cmd, '-p', $epw, $ruid;
- die "no such user ('$username')\n" if !$noerr;
+ run_command($cmd, errmsg => "change password for '$ruid' failed");
- return undef;
+ } elsif ($realm eq 'pmg') {
+ PMG::UserConfig->set_password($ruid, $password);
+ } else {
+ die "no such realm '$realm'\n";
+ }
}
+# test if user exists and is enabled
sub check_user_enabled {
- my ($usercfg, $username, $noerr) = @_;
+ my ($username, $noerr) = @_;
- my $data = check_user_exist($usercfg, $username, $noerr);
- return undef if !$data;
+ my ($userid, $ruid, $realm) = PMG::Utils::verify_username($username, 1);
- return 1 if $data->{enable};
+ if ($realm && $ruid) {
+ if ($realm eq 'pam') {
+ return 1 if $ruid eq 'root';
+ } elsif ($realm eq 'pmg') {
+ my $usercfg = PMG::UserConfig->new();
+ my $data = $usercfg->check_user_exist($ruid, $noerr);
+ return 1 if $data && $data->{enable};
+ }
+ }
die "user '$username' is disabled\n" if !$noerr;
return undef;
}
-sub is_valid_user_utf8 {
+sub authenticate_pam_user {
my ($username, $password) = @_;
- # user (www-data) need to be able to read /etc/passwd /etc/shadow
+ # user need to be able to read /etc/passwd /etc/shadow
my $pamh = Authen::PAM->new('common-auth', $username, sub {
my @res;
return 1;
}
-sub is_valid_user {
- my ($username, $password) = @_;
-
- return is_valid_user_utf8($username, encode("utf8", $password));
-}
-
1;
--- /dev/null
+package PMG::UserConfig;
+
+
+use strict;
+use warnings;
+
+use PVE::Tools;
+use PVE::INotify;
+use PVE::JSONSchema;
+
+use PMG::Utils;
+
+my $inotify_file_id = 'pmg-user.conf';
+my $config_filename = '/etc/pmg/user.conf';
+
+sub new {
+ my ($type) = @_;
+
+ my $class = ref($type) || $type;
+
+ my $cfg = PVE::INotify::read_file($inotify_file_id);
+
+ return bless $cfg, $class;
+}
+
+sub write {
+ my ($self) = @_;
+
+ PVE::INotify::write_file($inotify_file_id, $self);
+}
+
+my $lockfile = "/var/lock/pmguser.lck";
+
+sub lock_config {
+ my ($code, $errmsg) = @_;
+
+ my $p = PVE::Tools::lock_file($lockfile, undef, $code);
+ if (my $err = $@) {
+ $errmsg ? die "$errmsg: $err" : die $err;
+ }
+}
+
+sub read_user_conf {
+ my ($filename, $fh) = @_;
+
+ my $cfg = {};
+
+ if ($fh) {
+
+ my $comment = '';
+
+ while (defined(my $line = <$fh>)) {
+ next if $line =~ m/^\s*$/;
+ if ($line =~ m/^#(.*)$/) {
+ $comment = $1;
+ next;
+ }
+ if ($line =~ m/^\S+:([01]):\S+:[a-z]+:\S*:$/) {
+ my ($userid, $enable, $crypt_pass, $role, $email) = ($1, $2, $3, $4);
+ my $d = {
+ userid => $userid,
+ enable => $enable,
+ crypt_pass => $crypt_pass,
+ role => $role,
+ };
+ $d->{comment} = $comment if $comment;
+ $comment = '';
+ $d->{email} = $email if $email;
+ $cfg->{$userid} = $d;
+ } else {
+ warn "$filename: ignore invalid line $.\n";
+ $comment = '';
+ }
+ }
+ }
+
+ $cfg->{root} //= {};
+ $cfg->{root}->{userid} = 'root';
+ $cfg->{root}->{enable} = 1;
+ $cfg->{root}->{comment} = 'Unix Superuser';
+ $cfg->{root}->{role} = 'root';
+ delete $cfg->{root}->{crypt_pass};
+
+ return $cfg;
+}
+
+sub write_user_conf {
+ my ($filename, $fh, $cfg) = @_;
+
+ my $raw = '';
+
+
+ PVE::Tools::safe_print($filename, $fh, $raw);
+}
+
+PVE::INotify::register_file($inotify_file_id, $config_filename,
+ \&read_user_conf,
+ \&write_user_conf,
+ undef,
+ always_call_parser => 1);
+
+sub lookup_user_data {
+ my ($self, $username, $noerr) = @_;
+
+ return $self->{$username} if $self->{$username};
+
+ die "no such user ('$username')\n" if !$noerr;
+
+ return undef;
+}
+
+sub authenticate_user {
+ my ($self, $username, $password) = @_;
+
+ die "no password\n" if !$password;
+
+ my $data = $self->lookup_user_data($username);
+
+ if ($data->{crypt_pass}) {
+ my $encpw = crypt($password, $data->{crypt_pass});
+ die "invalid credentials\n" if ($encpw ne $data->{crypt_pass});
+ } else {
+ die "no password set\n";
+ }
+
+ return 1;
+}
+
+sub set_password {
+ my ($class, $username, $password) = @_;
+
+ lock_config(sub {
+ my $cfg = $class->new();
+ my $data = $cfg->lookup_user_data($username); # user exists
+ my $epw = PMG::Utils::encrypt_pw($password);
+ $data->{crypt_pass} = $epw;
+ $cfg->write();
+ });
+}
+
+1;
use Time::HiRes qw (gettimeofday);
use Xdgmime;
use Data::Dumper;
+use Digest::SHA;
use Net::IP;
use Socket;
use RRDs;
use PMG::MailQueue;
use PMG::SMTPPrinter;
+my $realm_regex = qr/[A-Za-z][A-Za-z0-9\.\-_]+/;
+
+PVE::JSONSchema::register_format('pmg-realm', \&verify_realm);
+sub 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 => 'pmg-realm',
+ maxLength => 32,
+});
+
+PVE::JSONSchema::register_format('pmg-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 => 'pmg-userid',
+ maxLength => 64,
+ });
+
+PVE::JSONSchema::register_standard_option('username', {
+ description => "Username (without realm)",
+ type => 'string',
+ pattern => '[^\s:\/\@]{3,60}',
+ maxLength => 64,
+});
+
sub msgquote {
my $msg = shift || '';
$msg =~ s/%/%%/g;
undef, undef, undef, undef, { sequence => $seq});
}
+sub encrypt_pw {
+ my ($pw) = @_;
+
+ my $time = substr(Digest::SHA::sha1_base64(time), 0, 8);
+ return crypt(encode("utf8", $pw), "\$5\$$time\$");
+}
+
sub file_older_than {
my ($filename, $lasttime) = @_;