From 9a89aa039c4d48dec621ebe93aa3a76e9e70216c Mon Sep 17 00:00:00 2001 From: cqc-a11y Date: Wed, 13 May 2026 19:37:59 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix(auth/login):=20=E5=A2=9E=E5=8A=A0exclud?= =?UTF-8?q?e=E5=8F=82=E6=95=B0=E4=BD=BF=E7=94=A8=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 当使用--exclude参数时,必须同时指定--scope、--domain或--recommend中的至少一个,避免非法参数调用 --- cmd/auth/login.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 916fda9ea..13882fca3 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -158,6 +158,11 @@ func authLoginRun(opts *LoginOptions) error { hasAnyOption := opts.Scope != "" || opts.Recommend || len(selectedDomains) > 0 + // Validate --exclude without --scope/--domain/--recommend + if len(opts.Exclude) > 0 && opts.Scope == "" && !opts.Recommend && len(selectedDomains) == 0 { + return output.ErrValidation("--exclude requires --scope, --domain, or --recommend to be specified") + } + if !hasAnyOption { if !opts.JSON && f.IOStreams.IsTerminal { result, err := runInteractiveLogin(f.IOStreams, lang, msg) From d558864a53633f40286e29b739b923241e69cb09 Mon Sep 17 00:00:00 2001 From: cqc-a11y Date: Wed, 13 May 2026 20:12:05 +0800 Subject: [PATCH 2/4] feat(auth/login): add --exclude flag and support combining scope options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增--exclude命令行标志用于排除指定的授权范围 2. 移除--scope与--domain/--recommend的互斥限制,改为叠加使用 3. 重构范围合并与排除逻辑,增加校验和辅助工具函数 4. 更新--scope参数的帮助文档说明叠加行为 --- cmd/auth/login.go | 104 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 90 insertions(+), 14 deletions(-) diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 13882fca3..887465da1 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -30,6 +30,7 @@ type LoginOptions struct { Scope string Recommend bool Domains []string + Exclude []string NoWait bool DeviceCode string } @@ -62,11 +63,13 @@ browser. Run it in the background and retrieve the verification URL from its out } cmdutil.SetSupportedIdentities(cmd, []string{"user"}) - cmd.Flags().StringVar(&opts.Scope, "scope", "", "scopes to request (space- or comma-separated)") + cmd.Flags().StringVar(&opts.Scope, "scope", "", "scopes to request (space- or comma-separated). Combines additively with --domain/--recommend") Combines additively with --domain/--recommend") cmd.Flags().BoolVar(&opts.Recommend, "recommend", false, "request only recommended (auto-approve) scopes") available := sortedKnownDomains() cmd.Flags().StringSliceVar(&opts.Domains, "domain", nil, fmt.Sprintf("domain (repeatable or comma-separated, e.g. --domain calendar,task)\navailable: %s, all", strings.Join(available, ", "))) + cmd.Flags().StringSliceVar(&opts.Exclude, "exclude", nil, + "scopes to exclude from the request (repeatable or comma-separated, e.g. --exclude drive:file:download)") cmd.Flags().BoolVar(&opts.JSON, "json", false, "structured JSON output") cmd.Flags().BoolVar(&opts.NoWait, "no-wait", false, "initiate device authorization and return immediately; use --device-code to complete") cmd.Flags().StringVar(&opts.DeviceCode, "device-code", "", "poll and complete authorization with a device code from a previous --no-wait call") @@ -156,12 +159,7 @@ func authLoginRun(opts *LoginOptions) error { } } - hasAnyOption := opts.Scope != "" || opts.Recommend || len(selectedDomains) > 0 - - // Validate --exclude without --scope/--domain/--recommend - if len(opts.Exclude) > 0 && opts.Scope == "" && !opts.Recommend && len(selectedDomains) == 0 { - return output.ErrValidation("--exclude requires --scope, --domain, or --recommend to be specified") - } + hasAnyOption := opts.Scope != "" || opts.Recommend || len(selectedDomains) > 0 || len(opts.Exclude) > 0 if !hasAnyOption { if !opts.JSON && f.IOStreams.IsTerminal { @@ -196,12 +194,11 @@ func authLoginRun(opts *LoginOptions) error { // endpoint rejects raw "a,b" strings as a single malformed scope. finalScope := normalizeScopeInput(opts.Scope) - // Resolve scopes from domain/permission filters + // Resolve scopes from domain/permission filters and merge with --scope. + // --scope, --domain, and --recommend combine additively so callers can, + // for example, request all `docs` scopes plus a few specific `drive` + // scopes in a single command. if len(selectedDomains) > 0 || opts.Recommend { - if opts.Scope != "" { - return output.ErrValidation("cannot use --scope together with --domain/--recommend") - } - var candidateScopes []string if len(selectedDomains) > 0 { candidateScopes = collectScopesForDomains(selectedDomains, "user") @@ -215,11 +212,35 @@ func authLoginRun(opts *LoginOptions) error { candidateScopes = registry.FilterAutoApproveScopes(candidateScopes) } - if len(candidateScopes) == 0 { + if len(candidateScopes) == 0 && opts.Scope == "" { return output.ErrValidation("no matching scopes found, check domain/scope options") } - finalScope = strings.Join(candidateScopes, " ") + // Merge --scope additively with the resolved domain scopes. + merged := make(map[string]bool, len(candidateScopes)+len(strings.Fields(opts.Scope))) + for _, s := range candidateScopes { + merged[s] = true + } + for _, s := range strings.Fields(opts.Scope) { + merged[s] = true + } + finalScope = joinSortedScopeSet(merged) + } + + // Apply --exclude on top of the resolved scope set. We honour exclude + // regardless of whether scopes came from --scope, --domain, --recommend, + // or any combination thereof. + if len(opts.Exclude) > 0 { + excluded, unknown := applyExcludeScopes(finalScope, opts.Exclude) + if len(unknown) > 0 { + return output.ErrValidation( + "these --exclude scopes are not present in the requested set: %s", + strings.Join(unknown, ", ")) + } + finalScope = excluded + if strings.TrimSpace(finalScope) == "" { + return output.ErrValidation("no scopes left after applying --exclude; nothing to authorize") + } } // Step 1: Request device authorization @@ -585,3 +606,58 @@ func suggestDomain(input string, known map[string]bool) string { } return "" } + +// joinSortedScopeSet returns a deterministic, space-separated scope string +// from a set, sorted alphabetically. Empty/blank scopes are dropped. +func joinSortedScopeSet(set map[string]bool) string { + out := make([]string, 0, len(set)) + for s := range set { + if strings.TrimSpace(s) == "" { + continue + } + out = append(out, s) + } + sort.Strings(out) + return strings.Join(out, " ") +} + +// applyExcludeScopes removes the provided exclude entries from the requested +// scope string. Each --exclude flag value may itself contain comma- or +// whitespace-separated scopes. Returns the filtered scope string and any +// exclude entries that were not present in the requested set (callers can +// surface those as a validation error to catch typos like +// `--exclude drive:file:downlod`). +func applyExcludeScopes(requested string, excludes []string) (string, []string) { + requestedSet := make(map[string]bool) + for _, s := range strings.Fields(requested) { + requestedSet[s] = true + } + + excludeSet := make(map[string]bool) + for _, raw := range excludes { + // --exclude already splits on commas (StringSliceVar), but also + // tolerate whitespace-separated entries inside a single value. + for _, s := range strings.Fields(strings.ReplaceAll(raw, ",", " ")) { + excludeSet[s] = true + } + } + + var unknown []string + for s := range excludeSet { + if !requestedSet[s] { + unknown = append(unknown, s) + } + } + if len(unknown) > 0 { + sort.Strings(unknown) + return requested, unknown + } + + kept := make(map[string]bool, len(requestedSet)) + for s := range requestedSet { + if !excludeSet[s] { + kept[s] = true + } + } + return joinSortedScopeSet(kept), nil +} From 792f3eb1ca2caeb2020b32afaf732f2d5ea0b4ac Mon Sep 17 00:00:00 2001 From: cqc-a11y Date: Wed, 13 May 2026 22:49:24 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix(auth/login):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E5=91=BD=E4=BB=A4scope=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E6=8F=8F=E8=BF=B0=E9=87=8D=E5=A4=8D=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除了重复的参数说明文本,整理冗余的注释内容,让帮助文档更清晰易读 --- cmd/auth/login.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 887465da1..6fa29b991 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -63,7 +63,7 @@ browser. Run it in the background and retrieve the verification URL from its out } cmdutil.SetSupportedIdentities(cmd, []string{"user"}) - cmd.Flags().StringVar(&opts.Scope, "scope", "", "scopes to request (space- or comma-separated). Combines additively with --domain/--recommend") Combines additively with --domain/--recommend") + cmd.Flags().StringVar(&opts.Scope, "scope", "", "scopes to request (space- or comma-separated). Combines additively with --domain/--recommend") cmd.Flags().BoolVar(&opts.Recommend, "recommend", false, "request only recommended (auto-approve) scopes") available := sortedKnownDomains() cmd.Flags().StringSliceVar(&opts.Domains, "domain", nil, @@ -217,11 +217,11 @@ func authLoginRun(opts *LoginOptions) error { } // Merge --scope additively with the resolved domain scopes. - merged := make(map[string]bool, len(candidateScopes)+len(strings.Fields(opts.Scope))) + merged := make(map[string]bool, len(candidateScopes)+len(strings.Fields(finalScope))) for _, s := range candidateScopes { merged[s] = true } - for _, s := range strings.Fields(opts.Scope) { + for _, s := range strings.Fields(finalScope) { merged[s] = true } finalScope = joinSortedScopeSet(merged) From 97ca98fd4e77826d0636c4c5c0f460db1eb11ee8 Mon Sep 17 00:00:00 2001 From: cqc-a11y Date: Thu, 14 May 2026 13:11:45 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix(auth/login):=20=E4=BF=AE=E5=A4=8Dexclud?= =?UTF-8?q?e=E5=8F=82=E6=95=B0=E6=A0=A1=E9=AA=8C=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加--exclude参数必须配合其他可选参数使用的校验,避免无效的exclude参数调用 --- cmd/auth/login.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 6fa29b991..97e2b8192 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -159,7 +159,11 @@ func authLoginRun(opts *LoginOptions) error { } } - hasAnyOption := opts.Scope != "" || opts.Recommend || len(selectedDomains) > 0 || len(opts.Exclude) > 0 + hasAnyOption := opts.Scope != "" || opts.Recommend || len(selectedDomains) > 0 + + if len(opts.Exclude) > 0 && !hasAnyOption { + return output.ErrValidation("--exclude requires --scope, --domain, or --recommend to be specified") + } if !hasAnyOption { if !opts.JSON && f.IOStreams.IsTerminal {