""" 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")