#!/usr/bin/perl
use strict;
use warnings;

use lib $ENV{GL_LIBDIR};
use Gitolite::Rc;
use Gitolite::Common;
use Gitolite::Easy;

=for usage
Usage for this command is not that simple. Please read the full
documentation in
https://github.com/sitaramc/gitolite-doc/blob/master/docs/contrib/ukm.mkd
or online at http://gitolite.com/gitolite/contrib/ukm.html.
=cut

usage() if @ARGV and $ARGV[0] eq '-h';

# Terms used in this file.
# pubkeypath: the (relative) filename of a public key starting from
#   gitolite-admin/keydir. Examples: alice.pub, foo/bar/alice.pub,
#   alice@home.pub, foo/alice@laptop.pub. You get more examples, if you
#   replace "alice" by "bob@example.com".
# userid: computed from a pubkeypath by removing any directory
#   part, the '.pub' extension and the "old-style" @NAME classifier.
#   The userid identifies a user in the gitolite.conf file.
# keyid: an identifier for a key given on the command line.
#   If the script is called by one of the super_key_managers, then the
#   keyid is the pubkeypath without the '.pub' extension. Otherwise it
#   is the userid for a guest.
#   The keyid is normalized to lowercase letters.

my $rb = $rc{GL_REPO_BASE};
my $ab = $rc{GL_ADMIN_BASE};

# This will be the subdirectory under "keydir" in which the guest
# keys will be stored. To prevent denial of service, this directory
# should better start with 'zzz'.
# The actual value can be set through the GUEST_DIRECTORY resource.
# WARNING: If this value is changed you must understand the consequences.
#          There will be no support if guestkeys_dir is anything else than
#          'zzz/guests'.
my $guestkeys_dir = 'zzz/guests';

# A guest key cannot have arbitrary names (keyid). Only keys that do *not*
# match $forbidden_guest_pattern are allowed. Super-key-managers can add
# any keyid.

# This is the directory for additional keys of a self key manager.
my $selfkeys_dir = 'zzz/self';
# There is no flexibility for selfkeys. One must specify a keyid that
# matches the regular expression '^@[a-z0-9]+$'. Note that all keyids
# are transformed to lowercase before checking.
my $required_self_pattern = qr([a-z0-9]+);
my $selfkey_management = 0; # disable selfkey managment

# For guest key managers the keyid must pass two tests.
#   1) It must match the $required_guest_pattern regular expression.
#   2) It must not match the $forbidden_guest_pattern regular expression.
# Default for $forbidden_guest_pattern is qr(.), i.e., every keyid is
# forbidden, or in other words, only the gitolite-admin can manage keys.
# Default for $required_guest_pattern is such that the keyid must look
# like an email address, i.e. must have exactly one @ and at least one
# dot after the @.
# Just setting 'ukm' => 1 in .gitolite.rc only allows the super-key-managers
# (i.e., only the gitolite admin(s)) to manage keys.
my $required_guest_pattern =
    qr(^[0-9a-z][-0-9a-z._+]*@[-0-9a-z._+]+[.][-0-9a-z._+]+$);
my $forbidden_guest_pattern = qr(.);

die "The command 'ukm' is not enabled.\n" if ! $rc{'COMMANDS'}{'ukm'};

my $km = $rc{'UKM_CONFIG'};
if(ref($km) eq 'HASH') {
    # If not set we only allow keyids that look like emails
    my $rgp = $rc{'UKM_CONFIG'}{'REQUIRED_GUEST_PATTERN'} || '';
    $required_guest_pattern = qr(^($rgp)$) if $rgp;
    $forbidden_guest_pattern = $rc{'UKM_CONFIG'}{'FORBIDDEN_GUEST_PATTERN'}
                            || $forbidden_guest_pattern;
    $selfkey_management = $rc{'UKM_CONFIG'}{'SELFKEY_MANAGEMENT'} || 0;
}

# get the actual userid
my $gl_user = $ENV{GL_USER};
my $super_key_manager = is_admin(); # or maybe is_super_admin() ?

# save arguments for later
my $operation = shift || 'list';
my $keyid     = shift || '';
$keyid = lc $keyid; # normalize to lowercase ids

my ($zop, $zfp, $zselector, $zuser) = get_pending($gl_user);
# The following will only be true if a selfkey manager logs in to
# perform a pending operation.
my $pending_self = ($zop ne '');

