Authentification et autorisation
Cette page explique comment la Navin Platform reconnaît une identité (utilisateur, clé d'API, service), comment elle en déduit le tenant, puis comment elle décide d'une action : rôles, portées, habilitation, relations OpenFGA, politiques OPA et délégations. Source : services/control-plane/src/eai/auth.py, services/control-plane/src/eai/policy.py, services/control-plane/src/eai/integrations/openfga.py, services/control-plane/src/eai/domains/identity.
English version: en/authentication-authorization.md
Identifiants acceptés
eai.auth.authenticate lit les identifiants dans cet ordre ; le premier présent décide, aucun autre n'est essayé ensuite.
| Ordre | Identifiant | Méthode (auth_method) | Principal obtenu |
|---|---|---|---|
| 1 | Authorization: Bearer eai_<préfixe>_<secret> | api_key | apikey:<id>, type service |
| 2 | Authorization: Bearer eais_<préfixe>_<secret> (session du portail ouverte par POST /v1/auth/login) | password | local:<identifiant>, type user |
| 3 | Authorization: Bearer <JWT OIDC> | oidc | sub du jeton, type user |
| 4 | X-API-Key: eai_<préfixe>_<secret> | api_key | comme 1 |
| 5 | X-SPIFFE-ID (seulement avec EAI_AUTH_SPIFFE=true et un pair de EAI_TRUSTED_PROXIES) | spiffe | service:<nom>, type service |
| 6 | X-Dev-Tenant, X-Dev-User, X-Dev-Roles (seulement avec EAI_AUTH_DEV=true hors prod) | dev | valeur de X-Dev-User |
Sans identifiant valide, la réponse est 401 unauthenticated avec WWW-Authenticate: Bearer. Chaque 401 compte dans le budget d'échecs de l'adresse IP (EAI_RATE_LIMIT_AUTH_FAILURES_PER_MINUTE) ; au-delà, toute tentative de cette IP reçoit 429 jusqu'à la fin de la fenêtre. Un jeton de plus de 8192 caractères est refusé avant tout décodage. GET /v1/identity/whoami renvoie l'identité résolue (identifiant, type, tenant, rôles, portées, habilitation, méthode).
Identité humaine : Keycloak et OIDC
- Le jeton est vérifié contre les clés JWKS de
EAI_OIDC_JWKS_URLou découvertes depuisEAI_OIDC_ISSUER(cache de 300 s, une seule relecture par fenêtre de 30 s pour unkidinconnu). Revendications obligatoires :exp,iat,iss,sub, etaudquandEAI_OIDC_AUDIENCEest défini. Seuls les algorithmes deEAI_JWT_ALGORITHMSpassent, jamaisHS*ninone. - Rôles : les rôles du jeton (rôles de royaume et du client Keycloak) qui figurent dans
eai.auth.ROLES; un rôle inconnu est ignoré. Les rôles attribués dans la plateforme (eai.memberships) s'y ajoutent, saufplatform_admin. Sans aucun rôle, l'utilisateur estviewer. - Groupes d'administrateurs de l'annuaire : un groupe de
EAI_OIDC_PLATFORM_ADMIN_GROUPS(défautnavin-platform-admins) donneplatform_admin; un groupe<EAI_OIDC_TENANT_ADMIN_GROUP_PREFIX><slug>(défautnavin-admins-) donnetenant_adminsur ce tenant. La comparaison ignore la casse et ne retient que les noms exacts. - Habilitation : la revendication
clearancesi elle vautpublic,internal,confidential,restrictedousecret, sinoninternal. Portées : la revendicationscope, ou*si elle est absente ou contientopenid. - MFA, SAML et déconnexion sont portés par Keycloak : voir identity-policy-plane.md.
Comptes locaux du portail
POST /v1/auth/login vérifie le mot de passe (scrypt) et ouvre une session eais_ affichée une seule fois ; seule son empreinte sha256 est stockée (eai.local_sessions). Durée : EAI_LOCAL_SESSION_TTL_SECONDS. Verrouillage après EAI_LOCAL_AUTH_MAX_FAILURES mots de passe faux pour EAI_LOCAL_AUTH_LOCKOUT_SECONDS. Tant que must_change_password est vrai, la session n'atteint que POST /v1/auth/password, POST /v1/auth/logout, GET /v1/me et GET /v1/identity/whoami (403 password_change_required ailleurs). Changer de mot de passe révoque les autres sessions du compte. Chaque résultat est audité.
Clés d'API
| Opération | Route | Rôles |
|---|---|---|
| Lister | GET /v1/api-keys | tenant_admin, security_officer, developer (un developer ne voit que ses clés) |
| Créer | POST /v1/api-keys | tenant_admin, developer |
| Faire tourner | POST /v1/api-keys/{key_id}/rotate | tenant_admin, security_officer, developer (ses clés seulement) |
| Révoquer | DELETE /v1/api-keys/{key_id} | tenant_admin, security_officer, developer (ses clés seulement) |
- Format
eai_<12 hexadécimaux>_<secret>, vérifié avant toute lecture de la base ; seule l'empreinte sha256 est stockée (eai.api_keys), comparée en temps constant. La valeur complète n'est affichée qu'à la création et à la rotation. - Une clé porte ses rôles, ses portées, son habilitation, un projet, un environnement et une expiration. On ne délègue jamais plus que ce qu'on détient : seul un
platform_admindonneplatform_admin, un appelant sanstenant_adminne donne que ses propres rôles, et chaque portée doit être détenue par l'appelant (403scope_not_held). - La rotation émet une nouvelle clé (mêmes nom, portées et rôles) et révoque l'ancienne, ou la fait expirer après
grace_seconds. Codes de refus : 401invalid_api_key,api_key_revoked,api_key_expired.
Identités de service (SPIFFE)
- Une identité de service (
eai.service_identities) lie un identifiant SPIFFE à un tenant, des rôles, des portées et une habilitation. Création et suppression :POST /v1/service-identitiesetDELETE /v1/service-identities/{identity_id}(tenant_admin,security_officer), avec les mêmes gardes de rôles et de portées que les clés d'API. X-SPIFFE-IDn'est cru que d'un pair de confiance (side-car du maillage) ; un identifiant inconnu ou inactif reçoit 401unknown_spiffe_id. Le SVID est délivré par SPIRE et vérifié en TLS 1.3 pareai.domains.identity.spiffe(liste blanche, refus par défaut).POST /v1/service-identities/{identity_id}/assumedonne à un appelant les droits d'une identité de service, jamais plus que les siens : rôles détenus des deux côtés (jamaisplatform_admin), portées accordées par l'identité et détenues par l'appelant (sinon 403service_identity_scope_exceeds), habilitation la plus basse des deux.
Résolution du tenant
Le tenant vient toujours de l'identifiant, jamais du corps de la requête :
- clé d'API, session locale et identité de service : le tenant enregistré avec l'identifiant ;
- jeton OIDC : la revendication
tenant_id(identifiant) outenant(slug), sinon le seul tenant administré par les groupes de l'annuaire. Plusieurs groupes d'administration sans revendication donnent 403tenant_required; unplatform_adminpeut agir sans tenant.
Un tenant inconnu répond 401 unknown_tenant, un tenant non actif 403 tenant_suspended. Après l'authentification, le registre des kill switches est relu : identifiant révoqué (401 credential_revoked), identité suspendue (403 identity_suspended), tenant suspendu (403 tenant_suspended). Dans la base, eai.db.tx positionne le tenant de la transaction et la RLS forcée filtre chaque ligne.
Rôles
Quinze rôles (eai.auth.ROLES, contrainte de eai.memberships dans sql/postgres/V161). platform_admin passe tout contrôle require_role (Principal.has_role), sauf les contrôles explicites qui l'excluent (déchiffrement des charges d'audit). Les capacités ci-dessous sont relevées dans les contrôles de rôles du code et dans portal/src/lib/kit.ts (ROLES).
| Rôle | Ce qu'il peut faire (exemples relevés dans le code) |
|---|---|
platform_admin | Créer les tenants, routes du Platform Manager et coffre-fort IT du Control Plane (seul rôle admis), licences, révocation des clés de la Context Authority ; jamais le déchiffrement des charges d'audit |
tenant_admin | Administrer le tenant : membres, correspondances de groupes et profils de quota, clés d'API, identités de service, politiques, réglages de sécurité, rétention, DLP, clés de chiffrement, risques, politique de sécurité des fichiers ; approbateur par défaut, responsable SOC, lecteur de preuves |
security_officer | Posture et réglages de sécurité, politiques, relations et identités de service, révocation des clés, DLP, clés de chiffrement, rétention, revue de quarantaine, sécurité des fichiers, déchiffrement des charges d'audit, sceaux et conservations légales, analyste et responsable SOC, approbation d'accès d'urgence |
auditor | Lire l'audit, les décisions, l'état des moteurs, les correspondances de groupes, les analyses de fichiers ; déchiffrer les charges d'audit avec une raison ; sceaux et conservations légales ; ne modifie rien d'autre |
developer | Créer ses clés d'API et ses secrets, simuler les politiques et lire leur historique sans publier, évaluations de risque et exceptions, enregistrer un consentement temps réel ; approbateur par défaut |
finops | Lire et administrer budgets, quotas et coûts ; demander une approbation |
agent | Identité technique d'un agent : demander une approbation, enregistrer un consentement, URL présignées de stockage, lecture des modèles |
viewer | Lecture des modèles, de la FinOps et de l'observabilité ; rôle par défaut sans autre rôle |
soc_analyst | Analyste SOC : alertes, détections, incidents, réactions permises par le garde de proportionnalité |
ai_governance_officer | Évaluations de risque, modèles et agents, qualité et écrans de sécurité en lecture |
data_steward | Politiques de rétention, effacement, conservation légale, revue de quarantaine RAG |
application_owner | Agents et applications dont il est propriétaire (relation owner) |
ai_platform_engineer | Administration et désactivation des fournisseurs, modèles et routes LLM |
business_owner | Approbateur par défaut des demandes (cas d'usage, actions sensibles) |
support_engineer | Observabilité et lecture des modèles, sans écriture |
La matrice de séparation est prouvée par services/control-plane/tests/test_iam_role_matrix.py ; le détail des rôles métiers est dans identity-policy-plane.md.
Portées et habilitation
- Portées :
require_scopeexige une portée du jeton ou de la clé ; les motifs (admin:*,*) sont admis (403insufficient_scope). - Habilitation : cinq niveaux ordonnés
public<internal<confidential<restricted<secret. Le moteur de politique compare la classification des données à l'habilitation du sujet (clearance_required,classification_max).
Relations OpenFGA
- Modèle :
deploy/policies/openfga/model.fga. Types :user,group,tenant,project,agent,model,tool,knowledge_base,dataset,prompt,connector,secret,collection,document,service_identity. Relations :owner,admin(tenant),editor,member,viewer,can_use,can_invoke,can_read. require_relationvérifie la ressource, puis son projet, puis le tenant. Niveau du tenant :tenant_admin=admin,developeretsecurity_officer=editor, les autres =member. Un refus est audité (authz.deny) et répond 403relation_required.- Sans
EAI_OPENFGA_URL, les tuples sont danseai.authz_tuples(tuples directs,user:*, un niveau de groupes,ownerimpliqueeditorimpliqueviewer). Avec OpenFGA, objets et sujets non utilisateurs sont préfixés par l'identifiant du tenant. Gestion :POST /v1/authz/tuples,DELETE /v1/authz/tuples,POST /v1/authz/check(tuples contextuels jamais stockés). - Panne d'OpenFGA : 502
authz_backend_error, refus audité avec la raisonbackend_unavailable.
Politiques OPA et moteur intégré
eai.policy.evaluate décide avant chaque action sensible : le registre des kill switches d'abord, puis les politiques du tenant (une règle deny qui s'applique l'emporte ; des règles allow qui visent l'action sans s'appliquer refusent), puis les gardes de base (habilitation, plafond de classification, régions), puis le défaut EAI_POLICY_DEFAULT ou le réglage policy_default du tenant. Chaque décision est enregistrée (eai.policy_decisions, GET /v1/policies/decisions) et auditée.
| Paquet OPA | Fichier | Décide |
|---|---|---|
eai.agent_autonomy | deploy/policies/opa/agent_autonomy.rego | Runs d'agents et actions autonomes |
eai.data_access | deploy/policies/opa/data_access.rego | Accès RAG et données (ACL, habilitation) |
eai.gateway_access | deploy/policies/opa/gateway_access.rego | Points d'entrée des passerelles hors modèles et outils |
eai.model_invocation | deploy/policies/opa/model_invocation.rego | Chaque appel de modèle |
eai.tool_invocation | deploy/policies/opa/tool_invocation.rego | Chaque appel d'outil |
eai.soc_response | deploy/policies/opa/soc_response.rego | Ordres de confinement SOC |
eai.lib | deploy/policies/opa/lib.rego | Fonctions communes, surcharges par tenant |
Une obligation (dlp, approval) est remontée par POST /v1/policies/evaluate. Publication, simulation et révisions : identity-policy-plane.md.
Délégations, mandats et accès d'urgence
Chaque action d'agent applique l'intersection des droits de l'utilisateur d'origine, de l'agent, de la délégation, de la ressource et de la politique : voir identity-context-delegation.md. Les routes d'administration des délégations sont réservées à tenant_admin et security_officer dans le portail (delegationAdmins).
Correspondances de groupes
eai.domains.identity.group_mappings applique, à chaque authentification OIDC (EAI_OIDC_GROUP_SYNC=true), les correspondances du tenant aux groupes du jeton (revendications groups et entra_groups) : appartenance à un projet, rôle et profil de quota. Ce qu'une correspondance a accordé est enregistré (eai.group_mapping_grants) et retiré quand le groupe quitte le jeton ; les appartenances données à la main ne sont jamais touchées.
| Opération | Route | Rôles |
|---|---|---|
| Lister, lire | GET /v1/identity/group-mappings, GET /v1/identity/group-mappings/{mapping_id} | tout membre du tenant |
| Créer, modifier, supprimer | POST /v1/identity/group-mappings, PATCH /v1/identity/group-mappings/{mapping_id}, DELETE /v1/identity/group-mappings/{mapping_id} | tenant_admin |
| Profils de quota | GET /v1/identity/quota-profiles, POST /v1/identity/quota-profiles | lecture : membre ; écriture : tenant_admin |
Rôles qu'une correspondance peut donner par l'API : tenant_admin, security_officer, finops, developer, auditor, agent, viewer ; jamais platform_admin.
Tests
services/control-plane/tests/test_security_auth.py (JWT, clés d'API), services/control-plane/tests/test_iam_role_matrix.py (séparation des rôles), services/control-plane/tests/test_iam_openfga_real.py, services/control-plane/tests/test_iam_fail_closed.py, services/control-plane/tests/test_iam_keycloak_real.py.