new API to change password
authorDietmar Maurer <dietmar@proxmox.com>
Fri, 20 Jan 2012 11:45:24 +0000 (12:45 +0100)
committerDietmar Maurer <dietmar@proxmox.com>
Fri, 20 Jan 2012 11:45:24 +0000 (12:45 +0100)
Started to implement fine grained permission checks.

PVE/API2/AccessControl.pm
PVE/API2/User.pm
PVE/AccessControl.pm
PVE/RPCEnvironment.pm

index 24699d2..6136ae5 100644 (file)
@@ -3,6 +3,7 @@ package PVE::API2::AccessControl;
 use strict;
 use warnings;
 
+use PVE::Exception qw(raise raise_perm_exc);
 use PVE::SafeSyslog;
 use PVE::RPCEnvironment;
 use PVE::Cluster qw(cfs_read_file);
@@ -77,6 +78,7 @@ __PACKAGE__->register_method ({
        }
 
        push @$res, { subdir => 'ticket' };
+       push @$res, { subdir => 'password' };
 
        return $res;
     }});
@@ -204,4 +206,60 @@ __PACKAGE__->register_method ({
        return $res;
     }});
 
+__PACKAGE__->register_method ({
+    name => 'change_passsword', 
+    path => 'password', 
+    method => 'PUT',
+    permissions => { user => 'all' },
+    protected => 1, # else we can't access shadow files
+    description => "Change user password.",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           userid => get_standard_option('userid'),
+           password => { 
+               description => "The new password.",
+               type => 'string',
+               minLength => 5, 
+               maxLength => 64,
+           },
+       }
+    },
+    returns => { type => "null" },
+    code => sub {
+       my ($param) = @_;
+
+       my $rpcenv = PVE::RPCEnvironment::get();
+       my $authuser = $rpcenv->get_user();
+
+       my ($userid, $ruid, $realm) = PVE::AccessControl::verify_username($param->{userid});
+
+       my $usercfg = $rpcenv->{user_cfg};
+       PVE::AccessControl::check_user_exist($usercfg, $userid);
+
+       if ($authuser eq 'root@pam') {
+           # OK - root can change anything
+       } else {
+           if ($authuser eq $userid) {
+               $rpcenv->check_user_enabled($userid);
+               # OK - each user can change its own password
+           } else {
+               raise_perm_exc() if $userid eq 'root@pam';
+
+               my $privs = [ 'Sys.UserMod', 'Sys.UserAdd' ];
+               if (!$rpcenv->check_any($authuser, "/access", $privs, 1)) {
+                   my $groups = $rpcenv->filter_groups($authuser, sub { return "/access/groups/" . shift; }, $privs, 1);
+                   my $allowed_users = $rpcenv->group_member_join([keys %$groups]);      
+                   raise_perm_exc() if !$allowed_users->{$userid};
+               }
+           }
+       }
+
+       PVE::AccessControl::domain_set_password($realm, $ruid, $param->{password});
+
+       PVE::Cluster::log_msg('info', 'root@pam', "changed password for user '$userid'");
+
+       return undef;
+    }});
+
 1;
index 8b06364..2d38dd1 100644 (file)
@@ -2,6 +2,7 @@ package PVE::API2::User;
 
 use strict;
 use warnings;
+use PVE::Exception qw(raise raise_perm_exc);
 use PVE::Cluster qw (cfs_read_file cfs_write_file);
 use PVE::Tools qw(split_list);
 use PVE::AccessControl;
@@ -61,15 +62,22 @@ __PACKAGE__->register_method ({
        my ($param) = @_;
     
        my $rpcenv = PVE::RPCEnvironment::get();
+       my $usercfg = $rpcenv->{user_cfg};
        my $authuser = $rpcenv->get_user();
 
        my $res = [];
 
-       my $usercfg = cfs_read_file("user.cfg");
+       my $privs = [ 'Sys.UserMod', 'Sys.UserAdd' ];
+
+       my $canUserMod = $rpcenv->check_any($authuser, "/access", $privs, 1);
+       my $groups = $rpcenv->filter_groups($authuser, sub { return "/access/groups/" . shift; }, $privs, 1);
+       my $allowed_users = $rpcenv->group_member_join([keys %$groups]);      
+
        foreach my $user (keys %{$usercfg->{users}}) {
-           # root sees all entries, a user only sees its own entry
-           next if $authuser ne 'root@pam' && $user ne $authuser;
+
+           if (!($canUserMod || $user eq $authuser)) {
+               next if !$allowed_users->{$user};
+           }
 
            my $entry = &$extract_user_data($usercfg->{users}->{$user});
 
@@ -95,7 +103,13 @@ __PACKAGE__->register_method ({
        additionalProperties => 0,
        properties => {
            userid => get_standard_option('userid'),
-           password => { type => 'string', optional => 1, minLength => 5, maxLength => 64 },
+           password => {
+               description => "Initial password.",
+               type => 'string', 
+               optional => 1, 
+               minLength => 5, 
+               maxLength => 64 
+           },
            groups => { type => 'string', optional => 1, format => 'pve-groupid-list'},
            firstname => { type => 'string', optional => 1 },
            lastname => { type => 'string', optional => 1 },
@@ -187,11 +201,9 @@ __PACKAGE__->register_method ({
            PVE::AccessControl::verify_username($param->{userid});
 
        my $usercfg = cfs_read_file("user.cfg");
-       my $data = $usercfg->{users}->{$username};
-
-       die "user '$username' does not exist\n" if !$data;
 
+       my $data = PVE::AccessControl::check_user_exist($usercfg, $username);
        return &$extract_user_data($data, 1);
     }});
 
@@ -205,7 +217,6 @@ __PACKAGE__->register_method ({
        additionalProperties => 0,
        properties => {
            userid => get_standard_option('userid'),
-           password => { type => 'string', optional => 1, minLength => 5, maxLength => 64 },
            groups => { type => 'string', optional => 1,  format => 'pve-groupid-list'  },
            append => { 
                type => 'boolean', 
@@ -232,20 +243,16 @@ __PACKAGE__->register_method ({
     returns => { type => 'null' },
     code => sub {
        my ($param) = @_;
+
+       my ($username, $ruid, $realm) = 
+           PVE::AccessControl::verify_username($param->{userid});
        
        PVE::AccessControl::lock_user_config(
            sub {
-
-               my ($username, $ruid, $realm) = 
-                   PVE::AccessControl::verify_username($param->{userid});
        
                my $usercfg = cfs_read_file("user.cfg");
 
-               die "user '$username' does not exist\n" 
-                   if !$usercfg->{users}->{$username};
-
-               PVE::AccessControl::domain_set_password($realm, $ruid, $param->{password})
-                   if defined($param->{password});
+               PVE::AccessControl::check_user_exist($usercfg, $username);
 
                $usercfg->{users}->{$username}->{enable} = $param->{enable} if defined($param->{enable});
 
@@ -281,6 +288,7 @@ __PACKAGE__->register_method ({
     path => '{userid}', 
     method => 'DELETE',
     description => "Delete user.",
+    permissions => { user => 'all' },
     parameters => {
        additionalProperties => 0,
        properties => {
@@ -290,23 +298,33 @@ __PACKAGE__->register_method ({
     returns => { type => 'null' },
     code => sub {
        my ($param) = @_;
+       
+       my $rpcenv = PVE::RPCEnvironment::get();
+       my $authuser = $rpcenv->get_user();
+
+       my ($userid, $ruid, $realm) = 
+           PVE::AccessControl::verify_username($param->{userid});
 
        PVE::AccessControl::lock_user_config(
            sub {
 
-               my ($username, $ruid, $realm) = 
-                   PVE::AccessControl::verify_username($param->{userid});
-
                my $usercfg = cfs_read_file("user.cfg");
 
-               die "user '$username' does not exist\n" 
-                   if !$usercfg->{users}->{$username};
+               PVE::AccessControl::check_user_exist($usercfg, $userid);
 
-               delete ($usercfg->{users}->{$username});
+               my $privs = [ 'Sys.UserAdd' ]; # there is no Sys.UserDel
+               if (!$rpcenv->check($authuser, "/access", $privs, 1)) {
+                   my $groups = $rpcenv->filter_groups($authuser, sub { return "/access/groups/" . shift; }, $privs, 1);
+                   my $allowed_users = $rpcenv->group_member_join([keys %$groups]);      
+                   raise_perm_exc() if !$allowed_users->{$userid};
+               }
+
+               delete ($usercfg->{users}->{$userid});
 
                PVE::AccessControl::delete_shadow_password($ruid) if $realm eq 'pve';
-               PVE::AccessControl::delete_user_group($username, $usercfg);
-               PVE::AccessControl::delete_user_acl($username, $usercfg);
+
+               PVE::AccessControl::delete_user_group($userid, $usercfg);
+               PVE::AccessControl::delete_user_acl($userid, $usercfg);
 
                cfs_write_file("user.cfg", $usercfg);
            }, "delete user failed");
index 9bb32ef..72ec12d 100644 (file)
@@ -373,18 +373,30 @@ sub authenticate_user_domain {
     }
 }
 
-sub check_user_enabled {
+sub check_user_exist {
     my ($usercfg, $username, $noerr) = @_;
 
     $username = verify_username($username, $noerr);
     return undef if !$username;
  
-    return 1 if $usercfg && $usercfg->{users}->{$username} &&
-       $usercfg->{users}->{$username}->{enable};
+    return $usercfg->{users}->{$username} if $usercfg && $usercfg->{users}->{$username};
+
+    die "no such user ('$username')\n" if !$noerr;
+    return undef;
+}
+
+sub check_user_enabled {
+    my ($usercfg, $username, $noerr) = @_;
+
+    my $data = check_user_exist($usercfg, $username, $noerr);
+    return undef if !$data;
+
+    return 1 if $data->{enable};
 
     return 1 if $username eq 'root@pam'; # root is always enabled
 
-    die "no such user ('$username')\n" if !$noerr;
+    die "user '$username' is disabled\n" if !$noerr;
  
     return undef;
 }
@@ -541,11 +553,13 @@ my $privgroups = {
     Sys => {
        root => [
            'Sys.PowerMgmt',     
-           'Sys.Modify', # edit/change node settings    
+           'Sys.Modify', # edit/change node settings
+           'Sys.UserAdd', # add/delete users
        ],
        admin => [
            'Sys.Console',    
            'Sys.Syslog',
+           'Sys.UserMod', # modify users settings
        ],
        user => [],
        audit => [
index d92320c..16b8ba8 100644 (file)
@@ -7,6 +7,7 @@ use IO::Handle;
 use IO::File;
 use IO::Select;
 use Fcntl qw(:flock);
+use PVE::Exception qw(raise raise_perm_exc);
 use PVE::SafeSyslog;
 use PVE::Tools;
 use PVE::INotify;
@@ -154,17 +155,42 @@ sub permissions {
 }
 
 sub check {
-    my ($self, $user, $path, $privs) = @_;
+    my ($self, $user, $path, $privs, $noerr) = @_;
 
     my $perm = $self->permissions($user, $path);
 
     foreach my $priv (@$privs) {
-       return undef if !$perm->{$priv};
+       PVE::AccessControl::verify_privname($priv);
+       if (!$perm->{$priv}) {
+           return undef if $noerr;
+           raise_perm_exc("$path, $priv"); 
+       }
     };
 
     return 1;
 };
 
+sub check_any {
+    my ($self, $user, $path, $privs, $noerr) = @_;
+
+    my $perm = $self->permissions($user, $path);
+
+    my $found = 0;
+    foreach my $priv (@$privs) {
+       PVE::AccessControl::verify_privname($priv);
+       if ($perm->{$priv}) {
+           $found = 1;
+           last;
+       }
+    };
+
+    return 1 if $found;
+
+    return undef if $noerr;
+
+    raise_perm_exc("$path, " . join("|", @$privs)); 
+};
+
 sub check_user_enabled {
     my ($self, $user, $noerr) = @_;
     
@@ -172,6 +198,61 @@ sub check_user_enabled {
     return PVE::AccessControl::check_user_enabled($cfg, $user, $noerr);
 }
 
+sub check_user_exist {
+    my ($self, $user, $noerr) = @_;
+    
+    my $cfg = $self->{user_cfg};
+    return PVE::AccessControl::check_user_exist($cfg, $user, $noerr);
+}
+
+sub is_group_member {
+    my ($self, $group, $user) = @_;
+
+    my $cfg = $self->{user_cfg};
+
+    return 0 if !$cfg->{groups}->{$group};
+
+    return defined($cfg->{groups}->{$group}->{users}->{$user});
+}
+
+sub filter_groups {
+    my ($self, $user, $getPath, $privs, $any) = @_;
+
+    my $cfg = $self->{user_cfg};
+
+    my $groups = {};
+    foreach my $group (keys %{$cfg->{groups}}) {
+       if ($any) {
+           if ($self->check_any($user, &$getPath($group), $privs, 1)) {
+               $groups->{$group} = $cfg->{groups}->{$group};
+           }
+       } else {
+           if ($self->check($user, &$getPath($group), $privs, 1)) {
+               $groups->{$group} = $cfg->{groups}->{$group};
+           }
+       }
+    }
+
+    return $groups;
+}
+
+sub group_member_join {
+    my ($self, $grouplist) = @_;
+
+    my $users = {};
+
+    my $cfg = $self->{user_cfg};
+    foreach my $group (@$grouplist) {
+       my $data = $cfg->{groups}->{$group};
+       next if !$data;
+       foreach my $user (keys %{$data->{users}}) {
+           $users->{$user} = 1;
+       }
+    }
+
+    return $users;
+}
+
 # initialize environment - must be called once at program startup
 sub init {
     my ($class, $type, %params) = @_;