/contrib/cvs/contrib/cvs_acls.html

https://bitbucket.org/freebsd/freebsd-head/ · HTML · 459 lines · 445 code · 12 blank · 2 comment · 0 complexity · 01250b4aac16784679d3ff4547ff766d MD5 · raw file

  1. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  2. <html xmlns="http://www.w3.org/1999/xhtml">
  3. <head>
  4. <title>cvs_acls</title>
  5. <link rev="made" href="mailto:root@localhost" />
  6. </head>
  7. <body style="background-color: white">
  8. <p><a name="__index__"></a></p>
  9. <!-- INDEX BEGIN -->
  10. <ul>
  11. <li><a href="#name">Name</a></li>
  12. <li><a href="#synopsis">Synopsis</a></li>
  13. <li><a href="#licensing">Licensing</a></li>
  14. <li><a href="#description">Description</a></li>
  15. <li><a href="#enhancements">Enhancements</a></li>
  16. <ul>
  17. <li><a href="#fixed_bugs">Fixed Bugs</a></li>
  18. <li><a href="#enhancements">Enhancements</a></li>
  19. <li><a href="#todos">ToDoS</a></li>
  20. </ul>
  21. <li><a href="#version_information">Version Information</a></li>
  22. <li><a href="#installation">Installation</a></li>
  23. <li><a href="#format_of_the_cvsacl_file">Format of the cvsacl file</a></li>
  24. <li><a href="#program_logic">Program Logic</a></li>
  25. <ul>
  26. <li><a href="#pseudocode">Pseudocode</a></li>
  27. <li><a href="#sanity_check">Sanity Check</a></li>
  28. </ul>
  29. </ul>
  30. <!-- INDEX END -->
  31. <hr />
  32. <p>
  33. </p>
  34. <h1><a name="name">Name</a></h1>
  35. <p>cvs_acls - Access Control List for CVS</p>
  36. <p>
  37. </p>
  38. <hr />
  39. <h1><a name="synopsis">Synopsis</a></h1>
  40. <p>In 'commitinfo':</p>
  41. <pre>
  42. repository/path/to/restrict $CVSROOT/CVSROOT/cvs_acls [-d][-u $USER][-f &lt;logfile&gt;]</pre>
  43. <p>where:</p>
  44. <pre>
  45. -d turns on debug information
  46. -u passes the client-side userId to the cvs_acls script
  47. -f specifies an alternate filename for the restrict_log file</pre>
  48. <p>In 'cvsacl':</p>
  49. <pre>
  50. {allow.*,deny.*} [|user,user,... [|repos,repos,... [|branch,branch,...]]]</pre>
  51. <p>where:</p>
  52. <pre>
  53. allow|deny - allow: commits are allowed; deny: prohibited
  54. user - userId to be allowed or restricted
  55. repos - file or directory to be allowed or restricted
  56. branch - branch to be allowed or restricted</pre>
  57. <p>See below for examples.</p>
  58. <p>
  59. </p>
  60. <hr />
  61. <h1><a name="licensing">Licensing</a></h1>
  62. <p>cvs_acls - provides access control list functionality for CVS
  63. </p>
  64. <pre>
  65. Copyright (c) 2004 by Peter Connolly &lt;peter.connolly@cnet.com&gt;
  66. All rights reserved.</pre>
  67. <p>This program is free software; you can redistribute it and/or modify
  68. it under the terms of the GNU General Public License as published by
  69. the Free Software Foundation; either version 2 of the License, or
  70. (at your option) any later version.</p>
  71. <p>This program is distributed in the hope that it will be useful,
  72. but WITHOUT ANY WARRANTY; without even the implied warranty of
  73. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  74. GNU General Public License for more details.</p>
  75. <p>You should have received a copy of the GNU General Public License
  76. along with this program; if not, write to the Free Software
  77. Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA</p>
  78. <p>
  79. </p>
  80. <hr />
  81. <h1><a name="description">Description</a></h1>
  82. <p>This script--cvs_acls--is invoked once for each directory within a
  83. ``cvs commit''. The set of files being committed for that directory as
  84. well as the directory itself, are passed to this script. This script
  85. checks its 'cvsacl' file to see if any of the files being committed
  86. are on the 'cvsacl' file's restricted list. If any of the files are
  87. restricted, then the cvs_acls script passes back an exit code of 1
  88. which disallows the commits for that directory.</p>
  89. <p>Messages are returned to the committer indicating the <a href="#item_file"><code>file(s)</code></a> that
  90. he/she are not allowed to committ. Additionally, a site-specific
  91. set of messages (e.g., contact information) can be included in these
  92. messages.</p>
  93. <p>When a commit is prohibited, log messages are written to a restrict_log
  94. file in $CVSROOT/CVSROOT. This default file can be redirected to
  95. another destination.</p>
  96. <p>The script is triggered from the 'commitinfo' file in $CVSROOT/CVSROOT/.</p>
  97. <p>
  98. </p>
  99. <hr />
  100. <h1><a name="enhancements">Enhancements</a></h1>
  101. <p>This section lists the bug fixes and enhancements added to cvs_acls
  102. that make up the current cvs_acls.</p>
  103. <p>
  104. </p>
  105. <h2><a name="fixed_bugs">Fixed Bugs</a></h2>
  106. <p>This version attempts to get rid the following bugs from the
  107. original version of cvs_acls:</p>
  108. <ul>
  109. <li><strong><a name="item_files">Multiple entries on an 'cvsacl' line will be matched individually,
  110. instead of requiring that all commit files *exactly* match all
  111. 'cvsacl' entries. Commiting a file not in the 'cvsacl' list would
  112. allow *all* files (including a restricted file) to be committed.</a></strong><br />
  113. </li>
  114. [IMO, this basically made the original script unuseable for our
  115. situation since any arbitrary combination of committed files could
  116. avoid matching the 'cvsacl's entries.]
  117. <p></p>
  118. <li><strong><a name="item_handle_specific_filename_restrictions_2e_cvs_acls_">Handle specific filename restrictions. cvs_acls didn't restrict
  119. individual files specified in 'cvsacl'.</a></strong><br />
  120. </li>
  121. <li><strong><a name="item_correctly_handle_multiple_2c_specific_filename_res">Correctly handle multiple, specific filename restrictions</a></strong><br />
  122. </li>
  123. <li><strong><a name="item_prohibit_mix_of_dirs_and_files_on_a_single__27cvsa">Prohibit mix of dirs and files on a single 'cvsacl' line
  124. [To simplify the logic and because this would be normal usage.]</a></strong><br />
  125. </li>
  126. <li><strong><a name="item_correctly_handle_a_mixture_of_branch_restrictions_">Correctly handle a mixture of branch restrictions within one work
  127. directory</a></strong><br />
  128. </li>
  129. <li><strong><a name="item__24cvsroot_existence_is_checked_too_late">$CVSROOT existence is checked too late</a></strong><br />
  130. </li>
  131. <li><strong><a name="item_option">Correctly handle the CVSROOT=:local:/... option (useful for
  132. interactive testing)</a></strong><br />
  133. </li>
  134. <li><strong><a name="item_logic">Replacing shoddy ``$universal_off'' logic
  135. (Thanks to Karl-Konig Konigsson for pointing this out.)</a></strong><br />
  136. </li>
  137. </ul>
  138. <p>
  139. </p>
  140. <h2><a name="enhancements">Enhancements</a></h2>
  141. <ul>
  142. <li><strong><a name="item_checks_modules_in_the__27cvsacl_27_file_for_valid_">Checks modules in the 'cvsacl' file for valid files and directories</a></strong><br />
  143. </li>
  144. <li><strong><a name="item_accurately_report_restricted_entries_and_their_mat">Accurately report restricted entries and their matching patterns</a></strong><br />
  145. </li>
  146. <li><strong><a name="item_simplified_and_commented_overly_complex_perl_regex">Simplified and commented overly complex PERL REGEXPs for readability
  147. and maintainability</a></strong><br />
  148. </li>
  149. <li><strong><a name="item_skip_the_rest_of_processing_if_a_mismatch_on_porti">Skip the rest of processing if a mismatch on portion of the 'cvsacl' line</a></strong><br />
  150. </li>
  151. <li><strong><a name="item_file">Get rid of opaque ``karma'' messages in favor of user-friendly messages
  152. that describe which user, <code>file(s)</code> and <code>branch(es)</code> were disallowed.</a></strong><br />
  153. </li>
  154. <li><strong><a name="item_add_optional__27restrict_msg_27_file_for_additiona">Add optional 'restrict_msg' file for additional, site-specific
  155. restriction messages.</a></strong><br />
  156. </li>
  157. <li><strong><a name="item_userid">Take a ``-u'' parameter for $USER from commit_prep so that the script
  158. can do restrictions based on the client-side userId rather than the
  159. server-side userId (usually 'cvs').</a></strong><br />
  160. </li>
  161. (See discussion below on ``Admin Setup'' for more on this point.)
  162. <p></p>
  163. <li><strong><a name="item_added_a_lot_more_debug_trace">Added a lot more debug trace</a></strong><br />
  164. </li>
  165. <li><strong><a name="item_tested_these_restrictions_with_concurrent_use_of_p">Tested these restrictions with concurrent use of pserver and SSH
  166. access to model our transition from pserver to ext access.</a></strong><br />
  167. </li>
  168. <li><strong><a name="item_added_logging_of_restricted_commit_attempts_2e_res">Added logging of restricted commit attempts.
  169. Restricted commits can be sent to a default file:
  170. $CVSROOT/CVSROOT/restrictlog or to one passed to the script
  171. via the -f command parameter.</a></strong><br />
  172. </li>
  173. </ul>
  174. <p>
  175. </p>
  176. <h2><a name="todos">ToDoS</a></h2>
  177. <ul>
  178. <li><strong><a name="item_need_to_deal_with_pserver_2fssh_transition_with_co">Need to deal with pserver/SSH transition with conflicting umasks?</a></strong><br />
  179. </li>
  180. <li><strong><a name="item_use_a_cpan_module_to_handle_command_parameters_2e">Use a CPAN module to handle command parameters.</a></strong><br />
  181. </li>
  182. <li><strong><a name="item_use_a_cpan_module_to_clone_data_structures_2e">Use a CPAN module to clone data structures.</a></strong><br />
  183. </li>
  184. </ul>
  185. <p>
  186. </p>
  187. <hr />
  188. <h1><a name="version_information">Version Information</a></h1>
  189. <p>This is not offered as a fix to the original 'cvs_acls' script since it
  190. differs substantially in goals and methods from the original and there
  191. are probably a significant number of people out there that still require
  192. the original version's functionality.</p>
  193. <p>The 'cvsacl' file flags of 'allow' and 'deny' were intentionally
  194. changed to 'allow' and 'deny' because there are enough differences
  195. between the original script's behavior and this one's that we wanted to
  196. make sure that users will rethink their 'cvsacl' file formats before
  197. plugging in this newer script.</p>
  198. <p>Please note that there has been very limited cross-platform testing of
  199. this script!!! (We did not have the time or resources to do exhaustive
  200. cross-platform testing.)</p>
  201. <p>It was developed and tested under Red Hat Linux 9.0 using PERL 5.8.0.
  202. Additionally, it was built and tested under Red Hat Linux 7.3 using
  203. PERL 5.6.1.</p>
  204. <p>$Id: cvs_acls.html,v 1.1.2.2 2005/09/01 13:44:49 dprice Exp $</p>
  205. <p>This version is based on the 1.11.13 version of cvs_acls
  206. <a href="mailto:peter.connolly@cnet.com">peter.connolly@cnet.com</a> (Peter Connolly)</p>
  207. <pre>
  208. Access control lists for CVS. dgg@ksr.com (David G. Grubbs)
  209. Branch specific controls added by voisine@bytemobile.com (Aaron Voisine)</pre>
  210. <p>
  211. </p>
  212. <hr />
  213. <h1><a name="installation">Installation</a></h1>
  214. <p>To use this program, do the following four things:</p>
  215. <p>0. Install PERL, version 5.6.1 or 5.8.0.</p>
  216. <p>1. Admin Setup:</p>
  217. <pre>
  218. There are two choices here.</pre>
  219. <pre>
  220. a) The first option is to use the $ENV{&quot;USER&quot;}, server-side userId
  221. (from the third column of your pserver 'passwd' file) as the basis for
  222. your restrictions. In this case, you will (at a minimum) want to set
  223. up a new &quot;cvsadmin&quot; userId and group on the pserver machine.
  224. CVS administrators will then set up their 'passwd' file entries to
  225. run either as &quot;cvs&quot; (for regular users) or as &quot;cvsadmin&quot; (for power
  226. users). Correspondingly, your 'cvsacl' file will only list 'cvs'
  227. and 'cvsadmin' as the userIds in the second column.</pre>
  228. <pre>
  229. Commentary: A potential weakness of this is that the xinetd
  230. cvspserver process will need to run as 'root' in order to switch
  231. between the 'cvs' and the 'cvsadmin' userIds. Some sysadmins don't
  232. like situations like this and may want to chroot the process.
  233. Talk to them about this point...</pre>
  234. <pre>
  235. b) The second option is to use the client-side userId as the basis for
  236. your restrictions. In this case, all the xinetd cvspserver processes
  237. can run as userId 'cvs' and no 'root' userId is required. If you have
  238. a 'passwd' file that lists 'cvs' as the effective run-time userId for
  239. all your users, then no changes to this file are needed. Your 'cvsacl'
  240. file will use the individual, client-side userIds in its 2nd column.</pre>
  241. <pre>
  242. As long as the userIds in pserver's 'passwd' file match those userIds
  243. that your Linux server know about, this approach is ideal if you are
  244. planning to move from pserver to SSH access at some later point in time.
  245. Just by switching the CVSROOT var from CVSROOT=:pserver:&lt;userId&gt;... to
  246. CVSROOT=:ext:&lt;userId&gt;..., users can switch over to SSH access without
  247. any other administrative changes. When all users have switched over to
  248. SSH, the inherently insecure xinetd cvspserver process can be disabled.
  249. [<a href="http://ximbiot.com/cvs/manual/cvs-1.11.17/cvs_2.html#SEC32">http://ximbiot.com/cvs/manual/cvs-1.11.17/cvs_2.html#SEC32</a>]</pre>
  250. <pre>
  251. :TODO: The only potential glitch with the SSH approach is the possibility
  252. that each user can have differing umasks that might interfere with one
  253. another, especially during a transition from pserver to SSH. As noted
  254. in the ToDo section, this needs a good strategy and set of tests for that
  255. yet...</pre>
  256. <p>2. Put two lines, as the *only* non-comment lines, in your commitinfo file:</p>
  257. <pre>
  258. ALL $CVSROOT/CVSROOT/commit_prep
  259. ALL $CVSROOT/CVSROOT/cvs_acls [-d][-u $USER ][-f &lt;logfilename&gt;]</pre>
  260. <pre>
  261. where &quot;-d&quot; turns on debug trace
  262. &quot;-u $USER&quot; passes the client-side userId to cvs_acls
  263. &quot;-f &lt;logfilename&quot;&gt; overrides the default filename used to log
  264. restricted commit attempts.</pre>
  265. <pre>
  266. (These are handled in the processArgs() subroutine.)</pre>
  267. <p>If you are using client-side userIds to restrict access to your
  268. repository, make sure that they are in this order since the commit_prep
  269. script is required in order to pass the $USER parameter.</p>
  270. <p>A final note about the repository matching pattern. The example above
  271. uses ``ALL'' but note that this means that the cvs_acls script will run
  272. for each and every commit in your repository. Obviously, in a large
  273. repository this adds up to a lot of overhead that may not be necesary.
  274. A better strategy is to use a repository pattern that is more specific
  275. to the areas that you wish to secure.</p>
  276. <p>3. Install this file as $CVSROOT/CVSROOT/cvs_acls and make it executable.</p>
  277. <p>4. Create a file named CVSROOT/cvsacl and optionally add it to
  278. CVSROOT/checkoutlist and check it in. See the CVS manual's
  279. administrative files section about checkoutlist. Typically:</p>
  280. <pre>
  281. $ cvs checkout CVSROOT
  282. $ cd CVSROOT
  283. [ create the cvsacl file, include 'commitinfo' line ]
  284. [ add cvsacl to checkoutlist ]
  285. $ cvs add cvsacl
  286. $ cvs commit -m 'Added cvsacl for use with cvs_acls.' cvsacl checkoutlist</pre>
  287. <p>Note: The format of the 'cvsacl' file is described in detail immediately
  288. below but here is an important set up point:</p>
  289. <pre>
  290. Make sure to include a line like the following:</pre>
  291. <pre>
  292. deny||CVSROOT/commitinfo CVSROOT/cvsacl
  293. allow|cvsadmin|CVSROOT/commitinfo CVSROOT/cvsacl</pre>
  294. <pre>
  295. that restricts access to commitinfo and cvsacl since this would be one of
  296. the easiest &quot;end runs&quot; around this ACL approach. ('commitinfo' has the
  297. line that executes the cvs_acls script and, of course, all the
  298. restrictions are in 'cvsacl'.)</pre>
  299. <p>5. (Optional) Create a 'restrict_msg' file in the $CVSROOT/CVSROOT directory.
  300. Whenever there is a restricted file or dir message, cvs_acls will look
  301. for this file and, if it exists, print its contents as part of the
  302. commit-denial message. This gives you a chance to print any site-specific
  303. information (e.g., who to call, what procedures to look up,...) whenever
  304. a commit is denied.</p>
  305. <p>
  306. </p>
  307. <hr />
  308. <h1><a name="format_of_the_cvsacl_file">Format of the cvsacl file</a></h1>
  309. <p>The 'cvsacl' file determines whether you may commit files. It contains lines
  310. read from top to bottom, keeping track of whether a given user, repository
  311. and branch combination is ``allowed'' or ``denied.'' The script will assume
  312. ``allowed'' on all repository paths until 'allow' and 'deny' rules change
  313. that default.</p>
  314. <p>The normal pattern is to specify an 'deny' rule to turn off
  315. access to ALL users, then follow it with a matching 'allow' rule that will
  316. turn on access for a select set of users. In the case of multiple rules for
  317. the same user, repository and branch, the last one takes precedence.</p>
  318. <p>Blank lines and lines with only comments are ignored. Any other lines not
  319. beginning with ``allow'' or ``deny'' are logged to the restrict_log file.</p>
  320. <p>Lines beginning with ``allow'' or ``deny'' are assumed to be '|'-separated
  321. triples: (All spaces and tabs are ignored in a line.)</p>
  322. <pre>
  323. {allow.*,deny.*} [|user,user,... [|repos,repos,... [|branch,branch,...]]]</pre>
  324. <pre>
  325. 1. String starting with &quot;allow&quot; or &quot;deny&quot;.
  326. 2. Optional, comma-separated list of usernames.
  327. 3. Optional, comma-separated list of repository pathnames.
  328. These are pathnames relative to $CVSROOT. They can be directories or
  329. filenames. A directory name allows or restricts access to all files and
  330. directories below it. One line can have either directories or filenames
  331. but not both.
  332. 4. Optional, comma-separated list of branch tags.
  333. If not specified, all branches are assumed. Use HEAD to reference the
  334. main branch.</pre>
  335. <p>Example: (Note: No in-line comments.)</p>
  336. <pre>
  337. # ----- Make whole repository unavailable.
  338. deny</pre>
  339. <pre>
  340. # ----- Except for user &quot;dgg&quot;.
  341. allow|dgg</pre>
  342. <pre>
  343. # ----- Except when &quot;fred&quot; or &quot;john&quot; commit to the
  344. # module whose repository is &quot;bin/ls&quot;
  345. allow|fred, john|bin/ls</pre>
  346. <pre>
  347. # ----- Except when &quot;ed&quot; commits to the &quot;stable&quot;
  348. # branch of the &quot;bin/ls&quot; repository
  349. allow|ed|/bin/ls|stable</pre>
  350. <p>
  351. </p>
  352. <hr />
  353. <h1><a name="program_logic">Program Logic</a></h1>
  354. <p>CVS passes to @ARGV an absolute directory pathname (the repository
  355. appended to your $CVSROOT variable), followed by a list of filenames
  356. within that directory that are to be committed.</p>
  357. <p>The script walks through the 'cvsacl' file looking for matches on
  358. the username, repository and branch.</p>
  359. <p>A username match is simply the user's name appearing in the second
  360. column of the cvsacl line in a space-or-comma separate list. If
  361. blank, then any user will match.</p>
  362. <p>A repository match:</p>
  363. <ul>
  364. <li><strong><a name="item_each_entry_in_the_modules_section_of_the_current__">Each entry in the modules section of the current 'cvsacl' line is
  365. examined to see if it is a dir or a file. The line must have
  366. either files or dirs, but not both. (To simplify the logic.)</a></strong><br />
  367. </li>
  368. <li><strong><a name="item_if_neither_2c_then_assume_the__27cvsacl_27_file_wa">If neither, then assume the 'cvsacl' file was set up in error and
  369. skip that 'allow' line.</a></strong><br />
  370. </li>
  371. <li><strong><a name="item_if_a_dir_2c_then_each_dir_pattern_is_matched_separ">If a dir, then each dir pattern is matched separately against the
  372. beginning of each of the committed files in @ARGV.</a></strong><br />
  373. </li>
  374. <li><strong><a name="item_if_a_file_2c_then_each_file_pattern_is_matched_exa">If a file, then each file pattern is matched exactly against each
  375. of the files to be committed in @ARGV.</a></strong><br />
  376. </li>
  377. <li><strong><a name="item_repository_and_branch_must_both_match_together_2e_">Repository and branch must BOTH match together. This is to cover
  378. the use case where a user has multiple branches checked out in
  379. a single work directory. Commit files can be from different
  380. branches.</a></strong><br />
  381. </li>
  382. A branch match is either:
  383. <ul>
  384. <li><strong><a name="item_when_no_branches_are_listed_in_the_fourth_column_2">When no branches are listed in the fourth column. (``Match any.'')</a></strong><br />
  385. </li>
  386. <li><strong><a name="item_all_elements_from_the_fourth_column_are_matched_ag">All elements from the fourth column are matched against each of
  387. the tag names for $ARGV[1..$#ARGV] found in the %branches file.</a></strong><br />
  388. </li>
  389. </ul>
  390. <li><strong><a name="item__27allow_27_match_remove_that_match_from_the_tally">'allow' match remove that match from the tally map.</a></strong><br />
  391. </li>
  392. <li><strong><a name="item_restricted">Restricted ('deny') matches are saved in the %repository_matches
  393. table.</a></strong><br />
  394. </li>
  395. <li><strong><a name="item_if_there_is_a_match_on_user_2c_repository_and_bran">If there is a match on user, repository and branch:</a></strong><br />
  396. </li>
  397. <pre>
  398. If repository, branch and user match
  399. if 'deny'
  400. add %repository_matches entries to %restricted_entries
  401. else if 'allow'
  402. remove %repository_matches entries from %restricted_entries</pre>
  403. <li><strong><a name="item_at_the_end_of_all_the__27cvsacl_27_line_checks_2c_">At the end of all the 'cvsacl' line checks, check to see if there
  404. are any entries in the %restricted_entries. If so, then deny the
  405. commit.</a></strong><br />
  406. </li>
  407. </ul>
  408. <p>
  409. </p>
  410. <h2><a name="pseudocode">Pseudocode</a></h2>
  411. <pre>
  412. read CVS/Entries file and create branch{file}-&gt;{branch} hash table
  413. + for each 'allow' and 'deny' line in the 'cvsacl' file:
  414. | user match?
  415. | - Yes: set $user_match = 1;
  416. | repository and branch match?
  417. | - Yes: add to %repository_matches;
  418. | did user, repository match?
  419. | - Yes: if 'deny' then
  420. | add %repository_matches -&gt; %restricted_entries
  421. | if 'allow' then
  422. | remove %repository_matches &lt;- %restricted_entries
  423. + end for loop
  424. any saved restrictions?
  425. no: exit,
  426. set exit code allowing commits and exit
  427. yes: report restrictions,
  428. set exit code prohibiting commits and exit</pre>
  429. <p>
  430. </p>
  431. <h2><a name="sanity_check">Sanity Check</a></h2>
  432. <pre>
  433. 1) file allow trumps a dir deny
  434. deny||java/lib
  435. allow||java/lib/README
  436. 2) dir allow can undo a file deny
  437. deny||java/lib/README
  438. allow||java/lib
  439. 3) file deny trumps a dir allow
  440. allow||java/lib
  441. deny||java/lib/README
  442. 4) dir deny trumps a file allow
  443. allow||java/lib/README
  444. deny||java/lib
  445. ... so last match always takes precedence</pre>
  446. </body>
  447. </html>