#!/usr/bin/perl
# mvids: "moves" uids and gids
# Tom Christiansen <tchrist@convex.com>
# Modified by: Tres Hofmeister <tres@ncar.ucar.edu>
#   3/29/93
#   Use find.pl and `lstat' rather than reading a pipe from find(1):
#       For better portability, as not all finds support -fstype or -ls;
#       indexing into the find string breaks on device special files and
#       filenames containing whitespace; `split' was breaking up the
#       string inconsistently because of leading whitespace (using
#       split(' ') and changing the indexing could also fix this).
#   Fixed the non-existent user/group tests, which were using the wrong
#       array names;
#   Allow negative uid/gid's.
#   Delete temporary files if -n is specified.
#   Changed the name of the default directions file to `mvids.cf'.
#   Various minor or cosmetic changes, including addition of comments.
#
# Usage: mvids [-n] [-d] [-f dfile] [startdir]
#	
# Takes a file of new user and group id's, creates and installs new
# passwd and group files, and traverses local filesystems starting with
# startdir (default is `/'), updating all changed id's.
#
# Read descriptions from `mvids.cf' file with format:
#     type name number
# e.g.:
#     user tom 1023
#     group staff 200
#
# Options:
# -n  Find files and create the new passwd and group files, but don't
#        actually change anything.
# -f  Specify an alternate description file than the default `mvids.cf'.
# -d  Use the passwd and group files in the current directory.

require 'find.pl';
require 'getopts.pl';

$| = 1;
$oops = 0;
($prog = $0) =~ s#.*/##;

&Getopts('dnf:');
$FILE = $opt_f || "mvids.cf";
$DIR  = $opt_d ? "." : "/etc";
$topdir = shift || '/';

die "Usage: $prog [-n] [-d] [-f dfile] [startdir]\n" if $#ARGV > -1;
die "$topdir: Not a directory" unless -d $topdir;

# Process directions file.
open(FILE) || die "Can't open directions file \"$FILE\": $!\n";
while (<FILE>) {
    s/\s*#.*//;
    next if /^$/;
    unless (/^(user|group)\s+(\w+)\s+(\d+)/) {
	print STDERR "malformed line at line $. of $FILE: $_";
	$oops++; next;
    }
    if ($3 > 32767) {
	print STDERR "$1 $2 has id that's too big ($3)\n";
	$oops++; next;
    }
    if ($3 == 0) {
	print STDERR "Too dangerous to move $1 $2 to 0\n";
	$oops++; next;
    }
    if ($2 eq 'root') {
	print STDERR "You don't really want to move root!\n";
	$oops++; $next;
    }
    if ($1 eq 'user') {
	if (defined $n_pwn2i{$2}) {
	    print STDERR "Saw user $2 again at line $. of $FILE\n";
	    $oops++; next;
	}
	if (defined $n_pwi2n{$3}) {
	    print STDERR "Saw uid $3 again at line $. of $FILE\n";
	    $oops++; next;
	}
	$uids++;

	# Build %n_pwn2i and %n_pwi2n.
	$n_pwn2i{$2} = $3;
	$n_pwi2n{$3} = $2;
    }
    else {
	if (defined $n_grn2i{$2}) {
	    print STDERR "Saw group $2 again at line $. of $FILE\n";
	    $oops++; next;
	}
	if (defined $n_gri2n{$3}) {
	    print STDERR "Saw gid $3 again at line $. of $FILE\n";
	    $oops++; next;
	}
	$gids++;

	# Build %n_grn2i and %n_gri2n.
	$n_grn2i{$2} = $3;
	$n_gri2n{$3} = $2;
    }
}

# Process the existing passwd file, build the new one.
if ($uids) {
    $PWD  = "$DIR/passwd";
    $NPWD = "$DIR/passwd.new";
    open(PWD)			|| die "Can't open $PWD: $!\n";
    open (NPWD, ">$NPWD") 	|| die "Can't create $NPWD: $!\n";

    while (<PWD>) {
	((($name, $uid) = /^(\w+):[^:]*:(-?[\d]+):/))
	    || die "Bad passwd entry at line $.\n";
	if (defined $n_pwi2n{$uid} && !defined $n_pwn2i{$name}) {
	    printf STDERR "Can't move user %s to uid %d, %s already has it\n",
		    $n_pwi2n{$uid}, $uid, $name;
	    $oops++;
	    next;
	}

	# Build %pwn2i.
	$pwn2i{$name} = $uid;

	# Edit the current line if necessary.
	s/:$uid:/:$n_pwn2i{$name}:/ if defined $n_pwn2i{$name};
	print NPWD;
    }
    close PWD;
    close NPWD;

    foreach $user (keys %n_pwn2i) {
	unless (defined $pwn2i{$user}) {
	    print STDERR "Can't move non-existent user $user\n";
	    $oops++;
	}
    }
}

