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();