more general 2FA configuration via priv/tfa.cfg
[pve-access-control.git] / PVE / AccessControl.pm
1 package PVE::AccessControl;
2
3 use strict;
4 use warnings;
5 use Encode;
6 use Crypt::OpenSSL::Random;
7 use Crypt::OpenSSL::RSA;
8 use Net::SSLeay;
9 use Net::IP;
10 use MIME::Base64;
11 use Digest::SHA;
12 use IO::File;
13 use File::stat;
14 use JSON;
15
16 use PVE::OTP;
17 use PVE::Ticket;
18 use PVE::Tools qw(run_command lock_file file_get_contents split_list safe_print);
19 use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_write_file cfs_lock_file);
20 use PVE::JSONSchema qw(register_standard_option get_standard_option);
21
22 use PVE::Auth::Plugin;
23 use PVE::Auth::AD;
24 use PVE::Auth::LDAP;
25 use PVE::Auth::PVE;
26 use PVE::Auth::PAM;
27
28 # load and initialize all plugins
29
30 PVE::Auth::AD->register();
31 PVE::Auth::LDAP->register();
32 PVE::Auth::PVE->register();
33 PVE::Auth::PAM->register();
34 PVE::Auth::Plugin->init();
35
36 # $authdir must be writable by root only!
37 my $confdir = "/etc/pve";
38 my $authdir = "$confdir/priv";
39
40 my $pve_www_key_fn = "$confdir/pve-www.key";
41
42 my $pve_auth_key_files = {
43     priv => "$authdir/authkey.key",
44     pub =>  "$confdir/authkey.pub",
45     pubold => "$confdir/authkey.pub.old",
46 };
47
48 my $pve_auth_key_cache = {};
49
50 my $ticket_lifetime = 3600*2; # 2 hours
51 # TODO: set to 24h for PVE 6.0
52 my $authkey_lifetime = 3600*0; # rotation disabled
53
54 Crypt::OpenSSL::RSA->import_random_seed();
55
56 cfs_register_file('user.cfg',
57                   \&parse_user_config,
58                   \&write_user_config);
59 cfs_register_file('priv/tfa.cfg',
60                   \&parse_priv_tfa_config,
61                   \&write_priv_tfa_config);
62
63 sub verify_username {
64     PVE::Auth::Plugin::verify_username(@_);
65 }
66
67 sub pve_verify_realm {
68     PVE::Auth::Plugin::pve_verify_realm(@_);
69 }
70
71 sub lock_user_config {
72     my ($code, $errmsg) = @_;
73
74     cfs_lock_file("user.cfg", undef, $code);
75     if (my $err = $@) {
76         $errmsg ? die "$errmsg: $err" : die $err;
77     }
78 }
79
80 my $cache_read_key = sub {
81     my ($type) = @_;
82
83     my $path = $pve_auth_key_files->{$type};
84
85     my $read_key_and_mtime = sub {
86         my $fh = IO::File->new($path, "r");
87
88         return undef if !defined($fh);
89
90         my $st = stat($fh);
91         my $pem = PVE::Tools::safe_read_from($fh, 0, 0, $path);
92
93         close $fh;
94
95         my $key;
96         if ($type eq 'pub' || $type eq 'pubold') {
97             $key = eval { Crypt::OpenSSL::RSA->new_public_key($pem); };
98         } elsif ($type eq 'priv') {
99             $key = eval { Crypt::OpenSSL::RSA->new_private_key($pem); };
100         } else {
101             die "Invalid authkey type '$type'\n";
102         }
103
104         return { key => $key, mtime => $st->mtime };
105     };
106
107     if (!defined($pve_auth_key_cache->{$type})) {
108         $pve_auth_key_cache->{$type} = $read_key_and_mtime->();
109     } else {
110         my $st = stat($path);
111         if (!$st || $st->mtime != $pve_auth_key_cache->{$type}->{mtime}) {
112             $pve_auth_key_cache->{$type} = $read_key_and_mtime->();
113         }
114     }
115
116     return $pve_auth_key_cache->{$type};
117 };
118
119 sub get_pubkey {
120     my ($old) = @_;
121
122     my $type = $old ? 'pubold' : 'pub';
123
124     my $res = $cache_read_key->($type);
125     return undef if !defined($res);
126
127     return wantarray ? ($res->{key}, $res->{mtime}) : $res->{key};
128 }
129
130 sub get_privkey {
131     my $res = $cache_read_key->('priv');
132
133     if (!defined($res) || !check_authkey(1)) {
134         rotate_authkey();
135         $res = $cache_read_key->('priv');
136     }
137
138     return wantarray ? ($res->{key}, $res->{mtime}) : $res->{key};
139 }
140
141 sub check_authkey {
142     my ($quiet) = @_;
143
144     # skip check if non-quorate, as rotation is not possible anyway
145     return 1 if !PVE::Cluster::check_cfs_quorum(1);
146
147     my ($pub_key, $mtime) = get_pubkey();
148     if (!$pub_key) {
149         warn "auth key pair missing, generating new one..\n"  if !$quiet;
150         return 0;
151     } else {
152         if (time() - $mtime >= $authkey_lifetime) {
153             warn "auth key pair too old, rotating..\n" if !$quiet;;
154             return 0;
155         } else {
156             warn "auth key new enough, skipping rotation\n" if !$quiet;;
157             return 1;
158         }
159     }
160 }
161
162 sub rotate_authkey {
163     return if $authkey_lifetime == 0;
164
165     PVE::Cluster::cfs_lock_authkey(undef, sub {
166         # re-check with lock to avoid double rotation in clusters
167         return if check_authkey();
168
169         my $old = get_pubkey();
170
171         if ($old) {
172             eval {
173                 my $pem = $old->get_public_key_x509_string();
174                 PVE::Tools::file_set_contents($pve_auth_key_files->{pubold}, $pem);
175             };
176             die "Failed to store old auth key: $@\n" if $@;
177         }
178
179         my $new = Crypt::OpenSSL::RSA->generate_key(2048);
180         eval {
181             my $pem = $new->get_public_key_x509_string();
182             PVE::Tools::file_set_contents($pve_auth_key_files->{pub}, $pem);
183         };
184         if ($@) {
185             if ($old) {
186                 warn "Failed to store new auth key - $@\n";
187                 warn "Reverting to previous auth key\n";
188                 eval {
189                     my $pem = $old->get_public_key_x509_string();
190                     PVE::Tools::file_set_contents($pve_auth_key_files->{pub}, $pem);
191                 };
192                 die "Failed to restore old auth key: $@\n" if $@;
193             } else {
194                 die "Failed to store new auth key - $@\n";
195             }
196         }
197
198         eval {
199             my $pem = $new->get_private_key_string();
200             PVE::Tools::file_set_contents($pve_auth_key_files->{priv}, $pem);
201         };
202         if ($@) {
203             warn "Failed to store new auth key - $@\n";
204             warn "Deleting auth key to force regeneration\n";
205             unlink $pve_auth_key_files->{pub};
206             unlink $pve_auth_key_files->{priv};
207         }
208     });
209     die $@ if $@;
210 }
211
212 my $csrf_prevention_secret;
213 my $get_csrfr_secret = sub {
214     if (!$csrf_prevention_secret) {
215         my $input = PVE::Tools::file_get_contents($pve_www_key_fn);
216         $csrf_prevention_secret = Digest::SHA::sha1_base64($input);
217     }
218     return $csrf_prevention_secret;
219 };
220
221 sub assemble_csrf_prevention_token {
222     my ($username) = @_;
223
224     my $secret =  &$get_csrfr_secret();
225
226     return PVE::Ticket::assemble_csrf_prevention_token ($secret, $username);
227 }
228
229 sub verify_csrf_prevention_token {
230     my ($username, $token, $noerr) = @_;
231
232     my $secret =  &$get_csrfr_secret();
233
234     return PVE::Ticket::verify_csrf_prevention_token(
235         $secret, $username, $token, -300, $ticket_lifetime, $noerr);
236 }
237
238 my $get_ticket_age_range = sub {
239     my ($now, $mtime, $rotated) = @_;
240
241     my $key_age = $now - $mtime;
242     $key_age = 0 if $key_age < 0;
243
244     my $min = -300;
245     my $max = $ticket_lifetime;
246
247     if ($rotated) {
248         # ticket creation after rotation is not allowed
249         $min = $key_age - 300;
250     } else {
251         if ($key_age > $authkey_lifetime && $authkey_lifetime > 0) {
252             if (PVE::Cluster::check_cfs_quorum(1)) {
253                 # key should have been rotated, clamp range accordingly
254                 $min = $key_age - $authkey_lifetime;
255             } else {
256                 warn "Cluster not quorate - extending auth key lifetime!\n";
257             }
258         }
259
260         $max = $key_age + 300 if $key_age < $ticket_lifetime;
261     }
262
263     return undef if $min > $ticket_lifetime;
264     return ($min, $max);
265 };
266
267 sub assemble_ticket {
268     my ($username) = @_;
269
270     my $rsa_priv = get_privkey();
271
272     return PVE::Ticket::assemble_rsa_ticket($rsa_priv, 'PVE', $username);
273 }
274
275 sub verify_ticket {
276     my ($ticket, $noerr) = @_;
277
278     my $now = time();
279
280     my $check = sub {
281         my ($old) = @_;
282
283         my ($rsa_pub, $rsa_mtime) = get_pubkey($old);
284         return undef if !$rsa_pub;
285
286         my ($min, $max) = $get_ticket_age_range->($now, $rsa_mtime, $old);
287         return undef if !$min;
288
289         return PVE::Ticket::verify_rsa_ticket(
290             $rsa_pub, 'PVE', $ticket, undef, $min, $max, 1);
291     };
292
293     my ($username, $age) = $check->();
294
295     # check with old, rotated key if current key failed
296     ($username, $age) = $check->(1) if !defined($username);
297
298     if (!defined($username)) {
299         if ($noerr) {
300             return undef;
301         } else {
302             # raise error via undef ticket
303             PVE::Ticket::verify_rsa_ticket(undef, 'PVE');
304         }
305     }
306
307     return undef if !PVE::Auth::Plugin::verify_username($username, $noerr);
308
309     return wantarray ? ($username, $age) : $username;
310 }
311
312 # VNC tickets
313 # - they do not contain the username in plain text
314 # - they are restricted to a specific resource path (example: '/vms/100')
315 sub assemble_vnc_ticket {
316     my ($username, $path) = @_;
317
318     my $rsa_priv = get_privkey();
319
320     $path = normalize_path($path);
321
322     my $secret_data = "$username:$path";
323
324     return PVE::Ticket::assemble_rsa_ticket(
325         $rsa_priv, 'PVEVNC', undef, $secret_data);
326 }
327
328 sub verify_vnc_ticket {
329     my ($ticket, $username, $path, $noerr) = @_;
330
331     my $secret_data = "$username:$path";
332
333     my ($rsa_pub, $rsa_mtime) = get_pubkey();
334     if (!$rsa_pub || (time() - $rsa_mtime > $authkey_lifetime && $authkey_lifetime > 0)) {
335         if ($noerr) {
336             return undef;
337         } else {
338             # raise error via undef ticket
339             PVE::Ticket::verify_rsa_ticket($rsa_pub, 'PVEVNC');
340         }
341     }
342
343     return PVE::Ticket::verify_rsa_ticket(
344         $rsa_pub, 'PVEVNC', $ticket, $secret_data, -20, 40, $noerr);
345 }
346
347 sub assemble_spice_ticket {
348     my ($username, $vmid, $node) = @_;
349
350     my $secret = &$get_csrfr_secret();
351
352     return PVE::Ticket::assemble_spice_ticket(
353         $secret, $username, $vmid, $node);
354 }
355
356 sub verify_spice_connect_url {
357     my ($connect_str) = @_;
358
359     my $secret = &$get_csrfr_secret();
360
361     return PVE::Ticket::verify_spice_connect_url($secret, $connect_str);
362 }
363
364 sub read_x509_subject_spice {
365     my ($filename) = @_;
366
367     # read x509 subject
368     my $bio = Net::SSLeay::BIO_new_file($filename, 'r');
369     die "Could not open $filename using OpenSSL\n"
370         if !$bio;
371
372     my $x509 = Net::SSLeay::PEM_read_bio_X509($bio);
373     Net::SSLeay::BIO_free($bio);
374
375     die "Could not parse X509 certificate in $filename\n"
376         if !$x509;
377
378     my $nameobj = Net::SSLeay::X509_get_subject_name($x509);
379     my $subject = Net::SSLeay::X509_NAME_oneline($nameobj);
380     Net::SSLeay::X509_free($x509);
381
382     # remote-viewer wants comma as seperator (not '/')
383     $subject =~ s!^/!!;
384     $subject =~ s!/(\w+=)!,$1!g;
385
386     return $subject;
387 }
388
389 # helper to generate SPICE remote-viewer configuration
390 sub remote_viewer_config {
391     my ($authuser, $vmid, $node, $proxy, $title, $port) = @_;
392
393     if (!$proxy) {
394         my $host = `hostname -f` || PVE::INotify::nodename();
395         chomp $host;
396         $proxy = $host;
397     }
398
399     my ($ticket, $proxyticket) = assemble_spice_ticket($authuser, $vmid, $node);
400
401     my $filename = "/etc/pve/local/pve-ssl.pem";
402     my $subject = read_x509_subject_spice($filename);
403
404     my $cacert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192);
405     $cacert =~ s/\n/\\n/g;
406
407     $proxy = "[$proxy]" if Net::IP::ip_is_ipv6($proxy);
408     my $config = {
409         'secure-attention' => "Ctrl+Alt+Ins",
410         'toggle-fullscreen' => "Shift+F11",
411         'release-cursor' => "Ctrl+Alt+R",
412         type => 'spice',
413         title => $title,
414         host => $proxyticket, # this breaks tls hostname verification, so we need to use 'host-subject'
415         proxy => "http://$proxy:3128",
416         'tls-port' => $port,
417         'host-subject' => $subject,
418         ca => $cacert,
419         password => $ticket,
420         'delete-this-file' => 1,
421     };
422
423     return ($ticket, $proxyticket, $config);
424 }
425
426 sub check_user_exist {
427     my ($usercfg, $username, $noerr) = @_;
428
429     $username = PVE::Auth::Plugin::verify_username($username, $noerr);
430     return undef if !$username;
431
432     return $usercfg->{users}->{$username} if $usercfg && $usercfg->{users}->{$username};
433
434     die "no such user ('$username')\n" if !$noerr;
435
436     return undef;
437 }
438
439 sub check_user_enabled {
440     my ($usercfg, $username, $noerr) = @_;
441
442     my $data = check_user_exist($usercfg, $username, $noerr);
443     return undef if !$data;
444
445     return 1 if $data->{enable};
446
447     die "user '$username' is disabled\n" if !$noerr;
448
449     return undef;
450 }
451
452 sub verify_one_time_pw {
453     my ($type, $username, $keys, $tfa_cfg, $otp) = @_;
454
455     die "missing one time password for two-factor authentication '$type'\n" if !$otp;
456
457     # fixme: proxy support?
458     my $proxy;
459
460     if ($type eq 'yubico') {
461         PVE::OTP::yubico_verify_otp($otp, $keys, $tfa_cfg->{url},
462                                     $tfa_cfg->{id}, $tfa_cfg->{key}, $proxy);
463     } elsif ($type eq 'oath') {
464         PVE::OTP::oath_verify_otp($otp, $keys, $tfa_cfg->{step}, $tfa_cfg->{digits});
465     } else {
466         die "unknown tfa type '$type'\n";
467     }
468 }
469
470 # password should be utf8 encoded
471 # Note: some plugins delay/sleep if auth fails
472 sub authenticate_user {
473     my ($username, $password, $otp) = @_;
474
475     die "no username specified\n" if !$username;
476
477     my ($ruid, $realm);
478
479     ($username, $ruid, $realm) = PVE::Auth::Plugin::verify_username($username);
480
481     my $usercfg = cfs_read_file('user.cfg');
482
483     check_user_enabled($usercfg, $username);
484
485     my $ctime = time();
486     my $expire = $usercfg->{users}->{$username}->{expire};
487
488     die "account expired\n" if $expire && ($expire < $ctime);
489
490     my $domain_cfg = cfs_read_file('domains.cfg');
491
492     my $cfg = $domain_cfg->{ids}->{$realm};
493     die "auth domain '$realm' does not exists\n" if !$cfg;
494     my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
495     $plugin->authenticate_user($cfg, $realm, $ruid, $password);
496
497     my $u2f;
498
499     my ($type, $tfa_data) = user_get_tfa($username, $realm);
500     if ($type) {
501         if ($type eq 'u2f') {
502             # Note that if the user did not manage to complete the initial u2f registration
503             # challenge we have a hash containing a 'challenge' entry in the user's tfa.cfg entry:
504             $u2f = $tfa_data if !exists $tfa_data->{challenge};
505         } else {
506             my $keys = $tfa_data->{keys};
507             my $tfa_cfg = $tfa_data->{config};
508             verify_one_time_pw($type, $username, $keys, $tfa_cfg, $otp);
509         }
510     }
511
512     return wantarray ? ($username, $u2f) : $username;
513 }
514
515 sub domain_set_password {
516     my ($realm, $username, $password) = @_;
517
518     die "no auth domain specified" if !$realm;
519
520     my $domain_cfg = cfs_read_file('domains.cfg');
521
522     my $cfg = $domain_cfg->{ids}->{$realm};
523     die "auth domain '$realm' does not exist\n" if !$cfg;
524     my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
525     $plugin->store_password($cfg, $realm, $username, $password);
526 }
527
528 sub add_user_group {
529     my ($username, $usercfg, $group) = @_;
530
531     $usercfg->{users}->{$username}->{groups}->{$group} = 1;
532     $usercfg->{groups}->{$group}->{users}->{$username} = 1;
533 }
534
535 sub delete_user_group {
536     my ($username, $usercfg) = @_;
537
538     foreach my $group (keys %{$usercfg->{groups}}) {
539
540         delete ($usercfg->{groups}->{$group}->{users}->{$username})
541             if $usercfg->{groups}->{$group}->{users}->{$username};
542     }
543 }
544
545 sub delete_user_acl {
546     my ($username, $usercfg) = @_;
547
548     foreach my $acl (keys %{$usercfg->{acl}}) {
549
550         delete ($usercfg->{acl}->{$acl}->{users}->{$username})
551             if $usercfg->{acl}->{$acl}->{users}->{$username};
552     }
553 }
554
555 sub delete_group_acl {
556     my ($group, $usercfg) = @_;
557
558     foreach my $acl (keys %{$usercfg->{acl}}) {
559
560         delete ($usercfg->{acl}->{$acl}->{groups}->{$group})
561             if $usercfg->{acl}->{$acl}->{groups}->{$group};
562     }
563 }
564
565 sub delete_pool_acl {
566     my ($pool, $usercfg) = @_;
567
568     my $path = "/pool/$pool";
569
570     delete ($usercfg->{acl}->{$path})
571 }
572
573 # we automatically create some predefined roles by splitting privs
574 # into 3 groups (per category)
575 # root: only root is allowed to do that
576 # admin: an administrator can to that
577 # user: a normal user/customer can to that
578 my $privgroups = {
579     VM => {
580         root => [],
581         admin => [
582             'VM.Config.Disk',
583             'VM.Config.CPU',
584             'VM.Config.Memory',
585             'VM.Config.Network',
586             'VM.Config.HWType',
587             'VM.Config.Options', # covers all other things
588             'VM.Allocate',
589             'VM.Clone',
590             'VM.Migrate',
591             'VM.Monitor',
592             'VM.Snapshot',
593             'VM.Snapshot.Rollback',
594         ],
595         user => [
596             'VM.Config.CDROM', # change CDROM media
597             'VM.Console',
598             'VM.Backup',
599             'VM.PowerMgmt',
600         ],
601         audit => [
602             'VM.Audit',
603         ],
604     },
605     Sys => {
606         root => [
607             'Sys.PowerMgmt',
608             'Sys.Modify', # edit/change node settings
609         ],
610         admin => [
611             'Permissions.Modify',
612             'Sys.Console',
613             'Sys.Syslog',
614         ],
615         user => [],
616         audit => [
617             'Sys.Audit',
618         ],
619     },
620     Datastore => {
621         root => [],
622         admin => [
623             'Datastore.Allocate',
624             'Datastore.AllocateTemplate',
625         ],
626         user => [
627             'Datastore.AllocateSpace',
628         ],
629         audit => [
630             'Datastore.Audit',
631         ],
632     },
633     User => {
634         root => [
635             'Realm.Allocate',
636         ],
637         admin => [
638             'User.Modify',
639             'Group.Allocate', # edit/change group settings
640             'Realm.AllocateUser',
641         ],
642         user => [],
643         audit => [],
644     },
645     Pool => {
646         root => [],
647         admin => [
648             'Pool.Allocate', # create/delete pools
649         ],
650         user => [],
651         audit => [],
652     },
653 };
654
655 my $valid_privs = {};
656
657 my $special_roles = {
658     'NoAccess' => {}, # no privileges
659     'Administrator' => $valid_privs, # all privileges
660 };
661
662 sub create_roles {
663
664     foreach my $cat (keys %$privgroups) {
665         my $cd = $privgroups->{$cat};
666         foreach my $p (@{$cd->{root}}, @{$cd->{admin}},
667                        @{$cd->{user}}, @{$cd->{audit}}) {
668             $valid_privs->{$p} = 1;
669         }
670         foreach my $p (@{$cd->{admin}}, @{$cd->{user}}, @{$cd->{audit}}) {
671
672             $special_roles->{"PVE${cat}Admin"}->{$p} = 1;
673             $special_roles->{"PVEAdmin"}->{$p} = 1;
674         }
675         if (scalar(@{$cd->{user}})) {
676             foreach my $p (@{$cd->{user}}, @{$cd->{audit}}) {
677                 $special_roles->{"PVE${cat}User"}->{$p} = 1;
678             }
679         }
680         foreach my $p (@{$cd->{audit}}) {
681             $special_roles->{"PVEAuditor"}->{$p} = 1;
682         }
683     }
684
685     $special_roles->{"PVETemplateUser"} = { 'VM.Clone' => 1, 'VM.Audit' => 1 };
686 };
687
688 create_roles();
689
690 sub create_priv_properties {
691     my $properties = {};
692     foreach my $priv (keys %$valid_privs) {
693         $properties->{$priv} = {
694             type => 'boolean',
695             optional => 1,
696         };
697     }
698     return $properties;
699 }
700
701 sub role_is_special {
702     my ($role) = @_;
703     return (exists $special_roles->{$role}) ? 1 : 0;
704 }
705
706 sub add_role_privs {
707     my ($role, $usercfg, $privs) = @_;
708
709     return if !$privs;
710
711     die "role '$role' does not exist\n" if !$usercfg->{roles}->{$role};
712
713     foreach my $priv (split_list($privs)) {
714         if (defined ($valid_privs->{$priv})) {
715             $usercfg->{roles}->{$role}->{$priv} = 1;
716         } else {
717             die "invalid privilege '$priv'\n";
718         }
719     }
720 }
721
722 sub normalize_path {
723     my $path = shift;
724
725     $path =~ s|/+|/|g;
726
727     $path =~ s|/$||;
728
729     $path = '/' if !$path;
730
731     $path = "/$path" if $path !~ m|^/|;
732
733     return undef if $path !~ m|^[[:alnum:]\.\-\_\/]+$|;
734
735     return $path;
736 }
737
738 PVE::JSONSchema::register_format('pve-groupid', \&verify_groupname);
739 sub verify_groupname {
740     my ($groupname, $noerr) = @_;
741
742     if ($groupname !~ m/^[A-Za-z0-9\.\-_]+$/) {
743
744         die "group name '$groupname' contains invalid characters\n" if !$noerr;
745
746         return undef;
747     }
748
749     return $groupname;
750 }
751
752 PVE::JSONSchema::register_format('pve-roleid', \&verify_rolename);
753 sub verify_rolename {
754     my ($rolename, $noerr) = @_;
755
756     if ($rolename !~ m/^[A-Za-z0-9\.\-_]+$/) {
757
758         die "role name '$rolename' contains invalid characters\n" if !$noerr;
759
760         return undef;
761     }
762
763     return $rolename;
764 }
765
766 PVE::JSONSchema::register_format('pve-poolid', \&verify_poolname);
767 sub verify_poolname {
768     my ($poolname, $noerr) = @_;
769
770     if ($poolname !~ m/^[A-Za-z0-9\.\-_]+$/) {
771
772         die "pool name '$poolname' contains invalid characters\n" if !$noerr;
773
774         return undef;
775     }
776
777     return $poolname;
778 }
779
780 PVE::JSONSchema::register_format('pve-priv', \&verify_privname);
781 sub verify_privname {
782     my ($priv, $noerr) = @_;
783
784     if (!$valid_privs->{$priv}) {
785         die "invalid privilege '$priv'\n" if !$noerr;
786
787         return undef;
788     }
789
790     return $priv;
791 }
792
793 sub userconfig_force_defaults {
794     my ($cfg) = @_;
795
796     foreach my $r (keys %$special_roles) {
797         $cfg->{roles}->{$r} = $special_roles->{$r};
798     }
799
800     # add root user if not exists
801     if (!$cfg->{users}->{'root@pam'}) {
802         $cfg->{users}->{'root@pam'}->{enable} = 1;
803     }
804 }
805
806 sub parse_user_config {
807     my ($filename, $raw) = @_;
808
809     my $cfg = {};
810
811     userconfig_force_defaults($cfg);
812
813     $raw = '' if !defined($raw);
814     while ($raw =~ /^\s*(.+?)\s*$/gm) {
815         my $line = $1;
816         my @data;
817
818         foreach my $d (split (/:/, $line)) {
819             $d =~ s/^\s+//;
820             $d =~ s/\s+$//;
821             push @data, $d
822         }
823
824         my $et = shift @data;
825
826         if ($et eq 'user') {
827             my ($user, $enable, $expire, $firstname, $lastname, $email, $comment, $keys) = @data;
828
829             my (undef, undef, $realm) = PVE::Auth::Plugin::verify_username($user, 1);
830             if (!$realm) {
831                 warn "user config - ignore user '$user' - invalid user name\n";
832                 next;
833             }
834
835             $enable = $enable ? 1 : 0;
836
837             $expire = 0 if !$expire;
838
839             if ($expire !~ m/^\d+$/) {
840                 warn "user config - ignore user '$user' - (illegal characters in expire '$expire')\n";
841                 next;
842             }
843             $expire = int($expire);
844
845             #if (!verify_groupname ($group, 1)) {
846             #    warn "user config - ignore user '$user' - invalid characters in group name\n";
847             #    next;
848             #}
849
850             $cfg->{users}->{$user} = {
851                 enable => $enable,
852                 # group => $group,
853             };
854             $cfg->{users}->{$user}->{firstname} = PVE::Tools::decode_text($firstname) if $firstname;
855             $cfg->{users}->{$user}->{lastname} = PVE::Tools::decode_text($lastname) if $lastname;
856             $cfg->{users}->{$user}->{email} = $email;
857             $cfg->{users}->{$user}->{comment} = PVE::Tools::decode_text($comment) if $comment;
858             $cfg->{users}->{$user}->{expire} = $expire;
859             # keys: allowed yubico key ids or oath secrets (base32 encoded)
860             $cfg->{users}->{$user}->{keys} = $keys if $keys;
861
862             #$cfg->{users}->{$user}->{groups}->{$group} = 1;
863             #$cfg->{groups}->{$group}->{$user} = 1;
864
865         } elsif ($et eq 'group') {
866             my ($group, $userlist, $comment) = @data;
867
868             if (!verify_groupname($group, 1)) {
869                 warn "user config - ignore group '$group' - invalid characters in group name\n";
870                 next;
871             }
872
873             # make sure to add the group (even if there are no members)
874             $cfg->{groups}->{$group} = { users => {} } if !$cfg->{groups}->{$group};
875
876             $cfg->{groups}->{$group}->{comment} = PVE::Tools::decode_text($comment) if $comment;
877
878             foreach my $user (split_list($userlist)) {
879
880                 if (!PVE::Auth::Plugin::verify_username($user, 1)) {
881                     warn "user config - ignore invalid group member '$user'\n";
882                     next;
883                 }
884
885                 if ($cfg->{users}->{$user}) { # user exists
886                     $cfg->{users}->{$user}->{groups}->{$group} = 1;
887                     $cfg->{groups}->{$group}->{users}->{$user} = 1;
888                 } else {
889                     warn "user config - ignore invalid group member '$user'\n";
890                 }
891             }
892
893         } elsif ($et eq 'role') {
894             my ($role, $privlist) = @data;
895
896             if (!verify_rolename($role, 1)) {
897                 warn "user config - ignore role '$role' - invalid characters in role name\n";
898                 next;
899             }
900
901             # make sure to add the role (even if there are no privileges)
902             $cfg->{roles}->{$role} = {} if !$cfg->{roles}->{$role};
903
904             foreach my $priv (split_list($privlist)) {
905                 if (defined ($valid_privs->{$priv})) {
906                     $cfg->{roles}->{$role}->{$priv} = 1;
907                 } else {
908                     warn "user config - ignore invalid priviledge '$priv'\n";
909                 }
910             }
911
912         } elsif ($et eq 'acl') {
913             my ($propagate, $pathtxt, $uglist, $rolelist) = @data;
914
915             if (my $path = normalize_path($pathtxt)) {
916                 foreach my $role (split_list($rolelist)) {
917
918                     if (!verify_rolename($role, 1)) {
919                         warn "user config - ignore invalid role name '$role' in acl\n";
920                         next;
921                     }
922
923                     foreach my $ug (split_list($uglist)) {
924                         if ($ug =~ m/^@(\S+)$/) {
925                             my $group = $1;
926                             if ($cfg->{groups}->{$group}) { # group exists
927                                 $cfg->{acl}->{$path}->{groups}->{$group}->{$role} = $propagate;
928                             } else {
929                                 warn "user config - ignore invalid acl group '$group'\n";
930                             }
931                         } elsif (PVE::Auth::Plugin::verify_username($ug, 1)) {
932                             if ($cfg->{users}->{$ug}) { # user exists
933                                 $cfg->{acl}->{$path}->{users}->{$ug}->{$role} = $propagate;
934                             } else {
935                                 warn "user config - ignore invalid acl member '$ug'\n";
936                             }
937                         } else {
938                             warn "user config - invalid user/group '$ug' in acl\n";
939                         }
940                     }
941                 }
942             } else {
943                 warn "user config - ignore invalid path in acl '$pathtxt'\n";
944             }
945         } elsif ($et eq 'pool') {
946             my ($pool, $comment, $vmlist, $storelist) = @data;
947
948             if (!verify_poolname($pool, 1)) {
949                 warn "user config - ignore pool '$pool' - invalid characters in pool name\n";
950                 next;
951             }
952
953             # make sure to add the pool (even if there are no members)
954             $cfg->{pools}->{$pool} = { vms => {}, storage => {} } if !$cfg->{pools}->{$pool};
955
956             $cfg->{pools}->{$pool}->{comment} = PVE::Tools::decode_text($comment) if $comment;
957
958             foreach my $vmid (split_list($vmlist)) {
959                 if ($vmid !~ m/^\d+$/) {
960                     warn "user config - ignore invalid vmid '$vmid' in pool '$pool'\n";
961                     next;
962                 }
963                 $vmid = int($vmid);
964
965                 if ($cfg->{vms}->{$vmid}) {
966                     warn "user config - ignore duplicate vmid '$vmid' in pool '$pool'\n";
967                     next;
968                 }
969
970                 $cfg->{pools}->{$pool}->{vms}->{$vmid} = 1;
971
972                 # record vmid ==> pool relation
973                 $cfg->{vms}->{$vmid} = $pool;
974             }
975
976             foreach my $storeid (split_list($storelist)) {
977                 if ($storeid !~ m/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i) {
978                     warn "user config - ignore invalid storage '$storeid' in pool '$pool'\n";
979                     next;
980                 }
981                 $cfg->{pools}->{$pool}->{storage}->{$storeid} = 1;
982             }
983         } else {
984             warn "user config - ignore config line: $line\n";
985         }
986     }
987
988     userconfig_force_defaults($cfg);
989
990     return $cfg;
991 }
992
993 sub write_user_config {
994     my ($filename, $cfg) = @_;
995
996     my $data = '';
997
998     foreach my $user (keys %{$cfg->{users}}) {
999         my $d = $cfg->{users}->{$user};
1000         my $firstname = $d->{firstname} ? PVE::Tools::encode_text($d->{firstname}) : '';
1001         my $lastname = $d->{lastname} ? PVE::Tools::encode_text($d->{lastname}) : '';
1002         my $email = $d->{email} || '';
1003         my $comment = $d->{comment} ? PVE::Tools::encode_text($d->{comment}) : '';
1004         my $expire = int($d->{expire} || 0);
1005         my $enable = $d->{enable} ? 1 : 0;
1006         my $keys = $d->{keys} ? $d->{keys} : '';
1007         $data .= "user:$user:$enable:$expire:$firstname:$lastname:$email:$comment:$keys:\n";
1008     }
1009
1010     $data .= "\n";
1011
1012     foreach my $group (keys %{$cfg->{groups}}) {
1013         my $d = $cfg->{groups}->{$group};
1014         my $list = join (',', keys %{$d->{users}});
1015         my $comment = $d->{comment} ? PVE::Tools::encode_text($d->{comment}) : '';
1016         $data .= "group:$group:$list:$comment:\n";
1017     }
1018
1019     $data .= "\n";
1020
1021     foreach my $pool (keys %{$cfg->{pools}}) {
1022         my $d = $cfg->{pools}->{$pool};
1023         my $vmlist = join (',', keys %{$d->{vms}});
1024         my $storelist = join (',', keys %{$d->{storage}});
1025         my $comment = $d->{comment} ? PVE::Tools::encode_text($d->{comment}) : '';
1026         $data .= "pool:$pool:$comment:$vmlist:$storelist:\n";
1027     }
1028
1029     $data .= "\n";
1030
1031     foreach my $role (keys %{$cfg->{roles}}) {
1032         next if $special_roles->{$role};
1033
1034         my $d = $cfg->{roles}->{$role};
1035         my $list = join (',', keys %$d);
1036         $data .= "role:$role:$list:\n";
1037     }
1038
1039     $data .= "\n";
1040
1041     foreach my $path (sort keys %{$cfg->{acl}}) {
1042         my $d = $cfg->{acl}->{$path};
1043
1044         my $ra = {};
1045
1046         foreach my $group (keys %{$d->{groups}}) {
1047             my $l0 = '';
1048             my $l1 = '';
1049             foreach my $role (sort keys %{$d->{groups}->{$group}}) {
1050                 my $propagate = $d->{groups}->{$group}->{$role};
1051                 if ($propagate) {
1052                     $l1 .= ',' if $l1;
1053                     $l1 .= $role;
1054                 } else {
1055                     $l0 .= ',' if $l0;
1056                     $l0 .= $role;
1057                 }
1058             }
1059             $ra->{0}->{$l0}->{"\@$group"} = 1 if $l0;
1060             $ra->{1}->{$l1}->{"\@$group"} = 1 if $l1;
1061         }
1062
1063         foreach my $user (keys %{$d->{users}}) {
1064             # no need to save, because root is always 'Administrator'
1065             next if $user eq 'root@pam';
1066
1067             my $l0 = '';
1068             my $l1 = '';
1069             foreach my $role (sort keys %{$d->{users}->{$user}}) {
1070                 my $propagate = $d->{users}->{$user}->{$role};
1071                 if ($propagate) {
1072                     $l1 .= ',' if $l1;
1073                     $l1 .= $role;
1074                 } else {
1075                     $l0 .= ',' if $l0;
1076                     $l0 .= $role;
1077                 }
1078             }
1079             $ra->{0}->{$l0}->{$user} = 1 if $l0;
1080             $ra->{1}->{$l1}->{$user} = 1 if $l1;
1081         }
1082
1083         foreach my $rolelist (sort keys %{$ra->{0}}) {
1084             my $uglist = join (',', keys %{$ra->{0}->{$rolelist}});
1085             $data .= "acl:0:$path:$uglist:$rolelist:\n";
1086         }
1087         foreach my $rolelist (sort keys %{$ra->{1}}) {
1088             my $uglist = join (',', keys %{$ra->{1}->{$rolelist}});
1089             $data .= "acl:1:$path:$uglist:$rolelist:\n";
1090         }
1091     }
1092
1093     return $data;
1094 }
1095
1096 # The TFA configuration in priv/tfa.cfg format contains one line per user of
1097 # the form:
1098 #     USER:TYPE:DATA
1099 # DATA is a base64 encoded json string and its format depends on the type.
1100 sub parse_priv_tfa_config {
1101     my ($filename, $raw) = @_;
1102
1103     my $users = {};
1104     my $cfg = { users => $users };
1105
1106     $raw = '' if !defined($raw);
1107     while ($raw =~ /^\s*(.+?)\s*$/gm) {
1108         my $line = $1;
1109         my ($user, $type, $data) = split(/:/, $line, 3);
1110
1111         my (undef, undef, $realm) = PVE::Auth::Plugin::verify_username($user, 1);
1112         if (!$realm) {
1113             warn "user tfa config - ignore user '$user' - invalid user name\n";
1114             next;
1115         }
1116
1117         $data = decode_json(decode_base64($data));
1118
1119         $users->{$user} = {
1120             type => $type,
1121             data => $data,
1122         };
1123     }
1124
1125     return $cfg;
1126 }
1127
1128 sub write_priv_tfa_config {
1129     my ($filename, $cfg) = @_;
1130
1131     my $output = '';
1132
1133     my $users = $cfg->{users};
1134     foreach my $user (sort keys %$users) {
1135         my $info = $users->{$user};
1136         next if !%$info; # skip empty entries
1137
1138         $info = {%$info}; # copy to verify contents:
1139
1140         my $type = delete $info->{type};
1141         my $data = delete $info->{data};
1142
1143         if (my @keys = keys %$info) {
1144             die "invalid keys in TFA config for user $user: " . join(', ', @keys) . "\n";
1145         }
1146
1147         $data = encode_base64(encode_json($data), '');
1148         $output .= "${user}:${type}:${data}\n";
1149     }
1150
1151     return $output;
1152 }
1153
1154 sub roles {
1155     my ($cfg, $user, $path) = @_;
1156
1157     # NOTE: we do not consider pools here.
1158     # You need to use $rpcenv->roles() instead if you want that.
1159
1160     return 'Administrator' if $user eq 'root@pam'; # root can do anything
1161
1162     my $perm = {};
1163
1164     foreach my $p (sort keys %{$cfg->{acl}}) {
1165         my $final = ($path eq $p);
1166
1167         next if !(($p eq '/') || $final || ($path =~ m|^$p/|));
1168
1169         my $acl = $cfg->{acl}->{$p};
1170
1171         #print "CHECKACL $path $p\n";
1172         #print "ACL $path = " . Dumper ($acl);
1173
1174         if (my $ri = $acl->{users}->{$user}) {
1175             my $new;
1176             foreach my $role (keys %$ri) {
1177                 my $propagate = $ri->{$role};
1178                 if ($final || $propagate) {
1179                     #print "APPLY ROLE $p $user $role\n";
1180                     $new = {} if !$new;
1181                     $new->{$role} = 1;
1182                 }
1183             }
1184             if ($new) {
1185                 $perm = $new; # overwrite previous settings
1186                 next; # user privs always override group privs
1187             }
1188         }
1189
1190         my $new;
1191         foreach my $g (keys %{$acl->{groups}}) {
1192             next if !$cfg->{groups}->{$g}->{users}->{$user};
1193             if (my $ri = $acl->{groups}->{$g}) {
1194                 foreach my $role (keys %$ri) {
1195                     my $propagate = $ri->{$role};
1196                     if ($final || $propagate) {
1197                         #print "APPLY ROLE $p \@$g $role\n";
1198                         $new = {} if !$new;
1199                         $new->{$role} = 1;
1200                     }
1201                 }
1202             }
1203         }
1204         if ($new) {
1205             $perm = $new; # overwrite previous settings
1206             next;
1207         }
1208     }
1209
1210     return ('NoAccess') if defined ($perm->{NoAccess});
1211     #return () if defined ($perm->{NoAccess});
1212
1213     #print "permission $user $path = " . Dumper ($perm);
1214
1215     my @ra = keys %$perm;
1216
1217     #print "roles $user $path = " . join (',', @ra) . "\n";
1218
1219     return @ra;
1220 }
1221
1222 sub permission {
1223     my ($cfg, $user, $path) = @_;
1224
1225     $user = PVE::Auth::Plugin::verify_username($user, 1);
1226     return {} if !$user;
1227
1228     my @ra = roles($cfg, $user, $path);
1229
1230     my $privs = {};
1231
1232     foreach my $role (@ra) {
1233         if (my $privset = $cfg->{roles}->{$role}) {
1234             foreach my $p (keys %$privset) {
1235                 $privs->{$p} = 1;
1236             }
1237         }
1238     }
1239
1240     #print "priviledges $user $path = " . Dumper ($privs);
1241
1242     return $privs;
1243 }
1244
1245 sub check_permissions {
1246     my ($username, $path, $privlist) = @_;
1247
1248     $path = normalize_path($path);
1249     my $usercfg = cfs_read_file('user.cfg');
1250     my $perm = permission($usercfg, $username, $path);
1251
1252     foreach my $priv (split_list($privlist)) {
1253         return undef if !$perm->{$priv};
1254     };
1255
1256     return 1;
1257 }
1258
1259 sub remove_vm_access {
1260     my ($vmid) = @_;
1261     my $delVMaccessFn = sub {
1262         my $usercfg = cfs_read_file("user.cfg");
1263         my $modified;
1264
1265         if (my $acl = $usercfg->{acl}->{"/vms/$vmid"}) {
1266             delete $usercfg->{acl}->{"/vms/$vmid"};
1267             $modified = 1;
1268         }
1269         if (my $pool = $usercfg->{vms}->{$vmid}) {
1270             if (my $data = $usercfg->{pools}->{$pool}) {
1271                 delete $data->{vms}->{$vmid};
1272                 delete $usercfg->{vms}->{$vmid};
1273                 $modified = 1;
1274             }
1275         }
1276         cfs_write_file("user.cfg", $usercfg) if $modified;
1277     };
1278
1279     lock_user_config($delVMaccessFn, "access permissions cleanup for VM $vmid failed");
1280 }
1281
1282 sub remove_storage_access {
1283     my ($storeid) = @_;
1284
1285     my $deleteStorageAccessFn = sub {
1286         my $usercfg = cfs_read_file("user.cfg");
1287         my $modified;
1288
1289         if (my $storage = $usercfg->{acl}->{"/storage/$storeid"}) {
1290             delete $usercfg->{acl}->{"/storage/$storeid"};
1291             $modified = 1;
1292         }
1293         foreach my $pool (keys %{$usercfg->{pools}}) {
1294             delete $usercfg->{pools}->{$pool}->{storage}->{$storeid};
1295             $modified = 1;
1296         }
1297         cfs_write_file("user.cfg", $usercfg) if $modified;
1298     };
1299
1300     lock_user_config($deleteStorageAccessFn,
1301                      "access permissions cleanup for storage $storeid failed");
1302 }
1303
1304 sub add_vm_to_pool {
1305     my ($vmid, $pool) = @_;
1306
1307     my $addVMtoPoolFn = sub {
1308         my $usercfg = cfs_read_file("user.cfg");
1309         if (my $data = $usercfg->{pools}->{$pool}) {
1310             $data->{vms}->{$vmid} = 1;
1311             $usercfg->{vms}->{$vmid} = $pool;
1312             cfs_write_file("user.cfg", $usercfg);
1313         }
1314     };
1315
1316     lock_user_config($addVMtoPoolFn, "can't add VM $vmid to pool '$pool'");
1317 }
1318
1319 sub remove_vm_from_pool {
1320     my ($vmid) = @_;
1321
1322     my $delVMfromPoolFn = sub {
1323         my $usercfg = cfs_read_file("user.cfg");
1324         if (my $pool = $usercfg->{vms}->{$vmid}) {
1325             if (my $data = $usercfg->{pools}->{$pool}) {
1326                 delete $data->{vms}->{$vmid};
1327                 delete $usercfg->{vms}->{$vmid};
1328                 cfs_write_file("user.cfg", $usercfg);
1329             }
1330         }
1331     };
1332
1333     lock_user_config($delVMfromPoolFn, "pool cleanup for VM $vmid failed");
1334 }
1335
1336 my $CUSTOM_TFA_TYPES = {
1337     u2f => 1,
1338     oath => 1,
1339 };
1340
1341 # Delete an entry by setting $data=undef in which case $type is ignored.
1342 # Otherwise both must be valid.
1343 sub user_set_tfa {
1344     my ($userid, $realm, $type, $data, $cached_usercfg, $cached_domaincfg) = @_;
1345
1346     if (defined($data) && !defined($type)) {
1347         # This is an internal usage error and should not happen
1348         die "cannot set tfa data without a type\n";
1349     }
1350
1351     my $user_cfg = $cached_usercfg || cfs_read_file('user.cfg');
1352     my $user = $user_cfg->{users}->{$userid}
1353         or die "user '$userid' not found\n";
1354
1355     my $domain_cfg = $cached_domaincfg || cfs_read_file('domains.cfg');
1356     my $realm_cfg = $domain_cfg->{ids}->{$realm};
1357     die "auth domain '$realm' does not exist\n" if !$realm_cfg;
1358
1359     my $realm_tfa = $realm_cfg->{tfa};
1360     if (defined($realm_tfa)) {
1361         $realm_tfa = PVE::Auth::Plugin::parse_tfa_config($realm_tfa);
1362         # If the realm has a TFA setting, we're only allowed to use that.
1363         if (defined($data)) {
1364             my $required_type = $realm_tfa->{type};
1365             if ($required_type ne $type) {
1366                 die "realm '$realm' only allows TFA of type '$required_type\n";
1367             }
1368
1369             if (defined($data->{config})) {
1370                 # XXX: Is it enough if the type matches? Or should the configuration also match?
1371             }
1372
1373             # realm-configured tfa always uses a simple key list, so use the user.cfg
1374             $user->{keys} = $data->{keys};
1375         } else {
1376             die "realm '$realm' does not allow removing the 2nd factor\n";
1377         }
1378     } else {
1379         # Without a realm-enforced TFA setting the user can add a u2f or totp entry by themselves.
1380         # The 'yubico' type requires yubico server settings, which have to be configured on the
1381         # realm, so this is not supported here:
1382         die "domain '$realm' does not support TFA type '$type'\n"
1383             if defined($data) && !$CUSTOM_TFA_TYPES->{$type};
1384     }
1385
1386     # Custom TFA entries are stored in priv/tfa.cfg as they can be more complet: u2f uses a
1387     # public key and a key handle, TOTP requires the usual totp settings...
1388
1389     my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
1390     my $tfa = ($tfa_cfg->{users}->{$userid} //= {});
1391
1392     if (defined($data)) {
1393         $tfa->{type} = $type;
1394         $tfa->{data} = $data;
1395         cfs_write_file('priv/tfa.cfg', $tfa_cfg);
1396
1397         $user->{keys} = 'x';
1398     } else {
1399         delete $tfa_cfg->{users}->{$userid};
1400         cfs_write_file('priv/tfa.cfg', $tfa_cfg);
1401
1402         delete $user->{keys};
1403     }
1404
1405     cfs_write_file('user.cfg', $user_cfg);
1406 }
1407
1408 sub user_get_tfa {
1409     my ($username, $realm) = @_;
1410
1411     my $user_cfg = cfs_read_file('user.cfg');
1412     my $user = $user_cfg->{users}->{$username}
1413         or die "user '$username' not found\n";
1414
1415     my $keys = $user->{keys};
1416     return if !$keys;
1417
1418     my $domain_cfg = cfs_read_file('domains.cfg');
1419     my $realm_cfg = $domain_cfg->{ids}->{$realm};
1420     die "auth domain '$realm' does not exist\n" if !$realm_cfg;
1421
1422     my $realm_tfa = $realm_cfg->{tfa};
1423     $realm_tfa = PVE::Auth::Plugin::parse_tfa_config($realm_tfa)
1424         if $realm_tfa;
1425
1426     if ($keys ne 'x') {
1427         # old style config, find the type via the realm
1428         return if !$realm_tfa;
1429         return ($realm_tfa->{type}, {
1430             keys => $keys,
1431             config => $realm_tfa,
1432         });
1433     } else {
1434         my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
1435         my $tfa = $tfa_cfg->{users}->{$username};
1436         return if !$tfa; # should not happen (user.cfg wasn't cleaned up?)
1437
1438         if ($realm_tfa) {
1439             # if the realm has a tfa setting we need to verify the type:
1440             die "auth domain '$realm' and user have mismatching TFA settings\n"
1441                 if $realm_tfa && $realm_tfa->{type} ne $tfa->{type};
1442         }
1443
1444         return ($tfa->{type}, $tfa->{data});
1445     }
1446 }
1447
1448 # bash completion helpers
1449
1450 register_standard_option('userid-completed',
1451     get_standard_option('userid', { completion => \&complete_username}),
1452 );
1453
1454 sub complete_username {
1455
1456     my $user_cfg = cfs_read_file('user.cfg');
1457
1458     return [ keys %{$user_cfg->{users}} ];
1459 }
1460
1461 sub complete_group {
1462
1463     my $user_cfg = cfs_read_file('user.cfg');
1464
1465     return [ keys %{$user_cfg->{groups}} ];
1466 }
1467
1468 sub complete_realm {
1469
1470     my $domain_cfg = cfs_read_file('domains.cfg');
1471
1472     return [ keys %{$domain_cfg->{ids}} ];
1473 }
1474
1475 1;