mirror of
				https://github.com/laurivosandi/certidude
				synced 2025-10-31 01:19:11 +00:00 
			
		
		
		
	api: Preliminary API call for listing client leases
This commit is contained in:
		| @@ -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 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user