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