File Coverage

lib/Provision/Unix/User.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


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