178 lines
6.2 KiB
Python
178 lines
6.2 KiB
Python
"""Idempotent provisioning for real Authentik-backed MCP machine clients."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from uuid import NAMESPACE_URL, UUID, uuid5
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from packages.hub_clients import ClientKind
|
|
|
|
from .models import (
|
|
ClientBindingRow,
|
|
ClientRow,
|
|
EnvironmentRow,
|
|
ProjectRow,
|
|
VirtualServerRow,
|
|
WorkspaceRow,
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class MachineClientResult:
|
|
client_id: UUID
|
|
binding_id: UUID
|
|
endpoint: str
|
|
|
|
|
|
def provision_machine_client(
|
|
session: Session,
|
|
*,
|
|
issuer: str,
|
|
subject: str,
|
|
oauth_username: str,
|
|
name: str,
|
|
project_slug: str,
|
|
environment_name: str = "prod",
|
|
workspace_slug: str = "itworx",
|
|
public_host: str = "mcp.example.com",
|
|
client_id: UUID | None = None,
|
|
) -> MachineClientResult:
|
|
"""Bind one validated Authentik subject to exactly one external virtual server.
|
|
|
|
The caller obtains ``subject`` from a validated Authentik token or the exact
|
|
Authentik user UUID. Credentials are deliberately not accepted here.
|
|
"""
|
|
|
|
issuer = issuer.strip()
|
|
subject = subject.strip()
|
|
oauth_username = oauth_username.strip()
|
|
name = name.strip()
|
|
if not issuer.startswith("https://") or not subject or not oauth_username or not name:
|
|
raise ValueError("machine client identity is incomplete")
|
|
workspace = session.scalar(select(WorkspaceRow).where(WorkspaceRow.slug == workspace_slug))
|
|
if workspace is None:
|
|
raise ValueError("machine client workspace is not registered")
|
|
project = session.scalar(
|
|
select(ProjectRow).where(
|
|
ProjectRow.workspace_id == workspace.id,
|
|
ProjectRow.slug == project_slug,
|
|
ProjectRow.status == "active",
|
|
)
|
|
)
|
|
if project is None:
|
|
raise ValueError("machine client project is not registered")
|
|
environment = session.scalar(
|
|
select(EnvironmentRow).where(
|
|
EnvironmentRow.workspace_id == workspace.id,
|
|
EnvironmentRow.project_id == project.id,
|
|
EnvironmentRow.name == environment_name,
|
|
EnvironmentRow.status == "active",
|
|
)
|
|
)
|
|
if environment is None:
|
|
raise ValueError("machine client environment is not registered")
|
|
servers = session.scalars(
|
|
select(VirtualServerRow).where(
|
|
VirtualServerRow.workspace_id == workspace.id,
|
|
VirtualServerRow.project_id == project.id,
|
|
VirtualServerRow.environment_id == environment.id,
|
|
VirtualServerRow.enabled.is_(True),
|
|
VirtualServerRow.gateway_external_id.is_not(None),
|
|
)
|
|
).all()
|
|
if len(servers) != 1:
|
|
raise ValueError(
|
|
"machine client virtual server is not uniquely determined "
|
|
f"({len(servers)} externally-routed servers found)"
|
|
)
|
|
server = servers[0]
|
|
deterministic_id = uuid5(NAMESPACE_URL, f"{issuer}\n{subject}")
|
|
principal = session.scalar(
|
|
select(ClientRow).where(
|
|
ClientRow.workspace_id == workspace.id,
|
|
ClientRow.issuer == issuer,
|
|
ClientRow.subject == subject,
|
|
)
|
|
)
|
|
selected_id = client_id or deterministic_id
|
|
by_id = session.get(ClientRow, selected_id)
|
|
if principal is not None and principal.id != selected_id:
|
|
raise ValueError("machine client principal is already managed by another identity")
|
|
if (
|
|
client_id is None
|
|
and by_id is not None
|
|
and (by_id.issuer != issuer or by_id.subject != subject)
|
|
):
|
|
raise ValueError("machine client deterministic identity collision")
|
|
if client_id is not None and by_id is not None:
|
|
existing_metadata = by_id.record_metadata if isinstance(by_id.record_metadata, dict) else {}
|
|
if (
|
|
existing_metadata.get("managedBy") != "runtime-onboarding"
|
|
or existing_metadata.get("project") != project_slug
|
|
or existing_metadata.get("environment") != environment_name
|
|
or by_id.status != "pending"
|
|
):
|
|
raise ValueError("existing Hub client is not an activatable pending identity")
|
|
try:
|
|
kind = ClientKind(existing_metadata.get("kind", ClientKind.STDIO_BRIDGE.value))
|
|
except (TypeError, ValueError) as exc:
|
|
raise ValueError("pending machine client has an invalid client kind") from exc
|
|
else:
|
|
kind = ClientKind.STDIO_BRIDGE
|
|
client = principal or by_id
|
|
metadata = {
|
|
"managedBy": "authentik-machine-provisioning",
|
|
"identityProvisioned": True,
|
|
"name": name,
|
|
"project": project_slug,
|
|
"environment": environment_name,
|
|
"kind": kind.value,
|
|
"oauthUsername": oauth_username,
|
|
"credentialReference": f"authentik://service-account/{oauth_username}",
|
|
"operatorDisabled": False,
|
|
}
|
|
if client is None:
|
|
client = ClientRow(
|
|
id=selected_id,
|
|
workspace_id=workspace.id,
|
|
subject=subject,
|
|
issuer=issuer,
|
|
client_type="mcp",
|
|
status="active",
|
|
record_metadata=metadata,
|
|
)
|
|
session.add(client)
|
|
session.flush()
|
|
else:
|
|
if client.status == "revoked":
|
|
raise ValueError("revoked machine client cannot be reprovisioned")
|
|
client.issuer = issuer
|
|
client.subject = subject
|
|
client.status = "active"
|
|
client.record_metadata = metadata
|
|
session.flush()
|
|
bindings = session.scalars(
|
|
select(ClientBindingRow).where(ClientBindingRow.client_id == client.id)
|
|
).all()
|
|
foreign = [item for item in bindings if item.virtual_server_id != server.id]
|
|
if foreign:
|
|
raise ValueError("machine client already has a different project binding")
|
|
binding = next(iter(bindings), None)
|
|
if binding is None:
|
|
binding = ClientBindingRow(
|
|
workspace_id=workspace.id,
|
|
project_id=project.id,
|
|
client_id=client.id,
|
|
virtual_server_id=server.id,
|
|
enabled=True,
|
|
)
|
|
session.add(binding)
|
|
else:
|
|
binding.enabled = True
|
|
session.flush()
|
|
endpoint = f"https://{public_host}/mcp/{project_slug}/{environment_name}"
|
|
return MachineClientResult(client_id=client.id, binding_id=binding.id, endpoint=endpoint)
|