mirror of
https://github.com/Stichting-MINIX-Research-Foundation/netbsd.git
synced 2025-09-12 16:46:33 -04:00
964 lines
37 KiB
Perl
Executable File
964 lines
37 KiB
Perl
Executable File
#! @PERL@ -T
|
|
# -*-Perl-*-
|
|
|
|
# Copyright (C) 1994-2005 The Free Software Foundation, Inc.
|
|
|
|
# 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, 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.
|
|
|
|
###############################################################################
|
|
###############################################################################
|
|
###############################################################################
|
|
#
|
|
# THIS SCRIPT IS PROBABLY BROKEN. REMOVING THE -T SWITCH ON THE #! LINE ABOVE
|
|
# WOULD FIX IT, BUT THIS IS INSECURE. WE RECOMMEND FIXING THE ERRORS WHICH THE
|
|
# -T SWITCH WILL CAUSE PERL TO REPORT BEFORE RUNNING THIS SCRIPT FROM A CVS
|
|
# SERVER TRIGGER. PLEASE SEND PATCHES CONTAINING THE CHANGES YOU FIND
|
|
# NECESSARY TO RUN THIS SCRIPT WITH THE TAINT-CHECKING ENABLED BACK TO THE
|
|
# <@PACKAGE_BUGREPORT@> MAILING LIST.
|
|
#
|
|
# For more on general Perl security and taint-checking, please try running the
|
|
# `perldoc perlsec' command.
|
|
#
|
|
###############################################################################
|
|
###############################################################################
|
|
###############################################################################
|
|
|
|
=head1 Name
|
|
|
|
cvs_acls - Access Control List for CVS
|
|
|
|
=head1 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.
|
|
|
|
=head1 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
|
|
|
|
=head1 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/.
|
|
|
|
=head1 Enhancements
|
|
|
|
This section lists the bug fixes and enhancements added to cvs_acls
|
|
that make up the current cvs_acls.
|
|
|
|
=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_acls.in,v 1.1.1.1 2009/04/07 22:10:10 christos 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)
|
|
|
|
=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.
|
|
[http://ximbiot.com/cvs/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.
|
|
|
|
=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'};
|
|
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
|
|
|
|
print("$$ \@ARGV after shift processing contains:",join("\, ",@ARGV),".\n") if $debug;
|
|
|
|
# ---------------------------------------------------------------- get cvsroot
|
|
($repository = shift) =~ s:^$cvsroot/::;
|
|
grep($_ = $repository . '/' . $_, @ARGV);
|
|
|
|
print("$$ \$cvsroot is: $cvsroot.\n") if $debug;
|
|
print "$$ Repos: $repository\n","$$ ==== ",join("\n$$ ==== ",@ARGV),"\n" if $debug;
|
|
|
|
$exit_val = 0; # presume good exit value for commit
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# ---------------------------------- create hash table $branch{file -> branch}
|
|
# ----------------------------------------------------------------------------
|
|
|
|
# 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*$/; # 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);
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# ------------------------------------- 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*\#/; # 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;
|
|
}
|
|
|
|
# -------------------------------------------------- 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
|
|
}
|
|
|
|
# ------------------------------------------------------------------------
|
|
# ---------------------------------------------------- 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 "$$ ==== End of processing for \'cvsacl\' line: $_.\n" if $debug;
|
|
}
|
|
close(CVSACL);
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# --------------------------------------- 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";
|
|
}
|
|
|
|
print "$$ ==== \$exit_val = $exit_val\n" if $debug;
|
|
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_acls 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);
|
|
}
|