#!/usr/bin/perl5.004 
#################################################################################
#     Copyright (C) 2001, 2002 Tilo Sloboda <unixNOSPAM@unixgods.org>
#
#     $Id: get_cvs_users_from_LDAP.pl,v 1.6 2002/10/23 05:39:53 tilo Exp $
#
#     This script is an example to show you how to write your own script..
#     to query an LDAP server and create Access Control Lists for being used 
#     in combination with a specially modified CVS-Server, which is described at:
#
#            http://www.unixgods.org/~tilo/CVS_ACL/index.html
#
#     The modified CVS executable checks if a user has access to the CVS content, 
#     by verifying that s/he is on an Access Control List (ACL).
#
#
#     YOU NEED TO MAKE CHANGES to this script, before you can run/use it!
#
#     You will also need perLDAP to run this script:  http://www.perldap.org/
#
#################################################################################
# 
#     This script is supposed to be run via cron, or manually.
#
#     This script creates an Access Control List, containing valid UNIX user-IDs,
#     by doing LDAP-queries, and then adding some users which are on an exception list.
#     The result is an ACL, which contains the list of all valid CVS-users, who will
#     be granted CVS-access.
#
#     The script uses the RCS Revision Control System to keep track of all versions of 
#     the ACL, and copies the new ACL to the production CVS-servers via SSH, after 
#     checking it in to a local RCS. - yes you want to store this only locally!
#
#     If you're running LDAP in a production environment, you need to be paraonid
#     (or course)..   
#
#     This script is assumed to run on a special "secure" machine - it will bail out
#     if run elsewhere.
#
#     To prevent that the ACLs or anything related to this mechanism was modified 
#     "by hand" on the production CVS-servers, we maintain a md5sum of each ACL, 
#     and keep all versions of the ACL in RCS, and we check if something changed unexpectedly..
#
#     There is a lot of PARANOIED checking done towards the end of this script, 
#     to make sure nobody messes around with the ACLs, or permissions, and other
#     things we expect..
#
# LDAP-Attributes:
#
#     This script creates an Access Control List, by querying LDAP for people who
#     are in your company, and by querying an LDAP-attribute which says that 
#     determines if they should or should not access CVS.
#    
#     Let's say you use a certain attribute in LDAP, let's call it e.g. "somestatusattribute".
#     If the flag is set to "Y", we DO NOT want the person to access CVS, although they 
#     might have a valid LDAP-entry in your company's LDAP-server.
#     (sorry for the inverted logic of the flag.. historic reasons can be blamed..)
#
#     All other LDAP-entries in "ou=people,dc=YOURCOMPANY,dc=com" are allowed to access CVS.
#
#     If you have users from other companies in your LDAP-tree, e.g. with
#     "ou=people,dc=OTHERCOMPANY,dc=com" , they will NOT be allowed to access CVS.
#     You need to explicitly grant it - e.g. on the exception list. /etc/yp/CVS-ACL-exceptions 
#
#     The resulting list is concatinated with an exception list, of users
#     who still need access to the CVS-server. (maybe people from other companies, or 
#     people who have the "somestatusattribute" set but still are allowed access).
#
#     The exception list is also checked against LDAP, users need a valid UNIXuserID
#     and their employeetype must NOT be UNKNOWN (those would be expired accounts)
#
# Directory Structure / Security:
# 
#     This script should be run on YOUR_MASTER_SERVER as root
#
#     For the access control mechanism to work, the directory /opt/admin/etc needs to exist, 
#     needs to be owned by root, and only writable by root.  
#     And the file /opt/admin/etc/valid_cvs_users also needs to exist, to be owned by root, 
#     and only writable by root.
#
#     The file /opt/admin/etc/valid_cvs_users needs to contain a complete list of all people, 
#     who have access to CVS. All users also need to have a valid UNIX account listed in NIS/yp.
#
#     The file  /etc/yp/CVS-ACL-exceptions  is supposed to be edited by hand.. 
#     I can only recommend to also check this file in to RCS locally (/etc/yp/RCS)
#
#     These permissions and directory structures are strictly enforced by this script.
#
#
# AUTHOR:   Tilo Sloboda <unixNOSPAM@unixgods.org> ,  2001 May 03
# UPDATED:                                            2002 Oct 22
#
# USAGE:    run via crontab, or from the command line. use -v for verbose output.
#
#################################################################################

