commit:80c2afe984166d32e534aabb2486b3ba77cf986e
author:Chip Black
committer:Chip Black
date:Mon Aug 11 23:09:34 2008 -0500
parents:
Initial commit from dead svn repository
diff --git a/ag-export b/ag-export
line changes: +40/-0
index 0000000..5a1043d
--- /dev/null
+++ b/ag-export
@@ -0,0 +1,40 @@
+#!/usr/bin/perl
+use strict;
+
+use AwesomeGrid;
+use AwesomeGrid::Config;
+use AwesomeGrid::User;
+use AwesomeGrid::Keyring;
+
+my @peers = AwesomeGrid::Config::trusted();
+my $admins = AwesomeGrid::Config::admins();
+
+my $home = AwesomeGrid::Config::hostname()
+	|| die "invalid hostname";
+
+my @exports = AwesomeGrid::Config::exports();
+foreach my $u (@exports) {
+	my $agu = AwesomeGrid::User->load($u);
+	if ($agu->{home} ne $home) {
+		print STDERR "WARNING: Exporting $u for non-home system $agu->{home}\n";
+	}
+}
+
+END {system('stty echo')}
+system('stty -echo');
+print STDERR "GPG Passphrase: ";
+my $passphrase = <STDIN>;
+system('stty echo');
+print "\n";
+
+my $pwd = `pwd`; chomp $pwd;
+chdir "$AwesomeGrid::confdir/users";
+
+my $exports = join(' ', @exports);
+system(qq!tar cfz "$pwd/export.tgz" $exports!);
+foreach my $peer (@peers) {
+	my $admin_id = $admins->{$peer};
+	print "Exporting for $peer ($admin_id)\n";
+	AwesomeGrid::Keyring::encrypt($admin_id,"$pwd/export.tgz","$pwd/$home-export-$peer.tgz.gpg",$passphrase);
+}
+unlink "$pwd/export.tgz";

diff --git a/ag-flush b/ag-flush
line changes: +23/-0
index 0000000..98930bc
--- /dev/null
+++ b/ag-flush
@@ -0,0 +1,23 @@
+#!/usr/bin/perl
+use strict;
+
+use AwesomeGrid;
+use AwesomeGrid::User;
+
+my $host = shift;
+unless ($host) {
+	print "No host specified. Use the -all flag to flush all users.\n";
+	exit 1;
+}
+
+opendir USERS, "$AwesomeGrid::confdir/users";
+my @users = grep {/^[^.]/} readdir USERS;
+closedir USERS;
+
+foreach my $u (@users) {
+	my $agu = AwesomeGrid::User->load($u);
+	if ($host eq '-all' || $agu->{home} eq $host) {
+		print "Deleting $agu->{username}\n";
+		AwesomeGrid::User::delete($u);
+	}
+}

diff --git a/ag-import b/ag-import
line changes: +59/-0
index 0000000..8d6deda
--- /dev/null
+++ b/ag-import
@@ -0,0 +1,59 @@
+#!/usr/bin/perl
+use strict;
+
+use AwesomeGrid;
+use AwesomeGrid::Config;
+use AwesomeGrid::User;
+use AwesomeGrid::Keyring;
+
+use File::Copy;
+
+my @files = @ARGV;
+my $pwd = `pwd`; chomp $pwd;
+mkdir "$AwesomeGrid::confdir/users.new";
+chdir "$AwesomeGrid::confdir/users.new";
+
+my @trusted = AwesomeGrid::Config::trusted();
+
+END {system('stty echo')}
+system('stty -echo');
+print STDERR "GPG Passphrase: ";
+my $passphrase = <STDIN>;
+system('stty echo');
+print "\n";
+
+for my $f (@files) {
+	AwesomeGrid::Keyring::gpgpass($passphrase, qq!--decrypt "$pwd/$f" | tar xfz -!);
+
+	opendir USERS, ".";
+	my @users = grep {/^[^.]/} readdir USERS;
+	closedir USERS;
+	
+	for my $u (@users) {
+		my $agu = AwesomeGrid::User->loadfrom($u);
+		unless ($agu) {
+			print "Unusable user info: $u\n";
+			unlink $u;
+			next;
+		}
+		unless (grep { $_ eq $agu->{home} } @trusted) {
+			print "Refusing to import ",$agu->{username}," from untrusted system ",$agu->{home},"\n";
+			unlink $u;
+			next;
+		}
+		if (AwesomeGrid::User::exists($u)) {
+			print "$u exists. Overwrite? [y/N] ";
+			my $ans = <STDIN>; chomp $ans;
+			if ($ans !~ /^[Yy]$/) {
+				unlink $u;
+				next;
+			}
+		}
+		move $u, "$AwesomeGrid::confdir/users/"
+			|| die "Could not write to $AwesomeGrid::confdir/users/";
+		print "Imported $u\n";
+	}
+}
+
+chdir $pwd;
+rmdir "$AwesomeGrid::confdir/users.new";

