Skip to content

fix(config): validate appId and appSecret keychain key consistency#295

Open
maochengwei1024-create wants to merge 2 commits intomainfrom
fix/validate-appid-secret-key-match
Open

fix(config): validate appId and appSecret keychain key consistency#295
maochengwei1024-create wants to merge 2 commits intomainfrom
fix/validate-appid-secret-key-match

Conversation

@maochengwei1024-create
Copy link
Copy Markdown
Collaborator

@maochengwei1024-create maochengwei1024-create commented Apr 7, 2026

Summary

  • Add ValidateSecretKeyMatch to detect when appId in config.json is out of sync with the appSecret keychain reference key (e.g. after hand-editing config)
  • Check runs in ResolveConfigFromMulti before any keychain lookup or OAPI request, failing fast with actionable error message
  • Add 10 new test cases covering match/mismatch/skip scenarios for both the validator and the integration in ResolveConfigFromMulti

Test plan

  • go test ./internal/core/ — all tests pass, no regressions
  • Manually edit config.json to mismatch appId vs appSecret.id, verify error message and hint
  • Verify normal config init + auth login flow is unaffected

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added pre-check that detects when an app ID and its secret reference are out of sync and returns a clear config error with guidance to resolve it.
  • Tests

    • Expanded test coverage for configuration validation: matching vs. mismatching secret references, plain-secret handling, skipped validation cases, and related error messages.

When config.json is hand-edited, the appId field can become out of sync
with the appSecret keychain reference (e.g. appId changed but
appSecret.id still points to the old app). This causes silent auth
failures at API call time. Add a pre-flight check in
ResolveConfigFromMulti that compares the two before any keychain lookup
or OAPI request, failing fast with actionable guidance.

Change-Id: I371b7da2fef402d9feec93ef9588136841674eff
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

📝 Walkthrough

Walkthrough

ResolveConfigFromMulti now pre-validates keychain secret refs by calling ValidateSecretKeyMatch(app.AppId, app.AppSecret). If validation fails it returns a ConfigError (Code: 2, Type: config) with a fixed message and the validation error text in Hint. Tests for matching and mismatching cases were added.

Changes

Cohort / File(s) Summary
Validation logic
internal/core/secret_resolve.go, internal/core/config.go
Added exported ValidateSecretKeyMatch(appId, secret) which checks keychain SecretRef IDs against the expected key derived from appId. ResolveConfigFromMulti now calls this and returns a ConfigError (Code:2, Type:config) with a fixed message and the validation error as Hint on mismatch.
Tests
internal/core/secret_resolve_test.go, internal/core/config_test.go
Added unit tests for ValidateSecretKeyMatch covering match, mismatch, and skip conditions (plain/file refs/zero-value). Added stubKeychain and three ResolveConfigFromMulti tests asserting mismatch error hints and success paths.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nudge the key, I sniff the keychain,
I check the app ID, hop once again,
If IDs align, the meadow's bright,
If not—I've flagged it, day or night. 🥕🔐

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main change: adding validation for appId and appSecret keychain key consistency, which is the core purpose of this PR.
Description check ✅ Passed The description covers all required template sections: Summary explains the change and motivation, Changes lists the main additions, and Test Plan includes both completed and pending verification steps.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/validate-appid-secret-key-match

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added the size/M Single-domain feat or fix with limited business impact label Apr 7, 2026
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 7, 2026

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@6403f24d1ff56344ac43b045e5b061aeff1f7103

🧩 Skill update

npx skills add larksuite/cli#fix/validate-appid-secret-key-match -y -g

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 7, 2026

Greptile Summary

This PR adds ValidateSecretKeyMatch to detect when appId in config.json has drifted from the keychain reference key in appSecret (e.g. after hand-editing the config). The validator is correctly integrated in ResolveConfigFromMulti before any keychain lookup, failing fast with a clear remediation message, and is covered by six focused unit tests plus three integration tests.

