(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.
This commit is contained in:
parent
7f203fc020
commit
98a4c79dce
|
@ -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
|
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
|
### contact and license
|
||||||
|
|
|
@ -259,3 +259,21 @@ repo gitolite
|
||||||
# security reasons.
|
# security reasons.
|
||||||
# - you can also use an absolute path if you like, although in the interests
|
# - 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!
|
# 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
|
||||||
|
|
|
@ -106,6 +106,17 @@ $GIT_PATH="";
|
||||||
# syntax: space separated list of gitolite usernames in *one* string variable.
|
# syntax: space separated list of gitolite usernames in *one* string variable.
|
||||||
# $SHELL_USERS = "alice bob";
|
# $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:
|
# per perl rules, this should be the last line in such a file:
|
||||||
1;
|
1;
|
||||||
|
|
|
@ -26,6 +26,7 @@ In this document:
|
||||||
* "exclude" (or "deny") rules
|
* "exclude" (or "deny") rules
|
||||||
* "personal" branches
|
* "personal" branches
|
||||||
* custom hooks and custom git config
|
* custom hooks and custom git config
|
||||||
|
* access control for external commands
|
||||||
* design choices
|
* design choices
|
||||||
* keeping the parser and the access control separate
|
* 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
|
per-repo "gitconfig" settings. Please see `doc/2-admin.mkd` and
|
||||||
`conf/example.conf` for details.
|
`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
|
### design choices
|
||||||
|
|
||||||
#### keeping the parser and the access control separate
|
#### keeping the parser and the access control separate
|
||||||
|
|
|
@ -156,4 +156,82 @@ sub report_basic
|
||||||
print "$perm\t$r\n\r" if $perm;
|
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;
|
1;
|
||||||
|
|
|
@ -24,7 +24,7 @@ use warnings;
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
# these are set by the "rc" file
|
# 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
|
# and these are set by gitolite.pm
|
||||||
our ($R_COMMANDS, $W_COMMANDS, $REPONAME_PATT);
|
our ($R_COMMANDS, $W_COMMANDS, $REPONAME_PATT);
|
||||||
our %repos;
|
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 ) {
|
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
|
# if the user is allowed a shell, just run the command
|
||||||
exec $ENV{SHELL}, "-c", $ENV{SSH_ORIGINAL_COMMAND} if $shell_allowed;
|
exec $ENV{SHELL}, "-c", $ENV{SSH_ORIGINAL_COMMAND} if $shell_allowed;
|
||||||
# otherwise, whine
|
# otherwise, call the external command helper
|
||||||
die "bad command: $cmd\n";
|
&ext_cmd($GL_CONF_COMPILED, $RSYNC_BASE, $cmd);
|
||||||
|
exit; # in case the external command helper forgot :-)
|
||||||
}
|
}
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
|
|
|
@ -355,6 +355,7 @@ my $repo_base_abs = ( $REPO_BASE =~ m(^/) ? $REPO_BASE : "$ENV{HOME}/$REPO_BASE"
|
||||||
wrap_chdir("$repo_base_abs");
|
wrap_chdir("$repo_base_abs");
|
||||||
|
|
||||||
for my $repo (sort keys %repos) {
|
for my $repo (sort keys %repos) {
|
||||||
|
next if $repo =~ m(^EXTCMD/); # these are not real repos
|
||||||
unless (-d "$repo.git") {
|
unless (-d "$repo.git") {
|
||||||
new_repo($repo, "$GL_ADMINDIR/src/hooks");
|
new_repo($repo, "$GL_ADMINDIR/src/hooks");
|
||||||
# new_repo would have chdir'd us away; come back
|
# new_repo would have chdir'd us away; come back
|
||||||
|
|
Loading…
Reference in a new issue