Release version 0.1.20

This commit is contained in:
Lauri Võsandi 2016-01-10 19:51:54 +02:00
parent 6a45592cd0
commit de08ba759d
22 changed files with 371 additions and 87 deletions

View File

@ -95,16 +95,14 @@ You can check it with:
The command should return ca.example.co
Certidude can set up CA relatively easily, following will set up
CA in /var/lib/certidude/hostname.domain:
CA in /var/lib/certidude/hostname.domain.tld:
.. code:: bash
certidude setup authority
Tweak command-line options until you meet your requirements and
then insert generated section to your /etc/ssl/openssl.cnf
Spawn the signer process:
Tweak the configuration in /etc/certidude/server.conf until you meet your requirements and
spawn the signer process:
.. code:: bash
@ -126,13 +124,13 @@ Use following command to request a certificate on a machine:
certidude setup client ca.example.com
Use following to list signing requests, certificates and revoked certificates:
Use following to list signing requests, certificates and revoked certificates on server:
.. code::
certidude list
Use web interface or following to sign a certificate on Certidude server:
Use web interface or following to sign a certificate on server:
.. code::
@ -173,7 +171,7 @@ Otherwise manually configure ``uwsgi`` application in ``/etc/uwsgi/apps-availabl
buffer-size = 32768
env = LANG=C.UTF-8
env = LC_ALL=C.UTF-8
env = KRB5_KTNAME=/etc/certidude.keytab
env = KRB5_KTNAME=/etc/certidude/server.keytab
Also enable the application:
@ -298,7 +296,7 @@ Set up Kerberos keytab for the web service:
.. code:: bash
KRB5_KTNAME=FILE:/etc/certidude.keytab net ads keytab add HTTP -U Administrator
KRB5_KTNAME=FILE:/etc/certidude/server.keytab net ads keytab add HTTP -U Administrator
Setting up authorization

View File

