From da6600e2e937667c51f0769543d3357ea485b2a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lauri=20V=C3=B5sandi?= Date: Wed, 16 Dec 2015 17:41:49 +0000 Subject: [PATCH] api: Added signed certificate tagging mechanism --- certidude/api/__init__.py | 8 +- certidude/api/request.py | 2 +- certidude/api/signed.py | 7 + certidude/api/tag.py | 90 +++++ certidude/api/whois.py | 17 +- certidude/authority.py | 6 +- certidude/cli.py | 20 +- certidude/common.py | 3 + certidude/helpers.py | 4 +- certidude/static/authority.html | 23 +- certidude/static/css/style.css | 32 +- ...hexa_pattern_cc0_by_black_light_studio.png | Bin 38713 -> 0 bytes .../static/img/iconmonstr-compass-7-icon.svg | 19 ++ .../static/img/iconmonstr-home-4-icon.svg | 19 ++ .../img/iconmonstr-mobile-phone-6-icon.svg | 17 + .../static/img/iconmonstr-tag-2-icon.svg | 25 ++ certidude/static/index.html | 12 +- certidude/static/js/certidude.js | 318 +++++++++++++----- certidude/static/signed.html | 11 +- .../templates/strongswan-site-to-client.conf | 10 +- certidude/wrappers.py | 1 - 21 files changed, 501 insertions(+), 143 deletions(-) create mode 100644 certidude/api/tag.py delete mode 100644 certidude/static/img/free_hexa_pattern_cc0_by_black_light_studio.png create mode 100644 certidude/static/img/iconmonstr-compass-7-icon.svg create mode 100644 certidude/static/img/iconmonstr-home-4-icon.svg create mode 100644 certidude/static/img/iconmonstr-mobile-phone-6-icon.svg create mode 100644 certidude/static/img/iconmonstr-tag-2-icon.svg diff --git a/certidude/api/__init__.py b/certidude/api/__init__.py index d366ab6..b350334 100644 --- a/certidude/api/__init__.py +++ b/certidude/api/__init__.py @@ -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) diff --git a/certidude/api/request.py b/certidude/api/request.py index 9b82415..80f0326 100644 --- a/certidude/api/request.py +++ b/certidude/api/request.py @@ -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"): diff --git a/certidude/api/signed.py b/certidude/api/signed.py index 14f0aff..753897b 100644 --- a/certidude/api/signed.py +++ b/certidude/api/signed.py @@ -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) diff --git a/certidude/api/tag.py b/certidude/api/tag.py new file mode 100644 index 0000000..4003a72 --- /dev/null +++ b/certidude/api/tag.py @@ -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) + + diff --git a/certidude/api/whois.py b/certidude/api/whois.py index 1ebeee1..9ec1df4 100644 --- a/certidude/api/whois.py +++ b/certidude/api/whois.py @@ -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)) } diff --git a/certidude/authority.py b/certidude/authority.py index cdd0c62..8ac2507 100644 --- a/certidude/authority.py +++ b/certidude/authority.py @@ -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 diff --git a/certidude/cli.py b/certidude/cli.py index f888446..2d5571d 100755 --- a/certidude/cli.py +++ b/certidude/cli.py @@ -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 diff --git a/certidude/common.py b/certidude/common.py index c325a2c..37973b2 100644 --- a/certidude/common.py +++ b/certidude/common.py @@ -1,4 +1,7 @@ +import os +import click + def expand_paths(): """ Prefix '..._path' keyword arguments of target function with 'directory' keyword argument diff --git a/certidude/helpers.py b/certidude/helpers.py index 7e65c61..f8f159c 100644 --- a/certidude/helpers.py +++ b/certidude/helpers.py @@ -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 diff --git a/certidude/static/authority.html b/certidude/static/authority.html index 45b2b7d..690a386 100644 --- a/certidude/static/authority.html +++ b/certidude/static/authority.html @@ -1,19 +1,19 @@ +

Hi {{session.username}},

Request submission is allowed from: {% if session.request_subnets %}{% for i in session.request_subnets %}{{ i }} {% endfor %}{% else %}anywhere{% endif %}

Autosign is allowed from: {% if session.autosign_subnets %}{% for i in session.autosign_subnets %}{{ i }} {% endfor %}{% else %}nowhere{% endif %}

Authority administration is allowed from: {% if session.admin_subnets %}{% for i in session.admin_subnets %}{{ i }} {% endfor %}{% else %}anywhere{% endif %}

Authority administration allowed for: {% for i in session.admin_users %}{{ i }} {% endfor %}

- +
{% set s = session.certificate.identity %} - - -
+

Pending requests

+
    {% for request in session.requests %} {% include "request.html" %} @@ -23,19 +23,20 @@
    certidude setup client {{session.common_name}}
-
+ -
+

Signed certificates

+
    {% for certificate in session.signed | sort | reverse %} {% include "signed.html" %} {% endfor %}
-
+ -
+

Log

@@ -46,9 +47,9 @@

-
+ -
+

Revoked certificates

To fetch certificate revocation list:

@@ -72,4 +73,4 @@
             
  • Great job! No certificate signing requests to sign.
  • {% endfor %} -
    + diff --git a/certidude/static/css/style.css b/certidude/static/css/style.css index 420945b..27c11bc 100644 --- a/certidude/static/css/style.css +++ b/certidude/static/css/style.css @@ -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");} diff --git a/certidude/static/img/free_hexa_pattern_cc0_by_black_light_studio.png b/certidude/static/img/free_hexa_pattern_cc0_by_black_light_studio.png deleted file mode 100644 index 8c5c542e8d85e1cf3a315b1916506319535d1bd9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38713 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 diff --git a/certidude/static/img/iconmonstr-compass-7-icon.svg b/certidude/static/img/iconmonstr-compass-7-icon.svg new file mode 100644 index 0000000..880bcc9 --- /dev/null +++ b/certidude/static/img/iconmonstr-compass-7-icon.svg @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/certidude/static/img/iconmonstr-home-4-icon.svg b/certidude/static/img/iconmonstr-home-4-icon.svg new file mode 100644 index 0000000..9d1a19b --- /dev/null +++ b/certidude/static/img/iconmonstr-home-4-icon.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + diff --git a/certidude/static/img/iconmonstr-mobile-phone-6-icon.svg b/certidude/static/img/iconmonstr-mobile-phone-6-icon.svg new file mode 100644 index 0000000..e9af4f5 --- /dev/null +++ b/certidude/static/img/iconmonstr-mobile-phone-6-icon.svg @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/certidude/static/img/iconmonstr-tag-2-icon.svg b/certidude/static/img/iconmonstr-tag-2-icon.svg new file mode 100644 index 0000000..ddbe528 --- /dev/null +++ b/certidude/static/img/iconmonstr-tag-2-icon.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + diff --git a/certidude/static/index.html b/certidude/static/index.html index d5a70b9..538eb20 100644 --- a/certidude/static/index.html +++ b/certidude/static/index.html @@ -11,14 +11,14 @@ - +
    Loading certificate authority...
    diff --git a/certidude/static/js/certidude.js b/certidude/static/js/certidude.js index 4a9a65d..9160cb0 100644 --- a/certidude/static/js/certidude.js +++ b/certidude/static/js/certidude.js @@ -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 = $("" + tag.value + ""); + $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 = $("" + tags[j].value + ""); + $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(); - } - }); - }); } }); } diff --git a/certidude/static/signed.html b/certidude/static/signed.html index 85ba3e6..792c4bc 100644 --- a/certidude/static/signed.html +++ b/certidude/static/signed.html @@ -1,4 +1,4 @@ -
  • +
  • Fetch @@ -25,6 +25,15 @@ {{certificate.key_usage}} +
    + +
    +
    {% include 'status.html' %}
    diff --git a/certidude/templates/strongswan-site-to-client.conf b/certidude/templates/strongswan-site-to-client.conf index 18447cb..2227539 100644 --- a/certidude/templates/strongswan-site-to-client.conf +++ b/certidude/templates/strongswan-site-to-client.conf @@ -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 %} diff --git a/certidude/wrappers.py b/certidude/wrappers.py index 1bc8bf7..5213d75 100644 --- a/certidude/wrappers.py +++ b/certidude/wrappers.py @@ -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