my $repository_name;
my %user_committer;
my @allow_rules;
+my @path_rules;
+my %diff_cache;
sub deny ($) {
print STDERR "-Deny- $_[0]\n" if $debug;
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;
}
}
-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;
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)$,;
&& $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;
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";