mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-23 00:25:18 +00:00
api: Preliminary API call for listing client leases
This commit is contained in:
parent
3d36b2f92c
commit
887743cc0b
@ -1,4 +1,5 @@
|
|||||||
import re
|
import re
|
||||||
|
import datetime
|
||||||
import falcon
|
import falcon
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import mimetypes
|
import mimetypes
|
||||||
@ -201,9 +202,68 @@ class SignedCertificateDetailResource(CertificateAuthorityBase):
|
|||||||
def on_delete(self, req, resp, ca, cn, user):
|
def on_delete(self, req, resp, ca, cn, user):
|
||||||
ca.revoke(cn)
|
ca.revoke(cn)
|
||||||
|
|
||||||
|
class LeaseResource(CertificateAuthorityBase):
|
||||||
|
@serialize
|
||||||
|
@login_required
|
||||||
|
@pop_certificate_authority
|
||||||
|
@authorize_admin
|
||||||
|
def on_get(self, req, resp, ca, user):
|
||||||
|
from ipaddress import ip_address
|
||||||
|
|
||||||
|
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?
|
||||||
|
for chunk in chunks:
|
||||||
|
for chunkette in chunk:
|
||||||
|
key, value = chunkette
|
||||||
|
dn += "/" + OIDS[key] + "=" + value
|
||||||
|
return str(dn)
|
||||||
|
|
||||||
|
# BUGBUG
|
||||||
|
SQL_LEASES = """
|
||||||
|
SELECT
|
||||||
|
acquired,
|
||||||
|
released,
|
||||||
|
address,
|
||||||
|
identities.data as dn
|
||||||
|
FROM
|
||||||
|
addresses
|
||||||
|
RIGHT JOIN
|
||||||
|
identities
|
||||||
|
ON
|
||||||
|
identities.id = addresses.identity
|
||||||
|
WHERE
|
||||||
|
addresses.released <> 1
|
||||||
|
"""
|
||||||
|
cnx = ca.database.get_connection()
|
||||||
|
cursor = cnx.cursor(dictionary=True)
|
||||||
|
query = (SQL_LEASES)
|
||||||
|
cursor.execute(query)
|
||||||
|
|
||||||
|
for row in cursor:
|
||||||
|
row["acquired"] = datetime.utcfromtimestamp(row["acquired"])
|
||||||
|
row["released"] = datetime.utcfromtimestamp(row["released"]) if row["released"] else None
|
||||||
|
row["address"] = ip_address(bytes(row["address"]))
|
||||||
|
row["dn"] = parse_dn(bytes(row["dn"]))
|
||||||
|
yield row
|
||||||
|
|
||||||
|
|
||||||
class SignedCertificateListResource(CertificateAuthorityBase):
|
class SignedCertificateListResource(CertificateAuthorityBase):
|
||||||
@serialize
|
@serialize
|
||||||
@pop_certificate_authority
|
@pop_certificate_authority
|
||||||
|
@authorize_admin
|
||||||
@validate_common_name
|
@validate_common_name
|
||||||
def on_get(self, req, resp, ca):
|
def on_get(self, req, resp, ca):
|
||||||
for j in authority.get_signed():
|
for j in authority.get_signed():
|
||||||
@ -258,6 +318,7 @@ class RequestDetailResource(CertificateAuthorityBase):
|
|||||||
class RequestListResource(CertificateAuthorityBase):
|
class RequestListResource(CertificateAuthorityBase):
|
||||||
@serialize
|
@serialize
|
||||||
@pop_certificate_authority
|
@pop_certificate_authority
|
||||||
|
@authorize_admin
|
||||||
def on_get(self, req, resp, ca):
|
def on_get(self, req, resp, ca):
|
||||||
for j in ca.get_requests():
|
for j in ca.get_requests():
|
||||||
yield omit(
|
yield omit(
|
||||||
@ -458,6 +519,7 @@ def certidude_app():
|
|||||||
app.add_route("/api/ca/{ca}/signed/", SignedCertificateListResource(config))
|
app.add_route("/api/ca/{ca}/signed/", SignedCertificateListResource(config))
|
||||||
app.add_route("/api/ca/{ca}/request/{cn}/", RequestDetailResource(config))
|
app.add_route("/api/ca/{ca}/request/{cn}/", RequestDetailResource(config))
|
||||||
app.add_route("/api/ca/{ca}/request/", RequestListResource(config))
|
app.add_route("/api/ca/{ca}/request/", RequestListResource(config))
|
||||||
|
app.add_route("/api/ca/{ca}/lease/", LeaseResource(config))
|
||||||
app.add_route("/api/ca/{ca}/", IndexResource(config))
|
app.add_route("/api/ca/{ca}/", IndexResource(config))
|
||||||
app.add_route("/api/ca/", AuthorityListResource(config))
|
app.add_route("/api/ca/", AuthorityListResource(config))
|
||||||
return app
|
return app
|
||||||
|
@ -0,0 +1,72 @@
|
|||||||
|
|
||||||
|
import click
|
||||||
|
import falcon
|
||||||
|
import kerberos
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import socket
|
||||||
|
|
||||||
|
# Vanilla Kerberos provides only username.
|
||||||
|
# AD also embeds PAC (Privilege Attribute Certificate), which
|
||||||
|
# is supposed to be sent via HTTP headers and it contains
|
||||||
|
# the groups user is part of.
|
||||||
|
# Even then we would have to manually look up the e-mail
|
||||||
|
# address eg via LDAP, hence to keep things simple
|
||||||
|
# we simply use Kerberos to authenticate.
|
||||||
|
|
||||||
|
FQDN = socket.getaddrinfo(socket.gethostname(), 0, flags=socket.AI_CANONNAME)[0][3]
|
||||||
|
|
||||||
|
if not os.getenv("KRB5_KTNAME"):
|
||||||
|
click.echo("Kerberos keytab not specified, set environment variable 'KRB5_KTNAME'", err=True)
|
||||||
|
exit(250)
|
||||||
|
|
||||||
|
try:
|
||||||
|
principal = kerberos.getServerPrincipalDetails("HTTP", FQDN)
|
||||||
|
except kerberos.KrbError as exc:
|
||||||
|
click.echo("Failed to initialize Kerberos, reason: %s" % exc, err=True)
|
||||||
|
exit(249)
|
||||||
|
else:
|
||||||
|
click.echo("Kerberos enabled, service principal is HTTP/%s" % FQDN)
|
||||||
|
|
||||||
|
def login_required(func):
|
||||||
|
def wrapped(resource, req, resp, *args, **kwargs):
|
||||||
|
authorization = req.get_header("Authorization")
|
||||||
|
|
||||||
|
if not authorization:
|
||||||
|
resp.append_header("WWW-Authenticate", "Negotiate")
|
||||||
|
raise falcon.HTTPUnauthorized("Unauthorized", "No Kerberos ticket offered?")
|
||||||
|
|
||||||
|
token = ''.join(authorization.split()[1:])
|
||||||
|
|
||||||
|
try:
|
||||||
|
result, context = kerberos.authGSSServerInit("HTTP@" + FQDN)
|
||||||
|
except kerberos.GSSError as ex:
|
||||||
|
raise falcon.HTTPForbidden("Forbidden", "Authentication System Failure: %s(%s)" % (ex[0][0], ex[1][0],))
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = kerberos.authGSSServerStep(context, token)
|
||||||
|
except kerberos.GSSError as ex:
|
||||||
|
kerberos.authGSSServerClean(context)
|
||||||
|
raise falcon.HTTPForbidden("Forbidden", "Bad credentials: %s(%s)" % (ex[0][0], ex[1][0],))
|
||||||
|
except kerberos.KrbError as ex:
|
||||||
|
kerberos.authGSSServerClean(context)
|
||||||
|
raise falcon.HTTPForbidden("Forbidden", "Bad credentials: %s" % (ex[0],))
|
||||||
|
|
||||||
|
kerberos_user = kerberos.authGSSServerUserName(context).split("@")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# BUGBUG: https://github.com/02strich/pykerberos/issues/6
|
||||||
|
#kerberos.authGSSServerClean(context)
|
||||||
|
pass
|
||||||
|
except kerberos.GSSError as ex:
|
||||||
|
raise error.LoginFailed('Authentication System Failure %s(%s)' % (ex[0][0], ex[1][0],))
|
||||||
|
|
||||||
|
if result == kerberos.AUTH_GSS_COMPLETE:
|
||||||
|
kwargs["user"] = kerberos_user
|
||||||
|
return func(resource, req, resp, *args, **kwargs)
|
||||||
|
elif result == kerberos.AUTH_GSS_CONTINUE:
|
||||||
|
raise falcon.HTTPUnauthorized("Unauthorized", "Tried GSSAPI")
|
||||||
|
else:
|
||||||
|
raise falcon.HTTPForbidden("Forbidden", "Tried GSSAPI")
|
||||||
|
|
||||||
|
return wrapped
|
@ -21,9 +21,9 @@
|
|||||||
|
|
||||||
<h1>Signed certificates</h1>
|
<h1>Signed certificates</h1>
|
||||||
|
|
||||||
<ul>
|
<ul id="signed_certificates">
|
||||||
{% for j in authority.signed | sort | reverse %}
|
{% for j in authority.signed | sort | reverse %}
|
||||||
<li id="certificate_{{ j.sha256sum }}">
|
<li id="certificate_{{ j.sha256sum }}" data-dn="{{ j.subject }}">
|
||||||
<a class="button" href="/api/{{authority.slug}}/signed/{{j.subject}}/">Fetch</a>
|
<a class="button" href="/api/{{authority.slug}}/signed/{{j.subject}}/">Fetch</a>
|
||||||
<button onClick="javascript:$.ajax({url:'/api/{{authority.slug}}/signed/{{j.subject.CN}}/',type:'delete'});">Revoke</button>
|
<button onClick="javascript:$.ajax({url:'/api/{{authority.slug}}/signed/{{j.subject.CN}}/',type:'delete'});">Revoke</button>
|
||||||
|
|
||||||
@ -51,6 +51,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="status">
|
||||||
|
{% include 'status.html' %}
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -49,6 +49,29 @@ $(document).ready(function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#container").html(nunjucks.render('authority.html', { authority: authority, session: session }));
|
$("#container").html(nunjucks.render('authority.html', { authority: authority, session: session }));
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
method: "GET",
|
||||||
|
url: "/api/ca/" + authority.slug + "/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].dn + "'] .status");
|
||||||
|
if (!$status.length) {
|
||||||
|
console.info("Detected rogue client:", leases[j]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$status.html(nunjucks.render('status.html', {
|
||||||
|
lease: {
|
||||||
|
address: leases[j].address,
|
||||||
|
dn: leases[j].dn,
|
||||||
|
acquired: new Date(leases[j].acquired).toLocaleString(),
|
||||||
|
released: leases[j].released ? new Date(leases[j].released).toLocaleString() : null
|
||||||
|
}}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
16
certidude/static/status.html
Normal file
16
certidude/static/status.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<svg height="32" width="32">
|
||||||
|
<circle cx="16" cy="16" r="13" stroke="black" stroke-width="3" fill="{% if lease %}{% if lease.released %}green{%else %}red{% endif %}{% else %}yellow{% endif %}" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
|
||||||
|
{% if lease %}
|
||||||
|
{% if lease.released %}
|
||||||
|
Last seen {{ lease.released }} at {{ lease.address }}
|
||||||
|
{% else %}
|
||||||
|
Online since {{ lease.acquired }} at {{ lease.address }}
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
Not seen
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
After Width: | Height: | Size: 428 B |
@ -88,7 +88,7 @@ class CertificateAuthorityConfig(object):
|
|||||||
section = "CA_" + slug
|
section = "CA_" + slug
|
||||||
|
|
||||||
dirs = dict([(key, self.get(section, key))
|
dirs = dict([(key, self.get(section, key))
|
||||||
for key in ("dir", "certificate", "crl", "certs", "new_certs_dir", "private_key", "revoked_certs_dir", "request_subnets", "autosign_subnets", "admin_subnets", "admin_users", "push_server", "inbox", "outbox")])
|
for key in ("dir", "certificate", "crl", "certs", "new_certs_dir", "private_key", "revoked_certs_dir", "request_subnets", "autosign_subnets", "admin_subnets", "admin_users", "push_server", "database", "inbox", "outbox")])
|
||||||
|
|
||||||
# Variable expansion, eg $dir
|
# Variable expansion, eg $dir
|
||||||
for key, value in dirs.items():
|
for key, value in dirs.items():
|
||||||
@ -433,7 +433,7 @@ class Certificate(CertificateBase):
|
|||||||
return self.signed <= other.signed
|
return self.signed <= other.signed
|
||||||
|
|
||||||
class CertificateAuthority(object):
|
class CertificateAuthority(object):
|
||||||
def __init__(self, slug, certificate, crl, certs, new_certs_dir, revoked_certs_dir=None, private_key=None, autosign_subnets=None, request_subnets=None, admin_subnets=None, admin_users=None, email_address=None, inbox=None, outbox=None, basic_constraints="CA:FALSE", key_usage="digitalSignature,keyEncipherment", extended_key_usage="clientAuth", certificate_lifetime=5*365, revocation_list_lifetime=1, push_server=None):
|
def __init__(self, slug, certificate, crl, certs, new_certs_dir, revoked_certs_dir=None, private_key=None, autosign_subnets=None, request_subnets=None, admin_subnets=None, admin_users=None, email_address=None, inbox=None, outbox=None, basic_constraints="CA:FALSE", key_usage="digitalSignature,keyEncipherment", extended_key_usage="clientAuth", certificate_lifetime=5*365, revocation_list_lifetime=1, push_server=None, database=None):
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
m = hashlib.sha512()
|
m = hashlib.sha512()
|
||||||
@ -455,6 +455,8 @@ class CertificateAuthority(object):
|
|||||||
self.certificate = Certificate(open(certificate))
|
self.certificate = Certificate(open(certificate))
|
||||||
self.mailer = Mailer(outbox) if outbox else None
|
self.mailer = Mailer(outbox) if outbox else None
|
||||||
self.push_server = push_server
|
self.push_server = push_server
|
||||||
|
self.database_url = database
|
||||||
|
self._database_pool = None
|
||||||
|
|
||||||
self.certificate_lifetime = certificate_lifetime
|
self.certificate_lifetime = certificate_lifetime
|
||||||
self.revocation_list_lifetime = revocation_list_lifetime
|
self.revocation_list_lifetime = revocation_list_lifetime
|
||||||
@ -474,6 +476,24 @@ class CertificateAuthority(object):
|
|||||||
else:
|
else:
|
||||||
self.admin_users = set([j for j in admin_users.split(" ") if j])
|
self.admin_users = set([j for j in admin_users.split(" ") if j])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def database(self):
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
if not self._database_pool:
|
||||||
|
o = urlparse(self.database_url)
|
||||||
|
if o.scheme == "mysql":
|
||||||
|
import mysql.connector
|
||||||
|
self._database_pool = mysql.connector.pooling.MySQLConnectionPool(
|
||||||
|
pool_size = 3,
|
||||||
|
user=o.username,
|
||||||
|
password=o.password,
|
||||||
|
host=o.hostname,
|
||||||
|
database=o.path[1:])
|
||||||
|
else:
|
||||||
|
raise NotImplementedError("Unsupported database scheme %s, currently only mysql://user:pass@host/database is supported" % o.scheme)
|
||||||
|
|
||||||
|
return self._database_pool
|
||||||
|
|
||||||
def event_publish(self, event_type, event_data):
|
def event_publish(self, event_type, event_data):
|
||||||
"""
|
"""
|
||||||
Publish event on push server
|
Publish event on push server
|
||||||
|
Loading…
Reference in New Issue
Block a user