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