6 $ENV{PATH
} = '/opt/git/bin';
7 my $acl_git = '/vcs/acls.git';
8 my $acl_branch = 'refs/heads/master';
12 Invoked as
: update refname old
-sha1 new
-sha1
14 This script is run by git
-receive
-pack once
for each ref that the
15 client is trying to modify
. If we
exit with a non
-zero
exit value
16 then the update
for that particular
ref is denied
, but updates
for
17 other refs
in the same run of receive
-pack may still be allowed
.
19 We are run after the objects have been uploaded
, but before the
20 ref is actually modified
. We take advantage of that fact
when we
21 look
for "new" commits
and tags
(the new objects won
't show up in
24 This script loads and parses the content of the config file
25 "users/$this_user.acl" from the $acl_branch commit of $acl_git ODB.
26 The acl file is a git-config style file, but uses a slightly more
27 restricted syntax as the Perl parser contained within this script
28 is not nearly as permissive as git-config.
33 committer = John Doe <john.doe@example.com>
34 committer = John R. Doe <john.doe@example.com>
38 allow = CDUR for heads/jd/
39 allow = C for ^tags/v\\d+$
41 For all new commit or tag objects the committer (or tagger) line
42 within the object must exactly match one of the user.committer
43 values listed in the acl file ("HEAD:users/$this_user.acl").
45 For a branch to be modified an allow line within the matching
46 repository section must be matched for both the refname and the
49 Repository sections are matched on the basename of the repository
50 (after removing the .git suffix).
52 The opcode abbrevations are:
55 D: delete existing ref
56 U: fast-forward existing ref (no commit loss)
57 R: rewind/rebase existing ref (commit loss)
59 if no opcodes are listed before the "for" keyword then "U" (for
60 fast-forward update only) is assumed as this is the most common
63 Refnames are matched by always assuming a prefix of "refs/".
64 This hook forbids pushing or deleting anything not under "refs/".
66 Refnames that start with ^ are Perl regular expressions, and the ^
67 is 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.
70 Refnames that don't start with
^ but that end with
/ are prefix
71 matches
(2nd allow line above
); all other refnames are strict
72 equality matches
(1st allow line
).
74 Anything pushed to
"heads/" (ok
, really
"refs/heads/") must be
75 a commit
. Tags are
not permitted here
.
77 Anything pushed to
"tags/" (err
, really
"refs/tags/") must be an
78 annotated tag
. Commits
, blobs
, trees
, etc
. are
not permitted here
.
79 Annotated tag signatures aren
't checked, nor are they required.
81 The special subrepository of 'info
/new
-commit
-check
' can
82 be created and used to allow users to push new commits and
83 tags from another local repository to this one, even if they
84 aren't the committer
/tagger of those objects
. In a nut shell
85 the info
/new
-commit
-check directory is a Git repository whose
86 objects
/info/alternates file lists this repository
and all other
87 possible sources
, and whose refs subdirectory contains symlinks
88 to this repository
's refs subdirectory, and to all other possible
89 sources refs subdirectories. Yes, this means that you cannot
90 use packed-refs in those repositories as they won't be resolved
95 my $git_dir = $ENV{GIT_DIR
};
96 my $new_commit_check = "$git_dir/info/new-commit-check";
101 my ($this_user) = getpwuid $<; # REAL_USER_ID
109 print STDERR
"-Deny- $_[0]\n" if $debug;
110 print STDERR
"\ndenied: $_[0]\n\n";
115 print STDERR
"-Grant- $_[0]\n" if $debug;
120 print STDERR
"-Info- $_[0]\n" if $debug;
124 open(T
,'-|','git',@_); local $_ = <T
>; chop; close T
; $_;
127 sub match_string
($$) {
128 my ($acl_n, $ref) = @_;
130 || ($acl_n =~ m
,/$, && substr($ref,0,length $acl_n) eq $acl_n)
131 || ($acl_n =~ m
,^\
^, && $ref =~ m
:$acl_n:);
134 sub parse_config
($$$$) {
136 local $ENV{GIT_DIR
} = shift;
139 info
"Loading $br:$fn";
140 open(I
,'-|','git','cat-file','blob',"$br:$fn");
144 if (/^\s*$/ || /^\s*#/) {
145 } elsif (/^\[([a-z]+)\]$/i) {
147 } elsif (/^\[([a-z]+)\s+"(.*)"\]$/i) {
148 $section = join('.',lc $1,$2);
149 } elsif (/^\s*([a-z][a-z0-9]+)\s*=\s*(.*?)\s*$/i) {
150 push @
{$data->{join('.',$section,lc $1)}}, $2;
152 deny
"bad config file line $. in $br:$fn";
158 sub all_new_committers
() {
159 local $ENV{GIT_DIR
} = $git_dir;
160 $ENV{GIT_DIR
} = $new_commit_check if -d
$new_commit_check;
162 info
"Getting committers of new commits.";
164 open(T
,'-|','git','rev-list','--pretty=raw',$new,'--not','--all');
166 next unless s/^committer //;
169 info
"Found $_." unless $used{$_}++;
172 info
"No new commits." unless %used;
176 sub all_new_taggers
() {
178 open(T
,'-|','git','for-each-ref','--format=%(objectname)','refs/tags');
185 info
"Getting taggers of new tags.";
188 my $obj_type = $new_type;
189 while ($obj_type eq 'tag') {
190 last if $exists{$obj};
192 open(T
,'-|','git','cat-file','tag',$obj);
195 if (/^object ([a-z0-9]{40})$/) {
197 } elsif (/^type (.+)$/) {
199 } elsif (s/^tagger //) {
201 info
"Found $_." unless $used{$_}++;
207 info
"No new tags." unless %used;
211 sub check_committers
(@
) {
213 foreach (@_) { push @bad, $_ unless $user_committer{$_}; }
216 print STDERR
"You are not $_.\n" foreach (sort @bad);
217 deny
"You cannot push changes not committed by you.";
223 my $d = $diff_cache{$base};
226 open(T
,'-|','git','diff-tree',
227 '-r','--name-status','-z',
228 $base,$new) or return undef;
237 $this_diff{$path} = $op;
239 close T
or return undef;
241 $diff_cache{$base} = $d;
246 deny
"No GIT_DIR inherited from caller" unless $git_dir;
247 deny
"Need a ref name" unless $ref;
248 deny
"Refusing funny ref $ref" unless $ref =~ s
,^refs
/,,;
249 deny
"Bad old value $old" unless $old =~ /^[a-z0-9]{40}$/;
250 deny
"Bad new value $new" unless $new =~ /^[a-z0-9]{40}$/;
251 deny
"Cannot determine who you are." unless $this_user;
253 $repository_name = File
::Spec
->rel2abs($git_dir);
254 $repository_name =~ m
,/([^/]+)(?
:\
.git
|/\
.git
)$,;
255 $repository_name = $1;
256 info
"Updating in '$repository_name'.";
259 if ($old =~ /^0{40}$/) { $op = 'C'; }
260 elsif ($new =~ /^0{40}$/) { $op = 'D'; }
263 # This is really an update (fast-forward) if the
264 # merge base of $old and $new is $old.
266 $op = 'U' if ($op eq 'R'
267 && $ref =~ m
,^heads
/,
268 && $old eq git_value
('merge-base',$old,$new));
270 # Load the user's ACL file. Expand groups (user.memberof) one level.
272 my %data = ('user.committer' => []);
273 parse_config
(\
%data,$acl_git,$acl_branch,"external/$repository_name.acl");
276 'user.committer' => $data{'user.committer'},
277 'user.memberof' => [],
279 parse_config
(\
%data,$acl_git,$acl_branch,"users/$this_user.acl");
281 %user_committer = map {$_ => $_} @
{$data{'user.committer'}};
282 my $rule_key = "repository.$repository_name.allow";
283 my $rules = $data{$rule_key} || [];
285 foreach my $group (@
{$data{'user.memberof'}}) {
287 parse_config
(\
%g,$acl_git,$acl_branch,"groups/$group.acl");
288 my $group_rules = $g{$rule_key};
289 push @
$rules, @
$group_rules if $group_rules;
294 while (/\${user\.([a-z][a-zA-Z0-9]+)}/) {
296 my $v = $data{"user.$k"};
297 next RULE
unless defined $v;
298 next RULE
if @
$v != 1;
299 next RULE
unless defined $v->[0];
300 s/\${user\.$k}/$v->[0]/g;
303 if (/^([AMD ]+)\s+of\s+([^\s]+)\s+for\s+([^\s]+)\s+diff\s+([^\s]+)$/) {
304 my ($ops, $pth, $ref, $bst) = ($1, $2, $3, $4);
308 push @path_rules, [$ops, $pth, $ref, $bst];
309 } elsif (/^([AMD ]+)\s+of\s+([^\s]+)\s+for\s+([^\s]+)$/) {
310 my ($ops, $pth, $ref) = ($1, $2, $3);
314 push @path_rules, [$ops, $pth, $ref, $old];
315 } elsif (/^([CDRU ]+)\s+for\s+([^\s]+)$/) {
320 push @allow_rules, [$ops, $ref];
321 } elsif (/^for\s+([^\s]+)$/) {
322 # Mentioned, but nothing granted?
323 } elsif (/^[^\s]+$/) {
325 push @allow_rules, ['U', $_];
331 $new_type = git_value
('cat-file','-t',$new);
333 if ($ref =~ m
,^heads
/,) {
334 deny
"$ref must be a commit." unless $new_type eq 'commit';
335 } elsif ($ref =~ m
,^tags
/,) {
336 deny
"$ref must be an annotated tag." unless $new_type eq 'tag';
339 check_committers
(all_new_committers
);
340 check_committers
(all_new_taggers
) if $new_type eq 'tag';
343 info
"$this_user wants $op for $ref";
344 foreach my $acl_entry (@allow_rules) {
345 my ($acl_ops, $acl_n) = @
$acl_entry;
346 next unless $acl_ops =~ /^[CDRU]+$/; # Uhh.... shouldn't happen.
348 next unless $op =~ /^[$acl_ops]$/;
349 next unless match_string
$acl_n, $ref;
351 # Don't test path rules on branch deletes.
353 grant
"Allowed by: $acl_ops for $acl_n" if $op eq 'D';
355 # Aggregate matching path rules; allow if there aren't
356 # any matching this ref.
359 foreach my $p_entry (@path_rules) {
360 my ($p_ops, $p_n, $p_ref, $p_bst) = @
$p_entry;
362 push @
{$pr{$p_bst}}, $p_entry if match_string
$p_ref, $ref;
364 grant
"Allowed by: $acl_ops for $acl_n" unless %pr;
366 # Allow only if all changes against a single base are
367 # allowed by file path rules.
370 foreach my $p_bst (keys %pr) {
371 my $diff_ref = load_diff
$p_bst;
372 deny
"Cannot difference trees." unless ref $diff_ref;
375 foreach my $p_entry (@
{$pr{$p_bst}}) {
376 my ($p_ops, $p_n, $p_ref, $p_bst) = @
$p_entry;
377 next unless $p_ops =~ /^[AMD]+$/;
380 foreach my $f_n (keys %fd) {
381 my $f_op = $fd{$f_n};
383 next unless $f_op =~ /^[$p_ops]$/;
384 delete $fd{$f_n} if match_string
$p_n, $f_n;
390 push @bad, [$p_bst, \
%fd];
392 # All changes relative to $p_bst were allowed.
394 grant
"Allowed by: $acl_ops for $acl_n diff $p_bst";
398 foreach my $bad_ref (@bad) {
399 my ($p_bst, $fd) = @
$bad_ref;
401 print STDERR
"Not allowed to make the following changes:\n";
402 print STDERR
"(base: $p_bst)\n";
403 foreach my $f_n (sort keys %$fd) {
404 print STDERR
" $fd->{$f_n} $f_n\n";
407 deny
"You are not permitted to $op $ref";
410 deny
"You are not permitted to $op $ref";