#!/usr/bin/perl -wT
#
# $FreeBSD: CVSROOT/cvs_acls.pl,v 1.7 1999/09/19 13:18:47 peter Exp $
#
# Access control lists for CVS.  dgg@ksr.com (David G. Grubbs)
#
# CVS "commitinfo" for matching repository names, running the program it finds
# on the same line.  More information is available in the CVS man pages.
#
# ==== INSTALLATION:
#
# To use this program as I intended, do the following four things:
#
# 0. Install PERL.  :-)
#
# 1. Put one line, as the *only* non-comment line, in your commitinfo file:
#
#	DEFAULT		/usr/local/bin/cvs_acls
#
# 2. Install this file as /usr/local/bin/cvs_acls and make it executable.
#
# 3. Create a file named $CVSROOT/CVSROOT/avail.
#
# ==== FORMAT OF THE avail FILE:
#
# The avail file determines whether you may commit files.  It contains lines
# read from top to bottom, keeping track of a single "bit".  The "bit"
# defaults to "on".  It can be turned "off" by "unavail" lines and "on" by
# "avail" lines.  ==> Last one counts.
#
# Any line not beginning with "avail" or "unavail" is ignored.
#
# Lines beginning with "avail" or "unavail" are assumed to be '|'-separated
# triples: (All spaces and tabs are ignored in a line.)
#
#   {avail.*,unavail.*} [| user,user,... [| repos,repos,...]]
#
#    1. String starting with "avail" or "unavail".
#    2. Optional, comma-separated list of usernames.
#    3. Optional, comma-separated list of repository pathnames.
#	These are pathnames relative to $CVSROOT.  They can be directories or
#	filenames.  A directory name allows access to all files and
#	directories below it.
#
# Example:  (Text from the ';;' rightward may not appear in the file.)
#
#	unavail			;; Make whole repository unavailable.
#	avail|dgg		;; Except for user "dgg".
#	avail|fred, john|bin/ls	;; Except when "fred" or "john" commit to
#				;; the module whose repository is "bin/ls"
#
# PROGRAM LOGIC:
#
#	CVS passes to @ARGV an absolute directory pathname (the repository
#	appended to your $CVSROOT variable), followed by a list of filenames
#	within that directory.
#
#	We walk through the avail file looking for a line that matches both
#	the username and repository.
#
#	A username match is simply the user's name appearing in the second
#	column of the avail line in a space-or-comma separate list.
#
#	A repository match is either:
#		- One element of the third column matches $ARGV[0], or some
#		  parent directory of $ARGV[0].
#		- Otherwise *all* file arguments ($ARGV[1..$#ARGV]) must be
#		  in the file list in one avail line.
#	    - In other words, using directory names in the third column of
#	      the avail file allows committing of any file (or group of
#	      files in a single commit) in the tree below that directory.
#	    - If individual file names are used in the third column of
#	      the avail file, then files must be committed individually or
#	      all files specified in a single commit must all appear in
#	      third column of a single avail line.
#

#XXX use strict;

my $CVSROOT = $ENV{'CVSROOT'} || "/home/ncvs";
my $PID     = getpgrp();
my $LOGIN   = $ENV{USER} || getlogin || (getpwuid($<))[0] ||
                sprintf("uid#%d",$<);
#$myname = $ENV{"USER"} if !($myname = $ENV{"LOGNAME"});

# Detaint the world.
$ENV{PATH}  = "/bin:/sbin:/usr/bin:/usr/sbin";
delete      @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
$CVSROOT    =~ /(.*)/;
$CVSROOT    = $1;

# Get our configuration.
require         "$CVSROOT/CVSROOT/config.pm";
use vars        qw($DEBUG $TMPDIR);
import config;


print "CVS_ACLS: ", join(",", @ARGV), "\n";
$die = '';
$availfile = "$CVSROOT/CVSROOT/avail";

eval "print STDERR \$die='Unknown parameter $1\n' if !defined \$$1; \$$1=\$';"
    while ($ARGV[0] =~ /^(\w+)=/ && shift(@ARGV));
exit 255 if $die;		# process any variable=value switches

die "Must set CVSROOT\n" unless $CVSROOT;
($repos = shift) =~ s:^$CVSROOT/::;
grep($_ = $repos . '/' . $_, @ARGV);

if ($DEBUG) {
	print "$PID Repos: $repos\n";
	print map { "$PID ==== $_\n" } @ARGV;
}

$exit_val = 0;				# Good Exit value

$universal_off = 0;
open AVAIL, "< $availfile" or exit(0);	# It is ok for avail file not to exist
while (<AVAIL>) {
    chomp;
    next if /^\s*#?$/;

    my ($flagstr, $u, $m) = split(/[\s,]*\|[\s,]*/, $_);

    # Skip anything not starting with "avail" or "unavail" and complain.
    unless ($flagstr =~ /^avail/ or $flagstr =~ /^unavail/) {
	print "Bad avail line: $_\n";
	next;
    }

    # Set which bit we are playing with. ('0' is OK == Available).
    $flag = (($& eq "avail") ? 0 : 1);

    # If we find a "universal off" flag (i.e. a simple "unavail") remember it
    $universal_off = 1 if $flag && !$u && !$m;

    # $LOGIN considered "in user list" if actually in list or is NULL
    $in_user = (!$u || grep ($_ eq $LOGIN, split(/[\s,]+/,$u)));
    print "$PID \$LOGIN($LOGIN) in user list: $_\n" if $DEBUG && $in_user;

    # Module matches if it is a NULL module list in the avail line.  If module
    # list is not null, we check every argument combination.
    $in_repo = (!$m || 0);
    unless ($in_repo) {
	@tmp = split(/[\s,]+/,$m);
	for $j (@tmp) {
	    # If the repos from avail is a parent(or equal) dir of $repos, OK
	    if ($repos eq $j || $repos =~ /^$j\//) {
		$in_repo = 1;
		last;
    }
	}
	unless ($in_repo) {
	    #$in_repo = 1;
	    for $j (@ARGV) {
		last unless ($in_repo = grep ($_ eq $j, @tmp));
	    }
	}
    }
    print "$PID \$repos($repos) in repository list: $_\n" if $DEBUG && $in_repo;

    $exit_val = $flag if $in_user && $in_repo;
    if ($DEBUG) {
	print "$PID ==== \$exit_val = $exit_val\n";
	print "$PID ==== \$flag = $flag\n";
    }
}
close AVAIL;

print "$PID ==== \$exit_val = $exit_val\n" if $DEBUG;
print "**** Access denied: Insufficient Karma ($LOGIN|$repos)\n" if $exit_val;
print "**** Access allowed: Personal Karma exceeds Environmental Karma.\n"
	if $DEBUG && $universal_off && !$exit_val;
exit($exit_val);

#end