@ -80,6 +80,7 @@ def certidude_app():
from .whois import WhoisResource
from .log import LogResource
from .tag import TagResource, TagDetailResource
from .cfg import ConfigResource, ScriptResource
app = falcon.API()
@ -94,6 +95,8 @@ def certidude_app():
app.add_route("/api/log/", LogResource())
app.add_route("/api/tag/", TagResource())
app.add_route("/api/tag/{identifier}/", TagDetailResource())
app.add_route("/api/config/", ConfigResource())
app.add_route("/api/script/", ScriptResource())
app.add_route("/api/", SessionResource())
# Gateway API calls, should this be moved to separate project?
@ -115,7 +118,6 @@ def certidude_app():
class PushLogHandler(logging.Handler):
def emit(self, record):
from certidude.push import publish
print("EVENT HAPPENED:", record.created)
publish("log-entry", dict(
created = datetime.fromtimestamp(record.created),
message = record.msg % record.args,

113
certidude/api/cfg.py Normal file
View File

@ -0,0 +1,113 @@
import falcon
import logging
import ipaddress
import string
from random import choice
from certidude import config
from certidude.auth import login_required, authorize_admin
from certidude.decorators import serialize
from jinja2 import Environment, FileSystemLoader
logger = logging.getLogger("api")
env = Environment(loader=FileSystemLoader("/etc/certidude/scripts"), trim_blocks=True)
SQL_SELECT_INHERITED = """
select `key`, `value` from tag_inheritance where tag_id in (select
tag.id
from
device_tag
join
tag on device_tag.tag_id = tag.id
join
device on device_tag.device_id = device.id
where
device.cn = %s)
"""
SQL_SELECT_TAGS = """
select
tag.`key` as `key`,
tag.`value` as `value`
from
device_tag
join
tag on device_tag.tag_id = tag.id
join
device on device_tag.device_id = device.id
where
device.cn = %s
"""
SQL_SELECT_INHERITANCE = """
select
tag_inheritance.`id` as `id`,
tag.id as `tag_id`,
tag.`key` as `match_key`,
tag.`value` as `match_value`,
tag_inheritance.`key` as `key`,
tag_inheritance.`value` as `value`
from tag_inheritance
join tag on tag.id = tag_inheritance.tag_id
"""
class ConfigResource(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(SQL_SELECT_INHERITANCE)
def g():
for row in cursor:
yield row
cursor.close()
conn.close()
return g()
class ScriptResource(object):
def on_get(self, req, resp):
from certidude.api.whois import address_to_identity
node = address_to_identity(
config.DATABASE_POOL.get_connection(),
ipaddress.ip_address(req.env["REMOTE_ADDR"])
)
if not node:
resp.body = "Could not map IP address: %s" % req.env["REMOTE_ADDR"]
resp.status = falcon.HTTP_404
return
address, acquired, identity = node
key, common_name = identity.split("=")
assert "=" not in common_name
conn = config.DATABASE_POOL.get_connection()
cursor = conn.cursor()
resp.set_header("Content-Type", "text/x-shellscript")
args = common_name,
ctx = dict()
for query in SQL_SELECT_INHERITED, SQL_SELECT_TAGS:
cursor.execute(query, args)
for key, value in cursor:
current = ctx
if "." in key:
path, key = key.rsplit(".", 1)
for component in path.split("."):
if component not in current:
current[component] = dict()
current = current[component]
current[key] = value
cursor.close()
conn.close()
resp.body = env.get_template("uci.sh").render(ctx)
# TODO: Assert time is within reasonable range

View File

@ -54,8 +54,8 @@ class LeaseResource(object):
addresses.id
desc
"""
cnx = config.DATABASE_POOL.get_connection()
cursor = cnx.cursor()
conn = config.DATABASE_POOL.get_connection()
cursor = conn.cursor()
cursor.execute(SQL_LEASES)
for acquired, released, address, identity in cursor:
@ -66,3 +66,5 @@ class LeaseResource(object):
"identity": parse_dn(bytes(identity))
}
cursor.close()
conn.close()

View File

@ -28,6 +28,9 @@ class SignedCertificateListResource(object):
class SignedCertificateDetailResource(object):
@serialize
def on_get(self, req, resp, cn):
# Compensate for NTP lag
from time import sleep
sleep(5)
try:
logger.info("Served certificate %s to %s", cn, req.env["REMOTE_ADDR"])
resp.set_header("Content-Disposition", "attachment; filename=%s.crt" % cn)

View File

@ -7,6 +7,26 @@ from certidude.decorators import serialize
logger = logging.getLogger("api")
SQL_TAG_LIST = """
select
device_tag.id as `id`,
tag.key as `key`,
tag.value as `value`,
device.cn as `cn`
from
device_tag
join
tag
on
device_tag.tag_id = tag.id
join
device
on
device_tag.device_id = device.id
"""
SQL_TAG_DETAIL = SQL_TAG_LIST + " where device_tag.id = %s"
class TagResource(object):
@serialize
@login_required
@ -14,7 +34,7 @@ class TagResource(object):
def on_get(self, req, resp):
conn = config.DATABASE_POOL.get_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute("select * from tag")
cursor.execute(SQL_TAG_LIST)
def g():
for row in cursor:
@ -30,10 +50,24 @@ class TagResource(object):
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")
args = req.get_param("cn"),
cursor.execute(
"insert into tag (`cn`, `key`, `value`) values (%s, %s, %s)", args)
"insert ignore device (`cn`) values (%s) on duplicate key update used = NOW();", args)
device_id = cursor.lastrowid
args = req.get_param("key"), req.get_param("value")
cursor.execute(
"insert into tag (`key`, `value`) values (%s, %s) on duplicate key update used = NOW();", args)
tag_id = cursor.lastrowid
args = device_id, tag_id
cursor.execute(
"insert into device_tag (`device_id`, `tag_id`) values (%s, %s);", args)
push.publish("tag-added", str(cursor.lastrowid))
args = req.get_param("cn"), req.get_param("key"), req.get_param("value")
logger.debug("Tag cn=%s, key=%s, value=%s added" % args)
conn.commit()
cursor.close()
@ -47,7 +81,7 @@ class TagDetailResource(object):
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,))
cursor.execute(SQL_TAG_DETAIL, (identifier,))
for row in cursor:
cursor.close()
conn.close()
@ -63,11 +97,21 @@ class TagDetailResource(object):
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))
# Create tag if necessary
args = req.get_param("key"), req.get_param("value")
cursor.execute(
"insert into tag (`key`, `value`) values (%s, %s) on duplicate key update used = NOW();", args)
tag_id = cursor.lastrowid
# Attach tag to device
cursor.execute("update device_tag set tag_id = %s where `id` = %s limit 1",
(tag_id, 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)
@ -80,7 +124,7 @@ class TagDetailResource(object):
from certidude import push
conn = config.DATABASE_POOL.get_connection()
cursor = conn.cursor()
cursor.execute("delete from tag where tag.id = %s", (identifier,))
cursor.execute("delete from device_tag where id = %s", (identifier,))
conn.commit()
cursor.close()
conn.close()

View File

@ -6,7 +6,7 @@ from certidude import config
from certidude.decorators import serialize
from certidude.api.lease import parse_dn
def address_to_identity(cnx, addr):
def address_to_identity(conn, addr):
"""
Translate currently online client's IP-address to distinguished name
"""
@ -27,29 +27,32 @@ def address_to_identity(cnx, addr):
released is not null
"""
cursor = cnx.cursor()
cursor = conn.cursor()
import struct
cursor.execute(SQL_LEASES, (struct.pack("!L", int(addr)),))
for acquired, released, identity in cursor:
return {
"address": addr,
"acquired": datetime.utcfromtimestamp(acquired),
"identity": parse_dn(bytes(identity))
}
cursor.close()
return addr, datetime.utcfromtimestamp(acquired), parse_dn(bytes(identity))
cursor.close()
return None
class WhoisResource(object):
@serialize
def on_get(self, req, resp):
conn = config.DATABASE_POOL.get_connection()
identity = address_to_identity(
config.DATABASE_POOL.get_connection(),
conn,
ipaddress.ip_address(req.get_param("address") or req.env["REMOTE_ADDR"])
)
conn.close()
if identity:
return identity
return dict(address=identity[0], acquired=identity[1], identity=identity[2])
else:
resp.status = falcon.HTTP_403
resp.body = "Failed to look up node %s" % req.env["REMOTE_ADDR"]

View File

@ -69,6 +69,9 @@ def certidude_spawn(kill, no_interaction):
"""
from certidude import config
_, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude")
os.setgid(gid)
# Check whether we have privileges
os.umask(0o027)
uid = os.getuid()
@ -472,7 +475,7 @@ def certidude_setup_strongswan_networkmanager(url, email_address, common_name, o
@click.option("--username", default="certidude", help="Service user account, created if necessary, 'certidude' by default")
@click.option("--hostname", default=HOSTNAME, help="nginx hostname, '%s' by default" % HOSTNAME)
@click.option("--static-path", default=os.path.join(os.path.dirname(__file__), "static"), help="Static files")
@click.option("--kerberos-keytab", default="/etc/certidude.keytab", help="Specify Kerberos keytab")
@click.option("--kerberos-keytab", default="/etc/certidude/server.keytab", help="Specify Kerberos keytab")
@click.option("--nginx-config", "-n",
default="/etc/nginx/nginx.conf",
type=click.File(mode="w", atomic=True, lazy=True),
@ -540,6 +543,10 @@ def certidude_setup_authority(parent, country, state, locality, organization, or
if os.path.lexists(directory):
raise click.ClickException("Output directory {} already exists.".format(directory))
certidude_conf = os.path.join("/etc/certidude/server.conf")
if os.path.exists(certidude_conf):
raise click.ClickException("Configuration file %s already exists" % certidude_conf)
click.echo("CA configuration files are saved to: {}".format(directory))
click.echo("Generating 4096-bit RSA key...")
@ -588,6 +595,10 @@ def certidude_setup_authority(parent, country, state, locality, organization, or
b"keyUsage",
True,
b"keyCertSign, cRLSign"),
crypto.X509Extension(
b"extendedKeyUsage",
True,
b"serverAuth,1.3.6.1.5.5.8.2.2"),
crypto.X509Extension(
b"subjectKeyIdentifier",
False,
@ -606,6 +617,12 @@ def certidude_setup_authority(parent, country, state, locality, organization, or
False,
subject_alt_name.encode("ascii"))
])
ca.add_extensions([
crypto.X509Extension(
b"subjectAltName",
True,
("DNS:%s" % common_name).encode("ascii"))
])
if ocsp_responder_url:
raise NotImplementedError()
@ -628,33 +645,40 @@ def certidude_setup_authority(parent, country, state, locality, organization, or
ca.sign(key, "sha256")
_, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude")
os.setgid(gid)
# Create authority directory with 750 permissions
os.umask(0o027)
if not os.path.exists(directory):
os.makedirs(directory)
# Create subdirectories with 770 permissions
os.umask(0o007)
for subdir in ("signed", "requests", "revoked"):
if not os.path.exists(os.path.join(directory, subdir)):
os.mkdir(os.path.join(directory, subdir))
# Create CRL and serial file with 644 permissions
os.umask(0o133)
with open(ca_crl, "wb") as fh:
crl = crypto.CRL()
fh.write(crl.export(ca, key, days=revocation_list_lifetime))
with open(os.path.join(directory, "serial"), "w") as fh:
fh.write("1")
os.umask(0o027)
# Set permission bits to 640
os.umask(0o137)
with open(certidude_conf, "w") as fh:
fh.write(env.get_template("certidude.conf").render(locals()))
with open(ca_crt, "wb") as fh:
fh.write(crypto.dump_certificate(crypto.FILETYPE_PEM, ca))
os.umask(0o077)
# Set permission bits to 600
os.umask(0o177)
with open(ca_key, "wb") as fh:
fh.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
certidude_conf = os.path.join("/etc/certidude.conf")
with open(certidude_conf, "w") as fh:
fh.write(env.get_template("certidude.conf").render(locals()))
click.echo()
click.echo("Use following commands to inspect the newly created files:")
click.echo()
@ -665,7 +689,7 @@ def certidude_setup_authority(parent, country, state, locality, organization, or
click.echo()
click.echo("Use following to launch privilege isolated signer processes:")
click.echo()
click.echo(" certidude spawn")
click.echo(" certidude spawn -k")
click.echo()
click.echo("Use following command to serve CA read-only:")
click.echo()

View File

@ -8,7 +8,7 @@ import string
from random import choice
cp = configparser.ConfigParser()
cp.read("/etc/certidude.conf")
cp.read("/etc/certidude/server.conf")
ADMIN_USERS = set([j for j in cp.get("authorization", "admin_users").split(" ") if j])
ADMIN_SUBNETS = set([ipaddress.ip_network(j) for j in cp.get("authorization", "admin_subnets").split(" ") if j])
@ -48,7 +48,6 @@ except configparser.NoOptionError:
PUSH_LONG_POLL = PUSH_SERVER + "/lp/%s"
PUSH_PUBLISH = PUSH_SERVER + "/pub?id=%s"
from urllib.parse import urlparse
o = urlparse(cp.get("authority", "database"))
if o.scheme == "mysql":

View File

@ -97,7 +97,7 @@ def raw_sign(private_key, ca_cert, request, basic_constraints, lifetime, key_usa
extended_key_usage.encode("ascii"))])
# Set certificate lifetime
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notBefore(-3600)
cert.gmtime_adj_notAfter(lifetime * 24 * 60 * 60)
# Generate serial from 0x10000000000000000000 to 0xffffffffffffffffffff

View File

@ -52,9 +52,7 @@
<section id="revoked">
<h1>Revoked certificates</h1>
<p>To fetch certificate revocation list:</p>
<pre>
curl {{window.location.href}}api/revoked/ | openssl crl -text -noout
</pre>
<pre>curl {{window.location.href}}api/revoked/ | openssl crl -text -noout</pre>
<!--
<p>To perform online certificate status request</p>
@ -74,3 +72,6 @@
{% endfor %}
</ul>
</section>
<section id="config">
</section>

View File

@ -0,0 +1,31 @@
<h1>Create a rule</h1>
<p>
<datalist id="tag_autocomplete">
</datalist>
<span>Filter</span>
<select id="tags_autocomplete"></select>
attaches attribute
<select>
{% include 'tagtypes.html' %}
</select>
<span contenteditable>something</span>
<button>Add rule</button>
</p>
{% for grouper, items in configuration | groupby('tag_id') %}
<h1>Filter {{ items[0].match_key }} is {{ items[0].match_value }}</h1>
<ul>
{% for item in items %}
<li>Attach {{ item.key }} attribute {{ item.value }}</li>
{% endfor %}
</ul>
{% endfor %}

View File

@ -216,5 +216,8 @@ select {
.icon.room { background-image: url("../img/iconmonstr-home-4-icon.svg"); }
.icon.serial { background-image: url("../img/iconmonstr-barcode-4-icon.svg"); }
.icon.wireless { background-image: url("../img/iconmonstr-wireless-6-icon.svg"); }
.icon.password { background-image: url("../img/iconmonstr-lock-3-icon.svg"); }
/* Make sure this is the last one */
.icon.busy{background-image:url("https://software.opensuse.org/assets/ajax-loader-ea46060b6c9f42822a3d58d075c83ea2.gif");}

View File

@ -0,0 +1,13 @@
<?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="lock-3-icon" d="M195.334,223.333h-50v-62.666C145.334,99.645,194.979,50,256,50c61.022,0,110.667,49.645,110.667,110.667
v62.666h-50v-62.666C316.667,127.215,289.452,100,256,100c-33.451,0-60.666,27.215-60.666,60.667V223.333z M404,253.333V462H108
V253.333H404z M283,341c0-14.912-12.088-27-27-27s-27,12.088-27,27c0,7.811,3.317,14.844,8.619,19.773
c4.385,4.075,6.881,9.8,6.881,15.785V399.5h23v-22.941c0-5.989,2.494-11.708,6.881-15.785C279.683,355.844,283,348.811,283,341z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,16 @@
<?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="wireless-6-icon" d="M50,178.599c52.72-52.72,125.552-85.328,206-85.328c80.448,0,153.28,32.608,206,85.328l-35,35
c-43.763-43.763-104.221-70.83-171-70.83c-66.78,0-127.237,27.067-171,70.83L50,178.599z M148.196,276.796
c27.589-27.59,65.704-44.654,107.804-44.654s80.215,17.064,107.804,44.654l35.935-35.936
c-36.785-36.787-87.604-59.539-143.738-59.539s-106.953,22.752-143.738,59.539L148.196,276.796z M211,339.599
c11.517-11.517,27.427-18.64,45-18.64s33.483,7.123,45,18.64l35.313-35.312c-20.554-20.554-48.949-33.269-80.313-33.269
s-59.76,12.715-80.313,33.269L211,339.599z M256,356.138c-17.284,0-31.299,14.01-31.299,31.297
c0,17.285,14.015,31.295,31.299,31.295c17.283,0,31.296-14.01,31.296-31.295C287.296,370.147,273.283,356.138,256,356.138z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -16,6 +16,7 @@
<li data-section="requests">Requests</li>
<li data-section="signed">Signed</li>
<li data-section="revoked">Revoked</li>
<li data-section="config">Configuration</li>
<li data-section="log">Log</li>
</ul>
</nav>

View File

@ -15,6 +15,7 @@ function onTagClicked() {
url: "/api/tag/" + $(this).attr("data-id"),
dataType: "json",
data: {
key: $(this).attr("data-key"),
value: updated
}
});
@ -40,6 +41,11 @@ function onNewTagClicked() {
});
}
function onTagFilterChanged() {
var key = $(event.target).val();
console.info("New key is:", key);
}
function onLogEntry (e) {
var entry = JSON.parse(e.data);
if ($("#log_level_" + entry.severity).prop("checked")) {
@ -128,7 +134,7 @@ function onTagAdded(e) {
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>");
$tag = $("<span id=\"tag_" + tag.id + "\" title=\"" + tag.key + "=" + tag.value + "\" class=\"" + tag.key.replace(/\./g, " ") + " icon tag\" data-id=\""+tag.id+"\" data-key=\"" + tag.key + "\">" + 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);
@ -201,29 +207,6 @@ $(document).ready(function() {
$("section#" + $(e.target).attr("data-section")).show();
});
/**
* Fetch log entries
*/
$.ajax({
method: "GET",
url: "/api/log/",
dataType: "json",
success:function(entries, status, xhr) {
console.info("Got", entries.length, "log entries");
for (var j = 0; j < entries.length; j++) {
if ($("#log_level_" + entries[j].severity).prop("checked")) {
$("#log_entries").append(nunjucks.render("logentry.html", {
entry: {
created: new Date(entries[j].created).toLocaleString("et-EE"),
message: entries[j].message,
severity: entries[j].severity
}
}));
}
}
}
});
/**
* Set up search bar
*/
@ -248,22 +231,35 @@ $(document).ready(function() {
});
/**
* Fetch tags for certificates
*/
$.ajax({
method: "GET",
url: "/api/tag/",
url: "/api/config/",
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);
}
success: function(configuration, status, xhr) {
console.info("Appending " + configuration.length + " configuration items");
$("#config").html(nunjucks.render('configuration.html', { configuration:configuration}));
/**
* 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 + "\" title=\"" + tags[j].key + "=" + tags[j].value + "\" class=\"" + tags[j].key.replace(/\./g, " ") + " icon tag\" data-id=\""+tags[j].id+"\" data-key=\"" + tags[j].key + "\">" + tags[j].value + "</span>");
console.info("Inserting tag", tags[j], $tag);
$tags = $("#signed_certificates [data-cn='" + tags[j].cn + "'] .tags").prepend(" ");
$tags = $("#signed_certificates [data-cn='" + tags[j].cn + "'] .tags").prepend($tag);
$tag.click(onTagClicked);
$("#tags_autocomplete").prepend("<option value=\"" + tags[j].id + "\">" + tags[j].key + "='" + tags[j].value + "'</option>");
}
}
});
}
});
@ -293,6 +289,29 @@ $(document).ready(function() {
}
});
return;
/**
* Fetch log entries
*/
$.ajax({
method: "GET",
url: "/api/log/",
dataType: "json",
success:function(entries, status, xhr) {
console.info("Got", entries.length, "log entries");
for (var j = 0; j < entries.length; j++) {
if ($("#log_level_" + entries[j].severity).prop("checked")) {
$("#log_entries").append(nunjucks.render("logentry.html", {
entry: {
created: new Date(entries[j].created).toLocaleString("et-EE"),
message: entries[j].message,
severity: entries[j].severity
}
}));
}
}
}
});
}
});
});

View File

@ -1,4 +1,4 @@
<li id="request_{{ request.sha256sum }}" class="filterable">
<li id="request_{{ request.common_name }}" class="filterable">
<a class="button icon download" href="/api/request/{{request.common_name}}/">Fetch</a>
{% if request.signable %}

View File

@ -11,6 +11,8 @@
<div class="email">{% include 'img/iconmonstr-email-2-icon.svg' %} {{ certificate.email_address }}</div>
{% endif %}
{#
<div class="monospace">
{% include 'img/iconmonstr-key-2-icon.svg' %}
<span title="SHA-256 of public key">
@ -25,13 +27,12 @@
{{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>
<option value="serial">Product serial</option>
{% include 'tagtypes.html' %}
</select>
</div>

View File

@ -8,7 +8,7 @@
{% if lease.released %}
Last seen {{ lease.released }} at {{ lease.address }}
{% else %}
Online since {{ lease.acquired }} at {{ lease.address }}
Online since {{ lease.acquired }} at <a target="{{ lease.address }}" href="http://{{ lease.address }}">{{ lease.address }}</a>
{% endif %}
{% else %}
Not seen

Before

Width:  |  Height:  |  Size: 428 B

After

Width:  |  Height:  |  Size: 498 B

View File

@ -0,0 +1,11 @@
<option value="location">Location</option>
<option value="phone">Phone</option>
<option value="room">Room</option>
<option value="serial">Product serial</option>
<option value="wireless.protected.password">Protected wireless network password</option>
<option value="wireless.protected.name">Protected wireless network name</option>
<option value="wireless.public.name">Public wireless network name</option>
<option value="wireless.channela">5GHz channel number</option>
<option value="wireless.channelb">2.4GHz channel number</option>
<option value="usb.approved">Approved USB device</option>

View File

@ -5,7 +5,7 @@ from setuptools import setup
setup(
name = "certidude",
version = "0.1.18",
version = "0.1.20",
author = u"Lauri Võsandi",
author_email = "lauri.vosandi@gmail.com",
description = "Certidude is a novel X.509 Certificate Authority management tool aiming to support PKCS#11 and in far future WebCrypto.",