项目文件夹

文件
wehub-resource-sync bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:22:54 +08:00

219 行
8.0 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package apps
import (
"context"
"encoding/json"
"fmt"
"io"
"strings"
"github.com/larksuite/cli/internal/validate"
"github.com/larksuite/cli/shortcuts/common"
)
var allowedAccessTargetTypes = map[string]bool{
"user": true,
"department": true,
"chat": true,
}
// AppsAccessScopeSet sets the app's access scope (specific / public / tenant).
var AppsAccessScopeSet = common.Shortcut{
Service: appsService,
Command: "+access-scope-set",
Description: "Set app access scope (specific / public / tenant)",
Risk: "write",
Tips: []string{
`Example: lark-cli apps +access-scope-set --app-id <app_id> --scope tenant`,
`Example: lark-cli apps +access-scope-set --app-id <app_id> --scope public --require-login`,
`Example: lark-cli apps +access-scope-set --app-id <app_id> --scope specific --targets '[{"type":"user","id":"<open_id>"}]'`,
},
Scopes: []string{"spark:app:write"},
AuthTypes: []string{"user"},
HasFormat: true,
Flags: []common.Flag{
{Name: "app-id", Desc: "app ID", Required: true},
{Name: "scope", Desc: "scope: specific | public | tenant", Required: true, Enum: []string{"specific", "public", "tenant"}},
{Name: "targets", Desc: `targets JSON array: [{"type":"user|department|chat","id":"..."}, ...]`},
{Name: "apply-enabled", Type: "bool", Desc: "allow apply for access (scope=specific)"},
{Name: "approver", Desc: "approver open_id (when --apply-enabled; server allows exactly one)"},
{Name: "require-login", Type: "bool", Desc: "require login (scope=public)"},
},
Validate: func(ctx context.Context, rctx *common.RuntimeContext) error {
if strings.TrimSpace(rctx.Str("app-id")) == "" {
return appsValidationParamError("--app-id", "--app-id is required")
}
return validateAccessScopeFlags(rctx)
},
DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI {
appID := strings.TrimSpace(rctx.Str("app-id"))
dry := common.NewDryRunAPI().
PUT(fmt.Sprintf("%s/apps/%s/access-scope", apiBasePath, validate.EncodePathSegment(appID))).
Desc("Set app access scope")
body, bodyErr := buildAccessScopeBody(rctx)
if bodyErr != nil {
dry.Set("body_error", bodyErr.Error())
} else {
dry.Body(body)
}
return dry
},
Execute: func(ctx context.Context, rctx *common.RuntimeContext) error {
body, err := buildAccessScopeBody(rctx)
if err != nil {
return err
}
appID := strings.TrimSpace(rctx.Str("app-id"))
path := fmt.Sprintf("%s/apps/%s/access-scope", apiBasePath, validate.EncodePathSegment(appID))
data, err := rctx.CallAPITyped("PUT", path, nil, body)
if err != nil {
return withAppsHint(err, "verify --app-id is correct; for scope=specific, each --targets id must be a valid open_id/department_id/chat_id and --approver a valid open_id; review the current scope with `lark-cli apps +access-scope-get --app-id <app_id>`")
}
rctx.OutFormat(data, nil, func(w io.Writer) {
fmt.Fprintf(w, "access-scope set: %s\n", rctx.Str("scope"))
})
return nil
},
}
func validateAccessScopeFlags(rctx *common.RuntimeContext) error {
scope := rctx.Str("scope")
targets := strings.TrimSpace(rctx.Str("targets"))
applyEnabled := rctx.Bool("apply-enabled")
approver := strings.TrimSpace(rctx.Str("approver"))
requireLogin := rctx.Bool("require-login")
switch scope {
case "specific":
if targets == "" {
return appsValidationParamError("--targets", "--targets is required when --scope=specific")
}
if err := validateTargetsJSON(targets); err != nil {
return err
}
if approver != "" && !applyEnabled {
return appsValidationParamError("--approver", "--approver requires --apply-enabled")
}
if requireLogin {
return appsValidationParamError("--require-login", "--require-login is not allowed when --scope=specific")
}
case "public":
if targets != "" {
return appsValidationParamError("--targets", "--targets is not allowed when --scope=public")
}
if applyEnabled {
return appsValidationParamError("--apply-enabled", "--apply-enabled is not allowed when --scope=public")
}
if approver != "" {
return appsValidationParamError("--approver", "--approver is not allowed when --scope=public")
}
if !rctx.Cmd.Flags().Changed("require-login") {
return appsValidationParamError("--require-login", "--require-login is required when --scope=public (pass true or false explicitly; do not rely on the default)")
}
case "tenant":
if targets != "" || applyEnabled || approver != "" || requireLogin {
return appsValidationError("no extra flags allowed when --scope=tenant").
WithParams(
appsInvalidParam("--targets", "not allowed when --scope=tenant"),
appsInvalidParam("--apply-enabled", "not allowed when --scope=tenant"),
appsInvalidParam("--approver", "not allowed when --scope=tenant"),
appsInvalidParam("--require-login", "not allowed when --scope=tenant"),
)
}
default:
return appsValidationParamError("--scope", "--scope must be specific / public / tenant")
}
return nil
}
func validateTargetsJSON(targetsJSON string) error {
var items []map[string]interface{}
if err := json.Unmarshal([]byte(targetsJSON), &items); err != nil {
return appsValidationParamError("--targets", "--targets is not valid JSON: %v", err).WithCause(err)
}
if len(items) == 0 {
return appsValidationParamError("--targets", "--targets must contain at least one entry; specific scope requires concrete user/department/chat ids")
}
for i, t := range items {
typ, _ := t["type"].(string)
if !allowedAccessTargetTypes[typ] {
return appsValidationParamError("--targets", "--targets[%d].type %q must be one of: user / department / chat", i, typ)
}
if id, _ := t["id"].(string); strings.TrimSpace(id) == "" {
return appsValidationParamError("--targets", "--targets[%d].id is empty", i)
}
}
return nil
}
// scopeStringToServerEnum 把 CLI 友好的 scope 字符串映射成后端字符串枚举。
// CLI 用户 / Agent 仍然写 specific / public / tenant,body 里发后端枚举名。
// 后端语义:All=互联网公开 / Tenant=组织内 / Range=部分人员。
var scopeStringToServerEnum = map[string]string{
"public": "All",
"tenant": "Tenant",
"specific": "Range",
}
func buildAccessScopeBody(rctx *common.RuntimeContext) (map[string]interface{}, error) {
scope := rctx.Str("scope")
enum, ok := scopeStringToServerEnum[scope]
if !ok {
return nil, appsValidationParamError("--scope", "--scope must be specific / public / tenant, got %q", scope)
}
body := map[string]interface{}{"scope": enum}
switch scope {
case "specific":
// 用户传统一格式 [{type:user|department|chat, id:...}],body 里拆 3 个并列数组发后端。
var targets []map[string]interface{}
if err := json.Unmarshal([]byte(rctx.Str("targets")), &targets); err != nil {
return nil, appsValidationParamError("--targets", "--targets is not valid JSON: %v", err).WithCause(err)
}
users, departments, chats := splitAccessScopeTargets(targets)
if len(users) > 0 {
body["users"] = users
}
if len(departments) > 0 {
body["departments"] = departments
}
if len(chats) > 0 {
body["chats"] = chats
}
if rctx.Bool("apply-enabled") {
applyConfig := map[string]interface{}{"enabled": true}
if approver := strings.TrimSpace(rctx.Str("approver")); approver != "" {
applyConfig["approvers"] = []string{approver}
}
body["apply_config"] = applyConfig
}
case "public":
body["require_login"] = rctx.Bool("require-login")
}
return body, nil
}
// splitAccessScopeTargets 把统一 [{type,id}] 形态拆成后端要求的 users/departments/chats 三个数组。
func splitAccessScopeTargets(targets []map[string]interface{}) (users, departments, chats []string) {
for _, t := range targets {
typ, _ := t["type"].(string)
id, _ := t["id"].(string)
id = strings.TrimSpace(id)
if id == "" {
continue
}
switch typ {
case "user":
users = append(users, id)
case "department":
departments = append(departments, id)
case "chat":
chats = append(chats, id)
}
}
return
}