mirror of
				https://github.com/laurivosandi/certidude
				synced 2025-10-31 01:19:11 +00:00 
			
		
		
		
	Complete overhaul
* Switch to Python 2.x due to lack of decent LDAP support in Python 3.x * Add LDAP backend for authentication/authorization * Add PAM backend for authentication * Add getent backend for authorization * Add preliminary CSRF protection * Update icons * Update push server documentation, use nchan from now on * Add P12 bundle generation * Add thin wrapper around Python's SQL connectors * Enable mailing subsystem * Add Kerberos TGT renewal cronjob * Add HTTPS server setup commands for nginx
This commit is contained in:
		| @@ -1,13 +1,19 @@ | ||||
| # encoding: utf-8 | ||||
|  | ||||
| import falcon | ||||
| import mimetypes | ||||
| import logging | ||||
| import os | ||||
| import click | ||||
| from datetime import datetime | ||||
| from time import sleep | ||||
| from certidude import authority | ||||
| from certidude import authority, mailer | ||||
| from certidude.auth import login_required, authorize_admin | ||||
| from certidude.decorators import serialize, event_source | ||||
| from certidude.decorators import serialize, event_source, csrf_protection | ||||
| from certidude.wrappers import Request, Certificate | ||||
| from certidude import config | ||||
| from certidude import constants, config | ||||
|  | ||||
| logger = logging.getLogger("api") | ||||
|  | ||||
| class CertificateStatusResource(object): | ||||
|     """ | ||||
| @@ -24,7 +30,9 @@ class CertificateStatusResource(object): | ||||
|  | ||||
| class CertificateAuthorityResource(object): | ||||
|     def on_get(self, req, resp): | ||||
|         logger.info("Served CA certificate to %s", req.context.get("remote_addr")) | ||||
|         resp.stream = open(config.AUTHORITY_CERTIFICATE_PATH, "rb") | ||||
|         resp.append_header("Content-Type", "application/x-x509-ca-cert") | ||||
|         resp.append_header("Content-Disposition", "attachment; filename=ca.crt") | ||||
|  | ||||
|  | ||||
| @@ -34,16 +42,54 @@ class SessionResource(object): | ||||
|     @authorize_admin | ||||
|     @event_source | ||||
|     def on_get(self, req, resp): | ||||
|         if config.ACCOUNTS_BACKEND == "ldap": | ||||
|             import ldap | ||||
|             ft = config.LDAP_MEMBERS_FILTER % (config.ADMINS_GROUP, "*") | ||||
|             r = req.context.get("ldap_conn").search_s(config.LDAP_BASE, | ||||
|                     ldap.SCOPE_SUBTREE, ft.encode("utf-8"), ["cn", "member"]) | ||||
|  | ||||
|             for dn,entry in r: | ||||
|                 cn, = entry.get("cn") | ||||
|                 break | ||||
|             else: | ||||
|                 raise ValueError("Failed to look up group %s in LDAP" % repr(group_name)) | ||||
|  | ||||
|             admins = dict([(j, j.split(",")[0].split("=")[1]) for j in entry.get("member")]) | ||||
|         elif config.ACCOUNTS_BACKEND == "posix": | ||||
|             import grp | ||||
|             _, _, gid, members = grp.getgrnam(config.ADMINS_GROUP) | ||||
|             admins = dict([(j, j) for j in members]) | ||||
|         else: | ||||
|             raise NotImplementedError("Authorization backend %s not supported" % config.AUTHORIZATION_BACKEND) | ||||
|  | ||||
|         return dict( | ||||
|             username=req.context.get("user"), | ||||
|             event_channel = config.PUSH_EVENT_SOURCE % config.PUSH_TOKEN, | ||||
|             user = dict( | ||||
|                 name=req.context.get("user").name, | ||||
|                 gn=req.context.get("user").given_name, | ||||
|                 sn=req.context.get("user").surname, | ||||
|                 mail=req.context.get("user").mail | ||||
|             ), | ||||
|             request_submission_allowed = sum( # Dirty hack! | ||||
|                 [req.context.get("remote_addr") in j | ||||
|                     for j in config.REQUEST_SUBNETS]), | ||||
|             user_subnets = config.USER_SUBNETS, | ||||
|             autosign_subnets = config.AUTOSIGN_SUBNETS, | ||||
|             request_subnets = config.REQUEST_SUBNETS, | ||||
|             admin_subnets=config.ADMIN_SUBNETS, | ||||
|             admin_users=config.ADMIN_USERS, | ||||
|             requests=authority.list_requests(), | ||||
|             signed=authority.list_signed(), | ||||
|             revoked=authority.list_revoked()) | ||||
|             admin_users = admins, | ||||
|             #admin_users=config.ADMIN_USERS, | ||||
|             authority = dict( | ||||
|                 outbox = config.OUTBOX, | ||||
|                 certificate = authority.certificate, | ||||
|                 events = config.PUSH_EVENT_SOURCE % config.PUSH_TOKEN, | ||||
|                 requests=authority.list_requests(), | ||||
|                 signed=authority.list_signed(), | ||||
|                 revoked=authority.list_revoked(), | ||||
|             ) if config.ADMINS_GROUP in req.context.get("groups") else None, | ||||
|             features=dict( | ||||
|                 tagging=config.TAGGING_BACKEND, | ||||
|                 leases=False, #config.LEASES_BACKEND, | ||||
|                 logging=config.LOGGING_BACKEND)) | ||||
|  | ||||
|  | ||||
| class StaticResource(object): | ||||
| @@ -58,7 +104,7 @@ class StaticResource(object): | ||||
|  | ||||
|         if os.path.isdir(path): | ||||
|             path = os.path.join(path, "index.html") | ||||
|         print("Serving:", path) | ||||
|         click.echo("Serving: %s" % path) | ||||
|  | ||||
|         if os.path.exists(path): | ||||
|             content_type, content_encoding = mimetypes.guess_type(path) | ||||
| @@ -72,7 +118,33 @@ class StaticResource(object): | ||||
|             resp.body = "File '%s' not found" % req.path | ||||
|  | ||||
|  | ||||
| class BundleResource(object): | ||||
|     @login_required | ||||
|     def on_get(self, req, resp): | ||||
|         common_name = req.context["user"].mail | ||||
|         logger.info("Signing bundle %s for %s", common_name, req.context.get("user")) | ||||
|         resp.set_header("Content-Type", "application/x-pkcs12") | ||||
|         resp.set_header("Content-Disposition", "attachment; filename=%s.p12" % common_name) | ||||
|         resp.body, cert = authority.generate_pkcs12_bundle(common_name, | ||||
|                                 owner=req.context.get("user")) | ||||
|  | ||||
|  | ||||
| import ipaddress | ||||
|  | ||||
| class NormalizeMiddleware(object): | ||||
|     @csrf_protection | ||||
|     def process_request(self, req, resp, *args): | ||||
|         assert not req.get_param("unicode") or req.get_param("unicode") == u"✓", "Unicode sanity check failed" | ||||
|         req.context["remote_addr"] = ipaddress.ip_address(req.env["REMOTE_ADDR"].decode("utf-8")) | ||||
|  | ||||
|     def process_response(self, req, resp, resource): | ||||
|         # wtf falcon?! | ||||
|         if isinstance(resp.location, unicode): | ||||
|             resp.location = resp.location.encode("ascii") | ||||
|  | ||||
| def certidude_app(): | ||||
|     from certidude import config | ||||
|  | ||||
|     from .revoked import RevocationListResource | ||||
|     from .signed import SignedCertificateListResource, SignedCertificateDetailResource | ||||
|     from .request import RequestListResource, RequestDetailResource | ||||
| @@ -82,60 +154,56 @@ def certidude_app(): | ||||
|     from .tag import TagResource, TagDetailResource | ||||
|     from .cfg import ConfigResource, ScriptResource | ||||
|  | ||||
|     app = falcon.API() | ||||
|     app = falcon.API(middleware=NormalizeMiddleware()) | ||||
|  | ||||
|     # Certificate authority API calls | ||||
|     app.add_route("/api/ocsp/", CertificateStatusResource()) | ||||
|     app.add_route("/api/bundle/", BundleResource()) | ||||
|     app.add_route("/api/certificate/", CertificateAuthorityResource()) | ||||
|     app.add_route("/api/revoked/", RevocationListResource()) | ||||
|     app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource()) | ||||
|     app.add_route("/api/signed/", SignedCertificateListResource()) | ||||
|     app.add_route("/api/request/{cn}/", RequestDetailResource()) | ||||
|     app.add_route("/api/request/", RequestListResource()) | ||||
|     app.add_route("/api/log/", LogResource()) | ||||
|     app.add_route("/api/tag/", TagResource()) | ||||
|     app.add_route("/api/tag/{identifier}/", TagDetailResource()) | ||||
|     app.add_route("/api/config/", ConfigResource()) | ||||
|     app.add_route("/api/script/", ScriptResource()) | ||||
|     app.add_route("/api/", SessionResource()) | ||||
|  | ||||
|     # Gateway API calls, should this be moved to separate project? | ||||
|     app.add_route("/api/lease/", LeaseResource()) | ||||
|     app.add_route("/api/whois/", WhoisResource()) | ||||
|  | ||||
|     """ | ||||
|     Set up logging | ||||
|     """ | ||||
|     log_handlers = [] | ||||
|     if config.LOGGING_BACKEND == "sql": | ||||
|         from certidude.mysqllog import LogHandler | ||||
|         uri = config.cp.get("logging", "database") | ||||
|         log_handlers.append(LogHandler(uri)) | ||||
|         app.add_route("/api/log/", LogResource(uri)) | ||||
|     elif config.LOGGING_BACKEND == "syslog": | ||||
|         from logging.handlers import SyslogHandler | ||||
|         log_handlers.append(SysLogHandler()) | ||||
|         # Browsing syslog via HTTP is obviously not possible out of the box | ||||
|     elif config.LOGGING_BACKEND: | ||||
|         raise ValueError("Invalid logging.backend = %s" % config.LOGGING_BACKEND) | ||||
|  | ||||
|     from certidude import config | ||||
|     from certidude.mysqllog import MySQLLogHandler | ||||
|     from datetime import datetime | ||||
|     import logging | ||||
|     import socket | ||||
|     import json | ||||
|     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) | ||||
|  | ||||
|  | ||||
|     class PushLogHandler(logging.Handler): | ||||
|         def emit(self, record): | ||||
|             from certidude.push import publish | ||||
|             publish("log-entry", dict( | ||||
|                 created = datetime.fromtimestamp(record.created), | ||||
|                 message = record.msg % record.args, | ||||
|                 severity = record.levelname.lower())) | ||||
|  | ||||
|     if config.DATABASE_POOL: | ||||
|         sql_handler = MySQLLogHandler(config.DATABASE_POOL) | ||||
|     push_handler = PushLogHandler() | ||||
|     if config.PUSH_PUBLISH: | ||||
|         from certidude.push import PushLogHandler | ||||
|         log_handlers.append(PushLogHandler()) | ||||
|  | ||||
|     for facility in "api", "cli": | ||||
|         logger = logging.getLogger(facility) | ||||
|         logger.setLevel(logging.DEBUG) | ||||
|         if config.DATABASE_POOL: | ||||
|             logger.addHandler(sql_handler) | ||||
|         logger.addHandler(push_handler) | ||||
|         for handler in log_handlers: | ||||
|             logger.addHandler(handler) | ||||
|  | ||||
|  | ||||
|     logging.getLogger("cli").debug("Started Certidude at %s", config.FQDN) | ||||
|     logging.getLogger("cli").debug("Started Certidude at %s", constants.FQDN) | ||||
|  | ||||
|     import atexit | ||||
|  | ||||
|   | ||||
| @@ -6,6 +6,7 @@ from random import choice | ||||
| from certidude import config | ||||
| from certidude.auth import login_required, authorize_admin | ||||
| from certidude.decorators import serialize | ||||
| from certidude.relational import RelationalMixin | ||||
| from jinja2 import Environment, FileSystemLoader | ||||
|  | ||||
| logger = logging.getLogger("api") | ||||
| @@ -39,43 +40,42 @@ where | ||||
|     device.cn = %s | ||||
| """ | ||||
|  | ||||
| SQL_SELECT_INHERITANCE = """ | ||||
|  | ||||
| SQL_SELECT_RULES = """ | ||||
| select | ||||
|     tag_inheritance.`id` as `id`, | ||||
|     tag.id as `tag_id`, | ||||
|     tag.`key` as `match_key`, | ||||
|     tag.`value` as `match_value`, | ||||
|     tag_inheritance.`key` as `key`, | ||||
|     tag_inheritance.`value` as `value` | ||||
| from tag_inheritance | ||||
| join tag on tag.id = tag_inheritance.tag_id | ||||
|     tag.cn as `cn`, | ||||
|     tag.key as `tag_key`, | ||||
|     tag.value as `tag_value`, | ||||
|     tag_properties.property_key as `property_key`, | ||||
|     tag_properties.property_value as `property_value` | ||||
| from | ||||
|     tag_properties | ||||
| join | ||||
|     tag | ||||
| on | ||||
|     tag.key = tag_properties.tag_key and | ||||
|     tag.value = tag_properties.tag_value | ||||
| """ | ||||
|  | ||||
| class ConfigResource(object): | ||||
|  | ||||
| class ConfigResource(RelationalMixin): | ||||
|     @serialize | ||||
|     @login_required | ||||
|     @authorize_admin | ||||
|     def on_get(self, req, resp): | ||||
|         conn = config.DATABASE_POOL.get_connection() | ||||
|         cursor = conn.cursor(dictionary=True) | ||||
|         cursor.execute(SQL_SELECT_INHERITANCE) | ||||
|         def g(): | ||||
|             for row in cursor: | ||||
|                 yield row | ||||
|             cursor.close() | ||||
|             conn.close() | ||||
|         return g() | ||||
|         return self.iterfetch(SQL_SELECT_RULES) | ||||
|  | ||||
| class ScriptResource(object): | ||||
|  | ||||
| class ScriptResource(RelationalMixin): | ||||
|     def on_get(self, req, resp): | ||||
|         from certidude.api.whois import address_to_identity | ||||
|  | ||||
|         node = address_to_identity( | ||||
|             config.DATABASE_POOL.get_connection(), | ||||
|             ipaddress.ip_address(req.env["REMOTE_ADDR"]) | ||||
|             self.connect(), | ||||
|             req.context.get("remote_addr") | ||||
|         ) | ||||
|         if not node: | ||||
|             resp.body = "Could not map IP address: %s" % req.env["REMOTE_ADDR"] | ||||
|             resp.body = "Could not map IP address: %s" % req.context.get("remote_addr") | ||||
|             resp.status = falcon.HTTP_404 | ||||
|             return | ||||
|  | ||||
| @@ -84,7 +84,7 @@ class ScriptResource(object): | ||||
|         key, common_name = identity.split("=") | ||||
|         assert "=" not in common_name | ||||
|  | ||||
|         conn = config.DATABASE_POOL.get_connection() | ||||
|         conn = self.connect() | ||||
|         cursor = conn.cursor() | ||||
|  | ||||
|         resp.set_header("Content-Type", "text/x-shellscript") | ||||
|   | ||||
| @@ -2,38 +2,14 @@ | ||||
| from certidude import config | ||||
| from certidude.auth import login_required, authorize_admin | ||||
| from certidude.decorators import serialize | ||||
| from certidude.relational import RelationalMixin | ||||
|  | ||||
| class LogResource(RelationalMixin): | ||||
|     SQL_CREATE_TABLES = "log_tables.sql" | ||||
|  | ||||
| class LogResource(object): | ||||
|     @serialize | ||||
|     @login_required | ||||
|     @authorize_admin | ||||
|     def on_get(self, req, resp): | ||||
|         """ | ||||
|         Translate currently online client's IP-address to distinguished name | ||||
|         """ | ||||
|  | ||||
|         SQL_LOG_ENTRIES = """ | ||||
|             SELECT | ||||
|                 * | ||||
|             FROM | ||||
|                 log | ||||
|             ORDER BY created DESC | ||||
|         """ | ||||
|         conn = config.DATABASE_POOL.get_connection() | ||||
|         cursor = conn.cursor(dictionary=True) | ||||
|         cursor.execute(SQL_LOG_ENTRIES) | ||||
|  | ||||
|         def g(): | ||||
|             for row in cursor: | ||||
|                 yield row | ||||
|             cursor.close() | ||||
|             conn.close() | ||||
|         return tuple(g()) | ||||
|  | ||||
| #        for acquired, released, identity in cursor: | ||||
| #            return { | ||||
| #                "acquired": datetime.utcfromtimestamp(acquired), | ||||
| #                "identity": parse_dn(bytes(identity)) | ||||
| #            } | ||||
| #        return None | ||||
|          | ||||
|         # TODO: Add last id parameter | ||||
|         return self.iterfetch("select * from log order by created desc") | ||||
|   | ||||
| @@ -5,45 +5,42 @@ import logging | ||||
| import ipaddress | ||||
| import os | ||||
| from certidude import config, authority, helpers, push, errors | ||||
| from certidude.auth import login_required, authorize_admin | ||||
| from certidude.auth import login_required, login_optional, authorize_admin | ||||
| from certidude.decorators import serialize | ||||
| from certidude.wrappers import Request, Certificate | ||||
| from certidude.firewall import whitelist_subnets, whitelist_content_types | ||||
|  | ||||
| logger = logging.getLogger("api") | ||||
|  | ||||
| class RequestListResource(object): | ||||
|     @serialize | ||||
|     @login_required | ||||
|     @authorize_admin | ||||
|     def on_get(self, req, resp): | ||||
|         return helpers.list_requests() | ||||
|         return authority.list_requests() | ||||
|  | ||||
|     @login_optional | ||||
|     @whitelist_subnets(config.REQUEST_SUBNETS) | ||||
|     @whitelist_content_types("application/pkcs10") | ||||
|     def on_post(self, req, resp): | ||||
|         """ | ||||
|         Submit certificate signing request (CSR) in PEM format | ||||
|         """ | ||||
|         # Parse remote IPv4/IPv6 address | ||||
|         remote_addr = ipaddress.ip_network(req.env["REMOTE_ADDR"].decode("utf-8")) | ||||
|  | ||||
|         # Check for CSR submission whitelist | ||||
|         if config.REQUEST_SUBNETS: | ||||
|             for subnet in config.REQUEST_SUBNETS: | ||||
|                 if subnet.overlaps(remote_addr): | ||||
|                     break | ||||
|             else: | ||||
|                logger.warning("Attempted to submit signing request from non-whitelisted address %s", remote_addr) | ||||
|                raise falcon.HTTPForbidden("Forbidden", "IP address %s not whitelisted" % remote_addr) | ||||
|  | ||||
|         if req.get_header("Content-Type") != "application/pkcs10": | ||||
|             raise falcon.HTTPUnsupportedMediaType( | ||||
|                 "This API call accepts only application/pkcs10 content type") | ||||
|  | ||||
|         body = req.stream.read(req.content_length).decode("ascii") | ||||
|         body = req.stream.read(req.content_length) | ||||
|         csr = Request(body) | ||||
|  | ||||
|         if not csr.common_name: | ||||
|             logger.warning("Rejected signing request without common name from %s", | ||||
|                 req.context.get("remote_addr")) | ||||
|             raise falcon.HTTPBadRequest( | ||||
|                 "Bad request", | ||||
|                 "No common name specified!") | ||||
|  | ||||
|         # Check if this request has been already signed and return corresponding certificte if it has been signed | ||||
|         try: | ||||
|             cert = authority.get_signed(csr.common_name) | ||||
|         except FileNotFoundError: | ||||
|         except EnvironmentError: | ||||
|             pass | ||||
|         else: | ||||
|             if cert.pubkey == csr.pubkey: | ||||
| @@ -56,12 +53,12 @@ class RequestListResource(object): | ||||
|         # Process automatic signing if the IP address is whitelisted and autosigning was requested | ||||
|         if req.get_param_as_bool("autosign"): | ||||
|             for subnet in config.AUTOSIGN_SUBNETS: | ||||
|                 if subnet.overlaps(remote_addr): | ||||
|                 if subnet.overlaps(req.context.get("remote_addr")): | ||||
|                     try: | ||||
|                         resp.set_header("Content-Type", "application/x-x509-user-cert") | ||||
|                         resp.body = authority.sign(csr).dump() | ||||
|                         return | ||||
|                     except FileExistsError: # Certificate already exists, try to save the request | ||||
|                     except EnvironmentError: # Certificate already exists, try to save the request | ||||
|                         pass | ||||
|                     break | ||||
|  | ||||
| @@ -73,7 +70,8 @@ class RequestListResource(object): | ||||
|             pass | ||||
|         except errors.DuplicateCommonNameError: | ||||
|             # TODO: Certificate renewal | ||||
|             logger.warning("Rejected signing request with overlapping common name from %s", req.env["REMOTE_ADDR"]) | ||||
|             logger.warning("Rejected signing request with overlapping common name from %s", | ||||
|                 req.context.get("remote_addr")) | ||||
|             raise falcon.HTTPConflict( | ||||
|                 "CSR with such CN already exists", | ||||
|                 "Will not overwrite existing certificate signing request, explicitly delete CSR and try again") | ||||
| @@ -86,12 +84,12 @@ class RequestListResource(object): | ||||
|             url = config.PUSH_LONG_POLL % csr.fingerprint() | ||||
|             click.echo("Redirecting to: %s"  % url) | ||||
|             resp.status = falcon.HTTP_SEE_OTHER | ||||
|             resp.set_header("Location", url) | ||||
|             logger.warning("Redirecting signing request from %s to %s", req.env["REMOTE_ADDR"], url) | ||||
|             resp.set_header("Location", url.encode("ascii")) | ||||
|             logger.debug("Redirecting signing request from %s to %s", req.context.get("remote_addr"), url) | ||||
|         else: | ||||
|             # Request was accepted, but not processed | ||||
|             resp.status = falcon.HTTP_202 | ||||
|             logger.info("Signing request from %s stored", req.env["REMOTE_ADDR"]) | ||||
|             logger.info("Signing request from %s stored", req.context.get("remote_addr")) | ||||
|  | ||||
|  | ||||
| class RequestDetailResource(object): | ||||
| @@ -101,11 +99,8 @@ class RequestDetailResource(object): | ||||
|         Fetch certificate signing request as PEM | ||||
|         """ | ||||
|         csr = authority.get_request(cn) | ||||
| #        if not os.path.exists(path): | ||||
| #            raise falcon.HTTPNotFound() | ||||
|  | ||||
|         resp.set_header("Content-Type", "application/pkcs10") | ||||
|         resp.set_header("Content-Disposition", "attachment; filename=%s.csr" % csr.common_name) | ||||
|         logger.debug("Signing request %s was downloaded by %s", | ||||
|             csr.common_name, req.context.get("remote_addr")) | ||||
|         return csr | ||||
|  | ||||
|     @login_required | ||||
| @@ -120,14 +115,17 @@ class RequestDetailResource(object): | ||||
|         resp.body = "Certificate successfully signed" | ||||
|         resp.status = falcon.HTTP_201 | ||||
|         resp.location = os.path.join(req.relative_uri, "..", "..", "signed", cn) | ||||
|         logger.info("Signing request %s signed by %s from %s", csr.common_name, req.context["user"], req.env["REMOTE_ADDR"]) | ||||
|         logger.info("Signing request %s signed by %s from %s", csr.common_name, | ||||
|             req.context.get("user"), req.context.get("remote_addr")) | ||||
|  | ||||
|     @login_required | ||||
|     @authorize_admin | ||||
|     def on_delete(self, req, resp, cn): | ||||
|         try: | ||||
|             authority.delete_request(cn) | ||||
|         except FileNotFoundError: | ||||
|             # Logging implemented in the function above | ||||
|         except EnvironmentError as e: | ||||
|             resp.body = "No certificate CN=%s found" % cn | ||||
|             logger.warning("User %s attempted to delete non-existant signing request %s from %s", req.context["user"], cn, req.env["REMOTE_ADDR"]) | ||||
|             logger.warning("User %s failed to delete signing request %s from %s, reason: %s", | ||||
|                 req.context["user"], cn, req.context.get("remote_addr"), e) | ||||
|             raise falcon.HTTPNotFound() | ||||
|   | ||||
| @@ -1,9 +1,12 @@ | ||||
|  | ||||
| import logging | ||||
| from certidude.authority import export_crl | ||||
|  | ||||
| logger = logging.getLogger("api") | ||||
|  | ||||
| class RevocationListResource(object): | ||||
|     def on_get(self, req, resp): | ||||
|         logger.debug("Revocation list requested by %s", req.context.get("remote_addr")) | ||||
|         resp.set_header("Content-Type", "application/x-pkcs7-crl") | ||||
|         resp.append_header("Content-Disposition", "attachment; filename=ca.crl") | ||||
|         resp.body = export_crl() | ||||
|  | ||||
|   | ||||
| @@ -9,40 +9,35 @@ logger = logging.getLogger("api") | ||||
|  | ||||
| class SignedCertificateListResource(object): | ||||
|     @serialize | ||||
|     @login_required | ||||
|     @authorize_admin | ||||
|     def on_get(self, req, resp): | ||||
|         for j in authority.list_signed(): | ||||
|             yield omit( | ||||
|                 key_type=j.key_type, | ||||
|                 key_length=j.key_length, | ||||
|                 identity=j.identity, | ||||
|                 cn=j.common_name, | ||||
|                 c=j.country_code, | ||||
|                 st=j.state_or_county, | ||||
|                 l=j.city, | ||||
|                 o=j.organization, | ||||
|                 ou=j.organizational_unit, | ||||
|                 fingerprint=j.fingerprint()) | ||||
|         return {"signed":authority.list_signed()} | ||||
|  | ||||
|  | ||||
| class SignedCertificateDetailResource(object): | ||||
|     @serialize | ||||
|     def on_get(self, req, resp, cn): | ||||
|         # Compensate for NTP lag | ||||
|         from time import sleep | ||||
|         sleep(5) | ||||
| #        from time import sleep | ||||
| #        sleep(5) | ||||
|         try: | ||||
|             logger.info("Served certificate %s to %s", cn, req.env["REMOTE_ADDR"]) | ||||
|             resp.set_header("Content-Disposition", "attachment; filename=%s.crt" % cn) | ||||
|             return authority.get_signed(cn) | ||||
|         except FileNotFoundError: | ||||
|             logger.warning("Failed to serve non-existant certificate %s to %s", cn, req.env["REMOTE_ADDR"]) | ||||
|             cert = authority.get_signed(cn) | ||||
|         except EnvironmentError: | ||||
|             logger.warning("Failed to serve non-existant certificate %s to %s", | ||||
|                 cn, req.context.get("remote_addr")) | ||||
|             resp.body = "No certificate CN=%s found" % cn | ||||
|             raise falcon.HTTPNotFound() | ||||
|         else: | ||||
|             logger.debug("Served certificate %s to %s", | ||||
|                 cn, req.context.get("remote_addr")) | ||||
|             return cert | ||||
|  | ||||
|  | ||||
|     @login_required | ||||
|     @authorize_admin | ||||
|     def on_delete(self, req, resp, cn): | ||||
|         logger.info("Revoked certificate %s by %s from %s", cn, req.context["user"], req.env["REMOTE_ADDR"]) | ||||
|         logger.info("Revoked certificate %s by %s from %s", | ||||
|             cn, req.context.get("user"), req.context.get("remote_addr")) | ||||
|         authority.revoke_certificate(cn) | ||||
|  | ||||
|   | ||||
| @@ -1,117 +1,63 @@ | ||||
|  | ||||
| import falcon | ||||
| import logging | ||||
| from certidude import config | ||||
| from certidude.relational import RelationalMixin | ||||
| from certidude.auth import login_required, authorize_admin | ||||
| from certidude.decorators import serialize | ||||
|  | ||||
| logger = logging.getLogger("api") | ||||
|  | ||||
| SQL_TAG_LIST = """ | ||||
| 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 | ||||
| """ | ||||
| class TagResource(RelationalMixin): | ||||
|     SQL_CREATE_TABLES = "tag_tables.sql" | ||||
|  | ||||
| SQL_TAG_DETAIL = SQL_TAG_LIST + " where device_tag.id = %s" | ||||
|  | ||||
| class TagResource(object): | ||||
|     @serialize | ||||
|     @login_required | ||||
|     @authorize_admin | ||||
|     def on_get(self, req, resp): | ||||
|         conn = config.DATABASE_POOL.get_connection() | ||||
|         cursor = conn.cursor(dictionary=True) | ||||
|         cursor.execute(SQL_TAG_LIST) | ||||
|         return self.iterfetch("select * from tag") | ||||
|  | ||||
|         def g(): | ||||
|             for row in cursor: | ||||
|                 yield row | ||||
|             cursor.close() | ||||
|             conn.close() | ||||
|         return tuple(g()) | ||||
|  | ||||
|     @serialize | ||||
|     @login_required | ||||
|     @authorize_admin | ||||
|     def on_post(self, req, resp): | ||||
|         from certidude import push | ||||
|         conn = config.DATABASE_POOL.get_connection() | ||||
|         cursor = conn.cursor() | ||||
|  | ||||
|         args = req.get_param("cn"), | ||||
|         cursor.execute( | ||||
|             "insert ignore device (`cn`) values (%s) on duplicate key update used = NOW();", args) | ||||
|         device_id = cursor.lastrowid | ||||
|  | ||||
|         args = req.get_param("key"), req.get_param("value") | ||||
|         cursor.execute( | ||||
|             "insert into tag (`key`, `value`) values (%s, %s) on duplicate key update used = NOW();", args) | ||||
|         tag_id = cursor.lastrowid | ||||
|  | ||||
|         args = device_id, tag_id | ||||
|         cursor.execute( | ||||
|             "insert into device_tag (`device_id`, `tag_id`) values (%s, %s);", args) | ||||
|  | ||||
|         push.publish("tag-added", str(cursor.lastrowid)) | ||||
|  | ||||
|         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("Tag cn=%s, key=%s, value=%s added" % args) | ||||
|         conn.commit() | ||||
|         cursor.close() | ||||
|         conn.close() | ||||
|  | ||||
|  | ||||
| class TagDetailResource(object): | ||||
| class TagDetailResource(RelationalMixin): | ||||
|     SQL_CREATE_TABLES = "tag_tables.sql" | ||||
|  | ||||
|     @serialize | ||||
|     @login_required | ||||
|     @authorize_admin | ||||
|     def on_get(self, req, resp, identifier): | ||||
|         conn = config.DATABASE_POOL.get_connection() | ||||
|         cursor = conn.cursor(dictionary=True) | ||||
|         cursor.execute(SQL_TAG_DETAIL, (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 row | ||||
|             return dict(zip(cols, row)) | ||||
|         cursor.close() | ||||
|         conn.close() | ||||
|         raise falcon.HTTPNotFound() | ||||
|  | ||||
|  | ||||
|     @serialize | ||||
|     @login_required | ||||
|     @authorize_admin | ||||
|     def on_put(self, req, resp, identifier): | ||||
|         from certidude import push | ||||
|         conn = config.DATABASE_POOL.get_connection() | ||||
|         cursor = conn.cursor() | ||||
|  | ||||
|         # Create tag if necessary | ||||
|         args = req.get_param("key"), req.get_param("value") | ||||
|         cursor.execute( | ||||
|             "insert into tag (`key`, `value`) values (%s, %s) on duplicate key update used = NOW();", args) | ||||
|         tag_id = cursor.lastrowid | ||||
|  | ||||
|         # Attach tag to device | ||||
|         cursor.execute("update device_tag set tag_id = %s where `id` = %s limit 1", | ||||
|             (tag_id, identifier)) | ||||
|         conn.commit() | ||||
|  | ||||
|         cursor.close() | ||||
|         conn.close() | ||||
|  | ||||
|         args = req.get_param("value"), identifier | ||||
|         self.sql_execute("tag_update.sql", *args) | ||||
|         logger.debug("Tag %s updated, value set to %s", | ||||
|             identifier, req.get_param("value")) | ||||
|         push.publish("tag-updated", identifier) | ||||
| @@ -122,13 +68,6 @@ class TagDetailResource(object): | ||||
|     @authorize_admin | ||||
|     def on_delete(self, req, resp, identifier): | ||||
|         from certidude import push | ||||
|         conn = config.DATABASE_POOL.get_connection() | ||||
|         cursor = conn.cursor() | ||||
|         cursor.execute("delete from device_tag where id = %s", (identifier,)) | ||||
|         conn.commit() | ||||
|         cursor.close() | ||||
|         conn.close() | ||||
|         self.sql_execute("tag_delete.sql", identifier) | ||||
|         push.publish("tag-removed", identifier) | ||||
|         logger.debug("Tag %s removed" % identifier) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -46,7 +46,7 @@ class WhoisResource(object): | ||||
|  | ||||
|         identity = address_to_identity( | ||||
|             conn, | ||||
|             ipaddress.ip_address(req.get_param("address") or req.env["REMOTE_ADDR"]) | ||||
|             req.context.get("remote_addr") | ||||
|         ) | ||||
|  | ||||
|         conn.close() | ||||
| @@ -55,4 +55,4 @@ class WhoisResource(object): | ||||
|             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.env["REMOTE_ADDR"] | ||||
|             resp.body = "Failed to look up node %s" % req.context.get("remote_addr") | ||||
|   | ||||
		Reference in New Issue
	
	Block a user