From be7dea3e292c124276966d63347826b41d8aa7f4 Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Wed, 28 Oct 2026 22:43:14 +0000 Subject: [PATCH 1/8] feat: list personal API keys with `kosli list api-keys --personal` The server now exposes /api/v2/user/api-keys for a user's own keys. `--personal` targets them instead of a service account's keys; it is mutually exclusive with `--service-account`, one of the two is required, and `--org` is not needed since personal keys belong to the user rather than an org. The owner selection lives in a shared apiKeyOwner so the other api-key commands can take the same flag. --- cmd/kosli/apiKey.go | 35 +++++++++++++++++++++++++++++++++++ cmd/kosli/apiKey_test.go | 38 ++++++++++++++++++++++++++++++++++---- cmd/kosli/listApiKeys.go | 23 +++++++++++------------ cmd/kosli/root.go | 3 ++- 4 files changed, 82 insertions(+), 17 deletions(-) diff --git a/cmd/kosli/apiKey.go b/cmd/kosli/apiKey.go index 0dfd62274..b43ee0c96 100644 --- a/cmd/kosli/apiKey.go +++ b/cmd/kosli/apiKey.go @@ -4,10 +4,45 @@ import ( "encoding/json" "fmt" "io" + "net/url" "strconv" "time" + + "github.com/spf13/cobra" ) +// apiKeyOwner selects whose API keys a command manages: a service account's, +// or with --personal the caller's own. +type apiKeyOwner struct { + serviceAccount string + personal bool +} + +func (o *apiKeyOwner) addFlags(cmd *cobra.Command) { + cmd.Flags().StringVarP(&o.serviceAccount, "service-account", "s", "", serviceAccountNameFlag) + cmd.Flags().BoolVar(&o.personal, "personal", false, personalApiKeyFlag) + cmd.MarkFlagsMutuallyExclusive("service-account", "personal") + cmd.MarkFlagsOneRequired("service-account", "personal") +} + +// requiredGlobalFlags leaves out Org for personal keys, which belong to the +// user rather than to an org. +func (o *apiKeyOwner) requiredGlobalFlags() []string { + if o.personal { + return []string{"ApiToken"} + } + return []string{"Org", "ApiToken"} +} + +// url builds the owner's API key collection URL, followed by elem. +func (o *apiKeyOwner) url(elem ...string) (string, error) { + base := []string{"api/v2/service-accounts", global.Org, o.serviceAccount, "api-keys"} + if o.personal { + base = []string{"api/v2/user/api-keys"} + } + return url.JoinPath(global.Host, append(base, elem...)...) +} + // apiKeyResponse models the JSON returned by the create and rotate endpoints. // The key value is only ever returned once, at creation/rotation time. type apiKeyResponse struct { diff --git a/cmd/kosli/apiKey_test.go b/cmd/kosli/apiKey_test.go index 386b92b86..3deed14e2 100644 --- a/cmd/kosli/apiKey_test.go +++ b/cmd/kosli/apiKey_test.go @@ -550,10 +550,40 @@ func (suite *ApiKeyCommandTestSuite) TestApiErrorsAreSurfaced() { func (suite *ApiKeyCommandTestSuite) TestListApiKeysCmd() { tests := []cmdTestCase{ { - wantError: true, - name: "list fails when --service-account is missing", - cmd: "list api-keys" + suite.defaultKosliArguments, - golden: "Error: required flag(s) \"service-account\" not set\n", + wantError: true, + name: "list fails when neither --service-account nor --personal is set", + cmd: "list api-keys" + suite.defaultKosliArguments, + goldenRegex: `Error: at least one of the flags in the group \[service-account personal\] is required`, + }, + { + wantError: true, + name: "list fails when both --service-account and --personal are set", + cmd: "list api-keys --service-account test-sa --personal" + suite.defaultKosliArguments, + goldenRegex: `Error: if any flags in the group \[service-account personal\] are set none of the others can be`, + }, + } + + runTestCmd(suite.T(), tests) +} + +func (suite *ApiKeyCommandTestSuite) TestPersonalApiKeysCmd() { + fake := httpfake.New() + defer fake.Close() + fake.NewHandler(). + Get("/api/v2/user/api-keys"). + Reply(200). + BodyString(apiKeyFixture(suite.T(), "listed_api_keys.json")) + + // No --org: personal keys belong to the user, not to an org. + args := fmt.Sprintf(" --host %s --api-token %s", fake.Server.URL, global.ApiToken) + tests := []cmdTestCase{ + { + wantError: false, + name: "list --personal prints the caller's keys without --org", + cmd: "list api-keys --personal --output json" + args, + goldenJson: []jsonCheck{ + {Path: "[0].id", Want: "id-1"}, + }, }, } diff --git a/cmd/kosli/listApiKeys.go b/cmd/kosli/listApiKeys.go index 69bdb2082..9f599c731 100644 --- a/cmd/kosli/listApiKeys.go +++ b/cmd/kosli/listApiKeys.go @@ -5,14 +5,13 @@ import ( "fmt" "io" "net/http" - "net/url" "github.com/kosli-dev/cli/internal/output" "github.com/kosli-dev/cli/internal/requests" "github.com/spf13/cobra" ) -const listApiKeysShortDesc = `List API keys for a service account.` +const listApiKeysShortDesc = `List API keys for a service account, or your own personal API keys.` const listApiKeysLongDesc = listApiKeysShortDesc + ` @@ -25,11 +24,16 @@ kosli list api-keys \ --service-account yourServiceAccountName \ --api-token yourAPIToken \ --org yourOrgName + +# list your own personal API keys: +kosli list api-keys \ + --personal \ + --api-token yourAPIToken ` type listApiKeysOptions struct { - serviceAccount string - output string + owner apiKeyOwner + output string } func newListApiKeysCmd(out io.Writer) *cobra.Command { @@ -42,7 +46,7 @@ func newListApiKeysCmd(out io.Writer) *cobra.Command { Example: listApiKeysExample, Args: cobra.NoArgs, PreRunE: func(cmd *cobra.Command, args []string) error { - if err := RequireGlobalFlags(global, []string{"Org", "ApiToken"}); err != nil { + if err := RequireGlobalFlags(global, o.owner.requiredGlobalFlags()); err != nil { return ErrorBeforePrintingUsage(cmd, err.Error()) } return nil @@ -52,19 +56,14 @@ func newListApiKeysCmd(out io.Writer) *cobra.Command { }, } - cmd.Flags().StringVarP(&o.serviceAccount, "service-account", "s", "", serviceAccountNameFlag) + o.owner.addFlags(cmd) cmd.Flags().StringVarP(&o.output, "output", "o", "table", outputFlag) - err := RequireFlags(cmd, []string{"service-account"}) - if err != nil { - logger.Error("failed to configure required flags: %v", err) - } - return cmd } func (o *listApiKeysOptions) run(out io.Writer, args []string) error { - url, err := url.JoinPath(global.Host, "api/v2/service-accounts", global.Org, o.serviceAccount, "api-keys") + url, err := o.owner.url() if err != nil { return err } diff --git a/cmd/kosli/root.go b/cmd/kosli/root.go index db0a779a8..074431e5a 100644 --- a/cmd/kosli/root.go +++ b/cmd/kosli/root.go @@ -171,7 +171,8 @@ Paths the list already matches stay excluded whatever is later added there, so k outputFlagWithMarkdown = "[defaulted] The format of the output. Valid formats are: [table, json, markdown]." searchByNameFlag = "[optional] Only list flows whose name contains this substring. The Kosli API supports alphanumeric characters and '-'." ignoreCaseFlag = "[optional] Perform case-insensitive matching for --name. By default matching is case sensitive." - serviceAccountNameFlag = "The name of the service account whose API keys are managed." + serviceAccountNameFlag = "The name of the service account whose API keys are managed. Either this or --personal is required." + personalApiKeyFlag = "Manage your own personal API keys instead of a service account's. --org is not needed. Either this or --service-account is required." serviceAccountDescriptionFlag = "[optional] A description for the service account." serviceAccountPrivilegeFlag = "The privilege granted to the service account. One of: [" + validServiceAccountPrivilegesList + "]." serviceAccountAssumeYesFlag = "[optional] Skip the confirmation prompt and delete the service account without asking. (alias: --yes)" From 4f6ae5db7da998b700059fe335f6d14a5c0167aa Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Wed, 28 Oct 2026 22:43:17 +0000 Subject: [PATCH 2/8] feat: get a personal API key with `kosli get api-key --personal` Reads one of the caller's own keys from /api/v2/user/api-keys/KEY-ID, using the same --personal/--service-account selection as list. --- cmd/kosli/apiKey_test.go | 20 ++++++++++++++++---- cmd/kosli/getApiKey.go | 23 +++++++++++------------ 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/cmd/kosli/apiKey_test.go b/cmd/kosli/apiKey_test.go index 3deed14e2..fd1995681 100644 --- a/cmd/kosli/apiKey_test.go +++ b/cmd/kosli/apiKey_test.go @@ -291,10 +291,10 @@ func (suite *ApiKeyCommandTestSuite) TestDeleteApiKeyCmd() { func (suite *ApiKeyCommandTestSuite) TestGetApiKeyCmd() { tests := []cmdTestCase{ { - wantError: true, - name: "get fails when --service-account is missing", - cmd: "get api-key key-123" + suite.defaultKosliArguments, - golden: "Error: required flag(s) \"service-account\" not set\n", + wantError: true, + name: "get fails when neither --service-account nor --personal is set", + cmd: "get api-key key-123" + suite.defaultKosliArguments, + goldenRegex: `Error: at least one of the flags in the group \[service-account personal\] is required`, }, { wantError: true, @@ -573,6 +573,10 @@ func (suite *ApiKeyCommandTestSuite) TestPersonalApiKeysCmd() { Get("/api/v2/user/api-keys"). Reply(200). BodyString(apiKeyFixture(suite.T(), "listed_api_keys.json")) + fake.NewHandler(). + Get("/api/v2/user/api-keys/id-1"). + Reply(200). + BodyString(apiKeyFixture(suite.T(), "api_key.json")) // No --org: personal keys belong to the user, not to an org. args := fmt.Sprintf(" --host %s --api-token %s", fake.Server.URL, global.ApiToken) @@ -585,6 +589,14 @@ func (suite *ApiKeyCommandTestSuite) TestPersonalApiKeysCmd() { {Path: "[0].id", Want: "id-1"}, }, }, + { + wantError: false, + name: "get --personal prints one of the caller's keys without --org", + cmd: "get api-key id-1 --personal --output json" + args, + goldenJson: []jsonCheck{ + {Path: "id", Want: "id-1"}, + }, + }, } runTestCmd(suite.T(), tests) diff --git a/cmd/kosli/getApiKey.go b/cmd/kosli/getApiKey.go index b8ac5feb8..69b92334a 100644 --- a/cmd/kosli/getApiKey.go +++ b/cmd/kosli/getApiKey.go @@ -3,14 +3,13 @@ package main import ( "io" "net/http" - "net/url" "github.com/kosli-dev/cli/internal/output" "github.com/kosli-dev/cli/internal/requests" "github.com/spf13/cobra" ) -const getApiKeyShortDesc = `Get an API key's metadata for a service account.` +const getApiKeyShortDesc = `Get an API key's metadata for a service account, or one of your own personal API keys.` const getApiKeyLongDesc = getApiKeyShortDesc + ` @@ -23,11 +22,16 @@ kosli get api-key yourApiKeyID \ --service-account yourServiceAccountName \ --api-token yourAPIToken \ --org yourOrgName + +# get the metadata of one of your own personal API keys: +kosli get api-key yourApiKeyID \ + --personal \ + --api-token yourAPIToken ` type getApiKeyOptions struct { - serviceAccount string - output string + owner apiKeyOwner + output string } func newGetApiKeyCmd(out io.Writer) *cobra.Command { @@ -40,7 +44,7 @@ func newGetApiKeyCmd(out io.Writer) *cobra.Command { Example: getApiKeyExample, Args: cobra.ExactArgs(1), PreRunE: func(cmd *cobra.Command, args []string) error { - if err := RequireGlobalFlags(global, []string{"Org", "ApiToken"}); err != nil { + if err := RequireGlobalFlags(global, o.owner.requiredGlobalFlags()); err != nil { return ErrorBeforePrintingUsage(cmd, err.Error()) } return nil @@ -50,19 +54,14 @@ func newGetApiKeyCmd(out io.Writer) *cobra.Command { }, } - cmd.Flags().StringVarP(&o.serviceAccount, "service-account", "s", "", serviceAccountNameFlag) + o.owner.addFlags(cmd) cmd.Flags().StringVarP(&o.output, "output", "o", "table", outputFlag) - err := RequireFlags(cmd, []string{"service-account"}) - if err != nil { - logger.Error("failed to configure required flags: %v", err) - } - return cmd } func (o *getApiKeyOptions) run(out io.Writer, args []string) error { - url, err := url.JoinPath(global.Host, "api/v2/service-accounts", global.Org, o.serviceAccount, "api-keys", args[0]) + url, err := o.owner.url(args[0]) if err != nil { return err } From 1011f5a0db5bcc23d70b14ff90deb671f65fe3d6 Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Wed, 28 Oct 2026 22:43:17 +0000 Subject: [PATCH 3/8] feat: create a personal API key with `kosli create api-key --personal` Posts to /api/v2/user/api-keys. The help notes the server's rules for keys created with another API key: they are revoked with that key and expire no later than it. --- cmd/kosli/apiKey_test.go | 24 ++++++++++++++++++++---- cmd/kosli/createApiKey.go | 30 +++++++++++++++++++----------- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/cmd/kosli/apiKey_test.go b/cmd/kosli/apiKey_test.go index fd1995681..9fd0fc0b0 100644 --- a/cmd/kosli/apiKey_test.go +++ b/cmd/kosli/apiKey_test.go @@ -160,10 +160,16 @@ func (suite *ApiKeyCommandTestSuite) TestCreateApiKeyCmd() { goldenRegex: `service-accounts/docs-cmd-test-user/test-sa/api-keys`, }, { - wantError: true, - name: "create fails when --service-account is missing", - cmd: "create api-key --description 'ci key'" + suite.defaultKosliArguments, - golden: "Error: required flag(s) \"service-account\" not set\n", + wantError: true, + name: "create fails when neither --service-account nor --personal is set", + cmd: "create api-key --description 'ci key'" + suite.defaultKosliArguments, + goldenRegex: `Error: at least one of the flags in the group \[service-account personal\] is required`, + }, + { + wantError: false, + name: "create --personal posts to the user's keys without --org (dry-run)", + cmd: fmt.Sprintf("create api-key --personal --description 'ci key' --dry-run --host %s --api-token %s", global.Host, global.ApiToken), + goldenRegex: `(?s)/api/v2/user/api-keys.*"description": "ci key"`, }, { wantError: true, @@ -577,6 +583,10 @@ func (suite *ApiKeyCommandTestSuite) TestPersonalApiKeysCmd() { Get("/api/v2/user/api-keys/id-1"). Reply(200). BodyString(apiKeyFixture(suite.T(), "api_key.json")) + fake.NewHandler(). + Post("/api/v2/user/api-keys"). + Reply(201). + BodyString(apiKeyFixture(suite.T(), "created_api_key.json")) // No --org: personal keys belong to the user, not to an org. args := fmt.Sprintf(" --host %s --api-token %s", fake.Server.URL, global.ApiToken) @@ -597,6 +607,12 @@ func (suite *ApiKeyCommandTestSuite) TestPersonalApiKeysCmd() { {Path: "id", Want: "id-1"}, }, }, + { + wantError: false, + name: "create --personal prints the new key value", + cmd: "create api-key --personal -d ci --output json" + args, + goldenRegex: `sk_created`, + }, } runTestCmd(suite.T(), tests) diff --git a/cmd/kosli/createApiKey.go b/cmd/kosli/createApiKey.go index a7f30cc6f..8a35c154c 100644 --- a/cmd/kosli/createApiKey.go +++ b/cmd/kosli/createApiKey.go @@ -3,21 +3,23 @@ package main import ( "io" "net/http" - "net/url" "github.com/kosli-dev/cli/internal/output" "github.com/kosli-dev/cli/internal/requests" "github.com/spf13/cobra" ) -const createApiKeyShortDesc = `Create an API key for a service account.` +const createApiKeyShortDesc = `Create an API key for a service account, or a personal API key for yourself.` const createApiKeyLongDesc = createApiKeyShortDesc + ` The key value is only returned once, at creation time, so make sure to store it securely. Every API key expires. Without ^--expires-at^ the key is given the maximum allowed -lifetime, and an ^--expires-at^ beyond that maximum is capped to it.` +lifetime, and an ^--expires-at^ beyond that maximum is capped to it. + +With ^--personal^, a key created using another API key is revoked along with that key, +and expires no later than it.` const createApiKeyExample = ` # create an API key for a service account: @@ -34,13 +36,19 @@ kosli create api-key \ --expires-at 2026-12-31 \ --api-token yourAPIToken \ --org yourOrgName + +# create a personal API key for yourself: +kosli create api-key \ + --personal \ + --description "key for CI" \ + --api-token yourAPIToken ` type createApiKeyOptions struct { - serviceAccount string - expiresAt string - output string - payload createApiKeyPayload + owner apiKeyOwner + expiresAt string + output string + payload createApiKeyPayload } type createApiKeyPayload struct { @@ -58,7 +66,7 @@ func newCreateApiKeyCmd(out io.Writer) *cobra.Command { Example: createApiKeyExample, Args: cobra.NoArgs, PreRunE: func(cmd *cobra.Command, args []string) error { - if err := RequireGlobalFlags(global, []string{"Org", "ApiToken"}); err != nil { + if err := RequireGlobalFlags(global, o.owner.requiredGlobalFlags()); err != nil { return ErrorBeforePrintingUsage(cmd, err.Error()) } return nil @@ -68,13 +76,13 @@ func newCreateApiKeyCmd(out io.Writer) *cobra.Command { }, } - cmd.Flags().StringVarP(&o.serviceAccount, "service-account", "s", "", serviceAccountNameFlag) + o.owner.addFlags(cmd) cmd.Flags().StringVarP(&o.payload.Description, "description", "d", "", apiKeyDescriptionFlag) cmd.Flags().StringVarP(&o.expiresAt, "expires-at", "e", "", createApiKeyExpiresAtFlag) cmd.Flags().StringVarP(&o.output, "output", "o", "table", outputFlag) addDryRunFlag(cmd) - err := RequireFlags(cmd, []string{"service-account", "description"}) + err := RequireFlags(cmd, []string{"description"}) if err != nil { logger.Error("failed to configure required flags: %v", err) } @@ -83,7 +91,7 @@ func newCreateApiKeyCmd(out io.Writer) *cobra.Command { } func (o *createApiKeyOptions) run(out io.Writer, args []string) error { - url, err := url.JoinPath(global.Host, "api/v2/service-accounts", global.Org, o.serviceAccount, "api-keys") + url, err := o.owner.url() if err != nil { return err } From 14d18e49cac47d271b3f12dfba76a4630e38f5f6 Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Wed, 28 Oct 2026 22:43:18 +0000 Subject: [PATCH 4/8] feat: rotate personal API keys with `kosli rotate api-key --personal` Posts to /api/v2/user/api-keys/KEY-ID/rotate for each key. The help notes that a request made with an API key can rotate only that key and keys created with it, and cannot extend expiry past the calling key's. --- cmd/kosli/apiKey_test.go | 31 +++++++++++++++++++++++++++---- cmd/kosli/rotateApiKey.go | 27 +++++++++++++++------------ 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/cmd/kosli/apiKey_test.go b/cmd/kosli/apiKey_test.go index 9fd0fc0b0..e02ae9ce6 100644 --- a/cmd/kosli/apiKey_test.go +++ b/cmd/kosli/apiKey_test.go @@ -227,10 +227,16 @@ func (suite *ApiKeyCommandTestSuite) TestRotateApiKeyCmd() { golden: "Error: requires at least 1 arg(s), only received 0\n", }, { - wantError: true, - name: "rotate fails when --service-account is missing", - cmd: "rotate api-key key-123" + suite.defaultKosliArguments, - golden: "Error: required flag(s) \"service-account\" not set\n", + wantError: true, + name: "rotate fails when neither --service-account nor --personal is set", + cmd: "rotate api-key key-123" + suite.defaultKosliArguments, + goldenRegex: `Error: at least one of the flags in the group \[service-account personal\] is required`, + }, + { + wantError: false, + name: "rotate --personal posts to the user's key without --org (dry-run)", + cmd: fmt.Sprintf("rotate api-key key-123 --personal --dry-run --host %s --api-token %s", global.Host, global.ApiToken), + goldenRegex: `/api/v2/user/api-keys/key-123/rotate`, }, } @@ -587,6 +593,14 @@ func (suite *ApiKeyCommandTestSuite) TestPersonalApiKeysCmd() { Post("/api/v2/user/api-keys"). Reply(201). BodyString(apiKeyFixture(suite.T(), "created_api_key.json")) + fake.NewHandler(). + Post("/api/v2/user/api-keys/k1/rotate"). + Reply(201). + BodyString(apiKeyFixture(suite.T(), "rotated_api_key.json")) + fake.NewHandler(). + Post("/api/v2/user/api-keys/k2/rotate"). + Reply(201). + BodyString(apiKeyFixture(suite.T(), "rotated_api_key.json")) // No --org: personal keys belong to the user, not to an org. args := fmt.Sprintf(" --host %s --api-token %s", fake.Server.URL, global.ApiToken) @@ -613,6 +627,15 @@ func (suite *ApiKeyCommandTestSuite) TestPersonalApiKeysCmd() { cmd: "create api-key --personal -d ci --output json" + args, goldenRegex: `sk_created`, }, + { + wantError: false, + name: "rotate --personal of multiple keys prints all rotated keys", + cmd: "rotate api-key k1 k2 --personal --output json" + args, + goldenJson: []jsonCheck{ + {Path: "", Want: "length:2"}, + {Path: "[0].key", Want: "sk_one"}, + }, + }, } runTestCmd(suite.T(), tests) diff --git a/cmd/kosli/rotateApiKey.go b/cmd/kosli/rotateApiKey.go index 18697fe66..ecbd3a435 100644 --- a/cmd/kosli/rotateApiKey.go +++ b/cmd/kosli/rotateApiKey.go @@ -5,21 +5,24 @@ import ( "fmt" "io" "net/http" - "net/url" "github.com/kosli-dev/cli/internal/output" "github.com/kosli-dev/cli/internal/requests" "github.com/spf13/cobra" ) -const rotateApiKeyShortDesc = `Rotate one or more API keys for a service account.` +const rotateApiKeyShortDesc = `Rotate one or more API keys for a service account, or your own personal API keys.` const rotateApiKeyLongDesc = rotateApiKeyShortDesc + ` A new API key is generated immediately. The old key remains valid for a grace period to allow time to update dependent systems; that grace period has a standard length unless overridden with ^--grace-period-hours^. The new key value is only returned once, so make -sure to store it securely.` +sure to store it securely. + +With ^--personal^ and an API key as ^--api-token^, you can rotate only that key and keys +created with it, and the rotated key expires no later than that key. Rotating any other +personal key, or extending a key's expiry, needs the Kosli web UI.` const rotateApiKeyExample = ` # rotate an API key for a service account: @@ -40,10 +43,15 @@ kosli rotate api-key yourApiKeyID \ --service-account yourServiceAccountName \ --api-token yourAPIToken \ --org yourOrgName + +# rotate one of your own personal API keys: +kosli rotate api-key yourApiKeyID \ + --personal \ + --api-token yourAPIToken ` type rotateApiKeyOptions struct { - serviceAccount string + owner apiKeyOwner expiresAt string gracePeriodHours int gracePeriodHoursSet bool @@ -66,7 +74,7 @@ func newRotateApiKeyCmd(out io.Writer) *cobra.Command { Example: rotateApiKeyExample, Args: cobra.MinimumNArgs(1), PreRunE: func(cmd *cobra.Command, args []string) error { - if err := RequireGlobalFlags(global, []string{"Org", "ApiToken"}); err != nil { + if err := RequireGlobalFlags(global, o.owner.requiredGlobalFlags()); err != nil { return ErrorBeforePrintingUsage(cmd, err.Error()) } return nil @@ -77,17 +85,12 @@ func newRotateApiKeyCmd(out io.Writer) *cobra.Command { }, } - cmd.Flags().StringVarP(&o.serviceAccount, "service-account", "s", "", serviceAccountNameFlag) + o.owner.addFlags(cmd) cmd.Flags().StringVarP(&o.expiresAt, "expires-at", "e", "", rotateApiKeyExpiresAtFlag) cmd.Flags().IntVarP(&o.gracePeriodHours, "grace-period-hours", "g", 0, apiKeyGracePeriodHoursFlag) cmd.Flags().StringVarP(&o.output, "output", "o", "table", outputFlag) addDryRunFlag(cmd) - err := RequireFlags(cmd, []string{"service-account"}) - if err != nil { - logger.Error("failed to configure required flags: %v", err) - } - return cmd } @@ -111,7 +114,7 @@ func (o *rotateApiKeyOptions) run(out io.Writer, args []string) error { keys := make([]json.RawMessage, 0, len(args)) var runErr error for _, keyID := range args { - url, err := url.JoinPath(global.Host, "api/v2/service-accounts", global.Org, o.serviceAccount, "api-keys", keyID, "rotate") + url, err := o.owner.url(keyID, "rotate") if err != nil { runErr = err break From 1dc4f39229b2c0d172f1726bdc65cee600507fca Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Wed, 28 Oct 2026 22:43:18 +0000 Subject: [PATCH 5/8] feat: delete personal API keys with `kosli delete api-key --personal` Deletes /api/v2/user/api-keys/KEY-ID for each key. The confirmation prompt and success message name the keys as personal rather than pointing at a service account. The help notes that deleting a key also deletes the keys created with it, and that a request made with an API key can delete only that key and keys created with it. --- cmd/kosli/apiKey_test.go | 32 ++++++++++++++++++++++---- cmd/kosli/deleteApiKey.go | 47 ++++++++++++++++++++++++--------------- 2 files changed, 57 insertions(+), 22 deletions(-) diff --git a/cmd/kosli/apiKey_test.go b/cmd/kosli/apiKey_test.go index e02ae9ce6..9cf6844bb 100644 --- a/cmd/kosli/apiKey_test.go +++ b/cmd/kosli/apiKey_test.go @@ -290,10 +290,23 @@ func (suite *ApiKeyCommandTestSuite) TestDeleteApiKeyCmd() { golden: "Error: requires at least 1 arg(s), only received 0\n", }, { - wantError: true, - name: "delete fails when --service-account is missing", - cmd: "delete api-key key-123" + suite.defaultKosliArguments, - golden: "Error: required flag(s) \"service-account\" not set\n", + wantError: true, + name: "delete fails when neither --service-account nor --personal is set", + cmd: "delete api-key key-123" + suite.defaultKosliArguments, + goldenRegex: `Error: at least one of the flags in the group \[service-account personal\] is required`, + }, + { + wantError: false, + name: "a refusal to delete personal keys names them as personal", + cmd: fmt.Sprintf("delete api-key key-123 --personal --host %s --api-token %s", global.Host, global.ApiToken), + stdin: "n\n", + golden: "Are you sure you want to delete your personal API key(s) key-123? [y/N] Deletion of API key(s) key-123 was cancelled.\n", + }, + { + wantError: false, + name: "delete --personal targets the user's key without --org (dry-run)", + cmd: fmt.Sprintf("delete api-key key-123 --personal -y --dry-run --host %s --api-token %s", global.Host, global.ApiToken), + goldenRegex: `/api/v2/user/api-keys/key-123`, }, } @@ -601,6 +614,10 @@ func (suite *ApiKeyCommandTestSuite) TestPersonalApiKeysCmd() { Post("/api/v2/user/api-keys/k2/rotate"). Reply(201). BodyString(apiKeyFixture(suite.T(), "rotated_api_key.json")) + fake.NewHandler(). + Delete("/api/v2/user/api-keys/key-123"). + Reply(200). + BodyString(apiKeyFixture(suite.T(), "revoke_success.json")) // No --org: personal keys belong to the user, not to an org. args := fmt.Sprintf(" --host %s --api-token %s", fake.Server.URL, global.ApiToken) @@ -636,6 +653,13 @@ func (suite *ApiKeyCommandTestSuite) TestPersonalApiKeysCmd() { {Path: "[0].key", Want: "sk_one"}, }, }, + { + wantError: false, + name: "delete --personal confirms and reports the personal key deleted", + cmd: "delete api-key key-123 --personal" + args, + stdin: "y", + goldenRegex: `Personal API key key-123 was deleted!`, + }, } runTestCmd(suite.T(), tests) diff --git a/cmd/kosli/deleteApiKey.go b/cmd/kosli/deleteApiKey.go index 9a49ae028..cc4a7d3d5 100644 --- a/cmd/kosli/deleteApiKey.go +++ b/cmd/kosli/deleteApiKey.go @@ -4,14 +4,13 @@ import ( "fmt" "io" "net/http" - "net/url" "strings" "github.com/kosli-dev/cli/internal/requests" "github.com/spf13/cobra" ) -const deleteApiKeyShortDesc = `Delete one or more API keys for a service account.` +const deleteApiKeyShortDesc = `Delete one or more API keys for a service account, or your own personal API keys.` const deleteApiKeyLongDesc = deleteApiKeyShortDesc + ` @@ -20,7 +19,10 @@ cannot be undone. You are asked to confirm before the key is deleted; use ^--assume-yes^/^--yes^ to skip the confirmation prompt. When stdin is not interactive (e.g. in CI) the prompt cannot be answered and the -command fails without deleting anything, so pass ^--assume-yes^ there.` +command fails without deleting anything, so pass ^--assume-yes^ there. + +Deleting a key also deletes every key that was created with it. With ^--personal^ and an +API key as ^--api-token^, you can delete only that key and keys created with it.` const deleteApiKeyExample = ` # delete an API key for a service account (asks for confirmation): @@ -41,11 +43,16 @@ kosli delete api-key yourApiKeyID \ --assume-yes \ --api-token yourAPIToken \ --org yourOrgName + +# delete one of your own personal API keys: +kosli delete api-key yourApiKeyID \ + --personal \ + --api-token yourAPIToken ` type deleteApiKeyOptions struct { - serviceAccount string - assumeYes bool + owner apiKeyOwner + assumeYes bool } func newDeleteApiKeyCmd(out io.Writer) *cobra.Command { @@ -58,7 +65,7 @@ func newDeleteApiKeyCmd(out io.Writer) *cobra.Command { Example: deleteApiKeyExample, Args: cobra.MinimumNArgs(1), PreRunE: func(cmd *cobra.Command, args []string) error { - if err := RequireGlobalFlags(global, []string{"Org", "ApiToken"}); err != nil { + if err := RequireGlobalFlags(global, o.owner.requiredGlobalFlags()); err != nil { return ErrorBeforePrintingUsage(cmd, err.Error()) } return nil @@ -68,7 +75,7 @@ func newDeleteApiKeyCmd(out io.Writer) *cobra.Command { }, } - cmd.Flags().StringVarP(&o.serviceAccount, "service-account", "s", "", serviceAccountNameFlag) + o.owner.addFlags(cmd) cmd.Flags().BoolVarP(&o.assumeYes, "assume-yes", "y", false, apiKeyAssumeYesFlag) // keep --yes as a hidden alias for --assume-yes (bound to the same option) cmd.Flags().BoolVar(&o.assumeYes, "yes", false, apiKeyAssumeYesFlag) @@ -77,17 +84,12 @@ func newDeleteApiKeyCmd(out io.Writer) *cobra.Command { } addDryRunFlag(cmd) - err := RequireFlags(cmd, []string{"service-account"}) - if err != nil { - logger.Error("failed to configure required flags: %v", err) - } - return cmd } func (o *deleteApiKeyOptions) run(in io.Reader, args []string) error { if !o.assumeYes && !global.DryRun { - confirmed, err := confirmApiKeyDeletion(args, o.serviceAccount, in) + confirmed, err := confirmApiKeyDeletion(args, o.owner, in) if err != nil { return err } @@ -106,7 +108,7 @@ func (o *deleteApiKeyOptions) run(in io.Reader, args []string) error { } for i, keyID := range args { - url, err := url.JoinPath(global.Host, "api/v2/service-accounts", global.Org, o.serviceAccount, "api-keys", keyID) + url, err := o.owner.url(keyID) if err != nil { reportAlreadyDeleted(i) return err @@ -123,7 +125,12 @@ func (o *deleteApiKeyOptions) run(in io.Reader, args []string) error { return fmt.Errorf("failed to delete API key: %w", err) } if !global.DryRun { - logger.Info("API key %s for service account %s was deleted!", style(logger.Out, keyID, ansiBold, ansiCyan), o.serviceAccount) + styledKey := style(logger.Out, keyID, ansiBold, ansiCyan) + if o.owner.personal { + logger.Info("Personal API key %s was deleted!", styledKey) + } else { + logger.Info("API key %s for service account %s was deleted!", styledKey, o.owner.serviceAccount) + } } } return nil @@ -142,8 +149,12 @@ func styleApiKeyIDs(keyIDs []string) []string { // confirmApiKeyDeletion prompts the user to confirm deletion and returns true // only when the answer is an affirmative "y"/"yes" (case-insensitive). It errors // when the prompt cannot be answered at all, see confirmDeletion. -func confirmApiKeyDeletion(keyIDs []string, serviceAccount string, in io.Reader) (bool, error) { - prompt := fmt.Sprintf("Are you sure you want to delete API key(s) %s for service account %s? [y/N] ", - strings.Join(styleApiKeyIDs(keyIDs), ", "), style(logger.Out, serviceAccount, ansiBold, ansiGreen)) +func confirmApiKeyDeletion(keyIDs []string, owner apiKeyOwner, in io.Reader) (bool, error) { + keys := strings.Join(styleApiKeyIDs(keyIDs), ", ") + prompt := fmt.Sprintf("Are you sure you want to delete your personal API key(s) %s? [y/N] ", keys) + if !owner.personal { + prompt = fmt.Sprintf("Are you sure you want to delete API key(s) %s for service account %s? [y/N] ", + keys, style(logger.Out, owner.serviceAccount, ansiBold, ansiGreen)) + } return confirmDeletion(prompt, in) } From 3c4ba5069809ffc9e2255a5fb2359f56c9b59b06 Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Wed, 28 Oct 2026 22:43:18 +0000 Subject: [PATCH 6/8] test: cover `--personal` in the empty-flag audit The api-key commands gained a --personal flag, which the coverage test requires the audit to cover. The audit's --personal runs drop --service-account, which the two flags cannot be given together, and get, rotate and delete act on a personal key created for the run. --- .../testdata/empty-flag-audit-coverage.json | 5 + hack/empty-flag-audit/spec.json | 93 ++++++++++++++++++- 2 files changed, 93 insertions(+), 5 deletions(-) diff --git a/cmd/kosli/testdata/empty-flag-audit-coverage.json b/cmd/kosli/testdata/empty-flag-audit-coverage.json index d2247dba3..51bd5e8e6 100644 --- a/cmd/kosli/testdata/empty-flag-audit-coverage.json +++ b/cmd/kosli/testdata/empty-flag-audit-coverage.json @@ -523,6 +523,7 @@ "dry-run": "bool", "expires-at": "string", "output": "string", + "personal": "bool", "service-account": "string" }, "create attestation-type": { @@ -569,6 +570,7 @@ "delete api-key": { "assume-yes": "bool", "dry-run": "bool", + "personal": "bool", "service-account": "string", "yes": "bool" }, @@ -647,6 +649,7 @@ }, "get api-key": { "output": "string", + "personal": "bool", "service-account": "string" }, "get artifact": { @@ -705,6 +708,7 @@ }, "list api-keys": { "output": "string", + "personal": "bool", "service-account": "string" }, "list artifacts": { @@ -831,6 +835,7 @@ "expires-at": "string", "grace-period-hours": "int", "output": "string", + "personal": "bool", "service-account": "string" }, "search": { diff --git a/hack/empty-flag-audit/spec.json b/hack/empty-flag-audit/spec.json index bd3a1cf80..18bdc5fc2 100644 --- a/hack/empty-flag-audit/spec.json +++ b/hack/empty-flag-audit/spec.json @@ -1352,6 +1352,7 @@ "dry-run", "expires-at", "output", + "personal", "service-account" ], "flag_values": { @@ -1359,6 +1360,7 @@ "dry-run": "true", "expires-at": "4102444800", "output": "json", + "personal": "true", "service-account": "{account}" }, "setup": [ @@ -1372,7 +1374,12 @@ ] } ], - "verify": [] + "verify": [], + "flags_for_flag": { + "personal": { + "description": "probe-description" + } + } }, "create control": { "args": [ @@ -1644,12 +1651,14 @@ "flags_to_test": [ "assume-yes", "dry-run", + "personal", "service-account", "yes" ], "flag_values": { "assume-yes": "true", "dry-run": "true", + "personal": "true", "service-account": "{account}", "yes": "true" }, @@ -1676,9 +1685,32 @@ "apikey", "ID:\\s+(\\S+)" ] + }, + { + "argv": [ + "create", + "api-key", + "--personal", + "--description", + "probe" + ], + "capture": [ + "personalkey", + "ID:\\s+(\\S+)" + ] } ], - "verify": [] + "verify": [], + "flags_for_flag": { + "personal": { + "assume-yes": "true" + } + }, + "args_for_flag": { + "personal": [ + "{personalkey}" + ] + } }, "diff snapshots": { "args": [ @@ -1932,10 +1964,12 @@ "baseline_output": "ID: {apikey}\nDescription: probe\nCreated At: {timestamp} \u2022 {ago}\nExpires At: N/A\nLast Used: N/A", "flags_to_test": [ "output", + "personal", "service-account" ], "flag_values": { "output": "json", + "personal": "true", "service-account": "{account}" }, "setup": [ @@ -1961,9 +1995,30 @@ "apikey", "ID:\\s+(\\S+)" ] + }, + { + "argv": [ + "create", + "api-key", + "--personal", + "--description", + "probe" + ], + "capture": [ + "personalkey", + "ID:\\s+(\\S+)" + ] } ], - "verify": [] + "verify": [], + "flags_for_flag": { + "personal": {} + }, + "args_for_flag": { + "personal": [ + "{personalkey}" + ] + } }, "get artifact": { "args": [ @@ -2379,10 +2434,12 @@ "baseline_output": "ID DESCRIPTION CREATED EXPIRES LAST USED\n{apikey} probe {timestamp} \u2022 {ago} N/A N/A", "flags_to_test": [ "output", + "personal", "service-account" ], "flag_values": { "output": "json", + "personal": "true", "service-account": "{account}" }, "setup": [ @@ -2410,7 +2467,10 @@ ] } ], - "verify": [] + "verify": [], + "flags_for_flag": { + "personal": {} + } }, "list artifacts": { "args": [], @@ -2775,6 +2835,7 @@ "expires-at", "grace-period-hours", "output", + "personal", "service-account" ], "flag_values": { @@ -2782,6 +2843,7 @@ "expires-at": "4102444800", "grace-period-hours": "24", "output": "json", + "personal": "true", "service-account": "{account}" }, "setup": [ @@ -2807,9 +2869,30 @@ "apikey", "ID:\\s+(\\S+)" ] + }, + { + "argv": [ + "create", + "api-key", + "--personal", + "--description", + "probe" + ], + "capture": [ + "personalkey", + "ID:\\s+(\\S+)" + ] } ], - "verify": [] + "verify": [], + "flags_for_flag": { + "personal": {} + }, + "args_for_flag": { + "personal": [ + "{personalkey}" + ] + } }, "search": { "args": [ From e20dea16000fd4ff6feb4b00943954e2507726e1 Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Wed, 28 Oct 2026 22:43:19 +0000 Subject: [PATCH 7/8] docs: point rotate help at the profile page for longer-lived keys The rotate help said extending a personal key's expiry needs the Kosli web UI, but the profile page cannot rotate keys; it can create one, which is the way to get a key that outlives the calling key. --- cmd/kosli/rotateApiKey.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/kosli/rotateApiKey.go b/cmd/kosli/rotateApiKey.go index ecbd3a435..c80e000a1 100644 --- a/cmd/kosli/rotateApiKey.go +++ b/cmd/kosli/rotateApiKey.go @@ -21,8 +21,8 @@ overridden with ^--grace-period-hours^. The new key value is only returned once, sure to store it securely. With ^--personal^ and an API key as ^--api-token^, you can rotate only that key and keys -created with it, and the rotated key expires no later than that key. Rotating any other -personal key, or extending a key's expiry, needs the Kosli web UI.` +created with it, and the rotated key expires no later than that key. To get a key that +outlives it, create one from your profile page in the Kosli app.` const rotateApiKeyExample = ` # rotate an API key for a service account: From bd93ed80046937eafb9a075e147223a836f50bf7 Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Wed, 28 Oct 2026 22:43:19 +0000 Subject: [PATCH 8/8] fix: require an API key owner when `--personal=false` is given Cobra's one-required flag group counts --personal=false as given, so `kosli list api-keys --personal=false` passed validation with no owner and sent its request to /api/v2/service-accounts//api-keys, the empty service account segment dropped by url.JoinPath. The server would read that as a service account named "api-keys". The owner is now checked in each command's PreRunE instead of by the flag group, giving one clear error whether neither flag is given or --personal is false: "--service-account is required unless --personal is set". The two flags stay mutually exclusive. --- cmd/kosli/apiKey.go | 16 ++++++++++------ cmd/kosli/apiKey_test.go | 33 ++++++++++++++++++++++++++++----- cmd/kosli/createApiKey.go | 2 +- cmd/kosli/deleteApiKey.go | 2 +- cmd/kosli/getApiKey.go | 2 +- cmd/kosli/listApiKeys.go | 2 +- cmd/kosli/rotateApiKey.go | 2 +- 7 files changed, 43 insertions(+), 16 deletions(-) diff --git a/cmd/kosli/apiKey.go b/cmd/kosli/apiKey.go index b43ee0c96..e2374f64e 100644 --- a/cmd/kosli/apiKey.go +++ b/cmd/kosli/apiKey.go @@ -22,16 +22,20 @@ func (o *apiKeyOwner) addFlags(cmd *cobra.Command) { cmd.Flags().StringVarP(&o.serviceAccount, "service-account", "s", "", serviceAccountNameFlag) cmd.Flags().BoolVar(&o.personal, "personal", false, personalApiKeyFlag) cmd.MarkFlagsMutuallyExclusive("service-account", "personal") - cmd.MarkFlagsOneRequired("service-account", "personal") } -// requiredGlobalFlags leaves out Org for personal keys, which belong to the -// user rather than to an org. -func (o *apiKeyOwner) requiredGlobalFlags() []string { +// validate checks the owner and the global flags it needs. The owner is not a +// cobra one-required flag group because that counts --personal=false as given. +// Org is not needed for personal keys, which belong to the user rather than to +// an org. +func (o *apiKeyOwner) validate() error { if o.personal { - return []string{"ApiToken"} + return RequireGlobalFlags(global, []string{"ApiToken"}) } - return []string{"Org", "ApiToken"} + if o.serviceAccount == "" { + return fmt.Errorf("--service-account is required unless --personal is set") + } + return RequireGlobalFlags(global, []string{"Org", "ApiToken"}) } // url builds the owner's API key collection URL, followed by elem. diff --git a/cmd/kosli/apiKey_test.go b/cmd/kosli/apiKey_test.go index 9cf6844bb..67704ccd2 100644 --- a/cmd/kosli/apiKey_test.go +++ b/cmd/kosli/apiKey_test.go @@ -163,7 +163,7 @@ func (suite *ApiKeyCommandTestSuite) TestCreateApiKeyCmd() { wantError: true, name: "create fails when neither --service-account nor --personal is set", cmd: "create api-key --description 'ci key'" + suite.defaultKosliArguments, - goldenRegex: `Error: at least one of the flags in the group \[service-account personal\] is required`, + goldenRegex: `Error: --service-account is required unless --personal is set`, }, { wantError: false, @@ -230,7 +230,7 @@ func (suite *ApiKeyCommandTestSuite) TestRotateApiKeyCmd() { wantError: true, name: "rotate fails when neither --service-account nor --personal is set", cmd: "rotate api-key key-123" + suite.defaultKosliArguments, - goldenRegex: `Error: at least one of the flags in the group \[service-account personal\] is required`, + goldenRegex: `Error: --service-account is required unless --personal is set`, }, { wantError: false, @@ -293,7 +293,7 @@ func (suite *ApiKeyCommandTestSuite) TestDeleteApiKeyCmd() { wantError: true, name: "delete fails when neither --service-account nor --personal is set", cmd: "delete api-key key-123" + suite.defaultKosliArguments, - goldenRegex: `Error: at least one of the flags in the group \[service-account personal\] is required`, + goldenRegex: `Error: --service-account is required unless --personal is set`, }, { wantError: false, @@ -319,7 +319,7 @@ func (suite *ApiKeyCommandTestSuite) TestGetApiKeyCmd() { wantError: true, name: "get fails when neither --service-account nor --personal is set", cmd: "get api-key key-123" + suite.defaultKosliArguments, - goldenRegex: `Error: at least one of the flags in the group \[service-account personal\] is required`, + goldenRegex: `Error: --service-account is required unless --personal is set`, }, { wantError: true, @@ -578,7 +578,7 @@ func (suite *ApiKeyCommandTestSuite) TestListApiKeysCmd() { wantError: true, name: "list fails when neither --service-account nor --personal is set", cmd: "list api-keys" + suite.defaultKosliArguments, - goldenRegex: `Error: at least one of the flags in the group \[service-account personal\] is required`, + goldenRegex: `Error: --service-account is required unless --personal is set`, }, { wantError: true, @@ -591,6 +591,29 @@ func (suite *ApiKeyCommandTestSuite) TestListApiKeysCmd() { runTestCmd(suite.T(), tests) } +// TestPersonalFalseNeedsServiceAccount covers --personal=false: cobra counts the +// flag as given, so only the owner check stops a request with no owner at all. +func (suite *ApiKeyCommandTestSuite) TestPersonalFalseNeedsServiceAccount() { + wantErr := `Error: --service-account is required unless --personal is set` + tests := []cmdTestCase{} + for _, cmd := range []string{ + "list api-keys", + "get api-key key-123", + "create api-key --description 'ci key'", + "rotate api-key key-123", + "delete api-key key-123 --assume-yes", + } { + tests = append(tests, cmdTestCase{ + wantError: true, + name: cmd + " with --personal=false and no --service-account fails", + cmd: cmd + " --personal=false" + suite.defaultKosliArguments, + goldenRegex: wantErr, + }) + } + + runTestCmd(suite.T(), tests) +} + func (suite *ApiKeyCommandTestSuite) TestPersonalApiKeysCmd() { fake := httpfake.New() defer fake.Close() diff --git a/cmd/kosli/createApiKey.go b/cmd/kosli/createApiKey.go index 8a35c154c..47f19f6d2 100644 --- a/cmd/kosli/createApiKey.go +++ b/cmd/kosli/createApiKey.go @@ -66,7 +66,7 @@ func newCreateApiKeyCmd(out io.Writer) *cobra.Command { Example: createApiKeyExample, Args: cobra.NoArgs, PreRunE: func(cmd *cobra.Command, args []string) error { - if err := RequireGlobalFlags(global, o.owner.requiredGlobalFlags()); err != nil { + if err := o.owner.validate(); err != nil { return ErrorBeforePrintingUsage(cmd, err.Error()) } return nil diff --git a/cmd/kosli/deleteApiKey.go b/cmd/kosli/deleteApiKey.go index cc4a7d3d5..b93dafd75 100644 --- a/cmd/kosli/deleteApiKey.go +++ b/cmd/kosli/deleteApiKey.go @@ -65,7 +65,7 @@ func newDeleteApiKeyCmd(out io.Writer) *cobra.Command { Example: deleteApiKeyExample, Args: cobra.MinimumNArgs(1), PreRunE: func(cmd *cobra.Command, args []string) error { - if err := RequireGlobalFlags(global, o.owner.requiredGlobalFlags()); err != nil { + if err := o.owner.validate(); err != nil { return ErrorBeforePrintingUsage(cmd, err.Error()) } return nil diff --git a/cmd/kosli/getApiKey.go b/cmd/kosli/getApiKey.go index 69b92334a..7436f2e91 100644 --- a/cmd/kosli/getApiKey.go +++ b/cmd/kosli/getApiKey.go @@ -44,7 +44,7 @@ func newGetApiKeyCmd(out io.Writer) *cobra.Command { Example: getApiKeyExample, Args: cobra.ExactArgs(1), PreRunE: func(cmd *cobra.Command, args []string) error { - if err := RequireGlobalFlags(global, o.owner.requiredGlobalFlags()); err != nil { + if err := o.owner.validate(); err != nil { return ErrorBeforePrintingUsage(cmd, err.Error()) } return nil diff --git a/cmd/kosli/listApiKeys.go b/cmd/kosli/listApiKeys.go index 9f599c731..9fad9bd9c 100644 --- a/cmd/kosli/listApiKeys.go +++ b/cmd/kosli/listApiKeys.go @@ -46,7 +46,7 @@ func newListApiKeysCmd(out io.Writer) *cobra.Command { Example: listApiKeysExample, Args: cobra.NoArgs, PreRunE: func(cmd *cobra.Command, args []string) error { - if err := RequireGlobalFlags(global, o.owner.requiredGlobalFlags()); err != nil { + if err := o.owner.validate(); err != nil { return ErrorBeforePrintingUsage(cmd, err.Error()) } return nil diff --git a/cmd/kosli/rotateApiKey.go b/cmd/kosli/rotateApiKey.go index c80e000a1..d6527fe20 100644 --- a/cmd/kosli/rotateApiKey.go +++ b/cmd/kosli/rotateApiKey.go @@ -74,7 +74,7 @@ func newRotateApiKeyCmd(out io.Writer) *cobra.Command { Example: rotateApiKeyExample, Args: cobra.MinimumNArgs(1), PreRunE: func(cmd *cobra.Command, args []string) error { - if err := RequireGlobalFlags(global, o.owner.requiredGlobalFlags()); err != nil { + if err := o.owner.validate(); err != nil { return ErrorBeforePrintingUsage(cmd, err.Error()) } return nil