From 98a4c79dce63ff0a7663d9f677f1a7339cb7c4fe Mon Sep 17 00:00:00 2001 From: Sitaram Chamarty Date: Sun, 31 Jan 2010 20:24:36 +0530 Subject: [PATCH 1/8] (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 From 18312de77ab6f85ce2e7535d47041717deb85912 Mon Sep 17 00:00:00 2001 From: Sitaram Chamarty Date: Sun, 31 Jan 2010 21:09:05 +0530 Subject: [PATCH 2/8] rsync: add support for delete/partial --- src/gitolite.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gitolite.pm b/src/gitolite.pm index c5bcd9e..54dd1da 100644 --- a/src/gitolite.pm +++ b/src/gitolite.pm @@ -216,7 +216,7 @@ sub ext_cmd_rsync # rsync --server -some.flags . some/path die "bad rsync command: $cmd" - unless $cmd =~ /^rsync --server( --sender)? -[\w.]+ \. (\S+)$/; + unless $cmd =~ /^rsync --server( --sender)? -[\w.]+(?: --(?:delete|partial))* \. (\S+)$/; my $perm = "W"; $perm = "R" if $1; my $path = $2; From 20c29c01450fc4b4f69a75ad93c50602a32262e2 Mon Sep 17 00:00:00 2001 From: Sitaram Chamarty Date: Mon, 1 Feb 2010 11:35:35 +0530 Subject: [PATCH 3/8] rsync: log the command used --- src/gitolite.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gitolite.pm b/src/gitolite.pm index 54dd1da..c6d75de 100644 --- a/src/gitolite.pm +++ b/src/gitolite.pm @@ -230,6 +230,7 @@ sub ext_cmd_rsync # 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}; } From 43da598c0852c4dcbd78fdb7251ed338c14fe5ce Mon Sep 17 00:00:00 2001 From: Sitaram Chamarty Date: Mon, 1 Feb 2010 11:36:24 +0530 Subject: [PATCH 4/8] auth: minor flow change when defaulting to "info" --- src/gl-auth-command | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/gl-auth-command b/src/gl-auth-command index 1dd2fba..5b3dc42 100755 --- a/src/gl-auth-command +++ b/src/gl-auth-command @@ -67,16 +67,16 @@ my $user=$ENV{GL_USER}=shift; # there; now that's available everywhere! # 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}; From 09195afd443e3fb92b5c8f62fb3971a176d09c31 Mon Sep 17 00:00:00 2001 From: Sitaram Chamarty Date: Sun, 31 Jan 2010 11:43:43 +0530 Subject: [PATCH 5/8] document deny rules a bit better --- conf/example.conf | 20 ++++++++++++-------- doc/3-faq-tips-etc.mkd | 9 +++------ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/conf/example.conf b/conf/example.conf index 6dcc4e2..6ad91ad 100644 --- a/conf/example.conf +++ b/conf/example.conf @@ -142,10 +142,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 @@ -161,10 +169,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 # -------------------------------- diff --git a/doc/3-faq-tips-etc.mkd b/doc/3-faq-tips-etc.mkd index edc6351..67ef956 100644 --- a/doc/3-faq-tips-etc.mkd +++ b/doc/3-faq-tips-etc.mkd @@ -499,12 +499,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 From 2d9c4c4ae9e04b9ce0fa91cb1e5857ad541858d6 Mon Sep 17 00:00:00 2001 From: Sitaram Chamarty Date: Mon, 1 Feb 2010 16:54:39 +0530 Subject: [PATCH 6/8] oops; logging bug --- src/gl-auth-command | 45 ++++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/src/gl-auth-command b/src/gl-auth-command index 5b3dc42..8fae9f5 100755 --- a/src/gl-auth-command +++ b/src/gl-auth-command @@ -63,6 +63,24 @@ 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 # ---------------------------------------------------------------------------- @@ -108,6 +126,8 @@ unless ( $verb and ( $verb =~ $R_COMMANDS or $verb =~ $W_COMMANDS ) and $repo an # first level permissions check # ---------------------------------------------------------------------------- +$ENV{GL_REPO}=$repo; + # parse the compiled acl; goes into %repos (global) &parse_acl($GL_CONF_COMPILED); @@ -128,32 +148,11 @@ if ( not -d "$repo_base_abs/$repo.git" ) { } } -# ---------------------------------------------------------------------------- -# 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"); From 0a7fa6c6b58b8c64e1c866f8781fbb5140b6f750 Mon Sep 17 00:00:00 2001 From: "martin f. krafft" Date: Wed, 3 Feb 2010 21:13:47 +1300 Subject: [PATCH 7/8] Tell gitweb about repo owner via git-config Gitolite uses projects.list to set the owners for gitweb's use. Unfortunately, this does not work for gitweb setups that set $projectroot to a directory, thus generating the list of repositories on the fly. This patch changes that: gitolite now writes the gitweb.owner configuration variable for each repository (and properly cleans up after itself if the owner is removed). The patch causes gitolite not to write the owner to projects.list anymore, as this would be redundant. The owner also needs no longer be escaped, so this patch removes the poor man's 's/ /+/g' escaping previously in place. Note that I am not a Perl coder. Thus there are probably better ways to implement this, but at least it works. Cc: Sitaram Chamarty Signed-off-by: martin f. krafft --- src/gl-compile-conf | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/gl-compile-conf b/src/gl-compile-conf index 0a8d369..bb05d1e 100755 --- a/src/gl-compile-conf +++ b/src/gl-compile-conf @@ -284,7 +284,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 @@ -415,16 +414,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; From 67c10a34fe4d934ae92cc9c8320a396c657d8344 Mon Sep 17 00:00:00 2001 From: Sitaram Chamarty Date: Mon, 1 Feb 2010 15:37:35 +0530 Subject: [PATCH 8/8] auth: new subcommand "htpasswd" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit great idea by Robin Smidsrød: since users are already capable of authenticating themselves to gitolite via ssh keys, use that to let them set or change their own HTTP passwords (ie, run the "htpasswd" command with the correct parameters on behalf of the "git" user on the server) code, rc para, and documentation. In fact everything except... ahem... testing ;-) and while we're about it, we also reorganised the way these helper commands (including the venerable "info" are called) --- conf/example.gitolite.rc | 9 ++++++++ doc/3-faq-tips-etc.mkd | 29 +++++++++++++++++++------ src/gitolite.pm | 46 +++++++++++++++++++++++++++++++++++----- src/gl-auth-command | 42 ++++++++++++++++++------------------ 4 files changed, 94 insertions(+), 32 deletions(-) diff --git a/conf/example.gitolite.rc b/conf/example.gitolite.rc index cf7fe3d..bcb281f 100644 --- a/conf/example.gitolite.rc +++ b/conf/example.gitolite.rc @@ -108,6 +108,15 @@ $GIT_PATH=""; # -------------------------------------- +# 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 diff --git a/doc/3-faq-tips-etc.mkd b/doc/3-faq-tips-etc.mkd index 67ef956..6723000 100644 --- a/doc/3-faq-tips-etc.mkd +++ b/doc/3-faq-tips-etc.mkd @@ -331,9 +331,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 +365,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 +375,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$//; diff --git a/src/gitolite.pm b/src/gitolite.pm index c6d75de..a8127e9 100644 --- a/src/gitolite.pm +++ b/src/gitolite.pm @@ -158,17 +158,28 @@ sub report_basic } # ---------------------------------------------------------------------------- -# E X T E R N A L C O M M A N D H E L P E R S +# S P E C I A L C O M M A N D S # ---------------------------------------------------------------------------- -sub ext_cmd +sub special_cmd { - my ($GL_CONF_COMPILED, $RSYNC_BASE, $cmd) = @_; + my ($GL_ADMINDIR, $GL_CONF_COMPILED, $RSYNC_BASE, $HTPASSWD_FILE) = @_; - # check each external command we know about and call it if enabled - if ($RSYNC_BASE and $cmd =~ /^rsync /) { + 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"; } } @@ -234,5 +245,30 @@ sub ext_cmd_rsync 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]*$//; + 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; diff --git a/src/gl-auth-command b/src/gl-auth-command index 8fae9f5..8aa2f65 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, $RSYNC_BASE); +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); our %repos; @@ -97,31 +97,31 @@ unless ($ENV{SSH_ORIGINAL_COMMAND}) { $ENV{SSH_ORIGINAL_COMMAND} = 'info'; } -my $cmd = $ENV{SSH_ORIGINAL_COMMAND}; -# 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 1; -} +# ---------------------------------------------------------------------------- +# 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, call the external command helper - &ext_cmd($GL_CONF_COMPILED, $RSYNC_BASE, $cmd); - exit; # in case the external command helper forgot :-) + # 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; } +# ---------------------------------------------------------------------------- +# the real git commands (git-receive-pack, etc...) +# ---------------------------------------------------------------------------- + # ---------------------------------------------------------------------------- # first level permissions check # ----------------------------------------------------------------------------