diff --git a/ag-keyring b/ag-keyring
line changes: +22/-0
index 0000000..8ee45b1
--- /dev/null
+++ b/ag-keyring
@@ -0,0 +1,22 @@
+#!/usr/bin/perl
+use strict;
+
+use AwesomeGrid;
+use AwesomeGrid::Config;
+use AwesomeGrid::Keyring;
+
+my $cmd = shift;
+
+if ($cmd eq 'initialize') {
+	AwesomeGrid::Keyring::initialize();
+} elsif ($cmd eq 'import') {
+	AwesomeGrid::Keyring::import_key(shift);
+} elsif ($cmd eq 'export') {
+	my $config = AwesomeGrid::Config::config();
+	print "gpg-id: ",$config->{'gpg-id'},"\n";
+	AwesomeGrid::Keyring::export_key($config->{'gpg-id'});
+} elsif ($cmd eq 'list') {
+	AwesomeGrid::Keyring::list();
+} elsif ($cmd eq 'gpg') {
+	AwesomeGrid::Keyring::gpg(@ARGV);
+}

diff --git a/ag-listusers b/ag-listusers
line changes: +18/-0
index 0000000..c7ca6b5
--- /dev/null
+++ b/ag-listusers
@@ -0,0 +1,18 @@
+#!/usr/bin/perl
+
+use AwesomeGrid::User;
+
+my $hostname = shift;
+opendir USERS, "$AwesomeGrid::confdir/users";
+my @users = grep {/^[^.]/} readdir USERS;
+closedir USERS;
+printf "%-10s%-20s\n","username","home system";
+printf '-' x 30;
+printf "\n";
+foreach (@users) {
+	my $agu = AwesomeGrid::User->load($_);
+	if ($hostname) {
+		next unless $agu->{home} eq $hostname;
+	}
+	printf "%-10s%-20s\n", $agu->{username}, $agu->{home};
+}

diff --git a/ag-passwd b/ag-passwd
line changes: +77/-0
index 0000000..6934a38
--- /dev/null
+++ b/ag-passwd
@@ -0,0 +1,77 @@
+#!/usr/bin/perl
+
+use AwesomeGrid::User;
+
+use Crypt::PasswdMD5;			# Linux, FreeBSD
+use Digest::SHA1 qw/sha1_hex/;		# OS X 10.4
+
+sub getsalt {
+	my $length = shift;
+	open RAND, "/dev/urandom";	# For production, this should be
+					# /dev/random
+	my $data;
+	read(RAND, $data, $length);
+	close RAND;
+	return $data;
+}
+
+sub unix_MD5 {
+	my $passwd = shift;
+	my $salt = unpack("h8",getsalt(8));
+	return unix_md5_crypt($passwd, $salt);
+}
+
+sub apache_MD5 {
+	my $passwd = shift;
+	my $salt = unpack("h8",getsalt(8));
+	return apache_md5_crypt($passwd, $salt);
+}
+
+sub OSX_SHA1 {
+	my $passwd = shift;
+	my $v = getsalt(4);
+	my $hash = sha1_hex("$v$passwd");
+	my $salt = unpack("H8",$v);
+	return uc("$salt$hash");
+}
+
+my $user = shift;
+if (!$user) {
+	print "usage: ag-passwd user\n";
+	exit 1;
+}
+
+if (!AwesomeGrid::User::exists($user)) {
+	print "User does not exist\n";
+	exit 1;
+}
+
+my $aguser = AwesomeGrid::User->load($user) || exit 1;
+
+# Restore sanity in case of emergency
+END { system("stty echo") }
+
+system("stty -echo");
+print STDERR "Password: ";
+my $passwd1 = <STDIN>;
+chomp $passwd1;
+print STDERR "\n";
+
+print STDERR "Repeat password: ";
+my $passwd2 = <STDIN>;
+chomp $passwd2;
+print STDERR "\n";
+
+system("stty echo");
+
+if ($passwd1 != $passwd1) {
+	print "Passwords don't match.\n";
+	exit 1;
+}
+
+$aguser->{'passwd-unix-md5'} = unix_MD5($passwd1);
+$aguser->{'passwd-apache-md5'} = apache_MD5($passwd1);
+$aguser->{'passwd-osx-sha1'} = OSX_SHA1($passwd1);
+
+$aguser->save();
+print "Password updated.\n";

