#! /usr/bin/perl -w
# $Header: /global/src/usr.bin/src/RCS/add-user.pl,v 1.33 2025/04/07 11:54:35 mas90 Exp $
# Use "pod2text" for info

use strict;

=head1 NAME

add-user - add (or delete) a user to a standalone machine

=head1 SYNOPSIS

B<add-user> [_]I<user>

=head1 OPTIONS

=over 4

=item B<--verbose>

Show what it's doing

=item B<--quiet>

Be a bit quieter

=item B<--help>

Generate help info.

=item B<--COs>

add COs

=item B<--UIS>

add UIS people

=item B<--local>

add local user

=item B<--auto>

add std entries, e.g. B<--COs> B<--UIS> B<--UIS> B<--purge>

=item B<-n>

Do not actually do it

=item B<--root>

Set the PW to that of localadmin or root, and set the account as expired

=item B<--purge>

Purge people who may in the past have has CO or UIS accounts

=item B<-ssh> I<$file>

Copy I<$file> to B</home/>I<$user>B</.ssh/authorized_keys>

=item B<-master> I<$file>

Generate B</home/>I<$user>B</.ssh/authorized_keys> from I<$file>
(e.g. a copy of B</global/src/usr.bin/ssh/C>)

=back

=head1 DESCRIPTION

Add a user, and setup ssh access.

If the ID starts with a B<_>, delete the user instead.

=cut

use Getopt::Long;
# abuse bundling_override to allow -W and -w as different
use Getopt::Long qw(:config require_order bundling_override);
use Pod::Usage;

my $cosl = "jh347 nb110 pb22 mas90 mdc56 drp35";
my $uisl = "";
my $locall = "localadmin";
my $purgel = "_rf10 _djsd100 _jas284 _bdj23 _gt19 _maj1 _ckh11";

my $localpw = 'localadmin:$6$m9KhPOHh$GXiy3yTiSmrlnmzWTsPx0sURgDlFtGTS7fwlgWIBjOHT6kMcMIQ.tiSX97i.aEm3rpzPaDLGn0SuXIAugqR6u.:1000:1000:localadmin,,,:/home/localadmin:/bin/bash';
my $localgroup = "localadmin:x:1000:\n";

my $debug = $ENV{'DEBUG'};

my ($help, $verbose, $quiet, @quiet, $n, $key, $ssh, $master, $root, $COs, $UIS, $local, $purge, $auto);
my $q_in_pwf	= 0;
my $q_in	= 1;
my $q_exists	= 2;
my %opts = (
	"verbose|v"	=> \$verbose,
	"quiet|q"	=> \$quiet,
	"auto|a"	=> \$auto,
	"help|h"	=> \$help,
	"COs|c"		=> \$COs,
	"UIS|u"		=> \$UIS,
	"local|l"	=> \$local,
	"purge|p"	=> \$purge,
	"n"		=> \$n,
	"root"		=> \$root,
	"ssh:s"		=> \$ssh,
	"master:s"	=> \$master,
);

my $cmd = $0;
$cmd =~ s=^/usr/bin/==;

my $URL = "https://sysdata.cl.cam.ac.uk/cgi-bin/getent?";
my $PASSWD = "passwd";
my $GROUPS = "group";

# see if there is a "pw" command.
my ($pwcmd) = &cmd("pw");
# HACK: only FeeBSD has pw.
my ($freebsd) = $pwcmd;

$auto = 1 if $#ARGV < 0;

GetOptions (%opts) || pod2usage();
pod2usage(-verbose => 2) if $help;
push(@ARGV, split(/ /, $cosl)) if $COs || $auto;
push(@ARGV, split(/ /, $locall)) if $local || $auto;
push(@ARGV, split(/ /, $uisl)) if $UIS;
push(@ARGV, split(/ /, $purgel)) if $purge || $COs || $auto;
pod2usage(-verbose => 2) if $#ARGV < 0;
pod2usage(-verbose => 2) if $ssh && $master;

$master = &www_get("http://sysdata.cl.cam.ac.uk/ssh/C") unless $master || $ssh;
$ENV{'PATH'} .= ":/sbin:/usr/sbin";

my $user;
my $groups;
my $foruser;

while ($user = shift) {
	if ($user =~ s/^_//) { # Delete the user
		my $info = `grep ^$user: /etc/passwd /etc/shadow`;
		next unless $info;
		chomp $info;
		print STDERR "$0: deluser $user\n" if $verbose;
		system("deluser $user");
		next;
	}
	my $home = "/home/$user"; # assume ...
	my $dstd = "$home/.ssh";
	my $dst = "$dstd/authorized_keys";
	$groups = "";

	&summary;
	$foruser = $user;

	print STDERR "# $0: Add user $user\n" if $verbose;
	# Have to create the user group before adding the user
	unless (&add_groups($user)) {
		print STDERR "** group add for $user failed so skipping rest\n";
		next;
	}

	if (&getent("local", $PASSWD))
	{ print STDERR "# $0: $user already in passwd file\n" if $verbose;
	  $quiet[$q_in_pwf]++; }
	elsif (! &add_user($user)) {
		print STDERR "** add_user $user failed so skipping rest\n";
		next;
	}
	if ($groups =~ s/^,// && ($groups =~ /,/ || $groups ne $user)) { &run( "usermod -a -G $groups $user")}
	
	next unless $ssh || $master;

	if (-e $dst) {
		print STDERR "# $0: $dst already exists so skipping rest\n" if $verbose;
		$quiet[$q_exists]++;
		next;
	}
	foreach my $dir ($home, $dstd) {
		&run("mkdir $dir && chown $user $dir && chmod 755 $dir") unless -e $dir
	}
	&run("cp $ssh $dst; chown $user $dst; chmod 400 $dst") if $ssh;
	if ($master) {
		my ($res) = '';
		open(MASTER, $master) || die("$0: failed to open $master: $!\n");
		while (<MASTER>) { $res .= $_ if s/^$user://; }
		unless (close(MASTER)) { warn("$0: failed to close $master: $!\n"); next; };
		if ($res) {
			unless (open(SSH, ">$dst")) { warn("$0: failed to open $dst: $!\n"); next; };
			print SSH $res;
			unless (close(SSH)) { warn("$0: failed to close $dst: $!\n"); next; };
			&run("chown $user $dst; chmod 400 $dst");
		} elsif ($locall =~ /$user/) { print STDERR "# $0: no entries for $user in $master, so using inbuilt values\n" if $verbose;
		} else { print STDERR "# $0: no entries for $user in $master\n" }
	}
}
&summary;

sub getent {
	my ($src, $type) = @_;
	my ($seen_OK) = 0;
	my $cmd = ($src eq "remote") ? &www_get("$URL$type") : "getent $type || cat /etc/$type|";
	print STDERR "## Use '$cmd' for $src, $type\n" if $verbose;
	open(CMD, $cmd) || die("$0: failed to '$cmd': $!\n");
	my (%p) = ();
	while (<CMD>) {
		$seen_OK = 1 if /^OK$/;
		if ($type eq $PASSWD) {
			close(CMD), return $_ if /^$user:/;
			if ($seen_OK) {
				close(CMD);
				return $localpw if $locall =~ /$user/ && $src eq "remote";
				return '';
			}
		} else {
			my $g = $1 if /^([^:]+):/;
			my $r = $_;
			$r =~ s/.*:/,/;
			$r =~ s/$/,/;
			$p{$g} = $_ if ($src ne "remote") || $r =~ /,$user,/;
		}
	}
	close(CMD) || die("$0: close of '$cmd' gave $!\n");
	$seen_OK || $src ne "remote" || die("$0: incomplete data from '$cmd' gave $!\n");
	return '' if $type eq $PASSWD && $src ne "remote";
	die("$0: failed to '$cmd'\n") if $type eq $PASSWD;
	if ($locall =~ /$user/ && $src eq "remote") {
		my($m) = (%p) ? "have info" : "no info";
		$p{$user} = $localgroup;
		my ($p) = join("> <", %p);
		print STDERR "# For $user, $src, $type: $m: <$p>\n" if $debug;
	}
	return %p;
}

sub add_user {
	print STDERR "# $0: add_user $user\n" if $verbose;
	my $PW = &getent("remote", $PASSWD);
	warn("$0: no information for $user\n"), return 0 unless $PW;
	chomp($PW);
	my @F = split(/:/, $PW);
	my $g = '';
	my $dup = '';
	my $exp = ($root) ? " -e 2000-01-01" : "";
	my $pw = &rootpw($F[1]);

	if ($groups =~ s/^,//) { $g = " -G $groups"; $groups = '' }
	$dup = "-o " if $locall =~ /$user/;
	return ! &run(($pwcmd) ?
		"echo '$pw' | pw useradd $user -H 0 -u $F[2] -g $F[3] -c '$F[4]' -d $F[5] -s $F[6] $dup$g":
		"useradd -p '$pw' -u $F[2] -g $F[3] -c '$F[4]' -d $F[5] -s $F[6] -K UMASK=022 $dup-m$g$exp $user"
	);
}

sub add_groups {
	print STDERR "# $0: add_group $user\n" if $verbose;
	my %localg = &getent("local", $GROUPS);
	my $rc = 1;
	if ($debug) {
		foreach $key (sort keys %localg) {
			print "## $key: $localg{$key}";
		}
	}
	my %remoteg = &getent("remote", $GROUPS);
	foreach $key (sort keys %remoteg) {
		if (($freebsd) && (length($key) > 16)) {
			print STDERR "# Skip group $key as too long\n";
			next;
		}
		unless ($localg{$key}) {
			my @F = split(/:/, $remoteg{$key});
			$rc = 0 if &run(
				($pwcmd) ? "pw groupadd $key -g $F[2]" :
					"groupadd -o -g $F[2] $key"
			);
		}
		unless ($localg{$key} && $localg{$key} =~ /[:,]$user/) {
			$groups .= ",$key";
		} else {
			print STDERR "# $0: $user already in $key\n" if $verbose;
			$quiet[$q_in] .= " $key";
		}
	}
	return $rc;
}

sub summary {
	return if $quiet;
	if ($quiet[$q_in_pwf] || $quiet[$q_in] || $quiet[$q_exists]) {
		print STDERR "# $foruser:";
		print STDERR " already in /etc/passwd" if $quiet[$q_in_pwf];
		$quiet[$q_in] =~ s/^ //;
		print STDERR " in groups '$quiet[$q_in]'" if $quiet[$q_in];
		print STDERR " already has authorized_keys" if $quiet[$q_exists];
		print STDERR "\n";
	}
	@quiet = (0,'',0);
}
sub run {
	my ($cmd) = @_;
	if ($n) {
		print STDERR "# RUN: $cmd\n";
		return 0;
	}
	my $rc = system($cmd);
	if ($rc) { print STDERR "** '$cmd' gave $rc / $!\n" }
	else { print STDERR "# ran: '$cmd' OK\n" }
	return $rc;
}

sub rootpw {
	my ($def) = @_;
	my ($file) = "/etc/shadow";
	$file = "/etc/master.passwd" unless -e $file;
	$file = "/etc/passwd" unless -e $file;

	unless (open(SHADOW, $file)) {
		warn("$0: failed to open $file: $!\n");
		return $def;
	}
	while (<SHADOW>) {
		my @F = split(/:/);
		$def = $F[1] if ($F[0] eq "root" && $F[1] ne "*") || $F[0] eq "localadmin";
	}
	warn("$0: failed to open /etc/shadow: $!\n") unless close(SHADOW);
	return $def;
}

# Use whatever command is available to fetch a web page
sub www_get {
	my ($url) = @_;
	my ($res) = '';
	$res .= "||fetch -q -o - $url" if  $freebsd && &cmd("fetch");
	$res .= "||wget $url -qO-" if  &cmd("wget");
	$res .= "||curl -s $url" if  &cmd("curl");
	$res .= "||fetch -q -o - $url" if (!$freebsd) && &cmd("fetch");
	$res =~ s/^\|\|//;
	return "$res|";
}

# Is this command available?
sub cmd {
	my ($cmd) = @_;
	my ($res) = `type $cmd 2> /dev/null`;
	$res  =~ s/.* not found\n*//m if $res;
	return $res;
}
