Exclude expired disabled NPM certificate dependencies

This commit is contained in:
Lyncoln
2026-09-20 10:53:26 -04:00
parent e4ca80d4d5
commit a93e067bfd
2 changed files with 289 additions and 1 deletions
+133 -1
View File
@@ -1,5 +1,5 @@
from collections.abc import Iterable from collections.abc import Iterable
from dataclasses import dataclass from dataclasses import dataclass, replace
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Protocol from typing import Protocol
@@ -20,6 +20,7 @@ from .object_normalizer import normalize_observations
from .persistence import SQLiteStateStore from .persistence import SQLiteStateStore
from .projections import project_proxy_hosts_to_dns from .projections import project_proxy_hosts_to_dns
from .reconciliation import ( from .reconciliation import (
ReconciliationConflict,
build_endpoint_plans, build_endpoint_plans,
merge_desired_with_previous, merge_desired_with_previous,
reconcile_observations, reconcile_observations,
@@ -133,6 +134,132 @@ def _authoritative_observations(
] ]
def _certificate_expired(
object: NormalizedObject,
now: datetime,
) -> bool:
value = object.payload.get("expires_on")
if value is None:
return False
if not isinstance(value, str):
raise ReconciliationConflict(
"Certificate expires_on must be normalized text"
)
normalized = value.strip()
if normalized.endswith("Z"):
normalized = normalized[:-1] + "+00:00"
try:
expires_at = datetime.fromisoformat(normalized)
except ValueError as exc:
raise ReconciliationConflict(
"Certificate expires_on is not a valid timestamp"
) from exc
if expires_at.tzinfo is None:
expires_at = expires_at.replace(tzinfo=timezone.utc)
return expires_at.astimezone(timezone.utc) <= now
def _filter_expired_certificate_dependencies(
observations: Iterable[Observation],
now: datetime,
) -> list[Observation]:
rows = list(observations)
expired_local: set[tuple[str, str]] = set()
for observation in rows:
object = observation.object
if (
object.object_type != CERTIFICATES
or not _certificate_expired(object, now)
):
continue
if object.local_key is None:
raise ReconciliationConflict(
"Expired observed certificate requires "
"endpoint-local identity"
)
expired_local.add(
(
observation.endpoint_id,
object.local_key,
)
)
filtered: list[Observation] = []
for observation in rows:
object = observation.object
if (
object.object_type == CERTIFICATES
and object.local_key is not None
and (
observation.endpoint_id,
object.local_key,
)
in expired_local
):
continue
if object.object_type != PROXY_HOSTS:
filtered.append(observation)
continue
expired_references = tuple(
reference
for reference in object.references
if (
reference.object_type == CERTIFICATES
and reference.local_key is not None
and (
observation.endpoint_id,
reference.local_key,
)
in expired_local
)
)
if not expired_references:
filtered.append(observation)
continue
if object.payload.get("enabled") is True:
raise ReconciliationConflict(
"Enabled proxy host references an expired "
f"certificate: {object.logical_key!r} "
f"on {observation.endpoint_id}"
)
retained_references = tuple(
reference
for reference in object.references
if reference not in expired_references
)
filtered.append(
replace(
observation,
object=replace(
object,
references=retained_references,
),
)
)
return filtered
def _planning_adapter_views( def _planning_adapter_views(
adapters: Iterable[ReconciliationAdapter], adapters: Iterable[ReconciliationAdapter],
live_pairs: frozenset[tuple[str, str]], live_pairs: frozenset[tuple[str, str]],
@@ -310,6 +437,11 @@ def discover_reconcile_plan(
), ),
) )
npm_observations = _filter_expired_certificate_dependencies(
npm_observations,
timestamp,
)
npm_desired = ( npm_desired = (
reconcile_observations(npm_observations) reconcile_observations(npm_observations)
if npm_observations if npm_observations
+156
View File
@@ -11,9 +11,11 @@ from dns_sync.adapters import (
DiscoveredObject, DiscoveredObject,
Endpoint, Endpoint,
NormalizedObject, NormalizedObject,
ObjectReference,
Observation, Observation,
) )
from dns_sync.orchestrator import discover_reconcile_plan from dns_sync.orchestrator import discover_reconcile_plan
from dns_sync.reconciliation import ReconciliationConflict
from dns_sync.persistence import SQLiteStateStore from dns_sync.persistence import SQLiteStateStore
@@ -343,6 +345,160 @@ class ObservationAuthorityTests(unittest.TestCase):
persisted, persisted,
) )
def test_expired_certificate_on_disabled_proxy_is_removed_from_desired(
self,
):
endpoint = Endpoint(
"npm",
"192.168.1.3:81",
)
adapter = FakeAdapter(
endpoint=endpoint,
capabilities=capability_set(
CERTIFICATES,
PROXY_HOSTS,
),
discovered={
CERTIFICATES: [
DiscoveredObject(
object_type=CERTIFICATES,
local_key="56",
payload={
"domain_names": [
"admin.trunavigate.org"
],
"provider": "letsencrypt",
"valid_from": None,
"expires_on": (
"2026-06-22T03:40:57.000Z"
),
},
)
],
PROXY_HOSTS: [
DiscoveredObject(
object_type=PROXY_HOSTS,
local_key="29",
payload={
"domain_names": [
"admin.trunavigate.org"
],
"forward_scheme": "http",
"forward_host": "192.168.1.4",
"forward_port": 3283,
"enabled": False,
},
references=(
ObjectReference(
relation="certificate",
object_type=CERTIFICATES,
local_key="56",
),
),
)
],
},
)
with SQLiteStateStore(":memory:") as store:
result = discover_reconcile_plan(
[adapter],
store,
target_ip="192.168.1.101",
run_at=T2,
)
certificates = [
object
for object in result.desired
if object.object_type == CERTIFICATES
]
proxies = [
object
for object in result.desired
if object.object_type == PROXY_HOSTS
]
self.assertEqual(certificates, [])
self.assertEqual(len(proxies), 1)
self.assertEqual(
proxies[0].logical_key,
"admin.trunavigate.org",
)
self.assertEqual(
proxies[0].references,
(),
)
def test_expired_certificate_on_enabled_proxy_fails_closed(
self,
):
endpoint = Endpoint(
"npm",
"192.168.1.3:81",
)
adapter = FakeAdapter(
endpoint=endpoint,
capabilities=capability_set(
CERTIFICATES,
PROXY_HOSTS,
),
discovered={
CERTIFICATES: [
DiscoveredObject(
object_type=CERTIFICATES,
local_key="56",
payload={
"domain_names": [
"active.example.com"
],
"provider": "letsencrypt",
"valid_from": None,
"expires_on": (
"2026-06-22T03:40:57.000Z"
),
},
)
],
PROXY_HOSTS: [
DiscoveredObject(
object_type=PROXY_HOSTS,
local_key="29",
payload={
"domain_names": [
"active.example.com"
],
"forward_scheme": "http",
"forward_host": "192.168.1.4",
"forward_port": 3283,
"enabled": True,
},
references=(
ObjectReference(
relation="certificate",
object_type=CERTIFICATES,
local_key="56",
),
),
)
],
},
)
with SQLiteStateStore(":memory:") as store:
with self.assertRaisesRegex(
ReconciliationConflict,
"Enabled proxy host references an expired certificate",
):
discover_reconcile_plan(
[adapter],
store,
target_ip="192.168.1.101",
run_at=T2,
)
def test_all_failed_sources_preserve_persisted_desired_without_planning(self): def test_all_failed_sources_preserve_persisted_desired_without_planning(self):
npm_endpoint = Endpoint( npm_endpoint = Endpoint(
"npm", "npm",