bug-cvs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: disabling branch commit


From: Mark D. Baushke
Subject: Re: disabling branch commit
Date: Mon, 13 Sep 2004 10:16:55 -0700

Hi Peter,

Before I do the final commit, be advised that I have made a few minor
changes to your script to rename it as cvs_acls from cvs_acls2 and to
reference the most recent documentation URL.

Please review this and let me know if it is reasonable to proceed.

        -- Mark

Index: ChangeLog
===================================================================
RCS file: /cvs/ccvs/ChangeLog,v
retrieving revision 1.1048
diff -u -p -r1.1048 ChangeLog
--- ChangeLog   9 Sep 2004 17:30:12 -0000       1.1048
+++ ChangeLog   13 Sep 2004 17:15:16 -0000
@@ -1,3 +1,8 @@
+2004-09-13  Mark D. Baushke  <mdb@cvshome.org>
+
+       * NEWS: Note change to contrib/cvs_acls and addition of
+       contrib/cvs_acls.html
+
 2004-09-09  Conrad T. Pino  <Conrad@Pino.com>
 
        * cvsnt.dep: Regenerated for "cvsnt.dsp" changes made 2004-09-08.
Index: NEWS
===================================================================
RCS file: /cvs/ccvs/NEWS,v
retrieving revision 1.269
diff -u -p -r1.269 NEWS
--- NEWS        7 Sep 2004 19:16:37 -0000       1.269
+++ NEWS        13 Sep 2004 17:15:16 -0000
@@ -65,6 +65,10 @@ NEW FEATURES
   using the new ImportNewFilesToVendorBranchOnly=yes option in
   CVSROOT/config.
 
+* contrib/cvs_acls.in has been revised. Users of the old version will
+  want to upgrade to use the new format. See the documentation in
+  contrib/cvs_acls.html for more information.
+
 BUG FIXES
 
 * Thanks to a report from Chris Bohn <cbohn@rrinc.com>, links from J.C. Hamlin
