/contrib/cvs/contrib/cvs_acls.in

https://bitbucket.org/freebsd/freebsd-head/ · Autoconf · 963 lines · 609 code · 200 blank · 154 comment · 143 complexity · 0e4a373ebb40e8191f679394fb7c4d9c MD5 · raw file

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