die "You are not a key manager.\n"
    unless $super_key_manager || $pending_self
           || in_group('guest-key-managers')
           || in_group('self-key-managers');

# Let's deal with the pending user first. The only allowed operations
# that are to confirm the add operation with the random code
# that must be provided via stdin or to undo a pending del operation.
if ($pending_self) {
    pending_user($gl_user, $zop, $zfp, $zselector, $zuser);
    exit;
}

my @available_operations = ('list','add','del');
die "unknown ukm subcommand: $operation\n"
    unless grep {$operation eq $_} @available_operations;

# get to the keydir
_chdir("$ab/keydir");

# Note that the program warns if it finds a fingerprint that maps to
# different userids.
my %userids = (); # mapping from fingerprint to userid
my %fingerprints = (); # mapping from pubkeypath to fingerprint
my %pubkeypaths = (); # mapping from userid to pubkeypaths
                      # note that the result is a list of pubkeypaths

# Guest keys are managed by people in the @guest-key-managers group.
# They can only add/del keys in the $guestkeys_dir directory. In fact,
# the guest key manager $gl_user has only access to keys inside
# %guest_pubkeypaths.
my %guest_pubkeypaths = (); # mapping from userid to pubkeypath for $gl_user

# Self keys are managed by people in the @self-key-managers group.
# They can only add/del keys in the $selfkeys_dir directory. In fact,
# the self key manager $gl_user has only access to keys inside
# %self_pubkeypaths.
my %self_pubkeypaths = ();

# These are the keys that are managed by a super key manager.
my @all_pubkeypaths = `find . -type f -name "*.pub" 2>/dev/null | sort`;

for my $pubkeypath (@all_pubkeypaths) {
    chomp($pubkeypath);
    my $fp = fingerprint($pubkeypath);
    $fingerprints{$pubkeypath} = $fp;
    my $userid = get_userid($pubkeypath);
    my ($zop, $zfp, $zselector, $zuser) = get_pending($userid);
    $userid = $zuser if $zop;
    if (! defined $userids{$fp}) {
        $userids{$fp} = $userid;
    } else {
        warn "key $fp is used for different user ids\n"
            unless $userids{$fp} eq $userid;
    }
    push @{$pubkeypaths{$userid}}, $pubkeypath;
    if ($pubkeypath =~ m|^./$guestkeys_dir/([^/]+)/[^/]+\.pub$|) {
        push @{$guest_pubkeypaths{$userid}}, $pubkeypath if $gl_user eq $1;
    }
    if ($pubkeypath =~ m|^./$selfkeys_dir/([^/]+)/[^/]+\.pub$|) {
        push @{$self_pubkeypaths{$userid}}, $pubkeypath if $gl_user eq $1;
    }
}

###################################################################
# do stuff according to the operation
###################################################################

if ( $operation eq 'list' ) {
    list_pubkeys();
    print "\n\n";
    exit;
}

die "keyid required\n" unless $keyid;
die "Not allowed to use '..' in keyid.\n" if $keyid =~ /\.\./;

if ( $operation eq 'add' ) {
    if ($super_key_manager) {
        add_pubkey($gl_user, "$keyid.pub", safe_stdin());
    } elsif (selfselector($keyid)) {
        add_self($gl_user, $keyid, safe_stdin());
    } else {
        # assert ingroup('guest-key-managers');
        add_guest($gl_user, $keyid, safe_stdin());
    }
} elsif ( $operation eq 'del' ) {
    if ($super_key_manager) {
        del_super($gl_user, "$keyid.pub");
    } elsif (selfselector($keyid)) {
        del_self($gl_user, $keyid);
    } else {
        # assert ingroup('guest-key-managers');
        del_guest($gl_user, $keyid);
    }
}

exit;


###################################################################
# only function definitions are following
###################################################################

# make a temp clone and switch to it
our $TEMPDIR;
BEGIN { $TEMPDIR = `mktemp -d -t tmp.XXXXXXXXXX`; chomp($TEMPDIR) }
END { my $err = $?; `/bin/rm -rf $TEMPDIR`; $? = $err; }

