Skip to content

feat: route rules in one routes.rego table; Bearer per RFC 7235 in JWTAuth and the policy - #38

Merged
aradng merged 4 commits into
mainfrom
fix/policy-bearer-scheme
Oct 10, 2026
Merged

aradng merged 4 commits into
mainfrom
fix/policy-bearer-scheme

Conversation

@aradng

@aradng aradng commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner

Two policy changes, version 0.5.7.

Route rules in one table

routes.rego ended in two statements per route. It now ends in three tables, one route per line:

prefix_rules := {
	"/api/notify/admin": [["ADMIN"]],
}

route_rules := {
	["GET", "/api/notify/healthcheck"]: "public",
	["PUT", "/api/iam/admin/organizations/{org_id}"]: [["ADMIN"]],
}

route_patterns := {
	["GET", "/api/notify/healthcheck"]: `^/api/notify/healthcheck$`,
	["PUT", "/api/iam/admin/organizations/{org_id}"]: `^/api/iam/admin/organizations/(?P<org_id>[^/]+)$`,
}
  • route_rules and prefix_rules are the service's; the hook reads each entry back and keeps it as written, multi-line rules included. One loop splits old statements and table entries by bracket depth, ignoring brackets inside strings.
  • Every mistake fails without writing, naming the line or text: other rego (x := 1, x := {}), a comment, text between a key and its colon, a key with extra elements or not a list, an unclosed table. Trailing spaces on a table's opener or closer are fine.
  • route_patterns is regenerated every run and never edited: Starlette's own compile_path regex, so OPA matches what FastAPI routes.
  • Empty tables are left out: an empty object fails opa check --strict. The preamble reads them through data.policy.*, which compiles either way.
  • A file in the old one-statement-per-route format is rewritten and keeps its rules.
  • OPA silently keeps the last of two equal keys in a table; the hook still fails the commit on a route or prefix listed twice.

Bearer per RFC 7235, in the app and the policy

The scheme is case-insensitive and any spaces before the token are ignored (RFC 7235), now in both places:

  • JWTAuth._transform_bearer used a case-sensitive "Bearer " prefix; it now uses FastAPI's get_authorization_scheme_param, as monitoring.py already does.
  • the policy's bearer matches the scheme in any case and trims spaces before the token.

One table of headers (Bearer, bearer, BEARER, extra spaces before or after, Bearerx…, a tab, Basic) is tested against both.

Verification

  • pytest: 281 passed, OPA decision tests included. The new parser, bearer and migration tests each fail against a mutant of the code they cover (string-blind bracket counting, escapes, comments, trailing spaces, x := {}, junk before the colon, extra key elements, case-sensitive or untrimmed bearer, old-format migration).
  • prek run --all-files clean; the rendered file is opa fmt-stable and passes opa check --strict and --v0-compatible with all tables, without prefix rules, and with no routes.
  • Migrated the filled routes.rego of notify, assistant, iam, persona, ai and twitter: first run rewrites, second run is a no-op, every rule kept (23/168/49/29/42/21), opa check --strict passes, notify's decisions unchanged.

Services pick it up by bumping fastloom and the hook rev together.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QA7Vjn4bbjxRsvyz8DNHPQ

aradng and others added 2 commits October 10, 2026 19:47
The generated preamble only read `Authorization: Bearer <jwt>`, while
FastAPI's security helpers compare the scheme case-insensitively. A client
sending `bearer <jwt>` was refused by OPA but accepted by the app. Version
0.5.7.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QA7Vjn4bbjxRsvyz8DNHPQ
A route was two statements, route_patterns[[method, path]] and
route_rules[[method, path]]. It is now one:
route_rules[[method, path, pattern]] := rule, with the pattern still
Starlette's compile_path regex, so OPA matches what FastAPI routes. The
preamble matches on route[2]; the hook reads a rule back by method and
path. The key is plain JSON, which `opa fmt` leaves as it is.

A file in the two-statement format is rewritten and keeps its rules.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QA7Vjn4bbjxRsvyz8DNHPQ
@aradng aradng changed the title fix: read a Bearer token whatever the case of its scheme feat: one routes.rego line per route; read any-case Bearer tokens Oct 10, 2026
routes.rego now ends in three tables, one route per line: prefix_rules
and route_rules hold the service's rules keyed by [method, path], and
route_patterns holds Starlette's compile_path regex for each route,
regenerated every run. A rule reads as one line of route_rules instead of
a statement per route, and the regex stays out of the way in its own
table. Entries are split by bracket depth, so a rule over several lines is
kept as written. Empty tables are left out, as an empty object fails
`opa check --strict`.

A file in the one-statement-per-route format is rewritten and keeps its
rules.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QA7Vjn4bbjxRsvyz8DNHPQ
@aradng aradng changed the title feat: one routes.rego line per route; read any-case Bearer tokens feat: route rules in one routes.rego table; read any-case Bearer tokens Oct 10, 2026
…table parser

Bearer: the scheme is case-insensitive and any spaces before the token are
ignored (RFC 7235), in both places. JWTAuth._transform_bearer used a
case-sensitive "Bearer " prefix and now uses FastAPI's
get_authorization_scheme_param, as monitoring already does; the policy
trims spaces before the token. One table of headers is tested against
both.

Parser: one loop splits old statements and table entries by bracket
depth, ignoring brackets inside strings. Every mistake now fails without
writing instead of being dropped or reset:
- a table opener or closer with trailing spaces is still recognised (it
  reset every rule to "todo");
- any `x := {}` and a comment line fail, naming the line (`x := {}` was
  dropped);
- text between a key and its colon, a key with extra elements and a
  non-list key fail, naming the text;
- an unclosed table names the table and its line;
- errors no longer print the retired `route_rules[...]` syntax.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QA7Vjn4bbjxRsvyz8DNHPQ
@aradng aradng changed the title feat: route rules in one routes.rego table; read any-case Bearer tokens feat: route rules in one routes.rego table; Bearer per RFC 7235 in JWTAuth and the policy Oct 10, 2026
@aradng
aradng merged commit 1088b16 into main Oct 10, 2026
3 checks passed
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