mirror of
https://github.com/mcloh/Enterprise-MCP-Service-Bus.git
synced 2026-11-05 22:14:11 +00:00
M0 e M1 revisados e fixes implementados
This commit is contained in:
0
RI/src/emcp_bus/downstream/__init__.py
Normal file
0
RI/src/emcp_bus/downstream/__init__.py
Normal file
75
RI/src/emcp_bus/downstream/identity.py
Normal file
75
RI/src/emcp_bus/downstream/identity.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""Outbound (backend) identity, distinct from inbound (MCP Client) identity
|
||||
(EP-07-T01, README.md §20, ADR-006).
|
||||
|
||||
inbound token (MCP Client -> Gateway) != outbound token (Fabric -> Backend)
|
||||
|
||||
The Gateway authenticates the *caller*; this module answers a completely
|
||||
separate question -- which credential the Fabric presents *to the backend*
|
||||
-- and never answers it by handing the inbound token through unmodified
|
||||
(README.md §35.6, "Token passthrough indiscriminado"). `service_account`
|
||||
mode resolves a static, backend-specific credential from an env var at call
|
||||
time; `token_exchange` (RFC 8693) is a documented, unimplemented extension
|
||||
(see `BackendConfig`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from emcp_bus.downstream.models import BackendConfig
|
||||
|
||||
|
||||
class UnknownBackendError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BackendCredentialUnavailableError(Exception):
|
||||
"""The configured env var for a backend's service-account token is unset.
|
||||
|
||||
Distinct from `UnknownBackendError`: the backend *is* configured, but
|
||||
its credential is missing at runtime -- callers (the Gateway) must
|
||||
treat this as DENY, not as "fall back to no credential" (README.md §38).
|
||||
"""
|
||||
|
||||
|
||||
class BackendCredential(BaseModel):
|
||||
backend: str
|
||||
url: str
|
||||
token: str
|
||||
audience: str
|
||||
|
||||
|
||||
class BackendCredentialProvider:
|
||||
def __init__(self, backend_configs: list[BackendConfig]) -> None:
|
||||
self._configs = {config.backend: config for config in backend_configs}
|
||||
|
||||
def credential_for(self, backend: str) -> BackendCredential:
|
||||
config = self._configs.get(backend)
|
||||
if config is None:
|
||||
raise UnknownBackendError(backend)
|
||||
|
||||
if config.credential_mode == "token_exchange":
|
||||
raise NotImplementedError(
|
||||
f"credential_mode='token_exchange' for backend {backend!r} is a documented "
|
||||
"extension point (RFC 8693 via an OIDC IdP), not implemented in this RI -- "
|
||||
"see BackendConfig docstring."
|
||||
)
|
||||
|
||||
assert config.service_account_token_env_var is not None # enforced by BackendConfig
|
||||
token = os.environ.get(config.service_account_token_env_var)
|
||||
if not token:
|
||||
raise BackendCredentialUnavailableError(
|
||||
f"Environment variable {config.service_account_token_env_var!r} for backend "
|
||||
f"{backend!r} is not set"
|
||||
)
|
||||
return BackendCredential(
|
||||
backend=backend, url=config.url, token=token, audience=config.audience
|
||||
)
|
||||
|
||||
def url_for(self, backend: str) -> str:
|
||||
config = self._configs.get(backend)
|
||||
if config is None:
|
||||
raise UnknownBackendError(backend)
|
||||
return config.url
|
||||
69
RI/src/emcp_bus/downstream/models.py
Normal file
69
RI/src/emcp_bus/downstream/models.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""Backend credential config schema (EP-07-T02, README.md §20).
|
||||
|
||||
One entry per backend the Fabric calls, declaring *how* the Gateway obtains
|
||||
an outbound credential for it -- never the credential's value. The RI
|
||||
implements `service_account` (a static credential resolved from an env var
|
||||
at call time); `token_exchange` (RFC 8693, e.g. via Keycloak) is modeled as
|
||||
a distinct, documented mode but not implemented in this milestone -- see
|
||||
`emcp_bus.downstream.identity.BackendCredentialProvider`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Literal, Self
|
||||
|
||||
from pydantic import BaseModel, field_validator, model_validator
|
||||
|
||||
_ENV_VAR_NAME_PATTERN = re.compile(r"^[A-Z][A-Z0-9_]*$")
|
||||
|
||||
|
||||
class BackendConfig(BaseModel):
|
||||
backend: str
|
||||
"""Adapter/service id, matching `CapabilityManifest.backend.service` (EP-02-T01)."""
|
||||
|
||||
url: str
|
||||
"""Where the Fabric router (EP-06-T01) connects to reach this backend --
|
||||
an MCP streamable-http endpoint for `backend.type == "mcp"`, or a REST
|
||||
base URL for `"rest"` (EP-06-T04). Not a secret, so unlike the credential
|
||||
fields below it may be a literal value, though it commonly uses
|
||||
`${VAR}` too since it differs between `make dev` (127.0.0.1) and Docker
|
||||
(the compose service DNS name)."""
|
||||
|
||||
audience: str
|
||||
"""The outbound token's audience -- deliberately never equal to the Gateway's
|
||||
own inbound `OIDC_AUDIENCE` (README.md §20: inbound and outbound authorization
|
||||
are distinct relations)."""
|
||||
|
||||
credential_mode: Literal["service_account", "token_exchange"] = "service_account"
|
||||
|
||||
service_account_token_env_var: str | None = None
|
||||
"""Name (not value) of the environment variable holding the backend's
|
||||
static credential. Required when `credential_mode == "service_account"`."""
|
||||
|
||||
token_exchange_endpoint: str | None = None
|
||||
"""RFC 8693 token endpoint URL. Documented extension point, not yet
|
||||
implemented -- see module docstring."""
|
||||
|
||||
@field_validator("service_account_token_env_var")
|
||||
@classmethod
|
||||
def _looks_like_an_env_var_name_not_a_secret(cls, value: str | None) -> str | None:
|
||||
if value is not None and not _ENV_VAR_NAME_PATTERN.match(value):
|
||||
raise ValueError(
|
||||
f"{value!r} does not look like an environment variable name "
|
||||
"(expected e.g. 'SALES_DOMAIN_SERVICE_TOKEN') -- this field must "
|
||||
"never hold a literal secret value (README.md §35.5)"
|
||||
)
|
||||
return value
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _required_field_matches_credential_mode(self) -> Self:
|
||||
if self.credential_mode == "service_account" and not self.service_account_token_env_var:
|
||||
raise ValueError(
|
||||
"service_account_token_env_var is required for credential_mode='service_account'"
|
||||
)
|
||||
if self.credential_mode == "token_exchange" and not self.token_exchange_endpoint:
|
||||
raise ValueError(
|
||||
"token_exchange_endpoint is required when credential_mode == 'token_exchange'"
|
||||
)
|
||||
return self
|
||||
Reference in New Issue
Block a user