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 <ludovic@probo.com>
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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 <host>
|
||||
prb auth login --hostname <host>
|
||||
```
|
||||
|
||||
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/<resource>/<resource>.go` with `NewCmd<Resource>(f)`, wiring all verb subcommands
|
||||
|
||||
79
e2e/console/oauth2_prb_cli_test.go
Normal file
79
e2e/console/oauth2_prb_cli_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
@@ -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(
|
||||
|
||||
61
pkg/coredata/migrations/20260618T120002Z.sql
Normal file
61
pkg/coredata/migrations/20260618T120002Z.sql
Normal file
@@ -0,0 +1,61 @@
|
||||
-- Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
--
|
||||
-- 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';
|
||||
56
pkg/coredata/migrations/20260618T120003Z.sql
Normal file
56
pkg/coredata/migrations/20260618T120003Z.sql
Normal file
@@ -0,0 +1,56 @@
|
||||
-- Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
--
|
||||
-- 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();
|
||||
Reference in New Issue
Block a user