mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-23 00:25:18 +00:00
api: Added signed certificate tagging mechanism
This commit is contained in:
parent
901b0f7224
commit
da6600e2e9
@ -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)
|
||||
|
||||
|
@ -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"):
|
||||
|
@ -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
90
certidude/api/tag.py
Normal 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)
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -1,4 +1,7 @@
|
||||
|
||||
import os
|
||||
import click
|
||||
|
||||
def expand_paths():
|
||||
"""
|
||||
Prefix '..._path' keyword arguments of target function with 'directory' keyword argument
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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 |
19
certidude/static/img/iconmonstr-compass-7-icon.svg
Normal file
19
certidude/static/img/iconmonstr-compass-7-icon.svg
Normal 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 |
19
certidude/static/img/iconmonstr-home-4-icon.svg
Normal file
19
certidude/static/img/iconmonstr-home-4-icon.svg
Normal 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 |
17
certidude/static/img/iconmonstr-mobile-phone-6-icon.svg
Normal file
17
certidude/static/img/iconmonstr-mobile-phone-6-icon.svg
Normal 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 |
25
certidude/static/img/iconmonstr-tag-2-icon.svg
Normal file
25
certidude/static/img/iconmonstr-tag-2-icon.svg
Normal 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 |
@ -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>
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user