diff --git a/ag-update-shadow b/ag-update-shadow
line changes: +131/-0
index 0000000..171ef4c
--- /dev/null
+++ b/ag-update-shadow
@@ -0,0 +1,131 @@
+#!/usr/bin/perl
+use strict;
+
+use AwesomeGrid;
+use AwesomeGrid::Config;
+use AwesomeGrid::User;
+
+my $etc = '/etc';	# In production, this should be '/etc'
+
+my $config = AwesomeGrid::Config::config();
+
+# Get a list of awesomegrid users
+opendir USERS, "$AwesomeGrid::confdir/users";
+my @agusers = grep {/^[^.]/} readdir USERS;
+close USERS;
+
+# Get a list of system users
+my @sysusers;
+my @allsysusers;
+
+open PASSWD, "$etc/passwd";
+while (<PASSWD>) {
+	my ($username, undef, $uid, $gid, $gecos, $home, $shell) = split(/:/);
+	push(@allsysusers, $username);
+	next unless $gecos =~ /^\[AG\]/;	# Skip non-AG-managed users
+	push(@sysusers, $username);
+}
+close PASSWD;
+
+# Check to see what there is in /etc/passwd that should be removed.
+my @deletes;
+for my $sysuser (@sysusers) {
+	unless (grep { $_ eq $sysuser } @agusers) {
+		push(@deletes, $sysuser);
+	}
+}
+
+# Check to see which users are going to be added
+my @adds;
+for my $aguser (@agusers) {
+	unless (grep { $_ eq $aguser } @allsysusers) {
+		push(@adds, $aguser);
+	}
+}
+
+# Check to see which users should be updated
+my @updates;
+for my $user (@allsysusers) {
+	if (grep { $_ eq $user } @agusers) {
+		push(@updates, $user);
+	}
+}
+
+unless (@adds or @deletes or @updates) {
+	print "Nothing to do.\n";
+	exit 0;
+}
+
+print "Adding: ",join(' ',@adds),"\n" if @adds;
+print "Deleting: ",join(' ',@deletes),"\n" if @deletes;
+print "Updating: ",join(' ',@updates),"\n" if @updates;
+
+print "Proceed? [y/N] ";
+my $ans = <STDIN>;
+unless ($ans =~ /^[Yy]$/) {
+	exit(0);
+}
+
+# Before proceeding, make a backup copy of /etc/passwd and /etc/shadow
+system("cp $etc/passwd $etc/passwd.ag_backup");
+system("cp $etc/shadow $etc/shadow.ag_backup");
+
+# Note that parsing /etc/passwd and /etc/shadow like this is pretty bad
+# form, but it's the only way I can test it without being root. It's
+# also the only way I can dump the hash directly into /etc/shadow.
+
+# Deletes first
+if (@deletes or @updates) {
+	open PASSWD, "$etc/passwd";
+	open PASSWDOUT, ">$etc/passwd.out";
+	while (<PASSWD>) {
+		my ($username) = split(/:/);
+		next if grep { $username eq $_ } (@deletes,@updates);
+		print PASSWDOUT;
+	}
+	close PASSWDOUT;
+	close PASSWD;
+
+	open SHADOW, "$etc/shadow";
+	open SHADOWOUT, ">$etc/shadow.out";
+	while (<SHADOW>) {
+		my ($username) = split(/:/);
+		next if grep { $username eq $_ } (@deletes,@updates);
+		print SHADOWOUT;
+	}
+	close SHADOWOUT;
+	close SHADOW;
+
+	rename "$etc/passwd.out","$etc/passwd";
+	chmod 0644, "$etc/passwd";
+	rename "$etc/shadow.out","$etc/shadow";
+	chmod 0640, "$etc/shadow";
+	system(qq{chown root.shadow "$etc/shadow"});
+}
+
+# Then add new ones.
+if (@adds or @updates) {
+	# Perhaps appending directly is a bad idea...
+	open PASSWD, ">>$etc/passwd";
+	open SHADOW, ">>$etc/shadow";
+
+	for my $add (@updates,@adds) {
+		my $agu = AwesomeGrid::User->load($add);
+		printf PASSWD "%s:x:%d:%d:[AG]:%s:%s\n", $agu->{username},
+				$agu->{uid}, $agu->{uid},
+				"$config->{homedir}/$agu->{username}",
+				$config->{'default-shell'};
+		printf SHADOW "%s:%s:%d:%d:%d:%d:::\n",
+				$agu->{username},
+				$agu->{'passwd-unix-md5'},
+				time / 86400, 0, 99999, 7;
+		unless (-d "$config->{homedir}/$agu->{username}") {
+			print "Creating homedir for $agu->{username}\n";
+			mkdir "$config->{homedir}/$agu->{username}";
+			chown $agu->{uid}, $agu->{uid},
+				"$config->{homedir}/$agu->{username}";
+		}
+	}
+	close PASSWD;
+	close SHADOW;
+}

