Files
larksuite-cli/shortcuts/apps/apps_access_scope_set.go
raistlin042 36ff632a13 fix(apps): update miaoda scopes after platform consolidation (#1127)
妙搭/spark consolidated the apps domain onto spark:app:read / spark:app:write.
The standalone spark:app:publish and spark:app.access_scope:* scopes are retired.

- +html-publish:      spark:app:publish            -> spark:app:write
- +access-scope-get:  spark:app.access_scope:read  -> spark:app:read
- +access-scope-set:  spark:app.access_scope:write -> spark:app:write

Verified against the official docs for upload_html_code_and_release,
get_app_visibility and update_app_visibility. +create/+update/+list were
already correct (spark:app:write / spark:app:read).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 19:51:59 +08:00

209 lines
6.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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/output"
"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 Miaoda app access scope (specific / public / tenant)",
Risk: "write",
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 output.ErrValidation("--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 Miaoda 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.CallAPI("PUT", path, nil, body)
if err != nil {
return err
}
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 output.ErrValidation("--targets is required when --scope=specific")
}
if err := validateTargetsJSON(targets); err != nil {
return err
}
if approver != "" && !applyEnabled {
return output.ErrValidation("--approver requires --apply-enabled")
}
if requireLogin {
return output.ErrValidation("--require-login is not allowed when --scope=specific")
}
case "public":
if targets != "" {
return output.ErrValidation("--targets is not allowed when --scope=public")
}
if applyEnabled {
return output.ErrValidation("--apply-enabled is not allowed when --scope=public")
}
if approver != "" {
return output.ErrValidation("--approver is not allowed when --scope=public")
}
if !rctx.Cmd.Flags().Changed("require-login") {
return output.ErrValidation("--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 output.ErrValidation("no extra flags allowed when --scope=tenant")
}
default:
return output.ErrValidation("--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 output.ErrValidation("--targets is not valid JSON: %v", err)
}
if len(items) == 0 {
return output.ErrValidation("--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 output.ErrValidation("--targets[%d].type %q must be one of: user / department / chat", i, typ)
}
if id, _ := t["id"].(string); strings.TrimSpace(id) == "" {
return output.ErrValidation("--targets[%d].id is empty", i)
}
}
return nil
}
// scopeStringToServerEnum 把 CLI 友好的 scope 字符串映射成后端字符串枚举。
// CLI 用户 / Agent 仍然写 specific / public / tenantbody 里发后端枚举名。
// 后端语义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, output.ErrValidation("--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, output.ErrValidation("--targets is not valid JSON: %v", 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
}