Ámbitos
Lo que se le permite hacer a una clave.
El vocabulario
Un conjunto cerrado, resource:action. Lo bastante pequeño como para mostrárselo a una persona en una lista de casillas, y lo bastante estable como para que una concesión guardada siga significando lo mismo un año después. Ese mismo vocabulario es con el que se escribe un ROL de espacio de trabajo y el que controla el acceso a las herramientas MCP, de modo que un cliente de solo lectura ni siquiera puede ver una herramienta de envío. Un alfabeto, tres superficies.
| Ámbito | Concede |
|---|---|
| emails:send | Enviar correo |
| emails:read | Leer los mensajes enviados y su estado de entrega |
| drafts:read | Leer borradores |
| drafts:write | Crear y editar borradores |
| threads:read | Leer hilos y mensajes |
| threads:write | Etiquetar, marcar como leídos y archivar hilos |
| labels:read | Leer etiquetas |
| labels:write | Crear y editar etiquetas |
| contacts:read | Leer contactos |
| contacts:write | Añadir, editar y eliminar contactos |
| audiences:read | Leer las audiencias y quién está en ellas |
| audiences:write | Crear y editar audiencias, y cambiar quién está en ellas |
| calendar:read | Leer eventos e invitaciones del calendario |
| calendar:write | Crear, modificar y responder a eventos del calendario |
| templates:read | Leer plantillas de correo y previsualizarlas |
| templates:write | Crear, editar y enviar con plantillas de correo |
| domains:read | Leer los dominios y su estado de DNS |
| domains:write | Verificar y configurar dominios |
| webhooks:read | Leer los endpoints de webhook y sus entregas |
| webhooks:write | Crear, editar y probar webhooks |
| rules:read | Leer las reglas de correo y probarlas |
| rules:write | Crear, editar y reordenar reglas de correo |
| connections:read | Leer qué buzones están conectados |
| members:read | Ver quién está en el espacio de trabajo y qué tiene asignado |
| members:write | Añadir y quitar personas, y cambiar a qué pueden acceder |
| roles:read | Leer los roles que define este espacio de trabajo |
| roles:write | Crear, editar y eliminar roles |
| settings:read | Leer los ajustes del buzón, incluida la firma |
| settings:write | Cambiar los ajustes del buzón y la firma |
| keys:write | Reemplazar su propio secreto sin que nadie abra la consola |
Una clave creada sin una lista de ámbitos meditada recibe emails:send y nada más. El valor predeterminado seguro para una credencial es lo más restringido que la haga útil.
Una clave está limitada por el rol que hay detrás
Una clave puede emitirse contra un ROL, y un rol es un techo, no una segunda concesión. Lo que la clave puede hacer realmente son sus propios ámbitos INTERSECADOS con los permisos de ese rol (key.scopes ∩ role.permissions), calculado una vez en el límite, en cada petición, antes de llegar a ningún endpoint. Nada de lo que hay más abajo sabe que existen los roles: un ámbito que el rol no tiene simplemente no está en la lista que leen las comprobaciones de ámbito.
Así que las dos listas se leen juntas y ninguna gana por sí sola. Una clave que lleva emails:send bajo un rol que no lo tiene no puede enviar; un rol que tiene emails:send no le da nada a una clave que nunca lo pidió. Marcar un ámbito es pedir autoridad, y el rol decide cuánto de lo que pediste recibes.
Una clave SIN rol no tiene techo y, por tanto, es tan amplia como el espacio de trabajo contra el que se emitió. Eso es lo que lleva toda clave creada antes de que existieran los roles y lo que sigue obteniendo un propietario si deja el campo en blanco, así que un rol nulo es el estado MÁS AMPLIO en el que puede estar una clave, no el más restringido. También es la razón por la que eliminar un rol te obliga a decir adónde deben ir sus claves: dejarlas huérfanas ascendería en silencio a todas y cada una de ellas.
La intersección se resuelve en cada petición en lugar de grabarse en la clave en el momento de emitirla. Eso convierte restringir un rol en una revocación en vivo, en vigor en la siguiente llamada del cliente sin necesidad de rotar la clave, y ampliarlo actúa en vivo exactamente igual, que es la mitad que conviene recordar.
GET /ping y GET /keys/self informan de scopes junto a grantedScopes y roleId por un fallo en particular. scopes es la lista efectiva y la única que autoriza algo; grantedScopes es aquello con lo que se emitió la clave. Todo lo que esté en la segunda y falte en la primera se lo quitó el rol, y esa diferencia es la respuesta completa a «mi clave tiene emails:send y estoy recibiendo insufficient_scope». La solución es un cambio de rol, no otra clave.
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"}Hay cinco permisos que nunca pueden llegar a una clave: api-keys:read, api-keys:write, billing:read, billing:write y workspace:manage. Son permisos pero no ámbitos, así que ningún rol, por generoso que sea, puede ponerlos en un token: acuñar otra clave, cambiar lo que otra clave puede hacer o mover el plan es algo que solo hace una persona con la sesión iniciada. Lo único que una clave puede hacerse a sí misma es reemplazar su propio secreto, detrás del ámbito keys:write. GET /roles/permissions marca los cinco con scope: false, que es lo que permite que un mismo componente dibuje tanto la matriz de roles como la lista de casillas de creación de claves.
roles:write es en la práctica todo el vocabulario, y fingir lo contrario sería la documentación más peligrosa. Una clave que lo tenga puede hacer PATCH sobre el propio rol que la limita y concederse todo lo demás, y como el techo se resuelve en cada petición, el más amplio se aplica en la llamada siguiente. Eso no es un agujero que tapar, ya que un editor de roles que no puede editar roles no es un editor de roles. Es una razón para no poner roles:write en una clave que solo necesitaba leer la lista de miembros.
Alcance de envío
Al margen de los ámbitos, una clave puede restringirse en cuanto a la identidad con la que puede enviar. Lleva dos listas. domainAllowlist contiene dominios enteros, y una clave que tiene un dominio puede enviar como cualquier dirección de ese dominio, incluidas las creadas después que la clave. addressAllowlist contiene direcciones sueltas. Deja las dos vacías y la clave es tan amplia como el espacio de trabajo, nunca más. GET /keys/self muestra ambas listas y GET /addresses informa de lo que una clave dada puede usar realmente, que es la respuesta a un from_address_forbidden inexplicable.
El mismo conjunto restringe lo que la clave lee. El correo enviado, el seguimiento y el calendario solo responden para las direcciones con las que la clave puede enviar, así que una clave acotada a un dominio ni envía ni lee en nombre de otro. Un dominio entero permite además que la clave configure el host de seguimiento de ese dominio, algo que una clave limitada a direcciones sueltas no puede hacer.
Tres restricciones, entonces, y se componen en lugar de anularse: los ámbitos de la clave, los permisos del rol que tiene por encima, y los dominios y direcciones que puede poner en una cabecera From. Un envío necesita las tres, y un rechazo nombra solo la primera con la que se topó.