diff --git a/ag-useradd b/ag-useradd
line changes: +20/-0
index 0000000..a800f17
--- /dev/null
+++ b/ag-useradd
@@ -0,0 +1,20 @@
+#!/usr/bin/perl
+use strict;
+
+use AwesomeGrid::User;
+
+my $user = shift;
+if (!$user) {
+	print "usage: ag-useradd user\n";
+	exit 1;
+}
+
+if (AwesomeGrid::User::exists($user)) {
+	print "User exists! I refuse to clobber them (even though I may want to).\n";
+	exit 1;
+}
+
+my $aguser = AwesomeGrid::User->new($user);
+
+$aguser->save();
+print "User $user created.\n";

diff --git a/ag-userdel b/ag-userdel
line changes: +19/-0
index 0000000..d264dd7
--- /dev/null
+++ b/ag-userdel
@@ -0,0 +1,19 @@
+#!/usr/bin/perl
+use strict;
+
+use AwesomeGrid::User;
+
+my $user = shift;
+if (!$user) {
+	print "usage: ag-userdel user\n";
+	exit 1;
+}
+
+if (!AwesomeGrid::User::exists($user)) {
+	print "User $user doesn't exist!\n";
+	exit 1;
+}
+
+AwesomeGrid::User::delete($user);
+
+print "User $user deleted.\n";

