Wednesday, November 08, 2006

An adduser Script

Although OS X provides a graphical interface for adding users, there's no simple command-line utility for doing it. So, I've taken the adduser script I use for the Linux computers I manage, and modified it to work under OS X.

OS X stores user account information in a NetInfo heirarchical database. Information can be modified, added to or extracted from the database through commands like "nireport" and "niutil". For example, to get a list of all user accounts, you could type:
nireport / /users name

Here's the adduser script I've come up with. This is influenced by examples found on the web, but I'm especially indebted to Dave Dribin for his clear, detailed description of shadow files under OS X. I've left out a central section of the following script, in which I attempt to look up the user in our institution's central database. I omitted this both for security-through-obscurity reasons, and also because it probably wouldn't be applicable elsewhere. Insert your own code here if you need it. After the code listing, I'll describe some features of the script.

use strict;
use Digest::SHA1 qw/sha1_hex/;

$ENV{'PATH'} = "/usr/sbin:/usr/bin:/bin";

my $userid;
my @userlist;
if ( $ARGV[0] ) {
chomp( $userid = shift() );
if ( "$userid" eq "list" ) {
print "Enter file name: ";
chop(my $userfile = <>);
open (USERFILE,"<$userfile") or die "Can't open $userfile: $!\n";
chomp(@userlist = <USERFILE>);
close (USERFILE);
} else {
push (@userlist,$userid);
} else {
print ( "Enter userid: " );
chop($userid = <>);
push (@userlist,$userid);

USERID: for my $userid (@userlist) {

if ( getpwnam($userid) ) {
print "$userid already has an account.\n";
mkhome ($userid);
next USERID;

# Look user up in LDAP database:
# ...insert appropriate magic here for your institution, if needed.

if ( "$name" ne "" ) {
print "Found $name in LDAP database. OK? (y|n) [n]: \n";
chop(my $ans = <>);
if ( "$ans" ne "y" && "$ans" ne "Y" ) {
$name = "";
if ( "$name" eq "" ) {
print ( "Enter user name: " );
chop($name = <>);

print ( "Creating new account for [$userid]. . .\n" );

# Find first unused uid:
chomp ( my $nextuid = `nireport / /users name uid | awk '{print \$2}' | sort -n|tail -1` );
# Find first unused gid:
chomp ( my $nextgid = `nireport / /groups name gid | awk '{print \$2}' | sort -n|tail -1` );

# Create NetInfo data:
`niutil -create / /users/$userid`;
`niutil -createprop / /users/$userid uid $nextuid`;
`niutil -createprop / /users/$userid realname "$name"`;
`niutil -createprop / /users/$userid home "/Users/$userid"`;
`niutil -createprop / /users/$userid shell "/bin/bash"`;
`niutil -createprop / /users/$userid gid $nextgid`;
`niutil -createprop / /users/$userid passwd "********"`;
`niutil -createprop / /users/$userid authentication_authority ";ShadowHash;"`;

# Generate a 'generateduid':
chomp( my $generateduid = `uuidgen` );
`niutil -createprop / /users/$userid generateduid "$generateduid"`;

`niutil -create / /groups/$userid`;
`niutil -createprop / /groups/$userid gid $nextgid`;
`niutil -appendprop / /groups/$userid users $userid`;

# Generate a 'generatedgid':
chomp( my $generatedgid = `uuidgen` );
`niutil -createprop / /groups/$userid generatedgid "$generatedgid"`;

# Make suggested password:
chomp( my $pwsugg = `/common/bin/pwsuggest` );
my $password;
print ( "Enter password [default $pwsugg]: " );
chop ($password = <>);
if ( "x$password" eq "x" ) {
$password = $pwsugg;

# Create password hash:
my $salt1 = chr(int(rand(26))+64);
my $salt2 = chr(int(rand(26))+64);
my $salt3 = chr(int(rand(26))+64);
my $salt4 = chr(int(rand(26))+64);
my $salt = $salt1.$salt2.$salt3.$salt4;
my $hex_salt = $salt;
$hex_salt =~ s/(.)/uc(sprintf("%2.2x",ord($1)))/eg;
my $cpw = sha1_hex($salt.$password);

my @data = ();
for (my $i=0;$i<1240;$i++) {
$data[$i] = 0;
@data[168..215] = split(//,$hex_salt.$cpw);

# Store password hash:
open ( HASHFILE, "> /var/db/shadow/hash/$generateduid" ) or die "Can't open hash file for generateduid=$generateduid: $!\n";
for (my $i=0;$i<1240;$i++) {
print HASHFILE uc($data[$i]);
close (HASHFILE);

mkhome ($userid);


sub mkhome () {
my $userid = shift;

my ($name,$passwd,$uid,$gid,
$quota,$comment,$gcos,$home,$shell,$expire) =
unless ( -d "$home" ) {
print "Making home directory $home...\n";
`cp -R "/System/Library/User\ Template/English.lproj" $home`;
`chown -R $userid:$userid $home`;

Some things to note about the script:

  • Even though user information is stored in a NetInfo database, the perl functions like "getpwnam" still work as expected.

  • You can use commands like "nireport / /users name uid" to fetch lists of selected properties (name and uid, in this case) from the NetInfo database. Also, although it isn't used in the script above, you can dump the NetInfo user data in plain old "passwd" format by using the command "nidump passwd ." (note the trailing dot). Similarly, "nidump group ." works.

  • The script creates a user in the NetInfo hierarchy by using "niutil -create" to create a new node for the given user, and then uses "niutil -createprop" to fill in the properties of that node. These should be self-explanatory, except for some subtleties in the way "passwd" works and the (to me) unfamiliar properties "authentication_authority" and "generateduid".

  • Beginning with recent versions of OS X, password hashes are stored using a shadow mechanism which makes the hashes unavailable to unprivileged users. Password hashes can still be stored in directly in the "passwd" property of a user's NetInfo entry, but this is not as secure. The "********" entry in the passwd property and the ";ShadowHash;" value of authentication_authority indicate that the accounts the script creates are using shadow passwords.

  • Under OS X shadow passwords are stored in files under /var/db/shadow/hash. Each file is named after a user's "generateduid". The generateduid is a "UUID" (Universally Unique Identifier) associated with each account. As in the script above, uuids can be generated by the command "uuidgen". The script assigns a uuid as each user's generateduid, and then creates the appropriate file in the /var/db/shadow/hash directory.

  • As described in Dave Dribin's article, the format of the shadow files has changed in OS X 10.4. The script above writes shadow files in the new format, so it probably won't work with earlier versions of OS X.

  • Finally, on Linux computers I use "mkpasswd" to generate random initial passwords. There doesn't seem to be anything similar packaged with OS X, so I just copied mkpasswd (which is just an expect script) and renamed it to "pwsuggest" for clarity.

No comments: