Skip to content

fix: gate the events/validation routes; unify the handler auth gate - #221

Merged
rorybyrne merged 2 commits into
mainfrom
fix/gated-routes-unified-handler-gate
Aug 16, 2026
Merged

fix: gate the events/validation routes; unify the handler auth gate#221
rorybyrne merged 2 commits into
mainfrom
fix/gated-routes-unified-handler-gate

Conversation

@rorybyrne

@rorybyrne rorybyrne commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Fixes findings F1 and F2 from the 2026-08-16 architecture survey (arch-report open).

F1 — two routes bypassed the __auth__ gate entirely

GET /events and GET /validation/runs/{id} injected a Service directly instead of a handler, so no gate existed on either path and the boot validator couldn't see the hole. Both now go through gated query handlers with public() as an explicit, boot-validated, documented declaration (the changefeed is the federation surface; run-status polling is anonymous today — tightening either is now a one-line gate change):

  • ListEventsHandler owns the changefeed read (look-ahead pagination, cursor, payload shaping). order becomes Literal["asc","desc"] — garbage values 422 instead of silently meaning ascending.
  • GetValidationRunHandler owns the status→shape rule (summary iff terminal, typed RunProgress iff running) and raises NotFoundError for the central mapper — removing the only business-logic decision tree in a route file (survey T8/F7 adjacency) and one of the three parallel error-translation paths.

Wire shapes preserved; the 404 body for a missing run now uses the central error envelope.

F2 — the auth-gate wrapper was duplicated and had drifted

CommandHandler and QueryHandler each carried a verbatim copy of the gate wrapper, metaclass, and Result base (~100 lines). The copies had already diverged: the query one built a logger inside the request path and debug-logged role checks — reads were logged, writes weren't, by accident.

One home now: shared/handler.py (wrap_run_with_auth + HandlerMeta + Result); the two base classes are facades contributing only their DTO base and result TypeVar bound. The request-path debug log is dropped deliberately (startup prints the gate table; denials raise typed errors mapped centrally). Import paths unchanged for all 46 importers.

New parity tests pin identical gate behavior across both handler kinds so the semantics cannot fork again.

Verification

@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown

Code Coverage

Package Line Rate Complexity Health
. 83% 0
application 100% 0
application.api 100% 0
application.api.mcp 80% 0
application.api.mcp.tools 89% 0
application.api.rest 76% 0
application.api.v1 88% 0
application.api.v1.routes 70% 0
application.api.v1.routes.data 93% 0
application.api.v1.routes.data.serializers 99% 0
application.event 100% 0
application.workflow 95% 0
domain 100% 0
domain.auth 100% 0
domain.auth.command 90% 0
domain.auth.event 100% 0
domain.auth.model 93% 0
domain.auth.port 99% 0
domain.auth.query 93% 0
domain.auth.service 91% 0
domain.auth.util 100% 0
domain.auth.util.di 79% 0
domain.curation 100% 0
domain.curation.adapter 100% 0
domain.curation.command 100% 0
domain.curation.event 100% 0
domain.curation.model 100% 0
domain.curation.port 100% 0
domain.curation.query 100% 0
domain.curation.service 100% 0
domain.data 100% 0
domain.data.command 73% 0
domain.data.model 97% 0
domain.data.port 100% 0
domain.data.query 93% 0
domain.data.service 82% 0
domain.data.util 100% 0
domain.data.util.di 89% 0
domain.deposition 100% 0
domain.deposition.adapter 100% 0
domain.deposition.command 91% 0
domain.deposition.event 100% 0
domain.deposition.model 94% 0
domain.deposition.port 100% 0
domain.deposition.query 87% 0
domain.deposition.service 97% 0
domain.deposition.util.di 95% 0
domain.feature 100% 0
domain.feature.event 0% 0
domain.feature.model 100% 0
domain.feature.port 100% 0
domain.feature.service 97% 0
domain.feature.util 100% 0
domain.feature.util.di 100% 0
domain.ingest 100% 0
domain.ingest.command 100% 0
domain.ingest.event 100% 0
domain.ingest.model 100% 0
domain.ingest.port 100% 0
domain.ingest.query 100% 0
domain.ingest.service 89% 0
domain.metadata 100% 0
domain.metadata.event 100% 0
domain.metadata.handler 100% 0
domain.metadata.model 0% 0
domain.metadata.port 100% 0
domain.metadata.service 93% 0
domain.metadata.util 100% 0
domain.metadata.util.di 100% 0
domain.record 100% 0
domain.record.adapter 100% 0
domain.record.command 100% 0
domain.record.event 100% 0
domain.record.model 100% 0
domain.record.port 100% 0
domain.record.query 100% 0
domain.record.service 67% 0
domain.semantics 100% 0
domain.semantics.command 94% 0
domain.semantics.event 100% 0
domain.semantics.handler 100% 0
domain.semantics.model 100% 0
domain.semantics.port 100% 0
domain.semantics.query 90% 0
domain.semantics.service 100% 0
domain.semantics.util 100% 0
domain.semantics.util.di 93% 0
domain.shared 96% 0
domain.shared.authorization 86% 0
domain.shared.model 93% 0
domain.shared.port 100% 0
domain.validation 100% 0
domain.validation.adapter 100% 0
domain.validation.command 98% 0
domain.validation.event 100% 0
domain.validation.model 99% 0
domain.validation.port 100% 0
domain.validation.query 100% 0
domain.validation.service 91% 0
domain.validation.util.di 95% 0
infrastructure 80% 0
infrastructure.auth 56% 0
infrastructure.data 34% 0
infrastructure.event 78% 0
infrastructure.http 92% 0
infrastructure.ingest 86% 0
infrastructure.k8s 77% 0
infrastructure.messaging 100% 0
infrastructure.oci 55% 0
infrastructure.persistence 72% 0
infrastructure.persistence.adapter 79% 0
infrastructure.persistence.mappers 62% 0
infrastructure.persistence.repository 37% 0
infrastructure.s3 39% 0
infrastructure.storage 100% 0
infrastructure.telemetry 100% 0
sdk 100% 0
util 100% 0
util.di 71% 0
Summary 80% (10230 / 12761) 0

