File Coverage

blib/lib/Sys/Export/Unix/UserDB.pm
Criterion Covered Total %
statement 494 560 88.2
branch 169 276 61.2
condition 66 118 55.9
subroutine 70 79 88.6
pod 18 18 100.0
total 817 1051 77.7


line stmt bran cond sub pod time code
1             package Sys::Export::Unix::UserDB;
2              
3             # ABSTRACT: Abstractions for Unix passwd/group/shadow files
4             our $VERSION = '0.003'; # VERSION
5              
6 2     2   295445 use v5.26;
  2         6  
7 2     2   10 use warnings;
  2         4  
  2         140  
8 2     2   12 use experimental qw( signatures );
  2         3  
  2         13  
9 2     2   289 use Carp ();
  2         3  
  2         61  
10 2     2   1030 use File::Spec::Functions qw( catfile );
  2         1727  
  2         167  
11 2     2   749 use Storable qw( dclone );
  2         5350  
  2         147  
12 2     2   13 use Scalar::Util ();
  2         3  
  2         52  
13 2     2   1174 use User::pwent qw( getpwnam pw_has );
  2         13654  
  2         194  
14 2     2   693 use Sys::Export qw( :isa );
  2         7  
  2         12  
15              
16             # making lexical subs allows these to be seen by inner packages as well
17             # and removes need for namespace::clean
18 2     2   357 my sub carp { goto \&Carp::carp }
19 9     9   1694 my sub croak { goto \&Carp::croak }
20 51     51   266 my sub isa_hash :prototype($) { ref $_[0] eq 'HASH' }
21 24     24   84 my sub isa_array :prototype($) { ref $_[0] eq 'ARRAY' }
22 119 100   119   789 my sub isa_int :prototype($) { Scalar::Util::looks_like_number($_[0]) && int($_[0]) == $_[0] }
23              
24              
25 13     13 1 293023 sub new($class, @args) {
  13         30  
  13         55  
  13         19  
26 13 100 66     79 my %args= @args == 1 && isa_hash($args[0])? $args[0]->%* : @args;
27 13         85 my $self = bless {
28             users => {},
29             uids => {},
30             groups => {},
31             gids => {},
32             }, $class;
33             # I don't want to declare this as an official attribute because a subclass might decide
34             # to implement is_valid_name using something other than a regex, and then the attribute
35             # would be inconsistent.
36 13 50       47 $self->{valid_name_regex}= $args{valid_name_regex} if defined $args{valid_name_regex};
37             # Extract initial user and group lists before initializing attributes, then apply afterward
38 13         31 my $g= delete $args{groups};
39 13         30 my $u= delete $args{users};
40              
41             # Apply writable attributes and/or initial methods
42 13         42 for (keys %args) {
43 5 100       49 croak "Unknown option '$_'" unless $self->can($_);
44 4         18 $self->$_($args{$_});
45             }
46 12 100       36 if ($g) {
47 2 50       7 if (isa_array $g) {
    50          
48 0         0 $self->add_group($_) for $g->@*;
49             } elsif (isa_hash $g) {
50 2         13 $self->add_group($_ => $g->{$_}->%*) for keys %$g;
51             } else {
52 0         0 croak "Option 'groups' must be arrayref or hashref of group objects";
53             }
54             }
55 12 100       47 if ($u) {
56 2 50       5 if (isa_array $u) {
    50          
57 0         0 $self->add_user($_) for $u->@*;
58             } elsif (isa_hash $u) {
59 2         15 $self->add_user($_ => $u->{$_}->%*) for keys %$u;
60             } else {
61 0         0 croak "Option 'users' must be arrayref or hashref of user objects";
62             }
63             }
64 12         49 return $self;
65             }
66              
67              
68 13     13 1 705 sub users($self) { $self->{users} }
  13         23  
  13         22  
  13         139  
69              
70 1     1 1 5 sub uids($self) { $self->{uids} }
  1         1  
  1         3  
  1         46  
71              
72 5     5 1 967 sub groups($self) { $self->{groups} }
  5         10  
  5         7  
  5         37  
73              
74 1     1 1 2 sub gids($self) { $self->{gids} }
  1         3  
  1         2  
  1         8  
75              
76              
77 66     66 1 80 sub auto_import($self, @val) {
  66         85  
  66         78  
  66         77  
78             # coerce anything other than a userdb into a boolean
79             @val? ($self->{auto_import}= isa_userdb($val[0])? $val[0] : !!$val[0]) : $self->{auto_import}
80 66 100       220 }
    100          
