Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions cmd/auth/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 <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
Expand Down
40 changes: 40 additions & 0 deletions cmd/auth/login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading