mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
219 lines
8.0 KiB
Go
219 lines
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
|
||
}
|