From 3b86500d7a47c9a3f43150807dac680774856ba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Tue, 21 Jul 2026 10:54:18 +0200 Subject: [PATCH] Clarify IAM OAuth2 scope mapping name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pkg/iam exports IAMOAuth2ScopeMappings, not OAuth2ScopeMappings. Call out that exception in the agent docs so IAM actions are not left unregistered for OAuth2 callers. Signed-off-by: Émile Ré --- contrib/claude/authorization.md | 8 ++++---- contrib/claude/mcp.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contrib/claude/authorization.md b/contrib/claude/authorization.md index 662d65926..62c31b818 100644 --- a/contrib/claude/authorization.md +++ b/contrib/claude/authorization.md @@ -291,11 +291,11 @@ Scopes are namespace- or product-level only — no resource segments (e.g. `v1:p - Authorization server (RFC 8414): `scopes_supported` on `/.well-known/oauth-authorization-server` lists OIDC + all API scopes; `protected_resources` links to the resource metadata document - Protected resource (RFC 9728): `scopes_supported` on `/.well-known/oauth-protected-resource` lists `openid` plus write API scopes only (no `:read` suffix); matches CIMD client registration -**Enforcement:** OAuth2 bearer-token requests carry the validated access token on the request context (`pkg/iam/oauth2/request_context.go`). Before IAM policy evaluation, `iam.Authorizer` checks registered `oauth2scope.Registry` mappings via `Registry.Allows`. Each domain package exports `OAuth2ScopeMappings` in its `oauth2_scopes.go`; `probod` registers all domain mappings on the shared registry before `iam.NewService`. The check uses explicit scope→action lists — no `:read` / `:get` heuristics at enforcement time. Session, personal API key, and SCIM auth skip the check (no access token on context). Unmapped IAM actions **deny** OAuth requests (fail closed). Enforcement reads scopes from the access token directly. +**Enforcement:** OAuth2 bearer-token requests carry the validated access token on the request context (`pkg/iam/oauth2/request_context.go`). Before IAM policy evaluation, `iam.Authorizer` checks registered `oauth2scope.Registry` mappings via `Registry.Allows`. Each domain package exports an OAuth2 scope mapping in its `oauth2_scopes.go` (`OAuth2ScopeMappings`, or `IAMOAuth2ScopeMappings` in `pkg/iam`); `probod` registers all domain mappings on the shared registry before `iam.NewService`. The check uses explicit scope→action lists — no `:read` / `:get` heuristics at enforcement time. Session, personal API key, and SCIM auth skip the check (no access token on context). Unmapped IAM actions **deny** OAuth requests (fail closed). Enforcement reads scopes from the access token directly. -**When adding or extending IAM actions:** every new action used by GraphQL, MCP, CLI, or n8n must be listed in the owning package's `OAuth2ScopeMappings`. Prefer an existing `v1:` / `v1::read` pair (e.g. commitment CRUD under `v1:compliance-page`). Only introduce a new namespace-level scope when no existing scope fits — then add constants in `oauth2_scopes.go`, map actions in `OAuth2ScopeMappings`, and register the mapping in `probod` wiring. Write scopes are registered only when their mutating IAM actions are mapped. +**When adding or extending IAM actions:** every new action used by GraphQL, MCP, CLI, or n8n must be listed in the owning package's OAuth2 scope mapping (`OAuth2ScopeMappings`, or `IAMOAuth2ScopeMappings` in `pkg/iam`). Prefer an existing `v1:` / `v1::read` pair (e.g. commitment CRUD under `v1:compliance-page`). Only introduce a new namespace-level scope when no existing scope fits — then add constants in `oauth2_scopes.go`, map actions in the package's OAuth2 scope mapping, and register the mapping in `probod` wiring. Write scopes are registered only when their mutating IAM actions are mapped. -E2E MCP tests often authenticate with personal API keys, which skip the OAuth2 scope gate. A green e2e suite does **not** prove OAuth2 clients can call the tool — always update `OAuth2ScopeMappings` when wiring new actions. +E2E MCP tests often authenticate with personal API keys, which skip the OAuth2 scope gate. A green e2e suite does **not** prove OAuth2 clients can call the tool — always update the package's OAuth2 scope mapping when wiring new actions. **Well-known Probo CLI client:** `iam_oauth2_clients` scopes for `AAAAAAAAAAAASwAAAAAAAAAAcHJiY2xp` must match `CLIClientScopes` in `pkg/cli/config/config.go` (requested by `prb auth login`). When adding API scopes, update the client migration, `CLIClientScopes`, and scope registration together. @@ -338,7 +338,7 @@ When adding a new entity that needs authorization: 1. **Action constants** — add `core::` constants in `pkg/probo/actions.go` (get, list, create, update, delete) 2. **Role policies** — wire actions into the appropriate role policies in `pkg/probo/policies.go` (`OwnerPolicy`, `AdminPolicy`, `ViewerPolicy`, etc.) with `organization_id` condition -3. **OAuth2 scope mappings** — add every new action to the owning package's `OAuth2ScopeMappings` (`pkg//oauth2_scopes.go`). Put list/get on `v1::read` and mutating verbs on `v1:`. Reuse an existing namespace when the feature belongs to one (e.g. compliance-page commitments → `v1:compliance-page`). Unmapped actions deny all OAuth2 callers (MCP included) even when role policies allow them. +3. **OAuth2 scope mappings** — add every new action to the owning package's OAuth2 scope mapping in `pkg//oauth2_scopes.go` (`OAuth2ScopeMappings`, or `IAMOAuth2ScopeMappings` in `pkg/iam`). Put list/get on `v1::read` and mutating verbs on `v1:`. Reuse an existing namespace when the feature belongs to one (e.g. compliance-page commitments → `v1:compliance-page`). Unmapped actions deny all OAuth2 callers (MCP included) even when role policies allow them. 4. **`AuthorizationAttributes`** — implement on the `coredata` entity struct, returning at minimum `{"organization_id": ...}` (use the denormalized `OrganizationID` field — see coredata doc) 5. **Entity type registry** — register in `pkg/coredata/entity_type_reg.go` and `NewEntityFromID` so the authorizer can construct the entity from its GID 6. **Resolver calls** — add `scope, err := r.authorize(ctx, id, probo.ActionEntityGet)` in GraphQL resolvers and `scope, err := r.Authorize(ctx, id, probo.ActionEntityGet)` in MCP resolvers, then pass `scope` to services diff --git a/contrib/claude/mcp.md b/contrib/claude/mcp.md index fea77b0e9..718494734 100644 --- a/contrib/claude/mcp.md +++ b/contrib/claude/mcp.md @@ -71,7 +71,7 @@ if err != nil { } ``` -MCP clients commonly authenticate with OAuth2 access tokens. Every action passed to `Authorize` must also appear in the owning package's `OAuth2ScopeMappings` (see [OAuth2 API scopes](authorization.md#oauth2-api-scopes)). Role policies alone are not enough — unmapped actions fail closed with insufficient scope. Personal API key e2e clients skip this gate, so do not treat a green MCP e2e as proof that OAuth2 works. +MCP clients commonly authenticate with OAuth2 access tokens. Every action passed to `Authorize` must also appear in the owning package's OAuth2 scope mapping (`OAuth2ScopeMappings`, or `IAMOAuth2ScopeMappings` in `pkg/iam`; see [OAuth2 API scopes](authorization.md#oauth2-api-scopes)). Role policies alone are not enough — unmapped actions fail closed with insufficient scope. Personal API key e2e clients skip this gate, so do not treat a green MCP e2e as proof that OAuth2 works. ## Common resolver patterns