use Getopt::Std;		# To parse command line arguments.
use Mozilla::LDAP::Conn;
use Mozilla::LDAP::Utils;
use POSIX qw/getpgrp tcgetpgrp/;

$ENV{'PATH'} = "/tools/bin:$ENV{'PATH'}";  # make sure /tools/bin is in the PATH

@warnings = ();

$email = "cvs-admin\@YOURCOMPANY.COM";  # where do we send email to? 

$iamroot        = ($> == 0);          # is the script run by root?
$iaminteractive = i_am_interactive(); # is the script run interactively? 

$righthost  = "YOUR_MASTER_SERVER";  # DO NOT CHANGE THIS! that's where we expect the script to be run

# You need to list the names all of your CVS-servers here:
# let's say you have 3 production CVS-servers ..

@cvsservers = ("server1","server2","server3");   # complete list of your CVS servers
 
$thishost       = `uname -n`;       # where is this script running? 
chomp $thishost;
@otherservers = grep {$_ ne $thishost} @cvsservers; # in case we run on one of the servers

$md5sum = "/tools/bin/md5sum";      # location of md5sum program   - you need to change this

if ($thishost eq 'YOUR_MASTER_SERVER') { # this might be a "very secure" host..
   $ssh = "/usr/sbin/ssh";             # use special ssh wrapper, if you have those
   $scp = "/usr/sbin/scp";             # use special scp wrapper, if you have those
} else {
   $ssh = "ssh";
   $scp = "scp";
}

# Constants, shouldn't have to edit these...
#
$APPNAM	= $0;
$USAGE	= "$APPNAM [-v]\n    -v    for verbose output\n";

#################################################################################
# NOTE: 
#    you might want to put those ACL files into a special directory, which 
#    might have very restricted access.. 
#
#    Let's say we use /opt/admin/etc/ for this
#################################################################################
# here is where the output files will be copied to, on the remote CVS-servers:
#
$CVSADM_DIR =              "/opt/admin/etc";
$CVSADM_VALID_USERS =      "/opt/admin/etc/valid_cvs_users";
$CVSADM_VALID_USERS_LOCK = "/opt/admin/etc/valid_cvs_users.lock";

#################################################################################
# Access Control Lists (ACLs):
#
# Let's say we use certain LDAP attributes, to derive our ACL, but we were not
# able to create new LDAP attributes specifically for controlling CVS-access 
# (because it's a big production LDAP server) -- but we're using other attributes
# to derive our ACL, and then use an "exception list" to still allow certain 
# (trustworthy?) people access to our CVS server..
#
# If you don't need an exception list, all the better.
#################################################################################
#
# The Cron Job is expected to run on one machine YOUR_MASTER_SERVER , which is 
# supposedly a very secure and locked down machine.
# 
# The directory SOURCE_DIR and subdirectories thereof are supposed to be locked down.
#
# The exception lists are stored in RCS locally (!) inside this locked down directory
# on this locked down server.. 
#
# you might want to use your MASTER YP Server to run this script.. this machine should
# be secured and locked down (hopefully ;-)) .. that's where the strange "yp" directory 
# comes from .. you might want to change the "yp" to something else though

$SOURCE_DIR         =      "/etc/yp";   # that's where the source file for the exceptions lives. 

$SOURCE_EXCEPTIONS  =      "/etc/yp/CVS-ACL-exceptions";
$SOURCE_EXCEPTIONS_RCS  =  "/etc/yp/RCS/CVS-ACL-exceptions,v";

$SOURCE_VALID_USERS     =  "/etc/yp/valid_cvs_users";
$SOURCE_VALID_USERS_RCS =  "/etc/yp/RCS/valid_cvs_users,v";

# to prevent two instances of the same program being run at the same time:
#   NOTE: we may want to create this lock file in /tmp/ , so it get's removed
#         automaticlly in case of a system reboot..  ;-)

$SELF_LOCK          =      "$SOURCE_DIR" . "/". "CVS_ACL_UPDATE.LOCK";

$MAX_CHANGES = 30;         # max. number of changes between versions of the ACL, 
                           # before we start complaining..  (.. layoffs? ;-))

#################################################################################
## SUBROUTINES
#################################################################################

# in case we're run interactively, we die via die(), otherwise send email and 
# commit suicide.. 

sub DIE {
  my ($message) = @_;

  unlink $SELF_LOCK;

  if ($iaminteractive) {

      die $message;

  } else {
      open(MAIL, "|/usr/lib/sendmail -t -f $email");
      print MAIL <<EOF;
To: $email
From: CVS ACL cron job \<$email\>
Subject: CVS PROBLEM DETECTED on $thishost - action required!

Following problem was detected by $0 on $thishost:

$message

@warnings

EOF

      close(MAIL);
      exit(0);
  }
}
#################################################################################

# send email with all the warnings we got, and exit OK..

sub warnNexit {

  if (@warnings) {
     if ($iaminteractive) {

        print "SUMMARY OF WARNINGS:\n @warnings\n";

     } else {
     
        open(MAIL, "|/usr/lib/sendmail -t -f $email");
        print MAIL <<EOF;
To: $email
From: CVS ACL cron job \<$email\>
Subject: CVS WARNINGS DETECTED on $thishost - please check!

Following warnings were generated by $0 on $thishost:

@warnings

EOF

        close(MAIL);
     }
  }
  unlink $SELF_LOCK;
  exit(0);
}

#################################################################################
# test if this script is running interactively..

sub i_am_interactive() {
  local *TTY;     # local file handle
  if ( open(TTY, "/dev/tty") ) {
     my $tpgrp = tcgetpgrp(fileno(TTY));
     my $pgrp  = getpgrp();
     close TTY;
     return ($tpgrp == $pgrp);
  }
  return 0;
}

#################################################################################
# add a warning to the buffer and print if interactive

sub WARN {
  my ($message) = @_;
  $warnings[++$#warnings] = $message;

  warn $message  if ($iaminteractive && $opt_v);
}

#################################################################################
# interrupt handler to remove stray files before exiting the program.

sub interrupt_handler {
  my ($signal) = @_;
 
  unlink $SOURCE_VALID_USERS;
  unlink $SELF_LOCK;
  `rcs -q -u $SOURCE_VALID_USERS`;   # unlock the RCS file with the valid-users file

  DIE "ERROR: died on signal $signal!\n";
}

#################################################################################
# bail out if we're running on the wrong machine. 

DIE "ERROR: you need to run $0 as root on $righthost!\n" if ($thishost ne $righthost);
DIE "ERROR: you need to be ROOT to run this script!\n" if (! $iamroot);

#################################################################################
# Check arguments, and configure some parameters accordingly..
#
if ( !getopts('v') ) {
  if ($iaminteractive) {
     print "usage: $USAGE\n";
     print "you need to be root to run this script!\n" if (! $iamroot);
     unlink $SELF_LOCK;
     exit;
   } else {
     DIE("ERROR: wrong parameters, calling $APPNAM\n");
   }
}
#################################################################################
# install interrupt handler, to remove stray files in case we get killed.. 

$SIG{"HUP"} = 'interrupt_handler';   
$SIG{"INT"} = 'interrupt_handler';   # in case user hits CTRL-C, 
$SIG{"QUIT"} = 'interrupt_handler';
$SIG{"ILL"} = 'interrupt_handler';
$SIG{"TRAP"} = 'interrupt_handler';
$SIG{"KILL"} = 'interrupt_handler';
$SIG{"BUS"} = 'interrupt_handler';   # ?? ;-)
$SIG{"SEGV"} = 'interrupt_handler';  # ?? ;-)
$SIG{"TERM"} = 'interrupt_handler';

#################################################################################
# bail out if we're running interactively and a lock file is present

if ($iaminteractive) {
   if (-f $SELF_LOCK) {
      DIE "ERROR: lock file $SELF_LOCK is present, but $0 is run interactively!\n       One instance of this script is probably run by cron right now! please wait.\n";
   }
} else {  # we're not interactive -- but run via crontab:

   # in case we find an old lock file, we wait up to 10 minutes for it to go away.
   $i = 0;
   while ((-f $SELF_LOCK) && ($i<30)) {
       sleep 20;     # sleep 20 seconds
       $i++
   }
   # if the lock file is still present, we commit suicide.. 
   DIE "ERROR: waited for 10 minutes for lock file $SELF_LOCK to disappear -- WEIRD STUFF HAPPENING!?\n" if (-f $SELF_LOCK);
}

# so we found no lock or it disappeared in time.. now we set the lock..

`touch $SELF_LOCK`;   # lock interactive instances of this script, so they don't update the ACL at the same time as we do. 

#################################################################################
# dates are expected to look like this:   Fri Apr 6 13:53:49 2001

$date = scalar localtime; chomp $date;
@date = split(/[ \t\n]+/,$date);
$date2 = join('_', $date[4],$date[1],$date[2],$date[3]);

$opt_h = "YOURLDAPHOST" unless defined($opt_h);
# $opt_w = 'ldappasswd here' unless defined($opt_w);  # only needed if you can't annonymously bind for reading
#                                                     # obviously DISCOURAGED!!!

$tmpfile = "/tmp/valid_cvs_users";
$outfile = "/tmp/valid_cvs_users_" . "$date2" . ".txt";  # just for verbose output

#################################################################################

####%ld = Mozilla::LDAP::Utils::ldapArgs("uid=ldapadmin", "ou=people,dc=YOURCOMPANY,dc=com");
%ld = Mozilla::LDAP::Utils::ldapArgs("", "dc=com");

$conn = new Mozilla::LDAP::Conn(\%ld);		# connection to LDAP SERVER
DIE "Could't connect to LDAP server $ld{host}" unless $conn;

#################################################################################
# A couple of sanity checks in regards to the SECURITY of this script.. 

DIE "PANIC: $APPNAM is not a plain file!??\n" if (! -f $APPNAM);

@stat = stat($APPNAM); 
DIE "PANIC: $APPNAM is not owned by root!\n" if ($stat[4] ne 0);
DIE "PANIC: $APPNAM is writable by non-root accounts!\n" if (($stat[2] & 0x2) || ($stat[2] & 0x10));
DIE "PANIC: $APPNAM is readable by non-root accounts!\n" if (($stat[2] & 0x20) || ($stat[2] & 0x4));
DIE "PANIC: $APPNAM is executable by non-root accounts!\n" if (($stat[2] & 0x1) || ($stat[2] & 0x8));

#################################################################################
# A couple of sanity checks in regards to the SECURITY of the ACL data files.. 

# check if SOURCE_EXCEPTIONS is a plain file owned by root and -r--r--r-- 

DIE "ERROR: $SOURCE_EXCEPTIONS does not exist, or is not a plain file! someone forgot \"ci -u\" perhaps?\n" if (! -f $SOURCE_EXCEPTIONS);
@stat = stat($SOURCE_EXCEPTIONS); 
DIE "ERROR: $SOURCE_EXCEPTIONS is not owned by root! PANIC!\n" if ($stat[4] ne 0);
DIE "ERROR: $SOURCE_EXCEPTIONS is writable by non-root accounts! PANIC!\n" if (($stat[2] & 0x2) || ($stat[2] & 0x10));

# bail out and send email in case somebody forgot to check in the exceptions-list
# (if we find a writable version lying around, or the RCS-diff has a non-zero length)

DIE "ERROR: $SOURCE_EXCEPTIONS is writable by root? Did someone forget to check it in?\n" if ($stat[2] & 0x80);

# check if the RCS file is there.. and permissions are -r--r--r-- 

DIE "ERROR: $SOURCE_EXCEPTIONS_RCS does not exist, or is not a plain file!\n" if (! -f $SOURCE_EXCEPTIONS_RCS);
@stat = stat($SOURCE_EXCEPTIONS_RCS); 
DIE "ERROR: $SOURCE_EXCEPTIONS_RCS is not owned by root! PANIC!\n" if ($stat[4] ne 0);
DIE "ERROR: $SOURCE_EXCEPTIONS_RCS is writable by non-root accounts! PANIC!\n" if (($stat[2] & 0x2) || ($stat[2] & 0x10));
DIE "ERROR: $SOURCE_EXCEPTIONS_RCS is writable by root? WHY??\n" if ($stat[2] & 0x80);

# check if the exceptions file identical to the checked in version
# (= that no changes were made by hand!)

chdir($SOURCE_DIR);

$changes = 0;
open(RCSDIFF, "rcsdiff -q $SOURCE_EXCEPTIONS|") || DIE "can not run rcsdiff\n!";
while(<RCSDIFF>) {
  $changes++
}
close(RCSDIFF);

# in case somebody edited the file by hand without using RCS, we need to bail out and send email
# to get the problem fixed.

if ($changes != 0) {
   DIE "ERROR: some changes were made to $SOURCE_EXCEPTIONS before $date2 ! Please check in the changes!\n";
}
#################################################################################
# make sure that the old valid_users_list is NOT checked out!

DIE "ERROR: $SOURCE_VALID_USERS is checked out! WHY??\n" if (-f $SOURCE_VALID_USERS);

# check if the RCS file is there.. and permissions are -r--r--r-- 

DIE "ERROR: $SOURCE_VALID_USERS_RCS does not exist, or is not a plain file!\n" if (! -f $SOURCE_VALID_USERS_RCS);
@stat = stat($SOURCE_VALID_USERS_RCS); 
DIE "ERROR: $SOURCE_VALID_USERS_RCS is not owned by root! PANIC!\n" if ($stat[4] ne 0);
DIE "ERROR: $SOURCE_VALID_USERS_RCS is writable by non-root accounts! PANIC!\n" if (($stat[2] & 0x2) || ($stat[2] & 0x10));
DIE "ERROR: $SOURCE_VALID_USERS_RCS is writable by root? WHY??\n" if ($stat[2] & 0x80);

# check out the old valid_users_list and get it's md5sum for later reference
`co -q $SOURCE_VALID_USERS`;
$old_md5sum = `$md5sum $SOURCE_VALID_USERS`;  
$old_md5sum =~ s/^(\w+).*/$1/; chomp $old_md5sum;

$old_lineN  = `wc -l $SOURCE_VALID_USERS`;
$old_lineN =~ s/\s*(\d+).*/$1/; chomp $old_lineN;

unlink $SOURCE_VALID_USERS;           # immediately remove it - we don't want any copy lying around at any time. 

#################################################################################
# read in the exceptions list ( A STATIC LIST of users..)
#
#     this script is robust against comments starting with # at any position
#     user name has to preceede comment 
#     user names may be (accidentially) indented
#     all comments and blank lines are ignored (comments start with #)

open(EX, "< $SOURCE_EXCEPTIONS") || DIE "ERROR: can not open \"$SOURCE_EXCEPTIONS\"\n";

$exceptionN2 = 0;

while(<EX>) {
   chomp;
   next if /^\s*$/;                           ### ignore this line if it is empty
   print "EXCEPTION: $_\n" if ($iaminteractive && $opt_v);

   next if /^#/;                              ### ignore this line if it is a comment
   next if /^\s*#/;                           ### ignore this line if it is an indented comment
   ($user,$comment) = split;
   if ($user ne "") {
      $exceptions{$user} =1;
      $exceptionN2++;
   }
}
close(EX);

@exceptions = sort keys %exceptions;
$exceptionN = $#exceptions +1;

WARN("WARNING: there are some DUPLICATES in the exception list! $exceptionN != $exceptionN2\n") if ($exceptionN != $exceptionN2);

print "read $exceptionN exceptions from file \"$SOURCE_EXCEPTIONS\"\n" if ($iaminteractive && $opt_v);

#################################################################################
# the list of exceptions is a STATIC list.. therefore we need to check if any
# of the users on this list has since expired! e.g. left the company..
#
# Some entries on the exceptions list can be "PseudoAccounts", which need a
# valid entry in LDAP under ou=PseudoAccounts,dc=YOURCOMPANY,dc=com , therefore
# we need to get the  dn  here as well!

@SYNCATTR = ( "uid", 
              "dn", 
              "unixuid",
              "employeetype",
              "somestatusattribute",
              "mail"
	      );

foreach $user (sort keys %exceptions) {
    # do LDAP query and look if EmployeeType is not equal Unknown
    # for the exception list we want to search dc=com , to include people outside of YOURCOMPANY
    
    $SEARCH = "uid=$user";
    
    print "searching LDAP server \"$ld{host}\" for user \"$user\"..\n" if ($iaminteractive && $opt_v);
    $source = $conn->search("dc=com", "subtree", $SEARCH, 0, @SYNCATTR);

    if($source) {
        $dn = $source->getDN();

        if( $dn =~ /ou=PseudoAccounts/i ) {
           # it's a Pseudo Account..

           if( ($source->{unixuid}[0] eq "") ) {
                 $exceptions{$user} = 0;
                 WARN("WARNING:  PseudoAccount \"$user\" has no valid UNIX user ID in LDAP!\n");
           }
        } elsif ( $dn =~ /ou=People/i ) {
          # it's a Person..

           if( ($source->{unixuid}[0] eq "") || ($source->{employeetype}[0] eq "Unknown") ) {
                 $exceptions{$user} = 0;
                 WARN("WARNING:  user \"$user\" is marked as \"Unknown\" or has no UNIXuid in LDAP!\n");
           }
        } else {
           # is it a T-H-I-N-G from outer space? 

           $exceptions{$user} = 0;
           WARN("WARNING:  found user \"$user\" in LDAP under \"$dn\" - oops! Will IGNORE user!\n");
        }
   } else {
           # it IS a T-H-I-N-G from outer space for sure!

           $exceptions{$user} = 0;
           WARN("WARNING:  LDAP query for user \"$user\" FAILED! user NOT in LDAP!!\n");
   }
}

#################################################################################

# Now find all LDAP entries and compare
#

@SYNCATTR = ( "uid",
              "unixuid",
              "employeetype",
              "somestatusattribute",
              "mail"
	      );

# $SEARCH = "(! (somestatusattribute=Y))";

$SEARCH = "(& (unixuid=*) (! (somestatusattribute=Y)))";   # we are searching the YOURCOMPANY tree.. 

print "searching LDAP server \"$ld{host}\"..\n" if ($iaminteractive && $opt_v);
print "      search('dc=YOURCOMPANY,dc=com', 'subtree', $SEARCH, 0, @SYNCATTR)\n\n" if ($iaminteractive && $opt_v);

$source = $conn->search("dc=YOURCOMPANY,dc=com", "subtree", $SEARCH, 0, @SYNCATTR);
print "done with search\n" if ($iaminteractive && $opt_v);

# Now we parse the results and store the results in a text file for later reference..
#
print "parsing search results..\n" if ($iaminteractive && $opt_v);

if ($opt_v) {
   open(OUT, "> $outfile") || DIE "ERROR: can not open output file \"$outfile\"\n";
}
$userN = 0;
$acceptN = 0;
$rejectN = 0;

while ($source)
{
 $userN++;

 if( $source->{unixuid}[0] ne "" ) {
   $user = $source->{unixuid}[0];
   
   if( $source->{employeetype}[0] ne "Unknown" ) {
      $acceptN++;
      printf(     "ACCEPT:  %s\t %s,%s,%s\n",  
                   $source->{uid}[0], $source->{unixuid}[0], 
                   $source->{employeetype}[0], $source->{somestatusattribute}[0]) if ($iaminteractive && $opt_v); 
      printf( OUT "%s\t %s,%s,%s\n",  
                   $source->{uid}[0], $source->{unixuid}[0], 
                   $source->{employeetype}[0], $source->{somestatusattribute}[0]) if $opt_v; 
      $validuser{$user} = 1;      
      
   } else {   # an expired account - we don't allow access for those! 
              # they need to get their PeopleSoft entry fixed by HR!
      $rejectN++;
      printf(     "REJECT:  %s\t %s,%s,%s\n",  
                   $source->{uid}[0], $source->{unixuid}[0], 
                   $source->{employeetype}[0], $source->{somestatusattribute}[0]) if ($iaminteractive && $opt_v);   
   }
# } else {    # user in LDAP without UNIX user ID - we ignore those!
#      printf(     "OOOOPS:  %s\t %s,%s,%s\n",  
#                   $source->{uid}[0], $source->{unixuid}[0], 
#                   $source->{employeetype}[0], $source->{somestatusattribute}[0]) if ($iaminteractive && $opt_v);   
# 
 }

 $source = $conn->nextEntry();
}

close(OUT) if $opt_v;

print "done parsing $userN entries, $acceptN accepted, $rejectN rejected.\n" if ($iaminteractive && $opt_v);


#################################################################################
# print the remaining list of VALID users (not "Unknown") to the output file. 

print "will use output file \"$tmpfile\"\n" if ($iaminteractive && $opt_v);

open(OUT,"> $tmpfile") || DIE "ERROR: can not open output file \"$tmpfile\"\n";

print OUT <<"EOF";
###
###  CVS ACCESS CONTROL LIST - AUTOMATICALLY GENERATED VIA CRON ON $thishost
###
###  DO NOT EDIT THIS FILE BY HAND!   
###
###  LOGIN TO $righthost and ONLY EDIT $SOURCE_EXCEPTIONS and CHECK IT IN to RCS!
###
EOF

foreach $user (sort keys %exceptions) {
   if($exceptions{$user} == 1) {
      printf(OUT "$user\n");      # allow access only if tested as OK in the loop above
      WARN("WARNING! user \"$user\" is appears valid in LDAP, but is still on the exceptions list!?\n") if $validuser{$user};
   }
}

foreach $user (sort keys %validuser) {
   if($validuser{$user} == 1) {
      printf(OUT "$user\n");      # allow access only if tested as OK in the loop above
   }
}

close(OUT);

#################################################################################
# Close the LDAP connection.
#
$conn->close if $conn;


#################################################################################
# get new md5sum of ACL file and compare nr. of changes between versions

$new_md5sum = `$md5sum $tmpfile`; 
$new_md5sum =~ s/(\w+).*/$1/; chomp $new_md5sum;

$new_lineN  = `wc -l $tmpfile`;
$new_lineN =~ s/\s*(\d+).*/$1/; chomp $new_lineN;

if ($old_lineN > $new_lineN) {
   # if we're run by cron, we need to bail out in case there are way to few entries in the ACL:
   if ( (! $iaminteractive) && (($old_lineN - $new_lineN) > (2 * $MAX_CHANGES)) ) {
      DIE "PANIC: cron job died!  New version of $CVSADM_VALID_USERS has SUSPICIOUSLY FEW entries!! Please check by hand! (old: $old_lineN , new: $new_lineN , threshhold to die: 2 * $MAX_CHANGES)\n";
   }  
   # in case the number of users decreases too much, we just send a warning:
   if (($old_lineN - $new_lineN) > $MAX_CHANGES) {
      WARN "WARNING: New version of $CVSADM_VALID_USERS has suspiciously few entries!! Please check by hand! (old: $old_lineN , new: $new_lineN , threshhold to warn: $MAX_CHANGES)\n";
   }  
}

if (($old_lineN < $new_lineN) && (($new_lineN - $old_lineN) > $MAX_CHANGES)) {
    WARN "WARNING: new version of $CVSADM_VALID_USERS has suspiciously many entries!! Please check by hand!! (old: $old_lineN , new: $new_lineN , threshhold to warn: $MAX_CHANGES)\n";
}

# Copy $tmpfile over to $SOURCE_VALID_USERS

print "copying $tmpfile over to $SOURCE_VALID_USERS .. \n" if ($iaminteractive && $opt_v);

chdir($SOURCE_DIR);

# now we will copy and check in the new version of the ACL file into RCS
#
$rcsmsg = "pulled from LDAP and $SOURCE_EXCEPTIONS at $date2";

`cp $tmpfile $SOURCE_VALID_USERS`;           # copy the new file to replace the old one.
`rcs -q -l $SOURCE_VALID_USERS`;             # lock the RCS file with the valid-users file
`ci -q -u -m"$rcsmsg" $SOURCE_VALID_USERS`;  # check in the new valid-users file

############################################################
# now copy the valid-users file over to ALL CVS-servers
#
foreach $server (@otherservers) {            # on each CVS-server:
   print "syncing $CVSADM_VALID_USERS to $server\n" if ($iaminteractive && $opt_v);

   $remote_md5sum = `$ssh $server $md5sum $CVSADM_VALID_USERS`; 
   $remote_md5sum =~ s/(\w+).*/$1/;  chomp $remote_md5sum;

   if ($remote_md5sum ne $old_md5sum) {
      WARN "PANIC: old remote md5sum $remote_md5sum of ACL file on $server does not match old local md5sum $old_md5sum of ACL file in RCS on $righthost\nDid someone fiddle around with it by hand??\n";
   } 
   
   if ($remote_md5sum ne $new_md5sum) { 
        # we only copy the new file if something changed, e.g. if the remote location does not contain the same list
        # this also covers the case where the remote location wasn't correctly updated..
   
       `$ssh $server touch $CVSADM_VALID_USERS_LOCK`;   # lock the file with the ACL, so CVS will wait for the updated list
       `$ssh $server mv $CVSADM_VALID_USERS $CVSADM_VALID_USERS.old`;  # keep a copy of the previous file - just in case
       `$scp  $SOURCE_VALID_USERS $server:$CVSADM_VALID_USERS`;    # copy the list
       `$ssh $server rm $CVSADM_VALID_USERS_LOCK`;                 # unlock the list, so CVS will comence operations

       $delta = `$ssh $server diff  $CVSADM_VALID_USERS $CVSADM_VALID_USERS.old`; # get a  diff to see what changed.. 
       
       WARN "NOTE: updated CVS ACL on $server!  Diff follows..\n$delta\n" if $opt_v;
   } else {
       WARN "NOTE: old and new ACL contents are identical. No update necessary!  md5sums new: $new_md5sum / old: $old_md5sum / remote: $remote_md5sum\n" if $opt_v;
   }
}

############################################################
unlink  $tmpfile;
unlink  $SOURCE_VALID_USERS;     # make sure that no version is lying around.. 

unlink  $SELF_LOCK;

warnNexit() if @warnings;

############################################################