sub cd_temp_clone {
    chomp($TEMPDIR);
    hushed_git( "clone", "$rb/gitolite-admin.git", "$TEMPDIR/gitolite-admin" );
    chdir("$TEMPDIR/gitolite-admin");
    my $ip = $ENV{SSH_CONNECTION};
    $ip =~ s/ .*//;
    my ($zop, $zfp, $zselector, $zuser) = get_pending($ENV{GL_USER});
    my $email = $zuser;
    $email .= '@' . $ip  unless $email =~ m(@);
    my $name = $zop ? "\@$zselector" : $zuser;
    # Record the keymanager in the gitolite-admin repo as author of the change.
    hushed_git( "config", "user.email", "$email" );
    hushed_git( "config", "user.name",  "'$name from $ip'" );
}

# compute the fingerprint from the full path of a pubkey file
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;
}


# Read one line from STDIN and return it.
#  If no data is available on STDIN after one second, the empty string
# is returned.
# If there is more than one line or there was an error in reading, the
# function dies.
sub safe_stdin {
    use IO::Select;
    my $s=IO::Select->new(); $s->add(\*STDIN);
    return '' unless $s->can_read(1);
    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;
}

# call git, be quiet
sub hushed_git {
    system("git " . join(" ", @_) . ">/dev/null 2>/dev/null");
}

# Extract the userid from the full path of the pubkey file (relative
# to keydir/ and including the '.pub' extension.
sub get_userid {
    my ($u) = @_; # filename of pubkey relative to keydir/.
    $u =~ s(.*/)();                # foo/bar/baz.pub -> baz.pub
    $u =~ s/(\@[^.]+)?\.pub$//;    # baz.pub, baz@home.pub -> baz
    return $u;
}

# Extract the @selector part from the full path of the pubkey file
# (relative to keydir/ and including the '.pub' extension).
# If there is no @selector part, the empty string is returned.
# We also correctly extract the selector part from pending keys.
sub get_selector {
    my ($u) = @_; # filename of pubkey relative to keydir/.
    $u =~ s(.*/)();                # foo/bar/baz.pub -> baz.pub
    $u =~ s(\.pub$)();             # baz@home.pub -> baz@home
    return $1 if $u =~ m/.\@($required_self_pattern)$/; # baz@home -> home
    my ($zop, $zfp, $zselector, $zuser) = get_pending($u);
    # If $u was not a pending key, then $zselector is the empty string.
    return $zselector;
}

# Extract fingerprint, operation, selector, and true userid from a
# pending userid.
sub get_pending {
    my ($gl_user) = @_;
    return ($1, $2, $3, $4)
       if ($gl_user=~/^zzz-(...)-([0-9a-f]{32})-($required_self_pattern)-(.*)/);
    return ('', '', '', $gl_user)
}

# multiple / and are simplified to one / and the path is made relative
sub sanitize_pubkeypath {
    my ($pubkeypath) = @_;
    $pubkeypath =~ s|//|/|g; # normalize path
    $pubkeypath =~ s,\./,,g; # remove './' from path
    return './'.$pubkeypath; # Don't allow absolute paths.
}

# This function is only relavant for guest key managers.
# It returns true if the pattern is OK and false otherwise.
sub required_guest_keyid {
    local ($_) = @_;
    /$required_guest_pattern/ and ! /$forbidden_guest_pattern/;
}

# The function takes a $keyid as input and returns the keyid with the
# initial @ stripped if everything is fine. It aborts with an error if
# selfkey management is not enabled or the function is called for a
# non-self-key-manager.
# If the required selfkey pattern is not matched, it returns an empty string.
# Thus the function can be used to check whether a given keyid is a
# proper selfkeyid.
sub selfselector {
    my ($keyid) = @_;
    return '' unless $keyid =~ m(^\@($required_self_pattern)$);
    $keyid = $1;
    die "selfkey management is not enabled\n" unless $selfkey_management;
    die "You are not a selfkey manager.\n" if ! in_group('self-key-managers');
    return $keyid;
}

# Return the number of characters reserved for the userid field.
sub userid_width {
    my ($paths) = @_;
    my (%pkpaths) = %{$paths};
    my (@userid_lengths) = sort {$a <=> $b} (map {length($_)} keys %pkpaths);
    @userid_lengths ? $userid_lengths[-1] : 0;
}

# List the keys given by a reference to a hash.
# The regular expression $re is used to remove the initial part of the
# keyid and replace it by what is matched inside the parentheses.
# $format and $width are used for pretty printing
sub list_keys {
    my ($paths, $tokeyid, $format, $width) = @_;
    my (%pkpaths) = %{$paths};
    for my $userid (sort keys %pkpaths) {
        for my $pubkeypath (sort @{$pkpaths{$userid}}) {
            my $fp = $fingerprints{$pubkeypath};
            my $userid = $userids{$fp};
            my $keyid = &{$tokeyid}($pubkeypath);
            printf $format,$fp,$userid,$width+1-length($userid),"",$keyid
                if ($super_key_manager
                    || required_guest_keyid($keyid)
                    || $keyid=~m(^\@));
        }
    }
}

