mirror of
https://github.com/laurivosandi/certidude
synced 2026-01-12 17:06:59 +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