mirror of
				https://github.com/laurivosandi/certidude
				synced 2025-10-31 09:29:13 +00:00 
			
		
		
		
	Move leases and tagging backend to filesystem extended attributes
This commit is contained in:
		| @@ -6,6 +6,7 @@ import logging | |||||||
| import os | import os | ||||||
| import click | import click | ||||||
| import hashlib | import hashlib | ||||||
|  | import xattr | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from time import sleep | from time import sleep | ||||||
| from certidude import authority, mailer | from certidude import authority, mailer | ||||||
| @@ -58,6 +59,15 @@ 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(): | ||||||
|  |                 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( |                 yield dict( | ||||||
|                     serial_number = "%x" % obj.serial_number, |                     serial_number = "%x" % obj.serial_number, | ||||||
|                     common_name = common_name, |                     common_name = common_name, | ||||||
| @@ -65,7 +75,12 @@ class SessionResource(object): | |||||||
|                     # TODO: key type, key length, key exponent, key modulo |                     # TODO: key type, key length, key exponent, key modulo | ||||||
|                     signed = obj.not_valid_before, |                     signed = obj.not_valid_before, | ||||||
|                     expires = obj.not_valid_after, |                     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(): |         if req.context.get("user").is_admin(): | ||||||
| @@ -81,6 +96,10 @@ class SessionResource(object): | |||||||
|             ), |             ), | ||||||
|             request_submission_allowed = config.REQUEST_SUBMISSION_ALLOWED, |             request_submission_allowed = config.REQUEST_SUBMISSION_ALLOWED, | ||||||
|             authority = dict( |             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( |                 common_name = authority.ca_cert.subject.get_attributes_for_oid( | ||||||
|                     NameOID.COMMON_NAME)[0].value, |                     NameOID.COMMON_NAME)[0].value, | ||||||
|                 outbox = dict( |                 outbox = dict( | ||||||
| @@ -108,8 +127,8 @@ class SessionResource(object): | |||||||
|                 ) |                 ) | ||||||
|             ) if req.context.get("user").is_admin() else None, |             ) if req.context.get("user").is_admin() else None, | ||||||
|             features=dict( |             features=dict( | ||||||
|                 tagging=config.TAGGING_BACKEND, |                 tagging=True, | ||||||
|                 leases=config.LEASES_BACKEND, |                 leases=True, | ||||||
|                 logging=config.LOGGING_BACKEND)) |                 logging=config.LOGGING_BACKEND)) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -155,12 +174,13 @@ def certidude_app(): | |||||||
|     from .revoked import RevocationListResource |     from .revoked import RevocationListResource | ||||||
|     from .signed import SignedCertificateDetailResource |     from .signed import SignedCertificateDetailResource | ||||||
|     from .request import RequestListResource, RequestDetailResource |     from .request import RequestListResource, RequestDetailResource | ||||||
|     from .lease import LeaseResource, StatusFileLeaseResource |     from .lease import LeaseResource, LeaseDetailResource | ||||||
|     from .whois import WhoisResource |  | ||||||
|     from .tag import TagResource, TagDetailResource |  | ||||||
|     from .cfg import ConfigResource, ScriptResource |     from .cfg import ConfigResource, ScriptResource | ||||||
|  |     from .tag import TagResource, TagDetailResource | ||||||
|  |     from .attrib import AttributeResource | ||||||
|  |  | ||||||
|     app = falcon.API(middleware=NormalizeMiddleware()) |     app = falcon.API(middleware=NormalizeMiddleware()) | ||||||
|  |     app.req_options.auto_parse_form_urlencoded = True | ||||||
|  |  | ||||||
|     # Certificate authority API calls |     # Certificate authority API calls | ||||||
|     app.add_route("/api/ocsp/", CertificateStatusResource()) |     app.add_route("/api/ocsp/", CertificateStatusResource()) | ||||||
| @@ -171,25 +191,21 @@ def certidude_app(): | |||||||
|     app.add_route("/api/request/", RequestListResource()) |     app.add_route("/api/request/", RequestListResource()) | ||||||
|     app.add_route("/api/", SessionResource()) |     app.add_route("/api/", SessionResource()) | ||||||
|  |  | ||||||
|     # Gateway API calls, should this be moved to separate project? |     # Extended attributes for scripting etc. | ||||||
|     if config.LEASES_BACKEND == "openvpn-status": |     app.add_route("/api/signed/{cn}/attr/", AttributeResource()) | ||||||
|         app.add_route("/api/lease/", StatusFileLeaseResource(config.OPENVPN_STATUS_URI)) |  | ||||||
|     elif config.LEASES_BACKEND == "sql": |     # 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()) |     app.add_route("/api/lease/", LeaseResource()) | ||||||
|         app.add_route("/api/whois/", WhoisResource()) |  | ||||||
|  |  | ||||||
|     # Optional user enrollment API call |     # Optional user enrollment API call | ||||||
|     if config.USER_ENROLLMENT_ALLOWED: |     if config.USER_ENROLLMENT_ALLOWED: | ||||||
|         app.add_route("/api/bundle/", BundleResource()) |         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 |     return app | ||||||
|   | |||||||
							
								
								
									
										46
									
								
								certidude/api/attrib.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								certidude/api/attrib.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
