"""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}