File Coverage

blib/lib/Unix/Passwd/File.pm
Criterion Covered Total %
statement 677 691 97.9
branch 370 438 84.4
condition 183 216 84.7
subroutine 53 53 100.0
pod 22 22 100.0
total 1305 1420 91.9


line stmt bran cond sub pod time code
1             ## no critic (InputOutput::RequireBriefOpen)
2              
3             package Unix::Passwd::File;
4              
5             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
6             our $DATE = '2020-04-29'; # DATE
7             our $DIST = 'Unix-Passwd-File'; # DIST
8             our $VERSION = '0.251'; # VERSION
9              
10 22     22   2062564 use 5.010001;
  22         317  
11 22     22   144 use strict;
  22         59  
  22         611  
12 22     22   129 use warnings;
  22         38  
  22         846  
13 22     22   10273 use experimental 'smartmatch';
  22         73364  
  22         123  
14             #use Log::ger;
15              
16 22     22   12244 use File::Flock::Retry;
  22         23803  
  22         758  
17 22     22   150 use List::Util qw(max first);
  22         45  
  22         2472  
18 22     22   11858 use List::MoreUtils qw(firstidx);
  22         270325  
  22         128  
19              
20             our @ISA = qw(Exporter);
21             our @EXPORT_OK = qw(
22             add_delete_user_groups
23             add_group
24             add_user
25             add_user_to_group
26             delete_group
27             delete_user
28             delete_user_from_group
29             get_group
30             get_max_gid
31             get_max_uid
32             get_user
33             get_user_groups
34             group_exists
35             is_member
36             list_groups
37             list_users
38             list_users_and_groups
39             modify_group
40             modify_user
41             set_user_groups
42             set_user_password
43             user_exists
44             );
45              
46             our %SPEC;
47              
48             $SPEC{':package'} = {
49             v => 1.1,
50             summary => 'Manipulate /etc/{passwd,shadow,group,gshadow} entries',
51             };
52              
53             my %common_args = (
54             etc_dir => {
55             summary => 'Specify location of passwd files',
56             schema => ['str*' => {default=>'/etc'}],
57             tags => ['common'],
58             },
59             );
60             my %write_args = (
61             backup => {
62             summary => 'Whether to backup when modifying files',
63             description => <<'_',
64              
65             Backup is written with `.bak` extension in the same directory. Unmodified file
66             will not be backed up. Previous backup will be overwritten.
67              
68             _
69             schema => ['bool' => {default=>0}],
70             },
71             );
72              
73             our $re_user = qr/\A[A-Za-z0-9._-]+\z/;
74             our $re_group = $re_user;
75             our $re_field = qr/\A[^\n:]*\z/;
76             our $re_posint = qr/\A[1-9][0-9]*\z/;
77              
78             our %passwd_fields = (
79             user => {
80             summary => 'User (login) name',
81             schema => ['unix::username*' => {match => $re_user}],
82             pos => 0,
83             },
84             pass => {
85             summary => 'Password, generally should be "x" which means password is '.
86             'encrypted in shadow',
87             schema => ['str*' => {match => $re_field}],
88             pos => 1,
89             },
90             uid => {
91             summary => 'Numeric user ID',
92             schema => 'unix::uid*',
93             pos => 2,
94             },
95             gid => {
96             summary => 'Numeric primary group ID for this user',
97             schema => 'unix::gid*',
98             pos => 3,
99             },
100             gecos => {
101             summary => 'Usually, it contains the full username',
102             schema => ['str*' => {match => $re_field}],
103             pos => 4,
104             },
105             home => {
106             summary => 'User\'s home directory',
107             schema => ['dirname*' => {match => $re_field}],
108             pos => 5,
109             },
110             shell => {
111             summary => 'User\'s shell',
112             schema => ['filename*' => {match=>qr/\A[^\n:]*\z/}],
113             pos => 6,
114             },
115             );
116             our @passwd_field_names;
117             for (keys %passwd_fields) {
118             $passwd_field_names[$passwd_fields{$_}{pos}] = $_;
119             delete $passwd_fields{$_}{pos};
120             }
121              
122             our %shadow_fields = (
123             user => {
124             summary => 'User (login) name',
125             schema => ['unix::username*' => {match => $re_user}],
126             pos => 0,
127             },
128             encpass => {
129             summary => 'Encrypted password',
130             schema => ['str*' => {match => $re_field}],
131             pos => 1,
132             },
133             last_pwchange => {
134             summary => 'The date of the last password change, '.
135             'expressed as the number of days since Jan 1, 1970.',
136             schema => 'int',
137             pos => 2,
138             },
139             min_pass_age => {
140             summary => 'The number of days the user will have to wait before she '.
141             'will be allowed to change her password again',
142             schema => 'int',
143             pos => 3,
144             },
145             max_pass_age => {
146             summary => 'The number of days after which the user will have to '.
147             'change her password',
148             schema => 'int',
149             pos => 4,
150             },
151             pass_warn_period => {
152             summary => 'The number of days before a password is going to expire '.
153             '(see max_pass_age) during which the user should be warned',
154             schema => 'int',
155             pos => 5,
156             },
157             pass_inactive_period => {
158             summary => 'The number of days after a password has expired (see '.
159             'max_pass_age) during which the password should still be accepted '.
160             '(and user should update her password during the next login)',
161             schema => 'int',
162             pos => 6,
163             },
164             expire_date => {
165             summary => 'The date of expiration of the account, expressed as the '.
166             'number of days since Jan 1, 1970',
167             schema => 'int',
168             pos => 7,
169             },
170             reserved => {
171             summary => 'This field is reserved for future use',
172             schema => ['str*' => {match => $re_field}],
173             pos => 8,
174             }
175             );
176             our @shadow_field_names;
177             for (keys %shadow_fields) {
178             $shadow_field_names[$shadow_fields{$_}{pos}] = $_;
179             delete $shadow_fields{$_}{pos};
180             }
181              
182             our %group_fields = (
183             group => {
184             summary => 'Group name',
185             schema => ['unix::groupname*' => {match => $re_group}],
186             pos => 0,
187             },
188             pass => {
189             summary => 'Password, generally should be "x" which means password is '.
190             'encrypted in gshadow',
191             schema => ['str*' => {match => $re_field}],
192             pos => 1,
193             },
194             gid => {
195             summary => 'Numeric group ID',
196             schema => 'unix::gid*',
197             pos => 2,
198             },
199             members => {
200             summary => 'List of usernames that are members of this group, '.
201             'separated by commas',
202             schema => ['str*' => {match => $re_field}],
203             pos => 3,
204             },
205             );
206             our @group_field_names;
207             for (keys %group_fields) {
208             $group_field_names[$group_fields{$_}{pos}] = $_;
209             delete $group_fields{$_}{pos};
210             }
211              
212             our %gshadow_fields = (
213             group => {
214             summary => 'Group name',
215             schema => ['unix::groupname*' => {match => $re_group}],
216             pos => 0,
217             },
218             encpass => {
219             summary => 'Encrypted password',
220             schema => ['str*' => {match=> $re_field}],
221             pos => 1,
222             },
223             admins => {
224             summary => 'It must be a comma-separated list of user names, or empty',
225             schema => ['str*' => {match => $re_field}],
226             pos => 2,
227             },
228             members => {
229             summary => 'List of usernames that are members of this group, '.
230             'separated by commas; You should use the same list of users as in '.
231             '/etc/group.',
232             schema => ['str*' => {match => $re_field}],
233             pos => 3,
234             },
235             );
236             our @gshadow_field_names;
237             for (keys %gshadow_fields) {
238             $gshadow_field_names[$gshadow_fields{$_}{pos}] = $_;
239             delete $gshadow_fields{$_}{pos};
240             }
241              
242             sub _arg_from_field {
243 418     418   798 my ($fields, $name, %extra) = @_;
244 418         510 my %spec = %{ $fields->{$name} };
  418         1469  
245 418         859 $spec{$_} = $extra{$_} for keys %extra;
246 418         1672 ($name => \%spec);
247             }
248              
249             sub _backup {
250 4     4   9 my ($fh, $path) = @_;
251 4 50       33 seek $fh, 0, 0 or return [500, "Can't seek: $!"];
252 4 50       294 open my($bak), ">", "$path.bak" or return [500, "Can't open $path.bak: $!"];
253 4         45 while (<$fh>) { print $bak $_ }
  22         79  
254 4 50       114 close $bak or return [500, "Can't write $path.bak: $!"];
255             # XXX set ctime & mtime of backup file?
256 4         26 [200];
257             }
258              
259             # all public functions in this module use the _routine(), which contains the
260             # basic flow, to avoid duplication of code. admittedly this makes _routine()
261             # quite convoluted, as it tries to accomodate all the functions' logic in a
262             # single routine. _routine() accepts these special arguments for flow control:
263             #
264             # - _read_shadow = 0*/1/2 (2 means optional, don't exit if fail)
265             # - _read_passwd = 0*/1
266             # - _read_gshadow = 0*/1/2 (2 means optional, don't exit if fail)
267             # - _read_group = 0*/1
268             # - _lock = 0*/1 (whether to lock)
269             # - _after_read = code (executed after reading all passwd/group files)
270             # - _after_read_passwd_entry = code (executed after reading a line in passwd)
271             # - _after_read_group_entry = code (executed after reading a line in group)
272             # - _write_shadow = 0*/1
273             # - _write_passwd = 0*/1
274             # - _write_gshadow = 0*/1
275             # - _write_group = 0*/1
276             #
277             # all the hooks are fed $stash, sort of like a bag or object containing all
278             # data. should return enveloped response. _routine() will return with response
279             # if response is non success. _routine() will also return immediately if
280             # $stash{exit} is set.
281             #
282             # to write, we open once but with mode '+<' instead of '<'. we read first then
283             # we seek back to beginning and write from in-memory data. if
284             # $stash{write_passwd} and so on is set to false, _routine() cancels the write
285             # (can be used e.g. when there is no change so no need to write).
286             #
287             # final result is in $stash{res} or non-success result returned by hook.
288             sub _routine {
289 95     95   529 my %args = @_;
290              
291 95   50     316 my $etc = $args{etc_dir} // "/etc";
292 95         173 my $detail = $args{detail};
293 95   100     283 my $wfn = $args{with_field_names} // 1;
294 95         151 my @locks;
295 95         229 my ($fhp, $fhs, $fhg, $fhgs);
296 95         0 my %stash;
297              
298 95         167 my $e = eval {
299              
300 95 100       268 if ($args{_lock}) {
301 38         96 for (qw/passwd shadow group gshadow/) {
302 152         14448 push @locks, File::Flock::Retry->lock("$etc/$_", {retries=>3});
303             }
304             }
305              
306             # read files
307              
308 95         4172 my @shadow;
309             my %shadow;
310 95         0 my @shadowh;
311 95         205 $stash{shadow} = \@shadow;
312 95         181 $stash{shadowh} = \@shadowh;
313 95 100 100     446 if ($args{_read_shadow} || $args{_write_shadow}) {
314 37 100       1416 unless (open $fhs, ($args{_write_shadow} ? "+":"")."<",
    100          
315             "$etc/shadow") {
316 2 50 33     17 if ($args{_read_shadow} == 2 && !$args{_write_shadow}) {
317 2         23 goto L1;
318             } else {
319 0         0 return [500, "Can't open $etc/shadow: $!"];
320             }
321             }
322 35         615 while (<$fhs>) {
323 178         882 chomp;
324 178 50       582 next unless /\S/; # skip empty line
325 178         930 my @r = split /:/, $_, scalar(keys %shadow_fields);
326 178         314 push @shadow, \@r;
327 178         374 $shadow{$r[0]} = \@r;
328 178 100       338 if ($wfn) {
329 168         255 my %r;
330 168         866 @r{@shadow_field_names} = @r;
331 168         810 push @shadowh, \%r;
332             }
333             }
334             }
335              
336             L1:
337 95         184 my @passwd;
338 95         133 my @passwdh;
339 95         201 $stash{passwd} = \@passwd;
340 95         201 $stash{passwdh} = \@passwdh;
341 95 100 100     392 if ($args{_read_passwd} || $args{_write_passwd}) {
342 47 100       1783 open $fhp, ($args{_write_passwd} ? "+":"")."<", "$etc/passwd"
    100          
343             or return [500, "Can't open $etc/passwd: $!"];
344 46         796 while (<$fhp>) {
345 255         416 chomp;
346 255 50       811 next unless /\S/; # skip empty line
347 255         1220 my @r = split /:/, $_, scalar(keys %passwd_fields);
348 255         465 push @passwd, \@r;
349 255 100       476 if ($wfn) {
350 158         204 my %r;
351 157         824 @r{@shadow_field_names} = @{ $shadow{$r[0]} }
352 158 100       332 if $shadow{$r[0]};
353 158         642 @r{@passwd_field_names} = @r;
354 158         299 push @passwdh, \%r;
355             }
356 255 100       1036 if ($args{_after_read_passwd_entry}) {
357 56         128 my $res = $args{_after_read_passwd_entry}->(\%stash);
358 56 50       123 return $res if $res->[0] != 200;
359 56 100       270 return if $stash{exit};
360             }
361             }
362             }
363              
364 85         260 my @gshadow;
365             my %gshadow;
366 85         0 my @gshadowh;
367 85         196 $stash{gshadow} = \@gshadow;
368 85         200 $stash{gshadowh} = \@gshadowh;
369 85 100 100     377 if ($args{_read_gshadow} || $args{_write_gshadow}) {
370 56 100       2032 unless (open $fhgs, ($args{_write_gshadow} ? "+":"")."<",
    100          
371             "$etc/gshadow") {
372 2 50 33     13 if ($args{_read_gshadow} == 2 && !$args{_write_gshadow}) {
373 2         20 goto L2;
374             } else {
375 0         0 return [500, "Can't open $etc/gshadow: $!"];
376             }
377             }
378 54         862 while (<$fhgs>) {
379 330         517 chomp;
380 330 50       948 next unless /\S/; # skip empty line
381 330         930 my @r = split /:/, $_, scalar(keys %gshadow_fields);
382 330         554 push @gshadow, \@r;
383 330         632 $gshadow{$r[0]} = \@r;
384 330 100       607 if ($wfn) {
385 318         414 my %r;
386 318         958 @r{@gshadow_field_names} = @r;
387 318         1315 push @gshadowh, \%r;
388             }
389             }
390             }
391              
392             L2:
393 85         194 my @group;
394 85         132 my @grouph;
395 85         231 $stash{group} = \@group;
396 85         207 $stash{grouph} = \@grouph;
397 85 100 100     394 if ($args{_read_group} || $args{_write_group}) {
398 70 100       2361 open $fhg, ($args{_write_group} ? "+":"")."<",
    100          
399             "$etc/group"
400             or return [500, "Can't open $etc/group: $!"];
401 69         1287 while (<$fhg>) {
402 465         712 chomp;
403 465 50       1320 next unless /\S/; # skip empty line
404 465         1331 my @r = split /:/, $_, scalar(keys %group_fields);
405 465         784 push @group, \@r;
406 465 100       789 if ($wfn) {
407 320         384 my %r;
408 301         1036 @r{@gshadow_field_names} = @{ $gshadow{$r[0]} }
409 320 100       664 if $gshadow{$r[0]};
410 320         845 @r{@group_field_names} = @r;
411 320         572 push @grouph, \%r;
412             }
413 465 100       1607 if ($args{_after_read_group_entry}) {
414 106         191 my $res = $args{_after_read_group_entry}->(\%stash);
415 106 50       260 return $res if $res->[0] != 200;
416 106 100       419 return if $stash{exit};
417             }
418             }
419             }
420              
421 68 50       287 if ($args{_after_read}) {
422 68         281 my $res = $args{_after_read}->(\%stash);
423 68 100       304 return $res if $res->[0] != 200;
424 47 100       219 return if $stash{exit};
425             }
426              
427             # write files
428              
429 28 100 100     161 if ($args{_write_shadow} && ($stash{write_shadow}//1)) {
      100        
430 12 100       34 if ($args{backup}) {
431 1         6 my $res = _backup($fhs, "$etc/shadow");
432 1 50       6 return $res if $res->[0] != 200;
433             }
434 12 50       124 seek $fhs, 0, 0 or return [500, "Can't seek in $etc/shadow: $!"];
435 12         42 for (@shadow) {
436 66   50     121 print $fhs join(":", map {$_//""} @$_), "\n";
  594         1123  
437             }
438 12         464 truncate $fhs, tell($fhs);
439 12 50       158 close $fhs or return [500, "Can't close $etc/shadow: $!"];
440 12         202 chmod 0640, "$etc/shadow"; # check error?
441             }
442              
443 28 100 100     144 if ($args{_write_passwd} && ($stash{write_passwd}//1)) {
      100        
444 12 100       31 if ($args{backup}) {
445 1         6 my $res = _backup($fhp, "$etc/passwd");
446 1 50       6 return $res if $res->[0] != 200;
447             }
448 12 50       109 seek $fhp, 0, 0 or return [500, "Can't seek in $etc/passwd: $!"];
449 12         36 for (@passwd) {
450 66   50     122 print $fhp join(":", map {$_//""} @$_), "\n";
  462         955  
451             }
452 12         317 truncate $fhp, tell($fhp);
453 12 50       140 close $fhp or return [500, "Can't close $etc/passwd: $!"];
454 12         183 chmod 0644, "$etc/passwd"; # check error?
455             }
456              
457 28 100 100     221 if ($args{_write_gshadow} && ($stash{write_gshadow}//1)) {
      100        
458 21 100       66 if ($args{backup}) {
459 1         6 my $res = _backup($fhgs, "$etc/gshadow");
460 1 50       7 return $res if $res->[0] != 200;
461             }
462 21 50       203 seek $fhgs, 0, 0 or return [500, "Can't seek in $etc/gshadow: $!"];
463 21         67 for (@gshadow) {
464 135   50     240 print $fhgs join(":", map {$_//""} @$_), "\n";
  540         1184  
465             }
466 21         660 truncate $fhgs, tell($fhgs);
467 21 50       257 close $fhgs or return [500, "Can't close $etc/gshadow: $!"];
468 21         332 chmod 0640, "$etc/gshadow"; # check error?
469             }
470              
471 28 100 100     207 if ($args{_write_group} && ($stash{write_group}//1)) {
      100        
472 23 100       66 if ($args{backup}) {
473 1         6 my $res = _backup($fhg, "$etc/group");
474 1 50       7 return $res if $res->[0] != 200;
475             }
476 23 50       219 seek $fhg, 0, 0 or return [500, "Can't seek in $etc/group: $!"];
477 23         77 for (@group) {
478 147   50     268 print $fhg join(":", map {$_//""} @$_), "\n";
  588         1283  
479             }
480 23         642 truncate $fhg, tell($fhg);
481 23 50       302 close $fhg or return [500, "Can't close $etc/group: $!"];
482 23         354 chmod 0644, "$etc/group"; # check error?
483             }
484              
485 28         218 [200, "OK"];
486             }; # eval
487 95 50       314 $e = [500, "Died: $@"] if $@;
488              
489             # release the locks
490 95         392 undef @locks;
491              
492 95 100 33     4962 $stash{res} //= $e if $e && $e->[0] != 200;
      100        
493 95 100 33     374 $stash{res} //= $e if $e && $e->[0] != 200;
      100        
494 95   50     274 $stash{res} //= [500, "BUG: res not set"];
495              
496 95         3477 $stash{res};
497             }
498              
499             $SPEC{list_users} = {
500             v => 1.1,
501             summary => 'List Unix users in passwd file',
502             args => {
503             %common_args,
504             detail => {
505             summary => 'If true, return all fields instead of just usernames',
506             schema => ['bool' => {default => 0}],
507             },
508             with_field_names => {
509             summary => 'If false, don\'t return hash for each entry',
510             schema => [bool => {default=>1}],
511             description => <<'_',
512              
513             By default, when `detail=>1`, a hashref is returned for each entry containing
514             field names and its values, e.g. `{user=>"titin", pass=>"x", uid=>500, ...}`.
515             With `with_field_names=>0`, an arrayref is returned instead: `["titin", "x",
516             500, ...]`.
517              
518             _
519             },
520             },
521             };
522             sub list_users {
523 4     4 1 11215 my %args = @_;
524 4         7 my $detail = $args{detail};
525 4 100 100     16 my $wfn = $args{with_field_names} // ($detail ? 1:0);
526              
527             _routine(
528             %args,
529             _read_passwd => 1,
530             _read_shadow => $detail ? 2:0,
531             with_field_names => $wfn,
532             _after_read => sub {
533 4     4   5 my $stash = shift;
534              
535 4         7 my @rows;
536 4         5 my $passwd = $stash->{passwd};
537 4         6 my $passwdh = $stash->{passwdh};
538              
539 4         35 for (my $i=0; $i < @$passwd; $i++) {
540 20 100       29 if (!$detail) {
    100          
541 10         22 push @rows, $passwd->[$i][0];
542             } elsif ($wfn) {
543 5         9 push @rows, $passwdh->[$i];
544             } else {
545 5         34 push @rows, $passwd->[$i];
546             }
547             }
548              
549 4         10 $stash->{res} = [200, "OK", \@rows];
550 4 100       19 $stash->{res}[3]{'table.fields'} = [\@passwd_field_names]
551             if $detail;
552 4         7 $stash->{exit}++;
553 4         11 [200];
554             },
555 4 100       25 );
556             }
557              
558             $SPEC{get_user} = {
559             v => 1.1,
560             summary => 'Get user details by username or uid',
561             description => <<'_',
562              
563             Either `user` OR `uid` must be specified.
564              
565             The function is not dissimilar to Unix's `getpwnam()` or `getpwuid()`.
566              
567             _
568             args_rels => {
569             'choose_one' => [qw/user uid/],
570             },
571             args => {
572             %common_args,
573             user => {
574             schema => 'unix::username*',
575             },
576             uid => {
577             schema => 'unix::uid*',
578             },
579             with_field_names => {
580             summary => 'If false, don\'t return hash',
581             schema => [bool => {default=>1}],
582             description => <<'_',
583              
584             By default, a hashref is returned containing field names and its values, e.g.
585             `{user=>"titin", pass=>"x", uid=>500, ...}`. With `with_field_names=>0`, an
586             arrayref is returned instead: `["titin", "x", 500, ...]`.
587              
588             _
589             },
590             },
591             };
592             sub get_user {
593 16     16 1 28835 my %args = @_;
594 16   50     91 my $wfn = $args{with_field_names} // 1;
595 16         36 my $user = $args{user};
596 16         30 my $uid = $args{uid};
597 16 100 75     88 return [400, "Please specify user OR uid"]
598             unless defined($user) xor defined($uid);
599              
600             _routine(
601             %args,
602             _read_passwd => 1,
603             _read_shadow => 2,
604             with_field_names => $wfn,
605             detail => 1,
606             _after_read_passwd_entry => sub {
607 56     56   79 my $stash = shift;
608              
609 56         80 my @rows;
610 56         84 my $passwd = $stash->{passwd};
611 56         79 my $passwdh = $stash->{passwdh};
612              
613 56 100 100     283 if (defined($user) && $passwd->[-1][0] eq $user ||
      100        
      100        
614             defined($uid) && $passwd->[-1][2] == $uid) {
615 9 50       66 $stash->{res} = [200,"OK", $wfn ? $passwdh->[-1]:$passwd->[-1]];
616 9         27 $stash->{exit}++;
617             }
618 56         127 [200];
619             },
620             _after_read => sub {
621 5     5   26 my $stash = shift;
622 5         15 [404, "Not found"];
623             },
624 15         140 );
625             }
626              
627             $SPEC{user_exists} = {
628             v => 1.1,
629             summary => 'Check whether user exists',
630             args_rels => {
631             choose_one => [qw/user uid/],
632             },
633             args => {
634             %common_args,
635             user => {
636             schema => 'unix::username*',
637             },
638             uid => {
639             schema => 'unix::uid*',
640             },
641             },
642             result_naked => 1,
643             result => {
644             schema => 'bool*',
645             },
646             };
647             sub user_exists {
648 2     2 1 112 my %args = @_;
649 2         10 my $res = get_user(%args);
650 2 100       26 if ($res->[0] == 404) { return 0 }
  1 50       9  
651 1         9 elsif ($res->[0] == 200) { return 1 }
652 0         0 else { return undef }
653             }
654              
655             $SPEC{list_groups} = {
656             v => 1.1,
657             summary => 'List Unix groups in group file',
658             args => {
659             %common_args,
660             detail => {
661             summary => 'If true, return all fields instead of just group names',
662             schema => ['bool' => {default => 0}],
663             },
664             with_field_names => {
665             summary => 'If false, don\'t return hash for each entry',
666             schema => [bool => {default=>1}],
667             description => <<'_',
668              
669             By default, when `detail=>1`, a hashref is returned for each entry containing
670             field names and its values, e.g. `{group=>"titin", pass=>"x", gid=>500, ...}`.
671             With `with_field_names=>0`, an arrayref is returned instead: `["titin", "x",
672             500, ...]`.
673              
674             _
675             },
676             },
677             };
678             sub list_groups {
679 5     5 1 11795 my %args = @_;
680 5         12 my $detail = $args{detail};
681 5 100 100     25 my $wfn = $args{with_field_names} // ($detail ? 1:0);
682              
683             _routine(
684             %args,
685             _read_group => 1,
686             _read_gshadow => $detail ? 2:0,
687             with_field_names => $wfn,
688             _after_read => sub {
689 5     5   9 my $stash = shift;
690              
691 5         7 my @rows;
692 5         10 my $group = $stash->{group};
693 5         10 my $grouph = $stash->{grouph};
694              
695 5         16 for (my $i=0; $i < @$group; $i++) {
696 30 100       49 if (!$detail) {
    100          
697 18         39 push @rows, $group->[$i][0];
698             } elsif ($wfn) {
699 6         11 push @rows, $grouph->[$i];
700             } else {
701 6         27 push @rows, $group->[$i];
702             }
703             }
704              
705 5         15 $stash->{res} = [200, "OK", \@rows];
706 5 100       16 $stash->{res}[3]{'table.fields'} = [\@group_field_names] if $detail;
707 5         9 $stash->{exit}++;
708 5         12 [200];
709             },
710 5 100       37 );
711             }
712              
713             $SPEC{get_group} = {
714             v => 1.1,
715             summary => 'Get group details by group name or gid',
716             description => <<'_',
717              
718             Either `group` OR `gid` must be specified.
719              
720             The function is not dissimilar to Unix's `getgrnam()` or `getgrgid()`.
721              
722             _
723             args_rels => {
724             choose_one => [qw/group gid/],
725             },
726             args => {
727             %common_args,
728             group => {
729             schema => 'unix::username*',
730             },
731             gid => {
732             schema => 'unix::gid*',
733             },
734             with_field_names => {
735             summary => 'If false, don\'t return hash',
736             schema => [bool => {default=>1}],
737             description => <<'_',
738              
739             By default, a hashref is returned containing field names and its values, e.g.
740             `{group=>"titin", pass=>"x", gid=>500, ...}`. With `with_field_names=>0`, an
741             arrayref is returned instead: `["titin", "x", 500, ...]`.
742              
743             _
744             },
745             },
746             };
747             sub get_group {
748 23     23 1 26084 my %args = @_;
749 23   50     122 my $wfn = $args{with_field_names} // 1;
750 23         46 my $gn = $args{group};
751 23         36 my $gid = $args{gid};
752 23 100 75     104 return [400, "Please specify group OR gid"]
753             unless defined($gn) xor defined($gid);
754              
755             _routine(
756             %args,
757             _read_group => 1,
758             _read_gshadow => 2,
759             with_field_names => $wfn,
760             detail => 1,
761             _after_read_group_entry => sub {
762 106     106   139 my $stash = shift;
763              
764 106         123 my @rows;
765 106         143 my $group = $stash->{group};
766 106         140 my $grouph = $stash->{grouph};
767              
768 106 100 100     473 if (defined($gn) && $group->[-1][0] eq $gn ||
      100        
      100        
769             defined($gid) && $group->[-1][2] == $gid) {
770 16 50       66 $stash->{res} = [200,"OK", $wfn ? $grouph->[-1]:$group->[-1]];
771 16         34 $stash->{exit}++;
772             }
773 106         218 [200];
774             },
775             _after_read => sub {
776 5     5   29 my $stash = shift;
777 5         14 [404, "Not found"];
778             },
779 22         201 );
780             }
781              
782             $SPEC{list_users_and_groups} = {
783             v => 1.1,
784             summary => 'List Unix users and groups in passwd/group files',
785             description => <<'_',
786              
787             This is basically `list_users()` and `list_groups()` combined, so you can get
788             both data in a single call. Data is returned in an array. Users list is in the
789             first element, groups list in the second.
790              
791             _
792             args => {
793             %common_args,
794             detail => {
795             summary => 'If true, return all fields instead of just names',
796             schema => ['bool' => {default => 0}],
797             },
798             with_field_names => {
799             summary => 'If false, don\'t return hash for each entry',
800             schema => [bool => {default=>1}],
801             },
802             },
803             };
804             sub list_users_and_groups {
805 4     4 1 12242 my %args = @_;
806 4         8 my $detail = $args{detail};
807 4 100 100     19 my $wfn = $args{with_field_names} // ($detail ? 1:0);
808              
809             _routine(
810             %args,
811             _read_passwd => 1,
812             _read_shadow => $detail ? 2:0,
813             _read_group => 1,
814             _read_gshadow => $detail ? 2:0,
815             with_field_names => $wfn,
816             _after_read => sub {
817 4     4   7 my $stash = shift;
818              
819 4         5 my @users;
820 4         6 my $passwd = $stash->{passwd};
821 4         8 my $passwdh = $stash->{passwdh};
822 4         10 for (my $i=0; $i < @$passwd; $i++) {
823 20 100       30 if (!$detail) {
    100          
824 10         24 push @users, $passwd->[$i][0];
825             } elsif ($wfn) {
826 5         10 push @users, $passwdh->[$i];
827             } else {
828 5         10 push @users, $passwd->[$i];
829             }
830             }
831              
832 4         5 my @groups;
833 4         7 my $group = $stash->{group};
834 4         4 my $grouph = $stash->{grouph};
835 4         10 for (my $i=0; $i < @$group; $i++) {
836 24 100       33 if (!$detail) {
    100          
837 12         21 push @groups, $group->[$i][0];
838             } elsif ($wfn) {
839 6         12 push @groups, $grouph->[$i];
840             } else {
841 6         12 push @groups, $group->[$i];
842             }
843             }
844              
845 4         14 $stash->{res} = [200, "OK", [\@users, \@groups]];
846              
847 4         9 $stash->{exit}++;
848 4         15 [200];
849             },
850 4 100       28 );
    100          
851             }
852              
853             $SPEC{group_exists} = {
854             v => 1.1,
855             summary => 'Check whether group exists',
856             args_rels => {
857             choose_one => [qw/group gid/],
858             },
859             args => {
860             %common_args,
861             group => {
862             schema => 'unix::groupname*',
863             },
864             gid => {
865             schema => 'unix::gid*',
866             },
867             },
868             result_naked => 1,
869             result => {
870             schema => 'bool',
871             },
872             };
873             sub group_exists {
874 2     2 1 89 my %args = @_;
875 2         8 my $res = get_group(%args);
876 2 100       23 if ($res->[0] == 404) { return 0 }
  1 50       8  
877 1         8 elsif ($res->[0] == 200) { return 1 }
878 0         0 else { return undef }
879             }
880              
881             $SPEC{get_user_groups} = {
882             v => 1.1,
883             summary => 'Return groups which the user belongs to',
884             args => {
885             %common_args,
886             user => {
887             schema => 'unix::username*',
888             req => 1,
889             pos => 0,
890             },
891             detail => {
892             summary => 'If true, return all fields instead of just group names',
893             schema => ['bool' => {default => 0}],
894             },
895             with_field_names => {
896             summary => 'If false, don\'t return hash for each entry',
897             schema => [bool => {default=>1}],
898             description => <<'_',
899              
900             By default, when `detail=>1`, a hashref is returned for each entry containing
901             field names and its values, e.g. `{group=>"titin", pass=>"x", gid=>500, ...}`.
902             With `with_field_names=>0`, an arrayref is returned instead: `["titin", "x",
903             500, ...]`.
904              
905             _
906             },
907             },
908             };
909             # this is a routine to list groups, but filtered using a criteria. can be
910             # refactored into a common routine (along with list_groups) if needed, to reduce
911             # duplication.
912             sub get_user_groups {
913 5     5 1 6187 my %args = @_;
914 5 50       25 my $user = $args{user} or return [400, "Please specify user"];
915 5         11 my $detail = $args{detail};
916 5 50 33     32 my $wfn = $args{with_field_names} // ($detail ? 1:0);
917              
918             _routine(
919             %args,
920             _read_passwd => 1,
921             _read_group => 1,
922             _read_gshadow => $detail ? 2:0,
923             with_field_names => $wfn,
924             _after_read => sub {
925 5     5   12 my $stash = shift;
926              
927 5         18 my $passwd = $stash->{passwd};
928             return [404, "User not found"]
929 5 100       46 unless first {$_->[0] eq $user} @$passwd;
  21         46  
930              
931 4         16 my @rows;
932 4         19 my $group = $stash->{group};
933 4         9 my $grouph = $stash->{grouph};
934              
935 4         22 for (my $i=0; $i < @$group; $i++) {
936 24         57 my @mm = split /,/, $group->[$i][3];
937 24 100 66     137 next unless $user ~~ @mm || $group->[$i][0] eq $user;
938 10 50       55 if (!$detail) {
    0          
939 10         37 push @rows, $group->[$i][0];
940             } elsif ($wfn) {
941 0         0 push @rows, $grouph->[$i];
942             } else {
943 0         0 push @rows, $group->[$i];
944             }
945             }
946              
947 4         16 $stash->{res} = [200, "OK", \@rows];
948              
949 4         11 $stash->{exit}++;
950 4         11 [200];
951             },
952 5 50       49 );
953             }
954              
955             $SPEC{is_member} = {
956             v => 1.1,
957             summary => 'Check whether user is member of a group',
958             args => {
959             %common_args,
960             user => {
961             schema => 'unix::username*',
962             req => 1,
963             pos => 0,
964             },
965             group => {
966             schema => 'unix::groupname*',
967             req => 1,
968             pos => 1,
969             },
970             },
971             result_naked => 1,
972             result => {
973             schema => 'bool',
974             },
975             };
976             sub is_member {
977 6     6 1 108 my %args = @_;
978 6 100       21 my $user = $args{user} or return undef;
979 5 100       16 my $group = $args{group} or return undef;
980 4         9 my $res = get_group(etc_dir=>$args{etc_dir}, group=>$group);
981 4 100       39 return undef unless $res->[0] == 200;
982 3         20 my @mm = split /,/, $res->[2]{members};
983 3 100       35 return $user ~~ @mm ? 1:0;
984             }
985              
986             $SPEC{get_max_uid} = {
987             v => 1.1,
988             summary => 'Get maximum UID used',
989             args => {
990             %common_args,
991             },
992             };
993             sub get_max_uid {
994 1     1 1 1209 my %args = @_;
995             _routine(
996             %args,
997             _read_passwd => 1,
998             detail => 0,
999             with_field_names => 0,
1000             _after_read => sub {
1001 1     1   4 my $stash = shift;
1002 1         3 my $passwd = $stash->{passwd};
1003             $stash->{res} = [200, "OK", max(
1004 1         4 map {$_->[2]} @$passwd
  42         78  
1005             )];
1006 1         5 $stash->{exit}++;
1007 1         5 [200];
1008             },
1009 1         9 );
1010             }
1011              
1012             $SPEC{get_max_gid} = {
1013             v => 1.1,
1014             summary => 'Get maximum GID used',
1015             args => {
1016             %common_args,
1017             },
1018             };
1019             sub get_max_gid {
1020 1     1 1 982 require List::Util;
1021              
1022 1         4 my %args = @_;
1023             _routine(
1024             %args,
1025             _read_group => 1,
1026             detail => 0,
1027             with_field_names => 0,
1028             _after_read => sub {
1029 1     1   2 my $stash = shift;
1030 1         2 my $group = $stash->{group};
1031             $stash->{res} = [200, "OK", List::Util::max(
1032 1         3 map {$_->[2]} @$group
  73         113  
1033             )];
1034 1         7 $stash->{exit}++;
1035 1         3 [200];
1036             },
1037 1         7 );
1038             }
1039              
1040             sub _enc_pass {
1041 3     3   29 require Crypt::Password::Util;
1042 3         17 Crypt::Password::Util::crypt(shift);
1043             }
1044              
1045             sub _add_group_or_user {
1046 27     27   144 my ($which, %args) = @_;
1047              
1048             # TMP,schema
1049 27         52 my ($user, $gn);
1050 27         49 my $create_group = 1;
1051 27 100       88 if ($which eq 'user') {
1052 18 100       59 $user = $args{user} or return [400, "Please specify user"];
1053 17 100       100 $user =~ /$re_user/o
1054             or return [400, "Invalid user, please use $re_user"];
1055 16   66     66 $gn = $args{group} // $user;
1056 16 100       37 $create_group = 0 if $gn ne $user;
1057             }
1058 25   100     83 $gn //= $args{group};
1059 25 100       63 $gn or return [400, "Please specify group"];
1060 24 100       107 $gn =~ /$re_group/o
1061             or return [400, "Invalid group, please use $re_group"];
1062              
1063 23         44 my $gid = $args{gid};
1064 23 50 100     81 my $min_gid = $args{min_gid} // 1000; $min_gid = 0 if $min_gid<0;
  23         47  
1065 23 50 100     70 my $max_gid = $args{max_gid} // 65535; $max_gid = 65535 if $max_gid>65535;
  23         74  
1066 23         33 my $members;
1067 23 100       60 if ($which eq 'group') {
1068 7         13 $members = $args{members};
1069 7 50 66     26 if ($members && ref($members) eq 'ARRAY') {
1070 0         0 $members = join(",",@$members);
1071             }
1072 7   100     26 $members //= "";
1073 7 100       37 $members =~ /$re_field/o
1074             or return [400, "Invalid members, please use $re_field"];
1075             } else {
1076 16         22 $members = "$user";
1077             }
1078              
1079 22         82 my ($uid, $min_uid, $max_uid);
1080 22         0 my ($pass, $gecos, $home, $shell);
1081 22         0 my ($encpass, $last_pwchange, $min_pass_age, $max_pass_age,
1082             $pass_warn_period, $pass_inactive_period, $expire_date);
1083 22 100       65 if ($which eq 'user') {
1084 16         25 $uid = $args{uid};
1085 16 50 100     39 $min_uid = $args{min_uid} // 1000; $min_uid = 0 if $min_uid<0;
  16         30  
1086 16 50 100     45 $max_uid = $args{max_uid} // 65535; $max_uid = 65535 if $min_uid>65535;
  16         28  
1087              
1088 16   100     45 $pass = $args{pass} // "";
1089 16 50       49 if ($pass !~ /$re_field/o) { return [400, "Invalid pass"] }
  0         0  
1090              
1091 16   100     43 $gecos = $args{gecos} // "";
1092 16 100       41 if ($gecos !~ /$re_field/o) { return [400, "Invalid gecos"] }
  1         6  
1093              
1094 15   100     41 $home = $args{home} // "";
1095 15 100       46 if ($home !~ /$re_field/o) { return [400, "Invalid home"] }
  1         21  
1096              
1097 14   100     35 $shell = $args{shell} // "";
1098 14 100       38 if ($shell !~ /$re_field/o) { return [400, "Invalid shell"] }
  1         6  
1099              
1100 13 100 66     49 $encpass = $args{encpass} // ($pass eq '' ? '*' : _enc_pass($pass));
1101 13 100       202072886 if ($encpass !~ /$re_field/o) { return [400, "Invalid encpass"] }
  1         7  
1102              
1103 12   66     58 $last_pwchange = int($args{last_pwchange} // time()/86400);
1104 12   50     37 $min_pass_age = int($args{min_pass_age} // 0);
1105 12   50     76 $max_pass_age = int($args{max_pass_age} // 99999);
1106 12   50     40 $pass_warn_period = int($args{max_pass_age} // 7);
1107 12   100     36 $pass_inactive_period = $args{pass_inactive_period} // "";
1108 12 100       36 if ($pass_inactive_period !~ /$re_field/o) {
1109 1         6 return [400, "Invalid pass_inactive_period"] }
1110 11   100     35 $expire_date = $args{expire_date} // "";
1111 11 100       36 if ($expire_date !~ /$re_field/o) {
1112 1         6 return [400, "Invalid expire_date"] }
1113             }
1114              
1115             _routine(
1116             %args,
1117             _lock => 1,
1118             _write_group => 1,
1119             _write_gshadow => 1,
1120             _write_passwd => $which eq 'user',
1121             _write_shadow => $which eq 'user',
1122             _after_read => sub {
1123 16     16   31 my $stash = shift;
1124              
1125 16         30 my $group = $stash->{group};
1126 16         25 my $gshadow = $stash->{gshadow};
1127 16         24 my $write_g;
1128 16         128 my $cur_g = first { $_->[0] eq $gn } @$group;
  89         134  
1129              
1130 16 100 100     107 if ($which eq 'group' && $cur_g) {
    100          
    100          
1131 1 50       8 return [412, "Group $gn already exists"] if $cur_g;
1132             } elsif ($cur_g) {
1133 2         5 $gid = $cur_g->[2];
1134             } elsif (!$create_group) {
1135 1         4 return [412, "Group $gn must already exist"];
1136             } else {
1137 12         28 my @gids = map { $_->[2] } @$group;
  72         132  
1138 12 100       38 if (!defined($gid)) {
1139 10         37 for ($min_gid .. $max_gid) {
1140 28 100       93 do { $gid = $_; last } unless $_ ~~ @gids;
  9         14  
  9         15  
1141             }
1142 10 100       22 return [412, "Can't find available GID"]
1143             unless defined($gid);
1144             }
1145 11         46 push @$group , [$gn, "x", $gid, $members];
1146 11         41 push @$gshadow, [$gn, "*", "", $members];
1147 11         28 $write_g++;
1148             }
1149 13         34 my $r = {gid=>$gid};
1150              
1151 13 100       30 if ($which eq 'user') {
1152 9         16 my $passwd = $stash->{passwd};
1153 9         14 my $shadow = $stash->{shadow};
1154             return [412, "User $gn already exists"]
1155 9 100       32 if first { $_->[0] eq $user } @$passwd;
  44         68  
1156 8         23 my @uids = map { $_->[2] } @$passwd;
  40         63  
1157 8 100       17 if (!defined($uid)) {
1158 6         14 for ($min_uid .. $max_uid) {
1159 15 100       38 do { $uid = $_; last } unless $_ ~~ @uids;
  5         6  
  5         7  
1160             }
1161 6 100       15 return [412, "Can't find available UID"]
1162             unless defined($uid);
1163             }
1164 7         12 $r->{uid} = $uid;
1165 7         40 push @$passwd, [$user, "x", $uid, $gid, $gecos, $home, $shell];
1166 7         30 push @$shadow, [$user, $encpass, $last_pwchange, $min_pass_age,
1167             $max_pass_age, $pass_warn_period,
1168             $pass_inactive_period, $expire_date, ""];
1169              
1170             # add user as member of group
1171 7         11 for my $l (@$group) {
1172 46 100       73 next unless $l->[0] eq $gn;
1173 7         28 my @mm = split /,/, $l->[3];
1174 7 100       26 unless ($user ~~ @mm) {
1175 1         3 $l->[3] = join(",", @mm, $user);
1176 1         10 $write_g++;
1177 1         4 last;
1178             }
1179             }
1180             }
1181              
1182 11 50       24 $stash->{write_group} = $stash->{write_gshadow} = 0 unless $write_g;
1183 11         29 $stash->{res} = [200, "OK", $r];
1184 11         25 [200];
1185             },
1186 16         190 );
1187             }
1188              
1189             $SPEC{add_group} = {
1190             v => 1.1,
1191             summary => 'Add a new group',
1192             args => {
1193             %common_args,
1194             %write_args,
1195             group => {
1196             schema => 'unix::groupname*',
1197             req => 1,
1198             pos => 0,
1199             },
1200             gid => {
1201             summary => 'Pick a specific new GID',
1202             schema => 'unix::gid*',
1203             description => <<'_',
1204              
1205             Adding a new group with duplicate GID is allowed.
1206              
1207             _
1208             },
1209             min_gid => {
1210             summary => 'Pick a range for new GID',
1211             schema => [int => {between=>[0, 65535], default=>1000}],
1212             description => <<'_',
1213              
1214             If a free GID between `min_gid` and `max_gid` is not found, error 412 is
1215             returned.
1216              
1217             _
1218             },
1219             max_gid => {
1220             summary => 'Pick a range for new GID',
1221             schema => [int => {between=>[0, 65535], default=>65535}],
1222             description => <<'_',
1223              
1224             If a free GID between `min_gid` and `max_gid` is not found, error 412 is
1225             returned.
1226              
1227             _
1228             },
1229             members => {
1230             summary => 'Fill initial members',
1231             },
1232             },
1233             };
1234             sub add_group {
1235 9     9 1 54964 _add_group_or_user('group', @_);
1236             }
1237              
1238             $SPEC{add_user} = {
1239             v => 1.1,
1240             summary => 'Add a new user',
1241             args => {
1242             %common_args,
1243             %write_args,
1244             user => {
1245             schema => 'unix::username*',
1246             req => 1,
1247             pos => 0,
1248             },
1249             group => {
1250             summary => 'Select primary group '.
1251             '(default is group with same name as user)',
1252             schema => 'unix::groupname*',
1253             description => <<'_',
1254              
1255             Normally, a user's primary group with group with the same name as user, which
1256             will be created if does not already exist. You can pick another group here,
1257             which must already exist (and in this case, the group with the same name as user
1258             will not be created).
1259              
1260             _
1261             },
1262             gid => {
1263             summary => 'Pick a specific GID when creating group',
1264             schema => 'int*',
1265             description => <<'_',
1266              
1267             Duplicate GID is allowed.
1268              
1269             _
1270             },
1271             min_gid => {
1272             summary => 'Pick a range for GID when creating group',
1273             schema => 'int*',
1274             },
1275             max_gid => {
1276             summary => 'Pick a range for GID when creating group',
1277             schema => 'int*',
1278             },
1279             uid => {
1280             summary => 'Pick a specific new UID',
1281             schema => 'int*',
1282             description => <<'_',
1283              
1284             Adding a new user with duplicate UID is allowed.
1285              
1286             _
1287             },
1288             min_uid => {
1289             summary => 'Pick a range for new UID',
1290             schema => [int => {between=>[0,65535], default=>1000}],
1291             description => <<'_',
1292              
1293             If a free UID between `min_uid` and `max_uid` is not found, error 412 is
1294             returned.
1295              
1296             _
1297             },
1298             max_uid => {
1299             summary => 'Pick a range for new UID',
1300             schema => [int => {between=>[0,65535], default=>65535}],
1301             description => <<'_',
1302              
1303             If a free UID between `min_uid` and `max_uid` is not found, error 412 is
1304             returned.
1305              
1306             _
1307             },
1308             map( {($_=>$passwd_fields{$_})} qw/pass gecos home shell/),
1309             map( {($_=>$shadow_fields{$_})}
1310             qw/encpass last_pwchange min_pass_age max_pass_age
1311             pass_warn_period pass_inactive_period expire_date/),
1312             },
1313             };
1314             sub add_user {
1315 18     18 1 107800 _add_group_or_user('user', @_);
1316             }
1317              
1318             sub _modify_group_or_user {
1319 35     35   173 my ($which, %args) = @_;
1320              
1321             # TMP,schema
1322 35         63 my ($user, $gn);
1323 35 100       111 if ($which eq 'user') {
1324 19 100       68 $user = $args{user} or return [400, "Please specify user"];
1325             } else {
1326 16 100       62 $gn = $args{group} or return [400, "Please specify group"];
1327             }
1328              
1329 31 100       84 if ($which eq 'user') {
1330 18 100 100     69 if (defined($args{uid}) && $args{uid} !~ /$re_posint/o) {
1331 1         5 return [400, "Invalid uid"] }
1332 17 100 100     63 if (defined($args{gid}) && $args{gid} !~ /$re_posint/o) {
1333 1         10 return [400, "Invalid gid"] }
1334 16 100 100     58 if (defined($args{gecos}) && $args{gecos} !~ /$re_field/o) {
1335 1         8 return [400, "Invalid gecos"] }
1336 15 100 100     48 if (defined($args{home}) && $args{home} !~ /$re_field/o) {
1337 1         6 return [400, "Invalid home"] }
1338 14 100 100     47 if (defined($args{shell}) && $args{shell} !~ /$re_field/o) {
1339 1         6 return [400, "Invalid shell"] }
1340 13 100       29 if (defined $args{pass}) {
1341 2 50       10 $args{encpass} = $args{pass} eq '' ? '*' : _enc_pass($args{pass});
1342 2         352291061 $args{pass} = "x";
1343             }
1344 13 100 100     86 if (defined($args{encpass}) && $args{encpass} !~ /$re_field/o) {
1345 2         11 return [400, "Invalid encpass"] }
1346 11 100 100     53 if (defined($args{last_pwchange}) && $args{last_pwchange} !~ /$re_posint/o) {
1347 1         6 return [400, "Invalid last_pwchange"] }
1348 10 100 100     49 if (defined($args{min_pass_age}) && $args{min_pass_age} !~ /$re_posint/o) {
1349 1         8 return [400, "Invalid min_pass_age"] }
1350 9 100 100     45 if (defined($args{max_pass_age}) && $args{max_pass_age} !~ /$re_posint/o) {
1351 1         7 return [400, "Invalid max_pass_age"] }
1352 8 100 100     38 if (defined($args{pass_warn_period}) && $args{pass_warn_period} !~ /$re_posint/o) {
1353 1         7 return [400, "Invalid pass_warn_period"] }
1354 7 100 100     32 if (defined($args{pass_inactive_period}) &&
1355             $args{pass_inactive_period} !~ /$re_posint/o) {
1356 1         6 return [400, "Invalid pass_inactive_period"] }
1357 6 100 100     33 if (defined($args{expire_date}) && $args{expire_date} !~ /$re_posint/o) {
1358 1         7 return [400, "Invalid expire_date"] }
1359             }
1360              
1361 18         40 my ($gid, $members);
1362 18 100       53 if ($which eq 'group') {
1363 13 100 100     65 if (defined($args{gid}) && $args{gid} !~ /$re_posint/o) {
1364 1         6 return [400, "Invalid gid"] }
1365 12 50       33 if (defined $args{pass}) {
1366 0 0       0 $args{encpass} = $args{pass} eq '' ? '*' : _enc_pass($args{pass});
1367 0         0 $args{pass} = "x";
1368             }
1369 12 100 100     51 if (defined($args{encpass}) && $args{encpass} !~ /$re_field/o) {
1370 1         6 return [400, "Invalid encpass"] }
1371 11 100       32 if (defined $args{members}) {
1372 2 50       7 if (ref($args{members}) eq 'ARRAY') { $args{members} = join(",",@{$args{members}}) }
  0         0  
  0         0  
1373 2 100       16 $args{members} =~ /$re_field/o or return [400, "Invalid members"];
1374             }
1375 10 100       27 if (defined $args{admins}) {
1376 2 50       7 if (ref($args{admins}) eq 'ARRAY') { $args{admins} = join(",",@{$args{admins}}) }
  0         0  
  0         0  
1377 2 100       15 $args{admins} =~ /$re_field/o or return [400, "Invalid admins"];
1378             }
1379             }
1380              
1381             _routine(
1382             %args,
1383             _lock => 1,
1384             _write_group => $which eq 'group',
1385             _write_gshadow => $which eq 'group',
1386             _write_passwd => $which eq 'user',
1387             _write_shadow => $which eq 'user',
1388             _after_read => sub {
1389 14     14   31 my $stash = shift;
1390              
1391 14         30 my ($found, $changed);
1392 14 100       45 if ($which eq 'user') {
1393 5         13 my $passwd = $stash->{passwd};
1394 5         32 for my $l (@$passwd) {
1395 22 100       52 next unless $l->[0] eq $user;
1396 3         9 $found++;
1397 3         9 for my $f (qw/pass uid gid gecos home shell/) {
1398 18 100       49 if (defined $args{$f}) {
1399 6         65 my $idx = firstidx {$_ eq $f} @passwd_field_names;
  27         44  
1400 6         24 $l->[$idx] = $args{$f};
1401 6         13 $changed++;
1402             }
1403             }
1404 3         7 last;
1405             }
1406 5 100       23 return [404, "Not found"] unless $found;
1407 3 100       19 $stash->{write_passwd} = 0 unless $changed;
1408              
1409 3         6 $changed = 0;
1410 3         15 my $shadow = $stash->{shadow};
1411 3         8 for my $l (@$shadow) {
1412 12 100       29 next unless $l->[0] eq $user;
1413 3         7 for my $f (qw/encpass last_pwchange min_pass_age max_pass_age
1414             pass_warn_period pass_inactive_period expire_date/) {
1415 21 100       46 if (defined $args{$f}) {
1416 8         25 my $idx = firstidx {$_ eq $f} @shadow_field_names;
  37         50  
1417 8         20 $l->[$idx] = $args{$f};
1418 8         15 $changed++;
1419             }
1420             }
1421 3         5 last;
1422             }
1423 3 100       11 $stash->{write_shadow} = 0 unless $changed;
1424             } else {
1425 9         88 my $group = $stash->{group};
1426 9         27 for my $l (@$group) {
1427 52 100       140 next unless $l->[0] eq $gn;
1428 6         16 $found++;
1429 6         14 for my $f (qw/pass gid members/) {
1430 18 100       42 if ($args{_before_set_group_field}) {
1431 12         24 $args{_before_set_group_field}->($l, $f, \%args);
1432             }
1433 18 100       51 if (defined $args{$f}) {
1434 6         82 my $idx = firstidx {$_ eq $f} @group_field_names;
  23         44  
1435 6         50 $l->[$idx] = $args{$f};
1436 6         15 $changed++;
1437             }
1438             }
1439 6         20 last;
1440             }
1441 9 100       33 return [404, "Not found"] unless $found;
1442 6 100       15 $stash->{write_group} = 0 unless $changed;
1443              
1444 6         10 $changed = 0;
1445 6         13 my $gshadow = $stash->{gshadow};
1446 6         12 for my $l (@$gshadow) {
1447 34 100       68 next unless $l->[0] eq $gn;
1448 6         11 for my $f (qw/encpass admins members/) {
1449 18 100       50 if (defined $args{$f}) {
1450 7         28 my $idx = firstidx {$_ eq $f} @gshadow_field_names;
  25         38  
1451 7         21 $l->[$idx] = $args{$f};
1452 7         13 $changed++;
1453             }
1454             }
1455 6         17 last;
1456             }
1457 6 100       33 $stash->{write_gshadow} = 0 unless $changed;
1458             }
1459 9         30 $stash->{res} = [200, "OK"];
1460 9         31 [200];
1461             },
1462 14         187 );
1463             }
1464              
1465             $SPEC{modify_group} = {
1466             v => 1.1,
1467             summary => 'Modify an existing group',
1468             description => <<'_',
1469              
1470             Specify arguments to modify corresponding fields. Unspecified fields will not be
1471             modified.
1472              
1473             _
1474             args => {
1475             %common_args,
1476             %write_args,
1477             _arg_from_field(\%group_fields, 'group', req=>1, pos=>0),
1478             _arg_from_field(\%group_fields, 'pass'),
1479             _arg_from_field(\%group_fields, 'gid'),
1480             _arg_from_field(\%group_fields, 'members'),
1481              
1482             _arg_from_field(\%gshadow_fields, 'encpass'),
1483             _arg_from_field(\%gshadow_fields, 'admins'),
1484             },
1485             };
1486             sub modify_group {
1487 8     8 1 49624 _modify_group_or_user('group', @_);
1488             }
1489              
1490             $SPEC{modify_user} = {
1491             v => 1.1,
1492             summary => 'Modify an existing user',
1493             description => <<'_',
1494              
1495             Specify arguments to modify corresponding fields. Unspecified fields will not be
1496             modified.
1497              
1498             _
1499             args => {
1500             %common_args,
1501             %write_args,
1502             _arg_from_field(\%passwd_fields, 'user', req=>1, pos=>0),
1503             _arg_from_field(\%passwd_fields, 'uid'),
1504             _arg_from_field(\%passwd_fields, 'gid'),
1505             _arg_from_field(\%passwd_fields, 'gecos'),
1506             _arg_from_field(\%passwd_fields, 'home'),
1507             _arg_from_field(\%passwd_fields, 'shell'),
1508              
1509             _arg_from_field(\%shadow_fields, 'encpass'),
1510             _arg_from_field(\%shadow_fields, 'last_pwchange'),
1511             _arg_from_field(\%shadow_fields, 'min_pass_age'),
1512             _arg_from_field(\%shadow_fields, 'max_pass_age'),
1513             _arg_from_field(\%shadow_fields, 'pass_warn_period'),
1514             _arg_from_field(\%shadow_fields, 'pass_inactive_period'),
1515             _arg_from_field(\%shadow_fields, 'expire_date'),
1516             },
1517             };
1518             sub modify_user {
1519 19     19 1 101734 _modify_group_or_user('user', @_);
1520             }
1521              
1522             $SPEC{add_user_to_group} = {
1523             v => 1.1,
1524             summary => 'Add user to a group',
1525             args => {
1526             %common_args,
1527             user => {
1528             schema => 'unix::username*',
1529             req => 1,
1530             pos => 0,
1531             },
1532             group => {
1533             schema => 'unix::groupname*',
1534             req => 1,
1535             pos => 1,
1536             },
1537             },
1538             };
1539             sub add_user_to_group {
1540 6     6 1 35550 my %args = @_;
1541 6 100       34 my $user = $args{user} or return [400, "Please specify user"];
1542 4 50       28 $user =~ /$re_user/o or return [400, "Invalid user"];
1543 4         11 my $gn = $args{group}; # will be required by modify_group
1544              
1545             # XXX check user exists
1546             _modify_group_or_user(
1547             'group',
1548             %args,
1549             _before_set_group_field => sub {
1550 6     6   11 my ($l, $f, $args) = @_;
1551 6 50       13 return unless $l->[0] eq $gn;
1552 6         13 my @mm = split /,/, $l->[3];
1553 6 50       17 return if $user ~~ @mm;
1554 6         13 push @mm, $user;
1555 6         20 $args->{members} = join(",", @mm);
1556             },
1557 4         41 );
1558             }
1559              
1560              
1561             $SPEC{delete_user_from_group} = {
1562             v => 1.1,
1563             summary => 'Delete user from a group',
1564             args => {
1565             %common_args,
1566             user => {
1567             schema => 'unix::username*',
1568             req => 1,
1569             pos => 0,
1570             },
1571             group => {
1572             schema => 'unix::groupname*',
1573             req => 1,
1574             pos => 1,
1575             },
1576             },
1577             };
1578             sub delete_user_from_group {
1579 6     6 1 35232 my %args = @_;
1580 6 100       32 my $user = $args{user} or return [400, "Please specify user"];
1581 4 50       41 $user =~ /$re_user/o or return [400, "Invalid user"];
1582 4         10 my $gn = $args{group}; # will be required by modify_group
1583              
1584             # XXX check user exists
1585             _modify_group_or_user(
1586             'group',
1587             %args,
1588             _before_set_group_field => sub {
1589 6     6   10 my ($l, $f, $args) = @_;
1590 6 50       23 return unless $l->[0] eq $gn;
1591 6         24 my @mm = split /,/, $l->[3];
1592 6 50       19 return unless $user ~~ @mm;
1593 6         11 @mm = grep {$_ ne $user} @mm;
  12         29  
1594 6         18 $args->{members} = join(",", @mm);
1595             },
1596 4         37 );
1597             }
1598              
1599             $SPEC{add_delete_user_groups} = {
1600             v => 1.1,
1601             summary => 'Add or delete user from one or several groups',
1602             description => <<'_',
1603              
1604             This can be used to reduce several `add_user_to_group()` and/or
1605             `delete_user_from_group()` calls to a single call. So:
1606              
1607             add_delete_user_groups(user=>'u',add_to=>['a','b'],delete_from=>['c','d']);
1608              
1609             is equivalent to:
1610              
1611             add_user_to_group (user=>'u', group=>'a');
1612             add_user_to_group (user=>'u', group=>'b');
1613             delete_user_from_group(user=>'u', group=>'c');
1614             delete_user_from_group(user=>'u', group=>'d');
1615              
1616             except that `add_delete_user_groups()` does it in one pass.
1617              
1618             _
1619             args => {
1620             %common_args,
1621             user => {
1622             schema => 'unix::username*',
1623             req => 1,
1624             pos => 0,
1625             },
1626             add_to => {
1627             summary => 'List of group names to add the user as member of',
1628             schema => [array => {of=>'unix::groupname*', default=>[]}],
1629             },
1630             delete_from => {
1631             summary => 'List of group names to remove the user as member of',
1632             schema => [array => {of=>'unix::groupname*', default=>[]}],
1633             },
1634             },
1635             };
1636             sub add_delete_user_groups {
1637 2     2 1 12414 my %args = @_;
1638 2 50       10 my $user = $args{user} or return [400, "Please specify user"];
1639 2 50       17 $user =~ /$re_user/o or return [400, "Invalid user"];
1640 2   50     8 my $add = $args{add_to} // [];
1641 2   50     6 my $del = $args{delete_from} // [];
1642              
1643             # XXX check user exists
1644              
1645             _routine(
1646             %args,
1647             _lock => 1,
1648             _write_group => 1,
1649             _after_read => sub {
1650 2     2   5 my $stash = shift;
1651              
1652 2         3 my $group = $stash->{group};
1653 2         4 my $changed;
1654              
1655 2         6 for my $l (@$group) {
1656 12         26 my @mm = split /,/, $l->[-1];
1657 12 100 66     39 if ($l->[0] ~~ $add && !($user ~~ @mm)) {
1658 2         5 $changed++;
1659 2         3 push @mm, $user;
1660             }
1661 12 100 66     32 if ($l->[0] ~~ $del && $user ~~ @mm) {
1662 1         3 $changed++;
1663 1         3 @mm = grep {$_ ne $user} @mm;
  2         6  
1664             }
1665 12 100       27 if ($changed) {
1666 5         15 $l->[-1] = join ",", @mm;
1667             }
1668             }
1669 2 100       6 $stash->{write_group} = 0 unless $changed;
1670 2         5 $stash->{res} = [200, "OK"];
1671 2         6 [200];
1672             },
1673 2         19 );
1674             }
1675              
1676             $SPEC{set_user_groups} = {
1677             v => 1.1,
1678             summary => 'Set the groups that a user is member of',
1679             args => {
1680             %common_args,
1681             user => {
1682             schema => 'unix::username*',
1683             req => 1,
1684             pos => 0,
1685             },
1686             groups => {
1687             summary => 'List of group names that user is member of',
1688             schema => [array => {of=>'unix::groupname*', default=>[]}],
1689             req => 1,
1690             pos => 1,
1691             greedy => 1,
1692             description => <<'_',
1693              
1694             Aside from this list, user will not belong to any other group.
1695              
1696             _
1697             },
1698             },
1699             };
1700             sub set_user_groups {
1701 1     1 1 5258 my %args = @_;
1702 1 50       7 my $user = $args{user} or return [400, "Please specify user"];
1703 1 50       9 $user =~ /$re_user/o or return [400, "Invalid user"];
1704 1 50       5 my $gg = $args{groups} or return [400, "Please specify groups"];
1705              
1706             # XXX check user exists
1707              
1708             _routine(
1709             %args,
1710             _lock => 1,
1711             _write_group => 1,
1712             _after_read => sub {
1713 1     1   4 my $stash = shift;
1714              
1715 1         2 my $group = $stash->{group};
1716 1         2 my $changed;
1717              
1718 1         3 for my $l (@$group) {
1719 6         15 my @mm = split /,/, $l->[-1];
1720 6 100 100     33 if ($l->[0] ~~ $gg && !($user ~~ @mm)) {
1721 2         5 $changed++;
1722 2         4 push @mm, $user;
1723             }
1724 6 100 100     34 if (!($l->[0] ~~ $gg) && $user ~~ @mm) {
1725 1         2 $changed++;
1726 1         2 @mm = grep {$_ ne $user} @mm;
  2         7  
1727             }
1728 6 100       13 if ($changed) {
1729 5         13 $l->[-1] = join ",", @mm;
1730             }
1731             }
1732 1 50       4 $stash->{write_group} = 0 unless $changed;
1733 1         4 $stash->{res} = [200, "OK"];
1734 1         3 [200];
1735             },
1736 1         11 );
1737             }
1738              
1739             $SPEC{set_user_password} = {
1740             v => 1.1,
1741             summary => 'Set user\'s password',
1742             args => {
1743             %common_args,
1744             %write_args,
1745             user => {
1746             schema => 'unix::username*',
1747             req => 1,
1748             pos => 0,
1749             },
1750             pass => {
1751             schema => 'str*',
1752             req => 1,
1753             pos => 1,
1754             },
1755             },
1756             };
1757             sub set_user_password {
1758 3     3 1 19357 my %args = @_;
1759              
1760 3 50       14 $args{user} or return [400, "Please specify user"];
1761 3 100       12 defined($args{pass}) or return [400, "Please specify pass"];
1762 2         11 modify_user(%args);
1763             }
1764              
1765             sub _delete_group_or_user {
1766 5     5   27 my ($which, %args) = @_;
1767              
1768             # TMP,schema
1769 5         10 my ($user, $gn);
1770 5 100       20 if ($which eq 'user') {
1771 3 50       10 $user = $args{user} or return [400, "Please specify user"];
1772 3         5 $gn = $user;
1773             }
1774 5   66     21 $gn //= $args{group};
1775 5 50       13 $gn or return [400, "Please specify group"];
1776              
1777             _routine(
1778             %args,
1779             _lock => 1,
1780             _write_group => 1,
1781             _write_gshadow => 1,
1782             _write_passwd => $which eq 'user',
1783             _write_shadow => $which eq 'user',
1784             _after_read => sub {
1785 5     5   11 my $stash = shift;
1786 5         9 my ($i, $changed);
1787              
1788 5         12 my $group = $stash->{group};
1789 5         9 $changed = 0; $i = 0;
  5         7  
1790 5         15 while ($i < @$group) {
1791 34 100       55 if ($which eq 'user') {
1792             # also delete all mention of the user in any group
1793 20         39 my @mm = split /,/, $group->[$i][3];
1794 20 100       61 if ($user ~~ @mm) {
1795 4         5 $changed++;
1796 4         7 $group->[$i][3] = join(",", grep {$_ ne $user} @mm);
  5         18  
1797             }
1798             }
1799 34 100       84 if ($group->[$i][0] eq $gn) {
1800 5         9 $changed++;
1801 5         10 splice @$group, $i, 1; $i--;
  5         12  
1802             }
1803 34         62 $i++;
1804             }
1805 5 50       11 $stash->{write_group} = 0 unless $changed;
1806              
1807 5         11 my $gshadow = $stash->{gshadow};
1808 5         7 $changed = 0; $i = 0;
  5         31  
1809 5         15 while ($i < @$gshadow) {
1810 33 100       58 if ($which eq 'user') {
1811             # also delete all mention of the user in any group
1812 19         33 my @mm = split /,/, $gshadow->[$i][3];
1813 19 100       38 if ($user ~~ @mm) {
1814 2         2 $changed++;
1815 2         4 $gshadow->[$i][3] = join(",", grep {$_ ne $user} @mm);
  2         6  
1816             }
1817             }
1818 33 100       64 if ($gshadow->[$i][0] eq $gn) {
1819 5         9 $changed++;
1820 5         11 splice @$gshadow, $i, 1; $i--;
  5         8  
1821 5         17 last;
1822             }
1823 28         64 $i++;
1824             }
1825 5 50       13 $stash->{write_gshadow} = 0 unless $changed;
1826              
1827 5 100       12 if ($which eq 'user') {
1828 3         6 my $passwd = $stash->{passwd};
1829 3         4 $changed = 0; $i = 0;
  3         6  
1830 3         7 while ($i < @$passwd) {
1831 16 100       35 if ($passwd->[$i][0] eq $user) {
1832 3         5 $changed++;
1833 3         6 splice @$passwd, $i, 1; $i--;
  3         7  
1834 3         6 last;
1835             }
1836 13         25 $i++;
1837             }
1838 3 50       7 $stash->{write_passwd} = 0 unless $changed;
1839              
1840 3         5 my $shadow = $stash->{shadow};
1841 3         6 $changed = 0; $i = 0;
  3         4  
1842 3         7 while ($i < @$shadow) {
1843 16 100       31 if ($shadow->[$i][0] eq $user) {
1844 3         11 $changed++;
1845 3         8 splice @$shadow, $i, 1; $i--;
  3         4  
1846 3         5 last;
1847             }
1848 13         21 $i++;
1849             }
1850 3 50       8 $stash->{write_shadow} = 0 unless $changed;
1851             }
1852              
1853 5         15 $stash->{res} = [200, "OK"];
1854 5         14 [200];
1855             },
1856 5         51 );
1857             }
1858              
1859             $SPEC{delete_group} = {
1860             v => 1.1,
1861             summary => 'Delete a group',
1862             args => {
1863             %common_args,
1864             %write_args,
1865             group => {
1866             schema => 'unix::username*',
1867             req => 1,
1868             pos => 0,
1869             },
1870             },
1871             };
1872             sub delete_group {
1873 2     2 1 11328 _delete_group_or_user('group', @_);
1874             }
1875              
1876             $SPEC{delete_user} = {
1877             v => 1.1,
1878             summary => 'Delete a user',
1879             args => {
1880             %common_args,
1881             %write_args,
1882             user => {
1883             schema => 'unix::username*',
1884             req => 1,
1885             pos => 0,
1886             },
1887             },
1888             };
1889             sub delete_user {
1890 3     3 1 23454 _delete_group_or_user('user', @_);
1891             }
1892              
1893             1;
1894             # ABSTRACT: Manipulate /etc/{passwd,shadow,group,gshadow} entries
1895              
1896             __END__