8daf10cf5f9763750af85cb70b7126d259bde296
[pve-access-control.git] / PVE / API2 / AccessControl.pm
1 package PVE::API2::AccessControl;
2
3 use strict;
4 use warnings;
5 use Time::HiRes qw(usleep gettimeofday tv_interval);
6
7 use PVE::Exception qw(raise raise_perm_exc);
8 use PVE::SafeSyslog;
9 use PVE::RPCEnvironment;
10 use PVE::Cluster qw(cfs_read_file);
11 use PVE::RESTHandler;
12 use PVE::AccessControl;
13 use PVE::JSONSchema qw(get_standard_option);
14 use PVE::API2::Domains;
15 use PVE::API2::User;
16 use PVE::API2::Group;
17 use PVE::API2::Role;
18 use PVE::API2::ACL;
19
20 use base qw(PVE::RESTHandler);
21
22 __PACKAGE__->register_method ({
23     subclass => "PVE::API2::User",  
24     path => 'users',
25 });
26
27 __PACKAGE__->register_method ({
28     subclass => "PVE::API2::Group",  
29     path => 'groups',
30 });
31
32 __PACKAGE__->register_method ({
33     subclass => "PVE::API2::Role",  
34     path => 'roles',
35 });
36
37 __PACKAGE__->register_method ({
38     subclass => "PVE::API2::ACL",  
39     path => 'acl',
40 });
41
42 __PACKAGE__->register_method ({
43     subclass => "PVE::API2::Domains",  
44     path => 'domains',
45 });
46
47 __PACKAGE__->register_method ({
48     name => 'index', 
49     path => '', 
50     method => 'GET',
51     description => "Directory index.",
52     permissions => { 
53         user => 'all',
54     },
55     parameters => {
56         additionalProperties => 0,
57         properties => {},
58     },
59     returns => {
60         type => 'array',
61         items => {
62             type => "object",
63             properties => {
64                 subdir => { type => 'string' },
65             },
66         },
67         links => [ { rel => 'child', href => "{subdir}" } ],
68     },
69     code => sub {
70         my ($param) = @_;
71     
72         my $res = [];
73
74         my $ma = __PACKAGE__->method_attributes();
75
76         foreach my $info (@$ma) {
77             next if !$info->{subclass};
78
79             my $subpath = $info->{match_re}->[0];
80
81             push @$res, { subdir => $subpath };
82         }
83
84         push @$res, { subdir => 'ticket' };
85         push @$res, { subdir => 'password' };
86
87         return $res;
88     }});
89
90
91 my $verify_auth = sub {
92     my ($rpcenv, $username, $pw_or_ticket, $path, $privs) = @_;
93
94     my $normpath = PVE::AccessControl::normalize_path($path);
95
96     my $ticketuser;
97     if (($ticketuser = PVE::AccessControl::verify_ticket($pw_or_ticket, 1)) &&
98         ($ticketuser eq $username)) {
99         # valid ticket
100     } elsif (PVE::AccessControl::verify_vnc_ticket($pw_or_ticket, $username, $normpath, 1)) {
101         # valid vnc ticket
102     } else {
103         $username = PVE::AccessControl::authenticate_user($username, $pw_or_ticket);
104     }
105
106     my $privlist = [ PVE::Tools::split_list($privs) ];
107     if (!($normpath && scalar(@$privlist) && $rpcenv->check($username, $normpath, $privlist))) {
108         die "no permission ($path, $privs)\n";
109     }
110
111     return { username => $username };
112 };
113
114 my $create_ticket = sub {
115     my ($rpcenv, $username, $pw_or_ticket) = @_;
116
117     my $ticketuser;
118     if (($ticketuser = PVE::AccessControl::verify_ticket($pw_or_ticket, 1)) &&
119         ($ticketuser eq 'root@pam' || $ticketuser eq $username)) {
120         # valid ticket. Note: root@pam can create tickets for other users
121     } else {
122         $username = PVE::AccessControl::authenticate_user($username, $pw_or_ticket);
123     }
124
125     my $ticket = PVE::AccessControl::assemble_ticket($username);
126     my $csrftoken = PVE::AccessControl::assemble_csrf_prevention_token($username);
127
128     return {
129         ticket => $ticket,
130         username => $username,
131         CSRFPreventionToken => $csrftoken,
132     };
133 };
134
135 my $compute_api_permission = sub {
136     my ($rpcenv, $authuser) = @_;
137
138     my $usercfg = $rpcenv->{user_cfg};
139
140     my $nodelist = PVE::Cluster::get_nodelist();
141     my $vmlist = PVE::Cluster::get_vmlist() || {};
142     my $idlist = $vmlist->{ids} || {};
143
144     my $cfg = PVE::Storage::config();
145     my @sids =  PVE::Storage::storage_ids ($cfg);
146
147     my $res = {
148         vms => {},
149         storage => {},
150         access => {},
151         nodes => {},
152         dc => {},
153     };
154
155     my $extract_vm_caps = sub {
156         my ($path) = @_;
157         
158         my $perm = $rpcenv->permissions($authuser, $path);
159         foreach my $priv (keys %$perm) {
160             next if !($priv eq 'Permissions.Modify' || $priv =~ m/^VM\./);
161             $res->{vms}->{$priv} = 1;   
162         }
163     };
164
165     foreach my $pool (keys %{$usercfg->{pools}}) {
166         &$extract_vm_caps("/pool/$pool");
167     }
168
169     foreach my $vmid (keys %$idlist, '__phantom__') {
170         &$extract_vm_caps("/vms/$vmid");
171     }
172
173     foreach my $storeid (@sids, '__phantom__') {
174         my $perm = $rpcenv->permissions($authuser, "/storage/$storeid");
175         foreach my $priv (keys %$perm) {
176             next if !($priv eq 'Permissions.Modify' || $priv =~ m/^Datastore\./);
177             $res->{storage}->{$priv} = 1;
178         }
179     }
180
181     foreach my $path (('/access/groups')) {
182         my $perm = $rpcenv->permissions($authuser, $path);
183         foreach my $priv (keys %$perm) {
184             next if $priv !~ m/^(User|Group)\./;
185             $res->{access}->{$priv} = 1;
186         }
187     }
188
189     foreach my $group (keys %{$usercfg->{users}->{$authuser}->{groups}}, '__phantom__') {
190         my $perm = $rpcenv->permissions($authuser, "/access/groups/$group");
191         if ($perm->{'User.Modify'}) {
192             $res->{access}->{'User.Modify'} = 1;
193         }
194     }
195
196     foreach my $node (@$nodelist) {
197         my $perm = $rpcenv->permissions($authuser, "/nodes/$node");
198         foreach my $priv (keys %$perm) {
199             next if $priv !~ m/^Sys\./;
200             $res->{nodes}->{$priv} = 1;
201         }
202     }
203
204     my $perm = $rpcenv->permissions($authuser, "/");
205     $res->{dc}->{'Sys.Audit'} = 1 if $perm->{'Sys.Audit'};
206
207     return $res;
208 };
209
210 __PACKAGE__->register_method ({
211     name => 'create_ticket', 
212     path => 'ticket', 
213     method => 'POST',
214     permissions => { 
215         description => "You need to pass valid credientials.",
216         user => 'world' 
217     },
218     protected => 1, # else we can't access shadow files
219     description => "Create or verify authentication ticket.",
220     parameters => {
221         additionalProperties => 0,
222         properties => {
223             username => {
224                 description => "User name",
225                 type => 'string',
226                 maxLength => 64,
227             },
228             realm =>  get_standard_option('realm', {
229                 description => "You can optionally pass the realm using this parameter. Normally the realm is simply added to the username <username>\@<relam>.",
230                 optional => 1}),
231             password => { 
232                 description => "The secret password. This can also be a valid ticket.",
233                 type => 'string',
234             },
235             path => {
236                 description => "Verify ticket, and check if user have access 'privs' on 'path'",
237                 type => 'string',
238                 requires => 'privs',
239                 optional => 1,
240                 maxLength => 64,
241             },
242             privs => { 
243                 description => "Verify ticket, and check if user have access 'privs' on 'path'",
244                 type => 'string' , format => 'pve-priv-list',
245                 requires => 'path',
246                 optional => 1,
247                 maxLength => 64,
248             },
249         }
250     },
251     returns => {
252         type => "object",
253         properties => {
254             username => { type => 'string' },
255             ticket => { type => 'string', optional => 1},
256             CSRFPreventionToken => { type => 'string', optional => 1 },
257         }
258     },
259     code => sub {
260         my ($param) = @_;
261     
262         my $username = $param->{username};
263         $username .= "\@$param->{realm}" if $param->{realm};
264
265         my $rpcenv = PVE::RPCEnvironment::get();
266
267         my $res;
268
269         my $starttime = [gettimeofday];
270
271         eval {
272             # test if user exists and is enabled
273             $rpcenv->check_user_enabled($username);
274
275             if ($param->{path} && $param->{privs}) {
276                 $res = &$verify_auth($rpcenv, $username, $param->{password},
277                                      $param->{path}, $param->{privs});
278             } else {
279                 $res = &$create_ticket($rpcenv, $username, $param->{password});
280             }
281         };
282         if (my $err = $@) {
283             my $clientip = $rpcenv->get_client_ip() || '';
284             syslog('err', "authentication failure; rhost=$clientip user=$username msg=$err");
285             # do not return any info to prevent user enumeration attacks
286             # always try to delay exactly 3 seconds to prevent timing attacks
287             my $elapsed;
288             while (($elapsed = tv_interval($starttime)) < 3) {
289                 usleep(int((3 - $elapsed)*1000000));
290             }
291             die "authentication failure\n"; 
292         }
293
294         $res->{cap} = &$compute_api_permission($rpcenv, $username);
295
296         PVE::Cluster::log_msg('info', 'root@pam', "successful auth for user '$username'");
297
298         return $res;
299     }});
300
301 __PACKAGE__->register_method ({
302     name => 'change_passsword', 
303     path => 'password', 
304     method => 'PUT',
305     permissions => { 
306         description => "Each user is allowed to change his own password. A user can change the password of another user if he has 'Realm.AllocateUser' (on the realm of user <userid>) and 'User.Modify' permission on /access/groups/<group> on a group where user <userid> is member of.",
307         check => [ 'or', 
308                    ['userid-param', 'self'],
309                    [ 'and',
310                      [ 'userid-param', 'Realm.AllocateUser'],
311                      [ 'userid-group', ['User.Modify']]
312                    ]
313             ],
314     },
315     protected => 1, # else we can't access shadow files
316     description => "Change user password.",
317     parameters => {
318         additionalProperties => 0,
319         properties => {
320             userid => get_standard_option('userid'),
321             password => { 
322                 description => "The new password.",
323                 type => 'string',
324                 minLength => 5, 
325                 maxLength => 64,
326             },
327         }
328     },
329     returns => { type => "null" },
330     code => sub {
331         my ($param) = @_;
332
333         my $rpcenv = PVE::RPCEnvironment::get();
334         my $authuser = $rpcenv->get_user();
335
336         my ($userid, $ruid, $realm) = PVE::AccessControl::verify_username($param->{userid});
337
338         $rpcenv->check_user_exist($userid);
339
340         if ($authuser eq 'root@pam') {
341             # OK - root can change anything
342         } else {
343             if ($authuser eq $userid) {
344                 $rpcenv->check_user_enabled($userid);
345                 # OK - each user can change its own password
346             } else {
347                 # only root may change root password
348                 raise_perm_exc() if $userid eq 'root@pam';
349                 # do not allow to change system user passwords
350                 raise_perm_exc() if $realm eq 'pam';
351             }
352         }
353
354         PVE::AccessControl::domain_set_password($realm, $ruid, $param->{password});
355
356         PVE::Cluster::log_msg('info', 'root@pam', "changed password for user '$userid'");
357
358         return undef;
359     }});
360
361 1;