| @@ -1,96 +1,39 @@ | |||||||
|  |  | ||||||
|  | import click | ||||||
|  | import xattr | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from dateutil import tz |  | ||||||
| from pyasn1.codec.der import decoder | 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.auth import login_required, authorize_admin | ||||||
| from certidude.decorators import serialize | from certidude.decorators import serialize | ||||||
|  |  | ||||||
| localtime = tz.tzlocal() | # TODO: lease namespacing (?) | ||||||
|  |  | ||||||
| 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 |  | ||||||
|  |  | ||||||
|  | class LeaseDetailResource(object): | ||||||
|     @serialize |     @serialize | ||||||
|     @login_required |     @login_required | ||||||
|     @authorize_admin |     @authorize_admin | ||||||
|     def on_get(self, req, resp): |     def on_get(self, req, resp, cn): | ||||||
|         from openvpn_status import parse_status |         path, buf, cert = authority.get_signed(cn) | ||||||
|         from urllib import urlopen |         return dict( | ||||||
|         fh = urlopen(self.uri) |             last_seen = xattr.getxattr(path, "user.last_seen"), | ||||||
|         # openvpn-status.log has no information about timezone |             address = xattr.getxattr(path, "user.address").decode("ascii") | ||||||
|         # 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 |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class LeaseResource(object): | class LeaseResource(object): | ||||||
|     @serialize |     def on_post(self, req, resp): | ||||||
|     @login_required |         # TODO: verify signature | ||||||
|     @authorize_admin |         common_name = req.get_param("client", required=True) | ||||||
|     def on_get(self, req, resp): |         path, buf, cert = authority.get_signed(common_name) # TODO: catch exceptions | ||||||
|         from ipaddress import ip_address |         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 |         # client-disconnect is pretty much unusable: | ||||||
|         SQL_LEASES = """ |         # - Android Connect Client results "IP packet with unknown IP version=2" on gateway | ||||||
|             select |         # - NetworkManager just kills OpenVPN client, disconnect is never reported | ||||||
|                 acquired, |         # - Disconnect is also not reported when uplink connection dies or laptop goes to sleep | ||||||
|                 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() |  | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import click | |||||||
| import falcon | import falcon | ||||||
| import logging | import logging | ||||||
| import ipaddress | import ipaddress | ||||||
|  | import json | ||||||
| import os | import os | ||||||
| import hashlib | import hashlib | ||||||
| from base64 import b64decode | from base64 import b64decode | ||||||
| @@ -154,10 +155,33 @@ class RequestDetailResource(object): | |||||||
|         Fetch certificate signing request as PEM |         Fetch certificate signing request as PEM | ||||||
|         """ |         """ | ||||||
|         resp.set_header("Content-Type", "application/pkcs10") |         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", |         logger.debug(u"Signing request %s was downloaded by %s", | ||||||
|             cn, req.context.get("remote_addr")) |             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 |     @csrf_protection | ||||||
|     @login_required |     @login_required | ||||||
|     @authorize_admin |     @authorize_admin | ||||||
|   | |||||||
| @@ -1,76 +1,42 @@ | |||||||
|  |  | ||||||
| import falcon | import falcon | ||||||
| import logging | import logging | ||||||
| from certidude.relational import RelationalMixin | import xattr | ||||||
|  | from certidude import authority | ||||||
| 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 | ||||||
|  |  | ||||||
| logger = logging.getLogger("api") | logger = logging.getLogger("api") | ||||||
|  |  | ||||||
| class TagResource(RelationalMixin): | class TagResource(object): | ||||||
|     SQL_CREATE_TABLES = "tag_tables.sql" |  | ||||||
|  |  | ||||||
|     @serialize |     @serialize | ||||||
|     @login_required |     @login_required | ||||||
|     @authorize_admin |     @authorize_admin | ||||||
|     def on_get(self, req, resp): |     def on_get(self, req, resp, cn): | ||||||
|         return self.iterfetch("select * from tag") |         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 |     @csrf_protection | ||||||
|     @serialize |  | ||||||
|     @login_required |     @login_required | ||||||
|     @authorize_admin |     @authorize_admin | ||||||
|     def on_post(self, req, resp): |     def on_post(self, req, resp, cn): | ||||||
|         from certidude import push |         from certidude import push | ||||||
|         args = req.get_param("cn"), req.get_param("key"), req.get_param("value") |         path, buf, cert = authority.get_signed(cn) | ||||||
|         rowid = self.sql_execute("tag_insert.sql", *args) |         key, value = req.get_param("key", required=True), req.get_param("value", required=True) | ||||||
|         push.publish("tag-added", str(rowid)) |         xattr.setxattr(path, "user.tag.%s" % key, value.encode("utf-8")) | ||||||
|         logger.debug(u"Tag cn=%s, key=%s, value=%s added" % args) |         logger.debug(u"Tag %s=%s set for %s" % (key, value, cn)) | ||||||
|  |         push.publish("tag-update", cn) | ||||||
|  |  | ||||||
| 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() |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TagDetailResource(object): | ||||||
|     @csrf_protection |     @csrf_protection | ||||||
|     @serialize |  | ||||||
|     @login_required |     @login_required | ||||||
|     @authorize_admin |     @authorize_admin | ||||||
|     def on_put(self, req, resp, identifier): |     def on_delete(self, req, resp, cn, key): | ||||||
|         from certidude import push |         from certidude import push | ||||||
|         args = req.get_param("value"), identifier |         path, buf, cert = authority.get_signed(cn) | ||||||
|         self.sql_execute("tag_update.sql", *args) |         xattr.removexattr(path, "user.tag.%s" % key) | ||||||
|         logger.debug(u"Tag %s updated, value set to %s", |         logger.debug(u"Tag %s removed for %s" % (key, cn)) | ||||||
|             identifier, req.get_param("value")) |         push.publish("tag-update", cn) | ||||||
|         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) |  | ||||||
|   | |||||||
| @@ -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") |  | ||||||
| @@ -15,6 +15,7 @@ from cryptography.hazmat.primitives import hashes, serialization | |||||||
| from certidude import config, push, mailer, const | from certidude import config, push, mailer, const | ||||||
| from certidude import errors | from certidude import errors | ||||||
| from jinja2 import Template | 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]))?$" | 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") |     cert_path = os.path.join(config.SIGNED_DIR, common_name.value + ".pem") | ||||||
|     renew = False |     renew = False | ||||||
|  |  | ||||||
|  |     signed_path = os.path.join(config.SIGNED_DIR, "%s.pem" % common_name.value) | ||||||
|  |     revoked_path = None | ||||||
|  |  | ||||||
|     # Move existing certificate if necessary |     # Move existing certificate if necessary | ||||||
|     if os.path.exists(cert_path): |     if os.path.exists(cert_path): | ||||||
|         with open(cert_path) as fh: |         with open(cert_path) as fh: | ||||||
| @@ -310,7 +314,6 @@ def _sign(csr, buf, overwrite=False): | |||||||
|         if overwrite: |         if overwrite: | ||||||
|             if renew: |             if renew: | ||||||
|                 # TODO: is this the best approach? |                 # 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) |                 revoked_path = os.path.join(config.REVOKED_DIR, "%x.pem" % prev.serial_number) | ||||||
|                 os.rename(signed_path, revoked_path) |                 os.rename(signed_path, revoked_path) | ||||||
|             else: |             else: | ||||||
| @@ -325,6 +328,13 @@ def _sign(csr, buf, overwrite=False): | |||||||
|         fh.write(cert_buf) |         fh.write(cert_buf) | ||||||
|     os.rename(cert_path + ".part", cert_path) |     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 |     # Send mail | ||||||
|     recipient = None |     recipient = None | ||||||
|  |  | ||||||
|   | |||||||
| @@ -72,12 +72,7 @@ EVENT_SOURCE_SUBSCRIBE = cp.get("push", "event source subscribe") | |||||||
| LONG_POLL_PUBLISH = cp.get("push", "long poll publish") | LONG_POLL_PUBLISH = cp.get("push", "long poll publish") | ||||||
| LONG_POLL_SUBSCRIBE = cp.get("push", "long poll subscribe") | LONG_POLL_SUBSCRIBE = cp.get("push", "long poll subscribe") | ||||||
|  |  | ||||||
| TAGGING_BACKEND = cp.get("tagging", "backend") |  | ||||||
| LOGGING_BACKEND = cp.get("logging", "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: | if "whitelist" == AUTHORIZATION_BACKEND: | ||||||
|     USERS_WHITELIST = set([j for j in  cp.get("authorization", "users whitelist").split(" ") if j]) |     USERS_WHITELIST = set([j for j in  cp.get("authorization", "users whitelist").split(" ") if j]) | ||||||
| @@ -93,4 +88,6 @@ elif "ldap" == AUTHORIZATION_BACKEND: | |||||||
| else: | else: | ||||||
|     raise NotImplementedError("Unknown authorization backend '%s'" % AUTHORIZATION_BACKEND) |     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 | # TODO: Check if we don't have base or servers | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import ipaddress | |||||||
| import json | import json | ||||||
| import logging | import logging | ||||||
| import types | import types | ||||||
| from datetime import date, time, datetime | from datetime import date, time, datetime, timedelta | ||||||
| from certidude.auth import User | from certidude.auth import User | ||||||
| from urlparse import urlparse | from urlparse import urlparse | ||||||
|  |  | ||||||
| @@ -58,6 +58,8 @@ class MyEncoder(json.JSONEncoder): | |||||||
|             return obj.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" |             return obj.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" | ||||||
|         if isinstance(obj, date): |         if isinstance(obj, date): | ||||||
|             return obj.strftime("%Y-%m-%d") |             return obj.strftime("%Y-%m-%d") | ||||||
|  |         if isinstance(obj, timedelta): | ||||||
|  |             return obj.total_seconds() | ||||||
|         if isinstance(obj, types.GeneratorType): |         if isinstance(obj, types.GeneratorType): | ||||||
|             return tuple(obj) |             return tuple(obj) | ||||||
|         if isinstance(obj, User): |         if isinstance(obj, User): | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
| import logging | import logging | ||||||
| import time | import time | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from certidude.api.tag import RelationalMixin | from certidude.relational import RelationalMixin | ||||||
|   |   | ||||||
| class LogHandler(logging.Handler, RelationalMixin): | class LogHandler(logging.Handler, RelationalMixin): | ||||||
|     SQL_CREATE_TABLES = "log_tables.sql" |     SQL_CREATE_TABLES = "log_tables.sql" | ||||||
|   | |||||||
| @@ -11,6 +11,8 @@ def publish(event_type, event_data): | |||||||
|     """ |     """ | ||||||
|     Publish event on nchan EventSource publisher |     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: |     if not config.EVENT_SOURCE_PUBLISH: | ||||||
|         # Push server disabled |         # Push server disabled | ||||||
|         return |         return | ||||||
|   | |||||||
| @@ -24,7 +24,7 @@ class RelationalMixin(object): | |||||||
|                 if self.uri.scheme == "sqlite": |                 if self.uri.scheme == "sqlite": | ||||||
|                     cur.executescript(fh.read()) |                     cur.executescript(fh.read()) | ||||||
|                 else: |                 else: | ||||||
|                     cur.execute(fh.read()) |                     cur.execute(fh.read(), multi=True) | ||||||
|             conn.commit() |             conn.commit() | ||||||
|             cur.close() |             cur.close() | ||||||
|             conn.close() |             conn.close() | ||||||
|   | |||||||
| @@ -1,9 +0,0 @@ | |||||||
| insert into tag ( |  | ||||||
|     `cn`, |  | ||||||
|     `key`, |  | ||||||
|     `value` |  | ||||||
| ) values ( |  | ||||||
|     %s, |  | ||||||
|     %s, |  | ||||||
|     %s |  | ||||||
| ) |  | ||||||
| @@ -1,3 +0,0 @@ | |||||||
| delete from tag |  | ||||||
| where id = ? |  | ||||||
| limit 1 |  | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| insert into tag ( |  | ||||||
|     `cn`, |  | ||||||
|     `key`, |  | ||||||
|     `value` |  | ||||||
| ) values ( |  | ||||||
|     ?, |  | ||||||
|     ?, |  | ||||||
|     ? |  | ||||||
| ); |  | ||||||
| @@ -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 |  | ||||||
|  |  | ||||||
| @@ -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`) |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| */ |  | ||||||
| @@ -1,4 +0,0 @@ | |||||||
| update `tag` |  | ||||||
| set `value` = ? |  | ||||||
| where `id` = ? |  | ||||||
| limit 1 |  | ||||||
| @@ -19,7 +19,6 @@ | |||||||
|           <li id="section-requests" data-section="requests" style="display:none;">Requests</li> |           <li id="section-requests" data-section="requests" style="display:none;">Requests</li> | ||||||
|           <li id="section-signed" data-section="signed" style="display:none;">Signed</li> |           <li id="section-signed" data-section="signed" style="display:none;">Signed</li> | ||||||
|           <li id="section-revoked" data-section="revoked" style="display:none;">Revoked</li> |           <li id="section-revoked" data-section="revoked" style="display:none;">Revoked</li> | ||||||
|           <li id="section-config" data-section="config" style="display:none;">Configuration</li> |  | ||||||
|           <li id="section-log" data-section="log" style="display:none;">Log</li> |           <li id="section-log" data-section="log" style="display:none;">Log</li> | ||||||
|         </ul> |         </ul> | ||||||
|     </nav> |     </nav> | ||||||
|   | |||||||
| @@ -1,45 +1,54 @@ | |||||||
| jQuery.timeago.settings.allowFuture = true; | jQuery.timeago.settings.allowFuture = true; | ||||||
|  |  | ||||||
| function onTagClicked() { | function normalizeCommonName(j) { | ||||||
|     var value = $(this).html(); |     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); |     var updated = prompt("Enter new tag or clear to remove the tag", value); | ||||||
|  |     $(event.target).addClass("busy"); | ||||||
|     if (updated == "") { |     if (updated == "") { | ||||||
|         $(this).addClass("busy"); |  | ||||||
|         $.ajax({ |         $.ajax({ | ||||||
|             method: "DELETE", |             method: "DELETE", | ||||||
|             url: "/api/tag/" + $(this).attr("data-id") |             url: "/api/signed/" + cn + "/tag/" + key + "/" | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|     } else if (updated && updated != value) { |     } else if (updated && updated != value) { | ||||||
|         $.ajax({ |         setTag(cn, key, updated, menu); | ||||||
|             method: "PUT", |  | ||||||
|             url: "/api/tag/" + $(this).attr("data-id"), |  | ||||||
|             dataType: "json", |  | ||||||
|             data: { |  | ||||||
|                 key: $(this).attr("data-key"), |  | ||||||
|                 value: updated |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| function onNewTagClicked() { | function onNewTagClicked(event) { | ||||||
|     var cn = $(event.target).attr("data-cn"); |     var menu = event.target; | ||||||
|     var key = $(event.target).val(); |     var cn = $(menu).attr("data-cn"); | ||||||
|     $(event.target).val(""); |     var key = $(menu).val(); | ||||||
|  |     $(menu).val(""); | ||||||
|     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; | ||||||
|     $.ajax({ |     $(menu).addClass("busy"); | ||||||
|         method: "POST", |     setTag(cn, key, value, event.target); | ||||||
|         url: "/api/tag/", |  | ||||||
|         dataType: "json", |  | ||||||
|         data: { |  | ||||||
|             cn: cn, |  | ||||||
|             value: value, |  | ||||||
|             key: key |  | ||||||
|         } |  | ||||||
|     }); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| function onTagFilterChanged() { | function onTagFilterChanged() { | ||||||
| @@ -68,47 +77,48 @@ function onRequestSubmitted(e) { | |||||||
|         url: "/api/request/" + e.data + "/", |         url: "/api/request/" + e.data + "/", | ||||||
|         dataType: "json", |         dataType: "json", | ||||||
|         success: function(request, status, xhr) { |         success: function(request, status, xhr) { | ||||||
|  |             console.info("Going to prepend:", request); | ||||||
|             onRequestDeleted(e); // Delete any existing ones just in case |             onRequestDeleted(e); // Delete any existing ones just in case | ||||||
|             $("#pending_requests").prepend( |             $("#pending_requests").prepend( | ||||||
|                 nunjucks.render('views/request.html', { request: request })); |                 nunjucks.render('views/request.html', { request: request })); | ||||||
|  |             $("#pending_requests time").timeago(); | ||||||
|  |         }, | ||||||
|  |         error: function(response) { | ||||||
|  |             console.info("Failed to retrieve certificate:", response); | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
| function onRequestDeleted(e) { | function onRequestDeleted(e) { | ||||||
|     console.log("Removing deleted request", e.data); |     console.log("Removing deleted request", e.data); | ||||||
|     $("#request-" + e.data.replace("@", "--").replace(".", "-")).remove(); |     $("#request-" + normalizeCommonName(e.data)).remove(); | ||||||
| } | } | ||||||
|  |  | ||||||
| function onClientUp(e) { | function onLeaseUpdate(e) { | ||||||
|     console.log("Adding security association:", e.data); |     console.log("Lease updated:", e.data); | ||||||
|     var lease = JSON.parse(e.data); |     $.ajax({ | ||||||
|     var $status = $("#signed_certificates [data-dn='" + lease.identity + "'] .status"); |         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', { |             $status.html(nunjucks.render('views/status.html', { | ||||||
|         lease: { |                 certificate: { | ||||||
|             address: lease.address, |                     lease: lease }})); | ||||||
|             identity: lease.identity, |             $("time", $status).timeago(); | ||||||
|             acquired: new Date(), |  | ||||||
|             released: null |  | ||||||
|         }})); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function onClientDown(e) { |         }, | ||||||
|     console.log("Removing security association:", e.data); |         error: function(response) { | ||||||
|     var lease = JSON.parse(e.data); |             console.info("Failed to retrieve certificate:", response); | ||||||
|     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() |  | ||||||
|         }})); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| function onRequestSigned(e) { | function onRequestSigned(e) { | ||||||
|     console.log("Request signed:", e.data); |     console.log("Request signed:", e.data); | ||||||
|     var slug = e.data.replace("@", "--").replace(".", "-"); |     var slug = normalizeCommonName(e.data); | ||||||
|     console.log("Removing:", slug); |     console.log("Removing:", slug); | ||||||
|  |  | ||||||
|     $("#request-" + slug).slideUp("normal", function() { $(this).remove(); }); |     $("#request-" + slug).slideUp("normal", function() { $(this).remove(); }); | ||||||
| @@ -122,6 +132,7 @@ function onRequestSigned(e) { | |||||||
|             console.info("Retrieved certificate:", certificate); |             console.info("Retrieved certificate:", certificate); | ||||||
|             $("#signed_certificates").prepend( |             $("#signed_certificates").prepend( | ||||||
|                 nunjucks.render('views/signed.html', { certificate: certificate })); |                 nunjucks.render('views/signed.html', { certificate: certificate })); | ||||||
|  |             $("#signed_certificates time").timeago(); // TODO: optimize? | ||||||
|         }, |         }, | ||||||
|         error: function(response) { |         error: function(response) { | ||||||
|             console.info("Failed to retrieve certificate:", response); |             console.info("Failed to retrieve certificate:", response); | ||||||
| @@ -129,42 +140,25 @@ function onRequestSigned(e) { | |||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| function onCertificateRevoked(e) { | function onCertificateRevoked(e) { | ||||||
|     console.log("Removing revoked certificate", e.data); |     console.log("Removing revoked certificate", e.data); | ||||||
|     $("#certificate-" + e.data.replace("@", "--").replace(".", "-")).slideUp("normal", function() { $(this).remove(); }); |     $("#certificate-" + normalizeCommonName(e.data)).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 = $("<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); |  | ||||||
|         } |  | ||||||
|     }) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function onTagRemoved(e) { |  | ||||||
|     console.log("Tag removed", e.data); |  | ||||||
|     $("#tag_" + e.data).remove(); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| function onTagUpdated(e) { | function onTagUpdated(e) { | ||||||
|     console.log("Tag updated", e.data); |     var cn = e.data; | ||||||
|  |     console.log("Tag updated", cn); | ||||||
|     $.ajax({ |     $.ajax({ | ||||||
|         method: "GET", |         method: "GET", | ||||||
|         url: "/api/tag/" + e.data + "/", |         url: "/api/signed/" + cn + "/tag/", | ||||||
|         dataType: "json", |         dataType: "json", | ||||||
|         success:function(tag, status, xhr) { |         success:function(tags, status, xhr) { | ||||||
|             console.info("Updated tag", tag); |             console.info("Updated", cn, "tags", tags); | ||||||
|             $("#tag_" + tag.id).html(tag.value); |             $(".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("log-entry", onLogEntry); | ||||||
|                 source.addEventListener("up-client", onClientUp); |                 source.addEventListener("lease-update", onLeaseUpdate); | ||||||
|                 source.addEventListener("down-client", onClientDown); |  | ||||||
|                 source.addEventListener("request-deleted", onRequestDeleted); |                 source.addEventListener("request-deleted", onRequestDeleted); | ||||||
|                 source.addEventListener("request-submitted", onRequestSubmitted); |                 source.addEventListener("request-submitted", onRequestSubmitted); | ||||||
|                 source.addEventListener("request-signed", onRequestSigned); |                 source.addEventListener("request-signed", onRequestSigned); | ||||||
|                 source.addEventListener("certificate-revoked", onCertificateRevoked); |                 source.addEventListener("certificate-revoked", onCertificateRevoked); | ||||||
|                 source.addEventListener("tag-added", onTagAdded); |                 source.addEventListener("tag-update", onTagUpdated); | ||||||
|                 source.addEventListener("tag-removed", onTagRemoved); |  | ||||||
|                 source.addEventListener("tag-updated", onTagUpdated); |  | ||||||
|  |  | ||||||
|                 console.info("Swtiching to requests section"); |                 console.info("Swtiching to requests section"); | ||||||
|                 $("section").hide(); |                 $("section").hide(); | ||||||
| @@ -258,41 +249,6 @@ $(document).ready(function() { | |||||||
|             }); |             }); | ||||||
|  |  | ||||||
|             console.log("Features enabled:", session.features); |             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 = $("<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>"); |  | ||||||
|                                 } |  | ||||||
|                             } |  | ||||||
|                         }); |  | ||||||
|                     } |  | ||||||
|                 }); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|             if (session.request_submission_allowed) { |             if (session.request_submission_allowed) { | ||||||
|                 $("#request_submit").click(function() { |                 $("#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 |              * Fetch log entries | ||||||
|              */ |              */ | ||||||
|   | |||||||
| @@ -46,12 +46,17 @@ | |||||||
|  |  | ||||||
|     {% if session.features.tagging %} |     {% if session.features.tagging %} | ||||||
|     <div class="tags"> |     <div class="tags"> | ||||||
|         <select class="icon tag" data-cn="{{ certificate.common_name }}" onChange="onNewTagClicked();"> |         <span data-cn="{{ certificate.common_name }}"> | ||||||
|  |             {% include 'views/tags.html' %} | ||||||
|  |         </span> | ||||||
|  |         <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' %} |                 {% include 'views/tagtypes.html' %} | ||||||
|         </select> |         </select> | ||||||
|     </div> |     </div> | ||||||
|     {% endif %} |     {% endif %} | ||||||
|  |  | ||||||
|     <div class="status"></div> |     <div class="status"> | ||||||
|  |     {% include 'views/status.html' %} | ||||||
|  |     </div> | ||||||
| </li> | </li> | ||||||
|   | |||||||
| @@ -1,16 +1,14 @@ | |||||||
| <svg height="32" width="32"> |  | ||||||
|     <circle cx="16" cy="16" r="13" stroke="black" stroke-width="3" fill="{% if lease %}{% if lease.released %}{% if lease.age > 1209600 %}#D6083B{% else %}#0072CF{% endif %}{%else %}#55A51C{% endif %}{% else %}#F3BD48{% endif %}" /> |  | ||||||
| </svg> |  | ||||||
|  |  | ||||||
| <span> | <span> | ||||||
|  |   {% if certificate.lease %} | ||||||
| {% if lease %} |     <svg height="32" width="32"> | ||||||
| {% if lease.released %} |       <circle cx="16" cy="16" r="13" stroke="black" stroke-width="3" fill="{% if certificate.lease %}{% if certificate.lease.age > session.authority.lease.offline %}#0072CF{% elif certificate.lease.age > session.authority.lease.dead %}#D6083B{%else %}#55A51C{% endif %}{% endif %}"/> | ||||||
| Last seen {{ lease.released }} at {{ lease.address }} |     </svg> | ||||||
|  |     {% if certificate.lease.age > session.authority.lease.offline %} | ||||||
|  |       Last seen <time class="timeago" datetime="{{ certificate.lease.last_seen }}">{{ certificate.lease.last_seen }}</time> | ||||||
|  |       at {{ certificate.lease.address }} | ||||||
|     {% else %} |     {% else %} | ||||||
| Online since {{ lease.acquired }} at <a target="{{ lease.address }}" href="http://{{ lease.address }}">{{ lease.address }}</a> |       Online since <time class="timeago" datetime="{{ certificate.lease.last_seen }}">{{ certificate.lease.last_seen }}</time> at | ||||||
|  |       <a target="{{ certificate.lease.address }}" href="http://{{ certificate.lease.address }}">{{ certificate.lease.address }}</a> | ||||||
|     {% endif %} |     {% endif %} | ||||||
| {% else %} |  | ||||||
| Not seen |  | ||||||
|   {% endif %} |   {% endif %} | ||||||
| </span> | </span> | ||||||
|   | |||||||
| Before Width: | Height: | Size: 561 B After Width: | Height: | Size: 913 B | 
| @@ -1,3 +1,6 @@ | |||||||
| <span id="tag_{{ tag.id }}" onclick="onTagClicked()" | {% for key, value in certificate.tags %} | ||||||
| title="{{ tag.key }}={{ tag.value }}" class="{{ tag.key | replace('.', ' ') }}" | <span onclick="onTagClicked(event);" | ||||||
| data-id="{{ tag.id }}" data-key="{{ tag.key }}">{{ tag.value }}</span> | title="{{ key }}={{ value }}" class="tag icon {{ key | replace('.', ' ') }}" | ||||||
|  | data-cn="{{ certificate.common_name }}" | ||||||
|  | data-key="{{ key }}">{{ value }}</span> | ||||||
|  | {% endfor %} | ||||||
|   | |||||||
| @@ -57,26 +57,6 @@ backend = | |||||||
| ;backend = sql | ;backend = sql | ||||||
| database = sqlite://{{ directory }}/db.sqlite | 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] | [signature] | ||||||
| # Server certificate is granted to certificate with | # Server certificate is granted to certificate with | ||||||
| # common name that includes period which translates to FQDN of the machine. | # common name that includes period which translates to FQDN of the machine. | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user