fastapi-gsap/gsap_broker/routers/connectors.py
Tyler J King 4dff879c84 feat: wire credential resolver and connectors into broker startup
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>
2026-04-14 06:03:57 -04:00

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}