Confidence Score: 5/5

Safe to merge; the change is a pure validation guard with no mutations to existing data paths.

No P0 or P1 issues remain after accounting for the already-flagged Message/Hint reversal in the previous review thread. All new logic is well-tested and the happy path is unaffected.

No files require special attention.

Important Files Changed

Filename Overview
internal/core/secret_resolve.go Adds ValidateSecretKeyMatch; correctly skips plain/file/zero-value secrets, only validates keychain refs against secretAccountKey(appId)
internal/core/config.go Integrates validator before ResolveSecretInput; ConfigError Message/Hint role reversal was flagged in a prior review thread
internal/core/secret_resolve_test.go New file: 6 unit tests covering match, mismatch, plain, file-ref, zero-value, and empty-appId cases — thorough coverage
internal/core/config_test.go Adds stubKeychain and 3 integration tests verifying mismatch rejection, plain-secret pass-through, and matching-ref acceptance

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[ResolveConfigFromMulti] --> B{app == nil?}
    B -- yes --> C[Return profile-not-found ConfigError]
    B -- no --> D{ValidateSecretKeyMatch}
    D -- Ref is nil or\nnon-keychain source --> E[Skip — return nil]
    D -- Ref.ID != appsecret:appId --> F[Return out-of-sync ConfigError\nHint: run lark-cli config init]
    D -- Ref.ID == appsecret:appId --> G[ResolveSecretInput]
    E --> G
    G -- keychain error ExitError --> H[Return ExitError]
    G -- other error --> I[Return ConfigError]
    G -- success --> J[Build and return CliConfig]
Loading

Greploops — Automatically fix all review issues by running /greploops in Claude Code. It iterates: fix, push, re-review, repeat until 5/5 confidence.
Use the Greptile plugin for Claude Code to query reviews, search comments, and manage custom context directly from your terminal.

Reviews (2): Last reviewed commit: "fix: align ConfigError Message/Hint with..." | Re-trigger Greptile

Per review feedback: Message should be a concise problem label,
Hint should contain the detailed remediation. Swap the two fields
so the mismatch error follows the same pattern as other ConfigErrors.

Change-Id: Ifdbf8adc8be6952c18e4fb9dde760ccd728ef4de
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/core/config_test.go`:
- Around line 88-165: These tests leak host config state; add isolation by
calling t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) at the start of each
impacted test (TestResolveConfigFromMulti_RejectsSecretKeyMismatch,
TestResolveConfigFromMulti_AcceptsPlainSecret,
TestResolveConfigFromMulti_MatchingKeychainRefPassesValidation) so each test
uses a fresh temp config dir before invoking ResolveConfigFromMulti or
stubKeychain; place the call as the first statement in each test to ensure
config-dir isolation.
🪄 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: f6f3d8e8-9bc0-4e37-9c0b-f5ec54614375

📥 Commits

Reviewing files that changed from the base of the PR and between 0799875 and 6403f24.

📒 Files selected for processing (2)
  • internal/core/config.go
  • internal/core/config_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/core/config.go

Comment on lines +88 to +165
func TestResolveConfigFromMulti_RejectsSecretKeyMismatch(t *testing.T) {
raw := &MultiAppConfig{
Apps: []AppConfig{
{
AppId: "cli_new_app",
AppSecret: SecretInput{Ref: &SecretRef{
Source: "keychain",
ID: "appsecret:cli_old_app",
}},
Brand: BrandFeishu,
},
},
}

_, err := ResolveConfigFromMulti(raw, nil, "")
if err == nil {
t.Fatal("expected error for mismatched appId and appSecret keychain key")
}
var cfgErr *ConfigError
if !errors.As(err, &cfgErr) {
t.Fatalf("expected ConfigError, got %T: %v", err, err)
}
if cfgErr.Hint == "" {
t.Error("expected non-empty hint in ConfigError")
}
}

func TestResolveConfigFromMulti_AcceptsPlainSecret(t *testing.T) {
raw := &MultiAppConfig{
Apps: []AppConfig{
{
AppId: "cli_abc",
AppSecret: PlainSecret("my-secret"),
Brand: BrandFeishu,
},
},
}

cfg, err := ResolveConfigFromMulti(raw, nil, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.AppID != "cli_abc" {
t.Errorf("AppID = %q, want %q", cfg.AppID, "cli_abc")
}
}

func TestResolveConfigFromMulti_MatchingKeychainRefPassesValidation(t *testing.T) {
// Keychain ref matches appId, so validation passes.
// The subsequent ResolveSecretInput will fail (no real keychain),
// but that proves the mismatch check itself passed.
raw := &MultiAppConfig{
Apps: []AppConfig{
{
AppId: "cli_abc",
AppSecret: SecretInput{Ref: &SecretRef{
Source: "keychain",
ID: "appsecret:cli_abc",
}},
Brand: BrandFeishu,
},
},
}

_, err := ResolveConfigFromMulti(raw, stubKeychain{}, "")
if err == nil {
// stubKeychain returns ErrNotFound, so we expect a keychain error,
// but NOT a mismatch error — that's the point of this test.
t.Fatal("expected error (keychain entry not found), got nil")
}
// The error should come from keychain resolution, NOT from our mismatch check.
var cfgErr *ConfigError
if errors.As(err, &cfgErr) {
if cfgErr.Message == "appId and appSecret keychain key are out of sync" {
t.Fatal("error came from mismatch check, but keys should match")
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add config-dir isolation in each new test.

Please set LARKSUITE_CLI_CONFIG_DIR to a temp dir at the start of these tests to avoid accidental coupling with host/local config state.

🔧 Suggested patch
 func TestResolveConfigFromMulti_RejectsSecretKeyMismatch(t *testing.T) {
+	t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
 	raw := &MultiAppConfig{
 		Apps: []AppConfig{
 			{
@@
 func TestResolveConfigFromMulti_AcceptsPlainSecret(t *testing.T) {
+	t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
 	raw := &MultiAppConfig{
 		Apps: []AppConfig{
 			{
@@
 func TestResolveConfigFromMulti_MatchingKeychainRefPassesValidation(t *testing.T) {
+	t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
 	// Keychain ref matches appId, so validation passes.
 	// The subsequent ResolveSecretInput will fail (no real keychain),

As per coding guidelines **/*_test.go: "Isolate config state in Go tests by using t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestResolveConfigFromMulti_RejectsSecretKeyMismatch(t *testing.T) {
raw := &MultiAppConfig{
Apps: []AppConfig{
{
AppId: "cli_new_app",
AppSecret: SecretInput{Ref: &SecretRef{
Source: "keychain",
ID: "appsecret:cli_old_app",
}},
Brand: BrandFeishu,
},
},
}
_, err := ResolveConfigFromMulti(raw, nil, "")
if err == nil {
t.Fatal("expected error for mismatched appId and appSecret keychain key")
}
var cfgErr *ConfigError
if !errors.As(err, &cfgErr) {
t.Fatalf("expected ConfigError, got %T: %v", err, err)
}
if cfgErr.Hint == "" {
t.Error("expected non-empty hint in ConfigError")
}
}
func TestResolveConfigFromMulti_AcceptsPlainSecret(t *testing.T) {
raw := &MultiAppConfig{
Apps: []AppConfig{
{
AppId: "cli_abc",
AppSecret: PlainSecret("my-secret"),
Brand: BrandFeishu,
},
},
}
cfg, err := ResolveConfigFromMulti(raw, nil, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.AppID != "cli_abc" {
t.Errorf("AppID = %q, want %q", cfg.AppID, "cli_abc")
}
}
func TestResolveConfigFromMulti_MatchingKeychainRefPassesValidation(t *testing.T) {
// Keychain ref matches appId, so validation passes.
// The subsequent ResolveSecretInput will fail (no real keychain),
// but that proves the mismatch check itself passed.
raw := &MultiAppConfig{
Apps: []AppConfig{
{
AppId: "cli_abc",
AppSecret: SecretInput{Ref: &SecretRef{
Source: "keychain",
ID: "appsecret:cli_abc",
}},
Brand: BrandFeishu,
},
},
}
_, err := ResolveConfigFromMulti(raw, stubKeychain{}, "")
if err == nil {
// stubKeychain returns ErrNotFound, so we expect a keychain error,
// but NOT a mismatch error — that's the point of this test.
t.Fatal("expected error (keychain entry not found), got nil")
}
// The error should come from keychain resolution, NOT from our mismatch check.
var cfgErr *ConfigError
if errors.As(err, &cfgErr) {
if cfgErr.Message == "appId and appSecret keychain key are out of sync" {
t.Fatal("error came from mismatch check, but keys should match")
}
}
}
func TestResolveConfigFromMulti_RejectsSecretKeyMismatch(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
raw := &MultiAppConfig{
Apps: []AppConfig{
{
AppId: "cli_new_app",
AppSecret: SecretInput{Ref: &SecretRef{
Source: "keychain",
ID: "appsecret:cli_old_app",
}},
Brand: BrandFeishu,
},
},
}
_, err := ResolveConfigFromMulti(raw, nil, "")
if err == nil {
t.Fatal("expected error for mismatched appId and appSecret keychain key")
}
var cfgErr *ConfigError
if !errors.As(err, &cfgErr) {
t.Fatalf("expected ConfigError, got %T: %v", err, err)
}
if cfgErr.Hint == "" {
t.Error("expected non-empty hint in ConfigError")
}
}
func TestResolveConfigFromMulti_AcceptsPlainSecret(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
raw := &MultiAppConfig{
Apps: []AppConfig{
{
AppId: "cli_abc",
AppSecret: PlainSecret("my-secret"),
Brand: BrandFeishu,
},
},
}
cfg, err := ResolveConfigFromMulti(raw, nil, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.AppID != "cli_abc" {
t.Errorf("AppID = %q, want %q", cfg.AppID, "cli_abc")
}
}
func TestResolveConfigFromMulti_MatchingKeychainRefPassesValidation(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
// Keychain ref matches appId, so validation passes.
// The subsequent ResolveSecretInput will fail (no real keychain),
// but that proves the mismatch check itself passed.
raw := &MultiAppConfig{
Apps: []AppConfig{
{
AppId: "cli_abc",
AppSecret: SecretInput{Ref: &SecretRef{
Source: "keychain",
ID: "appsecret:cli_abc",
}},
Brand: BrandFeishu,
},
},
}
_, err := ResolveConfigFromMulti(raw, stubKeychain{}, "")
if err == nil {
// stubKeychain returns ErrNotFound, so we expect a keychain error,
// but NOT a mismatch error — that's the point of this test.
t.Fatal("expected error (keychain entry not found), got nil")
}
// The error should come from keychain resolution, NOT from our mismatch check.
var cfgErr *ConfigError
if errors.As(err, &cfgErr) {
if cfgErr.Message == "appId and appSecret keychain key are out of sync" {
t.Fatal("error came from mismatch check, but keys should match")
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/config_test.go` around lines 88 - 165, These tests leak host
config state; add isolation by calling t.Setenv("LARKSUITE_CLI_CONFIG_DIR",
t.TempDir()) at the start of each impacted test
(TestResolveConfigFromMulti_RejectsSecretKeyMismatch,
TestResolveConfigFromMulti_AcceptsPlainSecret,
TestResolveConfigFromMulti_MatchingKeychainRefPassesValidation) so each test
uses a fresh temp config dir before invoking ResolveConfigFromMulti or
stubKeychain; place the call as the first statement in each test to ensure
config-dir isolation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/M Single-domain feat or fix with limited business impact

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant