From a74b59f55a9c0882351f36ef5df4b182b792ce2c Mon Sep 17 00:00:00 2001 From: voita Date: Mon, 13 Apr 2026 21:52:25 +0800 Subject: [PATCH] fix: validate auth login scopes locally --- cmd/auth/login.go | 58 ++++++++++++++++++++++++++++++++++++++++++ cmd/auth/login_test.go | 40 +++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 4b91dddf..0ddce1f2 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -187,6 +187,13 @@ func authLoginRun(opts *LoginOptions) error { } finalScope := opts.Scope + if finalScope != "" { + normalizedScope, err := validateExplicitScopes(finalScope, "user") + if err != nil { + return err + } + finalScope = normalizedScope + } // Resolve scopes from domain/permission filters if len(selectedDomains) > 0 || opts.Recommend { @@ -521,6 +528,57 @@ func shortcutSupportsIdentity(sc common.Shortcut, identity string) bool { return false } +func validateExplicitScopes(scope, identity string) (string, error) { + normalized := strings.Fields(scope) + if len(normalized) == 0 { + return "", output.ErrValidation("please specify at least one scope") + } + + knownScopes := knownScopesForIdentity(identity) + invalid := make([]string, 0) + result := make([]string, 0, len(normalized)) + seen := make(map[string]bool, len(normalized)) + for _, s := range normalized { + if !knownScopes[s] { + invalid = append(invalid, s) + continue + } + if seen[s] { + continue + } + seen[s] = true + result = append(result, s) + } + + if len(invalid) > 0 { + return "", output.ErrValidation( + "invalid scope(s): %s\ncheck the exact scope names with `lark-cli auth scopes --format pretty`, or use `lark-cli auth login --domain --recommend` to avoid manual scope typos", + strings.Join(invalid, ", "), + ) + } + + return strings.Join(result, " "), nil +} + +func knownScopesForIdentity(identity string) map[string]bool { + known := make(map[string]bool) + for scope := range registry.LoadScopePriorities() { + known[scope] = true + } + for _, scope := range registry.CollectAllScopesFromMeta(identity) { + known[scope] = true + } + for _, sc := range shortcuts.AllShortcuts() { + if shortcutSupportsIdentity(sc, identity) { + for _, scope := range sc.ScopesForIdentity(identity) { + known[scope] = true + } + } + } + known["offline_access"] = true + return known +} + // suggestDomain finds the best "did you mean" match for an unknown domain. func suggestDomain(input string, known map[string]bool) string { // Check common cases: prefix match or input is a substring diff --git a/cmd/auth/login_test.go b/cmd/auth/login_test.go index 8a20f9e0..b2ebd596 100644 --- a/cmd/auth/login_test.go +++ b/cmd/auth/login_test.go @@ -226,6 +226,46 @@ func TestCollectScopesForDomains_NonexistentDomain(t *testing.T) { } } +func TestValidateExplicitScopes_NormalizesWhitespaceAndDeduplicates(t *testing.T) { + got, err := validateExplicitScopes("base:app:create \n base:app:read\tbase:app:create", "user") + if err != nil { + t.Fatalf("validateExplicitScopes() error = %v", err) + } + if got != "base:app:create base:app:read" { + t.Fatalf("validateExplicitScopes() = %q, want %q", got, "base:app:create base:app:read") + } +} + +func TestValidateExplicitScopes_RejectsUnknownScopes(t *testing.T) { + _, err := validateExplicitScopes("base:app:create malformed:scope", "user") + if err == nil { + t.Fatal("expected validation error for unknown scope") + } + if !strings.Contains(err.Error(), "invalid scope(s): malformed:scope") { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(err.Error(), "auth scopes --format pretty") { + t.Fatalf("expected auth scopes hint, got: %v", err) + } +} + +func TestAuthLoginRun_ExplicitInvalidScopeFailsBeforeNetwork(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{ + AppID: "cli_test", AppSecret: "secret", Brand: core.BrandFeishu, + }) + err := authLoginRun(&LoginOptions{ + Factory: f, + Ctx: context.Background(), + Scope: "base:app:create malformed:scope", + }) + if err == nil { + t.Fatal("expected validation error for invalid scope") + } + if !strings.Contains(err.Error(), "invalid scope(s): malformed:scope") { + t.Fatalf("unexpected error: %v", err) + } +} + func TestGetDomainMetadata_IncludesFromMeta(t *testing.T) { domains := getDomainMetadata("zh") nameSet := make(map[string]bool)