From 98a4c79dce63ff0a7663d9f677f1a7339cb7c4fe Mon Sep 17 00:00:00 2001 From: Sitaram Chamarty Date: Sun, 31 Jan 2010 20:24:36 +0530 Subject: [PATCH] (read this in full) access control for non-git commands running over ssh This is actually a pretty big deal, and I am seriously starting wonder if calling this "gito*lite*" is justified anymore. Anyway, in for a penny, in for a pound... This patch implements a generic way to allow access control for 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. Please read the changes in this commit (at least the ones in conf/ and doc/) carefully. --- README.mkd | 5 +++ conf/example.conf | 18 ++++++++++ conf/example.gitolite.rc | 11 ++++++ doc/3-faq-tips-etc.mkd | 15 ++++++++ src/gitolite.pm | 78 ++++++++++++++++++++++++++++++++++++++++ src/gl-auth-command | 7 ++-- src/gl-compile-conf | 1 + 7 files changed, 132 insertions(+), 3 deletions(-) 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