Skip to content

feat: validate routing rule scope_id resolves to an existing entity at write time and during config.json sync - #5371

Merged
akshaydeo merged 1 commit into
devfrom
07-20-fix_validate_routing_rule_scope_id_references_a_real_entity
Jul 20, 2026
Merged

feat: validate routing rule scope_id resolves to an existing entity at write time and during config.json sync#5371
akshaydeo merged 1 commit into
devfrom
07-20-fix_validate_routing_rule_scope_id_references_a_real_entity

Conversation

@Pratham-Mishra04

Copy link
Copy Markdown
Collaborator

Summary

Routing rules with a non-global scope can reference a scope_id that doesn't correspond to any real entity (e.g. a name typed in place of an ID). When this happens, the routing engine silently matches zero requests because it caches rules keyed by the real entity ID. This PR rejects invalid scope_id values at write time — both via the HTTP API and during config file ingestion — so misconfigured rules fail loudly instead of silently doing nothing.

Changes

  • Added validateRoutingScopeID to the governance HTTP handler, which looks up the referenced entity (virtual_key, team, or customer) in the config store and returns a 400 error if it doesn't exist. This is called on createRoutingRule unconditionally and on updateRoutingRule only when scope or scope_id is part of the request payload.
  • Added entityIDSet helper in the config file merge path that builds a lookup set from already-persisted entities plus those pending insertion in the same sync, so a routing rule can validly reference an entity introduced in the same config file.
  • During mergeGovernanceConfig, routing rules whose scope_id doesn't resolve to a known entity of the declared scope type are now skipped with a warning log rather than being written to the store.

Type of change

  • Bug fix

Affected areas

  • Core (Go)
  • Transports (HTTP)

How to test

go test ./...
  1. Create a routing rule via the API with a scope of virtual_key and a scope_id that does not exist — expect a 400 response with a message indicating the virtual key was not found.
  2. Create a routing rule with a valid scope_id — expect success.
  3. Update a routing rule without changing scope or scope_id (e.g. toggle enabled) — expect no re-validation call and a successful response.
  4. Load a config file containing a routing rule whose scope_id is a name rather than an ID — expect a warning log and the rule to be skipped.

Breaking changes

  • No

Security considerations

Prevents routing rules from being written in a state where they silently match no traffic, which could mask misconfiguration or be exploited to create rules that appear active but are effectively no-ops.

Checklist

  • I read docs/contributing/README.md and followed the guidelines
  • I added/updated tests where appropriate
  • I updated documentation where needed
  • I verified builds succeed (Go and UI)
  • I verified the CI pipeline passes locally if applicable

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added validation for routing rule scopes when creating or updating rules.
    • Added configuration safeguards to prevent routing rules from referencing missing teams, customers, or virtual keys.
  • Bug Fixes

    • Invalid new routing rules are now rejected or omitted.
    • Existing invalid rules retain their last valid configuration, with a warning recorded.
    • Infrastructure errors during validation now return an appropriate server error.

Walkthrough

Routing-rule configuration reconciliation now filters unresolved non-global scopes. Governance HTTP create and update handlers validate scope IDs against the config store, with conditional update validation and distinct client-error versus infrastructure-error responses.

Changes

Routing scope validation

Layer / File(s) Summary
Configuration reconciliation sanitization
transports/bifrost-http/lib/config.go
Builds entity ID sets including pending additions and filters unresolved routing rules, retaining persisted versions for existing rule IDs and dropping invalid new rules.
Routing scope existence validation
transports/bifrost-http/handlers/governance.go
Resolves scope IDs against virtual keys, teams, or customers and maps missing entities to HTTP 400 and store failures to HTTP 500.
Create and update enforcement
transports/bifrost-http/handlers/governance.go
Validates non-global scopes on creation and revalidates updates only when scope or scope_id changes.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant GovernanceHandler
  participant ConfigStore
  Client->>GovernanceHandler: Create or update routing rule
  GovernanceHandler->>ConfigStore: Validate scope_id for scope type
  ConfigStore-->>GovernanceHandler: Matching entity or error
  GovernanceHandler-->>Client: Success, HTTP 400, or HTTP 500
Loading

Possibly related PRs

