Aller au contenu principal

ADR-0156 : Hiérarchie du tenant, champs communs stampés par PostgreSQL et identifiant runtime séparé

Contexte​

La référence d'architecture place sous le tenant des organisations et des unités métier (A6.1 cœur, A6.2 « tenant vers organisations, projets, environnements »), demande sur les tables métier les champs communs environment_id, updated_by, version, metadata validé et correlation_id (B7.2), et sépare les connexions administrateur et runtime (B7.3).

Avant cette décision, eai.organizations et eai.business_units n'existaient pas, l'environnement n'était qu'un texte (usage_events.environment, application_deployments.environment), updated_by et version n'étaient portés que par quelques tables, metadata n'était validé nulle part en base, et l'API comme les migrations utilisaient le même identifiant de connexion (EAI_DATABASE_URL).

Décision​

  1. Hiérarchie (V162) : eai.organizations (tenant, slug unique par tenant, statut active ou archived) et eai.business_units (organisation, parent_id facultatif pour l'imbrication, fixé à la création pour qu'aucun cycle ne puisse apparaître). Un projet se rattache à une organisation et à une de ses unités (projects.organization_id, projects.business_unit_id). Les clés étrangères sont composées avec tenant_id ou organization_id : un contrôle de clé étrangère ne passe pas par la RLS, donc seule une clé composée empêche, même en contexte plateforme, de rattacher un projet ou une unité à l'organisation d'un autre tenant. RLS forcée sur les deux tables.
  2. Routes tenancy : GET et POST /v1/organizations, PATCH /v1/organizations/{organization_id}, GET et POST /v1/business-units, PATCH /v1/business-units/{business_unit_id}, et organization_id, business_unit_id, metadata sur les projets. tenant_admin écrit, tout rôle du tenant lit ; chaque écriture est auditée.
  3. Champs communs stampés par la base (V162, V164) : le déclencheur eai.stamp_common_fields() écrit created_by et updated_by depuis l'acteur vérifié (app.actor_id), incrémente version à chaque mise à jour et écrit trace_id (le correlation_id de la référence, nom déjà utilisé par audit_events, usage_events et telemetry_spans) depuis app.correlation_id, que eai.db.tx fixe à partir de la trace W3C de la requête. Il s'applique à organizations, business_units, projects, environments, agents, applications et incidents. Un appelant ne peut ni choisir sa version ni effacer created_by. Les routes PATCH acceptent expected_version : une version périmée donne 409 version_conflict.
  4. metadata est validé en base par eai.valid_metadata() (contrainte CHECK) : objet plat, clés en minuscules (^[a-z][a-z0-9_.-]{0,62}$), valeurs texte (1024 caractères au plus), nombre, booléen ou null, 16 Ko au plus. Une contrainte CHECK a été préférée à une extension JSON Schema (pg_jsonschema) : aucune extension à installer chez le client, et la règle tient pour tout écrivain, l'API comme un script. Une métadonnée refusée donne 422 invalid_metadata.
  5. environment_id (V163) : usage_events et application_deployments portent une clé étrangère vers eai.environments (ON DELETE SET NULL), calculée en base par déclencheur depuis (tenant, projet par id ou par slug, nom d'environnement). La colonne texte reste pour les lignes dont le projet n'a pas cet environnement : environment_id vaut alors NULL, jamais une supposition. Une valeur fournie par l'écrivain est ignorée et recalculée dans le tenant de la ligne.
  6. Identifiant runtime (V165) : le rôle groupe eai_runtime (NOLOGIN, membre de eai_app, sans CREATE sur aucun schéma, propriétaire de rien, lecture seule de schema_migrations). L'exploitant crée le login de l'API (CREATE ROLE eai_api LOGIN PASSWORD '...' IN ROLE eai_runtime) et le donne dans EAI_RUNTIME_DATABASE_URL : le pool des requêtes et le worker l'utilisent ; eai.migrate garde toujours EAI_DATABASE_URL, l'identifiant d'administration. Sans EAI_RUNTIME_DATABASE_URL, le comportement antérieur reste (un seul identifiant, RLS par SET LOCAL ROLE eai_app), pour ne casser aucune installation ; le contrôle de démarrage en production exige TLS sur les deux URL.

Conséquences​

  • Les réponses des projets, environnements, agents, applications et incidents gagnent created_by, updated_by, version, metadata et trace_id.
  • Un client qui écrit en parallèle sur un même projet ou une même organisation peut passer expected_version pour ne jamais écraser une modification.
  • La clé EAI_RUNTIME_DATABASE_URL s'ajoute au secret OpenBao du Control Plane quand l'exploitant crée le login runtime ; le chart n'impose pas encore cette clé, pour ne pas bloquer la synchronisation d'un secret existant.

Preuves​

services/control-plane/tests/test_tenancy_hierarchy.py (hiérarchie, isolation entre tenants jusqu'aux clés composées, champs stampés, concurrence optimiste, métadonnées refusées en base, environment_id résolu et recalculé, login runtime sans DDL qui sert l'API pendant que la migration le refuse), services/control-plane/tests/test_data_model_mapping.py, services/control-plane/tests/test_security_attack_suite.py, et les tests de forme de requête des quatre SDK.