#!/usr/bin/perl -w ####################################################################### ## ## ## pwx ## ## ## ## pw for macos-x ## ## ## ####################################################################### ## ## ## Summary: A command-line wrapper for the niutil (NetInfo ## ## Utility) program, provided to simplify user ## ## administration from a shell. ## ## ## ## Input: see DisplayUsage() ## ## ## ## Author: Mark V Zieg ## ## ## ## Version: 0.2 ## ## ## ## Date: Sep 30, 2000 ## ## ## ## History: ## ## 0.2 -- general clean-up, documentation, etc ## ## ## ## 0.1 -- initial public release; can only add users ## ## ## ## Notes: tabs should be set to 4 for editing ## ## ## ## Bugs: needs 'usermod', 'groupadd', 'userdel', & 'groupdel' ## ## ## ####################################################################### ########################### Dependencies ############################## use strict; ############################ Defaults ################################# my $DEBUG = 0; # set to zero to actually change DB my $QUIET = 1; # set to non-zero to supress output my $rh_Defaults = { 'comment' => 'darwin user', # default "real name" 'shell' => '/bin/tcsh', # default shell 'groupname' => 'guest' # default group }; ############################## Main ################################### MAIN: { my $rh_Opts; # ref to hash of chosen options my $ra_Cmds; # ref to array of shell commands # process command-line arguments $rh_Opts = ParseArgs( @ARGV ); if( $rh_Opts->{Noun} eq "user" ) { if( $rh_Opts->{Verb} eq "add" ) { $ra_Cmds = ProcessUserAdd( $rh_Opts ); } elsif( $rh_Opts->{Verb} eq "mod" ) { $ra_Cmds = ProcessUserMod( $rh_Opts ); } elsif( $rh_Opts->{Verb} eq "del" ) { $ra_Cmds = ProcessUserDel( $rh_Opts ); } } elsif( $rh_Opts->{Noun} eq "group" ) { if( $rh_Opts->{Verb} eq "add" ) { $ra_Cmds = ProcessGroupAdd( $rh_Opts ); } elsif( $rh_Opts->{Verb} eq "mod" ) { $ra_Cmds = ProcessGroupMod( $rh_Opts ); } elsif( $rh_Opts->{Verb} eq "del" ) { $ra_Cmds = ProcessGroupDel( $rh_Opts ); } } # logic check if( !defined( $ra_Cmds ) ) { die( "Internal logic error: $rh_Opts->{Verb} a $rh_Opts->{Noun}?!?\n" ); } # generate commands $ra_Cmds = GenerateCommands( $rh_Opts ); # display or execute if( $DEBUG ) { DisplayCommands( $ra_Cmds ); } else { ExecuteCommands( $ra_Cmds ); } } ######################## Function Implementation ############################ # notes for groupadd: # os-X [/www] mzieg $ sudo niutil -create / /group/mysql # os-X [/www] mzieg $ sudo niutil -createprop / /group/mysql gid 50 ################### sub DisplayCommands { my $ra_Cmds = shift; my $Cmd; print "DEBUG MODE ON\n\n"; print "None of these commands is actually being executed, but\n"; print "this is what the program WOULD do during normal execution.\n\n"; # iterate through commands foreach $Cmd ( @{$ra_Cmds} ) { print "Debug: $Cmd\n"; } } ################### sub ExecuteCommands { my $ra_Cmds = shift; my $Cmd; # iterate through commands foreach $Cmd ( @{$ra_Cmds} ) { if( !$QUIET ) { print "$Cmd\n"; } `$Cmd`; } } ################### sub ProcessUserAdd { my $rh_Opts = shift; my @Cmds = (); # convert groupname to gid $rh_Opts->{gid} = GetGID( $rh_Opts->{groupname} ); # was a uid specified? if( !defined( $rh_Opts->{uid} ) ) { # no; pick one $rh_Opts->{uid} = GetNextAvailUID(); } # was a home directory specified? if( !defined( $rh_Opts->{homedir} ) ) { $rh_Opts->{homedir} = sprintf( "/Users/%s", $rh_Opts->{username} ); } # validate user options ValidateOptions( $rh_Opts ); # generate commands push( @Cmds, sprintf( "niutil -create / /users/%s", $rh_Opts->{username} ) ); push( @Cmds, sprintf( "niutil -createprop / /users/%s uid %d", $rh_Opts->{username}, $rh_Opts->{uid} ) ); push( @Cmds, sprintf( "niutil -createprop / /users/%s gid %d", $rh_Opts->{username}, $rh_Opts->{gid} ) ); push( @Cmds, sprintf( "niutil -createprop / /users/%s shell %s", $rh_Opts->{username}, $rh_Opts->{shell} ) ); push( @Cmds, sprintf( "niutil -createprop / /users/%s home %s", $rh_Opts->{username}, $rh_Opts->{homedir} ) ); push( @Cmds, sprintf( "niutil -createprop / /users/%s realname '%s'", $rh_Opts->{username}, $rh_Opts->{comment} ) ); return( \@Cmds ); # return REFERENCE to the array } ################### sub ValidateOptions { my $rh_Opts = shift; # verify that username is all lowercase, starts with a letter, # and is between 3 and 16 alphanumeric characters if( !( $rh_Opts->{username} =~ /^[a-z][a-z_0-9]{2,15}$/ ) ) { die( "Error: invalid username\n" ); } # verify that username is not in use if( isUsernameInUse( $rh_Opts->{username} ) ) { die( "Error: invalid username (in use)\n" ); } # verify that comment/realname is no more than 32 alphanums, # plus a few common punctuations (beware of quote chars!) if( !( $rh_Opts->{comment} =~ /^[A-Za-z0-9_\-. ]{0,32}$/ ) ) { die( "Error: invalid comment/realname\n" ); } # verify that home directory starts with a slash, does not # end with a slash, and doesn't have any funky chars # (besides space :-( if( !( $rh_Opts->{homedir} =~ /^\/([A-Za-z0-9_\-. ]+(\/[A-Za-z0-9_\-. ]+)*)?$/ ) ) { die( "Error: invald home directory\n" ); } # verify that home directory exists if( !( ( -d $rh_Opts->{homedir} ) ) ) { die( "Error: please create $rh_Opts->{homedir} first\n" ); } # verify that uid is a positive integer (niutil doesn't take # negatives anyway) but NOT ZERO (ie, root) if( !( $rh_Opts->{uid} =~ /^[1-9][0-9]*$/ ) ) { die( "Error: invalid uid (not int)\n" ); } # verify that uid is not in use if( isUIDinUse( $rh_Opts->{uid} ) ) { die( "Error: invalid uid (in use)\n" ); } # verify that gid is a positive integer (niutil doesn't take # negatives anyway...) if( !( $rh_Opts->{gid} =~ /^[0-9]+$/ ) ) { die( "Error: invalid gid\n" ); } # verify that shell is listed in /etc/shells if( !isLegalShell( $rh_Opts->{shell} ) ) { die( "Error: invalid shell\n" ); } } ################### sub GetGID { my $groupname = shift; # does groupname look normal? if( $groupname =~ /^[a-z][a-z0-9_\-]*$/ ) { # grep for id my $gid = `nireport . /groups name gid | grep $groupname | awk '{print \$2}'`; chomp $gid; # did I get an integer? if( $gid =~ /^\d+$/ ) { # return on success return $gid; } } # fall-through to error die( "Error: invalid groupname\n" ); } ################### sub GetNextAvailUID { my $uid; # grep for highest current value $uid = `nireport . /users uid | sort -n | uniq | tail -1`; chomp $uid; # did I get an integer? if( $uid =~ /^\s*\d+\s*$/ ) { # yep; increment by one, and return on success return $uid + 1; } # fall-through to error die( "Error: couldn't obtain next avail uid\n" ); } ################### sub isUsernameInUse { my $username = shift; my $cmd = "nireport . /users name | grep '^" . $username . "[^a-z0-9_]*\$'"; my $result = `$cmd`; chomp $result; return( $result =~ /^\s*$username\s*$/ ); } ################### sub isUIDinUse { my $uid = shift; my $cmd = "nireport . /users uid | grep '^" . $uid . "[^0-9]*\$'"; my $result = `$cmd`; chomp $result; return( $result =~ /^\s*$uid\s*$/ ); } ################### sub isLegalShell { my $shell = shift; my $LegalShell; # open the system file which lists legal shells open( INFILE, " ) { chomp $LegalShell; next if( $LegalShell =~ /^\s*#/ ); # skip comments next if( $LegalShell =~ /^\s*$/ ); # skip blanks # does this match the specified shell? if( $LegalShell eq $shell ) { # yes, it does! close( INFILE ); return 1; } } return 0; } # this function is responsible for looking at the first one or two arguments # and working out what subject and predicate are being requested (add a user, # mod a group, del a user, etc), and then calling a specific function to # scan remaining arguments for function-specific options (name of the group, # whatever). ################### sub ParseArgs { my @ARGV = (@_); my $Arg; my $Noun; my $Verb; # were any arguments passed at all? if( $#ARGV == -1 ) { DisplayUsage(); } $Arg = shift( @ARGV ); # try to get both out of the one arg (ie, "adduser" ) ($Noun, $Verb) = ParseArg( $Arg ); # did we get anything? if( !defined( $Noun ) && !defined( $Verb ) ) { DisplayUsage(); } # are we still missing one? if( !defined( $Noun ) || !defined( $Verb ) ) { # fetch the missing one $Arg = shift( @ARGV ); # WAS there another one? if( !defined( $Arg ) ) { DisplayUsage(); } if( !defined( $Noun ) ) { ($Noun, undef) = ParseArg( $Arg ); } else { (undef, $Verb) = ParseArg( $Arg ); } } # do we now have both? if( !defined( $Noun ) || !defined( $Verb ) ) { DisplayUsage(); } # yes, we now have both. Parse the rest of the command line! if( $Noun eq "user" ) { if( $Verb eq "add" ) { return ParseArgsUserAdd( @ARGV ); } elsif( $Verb eq "mod" ) { return ParseArgsUserMod( @ARGV ); } elsif( $Verb eq "del" ) { return ParseArgsUserDel( @ARGV ); } } elsif( $Noun eq "group" ) { if( $Verb eq "add" ) { return ParseArgsGroupAdd( @ARGV ); } elsif( $Verb eq "mod" ) { return ParseArgsGroupMod( @ARGV ); } elsif( $Verb eq "del" ) { return ParseArgsGroupDel( @ARGV ); } } die( "Error parsing some weird combination: $Verb a $Noun?!?\n" ); } # this function is to look in a command line argument for recognized verbs and # nouns, allowing the concatenated syntax supported by pw (useradd, modgroup, # etc) ################### sub ParseArg { my $Arg = shift; my $NounFound = undef; my $VerbFound = undef my $Term; foreach $Term ('user', 'group') { if( $Arg =~ /$Term/ ) { $NounFound = $Term; last; } } foreach $Term ('add', 'mod', 'del') { if( $Arg =~ /$Term/ ) { $VerbFound = $Term; last; } } #warn( "ParseArg: returning ($NounFound, $VerbFound)" ); return( $NounFound, $VerbFound ); } ################### sub ParseArgsUserAdd { my @ARGV = (@_); my $Arg; my $rh_Opts; # initialize options to defaults $rh_Opts = $rh_Defaults; # if 1st argument isn't a flag, assume it's the username # grab the next arg $Arg = shift( @ARGV ); if( !defined( $Arg ) ) { die( "Error: missing username\n" ); } # does it start with a hyphen? if( $Arg =~ /^-/ ) { # yes...oops! Put the arg back :-) unshift( @ARGV, $Arg ); } elsif( $Arg eq "help" ) { # print context-sensitive help, if available DisplayUsage( "adduser" ); } else { # nope...assume it's the username $rh_Opts->{username} = $Arg; } # iterate through all remaining arguments passed on the command-line while( $Arg = shift( @ARGV ) ) { if( $Arg =~ /-[h?]/ ) { DisplayUsage(); } if( $Arg eq "-n" ) { # get next arg as value $rh_Opts->{username} = shift( @ARGV ); next; } if( $Arg eq "-u" ) { # get next arg as value $rh_Opts->{uid} = shift( @ARGV ); next; } if( $Arg eq "-s" ) { # get next arg as value $rh_Opts->{shell} = shift( @ARGV ); next; } if( $Arg eq "-g" ) { # get next arg as value $rh_Opts->{groupname} = shift( @ARGV ); next; } if( $Arg eq "-d" ) { # get next arg as value $rh_Opts->{homedir} = shift( @ARGV ); next; } if( $Arg eq "-c" ) { # get next arg as value $rh_Opts->{comment} = shift( @ARGV ); next; } if( $Arg eq "-D" ) { $DEBUG = !$DEBUG; next; } if( $Arg eq "-Q" ) { $QUIET = !$QUIET; next; } # all of the above, "legal" args had "next" statements, so... die( "Invalid argument: $Arg\n" ); } return $rh_Opts; } ################### sub DisplayUsage { print "usage:\n pw [user|group] [add|del|mod] [help|switches/values]\n"; exit( 1 ); }