@greptile-apps

greptile-apps Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR routes event-changefeed and validation-run reads through explicitly public, boot-validated query handlers and consolidates command/query authorization mechanics.

  • Moves event pagination, cursor generation, and payload shaping into ListEventsHandler.
  • Moves validation status shaping and missing-run handling into GetValidationRunHandler.
  • Replaces duplicated command/query gate metaclasses and wrappers with shared handler infrastructure.
  • Adds DI registrations and parity-focused unit coverage.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
server/osa/domain/shared/handler.py Centralizes the previously duplicated command/query authorization wrapper and metaclass while preserving gate behavior.
server/osa/domain/shared/event_log.py Adds an explicitly public query handler that preserves the existing event pagination and payload-shaping behavior.
server/osa/domain/validation/query/get_validation_run.py Adds an explicitly public handler for validation-run lookup, status-dependent shaping, and centralized not-found handling.
server/osa/application/api/v1/routes/events.py Converts the event route to thin DTO coercion and restricts the order parameter to its documented values.
server/osa/application/api/v1/routes/validation.py Converts validation-run polling to a thin route over the new query handler.
server/osa/domain/shared/command.py Retains the command facade and import compatibility while delegating shared mechanics.
server/osa/domain/shared/query.py Retains the query facade while delegating shared mechanics to the unified handler module.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Route as FastAPI Route
    participant Handler as Query Handler
    participant Gate as Shared Auth Gate
    participant Service as Domain Service
    Client->>Route: "GET /events or /validation/runs/{id}"
    Route->>Handler: run(Query DTO)
    Handler->>Gate: evaluate public()
    Gate-->>Handler: allow
    Handler->>Service: read events or validation run
    Service-->>Handler: domain data
    Handler-->>Route: shaped Result DTO
    Route-->>Client: HTTP response
Loading

Reviews (2): Last reviewed commit: "fix: gate the events and validation rout..." | Re-trigger Greptile

The gate wrapper, metaclass, and Result base were duplicated between
shared/command.py and shared/query.py (~100 lines) and had already
drifted: the query copy built a logger inside the request path and
debug-logged role checks — reads were logged, writes weren't, by
accident. The startup validator already treated the two handler kinds
as one thing.

Everything shared now lives once in shared/handler.py (wrap_run_with_auth
+ HandlerMeta + Result); CommandHandler/QueryHandler become facades
contributing only their DTO base and result TypeVar bound (Command with
Result-bound R; Query with unbound R, reason documented). The
request-path debug log is dropped deliberately: startup prints the gate
table once, and denials raise typed AuthorizationErrors recorded by the
central error mapper.

Parity tests pin the contract for both facades — public/at_least/
requires_scope/missing-gate behave identically on the write and read
sides, and the Result base is one class — so the gate semantics cannot
fork again.

Arch-survey 2026-08-16 finding F2.
GET /events and GET /validation/runs/{id} were the only two routes
injecting a Service directly instead of a handler — so no __auth__ gate
existed on either path and the startup validator (which walks handler
factories) could not see the hole. /events returned full event payloads
to anonymous callers; the validation route also owned the only
business-logic decision tree in a route file.

Both now go through gated query handlers, and the access level is a
deliberate, boot-validated declaration instead of an accident:

- ListEventsHandler (shared/event_log.py) owns the changefeed read —
  limit+1 look-ahead, cursor, payload shaping — with public() documented
  as the federation-surface choice. The order param is now
  Literal["asc","desc"]: garbage values 422 instead of silently
  meaning ascending.
- GetValidationRunHandler (validation/query/) owns the status→shape
  rule (summary iff terminal, typed RunProgress iff running) and raises
  NotFoundError for the central mapper — the route's HTTPException and
  its untyped progress dict are gone. public() preserves today's
  anonymous polling; an ownership check is now a one-line gate change.

Wire shapes preserved (response models unchanged; contract suite green).
The 404 body for a missing validation run now uses the central error
envelope rather than a bespoke detail string.

Arch-survey 2026-08-16 finding F1 (fixes the two exceptions to the
'routes carry no auth logic' convention; F7's route decision tree rides
along).
@rorybyrne
rorybyrne force-pushed the fix/gated-routes-unified-handler-gate branch from c30dc84 to d8f7c7e Compare August 16, 2026 12:50
@rorybyrne
rorybyrne merged commit 176242d into main Aug 16, 2026
14 checks passed
@rorybyrne
rorybyrne deleted the fix/gated-routes-unified-handler-gate branch August 16, 2026 12:54
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