api: Added signed certificate tagging mechanism

This commit is contained in:
Lauri Võsandi 2015-12-16 17:41:49 +00:00
parent 901b0f7224
commit da6600e2e9
21 changed files with 501 additions and 143 deletions

View File

@ -79,6 +79,7 @@ def certidude_app():
from .lease import LeaseResource
from .whois import WhoisResource
from .log import LogResource
from .tag import TagResource, TagDetailResource
app = falcon.API()
@ -91,6 +92,8 @@ def certidude_app():
app.add_route("/api/request/{cn}/", RequestDetailResource())
app.add_route("/api/request/", RequestListResource())
app.add_route("/api/log/", LogResource())
app.add_route("/api/tag/", TagResource())
app.add_route("/api/tag/{identifier}/", TagDetailResource())
app.add_route("/api/", SessionResource())
# Gateway API calls, should this be moved to separate project?
@ -128,12 +131,13 @@ def certidude_app():
logger.addHandler(push_handler)
logging.getLogger("cli").info("Started Certidude at %s", socket.getaddrinfo(socket.gethostname(), 0, flags=socket.AI_CANONNAME)[0][3])
logging.getLogger("cli").debug("Started Certidude at %s",
socket.getaddrinfo(socket.gethostname(), 0, flags=socket.AI_CANONNAME)[0][3])
import atexit
def exit_handler():
logging.getLogger("cli").info("Shutting down Certidude")
logging.getLogger("cli").debug("Shutting down Certidude")
atexit.register(exit_handler)

View File

@ -73,7 +73,7 @@ class RequestListResource(object):
raise falcon.HTTPConflict(
"CSR with such CN already exists",
"Will not overwrite existing certificate signing request, explicitly delete CSR and try again")
push.publish("request_submitted", csr.common_name)
push.publish("request-submitted", csr.common_name)
# Wait the certificate to be signed if waiting is requested
if req.get_param("wait"):

View File

@ -1,9 +1,12 @@
import falcon
import logging
from certidude import authority
from certidude.auth import login_required, authorize_admin
from certidude.decorators import serialize
logger = logging.getLogger("api")
class SignedCertificateListResource(object):
@serialize
@authorize_admin
@ -26,13 +29,17 @@ class SignedCertificateDetailResource(object):
@serialize
def on_get(self, req, resp, cn):
try:
logger.info("Served certificate %s to %s", cn, req.env["REMOTE_ADDR"])
resp.set_header("Content-Disposition", "attachment; filename=%s.crt" % cn)
return authority.get_signed(cn)
except FileNotFoundError:
logger.warning("Failed to serve non-existant certificate %s to %s", cn, req.env["REMOTE_ADDR"])
resp.body = "No certificate CN=%s found" % cn
raise falcon.HTTPNotFound()
@login_required
@authorize_admin
def on_delete(self, req, resp, cn):
logger.info("Revoked certificate %s by %s from %s", cn, req.context["user"], req.env["REMOTE_ADDR"])
authority.revoke_certificate(cn)

90
certidude/api/tag.py Normal file
View File

@ -0,0 +1,90 @@
import falcon
import logging
from certidude import config
from certidude.auth import login_required, authorize_admin
from certidude.decorators import serialize
logger = logging.getLogger("api")
class TagResource(object):
@serialize
@login_required
@authorize_admin
def on_get(self, req, resp):
conn = config.DATABASE_POOL.get_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute("select * from tag")
def g():
for row in cursor:
yield row
cursor.close()
conn.close()
return tuple(g())
@serialize
@login_required
@authorize_admin
def on_post(self, req, resp):
from certidude import push
conn = config.DATABASE_POOL.get_connection()
cursor = conn.cursor()
args = req.get_param("cn"), req.get_param("key"), req.get_param("value")
cursor.execute(
"insert into tag (`cn`, `key`, `value`) values (%s, %s, %s)", args)
push.publish("tag-added", str(cursor.lastrowid))
logger.debug("Tag cn=%s, key=%s, value=%s added" % args)
conn.commit()
cursor.close()
conn.close()
class TagDetailResource(object):
@serialize
@login_required
@authorize_admin
def on_get(self, req, resp, identifier):
conn = config.DATABASE_POOL.get_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute("select * from tag where `id` = %s", (identifier,))
for row in cursor:
cursor.close()
conn.close()
return row
cursor.close()
conn.close()
raise falcon.HTTPNotFound()
@serialize
@login_required
@authorize_admin
def on_put(self, req, resp, identifier):
from certidude import push
conn = config.DATABASE_POOL.get_connection()
cursor = conn.cursor()
cursor.execute("update tag set `value` = %s where `id` = %s limit 1",
(req.get_param("value"), identifier))
conn.commit()
cursor.close()
conn.close()
logger.debug("Tag %s updated, value set to %s",
identifier, req.get_param("value"))
push.publish("tag-updated", identifier)
@serialize
@login_required
@authorize_admin
def on_delete(self, req, resp, identifier):
from certidude import push
conn = config.DATABASE_POOL.get_connection()
cursor = conn.cursor()
cursor.execute("delete from tag where tag.id = %s", (identifier,))
conn.commit()
cursor.close()
conn.close()
push.publish("tag-removed", identifier)
logger.debug("Tag %s removed" % identifier)

View File

@ -1,8 +1,10 @@
import falcon
import ipaddress
from datetime import datetime
from certidude import config
from certidude.decorators import serialize
from certidude.api.lease import parse_dn
def address_to_identity(cnx, addr):
"""
@ -10,19 +12,19 @@ def address_to_identity(cnx, addr):
"""
SQL_LEASES = """
SELECT
select
acquired,
released,
identities.data as identity
FROM
from
addresses
RIGHT JOIN
right join
identities
ON
on
identities.id = addresses.identity
WHERE
address = %s AND
released IS NOT NULL
where
address = %s and
released is not null
"""
cursor = cnx.cursor()
@ -31,6 +33,7 @@ def address_to_identity(cnx, addr):
for acquired, released, identity in cursor:
return {
"address": addr,
"acquired": datetime.utcfromtimestamp(acquired),
"identity": parse_dn(bytes(identity))
}

View File

@ -29,7 +29,7 @@ def publish_certificate(func):
click.echo("Publishing certificate at %s, waiting for response..." % url)
response = urllib.request.urlopen(notification)
response.read()
push.publish("request_signed", csr.common_name)
push.publish("request-signed", csr.common_name)
return cert
return wrapped
@ -93,7 +93,7 @@ def revoke_certificate(common_name):
cert = get_signed(common_name)
revoked_filename = os.path.join(config.REVOKED_DIR, "%s.pem" % cert.serial_number)
os.rename(cert.path, revoked_filename)
push.publish("certificate_revoked", cert.fingerprint())
push.publish("certificate-revoked", cert.fingerprint())
def list_requests(directory=config.REQUESTS_DIR):
@ -141,7 +141,7 @@ def delete_request(common_name):
os.unlink(path)
# Publish event at CA channel
push.publish("request_deleted", request_sha1sum)
push.publish("request-deleted", request_sha1sum)
# Write empty certificate to long-polling URL
url = config.PUSH_PUBLISH % request_sha1sum

View File

