-
Notifications
You must be signed in to change notification settings - Fork 823
feat(drive): add secure label shortcuts #985
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
caojie0621
wants to merge
1
commit into
main
Choose a base branch
from
feat/srcret
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| // Copyright (c) 2026 Lark Technologies Pte. Ltd. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| package drive | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
|
|
||
| "github.com/larksuite/cli/internal/output" | ||
| "github.com/larksuite/cli/internal/validate" | ||
| "github.com/larksuite/cli/shortcuts/common" | ||
| ) | ||
|
|
||
| const ( | ||
| secureLabelReadScope = "drive:file.meta.sec_label.read_only" | ||
| secureLabelUpdateScope = "docs:secure_label:write_only" | ||
| ) | ||
|
|
||
| var secureLabelTypes = permApplyTypes | ||
|
|
||
| // DriveSecureLabelList lists secure labels available to the current user. | ||
| var DriveSecureLabelList = common.Shortcut{ | ||
| Service: "drive", | ||
| Command: "+secure-label-list", | ||
| Description: "List secure labels available to the current user", | ||
| Risk: "read", | ||
| Scopes: []string{secureLabelReadScope}, | ||
| AuthTypes: []string{"user"}, | ||
| HasFormat: true, | ||
| Flags: []common.Flag{ | ||
| {Name: "page-size", Type: "int", Default: "10", Desc: "page size, 1-10"}, | ||
| {Name: "page-token", Desc: "pagination token from previous response"}, | ||
| {Name: "lang", Desc: "label language", Enum: []string{"zh", "en", "ja"}}, | ||
| }, | ||
| Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { | ||
| pageSize := runtime.Int("page-size") | ||
| if pageSize < 1 || pageSize > 10 { | ||
| return output.ErrValidation("--page-size must be between 1 and 10") | ||
| } | ||
| return nil | ||
| }, | ||
| DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { | ||
| return common.NewDryRunAPI(). | ||
| Desc("List secure labels available to the current user"). | ||
| GET("/open-apis/drive/v2/my_secure_labels"). | ||
| Params(buildSecureLabelListParams(runtime)) | ||
| }, | ||
| Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { | ||
| data, err := runtime.CallAPI("GET", | ||
| "/open-apis/drive/v2/my_secure_labels", | ||
| buildSecureLabelListParams(runtime), | ||
| nil, | ||
| ) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| runtime.OutFormat(data, nil, nil) | ||
| return nil | ||
| }, | ||
| } | ||
|
|
||
| // DriveSecureLabelUpdate updates the secure label on a Drive file/document. | ||
| var DriveSecureLabelUpdate = common.Shortcut{ | ||
| Service: "drive", | ||
| Command: "+secure-label-update", | ||
| Description: "Update the secure label on a Drive file or document", | ||
| Risk: "write", | ||
| Scopes: []string{secureLabelUpdateScope}, | ||
| AuthTypes: []string{"user"}, | ||
| Flags: []common.Flag{ | ||
| {Name: "token", Desc: "target file token or document URL (docx/sheets/base/file/wiki/doc/mindnote/slides)", Required: true}, | ||
| {Name: "type", Desc: "target type; auto-inferred from URL when omitted", Enum: secureLabelTypes}, | ||
| {Name: "label-id", Desc: "secure label ID to set", Required: true}, | ||
| }, | ||
| Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { | ||
| _, _, err := resolveSecureLabelTarget(runtime.Str("token"), runtime.Str("type")) | ||
| return err | ||
| }, | ||
| DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { | ||
| token, docType, err := resolveSecureLabelTarget(runtime.Str("token"), runtime.Str("type")) | ||
| if err != nil { | ||
| return common.NewDryRunAPI().Set("error", err.Error()) | ||
| } | ||
| return common.NewDryRunAPI(). | ||
| Desc("Update Drive secure label"). | ||
| PATCH("/open-apis/drive/v2/files/:file_token/secure_label"). | ||
| Params(map[string]interface{}{"type": docType}). | ||
| Body(map[string]interface{}{"id": runtime.Str("label-id")}). | ||
| Set("file_token", token) | ||
| }, | ||
| Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { | ||
| token, docType, err := resolveSecureLabelTarget(runtime.Str("token"), runtime.Str("type")) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| body := map[string]interface{}{"id": runtime.Str("label-id")} | ||
| data, err := runtime.CallAPI("PATCH", | ||
| fmt.Sprintf("/open-apis/drive/v2/files/%s/secure_label", validate.EncodePathSegment(token)), | ||
| map[string]interface{}{"type": docType}, | ||
| body, | ||
| ) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| runtime.Out(data, nil) | ||
| return nil | ||
| }, | ||
| } | ||
|
|
||
| func buildSecureLabelListParams(runtime *common.RuntimeContext) map[string]interface{} { | ||
| params := map[string]interface{}{"page_size": runtime.Int("page-size")} | ||
| if pageToken := runtime.Str("page-token"); pageToken != "" { | ||
| params["page_token"] = pageToken | ||
| } | ||
| if lang := runtime.Str("lang"); lang != "" { | ||
| params["lang"] = lang | ||
| } | ||
| return params | ||
| } | ||
|
|
||
| func resolveSecureLabelTarget(raw, explicitType string) (token, docType string, err error) { | ||
| return resolvePermApplyTarget(raw, explicitType) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| // Copyright (c) 2026 Lark Technologies Pte. Ltd. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| package drive | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/larksuite/cli/internal/cmdutil" | ||
| "github.com/larksuite/cli/internal/httpmock" | ||
| ) | ||
|
|
||
| func TestDriveSecureLabelList_DryRun(t *testing.T) { | ||
| t.Parallel() | ||
| f, stdout, _, _ := cmdutil.TestFactory(t, driveTestConfig()) | ||
| err := mountAndRunDrive(t, DriveSecureLabelList, []string{ | ||
| "+secure-label-list", | ||
| "--page-size", "5", | ||
| "--page-token", "page_1", | ||
| "--lang", "zh", | ||
| "--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/my_secure_labels", | ||
| `"GET"`, | ||
| `"page_size": 5`, | ||
| `"page_token": "page_1"`, | ||
| `"lang": "zh"`, | ||
| } { | ||
| if !strings.Contains(out, want) { | ||
| t.Fatalf("dry-run output missing %q:\n%s", want, out) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestDriveSecureLabelList_ValidatePageSize(t *testing.T) { | ||
| t.Parallel() | ||
| f, stdout, _, _ := cmdutil.TestFactory(t, driveTestConfig()) | ||
| err := mountAndRunDrive(t, DriveSecureLabelList, []string{ | ||
| "+secure-label-list", | ||
| "--page-size", "11", | ||
| "--as", "user", | ||
| }, f, stdout) | ||
| if err == nil || !strings.Contains(err.Error(), "page-size") { | ||
| t.Fatalf("expected page-size validation error, got: %v", err) | ||
| } | ||
| } | ||
|
|
||
| func TestDriveSecureLabelList_ExecuteSuccess(t *testing.T) { | ||
| f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig()) | ||
| reg.Register(&httpmock.Stub{ | ||
| Method: "GET", | ||
| URL: "/open-apis/drive/v2/my_secure_labels?page_size=10", | ||
| Body: map[string]interface{}{ | ||
| "code": 0, "msg": "success", | ||
| "data": map[string]interface{}{ | ||
| "items": []interface{}{ | ||
| map[string]interface{}{"id": "7217780879644737540", "name": "L1"}, | ||
| }, | ||
| }, | ||
| }, | ||
| }) | ||
|
|
||
| err := mountAndRunDrive(t, DriveSecureLabelList, []string{ | ||
| "+secure-label-list", | ||
| "--as", "user", | ||
| }, f, stdout) | ||
| if err != nil { | ||
| t.Fatalf("unexpected error: %v", err) | ||
| } | ||
| if !strings.Contains(stdout.String(), `"L1"`) { | ||
| t.Fatalf("stdout missing label:\n%s", stdout.String()) | ||
| } | ||
| } | ||
|
|
||
| func TestDriveSecureLabelUpdate_DryRunInfersTypeFromURL(t *testing.T) { | ||
| t.Parallel() | ||
| f, stdout, _, _ := cmdutil.TestFactory(t, driveTestConfig()) | ||
| err := mountAndRunDrive(t, DriveSecureLabelUpdate, []string{ | ||
| "+secure-label-update", | ||
| "--token", "https://example.feishu.cn/docx/doxTok123?from=share", | ||
| "--label-id", "7217780879644737539", | ||
| "--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/files/doxTok123/secure_label", | ||
| `"PATCH"`, | ||
| `"docx"`, | ||
| `"id": "7217780879644737539"`, | ||
| `"file_token": "doxTok123"`, | ||
| } { | ||
| if !strings.Contains(out, want) { | ||
| t.Fatalf("dry-run output missing %q:\n%s", want, out) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestDriveSecureLabelUpdate_ExecuteSuccess(t *testing.T) { | ||
| f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig()) | ||
| stub := &httpmock.Stub{ | ||
| Method: "PATCH", | ||
| URL: "/open-apis/drive/v2/files/doxTok123/secure_label?type=docx", | ||
| Body: map[string]interface{}{ | ||
| "code": 0, "msg": "success", | ||
| "data": map[string]interface{}{}, | ||
| }, | ||
| } | ||
| reg.Register(stub) | ||
|
|
||
| err := mountAndRunDrive(t, DriveSecureLabelUpdate, []string{ | ||
| "+secure-label-update", | ||
| "--token", "doxTok123", | ||
| "--type", "docx", | ||
| "--label-id", "7217780879644737539", | ||
| "--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["id"] != "7217780879644737539" { | ||
| t.Fatalf("id = %v, want label id", body["id"]) | ||
| } | ||
| } | ||
|
|
||
| func TestDriveSecureLabelUpdate_DowngradeApprovalReturnsAPIError(t *testing.T) { | ||
| f, _, _, reg := cmdutil.TestFactory(t, driveTestConfig()) | ||
| reg.Register(&httpmock.Stub{ | ||
| Method: "PATCH", | ||
| URL: "/open-apis/drive/v2/files/doxTok123/secure_label", | ||
| Status: 403, | ||
| Body: map[string]interface{}{ | ||
| "code": 1063013, "msg": "Security label downgrade requires approval", | ||
| }, | ||
| }) | ||
|
|
||
| targetURL := "https://example.feishu.cn/docx/doxTok123" | ||
| err := mountAndRunDrive(t, DriveSecureLabelUpdate, []string{ | ||
| "+secure-label-update", | ||
| "--token", targetURL, | ||
| "--label-id", "7217780879644737539", | ||
| "--as", "user", | ||
| }, f, nil) | ||
| if err == nil { | ||
| t.Fatal("expected 1063013 error") | ||
| } | ||
| if !strings.Contains(err.Error(), "Security label downgrade requires approval") { | ||
| t.Fatalf("expected raw API error message, got: %v", err) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| # drive +secure-label-list / +secure-label-update(云文档密级标签) | ||
|
|
||
| ## 何时使用 | ||
|
|
||
| - `drive +secure-label-list`:查询当前用户可用的密级标签,先拿到目标 `id`。 | ||
| - `drive +secure-label-update`:把目标云文档调整为指定密级标签。 | ||
|
|
||
| 这两个 shortcut 都使用用户身份(`--as user`)。修改密级前,通常先执行 `+secure-label-list` 确认可用标签 ID。 | ||
|
|
||
| ## 查询可用密级标签 | ||
|
|
||
| ```bash | ||
| lark-cli drive +secure-label-list --page-size 10 --lang zh | ||
| ``` | ||
|
|
||
| 可选参数: | ||
|
|
||
| | 参数 | 说明 | | ||
| |------|------| | ||
| | `--page-size` | 分页大小,范围 `1..10`,默认 `10` | | ||
| | `--page-token` | 上一页响应里的 `page_token` | | ||
| | `--lang` | 标签语言:`zh`、`en`、`ja` | | ||
|
|
||
| 底层接口:`GET /open-apis/drive/v2/my_secure_labels`。 | ||
|
|
||
| ## 修改文档密级 | ||
|
|
||
| ```bash | ||
| lark-cli drive +secure-label-update \ | ||
| --token "https://example.feishu.cn/docx/doxcnxxxx" \ | ||
| --label-id "7217780879644737539" | ||
| ``` | ||
|
|
||
| 参数: | ||
|
|
||
| | 参数 | 说明 | | ||
| |------|------| | ||
| | `--token` | 目标文档 URL 或 bare token;URL 可自动推断 `--type` | | ||
| | `--type` | bare token 必填;URL 输入时可省略。可选:`doc`、`docx`、`sheet`、`file`、`bitable`、`mindnote`、`slides` | | ||
| | `--label-id` | 要设置的密级标签 ID | | ||
|
|
||
| 底层接口:`PATCH /open-apis/drive/v2/files/:file_token/secure_label`,query 参数 `type`,请求体 `{ "id": "<label-id>" }`。 | ||
|
|
||
| ## 错误处理 | ||
|
|
||
| CLI 不会在 shortcut 中为密级错误码追加专用 hint;agent 必须根据返回的 `error.code` 做以下引导。 | ||
|
|
||
| | 错误码 | 含义 | 引导 | | ||
| |--------|------|------| | ||
| | `1063013` | 密级降级需要审批 | 提示用户打开目标文档,在文档界面完成密级降级审批后重试;如果用户传入的是文档 URL,必须把该 URL 一并给用户作为操作入口 | | ||
|
|
||
| 遇到 `1063013` 时,不要继续重试 API,也不要提示补 scope;这是文档侧审批流程要求,需要用户到文档里操作。 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.