mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-22 08:15:18 +00:00
Move leases and tagging backend to filesystem extended attributes
This commit is contained in:
parent
79aa1e18c0
commit
1813056fc7
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user