 8915297925
			
		
	
	8915297925
	
	
	
		
			
			Reported-by: Jens Schleusener <Jens.Schleusener@fossies.org> Signed-off-by: Elijah Newren <newren@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
		
			
				
	
	
		
			422 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Perl
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			422 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Perl
		
	
	
		
			Executable File
		
	
	
	
	
| #!/usr/bin/perl
 | |
| 
 | |
| use strict;
 | |
| use File::Spec;
 | |
| 
 | |
| $ENV{PATH}     = '/opt/git/bin';
 | |
| my $acl_git    = '/vcs/acls.git';
 | |
| my $acl_branch = 'refs/heads/master';
 | |
| my $debug      = 0;
 | |
| 
 | |
| =doc
 | |
| Invoked as: update refname old-sha1 new-sha1
 | |
| 
 | |
| This script is run by git-receive-pack once for each ref that the
 | |
| client is trying to modify.  If we exit with a non-zero exit value
 | |
| then the update for that particular ref is denied, but updates for
 | |
| other refs in the same run of receive-pack may still be allowed.
 | |
| 
 | |
| We are run after the objects have been uploaded, but before the
 | |
| ref is actually modified.  We take advantage of that fact when we
 | |
| look for "new" commits and tags (the new objects won't show up in
 | |
| `rev-list --all`).
 | |
| 
 | |
| This script loads and parses the content of the config file
 | |
| "users/$this_user.acl" from the $acl_branch commit of $acl_git ODB.
 | |
| The acl file is a git-config style file, but uses a slightly more
 | |
| restricted syntax as the Perl parser contained within this script
 | |
| is not nearly as permissive as git-config.
 | |
| 
 | |
| Example:
 | |
| 
 | |
|   [user]
 | |
|     committer = John Doe <john.doe@example.com>
 | |
|     committer = John R. Doe <john.doe@example.com>
 | |
| 
 | |
|   [repository "acls"]
 | |
|     allow = heads/master
 | |
|     allow = CDUR for heads/jd/
 | |
|     allow = C    for ^tags/v\\d+$
 | |
| 
 | |
| For all new commit or tag objects the committer (or tagger) line
 | |
| within the object must exactly match one of the user.committer
 | |
| values listed in the acl file ("HEAD:users/$this_user.acl").
 | |
| 
 | |
| For a branch to be modified an allow line within the matching
 | |
| repository section must be matched for both the refname and the
 | |
| opcode.
 | |
| 
 | |
| Repository sections are matched on the basename of the repository
 | |
| (after removing the .git suffix).
 | |
| 
 | |
| The opcode abbreviations are:
 | |
| 
 | |
|   C: create new ref
 | |
|   D: delete existing ref
 | |
|   U: fast-forward existing ref (no commit loss)
 | |
|   R: rewind/rebase existing ref (commit loss)
 | |
| 
 | |
| if no opcodes are listed before the "for" keyword then "U" (for
 | |
| fast-forward update only) is assumed as this is the most common
 | |
| usage.
 | |
| 
 | |
| Refnames are matched by always assuming a prefix of "refs/".
 | |
| This hook forbids pushing or deleting anything not under "refs/".
 | |
| 
 | |
| Refnames that start with ^ are Perl regular expressions, and the ^
 | |
| is kept as part of the regexp.  \\ is needed to get just one \, so
 | |
| \\d expands to \d in Perl.  The 3rd allow line above is an example.
 | |
| 
 | |
| Refnames that don't start with ^ but that end with / are prefix
 | |
| matches (2nd allow line above); all other refnames are strict
 | |
| equality matches (1st allow line).
 | |
| 
 | |
| Anything pushed to "heads/" (ok, really "refs/heads/") must be
 | |
| a commit.  Tags are not permitted here.
 | |
| 
 | |
| Anything pushed to "tags/" (err, really "refs/tags/") must be an
 | |
| annotated tag.  Commits, blobs, trees, etc. are not permitted here.
 | |
| Annotated tag signatures aren't checked, nor are they required.
 | |
| 
 | |
| The special subrepository of 'info/new-commit-check' can
 | |
| be created and used to allow users to push new commits and
 | |
| tags from another local repository to this one, even if they
 | |
| aren't the committer/tagger of those objects.  In a nut shell
 | |
| the info/new-commit-check directory is a Git repository whose
 | |
| objects/info/alternates file lists this repository and all other
 | |
| possible sources, and whose refs subdirectory contains symlinks
 | |
| to this repository's refs subdirectory, and to all other possible
 | |
| sources refs subdirectories.  Yes, this means that you cannot
 | |
| use packed-refs in those repositories as they won't be resolved
 | |
| correctly.
 | |
| 
 | |
| =cut
 | |
| 
 | |
| my $git_dir = $ENV{GIT_DIR};
 | |
| my $new_commit_check = "$git_dir/info/new-commit-check";
 | |
| my $ref = $ARGV[0];
 | |
| my $old = $ARGV[1];
 | |
| my $new = $ARGV[2];
 | |
| my $new_type;
 | |
| 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;
 | |
| 	print STDERR "\ndenied: $_[0]\n\n";
 | |
| 	exit 1;
 | |
| }
 | |
| 
 | |
| sub grant ($) {
 | |
| 	print STDERR "-Grant-   $_[0]\n" if $debug;
 | |
| 	exit 0;
 | |
| }
 | |
| 
 | |
| sub info ($) {
 | |
| 	print STDERR "-Info-    $_[0]\n" if $debug;
 | |
| }
 | |
| 
 | |
| 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 = lc $1;
 | |
