Resolve NPM certificate material sources
This commit is contained in:
@@ -9,6 +9,7 @@ from ...adapters import (
|
||||
NormalizedObject,
|
||||
ObjectReference,
|
||||
)
|
||||
from ...certificates.material import CertificateMaterial
|
||||
from ...npm_client import NpmClient
|
||||
from ...object_normalizer import normalize_discovered_object
|
||||
|
||||
@@ -180,6 +181,137 @@ class Plugin:
|
||||
for row in self.client.certificates()
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _portable_certificate_matches(
|
||||
actual: NormalizedObject,
|
||||
desired: NormalizedObject,
|
||||
) -> bool:
|
||||
if actual.object_type != CERTIFICATES:
|
||||
return False
|
||||
|
||||
if desired.object_type != CERTIFICATES:
|
||||
return False
|
||||
|
||||
if actual.logical_key != desired.logical_key:
|
||||
return False
|
||||
|
||||
for field in (
|
||||
"domain_names",
|
||||
"valid_from",
|
||||
"expires_on",
|
||||
):
|
||||
if (
|
||||
actual.payload.get(field)
|
||||
!= desired.payload.get(field)
|
||||
):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _certificate_material_source(
|
||||
self,
|
||||
desired: NormalizedObject,
|
||||
) -> CertificateMaterial:
|
||||
if desired.object_type != CERTIFICATES:
|
||||
raise RuntimeError(
|
||||
"Certificate material source requires "
|
||||
"a certificate object"
|
||||
)
|
||||
|
||||
matches: list[
|
||||
tuple[str, str, CertificateMaterial]
|
||||
] = []
|
||||
source_errors: list[str] = []
|
||||
|
||||
for endpoint in sorted(
|
||||
self.config.npm_endpoints,
|
||||
key=lambda item: item.identity,
|
||||
):
|
||||
if endpoint.identity == self.endpoint.identity:
|
||||
continue
|
||||
|
||||
client = NpmClient(
|
||||
self.config,
|
||||
endpoint,
|
||||
)
|
||||
|
||||
try:
|
||||
rows = client.certificates()
|
||||
except Exception as exc:
|
||||
source_errors.append(
|
||||
f"{endpoint.identity}: "
|
||||
f"{type(exc).__name__}"
|
||||
)
|
||||
continue
|
||||
|
||||
for row in rows:
|
||||
candidate = normalize_discovered_object(
|
||||
self._certificate_from_row(row)
|
||||
)
|
||||
|
||||
if not self._portable_certificate_matches(
|
||||
candidate,
|
||||
desired,
|
||||
):
|
||||
continue
|
||||
|
||||
if candidate.local_key is None:
|
||||
raise RuntimeError(
|
||||
"Matching NPM certificate has no "
|
||||
"endpoint-local key"
|
||||
)
|
||||
|
||||
material = client.certificate_material(
|
||||
candidate.local_key
|
||||
)
|
||||
|
||||
matches.append(
|
||||
(
|
||||
endpoint.identity,
|
||||
candidate.local_key,
|
||||
material,
|
||||
)
|
||||
)
|
||||
|
||||
if not matches:
|
||||
detail = ""
|
||||
|
||||
if source_errors:
|
||||
detail = (
|
||||
"; unavailable sources: "
|
||||
+ ", ".join(source_errors)
|
||||
)
|
||||
|
||||
raise RuntimeError(
|
||||
"No NPM source certificate material found for "
|
||||
f"{desired.logical_key!r}"
|
||||
f"{detail}"
|
||||
)
|
||||
|
||||
baseline = matches[0][2]
|
||||
|
||||
for _, _, material in matches[1:]:
|
||||
if material != baseline:
|
||||
endpoints = ", ".join(
|
||||
sorted(
|
||||
{
|
||||
endpoint_id
|
||||
for (
|
||||
endpoint_id,
|
||||
_,
|
||||
_,
|
||||
) in matches
|
||||
}
|
||||
)
|
||||
)
|
||||
raise RuntimeError(
|
||||
"Conflicting NPM certificate material "
|
||||
f"sources for {desired.logical_key!r}: "
|
||||
f"{endpoints}"
|
||||
)
|
||||
|
||||
return baseline
|
||||
|
||||
def apply(self, action):
|
||||
raise RuntimeError(
|
||||
f"{self.endpoint.identity} does not support apply yet"
|
||||
@@ -210,28 +342,14 @@ class Plugin:
|
||||
if actual.local_key != object.local_key:
|
||||
return False
|
||||
|
||||
if actual.logical_key != object.logical_key:
|
||||
return False
|
||||
|
||||
# Provider, display name, and renewal flags describe how
|
||||
# this NPM endpoint manages the certificate. They are not
|
||||
# portable certificate identity. A copied Let's Encrypt
|
||||
# certificate becomes provider="other" on the recipient.
|
||||
#
|
||||
# The portable verification state is the domain identity
|
||||
# plus the certificate validity period exposed by NPM.
|
||||
for field in (
|
||||
"domain_names",
|
||||
"valid_from",
|
||||
"expires_on",
|
||||
):
|
||||
if (
|
||||
actual.payload.get(field)
|
||||
!= object.payload.get(field)
|
||||
):
|
||||
return False
|
||||
|
||||
return True
|
||||
return self._portable_certificate_matches(
|
||||
actual,
|
||||
object,
|
||||
)
|
||||
|
||||
raise RuntimeError(
|
||||
f"{self.endpoint.identity} cannot verify "
|
||||
|
||||
Reference in New Issue
Block a user