Teach the update-paranoid to look at file differences

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@spearce.org>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Shawn O. Pearce 2007-08-09 02:38:12 -04:00 committed by Junio C Hamano
parent b767c792fa
commit d47eed3272

View 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;
@ -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;
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:)
);
# 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;
# 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";