Skip to content

Instantly share code, notes, and snippets.

@patrick-higgins
Last active December 27, 2015 05:29
Show Gist options
  • Save patrick-higgins/7273942 to your computer and use it in GitHub Desktop.
Save patrick-higgins/7273942 to your computer and use it in GitHub Desktop.
Self-service key management CGI form for gitolite
#!/usr/bin/perl
use strict;
use warnings;
use CGI;
use File::Spec;
use lib $ENV{GL_LIBDIR};
use Gitolite::Rc;
use Gitolite::Common;
########
# Models
########
our $req = CGI->new;
our %flash;
our $gl_user = $req->remote_user();
our @pubkeys;
# make a temp dir
our $TEMPDIR;
BEGIN { $TEMPDIR = `mktemp -d -t tmp.XXXXXXXXXX`; chomp($TEMPDIR); }
END { `/bin/rm -rf $TEMPDIR`; }
# Would like to name this log, but log is already taken for logarithms.
sub logm {
print STDERR "[$gl_user] @_\n";
}
sub fingerprint {
my $fp = `ssh-keygen -l -f $_[0]`;
if ($fp =~ /(([0-9a-f]+:)+[0-9a-f]+) /i) {
return $1;
}
return undef;
}
sub hushed_git {
open my $saveout, ">&STDOUT";
open STDOUT, ">", File::Spec->devnull();
my $rc = system( "git", @_ );
open STDOUT, ">&", $saveout;
return $rc;
}
sub cd_temp_clone {
hushed_git( "clone", "$rc{GL_REPO_BASE}/gitolite-admin.git", "$TEMPDIR" );
chdir($TEMPDIR);
hushed_git( "config", "user.email", "sskm\@gitolite" );
hushed_git( "config", "user.name", "Gitolite SSKM" );
}
# Returns true if first argument is a key owned by $gl_user, false otherwise.
sub ownsKey {
my $pubkey = shift;
my $user = $pubkey;
$user =~ s(.*/)(); # foo/bar/baz.pub -> baz.pub
$user =~ s/(\@[^.]+)?\.pub$//; # baz.pub, [email protected] -> baz
return $user eq $gl_user;
}
sub loadKeys {
# get to the keydir
_chdir("$rc{GL_ADMIN_BASE}/keydir");
for my $pubkey (`find . -type f -name "*.pub" | sort`) {
chomp($pubkey);
$pubkey =~ s(^./)(); # artifact of the find command
if (ownsKey($pubkey)) {
push @pubkeys, $pubkey;
}
}
}
sub flashCookie {
return $req->cookie(
-name => "flash",
-value => \%flash,
-path => $req->url(-absolute=>1, -query=>0),
);
}
#######
# Views
#######
sub showFlash {
my $level = shift;
my $msg = shift;
if (defined $msg) {
$msg = $req->escapeHTML($msg);
print qq{<div class="alert alert-$level">$msg</div>};
}
}
sub layout($) {
my $bodyCallback = shift;
print $req->header(
-type => "text/html",
-cookie => flashCookie(),
);
print qq{<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Gitolite Self-Service Key Management</title>
<!-- Bootstrap -->
<link href="css/bootstrap.min.css" rel="stylesheet">
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="js/html5shiv.js"></script>
<script src="js/respond.min.js"></script>
<![endif]-->
</head>
<body>
};
my %fl = $req->cookie("flash");
showFlash("success", $fl{info});
showFlash("danger", $fl{error});
$bodyCallback->();
print qq{
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="js/jquery.min.js"></script>
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="js/bootstrap.min.js"></script>
</body>
</html>
};
}
sub listKeysView {
print qq{
<div class="container">
<h3>Your gitolite keys</h3>
<form id="delform" method="POST" role="form">
<input type="hidden" name="do" value="del">
<input type="hidden" id="key" name="key" value="">
};
for my $key (@pubkeys) {
my $fp = fingerprint($key);
my $ekey = $req->escapeHTML($key);
print qq{
<div class="row">
<div class="col-md-9"><pre>$ekey ($fp)</pre></div>
<div clss="col-md-2">
<button onclick="\$('#key').val('$ekey'); \$('#delform').submit();" class="btn btn-danger">Delete</button>
</div>
</div>
};
}
print qq{
</form>
<a href="sskm.cgi?do=add" class="btn btn-success">Add Key</a>
</div>
};
}
sub addKeyView {
print qq{
<div class="container">
<h3>Add an SSH Key</h3>
<form method="POST" role="form">
<input type="hidden" name="do" value="add">
<div class="form-group">
<label for="title">Title</label>
<input type="text" class="form-control" id="title" name="title" placeholder="Enter title">
<span class="help-block">A name for your key. Must be alphanumeric words only.</span>
</div>
<div class="form-group">
<label for="key">Key</label>
<textarea id="key" name="key" class="form-control" rows="3"></textarea>
<span class="help-block">Paste your public key here. Read more about how to generate it below.</span>
</div>
<button type="submit" class="btn btn-success">Add Key</button>
</form>
</div>
<div class="container">
<div class="panel panel-default">
<div class="panel-heading">
<h3>About SSH keys</h3>
</div>
<div class="panel-body">
<p>An SSH key allows you to establish a secure connection between your computer and gitolite.</p>
<p>To generate a new SSH key, open your terminal and use code below.</p>
<pre>ssh-keygen -t rsa
# Generating public/private rsa key pair...</pre>
<p>Next, use code below to dump your public key and add to gitolite SSH keys.
<pre>cat ~/.ssh/id_rsa.pub
# ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC6eNtGpNGwstc....</pre>
</div>
</div>
</div>
};
}
sub redirect {
my $uri = shift || $req->url();
print $req->redirect(
-uri => $uri,
-cookie => flashCookie());
}
sub fail {
my $msg = shift;
my $target = shift || $req->url(-full=>1, -query=>1);
logm "Failure: $msg";
$flash{error} = $msg;
redirect($target);
return 0;
}
#############
# Controllers
#############
sub listKeys {
loadKeys();
layout(\&listKeysView);
}
sub addForm {
layout(\&addKeyView);
}
sub addKey {
my $title = $req->param("title");
my $keymaterial = $req->param("key");
$title =~ s/^ +//;
$title =~ s/ +$//;
$title =~ s/ +/_/g;
if ($title !~ /^\w+$/) {
return fail "Invalid title.";
}
# Load these now so we can look for duplicates.
# This runs _chdir, so do it now before switching
# to our temp clone.
loadKeys();
cd_temp_clone();
chdir("keydir");
_print( "$gl_user\@$title.pub", $keymaterial );
my $fp = fingerprint("$gl_user\@$title.pub");
if (!defined($fp)) {
return fail "Invalid key.";
}
for my $key (@pubkeys) {
if ($fp eq fingerprint($key)) {
return fail "You have already added that key ($fp).";
}
}
if (hushed_git( "add", "." ) != 0) {
return fail "git add failed";
}
if (hushed_git( "commit", "-m", "Add $gl_user\@$title ($fp)" ) != 0) {
return fail "git commit failed";
}
if (system("gitolite push >/dev/null 2>/dev/null") != 0) {
return fail "git push failed";
}
$flash{info} = "Added key: $title";
redirect();
}
sub delKey {
my $key = $req->param("key");
if (!ownsKey($key)) {
return fail "You cannot delete keys which are not yours! ($key)";
}
cd_temp_clone();
chdir("keydir");
if (hushed_git( "rm", "-f", $key ) != 0) {
return fail "git rm failed";
}
if (hushed_git( "commit", "-m", "Remove $key" ) != 0) {
return fail "git commit failed";
}
if (system("gitolite push >/dev/null 2>/dev/null") != 0) {
return fail "git push failed";
}
$flash{info} = "Deleted key: $key";
redirect();
}
############
# Dispatcher
############
my $dispatch = {
"GET" => {
"" => \&listKeys,
"add" => \&addForm,
"del" => \&listKeys,
},
"POST" => {
"add" => \&addKey,
"del" => \&delKey,
},
};
my $method = $req->request_method() || "GET";
my $do = $req->param("do") || "";
if (defined($dispatch->{$method}) && defined($dispatch->{$method}->{$do})) {
$dispatch->{$method}->{$do}->();
} else {
fail("Invalid method/do combination: $method/\"$do\"", $req->url(-query=>0));
}
exit 0;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment