スコープ
キーに許されていること。
語彙
resource:action の形をした閉じた集合です。チェックボックスの一覧として人に見せられるほど小さく、保存された権限が 1 年後も同じ意味を保つほど安定しています。同じ語彙がワークスペースのロールを書くのに使われ、MCP ツールのゲートにも使われるので、読み取り専用のクライアントには送信系のツールがそもそも見えません。1 つのアルファベット、3 つの面です。
| スコープ | 許可される操作 |
|---|---|
| emails:send | メールを送信する |
| emails:read | 送信済みメッセージとその配信状況を読む |
| drafts:read | 下書きを読む |
| drafts:write | 下書きを作成・編集する |
| threads:read | スレッドとメッセージを読む |
| threads:write | スレッドにラベルを付け、既読にし、アーカイブする |
| labels:read | ラベルを読む |
| labels:write | ラベルを作成・編集する |
| contacts:read | 連絡先を読む |
| contacts:write | 連絡先を追加・編集・削除する |
| audiences:read | オーディエンスと、その構成員を読む |
| audiences:write | オーディエンスを作成・編集し、構成員を変更する |
| calendar:read | カレンダーの予定と招待を読む |
| calendar:write | カレンダーの予定を作成・変更し、返答する |
| templates:read | メールテンプレートを読み、プレビューする |
| templates:write | メールテンプレートを作成・編集し、それを使って送信する |
| domains:read | ドメインとその DNS の状態を読む |
| domains:write | ドメインを検証し、設定する |
| webhooks:read | webhook のエンドポイントと配信を読む |
| webhooks:write | webhook を作成・編集し、テストする |
| rules:read | メールルールを読み、テストする |
| rules:write | メールルールを作成・編集し、並べ替える |
| connections:read | どのメールボックスが接続されているかを読む |
| members:read | ワークスペースに誰がいて、何を持っているかを見る |
| members:write | 人を追加・削除し、到達できる範囲を変更する |
| roles:read | このワークスペースが定義するロールを読む |
| roles:write | ロールを作成・編集・削除する |
| settings:read | 署名を含むメールボックスの設定を読む |
| settings:write | メールボックスの設定と署名を変更する |
| keys:write | 誰もコンソールを開くことなく、自身のシークレットを置き換える |
スコープをよく考えずに作成したキーには emails:send だけが付きます。資格情報にとって安全な既定値は、役に立つ最小限のものです。
キーは、その背後のロールに制限される
キーはロールに対して発行でき、ロールは 2 つ目の権限付与ではなく上限です。キーが実際にできることは、自身のスコープとそのロールのパーミッションの積(key.scopes ∩ role.permissions)で、境界でリクエストごとに、どのエンドポイントに到達するよりも前に一度だけ計算されます。その先のコードはロールの存在を知りません。ロールが持たないスコープは、スコープチェックが読む一覧に単に載らないだけです。
したがって 2 つの一覧は合わせて読まれ、どちらか一方だけで決まることはありません。emails:send を持つキーでも、それを持たないロールの下では送信できません。emails:send を持つロールも、それを要求しなかったキーには何も与えません。スコープにチェックを入れることは権限を要求することであり、要求したもののうちどれだけを得られるかはロールが決めます。
ロールを持たないキーには上限がなく、したがって発行元のワークスペースと同じ広さになります。ロールが存在する前に作られたすべてのキーがその状態であり、オーナーがこの欄に触れなければ今もそうなります。つまり null のロールは、キーが取りうる最も「狭い」状態ではなく最も「広い」状態です。ロールを削除するときにキーの移動先を指定させるのも、そのためです。孤立させれば、それらすべてを静かに昇格させてしまいます。
この積は、発行時にキーへ刻み込まれるのではなく、リクエストごとに解決されます。そのためロールを狭めることは、キーをローテーションしなくても呼び出し側の次の呼び出しから効く即時の失効になり、広げるほうもまったく同じように即時です。そちらが覚えておく価値のある半面です。
GET /ping と GET /keys/self が scopes の隣に grantedScopes と roleId を返すのは、ある 1 つの失敗のためです。scopes は実効的な一覧で、何かを認可するのはこれだけです。grantedScopes はキーが発行されたときの一覧です。後者にあって前者にないものはロールが取り上げたものであり、その差こそが「キーには emails:send があるのに insufficient_scope が返る」への答えのすべてです。対処はキーの作り直しではなくロールの変更です。
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"}5 つのパーミッションはキーに一切到達できません。api-keys:read、api-keys:write、billing:read、billing:write、workspace:manage です。これらはパーミッションではありますがスコープではないので、どれほど寛大なロールでもトークンに載せることはできません。別のキーを発行すること、別のキーにできることを変えること、プランを変更することは、サインインした人だけが行うことです。キーが自分自身に対してできる唯一のことは、keys:write スコープの下で自身のシークレットを置き換えることです。GET /roles/permissions はこの 5 つに scope: false を付けます。そのおかげで、ロールのマトリクスとキー作成時のチェックボックス一覧を 1 つのコンポーネントで描画できます。
roles:write は事実上語彙全体に等しく、そうでないふりをするほうが危険なドキュメントになります。これを持つキーは、自分を制限しているまさにそのロールを PATCH して、自分に他のすべてを与えられます。上限はリクエストごとに解決されるので、広げた側は次の呼び出しからすぐ効きます。ロールを編集できないロールエディターはロールエディターではないので、これは塞ぐべき穴ではありません。メンバー一覧を読むだけでよいキーに roles:write を付けない理由だ、ということです。
送信できる範囲
スコープとは別に、キーは送信元にできる範囲でも絞り込めます。キーは 2 つの一覧を持ちます。domainAllowlist はドメイン全体を保持し、あるドメインを持つキーは、キーの作成後に作られたものも含め、そのドメイン上の任意のアドレスとして送信できます。addressAllowlist は個別のアドレスを保持します。どちらも空にしておけば、キーはワークスペースと同じ広さになり、それ以上広くなることはありません。GET /keys/self は両方の一覧を表示し、GET /addresses はそのキーが実際に使えるものを報告します。説明の付かない from_address_forbidden への答えはそこにあります。
同じ集合が、キーの読み取り範囲も狭めます。送信済みメール、トラッキング、カレンダーは、そのキーが送信元にできるアドレスについてのみ応答するので、1 つのドメインに絞られたキーは、別のドメインの代わりに送信することも読むこともできません。ドメイン全体を持つ場合は、そのドメインのトラッキングホストも設定できます。個別のアドレスに限られたキーにはできません。
つまり絞り込みは 3 つあり、互いを上書きせず重ね合わさります。キー自身のスコープ、その上のロールのパーミッション、そして From ヘッダーに入れてよいドメインとアドレスです。送信にはその 3 つすべてが必要で、拒否は最初に引っかかったものだけを示します。