[PATCH 2/3] Teach the update-paranoid to look at file differences

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



In some applications of the update hook a user may be allowed to
modify a branch, but only if the file level difference is also an
allowed change.  This is the commonly requested feature of allowing
users to modify only certain files.

A new repository.*.allow syntax permits granting the three basic
file level operations:

  A: file is added relative to the other tree
  M: file exists in both trees, but its SHA-1 or mode differs
  D: file is removed relative to the other tree

on a per-branch and path-name basis.  The user must also have a
branch level allow line already granting them access to create,
rewind or update (CRU) that branch before the hook will consult
any file level rules.

In order for a branch change to succeed _all_ files that differ
relative to some base (by default the old value of this branch,
but it can also be any valid tree-ish) must be allowed by file
level allow rules.  A push is rejected if any diff exists that
is not covered by at least one allow rule.

Signed-off-by: Shawn O. Pearce <spearce@xxxxxxxxxxx>
---
 contrib/hooks/update-paranoid |  112 ++++++++++++++++++++++++++++++++++++++---
 1 files changed, 105 insertions(+), 7 deletions(-)

diff --git a/contrib/hooks/update-paranoid b/contrib/hooks/update-paranoid
index fb2aca3..84ed452 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;
@@ -122,6 +124,13 @@ 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;
@@ -209,6 +218,31 @@ sub check_committers (@) {
 	}
 }
 
+sub load_diff ($) {
+	my $base = shift;
+	my $d = $diff_cache{$base};
+	unless ($d) {
+		local $/ = "\0";
+		open(T,'-|','git','diff-tree',
+			'-r','--name-status','-z',
+			$base,$new) or return undef;
+		my %this_diff;
+		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;
 deny "Need a ref name" unless $ref;
 deny "Refusing funny ref $ref" unless $ref =~ s,^refs/,,;
@@ -266,7 +300,19 @@ RULE:
 			s/\${user\.$k}/$v->[0]/g;
 		}
 
-		if (/^([CDRU ]+)\s+for\s+([^\s]+)$/) {
+		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;
@@ -300,13 +346,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";
-- 
1.5.3.rc4.29.g74276

-
To unsubscribe from this list: send the line "unsubscribe git" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html

[Index of Archives]     [Linux Kernel Development]     [Gcc Help]     [IETF Annouce]     [DCCP]     [Netdev]     [Networking]     [Security]     [V4L]     [Bugtraq]     [Yosemite]     [MIPS Linux]     [ARM Linux]     [Linux Security]     [Linux RAID]     [Linux SCSI]     [Fedora Users]

  Powered by Linux