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 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/lease/", LeaseResource())
|
app.add_route("/api/signed/{cn}/tag/", TagResource())
|
||||||
app.add_route("/api/whois/", WhoisResource())
|
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())
|
||||||
|
|
||||||
# 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",
|
||||||
$status.html(nunjucks.render('views/status.html', {
|
url: "/api/signed/" + e.data + "/lease/",
|
||||||
lease: {
|
dataType: "json",
|
||||||
address: lease.address,
|
success: function(lease, status, xhr) {
|
||||||
identity: lease.identity,
|
console.info("Retrieved lease update details:", lease);
|
||||||
acquired: new Date(),
|
lease.age = (new Date() - new Date(lease.last_seen)) / 1000.0
|
||||||
released: null
|
var $status = $("#signed_certificates [data-cn='" + e.data + "'] .status");
|
||||||
}}));
|
$status.html(nunjucks.render('views/status.html', {
|
||||||
}
|
certificate: {
|
||||||
|
lease: lease }}));
|
||||||
|
$("time", $status).timeago();
|
||||||
|
|
||||||
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 }}">
|
||||||
<option value="">Add tag...</option>
|
{% include 'views/tags.html' %}
|
||||||
{% include 'views/tagtypes.html' %}
|
</span>
|
||||||
|
<select class="icon tag" data-cn="{{ certificate.common_name }}" onChange="onNewTagClicked(event);">
|
||||||
|
<option value="">Add tag...</option>
|
||||||
|
{% 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>
|
||||||
{% else %}
|
{% if certificate.lease.age > session.authority.lease.offline %}
|
||||||
Online since {{ lease.acquired }} at <a target="{{ lease.address }}" href="http://{{ lease.address }}">{{ lease.address }}</a>
|
Last seen <time class="timeago" datetime="{{ certificate.lease.last_seen }}">{{ certificate.lease.last_seen }}</time>
|
||||||
{% endif %}
|
at {{ certificate.lease.address }}
|
||||||
{% else %}
|
{% else %}
|
||||||
Not seen
|
Online since <time class="timeago" datetime="{{ certificate.lease.last_seen }}">{{ certificate.lease.last_seen }}</time> at
|
||||||
{% endif %}
|
<a target="{{ certificate.lease.address }}" href="http://{{ certificate.lease.address }}">{{ certificate.lease.address }}</a>
|
||||||
|
{% 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.
|
||||||
|
Loading…
Reference in New Issue
Block a user