All connectors registered conditionally based on settings. CredentialResolver with Entra backend (production) or Stub backend (dev mode). 15 new tests covering credential resolution, session lifecycle, orchestrator workflows, and device routing. Signed-off-by: Tyler King <tking@guildhouse.dev>
135 lines
4.9 KiB
Python
135 lines
4.9 KiB
Python
"""Connectors router — governed connector catalogue and invocation."""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
from pydantic import BaseModel
|
|
|
|
from gsap_broker.connectors.base import ConnectorContext
|
|
from gsap_broker.connectors.registry import ConnectorRegistry
|
|
from gsap_broker.connectors.runtime import ConnectorRuntime
|
|
from gsap_broker.connectors.examples.echo_connector import EchoConnector
|
|
from gsap_broker.settings import settings
|
|
|
|
router = APIRouter()
|
|
|
|
# Module-level registry and runtime
|
|
_registry = ConnectorRegistry()
|
|
_runtime = ConnectorRuntime(registry=_registry)
|
|
|
|
# Register built-in connectors
|
|
_registry.register(EchoConnector())
|
|
|
|
# ── Credential resolver (shared by session connectors) ──────────
|
|
from gsap_broker.credentials.resolver import CredentialResolver
|
|
|
|
_credential_resolver = CredentialResolver()
|
|
|
|
if settings.credential_backend == "stub" or (
|
|
settings.credential_backend == "auto" and not settings.entra_client_secret
|
|
):
|
|
from gsap_broker.credentials.stub_backend import StubCredentialBackend
|
|
_credential_resolver.register(StubCredentialBackend())
|
|
elif settings.entra_client_secret:
|
|
from gsap_broker.credentials.entra_backend import EntraCredentialBackend
|
|
_credential_resolver.register(EntraCredentialBackend(
|
|
tenant_id=settings.entra_tenant_id,
|
|
client_id=settings.entra_client_id,
|
|
client_secret=settings.entra_client_secret,
|
|
))
|
|
|
|
# ── Conditionally register connectors ───────────────────────────
|
|
|
|
# Intune (API-mediated — uses GraphClient, not CredentialResolver)
|
|
if settings.intune_enabled and settings.entra_client_secret:
|
|
from gsap_broker.intune.graph_client import GraphClient
|
|
from gsap_broker.intune.device_cache import DeviceComplianceCache
|
|
from gsap_broker.connectors.intune import IntuneConnector
|
|
|
|
_intune_graph = GraphClient(
|
|
tenant_id=settings.entra_tenant_id,
|
|
client_id=settings.entra_client_id,
|
|
client_secret=settings.entra_client_secret,
|
|
)
|
|
_intune_cache = DeviceComplianceCache(ttl_seconds=settings.intune_compliance_cache_ttl)
|
|
_intune_connector = IntuneConnector(graph_client=_intune_graph, cache=_intune_cache)
|
|
_registry.register(_intune_connector)
|
|
|
|
# Bascule (session-based — uses CredentialResolver)
|
|
if settings.bascule_enabled:
|
|
from gsap_broker.connectors.bascule import BasculeConnector
|
|
_registry.register(BasculeConnector(credential_resolver=_credential_resolver))
|
|
|
|
# PowerShell (session-based — uses CredentialResolver)
|
|
if settings.powershell_enabled:
|
|
from gsap_broker.connectors.powershell import PowerShellConnector
|
|
_registry.register(PowerShellConnector(credential_resolver=_credential_resolver))
|
|
|
|
# Ansible (orchestrator — uses CredentialResolver)
|
|
if settings.ansible_enabled:
|
|
from gsap_broker.connectors.ansible import AnsibleConnector
|
|
_registry.register(AnsibleConnector(credential_resolver=_credential_resolver))
|
|
|
|
|
|
class InvokeRequest(BaseModel):
|
|
operation: str
|
|
parameters: dict[str, Any] = {}
|
|
chronicle_session_id: str = ""
|
|
gsap_context_id: str = ""
|
|
pipeline_run_id: str = ""
|
|
dag_id: str = ""
|
|
|
|
|
|
class InvokeResponse(BaseModel):
|
|
success: bool
|
|
data: Any = None
|
|
error: str | None = None
|
|
lineage_cid: str = ""
|
|
connector_id: str = ""
|
|
operation: str = ""
|
|
|
|
|
|
@router.get("/")
|
|
async def catalogue() -> list[dict]:
|
|
return _registry.catalogue()
|
|
|
|
|
|
@router.get("/{connector_id}/")
|
|
async def get_descriptor(connector_id: str) -> dict:
|
|
connector = _registry.get(connector_id)
|
|
if connector is None:
|
|
raise HTTPException(status_code=404, detail=f"Connector not found: {connector_id}")
|
|
return connector.descriptor()
|
|
|
|
|
|
@router.post("/{connector_id}/invoke/")
|
|
async def invoke_connector(connector_id: str, body: InvokeRequest) -> InvokeResponse:
|
|
connector = _registry.get(connector_id)
|
|
if connector is None:
|
|
raise HTTPException(status_code=404, detail=f"Connector not found: {connector_id}")
|
|
|
|
ctx = ConnectorContext(
|
|
chronicle_session_id=body.chronicle_session_id,
|
|
gsap_context_id=body.gsap_context_id,
|
|
pipeline_run_id=body.pipeline_run_id,
|
|
dag_id=body.dag_id,
|
|
)
|
|
result = await _runtime.invoke(connector_id, body.operation, body.parameters, ctx)
|
|
return InvokeResponse(
|
|
success=result.success,
|
|
data=result.data,
|
|
error=result.error,
|
|
lineage_cid=result.lineage_cid,
|
|
connector_id=connector_id,
|
|
operation=body.operation,
|
|
)
|
|
|
|
|
|
@router.get("/{connector_id}/health/")
|
|
async def health_check(connector_id: str) -> dict:
|
|
connector = _registry.get(connector_id)
|
|
if connector is None:
|
|
raise HTTPException(status_code=404, detail=f"Connector not found: {connector_id}")
|
|
healthy = connector.health_check()
|
|
return {"connector_id": connector_id, "healthy": healthy}
|