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