Skip to content

Commit daba3c9

Browse files
authored
feat(apps): gate apps domain off on Lark brand (#1025)
* feat(apps): gate apps domain off on Lark brand The Miaoda apps OpenAPI is Feishu-only. On Lark brand: - shortcut subtree is registered + hidden, RunE returns a structured brand-restriction error so users see a clear message instead of cobra's generic "unknown command" - auth login `--domain apps` is treated as unknown; `--domain all` skips apps; help text omits it - scope collection skips apps shortcuts so spark:* scopes are never requested The leaf-stub pattern mirrors internal/cmdpolicy/apply.go::installDenyStub (DisableFlagParsing + ArbitraryArgs + leaf-level PersistentPreRunE override) so cobra can't short-circuit the stub with a missing-flag or parent-PreRunE detour. Change-Id: I5817e87ae6fedabdb5faf05d0d32ea988f7effc9
1 parent e54220a commit daba3c9

7 files changed

Lines changed: 262 additions & 21 deletions

File tree

cmd/auth/login.go

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,13 @@ run --device-code in a later step after the user confirms authorization.`,
6868

6969
cmd.Flags().StringVar(&opts.Scope, "scope", "", "scopes to request (space- or comma-separated). Combines additively with --domain/--recommend")
7070
cmd.Flags().BoolVar(&opts.Recommend, "recommend", false, "request only recommended (auto-approve) scopes")
71-
available := sortedKnownDomains()
71+
var helpBrand core.LarkBrand
72+
if f != nil && f.Config != nil {
73+
if cfg, err := f.Config(); err == nil && cfg != nil {
74+
helpBrand = cfg.Brand
75+
}
76+
}
77+
available := sortedKnownDomains(helpBrand)
7278
cmd.Flags().StringSliceVar(&opts.Domains, "domain", nil,
7379
fmt.Sprintf("domain (repeatable or comma-separated, e.g. --domain calendar,task)\navailable: %s, all", strings.Join(available, ", ")))
7480
cmd.Flags().StringSliceVar(&opts.Exclude, "exclude", nil,
@@ -139,14 +145,14 @@ func authLoginRun(opts *LoginOptions) error {
139145
// Expand --domain all to all available domains (from_meta projects + shortcut services)
140146
for _, d := range selectedDomains {
141147
if strings.EqualFold(d, "all") {
142-
selectedDomains = sortedKnownDomains()
148+
selectedDomains = sortedKnownDomains(config.Brand)
143149
break
144150
}
145151
}
146152

147153
// Validate domain names and suggest corrections for unknown ones
148154
if len(selectedDomains) > 0 {
149-
knownDomains := allKnownDomains()
155+
knownDomains := allKnownDomains(config.Brand)
150156
for _, d := range selectedDomains {
151157
if !knownDomains[d] {
152158
if suggestion := suggestDomain(d, knownDomains); suggestion != "" {
@@ -170,7 +176,7 @@ func authLoginRun(opts *LoginOptions) error {
170176

171177
if !hasAnyOption {
172178
if !opts.JSON && f.IOStreams.IsTerminal {
173-
result, err := runInteractiveLogin(f.IOStreams, lang, msg)
179+
result, err := runInteractiveLogin(f.IOStreams, lang, msg, config.Brand)
174180
if err != nil {
175181
return err
176182
}
@@ -208,10 +214,10 @@ func authLoginRun(opts *LoginOptions) error {
208214
if len(selectedDomains) > 0 || opts.Recommend {
209215
var candidateScopes []string
210216
if len(selectedDomains) > 0 {
211-
candidateScopes = collectScopesForDomains(selectedDomains, "user")
217+
candidateScopes = collectScopesForDomains(selectedDomains, "user", config.Brand)
212218
} else {
213219
// --recommend without --domain: all domains
214-
candidateScopes = collectScopesForDomains(sortedKnownDomains(), "user")
220+
candidateScopes = collectScopesForDomains(sortedKnownDomains(config.Brand), "user", config.Brand)
215221
}
216222

217223
// Filter to auto-approve scopes if --recommend or interactive "common"
@@ -490,7 +496,7 @@ func findProfileByName(multi *core.MultiAppConfig, profileName string) *core.App
490496
// shortcut scopes for the given domain names.
491497
// Domains with auth_domain children are automatically expanded to include
492498
// their children's scopes.
493-
func collectScopesForDomains(domains []string, identity string) []string {
499+
func collectScopesForDomains(domains []string, identity string, brand core.LarkBrand) []string {
494500
scopeSet := make(map[string]bool)
495501

496502
// 1. API scopes from from_meta projects
@@ -509,6 +515,9 @@ func collectScopesForDomains(domains []string, identity string) []string {
509515

510516
// 3. Shortcut scopes matching by Service (only include shortcuts supporting the identity)
511517
for _, sc := range shortcuts.AllShortcuts() {
518+
if !shortcuts.IsShortcutServiceAvailable(sc.Service, brand) {
519+
continue
520+
}
512521
if domainSet[sc.Service] && shortcutSupportsIdentity(sc, identity) {
513522
for _, s := range sc.DeclaredScopesForIdentity(identity) {
514523
scopeSet[s] = true
@@ -528,14 +537,17 @@ func collectScopesForDomains(domains []string, identity string) []string {
528537
// allKnownDomains returns all valid auth domain names (from_meta projects +
529538
// shortcut services), excluding domains that have auth_domain set (they are
530539
// folded into their parent domain).
531-
func allKnownDomains() map[string]bool {
540+
func allKnownDomains(brand core.LarkBrand) map[string]bool {
532541
domains := make(map[string]bool)
533542
for _, p := range registry.ListFromMetaProjects() {
534543
if !registry.HasAuthDomain(p) {
535544
domains[p] = true
536545
}
537546
}
538547
for _, sc := range shortcuts.AllShortcuts() {
548+
if !shortcuts.IsShortcutServiceAvailable(sc.Service, brand) {
549+
continue
550+
}
539551
if !registry.HasAuthDomain(sc.Service) {
540552
domains[sc.Service] = true
541553
}
@@ -544,8 +556,8 @@ func allKnownDomains() map[string]bool {
544556
}
545557

546558
// sortedKnownDomains returns all valid domain names sorted alphabetically.
547-
func sortedKnownDomains() []string {
548-
m := allKnownDomains()
559+
func sortedKnownDomains(brand core.LarkBrand) []string {
560+
m := allKnownDomains(brand)
549561
domains := make([]string, 0, len(m))
550562
for d := range m {
551563
domains = append(domains, d)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
2+
// SPDX-License-Identifier: MIT
3+
4+
package auth
5+
6+
import (
7+
"testing"
8+
9+
"github.com/larksuite/cli/internal/core"
10+
)
11+
12+
func TestBrandFilter_AppsExcludedOnLark(t *testing.T) {
13+
feishuDomains := allKnownDomains(core.BrandFeishu)
14+
if !feishuDomains["apps"] {
15+
t.Errorf("expected apps domain to be known on Feishu brand")
16+
}
17+
18+
larkDomains := allKnownDomains(core.BrandLark)
19+
if larkDomains["apps"] {
20+
t.Errorf("expected apps domain to be EXCLUDED on Lark brand")
21+
}
22+
23+
feishuScopes := collectScopesForDomains([]string{"apps"}, "user", core.BrandFeishu)
24+
if len(feishuScopes) == 0 {
25+
t.Errorf("expected non-empty scopes for apps on Feishu brand, got %d", len(feishuScopes))
26+
}
27+
28+
larkScopes := collectScopesForDomains([]string{"apps"}, "user", core.BrandLark)
29+
if len(larkScopes) != 0 {
30+
t.Errorf("expected empty scopes for apps on Lark brand, got %d: %v", len(larkScopes), larkScopes)
31+
}
32+
}

cmd/auth/login_interactive.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/charmbracelet/huh"
1212

1313
"github.com/larksuite/cli/internal/cmdutil"
14+
"github.com/larksuite/cli/internal/core"
1415
"github.com/larksuite/cli/internal/output"
1516
"github.com/larksuite/cli/internal/registry"
1617
"github.com/larksuite/cli/shortcuts"
@@ -105,7 +106,7 @@ func buildDomainMeta(name, lang string) domainMeta {
105106
}
106107

107108
// runInteractiveLogin shows an interactive TUI form for domain and permission selection.
108-
func runInteractiveLogin(ios *cmdutil.IOStreams, lang string, msg *loginMsg) (*interactiveResult, error) {
109+
func runInteractiveLogin(ios *cmdutil.IOStreams, lang string, msg *loginMsg, brand core.LarkBrand) (*interactiveResult, error) {
109110
allDomains := getDomainMetadata(lang)
110111

111112
// Build multi-select options
@@ -165,7 +166,7 @@ func runInteractiveLogin(ios *cmdutil.IOStreams, lang string, msg *loginMsg) (*i
165166
}
166167

167168
// Compute scope summary
168-
scopes := collectScopesForDomains(selectedDomains, "user")
169+
scopes := collectScopesForDomains(selectedDomains, "user", brand)
169170
if permLevel == "common" {
170171
scopes = registry.FilterAutoApproveScopes(scopes)
171172
}

cmd/auth/login_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ func TestCompleteDomain_CommaSeparated(t *testing.T) {
171171
}
172172

173173
func TestAllKnownDomains(t *testing.T) {
174-
domains := allKnownDomains()
174+
domains := allKnownDomains("")
175175
if len(domains) == 0 {
176176
t.Fatal("expected non-empty known domains")
177177
}
@@ -185,7 +185,7 @@ func TestAllKnownDomains(t *testing.T) {
185185
}
186186

187187
func TestSortedKnownDomains(t *testing.T) {
188-
sorted := sortedKnownDomains()
188+
sorted := sortedKnownDomains("")
189189
if len(sorted) == 0 {
190190
t.Fatal("expected non-empty sorted domains")
191191
}
@@ -195,7 +195,7 @@ func TestSortedKnownDomains(t *testing.T) {
195195
}
196196

197197
// Should match allKnownDomains
198-
known := allKnownDomains()
198+
known := allKnownDomains("")
199199
if len(sorted) != len(known) {
200200
t.Errorf("sorted (%d) and known (%d) length mismatch", len(sorted), len(known))
201201
}
@@ -220,7 +220,7 @@ func TestCollectScopesForDomains(t *testing.T) {
220220
t.Skip("no from_meta data available")
221221
}
222222

223-
scopes := collectScopesForDomains([]string{"calendar"}, "user")
223+
scopes := collectScopesForDomains([]string{"calendar"}, "user", "")
224224
if len(scopes) == 0 {
225225
t.Fatal("expected non-empty scopes for calendar domain")
226226
}
@@ -247,7 +247,7 @@ func TestCollectScopesForDomains(t *testing.T) {
247247
}
248248

249249
func TestCollectScopesForDomains_NonexistentDomain(t *testing.T) {
250-
scopes := collectScopesForDomains([]string{"nonexistent_domain_xyz"}, "user")
250+
scopes := collectScopesForDomains([]string{"nonexistent_domain_xyz"}, "user", "")
251251
if len(scopes) != 0 {
252252
t.Errorf("expected empty scopes for nonexistent domain, got %d", len(scopes))
253253
}
@@ -1077,7 +1077,7 @@ func TestGetDomainMetadata_ExcludesEvent(t *testing.T) {
10771077
}
10781078

10791079
func TestAllKnownDomains_ExcludesAuthDomainChildren(t *testing.T) {
1080-
domains := allKnownDomains()
1080+
domains := allKnownDomains("")
10811081
if domains["whiteboard"] {
10821082
t.Error("whiteboard should not appear in known auth domains (it has auth_domain=docs)")
10831083
}
@@ -1087,7 +1087,7 @@ func TestAllKnownDomains_ExcludesAuthDomainChildren(t *testing.T) {
10871087
}
10881088

10891089
func TestCollectScopesForDomains_ExpandsAuthDomainChildren(t *testing.T) {
1090-
scopes := collectScopesForDomains([]string{"docs"}, "user")
1090+
scopes := collectScopesForDomains([]string{"docs"}, "user", "")
10911091
// docs domain should include whiteboard shortcut scopes (board:whiteboard:*)
10921092
found := false
10931093
for _, s := range scopes {

shortcuts/register.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@ package shortcuts
55

66
import (
77
"context"
8+
"fmt"
9+
"slices"
810

911
"github.com/larksuite/cli/shortcuts/okr"
1012
"github.com/spf13/cobra"
1113

1214
"github.com/larksuite/cli/internal/cmdmeta"
1315
"github.com/larksuite/cli/internal/cmdutil"
16+
"github.com/larksuite/cli/internal/core"
17+
"github.com/larksuite/cli/internal/output"
1418
"github.com/larksuite/cli/internal/registry"
1519
"github.com/larksuite/cli/shortcuts/apps"
1620
"github.com/larksuite/cli/shortcuts/base"
@@ -32,6 +36,23 @@ import (
3236
"github.com/larksuite/cli/shortcuts/wiki"
3337
)
3438

39+
// Empty brand (no config loaded) is treated as no-restriction so bootstrap
40+
// paths and tests without config still see the full service list.
41+
var brandRestrictedServices = map[string][]core.LarkBrand{
42+
"apps": {core.BrandFeishu},
43+
}
44+
45+
func IsShortcutServiceAvailable(service string, brand core.LarkBrand) bool {
46+
allowed, ok := brandRestrictedServices[service]
47+
if !ok {
48+
return true
49+
}
50+
if brand == "" {
51+
return true
52+
}
53+
return slices.Contains(allowed, brand)
54+
}
55+
3556
// allShortcuts aggregates shortcuts from all domain packages.
3657
var allShortcuts []common.Shortcut
3758

@@ -69,6 +90,14 @@ func RegisterShortcuts(program *cobra.Command, f *cmdutil.Factory) {
6990
}
7091

7192
func RegisterShortcutsWithContext(ctx context.Context, program *cobra.Command, f *cmdutil.Factory) {
93+
// Factory.Config may be nil in tests that pass a zero-value factory.
94+
var brand core.LarkBrand
95+
if f != nil && f.Config != nil {
96+
if cfg, err := f.Config(); err == nil && cfg != nil {
97+
brand = cfg.Brand
98+
}
99+
}
100+
72101
// Group by service
73102
byService := make(map[string][]common.Shortcut)
74103
for _, s := range allShortcuts {
@@ -117,5 +146,46 @@ func RegisterShortcutsWithContext(ctx context.Context, program *cobra.Command, f
117146
if service == "mail" {
118147
mail.InstallOnMail(svc)
119148
}
149+
150+
if !IsShortcutServiceAvailable(service, brand) {
151+
installBrandRestrictionGuard(svc, service, brand)
152+
}
153+
}
154+
}
155+
156+
// Mirrors internal/cmdpolicy/apply.go::installDenyStub: DisableFlagParsing +
157+
// ArbitraryArgs keep cobra from short-circuiting with "missing required flag"
158+
// before our RunE runs; leaf-level PersistentPreRunE defeats cobra's "first
159+
// PreRunE wins" walk-up that would otherwise shadow the stub.
160+
func installBrandRestrictionGuard(svc *cobra.Command, service string, brand core.LarkBrand) {
161+
stub := func(c *cobra.Command, _ []string) error {
162+
c.SilenceUsage = true
163+
return output.ErrValidation(
164+
"the %q feature is not yet supported on the %s brand",
165+
service, brand,
166+
)
120167
}
168+
noopPreRun := func(c *cobra.Command, _ []string) error {
169+
c.SilenceUsage = true
170+
return nil
171+
}
172+
var walk func(c *cobra.Command)
173+
walk = func(c *cobra.Command) {
174+
c.Hidden = true
175+
c.DisableFlagParsing = true
176+
c.Args = cobra.ArbitraryArgs
177+
c.PreRunE = nil
178+
c.PreRun = nil
179+
c.PersistentPreRunE = noopPreRun
180+
c.PersistentPreRun = nil
181+
c.RunE = stub
182+
c.Run = nil
183+
for _, child := range c.Commands() {
184+
walk(child)
185+
}
186+
}
187+
walk(svc)
188+
189+
// --help bypasses RunE, so surface the restriction in Long too.
190+
svc.Long = fmt.Sprintf("The %q feature is not yet supported on the %s brand.", service, brand)
121191
}

0 commit comments

Comments
 (0)