1#! @PERL@ -T
2# -*-Perl-*-
3
4# Copyright (C) 1994-2005 The Free Software Foundation, Inc.
5
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; either version 2, or (at your option)
9# any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15
16###############################################################################
17###############################################################################
18###############################################################################
19#
20# THIS SCRIPT IS PROBABLY BROKEN.  REMOVING THE -T SWITCH ON THE #! LINE ABOVE
21# WOULD FIX IT, BUT THIS IS INSECURE.  WE RECOMMEND FIXING THE ERRORS WHICH THE
22# -T SWITCH WILL CAUSE PERL TO REPORT BEFORE RUNNING THIS SCRIPT FROM A CVS
23# SERVER TRIGGER.  PLEASE SEND PATCHES CONTAINING THE CHANGES YOU FIND
24# NECESSARY TO RUN THIS SCRIPT WITH THE TAINT-CHECKING ENABLED BACK TO THE
25# <@PACKAGE_BUGREPORT@> MAILING LIST.
26#
27# For more on general Perl security and taint-checking, please try running the
28# `perldoc perlsec' command.
29#
30###############################################################################
31###############################################################################
32###############################################################################
33
34=head1 Name
35
36cvs_acls - Access Control List for CVS
37
38=head1 Synopsis
39
40In 'commitinfo':
41
42  repository/path/to/restrict $CVSROOT/CVSROOT/cvs_acls [-d][-u $USER][-f <logfile>]
43
44where:
45
46  -d  turns on debug information
47  -u  passes the client-side userId to the cvs_acls script
48  -f  specifies an alternate filename for the restrict_log file
49
50In 'cvsacl':
51
52  {allow.*,deny.*} [|user,user,... [|repos,repos,... [|branch,branch,...]]]
53
54where:
55
56  allow|deny - allow: commits are allowed; deny: prohibited
57  user          - userId to be allowed or restricted
58  repos         - file or directory to be allowed or restricted
59  branch        - branch to be allowed or restricted
60
61See below for examples.
62
63=head1 Licensing
64
65cvs_acls - provides access control list functionality for CVS
66
67Copyright (c) 2004 by Peter Connolly <peter.connolly@cnet.com>
68All rights reserved.
69
70This program is free software; you can redistribute it and/or modify
71it under the terms of the GNU General Public License as published by
72the Free Software Foundation; either version 2 of the License, or
73(at your option) any later version.
74
75This program is distributed in the hope that it will be useful,
76but WITHOUT ANY WARRANTY; without even the implied warranty of
77MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
78GNU General Public License for more details.
79
80You should have received a copy of the GNU General Public License
81along with this program; if not, write to the Free Software
82Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
83
84=head1 Description
85
86This script--cvs_acls--is invoked once for each directory within a
87"cvs commit". The set of files being committed for that directory as
88well as the directory itself, are passed to this script.  This script
89checks its 'cvsacl' file to see if any of the files being committed
90are on the 'cvsacl' file's restricted list.  If any of the files are
91restricted, then the cvs_acls script passes back an exit code of 1
92which disallows the commits for that directory.
93
94Messages are returned to the committer indicating the file(s) that
95he/she are not allowed to committ.  Additionally, a site-specific
96set of messages (e.g., contact information) can be included in these
97messages.
98
99When a commit is prohibited, log messages are written to a restrict_log
100file in $CVSROOT/CVSROOT.  This default file can be redirected to
101another destination.
102
103The script is triggered from the 'commitinfo' file in $CVSROOT/CVSROOT/.
104
105=head1 Enhancements
106
107This section lists the bug fixes and enhancements added to cvs_acls
108that make up the current cvs_acls.
109
110=head2 Fixed Bugs
111
112This version attempts to get rid the following bugs from the
113original version of cvs_acls:
114
115=over 2
116
117=item *
118Multiple entries on an 'cvsacl' line will be matched individually,
119instead of requiring that all commit files *exactly* match all
120'cvsacl' entries. Commiting a file not in the 'cvsacl' list would
121allow *all* files (including a restricted file) to be committed.
122
123[IMO, this basically made the original script unuseable for our
124situation since any arbitrary combination of committed files could
125avoid matching the 'cvsacl's entries.]
126
127=item *
128Handle specific filename restrictions. cvs_acls didn't restrict
129individual files specified in 'cvsacl'.
130
131=item *
132Correctly handle multiple, specific filename restrictions
133
134=item *
135Prohibit mix of dirs and files on a single 'cvsacl' line
136[To simplify the logic and because this would be normal usage.]
137
138=item *
139Correctly handle a mixture of branch restrictions within one work
140directory
141
142=item *
143$CVSROOT existence is checked too late
144
145=item *
146Correctly handle the CVSROOT=:local:/... option (useful for
147interactive testing)
148
149=item *
150Replacing shoddy "$universal_off" logic
151(Thanks to Karl-Konig Konigsson for pointing this out.)
152
153=back
154
155=head2 Enhancements
156
157=over 2
158
159=item *
160Checks modules in the 'cvsacl' file for valid files and directories
161
162=item *
163Accurately report restricted entries and their matching patterns
164
165=item *
166Simplified and commented overly complex PERL REGEXPs for readability
167and maintainability
168
169=item *
170Skip the rest of processing if a mismatch on portion of the 'cvsacl' line
171
172=item *
173Get rid of opaque "karma" messages in favor of user-friendly messages
174that describe which user, file(s) and branch(es) were disallowed.
175
176=item *
177Add optional 'restrict_msg' file for additional, site-specific
178restriction messages.
179
180=item *
181Take a "-u" parameter for $USER from commit_prep so that the script
182can do restrictions based on the client-side userId rather than the
183server-side userId (usually 'cvs').
184
185(See discussion below on "Admin Setup" for more on this point.)
186
187=item *
188Added a lot more debug trace
189
190=item *
191Tested these restrictions with concurrent use of pserver and SSH
192access to model our transition from pserver to ext access.
193
194=item *
195Added logging of restricted commit attempts.
196Restricted commits can be sent to a default file:
197$CVSROOT/CVSROOT/restrictlog or to one passed to the script
198via the -f command parameter.
199
200=back
201
202=head2 ToDoS
203
204=over 2
205
206=item *
207Need to deal with pserver/SSH transition with conflicting umasks?
208
209=item *
210Use a CPAN module to handle command parameters.
211
212=item *
213Use a CPAN module to clone data structures.
214
215=back
216
217=head1 Version Information
218
219This is not offered as a fix to the original 'cvs_acls' script since it
220differs substantially in goals and methods from the original and there
221are probably a significant number of people out there that still require
222the original version's functionality.
223
224The 'cvsacl' file flags of 'allow' and 'deny' were intentionally
225changed to 'allow' and 'deny' because there are enough differences
226between the original script's behavior and this one's that we wanted to
227make sure that users will rethink their 'cvsacl' file formats before
228plugging in this newer script.
229
230Please note that there has been very limited cross-platform testing of
231this script!!! (We did not have the time or resources to do exhaustive
232cross-platform testing.)
233
234It was developed and tested under Red Hat Linux 9.0 using PERL 5.8.0.
235Additionally, it was built and tested under Red Hat Linux 7.3 using
236PERL 5.6.1.
237
238$Id: cvs_acls.in,v 1.11 2005/09/01 13:48:57 dprice Exp $
239
240This version is based on the 1.11.13 version of cvs_acls
241peter.connolly@cnet.com (Peter Connolly)
242
243  Access control lists for CVS.  dgg@ksr.com (David G. Grubbs)
244  Branch specific controls added by voisine@bytemobile.com (Aaron Voisine)
245
246=head1 Installation
247
248To use this program, do the following four things:
249
2500. Install PERL, version 5.6.1 or 5.8.0.
251
2521. Admin Setup:
253
254   There are two choices here.
255
256   a) The first option is to use the $ENV{"USER"}, server-side userId
257      (from the third column of your pserver 'passwd' file) as the basis for
258      your restrictions.  In this case, you will (at a minimum) want to set
259      up a new "cvsadmin" userId and group on the pserver machine.
260      CVS administrators will then set up their 'passwd' file entries to
261      run either as "cvs" (for regular users) or as "cvsadmin" (for power
262      users).  Correspondingly, your 'cvsacl' file will only list 'cvs'
263      and 'cvsadmin' as the userIds in the second column.
264
265      Commentary: A potential weakness of this is that the xinetd
266      cvspserver process will need to run as 'root' in order to switch
267      between the 'cvs' and the 'cvsadmin' userIds.  Some sysadmins don't
268      like situations like this and may want to chroot the process.
269      Talk to them about this point...
270
271   b) The second option is to use the client-side userId as the basis for
272      your restrictions.  In this case, all the xinetd cvspserver processes
273      can run as userId 'cvs' and no 'root' userId is required.  If you have
274      a 'passwd' file that lists 'cvs' as the effective run-time userId for
275      all your users, then no changes to this file are needed.  Your 'cvsacl'
276      file will use the individual, client-side userIds in its 2nd column.
277
278      As long as the userIds in pserver's 'passwd' file match those userIds
279      that your Linux server know about, this approach is ideal if you are
280      planning to move from pserver to SSH access at some later point in time.
281      Just by switching the CVSROOT var from CVSROOT=:pserver:<userId>... to
282      CVSROOT=:ext:<userId>..., users can switch over to SSH access without
283      any other administrative changes.  When all users have switched over to
284      SSH, the inherently insecure xinetd cvspserver process can be disabled.
285      [http://ximbiot.com/cvs/manual/cvs-1.11.17/cvs_2.html#SEC32]
286
287      :TODO: The only potential glitch with the SSH approach is the possibility
288      that each user can have differing umasks that might interfere with one
289      another, especially during a transition from pserver to SSH.  As noted
290      in the ToDo section, this needs a good strategy and set of tests for that
291      yet...
292
2932. Put two lines, as the *only* non-comment lines, in your commitinfo file:
294
295   ALL $CVSROOT/CVSROOT/commit_prep
296   ALL $CVSROOT/CVSROOT/cvs_acls [-d][-u $USER ][-f <logfilename>]
297
298   where "-d" turns on debug trace
299         "-u $USER" passes the client-side userId to cvs_acls
300         "-f <logfilename"> overrides the default filename used to log
301                            restricted commit attempts.
302
303   (These are handled in the processArgs() subroutine.)
304
305If you are using client-side userIds to restrict access to your
306repository, make sure that they are in this order since the commit_prep
307script is required in order to pass the $USER parameter.
308
309A final note about the repository matching pattern.  The example above
310uses "ALL" but note that this means that the cvs_acls script will run
311for each and every commit in your repository.  Obviously, in a large
312repository this adds up to a lot of overhead that may not be necesary.
313A better strategy is to use a repository pattern that is more specific
314to the areas that you wish to secure.
315
3163. Install this file as $CVSROOT/CVSROOT/cvs_acls and make it executable.
317
3184. Create a file named CVSROOT/cvsacl and optionally add it to
319   CVSROOT/checkoutlist and check it in.  See the CVS manual's
320   administrative files section about checkoutlist.  Typically:
321
322   $ cvs checkout CVSROOT
323   $ cd CVSROOT
324   [ create the cvsacl file, include 'commitinfo' line ]
325   [ add cvsacl to checkoutlist ]
326   $ cvs add cvsacl
327   $ cvs commit -m 'Added cvsacl for use with cvs_acls.' cvsacl checkoutlist
328
329Note: The format of the 'cvsacl' file is described in detail immediately
330below but here is an important set up point:
331
332   Make sure to include a line like the following:
333
334     deny||CVSROOT/commitinfo CVSROOT/cvsacl
335     allow|cvsadmin|CVSROOT/commitinfo CVSROOT/cvsacl
336
337   that restricts access to commitinfo and cvsacl since this would be one of
338   the easiest "end runs" around this ACL approach. ('commitinfo' has the
339   line that executes the cvs_acls script and, of course, all the
340   restrictions are in 'cvsacl'.)
341
3425. (Optional) Create a 'restrict_msg' file in the $CVSROOT/CVSROOT directory.
343   Whenever there is a restricted file or dir message, cvs_acls will look
344   for this file and, if it exists, print its contents as part of the
345   commit-denial message.  This gives you a chance to print any site-specific
346   information (e.g., who to call, what procedures to look up,...) whenever
347   a commit is denied.
348
349=head1 Format of the cvsacl file
350
351The 'cvsacl' file determines whether you may commit files.  It contains lines
352read from top to bottom, keeping track of whether a given user, repository
353and branch combination is "allowed" or "denied."  The script will assume
354"allowed" on all repository paths until 'allow' and 'deny' rules change
355that default.
356
357The normal pattern is to specify an 'deny' rule to turn off
358access to ALL users, then follow it with a matching 'allow' rule that will
359turn on access for a select set of users.  In the case of multiple rules for
360the same user, repository and branch, the last one takes precedence.
361
362Blank lines and lines with only comments are ignored.  Any other lines not
363beginning with "allow" or "deny" are logged to the restrict_log file.
364
365Lines beginning with "allow" or "deny" are assumed to be '|'-separated
366triples: (All spaces and tabs are ignored in a line.)
367
368  {allow.*,deny.*} [|user,user,... [|repos,repos,... [|branch,branch,...]]]
369
370   1. String starting with "allow" or "deny".
371   2. Optional, comma-separated list of usernames.
372   3. Optional, comma-separated list of repository pathnames.
373      These are pathnames relative to $CVSROOT.  They can be directories or
374      filenames.  A directory name allows or restricts access to all files and
375      directories below it. One line can have either directories or filenames
376      but not both.
377   4. Optional, comma-separated list of branch tags.
378      If not specified, all branches are assumed. Use HEAD to reference the
379      main branch.
380
381Example:  (Note: No in-line comments.)
382
383   # ----- Make whole repository unavailable.
384   deny
385
386   # ----- Except for user "dgg".
387   allow|dgg
388
389   # ----- Except when "fred" or "john" commit to the
390   #       module whose repository is "bin/ls"
391   allow|fred, john|bin/ls
392
393   # ----- Except when "ed" commits to the "stable"
394   #       branch of the "bin/ls" repository
395   allow|ed|/bin/ls|stable
396
397=head1 Program Logic
398
399CVS passes to @ARGV an absolute directory pathname (the repository
400appended to your $CVSROOT variable), followed by a list of filenames
401within that directory that are to be committed.
402
403The script walks through the 'cvsacl' file looking for matches on
404the username, repository and branch.
405
406A username match is simply the user's name appearing in the second
407column of the cvsacl line in a space-or-comma separate list. If
408blank, then any user will match.
409
410A repository match:
411
412=over 2
413
414=item *
415Each entry in the modules section of the current 'cvsacl' line is
416examined to see if it is a dir or a file. The line must have
417either files or dirs, but not both. (To simplify the logic.)
418
419=item *
420If neither, then assume the 'cvsacl' file was set up in error and
421skip that 'allow' line.
422
423=item *
424If a dir, then each dir pattern is matched separately against the
425beginning of each of the committed files in @ARGV.
426
427=item *
428If a file, then each file pattern is matched exactly against each
429of the files to be committed in @ARGV.
430
431=item *
432Repository and branch must BOTH match together. This is to cover
433the use case where a user has multiple branches checked out in
434a single work directory. Commit files can be from different
435branches.
436
437A branch match is either:
438
439=over 4
440
441=item *
442When no branches are listed in the fourth column. ("Match any.")
443
444=item *
445All elements from the fourth column are matched against each of
446the tag names for $ARGV[1..$#ARGV] found in the %branches file.
447
448=back
449
450=item *
451'allow' match remove that match from the tally map.
452
453=item *
454Restricted ('deny') matches are saved in the %repository_matches
455table.
456
457=item *
458If there is a match on user, repository and branch:
459
460  If repository, branch and user match
461    if 'deny'
462      add %repository_matches entries to %restricted_entries
463    else if 'allow'
464      remove %repository_matches entries from %restricted_entries
465
466=item *
467At the end of all the 'cvsacl' line checks, check to see if there
468are any entries in the %restricted_entries.  If so, then deny the
469commit.
470
471=back
472
473=head2 Pseudocode
474
475     read CVS/Entries file and create branch{file}->{branch} hash table
476   + for each 'allow' and 'deny' line in the 'cvsacl' file:
477   |   user match?
478   |     - Yes: set $user_match       = 1;
479   |   repository and branch match?
480   |     - Yes: add to %repository_matches;
481   |   did user, repository match?
482   |     - Yes: if 'deny' then
483   |                add %repository_matches -> %restricted_entries
484   |            if 'allow'   then
485   |                remove %repository_matches <- %restricted_entries
486   + end for loop
487     any saved restrictions?
488       no:  exit,
489            set exit code allowing commits and exit
490       yes: report restrictions,
491            set exit code prohibiting commits and exit
492
493=head2 Sanity Check
494
495  1) file allow trumps a dir deny
496     deny||java/lib
497     allow||java/lib/README
498  2) dir allow can undo a file deny
499     deny||java/lib/README
500     allow||java/lib
501  3) file deny trumps a dir allow
502     allow||java/lib
503     deny||java/lib/README
504  4) dir deny trumps a file allow
505     allow||java/lib/README
506     deny||java/lib
507  ... so last match always takes precedence
508
509=cut
510
511$debug = 0;                     # Set to 1 for debug messages
512
513%repository_matches = ();       # hash of match file and pattern from 'cvsacl'
514                                # repository_matches --> [branch, matching-pattern]
515                                # (Used during module/branch matching loop)
516
517%restricted_entries = ();       # hash table of restricted commit files (from @ARGV)
518                                # restricted_entries --> branch
519                                # (If user/module/branch all match on an 'deny'
520                                #  line, then entries added to this map.)
521
522%branch;                        # hash table of key: commit file; value: branch
523                                # Built from ".../CVS/Entries" file of directory
524                                # currently being examined
525
526# ---------------------------------------------------------------- get CVSROOT
527$cvsroot = $ENV{'CVSROOT'};
528die "Must set CVSROOT\n" if !$cvsroot;
529if ($cvsroot =~ /:([\/\w]*)$/) { # Filter ":pserver:", ":local:"-type prefixes
530    $cvsroot = $1;
531}
532
533# ------------------------------------------------------------- set file paths
534$entries = "CVS/Entries";                                # client-side file???
535$cvsaclfile = $cvsroot . "/CVSROOT/cvsacl";
536$restrictfile = $cvsroot . "/CVSROOT/restrict_msg";
537$restrictlog = $cvsroot . "/CVSROOT/restrict_log";
538
539# --------------------------------------------------------------- process args
540$user_name = processArgs(\@ARGV);
541
542print("$$ \@ARGV after processArgs is: @ARGV.\n") if $debug;
543print("$$ ========== Begin $PROGRAM_NAME for \"$ARGV[0]\" repository. ========== \n") if $debug;
544
545# --------------------------------------------------------------- filter @ARGV
546eval "print STDERR \$die='Unknown parameter $1\n' if !defined \$$1; \$$1=\$';"
547    while ($ARGV[0] =~ /^(\w+)=/ && shift(@ARGV));
548exit 255 if $die;		         # process any variable=value switches
549
550print("$$ \@ARGV after shift processing contains:",join("\, ",@ARGV),".\n") if $debug;
551
552# ---------------------------------------------------------------- get cvsroot
553($repository = shift) =~ s:^$cvsroot/::;
554grep($_ = $repository . '/' . $_, @ARGV);
555
556print("$$ \$cvsroot is: $cvsroot.\n") if $debug;
557print "$$ Repos: $repository\n","$$ ==== ",join("\n$$ ==== ",@ARGV),"\n" if $debug;
558
559$exit_val = 0;			         # presume good exit value for commit
560
561# ----------------------------------------------------------------------------
562# ---------------------------------- create hash table $branch{file -> branch}
563# ----------------------------------------------------------------------------
564
565# Here's a typical Entries file:
566#
567#   /checkoutlist/1.4/Wed Feb  4 23:51:23 2004//
568#   /cvsacl/1.3/Tue Feb 24 23:05:43 2004//
569#   ...
570#   /verifymsg/1.1/Fri Mar 16 19:56:24 2001//
571#   D/backup////
572#   D/temp////
573
574open(ENTRIES, $entries) || die("Cannot open $entries.\n");
575print("$$ File / Branch\n") if $debug;
576my $i = 0;
577while(<ENTRIES>) {
578    chop;
579    next if /^\s*$/;                    # Skip blank lines
580    $i = $i + 1;
581    if (m|
582          /                             # 1st slash
583          ([\w.-]*)                     # file name -> $1
584          /                             # 2nd slash
585          .*                            # revision number
586          /                             # 3rd slash
587          .*                            # date and time
588          /                             # 4th slash
589          .*                            # keyword
590          /                             # 5th slash
591          T?                            # 'T' constant
592          (\w*)                         # branch    -> #2
593	      |x) {
594	$branch{$repository . '/' . $1} = ($2) ? $2 : "HEAD";
595	print "$$ CVS Entry $i: $1/$2\n" if $debug;
596    }
597}
598close(ENTRIES);
599
600# ----------------------------------------------------------------------------
601# ------------------------------------- evaluate each active line from 'cvsacl'
602# ----------------------------------------------------------------------------
603open (CVSACL, $cvsaclfile) || exit(0);	# It is ok for cvsacl file not to exist
604while (<CVSACL>) {
605    chop;
606    next if /^\s*\#/;                               # skip comments
607    next if /^\s*$/;                                # skip blank lines
608    # --------------------------------------------- parse current 'cvsacl' line
609    print("$$ ==========\n$$ Processing \'cvsacl\' line: $_.\n") if $debug;
610    ($cvsacl_flag, $cvsacl_userIds, $cvsacl_modules, $cvsacl_branches) = split(/[\s,]*\|[\s,]*/, $_);
611
612    # ------------------------------ Validate 'allow' or 'deny' line prefix
613    if ($cvsacl_flag !~ /^allow/ && $cvsacl_flag !~ /^deny/) {
614	print ("Bad cvsacl line: $_\n") if $debug;
615	$log_text = sprintf "Bad cvsacl line: %s", $_;
616	write_restrictlog_record($log_text);
617        next;
618    }
619
620    # -------------------------------------------------- init loop match flags
621    $user_match = 0;
622    %repository_matches = ();
623
624    # ------------------------------------------------------------------------
625    # ---------------------------------------------------------- user matching
626    # ------------------------------------------------------------------------
627    # $user_name considered "in user list" if actually in list or is NULL
628    $user_match = (!$cvsacl_userIds || grep ($_ eq $user_name, split(/[\s,]+/,$cvsacl_userIds)));
629    print "$$ \$user_name: $user_name \$user_match match flag is: $user_match.\n" if $debug;
630    if (!$user_match) {
631	next;                            # no match, skip to next 'cvsacl' line
632    }
633
634    # ------------------------------------------------------------------------
635    # ---------------------------------------------------- repository matching
636    # ------------------------------------------------------------------------
637    if (!$cvsacl_modules) {                  # blank module list = all modules
638	if (!$cvsacl_branches) {            # blank branch list = all branches
639	    print("$$ Adding all modules to \%repository_matches; null " .
640                  "\$cvsacl_modules and \$cvsacl_branches.\n") if $debug;
641	    for $commit_object (@ARGV) {
642		$repository_matches{$commit_object} = [$branch{$commit_object}, $cvsacl_modules];
643                print("$$ \$repository_matches{$commit_object} = " .
644                      "[$branch{$commit_object}, $cvsacl_modules].\n") if $debug;
645	    }
646	}
647	else {                            # need to check for repository match
648	    @branch_list = split (/[\s,]+/,$cvsacl_branches);
649	    print("$$ Branches from \'cvsacl\' record: ", join(", ",@branch_list),".\n") if $debug;
650	    for $commit_object (@ARGV) {
651		if (grep($branch{$commit_object}, @branch_list)) {
652		    $repository_matches{$commit_object} = [$branch{$commit_object}, $cvsacl_modules];
653		    print("$$ \$repository_matches{$commit_object} = " .
654                          "[$branch{$commit_object}, $cvsacl_modules].\n") if $debug;
655		}
656	    }
657	}
658    }
659    else {
660	# ----------------------------------- check every argument combination
661	# parse 'cvsacl' modules to array
662	my @module_list = split(/[\s,]+/,$cvsacl_modules);
663        # ------------- Check all modules in list for either file or directory
664	my $fileType = "";
665	if (($fileType = checkFileness(@module_list)) eq "") {
666	    next;                                        # skip bad file types
667	}
668	# ---------- Check each combination of 'cvsacl' modules vs. @ARGV files
669	print("$$ Checking matches for \@module_list: ", join("\, ",@module_list), ".\n") if $debug;
670	# loop thru all command-line commit objects
671        for $commit_object (@ARGV) {
672	    # loop thru all modules on 'cvsacl' line
673            for $cvsacl_module (@module_list) {
674	        print("$$ Is \'cvsacl\': $cvsacl_modules pattern in: \@ARGV " .
675                      "\$commit_object: $commit_object?\n") if $debug;
676		# Do match of beginning of $commit_object
677	        checkModuleMatch($fileType, $commit_object, $cvsacl_module);
678	    } # end for commit objects
679	} # end for cvsacl modules
680    } # end if
681
682    print("$$ Matches for: \%repository_matches: ", join("\, ", (keys %repository_matches)), ".\n") if $debug;
683
684    # ------------------------------------------------------------------------
685    # ----------------------------------------------------- setting exit value
686    # ------------------------------------------------------------------------
687    if ($user_match && %repository_matches) {
688        print("$$ An \"$cvsacl_flag\" match on User(s): $cvsacl_userIds; Module(s):" .
689              " $cvsacl_modules; Branch(es): $cvsacl_branches.\n") if $debug;
690	if ($cvsacl_flag eq "deny") {
691	    # Add all matches to the hash of restricted modules
692	    foreach $commitFile (keys %repository_matches) {
693		print("$$ Adding \%repository_matches entry: $commitFile.\n") if $debug;
694		$restricted_entries{$commitFile} = $repository_matches{$commitFile}[0];
695	    }
696	}
697	else {
698	    # Remove all matches from the restricted modules hash
699	    foreach $commitFile (keys %repository_matches) {
700		print("$$ Removing \%repository_matches entry: $commitFile.\n") if $debug;
701		delete $restricted_entries{$commitFile};
702	    }
703	}
704    }
705    print "$$ ==== End of processing for \'cvsacl\' line: $_.\n" if $debug;
706}
707close(CVSACL);
708
709# ----------------------------------------------------------------------------
710# --------------------------------------- determine final 'commit' disposition
711# ----------------------------------------------------------------------------
712if (%restricted_entries) {                           # any restricted entries?
713    $exit_val = 1;                                              # don't commit
714    print("**** Access denied: Insufficient authority for user: '$user_name\' " .
715          "to commit to \'$repository\'.\n**** Contact CVS Administrators if " .
716          "you require update access to these directories or files.\n");
717    print("**** file(s)/dir(s) restricted were:\n\t", join("\n\t",keys %restricted_entries), "\n");
718    printOptionalRestrictionMessage();
719    write_restrictlog();
720}
721elsif (!$exit_val && $debug) {
722    print "**** Access allowed: Sufficient authority for commit.\n";
723}
724
725print "$$ ==== \$exit_val = $exit_val\n" if $debug;
726exit($exit_val);
727
728# ----------------------------------------------------------------------------
729# -------------------------------------------------------------- end of "main"
730# ----------------------------------------------------------------------------
731
732
733# ----------------------------------------------------------------------------
734# -------------------------------------------------------- process script args
735# ----------------------------------------------------------------------------
736sub processArgs {
737
738# This subroutine is passed a reference to @ARGV.
739
740# If @ARGV contains a "-u" entry, use that as the effective userId.  In this
741# case, the userId is the client-side userId that has been passed to this
742# script by the commit_prep script.  (This is why the commit_prep script must
743# be placed *before* the cvs_acls script in the commitinfo admin file.)
744
745# Otherwise, pull the userId from the server-side environment.
746
747    my $userId = "";
748    my ($argv) = shift;             # pick up ref to @ARGV
749    my @argvClone = ();             # immutable copy for foreach loop
750    for ($i=0; $i<(scalar @{$argv}); $i++) {
751	$argvClone[$i]=$argv->[$i];
752    }
753
754    print("$$ \@_ to processArgs is: @_.\n") if $debug;
755
756    # Parse command line arguments (file list is seen as one arg)
757    foreach $arg (@argvClone) {
758	print("$$ \$arg for processArgs loop is: $arg.\n") if $debug;
759	# Set $debug flag?
760	if ($arg eq '-d') {
761	    shift @ARGV;
762	    $debug = 1;
763	    print("$$ \$debug flag set on.\n") if $debug;
764	    print STDERR "Debug turned on...\n";
765	}
766	# Passing in a client-side userId?
767	elsif ($arg eq '-u') {
768	    shift @ARGV;
769	    $userId = shift @ARGV;
770	    print("$$ client-side \$userId set to: $userId.\n") if $debug;
771	}
772	# An override for the default restrictlog file?
773	elsif ($arg eq '-f') {
774	    shift @ARGV;
775	    $restrictlog = shift @ARGV;
776	}
777	else {
778	    next;
779	}
780    }
781
782    # No client-side userId passed? then get from server env
783    if (!$userId) {
784        $userId = $ENV{"USER"} if !($userId = $ENV{"LOGNAME"});
785	    print("$$ server-side \$userId set to: $userId.\n") if $debug;
786    }
787
788    print("$$ processArgs returning \$userId: $userId.\n") if $debug;
789    return $userId;
790
791}
792
793
794# ----------------------------------------------------------------------------
795# --------------------- Check all modules in list for either file or directory
796# ----------------------------------------------------------------------------
797sub checkFileness {
798
799# Module patterns on the 'cvsacl' record can be files or directories.
800# If it's a directory, we pattern-match the directory name from 'cvsacl'
801# against the left side of the committed filename to see if the file is in
802# that hierarchy.  By contrast, files use an explicit match.  If the entries
803# are neither files nor directories, then the cvsacl file has been set up
804# incorrectly; we return a "" and the caller skips that line as invalid.
805#
806# This function determines whether the entries on the 'cvsacl' record are all
807# directories or all files; it cannot be a mixture.  This restriction put in
808# to simplify the logic (without taking away much functionality).
809
810    my @module_list = @_;
811    print("$$ Checking \"fileness\" or \"dir-ness\" for \@module_list entries.\n") if $debug;
812    print("$$     Entries are: ", join("\, ",@module_list), ".\n") if $debug;
813    my $filetype = "";
814    for $cvsacl_module (@module_list) {
815        my $reposDirName = $cvsroot . '/' . $cvsacl_module;
816        my $reposFileName = $reposDirName . "\,v";
817        print("$$ In checkFileness: \$reposDirName: $reposDirName; \$reposFileName: $reposFileName.\n") if $debug;
818        if (((-d $reposDirName) && ($filetype eq "file")) || ((-f $reposFileName) && ($filetype eq "dir"))) {
819            print("Can\'t mix files and directories on single \'cvsacl\' file record; skipping entry.\n");
820	    print("    Please contact a CVS administrator.\n");
821	    $filetype = "";
822	    last;
823        }
824        elsif (-d $reposDirName) {
825            $filetype = "dir";
826	    print("$$ $reposDirName is a directory.\n") if $debug;
827	}
828        elsif (-f $reposFileName) {
829            $filetype = "file";
830	    print("$$ $reposFileName is a regular file.\n") if $debug;
831	}
832        else {
833            print("***** Item to commit was neither a regular file nor a directory.\n");
834	    print("***** Current \'cvsacl\' line ignored.\n");
835	    print("***** Possible problem with \'cvsacl\' admin file. Please contact a CVS administrator.\n");
836	    $filetype = "";
837	    $text = sprintf("Module entry on cvsacl line: %s is not a valid file or directory.\n", $cvsacl_module);
838	    write_restrictlog_record($text);
839	    last;
840        } # end if
841    } # end for
842
843    print("$$ checkFileness will return \$filetype: $filetype.\n") if $debug;
844    return $filetype;
845}
846
847
848# ----------------------------------------------------------------------------
849# ----------------------------------------------------- check for module match
850# ----------------------------------------------------------------------------
851sub checkModuleMatch {
852
853# This subroutine checks for a match between the directory or file pattern
854# specified in the 'cvsacl' file (i.e., $cvsacl_modules) versus the commit file
855# objects passed into the script via @ARGV (i.e., $commit_object).
856
857# The directory pattern only has to match the beginning portion of the commit
858# file's name for a match since all files under that directory are considered
859# a match. File patterns must exactly match.
860
861# Since (theoretically, if not normally in practice) a working directory can
862# contain a mixture of files from different branches, this routine checks to
863# see if there is also a match on branch before considering the file
864# comparison a match.
865
866    my $match_flag = "";
867
868    print("$$ \@_ in checkModuleMatch is: @_.\n") if $debug;
869    my ($type,$commit_object,$cvsacl_module) = @_;
870
871    if ($type eq "file") {             # Do exact file match of $commit_object
872	if ($commit_object eq $cvsacl_module) {
873	    $match_flag = "file";
874	}                        # Do dir match at beginning of $commit_object
875    }
876    elsif ($commit_object =~ /^$cvsacl_module\//) {
877        $match_flag = "dir";
878    }
879
880    if ($match_flag) {
881	print("$$ \$repository: $repository matches \$commit_object: $commit_object.\n") if $debug;
882	if (!$cvsacl_branches) {             # empty branch pattern matches all
883	    print("$$ blank \'cvsacl\' branch matches all commit files.\n") if $debug;
884	    $repository_matches{$commit_object} = [$branch{$commit_object}, $cvsacl_module];
885	    print("$$ \$repository_matches{$commit_object} = [$branch{$commit_object}, $cvsacl_module].\n") if $debug;
886	}
887	else {                             # otherwise check branch hash table
888	    @branch_list = split (/[\s,]+/,$cvsacl_branches);
889	    print("$$ Branches from \'cvsacl\' record: ", join(", ",@branch_list),".\n") if $debug;
890	    if (grep(/$branch{$commit_object}/, @branch_list)) {
891		$repository_matches{$commit_object} = [$branch{$commit_object}, $cvsacl_module];
892		print("$$ \$repository_matches{$commit_object} = [$branch{$commit_object}, " .
893                      "$cvsacl_module].\n") if $debug;
894	    }
895	}
896    }
897
898}
899
900# ----------------------------------------------------------------------------
901# ------------------------------------------------------- check for file match
902# ----------------------------------------------------------------------------
903sub printOptionalRestrictionMessage {
904
905# This subroutine optionally prints site-specific file restriction information
906# whenever a restriction condition is met.  If the file 'restrict_msg' does
907# not exist, the routine immediately exits.  If there is a 'restrict_msg' file
908# then all the contents are printed at the end of the standard restriction
909# message.
910
911# As seen from examining the definition of $restrictfile, the default filename
912# is: $CVSROOT/CVSROOT/restrict_msg.
913
914    open (RESTRICT, $restrictfile) || return;	# It is ok for cvsacl file not to exist
915    while (<RESTRICT>) {
916	chop;
917	# print out each line
918	print("**** $_\n");
919    }
920
921}
922
923# ----------------------------------------------------------------------------
924# ---------------------------------------------------------- write log message
925# ----------------------------------------------------------------------------
926sub write_restrictlog {
927
928# This subroutine iterates through the list of restricted entries and logs
929# each one to the error logfile.
930
931    # write each line in @text out separately
932    foreach $commitfile (keys %restricted_entries) {
933	$log_text = sprintf "Commit attempt by: %s for: %s on branch: %s",
934	                    $user_name, $commitfile, $branch{$commitfile};
935	write_restrictlog_record($log_text);
936    }
937
938}
939
940# ----------------------------------------------------------------------------
941# ---------------------------------------------------------- write log message
942# ----------------------------------------------------------------------------
943sub write_restrictlog_record {
944
945# This subroutine receives a scalar string and writes it out to the
946# $restrictlog file as a separate line. Each line is prepended with the date
947# and time in the format: "2004/01/30 12:00:00 ".
948
949    $text = shift;
950
951    # return quietly if there is a problem opening the log file.
952    open(FILE, ">>$restrictlog") || return;
953
954    (@time) = localtime();
955
956    # write each line in @text out separately
957    $log_record = sprintf "%04d/%02d/%02d %02d:%02d:%02d %s.\n",
958                      $time[5]+1900, $time[4]+1, $time[3], $time[2], $time[1], $time[0], $text;
959    print FILE $log_record;
960    print("$$ restrict_log record being written: $log_record to $restrictlog.\n") if $debug;
961
962    close(FILE);
963}
964