From 5b089678b03537f27fac73a2a08136ff9b1cbf66 Mon Sep 17 00:00:00 2001 From: Sergey Poznyakoff Date: Thu, 11 Apr 2013 12:07:26 +0300 Subject: gitaclhook: an update hook for git implementing ACLs. * git/gitaclhook: New file. * upload/gnupload: Bugfix. --- git/gitaclhook | 276 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ upload/gnupload | 3 +- 2 files changed, 278 insertions(+), 1 deletion(-) create mode 100755 git/gitaclhook diff --git a/git/gitaclhook b/git/gitaclhook new file mode 100755 index 0000000..f9b3d3a --- /dev/null +++ b/git/gitaclhook @@ -0,0 +1,276 @@ +#! /usr/bin/perl +# Copyright (C) 2013 Sergey Poznyakoff +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +use strict; +use File::Spec; + +=doc +This hook is intended to be run as an "update" hook by git. +It is called by git-receive-pack with arguments: refname old-sha1 new-sha1. + +If the hooks.aclfile keyword is defined in the repository's config file, +this hook will parse the file and allow or deny update depending on +its settings. If hooks.aclfile is not defined, update is allowed +unconditionally. + +The ACL file has the usual line-oriented syntax. Comments are introduced +by the # sign and extend to the end of the physical line. Comments and +empty lines are ignored. + +Non-empty lines introduce ACL rules. The syntax is: + + VERB PROJECT USER [OP REF] + +where brackets denote optional parts. The parts of an ACL are: + +VERB Either 'allow' or 'deny', to allow or deny the operation, + correspondingly. + +PROJECT The name of the project. It is obtained by removing the directory + and suffix parts of the repository pathname. Thus, if the repository + is located in /var/gitroot/foobar.git, then the corresponding name of + the project is 'foobar'. + +USER Name of the user. The word 'all' stands for any user, the word 'none' + matches no one at all. Otherwise, if this part begins with a percent + sign (%), the rest of characters aretreated as the name of the UNIX + group to check and the rule matches any user in that group. Otherwise, + the literal match is assumed. + +The optional parts are: + +OP Requested operation codes. It is a string consisting of one or more + of the following letters (case-insensitive): + + C: create new ref + D: delete existing ref + U: fast-forward existing ref (no commit loss) + R: rewind or rebase existing ref (commit loss) + +REF Affected ref, relative to the git refs/ directory. If it begins with + a caret (^), it is treated as a Perl regular expression (with the ^ + being its part). If it ends with a /, it is treated as a prefix match, + so, e.g., "heads/baz/" matches "refs/heads/baz" and anything below. + Otherwise, it must match exactly the affected ref. + +The rule applies only if its PROJECT and USER parts match the project which is +being updated and the user who requests the update, its OP contains the opcode +of the requested operation and REF matches the affected ref. Missing REF +and/or OP are treated as a match. + +If no rule applies, the operation is allowed. + +For example, assume you have the following ACL file: + +allow myprog %devel U heads/master +allow myprog %pm CDUR heads/ +allow myprog %pm C ^heads/tags/v\\d+$ +allow myprog admin CDUR +deny myprog all + +Then the users from the 'devel' group will be able to push updates to +refs/heads/master, the users from the 'pm' group will be allowed to do +anything with refs under refs/heads and to create tags with names beginning +with 'v' and containing only digits afterwards, and the user 'admin' will +be allowed to do anything he pleases. No other users will be allowed to +update that repository. + +Configuration settings: + +hooks.aclfile STRING Name of the ACL file +hooks.acllog STRING Send log info to this file +hooks.acldebug BOOL Enable debugging +hooks.aclquiet BOOL Suppress diagnostics on stderr + +=cut + +my $debug = $ENV{GIT_UPDATE_DEBUG} > 0; +my $logfile; +my $quiet; +my ($user_name) = getpwuid $<; +my $git_dir = $ENV{GIT_DIR}; +my $ref = $ARGV[0]; +my $old = $ARGV[1]; +my $new = $ARGV[2]; + +my $project_name; +my $op; + +my %opstr = ('C' => 'create', + 'D' => 'delete', + 'U' => 'update', + 'R' => 'rewind/rebase'); + +sub logmsg($$;$) { + return 0 unless $logfile; + + my $status = shift; + my $message = shift; + my $loc = shift; + my $fd; + + open($fd, $logfile); + if ($loc) { + print $fd "$status:$loc: $message\n"; + } else { + print $fd "$status: $message\n"; + } + close($fd); +} + +sub deny ($;$) { + my $msg = shift; + my $loc = shift; + + logmsg("DENY", + "$project_name:$user_name:$opstr{$op}:$ref:$old:$new: $msg", + $loc); + + print STDERR "debug: denied by $loc\n" if ($debug and $loc); + print STDERR "denied: $msg\n" unless $quiet; + exit 1; +} + +sub allow ($) { + logmsg("ALLOW", + "$project_name:$user_name:$opstr{$op}:$ref:$old:$new", + $_[0]); + print STDERR "debug: allow $_[0]\n" if $debug; + exit 0; +} + +sub info ($) { + logmsg("INFO", $_[0]); + print STDERR "info: $_[0]\n" if $debug; +} + +sub git_value (@) { + my $fd; + + open($fd,'-|','git',@_); + local $_ = <$fd>; + chop; + close($fd); + return $_; +} + +sub match_user($) { + my $user = shift; + return 1 if ($user eq 'all'); + return 0 if ($user eq 'none'); + if ($user =~ /^%(.+)/) { + my ($name,$passwd,$gid,$members) = getgrnam($1) or return 0; + my @a = split(/\s+/,$members); + for (my $i = 0; $i <= $#a; $i++) { + return 1 if $a[$i] eq $user_name; + } + } elsif ($user eq $user_name) { + return 1; + } + return 0; +} + +sub match_ref($) { + my $expr = shift; + return ($ref =~ /$expr/) if ($expr =~ /^\^/); + return ("$ref/" eq $expr or index($ref, $expr) == 0) if ($expr =~ /\/$/); + return $ref eq $expr; +} + +sub check_acl($$$) { + my $project = shift; + my $op = shift; + my $ref = shift; + my $fd; + my $line = 0; + my @ret; + + my $filename = git_value('config', 'hooks.aclfile'); + allow("no ACL configured for $project") + unless defined($filename); + + open($fd, "<", $filename) or deny("cannot open configuration file: $!"); + while (<$fd>) { + ++$line; + chomp; + s/^\s+//; + s/\s+$//; + s/#.*//; + next if ($_ eq ""); + my @x = split(/\s+/, $_, 5); + + deny("unknown keyword", "$filename:$line") + unless ($x[0] eq 'allow' || $x[0] eq 'deny'); + deny("malformed line", "$filename:$line") + unless $#x >= 2; + + next if ($x[1] ne $project); + next unless match_user($x[2]); + next if ($#x >= 3 && index(uc $x[3], $op) == -1); + next if ($#x == 4 && !match_ref($x[4])); + + allow("$filename:$line") if ($x[0] eq 'allow'); + deny("you are not permitted to " . $opstr{$op} . " $ref", + "$filename:$line"); + } + close($fd); + allow("default rule"); +} + +#### + +# Sanity checks +deny "don't run this script from the command line" unless ($git_dir); + +$debug = git_value('config', '--bool', 'hooks.acldebug') unless ($debug); +$logfile = git_value('config', 'hooks.acllog'); +if ($logfile && $logfile !~ /[>|]/) { + $logfile = ">>$logfile"; +} +$quiet = git_value('config', 'hooks.aclquiet') unless ($debug); + +deny "need a ref name" unless $ref; +deny "bogus 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 "no such user" unless $user_name; +allow "no change requested" if $old eq $new; + +$project_name = File::Spec->rel2abs($git_dir); +$project_name =~ m,/([^/]+)(?:\.git|/\.git)$,; +$project_name = $1; + +if ($old =~ /^0{40}$/) { + $op = 'C'; +} elsif ($new =~ /^0{40}$/) { + $op = 'D'; +} elsif ($ref =~ m,^heads/, && $old eq git_value('merge-base',$old,$new)) { + $op = 'U'; +} else { + $op = 'R'; +} + +info "$user_name requested $opstr{$op} on $ref in $project_name"; + +check_acl($project_name, $op, $ref); + +# Finis + + + + + + diff --git a/upload/gnupload b/upload/gnupload index 3ec9761..8863771 100755 --- a/upload/gnupload +++ b/upload/gnupload @@ -344,12 +344,13 @@ upload() { done | $dbg sftp -b - download.gnu.org.ua:/incoming/${destdir%%/*} ;; *@download.gnu.org.ua:alpha/*|*@download.gnu.org.ua:ftp/*|*@download.gnu.org.ua:test/*) + user=${dest%%@*} mkdirective "${destdir#*/}" "$base" "$file" "$stmt" echo "$passphrase" | $GPG --passphrase-fd 0 --clearsign $base.directive for f in $files $base.directive.asc do echo put $f - done | $dbg sftp -b - download.gnu.org.ua:/incoming/${destdir%%/*} + done | $dbg sftp -b - ${user}@download.gnu.org.ua:/incoming/${destdir%%/*} ;; /*) mkdirective "$destdir" "$base" "$file" "$stmt" -- cgit v1.2.1