#!/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";
}