#!/usr/bin/perl -w
# adduser script for Open Directory (works on local and remote nodes)
# by Andre LaBranche, dre@mac.com, updated 7/9/06
# To add support for other user attributes:
# 1) Add a line to the GetOptions block of the collect_data subroutine
# (example below for LastName attrib):
# 'LastName=s' => \$SuppliedData{LastName},
# *IMPORTANT* - you must use the DS attribute name
# 2) Add a line in the show_usage subroutine to describe the new attribute.
# For more information about DS attribute names, view the DirectoryService
# man page. In Tiger, the DS attributes are listed in DirServicesConst.h, in
# /System/Library/Frameworks/DirectoryService.framework/Headers/
use Getopt::Long;
use strict;
### Variable declarations
# Require at least the following attributes (include default values)
my %RequiredData = (
'RecordName' => "username",
'RealName' => "User Name",
'NFSHomeDirectory' => "",
'PrimaryGroupID' => "20",
'UserShell' => "/bin/bash"
);
my %SuppliedData = (); # The attributes fed to dsimport will be stored here
my $attribute_count = 0; # used in the dsimport record description header
my $debug; # switch for debug mode
my $help; # switch for showing usage help
my $listnodes; # switch for listing directory service nodes
my $node; # the target directory node for the new user
my $admin; # admin username for authorizing the user add
my $adminpass; # admin password for authorizing the user add
my $dsimport_log; # name of current dsimport log file
my $attrib; # holds individual attributes during loops
# Get user data from command line options or interactive prompt
&collect_data;
# open a file for dsimport
open( OUT, ">/tmp/adduser.$$" ) || die("Can't open file for dsimport.\n");
# Write record description header - this is all one line.
print OUT "0x0A 0x5C 0x3A 0x2C dsRecTypeStandard:Users $attribute_count ";
foreach $attrib ( sort keys %SuppliedData ) {
print OUT "dsAttrTypeStandard:$attrib "
if defined $SuppliedData{ ${ \$attrib } };
}
print OUT "dsAttrTypeStandard:AuthMethod\n";
# iterate again and write out the actual data
foreach $attrib ( sort keys %SuppliedData ) {
print OUT "$SuppliedData{${\$attrib}}:"
if defined $SuppliedData{ ${ \$attrib } };
}
print OUT "dsAuthMethodStandard\\:dsAuthClearText\n";
close(OUT);
print "Assembled dsimport file:\n" if $debug;
system("/bin/cat /tmp/adduser.$$") if $debug;
# if we have an admin password from the cli options, use it. Otherwise, let
# dsimport prompt for the admin password
if ($adminpass) {
print
"/usr/bin/dsimport -g /tmp/adduser.$$ $node -I -u $admin -p $adminpass -v\n"
if $debug;
system(
"/usr/bin/dsimport -g /tmp/adduser.$$ $node -I -u $admin -p $adminpass -v"
);
}
else {
print "/usr/bin/dsimport -g /tmp/adduser.$$ $node -I -u $admin -v\n"
if $debug;
system("/usr/bin/dsimport -g /tmp/adduser.$$ $node -I -u $admin -v");
}
if ($debug) {
chomp( $dsimport_log =
`/bin/ls -tr1 ~/Library/Logs/ImportExport | /usr/bin/tail -n 1` );
system("/bin/cat ~/Library/Logs/ImportExport/$dsimport_log");
}
# clean up the temp file and exit
system("/bin/rm /tmp/adduser.$$");
exit;
### Begin Subroutines
sub show_usage {
my @exe_path; # path to this executable
my $name; # this executable's name
# get the executable name
@exe_path = split( /\W/, $0 );
$name = $exe_path[$#exe_path]; # grabs last item of @exe_path array
print <<EOF;
Usage: $name [<attributes>] [-DSnode <node>] [-admin <admin>]
[-adminpass <password>] [-listnodes] [-debug]
-DSNode node Add user to specified DS node
(defaults to local)
-admin user admin username
-adminpass password admin password
-listnodes list available DS nodes
-debug Enable debug output
-help Show this usage help
<attributes> are any of:
-RecordName username Short username
-FirstName First First name
-LastName Last Last name
-RealName "First Last" Full name
-NFSHomeDirectory home Home directory path
-UniqueID uid Unix user ID
-PrimaryGroupID gid Unix primary group
-GeneratedUID globally unique id
-UserShell shell Login shell
-Password password Password
-Comment "a comment" Comment
EOF
}
sub prompt_for_new_user_pw {
my $pw1; # interactive password entry
my $pw2; # double check password entry
# prompt twice for password
print "Password for $SuppliedData{RecordName}: ";
system("/bin/stty -echo");
chomp( $pw1 = <STDIN> );
print "\nRetype password: ";
chomp( $pw2 = <STDIN> );
system("/bin/stty echo");
print "\n";
# verify password
if ( $pw1 ne $pw2 ) {
die "Password mismatch, please try again\n";
}
# deal with special characters in the password
$pw1 =~ s/\\/\\\\/g; # sub \ to \\
$pw1 =~ s/:/\\:/g; # sub : to \:
$SuppliedData{Password} = $pw1;
}
sub collect_data {
my $DSnode; # cli option for target DS node
my $fullname_check; # holds data used in checking for full name collisions
my $username_check; # holds data used in checking for short name collisions
# Each option that needs a value is followed by a short token
# that defines the data type. s is for string, i is for integer
GetOptions(
'RecordName=s' => \$SuppliedData{RecordName},
'FirstName=s' => \$SuppliedData{FirstName},
'LastName=s' => \$SuppliedData{LastName},
'RealName=s' => \$SuppliedData{RealName},
'NFSHomeDirectory=s' => \$SuppliedData{NFSHomeDirectory},
'UniqueID=i' => \$SuppliedData{UniqueID},
'PrimaryGroupID=i' => \$SuppliedData{PrimaryGroupID},
'GeneratedUID=s' => \$SuppliedData{GeneratedUID},
'UserShell=s' => \$SuppliedData{UserShell},
'Password=s' => \$SuppliedData{Password},
'Comment=s' => \$SuppliedData{Comment},
'DSnode=s' => \$DSnode,
'admin=s' => \$admin,
'adminpass=s' => \$adminpass,
'h' => \$help,
'help' => \$help,
'debug' => \$debug,
'listnodes' => \$listnodes
);
&show_usage && exit if $help;
if ($listnodes) { &list_nodes; exit }
# make sure we got everything we need from cli options, prompt if not
foreach $attrib ( sort keys %RequiredData ) {
if ( !defined $SuppliedData{$attrib} ) {
# prompt with default value (if any)
print "$attrib [$RequiredData{$attrib}]: ";
chomp( $SuppliedData{$attrib} = <STDIN> );
# if user pressed enter for default, set it accordingly
if ( $SuppliedData{$attrib} eq "" ) {
$SuppliedData{$attrib} = $RequiredData{$attrib};
}
print "$attrib = $SuppliedData{$attrib}\n" if $debug;
}
}
# Use the local node unless told otherwise
if ( !defined $DSnode ) {
$node = "/NetInfo/DefaultLocalNode";
}
else {
$node = $DSnode;
}
if ( defined $DSnode && $DSnode eq "/BSD/local" ) {
die("/BSD/local is not supported by this script\n");
}
print "Using DS node $node\n" if $debug;
# duplicate checking
chomp( $username_check =
`dscl $node read /Users/$SuppliedData{RecordName} cn` );
if ( $username_check =~ /$SuppliedData{RecordName}/ ) {
die("RecordName $SuppliedData{RecordName} already exists in $node\n");
}
chomp( $fullname_check =
`dscl $node search Users RealName "$SuppliedData{RealName}" RealName`
);
if ($fullname_check) {
die("RealName $SuppliedData{RealName} already exists in $node\n");
}
# prompt for new user password if not supplied on command line
if ( !defined $SuppliedData{Password} ) {
&prompt_for_new_user_pw;
print "Using password $SuppliedData{Password}\n" if $debug;
}
# Count attributes
foreach $attrib ( keys %SuppliedData ) {
$attribute_count++ if defined $SuppliedData{$attrib};
}
# We'll add the AuthMethod attribute manually, so account for that
$attribute_count++;
# prompt for admin user if it was not supplied in the options
if ( !$admin ) {
if ( !defined $SuppliedData{node} ) {
print "Administrator username: ";
}
else {
print "Directory admin username for $node: ";
}
chomp( $admin = <STDIN> );
}
}
sub list_nodes {
my $available_nodes; # list of available DS nodes
my $dscl_out; # holds dscl output
$dscl_out = `dscl localhost read /Search | egrep '^CSPSearchPath'`;
$dscl_out =~ /^.*? (.*?)$/;
$available_nodes = $1;
$available_nodes =~ s/\s/\n/g;
print "$available_nodes\n";
}