#!/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 # - anytime gitosis-lite.conf is changed # input: # - GL_CONF (default: ~/.gitosis-lite/gitosis-lite.conf) # - GL_KEYDIR (default: ~/.gitosis-lite/keydir) # output: # - ~/.ssh/authorized_keys (dictated by sshd) # - GL_CONF_COMPILED (default: ~/.gitosis-lite/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... # ---------------------------------------------------------------------------- # common definitions # ---------------------------------------------------------------------------- our $GL_ADMINDIR; our $GL_CONF; our $GL_KEYDIR; our $GL_CONF_COMPILED; our $REPO_BASE; my $glrc = $ENV{HOME} . "/.gitosis-lite.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 our $AUTH_COMMAND="$GL_ADMINDIR/gl-auth-command"; our $AUTH_OPTIONS="no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty"; our %groups = (); our %repos = (); # ---------------------------------------------------------------------------- # 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 () { 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($GL_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($GL_ADMINDIR); if (-d ".git") { system("git add -A keydir"); # stage all changes in keydir # and if there are any if (system("git diff --cached --quiet") ) { 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, "<", $GL_CONF) or die "open GL conf failed: $!"; open(OUT, ">", $GL_CONF_COMPILED) or die "open GL conf compiled failed: $!"; # the syntax is fairly simple, so we parse it inline my @repos; while () { # 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; @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) { for my $ref (@refs) { $repos{$repo}{$perm}{$user}{$ref} = 1; } } } } } } print OUT Data::Dumper->Dump([\%repos], [qw(*repos)]); close(OUT); # ---------------------------------------------------------------------------- # 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 my_chdir("$ENV{HOME}/$REPO_BASE"); for my $repo (keys %repos) { unless (-d "$repo.git") { mkdir("$repo.git") or die "mkdir $repo.git failed: $!"; my_chdir("$repo.git"); system("git init --bare"); my_chdir("$ENV{HOME}/$REPO_BASE"); } }