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