# Turn a pubkeypath into a keyid for super-key-managers, guest-keys,
# and self-keys.
sub superkeyid {
    my ($keyid) = @_;
    $keyid =~ s(\.pub$)();
    $keyid =~ s(^\./)();
    return $keyid;
}

sub guestkeyid {
    my ($keyid) = @_;
    $keyid =~ s(\.pub$)();
    $keyid =~ s(^.*/)();
    return $keyid;
}

sub selfkeyid {
    my ($keyid) = @_;
    $keyid =~ s(\.pub$)();
    $keyid =~ s(^.*/)();
    my ($zop, $zfp, $zselector, $zuser) = get_pending($keyid);
    return "\@$zselector (pending $zop)" if $zop;
    $keyid =~ s(.*@)(@);
    return $keyid;
}

###################################################################

# List public keys managed by the respective user.
# The fingerprints, userids and keyids are printed.
# keyids are shown in a form that can be used for add and del
# subcommands. 
sub list_pubkeys {
    print "Hello $gl_user, you manage the following keys:\n";
    my $format = "%-47s %s%*s%s\n";
    my $width = 0;
    if ($super_key_manager) {
        $width = userid_width(\%pubkeypaths);
        $width = 6 if $width < 6; # length("userid")==6
        printf $format, "fingerprint", "userid", ($width-5), "", "keyid";
        list_keys(\%pubkeypaths, , \&superkeyid, $format, $width);
    } else {
        my $widths = $selfkey_management?userid_width(\%self_pubkeypaths):0;
        my $widthg = userid_width(\%guest_pubkeypaths);
        $width = $widths > $widthg ? $widths : $widthg; # maximum width
        return unless $width; # there are no keys
        $width = 6 if $width < 6; # length("userid")==6
        printf $format, "fingerprint", "userid", ($width-5), "", "keyid";
        list_keys(\%self_pubkeypaths, \&selfkeyid, $format, $width)
            if $selfkey_management;
        list_keys(\%guest_pubkeypaths, \&guestkeyid,  $format, $width);
    }
}


###################################################################

# Add a public key for the user $gl_user.
# $pubkeypath is the place where the new key will be stored.
# If the file or its fingerprint already exists, the operation is
# rejected.
sub add_pubkey {
    my ( $gl_user, $pubkeypath, $keymaterial ) = @_;
    if(! $keymaterial) {
        print STDERR "Please supply the new key on STDIN.\n";
        print STDERR "Try something like this:\n";
        print STDERR "cat FOO.pub | ssh GIT\@GITOLITESERVER ukm add KEYID\n";
        die "missing public key data\n";
    }
    # clean pubkeypath a bit
    $pubkeypath = sanitize_pubkeypath($pubkeypath);
    # Check that there is not yet something there already.
    die "cannot override existing key\n" if $fingerprints{$pubkeypath};

    my $userid = get_userid($pubkeypath);
    # Super key managers shouldn't be able to add a that leads to
    # either an empty userid or to a userid that starts with @.
    #
    # To avoid confusion, all keyids for super key managers must be in
    # a full path format. Having a public key of the form
    # gitolite-admin/keydir/@foo.pub might be confusing and might lead
    # to other problems elsewhere.
    die "cannot add key that starts with \@\n" if (!$userid) || $userid=~/^@/;

    cd_temp_clone();
    _chdir("keydir");
    $pubkeypath =~ m((.*)/); # get the directory part
    _mkdir($1);
    _print($pubkeypath, $keymaterial);
    my $fp = fingerprint($pubkeypath);

    # Maybe we are adding a selfkey.
    my ($zop, $zfp, $zselector, $zuser) = get_pending($userid);
    my $user = $zop ? "$zuser\@$zselector" : $userid;
    $userid = $zuser;
    # Check that there isn't a key with the same fingerprint under a
    # different userid.
    if (defined $userids{$fp}) {
        if ($userid ne $userids{$fp}) {
            print STDERR "Found  $fp $userids{$fp}\n" if $super_key_manager;
            print STDERR "Same key is already available under another userid.\n";
            die "cannot add key\n";
        } elsif ($zop) {
            # Because of the way a key is confirmed with ukm, it is
            # impossible to confirm the initial key of the user as a
            # new selfkey. (It will lead to the function list_pubkeys
            # instead of pending_user_add, because the gl_user will
            # not be that of a pending user.) To avoid confusion, we,
            # therefore, forbid to add the user's initial key
            # altogether.
            # In fact, we here also forbid to add any key for that
            # user that is already in the system.
            die "You cannot add a key that already belongs to you.\n";
        }
    } else {# this fingerprint does not yet exist
        my @paths = @{$pubkeypaths{$userid}} if defined $pubkeypaths{$userid};
        if (@paths) {# there are already keys for $userid
            if (grep {$pubkeypath eq $_} @paths) {
                print STDERR "The keyid is already present. Nothing changed.\n";
            } elsif ($super_key_manager) {
                # It's OK to add new selfkeys, but here we are in the case
                # of adding multiple keys for guests. That is forbidden.
                print STDERR "Adding new public key for $userid.\n";
            } elsif ($pubkeypath =~ m(^\./$guestkeys_dir/)) {
                # Arriving here means we are about to add a *new*
                # guest key, because the fingerprint is not yet
                # existing. This would be for an already existing
                # userid (added by another guest key manager). Since
                # that effectively means to (silently) add an
                # additional key for an existing user, it must be
                # forbidden.
                die "cannot add another public key for an existing user\n";
            }
        }
    }
    exit if (`git status -s` eq ''); # OK to add identical keys twice
    hushed_git( "add", "." ) and die "git add failed\n";
    hushed_git( "commit", "-m", "'ukm add $gl_user $userid\n\n$fp'" )
        and die "git commit failed\n";
    system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n";
}