diff --git a/install.pl b/install.pl
line changes: +173/-0
index 0000000..1aa90b6
--- /dev/null
+++ b/install.pl
@@ -0,0 +1,173 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Config;
+use Sys::Hostname;
+
+my $etc = '/etc';
+my $bin = '/usr/bin';
+my $sbin = '/usr/sbin';
+
+# This should work better.
+my $perllib = $Config{sitelib};
+
+my $hostname = hostname();
+if (($hostname =~ /^([a-z0-9][a-z0-9-]*)(\.|$)/) && $1 !~ /-$/) {
+	$hostname = $1;
+} else {
+	print <<EOD;
+
+Your hostname ($hostname) is weird, and is probably
+not a valid internet hostname.  A valid hostname uses only alphanumeric
+characters and hyphens.  The hostname cannot begin or end with a hyphen.
+
+EOD
+	exit 1;
+}
+
+if ($> != 0) {
+	print <<EOD
+
+You are not running with root privs. This could make installation problematic 
+if you're using the default paths.
+EOD
+}
+
+print <<EOD;
+
+This script will install the AwesomeGrid perl libraries, utilities, and
+configuration. A short summary is printed below. If this doesn't look
+right to you, please edit this script.
+
+Configuration will be installed to:
+	$etc/awesomegrid
+
+User binaries will be installed to:
+	$bin
+
+Superuser binaries will be installed to:
+	$sbin
+
+Perl libraries will be installed to:
+	$perllib
+
+EOD
+
+# Make sure that $perllib is actually in the @INC path, and warn if not.
+unless (grep $perllib, @INC) {
+  print <<EOD;
+Caution!  $perllib not found in your \@INC path.
+AwesomeGrid may not run correctly during and/or after installation!
+
+EOD
+}
+
+print "Continue? [Y/n] ";
+my $ans = <STDIN>;
+unless ($ans =~ /^([Yy]|)$/) {
+	print "Aborting.\n";
+	exit 0;
+}
+
+print "Installing user binaries...\n";
+# No user binaries, LOL!
+
+print "Installing superuser binaries...\n";
+system("cp -fv ag-export ag-flush ag-import ag-keyring ag-listusers ag-passwd ag-update-shadow ag-useradd ag-userdel $sbin/");
+
+print "Installing perl libraries...\n";
+system("mkdir -p $perllib/AwesomeGrid") unless -d "$perllib/AwesomeGrid";
+system("cp -rfv lib/AwesomeGrid.pm $perllib/");
+system("cp -rfv lib/AwesomeGrid/{Config,Keyring,User}.pm $perllib/AwesomeGrid/");
+
+# Be extra careful for configuration bits
+umask 0077;
+
+print "Installing configuration...\n";
+mkdir "$etc/awesomegrid" unless -d "$etc/awesomegrid";
+unless (-f "$etc/awesomegrid/admins") {
+	open ADMINS, ">$etc/awesomegrid/admins";
+	print ADMINS <<EOD;
+# hostname	admin GPG id
+$hostname	foo\@bar.com
+EOD
+	close ADMINS;
+}
+unless (-f "$etc/awesomegrid/exports") {
+	open EXPORTS, ">$etc/awesomegrid/exports";
+	print EXPORTS <<EOD;
+# List here, one per line, users to export to other trusted systems (as
+# defined in the 'trusted' file)
+EOD
+	close EXPORTS;
+}
+unless (-f "$etc/awesomegrid/trusted") {
+	open TRUSTED, ">$etc/awesomegrid/trusted";
+	print TRUSTED <<EOD;
+# List here, one per line, trusted systems (short hostname)
+EOD
+	close TRUSTED;
+}
+unless (-f "$etc/awesomegrid/awesomegrid.conf") {
+	open CONF, ">$etc/awesomegrid/awesomegrid.conf";
+	print CONF <<EOD;
+# The GPG id of the admin of this machine
+gpg-id: foo\@bar.com
+# The place to install new home directories for imported users
+homedir: /home
+# The shell to give imported users if they don't specify one
+default-shell: /bin/bash
+EOD
+	close CONF;
+}
+mkdir "$etc/awesomegrid/users" unless -d "$etc/awesomegrid/users";
+unless (-d "$etc/awesomegrid/keyring") {
+	print <<EOD;
+It appears that you don't have an AwesomeGrid keyring. You will need one
+to export users to other systems (and have other admins import from
+you).
+
+EOD
+	print "Shall I run 'ag-keyring initialize' to generate a new keyring? [Y/n] ";
+	my $ans = <STDIN>;
+	if ($ans =~ /^([Yy]|)$/) {
+		system("$sbin/ag-keyring initialize");
+	}
+}
+
+print <<EOD;
+
+Installation finished. Please look over the configuration in
+$etc/awesomegrid and customize it to suit your system.
+EOD
+__END__
+
+=head1 NAME
+
+install.pl - Install AwesomeGrid
+
+=head1 SYNOPSIS
+
+install.pl
+
+=head1 DESCRIPTION
+
+Installs AwesomeGrid on the current system, with some basic sanity checking.
+Currently, it is pretty simple, and verified only on Slackware, Debian (and
+variants), and OpenSolaris.
+
+=head1 OPTIONS
+
+None.
+
+=head1 BUGS
+
+Too many.  Far, far too many.
+
+=head1 AUTHOR
+
+Chip Black (bytex64@bytex64.net), Matt Erickson (peawee@peawee.net)
+
+=cut

