fix: cached GraphQL schema can result in missing members if used with access policies#10590
Merged
paveltiunov merged 7 commits intomasterfrom Apr 1, 2026
Merged
Conversation
…at query time Instead of adding JS-side member validation for GraphQL RBAC enforcement, rely on the existing Rust-side validation in query_result_transform.rs. The Rust transform layer already checks if requested members are present in the RBAC-filtered annotation map and throws 'You requested hidden member' errors for inaccessible members. Changes: - Build GraphQL schema from unfiltered metadata (skipVisibilityPatch) so it can be safely cached across security contexts sharing a CompilerApi - Add skipVisibilityPatch option to CompilerApi.metaConfig() to bypass RBAC visibility patching when building the shared GraphQL schema - Add integration test verifying RBAC enforcement through GraphQL Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #10590 +/- ##
===========================================
- Coverage 78.58% 57.88% -20.71%
===========================================
Files 475 225 -250
Lines 92822 17581 -75241
Branches 3612 3614 +2
===========================================
- Hits 72945 10176 -62769
+ Misses 19333 6861 -12472
Partials 544 544
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Add the new RBAC GraphQL integration test to: - packages/cubejs-testing/package.json as smoke:rbac-graphql script - .github/actions/smoke.sh to run in integration-smoke CI job Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
… with empty results When RBAC denies access to a member, the query gets a '1=0' filter that returns zero rows. Previously, the Rust get_members() function would short-circuit on empty columns (derived from zero rows) without checking if the requested members were actually accessible. Now validate_query_members_in_annotation() checks the query's measures, dimensions, segments, and time dimensions against the annotation map even when db_data.columns is empty. This ensures 'You requested hidden member' errors are returned for RBAC-denied members regardless of whether the query returns data. Also adds a test for the hidden member + empty dataset scenario. Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
…dation Segments are not included in the annotation map (which only contains measures, dimensions, and time dimensions). The rlsAccessDenied synthetic segment added by RBAC denial would incorrectly trigger the hidden member error when validated against the annotation. Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
Now that the Rust transform validates query members against the annotation even with empty results, RBAC-denied members return 'You requested hidden member' errors instead of silently returning empty data. Updated tests: - line_items hidden price_dim: expect error instead of empty result - orders_view and cube with default policy: expect error for orders_view.count when user has no matching access policy on the view Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
The orders_view_rest snapshot is no longer used after updating the test to expect a hidden member error instead of empty results. Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
…duplicate hidden member checks The hidden member error was duplicated in three places: - get_members (column-based check for non-empty results) - get_vanilla_row (per-row alias check) - validate_query_members_in_annotation (query member check for empty results) Extract ensure_member_in_annotation() and use it in all three places. Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
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.
Check List
Issue Reference this PR resolves
Addresses the GraphQL schema caching issue causing intermittent 400s when different security contexts share a CompilerApi instance.
Description of Changes Made
Problem
The GraphQL schema was previously built from RBAC-filtered metadata and cached per
CompilerApiinstance. When multiple users with different RBAC roles shared the sameCompilerApi(viacontextToAppId), the first user's RBAC-filtered schema got cached and used for ALL users — causing intermittent 400 errors for fields that should be accessible.The branch
igor/core-384-investigate-graphql-schema-caching-causing-intermittent-400sproposed adding JS-side member validation ingraphql.tsbefore callingload(). However, this validation is redundant because the Rust-side result transform layer can perform this check.Solution
Instead of adding JS-side validation, this PR fixes the existing Rust-side validation in
query_result_transform.rsto properly handle RBAC-denied members even when query results are empty.Root cause of the Rust-side gap: When RBAC denies access,
applyRowLevelSecurityadds a1=0filter that returns zero rows. With zero rows,QueryResult::from_js_raw_dataproduces emptycolumns. The Rustget_members()function previously short-circuited ondb_data.columns.is_empty()without checking if requested members were actually accessible in the annotation.Changes:
query_result_transform.rs(Rust):ensure_member_in_annotation()to deduplicate the hidden member check that was previously copy-pasted inget_members,get_vanilla_row, and the new validation functionvalidate_query_members_in_annotation()that checks the query's measures, dimensions, and time dimensions against the annotation map even whendb_data.columnsis empty (segments are excluded since the annotation map only contains measures, dimensions, and time dimensions)ensure_member_in_annotation()in all three check sitesgateway.ts: Build GraphQL schema from unfiltered metadata (skipVisibilityPatch: true) so it can be safely cached across all security contexts sharing aCompilerApi.CompilerApi.ts: AddedskipVisibilityPatchoption tometaConfig()to bypass RBAC visibility patching when building the shared GraphQL schema.Integration test: Added
smoke-rbac-graphql.test.tswith test fixtures verifying:includes: []) blocks all member accessCI: Added
smoke:rbac-graphqlto CI pipeline.Updated existing tests:
smoke-rbac.test.tstests that previously expected silent empty results for hidden members now correctly expect "You requested hidden member" errors.How RBAC enforcement works end-to-end
load()fetchesmetaConfig()with RBAC visibility patching → hidden members getisVisible: falsefilterVisibleItemsInMeta()strips items whereisVisible === falseprepareAnnotation()builds annotation from filtered metadata → hidden members are not in the annotationTransformedData::transformcallsget_memberswhich:ensure_member_in_annotation1=0): callsvalidate_query_members_in_annotationto check the query's requested membersensure_member_in_annotationthrows "You requested hidden member" errorgetFinalResult()reject →handleError→ GraphQL error response