mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 22:24:31 +08:00
Compare commits
1 Commits
main
...
feat/open_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db47ea0f47 |
287
shortcuts/drive/drive_public_permission.go
Normal file
287
shortcuts/drive/drive_public_permission.go
Normal file
@@ -0,0 +1,287 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package drive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/validate"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
const drivePublicPermissionScope = "docs:permission.setting:write_only"
|
||||
|
||||
var drivePublicPermissionTypes = []string{
|
||||
"doc", "sheet", "file", "wiki", "bitable", "docx",
|
||||
"mindnote", "minutes", "slides",
|
||||
}
|
||||
|
||||
var drivePublicPermissionURLPathToType = []struct {
|
||||
Prefix string
|
||||
Type string
|
||||
}{
|
||||
{"/mindnotes/", "mindnote"},
|
||||
{"/bitable/", "bitable"},
|
||||
{"/sheets/", "sheet"},
|
||||
{"/minutes/", "minutes"},
|
||||
{"/slides/", "slides"},
|
||||
{"/docx/", "docx"},
|
||||
{"/wiki/", "wiki"},
|
||||
{"/base/", "bitable"},
|
||||
{"/file/", "file"},
|
||||
{"/doc/", "doc"},
|
||||
}
|
||||
|
||||
var (
|
||||
drivePublicPermissionSecurityEntities = []string{"anyone_can_view", "anyone_can_edit", "only_full_access"}
|
||||
drivePublicPermissionCommentEntities = []string{"anyone_can_view", "anyone_can_edit"}
|
||||
drivePublicPermissionShareEntities = []string{"anyone", "same_tenant"}
|
||||
drivePublicPermissionManageEntities = []string{"collaborator_can_view", "collaborator_can_edit", "collaborator_full_access"}
|
||||
drivePublicPermissionLinkEntities = []string{
|
||||
"tenant_readable", "tenant_editable",
|
||||
"anyone_readable", "anyone_editable",
|
||||
"partner_tenant_readable", "partner_tenant_editable",
|
||||
"closed",
|
||||
}
|
||||
drivePublicPermissionCopyEntities = drivePublicPermissionSecurityEntities
|
||||
drivePublicPermissionExternalEntities = []string{"open", "closed", "allow_share_partner_tenant"}
|
||||
drivePublicPermissionPermTypes = []string{"container", "single_page"}
|
||||
)
|
||||
|
||||
var drivePublicPermissionBodyFlags = []struct {
|
||||
Flag string
|
||||
JSON string
|
||||
}{
|
||||
{"security-entity", "security_entity"},
|
||||
{"comment-entity", "comment_entity"},
|
||||
{"share-entity", "share_entity"},
|
||||
{"manage-collaborator-entity", "manage_collaborator_entity"},
|
||||
{"link-share-entity", "link_share_entity"},
|
||||
{"copy-entity", "copy_entity"},
|
||||
{"external-access-entity", "external_access_entity"},
|
||||
}
|
||||
|
||||
// DrivePublicPermissionUpdate updates public permission settings using the
|
||||
// drive/v2 public-permission PATCH endpoint.
|
||||
var DrivePublicPermissionUpdate = common.Shortcut{
|
||||
Service: "drive",
|
||||
Command: "+public-permission-update",
|
||||
Description: "Update public permission settings on a Drive document or file",
|
||||
Risk: "high-risk-write",
|
||||
Scopes: []string{drivePublicPermissionScope},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
Flags: []common.Flag{
|
||||
{Name: "token", Desc: "target token or document URL (docx/sheets/base/file/wiki/doc/mindnotes/minutes/slides)", Required: true},
|
||||
{Name: "type", Desc: "target type; auto-inferred from URL when omitted", Enum: drivePublicPermissionTypes},
|
||||
{Name: "security-entity", Desc: "who can copy, duplicate, print, and download", Enum: drivePublicPermissionSecurityEntities},
|
||||
{Name: "comment-entity", Desc: "who can comment", Enum: drivePublicPermissionCommentEntities},
|
||||
{Name: "share-entity", Desc: "who can view, add, or remove collaborators at the org level", Enum: drivePublicPermissionShareEntities},
|
||||
{Name: "manage-collaborator-entity", Desc: "who can manage collaborators", Enum: drivePublicPermissionManageEntities},
|
||||
{Name: "link-share-entity", Desc: "link sharing setting", Enum: drivePublicPermissionLinkEntities},
|
||||
{Name: "copy-entity", Desc: "who can create copies", Enum: drivePublicPermissionCopyEntities},
|
||||
{Name: "external-access-entity", Desc: "external sharing setting", Enum: drivePublicPermissionExternalEntities},
|
||||
{Name: "perm-type", Desc: "permission scope for link/external access changes", Enum: drivePublicPermissionPermTypes},
|
||||
},
|
||||
Tips: []string{
|
||||
"Calls PATCH /open-apis/drive/v2/permissions/:token/public; use --dry-run first to inspect the exact body.",
|
||||
"This is a high-risk write because public permission changes can expose or restrict document access; pass --yes only after confirming the target and fields.",
|
||||
},
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
_, _, err := resolveDrivePublicPermissionTarget(runtime.Str("token"), runtime.Str("type"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateDrivePublicPermissionBody(runtime); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
token, docType, err := resolveDrivePublicPermissionTarget(runtime.Str("token"), runtime.Str("type"))
|
||||
if err != nil {
|
||||
return common.NewDryRunAPI().Set("error", err.Error())
|
||||
}
|
||||
if err := validateDrivePublicPermissionBody(runtime); err != nil {
|
||||
return common.NewDryRunAPI().Set("error", err.Error())
|
||||
}
|
||||
return common.NewDryRunAPI().
|
||||
Desc("Update Drive public permission settings").
|
||||
PATCH("/open-apis/drive/v2/permissions/:token/public").
|
||||
Params(map[string]interface{}{"type": docType}).
|
||||
Body(buildDrivePublicPermissionBody(runtime)).
|
||||
Set("token", token)
|
||||
},
|
||||
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
token, docType, err := resolveDrivePublicPermissionTarget(runtime.Str("token"), runtime.Str("type"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body := buildDrivePublicPermissionBody(runtime)
|
||||
|
||||
fmt.Fprintf(runtime.IO().ErrOut, "Updating public permission settings on %s %s...\n",
|
||||
docType, common.MaskToken(token))
|
||||
|
||||
data, err := runtime.CallAPITyped("PATCH",
|
||||
fmt.Sprintf("/open-apis/drive/v2/permissions/%s/public", validate.EncodePathSegment(token)),
|
||||
map[string]interface{}{"type": docType},
|
||||
body,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.Out(data, nil)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func resolveDrivePublicPermissionTarget(raw, explicitType string) (resourceID, docType string, err error) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
explicitType = strings.TrimSpace(explicitType)
|
||||
if raw == "" {
|
||||
return "", "", errs.NewValidationError(errs.SubtypeInvalidArgument, "--token is required").WithParam("--token")
|
||||
}
|
||||
|
||||
if strings.Contains(raw, "://") {
|
||||
parsed, parseErr := url.Parse(raw)
|
||||
if parseErr != nil {
|
||||
return "", "", errs.NewValidationError(errs.SubtypeInvalidArgument,
|
||||
"invalid URL %q: %v",
|
||||
raw,
|
||||
parseErr,
|
||||
).WithParam("--token").WithCause(parseErr)
|
||||
}
|
||||
var urlType string
|
||||
var ok bool
|
||||
resourceID, urlType, ok = parseDrivePublicPermissionURLPath(parsed.Path)
|
||||
if resourceID == "" {
|
||||
return "", "", errs.NewValidationError(errs.SubtypeInvalidArgument,
|
||||
"could not infer token from URL %q: supported paths are /docx/, /sheets/, /base/, /bitable/, /file/, /wiki/, /doc/, /mindnotes/, /minutes/, /slides/. Pass a bare token with --type instead if the URL shape is unusual",
|
||||
raw,
|
||||
).WithParam("--token")
|
||||
}
|
||||
if ok && explicitType == "" {
|
||||
docType = urlType
|
||||
}
|
||||
} else {
|
||||
resourceID = raw
|
||||
}
|
||||
|
||||
if explicitType != "" {
|
||||
docType = explicitType
|
||||
}
|
||||
if docType == "" {
|
||||
return "", "", errs.NewValidationError(errs.SubtypeInvalidArgument,
|
||||
"--type is required when --token is a bare token; accepted values: %s",
|
||||
strings.Join(drivePublicPermissionTypes, ", "),
|
||||
).WithParam("--type")
|
||||
}
|
||||
if err := validate.ResourceName(resourceID, "--token"); err != nil {
|
||||
return "", "", errs.NewValidationError(errs.SubtypeInvalidArgument, "%s", err).WithParam("--token").WithCause(err)
|
||||
}
|
||||
return resourceID, docType, nil
|
||||
}
|
||||
|
||||
func parseDrivePublicPermissionURLPath(path string) (resourceID, docType string, ok bool) {
|
||||
for _, mapping := range drivePublicPermissionURLPathToType {
|
||||
if !strings.HasPrefix(path, mapping.Prefix) {
|
||||
continue
|
||||
}
|
||||
candidate := path[len(mapping.Prefix):]
|
||||
candidate = strings.TrimRight(candidate, "/")
|
||||
if idx := strings.IndexByte(candidate, '/'); idx >= 0 {
|
||||
candidate = candidate[:idx]
|
||||
}
|
||||
candidate = strings.TrimSpace(candidate)
|
||||
if candidate == "" {
|
||||
return "", "", false
|
||||
}
|
||||
return candidate, mapping.Type, true
|
||||
}
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
func validateDrivePublicPermissionBody(runtime *common.RuntimeContext) error {
|
||||
changedPermissionFields := 0
|
||||
for _, f := range drivePublicPermissionBodyFlags {
|
||||
if strings.TrimSpace(runtime.Str(f.Flag)) != "" {
|
||||
changedPermissionFields++
|
||||
}
|
||||
}
|
||||
if changedPermissionFields == 0 {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument,
|
||||
"nothing to update: specify at least one of --security-entity, --comment-entity, --share-entity, --manage-collaborator-entity, --link-share-entity, --copy-entity, or --external-access-entity",
|
||||
)
|
||||
}
|
||||
if err := validateExternalLinkShareCombo(
|
||||
strings.TrimSpace(runtime.Str("external-access-entity")),
|
||||
strings.TrimSpace(runtime.Str("link-share-entity")),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if runtime.Str("perm-type") == "single_page" {
|
||||
hasLinkOrExternal := strings.TrimSpace(runtime.Str("link-share-entity")) != "" ||
|
||||
strings.TrimSpace(runtime.Str("external-access-entity")) != ""
|
||||
if !hasLinkOrExternal {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument,
|
||||
"--perm-type single_page is only supported with --link-share-entity or --external-access-entity",
|
||||
).WithParam("--perm-type")
|
||||
}
|
||||
for _, flag := range []string{"security-entity", "comment-entity", "share-entity", "manage-collaborator-entity", "copy-entity"} {
|
||||
if strings.TrimSpace(runtime.Str(flag)) != "" {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument,
|
||||
"--perm-type single_page only supports --link-share-entity and --external-access-entity; remove --%s",
|
||||
flag,
|
||||
).WithParam("--perm-type")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateExternalLinkShareCombo checks that link_share_entity does not exceed
|
||||
// the external sharing boundary set by external_access_entity.
|
||||
//
|
||||
// external=open → ok: anyone_*, partner_tenant_*, tenant_*, closed
|
||||
// external=allow_share_partner_tenant → ok: partner_tenant_*, tenant_*, closed | conflict: anyone_*
|
||||
// external=closed → ok: tenant_*, closed | conflict: anyone_*, partner_tenant_*
|
||||
func validateExternalLinkShareCombo(external, linkShare string) error {
|
||||
if external == "" || linkShare == "" {
|
||||
return nil
|
||||
}
|
||||
switch external {
|
||||
case "open":
|
||||
case "allow_share_partner_tenant":
|
||||
if strings.HasPrefix(linkShare, "anyone_") {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument,
|
||||
"--link-share-entity %q conflicts with --external-access-entity allow_share_partner_tenant: anyone_* requires external=open",
|
||||
linkShare,
|
||||
).WithParam("--link-share-entity")
|
||||
}
|
||||
case "closed":
|
||||
if strings.HasPrefix(linkShare, "anyone_") || strings.HasPrefix(linkShare, "partner_tenant_") {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument,
|
||||
"--link-share-entity %q conflicts with --external-access-entity closed: only tenant_* or closed are allowed",
|
||||
linkShare,
|
||||
).WithParam("--link-share-entity")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildDrivePublicPermissionBody(runtime *common.RuntimeContext) map[string]interface{} {
|
||||
body := make(map[string]interface{})
|
||||
for _, f := range drivePublicPermissionBodyFlags {
|
||||
if value := strings.TrimSpace(runtime.Str(f.Flag)); value != "" {
|
||||
body[f.JSON] = value
|
||||
}
|
||||
}
|
||||
if value := strings.TrimSpace(runtime.Str("perm-type")); value != "" {
|
||||
body["perm_type"] = value
|
||||
}
|
||||
return body
|
||||
}
|
||||
329
shortcuts/drive/drive_public_permission_test.go
Normal file
329
shortcuts/drive/drive_public_permission_test.go
Normal file
@@ -0,0 +1,329 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package drive
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/httpmock"
|
||||
)
|
||||
|
||||
func TestResolveDrivePublicPermissionTarget_BareTokenNeedsType(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, _, err := resolveDrivePublicPermissionTarget("doxTok123", "")
|
||||
assertDrivePublicPermissionValidationError(t, err, "--type", "--type is required")
|
||||
}
|
||||
|
||||
func TestResolveDrivePublicPermissionTarget_URLInference(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
raw string
|
||||
wantTok string
|
||||
wantType string
|
||||
}{
|
||||
{"docx", "https://example.feishu.cn/docx/doxTok123?from=share", "doxTok123", "docx"},
|
||||
{"docx trailing path", "https://example.feishu.cn/docx/doxTok123/extra/path?from=share", "doxTok123", "docx"},
|
||||
{"sheet", "https://example.feishu.cn/sheets/shtTok456?sheet=abc", "shtTok456", "sheet"},
|
||||
{"base", "https://example.feishu.cn/base/bscTok789", "bscTok789", "bitable"},
|
||||
{"file", "https://example.feishu.cn/file/boxTok111", "boxTok111", "file"},
|
||||
{"wiki", "https://example.feishu.cn/wiki/wikTok222", "wikTok222", "wiki"},
|
||||
{"legacy doc", "https://example.feishu.cn/doc/docTok333", "docTok333", "doc"},
|
||||
{"mindnote", "https://example.feishu.cn/mindnotes/mnTok444", "mnTok444", "mindnote"},
|
||||
{"minutes", "https://example.feishu.cn/minutes/obcnTok555", "obcnTok555", "minutes"},
|
||||
{"slides", "https://example.feishu.cn/slides/slTok666", "slTok666", "slides"},
|
||||
}
|
||||
for _, temp := range tests {
|
||||
tt := temp
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
token, docType, err := resolveDrivePublicPermissionTarget(tt.raw, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if token != tt.wantTok || docType != tt.wantType {
|
||||
t.Fatalf("got target (%q, %q), want (%q, %q)", token, docType, tt.wantTok, tt.wantType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveDrivePublicPermissionTarget_ExplicitTypeOverridesURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
token, docType, err := resolveDrivePublicPermissionTarget("https://example.feishu.cn/docx/doxTok123", "wiki")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if token != "doxTok123" || docType != "wiki" {
|
||||
t.Fatalf("got target (%q, %q), want (doxTok123, wiki)", token, docType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveDrivePublicPermissionTarget_RejectsMarkerOutsidePath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []string{
|
||||
"https://example.feishu.cn/share?redirect=/docx/doxTok123",
|
||||
"https://example.feishu.cn/share#/docx/doxTok123",
|
||||
"https://example.feishu.cn/space/docx/doxTok123",
|
||||
"https://example.feishu.cn/foo/bitable/bscTok789",
|
||||
}
|
||||
for _, raw := range tests {
|
||||
t.Run(raw, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, _, err := resolveDrivePublicPermissionTarget(raw, "")
|
||||
assertDrivePublicPermissionValidationError(t, err, "--token", "could not infer token from URL")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDrivePublicPermissionUpdate_DryRun(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, driveTestConfig())
|
||||
err := mountAndRunDrive(t, DrivePublicPermissionUpdate, []string{
|
||||
"+public-permission-update",
|
||||
"--token", "https://example.feishu.cn/docx/doxTok123?from=share",
|
||||
"--external-access-entity", "open",
|
||||
"--link-share-entity", "anyone_readable",
|
||||
"--perm-type", "single_page",
|
||||
"--dry-run", "--as", "user",
|
||||
}, f, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
out := stdout.String()
|
||||
for _, want := range []string{
|
||||
"/open-apis/drive/v2/permissions/doxTok123/public",
|
||||
`"PATCH"`,
|
||||
`"type": "docx"`,
|
||||
`"external_access_entity": "open"`,
|
||||
`"link_share_entity": "anyone_readable"`,
|
||||
`"perm_type": "single_page"`,
|
||||
`"` + "to" + `ken": "doxTok123"`,
|
||||
} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Fatalf("dry-run output missing %q:\n%s", want, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDrivePublicPermissionUpdate_ValidateRejectsNoBodyFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, driveTestConfig())
|
||||
err := mountAndRunDrive(t, DrivePublicPermissionUpdate, []string{
|
||||
"+public-permission-update",
|
||||
"--token", "doxTok123",
|
||||
"--type", "docx",
|
||||
"--as", "user",
|
||||
}, f, stdout)
|
||||
assertDrivePublicPermissionValidationError(t, err, "", "nothing to update")
|
||||
}
|
||||
|
||||
func TestDrivePublicPermissionUpdate_ValidateRejectsPermTypeOnly(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, driveTestConfig())
|
||||
err := mountAndRunDrive(t, DrivePublicPermissionUpdate, []string{
|
||||
"+public-permission-update",
|
||||
"--token", "doxTok123",
|
||||
"--type", "docx",
|
||||
"--perm-type", "single_page",
|
||||
"--as", "user",
|
||||
}, f, stdout)
|
||||
assertDrivePublicPermissionValidationError(t, err, "", "nothing to update")
|
||||
}
|
||||
|
||||
func TestDrivePublicPermissionUpdate_ValidateRejectsSinglePageForUnsupportedField(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, driveTestConfig())
|
||||
err := mountAndRunDrive(t, DrivePublicPermissionUpdate, []string{
|
||||
"+public-permission-update",
|
||||
"--token", "doxTok123",
|
||||
"--type", "docx",
|
||||
"--security-entity", "only_full_access",
|
||||
"--perm-type", "single_page",
|
||||
"--as", "user",
|
||||
}, f, stdout)
|
||||
assertDrivePublicPermissionValidationError(t, err, "--perm-type", "--perm-type single_page")
|
||||
}
|
||||
|
||||
func TestDrivePublicPermissionUpdate_ValidateRejectsSinglePageMixedFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, driveTestConfig())
|
||||
err := mountAndRunDrive(t, DrivePublicPermissionUpdate, []string{
|
||||
"+public-permission-update",
|
||||
"--token", "doxTok123",
|
||||
"--type", "docx",
|
||||
"--link-share-entity", "closed",
|
||||
"--copy-entity", "only_full_access",
|
||||
"--perm-type", "single_page",
|
||||
"--as", "user",
|
||||
}, f, stdout)
|
||||
assertDrivePublicPermissionValidationError(t, err, "--perm-type", "remove --copy-entity")
|
||||
}
|
||||
|
||||
func TestValidateExternalLinkShareCombo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
external string
|
||||
link string
|
||||
wantErr bool
|
||||
contains string
|
||||
}{
|
||||
// Both empty: no validation needed.
|
||||
{"both empty", "", "", false, ""},
|
||||
// Only one set: no combo to check.
|
||||
{"only external", "open", "", false, ""},
|
||||
{"only link", "", "anyone_readable", false, ""},
|
||||
|
||||
// external=open
|
||||
{"open + anyone_readable", "open", "anyone_readable", false, ""},
|
||||
{"open + tenant_readable", "open", "tenant_readable", false, ""},
|
||||
{"open + closed", "open", "closed", false, ""},
|
||||
{"open + partner_tenant_readable", "open", "partner_tenant_readable", false, ""},
|
||||
|
||||
// external=allow_share_partner_tenant
|
||||
{"partner + partner_tenant_readable", "allow_share_partner_tenant", "partner_tenant_readable", false, ""},
|
||||
{"partner + tenant_readable", "allow_share_partner_tenant", "tenant_readable", false, ""},
|
||||
{"partner + closed", "allow_share_partner_tenant", "closed", false, ""},
|
||||
{"partner + anyone_readable", "allow_share_partner_tenant", "anyone_readable", true, "anyone_* requires external=open"},
|
||||
|
||||
// external=closed
|
||||
{"closed + tenant_readable", "closed", "tenant_readable", false, ""},
|
||||
{"closed + closed", "closed", "closed", false, ""},
|
||||
{"closed + anyone_readable", "closed", "anyone_readable", true, "only tenant_* or closed are allowed"},
|
||||
{"closed + partner_tenant_readable", "closed", "partner_tenant_readable", true, "only tenant_* or closed are allowed"},
|
||||
}
|
||||
for _, temp := range tests {
|
||||
tt := temp
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := validateExternalLinkShareCombo(tt.external, tt.link)
|
||||
if tt.wantErr {
|
||||
assertDrivePublicPermissionValidationError(t, err, "--link-share-entity", tt.contains)
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDrivePublicPermissionUpdate_ValidateRejectsExternalLinkCombo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, driveTestConfig())
|
||||
err := mountAndRunDrive(t, DrivePublicPermissionUpdate, []string{
|
||||
"+public-permission-update",
|
||||
"--token", "doxTok123",
|
||||
"--type", "docx",
|
||||
"--external-access-entity", "closed",
|
||||
"--link-share-entity", "anyone_readable",
|
||||
"--as", "user",
|
||||
}, f, stdout)
|
||||
assertDrivePublicPermissionValidationError(t, err, "--link-share-entity", "only tenant_* or closed are allowed")
|
||||
}
|
||||
|
||||
func TestDrivePublicPermissionUpdate_HighRiskRequiresYes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if DrivePublicPermissionUpdate.Risk != "high-risk-write" {
|
||||
t.Fatalf("Risk = %q, want high-risk-write", DrivePublicPermissionUpdate.Risk)
|
||||
}
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, driveTestConfig())
|
||||
err := mountAndRunDrive(t, DrivePublicPermissionUpdate, []string{
|
||||
"+public-permission-update",
|
||||
"--token", "doxTok123",
|
||||
"--type", "docx",
|
||||
"--link-share-entity", "closed",
|
||||
"--as", "user",
|
||||
}, f, stdout)
|
||||
if err == nil || !strings.Contains(err.Error(), "requires confirmation") {
|
||||
t.Fatalf("expected confirmation error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDrivePublicPermissionUpdate_ExecuteSuccess(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
|
||||
stub := &httpmock.Stub{
|
||||
Method: "PATCH",
|
||||
URL: "/open-apis/drive/v2/permissions/doxTok123/public?type=docx",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0, "msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"permission_public": map[string]interface{}{
|
||||
"link_share_entity": "closed",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
reg.Register(stub)
|
||||
|
||||
err := mountAndRunDrive(t, DrivePublicPermissionUpdate, []string{
|
||||
"+public-permission-update",
|
||||
"--token", "doxTok123",
|
||||
"--type", "docx",
|
||||
"--link-share-entity", "closed",
|
||||
"--copy-entity", "only_full_access",
|
||||
"--yes",
|
||||
"--as", "user",
|
||||
}, f, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var body map[string]interface{}
|
||||
if err := json.Unmarshal(stub.CapturedBody, &body); err != nil {
|
||||
t.Fatalf("parse body: %v", err)
|
||||
}
|
||||
if body["link_share_entity"] != "closed" || body["copy_entity"] != "only_full_access" {
|
||||
t.Fatalf("unexpected request body: %#v", body)
|
||||
}
|
||||
if _, ok := body["permission_public"]; ok {
|
||||
t.Fatalf("body must be flat, got nested permission_public: %#v", body)
|
||||
}
|
||||
}
|
||||
|
||||
func assertDrivePublicPermissionValidationError(t *testing.T, err error, wantParam, wantMessage string) {
|
||||
t.Helper()
|
||||
if err == nil {
|
||||
t.Fatalf("expected validation error, got nil")
|
||||
}
|
||||
p, ok := errs.ProblemOf(err)
|
||||
if !ok {
|
||||
t.Fatalf("expected typed error, got %T: %v", err, err)
|
||||
}
|
||||
if p.Category != errs.CategoryValidation {
|
||||
t.Fatalf("category = %q, want %q; err=%v", p.Category, errs.CategoryValidation, err)
|
||||
}
|
||||
if p.Subtype != errs.SubtypeInvalidArgument {
|
||||
t.Fatalf("subtype = %q, want %q; err=%v", p.Subtype, errs.SubtypeInvalidArgument, err)
|
||||
}
|
||||
if wantMessage != "" && !strings.Contains(err.Error(), wantMessage) {
|
||||
t.Fatalf("error %q does not contain %q", err.Error(), wantMessage)
|
||||
}
|
||||
var validationErr *errs.ValidationError
|
||||
if !errors.As(err, &validationErr) {
|
||||
t.Fatalf("expected *errs.ValidationError, got %T: %v", err, err)
|
||||
}
|
||||
if wantParam != "" && validationErr.Param != wantParam {
|
||||
t.Fatalf("param = %q, want %q; err=%v", validationErr.Param, wantParam, err)
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ func Shortcuts() []common.Shortcut {
|
||||
DriveTaskResult,
|
||||
DriveApplyPermission,
|
||||
DriveMemberAdd,
|
||||
DrivePublicPermissionUpdate,
|
||||
DriveSecureLabelList,
|
||||
DriveSecureLabelUpdate,
|
||||
DriveSearch,
|
||||
|
||||
@@ -34,6 +34,7 @@ func TestShortcutsIncludesExpectedCommands(t *testing.T) {
|
||||
"+task_result",
|
||||
"+apply-permission",
|
||||
"+member-add",
|
||||
"+public-permission-update",
|
||||
"+secure-label-list",
|
||||
"+secure-label-update",
|
||||
"+search",
|
||||
|
||||
@@ -32,6 +32,7 @@ metadata:
|
||||
- 用户要在 Drive 里上传、创建、读取、局部 patch 或覆盖更新**原生 `.md` 文件**(不是导入成 docx),切到 [`lark-markdown`](../lark-markdown/SKILL.md)。
|
||||
- 用户要比较原生 `.md` 文件的**历史版本差异**,或比较远端 Markdown 与本地草稿,切到 [`lark-markdown`](../lark-markdown/SKILL.md) 的 `lark-cli markdown +diff`;需要版本号时先用 `drive +version-history`。
|
||||
- 用户要查看、下载、回滚或删除文件的**历史版本**,使用 `drive +version-history`、`drive +version-get`、`drive +version-revert`、`drive +version-delete`;这组命令同时支持 `--as user` 和 `--as bot`,自动化场景优先 `--as bot`。
|
||||
- 用户要修改文档公开权限设置(链接分享、对外分享、谁可评论/复制/管理协作者),优先使用 `lark-cli drive +public-permission-update`,并先阅读 [`references/lark-drive-public-permission-update.md`](references/lark-drive-public-permission-update.md)。这是 `high-risk-write`,执行前必须 `--dry-run`,真正执行需 `--yes`。
|
||||
- 用户要把本地 `.xlsx` / `.xls` / `.csv` 导入成电子表格,使用 `lark-cli drive +import --type sheet`。
|
||||
- 用户要在云空间(云盘/云存储)里新建文件夹,优先使用 `lark-cli drive +create-folder`。
|
||||
- 用户要查看某个文件有哪些可下载预览格式,或想下载 PDF / HTML / 文本 / 图片等预览产物,使用 `lark-cli drive +preview`。
|
||||
@@ -105,7 +106,7 @@ lark-cli drive +inspect --url 'https://xxx.feishu.cn/wiki/wikcnXXX'
|
||||
|
||||
### 权限能力入口
|
||||
|
||||
- 用户要管理 Drive 文档/文件协作者、公开权限、授权当前应用访问文档,或处理 `permission.public.patch` 的 `91009` / `91010` / `91011` / `91012` 错误时,先读 [`lark-drive-permission-guide.md`](references/lark-drive-permission-guide.md)。
|
||||
- 用户要管理 Drive 文档/文件协作者、授权当前应用访问文档,或做权限治理 / 排障时,先读 [`lark-drive-permission-guide.md`](references/lark-drive-permission-guide.md);如果是修改公开权限设置(链接分享、对外分享、谁可评论 / 复制 / 管理协作者),仍按快速决策走 [`lark-drive-public-permission-update.md`](references/lark-drive-public-permission-update.md)。
|
||||
- 用户只是没有访问权限并希望向 owner 申请访问,优先使用 [`+apply-permission`](references/lark-drive-apply-permission.md)。
|
||||
- 普通 scope、身份或登录问题仍按 [`lark-shared`](../lark-shared/SKILL.md) 处理;不要把租户安全策略、对外分享、密级拦截简单归类为缺 scope。
|
||||
|
||||
@@ -148,6 +149,7 @@ Shortcut 是对常用操作的高级封装(`lark-cli drive +<verb> [flags]`)
|
||||
| [`+inspect`](references/lark-drive-inspect.md) | 检视 URL 的类型、标题和 canonical token;wiki URL 会自动解包到底层文档。 |
|
||||
| [`+apply-permission`](references/lark-drive-apply-permission.md) | 以 user 身份向文档 owner 申请访问权限。 |
|
||||
| [`+member-add`](references/lark-drive-member-add.md) | 添加一个或最多 10 个 Drive 文档、文件、文件夹或 wiki 节点协作者/授权成员;封装 Drive permission member create/batch_create,真实写入需要 `--yes`。 |
|
||||
| [`+public-permission-update`](references/lark-drive-public-permission-update.md) | 更新公开权限设置。高风险写操作:先 `--dry-run`,确认目标和字段后再传 `--yes`。 |
|
||||
| [`+secure-label-list`](references/lark-drive-secure-label.md) | 列出当前用户可用的密级标签。 |
|
||||
| [`+secure-label-update`](references/lark-drive-secure-label.md) | 更新 Drive 文件或文档的密级标签。 |
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
# drive +public-permission-update(更新公开权限设置)
|
||||
|
||||
本 skill 对应 shortcut:`lark-cli drive +public-permission-update`。
|
||||
|
||||
用于更新云文档/云文件的公开权限设置。
|
||||
|
||||
> [!CAUTION]
|
||||
> 这是 `high-risk-write` 操作,会改变文档公开访问或协作边界。必须先用 `--dry-run` 确认请求体;真正执行时需要 `--yes`。
|
||||
|
||||
## 何时使用
|
||||
|
||||
- 用户明确要求修改“链接分享”“对外分享”“谁可以评论/复制/下载/管理协作者”等公开权限设置。
|
||||
- 用户提供文档 URL 或 token,并且已经确认要修改目标文档的权限策略。
|
||||
|
||||
不要用它来“申请自己访问文档”;申请权限走 [`drive +apply-permission`](lark-drive-apply-permission.md)。
|
||||
|
||||
## 身份与权限
|
||||
|
||||
- 支持 `--as user` 和 `--as bot`。
|
||||
- 所需 scope:`docs:permission.setting:write_only`。
|
||||
- `--type` 会作为 query 参数传给接口。URL 输入可自动推断;bare token 必须显式传。
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
# 先预览:关闭对外分享,并关闭链接分享
|
||||
lark-cli drive +public-permission-update \
|
||||
--token "https://example.feishu.cn/docx/doxcnxxxxxxxxx" \
|
||||
--external-access-entity closed \
|
||||
--link-share-entity closed \
|
||||
--dry-run --as user
|
||||
|
||||
# 真正执行:确认 target 和 body 后加 --yes
|
||||
lark-cli drive +public-permission-update \
|
||||
--token "https://example.feishu.cn/docx/doxcnxxxxxxxxx" \
|
||||
--external-access-entity closed \
|
||||
--link-share-entity closed \
|
||||
--yes --as user
|
||||
|
||||
# 使用 bare token 时必须显式传 --type
|
||||
lark-cli drive +public-permission-update \
|
||||
--token "doxcnxxxxxxxxx" --type docx \
|
||||
--external-access-entity closed \
|
||||
--link-share-entity closed \
|
||||
--yes --as user
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--token` | 是 | 目标 token 或完整 URL。支持 `/docx/`、`/sheets/`、`/base/`、`/bitable/`、`/file/`、`/wiki/`、`/doc/`、`/mindnotes/`、`/minutes/`、`/slides/` 路径自动提取 token |
|
||||
| `--type` | URL 可省略;bare token 必填 | 目标类型:`doc`、`sheet`、`file`、`wiki`、`bitable`、`docx`、`mindnote`、`minutes`、`slides` |
|
||||
| `--security-entity` | 否 | 谁可以复制内容、创建副本、打印、下载:`anyone_can_view`、`anyone_can_edit`、`only_full_access` |
|
||||
| `--comment-entity` | 否 | 谁可以评论:`anyone_can_view`、`anyone_can_edit` |
|
||||
| `--share-entity` | 否 | 从组织维度,设置谁可以查看、添加、移除协作者:`anyone`、`same_tenant` |
|
||||
| `--manage-collaborator-entity` | 否 | 谁可以管理协作者:`collaborator_can_view`、`collaborator_can_edit`、`collaborator_full_access` |
|
||||
| `--link-share-entity` | 否 | 链接分享设置:`tenant_readable`、`tenant_editable`、`anyone_readable`、`anyone_editable`、`partner_tenant_readable`、`partner_tenant_editable`、`closed` |
|
||||
| `--copy-entity` | 否 | 谁可以创建副本:`anyone_can_view`、`anyone_can_edit`、`only_full_access` |
|
||||
| `--external-access-entity` | 否 | 对外分享设置:`open`、`closed`、`allow_share_partner_tenant` |
|
||||
| `--perm-type` | 否 | 权限范围:`container`、`single_page`。`single_page` 仅支持 `--link-share-entity` 和/或 `--external-access-entity`,不能混用其它权限字段 |
|
||||
| `--dry-run` | 否 | 只打印请求,不执行 |
|
||||
| `--yes` | 执行时必填 | 确认 high-risk-write 操作 |
|
||||
|
||||
至少要指定一个实际权限字段:`--security-entity`、`--comment-entity`、`--share-entity`、`--manage-collaborator-entity`、`--link-share-entity`、`--copy-entity`、`--external-access-entity`。单独传 `--perm-type` 会被拒绝。
|
||||
|
||||
## 请求体形状
|
||||
|
||||
```json
|
||||
{
|
||||
"external_access_entity": "closed",
|
||||
"link_share_entity": "closed"
|
||||
}
|
||||
```
|
||||
|
||||
## Wiki URL
|
||||
|
||||
传入 `/wiki/<node_token>` 时,shortcut 会以 `type=wiki` 直接调用公开权限接口。如果你要修改 wiki 背后的实际 docx/sheet/bitable 对象,先用 `drive +inspect` 或 `wiki spaces get_node` 拿到底层 `obj_token` 和 `obj_type`,再用 bare token + `--type <obj_type>` 调用。
|
||||
|
||||
## 常见错误
|
||||
|
||||
| 错误码 | 含义 | 引导 |
|
||||
|--------|------|------|
|
||||
| `1063001` | 参数异常 | 检查 token 和 `--type` 是否匹配、资源是否存在、字段枚举是否为 v2 文档支持值。`--external-access-entity` 与 `--link-share-entity` 同时传时可能出现参数冲突;`--perm-type single_page` 也有字段限制 |
|
||||
| `1063002` | 权限不足 | 确认当前 user 或 bot 是目标文档协作者,并具备编辑或管理权限;bot 场景需要先给文档添加应用权限 |
|
||||
| `1063003` | 操作不被允许 | 通常是企业策略、可见性、协作者上限或已有权限更高导致;不要简单提示补 scope |
|
||||
| `1063004` | 用户无分享权限 | 确认调用身份对目标文档有分享权限 |
|
||||
| `1063005` | 资源已删除 | 确认目标云文档仍存在 |
|
||||
|
||||
### 策略 / 密级拦截错误
|
||||
|
||||
调用 `lark-cli drive +public-permission-update` 返回以下错误码时,优先按租户策略、对外分享开关或文档密级处理,不要简单提示补 scope。
|
||||
|
||||
| 错误码 | 含义 | 引导 |
|
||||
|--------|------|------|
|
||||
| `91009` | 对外分享被租户安全策略管控,当前用户无法开启 | 提示用户:对外分享能力被租户安全策略统一管控,无法通过当前命令或当前用户直接开启;需要联系租户管理员调整组织级对外分享策略。 |
|
||||
| `91010` | 文档对外分享未打开 | 按用户目标分流,不要扩大变更面:只关闭对外分享时仅传 `--external-access-entity closed`;只关闭链接分享时仅传 `--link-share-entity closed`;只有用户明确要求彻底关闭公开访问时,才同时传 `--external-access-entity closed --link-share-entity closed`。只有用户明确要求开放外部访问时,才提示先在文档权限设置中打开对外分享并确认风险后重试。 |
|
||||
| `91011` | 对外分享被文档密级管控 | 提示用户:对外分享被密级策略拦截,需要打开目标文档,在文档内发起密级豁免或进行密级降级后再重试;回复中必须给出目标文档 URL。 |
|
||||
| `91012` | 权限设置被文档密级管控 | 提示用户:该权限设置被密级策略拦截,需要打开目标文档,在文档内发起密级豁免或进行密级降级后再重试;回复中必须给出目标文档 URL。 |
|
||||
|
||||
遇到 `91011` 或 `91012` 时,如果用户最初提供的是文档 URL,直接把该 URL 原样返回给用户作为操作入口;如果上下文只有 token,先尽量通过已有上下文、搜索结果或元数据恢复目标文档 URL,再给出可点击的文档 URL。
|
||||
|
||||
### 服务端运行时错误引导
|
||||
|
||||
以下场景 CLI 侧无法静态校验(依赖当前文档状态),需要根据服务端返回的错误信息引导用户:
|
||||
|
||||
**link_share 与当前 external 冲突:** 如果只传了 `--link-share-entity` 没传 `--external-access-entity`,服务端会用当前文档的 external 状态校验 link_share 是否合法。例如当前文档 external=closed,设置 link_share=anyone_readable 会被拒绝。按用户目标分流:如果目标是收紧或关闭公开访问,改为同时传 `--external-access-entity closed` 和 `--link-share-entity closed`;只有用户明确要求互联网或外部可访问时,才提示先确认风险,再同时传 `--external-access-entity open` 和对应的 `--link-share-entity`。不要把这类冲突默认解释为需要打开对外分享。
|
||||
|
||||
**单页面独立权限:** `--perm-type single_page` 只适用于有 container 的文档,会只修改当前页面的链接分享或对外分享设置;legacy doc 不支持该能力,会被服务端按参数错误拒绝。单页面权限可以与容器权限不同;服务端返回或查询结果中的 `lock_switch=true` 表示当前页面已限制权限、不再继承父级页面权限。
|
||||
|
||||
遇到企业策略、对外分享或密级拦截时,不要把它们简单归类成缺少 scope,应引导用户检查租户安全策略和文档权限设置。
|
||||
175
tests/cli_e2e/drive/drive_public_permission_dryrun_test.go
Normal file
175
tests/cli_e2e/drive/drive_public_permission_dryrun_test.go
Normal file
@@ -0,0 +1,175 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package drive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestDrive_PublicPermissionUpdateDryRun(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
t.Setenv("LARKSUITE_CLI_APP_ID", "app")
|
||||
t.Setenv("LARKSUITE_CLI_APP_SECRET", "secret")
|
||||
t.Setenv("LARKSUITE_CLI_BRAND", "feishu")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
wantURL string
|
||||
wantType string
|
||||
assert func(t *testing.T, out string)
|
||||
}{
|
||||
{
|
||||
name: "URL input infers type and sends v2 fields",
|
||||
args: []string{
|
||||
"drive", "+public-permission-update",
|
||||
"--token", "https://example.feishu.cn/docx/doxcnE2E001?from=share",
|
||||
"--external-access-entity", "open",
|
||||
"--link-share-entity", "anyone_readable",
|
||||
"--perm-type", "single_page",
|
||||
"--dry-run",
|
||||
},
|
||||
wantURL: "/open-apis/drive/v2/permissions/doxcnE2E001/public",
|
||||
wantType: "docx",
|
||||
assert: func(t *testing.T, out string) {
|
||||
if got := gjson.Get(out, "api.0.body.external_access_entity").String(); got != "open" {
|
||||
t.Fatalf("body.external_access_entity = %q, want open\nstdout:\n%s", got, out)
|
||||
}
|
||||
if got := gjson.Get(out, "api.0.body.link_share_entity").String(); got != "anyone_readable" {
|
||||
t.Fatalf("body.link_share_entity = %q, want anyone_readable\nstdout:\n%s", got, out)
|
||||
}
|
||||
if got := gjson.Get(out, "api.0.body.perm_type").String(); got != "single_page" {
|
||||
t.Fatalf("body.perm_type = %q, want single_page\nstdout:\n%s", got, out)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bare token requires explicit type and sends flat body",
|
||||
args: []string{
|
||||
"drive", "+public-permission-update",
|
||||
"--token", "shtcnE2E002",
|
||||
"--type", "sheet",
|
||||
"--security-entity", "only_full_access",
|
||||
"--comment-entity", "anyone_can_edit",
|
||||
"--manage-collaborator-entity", "collaborator_full_access",
|
||||
"--copy-entity", "anyone_can_view",
|
||||
"--dry-run",
|
||||
},
|
||||
wantURL: "/open-apis/drive/v2/permissions/shtcnE2E002/public",
|
||||
wantType: "sheet",
|
||||
assert: func(t *testing.T, out string) {
|
||||
if got := gjson.Get(out, "api.0.body.security_entity").String(); got != "only_full_access" {
|
||||
t.Fatalf("body.security_entity = %q, want only_full_access\nstdout:\n%s", got, out)
|
||||
}
|
||||
if got := gjson.Get(out, "api.0.body.comment_entity").String(); got != "anyone_can_edit" {
|
||||
t.Fatalf("body.comment_entity = %q, want anyone_can_edit\nstdout:\n%s", got, out)
|
||||
}
|
||||
if gjson.Get(out, "api.0.body.permission_public").Exists() {
|
||||
t.Fatalf("body must be flat, stdout:\n%s", out)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, temp := range tests {
|
||||
tt := temp
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: tt.args,
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
|
||||
out := result.Stdout
|
||||
if got := gjson.Get(out, "api.0.method").String(); got != "PATCH" {
|
||||
t.Fatalf("method = %q, want PATCH\nstdout:\n%s", got, out)
|
||||
}
|
||||
if got := gjson.Get(out, "api.0.url").String(); got != tt.wantURL {
|
||||
t.Fatalf("url = %q, want %q\nstdout:\n%s", got, tt.wantURL, out)
|
||||
}
|
||||
if got := gjson.Get(out, "api.0.params.type").String(); got != tt.wantType {
|
||||
t.Fatalf("params.type = %q, want %q\nstdout:\n%s", got, tt.wantType, out)
|
||||
}
|
||||
tt.assert(t, out)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDrive_PublicPermissionUpdateDryRunRejectsMissingTypeForBareToken(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
t.Setenv("LARKSUITE_CLI_APP_ID", "app")
|
||||
t.Setenv("LARKSUITE_CLI_APP_SECRET", "secret")
|
||||
t.Setenv("LARKSUITE_CLI_BRAND", "feishu")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"drive", "+public-permission-update",
|
||||
"--token", "doxcnE2E999",
|
||||
"--link-share-entity", "closed",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
requireDrivePublicPermissionValidationEnvelope(t, result, "--type", "--type is required")
|
||||
}
|
||||
|
||||
func TestDrive_PublicPermissionUpdateDryRunRejectsSinglePageMixedFields(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
t.Setenv("LARKSUITE_CLI_APP_ID", "app")
|
||||
t.Setenv("LARKSUITE_CLI_APP_SECRET", "secret")
|
||||
t.Setenv("LARKSUITE_CLI_BRAND", "feishu")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"drive", "+public-permission-update",
|
||||
"--token", "doxcnE2E999",
|
||||
"--type", "docx",
|
||||
"--link-share-entity", "closed",
|
||||
"--copy-entity", "only_full_access",
|
||||
"--perm-type", "single_page",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
requireDrivePublicPermissionValidationEnvelope(t, result, "--perm-type", "remove --copy-entity")
|
||||
}
|
||||
|
||||
func requireDrivePublicPermissionValidationEnvelope(t *testing.T, result *clie2e.Result, wantParam, wantMessage string) {
|
||||
t.Helper()
|
||||
if result.ExitCode == 0 {
|
||||
t.Fatalf("command must be rejected, stdout:\n%s", result.Stdout)
|
||||
}
|
||||
if got := gjson.Get(result.Stderr, "error.type").String(); got != "validation" {
|
||||
t.Fatalf("error.type = %q, want validation\nstdout:\n%s\nstderr:\n%s", got, result.Stdout, result.Stderr)
|
||||
}
|
||||
if got := gjson.Get(result.Stderr, "error.subtype").String(); got != "invalid_argument" {
|
||||
t.Fatalf("error.subtype = %q, want invalid_argument\nstdout:\n%s\nstderr:\n%s", got, result.Stdout, result.Stderr)
|
||||
}
|
||||
if got := gjson.Get(result.Stderr, "error.param").String(); got != wantParam {
|
||||
t.Fatalf("error.param = %q, want %q\nstdout:\n%s\nstderr:\n%s", got, wantParam, result.Stdout, result.Stderr)
|
||||
}
|
||||
message := gjson.Get(result.Stderr, "error.message").String()
|
||||
if !strings.Contains(message, wantMessage) {
|
||||
t.Fatalf("error.message %q does not contain %q\nstdout:\n%s\nstderr:\n%s", message, wantMessage, result.Stdout, result.Stderr)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user