diff --git a/lib/AwesomeGrid.pm b/lib/AwesomeGrid.pm
line changes: +15/-0
index 0000000..d3fac0f
--- /dev/null
+++ b/lib/AwesomeGrid.pm
@@ -0,0 +1,15 @@
+package AwesomeGrid;
+
+# For paranoia's sake
+umask 0077;
+
+our $version = 0.1;
+
+our $confdir;
+if ($ENV{AWESOMEGRID_CONFDIR}) {
+	$confdir = $ENV{AWESOMEGRID_CONFDIR};
+} else {
+	$confdir = '/etc/awesomegrid';
+}
+
+1;

diff --git a/lib/AwesomeGrid/Config.pm b/lib/AwesomeGrid/Config.pm
line changes: +77/-0
index 0000000..e7f7f03
--- /dev/null
+++ b/lib/AwesomeGrid/Config.pm
@@ -0,0 +1,77 @@
+package AwesomeGrid::Config;
+use strict;
+
+use AwesomeGrid;
+
+use Sys::Hostname;
+
+# Return a list of trusted machines
+sub trusted {
+	open PEERS, "$AwesomeGrid::confdir/trusted";
+	my @peers;
+	while (<PEERS>) {
+		chomp;
+		s/#.*$//;
+		next if /^$/;
+		push @peers, $_;
+	}
+	close PEERS;
+	return @peers;
+}
+
+# Return a list of exported users
+sub exports {
+	open EXPORTS, "$AwesomeGrid::confdir/exports";
+	my @exports;
+	while (<EXPORTS>) {
+		chomp;
+		s/#.*$//;
+		next if /^$/;
+		push @exports, $_;
+	}
+	close EXPORTS;
+	return @exports;
+}
+
+# Return a hashref of admin IDs for each machine.
+sub admins {
+	my %admins;
+	open ADMINS, "$AwesomeGrid::confdir/admins";
+	while (<ADMINS>) {
+		chomp;
+		my ($machine, $admin) = split(/\s+/);
+		$admins{$machine} = $admin;
+	}
+	close ADMINS;
+	return \%admins;
+}
+
+# Return a hashref of values from awesomegrid.conf
+sub config {
+	my %config;
+	open CONFIG, "$AwesomeGrid::confdir/awesomegrid.conf";
+	while (<CONFIG>) {
+		s/#.*$//;
+		next if /^\s+$/;
+		if (/^([A-Za-z0-9_-]+):\s+(.*)$/) {
+			$config{$1} = $2;
+		} else {
+			print "Config error: line $.\n";
+		}
+	}
+	close CONFIG;
+	return \%config;
+}
+
+# Return the short hostname for this host, or undef if the hostname is
+# weird
+sub hostname {
+	my $hostname = Sys::Hostname::hostname();
+	if (($hostname =~ /^([a-z0-9][a-z0-9-]*)(\.|$)/) && $1 !~ /-$/) {
+		return $1;
+	} else {
+		return undef;
+	}
+}
+
+1;

