]> git.proxmox.com Git - pmg-api.git/commitdiff
add user config, rework access control
authorDietmar Maurer <dietmar@proxmox.com>
Sat, 25 Mar 2017 16:05:07 +0000 (17:05 +0100)
committerDietmar Maurer <dietmar@proxmox.com>
Sat, 25 Mar 2017 16:05:07 +0000 (17:05 +0100)
Makefile
PMG/API2/AccessControl.pm
PMG/API2/Config.pm
PMG/API2/Users.pm [new file with mode: 0644]
PMG/AccessControl.pm
PMG/UserConfig.pm [new file with mode: 0644]
PMG/Utils.pm

index 7a6f488e23d6e3abca662c3793f7c5d3cf8b867f..2a2d1bb64019f4fd4d5317c2c3b5d54be30de276 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -68,6 +68,7 @@ LIBSOURCES =                          \
        PMG/Unpack.pm                   \
        PMG/RuleCache.pm                \
        PMG/Statistic.pm                \
+       PMG/UserConfig.pm               \
        PMG/LDAPConfig.pm               \
        PMG/LDAPSet.pm                  \
        PMG/LDAPCache.pm                \
@@ -111,6 +112,7 @@ LIBSOURCES =                                \
        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              \
index dc712b71c105b61125f671d7ee40f058fc55f32a..9c3c755949146e25d1b40638d02a8bf4b29faa5e 100644 (file)
@@ -9,6 +9,8 @@ use PVE::RESTEnvironment;
 use PVE::RESTHandler;
 use PVE::JSONSchema qw(get_standard_option);
 
+use PMG::Utils;
+use PMG::UserConfig;
 use PMG::AccessControl;
 
 use Data::Dumper;
@@ -133,12 +135,9 @@ __PACKAGE__->register_method ({
 
        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 = $@) {
@@ -178,24 +177,20 @@ __PACKAGE__->register_method ({
        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'");
 
index a52281c7fec97f3d60ffc24adf2434b85d4be9c1..a2d885475db5735cacc2acbe6d9af8541ce9ee3e 100644 (file)
@@ -19,11 +19,17 @@ use PMG::API2::Transport;
 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',
@@ -89,6 +95,7 @@ __PACKAGE__->register_method ({
 
        push @$res, { section => 'ldap' };
        push @$res, { section => 'mynetworks' };
+       push @$res, { section => 'users' };
        push @$res, { section => 'domains' };
        push @$res, { section => 'cluster' };
        push @$res, { section => 'ruledb' };
diff --git a/PMG/API2/Users.pm b/PMG/API2/Users.pm
new file mode 100644 (file)
index 0000000..ee0f6d6
--- /dev/null
@@ -0,0 +1,196 @@
+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;
index 56b996f78358803448a974bdc68c1901476740d7..08551b592ec41e16707f7c20714be64d7d9b44d3 100644 (file)
@@ -7,59 +7,7 @@ use PVE::Tools;
 
 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;
@@ -86,54 +34,71 @@ sub authenticate_user {
 
     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;
@@ -168,10 +133,4 @@ sub is_valid_user_utf8 {
     return 1;
 }
 
-sub is_valid_user {
-    my ($username, $password) = @_;
-
-    return is_valid_user_utf8($username, encode("utf8", $password));
-}
-
 1;
diff --git a/PMG/UserConfig.pm b/PMG/UserConfig.pm
new file mode 100644 (file)
index 0000000..3f8518e
--- /dev/null
@@ -0,0 +1,141 @@
+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;
index ca7cfa1fa5f79d701c4e955ad94cb7e469cfbb80..617ca7d69a7eb8dd4db1ffe2027af5704c95b0bb 100644 (file)
@@ -15,6 +15,7 @@ use MIME::Parser;
 use Time::HiRes qw (gettimeofday);
 use Xdgmime;
 use Data::Dumper;
+use Digest::SHA;
 use Net::IP;
 use Socket;
 use RRDs;
@@ -29,6 +30,67 @@ use PMG::AtomicFile;
 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;
@@ -42,6 +104,13 @@ sub lastid {
        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) = @_;