feat: validate credentials after config init - #1151
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughCentralizes tenant-access-token exchange in ChangesPost-Config Credential Probing
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cmd/config/init_probe_test.go`:
- Around line 63-79: The test builds a Factory directly in fakeFactory; replace
that with the repo-standard TestFactory to ensure proper test isolation: in the
shared setup call t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) and create
the factory via cmdutil.TestFactory(t, config) instead of manually constructing
&cmdutil.Factory and HttpClient; update fakeFactory (and the similar block at
the other occurrence around lines 255-266) to return the factory and error
buffer from cmdutil.TestFactory so tests use the canonical TestFactory and
isolated config dir.
In `@internal/credential/tat_fetch.go`:
- Around line 23-31: The comment block describing error classification in
internal/credential/tat_fetch.go is out of sync: update the line that currently
states “other non-2xx → *errs.NetworkError” to match the runtime behavior where
non-2xx responses (excluding 401/403 and 5xx) are returned as
*errs.InternalError (subtype "sdk_error"); keep the existing rules for
*errs.AuthenticationError (401/403 or body code via Problem.Code) and
*errs.NetworkError (5xx or transport failures wrapped via Cause) so callers see
the same contract as the code path implemented around the non-2xx handling in
the block that returns *errs.InternalError (see the code near the current return
sites for *errs.InternalError in the 112-119 region).
- Around line 137-147: When parsed.Code == 0 you must also validate
parsed.TenantAccessToken is non-empty; if TenantAccessToken is empty, return an
internal SDK error instead of a successful empty token. Update the success
branch in tat_fetch.go (the block that currently returns
parsed.TenantAccessToken) to check parsed.TenantAccessToken (and/or
parsed.TenantAccessToken == "") and return an errs.InternalError (populate
errs.Problem with Category: errs.CategoryInternal, an appropriate Subtype such
as errs.SubtypeSDK, and a clear Message like "missing tenant_access_token on TAT
API success") so callers fail fast with a clear cause.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d60a3a6b-dc9b-4969-b50f-cab448016cba
📒 Files selected for processing (6)
cmd/config/init.gocmd/config/init_probe.gocmd/config/init_probe_test.gointernal/credential/default_provider.gointernal/credential/tat_fetch.gointernal/credential/tat_fetch_test.go
🚀 PR Preview Install Guide🧰 CLI updatenpm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@aae1c06337ec6dba184ec860cfb7946cb7696faf🧩 Skill updatenpx skills add dc-bytedance/cli#feat/config-probe -y -g |
e300c0e to
72dabf5
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1151 +/- ##
==========================================
+ Coverage 69.19% 69.22% +0.02%
==========================================
Files 634 636 +2
Lines 59482 59523 +41
==========================================
+ Hits 41161 41205 +44
+ Misses 15007 14999 -8
- Partials 3314 3319 +5 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
10924af to
136c07c
Compare
doResolveTAT minted the tenant access token inline. Extract the HTTP call into FetchTAT(ctx, httpClient, brand, appID, appSecret) so callers that already hold plaintext credentials — notably the post-config-init probe — can validate them without a second keychain round-trip. FetchTAT routes a non-zero TAT body code through the same classifyTATResponseCode the credential layer already uses, so a rejection is the canonical CategoryConfig / SubtypeInvalidClient (10003 / 10014) typed error — identical to what every token-resolving command returns. Transport, HTTP-status and JSON-parse failures stay raw (untyped) so callers can use errs.IsTyped to separate a deterministic credential rejection from upstream noise. doResolveTAT now delegates to FetchTAT; observable behavior unchanged.
After config init saves the App ID / App Secret, fire a best-effort probe: mint a tenant access token with the just-saved credentials, then POST the application probe endpoint. When the credentials are deterministically rejected, FetchTAT returns a typed errs.* error and runProbe propagates it, so config init exits non-zero with the canonical ConfigError / invalid_client envelope (the same one every other command shows for the same bad creds) instead of letting the user discover the mistake on a later request. Ambiguous failures (transport, HTTP non-200, JSON parse, timeout, http-client init) come back untyped and are swallowed (errs.IsTyped is the discriminator), so a valid configuration is never blocked by upstream noise. The probe is wired into all four init paths and skipped when the user reused an existing secret. The saved config is not rolled back on rejection: stdout still records what was saved, stderr carries the typed error envelope.
136c07c to
aae1c06
Compare
* refactor: extract FetchTAT sharing the TAT-rejection classifier doResolveTAT minted the tenant access token inline. Extract the HTTP call into FetchTAT(ctx, httpClient, brand, appID, appSecret) so callers that already hold plaintext credentials — notably the post-config-init probe — can validate them without a second keychain round-trip. FetchTAT routes a non-zero TAT body code through the same classifyTATResponseCode the credential layer already uses, so a rejection is the canonical CategoryConfig / SubtypeInvalidClient (10003 / 10014) typed error — identical to what every token-resolving command returns. Transport, HTTP-status and JSON-parse failures stay raw (untyped) so callers can use errs.IsTyped to separate a deterministic credential rejection from upstream noise. doResolveTAT now delegates to FetchTAT; observable behavior unchanged. * feat: validate credentials after config init After config init saves the App ID / App Secret, fire a best-effort probe: mint a tenant access token with the just-saved credentials, then POST the application probe endpoint. When the credentials are deterministically rejected, FetchTAT returns a typed errs.* error and runProbe propagates it, so config init exits non-zero with the canonical ConfigError / invalid_client envelope (the same one every other command shows for the same bad creds) instead of letting the user discover the mistake on a later request. Ambiguous failures (transport, HTTP non-200, JSON parse, timeout, http-client init) come back untyped and are swallowed (errs.IsTyped is the discriminator), so a valid configuration is never blocked by upstream noise. The probe is wired into all four init paths and skipped when the user reused an existing secret. The saved config is not rolled back on rejection: stdout still records what was saved, stderr carries the typed error envelope.
Summary
After
lark-cli config initsaves the App ID / App Secret, validate them against the open platform so the user gets immediate, hard-to-miss feedback when the credentials are wrong, instead of discovering it later from a failed business request. The check is best-effort: it stays silent on anything ambiguous (network errors, server 5xx, unknown transport failures, timeout) so a valid configuration is never blocked by upstream noise.Changes
internal/credential/tat_fetch.go::FetchTATfromdoResolveTAT— a pure-HTTP tenant-access-token mint that takes credentials directly (no config / no keychain read), so a caller holding plaintext credentials (the probe) can validate them without a second keychain round-trip. A non-zero TAT body code is routed through the credential layer's sharedclassifyTATResponseCode, so a rejection is the canonicalCategoryConfig / SubtypeInvalidClient(RFC 6749 invalid_client) typed error — code 10003 (bad/absent app_id) and 10014 (wrong app_secret) both land here. Transport / HTTP-non-200 / JSON-parse failures stay raw (untyped).doResolveTATnow delegates toFetchTAT(one HTTP path, one classifier; behavior unchanged).cmd/config/init_probe.go::runProbe— best-effort post-init validator under a single 3-secondcontext.WithTimeout. It mints a TAT with the just-saved credentials, then POSTs the application probe endpoint (outcome ignored). WhenFetchTATreturns a typed error (a deterministic credential rejection),runProbepropagates it soconfig initexits 3 with the canonicaltype: config/subtype: invalid_clientenvelope — byte-identical to what every other token-resolving command (im,calendar, …) shows for the same bad credentials.errs.IsTypedis the discriminator: untyped errors (transport / HTTP / parse / timeout / http-client init) are swallowed (return nil, exit 0).runProbeinto all fourcmd/config/init.gopaths (non-interactive,--new, interactive TUI, legacy readline) asif err := runProbe(...); err != nil { return err }. Skipped when the user reused an existing secret. The saved config is intentionally not rolled back on rejection — stdout still records the saved-config JSON, while stderr carries the typed error envelope and exit is 3.internal/credential/tat_fetch_test.go(code 10003/10014 → ConfigError/invalid_client; unknown code → typed APIError; transport/HTTP-non-200/parse → untyped; brand routing; context-cancel) andcmd/config/init_probe_test.go(rejection → propagated ConfigError; ambiguous → nil; probe request shape; brand routing; timeout; stderr-stays-empty invariant).Test Plan
make unit-testpassed (cmd/config+internal/credential, under-race)make build+go vet ./...+gofmtcleangolangci-lint --new-from-rev=upstream/mainoncmd/config+internal/credential→ 0 issues (errs-typed-only / forbidigo / errscontract all pass)tests_e2e/config/passed — verified viaLARK_CLI_BIN=$PWD/lark-cli go test ./tests_e2e/config/against the live endpoint, includingTestConfigInit_CredentialRejection_ExitsThree(real endpoint rejects a fake App ID → exit 3 +config/invalid_clientenvelope). The Docker eval sandbox failed at its ownauth statussetup gate (environmental, unrelated), so the E2E suite was run directly against the real binary.type:config / subtype:invalid_client / code:10003, stdout JSON intact; (2) real App ID + wrong secret →code:10014; (3) network-unreachable → silent, exit 0; (4)--brand larkroutes to open.larksuite.com (CONNECT-layer proof); (5) the probe rejection envelope is byte-identical toim +chat-listfor the same bad credentials — the point of the rebase alignment; (6) §2.6 vocabulary clean in the envelope and both source files.{appId, appSecret:"****", brand}, stderrOK: Configuration saved+{"type":"config","subtype":"invalid_client","code":10014,"message":"app secret invalid","hint":"run \lark-cli config init` to set valid app_id and app_secret"}`.Consumer note
When
config initexits non-zero, key off the exit code, read stdout as the config record and stderr as the diagnostic. On a credential rejection, stdout still carries the saved-config JSON (the config was written) while stderr carries the typed error envelope and exit is 3 — these are not contradictory (saved ≠ authenticates). Do not parse stderr as a single JSON document: it may contain a leadingOK: Configuration savedline before the error envelope. The rejection is classifiedtype: config/subtype: invalid_client, consistent with every other command — a consumer that previously matchedtype == authenticationfor this case must switch totype == config.Related Issues
N/A