diff --git a/lib/AwesomeGrid/Keyring.pm b/lib/AwesomeGrid/Keyring.pm
line changes: +56/-0
index 0000000..e9f5d0b
--- /dev/null
+++ b/lib/AwesomeGrid/Keyring.pm
@@ -0,0 +1,56 @@
+package AwesomeGrid::Keyring;
+use strict;
+
+use AwesomeGrid;
+use AwesomeGrid::Config;
+
+my $config = AwesomeGrid::Config::config();
+my $gpg = $config->{gpg} || '/usr/bin/gpg';
+
+sub gpg {
+	system(qq!$gpg --no-default-keyring --keyring "$AwesomeGrid::confdir/keyring/pubring.gpg" --secret-keyring "$AwesomeGrid::confdir/keyring/secring.gpg" --trustdb-name "$AwesomeGrid::confdir/keyring/trustdb.gpg" ! . join(' ',@_));
+}
+
+sub gpgpass {
+	my $passphrase = shift;
+	open GPG, qq!|$gpg --batch --passphrase-fd 0 --no-default-keyring --keyring "$AwesomeGrid::confdir/keyring/pubring.gpg" --secret-keyring "$AwesomeGrid::confdir/keyring/secring.gpg" --trustdb-name "$AwesomeGrid::confdir/keyring/trustdb.gpg" ! . join(' ',@_);
+	print GPG $passphrase;
+	close GPG;
+}
+
+sub initialize {
+	if (-e "$AwesomeGrid::confdir/keyring/secring.gpg") {
+		print "Refusing to clobber existing keyring.\n";
+		return;
+	}
+	if (!-d "$AwesomeGrid::confdir/keyring") {
+	        mkdir "$AwesomeGrid::confdir/keyring";
+	}
+	gpg('--gen-key');
+}
+
+sub import_key {
+	my $file = shift;
+	gpg(qq{--import "$file"});
+}
+
+sub export_key {
+	my $who = shift;
+	gpg(qq{--output $who-pubkey.asc --armor --export $who});
+	print "Wrote $who-pubkey.asc\n";
+}
+
+sub list {
+	gpg('--list-keys');
+}
+
+sub encrypt {
+	my $who = shift;
+	my $in = shift;
+	my $out = shift;
+	my $passphrase = shift;
+
+	gpgpass($passphrase,qq{--recipient $who --sign --encrypt --output "$out" "$in"});
+}
+
+1;

diff --git a/lib/AwesomeGrid/User.pm b/lib/AwesomeGrid/User.pm
line changes: +80/-0
index 0000000..4706c10
--- /dev/null
+++ b/lib/AwesomeGrid/User.pm
@@ -0,0 +1,80 @@
+package AwesomeGrid::User;
+use strict;
+
+use AwesomeGrid;
+use AwesomeGrid::Config;
+
+# Class methods
+
+sub exists {
+	my $user = shift;
+	return -f "$AwesomeGrid::confdir/users/$user";
+}
+
+sub delete {
+	my $user = shift;
+	unlink "$AwesomeGrid::confdir/users/$user";
+}
+
+# Constructors
+
+sub new {
+	my $class = shift;
+	my $user = shift;
+	my $obj = {};
+	$obj->{username} = $user;
+	# Pick a random UID >= 10000 and < 65535
+	$obj->{uid} = int(rand(55534)) + 10000;
+	$obj->{home} = AwesomeGrid::Config::hostname();
+	warn "invalid hostname" unless $obj->{home};
+	chomp $obj->{home};
+	return bless($obj, $class);
+}
+
+sub load {
+	my $class = shift;
+	my $user = shift;
+
+	return $class->loadfrom("$AwesomeGrid::confdir/users/$user");
+}
+
+sub loadfrom {
+	my $class = shift;
+	my $file = shift;
+	local $_;
+
+	open USER, $file || return undef;
+	$_ = <USER>;
+	unless (/^AWESOMEGRID USER (\d+\.\d+)$/) {
+		print STDERR "Invalid AwesomeGrid User format\n";
+		return undef;
+	}
+	if ($1 > $AwesomeGrid::version) {
+		print STDERR "Cowardly refusing to parse future AwesomeGrid format.\n";
+		return undef;
+	}
+
+	my $obj = {};
+	while (<USER>) {
+		if (/([A-Za-z0-9_-]+):\s+(.*)$/) {
+			$obj->{$1} = $2;
+		}
+	}
+	close USER;
+
+	return bless($obj, $class);
+}
+
+# Object methods (isn't all this confusion WONDERFUL?)
+
+sub save {
+	my $class = shift;
+	open USER, ">$AwesomeGrid::confdir/users/$class->{username}";
+	print USER "AWESOMEGRID USER $AwesomeGrid::version\n";
+	for (keys %$class) {
+		print USER "$_: $class->{$_}\n";
+	}
+	close USER;
+}
+
+1;