| 		} elsif (/^\[([a-z]+)\s+"(.*)"\]$/i) {
 | |
| 			$section = join('.',lc $1,$2);
 | |
| 		} elsif (/^\s*([a-z][a-z0-9]+)\s*=\s*(.*?)\s*$/i) {
 | |
| 			push @{$data->{join('.',$section,lc $1)}}, $2;
 | |
| 		} else {
 | |
| 			deny "bad config file line $. in $br:$fn";
 | |
| 		}
 | |
| 	}
 | |
| 	close I;
 | |
| }
 | |
| 
 | |
| sub all_new_committers () {
 | |
| 	local $ENV{GIT_DIR} = $git_dir;
 | |
| 	$ENV{GIT_DIR} = $new_commit_check if -d $new_commit_check;
 | |
| 
 | |
| 	info "Getting committers of new commits.";
 | |
| 	my %used;
 | |
| 	open(T,'-|','git','rev-list','--pretty=raw',$new,'--not','--all');
 | |
| 	while (<T>) {
 | |
| 		next unless s/^committer //;
 | |
| 		chop;
 | |
| 		s/>.*$/>/;
 | |
| 		info "Found $_." unless $used{$_}++;
 | |
| 	}
 | |
| 	close T;
 | |
| 	info "No new commits." unless %used;
 | |
| 	keys %used;
 | |
| }
 | |
| 
 | |
| sub all_new_taggers () {
 | |
| 	my %exists;
 | |
| 	open(T,'-|','git','for-each-ref','--format=%(objectname)','refs/tags');
 | |
| 	while (<T>) {
 | |
| 		chop;
 | |
| 		$exists{$_} = 1;
 | |
| 	}
 | |
| 	close T;
 | |
| 
 | |
| 	info "Getting taggers of new tags.";
 | |
| 	my %used;
 | |
| 	my $obj = $new;
 | |
| 	my $obj_type = $new_type;
 | |
| 	while ($obj_type eq 'tag') {
 | |
| 		last if $exists{$obj};
 | |
| 		$obj_type = '';
 | |
| 		open(T,'-|','git','cat-file','tag',$obj);
 | |
| 		while (<T>) {
 | |
| 			chop;
 | |
| 			if (/^object ([a-z0-9]{40})$/) {
 | |
| 				$obj = $1;
 | |
| 			} elsif (/^type (.+)$/) {
 | |
| 				$obj_type = $1;
 | |
| 			} elsif (s/^tagger //) {
 | |
| 				s/>.*$/>/;
 | |
| 				info "Found $_." unless $used{$_}++;
 | |
| 				last;
 | |
| 			}
 | |
| 		}
 | |
| 		close T;
 | |
| 	}
 | |
| 	info "No new tags." unless %used;
 | |
| 	keys %used;
 | |
| }
 | |
| 
 | |
| sub check_committers (@) {
 | |
| 	my @bad;
 | |
| 	foreach (@_) { push @bad, $_ unless $user_committer{$_}; }
 | |
| 	if (@bad) {
 | |
| 		print STDERR "\n";
 | |
| 		print STDERR "You are not $_.\n" foreach (sort @bad);
 | |
| 		deny "You cannot push changes not committed by you.";
 | |
| 	}
 | |
| }
 | |
| 
 | |
| 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 "Need a ref name" unless $ref;
 | |
| 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)$,;
 | |
| $repository_name = $1;
 | |
| info "Updating in '$repository_name'.";
 | |
| 
 | |
| my $op;
 | |
| if    ($old =~ /^0{40}$/) { $op = 'C'; }
 | |
| elsif ($new =~ /^0{40}$/) { $op = 'D'; }
 | |
| else                      { $op = 'R'; }
 | |
| 
 | |
| # This is really an update (fast-forward) if the
 | |
| # merge base of $old and $new is $old.
 | |
| #
 | |
| $op = 'U' if ($op eq 'R'
 | |
| 	&& $ref =~ m,^heads/,
 | |
| 	&& $old eq git_value('merge-base',$old,$new));
 | |
| 
 | |
| # Load the user's ACL file. Expand groups (user.memberof) one level.
 | |
| {
 | |
| 	my %data = ('user.committer' => []);
 | |
| 	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 $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) {
 | |
| 		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;
 | |
| 			$ref =~ s/\\\\/\\/g;
 | |
| 			push @allow_rules, [$ops, $ref];
 | |
| 		} elsif (/^for\s+([^\s]+)$/) {
 | |
| 			# Mentioned, but nothing granted?
 | |
| 		} elsif (/^[^\s]+$/) {
 | |
| 			s/\\\\/\\/g;
 | |
| 			push @allow_rules, ['U', $_];
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| if ($op ne 'D') {
 | |
| 	$new_type = git_value('cat-file','-t',$new);
 | |
| 
 | |
| 	if ($ref =~ m,^heads/,) {
 | |
| 		deny "$ref must be a commit." unless $new_type eq 'commit';
 | |
| 	} elsif ($ref =~ m,^tags/,) {
 | |
| 		deny "$ref must be an annotated tag." unless $new_type eq 'tag';
 | |
| 	}
 | |
| 
 | |
| 	check_committers (all_new_committers);
 | |
| 	check_committers (all_new_taggers) if $new_type eq 'tag';
 | |
| }
 | |
| 
 | |
| info "$this_user wants $op for $ref";
 | |
| foreach my $acl_entry (@allow_rules) {
 | |
| 	my ($acl_ops, $acl_n) = @$acl_entry;
 | |
| 	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;
 | |
| 
 | |
| 	# 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";
 |