gitolite/gl-compile-conf

199 lines
6 KiB
Perl
Executable file

#!/usr/bin/perl -w
use strict;
use Data::Dumper;
# === add-auth-keys ===
# part of the gitosis-lite (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 (i.e., contents of
# ~/.gitosis-lite/keydir change)
# - anytime gitosis-lite.conf is changed
# input:
# - ~/.gitosis-lite/gitosis-lite.conf
# - ~/.gitosis-lite/keydir
# output:
# - ~/.ssh/authorized_keys
# - ~/.ssh/gitosis-lite.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...
# other notes:
# - keys are added/deleted from the keystore **manually**, and all keys
# are named "name.pub". Keep the names simple.
# command and options for authorized_keys
our $AUTH_COMMAND=$ENV{HOME} . "/.gitosis-lite/gl-auth-command";
our $AUTH_OPTIONS="no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty";
our %groups = ();
our %repos = ();
# quick subroutines
sub my_chdir
{
chdir($_[0]) or die "chdir $_[0] failed: $!";
}
sub expand_userlist
{
my @list = @_;
my @new_list = ();
for my $item (@list)
{
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" ssh authorized_keys
# ----------------------------------------------------------------------------
open(INF, "<", $ENV{HOME} . "/.ssh/authorized_keys") or die "open old authkeys failed: $!";
open(OUT, ">", $ENV{HOME} . "/.ssh/new_authkeys") or die "open new authkeys failed: $!";
# save existing authkeys minus the GL-added stuff
while (<INF>)
{
print OUT unless (/^# gitosis-lite start/../^# gitosis-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 OUT "# gitosis-lite start\n";
my_chdir($ENV{HOME} . "/.gitosis-lite/keydir");
for my $pubkey (glob("*.pub"))
{
my $user = $pubkey; $user =~ s/\.pub$//;
print OUT "command=\"$AUTH_COMMAND $user\",$AUTH_OPTIONS ";
print OUT `cat $pubkey`;
}
print OUT "# gitosis-lite end\n";
close(OUT);
# 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 ~/.ssh/new_authkeys > ~/.ssh/authorized_keys");
system("rm ~/.ssh/new_authkeys");
# if the gl admin directory (~/.gitosis-lite) is itself a git repo, do an
# autocheckin. nothing fancy; this is a "just in case" type of thing.
my_chdir($ENV{HOME} . "/.gitosis-lite");
if (-d ".git")
{
system("git add -A keydir"); # stage all changes in keydir
if (! system("git diff --cached --quiet") )
# and if there are any
{
open(COMMIT, "|-", "git commit -F -")
or die "pipe commit failed: $!";
print COMMIT "keydir changed\n\n";
print COMMIT `git diff --cached --name-status`;
close(COMMIT) or die "close commit failed: $!";
}
}
# ----------------------------------------------------------------------------
# "compile" GL conf
# ----------------------------------------------------------------------------
open(INF, "<", $ENV{HOME} . "/.gitosis-lite/gitosis-lite.conf")
or die "open GL conf failed: $!";
open(OUT, ">", $ENV{HOME} . "/.ssh/gitosis-lite.conf-compiled.pm")
or die "open GL conf compiled failed: $!";
# the syntax is fairly simple, so we parse it inline
my @repos;
while (<INF>)
{
# 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) );
}
# repo(s)
elsif (/^repo (.*)/)
{
@repos = split(' ', $1);
}
# actual permission line
elsif (/^(R|RW|RW\+) (.* )?= (.+)/)
{
my @perms = split //, $1;
my @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 $ref (@refs)
{
for my $user (@users)
{
$repos{$repo}{$perm}{$ref}{$user} = 1;
}
}
}
}
}
}
print OUT Data::Dumper->Dump([\%repos], [qw(*repos)]);
close(OUT);