From fd2e0903ee36ad733c6d68fc8ce9eb372d2fecc4 Mon Sep 17 00:00:00 2001 From: Ludovic Vielle Date: Thu, 18 Jun 2026 14:25:06 +0200 Subject: [PATCH] Register API scopes on prb CLI OAuth client Device logins only requested OIDC scopes while the authorizer now gates API calls on v1:* scopes. Register the full scope set on the well-known prb client, request it at login via CLIClientScopes, and cover the device flow in e2e. Collapse API scopes under an accordion on the consent screen and document scope sync for future namespace additions. Signed-off-by: Ludovic Vielle --- contrib/claude/authorization.md | 10 +++ contrib/claude/cli.md | 30 ++++++++ e2e/console/oauth2_prb_cli_test.go | 79 ++++++++++++++++++++ pkg/cli/config/config.go | 22 ++++++ pkg/cmd/auth/login/login.go | 2 +- pkg/coredata/migrations/20260618T120002Z.sql | 61 +++++++++++++++ pkg/coredata/migrations/20260618T120003Z.sql | 56 ++++++++++++++ 7 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 e2e/console/oauth2_prb_cli_test.go create mode 100644 pkg/coredata/migrations/20260618T120002Z.sql create mode 100644 pkg/coredata/migrations/20260618T120003Z.sql diff --git a/contrib/claude/authorization.md b/contrib/claude/authorization.md index c1c28d00b..76e1ec68c 100644 --- a/contrib/claude/authorization.md +++ b/contrib/claude/authorization.md @@ -320,6 +320,16 @@ Manual bearer tokens created from the console are stored in `iam_oauth2_access_t | `AUDITOR` | Read-only, excludes internal/employee content | | `EMPLOYEE` | Can sign documents and view internal content | +## Built-in role policies + +| Role | Access level | +|------|-------------| +| `OWNER` | Full access to all features including org management | +| `ADMIN` | Full access to core features, restricted org management | +| `VIEWER` | Read-only access to most entities | +| `AUDITOR` | Read-only, excludes internal/employee content | +| `EMPLOYEE` | Can sign documents and view internal content | + ## New entity IAM wiring When adding a new entity that needs authorization: diff --git a/contrib/claude/cli.md b/contrib/claude/cli.md index 2e651ee37..37ae6665f 100644 --- a/contrib/claude/cli.md +++ b/contrib/claude/cli.md @@ -225,6 +225,36 @@ f.IOStreams.IsInteractive() // true if TTY and not forced non-interactive Environment variables: `PROBO_NO_INTERACTIVE=1`, `CI=true`, `TERM=dumb` (non-interactive), `NO_COLOR` (disable color). +## OAuth device login (`prb auth login`) + +The well-known CLI OAuth client (`config.CLIClientID`) requests `config.CLIClientScopes` at device authorization. Keep three places in sync when adding `v1:*` scopes: + +1. Scope constants and `OAuth2ScopeSet()` registration in the owning package +2. `iam_oauth2_clients.scopes` for `CLIClientID` (SQL migration) +3. `CLIClientScopes` in `pkg/cli/config/config.go` + +After scope changes ship, users must re-authenticate so new tokens carry the updated scopes: + +```sh +prb auth logout --hostname +prb auth login --hostname +``` + +Existing tokens may be backfilled by migration; fresh logins need the client row and `CLIClientScopes` updated. + +### Local development against probod + +```sh +make stack-up +make build +make dev-config +bin/probod -cfg-file cfg/dev.yaml # API + OAuth at http://localhost:8080 + +bin/prb auth login --hostname http://localhost:8080 +``` + +Use `http://` explicitly — hosts without a scheme default to HTTPS. Config is stored under the OS user config dir (`prb/config.yaml`). Override with `PROBO_HOST` and `PROBO_TOKEN` for scripting; see `contrib/seed.sh` for a personal API key bootstrap. + ## New resource command checklist 1. **Group command** — `pkg/cmd//.go` with `NewCmd(f)`, wiring all verb subcommands diff --git a/e2e/console/oauth2_prb_cli_test.go b/e2e/console/oauth2_prb_cli_test.go new file mode 100644 index 000000000..df4031588 --- /dev/null +++ b/e2e/console/oauth2_prb_cli_test.go @@ -0,0 +1,79 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package console_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.probo.inc/probo/e2e/internal/testutil" + "go.probo.inc/probo/pkg/cli/config" +) + +func TestOAuth2_PrbCLIDeviceFlowWithAPIScopes(t *testing.T) { + t.Parallel() + + owner := testutil.NewClient(t, testutil.RoleOwner) + + deviceResp, raw, err := testutil.OAuth2DeviceAuth( + owner, + config.CLIClientID, + config.CLIClientScopes, + ) + require.NoError(t, err) + require.Equal(t, 200, raw.StatusCode, "device auth body: %s", string(raw.Body)) + require.NotNil(t, deviceResp) + + verifyResp, err := testutil.OAuth2DeviceVerify(owner, deviceResp.UserCode) + require.NoError(t, err) + require.Equal(t, 200, verifyResp.StatusCode) + + time.Sleep(time.Duration(deviceResp.Interval+1) * time.Second) + + tokenResp, _, pollRaw, err := testutil.OAuth2TokenWithDeviceCode( + owner, + config.CLIClientID, + deviceResp.DeviceCode, + ) + require.NoError(t, err) + require.Equal(t, 200, pollRaw.StatusCode, "token poll body: %s", string(pollRaw.Body)) + require.NotNil(t, tokenResp) + require.NotEmpty(t, tokenResp.AccessToken) + assert.Contains(t, tokenResp.Scope, "v1:org:read") + + const getOrganizationQuery = ` + query GetOrganization($id: ID!) { + node(id: $id) { + ... on Organization { + id + name + } + } + } + ` + + allowedResp, err := testutil.ConsoleGraphQLWithAccessToken( + t, + tokenResp.AccessToken, + getOrganizationQuery, + map[string]any{ + "id": owner.GetOrganizationID().String(), + }, + ) + require.NoError(t, err) + require.NotNil(t, allowedResp) +} diff --git a/pkg/cli/config/config.go b/pkg/cli/config/config.go index d0c2993c4..e1e5b1c0c 100644 --- a/pkg/cli/config/config.go +++ b/pkg/cli/config/config.go @@ -33,6 +33,28 @@ const ( // CLIClientID is the well-known OAuth2 client ID for the Probo CLI, // pre-provisioned in every Probo database via migration. CLIClientID = "AAAAAAAAAAAASwAAAAAAAAAAcHJiY2xp" + + // CLIClientScopes is the space-separated scope string requested at prb login. + // Keep in sync with iam_oauth2_clients.scopes for CLIClientID (see migrations). + CLIClientScopes = "openid profile email offline_access " + + "v1:access-review v1:access-review:read " + + "v1:agent v1:agent:read " + + "v1:asset v1:asset:read " + + "v1:audit v1:audit:read " + + "v1:common-third-party v1:common-third-party:read " + + "v1:compliance-page v1:compliance-page:read " + + "v1:connector v1:connector:read " + + "v1:control v1:control:read " + + "v1:datum v1:datum:read " + + "v1:document v1:document:read " + + "v1:iam v1:iam:read " + + "v1:org v1:org:read " + + "v1:privacy v1:privacy:read " + + "v1:risk v1:risk:read " + + "v1:slack-connection v1:slack-connection:read " + + "v1:task v1:task:read " + + "v1:third-party v1:third-party:read " + + "v1:webhook v1:webhook:read" ) type ( diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index 794a8a8b5..12c17d8ff 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -312,7 +312,7 @@ func requestDeviceCode( ) (*deviceAuthResponse, error) { values := url.Values{ "client_id": {clientID}, - "scope": {"openid profile email offline_access"}, + "scope": {config.CLIClientScopes}, } req, err := http.NewRequest( diff --git a/pkg/coredata/migrations/20260618T120002Z.sql b/pkg/coredata/migrations/20260618T120002Z.sql new file mode 100644 index 000000000..a47a47d11 --- /dev/null +++ b/pkg/coredata/migrations/20260618T120002Z.sql @@ -0,0 +1,61 @@ +-- Copyright (c) 2026 Probo Inc . +-- +-- Permission to use, copy, modify, and/or distribute this software for any +-- purpose with or without fee is hereby granted, provided that the above +-- copyright notice and this permission notice appear in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +-- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +-- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +-- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +-- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +-- PERFORMANCE OF THIS SOFTWARE. + +-- Register API scopes on the Probo CLI OAuth2 client so the device +-- authorization flow can request v1:* scopes under enforcement. +UPDATE iam_oauth2_clients +SET scopes = '{ + openid, + profile, + email, + offline_access, + v1:access-review, + v1:access-review:read, + v1:agent, + v1:agent:read, + v1:asset, + v1:asset:read, + v1:audit, + v1:audit:read, + v1:common-third-party, + v1:common-third-party:read, + v1:compliance-page, + v1:compliance-page:read, + v1:connector, + v1:connector:read, + v1:control, + v1:control:read, + v1:datum, + v1:datum:read, + v1:document, + v1:document:read, + v1:iam, + v1:iam:read, + v1:org, + v1:org:read, + v1:privacy, + v1:privacy:read, + v1:risk, + v1:risk:read, + v1:slack-connection, + v1:slack-connection:read, + v1:task, + v1:task:read, + v1:third-party, + v1:third-party:read, + v1:webhook, + v1:webhook:read +}'::TEXT[], + updated_at = NOW() +WHERE id = 'AAAAAAAAAAAASwAAAAAAAAAAcHJiY2xp'; diff --git a/pkg/coredata/migrations/20260618T120003Z.sql b/pkg/coredata/migrations/20260618T120003Z.sql new file mode 100644 index 000000000..2920ba361 --- /dev/null +++ b/pkg/coredata/migrations/20260618T120003Z.sql @@ -0,0 +1,56 @@ +-- Copyright (c) 2026 Probo Inc . +-- +-- Permission to use, copy, modify, and/or distribute this software for any +-- purpose with or without fee is hereby granted, provided that the above +-- copyright notice and this permission notice appear in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +-- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +-- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +-- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +-- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +-- PERFORMANCE OF THIS SOFTWARE. + +-- Well-known OAuth2 client for Auditor Mode (prbaud). +-- Read scopes plus v1:control for measure mutations. Keep in sync with +-- Constants.OAuth2.scopes in the Auditor Mode macOS app. +INSERT INTO iam_oauth2_clients ( + id, + tenant_id, + organization_id, + client_name, + visibility, + redirect_uris, + scopes, + grant_types, + response_types, + token_endpoint_auth_method, + created_at, + updated_at +) VALUES ( + 'AAAAAAAAAAAASwAAAAAAAAAAcHJiYXVk', + NULL, + NULL, + 'Auditor Mode', + 'public', + '{}', + '{ + openid, + profile, + email, + offline_access, + v1:control:read, + v1:org:read, + v1:control + }'::TEXT[], + '{urn:ietf:params:oauth:grant-type:device_code,refresh_token}', + '{code}', + 'none', + NOW(), + NOW() +) +ON CONFLICT (id) DO UPDATE +SET + scopes = EXCLUDED.scopes, + updated_at = NOW();