X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=contrib%2Fhooks%2Fupdate-paranoid;h=d18b317b2f018d1d1a5a9677a7bdaf8956d65186;hb=ac538e5dbaf13180b7e7a9c4471f1007b8b11d68;hp=5ee1835c801fc2fea8284aa253c966bd65be0549;hpb=31c74ca67162674e3ff8fcc294b75881c3e7cc15;p=git.git diff --git a/contrib/hooks/update-paranoid b/contrib/hooks/update-paranoid index 5ee1835c8..d18b317b2 100644 --- a/contrib/hooks/update-paranoid +++ b/contrib/hooks/update-paranoid @@ -102,6 +102,8 @@ my ($this_user) = getpwuid $<; # REAL_USER_ID my $repository_name; my %user_committer; my @allow_rules; +my @path_rules; +my %diff_cache; sub deny ($) { print STDERR "-Deny- $_[0]\n" if $debug; @@ -118,22 +120,37 @@ sub info ($) { print STDERR "-Info- $_[0]\n" if $debug; } -sub parse_config ($$) { - my ($data, $fn) = @_; - info "Loading $fn"; - open(I,'-|','git',"--git-dir=$acl_git",'cat-file','blob',$fn); +sub git_value (@) { + open(T,'-|','git',@_); local $_ = ; chop; close T; $_; +} + +sub match_string ($$) { + my ($acl_n, $ref) = @_; + ($acl_n eq $ref) + || ($acl_n =~ m,/$, && substr($ref,0,length $acl_n) eq $acl_n) + || ($acl_n =~ m,^\^, && $ref =~ m:$acl_n:); +} + +sub parse_config ($$$$) { + my $data = shift; + local $ENV{GIT_DIR} = shift; + my $br = shift; + my $fn = shift; + return unless git_value('rev-list','--max-count=1',$br,'--',$fn); + info "Loading $br:$fn"; + open(I,'-|','git','cat-file','blob',"$br:$fn"); my $section = ''; while () { chomp; if (/^\s*$/ || /^\s*#/) { } elsif (/^\[([a-z]+)\]$/i) { - $section = $1; + $section = lc $1; } elsif (/^\[([a-z]+)\s+"(.*)"\]$/i) { - $section = "$1.$2"; + $section = join('.',lc $1,$2); } elsif (/^\s*([a-z][a-z0-9]+)\s*=\s*(.*?)\s*$/i) { - push @{$data->{"$section.$1"}}, $2; + push @{$data->{join('.',$section,lc $1)}}, $2; } else { - deny "bad config file line $. in $fn"; + deny "bad config file line $. in $br:$fn"; } } close I; @@ -202,9 +219,38 @@ sub check_committers (@) { } } -sub git_value (@) { - open(T,'-|','git',@_); local $_ = ; chop; close T; - $_; +sub load_diff ($) { + my $base = shift; + my $d = $diff_cache{$base}; + unless ($d) { + local $/ = "\0"; + my %this_diff; + if ($base =~ /^0{40}$/) { + # Don't load the diff at all; we are making the + # branch and have no base to compare to in this + # case. A file level ACL makes no sense in this + # context. Having an empty diff will allow the + # branch creation. + # + } else { + open(T,'-|','git','diff-tree', + '-r','--name-status','-z', + $base,$new) or return undef; + while () { + my $op = $_; + chop $op; + + my $path = ; + chop $path; + + $this_diff{$path} = $op; + } + close T or return undef; + } + $d = \%this_diff; + $diff_cache{$base} = $d; + } + return $d; } deny "No GIT_DIR inherited from caller" unless $git_dir; @@ -213,6 +259,7 @@ deny "Refusing funny ref $ref" unless $ref =~ s,^refs/,,; deny "Bad old value $old" unless $old =~ /^[a-z0-9]{40}$/; deny "Bad new value $new" unless $new =~ /^[a-z0-9]{40}$/; deny "Cannot determine who you are." unless $this_user; +grant "No change requested." if $old eq $new; $repository_name = File::Spec->rel2abs($git_dir); $repository_name =~ m,/([^/]+)(?:\.git|/\.git)$,; @@ -231,14 +278,52 @@ $op = 'U' if ($op eq 'R' && $ref =~ m,^heads/, && $old eq git_value('merge-base',$old,$new)); -# Load the user's ACL file. +# Load the user's ACL file. Expand groups (user.memberof) one level. { my %data = ('user.committer' => []); - parse_config(\%data, "$acl_branch:users/$this_user.acl"); + parse_config(\%data,$acl_git,$acl_branch,"external/$repository_name.acl"); + + %data = ( + 'user.committer' => $data{'user.committer'}, + 'user.memberof' => [], + ); + parse_config(\%data,$acl_git,$acl_branch,"users/$this_user.acl"); + %user_committer = map {$_ => $_} @{$data{'user.committer'}}; - my $rules = $data{"repository.$repository_name.allow"} || []; + my $rule_key = "repository.$repository_name.allow"; + my $rules = $data{$rule_key} || []; + + foreach my $group (@{$data{'user.memberof'}}) { + my %g; + parse_config(\%g,$acl_git,$acl_branch,"groups/$group.acl"); + my $group_rules = $g{$rule_key}; + push @$rules, @$group_rules if $group_rules; + } + +RULE: foreach (@$rules) { - if (/^([CDRU ]+)\s+for\s+([^\s]+)$/) { + while (/\${user\.([a-z][a-zA-Z0-9]+)}/) { + my $k = lc $1; + my $v = $data{"user.$k"}; + next RULE unless defined $v; + next RULE if @$v != 1; + next RULE unless defined $v->[0]; + s/\${user\.$k}/$v->[0]/g; + } + + if (/^([AMD ]+)\s+of\s+([^\s]+)\s+for\s+([^\s]+)\s+diff\s+([^\s]+)$/) { + my ($ops, $pth, $ref, $bst) = ($1, $2, $3, $4); + $ops =~ s/ //g; + $pth =~ s/\\\\/\\/g; + $ref =~ s/\\\\/\\/g; + push @path_rules, [$ops, $pth, $ref, $bst]; + } elsif (/^([AMD ]+)\s+of\s+([^\s]+)\s+for\s+([^\s]+)$/) { + my ($ops, $pth, $ref) = ($1, $2, $3); + $ops =~ s/ //g; + $pth =~ s/\\\\/\\/g; + $ref =~ s/\\\\/\\/g; + push @path_rules, [$ops, $pth, $ref, $old]; + } elsif (/^([CDRU ]+)\s+for\s+([^\s]+)$/) { my $ops = $1; my $ref = $2; $ops =~ s/ //g; @@ -272,13 +357,65 @@ foreach my $acl_entry (@allow_rules) { next unless $acl_ops =~ /^[CDRU]+$/; # Uhh.... shouldn't happen. next unless $acl_n; next unless $op =~ /^[$acl_ops]$/; + next unless match_string $acl_n, $ref; + + # Don't test path rules on branch deletes. + # + grant "Allowed by: $acl_ops for $acl_n" if $op eq 'D'; + + # Aggregate matching path rules; allow if there aren't + # any matching this ref. + # + my %pr; + foreach my $p_entry (@path_rules) { + my ($p_ops, $p_n, $p_ref, $p_bst) = @$p_entry; + next unless $p_ref; + push @{$pr{$p_bst}}, $p_entry if match_string $p_ref, $ref; + } + grant "Allowed by: $acl_ops for $acl_n" unless %pr; - grant "Allowed by: $acl_ops for $acl_n" - if ( - ($acl_n eq $ref) - || ($acl_n =~ m,/$, && substr($ref,0,length $acl_n) eq $acl_n) - || ($acl_n =~ m,^\^, && $ref =~ m:$acl_n:) - ); + # Allow only if all changes against a single base are + # allowed by file path rules. + # + my @bad; + foreach my $p_bst (keys %pr) { + my $diff_ref = load_diff $p_bst; + deny "Cannot difference trees." unless ref $diff_ref; + + my %fd = %$diff_ref; + foreach my $p_entry (@{$pr{$p_bst}}) { + my ($p_ops, $p_n, $p_ref, $p_bst) = @$p_entry; + next unless $p_ops =~ /^[AMD]+$/; + next unless $p_n; + + foreach my $f_n (keys %fd) { + my $f_op = $fd{$f_n}; + next unless $f_op; + next unless $f_op =~ /^[$p_ops]$/; + delete $fd{$f_n} if match_string $p_n, $f_n; + } + last unless %fd; + } + + if (%fd) { + push @bad, [$p_bst, \%fd]; + } else { + # All changes relative to $p_bst were allowed. + # + grant "Allowed by: $acl_ops for $acl_n diff $p_bst"; + } + } + + foreach my $bad_ref (@bad) { + my ($p_bst, $fd) = @$bad_ref; + print STDERR "\n"; + print STDERR "Not allowed to make the following changes:\n"; + print STDERR "(base: $p_bst)\n"; + foreach my $f_n (sort keys %$fd) { + print STDERR " $fd->{$f_n} $f_n\n"; + } + } + deny "You are not permitted to $op $ref"; } close A; deny "You are not permitted to $op $ref";