mirror of
				https://github.com/laurivosandi/certidude
				synced 2025-10-31 09:29:13 +00:00 
			
		
		
		
	Use filesystem extended attribute user.xdg.tags for tags, move leases to user.lease namespace
This commit is contained in:
		| @@ -59,15 +59,29 @@ class SessionResource(object): | |||||||
|  |  | ||||||
|         def serialize_certificates(g): |         def serialize_certificates(g): | ||||||
|             for common_name, path, buf, obj, server in g(): |             for common_name, path, buf, obj, server in g(): | ||||||
|  |                 # Extract certificate tags from filesystem | ||||||
|                 try: |                 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( |                     lease = dict( | ||||||
|                         address = xattr.getxattr(path, "user.address"), |                         address = xattr.getxattr(path, "user.lease.address"), | ||||||
|                         last_seen = last_seen, |                         last_seen = last_seen, | ||||||
|                         age = datetime.utcnow() - last_seen |                         age = datetime.utcnow() - last_seen | ||||||
|                     ) |                     ) | ||||||
|                 except IOError: # No such attribute(s) |                 except IOError: # No such attribute(s) | ||||||
|                     lease = None |                     lease = None | ||||||
|  |  | ||||||
|                 yield dict( |                 yield dict( | ||||||
|                     serial_number = "%x" % obj.serial_number, |                     serial_number = "%x" % obj.serial_number, | ||||||
|                     common_name = common_name, |                     common_name = common_name, | ||||||
| @@ -77,10 +91,7 @@ class SessionResource(object): | |||||||
|                     expires = obj.not_valid_after, |                     expires = obj.not_valid_after, | ||||||
|                     sha256sum = hashlib.sha256(buf).hexdigest(), |                     sha256sum = hashlib.sha256(buf).hexdigest(), | ||||||
|                     lease = lease, |                     lease = lease, | ||||||
|                     tags = dict([ |                     tags = tags | ||||||
|                         (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(): |         if req.context.get("user").is_admin(): | ||||||
| @@ -96,6 +107,7 @@ class SessionResource(object): | |||||||
|             ), |             ), | ||||||
|             request_submission_allowed = config.REQUEST_SUBMISSION_ALLOWED, |             request_submission_allowed = config.REQUEST_SUBMISSION_ALLOWED, | ||||||
|             authority = dict( |             authority = dict( | ||||||
|  |                 tagging = [dict(name=t[0], type=t[1], title=t[2]) for t in config.TAG_TYPES], | ||||||
|                 lease = dict( |                 lease = dict( | ||||||
|                     offline = 600, # Seconds from last seen activity to consider lease offline, OpenVPN reneg-sec option |                     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 |                     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()) |     app.add_route("/api/signed/{cn}/lease/", LeaseDetailResource()) | ||||||
|  |  | ||||||
|     # API call used to delete existing tags |     # 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 |     # Gateways can submit leases via this API call | ||||||
|     app.add_route("/api/lease/", LeaseResource()) |     app.add_route("/api/lease/", LeaseResource()) | ||||||
|   | |||||||
| @@ -16,8 +16,8 @@ class LeaseDetailResource(object): | |||||||
|     def on_get(self, req, resp, cn): |     def on_get(self, req, resp, cn): | ||||||
|         path, buf, cert = authority.get_signed(cn) |         path, buf, cert = authority.get_signed(cn) | ||||||
|         return dict( |         return dict( | ||||||
|             last_seen = xattr.getxattr(path, "user.last_seen"), |             last_seen = xattr.getxattr(path, "user.lease.last_seen"), | ||||||
|             address = xattr.getxattr(path, "user.address").decode("ascii") |             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! |         if cert.serial != req.get_param_as_int("serial", required=True): # Badum we have OCSP! | ||||||
|             raise # TODO proper exception |             raise # TODO proper exception | ||||||
|         if req.get_param("action") == "client-connect": |         if req.get_param("action") == "client-connect": | ||||||
|             xattr.setxattr(path, "user.address", req.get_param("address", required=True).encode("ascii")) |             xattr.setxattr(path, "user.lease.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.last_seen", datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z") | ||||||
|             push.publish("lease-update", common_name) |             push.publish("lease-update", common_name) | ||||||
|  |  | ||||||
|         # client-disconnect is pretty much unusable: |         # client-disconnect is pretty much unusable: | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import falcon | import falcon | ||||||
| import logging | import logging | ||||||
| import xattr | from xattr import getxattr, removexattr, setxattr | ||||||
| from certidude import authority | from certidude import authority, push | ||||||
| from certidude.auth import login_required, authorize_admin | from certidude.auth import login_required, authorize_admin | ||||||
| from certidude.decorators import serialize, csrf_protection | from certidude.decorators import serialize, csrf_protection | ||||||
|  |  | ||||||
| @@ -13,19 +13,34 @@ class TagResource(object): | |||||||
|     @authorize_admin |     @authorize_admin | ||||||
|     def on_get(self, req, resp, cn): |     def on_get(self, req, resp, cn): | ||||||
|         path, buf, cert = authority.get_signed(cn) |         path, buf, cert = authority.get_signed(cn) | ||||||
|         return dict([ |         tags = [] | ||||||
|             (k[9:], xattr.getxattr(path, k)) |         try: | ||||||
|             for k in xattr.listxattr(path) |             for tag in getxattr(path, "user.xdg.tags").split(","): | ||||||
|             if k.startswith("user.tag.")]) |                 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 |     @csrf_protection | ||||||
|     @login_required |     @login_required | ||||||
|     @authorize_admin |     @authorize_admin | ||||||
|     def on_post(self, req, resp, cn): |     def on_post(self, req, resp, cn): | ||||||
|         from certidude import push |  | ||||||
|         path, buf, cert = authority.get_signed(cn) |         path, buf, cert = authority.get_signed(cn) | ||||||
|         key, value = req.get_param("key", required=True), req.get_param("value", required=True) |         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)) |         logger.debug(u"Tag %s=%s set for %s" % (key, value, cn)) | ||||||
|         push.publish("tag-update", cn) |         push.publish("tag-update", cn) | ||||||
|  |  | ||||||
| @@ -34,9 +49,32 @@ class TagDetailResource(object): | |||||||
|     @csrf_protection |     @csrf_protection | ||||||
|     @login_required |     @login_required | ||||||
|     @authorize_admin |     @authorize_admin | ||||||
|     def on_delete(self, req, resp, cn, key): |     def on_put(self, req, resp, cn, tag): | ||||||
|         from certidude import push |  | ||||||
|         path, buf, cert = authority.get_signed(cn) |         path, buf, cert = authority.get_signed(cn) | ||||||
|         xattr.removexattr(path, "user.tag.%s" % key) |         value = req.get_param("value", required=True) | ||||||
|         logger.debug(u"Tag %s removed for %s" % (key, cn)) |         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) |         push.publish("tag-update", cn) | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ function normalizeCommonName(j) { | |||||||
| } | } | ||||||
|  |  | ||||||
| function setTag(cn, key, value, indicator) { | function setTag(cn, key, value, indicator) { | ||||||
|  |     $(indicator).addClass("busy"); | ||||||
|     $.ajax({ |     $.ajax({ | ||||||
|         method: "POST", |         method: "POST", | ||||||
|         url: "/api/signed/" + cn + "/tag/", |         url: "/api/signed/" + cn + "/tag/", | ||||||
| @@ -24,18 +25,36 @@ function setTag(cn, key, value, indicator) { | |||||||
| } | } | ||||||
|  |  | ||||||
| function onTagClicked(event) { | function onTagClicked(event) { | ||||||
|  |     var tag = event.target; | ||||||
|     var cn = $(event.target).attr("data-cn"); |     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 value = $(event.target).html(); | ||||||
|     var updated = prompt("Enter new tag or clear to remove the tag", value); |     var updated = prompt("Enter new tag or clear to remove the tag", value); | ||||||
|     $(event.target).addClass("busy"); |  | ||||||
|     if (updated == "") { |     if (updated == "") { | ||||||
|  |         $(event.target).addClass("busy"); | ||||||
|         $.ajax({ |         $.ajax({ | ||||||
|             method: "DELETE", |             method: "DELETE", | ||||||
|             url: "/api/signed/" + cn + "/tag/" + key + "/" |             url: "/api/signed/" + cn + "/tag/" + id + "/" | ||||||
|         }); |         }); | ||||||
|     } else if (updated && updated != value) { |     } 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); |     var value = prompt("Enter new " + key + " tag for " + cn); | ||||||
|     if (!value) return; |     if (!value) return; | ||||||
|     if (value.length == 0) return; |     if (value.length == 0) return; | ||||||
|     $(menu).addClass("busy"); |  | ||||||
|     setTag(cn, key, value, event.target); |     setTag(cn, key, value, event.target); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -51,7 +51,9 @@ | |||||||
|         </span> |         </span> | ||||||
|         <select class="icon tag" data-cn="{{ certificate.common_name }}" onChange="onNewTagClicked(event);"> |         <select class="icon tag" data-cn="{{ certificate.common_name }}" onChange="onNewTagClicked(event);"> | ||||||
|             <option value="">Add tag...</option> |             <option value="">Add tag...</option> | ||||||
|                 {% include 'views/tagtypes.html' %} |             {% for tag_type in session.authority.tagging %} | ||||||
|  |             <option value="{{ tag_type.name }}">{{ tag_type.title }}</option> | ||||||
|  |             {% endfor %} | ||||||
|         </select> |         </select> | ||||||
|     </div> |     </div> | ||||||
|     {% endif %} |     {% endif %} | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| {% for key, value in certificate.tags %} | {% for tag in certificate.tags %} | ||||||
| <span onclick="onTagClicked(event);" | <span onclick="onTagClicked(event);" | ||||||
| title="{{ key }}={{ value }}" class="tag icon {{ key | replace('.', ' ') }}" | title="{{ tag.id }}" class="tag icon {{ tag.key | replace('.', ' ') }}" | ||||||
| data-cn="{{ certificate.common_name }}" | data-cn="{{ certificate.common_name }}">{{ tag.value }}</span> | ||||||
| data-key="{{ key }}">{{ value }}</span> |  | ||||||
| {% endfor %} | {% endfor %} | ||||||
|   | |||||||
| @@ -1,10 +0,0 @@ | |||||||
| <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.channel">Channel number</option> |  | ||||||
| <option value="usb.approved">Approved USB device</option> |  | ||||||
| @@ -137,3 +137,9 @@ format = p12 | |||||||
| # Template for OpenVPN profile, copy certidude/templates/openvpn-client.conf | # Template for OpenVPN profile, copy certidude/templates/openvpn-client.conf | ||||||
| # to /etc/certidude/ and make modifications as necessary | # to /etc/certidude/ and make modifications as necessary | ||||||
| openvpn profile template = {{ openvpn_profile_template_path }} | openvpn profile template = {{ openvpn_profile_template_path }} | ||||||
|  |  | ||||||
|  | [tagging] | ||||||
|  | owner/string = Owner | ||||||
|  | location/string = Location | ||||||
|  | phone/string = Phone | ||||||
|  | other/ = Other | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user