ArchitectureLa console d'octroi de mandat

La console d'octroi de mandat

Comment on ACCORDE un acces d'equipe. Le pendant cote operateur de content/handbook/fr/premier-jour.mdx, qui dit comment on le RECOIT. Livre par security-identity/2760. Le modele lui-meme (portees, mandats, resolveur)…

Comment on ACCORDE un acces d'equipe. Le pendant cote operateur de content/handbook/fr/premier-jour.mdx, qui dit comment on le RECOIT.

Livre par security-identity/2760. Le modele lui-meme (portees, mandats, resolveur) est decrit par ADR 0013 et acces-delegue-et-frontiere-studio.md.

Le defaut que cet ecran ferme

Le mecanisme d'acces d'equipe etait complet : AccessGrant, la portee platform, platformPermissionsFor, requirePlatformPermission, et des pages /ops qui nomment chacune leur permission. Il n'y avait aucun ecran pour accorder.

Le seul chemin etait POST /api/admin/access/grants avec un JSON compose a la main, portant un subjectUserId brut. Recruter la personne qui relit les posts demandait donc de la faire s'inscrire, retrouver son id en base, composer le bon scopeType, la bonne liste d'allow et une echeance. Consequence mesuree : /ops/content tournait en production et son unique porteur possible etait le proprietaire, qui n'en a pas besoin.

Ou ca se passe

/admin/people/access, quatrieme outil de la categorie Personnes.

Role ADMIN uniquement, et c'est delibere : accorder reste un geste du proprietaire. L'en-tete de src/app/api/admin/access/grants/route.ts explique pourquoi l'octroi n'est pas encore ouvert aux owners d'organisation — /api/me livre aujourd'hui le roster avec les e-mails, l'abonnement et le solde, donc une console d'octroi sans projection scopee serait une fuite avec un bouton.

Le geste, en quatre temps

  1. Chercher la personne par son e-mail. La recherche porte sur la table User entiere, cote serveur, et l'ecran ne montre jamais un id. Un cuid ne se relit pas : accorder au mauvais compte ne se verrait qu'apres coup.
  2. Cocher un ou plusieurs POLES. Un pole est une constante du code (GRANT_PRESETS, scope: "platform"), pas une liste tapee dans l'ecran. Les platform.* qu'il porte sont affichees sous sa case : le mandat se relit avant d'etre emis.
  3. Poser une echeance en jours. Plafonnee par MAX_GRANT_DAYS (180). L'ecran l'annonce ; c'est createGrant qui refuse. Un ecran qui recalcule une regle de securite est une deuxieme regle a maintenir.
  4. Accorder. La ligne apparait dans « Mandats vivants », avec son echeance et un bouton de revocation.

Les poles, et pourquoi il y en a trois

PoleCe qu'il ouvre
Contenu et growthplatform.content.operate, platform.growth.manage, platform.bulletin.approve
Production creativeplatform.creative.review
Veille marcheplatform.hub.read

Un pole est derive du REGISTRE des permissions, jamais d'un organigramme. Il n'existe aujourd'hui aucune platform.* de support ni de revenus, donc un pole « Support » serait une case a cocher qui n'accorde rien — et createGrant refuse d'ailleurs un mandat vide.

src/test/grant-poles-cover-platform-permissions.test.ts refuse qu'une platform.* du registre n'appartienne a aucun pole : une permission qu'aucun ecran ne sait accorder est une surface injoignable, ce qui etait exactement le defaut du jour.

Une adresse sans compte est un cas normal

AccessGrant.subjectUserId est nullable, et subjectEmail vit a cote. Accorder a une adresse inconnue emet un mandat EN ATTENTE : la ligne se lie au User a la premiere connexion de cette adresse (linkPendingGrants, appelee depuis l'event signIn de NextAuth).

Un mandat en attente n'accorde RIEN, et cela tient par la FORME de la requete, pas par un filtre a maintenir : toutes les lectures du resolveur filtrent sur subjectUserId, et un NULL n'y correspond jamais. La colonne Etat de la liste le dit en toutes lettres plutot que de laisser lire un acces qui n'existe pas encore.

Ce que l'ecran n'ajoute pas

Aucune nouvelle porte d'ecriture. Les deux server actions (grantPlatformAccess, revokePlatformGrant) appellent les fonctions existantes de src/lib/security/access-grants.ts, qui portent deja :

  • les trois refus (NON_DELEGABLE, une platform.* hors portee plateforme, une portee plateforme emise par une organisation) ;
  • le plafond de duree et le refus d'une echeance passee ;
  • le journal des quatre actes (grant.granted, grant.revoked, grant.narrowed, grant.renewed), ecrit DANS le module.

Les deux actions portent requireAdmin chacune — un export "use server" est une entree HTTP publique — et ajoutent une ligne logAdminAction : qui a accorde quoi, a qui, jusqu'a quand.

La revocation passe par grantBelongsToScopes(id, ["platform"]) avant revokeGrant : revoquer se fait par id, et un id ne dit pas d'ou il vient. Sans ce controle, la console plateforme pourrait couper le mandat de marque d'un prestataire.

Ce qu'une revocation ne fait pas

Elle ferme l'acces des la requete suivante (rien n'est mis en cache au-dela de React.cache). Elle ne reprend pas les octets deja copies : les assets Studio sont des URLs Blob publiques sans TTL. Couper un acces borne la duree d'exposition d'une PAGE, pas celle d'un fichier que quelqu'un a deja enregistre.

La porte que le porteur voit

Accorder ne suffit pas : encore faut-il que la personne TROUVE son acces. Jusqu'a security-identity/2803, le seul lien vers /ops de toute l'application authentifiee vivait dans le menu de compte (src/components/shared/account/dropdown.tsx), sous isAdminLike(userRole) — exactement la condition que le mandat existe pour eviter. Les deux autres liens partaient de pages /admin, gardees sur le meme role. Une recrue mandatee devait donc connaitre l'URL par coeur, et un acces qu'on ne peut pas trouver est un acces qui n'existe pas.

Le menu porte desormais une entree « Siege » separee de l'entree « Admin », les deux pouvant coexister : un admin plateforme tient souvent les deux. Sa condition est un booleen resolu SERVEUR par GET /api/me/ops-access, qui appelle platformPermissionsFor et ne rend qu'un bit — jamais la liste, que le navigateur n'a pas a interpreter (client-does-not-resolve-permissions.test.ts). Une route a part plutot qu'un champ de /api/me : un mandat ne depend d'aucune organisation et n'a pas a faire grossir le chemin de lecture le plus chaud du produit.

L'entree ne nomme aucune surface. Le rail du cockpit filtre deja ce que le porteur peut ouvrir (OPS_ROUTES), et chaque page porte sa propre porte. src/test/ops-door-is-not-gated-on-a-role.test.ts garde la chaine entiere : condition sans role dans le menu, verdict recu en prop et non recalcule, endpoint resolu par les mandats.

Ce qui reste a faire

L'octroi par le proprietaire d'une organisation (phase 5) attend la projection scopee de /api/me. Le panneau prestataires de ~/members couvre deja l'octroi a l'echelle d'une ORGANISATION, avec ses presets tenant ; la portee platform reste emise ici, et par un admin.