Files
npm-acme-authority/functions/cluster_cert.py
T

282 lines
8.5 KiB
Python

"""
Cluster-managed custom certificate mutations.
This module does NOT implement ACME renewal.
It only:
1. Adds cluster lifecycle flags to the certificate meta schema.
2. Preserves logical domain_names when custom certificate material is uploaded.
"""
from __future__ import annotations
import json
from functions.common import log
from functions.common import replace_exactly_once
from functions.common import require_fragment
SCHEMA_PROPERTIES = {
"cluster_managed": {
"type": "boolean",
"default": False,
},
"acme_renew": {
"type": "boolean",
"default": False,
},
}
UPLOAD_DOMAIN_ANCHOR = '''\t\tconst certificate = await internalCertificate.update(access, {
\t\t\tid: data.id,
\t\t\texpires_on: moment(validations.certificate.dates.to, "X").format("YYYY-MM-DD HH:mm:ss"),
\t\t\tdomain_names: validations.certificate.cn ? [validations.certificate.cn] : [],
\t\t\tmeta: _.clone(row.meta), // Prevent the update method from changing this value that we'll use later
\t\t});
'''
UPLOAD_DOMAIN_FRAGMENT = '''\t\tconst certificate = await internalCertificate.update(access, {
\t\t\tid: data.id,
\t\t\texpires_on: moment(validations.certificate.dates.to, "X").format("YYYY-MM-DD HH:mm:ss"),
\t\t\tdomain_names:
\t\t\t\tArray.isArray(row.domain_names) && row.domain_names.length
\t\t\t\t\t? row.domain_names
\t\t\t\t\t: validations.certificate.cn
\t\t\t\t\t\t? [validations.certificate.cn]
\t\t\t\t\t\t: [],
\t\t\tmeta: _.clone(row.meta), // Prevent the update method from changing this value that we'll use later
\t\t});
'''
SCHEDULER_PROVIDER_ANCHOR = '.andWhere("provider", "letsencrypt")\n'
SCHEDULER_FILTER_ANCHOR = "if (!certificates?.length) {"
SCHEDULER_FILTER_FRAGMENT = '''const renewableCertificates = (certificates || []).filter(
(certificate) =>
certificate.provider === "letsencrypt" ||
(
certificate.provider === "other" &&
certificate.meta?.cluster_managed === true &&
certificate.meta?.acme_renew === true
),
);
if (!renewableCertificates.length) {'''
SCHEDULER_FOREACH_ANCHOR = "certificates.forEach((certificate) => {"
SCHEDULER_FOREACH_FRAGMENT = (
"renewableCertificates.forEach((certificate) => {"
)
CLUSTER_RENEW_THROW_ANCHOR = (
"throw new error.ValidationError("
"\"Only Let'sEncrypt certificates can be renewed\");"
)
CLUSTER_RENEW_THROW_FRAGMENT = '''if (
certificate.provider === "other" &&
certificate.meta?.cluster_managed === true &&
certificate.meta?.acme_renew === true
) {
throw new error.ValidationError(
"Cluster-managed ACME renewal path is not yet enabled",
);
}
throw new error.ValidationError("Only Let'sEncrypt certificates can be renewed");'''
def apply_certificate_js(source: str) -> str:
if UPLOAD_DOMAIN_FRAGMENT in source:
log("cluster_cert_upload_domains=already-present")
else:
source = replace_exactly_once(
source,
UPLOAD_DOMAIN_ANCHOR,
UPLOAD_DOMAIN_FRAGMENT,
description="cluster certificate upload domain preservation",
)
log("cluster_cert_upload_domains=added")
if SCHEDULER_PROVIDER_ANCHOR in source:
source = replace_exactly_once(
source,
SCHEDULER_PROVIDER_ANCHOR,
"",
description="cluster certificate scheduler provider restriction",
)
log("cluster_cert_scheduler_query=added")
elif SCHEDULER_FILTER_FRAGMENT in source:
log("cluster_cert_scheduler_query=already-present")
else:
raise RuntimeError(
"cluster certificate scheduler query: "
"provider restriction missing but classifier is not present"
)
if SCHEDULER_FILTER_FRAGMENT in source:
log("cluster_cert_scheduler_filter=already-present")
else:
source = replace_exactly_once(
source,
SCHEDULER_FILTER_ANCHOR,
SCHEDULER_FILTER_FRAGMENT,
description="cluster certificate scheduler filter",
)
log("cluster_cert_scheduler_filter=added")
if SCHEDULER_FOREACH_FRAGMENT in source:
log("cluster_cert_scheduler_foreach=already-present")
else:
source = replace_exactly_once(
source,
SCHEDULER_FOREACH_ANCHOR,
SCHEDULER_FOREACH_FRAGMENT,
description="cluster certificate scheduler foreach",
)
log("cluster_cert_scheduler_foreach=added")
if CLUSTER_RENEW_THROW_FRAGMENT in source:
log("cluster_cert_renew_dispatch=already-present")
else:
source = replace_exactly_once(
source,
CLUSTER_RENEW_THROW_ANCHOR,
CLUSTER_RENEW_THROW_FRAGMENT,
description="cluster certificate renewal dispatch",
)
log("cluster_cert_renew_dispatch=added")
return source
def verify_certificate_js(source: str) -> None:
require_fragment(
source,
UPLOAD_DOMAIN_FRAGMENT,
description="cluster certificate upload domain verification",
)
require_fragment(
source,
"await internalCertificate.writeCustomCert(certificate);",
description="cluster certificate custom write verification",
)
if SCHEDULER_PROVIDER_ANCHOR in source:
raise RuntimeError(
"cluster certificate scheduler verification: "
"old letsencrypt-only provider restriction remains"
)
require_fragment(
source,
SCHEDULER_FILTER_FRAGMENT,
description="cluster certificate scheduler filter verification",
)
require_fragment(
source,
SCHEDULER_FOREACH_FRAGMENT,
description="cluster certificate scheduler foreach verification",
)
require_fragment(
source,
CLUSTER_RENEW_THROW_FRAGMENT,
description="cluster certificate renewal dispatch verification",
)
log("cluster_cert_upload_domains_verification=passed")
log("cluster_cert_scheduler_verification=passed")
log("cluster_cert_renew_dispatch_verification=passed")
def apply_schema(source: str) -> str:
try:
document = json.loads(source)
except json.JSONDecodeError as exc:
raise RuntimeError(
f"cluster certificate schema: invalid JSON: {exc}"
) from exc
try:
meta = document["properties"]["meta"]
meta_properties = meta["properties"]
except (KeyError, TypeError) as exc:
raise RuntimeError(
"cluster certificate schema: "
"properties.meta.properties not found"
) from exc
if meta.get("additionalProperties") is not False:
raise RuntimeError(
"cluster certificate schema: "
"meta.additionalProperties is not false"
)
changed = False
for name, definition in SCHEMA_PROPERTIES.items():
existing = meta_properties.get(name)
if existing is None:
meta_properties[name] = definition
changed = True
elif existing != definition:
raise RuntimeError(
"cluster certificate schema: "
f"{name} already exists with unexpected definition"
)
if not changed:
verify_schema(source)
log("cluster_cert_schema=already-present")
return source
result = json.dumps(
document,
indent="\t",
ensure_ascii=False,
) + "\n"
verify_schema(result)
log("cluster_cert_schema=added")
return result
def verify_schema(source: str) -> None:
try:
document = json.loads(source)
except json.JSONDecodeError as exc:
raise RuntimeError(
f"cluster certificate schema verification: invalid JSON: {exc}"
) from exc
try:
meta = document["properties"]["meta"]
meta_properties = meta["properties"]
except (KeyError, TypeError) as exc:
raise RuntimeError(
"cluster certificate schema verification: "
"properties.meta.properties not found"
) from exc
if meta.get("additionalProperties") is not False:
raise RuntimeError(
"cluster certificate schema verification: "
"meta.additionalProperties is not false"
)
for name, definition in SCHEMA_PROPERTIES.items():
if meta_properties.get(name) != definition:
raise RuntimeError(
"cluster certificate schema verification: "
f"{name} definition is missing or unexpected"
)
log("cluster_cert_schema_verification=passed")