# Process the existing group file, build the new one.
if ($gids) {
    $GRP  = "$DIR/group";
    $NGRP = "$DIR/group.new";
    open(GRP)			|| die "Can't open $GRP: $!\n";
    open (NGRP, ">$NGRP") 	|| die "Can't create $NGRP: $!\n";

    while (<GRP>) {
	((($name, $gid) = /^(\w+):[^:]*:(-?[\d]+):/))
	    || die "Bad group entry at line $.\n";
	if (defined $n_gri2n{$gid} && !defined $n_grn2i{$name}) {
	    printf STDERR "Can't move gid %s to %d, %s already has it\n",
		    $n_gri2n{$gid}, $gid, $name;
	    $oops++;
	    next;
	}

	# Build %grn2i.
	$grn2i{$name} = $gid;

	# Edit the current line if necessary.
	s/:$gid:/:$n_grn2i{$name}:/ if defined $n_grn2i{$name};
	print NGRP;
    }
    close GRP;
    close NGRP;

    foreach $group (keys %n_grn2i) {
	unless (defined $grn2i{$group}) {
	    print STDERR "Can't move non-existent group $group\n";
	    $oops++;
	}
    }
}

# Exit if there were errors processing files, or if there's nothing to do.
die "$prog: no ids to move\n" unless $uids || $gids;
die "$prog: $oops error" . ($oops > 1 ? "s" : "") .
	" in remapping directions.\n" if $oops;

# Ok, now do it.

# Build %pwi2n from %pwn2i.
foreach $key (keys %pwn2i) {
    $pwi2n{$pwn2i{$key}} = $key;
}
# Build %gri2n from %grn2i.
foreach $key (keys %grn2i) {
    $gri2n{$grn2i{$key}} = $key;
}

&find("$topdir");

sub wanted {
    # Called by &find.  $name contains the current pathname,
    # $_ contains the filename component of the pathname.

    # Prune NFS filesystems.
    unless ((($dev, $user, $group) = (lstat($_))[0,4,5])
	&& $dev < 0 && ($prune = 1)) {

	$uid = $gid = -1;
	$file = $name;

	# Convert numeric id's to names.
	$user = $pwi2n{$user};
	$group = $gri2n{$group};

	# If this file is owned by a user to be changed...
	if (defined $n_pwn2i{$user}) {
	    $uid = $n_pwn2i{$user};
	    print "changing owner $user of $file from ",
		    "$pwn2i{$user} to $n_pwn2i{$user}\n";
	}

	# If this file is in a group to be changed...
	if (defined $n_grn2i{$group}) {
	    $gid = $n_grn2i{$group};
	    print "changing group $group of $file from ",
		    "$grn2i{$group} to $n_grn2i{$group}\n";
	}

	# Change the uid and/or gid of the file.  If both are still -1,
	# this file doesn't need changing.  Passing chown -1 for the
	# uid or gid leaves it unchanged.
	if (!$opt_n && ($uid != -1 || $gid != -1)) {
	    if (!chown $uid, $gid, $_) {
		print STDERR "couldn't chown $file to $uid.$gid: $!\n";
		$oops++;
	    }
	}
    }
}

# Install the new passwd and group files...
unless ($opt_n) {
    if ($uids) {
	rename($PWD, "$PWD.bak") 	
	    || die "Can't mv $PWD to $PWD.bak: $!\n";
	rename($NPWD, $PWD)		
	    || die "Can't mv $NPWD to $PWD: $!\n";
    }
    if ($gids) {
	rename($GRP, "$GRP.bak")
	    || die "Can't mv $GRP to $GRP.bak: $!\n";
	rename($NGRP, $GRP)
	    || die "Can't mv $NGRP to $GRP: $!\n";
    }
}
else {
    # Clean up if we were just testing.
    unlink("$NPWD", "$NGRP");
}

exit ($oops != 0);