Add Row-Level Security (RLS) Support for ZooKeeper-Based Authentication (#17220)
This commit extends Row-Level Security (RLS) functionality to ZooKeeper-based authentication (ZkBasicAuthAccessControlFactory), enabling dynamic user management with table-level filter controls via REST API.
Previously, RLS filters were only supported with file-based BasicAuth configuration. ZK-based authentication passed empty RLS filter maps, preventing users from applying row-level restrictions when using ZooKeeper for user management.
Changes:
- Extended UserConfig to store RLS filters per table (
Map<String, List<String>>) - Updated
AccessControlUserConfigUtilsto serialize/deserialize RLS filters to/fromZNRecord - Modified
BasicAuthUtilsto extract RLS filters fromUserConfigand pass toZkBasicAuthPrincipal - Implemented
getRowColFilters()inZkBasicAuthAccessControlFactoryto return RLS filters - Updated
UserConfigBuilderto support RLS filter configuration - Added comprehensive unit tests for
UserConfig,UserConfigBuilder,BasicAuthTests,PinotAccessControlUserRestletResourceTest,AccessControlUserConfigUtilsetc. - Added integration tests for ZkAuth RLS scenarios by extracting base class from existing
BasicAuthtests
API Example:
POST /users
{
"username": "user",
"password": "secret",
"component": "BROKER",
"role": "USER",
"tables": ["table1", "table2"],
"permissions": ["READ"],
"rlsFilters": {
"table1": ["country='US'"],
"table2": ["department='Engineering'", "level='Senior'"]
}
}
Manual testing:
- Tested REST API create/get/delete operations with RLS filters
-
POST
curl -X 'POST' \ 'http://localhost:9001/users' \ -H 'accept: application/json' \ -H 'Authorization: Basic YWRtaW46dmVyeXNlY3JldA==' \ -H 'Content-Type: application/json' \ -d '{ "username": "userRLS_Simple", "password": "secretSimple", "component": "BROKER", "role": "USER", "tables": ["airlineStats"], "permissions": ["READ"], "rlsFilters": {"airlineStats": ["AirlineID='\''19805'\''"]} }'{ "status": "User userRLS_Simple_BROKER has been successfully added!" } -
GET
curl -X 'GET' \ 'http://localhost:9001/users/userRLS_Simple?component=BROKER' \ -H 'accept: application/json' \ -H 'Authorization: Basic YWRtaW46dmVyeXNlY3JldA=='{ "userRLS_Simple_BROKER": { "username": "userRLS_Simple", "password": "$2a$10$lA8P0gGKpG66Ype3wTmrp.2Bel8jHbQNglf8g/Cai7DHUbY/LZnzS", "component": "BROKER", "role": "USER", "tables": [ "airlineStats" ], "permissions": [ "READ" ], "rlsFilters": { "airlineStats": [ "AirlineID='19805'" ] }, "usernameWithComponent": "userRLS_Simple_BROKER" } } -
DELETE
curl -X 'DELETE' \ 'http://localhost:9001/users/userRLS_Simple?component=BROKER' \ -H 'accept: application/json' \ -H 'Authorization: Basic YWRtaW46dmVyeXNlY3JldA=='{ "status": "User: userRLS_Simple_BROKER has been successfully deleted" }
- Tested queries work as expected with rls for the ZK-based authentication
▸ Row-Level Security (RLS) Testing
Testing: RLS filter application on airlineStats:
DEBUG: curl -s -w \n%{http_code} -u admin:verysecret -X POST -H Content-Type: application/json -d {"sql":"SELECT COUNT(*) FROM airlineStats LIMIT 1"} http://localhost:8099/query/sql
DEBUG: Response body: {"resultTable":{"dataSchema":{"columnNames":["count(*)"],"columnDataTypes":["LONG"]},"rows":[[289]]},"numRowsResultSet":1,"partialResult":false,"exceptions":[],"numGroupsLimitReached":false,"numGroupsWarningLimitReached":false,"timeUsedMs":2,"requestId":"97156851000000116","clientRequestId":null,"brokerId":"Broker_localhost_8099","numDocsScanned":1,"totalDocs":289,"numEntriesScannedInFilter":0,"numEntriesScannedPostFilter":1,"numServersQueried":1,"numServersResponded":1,"numSegmentsQueried":1,"numSegmentsProcessed":1,"numSegmentsMatched":1,"numConsumingSegmentsQueried":0,"numConsumingSegmentsProcessed":0,"numConsumingSegmentsMatched":0,"minConsumingFreshnessTimeMs":0,"numSegmentsPrunedByBroker":0,"numSegmentsPrunedByServer":0,"numSegmentsPrunedInvalid":0,"numSegmentsPrunedByLimit":0,"numSegmentsPrunedByValue":0,"brokerReduceTimeMs":0,"offlineThreadCpuTimeNs":0,"realtimeThreadCpuTimeNs":0,"offlineSystemActivitiesCpuTimeNs":0,"realtimeSystemActivitiesCpuTimeNs":0,"offlineResponseSerializationCpuTimeNs":0,"realtimeResponseSerializationCpuTimeNs":0,"offlineTotalCpuTimeNs":0,"realtimeTotalCpuTimeNs":0,"explainPlanNumEmptyFilterSegments":0,"explainPlanNumMatchAllFilterSegments":0,"traceInfo":{},"tablesQueried":["airlineStats"],"offlineThreadMemAllocatedBytes":0,"realtimeThreadMemAllocatedBytes":0,"offlineResponseSerMemAllocatedBytes":0,"realtimeResponseSerMemAllocatedBytes":0,"offlineTotalMemAllocatedBytes":0,"realtimeTotalMemAllocatedBytes":0,"pools":[-1],"rlsFiltersApplied":false,"groupsTrimmed":false}
DEBUG: Response status: 200
✓ Admin sees all rows (no RLS, count=289) → 200 OK, count=289
DEBUG: curl -s -w \n%{http_code} -u userRLS:secretRLS -X POST -H Content-Type: application/json -d {"sql":"SELECT COUNT(*) FROM airlineStats LIMIT 1"} http://localhost:8099/query/sql
DEBUG: Response body: {"resultTable":{"dataSchema":{"columnNames":["count(*)"],"columnDataTypes":["LONG"]},"rows":[[45]]},"numRowsResultSet":1,"partialResult":false,"exceptions":[],"numGroupsLimitReached":false,"numGroupsWarningLimitReached":false,"timeUsedMs":3,"requestId":"97156851000000117","clientRequestId":null,"brokerId":"Broker_localhost_8099","numDocsScanned":45,"totalDocs":289,"numEntriesScannedInFilter":289,"numEntriesScannedPostFilter":0,"numServersQueried":1,"numServersResponded":1,"numSegmentsQueried":1,"numSegmentsProcessed":1,"numSegmentsMatched":1,"numConsumingSegmentsQueried":0,"numConsumingSegmentsProcessed":0,"numConsumingSegmentsMatched":0,"minConsumingFreshnessTimeMs":0,"numSegmentsPrunedByBroker":0,"numSegmentsPrunedByServer":0,"numSegmentsPrunedInvalid":0,"numSegmentsPrunedByLimit":0,"numSegmentsPrunedByValue":0,"brokerReduceTimeMs":0,"offlineThreadCpuTimeNs":0,"realtimeThreadCpuTimeNs":0,"offlineSystemActivitiesCpuTimeNs":0,"realtimeSystemActivitiesCpuTimeNs":0,"offlineResponseSerializationCpuTimeNs":0,"realtimeResponseSerializationCpuTimeNs":0,"offlineTotalCpuTimeNs":0,"realtimeTotalCpuTimeNs":0,"explainPlanNumEmptyFilterSegments":0,"explainPlanNumMatchAllFilterSegments":0,"traceInfo":{},"tablesQueried":["airlineStats"],"offlineThreadMemAllocatedBytes":0,"realtimeThreadMemAllocatedBytes":0,"offlineResponseSerMemAllocatedBytes":0,"realtimeResponseSerMemAllocatedBytes":0,"offlineTotalMemAllocatedBytes":0,"realtimeTotalMemAllocatedBytes":0,"pools":[-1],"rlsFiltersApplied":true,"groupsTrimmed":false}
DEBUG: Response status: 200
✓ UserRLS sees only TX rows (RLS applied, count=45) → 200 OK, count=45
DEBUG: curl -s -w \n%{http_code} -u userRLS:secretRLS -X POST -H Content-Type: application/json -d {"sql":"SELECT COUNT(*) FROM airlineStats WHERE OriginState='TX' LIMIT 1"} http://localhost:8099/query/sql
DEBUG: Response body: {"resultTable":{"dataSchema":{"columnNames":["count(*)"],"columnDataTypes":["LONG"]},"rows":[[45]]},"numRowsResultSet":1,"partialResult":false,"exceptions":[],"numGroupsLimitReached":false,"numGroupsWarningLimitReached":false,"timeUsedMs":3,"requestId":"97156851000000118","clientRequestId":null,"brokerId":"Broker_localhost_8099","numDocsScanned":45,"totalDocs":289,"numEntriesScannedInFilter":334,"numEntriesScannedPostFilter":0,"numServersQueried":1,"numServersResponded":1,"numSegmentsQueried":1,"numSegmentsProcessed":1,"numSegmentsMatched":1,"numConsumingSegmentsQueried":0,"numConsumingSegmentsProcessed":0,"numConsumingSegmentsMatched":0,"minConsumingFreshnessTimeMs":0,"numSegmentsPrunedByBroker":0,"numSegmentsPrunedByServer":0,"numSegmentsPrunedInvalid":0,"numSegmentsPrunedByLimit":0,"numSegmentsPrunedByValue":0,"brokerReduceTimeMs":0,"offlineThreadCpuTimeNs":0,"realtimeThreadCpuTimeNs":0,"offlineSystemActivitiesCpuTimeNs":0,"realtimeSystemActivitiesCpuTimeNs":0,"offlineResponseSerializationCpuTimeNs":0,"realtimeResponseSerializationCpuTimeNs":0,"offlineTotalCpuTimeNs":0,"realtimeTotalCpuTimeNs":0,"explainPlanNumEmptyFilterSegments":0,"explainPlanNumMatchAllFilterSegments":0,"traceInfo":{},"tablesQueried":["airlineStats"],"offlineThreadMemAllocatedBytes":0,"realtimeThreadMemAllocatedBytes":0,"offlineResponseSerMemAllocatedBytes":0,"realtimeResponseSerMemAllocatedBytes":0,"offlineTotalMemAllocatedBytes":0,"realtimeTotalMemAllocatedBytes":0,"pools":[-1],"rlsFiltersApplied":true,"groupsTrimmed":false}
DEBUG: Response status: 200
✓ UserRLS explicit TX query (count=45) → 200 OK, count=45
DEBUG: curl -s -w \n%{http_code} -u userRLS:secretRLS -X POST -H Content-Type: application/json -d {"sql":"SELECT COUNT(*) FROM airlineStats WHERE OriginState='CA' LIMIT 1"} http://localhost:8099/query/sql
DEBUG: Response body: {"resultTable":{"dataSchema":{"columnNames":["count(*)"],"columnDataTypes":["LONG"]},"rows":[[0]]},"numRowsResultSet":1,"partialResult":false,"exceptions":[],"numGroupsLimitReached":false,"numGroupsWarningLimitReached":false,"timeUsedMs":3,"requestId":"97156851000000119","clientRequestId":null,"brokerId":"Broker_localhost_8099","numDocsScanned":0,"totalDocs":289,"numEntriesScannedInFilter":313,"numEntriesScannedPostFilter":0,"numServersQueried":1,"numServersResponded":1,"numSegmentsQueried":1,"numSegmentsProcessed":1,"numSegmentsMatched":0,"numConsumingSegmentsQueried":0,"numConsumingSegmentsProcessed":0,"numConsumingSegmentsMatched":0,"minConsumingFreshnessTimeMs":0,"numSegmentsPrunedByBroker":0,"numSegmentsPrunedByServer":0,"numSegmentsPrunedInvalid":0,"numSegmentsPrunedByLimit":0,"numSegmentsPrunedByValue":0,"brokerReduceTimeMs":0,"offlineThreadCpuTimeNs":0,"realtimeThreadCpuTimeNs":0,"offlineSystemActivitiesCpuTimeNs":0,"realtimeSystemActivitiesCpuTimeNs":0,"offlineResponseSerializationCpuTimeNs":0,"realtimeResponseSerializationCpuTimeNs":0,"offlineTotalCpuTimeNs":0,"realtimeTotalCpuTimeNs":0,"explainPlanNumEmptyFilterSegments":0,"explainPlanNumMatchAllFilterSegments":0,"traceInfo":{},"tablesQueried":["airlineStats"],"offlineThreadMemAllocatedBytes":0,"realtimeThreadMemAllocatedBytes":0,"offlineResponseSerMemAllocatedBytes":0,"realtimeResponseSerMemAllocatedBytes":0,"offlineTotalMemAllocatedBytes":0,"realtimeTotalMemAllocatedBytes":0,"pools":[-1],"rlsFiltersApplied":true,"groupsTrimmed":false}
DEBUG: Response status: 200
✓ UserRLS CA query returns 0 (RLS blocks CA, count=0) → 200 OK, count=0
Testing: RLS Simple filter (AirlineID='19805'):
DEBUG: curl -s -w \n%{http_code} -u userRLS_Simple:secretSimple -X POST -H Content-Type: application/json -d {"sql":"SELECT COUNT(*) FROM airlineStats LIMIT 1"} http://localhost:8099/query/sql
DEBUG: Response body: {"resultTable":{"dataSchema":{"columnNames":["count(*)"],"columnDataTypes":["LONG"]},"rows":[[40]]},"numRowsResultSet":1,"partialResult":false,"exceptions":[],"numGroupsLimitReached":false,"numGroupsWarningLimitReached":false,"timeUsedMs":3,"requestId":"97156851000000120","clientRequestId":null,"brokerId":"Broker_localhost_8099","numDocsScanned":1,"totalDocs":289,"numEntriesScannedInFilter":0,"numEntriesScannedPostFilter":1,"numServersQueried":1,"numServersResponded":1,"numSegmentsQueried":1,"numSegmentsProcessed":1,"numSegmentsMatched":1,"numConsumingSegmentsQueried":0,"numConsumingSegmentsProcessed":0,"numConsumingSegmentsMatched":0,"minConsumingFreshnessTimeMs":0,"numSegmentsPrunedByBroker":0,"numSegmentsPrunedByServer":0,"numSegmentsPrunedInvalid":0,"numSegmentsPrunedByLimit":0,"numSegmentsPrunedByValue":0,"brokerReduceTimeMs":0,"offlineThreadCpuTimeNs":0,"realtimeThreadCpuTimeNs":0,"offlineSystemActivitiesCpuTimeNs":0,"realtimeSystemActivitiesCpuTimeNs":0,"offlineResponseSerializationCpuTimeNs":0,"realtimeResponseSerializationCpuTimeNs":0,"offlineTotalCpuTimeNs":0,"realtimeTotalCpuTimeNs":0,"explainPlanNumEmptyFilterSegments":0,"explainPlanNumMatchAllFilterSegments":0,"traceInfo":{},"tablesQueried":["airlineStats"],"offlineThreadMemAllocatedBytes":0,"realtimeThreadMemAllocatedBytes":0,"offlineResponseSerMemAllocatedBytes":0,"realtimeResponseSerMemAllocatedBytes":0,"offlineTotalMemAllocatedBytes":0,"realtimeTotalMemAllocatedBytes":0,"pools":[-1],"rlsFiltersApplied":true,"groupsTrimmed":false}
DEBUG: Response status: 200
✓ UserRLS_Simple sees filtered rows (RLS applied, count=40) → 200 OK, count=40
Testing: RLS AND filter (OriginState='TX' AND AirlineID='19805'):
DEBUG: curl -s -w \n%{http_code} -u userRLS_And:secretAnd -X POST -H Content-Type: application/json -d {"sql":"SELECT COUNT(*) FROM airlineStats LIMIT 1"} http://localhost:8099/query/sql
DEBUG: Response body: {"resultTable":{"dataSchema":{"columnNames":["count(*)"],"columnDataTypes":["LONG"]},"rows":[[14]]},"numRowsResultSet":1,"partialResult":false,"exceptions":[],"numGroupsLimitReached":false,"numGroupsWarningLimitReached":false,"timeUsedMs":3,"requestId":"97156851000000121","clientRequestId":null,"brokerId":"Broker_localhost_8099","numDocsScanned":14,"totalDocs":289,"numEntriesScannedInFilter":304,"numEntriesScannedPostFilter":0,"numServersQueried":1,"numServersResponded":1,"numSegmentsQueried":1,"numSegmentsProcessed":1,"numSegmentsMatched":1,"numConsumingSegmentsQueried":0,"numConsumingSegmentsProcessed":0,"numConsumingSegmentsMatched":0,"minConsumingFreshnessTimeMs":0,"numSegmentsPrunedByBroker":0,"numSegmentsPrunedByServer":0,"numSegmentsPrunedInvalid":0,"numSegmentsPrunedByLimit":0,"numSegmentsPrunedByValue":0,"brokerReduceTimeMs":0,"offlineThreadCpuTimeNs":0,"realtimeThreadCpuTimeNs":0,"offlineSystemActivitiesCpuTimeNs":0,"realtimeSystemActivitiesCpuTimeNs":0,"offlineResponseSerializationCpuTimeNs":0,"realtimeResponseSerializationCpuTimeNs":0,"offlineTotalCpuTimeNs":0,"realtimeTotalCpuTimeNs":0,"explainPlanNumEmptyFilterSegments":0,"explainPlanNumMatchAllFilterSegments":0,"traceInfo":{},"tablesQueried":["airlineStats"],"offlineThreadMemAllocatedBytes":0,"realtimeThreadMemAllocatedBytes":0,"offlineResponseSerMemAllocatedBytes":0,"realtimeResponseSerMemAllocatedBytes":0,"offlineTotalMemAllocatedBytes":0,"realtimeTotalMemAllocatedBytes":0,"pools":[-1],"rlsFiltersApplied":true,"groupsTrimmed":false}
DEBUG: Response status: 200
✓ UserRLS_And sees filtered rows (RLS applied, count=14) → 200 OK, count=14
Testing: RLS OR filter (OriginState='TX' OR OriginState='CA'):
DEBUG: curl -s -w \n%{http_code} -u userRLS_Or:secretOr -X POST -H Content-Type: application/json -d {"sql":"SELECT COUNT(*) FROM airlineStats LIMIT 1"} http://localhost:8099/query/sql
DEBUG: Response body: {"resultTable":{"dataSchema":{"columnNames":["count(*)"],"columnDataTypes":["LONG"]},"rows":[[84]]},"numRowsResultSet":1,"partialResult":false,"exceptions":[],"numGroupsLimitReached":false,"numGroupsWarningLimitReached":false,"timeUsedMs":6,"requestId":"97156851000000122","clientRequestId":null,"brokerId":"Broker_localhost_8099","numDocsScanned":84,"totalDocs":289,"numEntriesScannedInFilter":578,"numEntriesScannedPostFilter":0,"numServersQueried":1,"numServersResponded":1,"numSegmentsQueried":1,"numSegmentsProcessed":1,"numSegmentsMatched":1,"numConsumingSegmentsQueried":0,"numConsumingSegmentsProcessed":0,"numConsumingSegmentsMatched":0,"minConsumingFreshnessTimeMs":0,"numSegmentsPrunedByBroker":0,"numSegmentsPrunedByServer":0,"numSegmentsPrunedInvalid":0,"numSegmentsPrunedByLimit":0,"numSegmentsPrunedByValue":0,"brokerReduceTimeMs":0,"offlineThreadCpuTimeNs":0,"realtimeThreadCpuTimeNs":0,"offlineSystemActivitiesCpuTimeNs":0,"realtimeSystemActivitiesCpuTimeNs":0,"offlineResponseSerializationCpuTimeNs":0,"realtimeResponseSerializationCpuTimeNs":0,"offlineTotalCpuTimeNs":0,"realtimeTotalCpuTimeNs":0,"explainPlanNumEmptyFilterSegments":0,"explainPlanNumMatchAllFilterSegments":0,"traceInfo":{},"tablesQueried":["airlineStats"],"offlineThreadMemAllocatedBytes":0,"realtimeThreadMemAllocatedBytes":0,"offlineResponseSerMemAllocatedBytes":0,"realtimeResponseSerMemAllocatedBytes":0,"offlineTotalMemAllocatedBytes":0,"realtimeTotalMemAllocatedBytes":0,"pools":[-1],"rlsFiltersApplied":true,"groupsTrimmed":false}
DEBUG: Response status: 200
✓ UserRLS_Or sees filtered rows (RLS applied, count=84) → 200 OK, count=84
PS: This was done with a bash script which I plan to contribute later with another PR
Codecov Report
:x: Patch coverage is 64.70588% with 12 lines in your changes missing coverage. Please review.
:white_check_mark: Project coverage is 63.30%. Comparing base (4e3599c) to head (6d96961).
:warning: Report is 37 commits behind head on master.
Additional details and impacted files
@@ Coverage Diff @@
## master #17295 +/- ##
============================================
+ Coverage 63.23% 63.30% +0.07%
- Complexity 1433 1434 +1
============================================
Files 3133 3133
Lines 186131 186180 +49
Branches 28408 28415 +7
============================================
+ Hits 117704 117866 +162
+ Misses 59363 59262 -101
+ Partials 9064 9052 -12
| Flag | Coverage Δ | |
|---|---|---|
| custom-integration1 | 100.00% <ø> (ø) |
|
| integration | 100.00% <ø> (ø) |
|
| integration1 | 100.00% <ø> (ø) |
|
| integration2 | 0.00% <ø> (ø) |
|
| java-11 | 63.25% <64.70%> (+0.03%) |
:arrow_up: |
| java-21 | 63.26% <64.70%> (+7.65%) |
:arrow_up: |
| temurin | 63.30% <64.70%> (+0.07%) |
:arrow_up: |
| unittests | 63.30% <64.70%> (+0.07%) |
:arrow_up: |
| unittests1 | 55.73% <91.66%> (+0.07%) |
:arrow_up: |
| unittests2 | 33.92% <50.00%> (+0.05%) |
:arrow_up: |
Flags with carried forward coverage won't be shown. Click here to find out more.
:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.
:rocket: New features to boost your workflow:
- :package: JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
Please review @Jackie-Jiang @vrajat @9aman @yashmayya
Ok so the test failure is due to a test added in this pr. i reverted the unrelated user config change in 2nd commit, but forgot to remove the relevant newly added test and hence the failure. will remove this test in next commit and add as part of user config validation enhancement pr later.
Failure is not related.
Hi team could you please provide some feedback, I need to start re-work on https://github.com/apache/pinot/pull/15861 after this PR.
@9aman Can you help review this?