larksuite--cli
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
219 行
8.0 KiB
Go
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
|
|
}
|