Name

cvs_acls - Access Control List for CVS


Synopsis

In 'commitinfo':

  repository/path/to/restrict $CVSROOT/CVSROOT/cvs_acls [-d][-u $USER][-f <logfile>]

where:

  -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

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.


Licensing

cvs_acls - 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


Description

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.

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/.


Enhancements

This section lists the bug fixes and enhancements added to cvs_acls that make up the current cvs_acls.

Fixed Bugs

This version attempts to get rid the following bugs from the original version of cvs_acls:

Enhancements

ToDoS


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_acls.html,v 1.1.2.1 2005/01/25 18:58:44 mdb Exp $

This version 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)


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_acls [-d][-u $USER ][-f <logfilename>]
   where "-d" turns on debug trace
         "-u $USER" passes the client-side userId to cvs_acls 
         "-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_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.

3. Install this file as $CVSROOT/CVSROOT/cvs_acls 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_acls.' 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_acls 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_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.


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


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:

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

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