feat(graphql): [TraxAuthorize] on [TraxQueryModel] entities#54
Merged
Conversation
Honors [TraxAuthorize] on entity classes exposed via [TraxQueryModel], applying HotChocolate's @authorize directive at both the entry-field level (under discover.*) and the ObjectType level. Field-level gating blocks the entry point unconditionally — including Connection scalars like totalCount, pageInfo, and where: filter probes. Type-level gating catches transitive navigation: an unauthorized caller cannot read a gated entity by reaching it through a navigation property on an ungated parent. Combinator semantics mirror the train-side TraxAuthorize: policies AND across attributes, roles OR (CSV split, unioned across attributes). Bare [TraxAuthorize] requires an authenticated user. Errors surface as TRAX_AUTHORIZATION with the public "Not authorized." message; the entity name, policy name, and role names never reach the wire. QueryModelAuthorizationValidator throws at host start if any entity references an authorization policy that is not registered with services.AddAuthorization. Build-time validation rejects whitespace policy values and all-empty roles CSV.
Prevents a consumer from subclassing the type module and registering a replacement via services.Replace<QueryModelTypeModule>(...) that skips emitting the @authorize directives. The DI factory now must return an exact QueryModelTypeModule instance, and there's no other class that satisfies that constraint.
…xQueryModel] Adds a QueryModelAuthorizationSchemaValidator hosted service that runs at host start, materialises the GraphQL schema, and re-asserts that every [TraxAuthorize]-gated entity still carries the @authorize directive at both the entry-field and ObjectType levels. Closes the only remaining escape hatch in the configuration surface — a consumer-supplied ConfigureSchema callback that installs a TypeInterceptor capable of stripping directives Trax emitted earlier in the pipeline. If a gate has been removed, the host refuses to start with a message naming the entity and the missing directive location. Coverage: - Unit tests build minimal schemas with the directive stripped at type, field, and both levels — validator throws naming entity / gate. - E2E test boots a real host with the standard wiring and confirms the validator signs off on a normally-configured schema.
When any [TraxQueryModel] entity carries [TraxAuthorize], wire a HotChocolate IHttpRequestInterceptor (QueryModelAuthenticationInterceptor) that populates HttpContext.User by walking every registered authentication scheme until one succeeds. Without it, multi-scheme hosts (e.g. api-key + JWT) without an explicit default scheme leave HC seeing an anonymous principal, causing every gated query to reject every caller — including those whose credentials would have authenticated against any of the registered schemes. Runs only for HTTP GraphQL execution requests, so WebSocket subscription upgrades and the Banana Cake Pop tool page are not affected. Subscriptions authenticate via the per-scheme socket interceptors as before. Zero consumer configuration required; the interceptor activates automatically alongside the @authorize directives.
QueryModelAuthorizeE2ETests uses `trax_api_auth_querymodel` and QueryModelAuthorizeSchemaInvariantE2ETests uses `trax_api_auth_schema_inv`. The CI runner provisions per-class databases upfront so test fixtures don't race to CREATE them; both new names need to be in the provisioning list.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
The previous failure mode: adding a new AuthE2E test class with its own per-fixture database required also updating two GitHub Actions workflows to add the database name to a CREATE DATABASE loop. Forgetting that step (easy to do) produced a setup-time Postgres "database does not exist" failure on CI for every test in the new class. Move provisioning into the test code itself: AuthE2EHost.EnsureDatabaseExists connects to the maintenance database and idempotently creates the per-class DB, called from both StartAsync and the EnsureSeeded helpers on the test DbContexts. Validates the database identifier shape since CREATE DATABASE does not support parameter binding for identifiers. CI workflows no longer enumerate the AuthE2E database list — adding a new fixture is a code-only change.
…eptor Three new direct-unit test classes pin the failure paths that codecov flagged on the original PR: QueryModelAuthorizationSchemaValidatorTests — new cases for the schema-shape failures: discover field deleted, entry field deleted, namespace-routed entity with the namespace field deleted, and the early-return path when no entity is gated (proved with a ThrowingServiceProvider that fails any DI access so the assertion is "the validator did not attempt to resolve the schema," not just "no exception thrown"). QueryModelAuthorizationValidatorTests — the missing-policy throw path (message must name the policy, the entity, and the AddAuthorization fix), plus a ThrowingPolicyProvider that proves roles-only attributes never consult the provider, and a CountingPolicyProvider that proves the validator dedupes by policy name across entities (one provider call for N entities sharing a policy). QueryModelAuthenticationInterceptorTests — every branch of the scheme walk: already-authenticated short-circuit (no scheme is queried, so the upstream principal cannot be downgraded), no-scheme-matches keeps user anonymous, first-scheme-succeeds stops the walk, second-scheme-succeeds proves the walk continues past NoResult, and the defensive guard that Success with a null Principal must not assign the anonymous user. Every assertion pins behaviour that would break in production if the code regressed: schema-shape errors must name what is missing so maintainers can debug their override; policy-name typos must fail loud; an interceptor that stops walking after NoResult would prevent JWT requests from ever authenticating on api-key+JWT hosts.
…del] Adds direct unit coverage for the failure modes the Codecov report flagged: - QueryModelAuthorizationSchemaValidator: namespace happy path, namespace-field-missing, entry-field-missing, discover-field-missing, ObjectType-missing-from-schema, no-gated-entities early return (with a throwing service provider that fails the test if the validator resolves anything). - QueryModelAuthenticationInterceptor: already-authenticated short circuit (asserts the scheme provider is never asked), no-scheme-matches leaves principal anonymous, first-success wins and stops walking. - GraphQLModelExposureWarningService: no-models silent skip, all-gated silent skip, some-ungated warning carries both counts. - QueryModelAuthorizationValidator: same policy referenced by multiple entities hits the provider exactly once (dedup branch). - QueryModelTypeModule: DeprecationReason wired through to @deprecated, BindFields.Explicit + [TraxAuthorize] compose without dropping either the column-only field set or the @authorize directive. Every test asserts a behaviour that would break under a realistic regression — none are added purely to bump the coverage number.
|
This PR is included in version 1.30.0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
[TraxAuthorize]on[TraxQueryModel]entities, attaching HotChocolate's@authorizedirective at both the entry field (underdiscover.*) and theObjectType. Field-level gating blocks Connection scalars (totalCount,pageInfo,where:filter probes); type-level gating catches transitive navigation through ungated parents.[TraxAuthorize]exactly: policies AND across attributes, roles OR within CSV / OR across attributes.QueryModelAuthenticationInterceptorpopulatesHttpContext.Userfor inbound HTTP GraphQL requests by walking every registered authentication scheme, so multi-scheme hosts (api-key + JWT, etc.) work without consumer configuration.QueryModelAuthorizationValidatorthrows at host start if any entity references an unregistered policy.QueryModelAuthorizationSchemaValidatorre-asserts directive presence on the materialised schema, catching consumer-sideConfigureSchemacallbacks that strip directives.QueryModelTypeModuleis sealed to prevent DI replacement bypasses.Test plan
QueryModelAuthorizeE2ETestsincluding the burning transitive-navigation contract (Player traversingowners[].booksis rejected and the response body contains zero leaked title strings) and the Connection-scalar side-channel closures (totalCount-only,pageInfo-only).QueryModelAuthorizeSchemaValidatorTestsproving the post-build validator catches stripped directives.QueryModelAuthorizeDiscoveryTestspinning attribute discovery, inheritance, ExposeAs composition, and build-time shape validation.Security contract pinned by the tests
A Player API key cannot read a gated entity (a) directly, (b) transitively through a navigation property on an ungated parent, or (c) via Connection scalars that never resolve a node. The transitive-navigation regression guards explicitly assert no seeded title strings appear anywhere in the response body — data, errors, or extensions.