#!/usr/bin/perl
# This script takes passwd/groups databases necessary
# to access NFS filespace available in an organization
# (normally distributed via LDAP) and installs them locally as static files
# in /etc, such that no online LDAP connection has to be maintained to the
# organization when accessing their file servers via NFS.
#
# Markus Kuhn -- 2016-05
#
# Usage: sudo ./merge-pwgr -p $LOGNAME:$LOGNAME

use strict;

# uid and gid range controlled by remote LDAP tables
# [CL defaults: https://wiki.cam.ac.uk/cl-sys-admin/UID/GID_allocation]
my @ldap_uids = (1100, 9999);
my @ldap_gids = (500, 599, 1100, 9999);

# users and groups considered local even if they appear in the LDAP tables
my %protected_user; 
my %protected_group; 

sub inrange {
    my ($x, @limits) = @_;

    while (@limits > 1) {
	my $min = shift @limits;
	my $max = shift @limits;
	return 1 if $x >= $min && $x <= $max;
    }
    die if @limits;
    
    return 0;
}

sub load_colon_table {
    my ($fn) = @_;
    my $f;
    my @table;

    open($f, $fn) or die("Can't read '$f': $!\n");
    while (<$f>) {
	chomp;
	push @table, [split(/:/, $_, -1)];
    }
    close($f);
    return @table;
}

sub save_colon_table {
    my ($fn, @table) = @_;
    my $fntmp = $fn;
    my $f;

    if ($fn) {
	$fntmp = "$fn~$$";
	open($f, '>', $fntmp) || die("cannot write '$fntmp': $!\n");
    } else {
	open($f, '>-') || die;
    }

    eval {
	foreach my $row (@table) {
	    print $f join(':', @{$row}), "\n";
	}
	close($f);
	rename $fntmp, $fn || die("cannot rename '$fntmp' to '$fn': $!\n")
	    unless $fntmp eq $fn;
    };
    if ($@) {
	unlink $fntmp;
	die $@;
    }
}

my $delete = 0;

# parse options
while (@ARGV, $ARGV[0] =~ /^-/) {
    $_ = shift @ARGV;
    if (/^-d$/) {
	$delete = 1;
    } elsif (/^-p$/) {
	my $user = shift @ARGV;
	if ($user =~ /^([\w-]*):([\w-]*)$/) {
	    $protected_user{$1}  = 1;
	    $protected_group{$2} = 1;
	} elsif ($user =~ /^([\w-]+)$/) {
	    $protected_user{$1}  = 1;
	}
    } else {
	die("Unknown command-line argument '$_'\n");
    }
}

my $ldap_passwd_fn = '/etc/passwd-cl';
my $ldap_group_fn  = '/etc/group-cl';

my @ldap_passwd;
my @ldap_group;

my $nologin_shell = '/bin/false';

unless ($delete) {
    # load LDAP-derived passwd and group tables
    @ldap_passwd = load_colon_table($ldap_passwd_fn);
    @ldap_group  = load_colon_table($ldap_group_fn);
    
    # trim LDAP tables to allowed uid/gid ranges
    @ldap_passwd = grep { inrange($_->[2], @ldap_uids) &&
			      !$protected_user{$_->[0]} } @ldap_passwd;
    @ldap_group  = grep { inrange($_->[2], @ldap_gids) &&
			      !$protected_group{$_->[0]} } @ldap_group;
    
    # disabling login shell for LDAP users
    if ($nologin_shell) {
	foreach my $pw (@ldap_passwd) { $pw->[6] = $nologin_shell }
    }
}

# fetch local passwd and group tables
my @local_passwd = load_colon_table("/etc/passwd");
my @local_group  = load_colon_table("/etc/group");

# strip LDAP users and groups from local tables
@local_passwd = grep { !inrange($_->[2], @ldap_uids) ||
			   $protected_user{$_->[0]} } @local_passwd;
@local_group  = grep { !inrange($_->[2], @ldap_gids) ||
			   $protected_group{$_->[0]} } @local_group;

# index ldap tables
my %local_passwd_by_uid  = map { $_->[2] => $_ } @local_passwd;
my %local_passwd_by_name = map { $_->[0] => $_ } @local_passwd;
my %local_group_by_gid   = map { $_->[2] => $_ } @local_group;
my %local_group_by_name  = map { $_->[0] => $_ } @local_group;

# search for collisions
foreach my $pw (@ldap_passwd) {
    my $collission = ( $local_passwd_by_name{$pw->[0]} //
		       $local_passwd_by_uid{$pw->[2]} );
    if ($collission) {
	warn("duplicate user entries\n" .
	     "  local: " . join(':', @{$collission}) . "\n",
	     "  ldap:  " . join(':', @{$pw}) . "\n");
    }
}
foreach my $gr (@ldap_group) {
    my $collission = ( $local_group_by_name{$gr->[0]} //
		       $local_group_by_gid{$gr->[2]} );
    if ($collission) {
	warn("duplicate group entries\n" .
	     "  local: " . join(':', @{$collission}) . "\n",
	     "  ldap:  " . join(':', @{$gr}) . "\n");
    }
}

save_colon_table('/etc/passwd', @local_passwd, @ldap_passwd);
save_colon_table('/etc/group',  @local_group,  @ldap_group);
