From afc5eefdab7cc273779abd88af5896752fe01182 Mon Sep 17 00:00:00 2001 From: Ted Kim Date: Fri, 25 Sep 2026 20:42:11 -0400 Subject: [PATCH 1/2] feat(cli): manage access tokens under project keys --- docs/README.md | 6 +- docs/access-tokens.md | 39 +++++----- docs/authentication.md | 2 +- internal/cmd/project/access_tokens_test.go | 86 ++++++++++++++++++++++ internal/cmd/project/project.go | 7 +- internal/cmd/project/project_test.go | 2 +- 6 files changed, 117 insertions(+), 25 deletions(-) create mode 100644 internal/cmd/project/access_tokens_test.go diff --git a/docs/README.md b/docs/README.md index 7559528..0eea149 100644 --- a/docs/README.md +++ b/docs/README.md @@ -81,7 +81,7 @@ Piped, CI, and `NO_COLOR` output remains plain. Machine output is unchanged. | Element | CLI can… | Commands | Details | |---|---|---|---| | Account / auth | sign up, log in/out | `signup`, `login`, `logout` | [authentication.md](authentication.md) | -| Access tokens | create, list, get, revoke project credentials | `cloud access-tokens …` | [access-tokens.md](access-tokens.md) | +| Access tokens | create, list, get, revoke project credentials | `projects keys access-tokens …`, `cloud access-tokens …` | [access-tokens.md](access-tokens.md) | | Project | create, list, get, rename, delete, select, get keys and usage | `projects …`, `use` | below | | Functions | deploy, invoke, inspect, schedule, alias | `functions …` | [functions.md](functions.md) | | Durable functions | deploy, start, inspect executions, read logs, schedule | `durable …`, `cloud durable …` | [durable-functions.md](durable-functions.md) | @@ -107,6 +107,7 @@ volcano projects get # details for the active project volcano projects rename eac37d5a-5f6f-42d8-acf6-0f2ae9c7a550 new-name # rename a project volcano projects keys anon list # anon (publishable) API keys for the browser/SDK volcano projects keys service list # backend service-key metadata for the active project +volcano projects keys access-tokens list # project access tokens for CI volcano projects usage # current-month and all-time usage totals volcano projects delete my-app # delete ``` @@ -116,7 +117,8 @@ volcano projects delete my-app # delete ### Project keys -`volcano projects keys` requires an explicit key type. +`volcano projects keys` requires an explicit key type: `anon`, `service`, or +`access-tokens`. See [access-tokens.md](access-tokens.md) for token operations. `volcano projects keys anon list [project-id]` lists publishable anon keys. `volcano projects keys anon create [project-id]` creates a publishable auth-only key using the server default. diff --git a/docs/access-tokens.md b/docs/access-tokens.md index 7ad69a7..ed45edb 100644 --- a/docs/access-tokens.md +++ b/docs/access-tokens.md @@ -20,8 +20,8 @@ every project you own. Only an account token can manage access tokens. - Cannot create, inspect, or revoke tokens, and cannot run account-wide commands: `volcano projects list`, `volcano projects create`, `volcano projects rename`, `volcano projects delete`, selecting a project by name, or - `volcano git connect`. `volcano cloud access-tokens usage` is the exception: - a token can report its own project's consumption, and its own day-by-day + `volcano git connect`. `volcano projects keys access-tokens usage` is the + exception: a token can report its own project's consumption, and its own day-by-day series by ID, so a CI job needs nothing but the credential it already runs with. `get --usage` is not, because reading one token's record is itself a token operation. @@ -32,19 +32,20 @@ every project you own. Only an account token can manage access tokens. | Operation | Command | |---|---| -| Create | `volcano cloud access-tokens create [--scope ] [--expires-at ] [--json]` | -| List | `volcano cloud access-tokens list [--search ] [--include-revoked] [--json]` | -| Get | `volcano cloud access-tokens get [--usage] [--days ] [--json]` | -| Usage | `volcano cloud access-tokens usage [] [--days ] [--json]` | -| Revoke | `volcano cloud access-tokens revoke [--yes]` | +| Create | `volcano projects keys access-tokens create [--scope ] [--expires-at ] [--json]` | +| List | `volcano projects keys access-tokens list [--search ] [--include-revoked] [--json]` | +| Get | `volcano projects keys access-tokens get [--usage] [--days ] [--json]` | +| Usage | `volcano projects keys access-tokens usage [] [--days ] [--json]` | +| Revoke | `volcano projects keys access-tokens revoke [--yes]` | -`tokens` is an alias for `access-tokens`. These are cloud commands: local -development issues no credentials. +`tokens` is an alias for `access-tokens`. `volcano cloud access-tokens` +remains available with the same operations and flags. Both paths target the +current cloud project; local development issues no credentials. ## Create a token ```bash -volcano cloud access-tokens create ci-deploy +volcano projects keys access-tokens create ci-deploy ``` ```text @@ -63,7 +64,7 @@ never the secret, so store it when you create it. Scope the token down and give it an expiry when you can: ```bash -volcano cloud access-tokens create ci-audit \ +volcano projects keys access-tokens create ci-audit \ --scope read_only \ --expires-at 2027-01-31T00:00:00Z ``` @@ -74,7 +75,7 @@ The secret is printed once and never again. For a script, take it from `--json` rather than parsing the human output: ```bash -secret=$(volcano cloud access-tokens create ci-deploy --json | jq -r .token) +secret=$(volcano projects keys access-tokens create ci-deploy --json | jq -r .token) ``` ## Set the right project @@ -126,9 +127,9 @@ project access token (pt-), which only reaches the project it was minted in. Run ## Inspect and revoke ```bash -volcano cloud access-tokens list -volcano cloud access-tokens get ci-deploy --usage --days 7 -volcano cloud access-tokens revoke ci-deploy +volcano projects keys access-tokens list +volcano projects keys access-tokens get ci-deploy --usage --days 7 +volcano projects keys access-tokens revoke ci-deploy ``` `list` shows the tokens that can still authenticate. A token that stopped — @@ -136,7 +137,7 @@ revoked, or past its `--expires-at` — is hidden until you ask for it, and then reports which it was in the `Status` column: ```bash -volcano cloud access-tokens list --include-revoked +volcano projects keys access-tokens list --include-revoked ``` ```text @@ -163,7 +164,7 @@ To compare tokens instead of days, `usage` totals the window for each one, revoked tokens included: ```bash -volcano cloud access-tokens usage --days 7 +volcano projects keys access-tokens usage --days 7 ``` ```text @@ -184,8 +185,8 @@ holds — the whole project, or one token's day-by-day series: ```bash export VOLCANO_TOKEN=pt-Wq9l2m4XcR7tFv1sN8bK3hJ0 export VOLCANO_PROJECT_ID=eac37d5a-5f6f-42d8-acf6-0f2ae9c7a550 -volcano cloud access-tokens usage --days 7 -volcano cloud access-tokens usage 7f1c2e94-2a6b-4c17-9a42-1b0c8f5d3e77 --days 7 +volcano projects keys access-tokens usage --days 7 +volcano projects keys access-tokens usage 7f1c2e94-2a6b-4c17-9a42-1b0c8f5d3e77 --days 7 ``` The series is addressed by token ID, the one `create` printed, because turning diff --git a/docs/authentication.md b/docs/authentication.md index 75351d1..3269548 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -36,7 +36,7 @@ The CLI accepts two kinds of credential: | Credential | Reaches | Get one with | |---|---|---| | Account token (`pk-`) | Every project you own | `volcano login` | -| Project access token (`pt-`) | One project | `volcano cloud access-tokens create` | +| Project access token (`pt-`) | One project | `volcano projects keys access-tokens create` | Both work with `volcano login --token` and with `VOLCANO_TOKEN`. A project access token is the one to give CI: it is scoped to a single project, carries diff --git a/internal/cmd/project/access_tokens_test.go b/internal/cmd/project/access_tokens_test.go new file mode 100644 index 0000000..965156d --- /dev/null +++ b/internal/cmd/project/access_tokens_test.go @@ -0,0 +1,86 @@ +package project + +import ( + "maps" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + cliconfig "github.com/Kong/volcano-cli/internal/config" + cliruntime "github.com/Kong/volcano-cli/internal/runtime" +) + +const projectAccessTokenID = "77777777-7777-4777-8777-777777777777" + +func TestProjectAccessTokenKeys(t *testing.T) { + setProjectCommandTestHome(t) + saveProjectCommandTestConfig(t, &cliconfig.Config{ + UserToken: "token", CurrentProject: &cliconfig.ProjectConfig{ID: projectBetaID, Name: "Beta"}, + }) + + base := "/projects/" + projectBetaID + "/access-tokens" + token := map[string]any{ + "id": projectAccessTokenID, "project_id": projectBetaID, "name": "ci-deploy", + "scope": "full", "status": "active", "token_prefix": "pt-Wq9l2m4X", + "token_source": "cli", "all_time_requests": 42, + "created_at": time.Now().UTC().Format(time.RFC3339), + } + var requests []string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "Bearer token", r.Header.Get("Authorization")) + requests = append(requests, r.Method+" "+r.URL.Path) + switch { + case r.Method == http.MethodPost && r.URL.Path == base: + created := maps.Clone(token) + created["token"] = "pt-secret" + writeProjectCommandJSON(t, w, http.StatusCreated, created) + case r.Method == http.MethodGet && r.URL.Path == base: + assert.Equal(t, "page=1&limit=1", r.URL.RawQuery) + writeProjectCommandJSON(t, w, http.StatusOK, map[string]any{ + "data": []any{token}, "page": 1, "limit": 1, "total": 2, "has_more": true, + }) + case r.Method == http.MethodGet && r.URL.Path == base+"/"+projectAccessTokenID: + writeProjectCommandJSON(t, w, http.StatusOK, token) + case r.Method == http.MethodGet && r.URL.Path == base+"/usage": + writeProjectCommandJSON(t, w, http.StatusOK, []any{}) + case r.Method == http.MethodDelete && r.URL.Path == base+"/"+projectAccessTokenID: + w.WriteHeader(http.StatusNoContent) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + deps := cliruntime.Deps{HTTPClient: server.Client(), APIBaseURL: server.URL} + help, err := executeProjectCommand(t, NewProjects(deps), "keys", "--help") + require.NoError(t, err) + assert.Contains(t, help, "access-tokens") + help, err = executeProjectCommand(t, NewProjects(deps), "keys", "access-tokens", "create", "--help") + require.NoError(t, err) + assert.Contains(t, help, "volcano projects keys access-tokens create ci-deploy") + for _, tc := range []struct { + args []string + want string + }{ + {[]string{"create", "ci-deploy"}, "pt-secret"}, + {[]string{"list", "--limit", "1"}, "Next page: volcano projects keys access-tokens list --page 2 --limit 1"}, + {[]string{"get", projectAccessTokenID}, "Name: ci-deploy"}, + {[]string{"usage"}, "No access tokens created"}, + {[]string{"revoke", projectAccessTokenID, "--yes"}, "Access token 'ci-deploy' revoked"}, + } { + out, err := executeProjectCommand(t, NewProjects(deps), append([]string{"keys", "access-tokens"}, tc.args...)...) + require.NoError(t, err, "%v", tc.args) + assert.Contains(t, out, tc.want) + if tc.args[0] != "create" { + assert.NotContains(t, out, "pt-secret") + } + } + assert.Equal(t, []string{ + "POST " + base, "GET " + base, "GET " + base + "/" + projectAccessTokenID, + "GET " + base + "/usage", "GET " + base + "/" + projectAccessTokenID, + "DELETE " + base + "/" + projectAccessTokenID, + }, requests) +} diff --git a/internal/cmd/project/project.go b/internal/cmd/project/project.go index b85f0e0..978eed6 100644 --- a/internal/cmd/project/project.go +++ b/internal/cmd/project/project.go @@ -12,6 +12,7 @@ import ( "github.com/Kong/volcano-cli/internal/api" "github.com/Kong/volcano-cli/internal/apiclient" + accesstokenscmd "github.com/Kong/volcano-cli/internal/cmd/accesstokens" "github.com/Kong/volcano-cli/internal/confirm" "github.com/Kong/volcano-cli/internal/output" cliproject "github.com/Kong/volcano-cli/internal/project" @@ -207,14 +208,16 @@ func runRename(ctx context.Context, opts renameOptions) error { func newKeys(deps cliruntime.Deps) *cobra.Command { cmd := &cobra.Command{ Use: "keys", - Short: "Manage project anon and service keys", + Short: "Manage project anon, service, and access-token keys", Args: cobra.NoArgs, RunE: func(_ *cobra.Command, _ []string) error { - return errors.New("specify a key type: anon or service") + return errors.New("specify a key type: anon, service, or access-tokens") }, } cmd.AddCommand(newAnonKeys(deps)) cmd.AddCommand(newServiceKeys(deps)) + deps.CommandPathPrefix = "volcano projects keys" + cmd.AddCommand(accesstokenscmd.New(deps)) return cmd } diff --git a/internal/cmd/project/project_test.go b/internal/cmd/project/project_test.go index 77ca83e..9c42436 100644 --- a/internal/cmd/project/project_test.go +++ b/internal/cmd/project/project_test.go @@ -450,7 +450,7 @@ func TestProjectsKeysRequiresKeyType(t *testing.T) { _, err := executeProjectCommand(t, NewProjects(cliruntime.Deps{}), args...) require.Error(t, err) if len(args) == 1 { - assert.ErrorContains(t, err, "specify a key type: anon or service") + assert.ErrorContains(t, err, "specify a key type: anon, service, or access-tokens") } else { assert.ErrorContains(t, err, "unknown command") } From f4cd5f376f420433267cb6b998879958949ce420 Mon Sep 17 00:00:00 2001 From: Ted Kim Date: Fri, 25 Sep 2026 21:52:12 -0400 Subject: [PATCH 2/2] docs(cli): preserve cloud access-token examples --- docs/access-tokens.md | 33 +++++++++++++++++++++------------ docs/authentication.md | 2 +- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/docs/access-tokens.md b/docs/access-tokens.md index ed45edb..0538eba 100644 --- a/docs/access-tokens.md +++ b/docs/access-tokens.md @@ -38,12 +38,21 @@ every project you own. Only an account token can manage access tokens. | Usage | `volcano projects keys access-tokens usage [] [--days ] [--json]` | | Revoke | `volcano projects keys access-tokens revoke [--yes]` | -`tokens` is an alias for `access-tokens`. `volcano cloud access-tokens` -remains available with the same operations and flags. Both paths target the -current cloud project; local development issues no credentials. +`tokens` is an alias for `access-tokens`. Both `volcano cloud access-tokens` +and `volcano projects keys access-tokens` target the current cloud project +and support the same operations and flags. Local development issues no +credentials. ## Create a token +Use either command path to create a token for the current cloud project: + +```bash +volcano cloud access-tokens create ci-deploy +``` + +The same operation is available under project keys: + ```bash volcano projects keys access-tokens create ci-deploy ``` @@ -64,7 +73,7 @@ never the secret, so store it when you create it. Scope the token down and give it an expiry when you can: ```bash -volcano projects keys access-tokens create ci-audit \ +volcano cloud access-tokens create ci-audit \ --scope read_only \ --expires-at 2027-01-31T00:00:00Z ``` @@ -75,7 +84,7 @@ The secret is printed once and never again. For a script, take it from `--json` rather than parsing the human output: ```bash -secret=$(volcano projects keys access-tokens create ci-deploy --json | jq -r .token) +secret=$(volcano cloud access-tokens create ci-deploy --json | jq -r .token) ``` ## Set the right project @@ -127,9 +136,9 @@ project access token (pt-), which only reaches the project it was minted in. Run ## Inspect and revoke ```bash -volcano projects keys access-tokens list -volcano projects keys access-tokens get ci-deploy --usage --days 7 -volcano projects keys access-tokens revoke ci-deploy +volcano cloud access-tokens list +volcano cloud access-tokens get ci-deploy --usage --days 7 +volcano cloud access-tokens revoke ci-deploy ``` `list` shows the tokens that can still authenticate. A token that stopped — @@ -137,7 +146,7 @@ revoked, or past its `--expires-at` — is hidden until you ask for it, and then reports which it was in the `Status` column: ```bash -volcano projects keys access-tokens list --include-revoked +volcano cloud access-tokens list --include-revoked ``` ```text @@ -164,7 +173,7 @@ To compare tokens instead of days, `usage` totals the window for each one, revoked tokens included: ```bash -volcano projects keys access-tokens usage --days 7 +volcano cloud access-tokens usage --days 7 ``` ```text @@ -185,8 +194,8 @@ holds — the whole project, or one token's day-by-day series: ```bash export VOLCANO_TOKEN=pt-Wq9l2m4XcR7tFv1sN8bK3hJ0 export VOLCANO_PROJECT_ID=eac37d5a-5f6f-42d8-acf6-0f2ae9c7a550 -volcano projects keys access-tokens usage --days 7 -volcano projects keys access-tokens usage 7f1c2e94-2a6b-4c17-9a42-1b0c8f5d3e77 --days 7 +volcano cloud access-tokens usage --days 7 +volcano cloud access-tokens usage 7f1c2e94-2a6b-4c17-9a42-1b0c8f5d3e77 --days 7 ``` The series is addressed by token ID, the one `create` printed, because turning diff --git a/docs/authentication.md b/docs/authentication.md index 3269548..75351d1 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -36,7 +36,7 @@ The CLI accepts two kinds of credential: | Credential | Reaches | Get one with | |---|---|---| | Account token (`pk-`) | Every project you own | `volcano login` | -| Project access token (`pt-`) | One project | `volcano projects keys access-tokens create` | +| Project access token (`pt-`) | One project | `volcano cloud access-tokens create` | Both work with `volcano login --token` and with `VOLCANO_TOKEN`. A project access token is the one to give CI: it is scoped to a single project, carries