diff --git a/conf/example.conf b/conf/example.conf index 8e6214b..49a05f7 100644 --- a/conf/example.conf +++ b/conf/example.conf @@ -45,28 +45,3 @@ repo foo # rules. So, lead_dev can push changes to any files, dev1/2 can push # changes to files in "doc/" and "src/" (but not the top level README), and # dev3/4 can only push changes to files in "src/". - - - -# (2) rsync helper: this is an oddball feature that only the person who asked -# me for is apparently using, created long ago, before I learned the value of -# saying "no" :-) These items just don't seem to fit in any of the existing -# documents. - -# 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 given in "FILE/DIR NAME BASED RESTRICTIONS" above - 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/contrib/adc/htpasswd b/contrib/adc/htpasswd new file mode 100644 index 0000000..6dd0948 --- /dev/null +++ b/contrib/adc/htpasswd @@ -0,0 +1,30 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +BEGIN { + die "ENV GL_RC not set\n" unless $ENV{GL_RC}; + die "ENV GL_BINDIR not set\n" unless $ENV{GL_BINDIR}; + unshift @INC, $ENV{GL_BINDIR}; +} +use gitolite_rc; +use gitolite; + +die "$HTPASSWD_FILE doesn't exist or is not writable\n" unless -w $HTPASSWD_FILE; +$|++; +print <; +$password =~ s/[\n\r]*$//; +die "empty passwords are not allowed\n" unless $password; +my $rc = system("htpasswd", "-mb", $HTPASSWD_FILE, $ENV{GL_USER}, $password); +die "htpasswd command seems to have failed with $rc return code...\n" if $rc; diff --git a/contrib/adc/rsync b/contrib/adc/rsync new file mode 100755 index 0000000..41a70a1 --- /dev/null +++ b/contrib/adc/rsync @@ -0,0 +1,63 @@ +#!/usr/bin/perl + +# 'rsync' helper ADC. See bottom of this file for more info + +use strict; +use warnings; + +BEGIN { + die "ENV GL_RC not set\n" unless $ENV{GL_RC}; + die "ENV GL_BINDIR not set\n" unless $ENV{GL_BINDIR}; + unshift @INC, $ENV{GL_BINDIR}; +} +use gitolite_rc; +use gitolite; + +my $cmd = $ENV{SSH_ORIGINAL_COMMAND}; + +# 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 some of the characters in $path\n" unless $path =~ $REPONAME_PATT; + # XXX make a better pattern for this if people complain ;-) +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('EXTCMD/rsync', "NAME/$path", $perm); + # that should "die" if there's a problem + +wrap_chdir($RSYNC_BASE); +log_it(); +exec $ENV{SHELL}, "-c", $ENV{SSH_ORIGINAL_COMMAND}; + +__END__ + +This is an rsync helper ADC. It is an example of using gitolite's config +language, combined with the 'check_access()' function, to implement access +control for non-git software using a "fake" repo. For historical reasons, +fake repos start with "EXTCMD/". Gitolite does not auto-create fake repos, so +you can use those as namespaces to hold collections of rules for various +purposes. + +So here's a fake git repository to collect rsync rules in one place. It 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 given in "FILE/DIR NAME BASED RESTRICTIONS" elsewhere in the +gitolite documentation. + + repo EXTCMD/rsync + RW NAME/ = sitaram + RW NAME/foo/ = user1 + R NAME/bar/ = user2 + RW NAME/baz/.*/.*\.c$ = user3 diff --git a/contrib/adc/svnserve b/contrib/adc/svnserve new file mode 100644 index 0000000..9f0528d --- /dev/null +++ b/contrib/adc/svnserve @@ -0,0 +1,20 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +BEGIN { + die "ENV GL_RC not set\n" unless $ENV{GL_RC}; + die "ENV GL_BINDIR not set\n" unless $ENV{GL_BINDIR}; + unshift @INC, $ENV{GL_BINDIR}; +} +use gitolite_rc; +use gitolite; + +my $cmd = $ENV{SSH_ORIGINAL_COMMAND}; + +die "expecting 'svnserve -t', got '$cmd'\n" unless $cmd eq 'svnserve -t'; + +$SVNSERVE =~ s/%u/$ENV{GL_USER}/g; +exec $SVNSERVE; +die "svnserve exec failed\n"; diff --git a/doc/admin-defined-commands.mkd b/doc/admin-defined-commands.mkd index fa2d2b7..ea9eb9e 100644 --- a/doc/admin-defined-commands.mkd +++ b/doc/admin-defined-commands.mkd @@ -1,25 +1,22 @@ ## admin defined commands -Summary: this document describes a mechanism to allow users controlled access -to specific programs or scripts, without giving them full shell access. - -**WARNING**: careless use of this feature, including inadequate review of -allowed commands or scripts, could compromise this security and allow users to -grant themselves full shell access, accidentally or otherwise. - ---- In this document: * background - * setting it up - * configuring ADCs + * details + * installing ADCs + * user invocation + * checking authorisation + * checking arguments + * passing unchecked arguments + * "fake" repos and access control for non-git programs * anatomy of a command - * example uses and sample commands in contrib + * example uses and sample commands in `contrib/adc` * fork * deleting/trashing repos * enable/disable push access temporarily - * (bonus) restricted admin * how this feature came about ---- @@ -28,66 +25,115 @@ In this document: ### background -Allowing users to get a shell is a no-no if you're using gitolite, but there -are times when you want them to be able to run specific commands or custom -scripts that you (the admin) have pre-approved to be "safe". Here's how to -enable such commands. +The admin-defined commands (ADCs) feature allows controlled access to +specific, "safe", programs or scripts, without giving users full shell access. -However, as the warning at the top says, careless use could allow users to -defeat this security and get a shell. Every command you approve must be -checked to be sure it cannot be compromised. +**WARNING**: regardless of what you read below, the security of the code in +the commands or scripts you install as ADCs is **your responsibility**. The +sample ADCs shipped with gitolite (in `contrib/adc`) should be safe enough, +but an extra pair of eyes never hurt, so please review before use. -To help you with this, gitolite restricts ADCs arguments to only some very -safe characters (see `$ADC_CMD_ARGS_PATT` in `src/gitolite_rc.pm`). The code -inside the ADC, however, is *your* responsibility. The sample ADCs shipped -with gitolite (in `contrib/adc`) should be OK, but an extra pair of eyes never -hurt :-) so please review before installing them. +Although this is a generic way to allow pretty much any +command to be run, most of the examples and sample ADCs pertain to allowing +users to manage their "own" repos. If that's your use case, please read +[doc/wildcard-repositories.mkd][wild] before you continue here. - +[wild]: http://sitaramc.github.com/gitolite/doc/wildcard-repositories.html -Finally, although this is a generic way to allow specific commands to be run, -most of the examples and sample ADCs pertain to allowing users to manage their -"own" repos. If that's your use case, please read -[doc/wildcard-repositories.mkd][wild] before you continue here. + - +### details - + -### setting it up +#### installing ADCs -This can only be setup by someone who has shell access to the server. Edit -the rc file and update the `$GL_ADC_PATH` variable to point to, say, -`/home/git/bin/adc`. *Nothing happens unless this variable is set and -pointing to a directory*. Then put in whatever such commands you create into -that directory. If you have a command called "foo" in that directory, then a -user can invoke it by saying: +ADCs can only be installed by someone with shell access to the server; merely +having push rights to the admin repo is not enough. + + * edit `~/.gitolite.rc` and set `$GL_ADC_PATH` to a directory that is *not* + in `$PATH`. + + * add your "safe" executables to this directory. + +**Warning**: An ADC can hide (or override) gitolite's built-in commands like +'info', 'expand', 'setperms', or even 'git-receive-pack' or 'git-upload-pack'! +This is by design. So be careful what you name your scripts. + +However, it is perfectly ok, and may even be necessary in some cases, to name +them after system executables (like 'rsync'). + + + +#### user invocation + +If you have a command called "foo" in that directory, then a user can invoke +it by saying: ssh git@server foo argument list -**WARNING: When gitolite takes control, this directory is checked first, and -if the requested command exists, it is executed. It is therefore quite easy -to inadvertently *hide* some of the "official" commands (like "info", -"expand", "setperms", etc., or worse, say "git-upload-pack"!) by creating -executable files with those names in this directory. So don't do that -- you -have been warned!** + - +#### checking authorisation -#### configuring ADCs +Once an ADC is installed, *all* users can run it. But sometimes you want only +some people to be able to do so. -I didn't want to put configuration variables for ADCs also into the main 'rc' -file, so I chose to put them in the `adc.common-functions` file instead. Take -a look at it sometime. +While you cannot prevent the ADC from running at all, you can *start* the ADC +with code that checks the user's access to *any* arbitrary repo. For example, +you can bail out if the user does not have "W" access to the "gitolite-admin" +repo, which is an easy way of making sure an ADC is only run by admins. + +See the section on "the anatomy of a command" later for this and many more +details. + + + +#### checking arguments + +Gitolite will call an ADC only if the arguments passed to it match a very +strict pattern (see `$ADC_CMD_ARGS_PATT` in `src/gitolite_rc.pm`). This +reduces the risk of various kinds of shell-meta related compromises. + + + +#### passing unchecked arguments + +Some commands need arguments with a broader range of characters than +`$ADC_CMD_ARGS_PATT` will allow. As long as you are sure those commands are +doing their own argument checking and sanitisation, you can place such +commands in `$GL_ADC_PATH/ua` and they will be run with **no checks on the +arguments**. + +The "ua" stand for "unchecked arguments". Consider this your last warning ;-) + + + +### "fake" repos and access control for non-git programs + +A "fake" repo is a repo that exists in the config file but is specially named +(starts with "EXTCMD/") so that gitolite will not create an actual repo on +disk for it. It serves as a place holder for different sets of rules. + +If you install the 'rsync' ADC, you can use a fake repo called 'EXTCMD/rsync' +to collect a set of rules that specify what user is allowed to read/write what +files using the rsync command on his workstation. See `contrib/adc/rsync` for +more on this. + +*Any* non-git program can be similarly access controlled, +as long as the *command line* that the client attempts to execute on the +server has sufficient information to decide. Protocols where the command line +is just one word and everything else happens in the conversation later cannot +be helped by this mechanism. ### anatomy of a command -You can basically do whatever you want in such a command -- go wild! It's -upto you to check the permissions of *each* repo that the user is manipulating -using your command -- you can `rm -rf $GL_REPO_BASE_ABS` if you like and -gitolite wouldn't stop you. +You can do whatever you want in an ADC! It's upto you to check the +permissions of *each* repo that the user is manipulating using your ADC -- +your code can `rm -rf $GL_REPO_BASE_ABS` if you like and gitolite wouldn't +stop you. The current directory (`$PWD`) will be set to the `$HOME` of `git@server` (or whatever id you're using). It won't be any specific repo, it won't even be @@ -112,15 +158,9 @@ internal/undocumented/subject to change. `GL_BINDIR`. I have not tested this in those scenarios, but they probably put `gitolite.pm` somewhere in perl's lib path anyway, so it ought to work]. -In addition, all the arguments of the command are also available to you, so -you can define your own command syntaxes. Gitolite checks these arguments to -make sure they fit a very conservative pattern (see `$ADC_CMD_ARGS_PATT` in -`src/gitolite.pm`), so take that into consideration when designing your -commands and usage. - -Finally, you can call gitolite to query ownership and permissions for the -current user (which may not necessarily be the owner). This is done loosely -as follows (don't use this exact code yet though): +You can query ownership and permissions for the current user (which may not +necessarily be the owner). This is done loosely as follows (don't use this +exact code yet though): perl -I$GL_BINDIR -Mgitolite -e "cli_repo_rights('reponame')" @@ -133,12 +173,12 @@ But that's cumbersome. There's a bash shell function called `get_rights_and_owner` in `contrib/adc/adc.common-functions` that is much more convenient. See any of the other samples for how to use it. -If you don't like this, roll your own. If you don't like bash, do the eqvt in -your language of choice. +If you prefer perl, there is a nicely commented example in +`contrib/adc/get-rights-and-owner.in-perl`. - + -### example uses and sample commands in contrib +### example uses and sample commands in `contrib/adc` @@ -197,33 +237,6 @@ Note: please see [this][diswr] for more on this. [diswr]: http://sitaramc.github.com/gitolite/doc/3-faq-tips-etc.html#_disabling_write_access_to_take_backups - - -#### (bonus) restricted admin - -It's rather important to me (and presumably others in the "corporate" world) -to separate permission to push to the "gitolite-admin" repo from unrestricted -shell access to the server. This issue has been visited often in the past. - -Until now, though, this was binary -- you either had full shell access or none -at all. If there were tasks that legitimately needed to be done from the -shell on the server, it often meant you had to break that separation or load -the few people who did have shell access already. - -Now, however, it is possible to provide scripts to do what you want, and put -them in `$GL_ADC_PATH`. `contrib/adc/restrict-admin` is a commented sample -- -as you can see, it cleverly makes use of the fact that you can now check for -the invoking uses access to any repo in the system. In this case it checks if -he has "W" access to the gitolite-admin repo, and if he does, allows the -script to proceed. - -[Note that this particular use does not require `$GL_WILDREPOS` to be enabled, -because it's not using any wildcard repos]. - -[xkcd224]: http://xkcd.com/224/ -[lazy]: http://c2.com/cgi/wiki?LazinessImpatienceHubris -[wild]: http://sitaramc.github.com/gitolite/doc/wildcard-repositories.html - ---- @@ -242,6 +255,9 @@ resisted the urge to point him to [this][xkcd224], told him that's a great idea and he should go for it, mentally blessing him for letting me off the hook on coding it ;-) [Laziness][lazy] *is* the first virtue you know! +[xkcd224]: http://xkcd.com/224/ +[lazy]: http://c2.com/cgi/wiki?LazinessImpatienceHubris + And that was that. For a couple of days. Soon, though, I realised that there could be a pretty big bonus in this for @@ -249,4 +265,3 @@ tightly controlled setups, so I went and coded it all anyway. See the section on "restricted admin" for what's really exciting about this for *me*. - diff --git a/src/gitolite.pm b/src/gitolite.pm index c4e13f5..1d98fe7 100644 --- a/src/gitolite.pm +++ b/src/gitolite.pm @@ -1160,12 +1160,6 @@ sub special_cmd warn("ignoring illegal username $otheruser\n"), next unless $otheruser =~ $USERNAME_PATT; report_basic($repo, $otheruser); } - } 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); - } elsif ($SVNSERVE and $cmd eq 'svnserve -t') { - ext_cmd_svnserve($SVNSERVE); } else { # if the user is allowed a shell, just run the command log_it(); @@ -1208,88 +1202,21 @@ sub shell_out { sub try_adc { my ($cmd, @args) = split ' ', $ENV{SSH_ORIGINAL_COMMAND}; + die "I don't like $cmd\n" if $cmd =~ /\.\./; + + # try the default (strict arguments) version first if (-x "$GL_ADC_PATH/$cmd") { - die "I don't like $cmd\n" if $cmd =~ /\.\./; # yes this is rather strict, sorry. do { die "I don't like $_\n" unless $_ =~ $ADC_CMD_ARGS_PATT and $_ !~ m(\.\./) } for ($cmd, @args); log_it("$GL_ADC_PATH/$ENV{SSH_ORIGINAL_COMMAND}"); exec("$GL_ADC_PATH/$cmd", @args); } -} -# ---------------------------------------------------------------------------- -# 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 some of the characters in $path\n" unless $path =~ $REPONAME_PATT; - # XXX make a better pattern for this if people complain ;-) - 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('EXTCMD/rsync', "NAME/$path", $perm); - # that should "die" if there's a problem - - wrap_chdir($RSYNC_BASE); - log_it(); - 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 <; - $password =~ s/[\n\r]*$//; - die "empty passwords are not allowed\n" unless $password; - my $rc = system("htpasswd", "-mb", $HTPASSWD_FILE, $ENV{GL_USER}, $password); - die "htpasswd command seems to have failed with $rc return code...\n" if $rc; -} - -# ---------------------------------------------------------------------------- -# external command helper: svnserve -# ---------------------------------------------------------------------------- - -sub ext_cmd_svnserve -{ - my $SVNSERVE = shift; - - $SVNSERVE =~ s/%u/$ENV{GL_USER}/g; - exec $SVNSERVE; - die "svnserve exec failed\n"; + # now the "ua" (unrestricted/unchecked arguments) version + if (-x "$GL_ADC_PATH/ua/$cmd") { + log_it("$GL_ADC_PATH/ua/$ENV{SSH_ORIGINAL_COMMAND}"); + exec("$GL_ADC_PATH/ua/$cmd", @args); + } } # ---------------------------------------------------------------------------- diff --git a/src/gl-auth-command b/src/gl-auth-command index 990a604..851f614 100755 --- a/src/gl-auth-command +++ b/src/gl-auth-command @@ -8,7 +8,6 @@ # - one env var, SSH_ORIGINAL_COMMAND, containing the command # - command typically: git-(receive|upload)-pack 'reponame(.git)?' # - special gitolite commands: info, expand, (get|set)(perms|desc) -# - special non-gitolite commands: rsync, svnserve, htpasswd # - other commands: anything in $GL_ADC_PATH if defined (see rc file) # # (smart) http mode diff --git a/t/t65-rsync b/t/t65-rsync index 0efb8fd..c56b8df 100644 --- a/t/t65-rsync +++ b/t/t65-rsync @@ -2,6 +2,11 @@ cd $TESTDIR $TESTDIR/rollback || die "rollback failed" +rm -rf $ADC_PATH +mkdir $ADC_PATH || die "mkdir $ADC_PATH failed" +cp ../contrib/adc/* $ADC_PATH +echo "\$GL_ADC_PATH = '$ADC_PATH';" | addrc + runremote rm -rf /tmp/rsyncbase runremote mkdir /tmp/rsyncbase editrc REPO_UMASK 0022