A03 - Human Identity
| Group | Technology | Status | Service | Roadmap phase | Contracts |
|---|---|---|---|---|---|
| A - Governance, identity and security | Keycloak (OIDC, SAML) | open source integrated | identity | 1 | common |
Role and responsibilities
Human authentication, SSO federation, MFA, groups and role mapping.
Data model
Tables (schema eai, every tenant table carries tenant_id uuid NOT NULL with an RLS policy on current_setting('app.tenant_id', true)::uuid): eai.memberships, eai.api_keys, eai.audit_events.
API endpoints
GET /v1/meGET /v1/membersPOST /v1/membersPATCH /v1/members/{id}DELETE /v1/members/{id}GET /v1/api-keysPOST /v1/api-keysDELETE /v1/api-keys/{id}POST /v1/api-keys/{id}/rotate
POST /v1/members HTTP/1.1
Host: <control-plane-host>
X-API-Key: eai_xxx_secret
Accept-Language: en
Content-Type: application/json
{"user": "bob.martin", "email": "bob.martin@acme.example", "display_name": "Bob Martin", "role": "developer", "clearance": "internal"}
{
"id": "21d243df-3f84-486e-a0a7-dd9dbb831a68",
"tenant_id": "18de5695-21f9-4053-a4ea-beb4bf2a5932",
"user_id": "cbb14d37-895e-4567-9a4d-67dee437dfce",
"user": "bob.martin",
"email": "bob.martin@acme.example",
"display_name": "Bob Martin",
"role": "developer",
"clearance": "internal",
"project_id": null,
"created_at": "2026-10-30T02:35:52.697582+01:00"
}
Errors use the common envelope {"error": {"code", "message", "message_i18n", "details", "request_id"}}; lists are paginated with limit and cursor.
Directory federation and group mappings
Keycloak federates the enterprise directory; the platform stays the only source of access, keys and quotas.
- LDAP / Active Directory: the realm
deploy/keycloak/realm-enterprise-ai.jsondeclares thecorporate-ldapuser storage provider (read only,vendor: ad, periodic sync) and itsgroup-ldap-mapper. Connection values are${EAI_LDAP_*}placeholders thatkc.sh start --import-realmreplaces with the pod environment (deploy/keycloak/values.yaml):EAI_LDAP_URL,EAI_LDAP_BIND_DN,EAI_LDAP_USERS_DN,EAI_LDAP_GROUPS_DN, andEAI_LDAP_BIND_CREDENTIALread from the Secretkeycloak-directory(keyldap-bind-credential, synced from OpenBao). - Entra ID brokering: identity provider
entra-id(OIDC, PKCE S256, signature checked) withEAI_ENTRA_TENANT_ID,EAI_ENTRA_CLIENT_IDandEAI_ENTRA_CLIENT_SECRET(Secretkeycloak-directory, keyentra-client-secret). The Entragroupsclaim is stored in the user attributeentra_groups. - Claims: the client scope
eai-groups(default scope ofportalandcli) addsgroups(Keycloak group names, LDAP groups included) andentra_groupsto the access token. - Mappings (
/v1/identity/group-mappings, portal Administration > Directory groups): per tenant, a group grants a role on a project and, optionally, a quota profile (/v1/identity/quota-profiles). At each OIDC sign-in carrying a group claim, the Control Plane creates the membership and the user quota (scope_type: user), and revokes them when the group leaves the token, when the mapping is changed or deleted. What a mapping granted is recorded ineai.group_mapping_grants; a membership granted by hand, or assigned or edited by an administrator after the grant, is never removed. Changing a quota profile (PATCH) updates the quotas already granted from it. A token without any group claim changes nothing. Every grant, revocation and change is audited (identity.group_mapping.*,identity.quota_profile.*).EAI_OIDC_GROUP_SYNC=falseturns the synchronisation off. - What stays in LiteLLM: only the providers and models pushed by the platform (
POST /v1/litellm/sync). Team keys and quotas are held by the platform, never in LiteLLM.
SDK usage
Python (enterprise_ai):
from enterprise_ai import EnterpriseAI
client = EnterpriseAI(base_url="<control-plane-url>", api_key="eai_xxx_secret")
items = client.identity.list_members()
TypeScript (@enterprise-ai/sdk):
import { EnterpriseAI } from "@enterprise-ai/sdk";
const eai = new EnterpriseAI({ baseUrl: "<control-plane-url>", apiKey: process.env.EAI_API_KEY });
const items = await eai.members.list();
Java (enterprise-ai-java):
EnterpriseAIClient client = EnterpriseAIClient.builder()
.baseUrl("<control-plane-url>")
.apiKey(System.getenv("EAI_API_KEY"))
.build();
var items = client.members().list();
Go (enterprise-ai-go):
client, _ := eai.NewClient(eai.WithBaseURL("<control-plane-url>"), eai.WithAPIKey(os.Getenv("EAI_API_KEY")))
items, err := client.Members.List(ctx)
Configuration
- Environment:
EAI_DATABASE_URL,EAI_OTEL_EXPORTER_OTLP_ENDPOINT. - Helm values:
controlPlane.enabled,controlPlane.replicas,controlPlane.resources,controlPlane.config.
Security controls
- Authentication by OIDC JWT, tenant API key with scopes, or SPIFFE mTLS; tenant resolved from the credential, never from the body.
- Authorization through OpenFGA and OPA decisions; every mutation and decision writes a hash-chained
eai.audit_eventsrow. - Secrets never returned by the API; TLS everywhere; Cilium network policies restrict who can reach the service.
Observability
- Metrics:
eai_human_identity_requests_total,eai_human_identity_errors_total,eai_human_identity_latency_seconds(Prometheus). - Traces: OpenTelemetry spans
eai.members.*with W3Ctraceparentpropagation, AI spans forwarded to Opik. - Audit actions:
member.assign,member.update,member.remove,policy.evaluate.
FinOps aspects
Operating cost is allocated per tenant and project through labels on Usage Events and Kubernetes resource requests; no direct per-call cost.
Failure modes and resilience
- Stateless replicas behind the API Gateway, PostgreSQL HA for state; readiness probe
GET /readyzremoves unhealthy pods. - External system unavailable: fail closed for security decisions, explicit error code, no silent bypass.
Standalone adoption (client of a client)
A client of a client can adopt this brick alone with the Python SDK sub-client client.identity (TS eai.members, Java client.members(), Go client.Members). Minimal deployment: Control Plane API + PostgreSQL + Keycloak (or API keys), the Control Plane loads every domain of services/control-plane/src/eai/domains; the optional chart components are switched off in deploy/helm/enterprise-ai/values.yaml (vllm.enabled, qdrant.enabled, nats.enabled, workers.enabled).
Acceptance tests
- A tenant cannot read or modify another tenant's resources (RLS test).
- Every mutation produces an audit event verifiable by
GET /v1/audit/verify. - Errors are returned in English and French according to
Accept-Language.
Evidence on real components
See the identity and policy plane guide: components started from the official releases (tools/iam/fetch.sh), tests services/control-plane/tests/test_iam_*.py.