Suggested reviewers: akshaydeo, danpiths

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: validating routing rule scope IDs at write time and during config sync.
Description check ✅ Passed The description covers summary, changes, testing, affected areas, breaking changes, security, and checklist; only non-critical sections are missing.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ 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 07-20-fix_validate_routing_rule_scope_id_references_a_real_entity

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


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

@greptile-apps

greptile-apps Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 4/5

A contained config reconciliation issue should be fixed before merging.

  • HTTP validation now classifies missing entities and store failures correctly.
  • Authoritative sync validates against entities that will survive pruning.
  • Restoring a persisted rule can still preserve a reference to an entity deleted by the same sync.

transports/bifrost-http/lib/config.go

Important Files Changed

Filename Overview
transports/bifrost-http/handlers/governance.go Adds scope-ID validation for routing-rule creation and relevant updates, with separate responses for missing entities and store failures.
transports/bifrost-http/lib/config.go Adds post-sync entity lookup sets and routing-rule sanitization, but the persisted-rule fallback can retain a dangling scope reference.

Reviews (3): Last reviewed commit: "fix: validate routing rule scope_id refe..." | Re-trigger Greptile

Comment thread transports/bifrost-http/lib/config.go Outdated
Comment thread transports/bifrost-http/lib/config.go Outdated
Comment thread transports/bifrost-http/handlers/governance.go
@Pratham-Mishra04
Pratham-Mishra04 force-pushed the 07-20-fix_validate_routing_rule_scope_id_references_a_real_entity branch from 0135626 to 378f2de Compare July 20, 2026 07:17
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 20, 2026
@Pratham-Mishra04
Pratham-Mishra04 marked this pull request as draft July 20, 2026 07:24
@Pratham-Mishra04
Pratham-Mishra04 changed the base branch from 07-18-fix_response_message_output_struct_fixes_in_api_reference to graphite-base/5371 July 20, 2026 07:51
@Pratham-Mishra04
Pratham-Mishra04 force-pushed the 07-20-fix_validate_routing_rule_scope_id_references_a_real_entity branch from 378f2de to 80d1e92 Compare July 20, 2026 07:54
@graphite-app
graphite-app Bot changed the base branch from graphite-base/5371 to dev July 20, 2026 07:54
@graphite-app
graphite-app Bot dismissed coderabbitai[bot]’s stale review July 20, 2026 07:54

The base branch was changed.

@Pratham-Mishra04
Pratham-Mishra04 force-pushed the 07-20-fix_validate_routing_rule_scope_id_references_a_real_entity branch from 80d1e92 to c7c9565 Compare July 20, 2026 07:54
@akshaydeo
akshaydeo marked this pull request as ready for review July 20, 2026 08:35

akshaydeo commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Merge activity

  • Jul 20, 8:36 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Jul 20, 8:36 AM UTC: @akshaydeo merged this pull request with Graphite.

@akshaydeo
akshaydeo merged commit a4736f3 into dev Jul 20, 2026
14 of 16 checks passed
@akshaydeo
akshaydeo deleted the 07-20-fix_validate_routing_rule_scope_id_references_a_real_entity branch July 20, 2026 08:36
if scopeID == "" || !validRoutingScopeIDs[rule.Scope][scopeID] {
if existing, ok := existingRoutingRulesByID[rule.ID]; ok {
logger.Warn("routing rule %s: scope_id %q does not resolve to an existing %s (use the entity's id, not its name); keeping last persisted version", rule.ID, scopeID, rule.Scope)
sanitizedRoutingRules = append(sanitizedRoutingRules, existing)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Fallback preserves dangling rule When an authoritative file removes team A and changes an existing team-scoped rule to an invalid ID, this branch restores the persisted rule that still points to team A. The later prune deletes team A but keeps the restored rule, leaving it with a nonexistent scope_id. The rule then remains active in storage but silently matches no traffic. Only restore the persisted rule when its scope target will survive this sync; otherwise remove or reject the rule.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@transports/bifrost-http/lib/config.go`:
- Around line 2586-2589: Update the virtual-key merge flow and the
source-of-truth branch around vkScopeIDs to track IDs of virtual keys skipped
when secret-backed Value resolution fails, then exclude those IDs when
rebuilding vkScopeIDs from configData.Governance.VirtualKeys. Keep persisted
virtual keys included while ensuring routing-rule scope validation cannot treat
skipped, non-persisted virtual keys as valid targets.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3e05856e-bb0c-424e-b78c-89e074b80f15

📥 Commits

Reviewing files that changed from the base of the PR and between f53936d and c7c9565.

📒 Files selected for processing (2)
  • transports/bifrost-http/handlers/governance.go
  • transports/bifrost-http/lib/config.go

Comment on lines +2586 to +2589
vkScopeIDs := entityIDSet(governanceConfig.VirtualKeys, func(vk configstoreTables.TableVirtualKey) string { return vk.ID }, virtualKeysToAdd)
if configData.isConfigJSONSourceOfTruth() && configData.governanceSectionPresent("virtual_keys") {
vkScopeIDs = entityIDSet(configData.Governance.VirtualKeys, func(vk configstoreTables.TableVirtualKey) string { return vk.ID }, nil)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Skipped virtual keys (unresolved secret refs) are still counted as valid routing-rule scope targets in source-of-truth mode.

When source_of_truth=config.json and the virtual_keys section is present, vkScopeIDs is rebuilt from the raw configData.Governance.VirtualKeys slice. However, that slice can contain entries that were skipped from persistence — the not-found branch (around line 2555-2557) assigns the VK an ID and then continues without appending to virtualKeysToAdd when a secret-backed Value fails to resolve. That skipped VK's ID still ends up counted as "resolvable" here, so a routing rule scoped to it will pass sanitization and get persisted, even though the VK itself will never exist in the DB — reproducing exactly the "silently matches zero requests" failure this PR is meant to prevent. Teams/Customers don't have this asymmetry since their merge loops have no comparable skip path; this is virtual-key specific.

🐛 Proposed fix: track skipped VK IDs and exclude them from the scope-ID set
+	skippedVirtualKeyIDs := make(map[string]bool)
 	for i, newVirtualKey := range configData.Governance.VirtualKeys {
 		...
 		if !found {
 			...
 			resolvedVal := configData.Governance.VirtualKeys[i].Value.GetValue()
 			if resolvedVal == "" && configData.Governance.VirtualKeys[i].Value.IsFromSecret() {
 				logger.Warn("virtual key %s: env/vault ref %q could not be resolved, skipping", newVirtualKey.ID, configData.Governance.VirtualKeys[i].Value.GetRawRef())
+				skippedVirtualKeyIDs[configData.Governance.VirtualKeys[i].ID] = true
 				continue
 			}
 			...
 		}
 	}
 	...
 	vkScopeIDs := entityIDSet(governanceConfig.VirtualKeys, func(vk configstoreTables.TableVirtualKey) string { return vk.ID }, virtualKeysToAdd)
 	if configData.isConfigJSONSourceOfTruth() && configData.governanceSectionPresent("virtual_keys") {
 		vkScopeIDs = entityIDSet(configData.Governance.VirtualKeys, func(vk configstoreTables.TableVirtualKey) string { return vk.ID }, nil)
+		for id := range skippedVirtualKeyIDs {
+			delete(vkScopeIDs, id)
+		}
 	}
📝 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
vkScopeIDs := entityIDSet(governanceConfig.VirtualKeys, func(vk configstoreTables.TableVirtualKey) string { return vk.ID }, virtualKeysToAdd)
if configData.isConfigJSONSourceOfTruth() && configData.governanceSectionPresent("virtual_keys") {
vkScopeIDs = entityIDSet(configData.Governance.VirtualKeys, func(vk configstoreTables.TableVirtualKey) string { return vk.ID }, nil)
}
vkScopeIDs := entityIDSet(governanceConfig.VirtualKeys, func(vk configstoreTables.TableVirtualKey) string { return vk.ID }, virtualKeysToAdd)
if configData.isConfigJSONSourceOfTruth() && configData.governanceSectionPresent("virtual_keys") {
vkScopeIDs = entityIDSet(configData.Governance.VirtualKeys, func(vk configstoreTables.TableVirtualKey) string { return vk.ID }, nil)
for id := range skippedVirtualKeyIDs {
delete(vkScopeIDs, id)
}
}
🤖 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 `@transports/bifrost-http/lib/config.go` around lines 2586 - 2589, Update the
virtual-key merge flow and the source-of-truth branch around vkScopeIDs to track
IDs of virtual keys skipped when secret-backed Value resolution fails, then
exclude those IDs when rebuilding vkScopeIDs from
configData.Governance.VirtualKeys. Keep persisted virtual keys included while
ensuring routing-rule scope validation cannot treat skipped, non-persisted
virtual keys as valid targets.

Source: Path instructions

akhsaul pushed a commit to akhsaul/bifrost that referenced this pull request Aug 27, 2026
… at write time and during config.json sync (maximhq#5371)

## Summary

Routing rules with a non-global `scope` can reference a `scope_id` that doesn't correspond to any real entity (e.g. a name typed in place of an ID). When this happens, the routing engine silently matches zero requests because it caches rules keyed by the real entity ID. This PR rejects invalid `scope_id` values at write time — both via the HTTP API and during config file ingestion — so misconfigured rules fail loudly instead of silently doing nothing.

## Changes

- Added `validateRoutingScopeID` to the governance HTTP handler, which looks up the referenced entity (`virtual_key`, `team`, or `customer`) in the config store and returns a 400 error if it doesn't exist. This is called on `createRoutingRule` unconditionally and on `updateRoutingRule` only when `scope` or `scope_id` is part of the request payload.
- Added `entityIDSet` helper in the config file merge path that builds a lookup set from already-persisted entities plus those pending insertion in the same sync, so a routing rule can validly reference an entity introduced in the same config file.
- During `mergeGovernanceConfig`, routing rules whose `scope_id` doesn't resolve to a known entity of the declared scope type are now skipped with a warning log rather than being written to the store.

## Type of change

- [x] Bug fix

## Affected areas

- [x] Core (Go)
- [x] Transports (HTTP)

## How to test

```sh
go test ./...
```

1. Create a routing rule via the API with a `scope` of `virtual_key` and a `scope_id` that does not exist — expect a 400 response with a message indicating the virtual key was not found.
2. Create a routing rule with a valid `scope_id` — expect success.
3. Update a routing rule without changing `scope` or `scope_id` (e.g. toggle `enabled`) — expect no re-validation call and a successful response.
4. Load a config file containing a routing rule whose `scope_id` is a name rather than an ID — expect a warning log and the rule to be skipped.

## Breaking changes

- [x] No

## Security considerations

Prevents routing rules from being written in a state where they silently match no traffic, which could mask misconfiguration or be exploited to create rules that appear active but are effectively no-ops.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable
occcat pushed a commit to occcat/bifrost that referenced this pull request Sep 2, 2026
… at write time and during config.json sync (maximhq#5371)

## Summary

Routing rules with a non-global `scope` can reference a `scope_id` that doesn't correspond to any real entity (e.g. a name typed in place of an ID). When this happens, the routing engine silently matches zero requests because it caches rules keyed by the real entity ID. This PR rejects invalid `scope_id` values at write time — both via the HTTP API and during config file ingestion — so misconfigured rules fail loudly instead of silently doing nothing.

## Changes

- Added `validateRoutingScopeID` to the governance HTTP handler, which looks up the referenced entity (`virtual_key`, `team`, or `customer`) in the config store and returns a 400 error if it doesn't exist. This is called on `createRoutingRule` unconditionally and on `updateRoutingRule` only when `scope` or `scope_id` is part of the request payload.
- Added `entityIDSet` helper in the config file merge path that builds a lookup set from already-persisted entities plus those pending insertion in the same sync, so a routing rule can validly reference an entity introduced in the same config file.
- During `mergeGovernanceConfig`, routing rules whose `scope_id` doesn't resolve to a known entity of the declared scope type are now skipped with a warning log rather than being written to the store.

## Type of change

- [x] Bug fix

## Affected areas

- [x] Core (Go)
- [x] Transports (HTTP)

## How to test

```sh
go test ./...
```

1. Create a routing rule via the API with a `scope` of `virtual_key` and a `scope_id` that does not exist — expect a 400 response with a message indicating the virtual key was not found.
2. Create a routing rule with a valid `scope_id` — expect success.
3. Update a routing rule without changing `scope` or `scope_id` (e.g. toggle `enabled`) — expect no re-validation call and a successful response.
4. Load a config file containing a routing rule whose `scope_id` is a name rather than an ID — expect a warning log and the rule to be skipped.

## Breaking changes

- [x] No

## Security considerations

Prevents routing rules from being written in a state where they silently match no traffic, which could mask misconfiguration or be exploited to create rules that appear active but are effectively no-ops.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable
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.

3 participants