Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions shortcuts/common/permission_grant.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package common

import (
"fmt"
"strings"

"github.com/larksuite/cli/internal/validate"
)

const (
PermissionGrantGranted = "granted"
PermissionGrantSkipped = "skipped"
PermissionGrantFailed = "failed"
permissionGrantPerm = "full_access"
permissionGrantPermHint = "可管理权限"
)

// AutoGrantCurrentUserDrivePermission grants full_access on a newly created
// Drive resource to the current CLI user when the shortcut runs as bot.
//
// Callers should attach the returned result only when it is non-nil.
func AutoGrantCurrentUserDrivePermission(runtime *RuntimeContext, token, resourceType string) map[string]interface{} {
if runtime == nil || !runtime.IsBot() {
return nil
}

token = strings.TrimSpace(token)
resourceType = strings.TrimSpace(resourceType)
if token == "" || resourceType == "" {
return buildPermissionGrantResult(
PermissionGrantSkipped,
"",
fmt.Sprintf("The operation did not return a permission target (missing token/type), so current user %s was not granted. You can retry later or continue using bot identity.", permissionGrantPermMessage()),
)
}

return autoGrantCurrentUserDrivePermission(runtime, token, resourceType)
}

func autoGrantCurrentUserDrivePermission(runtime *RuntimeContext, token, resourceType string) map[string]interface{} {
userOpenID := strings.TrimSpace(runtime.UserOpenId())
if userOpenID == "" {
return buildPermissionGrantResult(
PermissionGrantSkipped,
"",
fmt.Sprintf("Resource was created with bot identity, but no current CLI user open_id is configured, so current user %s was not granted. You can retry later or continue using bot identity.", permissionGrantPermMessage()),
)
}

body := map[string]interface{}{
"member_type": "openid",
"member_id": userOpenID,
"perm": permissionGrantPerm,
"type": "user",
}
if permType := permissionGrantPermType(resourceType); permType != "" {
body["perm_type"] = permType
}

_, err := runtime.CallAPI(
"POST",
fmt.Sprintf("/open-apis/drive/v1/permissions/%s/members", validate.EncodePathSegment(token)),
map[string]interface{}{
"type": resourceType,
"need_notification": false,
},
body,
)
if err != nil {
return buildPermissionGrantResult(
PermissionGrantFailed,
userOpenID,
fmt.Sprintf("Resource was created, but granting current user %s failed: %s. You can retry later or continue using bot identity.", permissionGrantPermMessage(), compactPermissionGrantError(err)),
)
}

return buildPermissionGrantResult(
PermissionGrantGranted,
userOpenID,
fmt.Sprintf("Granted the current CLI user %s on the new %s.", permissionGrantPermMessage(), permissionTargetLabel(resourceType)),
)
}

func buildPermissionGrantResult(status, userOpenID, message string) map[string]interface{} {
result := map[string]interface{}{
"status": status,
"perm": permissionGrantPerm,
"message": message,
}
if userOpenID != "" {
result["user_open_id"] = userOpenID
result["member_type"] = "openid"
}
return result
}

func permissionGrantPermMessage() string {
return permissionGrantPerm + " (" + permissionGrantPermHint + ")"
}

func permissionGrantPermType(resourceType string) string {
switch resourceType {
case "wiki":
return "container"
default:
return ""
}
}

func permissionTargetLabel(resourceType string) string {
switch resourceType {
case "wiki":
return "wiki node"
case "doc", "docx":
return "document"
case "sheet":
return "spreadsheet"
case "bitable", "base":
return "base"
case "file":
return "file"
case "folder":
return "folder"
default:
return "resource"
}
}

func compactPermissionGrantError(err error) string {
if err == nil {
return ""
}
return strings.Join(strings.Fields(err.Error()), " ")
}
104 changes: 72 additions & 32 deletions shortcuts/doc/docs_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package doc

import (
"context"
"strings"

"github.com/larksuite/cli/shortcuts/common"
)
Expand Down Expand Up @@ -40,51 +41,90 @@ var DocsCreate = common.Shortcut{
return nil
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
args := map[string]interface{}{
"markdown": runtime.Str("markdown"),
}
if v := runtime.Str("title"); v != "" {
args["title"] = v
}
if v := runtime.Str("folder-token"); v != "" {
args["folder_token"] = v
}
if v := runtime.Str("wiki-node"); v != "" {
args["wiki_node"] = v
}
if v := runtime.Str("wiki-space"); v != "" {
args["wiki_space"] = v
}
return common.NewDryRunAPI().
args := buildDocsCreateArgs(runtime)
d := common.NewDryRunAPI().
POST(common.MCPEndpoint(runtime.Config.Brand)).
Desc("MCP tool: create-doc").
Body(map[string]interface{}{"method": "tools/call", "params": map[string]interface{}{"name": "create-doc", "arguments": args}}).
Set("mcp_tool", "create-doc").Set("args", args)
if runtime.IsBot() {
d.Desc("After create-doc succeeds in bot mode, the CLI will also try to grant the current CLI user full_access (可管理权限) on the new document.")
}
return d
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
args := map[string]interface{}{
"markdown": runtime.Str("markdown"),
}
if v := runtime.Str("title"); v != "" {
args["title"] = v
}
if v := runtime.Str("folder-token"); v != "" {
args["folder_token"] = v
}
if v := runtime.Str("wiki-node"); v != "" {
args["wiki_node"] = v
}
if v := runtime.Str("wiki-space"); v != "" {
args["wiki_space"] = v
}

args := buildDocsCreateArgs(runtime)
result, err := common.CallMCPTool(runtime, "create-doc", args)
if err != nil {
return err
}
augmentDocsCreateResult(runtime, result)

normalizeDocsUpdateResult(result, runtime.Str("markdown"))
runtime.Out(result, nil)
return nil
},
}

func buildDocsCreateArgs(runtime *common.RuntimeContext) map[string]interface{} {
args := map[string]interface{}{
"markdown": runtime.Str("markdown"),
}
if v := runtime.Str("title"); v != "" {
args["title"] = v
}
if v := runtime.Str("folder-token"); v != "" {
args["folder_token"] = v
}
if v := runtime.Str("wiki-node"); v != "" {
args["wiki_node"] = v
}
if v := runtime.Str("wiki-space"); v != "" {
args["wiki_space"] = v
}
return args
}

type docsPermissionTarget struct {
Token string
Type string
}

func augmentDocsCreateResult(runtime *common.RuntimeContext, result map[string]interface{}) {
target := selectDocsPermissionTarget(result)
if grant := common.AutoGrantCurrentUserDrivePermission(runtime, target.Token, target.Type); grant != nil {
result["permission_grant"] = grant
}
}

func selectDocsPermissionTarget(result map[string]interface{}) docsPermissionTarget {
if ref, ok := parseDocsPermissionTargetFromURL(common.GetString(result, "doc_url")); ok {
return ref
}

docID := strings.TrimSpace(common.GetString(result, "doc_id"))
if docID != "" {
return docsPermissionTarget{Token: docID, Type: "docx"}
}
return docsPermissionTarget{}
}

func parseDocsPermissionTargetFromURL(docURL string) (docsPermissionTarget, bool) {
if strings.TrimSpace(docURL) == "" {
return docsPermissionTarget{}, false
}

ref, err := parseDocumentRef(docURL)
if err != nil {
return docsPermissionTarget{}, false
}

switch ref.Kind {
case "wiki":
return docsPermissionTarget{Token: ref.Token, Type: "wiki"}, true
case "doc", "docx":
return docsPermissionTarget{Token: ref.Token, Type: ref.Kind}, true
default:
return docsPermissionTarget{}, false
}
}
Loading
Loading