diff options
author | Sergey Poznyakoff <gray@gnu.org.ua> | 2013-06-23 12:41:49 +0300 |
---|---|---|
committer | Sergey Poznyakoff <gray@gnu.org.ua> | 2013-06-23 12:41:49 +0300 |
commit | e5b2691b92e2d67b65ee3298b048124c761fd622 (patch) | |
tree | d77e407448eaf816283b9ff4191f2d10526d6222 /gitaclhook | |
parent | 1e744ae787312413fb8600dd622b5fd7374b45ae (diff) | |
download | gitaclhook-e5b2691b92e2d67b65ee3298b048124c761fd622.tar.gz gitaclhook-e5b2691b92e2d67b65ee3298b048124c761fd622.tar.bz2 |
Rewrite gitaclhook in a modular way.
The new version provides two storage engines: File and LDAP.
* git/MANIFEST: New file.
* git/Makefile.PL: New file.
* git/gitaclhook: Rewrite.
* git/lib/GitACL.pm: New file.
* git/lib/GitACL/File.pm: New file.
* git/lib/GitACL/LDAP.pm: New file.
Diffstat (limited to 'gitaclhook')
-rwxr-xr-x | gitaclhook | 355 |
1 files changed, 123 insertions, 232 deletions
@@ -15,11 +15,12 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. use strict; -use File::Spec; +use GitACL; +use GitACL::File; use Pod::Man; use Pod::Usage; use Getopt::Long qw(:config gnu_getopt no_ignore_case); - + =head1 NAME gitaclhook - control access to git repositories @@ -28,7 +29,7 @@ gitaclhook - control access to git repositories B<gitaclhook> I<refname> I<old-sha1> I<new-sha1> -B<gitacthook> B<--test> I<REPO> I<USER> I<OP> I<REF> +B<gitacthook> [B<--debug>] B<--test> I<REPO> I<USER> I<OP> I<REF> B<gitaclhook --help> @@ -38,12 +39,24 @@ This program is intended to be run as an "update" hook by git. It is called by B<git-receive-pack> with arguments: I<refname> I<old-sha1> I<new-sha1>. -If the B<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 B<hooks.aclfile> is not defined, update is allowed -unconditionally. - +The program reads access control lists from the storage engine +specified in the repository's config file and allows or denies +the update depending on their settings. If no storage engine is +defined update is allowed unconditionally. If it is defined, but +is not available (e.g. the disk file does not exist or the LDAP +server cannot be reached, the update is denied. + +Two storage engines are supported: B<File>, which reads access control +lists from a disk file, and B<LDAP>, which obtains them from LDAP. +The engine to use is defined by the B<hooks.acltype> configuration keyword. +The default is B<File>. + =head1 ACL FILE + +The ACL file is used when the B<File> storage engine is requested. The +path to the file must be given via the B<hooks.aclfile> configuration +keyword. If B<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 @@ -147,21 +160,88 @@ with B<v> and containing only digits afterwards, and the user B<admin> will be allowed to do anything he pleases. No other users will be allowed to update that repository. +=head1 LDAP + +The LDAP storage engine is requested by the following configuration statement: + + [hooks] + acltype = LDAP + +The URI of the LDAP server to use and other data necessary to access it +are read from the file name given in the B<hooks.aclldapconf> variable, +or from B</etc/ldap.conf>, if it is not defined. LDAP access control +entries are similar to the plaintext file ACLs. Each entry has the +following attrubutes: + +=over 4 + +=item B<gitAclProject> [mandatory] + +The project this entry applies to. + +=item B<gitAclVerb> [mandatory] + +The control verb. + +=item B<gitAclUser> [optional] + +The user name or group (B<%>I<GROUPNAME>) specification. + +=item B<gitAclOp> [optional] + +The list of operation codes. + +=item B<gitAclRef> [optional] + +Git ref. + +=item B<gitAclOrder> [optional] + +Sorting order (see below). + +=back + +The program first reads all entries with the B<gitAclProject> attribute +matching the requested project name. The obtained entries are sorted +by the value of B<gitAclOrder> attribute. Entries without this attributes +are assumed to have sorting order B<0>. Entries with the project name +B<all> are sorted last. Entries with the same sorting order are sorted +by the count of attributes they carry (in the reverse order). Thus, the +most specific entries precede the least specific entries in the resulting +list. + +Each list entry is then matched against the current tuple (I<PROJECT>, I<USER>, +I<OP>, I<REF>), much the same way as described in B<RULE MATCHING>. Missing +attributes always match. The special B<gitAclProject> value B<all> matches +all project names. + +If no matching entry is found, the update is allowed. + =head1 CONFIGURATION SETTINGS =over 4 +=item B<hooks.acltype> STRING + +Type of the storage engine. Valid values are B<File> (default) and B<LDAP>. + =item B<hooks.aclfile> STRING -Name of the ACL file. +For the B<File> storage engine, name of the ACL file. + +=item B<hooks.aclldapconf> STRING + +For the B<LDAP> storage engine, the name of the configuration file to use +instead of B</etc/ldap.conf>. =item B<hooks.acllog> STRING Send log info to this file. -=item B<hooks.acldebug> BOOL +=item B<hooks.acldebug> NUMBER -Enable debugging. +Enable debugging. The bigger the number, the more debugging info will +be displayed. =item B<hooks.aclquiet> BOOL @@ -178,19 +258,20 @@ from the environment variable B<REMOTE_USER>. =head1 TEST MODE -The B<--test> option provides a mechanism for testing access control lists -from the command line. When given this option, B<gitaclhook> expects four -arguments: +The B<--test> (B<-t>) option provides a mechanism for testing access control +lists from the command line. The syntax is: =over 4 -B<gitacthook> B<--test> I<REPO> I<USER> I<OP> I<REF> +B<gitacthook> [B<--debug>] [B<-d>] B<--test> I<REPO> I<USER> I<OP> I<REF> =back I<REPO> is a pathname of the repository to test, I<USER> is the username, I<OP> is the operation code and I<REF> is the reference. +Optional B<--debug> (B<-d>) options increment the debugging level. + =head1 ENVIRONMENT The program uses following environment variables: @@ -231,242 +312,52 @@ B<git-receive-pack>(1). =head1 AUTHOR -Sergey Poznyakoff, <gray@gno.org> +Sergey Poznyakoff, <gray@gnu.org> =cut -my $debug_level = $ENV{GIT_UPDATE_DEBUG} > 0; -my $logfile; -my $quiet; -my ($user_name) = getpwuid $<; -my $git_dir = $ENV{GIT_DIR}; -my $ref; -my $old; -my $new; - -my $project_name; -my $op; - -my %opstr = ('C' => 'create', - 'D' => 'delete', - 'U' => 'update', - 'R' => 'rewind/rebase'); - -sub debug($$) { - my ($level,$msg) = @_; - if ($level <= $debug_level) { - print STDERR "debug: $msg\n"; - } -} - -sub logmsg($$;$) { - return 0 unless $logfile; - - my $status = shift; - my $message = shift; - my $loc = shift; - my $fd; +my $script; +($script = $0) =~ s/.*\///; - open($fd, $logfile); - if ($loc) { - print $fd "$status:$loc: $message\n"; - } else { - print $fd "$status: $message\n"; - } - close($fd); -} - -sub deny($;$) { +sub abend($) { my $msg = shift; - my $loc = shift; - - logmsg("DENY", - "$project_name:$user_name:$opstr{$op}:$ref:$old:$new: $msg", - $loc); - - debug(1, "denied by $loc") if $loc; - print STDERR "denied: $msg\n" unless $quiet; - exit 1; -} - -sub allow($) { - logmsg("ALLOW", - "$project_name:$user_name:$opstr{$op}:$ref:$old:$new", - $_[0]); - debug(1, "allow $_[0]"); - exit 0; -} - -sub info($) { - logmsg("INFO", $_[0]); - print STDERR "info: $_[0]\n" if $debug_level; -} - -sub project_name($) { - my $dir = shift; - - File::Spec->rel2abs($dir) =~ m,/([^/]+)(?:\.git|/\.git)$,; - return $1; + print STDERR "$script: $msg\n"; + exit 2; } -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; -} +my %args; -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 match_tuple($) { - my $tuple = shift; - my @x = @{$tuple}; - - return ( \&deny, "malformed line" ) unless $#x >= 2; - return ( \&deny, "unknown keyword" ) - unless ($x[0] eq 'allow' || $x[0] eq 'deny'); - - return ( 0, "project mismatch" ) - if ($x[1] ne "*" and $x[1] ne $project_name); - return ( 0, "user mismatch" ) - unless match_user($x[2]); - return ( 0, "op mismatch" ) - if ($#x >= 3 && index(uc $x[3], $op) == -1); - return ( 0, "ref mismatch" ) - if ($#x == 4 && !match_ref($x[4])); - if ($x[0] eq 'allow') { - return ( \&allow ); - } else { - my $s = "you are not permitted to " . $opstr{$op} . " $ref"; - return ( \&deny, $s ); - } -} - - -sub check_acl { - my $fd; - my $line = 0; - my @ret; - - my $filename = git_value('config', 'hooks.aclfile'); - allow("no ACL configured for $project_name") - 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); - - my @res = match_tuple(\@x); - if ($res[0] == 0) { - debug(2, "$filename:$line: $res[1]"); - next; - } - close($fd); - if ($res[1]) { - $res[0]->($res[1], "$filename:$line"); - } else { - $res[0]->("$filename:$line"); - } - exit(127); - } - close($fd); - allow("default rule"); -} - -#### - -# Sanity checks -if ($git_dir) { - $ref = $ARGV[0]; - $old = $ARGV[1]; - $new = $ARGV[2]; - - deny "bad old value $old" unless $old =~ /^[a-z0-9]{40}$/; - deny "bad new value $new" unless $new =~ /^[a-z0-9]{40}$/; - - 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'; - } - - $debug_level = git_value('config', '--bool', 'hooks.acldebug') - unless ($debug_level); - $logfile = git_value('config', 'hooks.acllog'); - if ($logfile && $logfile !~ /[>|]/) { - $logfile = ">>$logfile"; - } - $quiet = git_value('config', 'hooks.aclquiet') unless ($debug_level); -} else { +unless ($ENV{GIT_DIR}) { + my $debug; my $test; GetOptions("help|h" => sub { pod2usage(-exitstatus => 0, -verbose => 2); }, - "debug|d+" => \$debug_level, + "debug|d+" => \$debug, "test|t" => \$test) or exit (3); if ($test) { abend("--test requires four arguments") unless ($#ARGV == 3); - $git_dir = $ENV{GIT_DIR} = $ARGV[0]; - $user_name = $ARGV[1]; - $op = $ARGV[2]; - deny("invalid op") unless defined($opstr{$op}); - $ref = $ARGV[3]; - $old = '0000000000000000000000000000000000000000'; - $new = '0000000000000000000000000000000000000001'; + $args{git_dir} = $ENV{GIT_DIR} = $ARGV[0]; + $args{user_name} = $ARGV[1]; + $args{op} = $ARGV[2]; + $args{ref} = $ARGV[3]; + $args{old} = '0000000000000000000000000000000000000000'; + $args{new} = '0000000000000000000000000000000000000001'; + $args{debug} = $debug; } else { - deny "try \"$0 --help\" for fore info" + abend("try \"$script --help\" for fore info") } +} else { + abend("bad number of arguments") unless ($#ARGV == 2); + $args{git_dir} = $ENV{GIT_DIR}; + $args{ref} = $ARGV[0]; + $args{old} = $ARGV[1]; + $args{new} = $ARGV[2]; } -my $httpdusr = git_value('config', 'hooks.httpd-user'); -if (defined($httpdusr) and $user_name eq $httpdusr) { - deny "need authenticated user" unless $ENV{AUTH_TYPE}; - $user_name = $ENV{REMOTE_USER}; -} - -deny "need a ref name" unless $ref; -deny "bogus ref $ref" unless $ref =~ s,^refs/,,; -deny "no such user" unless $user_name; -allow "no change requested" if $old eq $new; - -$project_name = project_name($git_dir); - -info "$user_name requested $opstr{$op} on $ref in $project_name"; +my $gitacl = GitACL->new(%args); +$gitacl->check; -&check_acl; +exit 0; # Finis |