Merge branch 'master' into wildrepos

lots of conflicts, esp in gl-auth-command, due to refactoring the
"special commands" stuff on master

Conflicts:
	doc/3-faq-tips-etc.mkd
	src/gitolite.pm
	src/gl-auth-command
	src/gl-compile-conf
This commit is contained in:
Sitaram Chamarty 2010-02-04 14:42:10 +05:30
commit c43560d2ef
7 changed files with 279 additions and 75 deletions

View file

@ -99,6 +99,11 @@ or in Unix, perl, shell, etc.)... well I can't afford 1000 USD rewards like
djb, so you'll have to settle for 1000 INR (Indian Rupees) as a "token" prize
:-)
Update 2010-01-31: this security promise does not apply if you enable any of
the external command helpers (like rsync). It's probably quite secure, but I
just haven't thought about it enough to be able to make such promises, like I
can for the rest of "master".
----
### contact and license

View file

@ -147,10 +147,18 @@ repo git
# DENY/EXCLUDE RULES
# ***IMPORTANT NOTE: if you use deny rules, the order of the rules also makes
# a difference, where earlier it did not. Please review your ruleset
# carefully or test it. In particular, do not use `@all` in a deny rule -- it
# won't work as you might expect***.
# ***IMPORTANT NOTES ABOUT "DENY" RULES***:
# - deny rules do NOT affect read access. They only apply to `W` and `+`.
#
# - when using deny rules, the order of your rules starts to matter, where
# earlier it did not. The first matching rule applies, where "matching" is
# defined as either permitting the operation you're attempting (`W` or `+`),
# which results in success, or a "deny" (`-`), which results in failure.
# (As before, a fallthrough also results in failure).
#
# - do not use `@all` when your config has any deny rules; it won't work as
# you probably expect it to!
# in the example above, you cannot easily say "anyone can write any tag,
# except version tags can only be written by junio". The following might look
@ -166,10 +174,6 @@ repo git
- refs/tags/v[0-9] = linus pasky @others
RW refs/tags/ = junio linus pasky @others
# Briefly, the rule is: the first matching refex that has the operation you're
# looking for (`W` or `+`), or a minus (`-`), results in success, or failure,
# respectively. A fallthrough also results in failure
# FILE/DIR NAME BASED RESTRICTIONS
# --------------------------------
@ -264,3 +268,21 @@ repo gitolite
# security reasons.
# - you can also use an absolute path if you like, although in the interests
# of cloning the admin-repo sanely you should avoid doing this!
# EXTERNAL COMMAND HELPERS -- RSYNC
# ---------------------------------
# If $RSYNC_BASE is non-empty, the following config entries come into play
# (otherwise they are ignored):
# a "fake" git repository to collect rsync rules. Gitolite does not
# auto-create any repo whose name starts with EXTCMD/
repo EXTCMD/rsync
# grant permissions to files/dirs within the $RSYNC_BASE tree. A leading
# NAME/ is required as a prefix; the actual path starts after that. Matching
# follows the same rules as elsewhere in gitolite.
RW NAME/ = sitaram
RW NAME/foo/ = user1
R NAME/bar/ = user2
# just to remind you that these are perl regexes, not shell globs
RW NAME/baz/.*/*.c = user3

View file

@ -106,6 +106,26 @@ $GIT_PATH="";
# syntax: space separated list of gitolite usernames in *one* string variable.
# $SHELL_USERS = "alice bob";
# --------------------------------------
# if you want to enable the "htpasswd" command, give this the absolute path to
# whatever file apache (etc) expect to find the passwords in.
$HTPASSWD_FILE = "";
# Look in doc/3 ("easier to link gitweb authorisation with gitolite" section)
# for more details on using this feature.
# --------------------------------------
# EXTERNAL COMMAND HELPER -- RSYNC
#
# base path of all the files that are accessible via rsync. Must be an
# absolute path. Leave it undefined or set to the empty string to disable the
# rsync helper.
$RSYNC_BASE = "";
# $RSYNC_BASE = "/home/git/up-down";
# $RSYNC_BASE = "/tmp/up-down";
# --------------------------------------
# per perl rules, this should be the last line in such a file:
1;

View file

@ -27,6 +27,7 @@ In this document:
* "personal" branches
* custom hooks and custom git config
* repos named with wildcards
* access control for external commands
* design choices
* keeping the parser and the access control separate
@ -331,9 +332,25 @@ This requires that:
* the HTTP auth should use the same username (like "sitaram") as used in the
gitolite config (for the corresponding user)
Once that is done, it's easy. Gitweb allows you to specify a subroutine to
decide on access. We use that feature and tie it to gitolite. Sample code
(untested, munged from something I saw [here][leho]) is given below.
Normally a superuser sets up passwords for users using the "htpasswd" command,
but this is an administrative chore.
Robin Smidsrød had the *great* idea that, since each user already has pubkey
access to `git@server`, this gives us a very neat way of using gitolite to let
the users *manage their own HTTP passwords*. Here's how:
* setup apache so that the htaccess file it looks for is owned by the "git"
user
* in the `~/.gitolite.rc` file, look for the variable `$HTPASSWD_FILE` and
point it to this file
* tell your users to type in `ssh git@server htpasswd` to set or change
their HTTP passwords
Here's the rest of how it hangs together.
Gitweb allows you to specify a subroutine to decide on access. We use that
feature and tie it to gitolite. Sample code (untested by me, but others do
use it, munged from something I saw [here][leho]) is given below.
Note the **utter simplicity** of the actual check (just 1 line!). This is an
unexpected piece of luck coming from the decision to keep the config parse
@ -349,7 +366,7 @@ already done and we just use it!
$projectroot = '/home/git/repositories/';
my $gl_conf_compiled = '/home/git/.gitolite/conf/gitolite.conf-compiled.pm';
# I assume this gives us the HTTP auth username
# I am told this gives us the HTTP auth username
my $username = $cgi->remote_user;
# ----------
@ -359,10 +376,11 @@ already done and we just use it!
die "parse $gl_conf_compiled failed: " . ($! or $@) unless do $gl_conf_compiled;
# this is gitweb's mechanism; it calls whatever sub is pointed at by this
# variable to decide access yes/no
# variable to decide access yes/no. Gitweb calls it with one argument
# containing the full path of the repo being accessed
$export_auth_hook = sub {
my $reponame = shift;
# gitweb passes us the full repo path; so we strip the beginning...
# take the full path provided, strip the beginning...
$reponame =~ s/\Q$projectroot\E\/?//;
# ...and the end, to get the repo name as it is specified in gitolite conf
$reponame =~ s/\.git$//;
@ -499,12 +517,9 @@ that code path to better use :-)
#### "exclude" (or "deny") rules
***IMPORTANT CAVEAT: if you use deny rules, the order of the rules also makes
a difference, where earlier it did not. Please review your ruleset carefully
or test it. In particular, do not use `@all` in a deny rule -- it won't work
as you might expect***. Also, deny rules are only processed in the second
level checks (see "two levels of access rights checking" above), which means
they only apply to write operations.
Here is an illustrative explanation of "deny" rules. However, please be sure
to read the "DENY/EXCLUDE RULES" section in `conf/example.conf` for important
notes/caveats before using "deny" rules.
Take a look at the following snippet, which *seems* to say that "bruce" can
write versioned tags (anything containing `refs/tags/v[0-9]`), but the other
@ -614,6 +629,20 @@ per-repo "gitconfig" settings. Please see `doc/2-admin.mkd` and
**This feature only exists in the "wildrepos" branch!** Please see
`doc/4-wildcard-repositories.mkd` for all the details.
#### access control for external commands
Gitolite now has a mechanism for allowing access control for arbitrary
external commands, as long as they are invoked via ssh and present a
server-side command that contains enough information to make an access control
decision. The first (and only, so far) such command implemented is rsync.
Note that this is incompatible with giving people shell access as described in
`doc/6-ssh-troubleshooting.mkd` -- people who have shell access are not
subject to this mechanism (it wouldn't make sense to try and control someone
who has shell access anyway).
Please see the config files (both of them) for examples and usage.
### design choices
#### keeping the parser and the access control separate

View file

@ -292,4 +292,118 @@ sub expand_wild
}
}
# ----------------------------------------------------------------------------
# S P E C I A L C O M M A N D S
# ----------------------------------------------------------------------------
sub special_cmd
{
my ($GL_ADMINDIR, $GL_CONF_COMPILED, $RSYNC_BASE, $HTPASSWD_FILE) = @_;
my $cmd = $ENV{SSH_ORIGINAL_COMMAND};
my $user = $ENV{GL_USER};
# check each special command we know about and call it if enabled
if ($cmd eq 'info') {
&report_basic($GL_ADMINDIR, $GL_CONF_COMPILED, $user);
print "you also have shell access\n\r" if $shell_allowed;
} elsif ($HTPASSWD_FILE and $cmd eq 'htpasswd') {
&ext_cmd_htpasswd($HTPASSWD_FILE);
} elsif ($RSYNC_BASE and $cmd =~ /^rsync /) {
&ext_cmd_rsync($GL_CONF_COMPILED, $RSYNC_BASE, $cmd);
} else {
# if the user is allowed a shell, just run the command
exec $ENV{SHELL}, "-c", $cmd if $shell_allowed;
die "bad command: $cmd\n";
}
}
# ----------------------------------------------------------------------------
# generic check access routine
# ----------------------------------------------------------------------------
sub check_access
{
my ($GL_CONF_COMPILED, $repo, $path, $perm) = @_;
my $ref = "NAME/$path";
&parse_acl($GL_CONF_COMPILED);
# until I do some major refactoring (which will bloat the update hook a
# bit, sadly), this code duplicates stuff in the current update hook.
my @allowed_refs;
# we want specific perms to override @all, so they come first
push @allowed_refs, @ { $repos{$repo}{$ENV{GL_USER}} || [] };
push @allowed_refs, @ { $repos{$repo}{'@all'} || [] };
for my $ar (@allowed_refs) {
my $refex = (keys %$ar)[0];
next unless $ref =~ /^$refex/;
die "$perm $ref $ENV{GL_USER} DENIED by $refex\n" if $ar->{$refex} eq '-';
return if ($ar->{$refex} =~ /\Q$perm/);
}
die "$perm $ref $ENV{GL_REPO} $ENV{GL_USER} DENIED by fallthru\n";
}
# ----------------------------------------------------------------------------
# external command helper: rsync
# ----------------------------------------------------------------------------
sub ext_cmd_rsync
{
my ($GL_CONF_COMPILED, $RSYNC_BASE, $cmd) = @_;
# test the command patterns; reject if they don't fit. Rsync sends
# commands that looks like one of these to the server (the first one is
# for a read, the second for a write)
# rsync --server --sender -some.flags . some/path
# rsync --server -some.flags . some/path
die "bad rsync command: $cmd"
unless $cmd =~ /^rsync --server( --sender)? -[\w.]+(?: --(?:delete|partial))* \. (\S+)$/;
my $perm = "W";
$perm = "R" if $1;
my $path = $2;
die "I dont like absolute paths in $cmd\n" if $path =~ /^\//;
die "I dont like '..' paths in $cmd\n" if $path =~ /\.\./;
# ok now check if we're permitted to execute a $perm action on $path
# (taken as a refex) using rsync.
&check_access($GL_CONF_COMPILED, 'EXTCMD/rsync', $path, $perm);
# that should "die" if there's a problem
wrap_chdir($RSYNC_BASE);
&log_it("$ENV{GL_TS}\t$ENV{SSH_ORIGINAL_COMMAND}\t$ENV{USER}\n");
exec $ENV{SHELL}, "-c", $ENV{SSH_ORIGINAL_COMMAND};
}
# ----------------------------------------------------------------------------
# external command helper: htpasswd
# ----------------------------------------------------------------------------
sub ext_cmd_htpasswd
{
my $HTPASSWD_FILE = shift;
die "$HTPASSWD_FILE doesn't exist or is not writable\n" unless -w $HTPASSWD_FILE;
$|++;
print <<EOFhtp;
Please type in your new htpasswd at the prompt. You only have to type it once.
NOTE THAT THE PASSWORD WILL BE ECHOED, so please make sure no one is
shoulder-surfing, and make sure you clear your screen as well as scrollback
history after you're done (or close your terminal instance).
EOFhtp
print "new htpasswd:";
my $password = <>;
$password =~ s/[\n\r]*$//;
my $rc = system("htpasswd", "-b", $HTPASSWD_FILE, $ENV{GL_USER}, $password);
die "htpasswd command seems to have failed with $rc return code...\n" if $rc;
}
1;

View file

@ -24,7 +24,7 @@ use warnings;
# ----------------------------------------------------------------------------
# these are set by the "rc" file
our ($GL_LOGT, $GL_CONF_COMPILED, $REPO_BASE, $GIT_PATH, $REPO_UMASK, $GL_ADMINDIR);
our ($GL_LOGT, $GL_CONF_COMPILED, $REPO_BASE, $GIT_PATH, $REPO_UMASK, $GL_ADMINDIR, $RSYNC_BASE, $HTPASSWD_FILE);
# and these are set by gitolite.pm
our ($R_COMMANDS, $W_COMMANDS, $REPONAME_PATT, $REPOPATT_PATT);
our %repos;
@ -48,6 +48,8 @@ $ENV{PATH} .= ":$GIT_PATH" if $GIT_PATH;
# set the umask before creating any files
umask($REPO_UMASK);
my $repo_base_abs = ( $REPO_BASE =~ m(^/) ? $REPO_BASE : "$ENV{HOME}/$REPO_BASE" );
# ----------------------------------------------------------------------------
# start...
# ----------------------------------------------------------------------------
@ -63,25 +65,40 @@ if ($ARGV[0] eq '-s') {
# first, fix the biggest gripe I have with gitosis, a 1-line change
my $user=$ENV{GL_USER}=shift; # there; now that's available everywhere!
# ----------------------------------------------------------------------------
# logging, timestamp env vars
# ----------------------------------------------------------------------------
# timestamp
my ($s, $min, $h, $d, $m, $y) = (localtime)[0..5];
$y += 1900; $m++; # usual adjustments
for ($s, $min, $h, $d, $m) {
$_ = "0$_" if $_ < 10;
}
$ENV{GL_TS} = "$y-$m-$d.$h:$min:$s";
# substitute template parameters and set the logfile name
$GL_LOGT =~ s/%y/$y/g;
$GL_LOGT =~ s/%m/$m/g;
$GL_LOGT =~ s/%d/$d/g;
$ENV{GL_LOG} = $GL_LOGT;
# ----------------------------------------------------------------------------
# sanity checks on SSH_ORIGINAL_COMMAND
# ----------------------------------------------------------------------------
# print basic access info if SSH_ORIGINAL_COMMAND does not exist
# no SSH_ORIGINAL_COMMAND given...
unless ($ENV{SSH_ORIGINAL_COMMAND}) {
# unless the user is allowed to use a shell
# if the user is allowed to use a shell, give him one
if ($shell_allowed) {
my $shell = $ENV{SHELL};
$shell =~ s/.*\//-/; # change "/bin/bash" to "-bash"
exec { $ENV{SHELL} } $shell;
}
&report_basic($GL_ADMINDIR, $GL_CONF_COMPILED, $user);
exit 1;
# otherwise, pretend he typed in "info" and carry on...
$ENV{SSH_ORIGINAL_COMMAND} = 'info';
}
my $cmd = $ENV{SSH_ORIGINAL_COMMAND};
my $repo_base_abs = ( $REPO_BASE =~ m(^/) ? $REPO_BASE : "$ENV{HOME}/$REPO_BASE" );
# ----------------------------------------------------------------------------
# get and set perms for actual repo created by wildcard-autoviv
# ----------------------------------------------------------------------------
@ -92,7 +109,8 @@ my $CUSTOM_COMMANDS=qr/^\s*(expand|getperms|setperms)\s/;
# back; they all blithely take advantage of the fact that processing custom
# commands is sort of a dead end for normal (git) processing
if ($cmd =~ $CUSTOM_COMMANDS) {
if ($ENV{SSH_ORIGINAL_COMMAND} =~ $CUSTOM_COMMANDS) {
my $cmd = $ENV{SSH_ORIGINAL_COMMAND};
my ($verb, $repo) = ($cmd =~ /^\s*(\S+)\s+\/?(.*?)(?:.git)?$/);
if ($repo =~ $REPONAME_PATT and $verb =~ /getperms|setperms/) {
# with an actual reponame, you can "getperms" or "setperms"
@ -108,35 +126,36 @@ if ($cmd =~ $CUSTOM_COMMANDS) {
exit 0;
}
# people allowed to get a shell can get basic access info by asking nicely
if ($cmd eq 'info') {
&report_basic($GL_ADMINDIR, $GL_CONF_COMPILED, $user);
print "you also have shell access\n\r" if $shell_allowed;
exit 0;
}
# ----------------------------------------------------------------------------
# normal (git) processing
# non-git commands
# ----------------------------------------------------------------------------
# split into command and arguments; the pattern allows old style as well as
# new style: "git-subcommand arg" or "git subcommand arg", just like gitosis
# does, although I'm not sure how necessary that is
#
# keep in mind this is how git sends across the command:
# git-receive-pack 'reponame.git'
# including the single quotes
# if the command does NOT fit the pattern of a normal git command, send it off
# somewhere else...
my ($verb, $repo) = ($cmd =~ /^\s*(git\s+\S+|\S+)\s+'\/?(.*?)(?:\.git)?'/);
# side notes on detecting a normal git command: the pattern we check allows
# old style as well as new style ("git-subcommand arg" or "git subcommand
# arg"), just like gitosis does, although I'm not sure how necessary that is.
# Currently, this is how git sends across the command (including the single
# quotes):
# git-receive-pack 'reponame.git'
my ($verb, $repo) = ($ENV{SSH_ORIGINAL_COMMAND} =~ /^\s*(git\s+\S+|\S+)\s+'\/?(.*?)(?:\.git)?'/);
unless ( $verb and ( $verb =~ $R_COMMANDS or $verb =~ $W_COMMANDS ) and $repo and $repo =~ $REPONAME_PATT ) {
# if the user is allowed a shell, just run the command
exec $ENV{SHELL}, "-c", $ENV{SSH_ORIGINAL_COMMAND} if $shell_allowed;
# otherwise, whine
die "bad command: $cmd\n";
# ok, it's not a normal git command; call the special command helper
&special_cmd ($GL_ADMINDIR, $GL_CONF_COMPILED, $RSYNC_BASE, $HTPASSWD_FILE);
exit;
}
die "$repo ends with a slash; I don't like that\n" if $repo =~ /\/$/;
die "$repo has two consecutive periods; I don't like that\n" if $repo =~ /\.\./;
# reponame
$ENV{GL_REPO}=$repo;
# ----------------------------------------------------------------------------
# the real git commands (git-receive-pack, etc...)
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# first level permissions check
# ----------------------------------------------------------------------------
@ -163,32 +182,11 @@ die "$perm access for $repo DENIED to $user\n"
unless $repos{$repo}{$perm}{$user}
or $repos{$repo}{$perm}{'@all'};
# ----------------------------------------------------------------------------
# logging, timestamp. also setup env vars for later
# ----------------------------------------------------------------------------
# reponame
$ENV{GL_REPO}=$repo;
# timestamp
my ($s, $min, $h, $d, $m, $y) = (localtime)[0..5];
$y += 1900; $m++; # usual adjustments
for ($s, $min, $h, $d, $m) {
$_ = "0$_" if $_ < 10;
}
$ENV{GL_TS} = "$y-$m-$d.$h:$min:$s";
# substitute template parameters and set the logfile name
$GL_LOGT =~ s/%y/$y/g;
$GL_LOGT =~ s/%m/$m/g;
$GL_LOGT =~ s/%d/$d/g;
$ENV{GL_LOG} = $GL_LOGT;
&log_it("$ENV{GL_TS}\t$ENV{SSH_ORIGINAL_COMMAND}\t$user\n");
# ----------------------------------------------------------------------------
# over to git now
# ----------------------------------------------------------------------------
&log_it("$ENV{GL_TS}\t$ENV{SSH_ORIGINAL_COMMAND}\t$user\n");
$repo = "'$REPO_BASE/$repo.git'";
exec("git", "shell", "-c", "$verb $repo");

View file

@ -312,7 +312,6 @@ sub parse_conf_file
die "$WARN $fragment attempting to set description for $repo\n" if
$fragment ne 'master' and $fragment ne $repo and ($groups{"\@$fragment"}{$repo} || '') ne 'master';
$desc{"$repo.git"} = $desc;
$owner =~ s/ /+/g if $owner; # gitweb/INSTALL wants more, but meh...!
$owner{"$repo.git"} = $owner || '';
}
else
@ -389,8 +388,9 @@ wrap_chdir("$repo_base_abs");
for my $repo (sort keys %repos) {
next unless $repo =~ $REPONAME_PATT;
print STDERR "creating $repo...\n";
next if $repo =~ m(^EXTCMD/); # these are not real repos
unless (-d "$repo.git") {
print STDERR "creating $repo...\n";
new_repo($repo, "$GL_ADMINDIR/src/hooks");
# new_repo would have chdir'd us away; come back
wrap_chdir("$repo_base_abs");
@ -451,16 +451,32 @@ for my $repo (sort keys %repos) {
$projlist{"$repo.git"} = 1;
# add the description file; no messages to user or error checking :)
$desc{"$repo.git"} and open(DESC, ">", $desc_file) and print DESC $desc{"$repo.git"} . "\n" and close DESC;
if ($owner{"$repo.git"}) {
# set the repository owner
system("git", "--git-dir=$repo.git", "config", "gitweb.owner", $owner{"$repo.git"});
} else {
# remove the repository owner setting
system("git --git-dir=$repo.git config --unset-all gitweb.owner 2>/dev/null");
}
} else {
# delete the description file; no messages to user or error checking :)
unlink $desc_file;
# remove the repository owner setting
system("git --git-dir=$repo.git config --unset-all gitweb.owner 2>/dev/null");
}
# unless there are other gitweb.* keys set, remove the section to keep the
# config file clean
my $keys = `git --git-dir=$repo.git config --get-regexp '^gitweb\\.' 2>/dev/null`;
if (length($keys) == 0) {
system("git --git-dir=$repo.git config --remove-section gitweb 2>/dev/null");
}
}
# update the project list
my $projlist_fh = wrap_open( ">", $PROJECTS_LIST);
for my $proj (sort keys %projlist) {
print $projlist_fh "$proj" . ( $owner{$proj} ? " $owner{$proj}" : "" ) . "\n";
print $projlist_fh "$proj\n";
}
close $projlist_fh;