1#!/usr/bin/perl 2 3use strict; 4use File::Spec; 5 6$ENV{PATH} ='/opt/git/bin'; 7my$acl_git='/vcs/acls.git'; 8my$acl_branch='refs/heads/master'; 9my$debug=0; 10 11=doc 12Invoked as: update refname old-sha1 new-sha1 13 14This script is run by git-receive-pack once foreach ref that the 15client is trying to modify. If we exit with a non-zero exit value 16then the update for that particular refis denied, but updates for 17other refs in the same run of receive-pack may still be allowed. 18 19We are run after the objects have been uploaded, but before the 20refis actually modified. We take advantage of that fact when we 21look for"new" commits and tags (the new objects won't show up in 22`rev-list --all`). 23 24This script loads and parses the content of the config file 25"users/$this_user.acl" from the$acl_branchcommit of$acl_gitODB. 26The acl file is a git-config style file, but uses a slightly more 27restricted syntax as the Perl parser contained within this script 28is not nearly as permissive as git-config. 29 30Example: 31 32 [user] 33 committer = John Doe <john.doe@example.com> 34 committer = John R. Doe <john.doe@example.com> 35 36 [repository "acls"] 37 allow = heads/master 38 allow = CDUR for heads/jd/ 39 allow = C for ^tags/v\\d+$ 40 41For all new commit or tag objects the committer (or tagger) line 42within the object must exactly match one of the user.committer 43values listed in the acl file ("HEAD:users/$this_user.acl"). 44 45For a branch to be modified an allow line within the matching 46repository section must be matched for both the refname and the 47opcode. 48 49Repository sections are matched on the basename of the repository 50(after removing the .git suffix). 51 52The opcode abbrevations are: 53 54 C: create new ref 55 D: delete existing ref 56 U: fast-forward existing ref (no commit loss) 57 R: rewind/rebase existing ref (commit loss) 58 59if no opcodes are listed before the "for" keyword then "U" (for 60fast-forward update only) is assumed as this is the most common 61usage. 62 63Refnames are matched by always assuming a prefix of "refs/". 64This hook forbids pushing or deleting anything not under "refs/". 65 66Refnames that start with ^ are Perl regular expressions, and the ^ 67is kept as part of the regexp.\\is needed to get just one \, so 68\\d expands to \d in Perl. The 3rd allow line above is an example. 69 70Refnames that don't start with ^ but that end with / are prefix 71matches (2nd allow line above); all other refnames are strict 72equality matches (1st allow line). 73 74Anything pushed to "heads/"(ok, really "refs/heads/") must be 75a commit. Tags are not permitted here. 76 77Anything pushed to "tags/"(err, really "refs/tags/") must be an 78annotated tag. Commits, blobs, trees, etc. are not permitted here. 79Annotated tag signatures aren't checked, nor are they required. 80 81The special subrepository of 'info/new-commit-check' can 82be created and used to allow users to push new commits and 83tags from another local repository to this one, even if they 84aren't the committer/tagger of those objects. In a nut shell 85the info/new-commit-check directory is a Git repository whose 86objects/info/alternates file lists this repository and all other 87possible sources,and whose refs subdirectory contains symlinks 88to this repository's refs subdirectory, and to all other possible 89sources refs subdirectories. Yes, this means that you cannot 90use packed-refs in those repositories as they won't be resolved 91correctly. 92 93=cut 94 95my$git_dir=$ENV{GIT_DIR}; 96my$new_commit_check="$git_dir/info/new-commit-check"; 97my$ref=$ARGV[0]; 98my$old=$ARGV[1]; 99my$new=$ARGV[2]; 100my$new_type; 101my($this_user) =getpwuid$<;# REAL_USER_ID 102my$repository_name; 103my%user_committer; 104my@allow_rules; 105my@path_rules; 106my%diff_cache; 107 108sub deny ($) { 109print STDERR "-Deny-$_[0]\n"if$debug; 110print STDERR "\ndenied:$_[0]\n\n"; 111exit1; 112} 113 114sub grant ($) { 115print STDERR "-Grant-$_[0]\n"if$debug; 116exit0; 117} 118 119sub info ($) { 120print STDERR "-Info-$_[0]\n"if$debug; 121} 122 123sub git_value (@) { 124open(T,'-|','git',@_);local$_= <T>;chop;close T;$_; 125} 126 127sub match_string ($$) { 128my($acl_n,$ref) =@_; 129($acl_neq$ref) 130|| ($acl_n=~ m,/$, &&substr($ref,0,length$acl_n)eq$acl_n) 131|| ($acl_n=~ m,^\^, &&$ref=~ m:$acl_n:); 132} 133 134sub parse_config ($$$$) { 135my$data=shift; 136local$ENV{GIT_DIR} =shift; 137my$br=shift; 138my$fn=shift; 139return unless git_value('rev-list','--max-count=1',$br,'--',$fn); 140 info "Loading$br:$fn"; 141open(I,'-|','git','cat-file','blob',"$br:$fn"); 142my$section=''; 143while(<I>) { 144chomp; 145if(/^\s*$/||/^\s*#/) { 146}elsif(/^\[([a-z]+)\]$/i) { 147$section=lc$1; 148}elsif(/^\[([a-z]+)\s+"(.*)"\]$/i) { 149$section=join('.',lc$1,$2); 150}elsif(/^\s*([a-z][a-z0-9]+)\s*=\s*(.*?)\s*$/i) { 151push@{$data->{join('.',$section,lc$1)}},$2; 152}else{ 153 deny "bad config file line $. in$br:$fn"; 154} 155} 156close I; 157} 158 159sub all_new_committers () { 160local$ENV{GIT_DIR} =$git_dir; 161$ENV{GIT_DIR} =$new_commit_checkif-d $new_commit_check; 162 163 info "Getting committers of new commits."; 164my%used; 165open(T,'-|','git','rev-list','--pretty=raw',$new,'--not','--all'); 166while(<T>) { 167next unlesss/^committer //; 168chop; 169s/>.*$/>/; 170 info "Found$_."unless$used{$_}++; 171} 172close T; 173 info "No new commits."unless%used; 174keys%used; 175} 176 177sub all_new_taggers () { 178my%exists; 179open(T,'-|','git','for-each-ref','--format=%(objectname)','refs/tags'); 180while(<T>) { 181chop; 182$exists{$_} =1; 183} 184close T; 185 186 info "Getting taggers of new tags."; 187my%used; 188my$obj=$new; 189my$obj_type=$new_type; 190while($obj_typeeq'tag') { 191last if$exists{$obj}; 192$obj_type=''; 193open(T,'-|','git','cat-file','tag',$obj); 194while(<T>) { 195chop; 196if(/^object ([a-z0-9]{40})$/) { 197$obj=$1; 198}elsif(/^type (.+)$/) { 199$obj_type=$1; 200}elsif(s/^tagger //) { 201s/>.*$/>/; 202 info "Found$_."unless$used{$_}++; 203last; 204} 205} 206close T; 207} 208 info "No new tags."unless%used; 209keys%used; 210} 211 212sub check_committers (@) { 213my@bad; 214foreach(@_) {push@bad,$_unless$user_committer{$_}; } 215if(@bad) { 216print STDERR "\n"; 217print STDERR "You are not$_.\n"foreach(sort@bad); 218 deny "You cannot push changes not committed by you."; 219} 220} 221 222sub load_diff ($) { 223my$base=shift; 224my$d=$diff_cache{$base}; 225unless($d) { 226local$/="\0"; 227my%this_diff; 228if($base=~/^0{40}$/) { 229# Don't load the diff at all; we are making the 230# branch and have no base to compare to in this 231# case. A file level ACL makes no sense in this 232# context. Having an empty diff will allow the 233# branch creation. 234# 235}else{ 236open(T,'-|','git','diff-tree', 237'-r','--name-status','-z', 238$base,$new)orreturnundef; 239while(<T>) { 240my$op=$_; 241chop$op; 242 243my$path= <T>; 244chop$path; 245 246$this_diff{$path} =$op; 247} 248close T orreturnundef; 249} 250$d= \%this_diff; 251$diff_cache{$base} =$d; 252} 253return$d; 254} 255 256deny "No GIT_DIR inherited from caller"unless$git_dir; 257deny "Need a ref name"unless$ref; 258deny "Refusing funny ref$ref"unless$ref=~ s,^refs/,,; 259deny "Bad old value$old"unless$old=~/^[a-z0-9]{40}$/; 260deny "Bad new value$new"unless$new=~/^[a-z0-9]{40}$/; 261deny "Cannot determine who you are."unless$this_user; 262grant "No change requested."if$oldeq$new; 263 264$repository_name= File::Spec->rel2abs($git_dir); 265$repository_name=~ m,/([^/]+)(?:\.git|/\.git)$,; 266$repository_name=$1; 267info "Updating in '$repository_name'."; 268 269my$op; 270if($old=~/^0{40}$/) {$op='C'; } 271elsif($new=~/^0{40}$/) {$op='D'; } 272else{$op='R'; } 273 274# This is really an update (fast-forward) if the 275# merge base of $old and $new is $old. 276# 277$op='U'if($opeq'R' 278&&$ref=~ m,^heads/, 279&&$oldeq git_value('merge-base',$old,$new)); 280 281# Load the user's ACL file. Expand groups (user.memberof) one level. 282{ 283my%data= ('user.committer'=> []); 284 parse_config(\%data,$acl_git,$acl_branch,"external/$repository_name.acl"); 285 286%data= ( 287'user.committer'=>$data{'user.committer'}, 288'user.memberof'=> [], 289); 290 parse_config(\%data,$acl_git,$acl_branch,"users/$this_user.acl"); 291 292%user_committer=map{$_=>$_} @{$data{'user.committer'}}; 293my$rule_key="repository.$repository_name.allow"; 294my$rules=$data{$rule_key} || []; 295 296foreachmy$group(@{$data{'user.memberof'}}) { 297my%g; 298 parse_config(\%g,$acl_git,$acl_branch,"groups/$group.acl"); 299my$group_rules=$g{$rule_key}; 300push@$rules,@$group_rulesif$group_rules; 301} 302 303RULE: 304foreach(@$rules) { 305while(/\${user\.([a-z][a-zA-Z0-9]+)}/) { 306my$k=lc$1; 307my$v=$data{"user.$k"}; 308next RULE unlessdefined$v; 309next RULE if@$v!=1; 310next RULE unlessdefined$v->[0]; 311s/\${user\.$k}/$v->[0]/g; 312} 313 314if(/^([AMD ]+)\s+of\s+([^\s]+)\s+for\s+([^\s]+)\s+diff\s+([^\s]+)$/) { 315my($ops,$pth,$ref,$bst) = ($1,$2,$3,$4); 316$ops=~s/ //g; 317$pth=~s/\\\\/\\/g; 318$ref=~s/\\\\/\\/g; 319push@path_rules, [$ops,$pth,$ref,$bst]; 320}elsif(/^([AMD ]+)\s+of\s+([^\s]+)\s+for\s+([^\s]+)$/) { 321my($ops,$pth,$ref) = ($1,$2,$3); 322$ops=~s/ //g; 323$pth=~s/\\\\/\\/g; 324$ref=~s/\\\\/\\/g; 325push@path_rules, [$ops,$pth,$ref,$old]; 326}elsif(/^([CDRU ]+)\s+for\s+([^\s]+)$/) { 327my$ops=$1; 328my$ref=$2; 329$ops=~s/ //g; 330$ref=~s/\\\\/\\/g; 331push@allow_rules, [$ops,$ref]; 332}elsif(/^for\s+([^\s]+)$/) { 333# Mentioned, but nothing granted? 334}elsif(/^[^\s]+$/) { 335s/\\\\/\\/g; 336push@allow_rules, ['U',$_]; 337} 338} 339} 340 341if($opne'D') { 342$new_type= git_value('cat-file','-t',$new); 343 344if($ref=~ m,^heads/,) { 345 deny "$refmust be a commit."unless$new_typeeq'commit'; 346}elsif($ref=~ m,^tags/,) { 347 deny "$refmust be an annotated tag."unless$new_typeeq'tag'; 348} 349 350 check_committers (all_new_committers); 351 check_committers (all_new_taggers)if$new_typeeq'tag'; 352} 353 354info "$this_userwants$opfor$ref"; 355foreachmy$acl_entry(@allow_rules) { 356my($acl_ops,$acl_n) =@$acl_entry; 357next unless$acl_ops=~/^[CDRU]+$/;# Uhh.... shouldn't happen. 358next unless$acl_n; 359next unless$op=~/^[$acl_ops]$/; 360next unless match_string $acl_n,$ref; 361 362# Don't test path rules on branch deletes. 363# 364 grant "Allowed by:$acl_opsfor$acl_n"if$opeq'D'; 365 366# Aggregate matching path rules; allow if there aren't 367# any matching this ref. 368# 369my%pr; 370foreachmy$p_entry(@path_rules) { 371my($p_ops,$p_n,$p_ref,$p_bst) =@$p_entry; 372next unless$p_ref; 373push@{$pr{$p_bst}},$p_entryif match_string $p_ref,$ref; 374} 375 grant "Allowed by:$acl_opsfor$acl_n"unless%pr; 376 377# Allow only if all changes against a single base are 378# allowed by file path rules. 379# 380my@bad; 381foreachmy$p_bst(keys%pr) { 382my$diff_ref= load_diff $p_bst; 383 deny "Cannot difference trees."unlessref$diff_ref; 384 385my%fd=%$diff_ref; 386foreachmy$p_entry(@{$pr{$p_bst}}) { 387my($p_ops,$p_n,$p_ref,$p_bst) =@$p_entry; 388next unless$p_ops=~/^[AMD]+$/; 389next unless$p_n; 390 391foreachmy$f_n(keys%fd) { 392my$f_op=$fd{$f_n}; 393next unless$f_op; 394next unless$f_op=~/^[$p_ops]$/; 395delete$fd{$f_n}if match_string $p_n,$f_n; 396} 397last unless%fd; 398} 399 400if(%fd) { 401push@bad, [$p_bst, \%fd]; 402}else{ 403# All changes relative to $p_bst were allowed. 404# 405 grant "Allowed by:$acl_opsfor$acl_ndiff$p_bst"; 406} 407} 408 409foreachmy$bad_ref(@bad) { 410my($p_bst,$fd) =@$bad_ref; 411print STDERR "\n"; 412print STDERR "Not allowed to make the following changes:\n"; 413print STDERR "(base:$p_bst)\n"; 414foreachmy$f_n(sort keys%$fd) { 415print STDERR "$fd->{$f_n}$f_n\n"; 416} 417} 418 deny "You are not permitted to$op$ref"; 419} 420close A; 421deny "You are not permitted to$op$ref";