81              
82              
83 1     1 1 2 sub clone($self) {
  1         2  
  1         3  
84 1         185 return dclone($self);
85             }
86              
87              
88 91     91 1 147 sub is_valid_name($self, $name) {
  91         132  
  91         129  
  91         121  
89             defined $self->{valid_name_regex}? scalar( $name =~ $self->{valid_name_regex} )
90 91 50       518 : scalar( $name =~ /^[A-Za-z_][-A-Za-z0-9_.]{0,30}[-A-Za-z0-9_.\$]?\z/ )
91             }
92              
93              
94 3     3 1 504 sub load($self, $path, %options) {
  3         7  
  3         6  
  3         7  
  3         6  
95 3 50       12 croak "Path is required" unless defined $path;
96 3         48 my $passwd_file = catfile($path, 'passwd');
97 3         16 my $group_file = catfile($path, 'group');
98 3         14 my $shadow_file = catfile($path, 'shadow');
99              
100 3 50       21 unless ($options{format}) {
101             # If we have passwd, group, and shadow, assume Linux.
102 3 50 66     265 $options{format}= (-f $passwd_file && -f $group_file && -f $shadow_file)? 'Linux'
    50          
    50          
    100          
103             : croak "Unable to detect format: passwd ".(-f $passwd_file? "[found]":"[not found]")
104             ." group ".(-f $group_file? "[found]" : "[not found]")
105             ." shadow ".(-f $shadow_file? "[found]" : "[not found]");
106             }
107 2         6 my $records;
108 2 50       9 if ($options{format} eq 'Linux') {
109 2 50       10 $records= $self->_parse_linux_passwd_format({
110             passwd => _slurp($passwd_file),
111             group => _slurp($group_file),
112             (-r $shadow_file? ( shadow => _slurp($shadow_file) ) : ()),
113             });
114             } else {
115 0         0 croak "Unsupported format '$options{format}'";
116             }
117              
118             # convert user primary gid to group name
119 2         10 my (%group_by_name, %group_by_gid, %user_by_name);
120 2         8 for ($records->{groups}->@*) {
121 9   33     45 $group_by_name{$_->{name}} //= $_;
122 9   33     40 $group_by_gid{$_->{gid}} //= $_;
123             }
124 2         7 for ($records->{users}->@*) {
125 7   33     34 $user_by_name{$_->{name}} //= $_;
126 7         15 my $gid= delete $_->{gid};
127 7 50       19 if (my $primary_group= $group_by_gid{$gid}) {
128 7         27 $_->{group}= $primary_group->{name};
129             } else {
130 0         0 carp "User '$_->{name}' references non-existent gid $gid";
131             # This allows the gid to pass through for a round trip, but will generate
132             # warning in various places.
133 0         0 $_->{group}= $gid;
134             }
135             }
136              
137             # move group membership to user records
138 2         7 for my $g ($records->{groups}->@*) {
139 9 100       28 if (my $members= delete $g->{members}) {
140 4         14 for (split ',', $members) {
141             my $u= $user_by_name{$_}
142 6 50       49 or do { carp "Group '$g->{name}' references non-existent user '$_'"; next; };
  0         0  
  0         0  
143 6         10 push @{$u->{groups}}, $g->{name};
  6         24  
144             }
145             }
146             }
147              
148             $self->_add_group_object(Sys::Export::Unix::UserDB::Group->new(%$_))
149 2         28 for $records->{groups}->@*;
150             $self->_add_user_object(Sys::Export::Unix::UserDB::User->new(%$_))
151 2         30 for $records->{users}->@*;
152              
153 2         41 return $self;
154             }
155              
156              
157 4     4 1 462 sub save($self, $target, %options) {
  4         15  
  4         9  
  4         9  
  4         8  
158 4   33     40 my $format= $options{format} // $self->{default_format} // 'Linux';
      50        
159 4         8 my $data;
160 4 50       17 if ($format eq 'Linux') {
161 4         15 $data= $self->_generate_linux_passwd_format(%options);
162             } else {
163 0         0 croak "Unsupported format $options{format}";
164             }
165 4 100       15 if (isa_hash $target) {
166 2         43 %$target= %$data;
167             } else {
168 2 50       62 croak "path does not exist: '$target'" unless -e $target;
169 2         12 for (keys %$data) {
170 6   100     33 my $public= $_ eq 'passwd' || $_ eq 'group';
171 6 100       69 _mkfile(catfile($target, $_), $data->{$_}, $public? 0755 : 0700);
172             }
173             }
174 4         31 return $self;
175             }
176              
177 6     6   12 sub _mkfile($name, $data, $mode=undef) {
  6         13  
  6         10  
  6         16  
  6         11  
178 6 50       1261 open my $fh, '>:raw', $name or croak "open(>$name): $!";
179 6 50       58 $fh->print($data) or croak "write($name): $!";
180 6 50       95 $fh->close or croak "close($name): $!";
181 6 50 33     658 chmod $mode, $name or croak "chmod($name, $mode): $!"
182             if defined $mode;
183             }
184 6     6   14 sub _slurp($name) {
  6         12  
  6         12  
185 6 50       224 open my $fh, '<:raw', $name or die "open(<$name): $!";
186 6         31 local $/;
187 6         230 my $ret= scalar <$fh>;
188 6 50       71 close $fh or die "close($name): $!";
189 6         121 $ret;
190             }
191              
192             our @_linux_shadow_fields= qw( passwd pw_change_time pw_min_days pw_max_days pw_warn_days pw_inactive_days expire_time );
193              
194             # Unix time ignores leap seconds, so the conversion from seconds to days is simple division
195 10 100   10   15 sub _time_to_days_since_1970($t) { !length $t? undef : int($t / 86400) }
  10         19  
  10         16  
  10         219  
