summaryrefslogtreecommitdiffstats
path: root/src/commands/sskm
diff options
context:
space:
mode:
Diffstat (limited to 'src/commands/sskm')
-rwxr-xr-xsrc/commands/sskm281
1 files changed, 281 insertions, 0 deletions
diff --git a/src/commands/sskm b/src/commands/sskm
new file mode 100755
index 0000000..eb51f69
--- /dev/null
+++ b/src/commands/sskm
@@ -0,0 +1,281 @@
+#!/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, $output) = ssh_fingerprint_file(shift);
+ # Do not print the output of $output to an untrusted destination.
+ die "does not seem to be a valid pubkey\n" unless $fp;
+ return $fp;
+}
+
+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";
+}