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