196 8 100   8   12 sub _days_since_1970_to_time($d) { !length $d? undef : $d * 86400; }
  8         16  
  8         12  
  8         37  
197              
198 2     2   5 sub _parse_linux_passwd_format($self, $files) {
  2         5  
  2         5  
  2         4  
199 2         7 my @users;
200             my %users;
201 2         14 for (split "\n", $files->{passwd}) {
202 7 50       31 next if /^\s*(#|\z)/;
203 7         14 my %r;
204 7         59 @r{qw( name pw_flag uid gid gecos dir shell )}= split ':';
205 7         17 delete $r{pw_flag};
206 7         33 push @users, \%r;
207 7   50     45 $users{$r{name}} //= \%r;
208             }
209              
210 2   50     14 for (split "\n", ($files->{shadow}//'')) {
211 4 50       20 next if /^\s*(#|\z)/;
212 4         21 my ($name,@vals)= split ':';
213             my $r= $users{$name}
214 4 50       17 or do { carp "Found shadow entry for non-existent user '$name'"; next; };
  0         0  
  0         0  
215 4         10 @{$r}{@_linux_shadow_fields}= @vals;
  4         30  
216 4         15 $r->{pw_change_time}= _days_since_1970_to_time($r->{pw_change_time});
217 4         11 $r->{expire_time}= _days_since_1970_to_time($r->{expire_time});
218             }
219              
220 2         5 my @groups;
221 2         10 for (split "\n", $files->{group}) {
222 9 50       31 next if /^\s*(#|\z)/;
223 9         17 my %r;
224 9         38 @r{qw( name passwd gid members )}= split ":";
225 9         24 push @groups, \%r;
226             }
227 2         15 return { users => \@users, groups => \@groups };
228             }
229              
230 4     4   9 sub _generate_linux_passwd_format($self, %options) {
  4         6  
  4         10  
  4         7  
231 4         15 my ($passwd, $group, $shadow)= ('','','');
232 4         49 my @users= sort { $a->uid <=> $b->uid } values $self->{users}->%*;
  5         19  
233 4         17 my @groups= sort { $a->gid <=> $b->gid } values $self->{groups}->%*;
  11         27  
234              
235             # Generate passwd content
236 4         15 for my $user (@users) {
237 9         15 my $gid;
238 9 50       24 if ($user->group) {
239 9 50       26 my $group= $self->group($user->group)
240             or croak "User '".$user->name."' has invalid group '".$user->group."'";
241 9         54 $gid= $group->gid;
242             } else {
243 0 0       0 $gid= $user->gid
244             or croak "User '".$user->name."' lacks 'group' or 'gid' attribute";
245             }
246             # If shadow fields exist, write a 'x' in passwd, else '*'.
247 9         21 my $pw_flag= '*';
248 9 100       20 if (grep defined, @{$user}{@_linux_shadow_fields}) {
  9         69  
249 5         8 $pw_flag= 'x';
250 5   50     16 $shadow .= sprintf "%s:%s:%s:%s:%s:%s:%s:%s:%s\n",
      100        
      100        
      100        
      100        
      50        
      50        
251             $user->name,
252             $user->passwd // '*',
253             _time_to_days_since_1970($user->pw_change_time) // '',
254             $user->pw_min_days // '',
255             $user->pw_max_days // '',
256             $user->pw_warn_days // '',
257             $user->pw_inactive_days // '',
258             _time_to_days_since_1970($user->expire_time) // '',
259             ''; # reserved field, no idea what to name it in user object...
260             }
261 9   100     31 $passwd .= sprintf "%s:%s:%d:%d:%s:%s:%s\n",
      100        
      100        
262             $user->name, $pw_flag, $user->uid, $gid,
263             $user->gecos//'', $user->dir//'', $user->shell//'';
264             }
265              
266             # Generate group content
267 4         11 for my $g (@groups) {
268 12         32 my $grnam= $g->name;
269             # Collect members from users who have this group, excluding users whose primary
270             # group is already the group.
271 12   100     37 my @members= map $_->name, grep $_->groups->{$grnam} && ($_->group//'') ne $grnam, @users;
272 12   100     91 $group .= sprintf "%s:%s:%d:%s\n",
273             $grnam, $g->passwd // '*', $g->gid, join ',', sort @members;
274             }
275              
276 4         34 return { passwd => $passwd, group => $group, shadow => $shadow };
277             }
278              
279              
280 3     3 1 5 sub import_user($self, $name_or_obj, %attrs) {
  3         5  
  3         6  
  3         4  
  3         5  
281 3 50 66     17 ref $name_or_obj or length $name_or_obj or croak "Attempt to import empty username";
282 3 100 66     8 if (isa_hash($name_or_obj) || isa_user($name_or_obj)) {
    50          
283 2         8 %attrs= ( %$name_or_obj, %attrs );
284             } elsif (keys %attrs) {
285 0         0 $attrs{name}= "$name_or_obj";
286             } else {
287 1 50       10 my $pw= getpwnam($name_or_obj)
288             or croak "User '$name_or_obj' not found in system";
289 1         1050 $attrs{name}= $pw->name;
290 1         30 $attrs{passwd}= $pw->passwd;
291 1         31 $attrs{uid}= $pw->uid;
292 1 50       12 $attrs{quota}= $pw->quota if pw_has('quota'); # BSD
293 1 50       58 $attrs{class}= $pw->class if pw_has('class'); # BSD
294 1 50       25 $attrs{comment}= $pw->comment if pw_has('comment'); # always empty on Linux and BSD...
295 1 50       21 $attrs{gecos}= $pw->gecos if pw_has('gecos');
296 1         72 $attrs{dir}= $pw->dir;
297 1         30 $attrs{shell}= $pw->shell;
298 1 50       12 if (pw_has('expire')) {
299             # Normalize this field to epoch seconds, based on the current platform.
300             # FreeBSD has expire in seconds. Linux has an expire field of days, in /etc/shadow, but
301             # all tests on Linux so far have had pw_has('expire') = false.
302             # getpwnam dies with 'not implemented' on Strawberry perl for Windows.
303 0 0       0 $attrs{expire_time}= $^O eq 'FreeBSD'? $pw->expire
    0          
304             : $^O eq 'linux'? _days_since_1970_to_time($pw->expire)
305             : undef;
306             }
307             # convert gid to group name
308 1         42 my $gid= $pw->gid;
309 1 50 0     164 if (my $grnam= getgrgid($gid)) {
    0          
310 1         19 $attrs{group}= $grnam;
311             } elsif ($gid == $pw->uid || $pw->uid < 1000) {
312             # If the group wasn't found, but the uid and gid are identical and look like system
313             # accounts then assume the group will be the same name.
314 0         0 $attrs{group}= $pw->name;
315             } else {
316 0         0 carp "User '$name_or_obj' primary group $gid doesn't exist";
317 0         0 $attrs{group}= 'nogroup'; # it has to be something. Could croak instead of this...
318             }
319             }
320 3   50     10 $attrs{group} //= 'nogroup';
321 3   100     14 $attrs{groups} //= {};
322              
323             # Check for UID collision
324             defined $attrs{uid}
325 3 50       9 or croak "Can't import user $attrs{name} without a 'uid'";
326 3 50       16 if ($self->{uids}{$attrs{uid}}) {
327 0 0       0 if ($attrs{uid} >= 1000) {
328 0         0 ++$attrs{uid} while exists $self->{uids}{$attrs{uid}};
329             } else {
330 0         0 for (101..999) {
331 0 0       0 if (!exists $self->{uids}{$_}) {
332 0         0 $attrs{uid}= $_;
333 0         0 last;
334             }
335             }
336             croak "No available UIDs below 1000 for $attrs{name}"
337 0 0       0 if $self->{uids}{$attrs{uid}};
338             }
339             }
340              
341             # do the groups exist? Calling ->group will trigger auto_import if enabled.
342 3         16 for my $gname (grep !$self->group($_), $attrs{group}, keys $attrs{groups}->%*) {
343             # Is the group name the same as the user name? Try creating with GID = UID
344 0 0       0 if ($gname eq $attrs{name}) {
    0          
345 0         0 $self->import_group($gname, gid => $attrs{uid});
346             } elsif ($gname =~ /^(nobody|nogroup)/) {
347 0         0 $self->import_group($gname, gid => 65534);
348             } else {
349 0         0 croak "User '$attrs{name}' references non-existent group '$gname'";
350             }
351             }
352              
353 3         18 my $u= Sys::Export::Unix::UserDB::User->new(%attrs);
354 3         12 $self->_add_user_object($u);
355 3         16 return $u;
356             }
357              
358              
359 4     4 1 6 sub import_group($self, $name_or_obj, %attrs) {
  4         6  
  4         5  
  4         7  
  4         4  
360 4 50 66     13 ref $name_or_obj or length $name_or_obj or croak "Attempt to import empty username";
361 4 100 66     9 if (isa_hash($name_or_obj) || isa_group($name_or_obj)) {
    50          
362 3         8 %attrs= ( %$name_or_obj, %attrs );
363             } elsif (keys %attrs) {
364 0         0 $attrs{name}= "$name_or_obj";
365             } else {
366 1 50       74 my ($grnam, $passwd, $gid, $members) = getgrnam($name_or_obj)
367             or croak "Group '$name_or_obj' not found in system";
368 1         7 $attrs{name}= $grnam;
369 1         5 $attrs{passwd}= $passwd;
370 1         3 $attrs{gid}= $gid;
371 1         5 $attrs{members}= $members;
372             }
373 4         8 my $members= delete $attrs{members};
374 4         20 my $g= Sys::Export::Unix::UserDB::Group->new(%attrs);
375 4         14 $self->_add_group_object($g);
376              
377             # Can't store member list in group, so store these for later when a user gets added
378 4 100       11 if (defined $members) {
379             $self->_lazy_add_user_to_group($_, $attrs{name})
380 1 50       4 for (isa_array $members? @$members
    50          
381             : isa_hash $members? keys %$members
382             : split / /, $members);
383             }
384 4         16 return $g;
385             }
386              
387              
388 15     15 1 749 sub add_user($self, $name_or_obj, %attrs) {
  15         38  
  15         23  
  15         56  
  15         22  
389 15 100 66     37 if (isa_hash($name_or_obj) || isa_user($name_or_obj)) {
390 1         9 %attrs= ( %$name_or_obj, %attrs );
391             } else {
392 14         26 my $name= "$name_or_obj";
393 14 50 0     32 if (keys %attrs) {
    0          
394 14         37 $attrs{name}= $name;
395             }
396             # trigger an import if just a name, and auto_import enabled
397             elsif ($self->auto_import && !$self->{users}{$name}) {
398 0 0       0 $self->user($name)
399             or croak "Failed to import user $name";
400 0         0 return $self;
401             }
402             }
403 15         67 $self->_add_user_object(Sys::Export::Unix::UserDB::User->new(%attrs));
404             }
405              
406              
407 14     14 1 1415 sub add_group($self, $name_or_obj, %attrs) {
  14         22  
  14         43  
  14         48  
  14         24  
408 14 100 66     32 if (isa_hash($name_or_obj) || isa_group($name_or_obj)) {
409 1         6 %attrs= ( %$name_or_obj, %attrs );
410             } else {
411 13         26 my $name= "$name_or_obj";
412 13 50 0     30 if (keys %attrs) {
    0          
413 13         35 $attrs{name}= $name;
414             }
415             # trigger an import if just a name, and auto_import enabled
416             elsif ($self->auto_import && !$self->{groups}{$name}) {
417 0 0       0 $self->group($name)
418             or croak "Failed to import group $name";
419 0         0 return $self;
420             }
421             }
422 14         59 $self->_add_group_object(Sys::Export::Unix::UserDB::Group->new(%attrs));
423             }
424              
425              
426 16     16 1 697 sub user($self, $spec) {
  16         26  
  16         29  
  16         19  
427 16 50       46 length $spec or return undef;
428 16 100       35 my $u= isa_int $spec? $self->{uids}{$spec} : $self->{users}{$spec};
429 16 100 100     54 if (!$u && $self->auto_import) {
430 3 100       8 if (isa_userdb $self->auto_import) {
431 2   50     3 my $peer_u= $self->auto_import->user($spec) // return undef;
432 2 50       3 $u= eval { $self->import_user($peer_u) } or warn $@;
  2         3  
433             } else {
434 1 50 0     5 my $name= isa_int $spec? (getpwuid($spec) // return undef) : $spec;
435 1 50       5 $u= eval { $self->import_user($name) } or warn $@;
  1         6  
436             }
437             }
438 16         73 $u;
439             }
440              
441 4     4 1 10 sub has_user($self, $spec) {
  4         10  
  4         8  
  4         9  
442 4 100       13 defined(isa_int $spec? $self->{uids}{$spec} : $self->{users}{$spec});
443             }
444              
445 69     69 1 640 sub group($self, $spec) {
  69         102  
  69         157  
  69         87  
446 69 50       135 length $spec or return undef;
447 69 100       126 my $g= isa_int $spec? $self->{gids}{$spec} : $self->{groups}{$spec};
448 69 100 100     205 if (!$g && $self->auto_import) {
449 4 100       10 if (isa_userdb $self->auto_import) {
450 3   50     5 my $peer_g= $self->auto_import->group($spec) // return undef;
451 3 50       4 $g= eval { $self->import_group($peer_g) } or warn $@;
  3         6  
452             } else {
453 1 50 0     5 my $name= isa_int $spec? (getgrgid($spec) // return undef) : $spec;
454 1 50       4 $g= eval { $self->import_group($name) } or warn $@;
  1         6  
455             }
456             }
457 69         226 $g;
458             }
459              
460 4     4 1 12 sub has_group($self, $spec) {
  4         39  
  4         9  
  4         6  
461 4 100       14 defined(isa_int $spec? $self->{gids}{$spec} : $self->{groups}{$spec});
462             }
463              
464             # Private methods
465              
466             # Allows adding user to group before user is defined
467 0     0   0 sub _lazy_add_user_to_group($self, $unam, $grnam) {
  0         0  
  0         0  
  0         0  
  0         0  
468 0 0       0 if (my $u= $self->{users}{$unam}) {
469 0         0 $u->add_group($grnam);
470             } else {
471 0         0 push $self->{_lazy_add_user_to_group}{$unam}->@*, $grnam;
472             }
473             }
474              
475 25     25   45 sub _add_user_object($self, $user) {
  25         39  
  25         40  
  25         35  
476 25         56 my $name = $user->name;
477 25         76 my $uid = $user->uid;
478 25 50       55 $self->is_valid_name($name) or croak "Invalid user name '$name'";
479            
480             # Check for name conflicts
481             croak "Username '$name' already exists"
482 25 100       110 if defined $self->{users}{$name};
483            
484             # Warn about UID conflicts
485             carp "UID $uid already exists for user '".$self->{uids}{$uid}->name."', now also used by '$name'"
486 24 100       74 if defined $self->{uids}{$uid};
487              
488             # Check for references to non-existent groups
489             # If auto_import is enabled, accessing ->group will trigger their creation.
490 24 50       112 for ((isa_int $user->group? () : ($user->group)), keys $user->groups->%*) {
491 39 50       79 $self->is_valid_name($_)
492             or croak "Invalid group name '$_' for user '$name'";
493             # add the user temporarily so auto_import feature can see it
494 39 100       113 local $self->{users}{$name} = $user
495             if $self->auto_import;
496 39 50       84 croak "User '$name' references non-existent group '$_'"
497             unless $self->group($_);
498             }
499              
500             # Add lazy group membership from earlier
501 24 50       84 if (my $lazy= delete $self->{_lazy_add_user_to_group}{$name}) {
502             $self->{groups}{$_} && $user->add_group($_)
503 0   0     0 for @$lazy;
504             }
505              
506 24   66     127 $self->{uids}{$uid} //= $user;
507 24         149 $self->{users}{$name} = $user;
508             }
509              
510 27     27   42 sub _add_group_object($self, $group) {
  27         42  
  27         54  
  27         39  
511 27         60 my $name = $group->name;
512 27         82 my $gid = $group->gid;
513 27 50       61 $self->is_valid_name($name) or croak "Invalid group name '$name'";
514            
515             # Check for name conflicts
516             croak "Group name '$name' already exists"
517 27 100       83 if defined $self->{groups}{$name};
518            
519             # Warn about GID conflicts
520             carp "GID $gid already exists for group '".$self->{gids}{$gid}->name."', now also used by '$name'"
521 26 100       73 if defined $self->{gids}{$gid};
522            
523 26   66     136 $self->{gids}{$gid} //= $group;
524 26         115 $self->{groups}{$name} = $group;
525             }
526              
527              
528             package Sys::Export::Unix::UserDB::User {
529 2     2   29 use v5.26;
  2         8  
530 2     2   11 use warnings;
  2         4  
  2         193  
531 2     2   13 use experimental qw( signatures );
  2         3  
  2         17  
532             our @CARP_NOT= qw( Sys::Export::Unix::UserDB );
533             our %known_attrs= map +($_ => 1), qw( name uid passwd group groups comment gecos dir shell
534             quota class pw_change_time pw_min_days pw_max_days pw_warn_days pw_inactive_days expire_time );
535 30     30   4151 sub new($class, %attrs) {
  30         60  
  30         152  
  30         43  
536             my $self= bless {
537             name => delete $attrs{name},
538             uid => delete $attrs{uid},
539             group => delete $attrs{group},
540 30         146 groups => {},
541             }, $class;
542 30 100       95 croak "User 'name' is required" unless defined $self->{name};
543 29 100       69 croak "User 'uid' is required" unless defined $self->{uid};
544 28 100       93 unless (defined $self->{group}) {
545             # pull primary group from first element of list if not provided
546 5 100       12 if (isa_array $attrs{groups}) {
547 4         7 $self->{group}= $attrs{groups}[0];
548             }
549 5 100       15 croak "User primary 'group' is required" unless length $self->{group};
550             }
551 27         73 for my $key (keys %attrs) {
552 83 50       231 carp "Unknown user attribute '$key'" unless $known_attrs{$key};
553 83         2223 $self->$key($attrs{$key});
554             }
555 27         133 return $self;
556             }
557 1     1   3 sub clone($self, %attrs) {
  1         3  
  1         2  
  1         19  
558 1         9 return ref($self)->new( %$self, %attrs );
559             }
560            
561             # Read-only attributes
562 48     48   1418 sub name($self) { $self->{name} }
  48         67  
  48         67  
  48         306  
563 54     54   5297 sub uid($self) { $self->{uid} }
  54         79  
  54         84  
  54         468  
564            
565             # Writable attributes
566 84     84   1408 sub group($self, @val) {
  84         113  
  84         121  
  84         108  
567 84 50       330 @val? ($self->{group}= $val[0]) : $self->{group};
568             }
569            
570 78     78   693 sub groups($self, @val) {
  78         106  
  78         116  
  78         106  
571 78 100       176 if (@val) {
572 14 50 33     83 if (@val > 1 || !ref $val[0]) {
    100          
    50          
573 0         0 $self->{groups}= { map +($_ => 1), @val };
574             } elsif (isa_array $val[0]) {
575 9         31 $self->{groups}= { map +($_ => 1), @{$val[0]} };
  9         55  
576             } elsif (isa_hash $val[0]) {
577 5         11 $self->{groups}= { %{$val[0]} };
  5         27  
578             } else {
579 0         0 $self->{groups}= { $val[0] => 1 }; # just stringify it
580             }
581             }
582             $self->{groups}
583 78         285 }
584              
585 7     7   16 sub add_group($self, $group_name) {
  7         12  
  7         62  
  7         12  
586 7         22 $self->{groups}{$group_name}= 1;
587 7         14 return $self;
588             }
589            
590 1     1   3 sub remove_group($self, $group_name) {
  1         2  
  1         3  
  1         2  
591 1         4 delete $self->{groups}{$group_name};
592 1         2 return $self;
593             }
594              
595             {
596             # Generate generic read/write accessors for all other known attributes
597             my $pl= join "\n",
598             map <<~PL, grep !__PACKAGE__->can($_), keys %known_attrs;
599             sub $_(\$self, \@val) {
600             \@val ? (\$self->{$_} = \$val[0]) : \$self->{$_};
601             }
602             PL
603 0 0   0   0 eval $pl." 1" or croak $@;
  0 0   0   0  
  0 100   23   0  
  0 100   9   0  
  0 100   24   0  
  0 100   22   0  
  0 100   11   0  
  0 100   9   0  
  0 100   9   0  
  0 100   9   0  
  23 100   9   555  
  23 0   0   44  
  23 100   23   66  
  23         37  
  23         321  
  9         22  
  9         17  
  9         18  
  9         12  
  9         56  
  24         91  
  24         51  
  24         48  
  24         42  
  24         318  
  22         1669  
  22         40  
  22         42  
  22         38  
  22         244  
  11         323  
  11         17  
  11         24  
  11         18  
  11         57  
  9         23  
  9         16  
  9         16  
  9         16  
  9         176  
  9         18  
  9         16  
  9         19  
  9         16  
  9         151  
  9         22  
  9         17  
  9         17  
  9         17  
  9         182  
  9         18  
  9         16  
  9         19  
  9         16  
  9         169  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  23         329  
  23         43  
  23         41  
  23         39  
  23         148  
604             }
605              
606             # Other generic read/write accessors
607             our $AUTOLOAD;
608             sub AUTOLOAD {
609 0     0   0 my $attr= substr($AUTOLOAD, rindex($AUTOLOAD, ':')+1);
610 0         0 my $self= shift;
611             carp "Unknown user attribute '$attr'"
612 0 0 0     0 unless exists $known_attrs{$attr} || exists $self->{$attr};
613 0 0       0 @_? ( $self->{$attr}= shift ) : $self->{$attr};
614             }
615              
616       0     sub import {}
617       0     sub DESTROY {}
618             }
619              
620              
621             package Sys::Export::Unix::UserDB::Group {
622 2     2   2933 use v5.26;
  2         8  
623 2     2   10 use warnings;
  2         25  
  2         112  
624 2     2   10 use experimental qw( signatures );
  2         4  
  2         9  
625             our @CARP_NOT= qw( Sys::Export::Unix::UserDB );
626             our %known_attrs= map +($_ => 1), qw( name gid passwd );
627              
628 31     31   4834 sub new($class, %attrs) {
  31         50  
  31         121  
  31         45  
629             my $self= bless {
630             name => delete $attrs{name},
631             gid => delete $attrs{gid},
632             passwd => delete $attrs{passwd},
633 31         167 }, $class;
634 31 100       99 croak "Group 'name' is required" unless defined $self->{name};
635 30 100       80 croak "Group 'gid' is required" unless defined $self->{gid};
636 29         70 for my $key (keys %attrs) {
637 0 0       0 carp "Unknown group attribute: '$key'" unless $known_attrs{$key};
638 0         0 $self->$key($attrs{$key});
639             }
640 29         97 return $self;
641             }
642              
643             # Read-only attributes
644 42     42   1364 sub name($self) { $self->{name} }
  42         68  
  42         54  
  42         125  
645 81     81   4097 sub gid($self) { $self->{gid} }
  81         119  
  81         124  
  81         250  
646            
647 1     1   3 sub clone($self, %attrs) {
  1         56  
  1         5  
  1         2  
648 1         8 return ref($self)->new( %$self, %attrs );
649             }
650              
651             # Other generic read/write accessors
652             our $AUTOLOAD;
653             sub AUTOLOAD {
654 15     15   50 my $attr= substr($AUTOLOAD, rindex($AUTOLOAD, ':')+1);
655 15         28 my $self= shift;
656             carp "Unknown group attribute '$attr'"
657 15 0 33     48 unless exists $known_attrs{$attr} || exists $self->{$attr};
658 15 100       82 @_? ( $self->{$attr}= shift ) : $self->{$attr};
659             }
660              
661       0     sub import {}
662       0     sub DESTROY {}
663             }
664              
665             # Avoiding dependency on namespace::clean
666 2     2   1461 { no strict 'refs';
  2         5  
  2         278  
667             delete @{"Sys::Export::Unix::"}{qw(
668             croak carp catfile dclone getpwnam pw_has
669             isa_export_dst isa_exporter isa_group isa_user isa_userdb
670             )};
671             }
672              
673             1;
674              
675             __END__