diff --git a/certidude/api/__init__.py b/certidude/api/__init__.py index 7a92c56..007ada5 100644 --- a/certidude/api/__init__.py +++ b/certidude/api/__init__.py @@ -59,15 +59,29 @@ class SessionResource(object): def serialize_certificates(g): for common_name, path, buf, obj, server in g(): + # Extract certificate tags from filesystem try: - last_seen = datetime.strptime(xattr.getxattr(path, "user.last_seen"), "%Y-%m-%dT%H:%M:%S.%fZ") + tags = [] + for tag in xattr.getxattr(path, "user.xdg.tags").split(","): + if "=" in tag: + k, v = tag.split("=", 1) + else: + k, v = "other", tag + tags.append(dict(id=tag, key=k, value=v)) + except IOError: # No such attribute(s) + tags = None + + # Extract lease information from filesystem + try: + last_seen = datetime.strptime(xattr.getxattr(path, "user.lease.last_seen"), "%Y-%m-%dT%H:%M:%S.%fZ") lease = dict( - address = xattr.getxattr(path, "user.address"), + address = xattr.getxattr(path, "user.lease.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, @@ -77,10 +91,7 @@ class SessionResource(object): expires = obj.not_valid_after, 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.")]) + tags = tags ) if req.context.get("user").is_admin(): @@ -96,6 +107,7 @@ class SessionResource(object): ), request_submission_allowed = config.REQUEST_SUBMISSION_ALLOWED, authority = dict( + tagging = [dict(name=t[0], type=t[1], title=t[2]) for t in config.TAG_TYPES], 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 @@ -199,7 +211,7 @@ def certidude_app(): app.add_route("/api/signed/{cn}/lease/", LeaseDetailResource()) # API call used to delete existing tags - app.add_route("/api/signed/{cn}/tag/{key}/", TagDetailResource()) + app.add_route("/api/signed/{cn}/tag/{tag}/", TagDetailResource()) # Gateways can submit leases via this API call app.add_route("/api/lease/", LeaseResource()) diff --git a/certidude/api/lease.py b/certidude/api/lease.py index 66e39a0..d27564e 100644 --- a/certidude/api/lease.py +++ b/certidude/api/lease.py @@ -16,8 +16,8 @@ class LeaseDetailResource(object): 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") + last_seen = xattr.getxattr(path, "user.lease.last_seen"), + address = xattr.getxattr(path, "user.lease.address").decode("ascii") ) @@ -29,8 +29,8 @@ class LeaseResource(object): 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") + xattr.setxattr(path, "user.lease.address", req.get_param("address", required=True).encode("ascii")) + xattr.setxattr(path, "user.lease.last_seen", datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z") push.publish("lease-update", common_name) # client-disconnect is pretty much unusable: diff --git a/certidude/api/tag.py b/certidude/api/tag.py index 4c2c0fb..8175207 100644 --- a/certidude/api/tag.py +++ b/certidude/api/tag.py @@ -1,7 +1,7 @@ import falcon import logging -import xattr -from certidude import authority +from xattr import getxattr, removexattr, setxattr +from certidude import authority, push from certidude.auth import login_required, authorize_admin from certidude.decorators import serialize, csrf_protection @@ -13,19 +13,34 @@ class TagResource(object): @authorize_admin 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.")]) + tags = [] + try: + for tag in getxattr(path, "user.xdg.tags").split(","): + if "=" in tag: + k, v = tag.split("=", 1) + else: + k, v = "other", tag + tags.append(dict(id=tag, key=k, value=v)) + except IOError: # No user.xdg.tags attribute + pass + return tags + @csrf_protection @login_required @authorize_admin def on_post(self, req, resp, cn): - from certidude import push 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")) + try: + tags = set(getxattr(path, "user.xdg.tags").decode("utf-8").split(",")) + except IOError: + tags = set() + if key == "other": + tags.add(value) + else: + tags.add("%s=%s" % (key,value)) + setxattr(path, "user.xdg.tags", ",".join(tags).encode("utf-8")) logger.debug(u"Tag %s=%s set for %s" % (key, value, cn)) push.publish("tag-update", cn) @@ -34,9 +49,32 @@ class TagDetailResource(object): @csrf_protection @login_required @authorize_admin - def on_delete(self, req, resp, cn, key): - from certidude import push + def on_put(self, req, resp, cn, tag): path, buf, cert = authority.get_signed(cn) - xattr.removexattr(path, "user.tag.%s" % key) - logger.debug(u"Tag %s removed for %s" % (key, cn)) + value = req.get_param("value", required=True) + try: + tags = set(getxattr(path, "user.xdg.tags").decode("utf-8").split(",")) + except IOError: + tags = set() + tags.remove(tag) + if "=" in tag: + tags.add("%s=%s" % (tag.split("=")[0], value)) + else: + tags.add(value) + setxattr(path, "user.xdg.tags", ",".join(tags).encode("utf-8")) + logger.debug(u"Tag %s set to %s for %s" % (tag, value, cn)) + push.publish("tag-update", cn) + + @csrf_protection + @login_required + @authorize_admin + def on_delete(self, req, resp, cn, tag): + path, buf, cert = authority.get_signed(cn) + tags = set(getxattr(path, "user.xdg.tags").split(",")) + tags.remove(tag) + if not tags: + removexattr(path, "user.xdg.tags") + else: + setxattr(path, "user.xdg.tags", ",".join(tags)) + logger.debug(u"Tag %s removed for %s" % (tag, cn)) push.publish("tag-update", cn) diff --git a/certidude/static/js/certidude.js b/certidude/static/js/certidude.js index ec8b7d5..17782ba 100644 --- a/certidude/static/js/certidude.js +++ b/certidude/static/js/certidude.js @@ -5,6 +5,7 @@ function normalizeCommonName(j) { } function setTag(cn, key, value, indicator) { + $(indicator).addClass("busy"); $.ajax({ method: "POST", url: "/api/signed/" + cn + "/tag/", @@ -24,18 +25,36 @@ function setTag(cn, key, value, indicator) { } function onTagClicked(event) { + var tag = event.target; var cn = $(event.target).attr("data-cn"); - var key = $(event.target).attr("data-key"); + var id = $(event.target).attr("title"); var value = $(event.target).html(); var updated = prompt("Enter new tag or clear to remove the tag", value); - $(event.target).addClass("busy"); if (updated == "") { + $(event.target).addClass("busy"); $.ajax({ method: "DELETE", - url: "/api/signed/" + cn + "/tag/" + key + "/" + url: "/api/signed/" + cn + "/tag/" + id + "/" }); } else if (updated && updated != value) { - setTag(cn, key, updated, menu); + $(tag).addClass("busy"); + $.ajax({ + method: "PUT", + url: "/api/signed/" + cn + "/tag/" + id + "/", + data: { value: updated }, + dataType: "text", + complete: function(xhr, status) { + console.info("Tag added successfully", xhr.status, status); + }, + success: function() { + $(tag).removeClass("busy"); + }, + error: function(xhr, status, e) { + console.info("Submitting request failed with:", status, e); + alert(e); + } + }); + } } @@ -47,7 +66,6 @@ function onNewTagClicked(event) { var value = prompt("Enter new " + key + " tag for " + cn); if (!value) return; if (value.length == 0) return; - $(menu).addClass("busy"); setTag(cn, key, value, event.target); } diff --git a/certidude/static/views/signed.html b/certidude/static/views/signed.html index 961e650..c7b9126 100644 --- a/certidude/static/views/signed.html +++ b/certidude/static/views/signed.html @@ -51,7 +51,9 @@ {% endif %} diff --git a/certidude/static/views/tags.html b/certidude/static/views/tags.html index 4bc377c..1ffe149 100644 --- a/certidude/static/views/tags.html +++ b/certidude/static/views/tags.html @@ -1,6 +1,5 @@ -{% for key, value in certificate.tags %} +{% for tag in certificate.tags %} {{ value }} +title="{{ tag.id }}" class="tag icon {{ tag.key | replace('.', ' ') }}" +data-cn="{{ certificate.common_name }}">{{ tag.value }} {% endfor %} diff --git a/certidude/static/views/tagtypes.html b/certidude/static/views/tagtypes.html deleted file mode 100644 index 9b6be4f..0000000 --- a/certidude/static/views/tagtypes.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/certidude/templates/certidude-server.conf b/certidude/templates/certidude-server.conf index 9bd31e4..1dc7453 100644 --- a/certidude/templates/certidude-server.conf +++ b/certidude/templates/certidude-server.conf @@ -137,3 +137,9 @@ format = p12 # Template for OpenVPN profile, copy certidude/templates/openvpn-client.conf # to /etc/certidude/ and make modifications as necessary openvpn profile template = {{ openvpn_profile_template_path }} + +[tagging] +owner/string = Owner +location/string = Location +phone/string = Phone +other/ = Other