diff --git a/certidude/api/__init__.py b/certidude/api/__init__.py index 562d364..7a92c56 100644 --- a/certidude/api/__init__.py +++ b/certidude/api/__init__.py @@ -6,6 +6,7 @@ import logging import os import click import hashlib +import xattr from datetime import datetime from time import sleep from certidude import authority, mailer @@ -58,6 +59,15 @@ class SessionResource(object): def serialize_certificates(g): for common_name, path, buf, obj, server in g(): + try: + last_seen = datetime.strptime(xattr.getxattr(path, "user.last_seen"), "%Y-%m-%dT%H:%M:%S.%fZ") + lease = dict( + address = xattr.getxattr(path, "user.address"), + last_seen = last_seen, + age = datetime.utcnow() - last_seen + ) + except IOError: # No such attribute(s) + lease = None yield dict( serial_number = "%x" % obj.serial_number, common_name = common_name, @@ -65,7 +75,12 @@ class SessionResource(object): # TODO: key type, key length, key exponent, key modulo signed = obj.not_valid_before, expires = obj.not_valid_after, - sha256sum = hashlib.sha256(buf).hexdigest() + sha256sum = hashlib.sha256(buf).hexdigest(), + lease = lease, + tags = dict([ + (j[9:], xattr.getxattr(path, j).decode("utf-8")) + for j in xattr.listxattr(path) + if j.startswith("user.tag.")]) ) if req.context.get("user").is_admin(): @@ -81,6 +96,10 @@ class SessionResource(object): ), request_submission_allowed = config.REQUEST_SUBMISSION_ALLOWED, authority = dict( + lease = dict( + offline = 600, # Seconds from last seen activity to consider lease offline, OpenVPN reneg-sec option + dead = 604800 # Seconds from last activity to consider lease dead, X509 chain broken or machine discarded + ), common_name = authority.ca_cert.subject.get_attributes_for_oid( NameOID.COMMON_NAME)[0].value, outbox = dict( @@ -108,8 +127,8 @@ class SessionResource(object): ) ) if req.context.get("user").is_admin() else None, features=dict( - tagging=config.TAGGING_BACKEND, - leases=config.LEASES_BACKEND, + tagging=True, + leases=True, logging=config.LOGGING_BACKEND)) @@ -155,12 +174,13 @@ def certidude_app(): from .revoked import RevocationListResource from .signed import SignedCertificateDetailResource from .request import RequestListResource, RequestDetailResource - from .lease import LeaseResource, StatusFileLeaseResource - from .whois import WhoisResource - from .tag import TagResource, TagDetailResource + from .lease import LeaseResource, LeaseDetailResource from .cfg import ConfigResource, ScriptResource + from .tag import TagResource, TagDetailResource + from .attrib import AttributeResource app = falcon.API(middleware=NormalizeMiddleware()) + app.req_options.auto_parse_form_urlencoded = True # Certificate authority API calls app.add_route("/api/ocsp/", CertificateStatusResource()) @@ -171,25 +191,21 @@ def certidude_app(): app.add_route("/api/request/", RequestListResource()) app.add_route("/api/", SessionResource()) - # Gateway API calls, should this be moved to separate project? - if config.LEASES_BACKEND == "openvpn-status": - app.add_route("/api/lease/", StatusFileLeaseResource(config.OPENVPN_STATUS_URI)) - elif config.LEASES_BACKEND == "sql": - app.add_route("/api/lease/", LeaseResource()) - app.add_route("/api/whois/", WhoisResource()) + # Extended attributes for scripting etc. + app.add_route("/api/signed/{cn}/attr/", AttributeResource()) + + # API calls used by pushed events on the JS end + app.add_route("/api/signed/{cn}/tag/", TagResource()) + app.add_route("/api/signed/{cn}/lease/", LeaseDetailResource()) + + # API call used to delete existing tags + app.add_route("/api/signed/{cn}/tag/{key}/", TagDetailResource()) + + # Gateways can submit leases via this API call + app.add_route("/api/lease/", LeaseResource()) # Optional user enrollment API call if config.USER_ENROLLMENT_ALLOWED: app.add_route("/api/bundle/", BundleResource()) - if config.TAGGING_BACKEND == "sql": - uri = config.cp.get("tagging", "database") - app.add_route("/api/tag/", TagResource(uri)) - app.add_route("/api/tag/{identifier}/", TagDetailResource(uri)) - app.add_route("/api/config/", ConfigResource(uri)) - app.add_route("/api/script/", ScriptResource(uri)) - elif config.TAGGING_BACKEND: - raise ValueError("Invalid tagging.backend = %s" % config.TAGGING_BACKEND) - - return app diff --git a/certidude/api/attrib.py b/certidude/api/attrib.py new file mode 100644 index 0000000..b92d25f --- /dev/null +++ b/certidude/api/attrib.py @@ -0,0 +1,46 @@ + +import falcon +import logging +import ipaddress +from xattr import getxattr, listxattr +from datetime import datetime +from certidude import config, authority +from certidude.decorators import serialize + +logger = logging.getLogger("api") + +class AttributeResource(object): + @serialize + def on_get(self, req, resp, cn): + """ + Return extended attributes stored on the server. + This not only contains tags and lease information, + but might also contain some other sensitive information. + """ + path, buf, cert = authority.get_signed(cn) + + attribs = dict() + for key in listxattr(path): + if not key.startswith("user."): + continue + value = getxattr(path, key) + current = attribs + if "." in key: + namespace, key = key.rsplit(".", 1) + for component in namespace.split("."): + if component not in current: + current[component] = dict() + current = current[component] + current[key] = value + + whitelist = attribs.get("user").get("address") + + if req.context.get("remote_addr") != whitelist: + logger.info("Attribute access denied from %s, expected %s for %s", + req.context.get("remote_addr"), + whitelist, + cn) + raise falcon.HTTPForbidden("Forbidden", + "Attributes only accessible to the machine") + + return attribs diff --git a/certidude/api/lease.py b/certidude/api/lease.py index aaf634f..66e39a0 100644 --- a/certidude/api/lease.py +++ b/certidude/api/lease.py @@ -1,96 +1,39 @@ +import click +import xattr from datetime import datetime -from dateutil import tz from pyasn1.codec.der import decoder -from certidude import config +from certidude import config, authority, push from certidude.auth import login_required, authorize_admin from certidude.decorators import serialize -localtime = tz.tzlocal() - -OIDS = { - (2, 5, 4, 3) : 'CN', # common name - (2, 5, 4, 6) : 'C', # country - (2, 5, 4, 7) : 'L', # locality - (2, 5, 4, 8) : 'ST', # stateOrProvince - (2, 5, 4, 10) : 'O', # organization - (2, 5, 4, 11) : 'OU', # organizationalUnit -} - -def parse_dn(data): - chunks, remainder = decoder.decode(data) - dn = "" - if remainder: - raise ValueError() - # TODO: Check for duplicate entries? - def generate(): - for chunk in chunks: - for chunkette in chunk: - key, value = chunkette - yield str(OIDS[key] + "=" + value) - return ", ".join(generate()) - - -class StatusFileLeaseResource(object): - def __init__(self, uri): - self.uri = uri +# TODO: lease namespacing (?) +class LeaseDetailResource(object): @serialize @login_required @authorize_admin - def on_get(self, req, resp): - from openvpn_status import parse_status - from urllib import urlopen - fh = urlopen(self.uri) - # openvpn-status.log has no information about timezone - # and dates marked there use local time instead of UTC - status = parse_status(fh.read()) - for cn, e in status.routing_table.items(): - yield { - "acquired": status.client_list[cn].connected_since.replace(tzinfo=localtime), - "released": None, - "address": e.virtual_address, - "identity": "CN=%s" % cn, # BUGBUG - } + def on_get(self, req, resp, cn): + path, buf, cert = authority.get_signed(cn) + return dict( + last_seen = xattr.getxattr(path, "user.last_seen"), + address = xattr.getxattr(path, "user.address").decode("ascii") + ) class LeaseResource(object): - @serialize - @login_required - @authorize_admin - def on_get(self, req, resp): - from ipaddress import ip_address + def on_post(self, req, resp): + # TODO: verify signature + common_name = req.get_param("client", required=True) + path, buf, cert = authority.get_signed(common_name) # TODO: catch exceptions + if cert.serial != req.get_param_as_int("serial", required=True): # Badum we have OCSP! + raise # TODO proper exception + if req.get_param("action") == "client-connect": + xattr.setxattr(path, "user.address", req.get_param("address", required=True).encode("ascii")) + xattr.setxattr(path, "user.last_seen", datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z") + push.publish("lease-update", common_name) - # BUGBUG - SQL_LEASES = """ - select - acquired, - released, - address, - identities.data as identity - from - addresses - right join - identities - on - identities.id = addresses.identity - where - addresses.released <> 1 - order by - addresses.id - desc - """ - conn = config.DATABASE_POOL.get_connection() - cursor = conn.cursor() - cursor.execute(SQL_LEASES) - - for acquired, released, address, identity in cursor: - yield { - "acquired": datetime.utcfromtimestamp(acquired), - "released": datetime.utcfromtimestamp(released) if released else None, - "address": ip_address(bytes(address)), - "identity": parse_dn(bytes(identity)) - } - - cursor.close() - conn.close() + # client-disconnect is pretty much unusable: + # - Android Connect Client results "IP packet with unknown IP version=2" on gateway + # - NetworkManager just kills OpenVPN client, disconnect is never reported + # - Disconnect is also not reported when uplink connection dies or laptop goes to sleep diff --git a/certidude/api/request.py b/certidude/api/request.py index 06021b7..81c1d5b 100644 --- a/certidude/api/request.py +++ b/certidude/api/request.py @@ -3,6 +3,7 @@ import click import falcon import logging import ipaddress +import json import os import hashlib from base64 import b64decode @@ -154,10 +155,33 @@ class RequestDetailResource(object): Fetch certificate signing request as PEM """ resp.set_header("Content-Type", "application/pkcs10") - _, resp.body, _ = authority.get_request(cn) + _, buf, _ = authority.get_request(cn) logger.debug(u"Signing request %s was downloaded by %s", cn, req.context.get("remote_addr")) + preferred_type = req.client_prefers(("application/json", "application/x-pem-file")) + + if preferred_type == "application/x-pem-file": + # For certidude client, curl scripts etc + resp.set_header("Content-Type", "application/x-pem-file") + resp.set_header("Content-Disposition", ("attachment; filename=%s.pem" % cn)) + resp.body = buf + elif preferred_type == "application/json": + # For web interface events + resp.set_header("Content-Type", "application/json") + resp.set_header("Content-Disposition", ("attachment; filename=%s.json" % cn)) + resp.body = json.dumps(dict( + common_name = cn, + server = authority.server_flags(cn), + md5sum = hashlib.md5(buf).hexdigest(), + sha1sum = hashlib.sha1(buf).hexdigest(), + sha256sum = hashlib.sha256(buf).hexdigest(), + sha512sum = hashlib.sha512(buf).hexdigest())) + else: + raise falcon.HTTPUnsupportedMediaType( + "Client did not accept application/json or application/x-pem-file") + + @csrf_protection @login_required @authorize_admin diff --git a/certidude/api/tag.py b/certidude/api/tag.py index cf0e807..4c2c0fb 100644 --- a/certidude/api/tag.py +++ b/certidude/api/tag.py @@ -1,76 +1,42 @@ - import falcon import logging -from certidude.relational import RelationalMixin +import xattr +from certidude import authority from certidude.auth import login_required, authorize_admin from certidude.decorators import serialize, csrf_protection logger = logging.getLogger("api") -class TagResource(RelationalMixin): - SQL_CREATE_TABLES = "tag_tables.sql" - +class TagResource(object): @serialize @login_required @authorize_admin - def on_get(self, req, resp): - return self.iterfetch("select * from tag") - + def on_get(self, req, resp, cn): + path, buf, cert = authority.get_signed(cn) + return dict([ + (k[9:], xattr.getxattr(path, k)) + for k in xattr.listxattr(path) + if k.startswith("user.tag.")]) @csrf_protection - @serialize @login_required @authorize_admin - def on_post(self, req, resp): + def on_post(self, req, resp, cn): from certidude import push - args = req.get_param("cn"), req.get_param("key"), req.get_param("value") - rowid = self.sql_execute("tag_insert.sql", *args) - push.publish("tag-added", str(rowid)) - logger.debug(u"Tag cn=%s, key=%s, value=%s added" % args) - - -class TagDetailResource(RelationalMixin): - SQL_CREATE_TABLES = "tag_tables.sql" - - @serialize - @login_required - @authorize_admin - def on_get(self, req, resp, identifier): - conn = self.sql_connect() - cursor = conn.cursor() - if self.uri.scheme == "mysql": - cursor.execute("select `cn`, `key`, `value` from tag where id = %s", (identifier,)) - else: - cursor.execute("select `cn`, `key`, `value` from tag where id = ?", (identifier,)) - cols = [j[0] for j in cursor.description] - for row in cursor: - cursor.close() - conn.close() - return dict(zip(cols, row)) - cursor.close() - conn.close() - raise falcon.HTTPNotFound() + path, buf, cert = authority.get_signed(cn) + key, value = req.get_param("key", required=True), req.get_param("value", required=True) + xattr.setxattr(path, "user.tag.%s" % key, value.encode("utf-8")) + logger.debug(u"Tag %s=%s set for %s" % (key, value, cn)) + push.publish("tag-update", cn) +class TagDetailResource(object): @csrf_protection - @serialize @login_required @authorize_admin - def on_put(self, req, resp, identifier): + def on_delete(self, req, resp, cn, key): from certidude import push - args = req.get_param("value"), identifier - self.sql_execute("tag_update.sql", *args) - logger.debug(u"Tag %s updated, value set to %s", - identifier, req.get_param("value")) - push.publish("tag-updated", identifier) - - - @csrf_protection - @serialize - @login_required - @authorize_admin - def on_delete(self, req, resp, identifier): - from certidude import push - self.sql_execute("tag_delete.sql", identifier) - push.publish("tag-removed", identifier) - logger.debug(u"Tag %s removed" % identifier) + path, buf, cert = authority.get_signed(cn) + xattr.removexattr(path, "user.tag.%s" % key) + logger.debug(u"Tag %s removed for %s" % (key, cn)) + push.publish("tag-update", cn) diff --git a/certidude/api/whois.py b/certidude/api/whois.py deleted file mode 100644 index 44ce255..0000000 --- a/certidude/api/whois.py +++ /dev/null @@ -1,58 +0,0 @@ - -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(conn, addr): - """ - Translate currently online client's IP-address to distinguished name - """ - - SQL_LEASES = """ - select - acquired, - released, - identities.data as identity - from - addresses - right join - identities - on - identities.id = addresses.identity - where - address = %s and - released is not null - """ - - cursor = conn.cursor() - import struct - cursor.execute(SQL_LEASES, (struct.pack("!L", int(addr)),)) - - for acquired, released, identity in cursor: - 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( - conn, - req.context.get("remote_addr") - ) - - conn.close() - - if 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.context.get("remote_addr") diff --git a/certidude/authority.py b/certidude/authority.py index c5d9b16..7a8db2d 100644 --- a/certidude/authority.py +++ b/certidude/authority.py @@ -15,6 +15,7 @@ from cryptography.hazmat.primitives import hashes, serialization from certidude import config, push, mailer, const from certidude import errors from jinja2 import Template +from xattr import getxattr, listxattr, setxattr RE_HOSTNAME = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])(@(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9]))?$" @@ -299,6 +300,9 @@ def _sign(csr, buf, overwrite=False): cert_path = os.path.join(config.SIGNED_DIR, common_name.value + ".pem") renew = False + signed_path = os.path.join(config.SIGNED_DIR, "%s.pem" % common_name.value) + revoked_path = None + # Move existing certificate if necessary if os.path.exists(cert_path): with open(cert_path) as fh: @@ -310,7 +314,6 @@ def _sign(csr, buf, overwrite=False): if overwrite: if renew: # TODO: is this the best approach? - signed_path = os.path.join(config.SIGNED_DIR, "%s.pem" % common_name.value) revoked_path = os.path.join(config.REVOKED_DIR, "%x.pem" % prev.serial_number) os.rename(signed_path, revoked_path) else: @@ -325,6 +328,13 @@ def _sign(csr, buf, overwrite=False): fh.write(cert_buf) os.rename(cert_path + ".part", cert_path) + # Copy filesystem attributes to newly signed certificate + if revoked_path: + for key in listxattr(revoked_path): + if not key.startswith("user."): + continue + setxattr(signed_path, key, getxattr(revoked_path, key)) + # Send mail recipient = None diff --git a/certidude/config.py b/certidude/config.py index 38e76f5..2d4822e 100644 --- a/certidude/config.py +++ b/certidude/config.py @@ -72,12 +72,7 @@ EVENT_SOURCE_SUBSCRIBE = cp.get("push", "event source subscribe") LONG_POLL_PUBLISH = cp.get("push", "long poll publish") LONG_POLL_SUBSCRIBE = cp.get("push", "long poll subscribe") -TAGGING_BACKEND = cp.get("tagging", "backend") LOGGING_BACKEND = cp.get("logging", "backend") -LEASES_BACKEND = cp.get("leases", "backend") - -OPENVPN_STATUS_URI = cp.get("leases", "openvpn status uri") - if "whitelist" == AUTHORIZATION_BACKEND: USERS_WHITELIST = set([j for j in cp.get("authorization", "users whitelist").split(" ") if j]) @@ -93,4 +88,6 @@ elif "ldap" == AUTHORIZATION_BACKEND: else: raise NotImplementedError("Unknown authorization backend '%s'" % AUTHORIZATION_BACKEND) +TAG_TYPES = [j.split("/", 1) + [cp.get("tag types", j)] for j in cp.options("tag types")] + # TODO: Check if we don't have base or servers diff --git a/certidude/decorators.py b/certidude/decorators.py index bb15fda..c21f4da 100644 --- a/certidude/decorators.py +++ b/certidude/decorators.py @@ -3,7 +3,7 @@ import ipaddress import json import logging import types -from datetime import date, time, datetime +from datetime import date, time, datetime, timedelta from certidude.auth import User from urlparse import urlparse @@ -58,6 +58,8 @@ class MyEncoder(json.JSONEncoder): return obj.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" if isinstance(obj, date): return obj.strftime("%Y-%m-%d") + if isinstance(obj, timedelta): + return obj.total_seconds() if isinstance(obj, types.GeneratorType): return tuple(obj) if isinstance(obj, User): diff --git a/certidude/mysqllog.py b/certidude/mysqllog.py index 514e2f9..15a0507 100644 --- a/certidude/mysqllog.py +++ b/certidude/mysqllog.py @@ -2,7 +2,7 @@ import logging import time from datetime import datetime -from certidude.api.tag import RelationalMixin +from certidude.relational import RelationalMixin class LogHandler(logging.Handler, RelationalMixin): SQL_CREATE_TABLES = "log_tables.sql" diff --git a/certidude/push.py b/certidude/push.py index 08ce8ed..e7a45b2 100644 --- a/certidude/push.py +++ b/certidude/push.py @@ -11,6 +11,8 @@ def publish(event_type, event_data): """ Publish event on nchan EventSource publisher """ + assert event_type, "No event type specified" + assert event_data, "No event data specified" if not config.EVENT_SOURCE_PUBLISH: # Push server disabled return diff --git a/certidude/relational.py b/certidude/relational.py index 0a91688..274012d 100644 --- a/certidude/relational.py +++ b/certidude/relational.py @@ -24,7 +24,7 @@ class RelationalMixin(object): if self.uri.scheme == "sqlite": cur.executescript(fh.read()) else: - cur.execute(fh.read()) + cur.execute(fh.read(), multi=True) conn.commit() cur.close() conn.close() diff --git a/certidude/sql/mysql/tag_insert.sql b/certidude/sql/mysql/tag_insert.sql deleted file mode 100644 index 3f69567..0000000 --- a/certidude/sql/mysql/tag_insert.sql +++ /dev/null @@ -1,9 +0,0 @@ -insert into tag ( - `cn`, - `key`, - `value` -) values ( - %s, - %s, - %s -) diff --git a/certidude/sql/mysql/tag_tables.sql b/certidude/sql/mysql/tag_tables.sql deleted file mode 100644 index e69de29..0000000 diff --git a/certidude/sql/sqlite/tag_delete.sql b/certidude/sql/sqlite/tag_delete.sql deleted file mode 100644 index b18e077..0000000 --- a/certidude/sql/sqlite/tag_delete.sql +++ /dev/null @@ -1,3 +0,0 @@ -delete from tag -where id = ? -limit 1 diff --git a/certidude/sql/sqlite/tag_insert.sql b/certidude/sql/sqlite/tag_insert.sql deleted file mode 100644 index 4459216..0000000 --- a/certidude/sql/sqlite/tag_insert.sql +++ /dev/null @@ -1,9 +0,0 @@ -insert into tag ( - `cn`, - `key`, - `value` -) values ( - ?, - ?, - ? -); diff --git a/certidude/sql/sqlite/tag_list.sql b/certidude/sql/sqlite/tag_list.sql deleted file mode 100644 index c2ae228..0000000 --- a/certidude/sql/sqlite/tag_list.sql +++ /dev/null @@ -1,16 +0,0 @@ -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 - diff --git a/certidude/sql/sqlite/tag_tables.sql b/certidude/sql/sqlite/tag_tables.sql deleted file mode 100644 index 98f72ed..0000000 --- a/certidude/sql/sqlite/tag_tables.sql +++ /dev/null @@ -1,36 +0,0 @@ -create table if not exists `tag` ( - `id` integer primary key, - `cn` varchar(255) not null, - `key` varchar(255) not null, - `value` varchar(255) not null -); - -create table if not exists `tag_properties` ( - `id` integer primary key, - `tag_key` varchar(255) not null, - `tag_value` varchar(255) not null, - `property_key` varchar(255) not null, - `property_value` varchar(255) not null -); - -/* - -create table if not exists `device_tag` ( - `id` int(11) not null, - `device_id` varchar(45) not null, - `tag_id` varchar(45) not null, - `attached` timestamp null default current_timestamp, - primary key (`id`) -); - -create table if not exists `device` ( - `id` int(11) not null, - `created` timestamp not null default current_timestamp, - `cn` varchar(255) not null, - `product_model` varchar(50) not null, - `product_serial` varchar(50) default null, - `hardware_address` varchar(17) unique not null, - primary key (`id`) -); - -*/ diff --git a/certidude/sql/sqlite/tag_update.sql b/certidude/sql/sqlite/tag_update.sql deleted file mode 100644 index 84b9660..0000000 --- a/certidude/sql/sqlite/tag_update.sql +++ /dev/null @@ -1,4 +0,0 @@ -update `tag` -set `value` = ? -where `id` = ? -limit 1 diff --git a/certidude/static/index.html b/certidude/static/index.html index 23fe920..4fdc2cd 100644 --- a/certidude/static/index.html +++ b/certidude/static/index.html @@ -19,7 +19,6 @@ - diff --git a/certidude/static/js/certidude.js b/certidude/static/js/certidude.js index 1b022b9..ec8b7d5 100644 --- a/certidude/static/js/certidude.js +++ b/certidude/static/js/certidude.js @@ -1,45 +1,54 @@ jQuery.timeago.settings.allowFuture = true; -function onTagClicked() { - var value = $(this).html(); +function normalizeCommonName(j) { + return j.replace("@", "--").split(".").join("-"); // dafuq ?! +} + +function setTag(cn, key, value, indicator) { + $.ajax({ + method: "POST", + url: "/api/signed/" + cn + "/tag/", + data: { value: value, key: key }, + dataType: "text", + complete: function(xhr, status) { + console.info("Tag added successfully", xhr.status, status); + }, + success: function() { + $(indicator).removeClass("busy"); + }, + error: function(xhr, status, e) { + console.info("Submitting request failed with:", status, e); + alert(e); + } + }); +} + +function onTagClicked(event) { + var cn = $(event.target).attr("data-cn"); + var key = $(event.target).attr("data-key"); + var value = $(event.target).html(); var updated = prompt("Enter new tag or clear to remove the tag", value); + $(event.target).addClass("busy"); if (updated == "") { - $(this).addClass("busy"); $.ajax({ method: "DELETE", - url: "/api/tag/" + $(this).attr("data-id") + url: "/api/signed/" + cn + "/tag/" + key + "/" }); - } else if (updated && updated != value) { - $.ajax({ - method: "PUT", - url: "/api/tag/" + $(this).attr("data-id"), - dataType: "json", - data: { - key: $(this).attr("data-key"), - value: updated - } - }); + setTag(cn, key, updated, menu); } } -function onNewTagClicked() { - var cn = $(event.target).attr("data-cn"); - var key = $(event.target).val(); - $(event.target).val(""); +function onNewTagClicked(event) { + var menu = event.target; + var cn = $(menu).attr("data-cn"); + var key = $(menu).val(); + $(menu).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 - } - }); + $(menu).addClass("busy"); + setTag(cn, key, value, event.target); } function onTagFilterChanged() { @@ -68,47 +77,48 @@ function onRequestSubmitted(e) { url: "/api/request/" + e.data + "/", dataType: "json", success: function(request, status, xhr) { + console.info("Going to prepend:", request); onRequestDeleted(e); // Delete any existing ones just in case $("#pending_requests").prepend( nunjucks.render('views/request.html', { request: request })); + $("#pending_requests time").timeago(); + }, + error: function(response) { + console.info("Failed to retrieve certificate:", response); } }); } function onRequestDeleted(e) { console.log("Removing deleted request", e.data); - $("#request-" + e.data.replace("@", "--").replace(".", "-")).remove(); + $("#request-" + normalizeCommonName(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('views/status.html', { - lease: { - address: lease.address, - identity: lease.identity, - acquired: new Date(), - released: null - }})); -} +function onLeaseUpdate(e) { + console.log("Lease updated:", e.data); + $.ajax({ + method: "GET", + url: "/api/signed/" + e.data + "/lease/", + dataType: "json", + success: function(lease, status, xhr) { + console.info("Retrieved lease update details:", lease); + lease.age = (new Date() - new Date(lease.last_seen)) / 1000.0 + var $status = $("#signed_certificates [data-cn='" + e.data + "'] .status"); + $status.html(nunjucks.render('views/status.html', { + certificate: { + lease: lease }})); + $("time", $status).timeago(); -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('views/status.html', { - lease: { - address: lease.address, - identity: lease.identity, - acquired: null, - released: new Date() - }})); + }, + error: function(response) { + console.info("Failed to retrieve certificate:", response); + } + }); } function onRequestSigned(e) { console.log("Request signed:", e.data); - var slug = e.data.replace("@", "--").replace(".", "-"); + var slug = normalizeCommonName(e.data); console.log("Removing:", slug); $("#request-" + slug).slideUp("normal", function() { $(this).remove(); }); @@ -122,6 +132,7 @@ function onRequestSigned(e) { console.info("Retrieved certificate:", certificate); $("#signed_certificates").prepend( nunjucks.render('views/signed.html', { certificate: certificate })); + $("#signed_certificates time").timeago(); // TODO: optimize? }, error: function(response) { console.info("Failed to retrieve certificate:", response); @@ -129,42 +140,25 @@ function onRequestSigned(e) { }); } - function onCertificateRevoked(e) { console.log("Removing revoked certificate", e.data); - $("#certificate-" + e.data.replace("@", "--").replace(".", "-")).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(); + $("#certificate-" + normalizeCommonName(e.data)).slideUp("normal", function() { $(this).remove(); }); } function onTagUpdated(e) { - console.log("Tag updated", e.data); + var cn = e.data; + console.log("Tag updated", cn); $.ajax({ method: "GET", - url: "/api/tag/" + e.data + "/", + url: "/api/signed/" + cn + "/tag/", dataType: "json", - success:function(tag, status, xhr) { - console.info("Updated tag", tag); - $("#tag_" + tag.id).html(tag.value); + success:function(tags, status, xhr) { + console.info("Updated", cn, "tags", tags); + $(".tags span[data-cn='" + cn + "']").html( + nunjucks.render('views/tags.html', { + certificate: { + common_name: cn, + tags:tags }})); } }) } @@ -210,15 +204,12 @@ $(document).ready(function() { } source.addEventListener("log-entry", onLogEntry); - source.addEventListener("up-client", onClientUp); - source.addEventListener("down-client", onClientDown); + source.addEventListener("lease-update", onLeaseUpdate); 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); + source.addEventListener("tag-update", onTagUpdated); console.info("Swtiching to requests section"); $("section").hide(); @@ -258,41 +249,6 @@ $(document).ready(function() { }); console.log("Features enabled:", session.features); - if (session.features.tagging) { - console.info("Tagging enabled"); - $("#section-config").show(); - $.ajax({ - method: "GET", - url: "/api/config/", - dataType: "json", - success: function(configuration, status, xhr) { - console.info("Appending", configuration.length, "configuration items"); - $("#config").html(nunjucks.render('views/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"); - $("#config").html(nunjucks.render('views/configuration.html', { configuration:configuration})); - for (var j = 0; j < tags.length; j++) { - // TODO: Deduplicate - $tag = $("" + tags[j].value + ""); - 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(""); - } - } - }); - } - }); - } - if (session.request_submission_allowed) { $("#request_submit").click(function() { @@ -320,36 +276,6 @@ $(document).ready(function() { }); } - /** - * Fetch leases associated with certificates - */ - if (session.features.leases) { - $.ajax({ - method: "GET", - url: "/api/lease/", - dataType: "json", - success: function(leases, status, xhr) { - console.info("Got leases:", leases); - for (var j = 0; j < leases.length; j++) { - var $status = $("#signed_certificates [data-dn='" + leases[j].identity + "'] .status"); - if (!$status.length) { - console.info("Detected rogue client:", leases[j]); - continue; - } - $status.html(nunjucks.render('views/status.html', { - lease: { - address: leases[j].address, - age: (new Date() - new Date(leases[j].released)) / 1000, - identity: leases[j].identity, - acquired: new Date(leases[j].acquired).toLocaleString(), - released: leases[j].released ? new Date(leases[j].released).toLocaleString() : null - }})); - } - - } - }); - } - /** * Fetch log entries */ diff --git a/certidude/static/views/signed.html b/certidude/static/views/signed.html index b423dbb..961e650 100644 --- a/certidude/static/views/signed.html +++ b/certidude/static/views/signed.html @@ -46,12 +46,17 @@ {% if session.features.tagging %}
- + + {% include 'views/tagtypes.html' %}
{% endif %} -
+
+ {% include 'views/status.html' %} +
diff --git a/certidude/static/views/status.html b/certidude/static/views/status.html index eb2aa0e..220769f 100644 --- a/certidude/static/views/status.html +++ b/certidude/static/views/status.html @@ -1,16 +1,14 @@ - - - - - -{% if lease %} -{% if lease.released %} -Last seen {{ lease.released }} at {{ lease.address }} -{% else %} -Online since {{ lease.acquired }} at {{ lease.address }} -{% endif %} -{% else %} -Not seen -{% endif %} + {% if certificate.lease %} + + + + {% if certificate.lease.age > session.authority.lease.offline %} + Last seen + at {{ certificate.lease.address }} + {% else %} + Online since at + {{ certificate.lease.address }} + {% endif %} + {% endif %} diff --git a/certidude/static/views/tags.html b/certidude/static/views/tags.html index 1c17eb3..4bc377c 100644 --- a/certidude/static/views/tags.html +++ b/certidude/static/views/tags.html @@ -1,3 +1,6 @@ -{{ tag.value }} +{% for key, value in certificate.tags %} +{{ value }} +{% endfor %} diff --git a/certidude/templates/certidude-server.conf b/certidude/templates/certidude-server.conf index 1959af9..9bd31e4 100644 --- a/certidude/templates/certidude-server.conf +++ b/certidude/templates/certidude-server.conf @@ -57,26 +57,6 @@ backend = ;backend = sql database = sqlite://{{ directory }}/db.sqlite -[tagging] -backend = - -;backend = sql -database = sqlite://{{ directory }}/db.sqlite - -[leases] -backend = - -;backend = sql -schema = strongswan -database = sqlite://{{ directory }}/db.sqlite - -# Following was used on an OpenWrt router -# uci set openvpn.s2c.status=/www/status.log -# uci commit; touch /www/status.log; chmod 755 /www/status.log -;backend = openvpn-status -;openvpn status uri = /var/log/openvpn-status.log -openvpn status uri = http://router.example.com/status.log - [signature] # Server certificate is granted to certificate with # common name that includes period which translates to FQDN of the machine.