#! /usr/bin/perl # Copyright (C) 2013, 2014 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 GitACL; use Pod::Man; use Pod::Usage; use Getopt::Long qw(:config gnu_getopt no_ignore_case); =head1 NAME gitaclhook - control access to git repositories =head1 SYNOPSIS B I I I B [B<--debug>] B<--test> I I I I B =head1 DESCRIPTION This program is intended to be run as an "update" hook by git. It is called by B with arguments: I I I. 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, which reads access control lists from a disk file, and B, which obtains them from LDAP. The engine to use is defined by the B configuration keyword. The default is B. =head1 ACL FILE The ACL file is used when the B storage engine is requested. The path to the file must be given via the B configuration keyword. If B 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: =over 4 I I I [I I] =back where brackets denote optional parts. The parts of an ACL are: =over 4 =item I Either B or B, to allow or deny the operation, correspondingly. =item I 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 B, then the corresponding name of the project is B. An asterisk matches any project name. =item I[B<@>I] Name of the user. The word B stands for any user, the word B matches no one at all. Otherwise, if this part begins with a percent sign (B<%>), the rest of characters are treated 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 I part, if present, restricts the rule to users coming from IP addresses that match one of the elements in the list. I is a comma-separated list of IP addresses, ranges or CIDRs. An IP range is defined as two IP addresses separated by a minus sign. A CIDR is defined as network address, followed by a slash and length of the netmask in decimal. For example: gray@10.0.0.0/16,192.168.1.0-192.168.10.255 =back The optional parts are: =over 4 =item I Requested operation codes. It is a string consisting of one or more of the following letters (case-insensitive): =over 8 =item B Create new ref. =item B Delete existing ref. =item B Fast-forward existing ref (no commit loss). =item B Rewind or rebase existing ref (commit loss). =back =item I Affected ref, relative to the git B directory. If it begins with a caret (B<^>), it is treated as a Perl regular expression (with the B<^> being its part). If it ends with a B, it is treated as a prefix match, so, e.g., B matches B and anything below. Otherwise, it must match exactly the affected ref. =back =head1 RULE MATCHING The rule applies only if its I and I parts match the project which is being updated and the user who requests the update, its I contains the opcode of the requested operation and I matches the affected ref. Missing I and/or I are treated as a match. If no rule applies, the operation is denied. This can be changed by setting B in Git configuration file. 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 B group will be able to push updates to B, the users from the B group will be allowed to do anything with refs under B and to create tags with names beginning with B and containing only digits afterwards, and the user B 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 variable, or from B, 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 [mandatory] The project this entry applies to. =item B [mandatory] The control verb. =item B [optional] The user name specification (see description in the B section). =item B [optional] The list of operation codes. =item B [optional] Git ref. =item B [optional] Sorting order (see below). =back The program first reads all entries with the B attribute matching the requested project name. The obtained entries are sorted by the value of B attribute. Entries without this attributes are assumed to have sorting order B<0>. Entries with the project name B 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, I, I, I), much the same way as described in B. Missing attributes always match. The special B value B matches all project names. If no matching entry is found, the update is allowed. =head1 CONFIGURATION SETTINGS =over 4 =item B I Type of the storage engine. Valid values are B (default) and B. =item B I For the B storage engine, name of the ACL file. =item B I For the B storage engine, the name of the configuration file to use instead of B. =item B I Send log info to this file. =item B I Enable debugging. The bigger the number, the more debugging info will be displayed. =item B I Suppress diagnostics on stderr. =item B B|B Sets the default rule, i.e. the one that will be executed if no other rule matched the request. Unless defined, B is assumed. =item B I Name of the user httpd runs as. Define it if the repository can be accessed via HTTP(S). If B is run as this user, it will get the name of the user on behalf of which the update is performed from the environment variable B. =item B I Name of the environment variable from where to retrieve remote IP address. Default is B, if B is defined and current user matches it, and B otherwise. The part of the string up to the first space character (if any) is taken as the IP address. =back =head1 TEST MODE The B<--test> (B<-t>) option provides a mechanism for testing access control lists from the command line. The syntax is: =over 4 B [B<--debug>] [B<-d>] B<--test> I I I I =back I is a pathname of the repository to test, I is the username, I is the operation code and I is the reference. Optional B<--debug> (B<-d>) options increment the debugging level. =head1 ENVIRONMENT The program uses following environment variables: =over 4 =item B When set to 1, enables debugging mode. The B configuration variable overrides this setting. =item B Path to the affected repository. =back If updates are performed via HTTP or HTTPS and the B configuration variable is set, the following two variables are used to determine the identity of the user: =over 4 =item B If this variable is not set or set to an empty value, the program will deny the update. =item B The authenticated name of the user. =back =head1 SEE ALSO B(1). =head1 AUTHOR Sergey Poznyakoff, =cut my $script; ($script = $0) =~ s/.*\///; sub abend($) { my $msg = shift; print STDERR "$script: $msg\n"; exit 2; } my %args; unless ($ENV{GIT_DIR}) { my $debug; my $test; GetOptions("help|h" => sub { pod2usage(-exitstatus => 0, -verbose => 2); }, "debug|d+" => \$debug, "test|t" => \$test) or exit (3); if ($test) { abend("--test requires four arguments") unless ($#ARGV == 3); $args{git_dir} = $ENV{GIT_DIR} = $ARGV[0]; $args{user} = $ARGV[1]; $args{op} = $ARGV[2]; $args{ref} = $ARGV[3]; $args{old} = '0000000000000000000000000000000000000000'; $args{new} = '0000000000000000000000000000000000000001'; $args{debug} = $debug; } else { 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 $gitacl = GitACL->new(%args); $gitacl->check; exit 0; # Finis