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             package Unix::Passwd::File;
2              
3             our $DATE = '2017-07-10'; # DATE
4             our $VERSION = '0.24'; # VERSION
5              
6 22     22   1523215 use 5.010001;
  22         113  
7 22     22   157 use strict;
  22         59  
  22         561  
8 22     22   128 use warnings;
  22         57  
  22         859  
9 22     22   12080 use experimental 'smartmatch';
  22         85347  
  22         163  
10             #use Log::ger;
11              
12 22     22   15143 use File::Flock::Retry;
  22         21326  
  22         950  
13 22     22   198 use List::Util qw(max first);
  22         62  
  22         2689  
14 22     22   13003 use List::MoreUtils qw(firstidx);
  22         216583  
  22         306  
15              
16             our @ISA = qw(Exporter);
17             our @EXPORT_OK = qw(
18             add_delete_user_groups
19             add_group
20             add_user
21             add_user_to_group
22             delete_group
23             delete_user
24             delete_user_from_group
25             get_group
26             get_max_gid
27             get_max_uid
28             get_user
29             get_user_groups
30             group_exists
31             is_member
32             list_groups
33             list_users
34             list_users_and_groups
35             modify_group
36             modify_user
37             set_user_groups
38             set_user_password
39             user_exists
40             );
41              
42             our %SPEC;
43              
44             $SPEC{':package'} = {
45             v => 1.1,
46             summary => 'Manipulate /etc/{passwd,shadow,group,gshadow} entries',
47             };
48              
49             my %common_args = (
50             etc_dir => {
51             summary => 'Specify location of passwd files',
52             schema => ['str*' => {default=>'/etc'}],
53             tags => ['common'],
54             },
55             );
56             my %write_args = (
57             backup => {
58             summary => 'Whether to backup when modifying files',
59             description => <<'_',
60              
61             Backup is written with `.bak` extension in the same directory. Unmodified file
62             will not be backed up. Previous backup will be overwritten.
63              
64             _
65             schema => ['bool' => {default=>0}],
66             },
67             );
68              
69             our $re_user = qr/\A[A-Za-z0-9._-]+\z/;
70             our $re_group = $re_user;
71             our $re_field = qr/\A[^\n:]*\z/;
72             our $re_posint = qr/\A[1-9][0-9]*\z/;
73              
74             our %passwd_fields = (
75             user => {
76             summary => 'User (login) name',
77             schema => ['str*' => {match => $re_user}],
78             pos => 0,
79             'x.schema.entity' => 'unix_user',
80             },
81             pass => {
82             summary => 'Password, generally should be "x" which means password is '.
83             'encrypted in shadow',
84             schema => ['str*' => {match => $re_field}],
85             pos => 1,
86             },
87             uid => {
88             summary => 'Numeric user ID',
89             schema => 'int*',
90             pos => 2,
91             'x.schema.entity' => 'unix_uid',
92             },
93             gid => {
94             summary => 'Numeric primary group ID for this user',
95             schema => 'int*',
96             pos => 3,
97             'x.schema.entity' => 'unix_gid',
98             },
99             gecos => {
100             summary => 'Usually, it contains the full username',
101             schema => ['str*' => {match => $re_field}],
102             pos => 4,
103             },
104             home => {
105             summary => 'User\'s home directory',
106             schema => ['str*' => {match => $re_field}],
107             pos => 5,
108             },
109             shell => {
110             summary => 'User\'s shell',
111             schema => ['str*' => {match=>qr/\A[^\n:]*\z/}],
112             pos => 6,
113             # XXX x.schema.entity => prog (or, filename + x filter)
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 => ['str*' => {match => $re_user}],
126             pos => 0,
127             'x.schema.entity' => 'unix_user',
128             },
129             encpass => {
130             summary => 'Encrypted password',
131             schema => ['str*' => {match => $re_field}],
132             pos => 1,
133             },
134             last_pwchange => {
135             summary => 'The date of the last password change, '.
136             'expressed as the number of days since Jan 1, 1970.',
137             schema => 'int',
138             pos => 2,
139             },
140             min_pass_age => {
141             summary => 'The number of days the user will have to wait before she '.
142             'will be allowed to change her password again',
143             schema => 'int',
144             pos => 3,
145             },
146             max_pass_age => {
147             summary => 'The number of days after which the user will have to '.
148             'change her password',
149             schema => 'int',
150             pos => 4,
151             },
152             pass_warn_period => {
153             summary => 'The number of days before a password is going to expire '.
154             '(see max_pass_age) during which the user should be warned',
155             schema => 'int',
156             pos => 5,
157             },
158             pass_inactive_period => {
159             summary => 'The number of days after a password has expired (see '.
160             'max_pass_age) during which the password should still be accepted '.
161             '(and user should update her password during the next login)',
162             schema => 'int',
163             pos => 6,
164             },
165             expire_date => {
166             summary => 'The date of expiration of the account, expressed as the '.
167             'number of days since Jan 1, 1970',
168             schema => 'int',
169             pos => 7,
170             },
171             reserved => {
172             summary => 'This field is reserved for future use',
173             schema => ['str*' => {match => $re_field}],
174             pos => 8,
175             }
176             );
177             our @shadow_field_names;
178             for (keys %shadow_fields) {
179             $shadow_field_names[$shadow_fields{$_}{pos}] = $_;
180             delete $shadow_fields{$_}{pos};
181             }
182              
183             our %group_fields = (
184             group => {
185             summary => 'Group name',
186             schema => ['str*' => {match => $re_group}],
187             pos => 0,
188             'x.schema.entity' => 'unix_group',
189             },
190             pass => {
191             summary => 'Password, generally should be "x" which means password is '.
192             'encrypted in gshadow',
193             schema => ['str*' => {match => $re_field}],
194             pos => 1,
195             },
196             gid => {
197             summary => 'Numeric group ID',
198             schema => 'int*',
199             pos => 2,
200             'x.schema.entity' => 'unix_gid',
201             },
202             members => {
203             summary => 'List of usernames that are members of this group, '.
204             'separated by commas',
205             schema => ['str*' => {match => $re_field}],
206             pos => 3,
207             },
208             );
209             our @group_field_names;
210             for (keys %group_fields) {
211             $group_field_names[$group_fields{$_}{pos}] = $_;
212             delete $group_fields{$_}{pos};
213             }
214              
215             our %gshadow_fields = (
216             group => {
217             summary => 'Group name',
218             schema => ['str*' => {match => $re_group}],
219             pos => 0,
220             'x.schema.entity' => 'unix_group',
221             },
222             encpass => {
223             summary => 'Encrypted password',
224             schema => ['str*' => {match=> $re_field}],
225             pos => 1,
226             },
227             admins => {
228             summary => 'It must be a comma-separated list of user names, or empty',
229             schema => ['str*' => {match => $re_field}],
230             pos => 2,
231             },
232             members => {
233             summary => 'List of usernames that are members of this group, '.
234             'separated by commas; You should use the same list of users as in '.
235             '/etc/group.',
236             schema => ['str*' => {match => $re_field}],
237             pos => 3,
238             },
239             );
240             our @gshadow_field_names;
241             for (keys %gshadow_fields) {
242             $gshadow_field_names[$gshadow_fields{$_}{pos}] = $_;
243             delete $gshadow_fields{$_}{pos};
244             }
245              
246             sub _arg_from_field {
247 418     418   1129 my ($fields, $name, %extra) = @_;
248 418         727 my %spec = %{ $fields->{$name} };
  418         1902  
249 418         1152 $spec{$_} = $extra{$_} for keys %extra;
250 418         2105 ($name => \%spec);
251             }
252              
253             sub _backup {
254 4     4   11 my ($fh, $path) = @_;
255 4 50       19 seek $fh, 0, 0 or return [500, "Can't seek: $!"];
256 4 50       280 open my($bak), ">", "$path.bak" or return [500, "Can't open $path.bak: $!"];
257 4         35 while (<$fh>) { print $bak $_ }
  22         103  
258 4 50       130 close $bak or return [500, "Can't write $path.bak: $!"];
259             # XXX set ctime & mtime of backup file?
260 4         27 [200];
261             }
262              
263             # all public functions in this module use the _routine(), which contains the
264             # basic flow, to avoid duplication of code. admittedly this makes _routine()
265             # quite convoluted, as it tries to accomodate all the functions' logic in a
266             # single routine. _routine() accepts these special arguments for flow control:
267             #
268             # - _read_shadow = 0*/1/2 (2 means optional, don't exit if fail)
269             # - _read_passwd = 0*/1
270             # - _read_gshadow = 0*/1/2 (2 means optional, don't exit if fail)
271             # - _read_group = 0*/1
272             # - _lock = 0*/1 (whether to lock)
273             # - _after_read = code (executed after reading all passwd/group files)
274             # - _after_read_passwd_entry = code (executed after reading a line in passwd)
275             # - _after_read_group_entry = code (executed after reading a line in group)
276             # - _write_shadow = 0*/1
277             # - _write_passwd = 0*/1
278             # - _write_gshadow = 0*/1
279             # - _write_group = 0*/1
280             #
281             # all the hooks are fed $stash, sort of like a bag or object containing all
282             # data. should return enveloped response. _routine() will return with response
283             # if response is non success. _routine() will also return immediately if
284             # $stash{exit} is set.
285             #
286             # to write, we open once but with mode '+<' instead of '<'. we read first then
287             # we seek back to beginning and write from in-memory data. if
288             # $stash{write_passwd} and so on is set to false, _routine() cancels the write
289             # (can be used e.g. when there is no change so no need to write).
290             #
291             # final result is in $stash{res} or non-success result returned by hook.
292             sub _routine {
293 95     95   704 my %args = @_;
294              
295 95   50     423 my $etc = $args{etc_dir} // "/etc";
296 95         242 my $detail = $args{detail};
297 95   100     454 my $wfn = $args{with_field_names} // 1;
298 95         224 my @locks;
299 95         335 my ($fhp, $fhs, $fhg, $fhgs);
300 95         0 my %stash;
301              
302 95         250 my $e = eval {
303              
304 95 100       340 if ($args{_lock}) {
305 38         142 for (qw/passwd shadow group gshadow/) {
306 152         14392 push @locks, File::Flock::Retry->lock("$etc/$_", {retries=>3});
307             }
308             }
309              
310             # read files
311              
312 95         3840 my @shadow;
313             my %shadow;
314 95         0 my @shadowh;
315 95         293 $stash{shadow} = \@shadow;
316 95         275 $stash{shadowh} = \@shadowh;
317 95 100 100     649 if ($args{_read_shadow} || $args{_write_shadow}) {
318 37 100       1579 unless (open $fhs, ($args{_write_shadow} ? "+":"")."<",
    100          
319             "$etc/shadow") {
320 2 50 33     24 if ($args{_read_shadow} == 2 && !$args{_write_shadow}) {
321 2         36 goto L1;
322             } else {
323 0         0 return [500, "Can't open $etc/shadow: $!"];
324             }
325             }
326 35         586 while (<$fhs>) {
327 178         394 chomp;
328 178 50       830 next unless /\S/; # skip empty line
329 178         1229 my @r = split /:/, $_, scalar(keys %shadow_fields);
330 178         439 push @shadow, \@r;
331 178         538 $shadow{$r[0]} = \@r;
332 178 100       527 if ($wfn) {
333 168         296 my %r;
334 168         1153 @r{@shadow_field_names} = @r;
335 168         967 push @shadowh, \%r;
336             }
337             }
338             }
339              
340             L1:
341 95         224 my @passwd;
342 95         178 my @passwdh;
343 95         261 $stash{passwd} = \@passwd;
344 95         247 $stash{passwdh} = \@passwdh;
345 95 100 100     558 if ($args{_read_passwd} || $args{_write_passwd}) {
346 47 100       1717 open $fhp, ($args{_write_passwd} ? "+":"")."<", "$etc/passwd"
    100          
347             or return [500, "Can't open $etc/passwd: $!"];
348 46         608 while (<$fhp>) {
349 255         542 chomp;
350 255 50       1072 next unless /\S/; # skip empty line
351 255         1591 my @r = split /:/, $_, scalar(keys %passwd_fields);
352 255         613 push @passwd, \@r;
353 255 100       685 if ($wfn) {
354 158         278 my %r;
355 157         1073 @r{@shadow_field_names} = @{ $shadow{$r[0]} }
356 158 100       455 if $shadow{$r[0]};
357 158         753 @r{@passwd_field_names} = @r;
358 158         407 push @passwdh, \%r;
359             }
360 255 100       1249 if ($args{_after_read_passwd_entry}) {
361 56         167 my $res = $args{_after_read_passwd_entry}->(\%stash);
362 56 50       173 return $res if $res->[0] != 200;
363 56 100       336 return if $stash{exit};
364             }
365             }
366             }
367              
368 85         326 my @gshadow;
369             my %gshadow;
370 85         0 my @gshadowh;
371 85         239 $stash{gshadow} = \@gshadow;
372 85         222 $stash{gshadowh} = \@gshadowh;
373 85 100 100     518 if ($args{_read_gshadow} || $args{_write_gshadow}) {
374 56 100       2053 unless (open $fhgs, ($args{_write_gshadow} ? "+":"")."<",
    100          
375             "$etc/gshadow") {
376 2 50 33     12 if ($args{_read_gshadow} == 2 && !$args{_write_gshadow}) {
377 2         19 goto L2;
378             } else {
379 0         0 return [500, "Can't open $etc/gshadow: $!"];
380             }
381             }
382 54         793 while (<$fhgs>) {
383 330         721 chomp;
384 330 50       1340 next unless /\S/; # skip empty line
385 330         1264 my @r = split /:/, $_, scalar(keys %gshadow_fields);
386 330         775 push @gshadow, \@r;
387 330         1169 $gshadow{$r[0]} = \@r;
388 330 100       860 if ($wfn) {
389 318         540 my %r;
390 318         1377 @r{@gshadow_field_names} = @r;
391 318         1499 push @gshadowh, \%r;
392             }
393             }
394             }
395              
396             L2:
397 85         203 my @group;
398 85         180 my @grouph;
399 85         237 $stash{group} = \@group;
400 85         272 $stash{grouph} = \@grouph;
401 85 100 100     491 if ($args{_read_group} || $args{_write_group}) {
402 70 100       2974 open $fhg, ($args{_write_group} ? "+":"")."<",
    100          
403             "$etc/group"
404             or return [500, "Can't open $etc/group: $!"];
405 69         1030 while (<$fhg>) {
406 465         988 chomp;
407 465 50       1762 next unless /\S/; # skip empty line
408 465         1726 my @r = split /:/, $_, scalar(keys %group_fields);
409 465         1065 push @group, \@r;
410 465 100       1164 if ($wfn) {
411 320         557 my %r;
412 301         1291 @r{@gshadow_field_names} = @{ $gshadow{$r[0]} }
413 320 100       876 if $gshadow{$r[0]};
414 320         1074 @r{@group_field_names} = @r;
415 320         792 push @grouph, \%r;
416             }
417 465 100       1995 if ($args{_after_read_group_entry}) {
418 106         284 my $res = $args{_after_read_group_entry}->(\%stash);
419 106 50       306 return $res if $res->[0] != 200;
420 106 100       530 return if $stash{exit};
421             }
422             }
423             }
424              
425 68 50       296 if ($args{_after_read}) {
426 68         294 my $res = $args{_after_read}->(\%stash);
427 68 100       381 return $res if $res->[0] != 200;
428 47 100       277 return if $stash{exit};
429             }
430              
431             # write files
432              
433 28 100 100     183 if ($args{_write_shadow} && ($stash{write_shadow}//1)) {
      100        
434 12 100       62 if ($args{backup}) {
435 1         9 my $res = _backup($fhs, "$etc/shadow");
436 1 50       5 return $res if $res->[0] != 200;
437             }
438 12 50       83 seek $fhs, 0, 0 or return [500, "Can't seek in $etc/shadow: $!"];
439 12         37 for (@shadow) {
440 66   50     163 print $fhs join(":", map {$_//""} @$_), "\n";
  594         1596  
441             }
442 12         511 truncate $fhs, tell($fhs);
443 12 50       140 close $fhs or return [500, "Can't close $etc/shadow: $!"];
444 12         262 chmod 0640, "$etc/shadow"; # check error?
445             }
446              
447 28 100 100     193 if ($args{_write_passwd} && ($stash{write_passwd}//1)) {
      100        
448 12 100       45 if ($args{backup}) {
449 1         7 my $res = _backup($fhp, "$etc/passwd");
450 1 50       8 return $res if $res->[0] != 200;
451             }
452 12 50       60 seek $fhp, 0, 0 or return [500, "Can't seek in $etc/passwd: $!"];
453 12         39 for (@passwd) {
454 66   50     140 print $fhp join(":", map {$_//""} @$_), "\n";
  462         1286  
455             }
456 12         289 truncate $fhp, tell($fhp);
457 12 50       108 close $fhp or return [500, "Can't close $etc/passwd: $!"];
458 12         228 chmod 0644, "$etc/passwd"; # check error?
459             }
460              
461 28 100 100     274 if ($args{_write_gshadow} && ($stash{write_gshadow}//1)) {
      100        
462 21 100       87 if ($args{backup}) {
463 1         7 my $res = _backup($fhgs, "$etc/gshadow");
464 1 50       9 return $res if $res->[0] != 200;
465             }
466 21 50       122 seek $fhgs, 0, 0 or return [500, "Can't seek in $etc/gshadow: $!"];
467 21         71 for (@gshadow) {
468 135   50     334 print $fhgs join(":", map {$_//""} @$_), "\n";
  540         1595  
469             }
470 21         704 truncate $fhgs, tell($fhgs);
471 21 50       215 close $fhgs or return [500, "Can't close $etc/gshadow: $!"];
472 21         420 chmod 0640, "$etc/gshadow"; # check error?
473             }
474              
475 28 100 100     255 if ($args{_write_group} && ($stash{write_group}//1)) {
      100        
476 23 100       104 if ($args{backup}) {
477 1         6 my $res = _backup($fhg, "$etc/group");
478 1 50       6 return $res if $res->[0] != 200;
479             }
480 23 50       122 seek $fhg, 0, 0 or return [500, "Can't seek in $etc/group: $!"];
481 23         74 for (@group) {
482 147   50     349 print $fhg join(":", map {$_//""} @$_), "\n";
  588         1871  
483             }
484 23         551 truncate $fhg, tell($fhg);
485 23 50       214 close $fhg or return [500, "Can't close $etc/group: $!"];
486 23         423 chmod 0644, "$etc/group"; # check error?
487             }
488              
489 28         248 [200, "OK"];
490             }; # eval
491 95 50       355 $e = [500, "Died: $@"] if $@;
492              
493             # release the locks
494 95         414 undef @locks;
495              
496 95 100 33     4508 $stash{res} //= $e if $e && $e->[0] != 200;
      100        
497 95 100 33     527 $stash{res} //= $e if $e && $e->[0] != 200;
      100        
498 95   50     336 $stash{res} //= [500, "BUG: res not set"];
499              
500 95         3343 $stash{res};
501             }
502              
503             $SPEC{list_users} = {
504             v => 1.1,
505             summary => 'List Unix users in passwd file',
506             args => {
507             %common_args,
508             detail => {
509             summary => 'If true, return all fields instead of just usernames',
510             schema => ['bool' => {default => 0}],
511             },
512             with_field_names => {
513             summary => 'If false, don\'t return hash for each entry',
514             schema => [bool => {default=>1}],
515             description => <<'_',
516              
517             By default, when `detail=>1`, a hashref is returned for each entry containing
518             field names and its values, e.g. `{user=>"titin", pass=>"x", uid=>500, ...}`.
519             With `with_field_names=>0`, an arrayref is returned instead: `["titin", "x",
520             500, ...]`.
521              
522             _
523             },
524             },
525             };
526             sub list_users {
527 4     4 1 16788 my %args = @_;
528 4         11 my $detail = $args{detail};
529 4 100 100     28 my $wfn = $args{with_field_names} // ($detail ? 1:0);
530              
531             _routine(
532             %args,
533             _read_passwd => 1,
534             _read_shadow => $detail ? 2:0,
535             with_field_names => $wfn,
536             _after_read => sub {
537 4     4   12 my $stash = shift;
538              
539 4         7 my @rows;
540 4         11 my $passwd = $stash->{passwd};
541 4         8 my $passwdh = $stash->{passwdh};
542              
543 4         17 for (my $i=0; $i < @$passwd; $i++) {
544 20 100       53 if (!$detail) {
    100          
545 10         35 push @rows, $passwd->[$i][0];
546             } elsif ($wfn) {
547 5         19 push @rows, $passwdh->[$i];
548             } else {
549 5         16 push @rows, $passwd->[$i];
550             }
551             }
552              
553 4         18 $stash->{res} = [200, "OK", \@rows];
554 4 100       41 $stash->{res}[3]{'table.fields'} = [\@passwd_field_names]
555             if $detail;
556 4         13 $stash->{exit}++;
557 4         16 [200];
558             },
559 4 100       38 );
560             }
561              
562             $SPEC{get_user} = {
563             v => 1.1,
564             summary => 'Get user details by username or uid',
565             description => <<'_',
566              
567             Either `user` OR `uid` must be specified.
568              
569             The function is not dissimilar to Unix's `getpwnam()` or `getpwuid()`.
570              
571             _
572             args_rels => {
573             'choose_one' => [qw/user uid/],
574             },
575             args => {
576             %common_args,
577             user => {
578             schema => 'str*',
579             'x.schema.entity' => 'unix_user',
580             },
581             uid => {
582             schema => 'int*',
583             'x.schema.entity' => 'unix_uid',
584             },
585             with_field_names => {
586             summary => 'If false, don\'t return hash',
587             schema => [bool => {default=>1}],
588             description => <<'_',
589              
590             By default, a hashref is returned containing field names and its values, e.g.
591             `{user=>"titin", pass=>"x", uid=>500, ...}`. With `with_field_names=>0`, an
592             arrayref is returned instead: `["titin", "x", 500, ...]`.
593              
594             _
595             },
596             },
597             };
598             sub get_user {
599 16     16 1 39282 my %args = @_;
600 16   50     137 my $wfn = $args{with_field_names} // 1;
601 16         49 my $user = $args{user};
602 16         45 my $uid = $args{uid};
603 16 100 75     122 return [400, "Please specify user OR uid"]
604             unless defined($user) xor defined($uid);
605              
606             _routine(
607             %args,
608             _read_passwd => 1,
609             _read_shadow => 2,
610             with_field_names => $wfn,
611             detail => 1,
612             _after_read_passwd_entry => sub {
613 56     56   107 my $stash = shift;
614              
615 56         101 my @rows;
616 56         113 my $passwd = $stash->{passwd};
617 56         112 my $passwdh = $stash->{passwdh};
618              
619 56 100 100     445 if (defined($user) && $passwd->[-1][0] eq $user ||
      100        
      100        
620             defined($uid) && $passwd->[-1][2] == $uid) {
621 9 50       77 $stash->{res} = [200,"OK", $wfn ? $passwdh->[-1]:$passwd->[-1]];
622 9         30 $stash->{exit}++;
623             }
624 56         174 [200];
625             },
626             _after_read => sub {
627 5     5   17 my $stash = shift;
628 5         20 [404, "Not found"];
629             },
630 15         197 );
631             }
632              
633             $SPEC{user_exists} = {
634             v => 1.1,
635             summary => 'Check whether user exists',
636             args_rels => {
637             choose_one => [qw/user uid/],
638             },
639             args => {
640             %common_args,
641             user => {
642             schema => 'str*',
643             'x.schema.entity' => 'unix_user',
644             },
645             uid => {
646             schema => 'int*',
647             'x.schema.entity' => 'unix_uid',
648             },
649             },
650             result_naked => 1,
651             result => {
652             schema => 'bool*',
653             },
654             };
655             sub user_exists {
656 2     2 1 163 my %args = @_;
657 2         15 my $res = get_user(%args);
658 2 100       32 if ($res->[0] == 404) { return 0 }
  1 50       12  
659 1         14 elsif ($res->[0] == 200) { return 1 }
660 0         0 else { return undef }
661             }
662              
663             $SPEC{list_groups} = {
664             v => 1.1,
665             summary => 'List Unix groups in group file',
666             args => {
667             %common_args,
668             detail => {
669             summary => 'If true, return all fields instead of just group names',
670             schema => ['bool' => {default => 0}],
671             },
672             with_field_names => {
673             summary => 'If false, don\'t return hash for each entry',
674             schema => [bool => {default=>1}],
675             description => <<'_',
676              
677             By default, when `detail=>1`, a hashref is returned for each entry containing
678             field names and its values, e.g. `{group=>"titin", pass=>"x", gid=>500, ...}`.
679             With `with_field_names=>0`, an arrayref is returned instead: `["titin", "x",
680             500, ...]`.
681              
682             _
683             },
684             },
685             };
686             sub list_groups {
687 5     5 1 14861 my %args = @_;
688 5         15 my $detail = $args{detail};
689 5 100 100     34 my $wfn = $args{with_field_names} // ($detail ? 1:0);
690              
691             _routine(
692             %args,
693             _read_group => 1,
694             _read_gshadow => $detail ? 2:0,
695             with_field_names => $wfn,
696             _after_read => sub {
697 5     5   11 my $stash = shift;
698              
699 5         8 my @rows;
700 5         13 my $group = $stash->{group};
701 5         10 my $grouph = $stash->{grouph};
702              
703 5         18 for (my $i=0; $i < @$group; $i++) {
704 30 100       62 if (!$detail) {
    100          
705 18         45 push @rows, $group->[$i][0];
706             } elsif ($wfn) {
707 6         15 push @rows, $grouph->[$i];
708             } else {
709 6         14 push @rows, $group->[$i];
710             }
711             }
712              
713 5         15 $stash->{res} = [200, "OK", \@rows];
714 5 100       19 $stash->{res}[3]{'table.fields'} = [\@group_field_names] if $detail;
715 5         11 $stash->{exit}++;
716 5         12 [200];
717             },
718 5 100       46 );
719             }
720              
721             $SPEC{get_group} = {
722             v => 1.1,
723             summary => 'Get group details by group name or gid',
724             description => <<'_',
725              
726             Either `group` OR `gid` must be specified.
727              
728             The function is not dissimilar to Unix's `getgrnam()` or `getgrgid()`.
729              
730             _
731             args_rels => {
732             choose_one => [qw/group gid/],
733             },
734             args => {
735             %common_args,
736             group => {
737             schema => 'str*',
738             'x.schema.entity' => 'unix_user',
739             },
740             gid => {
741             schema => 'int*',
742             'x.schema.entity' => 'unix_gid',
743             },
744             with_field_names => {
745             summary => 'If false, don\'t return hash',
746             schema => [bool => {default=>1}],
747             description => <<'_',
748              
749             By default, a hashref is returned containing field names and its values, e.g.
750             `{group=>"titin", pass=>"x", gid=>500, ...}`. With `with_field_names=>0`, an
751             arrayref is returned instead: `["titin", "x", 500, ...]`.
752              
753             _
754             },
755             },
756             };
757             sub get_group {
758 23     23 1 37316 my %args = @_;
759 23   50     156 my $wfn = $args{with_field_names} // 1;
760 23         59 my $gn = $args{group};
761 23         54 my $gid = $args{gid};
762 23 100 75     147 return [400, "Please specify group OR gid"]
763             unless defined($gn) xor defined($gid);
764              
765             _routine(
766             %args,
767             _read_group => 1,
768             _read_gshadow => 2,
769             with_field_names => $wfn,
770             detail => 1,
771             _after_read_group_entry => sub {
772 106     106   200 my $stash = shift;
773              
774 106         180 my @rows;
775 106         204 my $group = $stash->{group};
776 106         187 my $grouph = $stash->{grouph};
777              
778 106 100 100     768 if (defined($gn) && $group->[-1][0] eq $gn ||
      100        
      100        
779             defined($gid) && $group->[-1][2] == $gid) {
780 16 50       85 $stash->{res} = [200,"OK", $wfn ? $grouph->[-1]:$group->[-1]];
781 16         53 $stash->{exit}++;
782             }
783 106         304 [200];
784             },
785             _after_read => sub {
786 5     5   9 my $stash = shift;
787 5         13 [404, "Not found"];
788             },
789 22         213 );
790             }
791              
792             $SPEC{list_users_and_groups} = {
793             v => 1.1,
794             summary => 'List Unix users and groups in passwd/group files',
795             description => <<'_',
796              
797             This is basically `list_users()` and `list_groups()` combined, so you can get
798             both data in a single call. Data is returned in an array. Users list is in the
799             first element, groups list in the second.
800              
801             _
802             args => {
803             %common_args,
804             detail => {
805             summary => 'If true, return all fields instead of just names',
806             schema => ['bool' => {default => 0}],
807             },
808             with_field_names => {
809             summary => 'If false, don\'t return hash for each entry',
810             schema => [bool => {default=>1}],
811             },
812             },
813             };
814             sub list_users_and_groups {
815 4     4 1 20370 my %args = @_;
816 4         18 my $detail = $args{detail};
817 4 100 100     35 my $wfn = $args{with_field_names} // ($detail ? 1:0);
818              
819             _routine(
820             %args,
821             _read_passwd => 1,
822             _read_shadow => $detail ? 2:0,
823             _read_group => 1,
824             _read_gshadow => $detail ? 2:0,
825             with_field_names => $wfn,
826             _after_read => sub {
827 4     4   12 my $stash = shift;
828              
829 4         10 my @users;
830 4         12 my $passwd = $stash->{passwd};
831 4         10 my $passwdh = $stash->{passwdh};
832 4         18 for (my $i=0; $i < @$passwd; $i++) {
833 20 100       56 if (!$detail) {
    100          
834 10         27 push @users, $passwd->[$i][0];
835             } elsif ($wfn) {
836 5         18 push @users, $passwdh->[$i];
837             } else {
838 5         16 push @users, $passwd->[$i];
839             }
840             }
841              
842 4         10 my @groups;
843 4         10 my $group = $stash->{group};
844 4         9 my $grouph = $stash->{grouph};
845 4         18 for (my $i=0; $i < @$group; $i++) {
846 24 100       61 if (!$detail) {
    100          
847 12         33 push @groups, $group->[$i][0];
848             } elsif ($wfn) {
849 6         21 push @groups, $grouph->[$i];
850             } else {
851 6         59 push @groups, $group->[$i];
852             }
853             }
854              
855 4         18 $stash->{res} = [200, "OK", [\@users, \@groups]];
856              
857 4         14 $stash->{exit}++;
858 4         17 [200];
859             },
860 4 100       64 );
    100          
861             }
862              
863             $SPEC{group_exists} = {
864             v => 1.1,
865             summary => 'Check whether group exists',
866             args_rels => {
867             choose_one => [qw/group gid/],
868             },
869             args => {
870             %common_args,
871             group => {
872             schema => 'str*',
873             'x.schema.entity' => 'unix_group',
874             },
875             gid => {
876             schema => 'int*',
877             'x.schema.entity' => 'unix_gid',
878             },
879             },
880             result_naked => 1,
881             result => {
882             schema => 'bool',
883             },
884             };
885             sub group_exists {
886 2     2 1 84 my %args = @_;
887 2         8 my $res = get_group(%args);
888 2 100       17 if ($res->[0] == 404) { return 0 }
  1 50       6  
889 1         8 elsif ($res->[0] == 200) { return 1 }
890 0         0 else { return undef }
891             }
892              
893             $SPEC{get_user_groups} = {
894             v => 1.1,
895             summary => 'Return groups which the user belongs to',
896             args => {
897             %common_args,
898             user => {
899             schema => 'str*',
900             req => 1,
901             pos => 0,
902             'x.schema.entity' => 'unix_user',
903             },
904             detail => {
905             summary => 'If true, return all fields instead of just group names',
906             schema => ['bool' => {default => 0}],
907             },
908             with_field_names => {
909             summary => 'If false, don\'t return hash for each entry',
910             schema => [bool => {default=>1}],
911             description => <<'_',
912              
913             By default, when `detail=>1`, a hashref is returned for each entry containing
914             field names and its values, e.g. `{group=>"titin", pass=>"x", gid=>500, ...}`.
915             With `with_field_names=>0`, an arrayref is returned instead: `["titin", "x",
916             500, ...]`.
917              
918             _
919             },
920             },
921             };
922             # this is a routine to list groups, but filtered using a criteria. can be
923             # refactored into a common routine (along with list_groups) if needed, to reduce
924             # duplication.
925             sub get_user_groups {
926 5     5 1 8253 my %args = @_;
927 5 50       28 my $user = $args{user} or return [400, "Please specify user"];
928 5         15 my $detail = $args{detail};
929 5 50 33     40 my $wfn = $args{with_field_names} // ($detail ? 1:0);
930              
931             _routine(
932             %args,
933             _read_passwd => 1,
934             _read_group => 1,
935             _read_gshadow => $detail ? 2:0,
936             with_field_names => $wfn,
937             _after_read => sub {
938 5     5   17 my $stash = shift;
939              
940 5         13 my $passwd = $stash->{passwd};
941             return [404, "User not found"]
942 5 100       63 unless first {$_->[0] eq $user} @$passwd;
  21         101  
943              
944 4         43 my @rows;
945 4         17 my $group = $stash->{group};
946 4         13 my $grouph = $stash->{grouph};
947              
948 4         31 for (my $i=0; $i < @$group; $i++) {
949 24         148 my @mm = split /,/, $group->[$i][3];
950 24 100 66     187 next unless $user ~~ @mm || $group->[$i][0] eq $user;
951 10 50       32 if (!$detail) {
    0          
952 10         47 push @rows, $group->[$i][0];
953             } elsif ($wfn) {
954 0         0 push @rows, $grouph->[$i];
955             } else {
956 0         0 push @rows, $group->[$i];
957             }
958             }
959              
960 4         19 $stash->{res} = [200, "OK", \@rows];
961              
962 4         14 $stash->{exit}++;
963 4         20 [200];
964             },
965 5 50       54 );
966             }
967              
968             $SPEC{is_member} = {
969             v => 1.1,
970             summary => 'Check whether user is member of a group',
971             args => {
972             %common_args,
973             user => {
974             schema => 'str*',
975             req => 1,
976             pos => 0,
977             'x.schema.entity' => 'unix_user',
978             },
979             group => {
980             schema => 'str*',
981             req => 1,
982             pos => 1,
983             'x.schema.entity' => 'unix_group',
984             },
985             },
986             result_naked => 1,
987             result => {
988             schema => 'bool',
989             },
990             };
991             sub is_member {
992 6     6 1 178 my %args = @_;
993 6 100       28 my $user = $args{user} or return undef;
994 5 100       22 my $group = $args{group} or return undef;
995 4         14 my $res = get_group(etc_dir=>$args{etc_dir}, group=>$group);
996 4 100       41 return undef unless $res->[0] == 200;
997 3         12 my @mm = split /,/, $res->[2]{members};
998 3 100       31 return $user ~~ @mm ? 1:0;
999             }
1000              
1001             $SPEC{get_max_uid} = {
1002             v => 1.1,
1003             summary => 'Get maximum UID used',
1004             args => {
1005             %common_args,
1006             },
1007             };
1008             sub get_max_uid {
1009 1     1 1 1621 my %args = @_;
1010             _routine(
1011             %args,
1012             _read_passwd => 1,
1013             detail => 0,
1014             with_field_names => 0,
1015             _after_read => sub {
1016 1     1   4 my $stash = shift;
1017 1         4 my $passwd = $stash->{passwd};
1018             $stash->{res} = [200, "OK", max(
1019 1         5 map {$_->[2]} @$passwd
  42         121  
1020             )];
1021 1         7 $stash->{exit}++;
1022 1         5 [200];
1023             },
1024 1         13 );
1025             }
1026              
1027             $SPEC{get_max_gid} = {
1028             v => 1.1,
1029             summary => 'Get maximum GID used',
1030             args => {
1031             %common_args,
1032             },
1033             };
1034             sub get_max_gid {
1035 1     1 1 1383 require List::Util;
1036              
1037 1         4 my %args = @_;
1038             _routine(
1039             %args,
1040             _read_group => 1,
1041             detail => 0,
1042             with_field_names => 0,
1043             _after_read => sub {
1044 1     1   3 my $stash = shift;
1045 1         2 my $group = $stash->{group};
1046             $stash->{res} = [200, "OK", List::Util::max(
1047 1         3 map {$_->[2]} @$group
  73         164  
1048             )];
1049 1         6 $stash->{exit}++;
1050 1         3 [200];
1051             },
1052 1         7 );
1053             }
1054              
1055             sub _enc_pass {
1056 3     3   36 require Crypt::Password::Util;
1057 3         23 Crypt::Password::Util::crypt(shift);
1058             }
1059              
1060             sub _add_group_or_user {
1061 27     27   168 my ($which, %args) = @_;
1062              
1063             # TMP,schema
1064 27         75 my ($user, $gn);
1065 27         60 my $create_group = 1;
1066 27 100       111 if ($which eq 'user') {
1067 18 100       77 $user = $args{user} or return [400, "Please specify user"];
1068 17 100       118 $user =~ /$re_user/o
1069             or return [400, "Invalid user, please use $re_user"];
1070 16   66     93 $gn = $args{group} // $user;
1071 16 100       63 $create_group = 0 if $gn ne $user;
1072             }
1073 25   100     113 $gn //= $args{group};
1074 25 100       75 $gn or return [400, "Please specify group"];
1075 24 100       122 $gn =~ /$re_group/o
1076             or return [400, "Invalid group, please use $re_group"];
1077              
1078 23         60 my $gid = $args{gid};
1079 23 50 100     115 my $min_gid = $args{min_gid} // 1000; $min_gid = 0 if $min_gid<0;
  23         78  
1080 23 50 100     98 my $max_gid = $args{max_gid} // 65535; $max_gid = 65535 if $max_gid>65535;
  23         74  
1081 23         44 my $members;
1082 23 100       67 if ($which eq 'group') {
1083 7         17 $members = $args{members};
1084 7 50 66     27 if ($members && ref($members) eq 'ARRAY') {
1085 0         0 $members = join(",",@$members);
1086             }
1087 7   100     33 $members //= "";
1088 7 100       44 $members =~ /$re_field/o
1089             or return [400, "Invalid members, please use $re_field"];
1090             } else {
1091 16         42 $members = "$user";
1092             }
1093              
1094 22         100 my ($uid, $min_uid, $max_uid);
1095 22         0 my ($pass, $gecos, $home, $shell);
1096 22         0 my ($encpass, $last_pwchange, $min_pass_age, $max_pass_age,
1097             $pass_warn_period, $pass_inactive_period, $expire_date);
1098 22 100       71 if ($which eq 'user') {
1099 16         35 $uid = $args{uid};
1100 16 50 100     74 $min_uid = $args{min_uid} // 1000; $min_uid = 0 if $min_uid<0;
  16         49  
1101 16 50 100     69 $max_uid = $args{max_uid} // 65535; $max_uid = 65535 if $min_uid>65535;
  16         49  
1102              
1103 16   100     70 $pass = $args{pass} // "";
1104 16 50       85 if ($pass !~ /$re_field/o) { return [400, "Invalid pass"] }
  0         0  
1105              
1106 16   100     80 $gecos = $args{gecos} // "";
1107 16 100       72 if ($gecos !~ /$re_field/o) { return [400, "Invalid gecos"] }
  1         9  
1108              
1109 15   100     61 $home = $args{home} // "";
1110 15 100       58 if ($home !~ /$re_field/o) { return [400, "Invalid home"] }
  1         9  
1111              
1112 14   100     57 $shell = $args{shell} // "";
1113 14 100       60 if ($shell !~ /$re_field/o) { return [400, "Invalid shell"] }
  1         8  
1114              
1115 13 100 66     80 $encpass = $args{encpass} // ($pass eq '' ? '*' : _enc_pass($pass));
1116 13 100       1091779581 if ($encpass !~ /$re_field/o) { return [400, "Invalid encpass"] }
  1         8  
1117              
1118 12   66     81 $last_pwchange = int($args{last_pwchange} // time()/86400);
1119 12   50     55 $min_pass_age = int($args{min_pass_age} // 0);
1120 12   50     50 $max_pass_age = int($args{max_pass_age} // 99999);
1121 12   50     46 $pass_warn_period = int($args{max_pass_age} // 7);
1122 12   100     51 $pass_inactive_period = $args{pass_inactive_period} // "";
1123 12 100       47 if ($pass_inactive_period !~ /$re_field/o) {
1124 1         5 return [400, "Invalid pass_inactive_period"] }
1125 11   100     55 $expire_date = $args{expire_date} // "";
1126 11 100       52 if ($expire_date !~ /$re_field/o) {
1127 1         5 return [400, "Invalid expire_date"] }
1128             }
1129              
1130             _routine(
1131             %args,
1132             _lock => 1,
1133             _write_group => 1,
1134             _write_gshadow => 1,
1135             _write_passwd => $which eq 'user',
1136             _write_shadow => $which eq 'user',
1137             _after_read => sub {
1138 16     16   36 my $stash = shift;
1139              
1140 16         38 my $group = $stash->{group};
1141 16         29 my $gshadow = $stash->{gshadow};
1142 16         31 my $write_g;
1143 16         142 my $cur_g = first { $_->[0] eq $gn } @$group;
  89         161  
1144              
1145 16 100 100     126 if ($which eq 'group' && $cur_g) {
    100          
    100          
1146 1 50       9 return [412, "Group $gn already exists"] if $cur_g;
1147             } elsif ($cur_g) {
1148 2         6 $gid = $cur_g->[2];
1149             } elsif (!$create_group) {
1150 1         5 return [412, "Group $gn must already exist"];
1151             } else {
1152 12         30 my @gids = map { $_->[2] } @$group;
  72         154  
1153 12 100       42 if (!defined($gid)) {
1154 10         36 for ($min_gid .. $max_gid) {
1155 28 100       122 do { $gid = $_; last } unless $_ ~~ @gids;
  9         20  
  9         16  
1156             }
1157 10 100       33 return [412, "Can't find available GID"]
1158             unless defined($gid);
1159             }
1160 11         44 push @$group , [$gn, "x", $gid, $members];
1161 11         61 push @$gshadow, [$gn, "*", "", $members];
1162 11         34 $write_g++;
1163             }
1164 13         43 my $r = {gid=>$gid};
1165              
1166 13 100       50 if ($which eq 'user') {
1167 9         20 my $passwd = $stash->{passwd};
1168 9         19 my $shadow = $stash->{shadow};
1169             return [412, "User $gn already exists"]
1170 9 100       50 if first { $_->[0] eq $user } @$passwd;
  44         97  
1171 8         33 my @uids = map { $_->[2] } @$passwd;
  40         106  
1172 8 100       29 if (!defined($uid)) {
1173 6         20 for ($min_uid .. $max_uid) {
1174 15 100       56 do { $uid = $_; last } unless $_ ~~ @uids;
  5         13  
  5         10  
1175             }
1176 6 100       23 return [412, "Can't find available UID"]
1177             unless defined($uid);
1178             }
1179 7         21 $r->{uid} = $uid;
1180 7         38 push @$passwd, [$user, "x", $uid, $gid, $gecos, $home, $shell];
1181 7         33 push @$shadow, [$user, $encpass, $last_pwchange, $min_pass_age,
1182             $max_pass_age, $pass_warn_period,
1183             $pass_inactive_period, $expire_date, ""];
1184              
1185             # add user as member of group
1186 7         22 for my $l (@$group) {
1187 46 100       115 next unless $l->[0] eq $gn;
1188 7         27 my @mm = split /,/, $l->[3];
1189 7 100       39 unless ($user ~~ @mm) {
1190 1         3 $l->[3] = join(",", @mm, $user);
1191 1         3 $write_g++;
1192 1         3 last;
1193             }
1194             }
1195             }
1196              
1197 11 50       28 $stash->{write_group} = $stash->{write_gshadow} = 0 unless $write_g;
1198 11         33 $stash->{res} = [200, "OK", $r];
1199 11         29 [200];
1200             },
1201 16         225 );
1202             }
1203              
1204             $SPEC{add_group} = {
1205             v => 1.1,
1206             summary => 'Add a new group',
1207             args => {
1208             %common_args,
1209             %write_args,
1210             group => {
1211             schema => 'str*',
1212             req => 1,
1213             pos => 0,
1214             #'x.schema.entity' => 'unix_group', # XXX new
1215             },
1216             gid => {
1217             summary => 'Pick a specific new GID',
1218             schema => 'int*',
1219             description => <<'_',
1220              
1221             Adding a new group with duplicate GID is allowed.
1222              
1223             _
1224             #'x.schema.entity' => 'unix_gid', # XXX new
1225             },
1226             min_gid => {
1227             summary => 'Pick a range for new GID',
1228             schema => [int => {between=>[0, 65535], default=>1000}],
1229             description => <<'_',
1230              
1231             If a free GID between `min_gid` and `max_gid` is not found, error 412 is
1232             returned.
1233              
1234             _
1235             },
1236             max_gid => {
1237             summary => 'Pick a range for new GID',
1238             schema => [int => {between=>[0, 65535], default=>65535}],
1239             description => <<'_',
1240              
1241             If a free GID between `min_gid` and `max_gid` is not found, error 412 is
1242             returned.
1243              
1244             _
1245             },
1246             members => {
1247             summary => 'Fill initial members',
1248             },
1249             },
1250             };
1251             sub add_group {
1252 9     9 1 54139 _add_group_or_user('group', @_);
1253             }
1254              
1255             $SPEC{add_user} = {
1256             v => 1.1,
1257             summary => 'Add a new user',
1258             args => {
1259             %common_args,
1260             %write_args,
1261             user => {
1262             schema => 'str*',
1263             req => 1,
1264             pos => 0,
1265             #'x.schema.entity' => 'unix_user', # XXX new
1266             },
1267             group => {
1268             summary => 'Select primary group '.
1269             '(default is group with same name as user)',
1270             schema => 'str*',
1271             description => <<'_',
1272              
1273             Normally, a user's primary group with group with the same name as user, which
1274             will be created if does not already exist. You can pick another group here,
1275             which must already exist (and in this case, the group with the same name as user
1276             will not be created).
1277              
1278             _
1279             'x.schema.entity' => 'unix_group',
1280             },
1281             gid => {
1282             summary => 'Pick a specific GID when creating group',
1283             schema => 'int*',
1284             description => <<'_',
1285              
1286             Duplicate GID is allowed.
1287              
1288             _
1289             },
1290             min_gid => {
1291             summary => 'Pick a range for GID when creating group',
1292             schema => 'int*',
1293             },
1294             max_gid => {
1295             summary => 'Pick a range for GID when creating group',
1296             schema => 'int*',
1297             },
1298             uid => {
1299             summary => 'Pick a specific new UID',
1300             schema => 'int*',
1301             description => <<'_',
1302              
1303             Adding a new user with duplicate UID is allowed.
1304              
1305             _
1306             },
1307             min_uid => {
1308             summary => 'Pick a range for new UID',
1309             schema => [int => {between=>[0,65535], default=>1000}],
1310             description => <<'_',
1311              
1312             If a free UID between `min_uid` and `max_uid` is not found, error 412 is
1313             returned.
1314              
1315             _
1316             },
1317             max_uid => {
1318             summary => 'Pick a range for new UID',
1319             schema => [int => {between=>[0,65535], default=>65535}],
1320             description => <<'_',
1321              
1322             If a free UID between `min_uid` and `max_uid` is not found, error 412 is
1323             returned.
1324              
1325             _
1326             },
1327             map( {($_=>$passwd_fields{$_})} qw/pass gecos home shell/),
1328             map( {($_=>$shadow_fields{$_})}
1329             qw/encpass last_pwchange min_pass_age max_pass_age
1330             pass_warn_period pass_inactive_period expire_date/),
1331             },
1332             };
1333             sub add_user {
1334 18     18 1 129996 _add_group_or_user('user', @_);
1335             }
1336              
1337             sub _modify_group_or_user {
1338 35     35   312 my ($which, %args) = @_;
1339              
1340             # TMP,schema
1341 35         133 my ($user, $gn);
1342 35 100       186 if ($which eq 'user') {
1343 19 100       125 $user = $args{user} or return [400, "Please specify user"];
1344             } else {
1345 16 100       126 $gn = $args{group} or return [400, "Please specify group"];
1346             }
1347              
1348 31 100       184 if ($which eq 'user') {
1349 18 100 100     140 if (defined($args{uid}) && $args{uid} !~ /$re_posint/o) {
1350 1         7 return [400, "Invalid uid"] }
1351 17 100 100     180 if (defined($args{gid}) && $args{gid} !~ /$re_posint/o) {
1352 1         7 return [400, "Invalid gid"] }
1353 16 100 100     124 if (defined($args{gecos}) && $args{gecos} !~ /$re_field/o) {
1354 1         10 return [400, "Invalid gecos"] }
1355 15 100 100     95 if (defined($args{home}) && $args{home} !~ /$re_field/o) {
1356 1         7 return [400, "Invalid home"] }
1357 14 100 100     123 if (defined($args{shell}) && $args{shell} !~ /$re_field/o) {
1358 1         6 return [400, "Invalid shell"] }
1359 13 100       60 if (defined $args{pass}) {
1360 2 50       14 $args{encpass} = $args{pass} eq '' ? '*' : _enc_pass($args{pass});
1361 2         189101827 $args{pass} = "x";
1362             }
1363 13 100 100     120 if (defined($args{encpass}) && $args{encpass} !~ /$re_field/o) {
1364 2         11 return [400, "Invalid encpass"] }
1365 11 100 100     68 if (defined($args{last_pwchange}) && $args{last_pwchange} !~ /$re_posint/o) {
1366 1         8 return [400, "Invalid last_pwchange"] }
1367 10 100 100     68 if (defined($args{min_pass_age}) && $args{min_pass_age} !~ /$re_posint/o) {
1368 1         7 return [400, "Invalid min_pass_age"] }
1369 9 100 100     67 if (defined($args{max_pass_age}) && $args{max_pass_age} !~ /$re_posint/o) {
1370 1         8 return [400, "Invalid max_pass_age"] }
1371 8 100 100     59 if (defined($args{pass_warn_period}) && $args{pass_warn_period} !~ /$re_posint/o) {
1372 1         8 return [400, "Invalid pass_warn_period"] }
1373 7 100 100     75 if (defined($args{pass_inactive_period}) &&
1374             $args{pass_inactive_period} !~ /$re_posint/o) {
1375 1         8 return [400, "Invalid pass_inactive_period"] }
1376 6 100 100     54 if (defined($args{expire_date}) && $args{expire_date} !~ /$re_posint/o) {
1377 1         7 return [400, "Invalid expire_date"] }
1378             }
1379              
1380 18         64 my ($gid, $members);
1381 18 100       110 if ($which eq 'group') {
1382 13 100 100     118 if (defined($args{gid}) && $args{gid} !~ /$re_posint/o) {
1383 1         8 return [400, "Invalid gid"] }
1384 12 50       59 if (defined $args{pass}) {
1385 0 0       0 $args{encpass} = $args{pass} eq '' ? '*' : _enc_pass($args{pass});
1386 0         0 $args{pass} = "x";
1387             }
1388 12 100 100     123 if (defined($args{encpass}) && $args{encpass} !~ /$re_field/o) {
1389 1         11 return [400, "Invalid encpass"] }
1390 11 100       66 if (defined $args{members}) {
1391 2 50       14 if (ref($args{members}) eq 'ARRAY') { $args{members} = join(",",@{$args{members}}) }
  0         0  
  0         0  
1392 2 100       24 $args{members} =~ /$re_field/o or return [400, "Invalid members"];
1393             }
1394 10 100       48 if (defined $args{admins}) {
1395 2 50       14 if (ref($args{admins}) eq 'ARRAY') { $args{admins} = join(",",@{$args{admins}}) }
  0         0  
  0         0  
1396 2 100       22 $args{admins} =~ /$re_field/o or return [400, "Invalid admins"];
1397             }
1398             }
1399              
1400             _routine(
1401             %args,
1402             _lock => 1,
1403             _write_group => $which eq 'group',
1404             _write_gshadow => $which eq 'group',
1405             _write_passwd => $which eq 'user',
1406             _write_shadow => $which eq 'user',
1407             _after_read => sub {
1408 14     14   44 my $stash = shift;
1409              
1410 14         40 my ($found, $changed);
1411 14 100       67 if ($which eq 'user') {
1412 5         16 my $passwd = $stash->{passwd};
1413 5         16 for my $l (@$passwd) {
1414 22 100       68 next unless $l->[0] eq $user;
1415 3         74 $found++;
1416 3         12 for my $f (qw/pass uid gid gecos home shell/) {
1417 18 100       64 if (defined $args{$f}) {
1418 6         35 my $idx = firstidx {$_ eq $f} @passwd_field_names;
  27         196  
1419 6         31 $l->[$idx] = $args{$f};
1420 6         12 $changed++;
1421             }
1422             }
1423 3         9 last;
1424             }
1425 5 100       31 return [404, "Not found"] unless $found;
1426 3 100       21 $stash->{write_passwd} = 0 unless $changed;
1427              
1428 3         40 $changed = 0;
1429 3         11 my $shadow = $stash->{shadow};
1430 3         11 for my $l (@$shadow) {
1431 12 100       40 next unless $l->[0] eq $user;
1432 3         12 for my $f (qw/encpass last_pwchange min_pass_age max_pass_age
1433             pass_warn_period pass_inactive_period expire_date/) {
1434 21 100       62 if (defined $args{$f}) {
1435 8         47 my $idx = firstidx {$_ eq $f} @shadow_field_names;
  37         279  
1436 8         46 $l->[$idx] = $args{$f};
1437 8         21 $changed++;
1438             }
1439             }
1440 3         6 last;
1441             }
1442 3 100       15 $stash->{write_shadow} = 0 unless $changed;
1443             } else {
1444 9         34 my $group = $stash->{group};
1445 9         40 for my $l (@$group) {
1446 52 100       177 next unless $l->[0] eq $gn;
1447 6         22 $found++;
1448 6         21 for my $f (qw/pass gid members/) {
1449 18 100       69 if ($args{_before_set_group_field}) {
1450 12         49 $args{_before_set_group_field}->($l, $f, \%args);
1451             }
1452 18 100       73 if (defined $args{$f}) {
1453 6         89 my $idx = firstidx {$_ eq $f} @group_field_names;
  23         347  
1454 6         52 $l->[$idx] = $args{$f};
1455 6         24 $changed++;
1456             }
1457             }
1458 6         58 last;
1459             }
1460 9 100       51 return [404, "Not found"] unless $found;
1461 6 100       48 $stash->{write_group} = 0 unless $changed;
1462              
1463 6         25 $changed = 0;
1464 6         25 my $gshadow = $stash->{gshadow};
1465 6         25 for my $l (@$gshadow) {
1466 34 100       126 next unless $l->[0] eq $gn;
1467 6         44 for my $f (qw/encpass admins members/) {
1468 18 100       65 if (defined $args{$f}) {
1469 7         81 my $idx = firstidx {$_ eq $f} @gshadow_field_names;
  25         214  
1470 7         41 $l->[$idx] = $args{$f};
1471 7         21 $changed++;
1472             }
1473             }
1474 6         19 last;
1475             }
1476 6 100       45 $stash->{write_gshadow} = 0 unless $changed;
1477             }
1478 9         71 $stash->{res} = [200, "OK"];
1479 9         36 [200];
1480             },
1481 14         292 );
1482             }
1483              
1484             $SPEC{modify_group} = {
1485             v => 1.1,
1486             summary => 'Modify an existing group',
1487             description => <<'_',
1488              
1489             Specify arguments to modify corresponding fields. Unspecified fields will not be
1490             modified.
1491              
1492             _
1493             args => {
1494             %common_args,
1495             %write_args,
1496             _arg_from_field(\%group_fields, 'group', req=>1, pos=>0),
1497             _arg_from_field(\%group_fields, 'pass'),
1498             _arg_from_field(\%group_fields, 'gid'),
1499             _arg_from_field(\%group_fields, 'members'),
1500              
1501             _arg_from_field(\%gshadow_fields, 'encpass'),
1502             _arg_from_field(\%gshadow_fields, 'admins'),
1503             },
1504             };
1505             sub modify_group {
1506 8     8 1 65393 _modify_group_or_user('group', @_);
1507             }
1508              
1509             $SPEC{modify_user} = {
1510             v => 1.1,
1511             summary => 'Modify an existing user',
1512             description => <<'_',
1513              
1514             Specify arguments to modify corresponding fields. Unspecified fields will not be
1515             modified.
1516              
1517             _
1518             args => {
1519             %common_args,
1520             %write_args,
1521             _arg_from_field(\%passwd_fields, 'user', req=>1, pos=>0),
1522             _arg_from_field(\%passwd_fields, 'uid'),
1523             _arg_from_field(\%passwd_fields, 'gid'),
1524             _arg_from_field(\%passwd_fields, 'gecos'),
1525             _arg_from_field(\%passwd_fields, 'home'),
1526             _arg_from_field(\%passwd_fields, 'shell'),
1527              
1528             _arg_from_field(\%shadow_fields, 'encpass'),
1529             _arg_from_field(\%shadow_fields, 'last_pwchange'),
1530             _arg_from_field(\%shadow_fields, 'min_pass_age'),
1531             _arg_from_field(\%shadow_fields, 'max_pass_age'),
1532             _arg_from_field(\%shadow_fields, 'pass_warn_period'),
1533             _arg_from_field(\%shadow_fields, 'pass_inactive_period'),
1534             _arg_from_field(\%shadow_fields, 'expire_date'),
1535             },
1536             };
1537             sub modify_user {
1538 19     19 1 123371 _modify_group_or_user('user', @_);
1539             }
1540              
1541             $SPEC{add_user_to_group} = {
1542             v => 1.1,
1543             summary => 'Add user to a group',
1544             args => {
1545             %common_args,
1546             user => {
1547             schema => 'str*',
1548             req => 1,
1549             pos => 0,
1550             'x.schema.entity' => 'unix_user',
1551             },
1552             group => {
1553             schema => 'str*',
1554             req => 1,
1555             pos => 1,
1556             'x.schema.entity' => 'unix_group',
1557             },
1558             },
1559             };
1560             sub add_user_to_group {
1561 6     6 1 45215 my %args = @_;
1562 6 100       57 my $user = $args{user} or return [400, "Please specify user"];
1563 4 50       42 $user =~ /$re_user/o or return [400, "Invalid user"];
1564 4         16 my $gn = $args{group}; # will be required by modify_group
1565              
1566             # XXX check user exists
1567             _modify_group_or_user(
1568             'group',
1569             %args,
1570             _before_set_group_field => sub {
1571 6     6   18 my ($l, $f, $args) = @_;
1572 6 50       22 return unless $l->[0] eq $gn;
1573 6         24 my @mm = split /,/, $l->[3];
1574 6 50       25 return if $user ~~ @mm;
1575 6         16 push @mm, $user;
1576 6         27 $args->{members} = join(",", @mm);
1577             },
1578 4         61 );
1579             }
1580              
1581              
1582             $SPEC{delete_user_from_group} = {
1583             v => 1.1,
1584             summary => 'Delete user from a group',
1585             args => {
1586             %common_args,
1587             user => {
1588             schema => 'str*',
1589             req => 1,
1590             pos => 0,
1591             'x.schema.entity' => 'unix_user',
1592             },
1593             group => {
1594             schema => 'str*',
1595             req => 1,
1596             pos => 1,
1597             'x.schema.entity' => 'unix_group',
1598             },
1599             },
1600             };
1601             sub delete_user_from_group {
1602 6     6 1 86289 my %args = @_;
1603 6 100       66 my $user = $args{user} or return [400, "Please specify user"];
1604 4 50       51 $user =~ /$re_user/o or return [400, "Invalid user"];
1605 4         20 my $gn = $args{group}; # will be required by modify_group
1606              
1607             # XXX check user exists
1608             _modify_group_or_user(
1609             'group',
1610             %args,
1611             _before_set_group_field => sub {
1612 6     6   36 my ($l, $f, $args) = @_;
1613 6 50       27 return unless $l->[0] eq $gn;
1614 6         32 my @mm = split /,/, $l->[3];
1615 6 50       38 return unless $user ~~ @mm;
1616 6         53 @mm = grep {$_ ne $user} @mm;
  12         57  
1617 6         31 $args->{members} = join(",", @mm);
1618             },
1619 4         66 );
1620             }
1621              
1622             $SPEC{add_delete_user_groups} = {
1623             v => 1.1,
1624             summary => 'Add or delete user from one or several groups',
1625             description => <<'_',
1626              
1627             This can be used to reduce several `add_user_to_group()` and/or
1628             `delete_user_from_group()` calls to a single call. So:
1629              
1630             add_delete_user_groups(user=>'u',add_to=>['a','b'],delete_from=>['c','d']);
1631              
1632             is equivalent to:
1633              
1634             add_user_to_group (user=>'u', group=>'a');
1635             add_user_to_group (user=>'u', group=>'b');
1636             delete_user_from_group(user=>'u', group=>'c');
1637             delete_user_from_group(user=>'u', group=>'d');
1638              
1639             except that `add_delete_user_groups()` does it in one pass.
1640              
1641             _
1642             args => {
1643             %common_args,
1644             user => {
1645             schema => 'str*',
1646             req => 1,
1647             pos => 0,
1648             'x.schema.entity' => 'unix_user',
1649             },
1650             add_to => {
1651             summary => 'List of group names to add the user as member of',
1652             schema => [array => {of=>'str*', default=>[]}],
1653             'x.schema.element_entity' => 'unix_group',
1654             },
1655             delete_from => {
1656             summary => 'List of group names to remove the user as member of',
1657             schema => [array => {of=>'str*', default=>[]}],
1658             'x.schema.element_entity' => 'unix_group',
1659             },
1660             },
1661             };
1662             sub add_delete_user_groups {
1663 2     2 1 12822 my %args = @_;
1664 2 50       13 my $user = $args{user} or return [400, "Please specify user"];
1665 2 50       19 $user =~ /$re_user/o or return [400, "Invalid user"];
1666 2   50     9 my $add = $args{add_to} // [];
1667 2   50     10 my $del = $args{delete_from} // [];
1668              
1669             # XXX check user exists
1670              
1671             _routine(
1672             %args,
1673             _lock => 1,
1674             _write_group => 1,
1675             _after_read => sub {
1676 2     2   4 my $stash = shift;
1677              
1678 2         5 my $group = $stash->{group};
1679 2         4 my $changed;
1680              
1681 2         7 for my $l (@$group) {
1682 12         28 my @mm = split /,/, $l->[-1];
1683 12 100 66     47 if ($l->[0] ~~ $add && !($user ~~ @mm)) {
1684 2         3 $changed++;
1685 2         4 push @mm, $user;
1686             }
1687 12 100 66     44 if ($l->[0] ~~ $del && $user ~~ @mm) {
1688 1         4 $changed++;
1689 1         4 @mm = grep {$_ ne $user} @mm;
  2         11  
1690             }
1691 12 100       32 if ($changed) {
1692 5         14 $l->[-1] = join ",", @mm;
1693             }
1694             }
1695 2 100       14 $stash->{write_group} = 0 unless $changed;
1696 2         9 $stash->{res} = [200, "OK"];
1697 2         9 [200];
1698             },
1699 2         24 );
1700             }
1701              
1702             $SPEC{set_user_groups} = {
1703             v => 1.1,
1704             summary => 'Set the groups that a user is member of',
1705             args => {
1706             %common_args,
1707             user => {
1708             schema => 'str*',
1709             req => 1,
1710             pos => 0,
1711             'x.schema.entity' => 'unix_user',
1712             },
1713             groups => {
1714             summary => 'List of group names that user is member of',
1715             schema => [array => {of=>'str*', default=>[]}],
1716             req => 1,
1717             pos => 1,
1718             greedy => 1,
1719             description => <<'_',
1720              
1721             Aside from this list, user will not belong to any other group.
1722              
1723             _
1724             'x.schema.element_entity' => 'unix_group',
1725             },
1726             },
1727             };
1728             sub set_user_groups {
1729 1     1 1 4958 my %args = @_;
1730 1 50       8 my $user = $args{user} or return [400, "Please specify user"];
1731 1 50       12 $user =~ /$re_user/o or return [400, "Invalid user"];
1732 1 50       7 my $gg = $args{groups} or return [400, "Please specify groups"];
1733              
1734             # XXX check user exists
1735              
1736             _routine(
1737             %args,
1738             _lock => 1,
1739             _write_group => 1,
1740             _after_read => sub {
1741 1     1   2 my $stash = shift;
1742              
1743 1         2 my $group = $stash->{group};
1744 1         2 my $changed;
1745              
1746 1         3 for my $l (@$group) {
1747 6         14 my @mm = split /,/, $l->[-1];
1748 6 100 100     26 if ($l->[0] ~~ $gg && !($user ~~ @mm)) {
1749 2         3 $changed++;
1750 2         4 push @mm, $user;
1751             }
1752 6 100 100     20 if (!($l->[0] ~~ $gg) && $user ~~ @mm) {
1753 1         2 $changed++;
1754 1         2 @mm = grep {$_ ne $user} @mm;
  2         6  
1755             }
1756 6 100       15 if ($changed) {
1757 5         11 $l->[-1] = join ",", @mm;
1758             }
1759             }
1760 1 50       6 $stash->{write_group} = 0 unless $changed;
1761 1         4 $stash->{res} = [200, "OK"];
1762 1         2 [200];
1763             },
1764 1         15 );
1765             }
1766              
1767             $SPEC{set_user_password} = {
1768             v => 1.1,
1769             summary => 'Set user\'s password',
1770             args => {
1771             %common_args,
1772             %write_args,
1773             user => {
1774             schema => 'str*',
1775             req => 1,
1776             pos => 0,
1777             'x.schema.entity' => 'unix_user',
1778             },
1779             pass => {
1780             schema => 'str*',
1781             req => 1,
1782             pos => 1,
1783             },
1784             },
1785             };
1786             sub set_user_password {
1787 3     3 1 24642 my %args = @_;
1788              
1789 3 50       20 $args{user} or return [400, "Please specify user"];
1790 3 100       17 defined($args{pass}) or return [400, "Please specify pass"];
1791 2         15 modify_user(%args);
1792             }
1793              
1794             sub _delete_group_or_user {
1795 5     5   43 my ($which, %args) = @_;
1796              
1797             # TMP,schema
1798 5         21 my ($user, $gn);
1799 5 100       32 if ($which eq 'user') {
1800 3 50       22 $user = $args{user} or return [400, "Please specify user"];
1801 3         10 $gn = $user;
1802             }
1803 5   66     39 $gn //= $args{group};
1804 5 50       23 $gn or return [400, "Please specify group"];
1805              
1806             _routine(
1807             %args,
1808             _lock => 1,
1809             _write_group => 1,
1810             _write_gshadow => 1,
1811             _write_passwd => $which eq 'user',
1812             _write_shadow => $which eq 'user',
1813             _after_read => sub {
1814 5     5   19 my $stash = shift;
1815 5         12 my ($i, $changed);
1816              
1817 5         20 my $group = $stash->{group};
1818 5         16 $changed = 0; $i = 0;
  5         16  
1819 5         30 while ($i < @$group) {
1820 34 100       100 if ($which eq 'user') {
1821             # also delete all mention of the user in any group
1822 20         71 my @mm = split /,/, $group->[$i][3];
1823 20 100       86 if ($user ~~ @mm) {
1824 4         13 $changed++;
1825 4         15 $group->[$i][3] = join(",", grep {$_ ne $user} @mm);
  5         29  
1826             }
1827             }
1828 34 100       115 if ($group->[$i][0] eq $gn) {
1829 5         26 $changed++;
1830 5         18 splice @$group, $i, 1; $i--;
  5         41  
1831             }
1832 34         97 $i++;
1833             }
1834 5 50       21 $stash->{write_group} = 0 unless $changed;
1835              
1836 5         18 my $gshadow = $stash->{gshadow};
1837 5         11 $changed = 0; $i = 0;
  5         29  
1838 5         24 while ($i < @$gshadow) {
1839 33 100       96 if ($which eq 'user') {
1840             # also delete all mention of the user in any group
1841 19         62 my @mm = split /,/, $gshadow->[$i][3];
1842 19 100       70 if ($user ~~ @mm) {
1843 2         7 $changed++;
1844 2         9 $gshadow->[$i][3] = join(",", grep {$_ ne $user} @mm);
  2         12  
1845             }
1846             }
1847 33 100       107 if ($gshadow->[$i][0] eq $gn) {
1848 5         15 $changed++;
1849 5         19 splice @$gshadow, $i, 1; $i--;
  5         12  
1850 5         17 last;
1851             }
1852 28         71 $i++;
1853             }
1854 5 50       28 $stash->{write_gshadow} = 0 unless $changed;
1855              
1856 5 100       29 if ($which eq 'user') {
1857 3         12 my $passwd = $stash->{passwd};
1858 3         10 $changed = 0; $i = 0;
  3         9  
1859 3         15 while ($i < @$passwd) {
1860 16 100       53 if ($passwd->[$i][0] eq $user) {
1861 3         9 $changed++;
1862 3         24 splice @$passwd, $i, 1; $i--;
  3         14  
1863 3         7 last;
1864             }
1865 13         43 $i++;
1866             }
1867 3 50       14 $stash->{write_passwd} = 0 unless $changed;
1868              
1869 3         11 my $shadow = $stash->{shadow};
1870 3         9 $changed = 0; $i = 0;
  3         9  
1871 3         17 while ($i < @$shadow) {
1872 16 100       55 if ($shadow->[$i][0] eq $user) {
1873 3         10 $changed++;
1874 3         11 splice @$shadow, $i, 1; $i--;
  3         10  
1875 3         7 last;
1876             }
1877 13         40 $i++;
1878             }
1879 3 50       20 $stash->{write_shadow} = 0 unless $changed;
1880             }
1881              
1882 5         27 $stash->{res} = [200, "OK"];
1883 5         19 [200];
1884             },
1885 5         87 );
1886             }
1887              
1888             $SPEC{delete_group} = {
1889             v => 1.1,
1890             summary => 'Delete a group',
1891             args => {
1892             %common_args,
1893             %write_args,
1894             group => {
1895             schema => 'str*',
1896             req => 1,
1897             pos => 0,
1898             'x.schema.entity' => 'unix_group',
1899             },
1900             },
1901             };
1902             sub delete_group {
1903 2     2 1 12693 _delete_group_or_user('group', @_);
1904             }
1905              
1906             $SPEC{delete_user} = {
1907             v => 1.1,
1908             summary => 'Delete a user',
1909             args => {
1910             %common_args,
1911             %write_args,
1912             user => {
1913             schema => 'str*',
1914             req => 1,
1915             pos => 0,
1916             'x.schema.entity' => 'unix_user',
1917             },
1918             },
1919             };
1920             sub delete_user {
1921 3     3 1 31975 _delete_group_or_user('user', @_);
1922             }
1923              
1924             1;
1925             # ABSTRACT: Manipulate /etc/{passwd,shadow,group,gshadow} entries
1926              
1927             __END__
1928              
1929             =pod
1930              
1931             =encoding UTF-8
1932              
1933             =head1 NAME
1934              
1935             Unix::Passwd::File - Manipulate /etc/{passwd,shadow,group,gshadow} entries
1936              
1937             =head1 VERSION
1938              
1939             This document describes version 0.24 of Unix::Passwd::File (from Perl distribution Unix-Passwd-File), released on 2017-07-10.
1940              
1941             =head1 SYNOPSIS
1942              
1943             use Unix::Passwd::File;
1944              
1945             # list users. by default uses files in /etc (/etc/passwd, /etc/shadow, et al)
1946             my $res = list_users(); # [200, "OK", ["root", ...]]
1947              
1948             # change location of files, return details
1949             $res = list_users(etc_dir=>"/some/path", detail=>1);
1950             # [200, "OK", [{user=>"root", uid=>0, ...}, ...]]
1951              
1952             # also return detail, but return array entries instead of hash
1953             $res = list_users(detail=>1, with_field_names=>0);
1954             # [200, "OK", [["root", "x", 0, ...], ...]]
1955              
1956             # get user/group information
1957             $res = get_group(user=>"paijo"); # [200, "OK", {user=>"paijo", uid=>501, ...}]
1958             $res = get_user(user=>"titin"); # [404, "Not found"]
1959              
1960             # check whether user/group exists
1961             say user_exists(user=>"paijo"); # 1
1962             say group_exists(group=>"titin"); # 0
1963              
1964             # get all groups that user is member of
1965             $res = get_user_groups(user=>"paijo"); # [200, "OK", ["paijo", "satpam"]]
1966              
1967             # check whether user is member of a group
1968             $res = is_member(user=>"paijo", group=>"satpam"); # 1
1969              
1970             # adding user/group, by default adding user will also add a group with the same
1971             # name
1972             $res = add_user (user =>"ujang", ...); # [200, "OK", {uid=>540, gid=>541}]
1973             $res = add_group(group=>"ujang", ...); # [412, "Group already exists"]
1974              
1975             # modify user/group
1976             $res = modify_user(user=>"ujang", home=>"/newhome/ujang"); # [200, "OK"]
1977             $res = modify_group(group=>"titin"); # [404, "Not found"]
1978              
1979             # deleting user will also delete user's group
1980             $res = delete_user(user=>"titin");
1981              
1982             # change user password
1983             $res = set_user_password(user=>"ujang", pass=>"foobar");
1984             $res = modify_user(user=>"ujang", pass=>"foobar"); # same thing
1985              
1986             # add/delete user to/from group
1987             $res = add_user_to_group(user=>"ujang", group=>"wheel");
1988             $res = delete_user_from_group(user=>"ujang", group=>"wheel");
1989              
1990             # others
1991             $res = get_max_uid(); # [200, "OK", 65535]
1992             $res = get_max_gid(); # [200, "OK", 65534]
1993              
1994             =head1 DESCRIPTION
1995              
1996             This module can be used to read and manipulate entries in Unix system password
1997             files (/etc/passwd, /etc/group, /etc/group, /etc/gshadow; but can also be told
1998             to search in custom location, for testing purposes).
1999              
2000             This module uses a procedural (non-OO) interface. Each function in this module
2001             open and read the passwd files once. Read-only functions like `list_users()` and
2002             `get_max_gid()` open in read-only mode. Functions that might write to the files
2003             like `add_user()` or `delete_group()` first lock `passwd` file, open in
2004             read+write mode and also read the files in the first pass, then seek to the
2005             beginning and write back the files.
2006              
2007             No caching is done so you should do your own if you need to.
2008              
2009             =head1 FUNCTIONS
2010              
2011              
2012             =head2 add_delete_user_groups
2013              
2014             Usage:
2015              
2016             add_delete_user_groups(%args) -> [status, msg, result, meta]
2017              
2018             Add or delete user from one or several groups.
2019              
2020             This can be used to reduce several C<add_user_to_group()> and/or
2021             C<delete_user_from_group()> calls to a single call. So:
2022              
2023             add_delete_user_groups(user=>'u',add_to=>['a','b'],delete_from=>['c','d']);
2024              
2025             is equivalent to:
2026              
2027             add_user_to_group (user=>'u', group=>'a');
2028             add_user_to_group (user=>'u', group=>'b');
2029             delete_user_from_group(user=>'u', group=>'c');
2030             delete_user_from_group(user=>'u', group=>'d');
2031              
2032             except that C<add_delete_user_groups()> does it in one pass.
2033              
2034             This function is not exported by default, but exportable.
2035              
2036             Arguments ('*' denotes required arguments):
2037              
2038             =over 4
2039              
2040             =item * B<add_to> => I<array[str]> (default: [])
2041              
2042             List of group names to add the user as member of.
2043              
2044             =item * B<delete_from> => I<array[str]> (default: [])
2045              
2046             List of group names to remove the user as member of.
2047              
2048             =item * B<etc_dir> => I<str> (default: "/etc")
2049              
2050             Specify location of passwd files.
2051              
2052             =item * B<user>* => I<str>
2053              
2054             =back
2055              
2056             Returns an enveloped result (an array).
2057              
2058             First element (status) is an integer containing HTTP status code
2059             (200 means OK, 4xx caller error, 5xx function error). Second element
2060             (msg) is a string containing error message, or 'OK' if status is
2061             200. Third element (result) is optional, the actual result. Fourth
2062             element (meta) is called result metadata and is optional, a hash
2063             that contains extra information.
2064              
2065             Return value: (any)
2066              
2067              
2068             =head2 add_group
2069              
2070             Usage:
2071              
2072             add_group(%args) -> [status, msg, result, meta]
2073              
2074             Add a new group.
2075              
2076             This function is not exported by default, but exportable.
2077              
2078             Arguments ('*' denotes required arguments):
2079              
2080             =over 4
2081              
2082             =item * B<backup> => I<bool> (default: 0)
2083              
2084             Whether to backup when modifying files.
2085              
2086             Backup is written with C<.bak> extension in the same directory. Unmodified file
2087             will not be backed up. Previous backup will be overwritten.
2088              
2089             =item * B<etc_dir> => I<str> (default: "/etc")
2090              
2091             Specify location of passwd files.
2092              
2093             =item * B<gid> => I<int>
2094              
2095             Pick a specific new GID.
2096              
2097             Adding a new group with duplicate GID is allowed.
2098              
2099             =item * B<group>* => I<str>
2100              
2101             =item * B<max_gid> => I<int> (default: 65535)
2102              
2103             Pick a range for new GID.
2104              
2105             If a free GID between C<min_gid> and C<max_gid> is not found, error 412 is
2106             returned.
2107              
2108             =item * B<members> => I<any>
2109              
2110             Fill initial members.
2111              
2112             =item * B<min_gid> => I<int> (default: 1000)
2113              
2114             Pick a range for new GID.
2115              
2116             If a free GID between C<min_gid> and C<max_gid> is not found, error 412 is
2117             returned.
2118              
2119             =back
2120              
2121             Returns an enveloped result (an array).
2122              
2123             First element (status) is an integer containing HTTP status code
2124             (200 means OK, 4xx caller error, 5xx function error). Second element
2125             (msg) is a string containing error message, or 'OK' if status is
2126             200. Third element (result) is optional, the actual result. Fourth
2127             element (meta) is called result metadata and is optional, a hash
2128             that contains extra information.
2129              
2130             Return value: (any)
2131              
2132              
2133             =head2 add_user
2134              
2135             Usage:
2136              
2137             add_user(%args) -> [status, msg, result, meta]
2138              
2139             Add a new user.
2140              
2141             This function is not exported by default, but exportable.
2142              
2143             Arguments ('*' denotes required arguments):
2144              
2145             =over 4
2146              
2147             =item * B<backup> => I<bool> (default: 0)
2148              
2149             Whether to backup when modifying files.
2150              
2151             Backup is written with C<.bak> extension in the same directory. Unmodified file
2152             will not be backed up. Previous backup will be overwritten.
2153              
2154             =item * B<encpass> => I<str>
2155              
2156             Encrypted password.
2157              
2158             =item * B<etc_dir> => I<str> (default: "/etc")
2159              
2160             Specify location of passwd files.
2161              
2162             =item * B<expire_date> => I<int>
2163              
2164             The date of expiration of the account, expressed as the number of days since Jan 1, 1970.
2165              
2166             =item * B<gecos> => I<str>
2167              
2168             Usually, it contains the full username.
2169              
2170             =item * B<gid> => I<int>
2171              
2172             Pick a specific GID when creating group.
2173              
2174             Duplicate GID is allowed.
2175              
2176             =item * B<group> => I<str>
2177              
2178             Select primary group (default is group with same name as user).
2179              
2180             Normally, a user's primary group with group with the same name as user, which
2181             will be created if does not already exist. You can pick another group here,
2182             which must already exist (and in this case, the group with the same name as user
2183             will not be created).
2184              
2185             =item * B<home> => I<str>
2186              
2187             User's home directory.
2188              
2189             =item * B<last_pwchange> => I<int>
2190              
2191             The date of the last password change, expressed as the number of days since Jan 1, 1970.
2192              
2193             =item * B<max_gid> => I<int>
2194              
2195             Pick a range for GID when creating group.
2196              
2197             =item * B<max_pass_age> => I<int>
2198              
2199             The number of days after which the user will have to change her password.
2200              
2201             =item * B<max_uid> => I<int> (default: 65535)
2202              
2203             Pick a range for new UID.
2204              
2205             If a free UID between C<min_uid> and C<max_uid> is not found, error 412 is
2206             returned.
2207              
2208             =item * B<min_gid> => I<int>
2209              
2210             Pick a range for GID when creating group.
2211              
2212             =item * B<min_pass_age> => I<int>
2213              
2214             The number of days the user will have to wait before she will be allowed to change her password again.
2215              
2216             =item * B<min_uid> => I<int> (default: 1000)
2217              
2218             Pick a range for new UID.
2219              
2220             If a free UID between C<min_uid> and C<max_uid> is not found, error 412 is
2221             returned.
2222              
2223             =item * B<pass> => I<str>
2224              
2225             Password, generally should be "x" which means password is encrypted in shadow.
2226              
2227             =item * B<pass_inactive_period> => I<int>
2228              
2229             The number of days after a password has expired (see max_pass_age) during which the password should still be accepted (and user should update her password during the next login).
2230              
2231             =item * B<pass_warn_period> => I<int>
2232              
2233             The number of days before a password is going to expire (see max_pass_age) during which the user should be warned.
2234              
2235             =item * B<shell> => I<str>
2236              
2237             User's shell.
2238              
2239             =item * B<uid> => I<int>
2240              
2241             Pick a specific new UID.
2242              
2243             Adding a new user with duplicate UID is allowed.
2244              
2245             =item * B<user>* => I<str>
2246              
2247             =back
2248              
2249             Returns an enveloped result (an array).
2250              
2251             First element (status) is an integer containing HTTP status code
2252             (200 means OK, 4xx caller error, 5xx function error). Second element
2253             (msg) is a string containing error message, or 'OK' if status is
2254             200. Third element (result) is optional, the actual result. Fourth
2255             element (meta) is called result metadata and is optional, a hash
2256             that contains extra information.
2257              
2258             Return value: (any)
2259              
2260              
2261             =head2 add_user_to_group
2262              
2263             Usage:
2264              
2265             add_user_to_group(%args) -> [status, msg, result, meta]
2266              
2267             Add user to a group.
2268              
2269             This function is not exported by default, but exportable.
2270              
2271             Arguments ('*' denotes required arguments):
2272              
2273             =over 4
2274              
2275             =item * B<etc_dir> => I<str> (default: "/etc")
2276              
2277             Specify location of passwd files.
2278              
2279             =item * B<group>* => I<str>
2280              
2281             =item * B<user>* => I<str>
2282              
2283             =back
2284              
2285             Returns an enveloped result (an array).
2286              
2287             First element (status) is an integer containing HTTP status code
2288             (200 means OK, 4xx caller error, 5xx function error). Second element
2289             (msg) is a string containing error message, or 'OK' if status is
2290             200. Third element (result) is optional, the actual result. Fourth
2291             element (meta) is called result metadata and is optional, a hash
2292             that contains extra information.
2293              
2294             Return value: (any)
2295              
2296              
2297             =head2 delete_group
2298              
2299             Usage:
2300              
2301             delete_group(%args) -> [status, msg, result, meta]
2302              
2303             Delete a group.
2304              
2305             This function is not exported by default, but exportable.
2306              
2307             Arguments ('*' denotes required arguments):
2308              
2309             =over 4
2310              
2311             =item * B<backup> => I<bool> (default: 0)
2312              
2313             Whether to backup when modifying files.
2314              
2315             Backup is written with C<.bak> extension in the same directory. Unmodified file
2316             will not be backed up. Previous backup will be overwritten.
2317              
2318             =item * B<etc_dir> => I<str> (default: "/etc")
2319              
2320             Specify location of passwd files.
2321              
2322             =item * B<group>* => I<str>
2323              
2324             =back
2325              
2326             Returns an enveloped result (an array).
2327              
2328             First element (status) is an integer containing HTTP status code
2329             (200 means OK, 4xx caller error, 5xx function error). Second element
2330             (msg) is a string containing error message, or 'OK' if status is
2331             200. Third element (result) is optional, the actual result. Fourth
2332             element (meta) is called result metadata and is optional, a hash
2333             that contains extra information.
2334              
2335             Return value: (any)
2336              
2337              
2338             =head2 delete_user
2339              
2340             Usage:
2341              
2342             delete_user(%args) -> [status, msg, result, meta]
2343              
2344             Delete a user.
2345              
2346             This function is not exported by default, but exportable.
2347              
2348             Arguments ('*' denotes required arguments):
2349              
2350             =over 4
2351              
2352             =item * B<backup> => I<bool> (default: 0)
2353              
2354             Whether to backup when modifying files.
2355              
2356             Backup is written with C<.bak> extension in the same directory. Unmodified file
2357             will not be backed up. Previous backup will be overwritten.
2358              
2359             =item * B<etc_dir> => I<str> (default: "/etc")
2360              
2361             Specify location of passwd files.
2362              
2363             =item * B<user>* => I<str>
2364              
2365             =back
2366              
2367             Returns an enveloped result (an array).
2368              
2369             First element (status) is an integer containing HTTP status code
2370             (200 means OK, 4xx caller error, 5xx function error). Second element
2371             (msg) is a string containing error message, or 'OK' if status is
2372             200. Third element (result) is optional, the actual result. Fourth
2373             element (meta) is called result metadata and is optional, a hash
2374             that contains extra information.
2375              
2376             Return value: (any)
2377              
2378              
2379             =head2 delete_user_from_group
2380              
2381             Usage:
2382              
2383             delete_user_from_group(%args) -> [status, msg, result, meta]
2384              
2385             Delete user from a group.
2386              
2387             This function is not exported by default, but exportable.
2388              
2389             Arguments ('*' denotes required arguments):
2390              
2391             =over 4
2392              
2393             =item * B<etc_dir> => I<str> (default: "/etc")
2394              
2395             Specify location of passwd files.
2396              
2397             =item * B<group>* => I<str>
2398              
2399             =item * B<user>* => I<str>
2400              
2401             =back
2402              
2403             Returns an enveloped result (an array).
2404              
2405             First element (status) is an integer containing HTTP status code
2406             (200 means OK, 4xx caller error, 5xx function error). Second element
2407             (msg) is a string containing error message, or 'OK' if status is
2408             200. Third element (result) is optional, the actual result. Fourth
2409             element (meta) is called result metadata and is optional, a hash
2410             that contains extra information.
2411              
2412             Return value: (any)
2413              
2414              
2415             =head2 get_group
2416              
2417             Usage:
2418              
2419             get_group(%args) -> [status, msg, result, meta]
2420              
2421             Get group details by group name or gid.
2422              
2423             Either C<group> OR C<gid> must be specified.
2424              
2425             The function is not dissimilar to Unix's C<getgrnam()> or C<getgrgid()>.
2426              
2427             This function is not exported by default, but exportable.
2428              
2429             Arguments ('*' denotes required arguments):
2430              
2431             =over 4
2432              
2433             =item * B<etc_dir> => I<str> (default: "/etc")
2434              
2435             Specify location of passwd files.
2436              
2437             =item * B<gid> => I<int>
2438              
2439             =item * B<group> => I<str>
2440              
2441             =item * B<with_field_names> => I<bool> (default: 1)
2442              
2443             If false, don't return hash.
2444              
2445             By default, a hashref is returned containing field names and its values, e.g.
2446             C<< {group=E<gt>"titin", pass=E<gt>"x", gid=E<gt>500, ...} >>. With C<< with_field_names=E<gt>0 >>, an
2447             arrayref is returned instead: C<["titin", "x", 500, ...]>.
2448              
2449             =back
2450              
2451             Returns an enveloped result (an array).
2452              
2453             First element (status) is an integer containing HTTP status code
2454             (200 means OK, 4xx caller error, 5xx function error). Second element
2455             (msg) is a string containing error message, or 'OK' if status is
2456             200. Third element (result) is optional, the actual result. Fourth
2457             element (meta) is called result metadata and is optional, a hash
2458             that contains extra information.
2459              
2460             Return value: (any)
2461              
2462              
2463             =head2 get_max_gid
2464              
2465             Usage:
2466              
2467             get_max_gid(%args) -> [status, msg, result, meta]
2468              
2469             Get maximum GID used.
2470              
2471             This function is not exported by default, but exportable.
2472              
2473             Arguments ('*' denotes required arguments):
2474              
2475             =over 4
2476              
2477             =item * B<etc_dir> => I<str> (default: "/etc")
2478              
2479             Specify location of passwd files.
2480              
2481             =back
2482              
2483             Returns an enveloped result (an array).
2484              
2485             First element (status) is an integer containing HTTP status code
2486             (200 means OK, 4xx caller error, 5xx function error). Second element
2487             (msg) is a string containing error message, or 'OK' if status is
2488             200. Third element (result) is optional, the actual result. Fourth
2489             element (meta) is called result metadata and is optional, a hash
2490             that contains extra information.
2491              
2492             Return value: (any)
2493              
2494              
2495             =head2 get_max_uid
2496              
2497             Usage:
2498              
2499             get_max_uid(%args) -> [status, msg, result, meta]
2500              
2501             Get maximum UID used.
2502              
2503             This function is not exported by default, but exportable.
2504              
2505             Arguments ('*' denotes required arguments):
2506              
2507             =over 4
2508              
2509             =item * B<etc_dir> => I<str> (default: "/etc")
2510              
2511             Specify location of passwd files.
2512              
2513             =back
2514              
2515             Returns an enveloped result (an array).
2516              
2517             First element (status) is an integer containing HTTP status code
2518             (200 means OK, 4xx caller error, 5xx function error). Second element
2519             (msg) is a string containing error message, or 'OK' if status is
2520             200. Third element (result) is optional, the actual result. Fourth
2521             element (meta) is called result metadata and is optional, a hash
2522             that contains extra information.
2523              
2524             Return value: (any)
2525              
2526              
2527             =head2 get_user
2528              
2529             Usage:
2530              
2531             get_user(%args) -> [status, msg, result, meta]
2532              
2533             Get user details by username or uid.
2534              
2535             Either C<user> OR C<uid> must be specified.
2536              
2537             The function is not dissimilar to Unix's C<getpwnam()> or C<getpwuid()>.
2538              
2539             This function is not exported by default, but exportable.
2540              
2541             Arguments ('*' denotes required arguments):
2542              
2543             =over 4
2544              
2545             =item * B<etc_dir> => I<str> (default: "/etc")
2546              
2547             Specify location of passwd files.
2548              
2549             =item * B<uid> => I<int>
2550              
2551             =item * B<user> => I<str>
2552              
2553             =item * B<with_field_names> => I<bool> (default: 1)
2554              
2555             If false, don't return hash.
2556              
2557             By default, a hashref is returned containing field names and its values, e.g.
2558             C<< {user=E<gt>"titin", pass=E<gt>"x", uid=E<gt>500, ...} >>. With C<< with_field_names=E<gt>0 >>, an
2559             arrayref is returned instead: C<["titin", "x", 500, ...]>.
2560              
2561             =back
2562              
2563             Returns an enveloped result (an array).
2564              
2565             First element (status) is an integer containing HTTP status code
2566             (200 means OK, 4xx caller error, 5xx function error). Second element
2567             (msg) is a string containing error message, or 'OK' if status is
2568             200. Third element (result) is optional, the actual result. Fourth
2569             element (meta) is called result metadata and is optional, a hash
2570             that contains extra information.
2571              
2572             Return value: (any)
2573              
2574              
2575             =head2 get_user_groups
2576              
2577             Usage:
2578              
2579             get_user_groups(%args) -> [status, msg, result, meta]
2580              
2581             Return groups which the user belongs to.
2582              
2583             This function is not exported by default, but exportable.
2584              
2585             Arguments ('*' denotes required arguments):
2586              
2587             =over 4
2588              
2589             =item * B<detail> => I<bool> (default: 0)
2590              
2591             If true, return all fields instead of just group names.
2592              
2593             =item * B<etc_dir> => I<str> (default: "/etc")
2594              
2595             Specify location of passwd files.
2596              
2597             =item * B<user>* => I<str>
2598              
2599             =item * B<with_field_names> => I<bool> (default: 1)
2600              
2601             If false, don't return hash for each entry.
2602              
2603             By default, when C<< detail=E<gt>1 >>, a hashref is returned for each entry containing
2604             field names and its values, e.g. C<< {group=E<gt>"titin", pass=E<gt>"x", gid=E<gt>500, ...} >>.
2605             With C<< with_field_names=E<gt>0 >>, an arrayref is returned instead: C<["titin", "x",
2606             500, ...]>.
2607              
2608             =back
2609              
2610             Returns an enveloped result (an array).
2611              
2612             First element (status) is an integer containing HTTP status code
2613             (200 means OK, 4xx caller error, 5xx function error). Second element
2614             (msg) is a string containing error message, or 'OK' if status is
2615             200. Third element (result) is optional, the actual result. Fourth
2616             element (meta) is called result metadata and is optional, a hash
2617             that contains extra information.
2618              
2619             Return value: (any)
2620              
2621              
2622             =head2 group_exists
2623              
2624             Usage:
2625              
2626             group_exists(%args) -> bool
2627              
2628             Check whether group exists.
2629              
2630             This function is not exported by default, but exportable.
2631              
2632             Arguments ('*' denotes required arguments):
2633              
2634             =over 4
2635              
2636             =item * B<etc_dir> => I<str> (default: "/etc")
2637              
2638             Specify location of passwd files.
2639              
2640             =item * B<gid> => I<int>
2641              
2642             =item * B<group> => I<str>
2643              
2644             =back
2645              
2646             Return value: (bool)
2647              
2648              
2649             =head2 is_member
2650              
2651             Usage:
2652              
2653             is_member(%args) -> bool
2654              
2655             Check whether user is member of a group.
2656              
2657             This function is not exported by default, but exportable.
2658              
2659             Arguments ('*' denotes required arguments):
2660              
2661             =over 4
2662              
2663             =item * B<etc_dir> => I<str> (default: "/etc")
2664              
2665             Specify location of passwd files.
2666              
2667             =item * B<group>* => I<str>
2668              
2669             =item * B<user>* => I<str>
2670              
2671             =back
2672              
2673             Return value: (bool)
2674              
2675              
2676             =head2 list_groups
2677              
2678             Usage:
2679              
2680             list_groups(%args) -> [status, msg, result, meta]
2681              
2682             List Unix groups in group file.
2683              
2684             This function is not exported by default, but exportable.
2685              
2686             Arguments ('*' denotes required arguments):
2687              
2688             =over 4
2689              
2690             =item * B<detail> => I<bool> (default: 0)
2691              
2692             If true, return all fields instead of just group names.
2693              
2694             =item * B<etc_dir> => I<str> (default: "/etc")
2695              
2696             Specify location of passwd files.
2697              
2698             =item * B<with_field_names> => I<bool> (default: 1)
2699              
2700             If false, don't return hash for each entry.
2701              
2702             By default, when C<< detail=E<gt>1 >>, a hashref is returned for each entry containing
2703             field names and its values, e.g. C<< {group=E<gt>"titin", pass=E<gt>"x", gid=E<gt>500, ...} >>.
2704             With C<< with_field_names=E<gt>0 >>, an arrayref is returned instead: C<["titin", "x",
2705             500, ...]>.
2706              
2707             =back
2708              
2709             Returns an enveloped result (an array).
2710              
2711             First element (status) is an integer containing HTTP status code
2712             (200 means OK, 4xx caller error, 5xx function error). Second element
2713             (msg) is a string containing error message, or 'OK' if status is
2714             200. Third element (result) is optional, the actual result. Fourth
2715             element (meta) is called result metadata and is optional, a hash
2716             that contains extra information.
2717              
2718             Return value: (any)
2719              
2720              
2721             =head2 list_users
2722              
2723             Usage:
2724              
2725             list_users(%args) -> [status, msg, result, meta]
2726              
2727             List Unix users in passwd file.
2728              
2729             This function is not exported by default, but exportable.
2730              
2731             Arguments ('*' denotes required arguments):
2732              
2733             =over 4
2734              
2735             =item * B<detail> => I<bool> (default: 0)
2736              
2737             If true, return all fields instead of just usernames.
2738              
2739             =item * B<etc_dir> => I<str> (default: "/etc")
2740              
2741             Specify location of passwd files.
2742              
2743             =item * B<with_field_names> => I<bool> (default: 1)
2744              
2745             If false, don't return hash for each entry.
2746              
2747             By default, when C<< detail=E<gt>1 >>, a hashref is returned for each entry containing
2748             field names and its values, e.g. C<< {user=E<gt>"titin", pass=E<gt>"x", uid=E<gt>500, ...} >>.
2749             With C<< with_field_names=E<gt>0 >>, an arrayref is returned instead: C<["titin", "x",
2750             500, ...]>.
2751              
2752             =back
2753              
2754             Returns an enveloped result (an array).
2755              
2756             First element (status) is an integer containing HTTP status code
2757             (200 means OK, 4xx caller error, 5xx function error). Second element
2758             (msg) is a string containing error message, or 'OK' if status is
2759             200. Third element (result) is optional, the actual result. Fourth
2760             element (meta) is called result metadata and is optional, a hash
2761             that contains extra information.
2762              
2763             Return value: (any)
2764              
2765              
2766             =head2 list_users_and_groups
2767              
2768             Usage:
2769              
2770             list_users_and_groups(%args) -> [status, msg, result, meta]
2771              
2772             List Unix users and groups in passwd/group files.
2773              
2774             This is basically C<list_users()> and C<list_groups()> combined, so you can get
2775             both data in a single call. Data is returned in an array. Users list is in the
2776             first element, groups list in the second.
2777              
2778             This function is not exported by default, but exportable.
2779              
2780             Arguments ('*' denotes required arguments):
2781              
2782             =over 4
2783              
2784             =item * B<detail> => I<bool> (default: 0)
2785              
2786             If true, return all fields instead of just names.
2787              
2788             =item * B<etc_dir> => I<str> (default: "/etc")
2789              
2790             Specify location of passwd files.
2791              
2792             =item * B<with_field_names> => I<bool> (default: 1)
2793              
2794             If false, don't return hash for each entry.
2795              
2796             =back
2797              
2798             Returns an enveloped result (an array).
2799              
2800             First element (status) is an integer containing HTTP status code
2801             (200 means OK, 4xx caller error, 5xx function error). Second element
2802             (msg) is a string containing error message, or 'OK' if status is
2803             200. Third element (result) is optional, the actual result. Fourth
2804             element (meta) is called result metadata and is optional, a hash
2805             that contains extra information.
2806              
2807             Return value: (any)
2808              
2809              
2810             =head2 modify_group
2811              
2812             Usage:
2813              
2814             modify_group(%args) -> [status, msg, result, meta]
2815              
2816             Modify an existing group.
2817              
2818             Specify arguments to modify corresponding fields. Unspecified fields will not be
2819             modified.
2820              
2821             This function is not exported by default, but exportable.
2822              
2823             Arguments ('*' denotes required arguments):
2824              
2825             =over 4
2826              
2827             =item * B<admins> => I<str>
2828              
2829             It must be a comma-separated list of user names, or empty.
2830              
2831             =item * B<backup> => I<bool> (default: 0)
2832              
2833             Whether to backup when modifying files.
2834              
2835             Backup is written with C<.bak> extension in the same directory. Unmodified file
2836             will not be backed up. Previous backup will be overwritten.
2837              
2838             =item * B<encpass> => I<str>
2839              
2840             Encrypted password.
2841              
2842             =item * B<etc_dir> => I<str> (default: "/etc")
2843              
2844             Specify location of passwd files.
2845              
2846             =item * B<gid> => I<int>
2847              
2848             Numeric group ID.
2849              
2850             =item * B<group>* => I<str>
2851              
2852             Group name.
2853              
2854             =item * B<members> => I<str>
2855              
2856             List of usernames that are members of this group, separated by commas.
2857              
2858             =item * B<pass> => I<str>
2859              
2860             Password, generally should be "x" which means password is encrypted in gshadow.
2861              
2862             =back
2863              
2864             Returns an enveloped result (an array).
2865              
2866             First element (status) is an integer containing HTTP status code
2867             (200 means OK, 4xx caller error, 5xx function error). Second element
2868             (msg) is a string containing error message, or 'OK' if status is
2869             200. Third element (result) is optional, the actual result. Fourth
2870             element (meta) is called result metadata and is optional, a hash
2871             that contains extra information.
2872              
2873             Return value: (any)
2874              
2875              
2876             =head2 modify_user
2877              
2878             Usage:
2879              
2880             modify_user(%args) -> [status, msg, result, meta]
2881              
2882             Modify an existing user.
2883              
2884             Specify arguments to modify corresponding fields. Unspecified fields will not be
2885             modified.
2886              
2887             This function is not exported by default, but exportable.
2888              
2889             Arguments ('*' denotes required arguments):
2890              
2891             =over 4
2892              
2893             =item * B<backup> => I<bool> (default: 0)
2894              
2895             Whether to backup when modifying files.
2896              
2897             Backup is written with C<.bak> extension in the same directory. Unmodified file
2898             will not be backed up. Previous backup will be overwritten.
2899              
2900             =item * B<encpass> => I<str>
2901              
2902             Encrypted password.
2903              
2904             =item * B<etc_dir> => I<str> (default: "/etc")
2905              
2906             Specify location of passwd files.
2907              
2908             =item * B<expire_date> => I<int>
2909              
2910             The date of expiration of the account, expressed as the number of days since Jan 1, 1970.
2911              
2912             =item * B<gecos> => I<str>
2913              
2914             Usually, it contains the full username.
2915              
2916             =item * B<gid> => I<int>
2917              
2918             Numeric primary group ID for this user.
2919              
2920             =item * B<home> => I<str>
2921              
2922             User's home directory.
2923              
2924             =item * B<last_pwchange> => I<int>
2925              
2926             The date of the last password change, expressed as the number of days since Jan 1, 1970.
2927              
2928             =item * B<max_pass_age> => I<int>
2929              
2930             The number of days after which the user will have to change her password.
2931              
2932             =item * B<min_pass_age> => I<int>
2933              
2934             The number of days the user will have to wait before she will be allowed to change her password again.
2935              
2936             =item * B<pass_inactive_period> => I<int>
2937              
2938             The number of days after a password has expired (see max_pass_age) during which the password should still be accepted (and user should update her password during the next login).
2939              
2940             =item * B<pass_warn_period> => I<int>
2941              
2942             The number of days before a password is going to expire (see max_pass_age) during which the user should be warned.
2943              
2944             =item * B<shell> => I<str>
2945              
2946             User's shell.
2947              
2948             =item * B<uid> => I<int>
2949              
2950             Numeric user ID.
2951              
2952             =item * B<user>* => I<str>
2953              
2954             User (login) name.
2955              
2956             =back
2957              
2958             Returns an enveloped result (an array).
2959              
2960             First element (status) is an integer containing HTTP status code
2961             (200 means OK, 4xx caller error, 5xx function error). Second element
2962             (msg) is a string containing error message, or 'OK' if status is
2963             200. Third element (result) is optional, the actual result. Fourth
2964             element (meta) is called result metadata and is optional, a hash
2965             that contains extra information.
2966              
2967             Return value: (any)
2968              
2969              
2970             =head2 set_user_groups
2971              
2972             Usage:
2973              
2974             set_user_groups(%args) -> [status, msg, result, meta]
2975              
2976             Set the groups that a user is member of.
2977              
2978             This function is not exported by default, but exportable.
2979              
2980             Arguments ('*' denotes required arguments):
2981              
2982             =over 4
2983              
2984             =item * B<etc_dir> => I<str> (default: "/etc")
2985              
2986             Specify location of passwd files.
2987              
2988             =item * B<groups>* => I<array[str]> (default: [])
2989              
2990             List of group names that user is member of.
2991              
2992             Aside from this list, user will not belong to any other group.
2993              
2994             =item * B<user>* => I<str>
2995              
2996             =back
2997              
2998             Returns an enveloped result (an array).
2999              
3000             First element (status) is an integer containing HTTP status code
3001             (200 means OK, 4xx caller error, 5xx function error). Second element
3002             (msg) is a string containing error message, or 'OK' if status is
3003             200. Third element (result) is optional, the actual result. Fourth
3004             element (meta) is called result metadata and is optional, a hash
3005             that contains extra information.
3006              
3007             Return value: (any)
3008              
3009              
3010             =head2 set_user_password
3011              
3012             Usage:
3013              
3014             set_user_password(%args) -> [status, msg, result, meta]
3015              
3016             Set user's password.
3017              
3018             This function is not exported by default, but exportable.
3019              
3020             Arguments ('*' denotes required arguments):
3021              
3022             =over 4
3023              
3024             =item * B<backup> => I<bool> (default: 0)
3025              
3026             Whether to backup when modifying files.
3027              
3028             Backup is written with C<.bak> extension in the same directory. Unmodified file
3029             will not be backed up. Previous backup will be overwritten.
3030              
3031             =item * B<etc_dir> => I<str> (default: "/etc")
3032              
3033             Specify location of passwd files.
3034              
3035             =item * B<pass>* => I<str>
3036              
3037             =item * B<user>* => I<str>
3038              
3039             =back
3040              
3041             Returns an enveloped result (an array).
3042              
3043             First element (status) is an integer containing HTTP status code
3044             (200 means OK, 4xx caller error, 5xx function error). Second element
3045             (msg) is a string containing error message, or 'OK' if status is
3046             200. Third element (result) is optional, the actual result. Fourth
3047             element (meta) is called result metadata and is optional, a hash
3048             that contains extra information.
3049              
3050             Return value: (any)
3051              
3052              
3053             =head2 user_exists
3054              
3055             Usage:
3056              
3057             user_exists(%args) -> bool
3058              
3059             Check whether user exists.
3060              
3061             This function is not exported by default, but exportable.
3062              
3063             Arguments ('*' denotes required arguments):
3064              
3065             =over 4
3066              
3067             =item * B<etc_dir> => I<str> (default: "/etc")
3068              
3069             Specify location of passwd files.
3070              
3071             =item * B<uid> => I<int>
3072              
3073             =item * B<user> => I<str>
3074              
3075             =back
3076              
3077             Return value: (bool)
3078              
3079             =head1 HOMEPAGE
3080              
3081             Please visit the project's homepage at L<https://metacpan.org/release/Unix-Passwd-File>.
3082              
3083             =head1 SOURCE
3084              
3085             Source repository is at L<https://github.com/perlancar/perl-Unix-Passwd-File>.
3086              
3087             =head1 BUGS
3088              
3089             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Unix-Passwd-File>
3090              
3091             When submitting a bug or request, please include a test-file or a
3092             patch to an existing test-file that illustrates the bug or desired
3093             feature.
3094              
3095             =head1 SEE ALSO
3096              
3097             Old modules on CPAN which do not support shadow files are pretty useless to me
3098             (e.g. L<Unix::ConfigFile>). Shadow passwords have been around since 1988 (and in
3099             Linux since 1992), FFS!
3100              
3101             L<Passwd::Unix>. I created a fork of Passwd::Unix v0.52 called
3102             L<Passwd::Unix::Alt> in 2011 to fix some of the deficiencies/quirks in
3103             Passwd::Unix, including: lack of tests, insistence of running as root (despite
3104             allowing custom passwd files), use of not-so-ubiquitous bzip2, etc. Then in 2012
3105             I decided to create Unix::Passwd::File. Here are how Unix::Passwd::File differs
3106             compared to Passwd::Unix (and Passwd::Unix::Alt):
3107              
3108             =over 4
3109              
3110             =item * tests in distribution
3111              
3112             =item * no need to run as root
3113              
3114             =item * no need to be able to read the shadow file for some operations
3115              
3116             For example, C<list_users()> will simply not return the C<encpass> field if the
3117             shadow file is unreadable. Of course, access to shadow file is required when
3118             getting or setting password.
3119              
3120             =item * strictly procedural (non-OO) interface
3121              
3122             I consider this a feature :-)
3123              
3124             =item * detailed error message for each operation
3125              
3126             =item * removal of global error variable
3127              
3128             =item * working locking
3129              
3130             Locking is done by locking C<passwd> file.
3131              
3132             =back
3133              
3134             L<Setup::Unix::User> and L<Setup::Unix::Group>, which use this module.
3135              
3136             L<Rinci>
3137              
3138             =head1 AUTHOR
3139              
3140             perlancar <perlancar@cpan.org>
3141              
3142             =head1 COPYRIGHT AND LICENSE
3143              
3144             This software is copyright (c) 2017, 2016, 2015, 2014, 2012 by perlancar@cpan.org.
3145              
3146             This is free software; you can redistribute it and/or modify it under
3147             the same terms as the Perl 5 programming language system itself.
3148              
3149             =cut