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