#!/usr/bin/perl use strict; use warnings; die "ENV GL_RC not set\n" unless $ENV{GL_RC}; die "ENV GL_BINDIR not set\n" unless $ENV{GL_BINDIR}; # pull in modules we need unshift @INC, $ENV{GL_BINDIR}; require gitolite_rc or die "parse gitolite_rc.pm failed\n"; gitolite_rc->import; require gitolite or die "parse gitolite.pm failed\n"; gitolite->import; # get to the keydir die "keydir not accessible\n" unless -d $gitolite_rc::GL_KEYDIR; chdir($gitolite_rc::GL_KEYDIR); # save arguments for later my $operation = shift || 'list'; my $keyid = shift || ''; # keyid must fit a very specific pattern $keyid and $keyid !~ /^@[-0-9a-z_]+$/i and die "invalid keyid $keyid\n"; # get the actual userid and keytype my $gl_user = $ENV{GL_USER}; my $keytype = ''; $keytype = $1 if $gl_user =~ s/^zzz-marked-for-(...)-//; print STDERR "hello $gl_user, you are currently using " . ($keytype ? "a key in the 'marked for $keytype' state\n" : "a normal (\"active\") key\n" ); # ---- # first collect the keys my (@pubkeys, @marked_for_add, @marked_for_del); # get the list of pubkey files for this user, including pubkeys marked for # add/delete for my $pubkey (`find . -type f -name "*.pub" | sort`) { chomp($pubkey); $pubkey =~ s(^./)(); # artifact of the find command my $user = $pubkey; $user =~ s(.*/)(); # foo/bar/baz.pub -> baz.pub $user =~ s/(\@[^.]+)?\.pub$//; # baz.pub, baz@home.pub -> baz next unless $user eq $gl_user or $user =~ /^zzz-marked-for-...-$gl_user/; if ($user =~ m(^zzz-marked-for-add-)) { push @marked_for_add, $pubkey; } elsif ($user =~ m(^zzz-marked-for-del-)) { push @marked_for_del, $pubkey; } else { push @pubkeys, $pubkey; } } # ---- # list mode; just do it and exit sub print_keylist { my ($message, @list) = @_; return unless @list; print "== $message ==\n"; my $count=1; for (@list) { my $fp = fingerprint($_); s/zzz-marked(\/|-for-...-)//g; print $count++ . ": $fp : $_\n"; } } if ($operation eq 'list') { print "you have the following keys:\n"; print_keylist("active keys", @pubkeys); print_keylist("keys marked for addition/replacement", @marked_for_add); print_keylist("keys marked for deletion", @marked_for_del); print "\n\n"; exit; } # ---- # please see docs for details on how a user interacts with this if ($keytype eq '') { # user logging in with a normal key die "valid operations: add, del, undo-add, confirm-del\n" unless $operation =~ /^(add|del|confirm-del|undo-add)$/; if ($operation eq 'add') { print STDERR "please supply the new key on STDIN. (I recommend you don't try to do this interactively, but use a pipe)\n"; kf_add($gl_user, $keyid, safe_stdin()); } elsif ($operation eq 'del') { kf_del($gl_user, $keyid); } elsif ($operation eq 'confirm-del') { die "you dont have any keys marked for deletion\n" unless @marked_for_del; kf_confirm_del($gl_user, $keyid); } elsif ($operation eq 'undo-add') { die "you dont have any keys marked for addition\n" unless @marked_for_add; kf_undo_add($gl_user, $keyid); } } elsif ($keytype eq 'del') { # user is using a key that was marked for deletion. The only possible use # for this is that she changed her mind for some reason (maybe she marked # the wrong key for deletion) or is not able to get her client-side sshd # to stop using this key die "valid operations: undo-del\n" unless $operation eq 'undo-del'; # reinstate the key kf_undo_del($gl_user, $keyid); } elsif ($keytype eq 'add') { die "valid operations: confirm-add\n" unless $operation eq 'confirm-add'; # user is trying to validate a key that has been previously marked for # addition. This isn't interactive, but it *could* be... if someone asked kf_confirm_add($gl_user, $keyid); } exit; # ---- # make a temp clone and switch to it our $TEMPDIR; BEGIN { $TEMPDIR=`mktemp -d -t tmp.XXXXXXXXXX`; } END { `/bin/rm -rf $TEMPDIR`; } sub cd_temp_clone { chomp($TEMPDIR); hushed_git("clone", "$ENV{GL_REPO_BASE_ABS}/gitolite-admin.git", "$TEMPDIR"); chdir($TEMPDIR); my $hostname = `hostname`; chomp($hostname); hushed_git("config", "--get", "user.email") and hushed_git("config", "user.email", $ENV{USER} . "@" . $hostname); hushed_git("config", "--get", "user.name") and hushed_git("config", "user.name", "$ENV{USER} on $hostname"); } sub fingerprint { my $fp = `ssh-keygen -l -f $_[0]`; die "does not seem to be a valid pubkey\n" unless $fp =~ /(([0-9a-f]+:)+[0-9a-f]+ )/i; return $1; } sub safe_stdin { # read one line from STDIN my $data; my $ret = read STDIN, $data, 4096; # current pubkeys are approx 400 bytes so we go a little overboard die "could not read pubkey data" . (defined($ret) ? "" : ": $!") . "\n" unless $ret; die "pubkey data seems to have more than one line\n" if $data =~ /\n./; return $data; } sub hushed_git { local(*STDOUT) = \*STDOUT; local(*STDERR) = \*STDERR; open(STDOUT, ">", "/dev/null"); open(STDERR, ">", "/dev/null"); system("git", @_); } sub highlander { # there can be only one my($keyid, $die_if_empty, @a) = @_; # too many? if (@a > 1) { print STDERR " more than one key satisfies this condition, and I can't deal with that! The keys are: "; print STDERR "\t" . join("\n\t", @a), "\n\n"; exit 1; } # too few? die "no keys with " . ($keyid || "empty") . " keyid found\n" if $die_if_empty and not @a; return @a; } sub kf_add { my($gl_user, $keyid, $keymaterial) = @_; # add a new "marked for addition" key for $gl_user. cd_temp_clone(); chdir("keydir"); mkdir("zzz-marked"); wrap_print("zzz-marked/zzz-marked-for-add-$gl_user$keyid.pub", $keymaterial); hushed_git("add", ".") and die "git add failed\n"; my $fp = fingerprint("zzz-marked/zzz-marked-for-add-$gl_user$keyid.pub"); hushed_git("commit", "-m", "sskm: add $gl_user$keyid ($fp)") and die "git commit failed\n"; system("env GL_BYPASS_UPDATE_HOOK=1 git push >/dev/null 2>/dev/null") and die "git push failed\n"; } sub kf_confirm_add { my($gl_user, $keyid) = @_; # find entries in both @pubkeys and @marked_for_add whose basename matches $gl_user$keyid my @pk = highlander($keyid, 0, grep { m(^(.*/)?$gl_user$keyid.pub$) } @pubkeys); my @mfa = highlander($keyid, 1, grep { m(^zzz-marked/zzz-marked-for-add-$gl_user$keyid.pub$) } @marked_for_add); cd_temp_clone(); chdir("keydir"); my $fp = fingerprint($mfa[0]); if ($pk[0]) { hushed_git("mv", "-f", $mfa[0], $pk[0]); hushed_git("commit", "-m", "sskm: confirm-add (replace) $pk[0] ($fp)") and die "git commit failed\n"; } else { hushed_git("mv", "-f", $mfa[0], "$gl_user$keyid.pub"); hushed_git("commit", "-m", "sskm: confirm-add $gl_user$keyid ($fp)") and die "git commit failed\n"; } system("env GL_BYPASS_UPDATE_HOOK=1 git push >/dev/null 2>/dev/null") and die "git push failed\n"; } sub kf_undo_add { # XXX some code at start is shared with kf_confirm_add my($gl_user, $keyid) = @_; my @mfa = highlander($keyid, 1, grep { m(^zzz-marked/zzz-marked-for-add-$gl_user$keyid.pub$) } @marked_for_add); cd_temp_clone(); chdir("keydir"); my $fp = fingerprint($mfa[0]); hushed_git("rm", $mfa[0]); hushed_git("commit", "-m", "sskm: undo-add $gl_user$keyid ($fp)") and die "git commit failed\n"; system("env GL_BYPASS_UPDATE_HOOK=1 git push >/dev/null 2>/dev/null") and die "git push failed\n"; } sub kf_del { my($gl_user, $keyid) = @_; cd_temp_clone(); chdir("keydir"); mkdir("zzz-marked"); my @pk = highlander($keyid, 1, grep { m(^(.*/)?$gl_user$keyid.pub$) } @pubkeys); my $fp = fingerprint($pk[0]); hushed_git("mv", $pk[0], "zzz-marked/zzz-marked-for-del-$gl_user$keyid.pub") and die "git mv failed\n"; hushed_git("commit", "-m", "sskm: del $pk[0] ($fp)") and die "git commit failed\n"; system("env GL_BYPASS_UPDATE_HOOK=1 git push >/dev/null 2>/dev/null") and die "git push failed\n"; } sub kf_confirm_del { my($gl_user, $keyid) = @_; my @mfd = highlander($keyid, 1, grep { m(^zzz-marked/zzz-marked-for-del-$gl_user$keyid.pub$) } @marked_for_del); cd_temp_clone(); chdir("keydir"); my $fp = fingerprint($mfd[0]); hushed_git("rm", $mfd[0]); hushed_git("commit", "-m", "sskm: confirm-del $gl_user$keyid ($fp)") and die "git commit failed\n"; system("env GL_BYPASS_UPDATE_HOOK=1 git push >/dev/null 2>/dev/null") and die "git push failed\n"; } sub kf_undo_del { my ($gl_user, $keyid) = @_; my @mfd = highlander($keyid, 1, grep { m(^zzz-marked/zzz-marked-for-del-$gl_user$keyid.pub$) } @marked_for_del); print STDERR " You're undeleting a key that is currently marked for deletion. Hit ENTER to undelete this key Hit Ctrl-C to cancel the undelete Please see documentation for caveats on the undelete process as well as how to actually delete it. "; <>; # yeay... always wanted to do that -- throw away user input! cd_temp_clone(); chdir("keydir"); my $fp = fingerprint($mfd[0]); hushed_git("mv", "-f", $mfd[0], "$gl_user$keyid.pub" ); hushed_git("commit", "-m", "sskm: undo-del $gl_user$keyid ($fp)") and die "git commit failed\n"; system("env GL_BYPASS_UPDATE_HOOK=1 git push >/dev/null 2>/dev/null") and die "git push failed\n"; }