Commit 7c2b2693 authored by Konstantinos Togias's avatar Konstantinos Togias
Browse files

Reject requests addressed to another control-path instance

parent 976f6583
Loading
Loading
Loading
Loading
+15 −0
Changes for README.md: 15 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -193,6 +193,7 @@ its context and a namespace:

```python
adapter = build_adapter("kubernetes", {
    "control_path_ref": "k8s-athens",
    "kubeconfig_path": "/etc/openop/kubeconfigs/athens.yaml",
    "kube_context": "athens",
    "namespace": "applications",
@@ -230,3 +231,17 @@ system PATH. `helm_environment` accepts only absolute `HELM_REGISTRY_CONFIG`,
mounted artifact configuration. Proxy modes require separate qualification.
Operator tooling owns credential resolution and mounts; no secret resolver
is implemented in the SDK.

## Execution instance identity

Every contract execution adapter requires a nonempty `control_path_ref` in its
construction config. SRM derives it from the registry entry key; the operator
declares that identity once in the bootstrap control-path ref. Direct SDK
callers must also supply it. Missing construction identity raises ValidationError.

Every implemented lifecycle, query and runtime-state method checks the resolved
request's `target.control_path_ref` before loading credentials or contacting a
backend. A mismatch raises non-retryable InvalidTargetRef. This applies to
direct concrete adapter calls as well as factory construction. The check uses
an opaque identity; topology relationships and placement remain in SRM. It
does not establish authorization or the provenance of an arbitrary backend ID.
+27 −0
Changes for src/openop_tfsdk/contract/adapters/control_path.py: 27 added lines, 0 removed lines.
Original line number Diff line number Diff line
"""Opaque execution identity; topology relationships remain the consumer's concern."""

from dataclasses import dataclass

from openop_tfsdk.contract.errors import InvalidTargetRef, ValidationError


@dataclass(frozen=True)
class ControlPathScope:
    control_path_ref: str

    def __post_init__(self):
        if not isinstance(self.control_path_ref, str) or not self.control_path_ref.strip():
            raise ValidationError(
                "Execution adapters require a nonempty control_path_ref in config"
            )

    def check(self, request):
        ref = getattr(getattr(request, "target", None), "control_path_ref", None)
        if not isinstance(ref, str) or ref != self.control_path_ref:
            raise InvalidTargetRef(
                "Request control path does not match this adapter instance",
                details={
                    "expected_control_path_ref": self.control_path_ref,
                    "control_path_ref": ref if isinstance(ref, str) else None,
                },
            )
+5 −0
Changes for src/openop_tfsdk/contract/adapters/edgecloud.py: 5 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@ import pydantic
import structlog

from openop_tfsdk._diagnostics import backend_diagnostics, validation_diagnostics
from openop_tfsdk.contract.adapters.control_path import ControlPathScope
from openop_tfsdk.contract.errors import (
    DomainExecutionFailed,
    ResourceNotFound,
@@ -56,6 +57,7 @@ class I2edgeCapabilityAdapter:

    def __init__(self, infrastructure: Optional[dict] = None):
        infrastructure = infrastructure or {}
        self._scope = ControlPathScope(infrastructure.get("control_path_ref"))
        self._client = I2edgeClient(
            base_url=infrastructure.get("base_url"),
            flavour_id=infrastructure.get("flavour_id"),
@@ -68,6 +70,7 @@ class I2edgeCapabilityAdapter:
        return [], []

    def activate(self, request: CapabilityLifecycleRequest) -> CapabilityLifecycleResult:
        self._scope.check(request)
        if request.capability_kind != "workload":
            raise ValidationError(f"i2edge does not activate {request.capability_kind}")
        unit = request.deployment_unit
@@ -86,6 +89,7 @@ class I2edgeCapabilityAdapter:
        )

    def read_runtime_state(self, request: CapabilityRuntimeStateRequest) -> CapabilityRuntimeState:
        self._scope.check(request)
        if not request.external_id:
            raise ValidationError("read_runtime_state needs the external_id returned by activate")
        try:
@@ -99,6 +103,7 @@ class I2edgeCapabilityAdapter:
        )

    def terminate(self, request: CapabilityLifecycleRequest) -> CapabilityLifecycleResult:
        self._scope.check(request)
        if not request.external_id:
            raise ValidationError("terminate needs the external_id returned by activate")
        self._call("undeploy_app", request.external_id)
+6 −0
Changes for src/openop_tfsdk/contract/adapters/kubernetes.py: 6 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ from kubernetes.config.config_exception import ConfigException
from urllib3.exceptions import HTTPError

from openop_tfsdk._diagnostics import helm_diagnostics
from openop_tfsdk.contract.adapters.control_path import ControlPathScope
from openop_tfsdk.contract.adapters.kubernetes_connection import KubernetesConnection
from openop_tfsdk.contract.errors import (
    DomainExecutionFailed,
@@ -133,6 +134,7 @@ class KubernetesCapabilityAdapter:

    def __init__(self, infrastructure: Optional[dict] = None):
        self._infrastructure = dict(infrastructure or {})
        self._scope = ControlPathScope(self._infrastructure.get("control_path_ref"))
        self._connection = KubernetesConnection(self._infrastructure)

    @staticmethod
@@ -142,6 +144,7 @@ class KubernetesCapabilityAdapter:
        return [], []

    def activate(self, request: CapabilityLifecycleRequest) -> CapabilityLifecycleResult:
        self._scope.check(request)
        unit = request.deployment_unit
        if unit is None or not unit.artifact_ref:
            raise ValidationError("activate needs a deployment_unit with an artifact_ref")
@@ -222,6 +225,7 @@ class KubernetesCapabilityAdapter:
            return None

    def read_runtime_state(self, request: CapabilityRuntimeStateRequest) -> CapabilityRuntimeState:
        self._scope.check(request)
        if not request.external_id:
            raise ValidationError("read_runtime_state needs the external_id returned by activate")
        release_name = request.external_id
@@ -267,6 +271,7 @@ class KubernetesCapabilityAdapter:
        )

    def terminate(self, request: CapabilityLifecycleRequest) -> CapabilityLifecycleResult:
        self._scope.check(request)
        if not request.external_id:
            raise ValidationError("terminate needs the external_id returned by activate")
        release_name = request.external_id
@@ -298,6 +303,7 @@ class KubernetesCapabilityAdapter:
        )

    def update(self, request: CapabilityLifecycleRequest) -> CapabilityLifecycleResult:
        self._scope.check(request)
        unit = request.deployment_unit
        if not request.external_id:
            raise ValidationError("update needs the external_id returned by activate")
+7 −0
Changes for src/openop_tfsdk/contract/adapters/network.py: 7 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@ import requests
import structlog

from openop_tfsdk._diagnostics import backend_diagnostics, validation_diagnostics
from openop_tfsdk.contract.adapters.control_path import ControlPathScope
from openop_tfsdk.contract.errors import (
    DomainExecutionFailed,
    MaxAgeUnsatisfiable,
@@ -245,12 +246,14 @@ class NetworkCapabilityAdapter:

    def __init__(self, infrastructure: Optional[dict] = None):
        infrastructure = infrastructure or {}
        self._scope = ControlPathScope(infrastructure.get("control_path_ref"))
        self._client = self.client_class(
            base_url=infrastructure.get("base_url"), scs_as_id=infrastructure.get("scs_as_id")
        )
        self._notification_url = infrastructure.get("notification_url")

    def activate(self, request: CapabilityLifecycleRequest) -> CapabilityLifecycleResult:
        self._scope.check(request)
        self._require(request.capability_kind, SupportedAction.ACTIVATE)
        if request.capability_kind == "traffic_influence":
            resource = self._call(
@@ -273,6 +276,7 @@ class NetworkCapabilityAdapter:
        )

    def read_runtime_state(self, request: CapabilityRuntimeStateRequest) -> CapabilityRuntimeState:
        self._scope.check(request)
        self._require(request.capability_kind, SupportedAction.READ_RUNTIME_STATE)
        if not request.external_id:
            raise ValidationError("read_runtime_state needs the external_id returned by activate")
@@ -296,6 +300,7 @@ class NetworkCapabilityAdapter:
        )

    def terminate(self, request: CapabilityLifecycleRequest) -> CapabilityLifecycleResult:
        self._scope.check(request)
        self._require(request.capability_kind, SupportedAction.TERMINATE)
        if not request.external_id:
            raise ValidationError("terminate needs the external_id returned by activate")
@@ -311,6 +316,7 @@ class NetworkCapabilityAdapter:
        )

    def update(self, request: CapabilityLifecycleRequest) -> CapabilityLifecycleResult:
        self._scope.check(request)
        self._require(request.capability_kind, SupportedAction.UPDATE)
        if not request.external_id:
            raise ValidationError("update needs the external_id returned by activate")
@@ -327,6 +333,7 @@ class NetworkCapabilityAdapter:
        )

    def query(self, request: CapabilityQueryRequest) -> CapabilityQueryResult:
        self._scope.check(request)
        if (
            request.capability_kind != "location_context"
            or "location_retrieval" not in self.client_class.capabilities
Loading