From f5a5dcf3514bc30b402905fcb48c747ae309d19d Mon Sep 17 00:00:00 2001 From: wklken Date: Fri, 23 Oct 2026 16:13:29 +0800 Subject: [PATCH] fix(apigateway): require app credentials for permission applications --- AGENTS.md | 7 +- .../apigateway/apply_gateway_permission.go | 50 ++++-- .../apply_gateway_permission_test.go | 149 ++++++++++++++---- docs/design.md | 2 +- docs/user-guide.md | 2 +- skills/bk-cli-apigateway/SKILL.md | 6 +- ...7-apigateway-apply-gateway-permission.yaml | 2 +- ...apply-gateway-permission-confirmation.yaml | 57 ++++++- 8 files changed, 221 insertions(+), 54 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8fdb39a..e90f6f7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -259,8 +259,11 @@ preview, request/response logs, and response envelope headers. Action-specific credential restrictions belong in the Go command. Use `runtime.EnsureCredential()` to load or reuse credentials before checking them. -`apigateway apply_gateway_permission` rejects personal credentials and explicit -Bearer headers locally, including dry-run. +`apigateway apply_gateway_permission` requires `app_user` credentials with non-empty +`bk_app_code` and `bk_app_secret`. It rejects access-token/personal-token credentials +and explicit Bearer headers locally, including dry-run. An explicit +`X-Bkapi-Authorization` must also contain non-empty app code/secret and no +`access_token` or `personal_token` fields. Exact header payload shapes, validation, and redaction behavior remain defined in `docs/design.md` and the shared request/runtime packages. diff --git a/cmd/system/apigateway/apply_gateway_permission.go b/cmd/system/apigateway/apply_gateway_permission.go index 0c582af..05bbd07 100644 --- a/cmd/system/apigateway/apply_gateway_permission.go +++ b/cmd/system/apigateway/apply_gateway_permission.go @@ -22,6 +22,7 @@ import ( "fmt" "strings" + json "github.com/goccy/go-json" "github.com/spf13/cobra" "github.com/TencentBlueKing/bk-cli/internal/api" @@ -53,11 +54,15 @@ Batch required resource_names into one request per gateway to avoid excessive application records. Do not retry automatically: a retry can create another record. Use --dry-run to preview without confirmation or submitting an application. -This API requires application identity and does not support personal_token. -Personal-token contexts and explicit Authorization: Bearer headers are rejected -locally with unsupported_credential_type, including during --dry-run. Select an -application credential context or log in with app credentials or a suitable -access_token before previewing or submitting. +Only bk_app_code + bk_app_secret authentication is supported by this action. +access_token and personal_token contexts are forbidden, including during --dry-run. +Authorization: Bearer and X-Bkapi-Authorization overrides containing access_token +or personal_token are rejected locally with unsupported_credential_type. An +X-Bkapi-Authorization override must contain non-empty bk_app_code and bk_app_secret. +Agents must stop if only tokens are available and ask the application developer +to handle the permission application. Do not bypass this restriction with raw API +calls, header overrides or automatic retries. Select an authorized app credential +context before previewing or submitting; user confirmation is still required. The required --body accepts JSON, @file or - for stdin. Before preview or submission, the shared request layer reads the input and checks @@ -125,7 +130,7 @@ Do not invent approval links or approvers, or assume the CLI knows approval stat if err := runtime.EnsureCredential(); err != nil { return err } - if err := validatePersonalTokenAuth(runtime.Credential, headerMap); err != nil { + if err := validatePermissionApplicationAuth(runtime.Credential, headerMap); err != nil { return err } return systemcmd.ExecuteRequest(cmd, runtime, "apply_gateway_permission", syslib.RequestSpec{ @@ -175,21 +180,36 @@ Do not invent approval links or approvers, or assume the CLI knows approval stat return cmd } -func validatePersonalTokenAuth(cred *credential.Credential, headers map[string]string) error { - personalAuth := cred != nil && cred.Type == credential.TypePersonalToken +func validatePermissionApplicationAuth(cred *credential.Credential, headers map[string]string) error { + unsupported := cred == nil || cred.Type != credential.TypeAppUser || + strings.TrimSpace(cred.BkAppCode) == "" || strings.TrimSpace(cred.BkAppSecret) == "" for name, value := range headers { - if strings.EqualFold(name, "Authorization") { + switch { + case strings.EqualFold(name, "Authorization"): parts := strings.Fields(value) - personalAuth = personalAuth || (len(parts) > 0 && strings.EqualFold(parts[0], "Bearer")) + unsupported = unsupported || (len(parts) > 0 && strings.EqualFold(parts[0], "Bearer")) + case strings.EqualFold(name, "X-Bkapi-Authorization"): + var auth map[string]string + if err := json.Unmarshal([]byte(value), &auth); err != nil { + unsupported = true + continue + } + _, accessToken := auth["access_token"] + _, personalToken := auth["personal_token"] + unsupported = unsupported || accessToken || personalToken || + strings.TrimSpace( + auth["bk_app_code"], + ) == "" || strings.TrimSpace(auth["bk_app_secret"]) == "" } } - if personalAuth { + if unsupported { return output.UserError( "unsupported_credential_type", - "personal_token authentication is not supported for this action; application identity is required", - "Select an application credential context using --context, or run bk-cli auth login with "+ - "--bk_app_code/--bk_app_secret and --bk_token/--bk_ticket, or a suitable --access_token; "+ - "remove any Authorization: Bearer override before previewing or submitting.", + "this action requires application identity via bk_app_code + bk_app_secret; "+ + "access_token and personal_token authentication are forbidden", + "Agents must stop if only tokens are available and ask the application developer to apply. "+ + "Select an authorized app credential context with --context; remove token header overrides. "+ + "Do not bypass this restriction with raw API calls or retry automatically.", ) } return nil diff --git a/cmd/system/apigateway/apply_gateway_permission_test.go b/cmd/system/apigateway/apply_gateway_permission_test.go index 57aa3fe..8dc0ec8 100644 --- a/cmd/system/apigateway/apply_gateway_permission_test.go +++ b/cmd/system/apigateway/apply_gateway_permission_test.go @@ -47,6 +47,18 @@ var _ = Describe("apigateway apply_gateway_permission", func() { var status int var server *httptest.Server + setupAppContext := func() { + Expect(systemtest.SetupTestContext(server.URL)).To(Succeed()) + key, err := credential.DeriveKey() + Expect(err).NotTo(HaveOccurred()) + Expect(credential.Save(config.CredentialsPath("default"), &credential.Credential{ + Type: credential.TypeAppUser, + BkAppCode: "my-app", + BkAppSecret: "test-secret", + BkToken: "test-user-token", + }, key)).To(Succeed()) + } + newCommand := func(dryRun bool) *cobra.Command { parent := &cobra.Command{Use: "apigateway"} Expect(NewSystemSpec().RegisterGoActions(parent, systemtest.BuildDeps(dryRun))).To(Succeed()) @@ -96,7 +108,7 @@ var _ = Describe("apigateway apply_gateway_permission", func() { DescribeTable("refuses every submission without explicit user confirmation", func(args []string) { - Expect(systemtest.SetupTestContext(server.URL)).To(Succeed()) + setupAppContext() cmd := newCommand(false) args = append(args, "--gateway_name", "bk-iam", "--body", body) Expect(cmd.ParseFlags(args)).To(Succeed()) @@ -114,6 +126,9 @@ var _ = Describe("apigateway apply_gateway_permission", func() { cmd.SetOut(&help) Expect(cmd.Help()).To(Succeed()) Expect(help.String()).To(ContainSubstring("must ask the user")) + Expect(help.String()).To(ContainSubstring("Only bk_app_code + bk_app_secret")) + Expect(help.String()).To(ContainSubstring("Agents must stop")) + Expect(help.String()).To(ContainSubstring("access_token and personal_token contexts are forbidden")) Expect(help.String()).To(ContainSubstring("every submission")) Expect(help.String()).To(ContainSubstring("cannot be skipped")) Expect(help.String()).To(ContainSubstring("Batch")) @@ -126,7 +141,7 @@ var _ = Describe("apigateway apply_gateway_permission", func() { }) It("previews an unconfirmed batch without sending a request or claiming submission", func() { - Expect(systemtest.SetupTestContext(server.URL)).To(Succeed()) + setupAppContext() cmd := newCommand(true) Expect(cmd.Flags().Set("gateway_name", "bk-iam")).To(Succeed()) Expect(cmd.Flags().Set("body", body)).To(Succeed()) @@ -148,15 +163,19 @@ var _ = Describe("apigateway apply_gateway_permission", func() { Expect(requests.Load()).To(BeZero()) }) - DescribeTable("rejects personal token authentication before preview or submission", - func(dryRun, personalContext bool, headers []string) { - Expect(systemtest.SetupTestContext(server.URL)).To(Succeed()) - if personalContext { + DescribeTable("rejects token authentication before preview or submission", + func(dryRun bool, credType credential.CredentialType, headers []string) { + setupAppContext() + if credType != credential.TypeAppUser { key, err := credential.DeriveKey() Expect(err).NotTo(HaveOccurred()) - Expect(credential.Save(config.CredentialsPath("default"), &credential.Credential{ - Type: credential.TypePersonalToken, PersonalToken: "test-personal-token", - }, key)).To(Succeed()) + cred := &credential.Credential{Type: credType} + if credType == credential.TypePersonalToken { + cred.PersonalToken = "test-personal-token" + } else { + cred.AccessToken = "test-access-token" + } + Expect(credential.Save(config.CredentialsPath("default"), cred, key)).To(Succeed()) } cmd := newCommand(dryRun) args := []string{"--gateway_name", "bk-iam", "--body", body} @@ -173,47 +192,112 @@ var _ = Describe("apigateway apply_gateway_permission", func() { Expect(err).To(MatchError(ContainSubstring("unsupported_credential_type"))) Expect(err.Error()).To(ContainSubstring("application identity")) Expect(err.Error()).NotTo(ContainSubstring("test-personal-token")) + Expect(err.Error()).NotTo(ContainSubstring("test-access-token")) Expect(stdout.String()).To(BeEmpty()) Expect(requests.Load()).To(BeZero()) }, - Entry("personal context submission", false, true, nil), - Entry("personal context preview", true, true, nil), - Entry("personal context with app header override", false, true, - []string{`x-bkapi-authorization:{"access_token":"app-token"}`}), - Entry("app context with Bearer header submission", false, false, + Entry("access token submission", false, credential.TypeAccessToken, nil), + Entry("access token preview", true, credential.TypeAccessToken, nil), + Entry("access token with app header override", false, credential.TypeAccessToken, + []string{`X-Bkapi-Authorization:{"bk_app_code":"my-app","bk_app_secret":"test-secret"}`}), + Entry("app context with access token header", false, credential.TypeAppUser, + []string{`x-bkapi-authorization:{"access_token":"test-access-token"}`}), + Entry( + "app context with mixed auth preview", + true, + credential.TypeAppUser, + []string{ + `X-Bkapi-Authorization:{"bk_app_code":"my-app","bk_app_secret":"test-secret","access_token":"test-access-token"}`, + }, + ), + + Entry("malformed auth header", false, credential.TypeAppUser, + []string{`X-Bkapi-Authorization:{test-access-token`}), + Entry("missing app secret override", true, credential.TypeAppUser, + []string{`X-Bkapi-Authorization:{"bk_app_code":"my-app"}`}), + Entry("null auth override", false, credential.TypeAppUser, + []string{`X-Bkapi-Authorization:null`}), + Entry("non-string app secret", false, credential.TypeAppUser, + []string{`X-Bkapi-Authorization:{"bk_app_code":"my-app","bk_app_secret":123}`}), + Entry( + "personal token in app override", + false, + credential.TypeAppUser, + []string{ + `X-Bkapi-Authorization:{"bk_app_code":"my-app","bk_app_secret":"test-secret","personal_token":"test-personal-token"}`, + }, + ), + Entry( + "empty access token in app override", + true, + credential.TypeAppUser, + []string{ + `X-Bkapi-Authorization:{"bk_app_code":"my-app","bk_app_secret":"test-secret","access_token":""}`, + }, + ), + Entry("last case-insensitive override contains token", false, credential.TypeAppUser, + []string{ + `X-Bkapi-Authorization:{"bk_app_code":"my-app","bk_app_secret":"test-secret"}`, + `x-bkapi-authorization:{"access_token":"test-access-token"}`, + }), + + Entry("personal context submission", false, credential.TypePersonalToken, nil), + Entry("personal context preview", true, credential.TypePersonalToken, nil), + Entry("personal context with app header override", false, credential.TypePersonalToken, + []string{`x-bkapi-authorization:{"bk_app_code":"my-app","bk_app_secret":"test-secret"}`}), + Entry("app context with Bearer header submission", false, credential.TypeAppUser, []string{"authorization:bEaReR test-personal-token"}), - Entry("app context with Bearer header preview", true, false, + Entry("app context with Bearer header preview", true, credential.TypeAppUser, []string{"Authorization:Bearer test-personal-token"}), ) - DescribeTable("checks command-local personal token restrictions for cached credentials and header overrides", + DescribeTable( + "checks command-local application credential restrictions for cached credentials and header overrides", func(credType credential.CredentialType, headers map[string]string, rejected bool) { var cred *credential.Credential if credType != "" { - cred = &credential.Credential{Type: credType} + cred = &credential.Credential{ + Type: credType, + BkAppCode: "my-app", + BkAppSecret: "test-secret", + } } - err := validatePersonalTokenAuth(cred, headers) + err := validatePermissionApplicationAuth(cred, headers) if rejected { Expect(err).To(MatchError(ContainSubstring("unsupported_credential_type"))) } else { Expect(err).NotTo(HaveOccurred()) } }, - Entry("anonymous without headers", credential.CredentialType(""), nil, false), + Entry("anonymous without headers", credential.CredentialType(""), nil, true), Entry("application credentials", credential.TypeAppUser, nil, false), - Entry("access token credentials", credential.TypeAccessToken, nil, false), + Entry("access token credentials", credential.TypeAccessToken, nil, true), Entry("cached personal credentials", credential.TypePersonalToken, nil, true), Entry("explicit Bearer header without stored credentials", credential.CredentialType(""), map[string]string{"aUtHoRiZaTiOn": " bEaReR personal"}, true), - Entry("non-Bearer header", credential.TypeAccessToken, + Entry("non-Bearer header", credential.TypeAppUser, map[string]string{"Authorization": "Basic example"}, false), - Entry("empty header", credential.TypeAccessToken, + Entry("empty header", credential.TypeAppUser, map[string]string{"Authorization": ""}, false), ) + It("allows an app credential header override and preserves header precedence", func() { + setupAppContext() + cmd := newCommand(false) + Expect(cmd.ParseFlags([]string{ + "--confirmed", "--gateway_name", "bk-iam", "--body", body, + "--header", `X-Bkapi-Authorization:{"access_token":"discarded-token"}`, + "--header", `x-bkapi-authorization:{"bk_app_code":"my-app","bk_app_secret":"override-secret"}`, + })).To(Succeed()) + Expect(cmd.RunE(cmd, nil)).To(Succeed()) + Expect(requests.Load()).To(Equal(int32(1))) + Expect(capturedHeader.Get("X-Bkapi-Authorization")).To(MatchJSON( + `{"bk_app_code":"my-app","bk_app_secret":"override-secret"}`)) + }) + DescribeTable("submits exactly one batch and preserves all returned approval details", func(input string) { - Expect(systemtest.SetupTestContext(server.URL)).To(Succeed()) + setupAppContext() bodyInput := body if input == "file" { file := filepath.Join(GinkgoT().TempDir(), "body.json") @@ -249,7 +333,12 @@ var _ = Describe("apigateway apply_gateway_permission", func() { ) Expect(capturedBody).To(MatchJSON(body)) Expect(capturedHeader.Get("X-Custom")).To(Equal("batch")) - Expect(capturedHeader.Get("X-Bkapi-Authorization")).NotTo(BeEmpty()) + Expect( + capturedHeader.Get("X-Bkapi-Authorization"), + ).To( + MatchJSON(`{"bk_app_code":"my-app","bk_app_secret":"test-secret"}`), + ) + Expect(capturedHeader.Get("Authorization")).To(BeEmpty()) var env output.Envelope Expect(json.Unmarshal(stdout.Bytes(), &env)).To(Succeed()) Expect(env.OK).To(BeTrue()) @@ -272,7 +361,7 @@ var _ = Describe("apigateway apply_gateway_permission", func() { DescribeTable("rejects invalid local inputs before contacting the gateway", func(gateway, input, errorCode string) { - Expect(systemtest.SetupTestContext(server.URL)).To(Succeed()) + setupAppContext() cmd := newCommand(false) Expect( cmd.ParseFlags([]string{"--confirmed", "--gateway_name", gateway, "--body", input}), @@ -292,7 +381,7 @@ var _ = Describe("apigateway apply_gateway_permission", func() { DescribeTable( "delegates permission fields and body structure to the server", func(input string) { - Expect(systemtest.SetupTestContext(server.URL)).To(Succeed()) + setupAppContext() status = http.StatusBadRequest response = `{"code":1,"message":"Server rejected application fields"}` cmd := newCommand(false) @@ -377,7 +466,7 @@ var _ = Describe("apigateway apply_gateway_permission", func() { DescribeTable("rejects invalid JSON from inline, file and stdin during preview and submission", func(source string, dryRun bool) { - Expect(systemtest.SetupTestContext(server.URL)).To(Succeed()) + setupAppContext() input := `{` bodyInput := input if source == "file" { @@ -417,7 +506,7 @@ var _ = Describe("apigateway apply_gateway_permission", func() { DescribeTable( "keeps application bodies unchanged as the server contract evolves", func(input string) { - Expect(systemtest.SetupTestContext(server.URL)).To(Succeed()) + setupAppContext() cmd := newCommand(false) Expect( cmd.ParseFlags([]string{"--confirmed", "--gateway_name", "bk-iam", "--body", input}), @@ -453,7 +542,7 @@ var _ = Describe("apigateway apply_gateway_permission", func() { DescribeTable( "only reports submission when the upstream returns an application record", func(upstream, recordID string) { - Expect(systemtest.SetupTestContext(server.URL)).To(Succeed()) + setupAppContext() response = upstream cmd := newCommand(false) Expect( @@ -512,7 +601,7 @@ var _ = Describe("apigateway apply_gateway_permission", func() { ) It("preserves upstream errors without a successful application message", func() { - Expect(systemtest.SetupTestContext(server.URL)).To(Succeed()) + setupAppContext() status = http.StatusForbidden response = `{"code":1640301,"message":"Permission denied"}` cmd := newCommand(false) diff --git a/docs/design.md b/docs/design.md index 48c73f3..fc1c0d9 100644 --- a/docs/design.md +++ b/docs/design.md @@ -579,7 +579,7 @@ https://bkapi.example.com/api/bk-iam/prod/api/v2/systems/ `personal_token` 使用独立的 `--personal_token` 登录 flag,与其他五个凭据 flags 按显式出现互斥(包括显式空值)。输入必须符合 RFC 6750 §2.1 的单个 Bearer token 字符语法,不添加前缀、不裁剪原值。非法输入返回 `invalid_credentials` 且不回显原值,也不替换已有凭据。复用 AES-GCM 存储和 context 隔离,不改变旧凭据格式;存储读取后,在生成个人认证头前再次校验。 -个人模式不套用应用/用户字段拼装;默认要求任一认证维度的 action 都生成 Bearer 值,token 有效性、接口/MCP 权限及资源权限由服务端判断。`apigateway apply_gateway_permission` 需要应用身份,因此在命令内通过 `runtime.EnsureCredential()` 加载或复用凭据,拒绝个人凭据及显式 Bearer header,返回 `unsupported_credential_type`,dry-run 同样拒绝。原始 `api` 仍加载凭据;明确无需认证的 action 不加载凭据且不生成认证头,即使 runtime 已缓存个人凭据。`auth status/check` 仅展示凭据类型及本地状态,不验证远端;`logout` 删除当前 context 凭据。 +个人模式不套用应用/用户字段拼装;默认要求任一认证维度的 action 都生成 Bearer 值,token 有效性、接口/MCP 权限及资源权限由服务端判断。`apigateway apply_gateway_permission` 需要应用身份,因此在命令内通过 `runtime.EnsureCredential()` 加载或复用凭据,仅接受包含非空 `bk_app_code` / `bk_app_secret` 的 `app_user` 凭据,拒绝 `access_token` / `personal_token` 凭据及显式 Bearer header;覆盖 `X-Bkapi-Authorization` 时也必须包含非空应用 code/secret,且不得包含 `access_token` / `personal_token` 字段。不符合要求时返回 `unsupported_credential_type`,dry-run 同样拒绝。此限制仅由该子命令执行,不代替服务端对直接 API 调用的鉴权限制。原始 `api` 仍加载凭据;明确无需认证的 action 不加载凭据且不生成认证头,即使 runtime 已缓存个人凭据。`auth status/check` 仅展示凭据类型及本地状态,不验证远端;`logout` 删除当前 context 凭据。 YAML action 必须显式配置 `authConfig`,用于声明当前资源需要哪些认证维度: diff --git a/docs/user-guide.md b/docs/user-guide.md index bd7f114..d31a786 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -403,7 +403,7 @@ bk-cli api bk-iam GET /api/v2/systems/ ### 申请网关或接口权限 -遇到网关 API 权限不足(例如错误码 `1640301`)时,可以使用 `bk-cli apigateway apply_gateway_permission` 调用 `v2_open_apply_gateway_permission` 发起申请。该接口需要应用身份;`target_app_code` 应与请求使用的应用一致。不支持 `personal_token`:个人 token context 或显式 `Authorization: Bearer` header 在预览和提交时均返回 `unsupported_credential_type`,不会发送请求。请使用应用凭据(`bk_app_code` / `bk_app_secret` 加 `bk_token` / `bk_ticket`)或满足要求的 `access_token`,也可通过 `--context` 切换到应用 context;移除 Bearer header 后重新预览。业务系统自身的权限不足需按对应系统流程处理。 +遇到网关 API 权限不足(例如错误码 `1640301`)时,可以使用 `bk-cli apigateway apply_gateway_permission` 调用 `v2_open_apply_gateway_permission` 发起申请。该接口需要应用身份;`target_app_code` 应与请求使用的应用一致。仅支持 `bk_app_code` + `bk_app_secret`,禁止 `access_token` 和 `personal_token`:token context、显式 `Authorization: Bearer` 或含上述 token 字段的 `X-Bkapi-Authorization` 在预览和提交时均返回 `unsupported_credential_type`,不会发送请求。认证头覆盖也必须包含非空的应用 code 和 secret。只有 token 时,Agent 必须停止申请并联系应用开发者,不得通过原始 `api`、header 覆盖或自动重试绕过限制。经授权使用应用凭据时,通过 `--context` 切换对应 context,或使用 `bk_app_code` / `bk_app_secret` 加 `bk_token` / `bk_ticket` 登录。业务系统自身的权限不足需按对应系统流程处理。 **Agent 每次提交前必须询问用户,确认具体网关、申请应用、资源范围、有效期和原因;暂不允许跳过询问。** 申请会创建单据,触发通知和审批流程,成本较高。先汇总同一网关中需要的接口,将多个接口放入一个 `resource_names` 数组;只有用户明确同意整网关范围时才使用 `grant_dimension: gateway`。不要自动重试,避免重复申请单。 diff --git a/skills/bk-cli-apigateway/SKILL.md b/skills/bk-cli-apigateway/SKILL.md index 00637ab..eb45b31 100644 --- a/skills/bk-cli-apigateway/SKILL.md +++ b/skills/bk-cli-apigateway/SKILL.md @@ -86,7 +86,7 @@ CRITICAL — 开始前 MUST 先用 Read 工具读取 `../bk-cli-shared/SKILL.md` - `user_verified_required: true`:调用方需要带用户身份。 - `resource_perm_required: true`:除了有应用身份外,应用通常还需要先申请该 API 的资源权限。 -如果当前 context 使用个人 token,CLI 使用 `Authorization: Bearer `,无需补充应用或用户凭据。应结合目标部署对个人 token 的支持情况和服务端返回,判断 token 是否有效及是否具有接口权限;不能再叠加 `X-Bkapi-Authorization`。例外:下文 `apply_gateway_permission` 需要应用身份,不支持个人 token。共享登录、互斥和脱敏规则见 `../bk-cli-shared/SKILL.md`。 +如果当前 context 使用个人 token,CLI 使用 `Authorization: Bearer `,无需补充应用或用户凭据。应结合目标部署对个人 token 的支持情况和服务端返回,判断 token 是否有效及是否具有接口权限;不能再叠加 `X-Bkapi-Authorization`。例外:下文 `apply_gateway_permission` 只接受 `bk_app_code` + `bk_app_secret`,不支持 `access_token` 或个人 token。共享登录、互斥和脱敏规则见 `../bk-cli-shared/SKILL.md`。 对使用 app+user 或 `access_token` 的 agent 来说,推荐这样解读: @@ -219,7 +219,9 @@ bk-cli apigateway retrieve_gateway_api_details --gateway_name bk-iam --api_name 调用 `v2_open_apply_gateway_permission` 申请网关或批量 API 权限。需要应用身份;`target_app_code` 必须与请求使用的应用一致。 -不支持 `personal_token`。个人 token context 或显式 `Authorization: Bearer` header 在预览和提交时均返回 `unsupported_credential_type`,不会发送请求。先通过 `--context` 切换到应用 context,或使用 `bk_app_code` / `bk_app_secret` 加 `bk_token` / `bk_ticket`、满足要求的 `access_token` 登录,并移除 Bearer header。不要尝试在个人 token 上叠加 `X-Bkapi-Authorization`,也不要自动重试。 +**仅允许使用应用 `bk_app_code` + `bk_app_secret` 认证,禁止 `access_token` 和 `personal_token`。** token context、显式 `Authorization: Bearer`、含 `access_token` / `personal_token` 的 `X-Bkapi-Authorization` 在预览和提交时均返回 `unsupported_credential_type`,不会发送请求。覆盖 `X-Bkapi-Authorization` 时必须提供非空的 `bk_app_code` 和 `bk_app_secret`。 + +**Agent 若只有 access_token 或个人 token,必须停止申请,告知用户联系应用开发者处理。不得改用原始 `api`、覆盖 header 或自动重试来绕过限制。** 经授权使用应用凭据时,可通过 `--context` 选择对应 context,或使用 `bk_app_code` / `bk_app_secret` 加 `bk_token` / `bk_ticket` 登录;本命令生成的认证头仅携带应用凭据。 **Agent 每次实际提交前必须询问用户,确认目标网关、申请应用、资源范围、有效期和原因;暂不允许跳过询问。** 提交会创建申请单并触发通知和审批,成本较高。只有用户明确同意本次具体请求后才能传 `--confirmed`,该参数只是记录已获同意,不能代替询问。用户拒绝或尚未同意时停止提交;申请内容变化后需重新预览并确认。 diff --git a/tests/integration/cases/system/apigateway/APIGW-007-apigateway-apply-gateway-permission.yaml b/tests/integration/cases/system/apigateway/APIGW-007-apigateway-apply-gateway-permission.yaml index 2431492..3b2a98b 100644 --- a/tests/integration/cases/system/apigateway/APIGW-007-apigateway-apply-gateway-permission.yaml +++ b/tests/integration/cases/system/apigateway/APIGW-007-apigateway-apply-gateway-permission.yaml @@ -12,7 +12,7 @@ steps: - path: ok equals: true - name: login - command: [auth, login, --access_token, integration-token] + command: [auth, login, --bk_app_code, my-app, --bk_app_secret, integration-secret, --bk_token, integration-user] expect: exit_code: 0 checks: diff --git a/tests/integration/cases/system/apigateway/APIGW-NEG-001-apigateway-apply-gateway-permission-confirmation.yaml b/tests/integration/cases/system/apigateway/APIGW-NEG-001-apigateway-apply-gateway-permission-confirmation.yaml index c3dae66..7f94518 100644 --- a/tests/integration/cases/system/apigateway/APIGW-NEG-001-apigateway-apply-gateway-permission-confirmation.yaml +++ b/tests/integration/cases/system/apigateway/APIGW-NEG-001-apigateway-apply-gateway-permission-confirmation.yaml @@ -76,7 +76,7 @@ steps: - path: error.message contains: application identity - path: error.hint - contains: --access_token + contains: Agents must stop - name: reject personal token preview command: - apigateway @@ -91,7 +91,7 @@ steps: checks: - path: error.code equals: unsupported_credential_type - - name: login application access token + - name: login access token command: - auth - login @@ -102,6 +102,27 @@ steps: checks: - path: ok equals: true + - name: reject access token submission + command: [apigateway, apply_gateway_permission, --gateway_name, bk-iam, --confirmed, --body, '{"target_app_code":"my-app","grant_dimension":"gateway","applicant":"developer"}'] + expect: + exit_code: 1 + checks: + - path: error.code + equals: unsupported_credential_type + - name: reject access token preview + command: [apigateway, apply_gateway_permission, --gateway_name, bk-iam, --dry-run, --body, '{"target_app_code":"my-app","grant_dimension":"gateway","applicant":"developer"}'] + expect: + exit_code: 1 + checks: + - path: error.code + equals: unsupported_credential_type + - name: login application credentials + command: [auth, login, --bk_app_code, my-app, --bk_app_secret, integration-secret, --bk_token, integration-user] + expect: + exit_code: 0 + checks: + - path: ok + equals: true - name: reject explicit Bearer header with application credentials command: - apigateway @@ -118,3 +139,35 @@ steps: checks: - path: error.code equals: unsupported_credential_type + - name: reject access token header override + command: + - apigateway + - apply_gateway_permission + - --gateway_name + - bk-iam + - --confirmed + - --body + - '{"target_app_code":"my-app","grant_dimension":"gateway","applicant":"developer"}' + - --header + - 'x-bkapi-authorization:{"access_token":"integration-token"}' + expect: + exit_code: 1 + checks: + - path: error.code + equals: unsupported_credential_type + - name: reject mixed auth preview + command: + - apigateway + - apply_gateway_permission + - --gateway_name + - bk-iam + - --dry-run + - --body + - '{"target_app_code":"my-app","grant_dimension":"gateway","applicant":"developer"}' + - --header + - 'x-bkapi-authorization:{"bk_app_code":"my-app","bk_app_secret":"integration-secret","access_token":"integration-token"}' + expect: + exit_code: 1 + checks: + - path: error.code + equals: unsupported_credential_type