From 3f848df12dd54e79de0bccde3384416e8c5276c0 Mon Sep 17 00:00:00 2001 From: calendar-assistant Date: Thu, 2 Apr 2026 23:54:49 +0800 Subject: [PATCH] fix(calendar): block auto bot fallback without user login Change-Id: If0e4c9fc99b465014de936a41d5e49fc6a414db4 --- shortcuts/calendar/calendar_agenda.go | 3 + shortcuts/calendar/calendar_create.go | 3 + shortcuts/calendar/calendar_freebusy.go | 3 + shortcuts/calendar/calendar_rsvp.go | 3 + shortcuts/calendar/calendar_suggestion.go | 3 + shortcuts/calendar/calendar_test.go | 115 ++++++++++++++++++ shortcuts/calendar/helpers.go | 24 ++++ skills/lark-calendar/SKILL.md | 5 +- .../references/lark-calendar-agenda.md | 2 +- .../references/lark-calendar-suggestion.md | 3 +- 10 files changed, 158 insertions(+), 6 deletions(-) diff --git a/shortcuts/calendar/calendar_agenda.go b/shortcuts/calendar/calendar_agenda.go index 70093c83..85839deb 100644 --- a/shortcuts/calendar/calendar_agenda.go +++ b/shortcuts/calendar/calendar_agenda.go @@ -178,6 +178,9 @@ var CalendarAgenda = common.Shortcut{ {Name: "end", Desc: "end time (ISO 8601, default: end of start day)"}, {Name: "calendar-id", Desc: "calendar ID (default: primary)"}, }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + return rejectCalendarAutoBotFallback(runtime) + }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { startInt, endInt, err := parseTimeRange(runtime) if err != nil { diff --git a/shortcuts/calendar/calendar_create.go b/shortcuts/calendar/calendar_create.go index 329e9d52..4b6621dc 100644 --- a/shortcuts/calendar/calendar_create.go +++ b/shortcuts/calendar/calendar_create.go @@ -81,6 +81,9 @@ var CalendarCreate = common.Shortcut{ {Name: "rrule", Desc: "recurrence rule (rfc5545)"}, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + if err := rejectCalendarAutoBotFallback(runtime); err != nil { + return err + } for _, flag := range []string{"summary", "description", "rrule", "calendar-id"} { if val := runtime.Str(flag); val != "" { if err := common.RejectDangerousChars("--"+flag, val); err != nil { diff --git a/shortcuts/calendar/calendar_freebusy.go b/shortcuts/calendar/calendar_freebusy.go index f3b9dc9a..d1463ec6 100644 --- a/shortcuts/calendar/calendar_freebusy.go +++ b/shortcuts/calendar/calendar_freebusy.go @@ -68,6 +68,9 @@ var CalendarFreebusy = common.Shortcut{ Body(map[string]interface{}{"time_min": timeMin, "time_max": timeMax, "user_id": userId, "need_rsvp_status": true}) }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + if err := rejectCalendarAutoBotFallback(runtime); err != nil { + return err + } userId := runtime.Str("user-id") if userId == "" && runtime.IsBot() { return common.FlagErrorf("--user-id is required for bot identity") diff --git a/shortcuts/calendar/calendar_rsvp.go b/shortcuts/calendar/calendar_rsvp.go index 2f126087..ef0dd35f 100644 --- a/shortcuts/calendar/calendar_rsvp.go +++ b/shortcuts/calendar/calendar_rsvp.go @@ -46,6 +46,9 @@ var CalendarRsvp = common.Shortcut{ Set("event_id", eventId) }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + if err := rejectCalendarAutoBotFallback(runtime); err != nil { + return err + } for _, flag := range []string{"calendar-id", "event-id", "rsvp-status"} { if val := strings.TrimSpace(runtime.Str(flag)); val != "" { if err := common.RejectDangerousChars("--"+flag, val); err != nil { diff --git a/shortcuts/calendar/calendar_suggestion.go b/shortcuts/calendar/calendar_suggestion.go index 8d151529..a9941c22 100644 --- a/shortcuts/calendar/calendar_suggestion.go +++ b/shortcuts/calendar/calendar_suggestion.go @@ -214,6 +214,9 @@ var CalendarSuggestion = common.Shortcut{ Body(req) }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + if err := rejectCalendarAutoBotFallback(runtime); err != nil { + return err + } durationMinutes := runtime.Int(flagDurationMinutes) if durationMinutes != 0 && (durationMinutes < 1 || durationMinutes > 1440) { return output.ErrValidation("--duration-minutes must be between 1 and 1440") diff --git a/shortcuts/calendar/calendar_test.go b/shortcuts/calendar/calendar_test.go index 6b249324..b018af3b 100644 --- a/shortcuts/calendar/calendar_test.go +++ b/shortcuts/calendar/calendar_test.go @@ -82,6 +82,19 @@ func defaultConfig() *core.CliConfig { } } +func noLoginConfig() *core.CliConfig { + return &core.CliConfig{ + AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu, + } +} + +func noLoginBotDefaultConfig() *core.CliConfig { + return &core.CliConfig{ + AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu, + DefaultAs: "bot", + } +} + // --------------------------------------------------------------------------- // CalendarCreate tests // --------------------------------------------------------------------------- @@ -337,6 +350,108 @@ func TestCreate_NoEventIdReturned(t *testing.T) { // CalendarAgenda tests // --------------------------------------------------------------------------- +func TestCalendarShortcuts_RequireLoginUnlessExplicitBot(t *testing.T) { + cases := []struct { + name string + shortcut common.Shortcut + args []string + }{ + { + name: "agenda", + shortcut: CalendarAgenda, + args: []string{"+agenda", "--start", "2025-03-21", "--end", "2025-03-21"}, + }, + { + name: "create", + shortcut: CalendarCreate, + args: []string{"+create", "--summary", "Test Meeting", "--start", "2025-03-21T00:00:00+08:00", "--end", "2025-03-21T01:00:00+08:00"}, + }, + { + name: "freebusy", + shortcut: CalendarFreebusy, + args: []string{"+freebusy", "--start", "2025-03-21", "--end", "2025-03-21"}, + }, + { + name: "rsvp", + shortcut: CalendarRsvp, + args: []string{"+rsvp", "--event-id", "evt_rsvp1", "--rsvp-status", "accept"}, + }, + { + name: "suggestion", + shortcut: CalendarSuggestion, + args: []string{"+suggestion", "--start", "2025-03-21", "--end", "2025-03-21"}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, noLoginConfig()) + + err := mountAndRun(t, tc.shortcut, tc.args, f, nil) + if err == nil { + t.Fatal("expected auth guard error") + } + if !strings.Contains(err.Error(), "auth login") { + t.Fatalf("expected auth login guidance, got: %v", err) + } + if !strings.Contains(err.Error(), "--as bot") { + t.Fatalf("expected explicit bot guidance, got: %v", err) + } + }) + } +} + +func TestAgenda_ExplicitBotBypassesLoginGuard(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, noLoginConfig()) + + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "/events/instance_view", + Body: map[string]interface{}{ + "code": 0, "msg": "ok", + "data": map[string]interface{}{ + "items": []interface{}{}, + }, + }, + }) + + err := mountAndRun(t, CalendarAgenda, []string{ + "+agenda", + "--start", "2025-03-21", + "--end", "2025-03-21", + "--as", "bot", + }, f, stdout) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestAgenda_DefaultAsBotBypassesLoginGuard(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, noLoginBotDefaultConfig()) + + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "/events/instance_view", + Body: map[string]interface{}{ + "code": 0, "msg": "ok", + "data": map[string]interface{}{ + "items": []interface{}{}, + }, + }, + }) + + err := mountAndRun(t, CalendarAgenda, []string{ + "+agenda", + "--start", "2025-03-21", + "--end", "2025-03-21", + }, f, stdout) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + func TestAgenda_Success(t *testing.T) { f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig()) diff --git a/shortcuts/calendar/helpers.go b/shortcuts/calendar/helpers.go index a905b4a1..a61c04b0 100644 --- a/shortcuts/calendar/helpers.go +++ b/shortcuts/calendar/helpers.go @@ -4,9 +4,12 @@ package calendar import ( + "strings" "time" + "github.com/larksuite/cli/internal/output" "github.com/larksuite/cli/shortcuts/common" + "github.com/spf13/cobra" ) const ( @@ -26,3 +29,24 @@ func resolveStartEnd(runtime *common.RuntimeContext) (string, string) { } return startInput, endInput } + +func hasExplicitBotFlag(cmd *cobra.Command) bool { + if cmd == nil { + return false + } + flag := cmd.Flag("as") + return flag != nil && flag.Changed && flag.Value != nil && strings.TrimSpace(flag.Value.String()) == "bot" +} + +func rejectCalendarAutoBotFallback(runtime *common.RuntimeContext) error { + if runtime == nil || !runtime.IsBot() || hasExplicitBotFlag(runtime.Cmd) { + return nil + } + if runtime.Factory == nil || !runtime.Factory.IdentityAutoDetected { + return nil + } + + msg := "calendar commands require a valid user login by default; when no valid user login state is available, auto identity falls back to bot and may operate on the bot calendar instead of your own. Run `lark-cli auth login --domain calendar` for your calendar, or rerun with `--as bot` if bot identity is intentional." + hint := "restore user login: `lark-cli auth login --domain calendar`\nintentional bot usage: rerun with `--as bot`" + return output.ErrWithHint(output.ExitAuth, "calendar_user_login_required", msg, hint) +} diff --git a/skills/lark-calendar/SKILL.md b/skills/lark-calendar/SKILL.md index 056fc084..f85c9a08 100644 --- a/skills/lark-calendar/SKILL.md +++ b/skills/lark-calendar/SKILL.md @@ -12,7 +12,6 @@ metadata: **CRITICAL — 开始前 MUST 先用 Read 工具读取 [`../lark-shared/SKILL.md`](../lark-shared/SKILL.md),其中包含认证、权限处理** **CRITICAL — 所有的 Shortcuts 在执行之前,务必先使用 Read 工具读取其对应的说明文档,禁止直接盲目调用命令。** - ## 核心场景 日历技能包含以下核心场景: @@ -54,7 +53,7 @@ metadata: ## 核心概念 - **日历(Calendar)**:日程的容器。每个用户有一个主日历(primary calendar),也可以创建或订阅共享日历。 -- **日程(Event)**:日历中的单个事件条目,包含起止时间、地点、标题、参与人等属性。支持单次日程和重复日程,遵循RFC5545 iCalendar国际标准。 +- **日程(Event)**:日历中的单个日程,包含起止时间、地点、标题、参与人等属性。支持单次日程和重复日程,遵循RFC5545 iCalendar国际标准。 - ***全天日程(All-day Event)***: 只按日期占用、没有具体起止时刻的日程,结束日期是包含在日程时间内的。 - **日程实例(Instance)**:日程的具体时间实例,本质是对日程的展开。普通日程和例外日程对应1个Instance,重复性日程对应N个Instance。在按时间段查询时,可通过实例视图将重复日程展开为独立的实例返回,以便在时间线上准确展示和管理。 - **重复规则(Rrule/Recurrence Rule)**:定义重复性日程的重复规则,比如`FREQ=DAILY;UNTIL=20230307T155959Z;INTERVAL=14`表示每14天重复一次。 @@ -151,4 +150,4 @@ lark-cli calendar [flags] # 调用 API | `freebusys.list` | `calendar:calendar.free_busy:read` | **注意(强制性):** -- 涉及日期(时间)字符串与时间戳的相互转换时,务必调用系统命令或脚本代码等外部工具进行处理,以确保转换的绝对准确。违者将导致严重的逻辑错误! \ No newline at end of file +- 涉及日期(时间)字符串与时间戳的相互转换时,务必调用系统命令或脚本代码等外部工具进行处理,以确保转换的绝对准确。违者将导致严重的逻辑错误! diff --git a/skills/lark-calendar/references/lark-calendar-agenda.md b/skills/lark-calendar/references/lark-calendar-agenda.md index 30ad9599..86405ba8 100644 --- a/skills/lark-calendar/references/lark-calendar-agenda.md +++ b/skills/lark-calendar/references/lark-calendar-agenda.md @@ -63,7 +63,7 @@ lark-cli calendar +agenda --calendar-id cal_xxx (无日程) ``` -**注意:按日期分组,并严格按照开始时间升序(从早到晚的时间线)排序输出。** 显示标题、时长、忙闲状态和rsvp状态。 +**注意:按日期分组,并严格按照开始时间升序(从早到晚的时间线)排序输出。** 显示标题、时长 ## 提示 diff --git a/skills/lark-calendar/references/lark-calendar-suggestion.md b/skills/lark-calendar/references/lark-calendar-suggestion.md index 16b27b9f..25ad0add 100644 --- a/skills/lark-calendar/references/lark-calendar-suggestion.md +++ b/skills/lark-calendar/references/lark-calendar-suggestion.md @@ -122,5 +122,4 @@ lark-cli calendar +suggestion \ - [lark-calendar-create](lark-calendar-create.md) — 创建日程 - [lark-calendar-freebusy](lark-calendar-freebusy.md) — 查询忙闲时段和rsvp状态 -- [lark-calendar](../SKILL.md) — 日历完整 API - +- [lark-calendar](../SKILL.md) — 日历完整 API \ No newline at end of file