File Coverage

blib/lib/Sys/Export/Unix/UserDB.pm
Criterion Covered Total %
statement 491 557 88.1
branch 169 276 61.2
condition 66 118 55.9
subroutine 69 78 88.4
pod 18 18 100.0
total 813 1047 77.6


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.005'; # VERSION
5              
6 2     2   218159 use v5.26;
  2         6  
7 2     2   10 use warnings;
  2         3  
  2         93  
8 2     2   8 use experimental qw( signatures );
  2         3  
  2         14  
9 2     2   250 use Carp ();
  2         4  
  2         33  
10 2     2   857 use File::Spec::Functions qw( catfile );
  2         1321  
  2         160  
11 2     2   606 use Storable qw( dclone );
  2         3517  
  2         114  
12 2     2   10 use Scalar::Util ();
  2         2  
  2         65  
13 2     2   1022 use User::pwent qw( getpwnam pw_has );
  2         10729  
  2         140  
14 2     2   526 use Sys::Export qw( isa_userdb isa_user isa_group );
  2         4  
  2         11  
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   237 my sub carp { goto \&Carp::carp }
19 9     9   1192 my sub croak { goto \&Carp::croak }
20 51     51   185 my sub isa_hash :prototype($) { ref $_[0] eq 'HASH' }
21 24     24   58 my sub isa_array :prototype($) { ref $_[0] eq 'ARRAY' }
22 119 100   119   436 my sub isa_int :prototype($) { Scalar::Util::looks_like_number($_[0]) && int($_[0]) == $_[0] }
23              
24              
25 13     13 1 193448 sub new($class, @args) {
  13         21  
  13         21  
  13         13  
26 13 100 66     60 my %args= @args == 1 && isa_hash($args[0])? $args[0]->%* : @args;
27 13         65 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       38 $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         21 my $g= delete $args{groups};
39 13         17 my $u= delete $args{users};
40              
41             # Apply writable attributes and/or initial methods
42 13         30 for (keys %args) {
43 5 100       30 croak "Unknown option '$_'" unless $self->can($_);
44 4         14 $self->$_($args{$_});
45             }
46 12 100       22 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         12 $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       26 if ($u) {
56 2 50       4 if (isa_array $u) {
    50          
57 0         0 $self->add_user($_) for $u->@*;
58             } elsif (isa_hash $u) {
59 2         10 $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         36 return $self;
65             }
66              
67              
68 13     13 1 509 sub users($self) { $self->{users} }
  13         17  
  13         15  
  13         82  
69              
70 1     1 1 3 sub uids($self) { $self->{uids} }
  1         1  
  1         1  
  1         4  
71              
72 5     5 1 586 sub groups($self) { $self->{groups} }
  5         6  
  5         5  
  5         36  
73              
74 1     1 1 45 sub gids($self) { $self->{gids} }
  1         5  
  1         2  
  1         4  
75              
76              
77 66     66 1 62 sub auto_import($self, @val) {
  66         62  
  66         61  
  66         60  
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       180 }
    100          
81              
82              
83 1     1 1 2 sub clone($self) {
  1         2  
  1         1  
84 1         144 return dclone($self);
85             }
86              
87              
88 91     91 1 84 sub is_valid_name($self, $name) {
  91         82  
  91         99  
  91         81  
89             defined $self->{valid_name_regex}? scalar( $name =~ $self->{valid_name_regex} )
90 91 50       315 : scalar( $name =~ /^[A-Za-z_][-A-Za-z0-9_.]{0,30}[-A-Za-z0-9_.\$]?\z/ )
91             }
92              
93              
94 3     3 1 318 sub load($self, $path, %options) {
  3         3  
  3         5  
  3         3  
  3         5  
95 3 50       9 croak "Path is required" unless defined $path;
96 3         17 my $passwd_file = catfile($path, 'passwd');
97 3         8 my $group_file = catfile($path, 'group');
98 3         9 my $shadow_file = catfile($path, 'shadow');
99              
100 3 50       8 unless ($options{format}) {
101             # If we have passwd, group, and shadow, assume Linux.
102 3 50 66     175 $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         3 my $records;
108 2 50       6 if ($options{format} eq 'Linux') {
109 2 50       6 $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         6 my (%group_by_name, %group_by_gid, %user_by_name);
120 2         5 for ($records->{groups}->@*) {
121 9   33     23 $group_by_name{$_->{name}} //= $_;
122 9   33     22 $group_by_gid{$_->{gid}} //= $_;
123             }
124 2         5 for ($records->{users}->@*) {
125 7   33     44 $user_by_name{$_->{name}} //= $_;
126 7         10 my $gid= delete $_->{gid};
127 7 50       12 if (my $primary_group= $group_by_gid{$gid}) {
128 7         11 $_->{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         5 for my $g ($records->{groups}->@*) {
139 9 100       15 if (my $members= delete $g->{members}) {
140 4         9 for (split ',', $members) {
141             my $u= $user_by_name{$_}
142 6 50       9 or do { carp "Group '$g->{name}' references non-existent user '$_'"; next; };
  0         0  
  0         0  
143 6         7 push @{$u->{groups}}, $g->{name};
  6         11  
144             }
145             }
146             }
147              
148             $self->_add_group_object(Sys::Export::Unix::UserDB::Group->new(%$_))
149 2         15 for $records->{groups}->@*;
150             $self->_add_user_object(Sys::Export::Unix::UserDB::User->new(%$_))
151 2         13 for $records->{users}->@*;
152              
153 2         47 return $self;
154             }
155              
156              
157 4     4 1 323 sub save($self, $target, %options) {
  4         11  
  4         8  
  4         4  
  4         4  
158 4   33     21 my $format= $options{format} // $self->{default_format} // 'Linux';
      50        
159 4         5 my $data;
160 4 50       20 if ($format eq 'Linux') {
161 4         12 $data= $self->_generate_linux_passwd_format(%options);
162             } else {
163 0         0 croak "Unsupported format $options{format}";
164             }
165 4 100       10 if (isa_hash $target) {
166 2         7 %$target= %$data;
167             } else {
168 2 50       43 croak "path does not exist: '$target'" unless -e $target;
169 2         6 for (keys %$data) {
170 6   100     21 my $public= $_ eq 'passwd' || $_ eq 'group';
171 6 100       44 _mkfile(catfile($target, $_), $data->{$_}, $public? 0755 : 0700);
172             }
173             }
174 4         19 return $self;
175             }
176              
177 6     6   7 sub _mkfile($name, $data, $mode=undef) {
  6         7  
  6         7  
  6         22  
  6         6  
178 6 50       935 open my $fh, '>:raw', $name or croak "open(>$name): $!";
179 6 50       39 $fh->print($data) or croak "write($name): $!";
180 6 50       68 $fh->close or croak "close($name): $!";
181 6 50 33     481 chmod $mode, $name or croak "chmod($name, $mode): $!"
182             if defined $mode;
183             }
184 6     6   7 sub _slurp($name) {
  6         9  
  6         7  
185 6 50       186 open my $fh, '<:raw', $name or die "open(<$name): $!";
186 6         25 local $/;
187 6         105 my $ret= scalar <$fh>;
188 6 50       50 close $fh or die "close($name): $!";
189 6         71 $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   12 sub _time_to_days_since_1970($t) { !length $t? undef : int($t / 86400) }
  10         10  
  10         10  
  10         115  
196 8 100   8   8 sub _days_since_1970_to_time($d) { !length $d? undef : $d * 86400; }
  8         8  
  8         7  
  8         23  
197              
198 2     2   4 sub _parse_linux_passwd_format($self, $files) {
  2         3  
  2         3  
  2         2  
199 2         4 my @users;
200             my %users;
201 2         10 for (split "\n", $files->{passwd}) {
202 7 50       18 next if /^\s*(#|\z)/;
203 7         7 my %r;
204 7         33 @r{qw( name pw_flag uid gid gecos dir shell )}= split ':';
205 7         10 delete $r{pw_flag};
206 7         11 push @users, \%r;
207 7   50     24 $users{$r{name}} //= \%r;
208             }
209              
210 2   50     8 for (split "\n", ($files->{shadow}//'')) {
211 4 50       8 next if /^\s*(#|\z)/;
212 4         12 my ($name,@vals)= split ':';
213             my $r= $users{$name}
214 4 50       11 or do { carp "Found shadow entry for non-existent user '$name'"; next; };
  0         0  
  0         0  
215 4         6 @{$r}{@_linux_shadow_fields}= @vals;
  4         20  
216 4         8 $r->{pw_change_time}= _days_since_1970_to_time($r->{pw_change_time});
217 4         8 $r->{expire_time}= _days_since_1970_to_time($r->{expire_time});
218             }
219              
220 2         2 my @groups;
221 2         7 for (split "\n", $files->{group}) {
222 9 50       15 next if /^\s*(#|\z)/;
223 9         8 my %r;
224 9         21 @r{qw( name passwd gid members )}= split ":";
225 9         13 push @groups, \%r;
226             }
227 2         10 return { users => \@users, groups => \@groups };
228             }
229              
230 4     4   6 sub _generate_linux_passwd_format($self, %options) {
  4         12  
  4         9  
  4         5  
231 4         9 my ($passwd, $group, $shadow)= ('','','');
232 4         21 my @users= sort { $a->uid <=> $b->uid } values $self->{users}->%*;
  5         12  
233 4         10 my @groups= sort { $a->gid <=> $b->gid } values $self->{groups}->%*;
  14         21  
234              
235             # Generate passwd content
236 4         7 for my $user (@users) {
237 9         11 my $gid;
238 9 50       16 if ($user->group) {
239 9 50       13 my $group= $self->group($user->group)
240             or croak "User '".$user->name."' has invalid group '".$user->group."'";
241 9         16 $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         13 my $pw_flag= '*';
248 9 100       33 if (grep defined, @{$user}{@_linux_shadow_fields}) {
  9         71  
249 5         6 $pw_flag= 'x';
250 5   50     10 $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     20 $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         9 for my $g (@groups) {
268 12         16 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     26 my @members= map $_->name, grep $_->groups->{$grnam} && ($_->group//'') ne $grnam, @users;
272 12   100     40 $group .= sprintf "%s:%s:%d:%s\n",
273             $grnam, $g->passwd // '*', $g->gid, join ',', sort @members;
274             }
275              
276 4         19 return { passwd => $passwd, group => $group, shadow => $shadow };
277             }
278              
279              
280 3     3 1 6 sub import_user($self, $name_or_obj, %attrs) {
  3         4  
  3         3  
  3         4  
  3         4  
281 3 50 66     13 ref $name_or_obj or length $name_or_obj or croak "Attempt to import empty username";
282 3 100 66     6 if (isa_hash($name_or_obj) || isa_user($name_or_obj)) {
    50          
283 2         9 %attrs= ( %$name_or_obj, %attrs );
284             } elsif (keys %attrs) {
285 0         0 $attrs{name}= "$name_or_obj";
286             } else {
287 1 50       8 my $pw= getpwnam($name_or_obj)
288             or croak "User '$name_or_obj' not found in system";
289 1         533 $attrs{name}= $pw->name;
290 1         14 $attrs{passwd}= $pw->passwd;
291 1         51 $attrs{uid}= $pw->uid;
292 1 50       9 $attrs{quota}= $pw->quota if pw_has('quota'); # BSD
293 1 50       15 $attrs{class}= $pw->class if pw_has('class'); # BSD
294 1 50       10 $attrs{comment}= $pw->comment if pw_has('comment'); # always empty on Linux and BSD...
295 1 50       9 $attrs{gecos}= $pw->gecos if pw_has('gecos');
296 1         33 $attrs{dir}= $pw->dir;
297 1         12 $attrs{shell}= $pw->shell;
298 1 50       7 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         19 my $gid= $pw->gid;
309 1 50 0     52 if (my $grnam= getgrgid($gid)) {
    0          
310 1         10 $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     8 $attrs{groups} //= {};
322              
323             # Check for UID collision
324             defined $attrs{uid}
325 3 50       6 or croak "Can't import user $attrs{name} without a 'uid'";
326 3 50       10 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         10 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         13 my $u= Sys::Export::Unix::UserDB::User->new(%attrs);
354 3         8 $self->_add_user_object($u);
355 3         11 return $u;
356             }
357              
358              
359 4     4 1 5 sub import_group($self, $name_or_obj, %attrs) {
  4         4  
  4         5  
  4         93  
  4         14  
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     8 if (isa_hash($name_or_obj) || isa_group($name_or_obj)) {
    50          
362 3         7 %attrs= ( %$name_or_obj, %attrs );
363             } elsif (keys %attrs) {
364 0         0 $attrs{name}= "$name_or_obj";
365             } else {
366 1 50       52 my ($grnam, $passwd, $gid, $members) = getgrnam($name_or_obj)
367             or croak "Group '$name_or_obj' not found in system";
368 1         4 $attrs{name}= $grnam;
369 1         2 $attrs{passwd}= $passwd;
370 1         2 $attrs{gid}= $gid;
371 1         2 $attrs{members}= $members;
372             }
373 4         9 my $members= delete $attrs{members};
374 4         14 my $g= Sys::Export::Unix::UserDB::Group->new(%attrs);
375 4         11 $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       6 if (defined $members) {
379             $self->_lazy_add_user_to_group($_, $attrs{name})
380 1 50       3 for (isa_array $members? @$members
    50          
381             : isa_hash $members? keys %$members
382             : split / /, $members);
383             }
384 4         13 return $g;
385             }
386              
387              
388 15     15 1 484 sub add_user($self, $name_or_obj, %attrs) {
  15         16  
  15         13  
  15         45  
  15         15  
389 15 100 66     23 if (isa_hash($name_or_obj) || isa_user($name_or_obj)) {
390 1         5 %attrs= ( %$name_or_obj, %attrs );
391             } else {
392 14         17 my $name= "$name_or_obj";
393 14 50 0     21 if (keys %attrs) {
    0          
394 14         21 $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         44 $self->_add_user_object(Sys::Export::Unix::UserDB::User->new(%attrs));
404             }
405              
406              
407 14     14 1 863 sub add_group($self, $name_or_obj, %attrs) {
  14         28  
  14         18  
  14         22  
  14         12  
408 14 100 66     24 if (isa_hash($name_or_obj) || isa_group($name_or_obj)) {
409 1         4 %attrs= ( %$name_or_obj, %attrs );
410             } else {
411 13         19 my $name= "$name_or_obj";
412 13 50 0     21 if (keys %attrs) {
    0          
413 13         20 $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         57 $self->_add_group_object(Sys::Export::Unix::UserDB::Group->new(%attrs));
423             }
424              
425              
426 16     16 1 467 sub user($self, $spec) {
  16         16  
  16         18  
  16         26  
427 16 50       29 length $spec or return undef;
428 16 100       25 my $u= isa_int $spec? $self->{uids}{$spec} : $self->{users}{$spec};
429 16 100 100     36 if (!$u && $self->auto_import) {
430 3 100       8 if (isa_userdb $self->auto_import) {
431 2   50     4 my $peer_u= $self->auto_import->user($spec) // return undef;
432 2 50       3 $u= eval { $self->import_user($peer_u) } or warn $@;
  2         4  
433             } else {
434 1 50 0     4 my $name= isa_int $spec? (getpwuid($spec) // return undef) : $spec;
435 1 50       2 $u= eval { $self->import_user($name) } or warn $@;
  1         4  
436             }
437             }
438 16         55 $u;
439             }
440              
441 4     4 1 6 sub has_user($self, $spec) {
  4         5  
  4         6  
  4         5  
442 4 100       5 defined(isa_int $spec? $self->{uids}{$spec} : $self->{users}{$spec});
443             }
444              
445 69     69 1 357 sub group($self, $spec) {
  69         63  
  69         75  
  69         67  
446 69 50       99 length $spec or return undef;
447 69 100       87 my $g= isa_int $spec? $self->{gids}{$spec} : $self->{groups}{$spec};
448 69 100 100     126 if (!$g && $self->auto_import) {
449 4 100       9 if (isa_userdb $self->auto_import) {
450 3   50     5 my $peer_g= $self->auto_import->group($spec) // return undef;
451 3 50       5 $g= eval { $self->import_group($peer_g) } or warn $@;
  3         3  
452             } else {
453 1 50 0     2 my $name= isa_int $spec? (getgrgid($spec) // return undef) : $spec;
454 1 50       2 $g= eval { $self->import_group($name) } or warn $@;
  1         3  
455             }
456             }
457 69         161 $g;
458             }
459              
460 4     4 1 5 sub has_group($self, $spec) {
  4         4  
  4         6  
  4         4  
461 4 100       8 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   27 sub _add_user_object($self, $user) {
  25         25  
  25         22  
  25         28  
476 25         52 my $name = $user->name;
477 25         33 my $uid = $user->uid;
478 25 50       41 $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       51 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       72 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       44 for ((isa_int $user->group? () : ($user->group)), keys $user->groups->%*) {
491 39 50       93 $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       61 local $self->{users}{$name} = $user
495             if $self->auto_import;
496 39 50       59 croak "User '$name' references non-existent group '$_'"
497             unless $self->group($_);
498             }
499              
500             # Add lazy group membership from earlier
501 24 50       56 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     78 $self->{uids}{$uid} //= $user;
507 24         82 $self->{users}{$name} = $user;
508             }
509              
510 27     27   26 sub _add_group_object($self, $group) {
  27         27  
  27         22  
  27         24  
511 27         67 my $name = $group->name;
512 27         64 my $gid = $group->gid;
513 27 50       48 $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       55 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       44 if defined $self->{gids}{$gid};
522            
523 26   66     104 $self->{gids}{$gid} //= $group;
524 26         71 $self->{groups}{$name} = $group;
525             }
526              
527              
528             package Sys::Export::Unix::UserDB::User {
529 2     2   25 use v5.26;
  2         4  
530 2     2   8 use warnings;
  2         2  
  2         112  
531 2     2   9 use experimental qw( signatures );
  2         2  
  2         24  
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   3155 sub new($class, %attrs) {
  30         32  
  30         70  
  30         30  
536             my $self= bless {
537             name => delete $attrs{name},
538             uid => delete $attrs{uid},
539             group => delete $attrs{group},
540 30         90 groups => {},
541             }, $class;
542 30 100       62 croak "User 'name' is required" unless defined $self->{name};
543 29 100       47 croak "User 'uid' is required" unless defined $self->{uid};
544 28 100       38 unless (defined $self->{group}) {
545             # pull primary group from first element of list if not provided
546 5 100       10 if (isa_array $attrs{groups}) {
547 4         7 $self->{group}= $attrs{groups}[0];
548             }
549 5 100       11 croak "User primary 'group' is required" unless length $self->{group};
550             }
551 27         46 for my $key (keys %attrs) {
552 83 50       133 carp "Unknown user attribute '$key'" unless $known_attrs{$key};
553 83         1269 $self->$key($attrs{$key});
554             }
555 27         71 return $self;
556             }
557 1     1   2 sub clone($self, %attrs) {
  1         1  
  1         2  
  1         1  
558 1         5 return ref($self)->new( %$self, %attrs );
559             }
560            
561             # Read-only attributes
562 48     48   886 sub name($self) { $self->{name} }
  48         48  
  48         44  
  48         207  
563 54     54   2971 sub uid($self) { $self->{uid} }
  54         53  
  54         48  
  54         270  
564            
565             # Writable attributes
566 84     84   820 sub group($self, @val) {
  84         86  
  84         98  
  84         71  
567 84 50       189 @val? ($self->{group}= $val[0]) : $self->{group};
568             }
569            
570 78     78   410 sub groups($self, @val) {
  78         83  
  78         70  
  78         74  
571 78 100       125 if (@val) {
572 14 50 33     55 if (@val > 1 || !ref $val[0]) {
    100          
    50          
573 0         0 $self->{groups}= { map +($_ => 1), @val };
574             } elsif (isa_array $val[0]) {
575 9         12 $self->{groups}= { map +($_ => 1), @{$val[0]} };
  9         54  
576             } elsif (isa_hash $val[0]) {
577 5         5 $self->{groups}= { %{$val[0]} };
  5         14  
578             } else {
579 0         0 $self->{groups}= { $val[0] => 1 }; # just stringify it
580             }
581             }
582             $self->{groups}
583 78         190 }
584              
585 7     7   9 sub add_group($self, $group_name) {
  7         8  
  7         9  
  7         7  
586 7         13 $self->{groups}{$group_name}= 1;
587 7         8 return $self;
588             }
589            
590 1     1   3 sub remove_group($self, $group_name) {
  1         2  
  1         1  
  1         2  
591 1         2 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   335  
  23 0   0   27  
  23 100   23   30  
  23         66  
  23         210  
  9         12  
  9         11  
  9         12  
  9         10  
  9         39  
  24         33  
  24         67  
  24         33  
  24         24  
  24         340  
  22         918  
  22         28  
  22         26  
  22         22  
  22         140  
  11         149  
  11         16  
  11         14  
  11         70  
  11         51  
  9         10  
  9         10  
  9         11  
  9         10  
  9         80  
  9         12  
  9         11  
  9         11  
  9         11  
  9         78  
  9         12  
  9         10  
  9         14  
  9         10  
  9         84  
  9         13  
  9         10  
  9         13  
  9         10  
  9         84  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  23         201  
  23         29  
  23         26  
  23         24  
  23         95  
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   2191 use v5.26;
  2         5  
623 2     2   7 use warnings;
  2         2  
  2         87  
624 2     2   8 use experimental qw( signatures );
  2         2  
  2         7  
625             our @CARP_NOT= qw( Sys::Export::Unix::UserDB );
626             our %known_attrs= map +($_ => 1), qw( name gid passwd );
627              
628 31     31   3095 sub new($class, %attrs) {
  31         31  
  31         46  
  31         29  
629             my $self= bless {
630             name => delete $attrs{name},
631             gid => delete $attrs{gid},
632             passwd => delete $attrs{passwd},
633 31         87 }, $class;
634 31 100       70 croak "Group 'name' is required" unless defined $self->{name};
635 30 100       44 croak "Group 'gid' is required" unless defined $self->{gid};
636 29         84 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         57 return $self;
641             }
642              
643             # Read-only attributes
644 42     42   844 sub name($self) { $self->{name} }
  42         35  
  42         36  
  42         72  
645 87     87   2451 sub gid($self) { $self->{gid} }
  87         90  
  87         76  
  87         152  
646            
647 1     1   2 sub clone($self, %attrs) {
  1         2  
  1         1  
  1         1  
648 1         4 return ref($self)->new( %$self, %attrs );
649             }
650              
651             # Other generic read/write accessors
652             our $AUTOLOAD;
653             sub AUTOLOAD {
654 15     15   30 my $attr= substr($AUTOLOAD, rindex($AUTOLOAD, ':')+1);
655 15         18 my $self= shift;
656             carp "Unknown group attribute '$attr'"
657 15 0 33     25 unless exists $known_attrs{$attr} || exists $self->{$attr};
658 15 100       51 @_? ( $self->{$attr}= shift ) : $self->{$attr};
659             }
660              
661       0     sub import {}
662       0     sub DESTROY {}
663             }
664              
665             # Avoiding dependency on namespace::clean
666             delete @{Sys::Export::Unix::UserDB::}{qw( dclone isa_userdb isa_user isa_group getpwnam pw_has )};
667             1;
668              
669             __END__