Last active
December 27, 2015 05:29
-
-
Save patrick-higgins/7273942 to your computer and use it in GitHub Desktop.
Self-service key management CGI form for gitolite
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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