# Guest key managers should not be allowed to add directories or
# multiple keys via the @domain mechanism, since this might allow
# another guest key manager to give an attacker access to another
# user's repositories.
#
# Example: Alice adds bob.pub for bob@example.org. David adds eve.pub
# (where only Eve but not Bob has the private key) under the keyid
# bob@example.org@foo. This basically gives Eve the same rights as
# Bob.
sub add_guest {
    my ( $gl_user, $keyid, $keymaterial ) = @_;
    die "keyid not allowed: '$keyid'\n"
        if $keyid =~ m(@.*@) or $keyid =~ m(/) or !required_guest_keyid($keyid);
    add_pubkey($gl_user, "$guestkeys_dir/$gl_user/$keyid.pub", $keymaterial);
}

# Add a new selfkey for user $gl_user.
sub add_self {
    my ( $gl_user, $keyid, $keymaterial ) = @_;
    my $selector = "";
    $selector = selfselector($keyid); # might return empty string
    die "keyid not allowed: $keyid\n" unless $selector;

    # Check that the new selector is not already in use even not in a
    # pending state.
    die "keyid already in use: $keyid\n"
        if grep {selfkeyid($_)=~/^\@$selector( .*)?$/} @{$self_pubkeypaths{$gl_user}};
    # generate new pubkey create fingerprint
    system("ssh-keygen -N '' -q -f \"$TEMPDIR/session\" -C $gl_user");
    my $sessionfp = fingerprint("$TEMPDIR/session.pub");
    $sessionfp =~ s/://g;
    my $user = "zzz-add-$sessionfp-$selector-$gl_user";
    add_pubkey($gl_user, "$selfkeys_dir/$gl_user/$user.pub", $keymaterial);
    print `cat "$TEMPDIR/session.pub"`;
}

###################################################################


# Delete a key of user $gl_user.
sub del_pubkey {
    my ($gl_user, $pubkeypath) = @_;
    $pubkeypath = sanitize_pubkeypath($pubkeypath);
    my $fp = $fingerprints{$pubkeypath};
    die "key not found\n" unless $fp;
    cd_temp_clone();
    chdir("keydir");
    hushed_git( "rm", "$pubkeypath" ) and die "git rm failed\n";
    my $userid = get_userid($pubkeypath);
    hushed_git( "commit", "-m", "'ukm del $gl_user $userid\n\n$fp'" )
        and die "git commit failed\n";
    system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n";
}

# $gl_user is a super key manager. This function aborts if the
# superkey manager tries to remove his last key.
sub del_super {
    my ($gl_user, $pubkeypath) = @_;
    $pubkeypath = sanitize_pubkeypath($pubkeypath);
    die "You are not managing the key $keyid.\n"
        unless grep {$_ eq $pubkeypath} @all_pubkeypaths;
    my $userid = get_userid($pubkeypath);
    if ($gl_user eq $userid) {
        my @paths = @{$pubkeypaths{$userid}};
        die "You cannot delete your last key.\n"
            if scalar(grep {$userid eq get_userid($_)} @paths)<2;
    }
    del_pubkey($gl_user, $pubkeypath);
}

sub del_guest {
    my ($gl_user, $keyid) = @_;
    my $pubkeypath = sanitize_pubkeypath("$guestkeys_dir/$gl_user/$keyid.pub");
    my $userid = get_userid($pubkeypath);
    # Check whether $gl_user actually manages $keyid.
    my @paths = ();
    @paths = @{$guest_pubkeypaths{$userid}}
        if defined $guest_pubkeypaths{$userid};
    die "You are not managing the key $keyid.\n"
        unless grep {$_ eq $pubkeypath} @paths;
    del_pubkey($gl_user, $pubkeypath);
}

# Delete a selfkey of $gl_user. The first delete is a preparation of
# the deletion and only a second call will actually delete the key. If
# the second call is done with the key that is scheduled for deletion,
# it is basically undoing the previous del call. This last case is
# handled in function pending_user_del.
sub del_self {
    my ($gl_user, $keyid) = @_;
    my $selector = selfselector($keyid); # might return empty string
    die "keyid not allowed: '$keyid'\n" unless $selector;

    # Does $gl_user actually manage that keyid?
    # All (non-pending) selfkeys have an @selector part in their pubkeypath.
    my @paths = @{$self_pubkeypaths{$gl_user}};
    die "You are not managing the key $keyid.\n"
        unless grep {$selector eq get_selector($_)} @paths;

    cd_temp_clone();
    _chdir("keydir");
    my $fp = '';
    # Is it the first or the second del call? It's the second call, if
    # there is a scheduled-for-deletion or scheduled-for-addition
    # selfkey which has the given keyid as a selector part.
    @paths = grep {
        my ($zop, $zfp, $zselector, $zuser) = get_pending(get_userid($_));
        $zselector eq $selector
    } @paths;
    if (@paths) {# start actual deletion of the key (second call)
        my $pubkeypath = $paths[0];
        $fp = fingerprint($pubkeypath);
        my ($zop, $zf, $zs, $zu) = get_pending(get_userid($pubkeypath));
        $zop = $zop eq 'add' ? 'undo-add' : 'confirm-del';
        hushed_git("rm", "$pubkeypath") and die "git rm failed\n";
        hushed_git("commit", "-m", "'ukm $zop $gl_user\@$selector\n\n$fp'")
            and die "git commit failed\n";
        system("gitolite push >/dev/null 2>/dev/null")
            and die "git push failed\n";
        print STDERR "pending keyid deleted: \@$selector\n";
        return;
    }
    my $oldpubkeypath = "$selfkeys_dir/$gl_user/$gl_user\@$selector.pub";
    # generate new pubkey and create fingerprint to get a random number
    system("ssh-keygen -N '' -q -f \"$TEMPDIR/session\" -C $gl_user");
    my $sessionfp = fingerprint("$TEMPDIR/session.pub");
    $sessionfp =~ s/://g;
    my $user = "zzz-del-$sessionfp-$selector-$gl_user";
    my $newpubkeypath = "$selfkeys_dir/$gl_user/$user.pub";

    # A key for gitolite access that is in authorized_keys and not
    # existing in the expected place under keydir/ should actually not
    # happen, but one never knows.
    die "key not available\n" unless -r $oldpubkeypath;

    # For some strange reason the target key already exists.
    die "cannot override existing key\n" if -e $newpubkeypath;

    $fp = fingerprint($oldpubkeypath);
    print STDERR "prepare deletion of key \@$selector\n";
    hushed_git("mv", "$oldpubkeypath", "$newpubkeypath")
        and die "git mv failed\n";
    hushed_git("commit", "-m", "'ukm prepare-del $gl_user\@$selector\n\n$fp'")
        and die "git commit failed\n";
    system("gitolite push >/dev/null 2>/dev/null")
        and die "git push failed\n";
}

###################################################################
# Adding a selfkey should be done as follows.
#
#   cat newkey.pub | ssh git@host ukm add @selector > session
#   cat session | ssh -i newkey git@host ukm
#
# The provided random data will come from a newly generated ssh key
# whose fingerprint will be stored in $gl_user. So we compute the
# fingerprint of the data that is given to us. If it doesn't match the
# fingerprint, then something went wrong and the confirm operation is
# forbidden, in fact, the pending key will be removed from the system.
sub pending_user_add {
    my ($gl_user, $zfp, $zselector, $zuser) = @_;
    my $oldpubkeypath = "$selfkeys_dir/$zuser/$gl_user.pub";
    my $newpubkeypath = "$selfkeys_dir/$zuser/$zuser\@$zselector.pub";

    # A key for gitolite access that is in authorized_keys and not
    # existing in the expected place under keydir/ should actually not
    # happen, but one never knows.
    die "key not available\n" unless -r $oldpubkeypath;

    my $keymaterial = safe_stdin();
    # If there is no keymaterial (which corresponds to a session key
    # for the confirm-add operation), logging in to this key, removes
    # it from the system.
    my $session_key_not_provided = '';
    if (!$keymaterial) {
        $session_key_not_provided = "missing session key";
    } else {
        _print("$TEMPDIR/session.pub", $keymaterial);
        my $sessionfp = fingerprint("$TEMPDIR/session.pub");
        $sessionfp =~ s/://g;
        $session_key_not_provided = "session key not accepted"
            unless ($zfp eq $sessionfp)
    }
    my $fp = fingerprint($oldpubkeypath);
    if ($session_key_not_provided) {
        print STDERR "$session_key_not_provided\n";
        print STDERR "pending keyid deleted: \@$zselector\n";
        hushed_git("rm", "$oldpubkeypath") and die "git rm failed\n";
        hushed_git("commit", "-m", "'ukm del $zuser\@$zselector\n\n$fp'")
            and die "git commit failed\n";
        system("gitolite push >/dev/null 2>/dev/null")
            and die "git push failed\n";
        return;
    }

    # For some strange reason the target key already exists.
    die "cannot override existing key\n" if -e $newpubkeypath;

    print STDERR "pending keyid added: \@$zselector\n";
    hushed_git("mv", "$oldpubkeypath", "$newpubkeypath")
        and die "git mv failed\n";
    hushed_git("commit", "-m", "'ukm confirm-add $zuser\@$zselector\n\n$fp'")
        and die "git commit failed\n";
    system("gitolite push >/dev/null 2>/dev/null")
        and die "git push failed\n";
}

# To delete a key, one must first bring the key into a pending state
# and then truely delete it with another key. In case, the login
# happens with the pending key (implemented below), it means that the
# delete operation has to be undone.
sub pending_user_del {
    my ($gl_user, $zfp, $zselector, $zuser) = @_;
    my $oldpubkeypath = "$selfkeys_dir/$zuser/$gl_user.pub";
    my $newpubkeypath = "$selfkeys_dir/$zuser/$zuser\@$zselector.pub";
    print STDERR "undo pending deletion of keyid \@$zselector\n";
    # A key for gitolite access that is in authorized_keys and not
    # existing in the expected place under keydir/ should actually not
    # happen, but one never knows.
    die "key not available\n" unless -r $oldpubkeypath;
    # For some strange reason the target key already exists.
    die "cannot override existing key\n" if -e $newpubkeypath;
    my $fp = fingerprint($oldpubkeypath);
    hushed_git("mv", "$oldpubkeypath", "$newpubkeypath")
        and die "git mv failed\n";
    hushed_git("commit", "-m", "'ukm undo-del $zuser\@$zselector\n\n$fp'")
        and die "git commit failed\n";
}

# A user whose key is in pending state cannot do much. In fact,
# logging in as such a user simply takes back the "bringing into
# pending state", i.e. a key scheduled for adding is remove and a key
# scheduled for deletion is brought back into its properly added state.
sub pending_user {
    my ($gl_user, $zop, $zfp, $zselector, $zuser) = @_;
    cd_temp_clone();
    _chdir("keydir");
    if ($zop eq 'add') {
        pending_user_add($gl_user, $zfp, $zselector, $zuser);
    } elsif ($zop eq 'del') {
        pending_user_del($gl_user, $zfp, $zselector, $zuser);
    } else {
        die "unknown operation\n";
    }
    system("gitolite push >/dev/null 2>/dev/null")
        and die "git push failed\n";
}
