diff --git a/README.mkd b/README.mkd index 1df3bf8..7f3956a 100644 --- a/README.mkd +++ b/README.mkd @@ -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 diff --git a/conf/example.conf b/conf/example.conf index 9d73aca..6dcc4e2 100644 --- a/conf/example.conf +++ b/conf/example.conf @@ -259,3 +259,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 diff --git a/conf/example.gitolite.rc b/conf/example.gitolite.rc index d53b65f..cf7fe3d 100644 --- a/conf/example.gitolite.rc +++ b/conf/example.gitolite.rc @@ -106,6 +106,17 @@ $GIT_PATH=""; # syntax: space separated list of gitolite usernames in *one* string variable. # $SHELL_USERS = "alice bob"; +# -------------------------------------- + +# 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; diff --git a/doc/3-faq-tips-etc.mkd b/doc/3-faq-tips-etc.mkd index cd3bdb7..edc6351 100644 --- a/doc/3-faq-tips-etc.mkd +++ b/doc/3-faq-tips-etc.mkd @@ -26,6 +26,7 @@ In this document: * "exclude" (or "deny") rules * "personal" branches * custom hooks and custom git config + * access control for external commands * design choices * keeping the parser and the access control separate @@ -608,6 +609,20 @@ You can specify hooks that you want to propagate to all repos, as well as per-repo "gitconfig" settings. Please see `doc/2-admin.mkd` and `conf/example.conf` for 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 diff --git a/src/gitolite.pm b/src/gitolite.pm index 8ad5567..c5bcd9e 100644 --- a/src/gitolite.pm +++ b/src/gitolite.pm @@ -156,4 +156,82 @@ sub report_basic print "$perm\t$r\n\r" if $perm; } } + +# ---------------------------------------------------------------------------- +# E X T E R N A L C O M M A N D H E L P E R S +# ---------------------------------------------------------------------------- + +sub ext_cmd +{ + my ($GL_CONF_COMPILED, $RSYNC_BASE, $cmd) = @_; + + # check each external command we know about and call it if enabled + if ($RSYNC_BASE and $cmd =~ /^rsync /) { + &ext_cmd_rsync($GL_CONF_COMPILED, $RSYNC_BASE, $cmd); + } else { + 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.]+ \. (\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); + exec $ENV{SHELL}, "-c", $ENV{SSH_ORIGINAL_COMMAND}; +} + + 1; diff --git a/src/gl-auth-command b/src/gl-auth-command index f494cc8..1dd2fba 100755 --- a/src/gl-auth-command +++ b/src/gl-auth-command @@ -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); # and these are set by gitolite.pm our ($R_COMMANDS, $W_COMMANDS, $REPONAME_PATT); our %repos; @@ -99,8 +99,9 @@ my ($verb, $repo) = ($cmd =~ /^\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"; + # otherwise, call the external command helper + &ext_cmd($GL_CONF_COMPILED, $RSYNC_BASE, $cmd); + exit; # in case the external command helper forgot :-) } # ---------------------------------------------------------------------------- diff --git a/src/gl-compile-conf b/src/gl-compile-conf index e88819a..0a8d369 100755 --- a/src/gl-compile-conf +++ b/src/gl-compile-conf @@ -355,6 +355,7 @@ my $repo_base_abs = ( $REPO_BASE =~ m(^/) ? $REPO_BASE : "$ENV{HOME}/$REPO_BASE" wrap_chdir("$repo_base_abs"); for my $repo (sort keys %repos) { + next if $repo =~ m(^EXTCMD/); # these are not real repos unless (-d "$repo.git") { new_repo($repo, "$GL_ADMINDIR/src/hooks"); # new_repo would have chdir'd us away; come back