677592b2b27a794e36344040b068257eae2bcf4b
[pve-access-control.git] / PVE / API2 / AccessControl.pm
1 package PVE::API2::AccessControl;
2
3 use strict;
4 use warnings;
5
6 use JSON;
7 use MIME::Base64;
8
9 use PVE::Exception qw(raise raise_perm_exc);
10 use PVE::SafeSyslog;
11 use PVE::RPCEnvironment;
12 use PVE::Cluster qw(cfs_read_file);
13 use PVE::Corosync;
14 use PVE::RESTHandler;
15 use PVE::AccessControl;
16 use PVE::JSONSchema qw(get_standard_option);
17 use PVE::API2::Domains;
18 use PVE::API2::User;
19 use PVE::API2::Group;
20 use PVE::API2::Role;
21 use PVE::API2::ACL;
22
23 my $u2f_available = 0;
24 eval {
25     require PVE::U2F;
26     $u2f_available = 1;
27 };
28
29 use base qw(PVE::RESTHandler);
30
31 __PACKAGE__->register_method ({
32     subclass => "PVE::API2::User",  
33     path => 'users',
34 });
35
36 __PACKAGE__->register_method ({
37     subclass => "PVE::API2::Group",  
38     path => 'groups',
39 });
40
41 __PACKAGE__->register_method ({
42     subclass => "PVE::API2::Role",  
43     path => 'roles',
44 });
45
46 __PACKAGE__->register_method ({
47     subclass => "PVE::API2::ACL",  
48     path => 'acl',
49 });
50
51 __PACKAGE__->register_method ({
52     subclass => "PVE::API2::Domains",  
53     path => 'domains',
54 });
55
56 __PACKAGE__->register_method ({
57     name => 'index', 
58     path => '', 
59     method => 'GET',
60     description => "Directory index.",
61     permissions => { 
62         user => 'all',
63     },
64     parameters => {
65         additionalProperties => 0,
66         properties => {},
67     },
68     returns => {
69         type => 'array',
70         items => {
71             type => "object",
72             properties => {
73                 subdir => { type => 'string' },
74             },
75         },
76         links => [ { rel => 'child', href => "{subdir}" } ],
77     },
78     code => sub {
79         my ($param) = @_;
80     
81         my $res = [];
82
83         my $ma = __PACKAGE__->method_attributes();
84
85         foreach my $info (@$ma) {
86             next if !$info->{subclass};
87
88             my $subpath = $info->{match_re}->[0];
89
90             push @$res, { subdir => $subpath };
91         }
92
93         push @$res, { subdir => 'ticket' };
94         push @$res, { subdir => 'password' };
95
96         return $res;
97     }});
98
99
100 my $verify_auth = sub {
101     my ($rpcenv, $username, $pw_or_ticket, $otp, $path, $privs) = @_;
102
103     my $normpath = PVE::AccessControl::normalize_path($path);
104
105     my $ticketuser;
106     if (($ticketuser = PVE::AccessControl::verify_ticket($pw_or_ticket, 1)) &&
107         ($ticketuser eq $username)) {
108         # valid ticket
109     } elsif (PVE::AccessControl::verify_vnc_ticket($pw_or_ticket, $username, $normpath, 1)) {
110         # valid vnc ticket
111     } else {
112         $username = PVE::AccessControl::authenticate_user($username, $pw_or_ticket, $otp);
113     }
114
115     my $privlist = [ PVE::Tools::split_list($privs) ];
116     if (!($normpath && scalar(@$privlist) && $rpcenv->check($username, $normpath, $privlist))) {
117         die "no permission ($path, $privs)\n";
118     }
119
120     return { username => $username };
121 };
122
123 my $create_ticket = sub {
124     my ($rpcenv, $username, $pw_or_ticket, $otp) = @_;
125
126     my ($ticketuser, $u2fdata);
127     if (($ticketuser = PVE::AccessControl::verify_ticket($pw_or_ticket, 1)) &&
128         ($ticketuser eq 'root@pam' || $ticketuser eq $username)) {
129         # valid ticket. Note: root@pam can create tickets for other users
130     } else {
131         ($username, $u2fdata) = PVE::AccessControl::authenticate_user($username, $pw_or_ticket, $otp);
132     }
133
134     my %extra;
135     my $ticket_data = $username;
136     if (defined($u2fdata)) {
137         my $u2f = get_u2f_instance($rpcenv, $u2fdata->@{qw(publicKey keyHandle)});
138         my $challenge = $u2f->auth_challenge()
139             or die "failed to get u2f challenge\n";
140         $challenge = decode_json($challenge);
141         $extra{U2FChallenge} = $challenge;
142         $ticket_data = "u2f!$username!$challenge->{challenge}";
143     }
144
145     my $ticket = PVE::AccessControl::assemble_ticket($ticket_data);
146     my $csrftoken = PVE::AccessControl::assemble_csrf_prevention_token($username);
147
148     return {
149         ticket => $ticket,
150         username => $username,
151         CSRFPreventionToken => $csrftoken,
152         %extra,
153     };
154 };
155
156 my $compute_api_permission = sub {
157     my ($rpcenv, $authuser) = @_;
158
159     my $usercfg = $rpcenv->{user_cfg};
160
161     my $res = {};
162     my $priv_re_map = {
163         vms => qr/VM\.|Permissions\.Modify/,
164         access => qr/(User|Group)\.|Permissions\.Modify/,
165         storage => qr/Datastore\.|Permissions\.Modify/,
166         nodes => qr/Sys\.|Permissions\.Modify/,
167         dc => qr/Sys\.Audit/,
168     };
169     map { $res->{$_} = {} } keys %$priv_re_map;
170
171     my $required_paths = ['/', '/nodes', '/access/groups', '/vms', '/storage'];
172
173     my $checked_paths = {};
174     foreach my $path (@$required_paths, keys %{$usercfg->{acl}}) {
175         next if $checked_paths->{$path};
176         $checked_paths->{$path} = 1;
177
178         my $path_perm = $rpcenv->permissions($authuser, $path);
179
180         my $toplevel = ($path =~ /^\/(\w+)/) ? $1 : 'dc';
181         if ($toplevel eq 'pool') {
182             foreach my $priv (keys %$path_perm) {
183                 if ($priv =~ m/^VM\./) {
184                     $res->{vms}->{$priv} = 1;
185                 } elsif ($priv =~ m/^Datastore\./) {
186                     $res->{storage}->{$priv} = 1;
187                 } elsif ($priv eq 'Permissions.Modify') {
188                     $res->{storage}->{$priv} = 1;
189                     $res->{vms}->{$priv} = 1;
190                 }
191             }
192         } else {
193             my $priv_regex = $priv_re_map->{$toplevel} // next;
194             foreach my $priv (keys %$path_perm) {
195                 next if $priv !~ m/^($priv_regex)/;
196                 $res->{$toplevel}->{$priv} = 1;
197             }
198         }
199     }
200
201     return $res;
202 };
203
204 __PACKAGE__->register_method ({
205     name => 'get_ticket', 
206     path => 'ticket', 
207     method => 'GET',
208     permissions => { user => 'world' },
209     description => "Dummy. Useful for formatters which want to provide a login page.",
210     parameters => {
211         additionalProperties => 0,
212     },
213     returns => { type => "null" },
214     code => sub { return undef; }});
215   
216 __PACKAGE__->register_method ({
217     name => 'create_ticket', 
218     path => 'ticket', 
219     method => 'POST',
220     permissions => { 
221         description => "You need to pass valid credientials.",
222         user => 'world' 
223     },
224     protected => 1, # else we can't access shadow files
225     description => "Create or verify authentication ticket.",
226     parameters => {
227         additionalProperties => 0,
228         properties => {
229             username => {
230                 description => "User name",
231                 type => 'string',
232                 maxLength => 64,
233                 completion => \&PVE::AccessControl::complete_username,
234             },
235             realm =>  get_standard_option('realm', {
236                 description => "You can optionally pass the realm using this parameter. Normally the realm is simply added to the username <username>\@<relam>.",
237                 optional => 1,
238                 completion => \&PVE::AccessControl::complete_realm,
239             }),
240             password => { 
241                 description => "The secret password. This can also be a valid ticket.",
242                 type => 'string',
243             },
244             otp => {
245                 description => "One-time password for Two-factor authentication.",
246                 type => 'string',
247                 optional => 1,
248             },
249             path => {
250                 description => "Verify ticket, and check if user have access 'privs' on 'path'",
251                 type => 'string',
252                 requires => 'privs',
253                 optional => 1,
254                 maxLength => 64,
255             },
256             privs => { 
257                 description => "Verify ticket, and check if user have access 'privs' on 'path'",
258                 type => 'string' , format => 'pve-priv-list',
259                 requires => 'path',
260                 optional => 1,
261                 maxLength => 64,
262             },
263         }
264     },
265     returns => {
266         type => "object",
267         properties => {
268             username => { type => 'string' },
269             ticket => { type => 'string', optional => 1},
270             CSRFPreventionToken => { type => 'string', optional => 1 },
271             clustername => { type => 'string', optional => 1 },
272             # cap => computed api permissions, unless there's a u2f challenge
273         }
274     },
275     code => sub {
276         my ($param) = @_;
277     
278         my $username = $param->{username};
279         $username .= "\@$param->{realm}" if $param->{realm};
280
281         my $rpcenv = PVE::RPCEnvironment::get();
282
283         my $res;
284         eval {
285             # test if user exists and is enabled
286             $rpcenv->check_user_enabled($username);
287
288             if ($param->{path} && $param->{privs}) {
289                 $res = &$verify_auth($rpcenv, $username, $param->{password}, $param->{otp},
290                                      $param->{path}, $param->{privs});
291             } else {
292                 $res = &$create_ticket($rpcenv, $username, $param->{password}, $param->{otp});
293             }
294         };
295         if (my $err = $@) {
296             my $clientip = $rpcenv->get_client_ip() || '';
297             syslog('err', "authentication failure; rhost=$clientip user=$username msg=$err");
298             # do not return any info to prevent user enumeration attacks
299             die PVE::Exception->new("authentication failure\n", code => 401);
300         }
301
302         $res->{cap} = &$compute_api_permission($rpcenv, $username)
303             if !defined($res->{U2FChallenge});
304
305         if (PVE::Corosync::check_conf_exists(1)) {
306             if ($rpcenv->check($username, '/', ['Sys.Audit'], 1)) {
307                 eval {
308                     my $conf = cfs_read_file('corosync.conf');
309                     my $totem = PVE::Corosync::totem_config($conf);
310                     if ($totem->{cluster_name}) {
311                         $res->{clustername} = $totem->{cluster_name};
312                     }
313                 };
314                 warn "$@\n" if $@;
315             }
316         }
317
318         PVE::Cluster::log_msg('info', 'root@pam', "successful auth for user '$username'");
319
320         return $res;
321     }});
322
323 __PACKAGE__->register_method ({
324     name => 'change_password',
325     path => 'password', 
326     method => 'PUT',
327     permissions => { 
328         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.",
329         check => [ 'or', 
330                    ['userid-param', 'self'],
331                    [ 'and',
332                      [ 'userid-param', 'Realm.AllocateUser'],
333                      [ 'userid-group', ['User.Modify']]
334                    ]
335             ],
336     },
337     protected => 1, # else we can't access shadow files
338     description => "Change user password.",
339     parameters => {
340         additionalProperties => 0,
341         properties => {
342             userid => get_standard_option('userid-completed'),
343             password => { 
344                 description => "The new password.",
345                 type => 'string',
346                 minLength => 5, 
347                 maxLength => 64,
348             },
349         }
350     },
351     returns => { type => "null" },
352     code => sub {
353         my ($param) = @_;
354
355         my $rpcenv = PVE::RPCEnvironment::get();
356         my $authuser = $rpcenv->get_user();
357
358         my ($userid, $ruid, $realm) = PVE::AccessControl::verify_username($param->{userid});
359
360         $rpcenv->check_user_exist($userid);
361
362         if ($authuser eq 'root@pam') {
363             # OK - root can change anything
364         } else {
365             if ($authuser eq $userid) {
366                 $rpcenv->check_user_enabled($userid);
367                 # OK - each user can change its own password
368             } else {
369                 # only root may change root password
370                 raise_perm_exc() if $userid eq 'root@pam';
371                 # do not allow to change system user passwords
372                 raise_perm_exc() if $realm eq 'pam';
373             }
374         }
375
376         PVE::AccessControl::domain_set_password($realm, $ruid, $param->{password});
377
378         PVE::Cluster::log_msg('info', 'root@pam', "changed password for user '$userid'");
379
380         return undef;
381     }});
382
383 sub get_u2f_config() {
384     die "u2f support not available\n" if !$u2f_available;
385
386     my $dc = cfs_read_file('datacenter.cfg');
387     my $u2f = $dc->{u2f};
388     die "u2f not configured in datacenter.cfg\n" if !$u2f;
389     $u2f = PVE::JSONSchema::parse_property_string($PVE::Cluster::u2f_format, $u2f);
390     return $u2f;
391 }
392
393 sub get_u2f_instance {
394     my ($rpcenv, $publicKey, $keyHandle) = @_;
395
396     # We store the public key base64 encoded (as the api provides it in binary)
397     $publicKey = decode_base64($publicKey) if defined($publicKey);
398
399     my $u2fconfig = get_u2f_config();
400     my $u2f = PVE::U2F->new();
401
402     # via the 'Host' header (in case a node has multiple hosts available).
403     my $origin = $u2fconfig->{origin};
404     if (!defined($origin)) {
405         $origin = $rpcenv->get_request_host(1);
406         if ($origin) {
407             $origin = "https://$origin";
408         } else {
409             die "failed to figure out u2f origin\n";
410         }
411     }
412
413     my $appid = $u2fconfig->{appid} // $origin;
414     $u2f->set_appid($appid);
415     $u2f->set_origin($origin);
416     $u2f->set_publicKey($publicKey) if defined($publicKey);
417     $u2f->set_keyHandle($keyHandle) if defined($keyHandle);
418     return $u2f;
419 }
420
421 __PACKAGE__->register_method ({
422     name => 'change_tfa',
423     path => 'tfa',
424     method => 'PUT',
425     permissions => {
426         description => 'A user can change their own u2f token.',
427         check => [ 'or',
428                    ['userid-param', 'self'],
429                    [ 'and',
430                      [ 'userid-param', 'Realm.AllocateUser'],
431                      [ 'userid-group', ['User.Modify']]
432                    ]
433             ],
434     },
435     protected => 1, # else we can't access shadow files
436     description => "Change user u2f authentication.",
437     parameters => {
438         additionalProperties => 0,
439         properties => {
440             userid => get_standard_option('userid', {
441                 completion => \&PVE::AccessControl::complete_username,
442             }),
443             password => {
444                 optional => 1, # Only required if not root@pam
445                 description => "The current password.",
446                 type => 'string',
447                 minLength => 5,
448                 maxLength => 64,
449             },
450             action => {
451                 description => 'The action to perform',
452                 type => 'string',
453                 enum => [qw(delete new confirm)],
454             },
455             response => {
456                 optional => 1,
457                 description => 'The response to the current registration challenge.',
458                 type => 'string',
459             },
460         }
461     },
462     returns => { type => 'object' },
463     code => sub {
464         my ($param) = @_;
465
466         my $rpcenv = PVE::RPCEnvironment::get();
467         my $authuser = $rpcenv->get_user();
468
469         my $action = delete $param->{action};
470         my $response = delete $param->{response};
471         my $password = delete($param->{password}) // '';
472
473         my ($userid, $ruid, $realm) = PVE::AccessControl::verify_username($param->{userid});
474         $rpcenv->check_user_exist($userid);
475
476         # Only root may modify root
477         raise_perm_exc() if $userid eq 'root@pam' && $authuser ne 'root@pam';
478
479         # Regular users need to confirm their password to change u2f settings.
480         if ($authuser ne 'root@pam') {
481             raise_param_exc('password' => 'password is required to modify u2f data')
482                 if !defined($password);
483             my $domain_cfg = cfs_read_file('domains.cfg');
484             my $cfg = $domain_cfg->{ids}->{$realm};
485             die "auth domain '$realm' does not exists\n" if !$cfg;
486             my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
487             $plugin->authenticate_user($cfg, $realm, $ruid, $password);
488         }
489
490         if ($action eq 'delete') {
491             PVE::AccessControl::user_set_tfa($userid, $realm, undef, undef);
492             PVE::Cluster::log_msg('info', $authuser, "deleted u2f data for user '$userid'");
493         } elsif ($action eq 'new') {
494             my $u2f = get_u2f_instance($rpcenv);
495             my $challenge = $u2f->registration_challenge()
496                 or raise("failed to get u2f challenge");
497             $challenge = decode_json($challenge);
498             PVE::AccessControl::user_set_tfa($userid, $realm, 'u2f', $challenge);
499             return $challenge;
500         } elsif ($action eq 'confirm') {
501             raise_param_exc('response' => "confirm action requires the 'response' parameter to be set")
502                 if !defined($response);
503
504             my ($type, $u2fdata) = PVE::AccessControl::user_get_tfa($userid, $realm);
505             raise("no u2f data available")
506                 if (!defined($type) || $type ne 'u2f');
507
508             my $challenge = $u2fdata->{challenge}
509                 or raise("no active challenge");
510
511             my $u2f = get_u2f_instance($rpcenv);
512             $u2f->set_challenge($challenge);
513             my ($keyHandle, $publicKey) = $u2f->registration_verify($response);
514             PVE::AccessControl::user_set_tfa($userid, $realm, 'u2f', {
515                 keyHandle => $keyHandle,
516                 publicKey => encode_base64($publicKey, ''),
517             });
518         } else {
519             die "invalid action: $action\n";
520         }
521
522         return {};
523     }});
524
525 __PACKAGE__->register_method({
526     name => 'verify_tfa',
527     path => 'tfa',
528     method => 'POST',
529     permissions => { user => 'all' },
530     protected => 1, # else we can't access shadow files
531     description => 'Finish a u2f challenge.',
532     parameters => {
533         additionalProperties => 0,
534         properties => {
535             response => {
536                 type => 'string',
537                 description => 'The response to the current authentication challenge.',
538             },
539         }
540     },
541     returns => {
542         type => 'object',
543         properties => {
544             ticket => { type => 'string' },
545             # cap
546         }
547     },
548     code => sub {
549         my ($param) = @_;
550
551         my $rpcenv = PVE::RPCEnvironment::get();
552         my $challenge = $rpcenv->get_u2f_challenge()
553            or raise('no active challenge');
554         my $authuser = $rpcenv->get_user();
555         my ($username, undef, $realm) = PVE::AccessControl::verify_username($authuser);
556
557         my ($tfa_type, $u2fdata) = PVE::AccessControl::user_get_tfa($username, $realm);
558         if (!defined($tfa_type) || $tfa_type ne 'u2f') {
559             raise('no u2f data available');
560         }
561
562         my $keyHandle = $u2fdata->{keyHandle};
563         my $publicKey = $u2fdata->{publicKey};
564         raise("incomplete u2f setup")
565             if !defined($keyHandle) || !defined($publicKey);
566
567         my $u2f = get_u2f_instance($rpcenv, $publicKey, $keyHandle);
568         $u2f->set_challenge($challenge);
569
570         eval {
571             my ($counter, $present) = $u2f->auth_verify($param->{response});
572             # Do we want to do anything with these?
573         };
574         if (my $err = $@) {
575             my $clientip = $rpcenv->get_client_ip() || '';
576             syslog('err', "authentication verification failure; rhost=$clientip user=$authuser msg=$err");
577             die PVE::Exception->new("authentication failure\n", code => 401);
578         }
579
580         # create a new ticket for the user:
581         my $ticket_data = "u2f!$authuser!verified";
582         return {
583             ticket => PVE::AccessControl::assemble_ticket($ticket_data),
584             cap => &$compute_api_permission($rpcenv, $authuser),
585         }
586     }});
587
588 1;