Skip to content

fix: allow platform paths without tenant claim in JWT#1400

Merged
bjcoombs merged 2 commits intodevelopfrom
fix-platform-path-authz
Mar 4, 2026
Merged

fix: allow platform paths without tenant claim in JWT#1400
bjcoombs merged 2 commits intodevelopfrom
fix-platform-path-authz

Conversation

@bjcoombs
Copy link
Copy Markdown
Collaborator

@bjcoombs bjcoombs commented Mar 4, 2026

Summary

  • Export IsPlatformPath from shared/platform/gateway for reuse across middleware
  • Allow authenticated users to access platform paths (e.g., /v1/tenants, TenantService/*) without a tenant claim in their JWT
  • Extract authorizeWithoutTenantClaim helper to reduce cognitive complexity in TenantAuthorizationMiddleware.Handler

Context

Standard OIDC providers like Dex issue identity-only tokens without custom tenant claims. The frontend login flow calls ListTenants to discover available tenants and populate the tenant picker. This endpoint was blocked with 403 "missing tenant claim in token" because TenantAuthorizationMiddleware required a tenant claim even for platform-level bootstrap endpoints.

This is a chicken-and-egg problem: you need to list tenants to know which tenant to scope to, but listing tenants required tenant scope.

The fix adds a third fallback path in the empty-tenant-claim handler: after checking for platform-admin role and resolved tenant from subdomain, check if the request targets a platform path. Platform paths are bootstrap endpoints that handle their own access control.

Test plan

  • 14 TestTenantAuthorizationMiddleware tests pass (including 2 new platform path tests)
  • TestIsPlatformPath passes (exported function)
  • Full ./services/api-gateway/... test suite passes
  • golangci-lint passes (cognitive complexity resolved via method extraction)
  • Deploy to demo and verify Dex login → ListTenants → tenant picker works

Platform paths like /v1/tenants are bootstrap endpoints where the caller
discovers available tenants. Requiring a tenant claim to list tenants is
circular — the user needs to know which tenants exist before they can
scope requests to one.

Export IsPlatformPath from shared/platform/gateway and use it in
TenantAuthorizationMiddleware to bypass tenant authorization for
authenticated users hitting platform endpoints. The endpoint itself
handles access control based on the caller's identity.

Fixes the demo login flow where Dex OIDC tokens (which lack custom
tenant claims) could not call ListTenants to populate the tenant picker.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 4, 2026

📝 Walkthrough

Walkthrough

The changes refactor JWT tenant authorization handling in the API gateway by extracting logic for processing requests without tenant claims into a dedicated method. The platform path checking utility is simultaneously promoted from unexported to exported to support this new middleware logic.

Changes

Cohort / File(s) Summary
Middleware Refactoring
services/api-gateway/auth/combined_middleware.go, services/api-gateway/auth/combined_middleware_test.go
Extracts JWT-without-tenant-claim authorization logic into a new authorizeWithoutTenantClaim method on TenantAuthorizationMiddleware. The Handler now delegates this logic and proceeds based on its boolean result. New method enforces platform-admin/super-admin access, tenant resolution from subdomain, platform path allowances (via gateway.IsPlatformPath), and returns 403 otherwise. Two new tests validate platform path access for authenticated users without tenant claims.
Platform Path Utility Promotion
shared/platform/gateway/tenant_resolver.go, shared/platform/gateway/tenant_resolver_test.go
Promotes isPlatformPath from unexported to exported as IsPlatformPath. Updates docblock to reflect that platform paths bypass tenant resolution and authorization. Test is updated to call the now-public function.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.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 concisely and accurately summarizes the primary change: allowing platform paths to be accessed without a tenant claim in JWT.
Description check ✅ Passed The description is well-detailed, explaining the summary, context, and test plan; it clearly relates to the changeset and provides meaningful information.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-platform-path-authz

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

@claude
Copy link
Copy Markdown

claude Bot commented Mar 4, 2026

Claude Code Review

Commit: b949cec | CI: running (build, lint, tests still pending)

Summary

Clean, well-scoped fix for a real chicken-and-egg problem: OIDC tokens from Dex lack tenant claims, so users cannot call ListTenants to discover tenants, but the middleware requires a tenant claim. The fix adds platform path detection as a third fallback in the no-tenant-claim handler, after platform-admin and resolved-tenant checks.

The authorizeWithoutTenantClaim extraction is a behavior-preserving refactor with one new code path (platform path bypass). The export of IsPlatformPath is straightforward and all callers are updated.

Risk Assessment

Area Level Detail
Blast radius Low Only affects requests with empty JWT tenant claim hitting platform paths
Rollback Safe Single commit, no migrations, no data changes
Scale Low IsPlatformPath is O(n) over 2 prefixes — trivial
Cross-system Low Only api-gateway auth middleware; shared/platform/gateway export is additive
Migration N/A No migrations

Findings

Severity Location Description Status
No actionable findings

Analysis

Security model: The bypass is gated behind three conditions: (1) the user is authenticated (passed CombinedAuthMiddleware), (2) the JWT has no tenant claim, and (3) the path matches a platform prefix. The TenantService endpoints handle their own access control. This is consistent with how TenantResolverMiddleware.Handler already skips tenant resolution for these same paths — the authorization middleware is now aligned with the resolver middleware.

Refactoring correctness: The authorizeWithoutTenantClaim extraction preserves the original three paths (platform-admin, resolved-tenant, deny) and adds the platform-path check as fallback #3 before deny. The caller in Handler correctly gates next.ServeHTTP on the boolean return and always returns after the call, preventing double-writes.

Test coverage: Two new tests cover both REST (/v1/tenants) and Connect/gRPC (/meridian.tenant.v1.TenantService/ListTenants) platform paths. The existing "403 when no JWT tenant claim and no resolved tenant" test on /api/test confirms non-platform paths are still denied. The TestIsPlatformPath tests in tenant_resolver_test.go cover positive and negative cases.

Multi-tenant isolation: No risk — platform paths are tenant-agnostic by design. No tenant data is exposed or leaked by this change.

Bot Review Notes

No unresolved bot threads found on this PR.

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Clean fix for the tenant-claim bootstrap problem. Security model is sound — platform paths already bypass tenant resolution, and this aligns the authorization middleware with that pattern. Tests cover both REST and gRPC paths. No findings.

Copy link
Copy Markdown
Contributor

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
shared/platform/gateway/tenant_resolver.go (1)

84-90: ⚠️ Potential issue | 🟠 Major

Tighten platform-path matching to avoid unintended auth bypass.

Line 86 currently does raw prefix matching, so /v1/tenantship would be treated as a platform path. That can unintentionally bypass tenant resolution/authorization for non-platform routes.

Proposed fix
 func IsPlatformPath(path string) bool {
 	for _, prefix := range platformPaths {
-		if strings.HasPrefix(path, prefix) {
+		base := strings.TrimSuffix(prefix, "/")
+		if path == base || strings.HasPrefix(path, base+"/") {
 			return true
 		}
 	}
 	return false
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shared/platform/gateway/tenant_resolver.go` around lines 84 - 90,
IsPlatformPath currently treats any path starting with a platform prefix as a
match (e.g., "/v1/tenantship"), which can bypass tenant checks; change the logic
in IsPlatformPath to only return true when the path is exactly the prefix or the
prefix followed by a slash (i.e., path == prefix || strings.HasPrefix(path,
prefix + "/")) for each entry in platformPaths, so only the intended route
segments (and their subpaths) are considered platform paths.
🧹 Nitpick comments (1)
shared/platform/gateway/tenant_resolver_test.go (1)

931-946: Add a boundary regression assertion for non-platform near-prefix paths.

Consider adding a negative case like assert.False(t, IsPlatformPath("/v1/tenantship")) to lock down prefix-boundary behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shared/platform/gateway/tenant_resolver_test.go` around lines 931 - 946, Add
a negative boundary test to ensure prefix matches are exact: update
TestIsPlatformPath to include an assertion that a near-prefix path like
"/v1/tenantship" (and optionally "/meridian.tenant.v1x.TenantService/List")
returns false; locate the test function TestIsPlatformPath and add
assert.False(t, IsPlatformPath("/v1/tenantship")) to lock down prefix-boundary
behavior for IsPlatformPath.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@shared/platform/gateway/tenant_resolver.go`:
- Around line 84-90: IsPlatformPath currently treats any path starting with a
platform prefix as a match (e.g., "/v1/tenantship"), which can bypass tenant
checks; change the logic in IsPlatformPath to only return true when the path is
exactly the prefix or the prefix followed by a slash (i.e., path == prefix ||
strings.HasPrefix(path, prefix + "/")) for each entry in platformPaths, so only
the intended route segments (and their subpaths) are considered platform paths.

---

Nitpick comments:
In `@shared/platform/gateway/tenant_resolver_test.go`:
- Around line 931-946: Add a negative boundary test to ensure prefix matches are
exact: update TestIsPlatformPath to include an assertion that a near-prefix path
like "/v1/tenantship" (and optionally "/meridian.tenant.v1x.TenantService/List")
returns false; locate the test function TestIsPlatformPath and add
assert.False(t, IsPlatformPath("/v1/tenantship")) to lock down prefix-boundary
behavior for IsPlatformPath.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 78a695e3-589f-48fe-a00d-20858cc0375f

📥 Commits

Reviewing files that changed from the base of the PR and between 107b087 and 9827812.

📒 Files selected for processing (4)
  • services/api-gateway/auth/combined_middleware.go
  • services/api-gateway/auth/combined_middleware_test.go
  • shared/platform/gateway/tenant_resolver.go
  • shared/platform/gateway/tenant_resolver_test.go

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 4, 2026

⚠️ Breaking Proto Changes Detected

This PR contains breaking changes to protobuf definitions. Please review the changes carefully and consider:

  • Incrementing to v2 if this is a major breaking change
  • Using field deprecation instead of removal
  • Adding new fields with new field numbers

Run make proto-breaking locally for details.

💡 Tip: If this is an intentional breaking change, add the breaking-change label to skip this check.

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Clean fix for the tenant-claim chicken-and-egg problem. Security model is sound — platform path bypass requires authentication, and the endpoints handle their own access control. Refactoring preserves existing behavior while adding the new platform-path fallback. Tests cover both REST and gRPC paths. No multi-tenant isolation risk. See summary comment for full analysis.

@bjcoombs bjcoombs merged commit 37a10cb into develop Mar 4, 2026
38 checks passed
@bjcoombs bjcoombs deleted the fix-platform-path-authz branch March 4, 2026 16:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant