File Coverage

lib/Provision/Unix/User.pm
Criterion Covered Total %
statement 89 183 48.6
branch 15 72 20.8
condition 6 27 22.2
subroutine 19 28 67.8
pod 8 18 44.4
total 137 328 41.7


line stmt bran cond sub pod time code
1             package Provision::Unix::User;
2             # ABSTRACT: provision unix user accounts
3              
4 4     4   3284 use strict;
  4         11  
  4         579  
5 4     4   20 use warnings;
  4         8  
  4         175  
6              
7             our $VERSION = '0.28';
8              
9 4     4   23 use English qw( -no_match_vars );
  4         7  
  4         33  
10 4     4   2112 use File::Path;
  4         23  
  4         262  
11 4     4   20 use Params::Validate qw( :all );
  4         8  
  4         714  
12              
13 4     4   23 use lib 'lib';
  4         5  
  4         29  
14 4     4   3631 use Provision::Unix::Utility;
  4         15  
  4         9166  
15              
16             my ( $util, $prov );
17              
18             sub new {
19 2     2 1 91 my $class = shift;
20              
21 2         197 my %p = validate(
22             @_,
23             { prov => { type => OBJECT },
24             debug => { type => BOOLEAN, optional => 1, default => 1 },
25             fatal => { type => BOOLEAN, optional => 1, default => 1 },
26             }
27             );
28              
29 2         33 my $self = {
30             prov => $p{prov},
31             debug => $p{debug},
32             fatal => $p{fatal},
33             };
34 2         17 bless( $self, $class );
35              
36 2         8 $prov = $p{prov};
37 2         11 $prov->audit("loaded User");
38 2 50       33 $self->{os} = $self->_get_os() or return;
39              
40 2         93 $util = Provision::Unix::Utility->new( log => $prov );
41 2         17 return $self;
42             }
43              
44             sub create {
45              
46             ############################################
47             # Usage : $user->create( username=>'bob',uid=>501} );
48             # Purpose : creates a new system user
49             # Returns : uid of new user or undef on failure
50             # Parameters :
51             # Required : username
52             # : uid
53             # : guid
54             # Optional : password,
55             # : shell
56             # : homedir
57             # : gecos, quota, uid, gid, expire,
58             # : domain - if set, account homedir is $HOME/$domain
59             # Throws : exceptions
60              
61 6     6 0 631 my $self = shift;
62 6         102 return $self->{os}->create(@_);
63             }
64              
65             sub create_group {
66              
67 2     2 1 3001673 my $self = shift;
68 2         173 return $self->{os}->create_group(@_);
69             }
70              
71             sub modify {
72 0     0 0 0 my $self = shift;
73 0         0 $self->{os}->modify(@_);
74             }
75              
76             sub destroy {
77              
78 2     2 0 1084 my $self = shift;
79 2         41 return $self->{os}->destroy(@_);
80             }
81              
82             sub destroy_group {
83              
84 1     1 0 3001257 my $self = shift;
85 1         29 return $self->{os}->destroy_group(@_);
86             }
87              
88             sub exists {
89              
90             ############################################
91             # Usage : $user->exists('builder_bob')
92             # Purpose : Check if a user account exists
93             # Returns : the uid of the user or undef
94             # Parameters :
95             # Throws : no exceptions
96             # Comments : Use this before adding a new user (error trapping)
97             # and also after adding a user to verify success.
98              
99 2     2 0 1838 my $self = shift;
100 2         13 return $self->{os}->exists(@_);
101             }
102              
103             sub exists_group {
104              
105             ############################################
106             # Usage : $user->exists_group('builder_bob')
107             # Purpose : Check if a group exists
108             # Returns : the gid of the group or undef
109             # Parameters :
110             # Throws : no exceptions
111             # Comments : Use this before adding a new group (error trapping)
112             # and also after adding to verify success.
113              
114 7     7 0 32307 my $self = shift;
115 7         198 return $self->{os}->exists_group(@_);
116             }
117              
118             sub set_password {
119 0     0 0 0 my $self = shift;
120 0         0 return $self->{os}->set_password(@_);
121             };
122              
123             sub quota_set {
124              
125             # Quota::setqlim($dev, $uid, $bs, $bh, $is, $ih, $tlo, $isgrp);
126             # $dev - filesystem mount or device
127             # $bs, $is - soft limits for blocks and inodes
128             # $bh, $ih - hard limits for blocks and inodes
129             # $tlo - time limits (0 = first user write, 1 = 7 days)
130             # $isgrp - 1 means that uid = gid, group limits set
131              
132 1     1 0 26160 my $self = shift;
133              
134             # parameter validation here
135 1         301 my %p = validate(
136             @_,
137             { 'conf' => { type => HASHREF, optional => 1, },
138             'user' => { type => SCALAR, optional => 0, },
139             'quota' => { type => SCALAR, optional => 1, default => 100 },
140             'fatal' => { type => BOOLEAN, optional => 1, default => 1 },
141             'debug' => { type => BOOLEAN, optional => 1, default => 1 },
142             },
143             );
144              
145 1         18 my ( $conf, $username, $quota, $fatal, $debug )
146             = ( $p{conf}, $p{user}, $p{quota}, $p{fatal}, $p{debug} );
147              
148 1         12 require Quota;
149              
150 1   50     21 my $dev = $conf->{quota_filesystem} || "/home";
151 1 50       77 my $uid = getpwnam($username) or return $prov->error("no such user: $username");
152              
153             # set the soft limit a few megs higher than the hard limit
154 1         4 my $quotabump = $quota + 5;
155              
156 1 50       7 print "quota_set: setting $quota MB quota for $username ($uid) on $dev\n"
157             if $debug;
158              
159             # convert from megs to 1K blocks
160 1         7 my $bh = $quota * 1024;
161 1         3 my $bs = $quotabump * 1024;
162              
163 1   50     14 my $is = $conf->{quota_inodes_soft} || 0;
164 1   50     8 my $ih = $conf->{quota_inodes_hard} || 0;
165              
166 1         55 Quota::setqlim( $dev, $uid, $bs, $bh, $is, $ih, 1, 0 );
167              
168 1 50       5 print "user: end.\n" if $debug;
169              
170             # we should test the quota here and then return an appropriate result code
171 1         16 return 1;
172             }
173              
174             sub show {
175              
176              
177 0     0 1 0 my ( $self, $user ) = @_;
178              
179 0 0       0 unless ($user) {
180 0         0 return { 'error_code' => 500, 'error_desc' => 'invalid user' };
181             }
182              
183 0 0       0 print "user_show: $user show function...\n" if $self->{debug};
184 0         0 $prov->syscmd( "quota $user" );
185 0         0 return { 'error_code' => 100, 'error_desc' => 'all is well' };
186             }
187              
188             sub disable {
189              
190              
191 0     0 1 0 my ( $self, $user ) = @_;
192              
193 0         0 my $r;
194              
195 0   0     0 my $pw = $util->find_bin( 'pw' ) || '/usr/sbin/pw';
196              
197 0 0 0     0 if ( getpwnam($user) && getpwnam($user) > 0 ) # Make sure user exists
198             {
199 0         0 my $cmd = "$pw usermod -n $user -e -1m";
200              
201 0 0       0 if ( $util->syscmd( $cmd ) ) {
202             return {
203 0         0 'error_code' => 200,
204             'error_desc' => "disable: success. $user has been disabled."
205             };
206             }
207             else {
208             return {
209 0         0 'error_code' => 500,
210             'error_desc' => "disable: FAILED. $user not disabled."
211             };
212             }
213             }
214             else {
215             return {
216 0         0 'error_code' => 100,
217             'error_desc' => "disable: $user does not exist."
218             };
219             }
220             }
221              
222             sub enable {
223              
224              
225 0     0 1 0 my ( $self, $vals ) = @_;
226              
227 0         0 my $r;
228              
229 0         0 my $user = $vals->{user};
230 0         0 my $pw = '/usr/sbin/pw';
231              
232 0 0       0 if ( $self->exists($user) ) # Make sure user exists
233             {
234 0         0 my $cmd = "$pw usermod -n $user -e ''";
235              
236             # if ( $prov->syscmd( $cmd ) ) {
237             # $r = {
238             # 'error_code' => 200,
239             # 'error_desc' => "enable: success. $user has been enabled."
240             # };
241             # return $r;
242             # }
243             # else {
244 0         0 $r = {
245             'error_code' => 500,
246             'error_desc' => "enable: FAILED. $user not enabled."
247             };
248 0         0 return $r;
249              
250             # }
251             }
252             else {
253             return {
254 0         0 'error_code' => 100,
255             'error_desc' => "disable: $user does not exist."
256             };
257             }
258             }
259              
260             sub install_ssh_key {
261 0     0 0 0 my $self = shift;
262 0         0 my %p = validate( @_, {
263             homedir => { type => SCALAR },
264             ssh_key => { type => SCALAR },
265             ssh_restricted => { type => SCALAR|UNDEF, optional => 1 },
266             debug => { type => BOOLEAN, optional => 1 },
267             fatal => { type => BOOLEAN, optional => 1 },
268             username => { type => SCALAR, optional => 1 },
269             }
270             );
271              
272 0         0 my $homedir = $p{homedir};
273 0         0 my $key = $p{ssh_key};
274 0         0 my $restricted = $p{ssh_restricted};
275 0 0       0 my $debug = defined $p{debug} ? $p{debug} : $self->{debug};
276 0 0       0 my $fatal = defined $p{fatal} ? $p{fatal} : $self->{fatal};
277              
278 0 0       0 if ( ! -d $homedir ) {
279 0         0 return $prov->error( "dir '$homedir' does not exist!",
280             debug => $debug,
281             fatal => $fatal,
282             );
283             };
284              
285 0         0 my $ssh_dir = "$homedir/.ssh";
286 0 0 0     0 mkpath($ssh_dir, 0, oct(700)) if ( ! -d $ssh_dir && ! -e $ssh_dir );
287 0 0       0 -d $ssh_dir or return $prov->error( "unable to create $ssh_dir", fatal => $fatal );
288              
289 0         0 my $line;
290 0 0       0 $line .= "command=\"$restricted\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding "
291             if $restricted;
292 0         0 $line .= "$key\n";
293 0 0       0 $util->file_write( "$ssh_dir/authorized_keys",
294             lines => [ $line ],
295             mode => '0600',
296             debug => 0,
297             fatal => 0,
298             ) or return;
299              
300 0 0       0 if ( $p{username} ) {
301 0         0 my $uid = getpwnam $p{username};
302 0 0       0 if ( $uid ) {
303 0         0 $util->chown( $ssh_dir, uid => $uid, fatal => 0 );
304 0         0 $util->chown( "$ssh_dir/authorized_keys", uid => $uid, fatal => 0 );
305             }
306             else {
307 0         0 my $chown = $util->find_bin( 'chown', debug => 0 );
308 0         0 $util->syscmd( "$chown -R $p{username} $homedir/.ssh", fatal => 0, debug => 0 );
309             };
310             };
311             };
312              
313             sub is_valid_password {
314              
315              
316 0     0 1 0 my ( $self, $pass, $user ) = @_;
317 0         0 my %r = ( error_code => 400 );
318              
319             # min 6 characters
320 0 0       0 if ( length($pass) < 6 ) {
321 0         0 $r{error_desc}
322             = "Passwords must have at least six characters. $pass is too short.";
323 0         0 return \%r;
324             }
325              
326             # max 128 characters
327 0 0       0 if ( length($pass) > 128 ) {
328 0         0 $r{error_desc}
329             = "Passwords must have no more than 128 characters. $pass is too long.";
330 0         0 return \%r;
331             }
332              
333             # not purely alpha or numeric
334 0 0 0     0 if ( $pass =~ /a-z/ or $pass =~ /A-Z/ or $pass =~ /0-9/ ) {
      0        
335 0         0 $r{error_desc} = "Passwords must contain both letters and numbers!";
336 0         0 return \%r;
337             }
338              
339             # does not match username
340 0 0       0 if ( $pass eq $user ) {
341 0         0 $r{error_desc} = "The username and password must not match!";
342 0         0 return \%r;
343             }
344              
345 0 0       0 if ( -r "/usr/local/etc/passwd.badpass" ) {
346              
347 0         0 my @lines = $util->file_read( "/usr/local/etc/passwd.badpass" );
348 0         0 foreach my $line (@lines) {
349 0         0 chomp $line;
350 0 0       0 if ( $pass eq $line ) {
351 0         0 $r{error_desc}
352             = "$pass is a weak password. Please select another.";
353 0         0 return \%r;
354             }
355             }
356             }
357              
358 0         0 $r{error_code} = 100;
359 0         0 return \%r;
360             }
361              
362             sub get_crypted_password {
363              
364              
365 2     2 1 877 my $self = shift;
366 2         8 my $pass = shift;
367 2   33     17 my $salt = shift || $self->get_salt(8);
368              
369 2         2184 my $crypted = crypt($pass, $salt);
370 2         11 return $crypted;
371             };
372              
373             sub get_salt {
374 1     1 0 4 my $self = shift;
375 1   50     6 my $count = shift || 8; # default to 8 chars
376 1         43 my @salt_chars = ('.', '/', 0..9, 'A'..'Z', 'a'..'z'); # from perldoc crypt()
377              
378 1         3 my $salt;
379 1         6 for (1 .. $count) {
380 8         98 $salt .= (@salt_chars)[rand scalar(@salt_chars) ];
381             }
382              
383             # ick. crypt may return different results on platforms that support enhanced crypt
384             # algorithms (ie, DES vs MD5 vs SHA, etc). Use a special prefix to your salt to
385             # select the algorith to choose MD5 ($1$), blowfish ($2$), etc...
386             # real examples with pass 'T3stlN#PaSs' and salt 'ylhEgHiL':
387             # Linux $1$ : $1$ylhEgHiL$rNfB2rqa2JDH9/y8nVyKW. # MD5
388             # FreeBSD $1$ : $1$ylhEgHiL$rNfB2rqa2JDH9/y8nVyKW. # MD5
389             # Mac OS 10.5 $1$ : $1eiJVUGcT0JU # Gack, no MD5 support!
390             # Linux : yl0FgzQYzpoVU # DES
391             # FreeBSD : yl0FgzQYzpoVU # DES
392             # Mac OS 10.5 : yl0FgzQYzpoVU # DES
393             # More Info
394             # http://en.wikipedia.org/wiki/Crypt_(Unix)
395             # http://search.cpan.org/~luismunoz/Crypt-PasswdMD5-1.3/PasswdMD5.pm
396             # http://sial.org/howto/perl/password-crypt/
397              
398 1 50       27 if ( $OSNAME =~ /Linux|FreeBSD|Solaris/i ) {
399             #warn "using MD5 password\n";
400 1         15 return '$1$' . $salt;
401             };
402 0         0 return $salt;
403             }
404              
405 0     0 1 0 sub archive {
406              
407             }
408              
409             sub _get_os {
410              
411 2     2   6 my $self = shift;
412 2         17 my $prov = $self->{prov};
413              
414 2         25 my $os = lc($OSNAME);
415              
416 2 50       44 if ( $os eq 'darwin' ) {
    50          
    50          
417 0         0 require Provision::Unix::User::Darwin;
418 0         0 return Provision::Unix::User::Darwin->new(
419             prov => $prov,
420             user => $self
421             );
422             }
423             elsif ( lc($OSNAME) eq 'freebsd' ) {
424 0         0 require Provision::Unix::User::FreeBSD;
425 0         0 return Provision::Unix::User::FreeBSD->new(
426             prov => $prov,
427             user => $self
428             );
429             }
430             elsif ( lc($OSNAME) eq 'linux' ) {
431 2         3259 require Provision::Unix::User::Linux;
432 2         172 return Provision::Unix::User::Linux->new(
433             prov => $prov,
434             user => $self
435             );
436             }
437             else {
438 0         0 $prov->error( "There is no support for $OSNAME yet. Consider submitting a patch.",
439             fatal => 0,
440             );
441             }
442 0         0 return;
443             }
444              
445             sub _is_valid_request {
446              
447 0     0   0 my $self = shift;
448              
449 0         0 $self->{prov}->progress( num => 2, desc => 'validating input' );
450              
451             # check for missing username
452 0 0       0 if ( !$self->{username} ) {
453 0         0 return $prov->progress(
454             num => 10,
455             desc => 'error',
456             err => 'invalid request, missing a value for username',
457             );
458             }
459              
460             # make sure username is valid
461 0 0       0 if ( !$self->_is_valid_username() ) {
462 0         0 return $prov->progress(
463             num => 10,
464             desc => 'error',
465             err => $prov->{errors}->[-1]->{errmsg}
466             );
467             }
468              
469             # is uid set?
470 0 0       0 if ( !$self->{uid} ) {
471 0         0 return $prov->progress(
472             num => 10,
473             desc => 'error',
474             err => "no uid in request, using system assigned UID"
475             );
476             }
477 0         0 return 1;
478             }
479              
480             sub _is_valid_username {
481              
482 16     16   996 my $self = shift;
483              
484             # set this to fully define your username restrictions. It will
485             # get returned every time an invalid username is submitted.
486              
487 16   50     86 my $username
488             = shift
489             || $self->{username}
490             || return $self->{prov}->error( "username missing",
491             location => join( ',', caller ),
492             fatal => 0,
493             debug => 0,
494             );
495              
496             #$prov->audit("checking validity of username $username");
497 16         47 $self->{username} = $username;
498              
499             # min 2 characters
500 16 100       66 if ( length($username) < 2 ) {
501 1         13 return $prov->error( "username $username is too short",
502             location => join( ',', caller ),
503             fatal => 0,
504             debug => 0,
505             );
506             }
507              
508             # max 16 characters
509 15 100       59 if ( length($username) > 16 ) {
510 1         15 return $prov->error( "username $username is too long",
511             location => join( ',', caller ),
512             fatal => 0,
513             debug => 0,
514             );
515             }
516              
517             # only lower case letters and numbers
518 14 100       311 if ( $username =~ /[^a-z0-9]/ ) {
519 3         57 return $prov->error( "username $username has invalid characters",
520             location => join( ',', caller ),
521             fatal => 0,
522             debug => 0,
523             );
524             }
525              
526 11         45 my $reserved = "/usr/local/etc/passwd.reserved";
527 11 50       516 if ( -r $reserved ) {
528 0         0 foreach my $line (
529             $util->file_read( $reserved, fatal => 0, debug => 0 ) )
530             {
531 0 0       0 if ( $username eq $line ) {
532 0         0 return $prov->error( "\t$username is a reserved username.",
533             location => join( ',', caller ),
534             fatal => 0,
535             debug => 1,
536             );
537             }
538             }
539             }
540              
541 11         118 $prov->audit("\tusername $username looks valid");
542 11         63 return 1;
543             }
544              
545             1;
546              
547              
548              
549             =pod
550              
551             =head1 NAME
552              
553             Provision::Unix::User - provision unix user accounts
554              
555             =head1 VERSION
556              
557             version 1.06
558              
559             =head1 SYNOPSIS
560              
561             Handles provisioning operations (create, modify, destroy) for system users on UNIX based operating systems.
562              
563             use Provision::Unix::User;
564              
565             my $prov = Provision::Unix::User->new();
566             ...
567              
568             =head2 show
569              
570             Show user attributes. Right now it only shows quota info.
571              
572             $pass->show( {user=>"matt"} );
573              
574             returns a hashref with error_code and error_desc
575              
576             =head2 disable
577              
578             Disable an /etc/passwd user by expiring their account.
579              
580             $pass->disable( "matt" );
581              
582             =head2 enable
583              
584             Enable an /etc/passwd user by removing the expiration date.
585              
586             $pass->enable( {user=>"matt"} );
587              
588             input is a hashref
589              
590             returns a hashref with error_code and error_desc
591              
592             =head2 is_valid_password
593              
594             Check a password for sanity.
595              
596             $r = $user->is_valid_password($password, $username);
597              
598             $password is the password the user is attempting to use.
599              
600             $username is the username the user has selected.
601              
602             Checks:
603              
604             Passwords must have at least 6 characters.
605             Passwords must have no more than 128 characters.
606             Passwords must not be the same as the username
607             Passwords must not be purely alpha or purely numeric
608             Passwords must not be in reserved list
609             (/usr/local/etc/passwd.badpass)
610              
611             $r is a hashref that gets returned.
612              
613             $r->{error_code} will contain a result code of 100 (success) or (4-500) (failure)
614              
615             $r->{error_desc} will contain a string with a description of which test failed.
616              
617             =head2 get_crypted_password
618              
619             $user->get_crypted_password($pass, [$salt] )
620              
621             get the DES/MD5 digest of the plain text password that is passed in
622              
623             =head1 FUNCTIONS
624              
625             =head2 new
626              
627             Creates and returns a new Provision::Unix::User object.
628              
629             =head2 is_valid_username
630              
631             $user->is_valid_username($username, $denylist);
632              
633             $username is the username. Pass it along as a scalar (string).
634              
635             $denylist is a optional hashref. Define all usernames you want reserved (denied) and it will check to make sure $username is not in the hashref.
636              
637             Checks:
638              
639             * Usernames must be between 2 and 16 characters.
640             * Usernames must have only lower alpha and numeric chars
641             * Usernames must not be defined in $denylist or reserved list
642              
643             The format of $local/etc/passwd.reserved is one username per line.
644              
645             =head2 archive
646              
647             Create's a tarball of the users home directory. Typically done right before you rm -rf their home directory as part of a de-provisioning step.
648              
649             if ( $user->archive("user") )
650             {
651             print "user archived";
652             };
653              
654             returns a boolean.
655              
656             =head2 create_group
657              
658             Installs a system group.
659              
660             $r = $pass->create_group($group, $gid)
661              
662             $r->{error_code} == 200 ? print "success" : print $r->{error_desc};
663              
664             =head1 BUGS
665              
666             Please report any bugs or feature requests to C<bug-unix-provision-user at rt.cpan.org>, or through the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Provision-Unix>. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
667              
668             =head1 SUPPORT
669              
670             You can find documentation for this module with the perldoc command.
671              
672             perldoc Provision::Unix::User
673              
674             You can also look for information at:
675              
676             =over 4
677              
678             =item * RT: CPAN's request tracker
679              
680             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Provision-Unix>
681              
682             =item * AnnoCPAN: Annotated CPAN documentation
683              
684             L<http://annocpan.org/dist/Provision-Unix>
685              
686             =item * CPAN Ratings
687              
688             L<http://cpanratings.perl.org/d/Provision-Unix>
689              
690             =item * Search CPAN
691              
692             L<http://search.cpan.org/dist/Provision-Unix>
693              
694             =back
695              
696             =head1 AUTHOR
697              
698             Matt Simerson <msimerson@cpan.org>
699              
700             =head1 COPYRIGHT AND LICENSE
701              
702             This software is copyright (c) 2013 by The Network People, Inc..
703              
704             This is free software; you can redistribute it and/or modify it under
705             the same terms as the Perl 5 programming language system itself.
706              
707             =cut
708              
709              
710             __END__
711              
712              
713