Skip to content

feat(sidecar): support multi-client identity isolation in server-demo#934

Open
hGrany wants to merge 6 commits into
larksuite:mainfrom
hGrany:feat/multi-client-identity-isolation
Open

feat(sidecar): support multi-client identity isolation in server-demo#934
hGrany wants to merge 6 commits into
larksuite:mainfrom
hGrany:feat/multi-client-identity-isolation

Conversation

@hGrany
Copy link
Copy Markdown

@hGrany hGrany commented May 18, 2026

Summary

When multiple CLI sandbox environments share a single sidecar instance, user tokens (UAT) are not isolated — the last user to log in overwrites previous users' tokens, causing identity cross-contamination (user A operates Feishu as user B).

This PR adds per-client HMAC key isolation to server-demo, enabling each client environment to maintain its own Feishu identity.

Changes

File Type Description
auth_bridge.go New Management-plane endpoints (login/poll/status) with explicit clientName → feishuOpenId binding and persistent mapping
handler.go Modified Multi-key HMAC verification: tries the shared key first, then iterates per-client keys to identify the request origin
main.go Modified --keys-dir flag for client key directory; reuses existing proxy.key on restart instead of regenerating (fixes a race condition with multiple instances)

Design Decisions

  1. Why use HMAC key for client identification instead of a header field?

    • The HMAC key is the existing trust anchor. Using it for identification introduces no new trust assumptions and prevents a malicious client from spoofing another client's identity.
  2. Why separate management-plane and data-plane keys?

    • Management plane (login flow) needs a unified entry point — all clients use the shared proxy.key.
    • Data plane (API proxy) needs per-client isolation to inject the correct user token.
  3. Why no fallback when a client has no user mapping?

    • This is authentication — silently falling back to another user's token is a security violation. Unmapped clients receive an explicit error prompting them to log in.
  4. Why reuse proxy.key instead of regenerating on each start?

    • When multiple sidecar instances start concurrently (e.g. one per Lark app), they all write to the same proxy.key. The last writer wins, leaving other instances with an in-memory key that doesn't match the file. Reusing the existing key eliminates this race.

Backward Compatibility

  • Wire protocol (sidecar package) is unchanged — no client-side (lark-cli-sidecar) changes needed.
  • Existing single-client deployments continue to work: the shared key is tried first, and if it matches, the request proceeds as before.
  • TAT (bot token) resolution is unaffected — it always uses the shared credential provider.

Risk Assessment

Risk Level Mitigation
Key iteration performance Low Typical deployment < 50 clients; HMAC verification < 1μs each
Hot-reload of new keys Low Automatic rescan on cache miss; no restart needed
Mapping file loss Low Users simply re-run the login flow to rebuild binding
No-fallback breaking change Intentional Unmapped clients must authenticate — this is by-design security behavior

Signed-off-by: Gao Yang grany@yeah.net (topwin.tech)

Summary by CodeRabbit

  • New Features

    • Multi-tenant auth sidecar server with per-client key isolation, HMAC-protected management bridge, token injection, and graceful CLI startup.
    • OAuth device-flow login with persisted client→user mapping and token management; secure proxy forwarding with audit logging.
  • Documentation

    • Comprehensive README for the multi-tenant demo and added “See also” linking to it.
  • Tests

    • Extensive test suite covering proxy signing/verification, allowlists, sanitization, redirects, and multi-tenant workflows.

Review Change Stack

When multiple CLI sandbox environments share a single sidecar instance,
user tokens (UAT) were not isolated -- the last user to log in would
overwrite previous users' tokens, causing identity cross-contamination.

This change introduces per-client HMAC key isolation:
- Each client gets a unique client-*.key file for data-plane HMAC signing,
  allowing the sidecar to identify request origin.
- A new auth_bridge.go handles management endpoints (login/poll/status)
  with explicit client-to-feishuOpenId binding.
- User token resolution is strictly bound to the matched client -- no
  fallback to other users' tokens when a client has no mapping.
- The shared proxy.key is reused across restarts instead of regenerated,
  fixing a race condition when multiple sidecar instances start together.

Wire protocol (sidecar package) is unchanged; existing single-client
deployments are fully backward compatible.

Signed-off-by: Gao Yang <grany@yeah.net> (topwin.tech)
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 18, 2026

CLA assistant check
All committers have signed the CLA.

