Portées
Ce qu'une clé a le droit de faire.
Le vocabulaire
Un ensemble clos, resource:action. Assez petit pour être montré à un humain sous forme de cases à cocher, et assez stable pour qu'un octroi stocké signifie encore la même chose un an plus tard. C'est dans ce même vocabulaire qu'est écrit un RÔLE d'espace de travail et c'est lui qui garde les outils MCP, si bien qu'un client en lecture seule ne voit même pas un outil d'envoi. Un alphabet, trois surfaces.
| Portée | Accorde |
|---|---|
| emails:send | Envoyer des e-mails |
| emails:read | Lire les messages envoyés et leur statut de livraison |
| drafts:read | Lire les brouillons |
| drafts:write | Créer et modifier des brouillons |
| threads:read | Lire les fils et les messages |
| threads:write | Étiqueter, marquer comme lus et archiver des fils |
| labels:read | Lire les libellés |
| labels:write | Créer et modifier des libellés |
| contacts:read | Lire les contacts |
| contacts:write | Ajouter, modifier et supprimer des contacts |
| audiences:read | Lire les audiences et qui s'y trouve |
| audiences:write | Créer et modifier des audiences, et changer qui s'y trouve |
| calendar:read | Lire les événements et invitations du calendrier |
| calendar:write | Créer et modifier des événements de calendrier, et y répondre |
| templates:read | Lire les modèles d'e-mail et les prévisualiser |
| templates:write | Créer et modifier des modèles d'e-mail, et envoyer avec |
| domains:read | Lire les domaines et leur statut DNS |
| domains:write | Vérifier et configurer des domaines |
| webhooks:read | Lire les endpoints de webhook et les livraisons |
| webhooks:write | Créer, modifier et tester des webhooks |
| rules:read | Lire les règles de courrier et les tester |
| rules:write | Créer, modifier et réordonner les règles de courrier |
| connections:read | Lire quelles boîtes aux lettres sont connectées |
| members:read | Voir qui est dans l'espace de travail et ce que chacun détient |
| members:write | Ajouter et retirer des personnes, et changer ce à quoi elles ont accès |
| roles:read | Lire les rôles que cet espace de travail définit |
| roles:write | Créer, modifier et supprimer des rôles |
| settings:read | Lire les réglages de la boîte, y compris la signature |
| settings:write | Modifier les réglages de la boîte et la signature |
| keys:write | Remplacer son propre secret sans que personne n'ouvre la console |
Une clé créée sans liste de portées réfléchie reçoit emails:send et rien d'autre. La valeur par défaut sûre pour un identifiant est la plus étroite qui le rende utile.
Une clé est plafonnée par le rôle qui la porte
Une clé peut être émise sous un RÔLE, et un rôle est un plafond plutôt qu'un second octroi. Ce que la clé peut réellement faire, ce sont ses propres portées INTERSECTÉES avec les permissions de ce rôle (key.scopes ∩ role.permissions), calculé une fois à la frontière, à chaque requête, avant d'atteindre le moindre endpoint. Rien en aval ne sait que les rôles existent : une portée que le rôle ne détient pas est tout simplement absente de la liste que consultent les contrôles de portée.
Les deux listes se lisent donc ensemble et aucune ne l'emporte seule. Une clé portant emails:send sous un rôle qui ne le détient pas ne peut pas envoyer ; un rôle détenant emails:send n'apporte rien à une clé qui ne l'a jamais demandé. Cocher une portée, c'est demander une autorité, et le rôle décide de la part de ce que vous avez demandé que vous obtenez.
Une clé SANS rôle n'a pas de plafond et est donc aussi large que l'espace de travail sous lequel elle a été émise. C'est ce que porte toute clé créée avant l'existence des rôles et ce qu'un propriétaire obtient encore en laissant le champ tel quel : un rôle null est donc l'état le PLUS large d'une clé, pas le plus étroit. C'est aussi pourquoi supprimer un rôle vous oblige à dire où doivent aller ses clés : les laisser orphelines les promouvrait toutes en silence.
L'intersection est résolue par requête plutôt qu'estampillée sur la clé à l'émission. Cela fait de la restriction d'un rôle une révocation en direct, en vigueur dès l'appel suivant de l'appelant sans rotation de la clé, et de son élargissement quelque chose d'exactement aussi immédiat, ce qui est la moitié qu'il vaut la peine de retenir.
GET /ping et GET /keys/self renvoient scopes à côté de grantedScopes et roleId pour un échec bien précis. scopes est la liste effective et la seule qui autorise quoi que ce soit ; grantedScopes est ce avec quoi la clé a été émise. Tout ce qui figure dans la seconde et manque dans la première a été pris par le rôle, et cet écart est toute la réponse à « ma clé a emails:send et je reçois insufficient_scope ». Le correctif est un changement de rôle, pas une nouvelle clé.
curl "$OE/ping" -H "$AUTH" { "ok": true, "keyId": "4c1b257a66287fd113bd89d0", "mode": "live", "scopes": ["emails:read", "threads:read"], "roleId": "role_c40a95f21cc65d31c2a89e07", "grantedScopes": ["emails:send", "emails:read", "threads:read"], "workspaceId": "10417196-e324-4283-af98-66ec62167c47"}Cinq permissions ne peuvent jamais atteindre une clé : api-keys:read, api-keys:write, billing:read, billing:write et workspace:manage. Ce sont des permissions mais pas des portées : aucun rôle, si généreux soit-il, ne peut les poser sur un jeton, car émettre une autre clé, changer ce qu'une autre clé peut faire ou modifier le plan sont des choses que seule une personne connectée fait. La seule chose qu'une clé peut se faire à elle-même est de remplacer son propre secret, derrière la portée keys:write. GET /roles/permissions marque les cinq avec scope: false, ce qui permet à un seul composant de rendre à la fois la matrice des rôles et la liste de cases à cocher de création de clé.
roles:write équivaut en pratique à tout le vocabulaire, et prétendre le contraire serait la documentation la plus dangereuse. Une clé qui le détient peut faire un PATCH sur le rôle même qui la plafonne et s'octroyer tout le reste, et comme le plafond est résolu par requête, le plus large s'applique dès l'appel suivant. Ce n'est pas une faille à combler, puisqu'un éditeur de rôles qui ne peut pas modifier les rôles n'est pas un éditeur de rôles. C'est une raison de ne pas mettre roles:write sur une clé qui n'avait jamais eu besoin que de lire la liste des membres.
Portée d'envoi
Indépendamment des portées, une clé peut être restreinte dans ce en tant que quoi elle peut envoyer. Elle porte deux listes. domainAllowlist contient des domaines entiers, et une clé détenant un domaine peut envoyer depuis n'importe quelle adresse de ce domaine, y compris des adresses créées après la clé. addressAllowlist contient des adresses individuelles. Laissez les deux vides et la clé est aussi large que l'espace de travail, jamais plus. GET /keys/self montre les deux listes et GET /addresses indique ce qu'une clé donnée peut réellement utiliser, ce qui est la réponse à un from_address_forbidden inexpliqué.
Le même ensemble restreint ce que la clé lit. Le courrier envoyé, le suivi et le calendrier ne répondent que pour les adresses depuis lesquelles la clé peut envoyer : une clé limitée à un domaine n'envoie ni ne lit pour le compte d'un autre. Un domaine entier permet aussi à la clé de définir l'hôte de suivi de ce domaine, ce qu'une clé limitée à des adresses individuelles ne peut pas faire.
Trois restrictions, donc, et elles se composent plutôt qu'elles ne se remplacent : les portées de la clé, les permissions du rôle au-dessus d'elle, et les domaines et adresses qu'elle peut mettre dans un en-tête From. Un envoi a besoin des trois, et un refus ne nomme que la première rencontrée.