finally, bump the version
[alioth/cvs.git] / contrib / cvs_acls.in
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
36 cvs_acls - Access Control List for CVS
37
38 =head1 Synopsis
39
40 In 'commitinfo':
41
42   repository/path/to/restrict $CVSROOT/CVSROOT/cvs_acls [-d][-u $USER][-f <logfile>]
43
44 where:
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
50 In 'cvsacl':
51
52   {allow.*,deny.*} [|user,user,... [|repos,repos,... [|branch,branch,...]]]
53
54 where:
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
61 See below for examples.
62
63 =head1 Licensing
64
65 cvs_acls - provides access control list functionality for CVS
66   
67 Copyright (c) 2004 by Peter Connolly <peter.connolly@cnet.com>  
68 All rights reserved.
69
70 This program is free software; you can redistribute it and/or modify  
71 it under the terms of the GNU General Public License as published by  
72 the Free Software Foundation; either version 2 of the License, or  
73 (at your option) any later version. 
74
75 This program is distributed in the hope that it will be useful,  
76 but WITHOUT ANY WARRANTY; without even the implied warranty of  
77 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the  
78 GNU General Public License for more details.  
79
80 You should have received a copy of the GNU General Public License  
81 along with this program; if not, write to the Free Software  
82 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
83
84 =head1 Description
85
86 This script--cvs_acls--is invoked once for each directory within a 
87 "cvs commit". The set of files being committed for that directory as 
88 well as the directory itself, are passed to this script.  This script 
89 checks its 'cvsacl' file to see if any of the files being committed 
90 are on the 'cvsacl' file's restricted list.  If any of the files are
91 restricted, then the cvs_acls script passes back an exit code of 1
92 which disallows the commits for that directory.  
93
94 Messages are returned to the committer indicating the file(s) that 
95 he/she are not allowed to committ.  Additionally, a site-specific 
96 set of messages (e.g., contact information) can be included in these 
97 messages.
98
99 When a commit is prohibited, log messages are written to a restrict_log
100 file in $CVSROOT/CVSROOT.  This default file can be redirected to 
101 another destination.
102
103 The script is triggered from the 'commitinfo' file in $CVSROOT/CVSROOT/.
104
105 =head1 Enhancements
106
107 This section lists the bug fixes and enhancements added to cvs_acls
108 that make up the current cvs_acls.
109
110 =head2 Fixed Bugs
111
112 This version attempts to get rid the following bugs from the
113 original version of cvs_acls:
114
115 =over 2
116
117 =item *
118 Multiple entries on an 'cvsacl' line will be matched individually, 
119 instead of requiring that all commit files *exactly* match all 
120 'cvsacl' entries. Commiting a file not in the 'cvsacl' list would
121 allow *all* files (including a restricted file) to be committed.
122
123 [IMO, this basically made the original script unuseable for our 
124 situation since any arbitrary combination of committed files could 
125 avoid matching the 'cvsacl's entries.]
126
127 =item *
128 Handle specific filename restrictions. cvs_acls didn't restrict
129 individual files specified in 'cvsacl'.
130
131 =item *
132 Correctly handle multiple, specific filename restrictions
133
134 =item *
135 Prohibit 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 *
139 Correctly handle a mixture of branch restrictions within one work
140 directory
141
142 =item *
143 $CVSROOT existence is checked too late
144
145 =item *
146 Correctly handle the CVSROOT=:local:/... option (useful for 
147 interactive testing)
148
149 =item *
150 Replacing 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 *
160 Checks modules in the 'cvsacl' file for valid files and directories
161
162 =item *
163 Accurately report restricted entries and their matching patterns
164
165 =item *
166 Simplified and commented overly complex PERL REGEXPs for readability 
167 and maintainability
168
169 =item *
170 Skip the rest of processing if a mismatch on portion of the 'cvsacl' line
171
172 =item *
173 Get rid of opaque "karma" messages in favor of user-friendly messages
174 that describe which user, file(s) and branch(es) were disallowed.
175
176 =item *
177 Add optional 'restrict_msg' file for additional, site-specific 
178 restriction messages.
179
180 =item *
181 Take a "-u" parameter for $USER from commit_prep so that the script
182 can do restrictions based on the client-side userId rather than the
183 server-side userId (usually 'cvs').
184
185 (See discussion below on "Admin Setup" for more on this point.)
186
187 =item *
188 Added a lot more debug trace 
189
190 =item *
191 Tested these restrictions with concurrent use of pserver and SSH
192 access to model our transition from pserver to ext access.
193
194 =item *
195 Added logging of restricted commit attempts.
196 Restricted commits can be sent to a default file:
197 $CVSROOT/CVSROOT/restrictlog or to one passed to the script
198 via the -f command parameter.
199
200 =back
201
202 =head2 ToDoS 
203
204 =over 2
205
206 =item *
207 Need to deal with pserver/SSH transition with conflicting umasks?
208
209 =item *
210 Use a CPAN module to handle command parameters.
211
212 =item *
213 Use a CPAN module to clone data structures.
214
215 =back
216
217 =head1 Version Information
218
219 This is not offered as a fix to the original 'cvs_acls' script since it 
220 differs substantially in goals and methods from the original and there 
221 are probably a significant number of people out there that still require 
222 the original version's functionality.
223
224 The 'cvsacl' file flags of 'allow' and 'deny' were intentionally 
225 changed to 'allow' and 'deny' because there are enough differences 
226 between the original script's behavior and this one's that we wanted to
227 make sure that users will rethink their 'cvsacl' file formats before
228 plugging in this newer script.
229
230 Please note that there has been very limited cross-platform testing of 
231 this script!!! (We did not have the time or resources to do exhaustive
232 cross-platform testing.)
233
234 It was developed and tested under Red Hat Linux 9.0 using PERL 5.8.0.
235 Additionally, it was built and tested under Red Hat Linux 7.3 using 
236 PERL 5.6.1.
237
238 $Id: cvs_acls.in,v 1.11 2005/09/01 13:48:57 dprice Exp $
239
240 This version is based on the 1.11.13 version of cvs_acls
241 peter.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
248 To use this program, do the following four things:
249
250 0. Install PERL, version 5.6.1 or 5.8.0.
251
252 1. 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
293 2. 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
305 If you are using client-side userIds to restrict access to your 
306 repository, make sure that they are in this order since the commit_prep 
307 script is required in order to pass the $USER parameter.
308
309 A final note about the repository matching pattern.  The example above
310 uses "ALL" but note that this means that the cvs_acls script will run
311 for each and every commit in your repository.  Obviously, in a large
312 repository this adds up to a lot of overhead that may not be necesary. 
313 A better strategy is to use a repository pattern that is more specific 
314 to the areas that you wish to secure.
315
316 3. Install this file as $CVSROOT/CVSROOT/cvs_acls and make it executable.
317
318 4. 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
329 Note: The format of the 'cvsacl' file is described in detail immediately 
330 below 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
342 5. (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
351 The 'cvsacl' file determines whether you may commit files.  It contains lines
352 read from top to bottom, keeping track of whether a given user, repository
353 and branch combination is "allowed" or "denied."  The script will assume 
354 "allowed" on all repository paths until 'allow' and 'deny' rules change 
355 that default.  
356
357 The normal pattern is to specify an 'deny' rule to turn off
358 access to ALL users, then follow it with a matching 'allow' rule that will 
359 turn on access for a select set of users.  In the case of multiple rules for
360 the same user, repository and branch, the last one takes precedence.
361
362 Blank lines and lines with only comments are ignored.  Any other lines not 
363 beginning with "allow" or "deny" are logged to the restrict_log file.
364
365 Lines beginning with "allow" or "deny" are assumed to be '|'-separated
366 triples: (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
381 Example:  (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
399 CVS passes to @ARGV an absolute directory pathname (the repository
400 appended to your $CVSROOT variable), followed by a list of filenames
401 within that directory that are to be committed.
402
403 The script walks through the 'cvsacl' file looking for matches on 
404 the username, repository and branch.
405
406 A username match is simply the user's name appearing in the second
407 column of the cvsacl line in a space-or-comma separate list. If
408 blank, then any user will match.
409
410 A repository match:
411
412 =over 2
413
414 =item *
415 Each entry in the modules section of the current 'cvsacl' line is 
416 examined to see if it is a dir or a file. The line must have 
417 either files or dirs, but not both. (To simplify the logic.)
418
419 =item *
420 If neither, then assume the 'cvsacl' file was set up in error and
421 skip that 'allow' line.
422
423 =item *
424 If a dir, then each dir pattern is matched separately against the 
425 beginning of each of the committed files in @ARGV. 
426
427 =item *
428 If a file, then each file pattern is matched exactly against each
429 of the files to be committed in @ARGV.
430
431 =item *
432 Repository and branch must BOTH match together. This is to cover
433 the use case where a user has multiple branches checked out in
434 a single work directory. Commit files can be from different
435 branches.
436
437 A branch match is either:
438
439 =over 4
440
441 =item *
442 When no branches are listed in the fourth column. ("Match any.")
443
444 =item *
445 All elements from the fourth column are matched against each of 
446 the 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 *
454 Restricted ('deny') matches are saved in the %repository_matches 
455 table.
456
457 =item *
458 If 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 *
467 At the end of all the 'cvsacl' line checks, check to see if there
468 are any entries in the %restricted_entries.  If so, then deny the
469 commit.
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'};
528 die "Must set CVSROOT\n" if !$cvsroot;
529 if ($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
542 print("$$ \@ARGV after processArgs is: @ARGV.\n") if $debug;
543 print("$$ ========== Begin $PROGRAM_NAME for \"$ARGV[0]\" repository. ========== \n") if $debug;
544
545 # --------------------------------------------------------------- filter @ARGV
546 eval "print STDERR \$die='Unknown parameter $1\n' if !defined \$$1; \$$1=\$';"
547     while ($ARGV[0] =~ /^(\w+)=/ && shift(@ARGV));
548 exit 255 if $die;                        # process any variable=value switches
549
550 print("$$ \@ARGV after shift processing contains:",join("\, ",@ARGV),".\n") if $debug;
551
552 # ---------------------------------------------------------------- get cvsroot
553 ($repository = shift) =~ s:^$cvsroot/::;
554 grep($_ = $repository . '/' . $_, @ARGV);
555
556 print("$$ \$cvsroot is: $cvsroot.\n") if $debug;
557 print "$$ 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
574 open(ENTRIES, $entries) || die("Cannot open $entries.\n");
575 print("$$ File / Branch\n") if $debug;
576 my $i = 0;
577 while(<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 }
598 close(ENTRIES);
599
600 # ----------------------------------------------------------------------------
601 # ------------------------------------- evaluate each active line from 'cvsacl'
602 # ----------------------------------------------------------------------------
603 open (CVSACL, $cvsaclfile) || exit(0);  # It is ok for cvsacl file not to exist
604 while (<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 }
707 close(CVSACL);
708
709 # ----------------------------------------------------------------------------
710 # --------------------------------------- determine final 'commit' disposition
711 # ---------------------------------------------------------------------------- 
712 if (%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 }
721 elsif (!$exit_val && $debug) {
722     print "**** Access allowed: Sufficient authority for commit.\n";
723 }
724
725 print "$$ ==== \$exit_val = $exit_val\n" if $debug;
726 exit($exit_val);
727
728 # ----------------------------------------------------------------------------
729 # -------------------------------------------------------------- end of "main"
730 # ----------------------------------------------------------------------------
731
732
733 # ----------------------------------------------------------------------------
734 # -------------------------------------------------------- process script args
735 # ----------------------------------------------------------------------------
736 sub 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 # ----------------------------------------------------------------------------
797 sub 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 # ----------------------------------------------------------------------------
851 sub 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 # ----------------------------------------------------------------------------
903 sub 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 # ----------------------------------------------------------------------------
926 sub 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 # ----------------------------------------------------------------------------
943 sub 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 }