#!/usr/bin/perl use strict; use warnings; use lib $ENV{GL_LIBDIR}; use Gitolite::Rc; use Gitolite::Common; =for usage Usage for this command is not that simple. Please read the full documentation in doc/sskm.mkd or online at http://gitolite.com/gitolite/sskm.html. =cut usage() if @ARGV and $ARGV[0] eq '-h'; my $rb = $rc{GL_REPO_BASE}; my $ab = $rc{GL_ADMIN_BASE}; # get to the keydir _chdir("$ab/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", "$rb/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"); _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("gitolite 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("gitolite 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("gitolite 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("gitolite 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("gitolite 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("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n"; }