@ -13,7 +13,6 @@ import signal
import socket
import subprocess
import sys
from certidude import authority
from certidude.signer import SignServer
from certidude.common import expand_paths
from datetime import datetime
@ -165,7 +164,7 @@ def certidude_setup_client(quiet, **kwargs):
@click.command("server", help="Set up OpenVPN server")
@click.argument("url")
@click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, %s by default" % HOSTNAME)
@click.option("--common-name", "-cn", default=FQDN, help="Common name, %s by default" % FQDN)
@click.option("--org-unit", "-ou", help="Organizational unit")
@click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, '%s' by default" % EMAIL)
@click.option("--subnet", "-s", default="192.168.33.0/24", type=ip_network, help="OpenVPN subnet, 192.168.33.0/24 by default")
@ -252,7 +251,7 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co
@click.option("--authority-path", "-a", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to --dir by default")
@expand_paths()
def certidude_setup_openvpn_client(url, config, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, proto, remote):
from certidude.helpers import certidude_request_certificate
retval = certidude_request_certificate(
url,
key_path,
@ -280,7 +279,7 @@ def certidude_setup_openvpn_client(url, config, email_address, common_name, org_
@click.command("server", help="Set up strongSwan server")
@click.argument("url")
@click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, %s by default" % HOSTNAME)
@click.option("--common-name", "-cn", default=FQDN, help="Common name, %s by default" % FQDN)
@click.option("--org-unit", "-ou", help="Organizational unit")
@click.option("--fqdn", "-f", default=FQDN, help="Fully qualified hostname associated with the certificate")
@click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, %s by default" % EMAIL)
@ -302,13 +301,17 @@ def certidude_setup_openvpn_client(url, config, email_address, common_name, org_
@click.option("--authority-path", "-ca", default="cacerts/ca.pem", help="Certificate authority certificate path, cacerts/ca.pem by default")
@expand_paths()
def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, local, fqdn):
if "." not in common_name:
raise ValueError("Hostname has to be fully qualified!")
if not local:
raise ValueError("Please specify local IP address")
if not os.path.exists(certificate_path):
click.echo("As strongSwan server certificate needs specific key usage extensions please")
click.echo("use following command to sign on Certidude server instead of web interface:")
click.echo()
click.echo(" certidude sign %s" % common_name)
from certidude.helpers import certidude_request_certificate
retval = certidude_request_certificate(
url,
key_path,
@ -368,7 +371,7 @@ def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email
@click.option("--authority-path", "-ca", default="cacerts/ca.pem", help="Certificate authority certificate path, cacerts/ca.pem by default")
@expand_paths()
def certidude_setup_strongswan_client(url, config, secrets, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, remote, auto, dpdaction):
from certidude.helpers import certidude_request_certificate
retval = certidude_request_certificate(
url,
key_path,
@ -409,7 +412,7 @@ def certidude_setup_strongswan_client(url, config, secrets, email_address, commo
@click.option("--authority-path", "-ca", default="cacerts/ca.pem", help="Certificate authority certificate path, cacerts/ca.pem by default")
@expand_paths()
def certidude_setup_strongswan_networkmanager(url, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, remote):
from certidude.helpers import certidude_request_certificate
retval = certidude_request_certificate(
url,
key_path,
@ -685,6 +688,8 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign
# y - not valid yet
# r - revoked
from certidude import authority
from pycountry import countries
def dump_common(j):
@ -796,6 +801,7 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign
@click.option("--overwrite", "-o", default=False, is_flag=True, help="Revoke valid certificate with same CN")
@click.option("--lifetime", "-l", help="Lifetime")
def certidude_sign(common_name, overwrite, lifetime):
from certidude import authority
request = authority.get_request(common_name)
if request.signable:
# Sign via signer process

View File

@ -1,4 +1,7 @@
import os
import click
def expand_paths():
"""
Prefix '..._path' keyword arguments of target function with 'directory' keyword argument

View File

@ -2,11 +2,9 @@
import click
import os
import urllib.request
from certidude import config
from certidude.wrappers import Certificate, Request
from OpenSSL import crypto
def certidude_request_certificate(url, key_path, request_path, certificate_path, authority_path, common_name, org_unit, email_address=None, given_name=None, surname=None, autosign=False, wait=False, key_usage=None, extended_key_usage=None, ip_address=None, dns=None):
"""
Exchange CSR for certificate using Certidude HTTP API server

View File

@ -1,19 +1,19 @@
<section id="about">
<p>Hi {{session.username}},</p>
<p>Request submission is allowed from: {% if session.request_subnets %}{% for i in session.request_subnets %}{{ i }} {% endfor %}{% else %}anywhere{% endif %}</p>
<p>Autosign is allowed from: {% if session.autosign_subnets %}{% for i in session.autosign_subnets %}{{ i }} {% endfor %}{% else %}nowhere{% endif %}</p>
<p>Authority administration is allowed from: {% if session.admin_subnets %}{% for i in session.admin_subnets %}{{ i }} {% endfor %}{% else %}anywhere{% endif %}
<p>Authority administration allowed for: {% for i in session.admin_users %}{{ i }} {% endfor %}</p>
</section>
{% set s = session.certificate.identity %}
<div id="requests">
<section id="requests">
<h1>Pending requests</h1>
<ul id="pending_requests">
{% for request in session.requests %}
{% include "request.html" %}
@ -23,19 +23,20 @@
<pre>certidude setup client {{session.common_name}}</pre>
</li>
</ul>
</div>
</section>
<div id="signed">
<section id="signed">
<h1>Signed certificates</h1>
<input id="search" type="search" class="icon search">
<ul id="signed_certificates">
{% for certificate in session.signed | sort | reverse %}
{% include "signed.html" %}
{% endfor %}
</ul>
</div>
</section>
<div id="log">
<section id="log">
<h1>Log</h1>
<p>
<input id="log_level_critical" type="checkbox" checked/> <label for="log_level_critical">Critical</label>
@ -46,9 +47,9 @@
</p>
<ul id="log_entries">
</ul>
</div>
</section>
<div id="revoked">
<section id="revoked">
<h1>Revoked certificates</h1>
<p>To fetch certificate revocation list:</p>
<pre>
@ -72,4 +73,4 @@
<li>Great job! No certificate signing requests to sign.</li>
{% endfor %}
</ul>
</div>
</section>

View File

@ -119,7 +119,7 @@ h2 svg {
top: 16px;
}
p, td, footer, li, button, input {
p, td, footer, li, button, input, select {
font-family: 'PT Sans Narrow';
font-size: 14pt;
}
@ -159,6 +159,7 @@ pre {
display: inline;
margin: 1mm 5mm 1mm 0;
line-height: 200%;
cursor: pointer;
}
.icon{
@ -170,24 +171,49 @@ pre {
text-decoration: none;
}
li span.icon {
#log_entries li span.icon {
background-size: 32px;
padding-left: 42px;
padding-top: 2px;
padding-bottom: 2px;
}
.tags .tag {
display: inline;
background-size: 32px;
padding-top: 4px;
padding-bottom: 4px;
padding-right: 1em;
line-height: 200%;
cursor: pointer;
white-space: nowrap;
}
select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: none;
border: none;
box-sizing: border-box;
}
.icon.tag { background-image: url("../img/iconmonstr-tag-2-icon.svg"); }
.icon.critical { background-image: url("../img/iconmonstr-error-4-icon.svg"); }
.icon.error { background-image: url("../img/iconmonstr-error-4-icon.svg"); }
.icon.warning { background-image: url("../img/iconmonstr-warning-6-icon.svg"); }
.icon.info { background-image: url("../img/iconmonstr-info-6-icon.svg"); }
.icon.revoke { background-image: url("../img/iconmonstr-x-mark-5-icon.svg"); }
.icon.download { background-image: url("../img/iconmonstr-download-12-icon.svg"); }
.icon.sign { background-image: url("../img/iconmonstr-pen-10-icon.svg"); }
.icon.search { background-image: url("../img/iconmonstr-magnifier-4-icon.svg"); }
.icon.phone { background-image: url("../img/iconmonstr-mobile-phone-6-icon.svg"); }
.icon.location { background-image: url("../img/iconmonstr-compass-7-icon.svg"); }
.icon.room { background-image: url("../img/iconmonstr-home-4-icon.svg"); }
/* Make sure this is the last one */
.icon.busy{background-image:url("https://software.opensuse.org/assets/ajax-loader-ea46060b6c9f42822a3d58d075c83ea2.gif");}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.
You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="32px" height="32px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
<path id="compass-7-icon" d="M256,90c91.74,0,166,74.243,166,166c0,91.741-74.245,166-166,166c-91.741,0-166-74.245-166-166
C90,164.259,164.244,90,256,90 M256,50C142.229,50,50,142.229,50,256s92.229,206,206,206s206-92.229,206-206S369.771,50,256,50z
M197.686,216.466l-28.355-47.135l47.225,28.408C209.145,202.733,202.736,209.099,197.686,216.466z M296.709,198.612
c6.459,4.562,12.119,10.179,16.729,16.602l29.232-45.883L296.709,198.612z M198.312,297.179l-28.982,45.492l45.416-28.936
C208.398,309.163,202.838,303.563,198.312,297.179z M296.018,314.604l46.652,28.066l-28.117-46.74
C309.596,303.253,303.299,309.593,296.018,314.604z M400.199,256.001l-99.238,21.998c-4.369,8.913-11.312,16.328-19.859,21.295
L256,400.2l-25.104-100.908c-8.545-4.965-15.488-12.381-19.857-21.293l-99.238-21.998l99.238-21.999
c4.369-8.913,11.312-16.328,19.857-21.294L256,111.8l25.104,100.908c8.545,4.966,15.488,12.381,19.857,21.294L400.199,256.001z
M278.406,256c0-12.374-10.031-22.407-22.406-22.407S233.592,243.626,233.592,256c0,12.376,10.033,22.408,22.408,22.408
S278.406,268.376,278.406,256z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.
You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="32px" height="32px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
<path id="home-4-icon" d="M419.492,275.815v166.213H300.725v-90.33h-89.451v90.33H92.507V275.815H50L256,69.972l206,205.844H419.492
z M394.072,88.472h-47.917v38.311l47.917,48.023V88.472z"/>
</svg>

After

Width:  |  Height:  |  Size: 836 B

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.
You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="32px" height="32px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
<path id="mobile-phone-6-icon" d="M139.59,131.775c-13.807,0-25,11.197-25,25.01V436.99c0,13.812,11.193,25.01,25,25.01h150.49
c13.807,0,25-11.198,25-25.01V156.766c0-13.802-11.186-24.99-24.98-24.99H139.59z M179.832,416.514h-30.996v-24.51h30.996V416.514z
M179.832,372.203h-30.996v-24.51h30.996V372.203z M230.334,416.514h-30.996v-24.51h30.996V416.514z M230.334,372.203h-30.996
v-24.51h30.996V372.203z M280.836,416.514H249.84v-24.51h30.996V416.514z M280.836,372.203H249.84v-24.51h30.996V372.203z
M280.836,312.887h-132V183.226h132V312.887z M283.451,111.408c13.445-0.01,26.9,5.113,37.164,15.369s15.4,23.699,15.41,37.147
h22.121c-0.012-19.113-7.312-38.231-21.898-52.805c-14.588-14.573-33.691-21.854-52.797-21.842V111.408z M283.451,72.682
c23.354-0.015,46.691,8.882,64.52,26.696c17.828,17.812,26.75,41.187,26.766,64.547h22.674c-0.02-29.166-11.16-58.358-33.418-80.597
C341.734,61.089,312.605,49.982,283.451,50V72.682z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.
You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="32px" height="32px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
<path id="tag-2-icon" d="M234.508,50L50.068,50.262l-0.004,184.311L277.365,462l184.57-184.57L234.508,50z M114.877,167.365
c-15.027-15.027-15.027-39.395,0-54.424c15.029-15.029,39.396-15.029,54.426,0s15.029,39.396,0,54.424
C154.273,182.395,129.906,182.395,114.877,167.365z M242.316,327.94l-76.225-76.226l17.678-17.678l76.225,76.226L242.316,327.94z
M317.609,335.887L199.764,218.041l17.678-17.678l117.846,117.846L317.609,335.887z M351.818,301.678L233.973,183.832l17.678-17.678
L369.496,284L351.818,301.678z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -11,14 +11,14 @@
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
</head>
<body>
<div id="menu">
<nav id="menu">
<ul class="container">
<li>Requests</li>
<li>Signed</li>
<li>Revoked</li>
<li>Log</li>
<li data-section="requests">Requests</li>
<li data-section="signed">Signed</li>
<li data-section="revoked">Revoked</li>
<li data-section="log">Log</li>
</ul>
</div>
</nav>
<div id="container" class="container">
Loading certificate authority...
</div>

View File

@ -1,3 +1,159 @@
function onTagClicked() {
var value = $(this).html();
var updated = prompt("Enter new tag or clear to remove the tag", value);
if (updated == "") {
$(this).addClass("busy");
$.ajax({
method: "DELETE",
url: "/api/tag/" + $(this).attr("data-id")
});
} else if (updated && updated != value) {
$.ajax({
method: "PUT",
url: "/api/tag/" + $(this).attr("data-id"),
dataType: "json",
data: {
value: updated
}
});
}
}
function onNewTagClicked() {
var cn = $(event.target).attr("data-cn");
var key = $(event.target).val();
$(event.target).val("");
var value = prompt("Enter new " + key + " tag for " + cn);
if (!value) return;
if (value.length == 0) return;
$.ajax({
method: "POST",
url: "/api/tag/",
dataType: "json",
data: {
cn: cn,
value: value,
key: key
}
});
}
function onLogEntry (e) {
var entry = JSON.parse(e.data);
if ($("#log_level_" + entry.severity).prop("checked")) {
console.info("Received log entry:", entry);
$("#log_entries").prepend(nunjucks.render("logentry.html", {
entry: {
created: new Date(entry.created).toLocaleString(),
message: entry.message,
severity: entry.severity
}
}));
}
};
function onRequestSubmitted(e) {
console.log("Request submitted:", e.data);
$.ajax({
method: "GET",
url: "/api/request/" + e.data + "/",
dataType: "json",
success: function(request, status, xhr) {
console.info(request);
$("#pending_requests").prepend(
nunjucks.render('request.html', { request: request }));
}
});
}
function onRequestDeleted(e) {
console.log("Removing deleted request #" + e.data);
$("#request_" + e.data).remove();
}
function onClientUp(e) {
console.log("Adding security association:" + e.data);
var lease = JSON.parse(e.data);
var $status = $("#signed_certificates [data-dn='" + lease.identity + "'] .status");
$status.html(nunjucks.render('status.html', {
lease: {
address: lease.address,
identity: lease.identity,
acquired: new Date(),
released: null
}}));
}
function onClientDown(e) {
console.log("Removing security association:" + e.data);
var lease = JSON.parse(e.data);
var $status = $("#signed_certificates [data-dn='" + lease.identity + "'] .status");
$status.html(nunjucks.render('status.html', {
lease: {
address: lease.address,
identity: lease.identity,
acquired: null,
released: new Date()
}}));
}
function onRequestSigned(e) {
console.log("Request signed:", e.data);
$("#request_" + e.data).slideUp("normal", function() { $(this).remove(); });
$.ajax({
method: "GET",
url: "/api/signed/" + e.data + "/",
dataType: "json",
success: function(certificate, status, xhr) {
console.info(certificate);
$("#signed_certificates").prepend(
nunjucks.render('signed.html', { certificate: certificate }));
}
});
}
function onCertificateRevoked(e) {
console.log("Removing revoked certificate #" + e.data);
$("#certificate_" + e.data).slideUp("normal", function() { $(this).remove(); });
}
function onTagAdded(e) {
console.log("Tag added #" + e.data);
$.ajax({
method: "GET",
url: "/api/tag/" + e.data + "/",
dataType: "json",
success: function(tag, status, xhr) {
// TODO: Deduplicate
$tag = $("<span id=\"tag_" + tag.id + "\" class=\"" + tag.key + " icon tag\" data-id=\""+tag.id+"\">" + tag.value + "</span>");
$tags = $("#signed_certificates [data-cn='" + tag.cn + "'] .tags").prepend(" ");
$tags = $("#signed_certificates [data-cn='" + tag.cn + "'] .tags").prepend($tag);
$tag.click(onTagClicked);
}
})
}
function onTagRemoved(e) {
console.log("Tag removed #" + e.data);
$("#tag_" + e.data).remove();
}
function onTagUpdated(e) {
console.log("Tag updated #" + e.data);
$.ajax({
method: "GET",
url: "/api/tag/" + e.data + "/",
dataType: "json",
success:function(tag, status, xhr) {
console.info("Updated tag", tag);
$("#tag_" + tag.id).html(tag.value);
}
})
}
$(document).ready(function() {
console.info("Loading CA, to debug: curl " + window.location.href + " --negotiate -u : -H 'Accept: application/json'");
$.ajax({
@ -13,8 +169,6 @@ $(document).ready(function() {
$("#container").html(nunjucks.render('error.html', { message: msg }));
},
success: function(session, status, xhr) {
console.info("Got:", session);
console.info("Opening EventSource from:", session.event_channel);
var source = new EventSource(session.event_channel);
@ -23,90 +177,33 @@ $(document).ready(function() {
console.log("Received server-sent event:", event);
}
source.addEventListener("log-entry", function(e) {
var entry = JSON.parse(e.data);
console.info("Received log entry:", entry, "gonna prepend:", $("#log_level_" + entry.severity).prop("checked"));
if ($("#log_level_" + entry.severity).prop("checked")) {
$("#log_entries").prepend(nunjucks.render("logentry.html", {
entry: {
created: new Date(entry.created).toLocaleString(),
message: entry.message,
severity: entry.severity
}
}));
}
});
source.addEventListener("up-client", function(e) {
console.log("Adding security association:" + e.data);
var lease = JSON.parse(e.data);
var $status = $("#signed_certificates [data-dn='" + lease.identity + "'] .status");
$status.html(nunjucks.render('status.html', {
lease: {
address: lease.address,
identity: lease.identity,
acquired: new Date(),
released: null
}}));
});
source.addEventListener("down-client", function(e) {
console.log("Removing security association:" + e.data);
var lease = JSON.parse(e.data);
var $status = $("#signed_certificates [data-dn='" + lease.identity + "'] .status");
$status.html(nunjucks.render('status.html', {
lease: {
address: lease.address,
identity: lease.identity,
acquired: null,
released: new Date()
}}));
});
source.addEventListener("request_deleted", function(e) {
console.log("Removing deleted request #" + e.data);
$("#request_" + e.data).remove();
});
source.addEventListener("request_submitted", function(e) {
console.log("Request submitted:", e.data);
$.ajax({
method: "GET",
url: "/api/request/" + e.data + "/",
dataType: "json",
success: function(request, status, xhr) {
console.info(request);
$("#pending_requests").prepend(
nunjucks.render('request.html', { request: request }));
}
});
});
source.addEventListener("request_signed", function(e) {
console.log("Request signed:", e.data);
$("#request_" + e.data).slideUp("normal", function() { $(this).remove(); });
$.ajax({
method: "GET",
url: "/api/signed/" + e.data + "/",
dataType: "json",
success: function(certificate, status, xhr) {
console.info(certificate);
$("#signed_certificates").prepend(
nunjucks.render('signed.html', { certificate: certificate }));
}
});
});
source.addEventListener("certificate_revoked", function(e) {
console.log("Removing revoked certificate #" + e.data);
$("#certificate_" + e.data).slideUp("normal", function() { $(this).remove(); });
});
source.addEventListener("log-entry", onLogEntry);
source.addEventListener("up-client", onClientUp);
source.addEventListener("down-client", onClientDown);
source.addEventListener("request-deleted", onRequestDeleted);
source.addEventListener("request-submitted", onRequestSubmitted);
source.addEventListener("request-signed", onRequestSigned);
source.addEventListener("certificate-revoked", onCertificateRevoked);
source.addEventListener("tag-added", onTagAdded);
source.addEventListener("tag-removed", onTagRemoved);
source.addEventListener("tag-updated", onTagUpdated);
/**
* Render authority views
**/
$("#container").html(nunjucks.render('authority.html', { session: session, window: window }));
console.info("Swtiching to requests section");
$("section").hide();
$("section#requests").show();
$("nav#menu li").click(function(e) {
$("section").hide();
$("section#" + $(e.target).attr("data-section")).show();
});
/**
* Fetch log entries
*/
$.ajax({
method: "GET",
url: "/api/log/",
@ -127,6 +224,52 @@ $(document).ready(function() {
}
});
/**
* Set up search bar
*/
$(window).on("search", function() {
var q = $("#search").val();
$(".filterable").each(function(i, e) {
if ($(e).attr("data-dn").toLowerCase().indexOf(q) >= 0) {
$(e).show();
} else {
$(e).hide();
}
});
});
/**
* Bind key up event of search bar
*/
$("#search").on("keyup", function() {
if (window.searchTimeout) { clearTimeout(window.searchTimeout); }
window.searchTimeout = setTimeout(function() { $(window).trigger("search"); }, 500);
console.info("Setting timeout", window.searchTimeout);
});
/**
* Fetch tags for certificates
*/
$.ajax({
method: "GET",
url: "/api/tag/",
dataType: "json",
success:function(tags, status, xhr) {
console.info("Got", tags.length, "tags");
for (var j = 0; j < tags.length; j++) {
// TODO: Deduplicate
$tag = $("<span id=\"tag_" + tags[j].id + "\" class=\"" + tags[j].key + " icon tag\" data-id=\""+tags[j].id+"\">" + tags[j].value + "</span>");
$tags = $("#signed_certificates [data-cn='" + tags[j].cn + "'] .tags").prepend(" ");
$tags = $("#signed_certificates [data-cn='" + tags[j].cn + "'] .tags").prepend($tag);
$tag.click(onTagClicked);
}
}
});
/**
* Fetch leases associated with certificates
*/
$.ajax({
method: "GET",
url: "/api/lease/",
@ -148,17 +291,6 @@ $(document).ready(function() {
}}));
}
/* Set up search box */
$("#search").on("keyup", function() {
var q = $("#search").val().toLowerCase();
$(".filterable").each(function(i, e) {
if ($(e).attr("data-dn").toLowerCase().indexOf(q) >= 0) {
$(e).show();
} else {
$(e).hide();
}
});
});
}
});
}

View File

@ -1,4 +1,4 @@
<li id="certificate_{{ certificate.sha256sum }}" data-dn="{{ certificate.identity }}" class="filterable">
<li id="certificate_{{ certificate.sha256sum }}" data-dn="{{ certificate.identity }}" data-cn="{{ certificate.common_name }}" class="filterable">
<a class="button icon download" href="/api/signed/{{certificate.common_name}}/">Fetch</a>
<button class="icon revoke" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/signed/{{certificate.common_name}}/',type:'delete'});">Revoke</button>
@ -25,6 +25,15 @@
{{certificate.key_usage}}
</div>
<div class="tags">
<select class="icon tag" data-cn="{{ certificate.common_name }}" onChange="onNewTagClicked();">
<option value="">Add tag...</option>
<option value="location">Location</option>
<option value="phone">Phone</option>
<option value="room">Room</option>
</select>
</div>
<div class="status">
{% include 'status.html' %}
</div>

View File

@ -18,11 +18,11 @@ conn site-to-clients
rightsourceip={{subnet}} # Serve virtual IP-s from this pool
left={{local}} # Gateway IP address
leftcert={{certificate_path}} # Gateway certificate
{% if route %}
{% if route | length == 1 %}
{% if route %}
{% if route | length == 1 %}
leftsubnet={{route[0]}} # Advertise routes via this connection
{% else %}
{% else %}
leftsubnet={ {{ route | join(', ') }} }
{% endif %}
{% endif %}
{% endif %}
{% endif %}

View File

@ -3,7 +3,6 @@ import hashlib
import re
import click
import io
from certidude import push
from Crypto.Util import asn1
from OpenSSL import crypto
from datetime import datetime