Index: contrib/ChangeLog
===================================================================
RCS file: /cvs/ccvs/contrib/ChangeLog,v
retrieving revision 1.135
diff -u -p -r1.135 ChangeLog
--- contrib/ChangeLog   7 Sep 2004 05:15:05 -0000       1.135
+++ contrib/ChangeLog   13 Sep 2004 17:15:16 -0000
@@ -1,3 +1,12 @@
+2004-09-13  Mark D. Baushke  <mdb@cvshome.org>
+
+       * cvs_acls.in: New version from
+       "Peter Connolly" <Peter.Connolly@cnet.com>.
+       * cvs_acls.html: New file from
+       "Peter Connolly" <Peter.Connolly@cnet.com>.
+       * Makefile.am (EXTRA_DIST): Add cvs_acls2.html
+       (Close ccvs Issue #170.)
+       
 2004-09-06  Mark D. Baushke  <mdb@cvshome.org>
 
        * log_accum.in (UseNewFmtStrings, new_directory,
Index: contrib/Makefile.am
===================================================================
RCS file: /cvs/ccvs/contrib/Makefile.am,v
retrieving revision 1.15
diff -u -p -r1.15 Makefile.am
--- contrib/Makefile.am 17 Jul 2004 02:41:31 -0000      1.15
+++ contrib/Makefile.am 13 Sep 2004 17:15:16 -0000
@@ -3,6 +3,7 @@
 # Do not use this makefile directly, but only from `../Makefile'.
 # Copyright (C) 1986, 1987, 1988, 1989, 1990, 1991, 1992, 1993, 1994,
 #               1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003
+#               2004
 #               Free Software Foundation, Inc.
 
 # This program is free software; you can redistribute it and/or modify
@@ -55,6 +56,7 @@ EXTRA_DIST = \
        cvs2vendor.sh \
        sandbox_status.sh \
        cvshelp.man \
+       cvs_acls.html \
        debug_check_log.sh \
        descend.sh \
        descend.man \
Index: contrib/Makefile.in
===================================================================
RCS file: /cvs/ccvs/contrib/Makefile.in,v
retrieving revision 1.75
diff -u -p -r1.75 Makefile.in
--- contrib/Makefile.in 17 Jul 2004 02:41:31 -0000      1.75
+++ contrib/Makefile.in 13 Sep 2004 17:15:16 -0000
@@ -1,7 +1,7 @@
 # Makefile.in generated by automake 1.7.9 from Makefile.am.
 # @configure_input@
 
-# Copyright 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003
+# Copyright 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004
 # Free Software Foundation, Inc.
 # This Makefile.in is free software; the Free Software Foundation
 # gives unlimited permission to copy and/or distribute it,
@@ -17,7 +17,8 @@
 # Makefile for GNU CVS contributed sources.
 # Do not use this makefile directly, but only from `../Makefile'.
 # Copyright (C) 1986, 1987, 1988, 1989, 1990, 1991, 1992, 1993, 1994,
-#               1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003
+#               1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003,
+#               2004
 #               Free Software Foundation, Inc.
 
 # This program is free software; you can redistribute it and/or modify
@@ -219,6 +220,7 @@ EXTRA_DIST = \
        $(contrib_MANS) \
        cvs2vendor.sh \
        sandbox_status.sh \
+       cvs_acls.html \
        cvshelp.man \
        debug_check_log.sh \
        descend.sh \
Index: contrib/README
===================================================================
RCS file: /cvs/ccvs/contrib/README,v
retrieving revision 1.18
diff -u -p -r1.18 README
--- contrib/README      21 Nov 2002 19:04:38 -0000      1.18
+++ contrib/README      13 Sep 2004 17:15:16 -0000
@@ -57,9 +57,10 @@ An attempt at a table of Contents for th
                        Contributed by Greg A. Woods <woods@planix.com>.
 
        cvs_acls        A perl script that implements Access Control Lists
-                       by using the "commitinfo" hook provided with the
+       cvs_acls.html   by using the "commitinfo" hook provided with the
                        "cvs commit" command.
                        Contributed by David G. Grubbs <dgg@ksr.com>.
+                       Enhanced by Peter Connolly <Peter.Connolly@cnet.com>.
 
        cvscheck        Identifies files added, changed, or removed in a
        cvscheck.man    checked out CVS tree; also notices unknown files.
Index: contrib/cvs_acls.html
===================================================================
RCS file: contrib/cvs_acls.html
diff -N contrib/cvs_acls.html
--- /dev/null   1 Jan 1970 00:00:00 -0000
+++ contrib/cvs_acls.html       13 Sep 2004 17:15:16 -0000
@@ -0,0 +1,427 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd";>
+<html xmlns="http://www.w3.org/1999/xhtml";><head>
+
+<title>cvs_acls</title><link rev="made" href="mailto:root@localhost";></head>
+
+<body style="background-color: white;">
+
+<p><a name="__index__"></a></p>
+<!-- INDEX BEGIN -->
+
+<ul>
+
+       <li><a href="#name">Name</a></li>
+       <li><a href="#synopsis">Synopsis</a></li>
+       <li><a href="#licensing">Licensing</a></li>
+       <li><a href="#description">Description</a></li>
+       <li><a href="#enhancements">Enhancements</a></li>
+       <ul>
+
+               <li><a href="#fixed_bugs">Fixed Bugs</a></li>
+               <li><a href="#enhancements">Enhancements</a></li>
+               <li><a href="#todos">ToDoS</a></li>
+       </ul>
+
+       <li><a href="#version_information">Version Information</a></li>
+       <li><a href="#installation">Installation</a></li>
+       <li><a href="#format_of_the_cvsacl_file">Format of the cvsacl 
file</a></li>
+       <li><a href="#program_logic">Program Logic</a></li>
+       <ul>
+
+               <li><a href="#pseudocode">Pseudocode</a></li>
+               <li><a href="#sanity_check">Sanity Check</a></li>
+       </ul>
+
+</ul>
+<!-- INDEX END -->
+
+<hr>
+<p>
+</p>
+<h1><a name="name">Name</a></h1>
+<p>cvs_acls - Access Control List for CVS</p>
+<p>
+</p>
+<hr>
+<h1><a name="synopsis">Synopsis</a></h1>
+<p>In 'commitinfo':</p>
+<pre>  repository/path/to/restrict $CVSROOT/CVSROOT/cvs_acls [-d][-u $USER][-f 
&lt;logfile&gt;]</pre>
+<p>where:</p>
+<pre>  -d  turns on debug information
+  -u  passes the client-side userId to the cvs_acls script
+  -f  specifies an alternate filename for the restrict_log file</pre>
+<p>In 'cvsacl':</p>
+<pre>  {allow.*,deny.*} [|user,user,... [|repos,repos,... 
[|branch,branch,...]]]</pre>
+<p>where:</p>
+<pre>  allow|deny - allow: commits are allowed; deny: prohibited
+  user          - userId to be allowed or restricted
+  repos         - file or directory to be allowed or restricted
+  branch        - branch to be allowed or restricted</pre>
+<p>See below for examples.</p>
+<p>
+</p>
+<hr>
+<h1><a name="licensing">Licensing</a></h1>
+<p>cvs_acls - provides access control list functionality for CVS
+</p>
+<pre>
+Copyright (c) 2004 by Peter Connolly &lt;peter.connolly@cnet.com&gt;  
+All rights reserved.</pre>
+<p>This program is free software; you can redistribute it and/or modify  
+it under the terms of the GNU General Public License as published by  
+the Free Software Foundation; either version 2 of the License, or  
+(at your option) any later version.</p>
+<p>This program is distributed in the hope that it will be useful,  
+but WITHOUT ANY WARRANTY; without even the implied warranty of  
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the  
+GNU General Public License for more details.</p>
+<p>You should have received a copy of the GNU General Public License  
+along with this program; if not, write to the Free Software  
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA</p>
+<p>
+</p>
+<hr>
+<h1><a name="description">Description</a></h1>
+<p>This script--cvs_acls--is invoked once for each directory within a 
+``cvs commit''. The set of files being committed for that directory as 
+well as the directory itself, are passed to this script.  This script 
+checks its 'cvsacl' file to see if any of the files being committed 
+are on the 'cvsacl' file's restricted list.  If any of the files are
+restricted, then the cvs_acls script passes back an exit code of 1
+which disallows the commits for that directory.</p>
+<p>Messages are returned to the committer indicating the <a 
href="#item_file"><code>file(s)</code></a> that 
+he/she are not allowed to committ.  Additionally, a site-specific 
+set of messages (e.g., contact information) can be included in these 
+messages.</p>
+<p>When a commit is prohibited, log messages are written to a restrict_log
+file in $CVSROOT/CVSROOT.  This default file can be redirected to 
+another destination.</p>
+<p>The script is triggered from the 'commitinfo' file in $CVSROOT/CVSROOT/.</p>
+<p>
+</p>
+<hr>
+<h1><a name="enhancements">Enhancements</a></h1>
+<p>This section lists the bug fixes and enhancements added to cvs_acls
+that make up the current cvs_acls.</p>
+<p>
+</p>
+<h2><a name="fixed_bugs">Fixed Bugs</a></h2>
+<p>This version attempts to get rid the following bugs from the
+original version of cvs_acls:</p>
+<ul>
+<li><strong><a name="item_files">Multiple entries on an 'cvsacl' line will be 
matched individually, 
+instead of requiring that all commit files *exactly* match all 
+'cvsacl' entries. Commiting a file not in the 'cvsacl' list would
+allow *all* files (including a restricted file) to be 
committed.</a></strong><br>
+</li>
+[IMO, this basically made the original script unuseable for our 
+situation since any arbitrary combination of committed files could 
+avoid matching the 'cvsacl's entries.]
+<p></p>
+<li><strong><a 
name="item_handle_specific_filename_restrictions._cvs_acls_">Handle specific 
filename restrictions. cvs_acls didn't restrict
+individual files specified in 'cvsacl'.</a></strong><br>
+</li>
+<li><strong><a 
name="item_correctly_handle_multiple,_specific_filename_res">Correctly handle 
multiple, specific filename restrictions</a></strong><br>
+</li>
+<li><strong><a 
name="item_prohibit_mix_of_dirs_and_files_on_a_single_'cvsa">Prohibit mix of 
dirs and files on a single 'cvsacl' line
+[To simplify the logic and because this would be normal 
usage.]</a></strong><br>
+</li>
+<li><strong><a 
name="item_correctly_handle_a_mixture_of_branch_restrictions_">Correctly handle 
a mixture of branch restrictions within one work
+directory</a></strong><br>
+</li>
+<li><strong><a name="item_$cvsroot_existence_is_checked_too_late">$CVSROOT 
existence is checked too late</a></strong><br>
+</li>
+<li><strong><a name="item_option">Correctly handle the CVSROOT=:local:/... 
option (useful for 
+interactive testing)</a></strong><br>
+</li>
+<li><strong><a name="item_logic">Replacing shoddy ``$universal_off'' logic 
+(Thanks to Karl-Konig Konigsson for pointing this out.)</a></strong><br>
+</li>
+</ul>
+<p>
+</p>
+<h2><a name="enhancements">Enhancements</a></h2>
+<ul>
+<li><strong><a 
name="item_checks_modules_in_the_'cvsacl'_file_for_valid_">Checks modules in 
the 'cvsacl' file for valid files and directories</a></strong><br>
+</li>
+<li><strong><a 
name="item_accurately_report_restricted_entries_and_their_mat">Accurately 
report restricted entries and their matching patterns</a></strong><br>
+</li>
+<li><strong><a 
name="item_simplified_and_commented_overly_complex_perl_regex">Simplified and 
commented overly complex PERL REGEXPs for readability 
+and maintainability</a></strong><br>
+</li>
+<li><strong><a 
name="item_skip_the_rest_of_processing_if_a_mismatch_on_porti">Skip the rest of 
processing if a mismatch on portion of the 'cvsacl' line</a></strong><br>
+</li>
+<li><strong><a name="item_file">Get rid of opaque ``karma'' messages in favor 
of user-friendly messages
+that describe which user, <code>file(s)</code> and <code>branch(es)</code> 
were disallowed.</a></strong><br>
+</li>
+<li><strong><a name="item_add_optional_'restrict_msg'_file_for_additiona">Add 
optional 'restrict_msg' file for additional, site-specific 
+restriction messages.</a></strong><br>
+</li>
+<li><strong><a name="item_userid">Take a ``-u'' parameter for $USER from 
commit_prep so that the script
+can do restrictions based on the client-side userId rather than the
+server-side userId (usually 'cvs').</a></strong><br>
+</li>
+(See discussion below on ``Admin Setup'' for more on this point.)
+<p></p>
+<li><strong><a name="item_added_a_lot_more_debug_trace">Added a lot more debug 
trace</a></strong><br>
+</li>
+<li><strong><a 
name="item_tested_these_restrictions_with_concurrent_use_of_p">Tested these 
restrictions with concurrent use of pserver and SSH
+access to model our transition from pserver to ext access.</a></strong><br>
+</li>
+<li><strong><a 
name="item_added_logging_of_restricted_commit_attempts._res">Added logging of 
restricted commit attempts.
+Restricted commits can be sent to a default file:
+$CVSROOT/CVSROOT/restrictlog or to one passed to the script
+via the -f command parameter.</a></strong><br>
+</li>
+</ul>
+<p>
+</p>
+<h2><a name="todos">ToDoS</a></h2>
+<ul>
+<li><strong><a 
name="item_need_to_deal_with_pserver/ssh_transition_with_co">Need to deal with 
pserver/SSH transition with conflicting umasks?</a></strong><br>
+</li>
+<li><strong><a name="item_use_a_cpan_module_to_handle_command_parameters.">Use 
a CPAN module to handle command parameters.</a></strong><br>
+</li>
+<li><strong><a name="item_use_a_cpan_module_to_clone_data_structures.">Use a 
CPAN module to clone data structures.</a></strong><br>
+</li>
+</ul>
+<p>
+</p>
+<hr>
+<h1><a name="version_information">Version Information</a></h1>
+<p>This is not offered as a fix to the original 'cvs_acls' script since it 
+differs substantially in goals and methods from the original and there 
+are probably a significant number of people out there that still require 
+the original version's functionality.</p>
+<p>The 'cvsacl' file flags of 'allow' and 'deny' were intentionally 
+changed to 'allow' and 'deny' because there are enough differences 
+between the original script's behavior and this one's that we wanted to
+make sure that users will rethink their 'cvsacl' file formats before
+plugging in this newer script.</p>
+<p>Please note that there has been very limited cross-platform testing of 
+this script!!! (We did not have the time or resources to do exhaustive
+cross-platform testing.)</p>
+<p>It was developed and tested under Red Hat Linux 9.0 using PERL 5.8.0.
+Additionally, it was built and tested under Red Hat Linux 7.3 using 
+PERL 5.6.1.</p>
+<p>$Id$</p>
+<p>This Version is based on the 1.11.13 version of cvs_acls
+<a href="mailto:peter.connolly@cnet.com";>peter.connolly@cnet.com</a> (Peter 
Connolly)</p>
+<pre>  Access control lists for CVS.  dgg@ksr.com (David G. Grubbs)
+  Branch specific controls added by voisine@bytemobile.com (Aaron 
Voisine)</pre>
+<p>
+</p>
+<hr>
+<h1><a name="installation">Installation</a></h1>
+<p>To use this program, do the following four things:</p>
+<p>0. Install PERL, version 5.6.1 or 5.8.0.</p>
+<p>1. Admin Setup:</p>
+<pre>   There are two choices here.</pre>
+<pre>   a) The first option is to use the $ENV{"USER"}, server-side userId
+      (from the third column of your pserver 'passwd' file) as the basis for 
+      your restrictions.  In this case, you will (at a minimum) want to set
+      up a new "cvsadmin" userId and group on the pserver machine.  
+      CVS administrators will then set up their 'passwd' file entries to
+      run either as "cvs" (for regular users) or as "cvsadmin" (for power 
+      users).  Correspondingly, your 'cvsacl' file will only list 'cvs'
+      and 'cvsadmin' as the userIds in the second column.</pre>
+<pre>      Commentary: A potential weakness of this is that the xinetd 
+      cvspserver process will need to run as 'root' in order to switch 
+      between the 'cvs' and the 'cvsadmin' userIds.  Some sysadmins don't
+      like situations like this and may want to chroot the process.
+      Talk to them about this point...</pre>
+<pre>   b) The second option is to use the client-side userId as the basis for
+      your restrictions.  In this case, all the xinetd cvspserver processes 
+      can run as userId 'cvs' and no 'root' userId is required.  If you have
+      a 'passwd' file that lists 'cvs' as the effective run-time userId for
+      all your users, then no changes to this file are needed.  Your 'cvsacl'
+      file will use the individual, client-side userIds in its 2nd 
column.</pre>
+<pre>      As long as the userIds in pserver's 'passwd' file match those 
userIds 
+      that your Linux server know about, this approach is ideal if you are 
+      planning to move from pserver to SSH access at some later point in time.
+      Just by switching the CVSROOT var from 
CVSROOT=:pserver:&lt;userId&gt;... to 
+      CVSROOT=:ext:&lt;userId&gt;..., users can switch over to SSH access 
without
+      any other administrative changes.  When all users have switched over to
+      SSH, the inherently insecure xinetd cvspserver process can be disabled.
+      [<a 
href="https://www.cvshome.org/docs/manual/cvs-1.11.17/cvs_2.html#SEC32";>https://www.cvshome.org/docs/manual/cvs-1.11.17/cvs_2.html#SEC32</a>]</pre>
+<pre>      :TODO: The only potential glitch with the SSH approach is the 
possibility 
+      that each user can have differing umasks that might interfere with one 
+      another, especially during a transition from pserver to SSH.  As noted
+      in the ToDo section, this needs a good strategy and set of tests for 
that 
+      yet...</pre>
+<p>2. Put two lines, as the *only* non-comment lines, in your commitinfo 
file:</p>
+<pre>   ALL $CVSROOT/CVSROOT/commit_prep 
+   ALL $CVSROOT/CVSROOT/cvs_acls [-d][-u $USER ][-f &lt;logfilename&gt;]</pre>
+<pre>   where "-d" turns on debug trace
+         "-u $USER" passes the client-side userId to cvs_acls 
+         "-f &lt;logfilename"&gt; overrides the default filename used to log
+                            restricted commit attempts.</pre>
+<pre>   (These are handled in the processArgs() subroutine.)</pre>
+<p>If you are using client-side userIds to restrict access to your 
+repository, make sure that they are in this order since the commit_prep 
+script is required in order to pass the $USER parameter.</p>
+<p>A final note about the repository matching pattern.  The example above
+uses ``ALL'' but note that this means that the cvs_acls script will run
+for each and every commit in your repository.  Obviously, in a large
+repository this adds up to a lot of overhead that may not be necesary. 
+A better strategy is to use a repository pattern that is more specific 
+to the areas that you wish to secure.</p>
+<p>3. Install this file as $CVSROOT/CVSROOT/cvs_acls and make it 
executable.</p>
+<p>4. Create a file named CVSROOT/cvsacl and optionally add it to
+   CVSROOT/checkoutlist and check it in.  See the CVS manual's
+   administrative files section about checkoutlist.  Typically:</p>
+<pre>   $ cvs checkout CVSROOT
+   $ cd CVSROOT
+   [ create the cvsacl file, include 'commitinfo' line ]
+   [ add cvsacl to checkoutlist ]
+   $ cvs add cvsacl
+   $ cvs commit -m 'Added cvsacl for use with cvs_acls.' cvsacl 
checkoutlist</pre>
+<p>Note: The format of the 'cvsacl' file is described in detail immediately 
+below but here is an important set up point:</p>
+<pre>   Make sure to include a line like the following:</pre>
+<pre>     deny||CVSROOT/commitinfo CVSROOT/cvsacl
+     allow|cvsadmin|CVSROOT/commitinfo CVSROOT/cvsacl</pre>
+<pre>   that restricts access to commitinfo and cvsacl since this would be one 
of
+   the easiest "end runs" around this ACL approach. ('commitinfo' has the 
+   line that executes the cvs_acls script and, of course, all the 
+   restrictions are in 'cvsacl'.)</pre>
+<p>5. (Optional) Create a 'restrict_msg' file in the $CVSROOT/CVSROOT 
directory.
+   Whenever there is a restricted file or dir message, cvs_acls will look 
+   for this file and, if it exists, print its contents as part of the 
+   commit-denial message.  This gives you a chance to print any site-specific
+   information (e.g., who to call, what procedures to look up,...) whenever
+   a commit is denied.</p>
+<p>
+</p>
+<hr>
+<h1><a name="format_of_the_cvsacl_file">Format of the cvsacl file</a></h1>
+<p>The 'cvsacl' file determines whether you may commit files.  It contains 
lines
+read from top to bottom, keeping track of whether a given user, repository
+and branch combination is ``allowed'' or ``denied.''  The script will assume 
+``allowed'' on all repository paths until 'allow' and 'deny' rules change 
+that default.</p>
+<p>The normal pattern is to specify an 'deny' rule to turn off
+access to ALL users, then follow it with a matching 'allow' rule that will 
+turn on access for a select set of users.  In the case of multiple rules for
+the same user, repository and branch, the last one takes precedence.</p>
+<p>Blank lines and lines with only comments are ignored.  Any other lines not 
+beginning with ``allow'' or ``deny'' are logged to the restrict_log file.</p>
+<p>Lines beginning with ``allow'' or ``deny'' are assumed to be '|'-separated
+triples: (All spaces and tabs are ignored in a line.)</p>
+<pre>  {allow.*,deny.*} [|user,user,... [|repos,repos,... 
[|branch,branch,...]]]</pre>
+<pre>   1. String starting with "allow" or "deny".
+   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 or restricts access to all files and
+      directories below it. One line can have either directories or filenames
+      but not both.
+   4. Optional, comma-separated list of branch tags.
+      If not specified, all branches are assumed. Use HEAD to reference the
+      main branch.</pre>
+<p>Example:  (Note: No in-line comments.)</p>
+<pre>   # ----- Make whole repository unavailable.
+   deny</pre>
+<pre>   # ----- Except for user "dgg".
+   allow|dgg</pre>
+<pre>   # ----- Except when "fred" or "john" commit to the 
+   #       module whose repository is "bin/ls"
+   allow|fred, john|bin/ls</pre>
+<pre>   # ----- Except when "ed" commits to the "stable" 
+   #       branch of the "bin/ls" repository
+   allow|ed|/bin/ls|stable</pre>
+<p>
+</p>
+<hr>
+<h1><a name="program_logic">Program Logic</a></h1>
+<p>CVS passes to @ARGV an absolute directory pathname (the repository
+appended to your $CVSROOT variable), followed by a list of filenames
+within that directory that are to be committed.</p>
+<p>The script walks through the 'cvsacl' file looking for matches on 
+the username, repository and branch.</p>
+<p>A username match is simply the user's name appearing in the second
+column of the cvsacl line in a space-or-comma separate list. If
+blank, then any user will match.</p>
+<p>A repository match:</p>
+<ul>
+<li><strong><a 
name="item_each_entry_in_the_modules_section_of_the_current_%">Each entry in 
the modules section of the current 'cvsacl' line is 
+examined to see if it is a dir or a file. The line must have 
+either files or dirs, but not both. (To simplify the logic.)</a></strong><br>
+</li>
+<li><strong><a name="item_if_neither,_then_assume_the_'cvsacl'_file_wa">If 
neither, then assume the 'cvsacl' file was set up in error and
+skip that 'allow' line.</a></strong><br>
+</li>
+<li><strong><a name="item_if_a_dir,_then_each_dir_pattern_is_matched_separ">If 
a dir, then each dir pattern is matched separately against the 
+beginning of each of the committed files in @ARGV.</a></strong><br>
+</li>
+<li><strong><a name="item_if_a_file,_then_each_file_pattern_is_matched_exa">If 
a file, then each file pattern is matched exactly against each
+of the files to be committed in @ARGV.</a></strong><br>
+</li>
+<li><strong><a 
name="item_repository_and_branch_must_both_match_together._">Repository and 
branch must BOTH match together. This is to cover
+the use case where a user has multiple branches checked out in
+a single work directory. Commit files can be from different
+branches.</a></strong><br>
+</li>
+A branch match is either:
+<ul>
+<li><strong><a 
name="item_when_no_branches_are_listed_in_the_fourth_column%2">When no branches 
are listed in the fourth column. (``Match any.'')</a></strong><br>
+</li>
+<li><strong><a 
name="item_all_elements_from_the_fourth_column_are_matched_ag">All elements 
from the fourth column are matched against each of 
+the tag names for $ARGV[1..$#ARGV] found in the %branches 
file.</a></strong><br>
+</li>
+</ul>
+<li><strong><a 
name="item_'allow'_match_remove_that_match_from_the_tally">'allow' match remove 
that match from the tally map.</a></strong><br>
+</li>
+<li><strong><a name="item_restricted">Restricted ('deny') matches are saved in 
the %repository_matches 
+table.</a></strong><br>
+</li>
+<li><strong><a name="item_if_there_is_a_match_on_user,_repository_and_bran">If 
there is a match on user, repository and branch:</a></strong><br>
+</li>
+<pre>  If repository, branch and user match
+    if 'deny'
+      add %repository_matches entries to %restricted_entries
+    else if 'allow'
+      remove %repository_matches entries from %restricted_entries</pre>
+<li><strong><a name="item_at_the_end_of_all_the_'cvsacl'_line_checks,_">At the 
end of all the 'cvsacl' line checks, check to see if there
+are any entries in the %restricted_entries.  If so, then deny the
+commit.</a></strong><br>
+</li>
+</ul>
+<p>
+</p>
+<h2><a name="pseudocode">Pseudocode</a></h2>
+<pre>     read CVS/Entries file and create branch{file}-&gt;{branch} hash table
+   + for each 'allow' and 'deny' line in the 'cvsacl' file:
+   |   user match?   
+   |     - Yes: set $user_match       = 1;
+   |   repository and branch match?
+   |     - Yes: add to %repository_matches;
+   |   did user, repository match?
+   |     - Yes: if 'deny' then 
+   |                add %repository_matches -&gt; %restricted_entries
+   |            if 'allow'   then 
+   |                remove %repository_matches &lt;- %restricted_entries
+   + end for loop
+     any saved restrictions?
+       no:  exit, 
+            set exit code allowing commits and exit
+       yes: report restrictions, 
+            set exit code prohibiting commits and exit</pre>
+<p>
+</p>
+<h2><a name="sanity_check">Sanity Check</a></h2>
+<pre>  1) file allow trumps a dir deny
+     deny||java/lib
+     allow||java/lib/README
+  2) dir allow can undo a file deny
+     deny||java/lib/README
+     allow||java/lib
+  3) file deny trumps a dir allow
+     allow||java/lib
+     deny||java/lib/README
+  4) dir deny trumps a file allow
+     allow||java/lib/README
+     deny||java/lib
+  ... so last match always takes precedence</pre>
+
+</body></html>
Index: contrib/cvs_acls.in
===================================================================
RCS file: /cvs/ccvs/contrib/cvs_acls.in,v
retrieving revision 1.4
diff -u -p -r1.4 cvs_acls.in
--- contrib/cvs_acls.in 16 Dec 2002 15:30:43 -0000      1.4
+++ contrib/cvs_acls.in 13 Sep 2004 17:15:16 -0000
@@ -1,193 +1,933 @@
 #! @PERL@
 # -*-Perl-*-
-#
-# Access control lists for CVS.  dgg@ksr.com (David G. Grubbs)
-# Branch specific controls added by voisine@bytemobile.com (Aaron Voisine)
-#
-# 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/avail and optionally add it to
-#    CVSROOT/checkoutlist and check it in.  See the CVS manual's
-#    administrative files section about checkoutlist.  Typically:
-#
-#    $ cvs checkout CVSROOT
-#    $ cd CVSROOT
-#    [ create the avail file ]
-#    [ add avail to checkoutlist ]
-#    $ cvs add avail
-#    $ cvs commit -m 'Added avail for use with cvs_acls.' avail checkoutlist
-#
-# ==== 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,... [|branch,branch,...]]]
-#
-#    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.
-#    4. Optional, comma-separated list of branch tags.
-#      If not specified, all branches are assumed. Use HEAD to reference the
-#      main branch.
-#
-# 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"
-#      avail|ed|/bin/ls|stable ;; Except when "ed" commits to the "stable"
-#                              ;; branch of the "bin/ls" repository 
-#
-# 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 the
-#      username, repository and branch.
-#
-#      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.
-#
-#      A branch match is either:
-#              - When no branches are listed in the fourth column.
-#              - One element from the fourth column matches each of the tag
-#                names for $ARGV[1..$#ARGV] found in the CVS/Entries file.
-#              - HEAD specified in the fourth column will match if there
-#                is no tag listed in the CVS/Entries file.
-#
 
-$debug = 0;
+=head1 Name
+
+cvs_acls2 - Access Control List for CVS, Version 2
+
+=head1 Synopsis
+
+In 'commitinfo':
+
+  repository/path/to/restrict $CVSROOT/CVSROOT/cvs_acls2 [-d][-u $USER][-f 
<logfile>]
+
+where:
+
+  -d  turns on debug information
+  -u  passes the client-side userId to the cvs_acls2 script
+  -f  specifies an alternate filename for the restrict_log file
+
+In 'cvsacl':
+
+  {allow.*,deny.*} [|user,user,... [|repos,repos,... [|branch,branch,...]]]
+
+where:
+
+  allow|deny - allow: commits are allowed; deny: prohibited
+  user          - userId to be allowed or restricted
+  repos         - file or directory to be allowed or restricted
+  branch        - branch to be allowed or restricted
+
+See below for examples.
+
+=head1 Licensing
+
+cvs_acls2 - provides access control list functionality for CVS
+  
+Copyright (c) 2004 by Peter Connolly <peter.connolly@cnet.com>  
+All rights reserved.
+
+This program is free software; you can redistribute it and/or modify  
+it under the terms of the GNU General Public License as published by  
+the Free Software Foundation; either version 2 of the License, or  
+(at your option) any later version. 
+
+This program is distributed in the hope that it will be useful,  
+but WITHOUT ANY WARRANTY; without even the implied warranty of  
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the  
+GNU General Public License for more details.  
+
+You should have received a copy of the GNU General Public License  
+along with this program; if not, write to the Free Software  
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+=head1 Description
+
+This script--cvs_acls2--is invoked once for each directory within a 
+"cvs commit". The set of files being committed for that directory as 
+well as the directory itself, are passed to this script.  This script 
+checks its 'cvsacl' file to see if any of the files being committed 
+are on the 'cvsacl' file's restricted list.  If any of the files are
+restricted, then the cvs_acls2 script passes back an exit code of 1
+which disallows the commits for that directory.  
+
+Messages are returned to the committer indicating the file(s) that 
+he/she are not allowed to committ.  Additionally, a site-specific 
+set of messages (e.g., contact information) can be included in these 
+messages.
+
+When a commit is prohibited, log messages are written to a restrict_log
+file in $CVSROOT/CVSROOT.  This default file can be redirected to 
+another destination.
+
+The script is triggered from the 'commitinfo' file in $CVSROOT/CVSROOT/.
+
+=head1 Enhancements
+
+This section lists the bug fixes and enhancements added to cvs_acls
+that make up the current cvs_acls2.
+
+=head2 Fixed Bugs
+
+This version attempts to get rid the following bugs from the
+original version of cvs_acls:
+
+=over 2
+
+=item *
+Multiple entries on an 'cvsacl' line will be matched individually, 
+instead of requiring that all commit files *exactly* match all 
+'cvsacl' entries. Commiting a file not in the 'cvsacl' list would
+allow *all* files (including a restricted file) to be committed.
+
+[IMO, this basically made the original script unuseable for our 
+situation since any arbitrary combination of committed files could 
+avoid matching the 'cvsacl's entries.]
+
+=item *
+Handle specific filename restrictions. cvs_acls didn't restrict
+individual files specified in 'cvsacl'.
+
+=item *
+Correctly handle multiple, specific filename restrictions
+
+=item *
+Prohibit mix of dirs and files on a single 'cvsacl' line
+[To simplify the logic and because this would be normal usage.]
+
+=item *
+Correctly handle a mixture of branch restrictions within one work
+directory
+
+=item *
+$CVSROOT existence is checked too late
+
+=item *
+Correctly handle the CVSROOT=:local:/... option (useful for 
+interactive testing)
+
+=item *
+Replacing shoddy "$universal_off" logic 
+(Thanks to Karl-Konig Konigsson for pointing this out.)
+
+=back
+
+=head2 Enhancements
+
+=over 2
+
+=item *
+Checks modules in the 'cvsacl' file for valid files and directories
+
+=item *
+Accurately report restricted entries and their matching patterns
+
+=item *
+Simplified and commented overly complex PERL REGEXPs for readability 
+and maintainability
+
+=item *
+Skip the rest of processing if a mismatch on portion of the 'cvsacl' line
+
+=item *
+Get rid of opaque "karma" messages in favor of user-friendly messages
+that describe which user, file(s) and branch(es) were disallowed.
+
+=item *
+Add optional 'restrict_msg' file for additional, site-specific 
+restriction messages.
+
+=item *
+Take a "-u" parameter for $USER from commit_prep so that the script
+can do restrictions based on the client-side userId rather than the
+server-side userId (usually 'cvs').
+
+(See discussion below on "Admin Setup" for more on this point.)
+
+=item *
+Added a lot more debug trace 
+
+=item *
+Tested these restrictions with concurrent use of pserver and SSH
+access to model our transition from pserver to ext access.
+
+=item *
+Added logging of restricted commit attempts.
+Restricted commits can be sent to a default file:
+$CVSROOT/CVSROOT/restrictlog or to one passed to the script
+via the -f command parameter.
+
+=back
+
+=head2 ToDoS 
+
+=over 2
+
+=item *
+Need to deal with pserver/SSH transition with conflicting umasks?
+
+=item *
+Use a CPAN module to handle command parameters.
+
+=item *
+Use a CPAN module to clone data structures.
+
+=back
+
+=head1 Version Information
+
+This is not offered as a fix to the original 'cvs_acls' script since it 
+differs substantially in goals and methods from the original and there 
+are probably a significant number of people out there that still require 
+the original version's functionality.
+
+The 'cvsacl' file flags of 'allow' and 'deny' were intentionally 
+changed to 'allow' and 'deny' because there are enough differences 
+between the original script's behavior and this one's that we wanted to
+make sure that users will rethink their 'cvsacl' file formats before
+plugging in this newer script.
+
+Please note that there has been very limited cross-platform testing of 
+this script!!! (We did not have the time or resources to do exhaustive
+cross-platform testing.)
+
+It was developed and tested under Red Hat Linux 9.0 using PERL 5.8.0.
+Additionally, it was built and tested under Red Hat Linux 7.3 using 
+PERL 5.6.1.
+
+$Id: cvs_acls2,v 1.31 2004/03/17 06:01:13 peterc Exp $
+
+Version 2 is based on the 1.11.13 version of cvs_acls
+peter.connolly@cnet.com (Peter Connolly) 
+
+  Access control lists for CVS.  dgg@ksr.com (David G. Grubbs)
+  Branch specific controls added by voisine@bytemobile.com (Aaron Voisine)
+
+=head1 Installation
+
+To use this program, do the following four things:
+
+0. Install PERL, version 5.6.1 or 5.8.0.
+
+1. Admin Setup:
+
+   There are two choices here. 
+
+   a) The first option is to use the $ENV{"USER"}, server-side userId
+      (from the third column of your pserver 'passwd' file) as the basis for 
+      your restrictions.  In this case, you will (at a minimum) want to set
+      up a new "cvsadmin" userId and group on the pserver machine.  
+      CVS administrators will then set up their 'passwd' file entries to
+      run either as "cvs" (for regular users) or as "cvsadmin" (for power 
+      users).  Correspondingly, your 'cvsacl' file will only list 'cvs'
+      and 'cvsadmin' as the userIds in the second column.
+
+      Commentary: A potential weakness of this is that the xinetd 
+      cvspserver process will need to run as 'root' in order to switch 
+      between the 'cvs' and the 'cvsadmin' userIds.  Some sysadmins don't
+      like situations like this and may want to chroot the process.
+      Talk to them about this point...
+
+   b) The second option is to use the client-side userId as the basis for
+      your restrictions.  In this case, all the xinetd cvspserver processes 
+      can run as userId 'cvs' and no 'root' userId is required.  If you have
+      a 'passwd' file that lists 'cvs' as the effective run-time userId for
+      all your users, then no changes to this file are needed.  Your 'cvsacl'
+      file will use the individual, client-side userIds in its 2nd column.
+
+      As long as the userIds in pserver's 'passwd' file match those userIds 
+      that your Linux server know about, this approach is ideal if you are 
+      planning to move from pserver to SSH access at some later point in time.
+      Just by switching the CVSROOT var from CVSROOT=:pserver:<userId>... to 
+      CVSROOT=:ext:<userId>..., users can switch over to SSH access without
+      any other administrative changes.  When all users have switched over to
+      SSH, the inherently insecure xinetd cvspserver process can be disabled.
+      [https://www.cvshome.org/docs/manual/cvs-1.11.17/cvs_2.html#SEC32]
+
+      :TODO: The only potential glitch with the SSH approach is the 
possibility 
+      that each user can have differing umasks that might interfere with one 
+      another, especially during a transition from pserver to SSH.  As noted
+      in the ToDo section, this needs a good strategy and set of tests for 
that 
+      yet...
+
+2. Put two lines, as the *only* non-comment lines, in your commitinfo file:
+
+   ALL $CVSROOT/CVSROOT/commit_prep 
+   ALL $CVSROOT/CVSROOT/cvs_acls2 [-d][-u $USER ][-f <logfilename>]
+
+   where "-d" turns on debug trace
+         "-u $USER" passes the client-side userId to cvs_acls2 
+         "-f <logfilename"> overrides the default filename used to log
+                            restricted commit attempts.
+
+   (These are handled in the processArgs() subroutine.)
+
+If you are using client-side userIds to restrict access to your 
+repository, make sure that they are in this order since the commit_prep 
+script is required in order to pass the $USER parameter.
+
+A final note about the repository matching pattern.  The example above
+uses "ALL" but note that this means that the cvs_acls2 script will run
+for each and every commit in your repository.  Obviously, in a large
+repository this adds up to a lot of overhead that may not be necesary. 
+A better strategy is to use a repository pattern that is more specific 
+to the areas that you wish to secure.
+
+3. Install this file as $CVSROOT/CVSROOT/cvs_acls2 and make it executable.
+
+4. Create a file named CVSROOT/cvsacl and optionally add it to
+   CVSROOT/checkoutlist and check it in.  See the CVS manual's
+   administrative files section about checkoutlist.  Typically:
+
+   $ cvs checkout CVSROOT
+   $ cd CVSROOT
+   [ create the cvsacl file, include 'commitinfo' line ]
+   [ add cvsacl to checkoutlist ]
+   $ cvs add cvsacl
+   $ cvs commit -m 'Added cvsacl for use with cvs_acls2.' cvsacl checkoutlist
+
+Note: The format of the 'cvsacl' file is described in detail immediately 
+below but here is an important set up point:
+
+   Make sure to include a line like the following:
+
+     deny||CVSROOT/commitinfo CVSROOT/cvsacl
+     allow|cvsadmin|CVSROOT/commitinfo CVSROOT/cvsacl
+
+   that restricts access to commitinfo and cvsacl since this would be one of
+   the easiest "end runs" around this ACL approach. ('commitinfo' has the 
+   line that executes the cvs_acls2 script and, of course, all the 
+   restrictions are in 'cvsacl'.)
+
+5. (Optional) Create a 'restrict_msg' file in the $CVSROOT/CVSROOT directory.
+   Whenever there is a restricted file or dir message, cvs_acls2 will look 
+   for this file and, if it exists, print its contents as part of the 
+   commit-denial message.  This gives you a chance to print any site-specific
+   information (e.g., who to call, what procedures to look up,...) whenever
+   a commit is denied.
+
+=head1 Format of the cvsacl file
+
+The 'cvsacl' file determines whether you may commit files.  It contains lines
+read from top to bottom, keeping track of whether a given user, repository
+and branch combination is "allowed" or "denied."  The script will assume 
+"allowed" on all repository paths until 'allow' and 'deny' rules change 
+that default.  
+
+The normal pattern is to specify an 'deny' rule to turn off
+access to ALL users, then follow it with a matching 'allow' rule that will 
+turn on access for a select set of users.  In the case of multiple rules for
+the same user, repository and branch, the last one takes precedence.
+
+Blank lines and lines with only comments are ignored.  Any other lines not 
+beginning with "allow" or "deny" are logged to the restrict_log file.
+
+Lines beginning with "allow" or "deny" are assumed to be '|'-separated
+triples: (All spaces and tabs are ignored in a line.)
+
+  {allow.*,deny.*} [|user,user,... [|repos,repos,... [|branch,branch,...]]]
+
+   1. String starting with "allow" or "deny".
+   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 or restricts access to all files and
+      directories below it. One line can have either directories or filenames
+      but not both.
+   4. Optional, comma-separated list of branch tags.
+      If not specified, all branches are assumed. Use HEAD to reference the
+      main branch.
+
+Example:  (Note: No in-line comments.)
+
+   # ----- Make whole repository unavailable.
+   deny
+
+   # ----- Except for user "dgg".
+   allow|dgg
+
+   # ----- Except when "fred" or "john" commit to the 
+   #       module whose repository is "bin/ls"
+   allow|fred, john|bin/ls
+
+   # ----- Except when "ed" commits to the "stable" 
+   #       branch of the "bin/ls" repository
+   allow|ed|/bin/ls|stable
+
+=head1 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 that are to be committed.
+
+The script walks through the 'cvsacl' file looking for matches on 
+the username, repository and branch.
+
+A username match is simply the user's name appearing in the second
+column of the cvsacl line in a space-or-comma separate list. If
+blank, then any user will match.
+
+A repository match:
+
+=over 2
+
+=item *
+Each entry in the modules section of the current 'cvsacl' line is 
+examined to see if it is a dir or a file. The line must have 
+either files or dirs, but not both. (To simplify the logic.)
+
+=item *
+If neither, then assume the 'cvsacl' file was set up in error and
+skip that 'allow' line.
+
+=item *
+If a dir, then each dir pattern is matched separately against the 
+beginning of each of the committed files in @ARGV. 
+
+=item *
+If a file, then each file pattern is matched exactly against each
+of the files to be committed in @ARGV.
+
+=item *
+Repository and branch must BOTH match together. This is to cover
+the use case where a user has multiple branches checked out in
+a single work directory. Commit files can be from different
+branches.
+
+A branch match is either:
+
+=over 4
+
+=item *
+When no branches are listed in the fourth column. ("Match any.")
+
+=item *
+All elements from the fourth column are matched against each of 
+the tag names for $ARGV[1..$#ARGV] found in the %branches file.
+
+=back
+
+=item *
+'allow' match remove that match from the tally map.
+
+=item *
+Restricted ('deny') matches are saved in the %repository_matches 
+table.
+
+=item *
+If there is a match on user, repository and branch:
+
+  If repository, branch and user match
+    if 'deny'
+      add %repository_matches entries to %restricted_entries
+    else if 'allow'
+      remove %repository_matches entries from %restricted_entries
+
+=item *
+At the end of all the 'cvsacl' line checks, check to see if there
+are any entries in the %restricted_entries.  If so, then deny the
+commit.
+
+=back
+
+=head2 Pseudocode
+
+     read CVS/Entries file and create branch{file}->{branch} hash table
+   + for each 'allow' and 'deny' line in the 'cvsacl' file:
+   |   user match?   
+   |     - Yes: set $user_match       = 1;
+   |   repository and branch match?
+   |     - Yes: add to %repository_matches;
+   |   did user, repository match?
+   |     - Yes: if 'deny' then 
+   |                add %repository_matches -> %restricted_entries
+   |            if 'allow'   then 
+   |                remove %repository_matches <- %restricted_entries
+   + end for loop
+     any saved restrictions?
+       no:  exit, 
+            set exit code allowing commits and exit
+       yes: report restrictions, 
+            set exit code prohibiting commits and exit
+
+=head2 Sanity Check
+
+  1) file allow trumps a dir deny
+     deny||java/lib
+     allow||java/lib/README
+  2) dir allow can undo a file deny
+     deny||java/lib/README
+     allow||java/lib
+  3) file deny trumps a dir allow
+     allow||java/lib
+     deny||java/lib/README
+  4) dir deny trumps a file allow
+     allow||java/lib/README
+     deny||java/lib
+  ... so last match always takes precedence
+
+=cut
+
+$debug = 0;                     # Set to 1 for debug messages
+
+%repository_matches = ();       # hash of match file and pattern from 'cvsacl'
+                                # repository_matches --> [branch, 
matching-pattern]
+                                # (Used during module/branch matching loop)
+
+%restricted_entries = ();       # hash table of restricted commit files (from 
@ARGV)
+                                # restricted_entries --> branch
+                                # (If user/module/branch all match on an 'deny'
+                                #  line, then entries added to this map.)
+
+%branch;                        # hash table of key: commit file; value: branch
+                                # Built from ".../CVS/Entries" file of 
directory 
+                                # currently being examined
+
+# ---------------------------------------------------------------- get CVSROOT
 $cvsroot = $ENV{'CVSROOT'};
-$availfile = $cvsroot . "/CVSROOT/avail";
-$entries = "CVS/Entries";
-$myname = $ENV{"USER"} if !($myname = $ENV{"LOGNAME"});
+die "Must set CVSROOT\n" if !$cvsroot;
+if ($cvsroot =~ /:([\/\w]*)$/) { # Filter ":pserver:", ":local:"-type prefixes
+    $cvsroot = $1; 
+}
 
+# ------------------------------------------------------------- set file paths
+$entries = "CVS/Entries";                                # client-side file???
+$cvsaclfile = $cvsroot . "/CVSROOT/cvsacl";
+$restrictfile = $cvsroot . "/CVSROOT/restrict_msg";
+$restrictlog = $cvsroot . "/CVSROOT/restrict_log";
+
+# --------------------------------------------------------------- process args
+$user_name = processArgs(\@ARGV);
+
+print("$$ \@ARGV after processArgs is: @ARGV.\n") if $debug;
+print("$$ ========== Begin $PROGRAM_NAME for \"$ARGV[0]\" repository. 
========== \n") if $debug;
+
+# --------------------------------------------------------------- filter @ARGV
 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
+exit 255 if $die;                       # process any variable=value switches
 
-die "Must set CVSROOT\n" if !$cvsroot;
-($repos = shift) =~ s:^$cvsroot/::;
-grep($_ = $repos . '/' . $_, @ARGV);
+print("$$ \@ARGV after shift processing contains:",join("\, ",@ARGV),".\n") if 
$debug;
+
+# ---------------------------------------------------------------- get cvsroot
+($repository = shift) =~ s:^$cvsroot/::;
+grep($_ = $repository . '/' . $_, @ARGV);
 
-print "$$ Repos: $repos\n","$$ ==== ",join("\n$$ ==== ",@ARGV),"\n" if $debug;
+print("$$ \$cvsroot is: $cvsroot.\n") if $debug;
+print "$$ Repos: $repository\n","$$ ==== ",join("\n$$ ==== ",@ARGV),"\n" if 
$debug;
 
-$exit_val = 0;                         # Good Exit value
+$exit_val = 0;                          # presume good exit value for commit
 
-$universal_off = 0;
+# ----------------------------------------------------------------------------
+# ---------------------------------- create hash table $branch{file -> branch}
+# ----------------------------------------------------------------------------
 
-my %branch;
-my $f;
+# Here's a typical Entries file:
+#
+#   /checkoutlist/1.4/Wed Feb  4 23:51:23 2004//
+#   /cvsacl/1.3/Tue Feb 24 23:05:43 2004//
+#   ...
+#   /verifymsg/1.1/Fri Mar 16 19:56:24 2001//
+#   D/backup////
+#   D/temp////
 
 open(ENTRIES, $entries) || die("Cannot open $entries.\n");
+print("$$ File / Branch\n") if $debug;
+my $i = 0;
 while(<ENTRIES>) {
     chop;
-    next if /^\s*$/;
-    if(m|^[^/]*/([^/]*)/(?:[^/]*/)*[^/]?([^/]*)$|) {
-       $branch{$repos . '/' . $1} = ($2) ? $2 : "HEAD"; 
-       print "$$ $1/$2\n" if $debug;
+    next if /^\s*$/;                    # Skip blank lines
+    $i = $i + 1;
+    if (m|
+          /                             # 1st slash
+          ([\w.]*)                      # file name -> $1
+          /                             # 2nd slash
+          .*                            # revision number
+          /                             # 3rd slash
+          .*                            # date and time
+          /                             # 4th slash
+          .*                            # keyword
+          /                             # 5th slash
+          T?                            # 'T' constant
+          (\w*)                         # branch    -> #2
+             |x) {
+       $branch{$repository . '/' . $1} = ($2) ? $2 : "HEAD"; 
+       print "$$ CVS Entry $i: $1/$2\n" if $debug;
     }
 }
 close(ENTRIES);
 
-open (AVAIL, $availfile) || exit(0);   # It is ok for avail file not to exist
-while (<AVAIL>) {
+# ----------------------------------------------------------------------------
+# ------------------------------------- evaluate each active line from 'cvsacl'
+# ----------------------------------------------------------------------------
+open (CVSACL, $cvsaclfile) || exit(0); # It is ok for cvsacl file not to exist
+while (<CVSACL>) {
     chop;
-    next if /^\s*\#/;
-    next if /^\s*$/;
-    ($flagstr, $u, $m, $b) = split(/[\s,]*\|[\s,]*/, $_);
-
-    # Skip anything not starting with "avail" or "unavail" and complain.
-    (print "Bad avail line: $_\n"), next
-       if ($flagstr !~ /^avail/ && $flagstr !~ /^unavail/);
-
-    # 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 && !$b);
-
-    # $myname considered "in user list" if actually in list or is NULL
-    $in_user = (!$u || grep ($_ eq $myname, split(/[\s,]+/,$u)));
-    print "$$ \$myname($myname) 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.
-    if (!($in_repo = !$m)) {
-       my @tmp = split(/[\s,]+/,$m);
-       for $j (@tmp) {
-           # If the repos from avail is a parent(or equal) dir of $repos, OK
-           $in_repo = 1, last if ($repos eq $j || $repos =~ /^$j\//);
-       }
-       if (!$in_repo) {
-           $in_repo = 1;
-           for $j (@ARGV) {
-               last if !($in_repo = grep ($_ eq $j, @tmp));
-           }
-       }
+    next if /^\s*\#/;                               # skip comments
+    next if /^\s*$/;                                # skip blank lines
+    # --------------------------------------------- parse current 'cvsacl' line
+    print("$$ ==========\n$$ Processing \'cvsacl\' line: $_.\n") if $debug;
+    ($cvsacl_flag, $cvsacl_userIds, $cvsacl_modules, $cvsacl_branches) = 
split(/[\s,]*\|[\s,]*/, $_);
+
+    # ------------------------------ Validate 'allow' or 'deny' line prefix
+    if ($cvsacl_flag !~ /^allow/ && $cvsacl_flag !~ /^deny/) {
+       print ("Bad cvsacl line: $_\n") if $debug;
+       $log_text = sprintf "Bad cvsacl line: %s", $_; 
+       write_restrictlog_record($log_text);
+        next;
     }
-    print "$$ \$repos($repos) in repository list: $_\n" if $debug && $in_repo;
 
-    # Branch matches if it is in the branch list in the avail line, the branch
-    # list is NULL, or there is no branch and HEAD is in the branch list.
-    if(!($in_branch = !$b)) {
-       @bls = split (/[\s,]+/,$b);
+    # -------------------------------------------------- init loop match flags
+    $user_match = 0;
+    %repository_matches = ();
+
+    # ------------------------------------------------------------------------
+    # ---------------------------------------------------------- user matching
+    # ------------------------------------------------------------------------
+    # $user_name considered "in user list" if actually in list or is NULL
+    $user_match = (!$cvsacl_userIds || grep ($_ eq $user_name, 
split(/[\s,]+/,$cvsacl_userIds)));
+    print "$$ \$user_name: $user_name \$user_match match flag is: 
$user_match.\n" if $debug;
+    if (!$user_match) {
+       next;                            # no match, skip to next 'cvsacl' line
+    }
 
-       for $j (@ARGV) {
-          $f = $j;
-          last if !($in_branch = grep($_ eq $branch{$j}, @bls)); 
+    # ------------------------------------------------------------------------
+    # ---------------------------------------------------- repository matching
+    # ------------------------------------------------------------------------
+    if (!$cvsacl_modules) {                  # blank module list = all modules
+       if (!$cvsacl_branches) {            # blank branch list = all branches
+           print("$$ Adding all modules to \%repository_matches; null " . 
+                  "\$cvsacl_modules and \$cvsacl_branches.\n") if $debug;
+           for $commit_object (@ARGV) {
+               $repository_matches{$commit_object} = [$branch{$commit_object}, 
$cvsacl_modules];
+                print("$$ \$repository_matches{$commit_object} = " .
+                      "[$branch{$commit_object}, $cvsacl_modules].\n") if 
$debug;
+           }
+       }
+       else {                            # need to check for repository match
+           @branch_list = split (/[\s,]+/,$cvsacl_branches);
+           print("$$ Branches from \'cvsacl\' record: ", join(", 
",@branch_list),".\n") if $debug;
+           for $commit_object (@ARGV) {
+               if (grep($branch{$commit_object}, @branch_list)) {
+                   $repository_matches{$commit_object} = 
[$branch{$commit_object}, $cvsacl_modules];
+                   print("$$ \$repository_matches{$commit_object} = " .
+                          "[$branch{$commit_object}, $cvsacl_modules].\n") if 
$debug;
+               }
+           }
+       }
+    }
+    else {
+       # ----------------------------------- check every argument combination
+       # parse 'cvsacl' modules to array
+       my @module_list = split(/[\s,]+/,$cvsacl_modules);
+        # ------------- Check all modules in list for either file or directory
+       my $fileType = "";
+       if (($fileType = checkFileness(@module_list)) eq "") {
+           next;                                        # skip bad file types
+       }
+       # ---------- Check each combination of 'cvsacl' modules vs. @ARGV files
+       print("$$ Checking matches for \@module_list: ", join("\, 
",@module_list), ".\n") if $debug;
+       # loop thru all command-line commit objects
+        for $commit_object (@ARGV) {              
+           # loop thru all modules on 'cvsacl' line
+            for $cvsacl_module (@module_list) { 
+               print("$$ Is \'cvsacl\': $cvsacl_modules pattern in: \@ARGV " . 
+                      "\$commit_object: $commit_object?\n") if $debug;
+               # Do match of beginning of $commit_object
+               checkModuleMatch($fileType, $commit_object, $cvsacl_module);
+           } # end for commit objects
+       } # end for cvsacl modules
+    } # end if
+
+    print("$$ Matches for: \%repository_matches: ", join("\, ", (keys 
%repository_matches)), ".\n") if $debug;
+
+    # ------------------------------------------------------------------------
+    # ----------------------------------------------------- setting exit value
+    # ------------------------------------------------------------------------
+    if ($user_match && %repository_matches) {
+        print("$$ An \"$cvsacl_flag\" match on User(s): $cvsacl_userIds; 
Module(s):" .
+              " $cvsacl_modules; Branch(es): $cvsacl_branches.\n") if $debug;
+       if ($cvsacl_flag eq "deny") {
+           # Add all matches to the hash of restricted modules
+           foreach $commitFile (keys %repository_matches) {
+               print("$$ Adding \%repository_matches entry: $commitFile.\n") 
if $debug;
+               $restricted_entries{$commitFile} = 
$repository_matches{$commitFile}[0];
+           }
+       }
+       else {
+           # Remove all matches from the restricted modules hash
+           foreach $commitFile (keys %repository_matches) {
+               print("$$ Removing \%repository_matches entry: $commitFile.\n") 
if $debug;
+               delete $restricted_entries{$commitFile};
+           }
        }
     }
-    print "$$ \$branch($branch{$f}) in branch list: $_\n"
-       if $debug && $in_branch;
+    print "$$ ==== End of processing for \'cvsacl\' line: $_.\n" if $debug;
+}
+close(CVSACL);
 
-    $exit_val = $flag if ($in_user && $in_repo && $in_branch);
-    print "$$ ==== \$exit_val = $exit_val\n$$ ==== \$flag = $flag\n" if $debug;
+# ----------------------------------------------------------------------------
+# --------------------------------------- determine final 'commit' disposition
+# ---------------------------------------------------------------------------- 
+if (%restricted_entries) {                           # any restricted entries?
+    $exit_val = 1;                                              # don't commit
+    print("**** Access denied: Insufficient authority for user: '$user_name\' 
" .
+          "to commit to \'$repository\'.\n**** Contact CVS Administrators if " 
.
+          "you require update access to these directories or files.\n");
+    print("**** file(s)/dir(s) restricted were:\n\t", join("\n\t",keys 
%restricted_entries), "\n");
+    printOptionalRestrictionMessage();
+    write_restrictlog();
+}
+elsif (!$exit_val && $debug) {
+    print "**** Access allowed: Sufficient authority for commit.\n";
 }
-close(AVAIL);
+
 print "$$ ==== \$exit_val = $exit_val\n" if $debug;
-print "**** Access denied: Insufficient Karma ($myname|$repos|$branch{$f})\n"
-       if $exit_val;
-print "**** Access allowed: Personal Karma exceeds Environmental Karma.\n"
-       if $universal_off && !$exit_val;
 exit($exit_val);
+
+# ----------------------------------------------------------------------------
+# -------------------------------------------------------------- end of "main"
+# ----------------------------------------------------------------------------
+
+
+# ----------------------------------------------------------------------------
+# -------------------------------------------------------- process script args
+# ----------------------------------------------------------------------------
+sub processArgs {
+
+# This subroutine is passed a reference to @ARGV. 
+
+# If @ARGV contains a "-u" entry, use that as the effective userId.  In this 
+# case, the userId is the client-side userId that has been passed to this 
+# script by the commit_prep script.  (This is why the commit_prep script must 
+# be placed *before* the cvs_acls2 script in the commitinfo admin file.)
+
+# Otherwise, pull the userId from the server-side environment.
+
+    my $userId = "";
+    my ($argv) = shift;             # pick up ref to @ARGV
+    my @argvClone = ();             # immutable copy for foreach loop
+    for ($i=0; $i<(scalar @{$argv}); $i++) {
+       $argvClone[$i]=$argv->[$i]; 
+    }
+
+    print("$$ \@_ to processArgs is: @_.\n") if $debug;
+
+    # Parse command line arguments (file list is seen as one arg)
+    foreach $arg (@argvClone) {
+       print("$$ \$arg for processArgs loop is: $arg.\n") if $debug;
+       # Set $debug flag?
+       if ($arg eq '-d') {
+           shift @ARGV;
+           $debug = 1;
+           print("$$ \$debug flag set on.\n") if $debug;
+           print STDERR "Debug turned on...\n";
+       }
+       # Passing in a client-side userId?
+       elsif ($arg eq '-u') {
+           shift @ARGV;
+           $userId = shift @ARGV;
+           print("$$ client-side \$userId set to: $userId.\n") if $debug;
+       } 
+       # An override for the default restrictlog file?
+       elsif ($arg eq '-f') {
+           shift @ARGV;
+           $restrictlog = shift @ARGV;
+       } 
+       else {
+           next;
+       }
+    }
+
+    # No client-side userId passed? then get from server env
+    if (!$userId) {
+        $userId = $ENV{"USER"} if !($userId = $ENV{"LOGNAME"});
+           print("$$ server-side \$userId set to: $userId.\n") if $debug;
+    }
+
+    print("$$ processArgs returning \$userId: $userId.\n") if $debug;
+    return $userId;
+
+}
+
+
+# ----------------------------------------------------------------------------
+# --------------------- Check all modules in list for either file or directory
+# ----------------------------------------------------------------------------
+sub checkFileness {
+
+# Module patterns on the 'cvsacl' record can be files or directories. 
+# If it's a directory, we pattern-match the directory name from 'cvsacl' 
+# against the left side of the committed filename to see if the file is in 
+# that hierarchy.  By contrast, files use an explicit match.  If the entries
+# are neither files nor directories, then the cvsacl file has been set up
+# incorrectly; we return a "" and the caller skips that line as invalid.
+#
+# This function determines whether the entries on the 'cvsacl' record are all
+# directories or all files; it cannot be a mixture.  This restriction put in
+# to simplify the logic (without taking away much functionality).
+
+    my @module_list = @_;
+    print("$$ Checking \"fileness\" or \"dir-ness\" for \@module_list 
entries.\n") if $debug;
+    print("$$     Entries are: ", join("\, ",@module_list), ".\n") if $debug;
+    my $filetype = "";
+    for $cvsacl_module (@module_list) {
+        my $reposDirName = $cvsroot . '/' . $cvsacl_module;
+        my $reposFileName = $reposDirName . "\,v";
+        print("$$ In checkFileness: \$reposDirName: $reposDirName; 
\$reposFileName: $reposFileName.\n") if $debug;
+        if (((-d $reposDirName) && ($filetype eq "file")) || ((-f 
$reposFileName) && ($filetype eq "dir"))) {
+            print("Can\'t mix files and directories on single \'cvsacl\' file 
record; skipping entry.\n");
+           print("    Please contact a CVS administrator.\n");
+           $filetype = "";
+           last;
+        }
+        elsif (-d $reposDirName) { 
+            $filetype = "dir";
+           print("$$ $reposDirName is a directory.\n") if $debug;
+       }
+        elsif (-f $reposFileName) {
+            $filetype = "file";
+           print("$$ $reposFileName is a regular file.\n") if $debug;
+       }
+        else {
+            print("***** Item to commit was neither a regular file nor a 
directory.\n");
+           print("***** Current \'cvsacl\' line ignored.\n");
+           print("***** Possible problem with \'cvsacl\' admin file. Please 
contact a CVS administrator.\n");
+           $filetype = "";
+           $text = sprintf("Module entry on cvsacl line: %s is not a valid 
file or directory.\n", $cvsacl_module);
+           write_restrictlog_record($text);
+           last;
+        } # end if
+    } # end for
+
+    print("$$ checkFileness will return \$filetype: $filetype.\n") if $debug;
+    return $filetype;
+}
+
+
+# ----------------------------------------------------------------------------
+# ----------------------------------------------------- check for module match
+# ----------------------------------------------------------------------------
+sub checkModuleMatch {
+
+# This subroutine checks for a match between the directory or file pattern 
+# specified in the 'cvsacl' file (i.e., $cvsacl_modules) versus the commit 
file 
+# objects passed into the script via @ARGV (i.e., $commit_object). 
+
+# The directory pattern only has to match the beginning portion of the commit 
+# file's name for a match since all files under that directory are considered 
+# a match. File patterns must exactly match.
+
+# Since (theoretically, if not normally in practice) a working directory can
+# contain a mixture of files from different branches, this routine checks to 
+# see if there is also a match on branch before considering the file 
+# comparison a match.
+
+    my $match_flag = "";
+
+    print("$$ \@_ in checkModuleMatch is: @_.\n") if $debug;
+    my ($type,$commit_object,$cvsacl_module) = @_;
+
+    if ($type eq "file") {             # Do exact file match of $commit_object
+       if ($commit_object eq $cvsacl_module) {
+           $match_flag = "file";
+       }                        # Do dir match at beginning of $commit_object
+    }
+    elsif ($commit_object =~ /^$cvsacl_module\//) {
+        $match_flag = "dir";
+    }
+
+    if ($match_flag) {
+       print("$$ \$repository: $repository matches \$commit_object: 
$commit_object.\n") if $debug;
+       if (!$cvsacl_branches) {             # empty branch pattern matches all
+           print("$$ blank \'cvsacl\' branch matches all commit files.\n") if 
$debug;
+           $repository_matches{$commit_object} = [$branch{$commit_object}, 
$cvsacl_module];
+           print("$$ \$repository_matches{$commit_object} = 
[$branch{$commit_object}, $cvsacl_module].\n") if $debug;
+       }
+       else {                             # otherwise check branch hash table
+           @branch_list = split (/[\s,]+/,$cvsacl_branches);
+           print("$$ Branches from \'cvsacl\' record: ", join(", 
",@branch_list),".\n") if $debug;
+           if (grep(/$branch{$commit_object}/, @branch_list)) {
+               $repository_matches{$commit_object} = [$branch{$commit_object}, 
$cvsacl_module];
+               print("$$ \$repository_matches{$commit_object} = 
[$branch{$commit_object}, " .
+                      "$cvsacl_module].\n") if $debug;
+           }
+       }
+    }
+
+}
+
+# ----------------------------------------------------------------------------
+# ------------------------------------------------------- check for file match
+# ----------------------------------------------------------------------------
+sub printOptionalRestrictionMessage {
+
+# This subroutine optionally prints site-specific file restriction information
+# whenever a restriction condition is met.  If the file 'restrict_msg' does 
+# not exist, the routine immediately exits.  If there is a 'restrict_msg' file
+# then all the contents are printed at the end of the standard restriction 
+# message.
+
+# As seen from examining the definition of $restrictfile, the default filename
+# is: $CVSROOT/CVSROOT/restrict_msg.
+
+    open (RESTRICT, $restrictfile) || return;  # It is ok for cvsacl file not 
to exist
+    while (<RESTRICT>) {
+       chop;
+       # print out each line
+       print("**** $_\n");
+    }
+
+}
+
+# ----------------------------------------------------------------------------
+# ---------------------------------------------------------- write log message
+# ----------------------------------------------------------------------------
+sub write_restrictlog {
+
+# This subroutine iterates through the list of restricted entries and logs 
+# each one to the error logfile.
+
+    # write each line in @text out separately
+    foreach $commitfile (keys %restricted_entries) {
+       $log_text = sprintf "Commit attempt by: %s for: %s on branch: %s", 
+                           $user_name, $commitfile, $branch{$commitfile};
+       write_restrictlog_record($log_text);
+    }
+
+}
+
+# ----------------------------------------------------------------------------
+# ---------------------------------------------------------- write log message
+# ----------------------------------------------------------------------------
+sub write_restrictlog_record {
+
+# This subroutine receives a scalar string and writes it out to the 
+# $restrictlog file as a separate line. Each line is prepended with the date 
+# and time in the format: "2004/01/30 12:00:00 ".
+
+    $text = shift;
+
+    # return quietly if there is a problem opening the log file.
+    open(FILE, ">>$restrictlog") || return;
+
+    (@time) = localtime();
+
+    # write each line in @text out separately
+    $log_record = sprintf "%04d/%02d/%02d %02d:%02d:%02d %s.\n", 
+                      $time[5]+1900, $time[4]+1, $time[3], $time[2], $time[1], 
$time[0], $text;
+    print FILE $log_record;
+    print("$$ restrict_log record being written: $log_record to 
$restrictlog.\n") if $debug;
+    
+    close(FILE);
+}




reply via email to

[Prev in Thread] Current Thread [Next in Thread]