254 lines
8.1 KiB
Perl
Executable file
254 lines
8.1 KiB
Perl
Executable file
#!/usr/bin/perl
|
|
|
|
use strict;
|
|
use warnings;
|
|
use Data::Dumper;
|
|
|
|
# === add-auth-keys ===
|
|
|
|
# part of the gitolite (GL) suite
|
|
|
|
# (1) - "compiles" ~/.ssh/authorized_keys from the list of pub-keys
|
|
# (2) - also "compiles" the user-friendly GL conf file into something easier
|
|
# to parse. We're doing this because both the gl-auth-command and the
|
|
# (gl-)update hook need this, and it seems easier to do this than
|
|
# replicate the parsing code in both those places. As a bonus, it's
|
|
# probably more efficient.
|
|
|
|
# how run: manual, by GL admin
|
|
# when:
|
|
# - anytime a pubkey is added/deleted
|
|
# - anytime gitolite.conf is changed
|
|
# input:
|
|
# - GL_CONF (default: ~/.gitolite/gitolite.conf)
|
|
# - GL_KEYDIR (default: ~/.gitolite/keydir)
|
|
# output:
|
|
# - ~/.ssh/authorized_keys (dictated by sshd)
|
|
# - GL_CONF_COMPILED (default: ~/.gitolite/gitolite.conf-compiled.pm)
|
|
# security:
|
|
# - touches a very critical system file that manages the restrictions on
|
|
# incoming users. Be sure to audit AUTH_COMMAND and AUTH_OPTIONS (see
|
|
# below) on any change to this script
|
|
# - no security checks within program. The GL admin runs this manually
|
|
|
|
# warnings:
|
|
# - if the "start" line exists, but the "end" line does not, you lose the
|
|
# rest of the existing authkey file. In general, "don't do that (TM)",
|
|
# but we do have a "vim -d" popping up so you can see the changes being
|
|
# made, just in case...
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# common definitions
|
|
# ----------------------------------------------------------------------------
|
|
|
|
our $GL_ADMINDIR;
|
|
our $GL_CONF;
|
|
our $GL_KEYDIR;
|
|
our $GL_CONF_COMPILED;
|
|
our $REPO_BASE;
|
|
|
|
my $glrc = $ENV{HOME} . "/.gitolite.rc";
|
|
unless (my $ret = do $glrc)
|
|
{
|
|
die "parse $glrc failed: $@" if $@;
|
|
die "couldn't do $glrc: $!" unless defined $ret;
|
|
die "couldn't run $glrc" unless $ret;
|
|
}
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# definitions specific to this program
|
|
# ----------------------------------------------------------------------------
|
|
|
|
# command and options for authorized_keys
|
|
my $AUTH_COMMAND="$GL_ADMINDIR/src/gl-auth-command";
|
|
my $AUTH_OPTIONS="no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty";
|
|
my $USERNAME_PATT=qr(^\@?[0-9a-zA-Z][0-9a-zA-Z._-]*$); # very simple pattern
|
|
|
|
my %groups = ();
|
|
my %repos = ();
|
|
|
|
# set a restrictive umask, just in case
|
|
umask(0077);
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# subroutines
|
|
# ----------------------------------------------------------------------------
|
|
|
|
sub wrap_chdir {
|
|
chdir($_[0]) or die "chdir $_[0] failed: $! at ", (caller)[1], " line ", (caller)[2], "\n";
|
|
}
|
|
|
|
sub wrap_open {
|
|
open (my $fh, $_[0], $_[1]) or die "open $_[1] failed: $! at ", (caller)[1], " line ", (caller)[2], "\n";
|
|
return $fh;
|
|
}
|
|
|
|
sub expand_userlist
|
|
{
|
|
my @list = @_;
|
|
my @new_list = ();
|
|
|
|
for my $item (@list)
|
|
{
|
|
die "bad user $item\n" unless $item =~ $USERNAME_PATT;
|
|
if ($item =~ /^@/) # nested group
|
|
{
|
|
die "undefined group $item" unless $groups{$item};
|
|
# add those names to the list
|
|
push @new_list, @{ $groups{$item} };
|
|
}
|
|
else
|
|
{
|
|
push @new_list, $item;
|
|
}
|
|
}
|
|
|
|
return @new_list;
|
|
}
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# "compile" GL conf
|
|
# ----------------------------------------------------------------------------
|
|
|
|
my $conf_fh = wrap_open( "<", $GL_CONF );
|
|
|
|
# the syntax is fairly simple, so we parse it inline
|
|
|
|
my @repos;
|
|
while (<$conf_fh>)
|
|
{
|
|
# normalise whitespace; keeps later regexes very simple
|
|
s/=/ = /;
|
|
s/\s+/ /g;
|
|
s/^ //;
|
|
s/ $//;
|
|
# kill comments
|
|
s/#.*//;
|
|
# and blank lines
|
|
next unless /\S/;
|
|
|
|
# user groups
|
|
if (/^(@\S+) = (.*)/)
|
|
{
|
|
push @{ $groups{$1} }, expand_userlist( split(' ', $2) );
|
|
die "bad group $1\n" unless $1 =~ $USERNAME_PATT;
|
|
}
|
|
# repo(s)
|
|
elsif (/^repo (.*)/)
|
|
{
|
|
@repos = split(' ', $1);
|
|
}
|
|
# actual permission line
|
|
elsif (/^(R|RW|RW\+) (.* )?= (.+)/)
|
|
{
|
|
# split perms to separate out R, W, and +
|
|
my @perms = split //, $1;
|
|
my @refs; @refs = split(' ', $2) if $2;
|
|
my @users = split ' ', $3;
|
|
|
|
# if no ref is given, this PERM applies to all refs
|
|
@refs = qw(refs/.*) unless @refs;
|
|
# fully qualify refs that dont start with "refs/"; prefix them with
|
|
# "refs/heads/"
|
|
@refs = map { m(^refs/) or s(^)(refs/heads/); $_ } @refs;
|
|
|
|
# expand the user list, unless it is just "@all"
|
|
@users = expand_userlist ( @users )
|
|
unless (@users == 1 and $users[0] eq '@all');
|
|
|
|
# ok, we can finally populate the %repos hash
|
|
for my $repo (@repos) # each repo in the current stanza
|
|
{
|
|
for my $perm (@perms)
|
|
{
|
|
for my $user (@users)
|
|
{
|
|
push @{ $repos{$repo}{$perm}{$user} }, @refs;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
die "can't make head or tail of '$_'\n";
|
|
}
|
|
}
|
|
|
|
my $compiled_fh = wrap_open( ">", $GL_CONF_COMPILED );
|
|
print $compiled_fh Data::Dumper->Dump([\%repos], [qw(*repos)]);
|
|
close $compiled_fh or die "close compiled-conf failed: $!";
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# any new repos created?
|
|
# ----------------------------------------------------------------------------
|
|
|
|
# modern gits allow cloning from an empty repo, so we just create it. Gitosis
|
|
# did not have that luxury, so it was forced to detect the first push and
|
|
# create it then
|
|
|
|
# repo-base needs to be an absolute path for this loop to work right
|
|
# so if it was not already absolute, prefix $HOME.
|
|
my $repo_base_abs = ( $REPO_BASE =~ m(^/) ? $REPO_BASE : "$ENV{HOME}/$REPO_BASE" );
|
|
wrap_chdir("$repo_base_abs");
|
|
for my $repo (keys %repos)
|
|
{
|
|
unless (-d "$repo.git")
|
|
{
|
|
mkdir("$repo.git") or die "mkdir $repo.git failed: $!";
|
|
wrap_chdir("$repo.git");
|
|
system("git init --bare");
|
|
system("cp $GL_ADMINDIR/src/update-hook.pl hooks/update");
|
|
system("chmod 755 hooks/update");
|
|
wrap_chdir("$repo_base_abs");
|
|
}
|
|
}
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# "compile" ssh authorized_keys
|
|
# ----------------------------------------------------------------------------
|
|
|
|
my $authkeys_fh = wrap_open( "<", $ENV{HOME} . "/.ssh/authorized_keys" );
|
|
my $newkeys_fh = wrap_open( ">", $ENV{HOME} . "/.ssh/new_authkeys" );
|
|
# save existing authkeys minus the GL-added stuff
|
|
while (<$authkeys_fh>)
|
|
{
|
|
print $newkeys_fh $_ unless (/^# gito(sis-)?lite start/../^# gito(sis-)?lite end/);
|
|
}
|
|
|
|
# add our "start" line, each key on its own line (prefixed by command and
|
|
# options, in the standard ssh authorized_keys format), then the "end" line.
|
|
print $newkeys_fh "# gitolite start\n";
|
|
wrap_chdir($GL_KEYDIR);
|
|
for my $pubkey (glob("*.pub"))
|
|
{
|
|
my $user = $pubkey; $user =~ s/(\@.+)?\.pub$//;
|
|
print $newkeys_fh "command=\"$AUTH_COMMAND $user\",$AUTH_OPTIONS ";
|
|
print $newkeys_fh `cat $pubkey`;
|
|
}
|
|
print $newkeys_fh "# gitolite end\n";
|
|
close $newkeys_fh or die "close newkeys failed: $!";
|
|
|
|
# check what changes are being made; just a comfort factor
|
|
# system("vim -d ~/.ssh/authorized_keys ~/.ssh/new_authkeys");
|
|
|
|
# all done; overwrite the file (use cat to avoid perm changes)
|
|
system("cat $ENV{HOME}/.ssh/new_authkeys > $ENV{HOME}/.ssh/authorized_keys");
|
|
system("rm $ENV{HOME}/.ssh/new_authkeys");
|
|
|
|
# if the gl admin directory (~/.gitolite) is itself a git repo, do an
|
|
# autocheckin. nothing fancy; this is a "just in case" type of thing.
|
|
wrap_chdir($GL_ADMINDIR);
|
|
if (-d ".git")
|
|
{
|
|
system("git add -A conf keydir"); # stage all operational data
|
|
# and if there are any
|
|
if (system("git diff --cached --quiet") )
|
|
{
|
|
open my $commit_ph, "|-", "git commit -F -"
|
|
or die "open commit failed: $!";
|
|
print $commit_ph "keydir changed\n\n";
|
|
print $commit_ph `git diff --cached --name-status`;
|
|
close $commit_ph or die "close commit failed: $!";
|
|
}
|
|
}
|