Merge branch 'as/test-tweaks'
[gitweb.git] / contrib / hooks / update-paranoid
index 5ee1835c801fc2fea8284aa253c966bd65be0549..d18b317b2f018d1d1a5a9677a7bdaf8956d65186 100644 (file)
@@ -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 $_ = <T>; 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 (<I>) {
                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 $_ = <T>; 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 (<T>) {
+                               my $op = $_;
+                               chop $op;
+
+                               my $path = <T>;
+                               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";