@github-actions github-actions Bot added the size/L Large or sensitive change across domains or core paths label May 18, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8f7d518f-30da-4e66-bf6b-18483209f26b

📥 Commits

Reviewing files that changed from the base of the PR and between 3f907e3 and 1abdeb4.

📒 Files selected for processing (3)
  • sidecar/server-multi-tenant-demo/README.md
  • sidecar/server-multi-tenant-demo/handler_test.go
  • sidecar/server-multi-tenant-demo/main.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • sidecar/server-multi-tenant-demo/main.go
  • sidecar/server-multi-tenant-demo/handler_test.go

📝 Walkthrough

Walkthrough

This PR adds a multi-tenant auth sidecar demo: per-client HMAC key isolation, a management HMAC bridge with OAuth device-flow login/poll/status and persisted client→open_id mapping, strict allowlist validation, secure HTTPS forwarding with token injection, CLI/server wiring, and comprehensive tests and docs.

Changes

Multi-Tenant Auth Sidecar & Device-Flow OAuth

Layer / File(s) Summary
Documentation and architecture overview
sidecar/server-demo/README.md, sidecar/server-multi-tenant-demo/README.md
Cross-references multi-tenant demo from single-tenant README; adds complete documentation describing dual-key architecture (shared proxy.key + per-client {name}.key), CLI flags, key directory layout, client setup, management endpoints, and source file responsibilities.
Allowlist and audit helper functions
sidecar/server-multi-tenant-demo/allowlist.go, sidecar/server-multi-tenant-demo/audit.go
Implements buildAllowedHosts extracting hostnames from brand endpoints and buildAllowedIdentities handling bitmask identity flags; adds sanitizePath to normalize ID-like path segments, looksLikeID heuristic for segment classification, and sanitizeError truncation for safe logging.
Auth bridge type, persistence, and HMAC verification
sidecar/server-multi-tenant-demo/auth_bridge.go (lines 37–183)
Defines authBridge struct with HMAC key, credentials, and persisted client_user_map.json-backed client→openID mapping; implements verifyManagementHMAC validating timestamp/body-sha headers and verifying canonical HMAC signatures with constant-time comparison; routes authenticated management requests by URL path.
Device-flow login and polling endpoints
sidecar/server-multi-tenant-demo/auth_bridge.go (lines 184–327)
Implements /login endpoint initiating device authorization with scope resolution; implements /poll endpoint exchanging device code for tokens within a 10-minute timeout, fetching Feishu user info, storing token with computed expiry, and persisting the client→open_id mapping to disk under mutex lock.
Status endpoint and token/config helpers
sidecar/server-multi-tenant-demo/auth_bridge.go (lines 328–530)
Implements /status endpoint enumerating users and token status; adds resolveUserTokenByClient to fetch valid access tokens for mapped clients; adds addUserToConfig to upsert usernames into multi-app config; provides fetchUserInfoDirect to query Feishu user info and utility helpers for JSON responses and scope caching.
HTTP client setup for secure request forwarding
sidecar/server-multi-tenant-demo/forward.go
Constructs HTTP client disabling proxying, enforcing 30s timeout, limiting redirects to 10, and stripping Authorization plus sidecar headers on cross-host redirects; provides isProxyHeader to classify sidecar-specific auth headers.
Proxy handler types, client key loading, and per-client verification
sidecar/server-multi-tenant-demo/handler.go (lines 1–147)
Defines proxyHandler struct with shared/per-client HMAC state and synchronized client key cache; implements loadClientKeys scanning directory for client *.key files (skipping shared-key collisions and duplicates) and atomically replacing cache; implements verifyWithClientKeys iterating cached keys for verification with rescan-once fallback on miss.
Proxy request processing and header management
sidecar/server-multi-tenant-demo/handler.go (lines 148–372)
Implements ServeHTTP routing /_sidecar/ to authBridge, validating protocol/headers/target/identity/auth-header, verifying HMAC using shared then per-client keys with rescan fallback, resolving token via authBridge for user identity or credential provider for bot, forwarding request with header rewrites (stripping proxy-auth, injecting token), and logging an audit event with matched client; adds parseTarget to strictly validate HTTPS-only targets with no userinfo/path/query/fragment.
Server initialization, key handling, and signal-aware shutdown
sidecar/server-multi-tenant-demo/main.go
Defines CLI flags for listen/keyFile/keysDir/logFile/profile; validates environment, reuses or generates 32-byte HMAC key with 0600 permissions, defaults keysDir to key file parent; creates audit logger; loads lark-cli config; constructs listener and allowlists; wires authBridge and keysDir into proxyHandler; calls loadClientKeys; configures http.Server with timeouts; performs graceful shutdown on signals; prints startup information including key location.
Test suite covering request handling, validation, and multi-tenant behavior
sidecar/server-multi-tenant-demo/handler_test.go
Provides test scaffold and request signing helpers; validates rejection of unsupported versions and missing required headers; tests HMAC verification, body hash enforcement, target/identity/auth-header allowlists, parseTarget URL parsing, non-HTTPS rejection, identity/auth-header replay resistance, and auth-header allowlisting; verifies header stripping on redirects; validates utility functions; tests multi-tenant key loading with collision/duplicate skipping; validates correct client selection and user mapping persistence.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Bridge as Auth Bridge
  Client->>Bridge: POST /_sidecar/auth/* with headers
  Bridge->>Bridge: verifyManagementHMAC (validate timestamp, body-sha, signature)
  alt Valid
    Bridge->>Bridge: Route by path: /login, /poll, /status
  else Invalid
    Bridge-->>Client: 401 Unauthorized
  end
Loading
sequenceDiagram
  participant Client
  participant AuthBridge
  participant Feishu
  Client->>AuthBridge: POST /login (scope, client_id)
  AuthBridge->>Feishu: RequestDeviceAuthorization
  Feishu-->>AuthBridge: device_code, user_code, verification_url
  AuthBridge-->>Client: JSON response
  Client->>AuthBridge: POST /poll (device_code, client_id?)
  AuthBridge->>Feishu: PollDeviceToken
  Feishu-->>AuthBridge: access_token, refresh_token
  AuthBridge->>Feishu: GET /open-apis/authen/v1/user_info (Bearer token)
  Feishu-->>AuthBridge: open_id, name
  AuthBridge->>AuthBridge: Save token, update config, persist client mapping
  AuthBridge-->>Client: success JSON with open_id
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

feature, size/L, domain/base

Suggested reviewers

  • liangshuo-1
  • liuxinyanglxy

Poem

🐰 I hopped along the sidecar trail,
Keys in paw and tiny tail,
Device codes blinked like stars above,
I mapped each client with a hop and love,
Now secure requests sing, safe and hale.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.66% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main feature addition: per-client HMAC key isolation for identity management in the server-demo sidecar.
Description check ✅ Passed The PR description includes all required sections: Summary, Changes, Design Decisions, Backward Compatibility, and Risk Assessment, with comprehensive and detailed information addressing the motivation and scope.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

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

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: 6

🤖 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 `@sidecar/server-demo/auth_bridge.go`:
- Around line 76-96: Replace direct os.* calls in authBridge.loadUserMap and
authBridge.saveUserMap with the vfs package equivalents (use vfs.ReadFile and
vfs.WriteFile) so filesystem access is mockable; in loadUserMap keep the same
JSON unmarshal logic but read via vfs.ReadFile and propagate or log the
read/unmarshal error instead of silently returning, and in saveUserMap marshal
ab.userMap and use vfs.WriteFile while checking and handling the returned error
(do not ignore it) and preserve file mode 0600 semantics; update imports to
include "github.com/larksuite/cli/internal/vfs" and ensure references to
ab.mapFile and ab.userMap remain unchanged.
- Around line 141-146: Replace the unbounded io.ReadAll(r.Body) call with a
size-limited reader (e.g. wrap r.Body with http.MaxBytesReader or
io.LimitReader) before reading so attackers can't exhaust memory; check for the
limit-exceeded error and return an appropriate jsonError (use
http.StatusRequestEntityTooLarge or http.StatusBadRequest) and still ensure
r.Body.Close() is called. Specifically modify the code around io.ReadAll, r.Body
and jsonError to enforce a defined max (e.g. a small constant like maxBodyBytes)
and handle/read errors from the limited reader accordingly.
- Around line 493-519: The loadCachedScopes function should use the vfs
filesystem helpers instead of os.* so tests can mock FS; replace os.ReadDir(dir)
with vfs.ReadDir(dir) and os.ReadFile(filepath.Join(dir, e.Name())) with
vfs.ReadFile(filepath.Join(dir, e.Name())), import the vfs package, and keep the
same error handling/return behavior and the existing struct/json unmarshal logic
in loadCachedScopes to preserve functionality.

In `@sidecar/server-demo/handler.go`:
- Around line 64-84: The scan currently overwrites duplicate keys and doesn't
guard against a client key matching the shared proxy key; update the loop that
builds newKeys (map[string]clientKeyEntry) to: reject and log duplicate keyHex
values instead of overwriting (if keyHex already exists in newKeys, skip and
log), and also skip+log any keyHex that equals the shared proxy key (e.g.,
h.sharedKey or the shared-key variable used later) so it cannot collide with the
shared-key path that sets matchedClient; ensure clientKeyEntry population and
use of h.keysDir remain unchanged.
- Around line 54-75: The loadClientKeys function is reading user-supplied paths
with os.ReadDir/os.ReadFile and lacks SafeInputPath validation; update
loadClientKeys to first validate h.keysDir via validate.SafeInputPath (or
similar helper) and then replace direct os.* calls with the vfs equivalents
(e.g., vfs.ReadDir / vfs.ReadFile) so the keysDir is validated and filesystem
access is virtualized for tests; keep existing behavior for filtering
client-*.key filenames and error logging via h.logger.

In `@sidecar/server-demo/main.go`:
- Around line 71-89: The code reads the user-supplied keyFile with os.ReadFile
before validating and using the virtual FS; change the logic so you call
validate.SafeInputPath(keyFile) up front and use vfs.ReadFile/vfs.WriteFile (not
os.ReadFile) everywhere you touch keyFile; update the existing-key branch to use
vfs.ReadFile, trim/validate the hex length (64) as before, and keep
vfs.MkdirAll/vfs.WriteFile for creation so all file ops go through the vfs and
the path is validated before access.
🪄 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: 85f86690-c316-4d05-b4fc-d36da15b3881

📥 Commits

Reviewing files that changed from the base of the PR and between 898e0ee and 7550fea.

📒 Files selected for processing (3)
  • sidecar/server-demo/auth_bridge.go
  • sidecar/server-demo/handler.go
  • sidecar/server-demo/main.go

Comment thread sidecar/server-multi-tenant-demo/auth_bridge.go
Comment thread sidecar/server-demo/auth_bridge.go Outdated
Comment thread sidecar/server-multi-tenant-demo/auth_bridge.go
Comment thread sidecar/server-demo/handler.go Outdated
Comment thread sidecar/server-demo/handler.go Outdated
Comment thread sidecar/server-demo/main.go Outdated
Comment on lines +71 to +89
// Reuse existing key if present; generate a new one only on first run.
keyDir := filepath.Dir(keyFile)
if err := vfs.MkdirAll(keyDir, 0700); err != nil {
return fmt.Errorf("failed to create key directory: %v", err)
}
if err := vfs.WriteFile(keyFile, []byte(keyHex), 0600); err != nil {
return fmt.Errorf("failed to write key file: %v", err)

var keyHex string
if existing, err := os.ReadFile(keyFile); err == nil && len(strings.TrimSpace(string(existing))) == 64 {
keyHex = strings.TrimSpace(string(existing))
} else {
keyBytes := make([]byte, 32)
if _, err := rand.Read(keyBytes); err != nil {
return fmt.Errorf("failed to generate HMAC key: %v", err)
}
keyHex = hex.EncodeToString(keyBytes)
if err := vfs.WriteFile(keyFile, []byte(keyHex), 0600); err != nil {
return fmt.Errorf("failed to write key file: %v", err)
}
}
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 | 🟠 Major | ⚡ Quick win

Validate and virtualize --key-file reads at the boundary.

keyFile is user-supplied, but Line 78 reads it via os.ReadFile before any validate.SafeInputPath check. That bypasses the repo’s path validation contract and the vfs abstraction right at the CLI boundary.

Based on learnings, validate.SafeInputPath is required for user-supplied local file paths; as per coding guidelines, **/*.go: use vfs.* instead of os.* for all filesystem access to enable test mocking.

🤖 Prompt for 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.

In `@sidecar/server-demo/main.go` around lines 71 - 89, The code reads the
user-supplied keyFile with os.ReadFile before validating and using the virtual
FS; change the logic so you call validate.SafeInputPath(keyFile) up front and
use vfs.ReadFile/vfs.WriteFile (not os.ReadFile) everywhere you touch keyFile;
update the existing-key branch to use vfs.ReadFile, trim/validate the hex length
(64) as before, and keep vfs.MkdirAll/vfs.WriteFile for creation so all file ops
go through the vfs and the path is validated before access.

hGrany added 2 commits May 18, 2026 10:24
- Replace os.ReadFile/WriteFile/ReadDir with vfs.* equivalents for test
  mockability, consistent with project coding guidelines.
- Limit auth bridge request body to 64KB to prevent memory exhaustion.
- Log errors in saveUserMap instead of silently discarding them.
- Reject client keys that collide with the shared proxy key.
- Reject duplicate client keys instead of silently overwriting.

Signed-off-by: Gao Yang <grany@yeah.net> (topwin.tech)
- parseClientID: only accept "client_id" field, remove legacy fallback
- loadClientKeys: scan all *.key (excluding proxy.key), no prefix required
- Remove legacy file migration logic in newAuthBridge
- Update flag description to reflect generic key scanning

Signed-off-by: Gao Yang <grany@yeah.net> (topwin.tech)
@codecov
Copy link
Copy Markdown

codecov Bot commented May 18, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 66.78%. Comparing base (a18504b) to head (1abdeb4).
⚠️ Report is 50 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #934      +/-   ##
==========================================
+ Coverage   65.90%   66.78%   +0.88%     
==========================================
  Files         518      564      +46     
  Lines       48834    52441    +3607     
==========================================
+ Hits        32185    35024    +2839     
- Misses      13882    14516     +634     
- Partials     2767     2901     +134     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 18, 2026

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@1abdeb4a0fdfd5b1a34599c8cb4db20fb7a9ced8

🧩 Skill update

npx skills add hGrany/cli#feat/multi-client-identity-isolation -y -g

@sang-neo03
Copy link
Copy Markdown
Collaborator

hi @hGrany
Two things after reading the diff end-to-end:

1. Tests for the multi-key path

The new HMAC multi-key logic is correct (handler.go L611-619 explicitly skips shared-key collisions and duplicate keyHex with KEYS_SCAN_SKIP logs), but 530 LOC landed without
any test coverage. Three small unit tests would lock down the isolation guarantee that's the whole point of this PR:

  • loadClientKeys: shared-key collision and duplicate keyHex are both skipped + the KEYS_SCAN_SKIP log fires
  • verifyWithClientKeys: only the matching client key resolves to its clientName (and a stranger key → empty + error)
  • loadUserMap / saveUserMap: round-trip persistence across restart

Without these the "isolation by HMAC" contract lives only in your head and the PR description.

2. Placement: extract to a dedicated example

Right now server-demo reads as "the minimum sidecar you can compile and run" — useful as a starting template. Adding 530 lines of OAuth device-flow bridge + persistent client → openId mapping pushes it toward "production-ready multi-tenant gateway", which is a legitimate target but a different one. Mixing the two collapses the value of having a minimal
reference.

Suggest extracting the new code into examples/multi-tenant-sidecar/ (or sidecar/server-multi-tenant-demo/) and leaving server-demo as the single-tenant starting template,
with cross-links in both READMEs. That way readers picking up the simple case still get a simple example, and operators who actually need multi-tenant isolation get a full
reference without having to mentally subtract features.

Address review feedback from sang-neo03:

1. Extract multi-client code into sidecar/server-multi-tenant-demo/,
   keeping server-demo as the minimal single-tenant reference.

2. Add unit tests for the isolation guarantee:
   - loadClientKeys: shared-key collision and duplicate keyHex are skipped
   - verifyWithClientKeys: correct client matched, unknown key rejected
   - loadUserMap/saveUserMap: round-trip persistence across restart

3. Cross-link READMEs between server-demo and server-multi-tenant-demo.

Signed-off-by: Gao Yang <grany@yeah.net> (topwin.tech)
@github-actions github-actions Bot added size/XL Architecture-level or global-impact change and removed size/L Large or sensitive change across domains or core paths labels May 19, 2026
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: 4

🧹 Nitpick comments (1)
sidecar/server-multi-tenant-demo/main.go (1)

72-89: 💤 Low value

Consider validating user-supplied paths with validate.SafeInputPath.

The --key-file and --log-file flags are user-supplied CLI arguments. Per coding guidelines, CLI arguments should be validated with validate.SafeInputPath before filesystem operations. However, since this is a demo server run by administrators on their own machines, the risk is limited.

♻️ Optional: Add path validation
 func run(ctx context.Context, listen, keyFile, keysDir, logFile, profile string) error {
+	// Validate user-supplied paths
+	if err := validate.SafeInputPath(keyFile); err != nil {
+		return fmt.Errorf("invalid --key-file: %w", err)
+	}
+	if logFile != "" {
+		if err := validate.SafeInputPath(logFile); err != nil {
+			return fmt.Errorf("invalid --log-file: %w", err)
+		}
+	}
+	if keysDir != "" {
+		if err := validate.SafeInputPath(keysDir); err != nil {
+			return fmt.Errorf("invalid --keys-dir: %w", err)
+		}
+	}
+
 	if v := os.Getenv(envvars.CliAuthProxy); v != "" {

Add the import:

import "github.com/larksuite/cli/internal/validate"

As per coding guidelines: "Validate paths before reading with validate.SafeInputPath because CLI arguments are untrusted."

Also applies to: 98-107

🤖 Prompt for 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.

In `@sidecar/server-multi-tenant-demo/main.go` around lines 72 - 89, Validate the
user-supplied path strings before any filesystem ops by calling
validate.SafeInputPath on the CLI inputs (e.g., keyFile and logFile) at the
start of the routine that handles key file setup; add the import for
"github.com/larksuite/cli/internal/validate", call
validate.SafeInputPath(keyFile) (and validate.SafeInputPath(logFile) where
used), check and return the error if validation fails, then continue with the
existing logic that uses keyDir, vfs.ReadFile, vfs.WriteFile, etc.; also apply
the same SafeInputPath validation to the other path usage referenced around
lines 98-107.
🤖 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 `@sidecar/server-multi-tenant-demo/handler_test.go`:
- Line 754: Check the return value of os.MkdirAll in the test fixture setup and
fail the test on error: replace the current unchecked call to
os.MkdirAll(filepath.Join(dir, "subdir.key"), 0755) with an error check (e.g.,
capture err := os.MkdirAll(...); if err != nil { t.Fatalf("failed to create
fixture dir %q: %v", filepath.Join(dir, "subdir.key"), err) } or use
require.NoError/alpha equivalent) so setup failures are reported immediately.
- Around line 414-432: In TestRun_RejectsSelfProxy, isolate CLI config state by
calling t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) at the start of the
test (before manipulating envvars.CliAuthProxy); keep the existing
os.LookupEnv/os.Setenv handling for envvars.CliAuthProxy and the run(...)
invocation unchanged, so the test uses a temporary CLI config directory and does
not leak or read global CLI state.
- Around line 381-409: The test depends on a real external host and an unbounded
http.Client; make it fully offline by replacing h.forwardCl with a deterministic
fake client before calling h.ServeHTTP: implement a short-timeout or a fake
RoundTripper that immediately returns a controlled response (e.g., 502) for any
request so the request passes the allowlist check but does not perform network
I/O. Update the proxyHandler instance in the test (the variable h) to set
h.forwardCl to that fake http.Client (or a client with Timeout set) so
signedReq/resign and h.ServeHTTP remain unchanged and the test is bounded and
offline.

In `@sidecar/server-multi-tenant-demo/README.md`:
- Around line 25-41: The Markdown fenced code blocks containing the ASCII
diagram and the keys file tree are missing language identifiers (triggering
MD040); update each triple-backtick fence around the diagram block (the ASCII
"Sidecar Server" diagram) and the keys list block (the keys/ tree and the
similar block at lines 76-82) to include a neutral language like text/plain or
text (e.g., change ``` to ```text) so markdownlint no longer flags them; ensure
you update all occurrences referenced in the README (the diagram block and both
keys blocks).

---

Nitpick comments:
In `@sidecar/server-multi-tenant-demo/main.go`:
- Around line 72-89: Validate the user-supplied path strings before any
filesystem ops by calling validate.SafeInputPath on the CLI inputs (e.g.,
keyFile and logFile) at the start of the routine that handles key file setup;
add the import for "github.com/larksuite/cli/internal/validate", call
validate.SafeInputPath(keyFile) (and validate.SafeInputPath(logFile) where
used), check and return the error if validation fails, then continue with the
existing logic that uses keyDir, vfs.ReadFile, vfs.WriteFile, etc.; also apply
the same SafeInputPath validation to the other path usage referenced around
lines 98-107.
🪄 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: 931060dc-04b6-4193-acb7-e16c585ba9b8

📥 Commits

Reviewing files that changed from the base of the PR and between 95f3674 and 3f907e3.

📒 Files selected for processing (9)
  • sidecar/server-demo/README.md
  • sidecar/server-multi-tenant-demo/README.md
  • sidecar/server-multi-tenant-demo/allowlist.go
  • sidecar/server-multi-tenant-demo/audit.go
  • sidecar/server-multi-tenant-demo/auth_bridge.go
  • sidecar/server-multi-tenant-demo/forward.go
  • sidecar/server-multi-tenant-demo/handler.go
  • sidecar/server-multi-tenant-demo/handler_test.go
  • sidecar/server-multi-tenant-demo/main.go
✅ Files skipped from review due to trivial changes (1)
  • sidecar/server-demo/README.md

Comment thread sidecar/server-multi-tenant-demo/handler_test.go
Comment thread sidecar/server-multi-tenant-demo/handler_test.go
Comment thread sidecar/server-multi-tenant-demo/handler_test.go Outdated
Comment thread sidecar/server-multi-tenant-demo/README.md Outdated
hGrany added 2 commits May 19, 2026 09:47
…t and client guide

- Explain the multi-app credential isolation problem (app_secret must
  not be exposed to client environments)
- Document typical deployment topology with multiple sidecar instances
- Add complete client setup guide: env vars, multi-app switching, login
  flow, and end-to-end workflow example
- Document design decisions and management endpoint details

Signed-off-by: Gao Yang <grany@yeah.net> (topwin.tech)
- Make TestProxyHandler_AcceptsAllowedAuthHeaders fully offline by using
  httptest.NewTLSServer instead of depending on open.feishu.cn
- Isolate TestRun_RejectsSelfProxy config state with t.Setenv and temp dirs
- Check os.MkdirAll error in test fixture setup
- Add language identifiers to fenced code blocks (MD040)
- Validate user-supplied CLI paths with validate.SafeInputPath

Signed-off-by: Gao Yang <grany@yeah.net> (topwin.tech)
@hGrany
Copy link
Copy Markdown
Author

hGrany commented May 19, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 19, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@sang-neo03
Copy link
Copy Markdown
Collaborator

Thanks @hGrany — extraction to server-multi-tenant-demo/ and the three core tests (SkipsSharedKeyCollision, SkipsDuplicateKeyHex, VerifyWithClientKeys_MatchesCorrectClient,
UserMap_RoundTripPersistence) all landed. Good follow-through, and the extra 20+ tests around HMAC / SSRF / redirect / auth-header allowlist make the contract much more visible. Happy
with this part.

Before I can sign off though, the CodeRabbit findings need attention — I'd treat the following two as merge blockers:

1. DoS surface on the management plane/login, /poll, /status all use io.ReadAll(r.Body) without a cap. Please wrap with http.MaxBytesReader (these endpoints only take
small JSON, a few KB is plenty).

2. Path traversal via --keys-dir / --key-file — both are user-supplied paths but bypass validate.SafeInputPath. In a sidecar this is the trust boundary; the repo's path-guard
contract should apply here too. Same goes for swapping the direct os.ReadFile / os.ReadDir / os.WriteFile calls to vfs.* — it's both the repo convention and what makes these
paths mockable.

The remaining items are smaller but worth folding into the same pass:

  • saveUserMap silently discarding write errors (data loss masking)
  • Allowlist acceptance test hitting a real external host with no timeout (CI flakiness)
  • TestRun_RejectsSelfProxy missing t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) for config isolation
  • Ignored os.MkdirAll error in test fixtures
  • README code fence languages (MD040)

Once those are pushed I'll do another pass. Overall direction (HMAC-key-as-identity, no-fallback for unmapped clients, proxy.key reuse to fix the startup race) reads sound — just want
the boundary hygiene to match.

@hGrany
Copy link
Copy Markdown
Author

hGrany commented May 21, 2026

Thanks @sang-neo03 — all seven items were already addressed in the last two commits (c6959d1 + 1abdeb4) that landed before your comment:

  • Management body reads are capped at 64 KB via io.LimitReader
  • All three CLI path flags go through validate.SafeInputPath; os.* swapped to vfs.* throughout
  • saveUserMap errors are logged
  • Allowlist test uses httptest.NewTLSServer (fully offline)
  • TestRun_RejectsSelfProxy uses t.Setenv + temp config dir
  • os.MkdirAll error checked in fixture
  • README fences tagged with text

Waiting for your next look.

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

Labels

size/XL Architecture-level or global-impact change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants