Skip to content

Add Row-Level Security (RLS) Support for ZooKeeper-Based Authentication#18051

Open
NihalJain wants to merge 1 commit intoapache:masterfrom
NihalJain:rls_support_zkauth_rework
Open

Add Row-Level Security (RLS) Support for ZooKeeper-Based Authentication#18051
NihalJain wants to merge 1 commit intoapache:masterfrom
NihalJain:rls_support_zkauth_rework

Conversation

@NihalJain
Copy link
Copy Markdown
Contributor

Fixes #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 AccessControlUserConfigUtils to serialize/deserialize RLS filters to/from ZNRecord
  • Modified BasicAuthUtils to extract RLS filters from UserConfig and pass to ZkBasicAuthPrincipal
  • Implemented getRowColFilters() in ZkBasicAuthAccessControlFactory to return RLS filters
  • Updated UserConfigBuilder to support RLS filter configuration
  • Added comprehensive unit tests for UserConfig, UserConfigBuilder, and AccessControlUserConfigUtils
  • Added integration tests for ZkAuth RLS scenarios by extracting base class from existing BasicAuth tests

Manual Testing:

  • Tested REST API create/update/get operations with RLS filters
  • Tested queries work as expected with rls for the ZK-based authentication

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'"]
  }
}

Fixes apache#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 `AccessControlUserConfigUtils` to serialize/deserialize RLS filters to/from `ZNRecord`
- Modified `BasicAuthUtils` to extract RLS filters from `UserConfig` and pass to `ZkBasicAuthPrincipal`
- Implemented `getRowColFilters()` in `ZkBasicAuthAccessControlFactory` to return RLS filters
- Updated `UserConfigBuilder` to support RLS filter configuration
- Added comprehensive unit tests for `UserConfig`, `UserConfigBuilder`, and `AccessControlUserConfigUtils`
- Added integration tests for ZkAuth RLS scenarios by extracting base class from existing `BasicAuth` tests

Manual Testing:
- Tested REST API create/update/get operations with RLS filters
- Tested queries work as expected with rls for the ZK-based authentication

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'"]
  }
}
```
@NihalJain
Copy link
Copy Markdown
Contributor Author

NihalJain commented Mar 31, 2026

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"
    }
    

Summary of the RLS tests performed with both Basic Auth and ZooKeeper-based Auth:

Test Configuration

User RLS Filter Expected Rows
admin None (full access) All rows (full dataset)
userRLS OriginState='TX' 45 rows
userRLS_Simple AirlineID='19805' 40 rows
userRLS_And OriginState='TX' AND AirlineID='19805' 14 rows
userRLS_Or OriginState='TX' OR OriginState='CA' 84 rows

Tests Executed

  1. Basic RLS Filter Application (4 tests)

    • ✅ Admin sees all rows (no RLS applied)
    • userRLS sees only TX rows (45 rows) - RLS filter applied
    • userRLS explicit TX query returns correct count (45 rows)
    • userRLS CA query returns 0 rows - RLS blocks access to non-TX states
  2. Simple Filter Test (1 test)

    • userRLS_Simple sees only rows where AirlineID='19805' (40 rows)
  3. AND Filter Test (1 test)

    • userRLS_And sees only rows matching both conditions: OriginState='TX' AND AirlineID='19805' (14 rows)
  4. OR Filter Test (1 test)

    • userRLS_Or sees rows matching either condition: OriginState='TX' OR OriginState='CA' (84 rows)

Key Validations

  1. RLS enforcement - Users with RLS filters only see permitted rows
  2. Filter combinations - AND/OR logic works correctly in RLS expressions
  3. Blocking behavior - Users cannot access data outside their permitted scope

Auth Modes Tested

  • ✅ Basic Auth
  • ✅ ZooKeeper-based Auth (RLS stored in ZK)

PS: This was done with a bash script which I plan to contribute later with another PR

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Mar 31, 2026

Codecov Report

❌ Patch coverage is 71.79487% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 63.35%. Comparing base (b07deff) to head (d7d21e5).
⚠️ Report is 5 commits behind head on master.

Files with missing lines Patch % Lines
...broker/broker/ZkBasicAuthAccessControlFactory.java 0.00% 10 Missing ⚠️
...mon/utils/config/AccessControlUserConfigUtils.java 95.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #18051      +/-   ##
============================================
+ Coverage     63.32%   63.35%   +0.02%     
+ Complexity     1543     1537       -6     
============================================
  Files          3200     3200              
  Lines        194209   194253      +44     
  Branches      29932    29937       +5     
============================================
+ Hits         122986   123072      +86     
+ Misses        61557    61517      -40     
+ Partials       9666     9664       -2     
Flag Coverage Δ
custom-integration1 ?
integration ?
integration1 ?
integration2 ?
java-11 55.55% <96.55%> (-7.75%) ⬇️
java-21 63.33% <71.79%> (+0.03%) ⬆️
temurin 63.35% <71.79%> (+0.02%) ⬆️
unittests 63.35% <71.79%> (+0.03%) ⬆️
unittests1 55.58% <96.55%> (+0.07%) ⬆️
unittests2 34.25% <56.41%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@NihalJain
Copy link
Copy Markdown
Contributor Author

Hi team could you please provide some feedback

CC @Jackie-Jiang @vrajat @9aman @yashmayya @xiangfu0

}

@Override
public TableRowColAccessResult getRowColFilters(RequesterIdentity requesterIdentity, String table) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just for ease of reviewers: method is copy pasted from basic auth, will be refactored as common code with refactor task i will follow up with.

.stream().map(x -> x.toString())
.collect(Collectors.toSet());
//todo: Handle RLS filters properly
// Extract RLS filters from UserConfig
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just for ease of reviewers: here we simply pass rls for zk principal so that it can be applied during auth check

List<String> excludeTableList = znRecord.getListField(UserConfig.EXCLUDE_TABLES_KEY);

List<String> permissionListFromZNRecord = znRecord.getListField(UserConfig.PERMISSIONS_KEY);
List<String> permissionListFromZNRecord = znRecord.getListField(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just for ease of reviewers: logic to persist rls in zk

@JsonProperty(value = EXCLUDE_TABLES_KEY) @Nullable List<String> excludeTableList,
@JsonProperty(value = PERMISSIONS_KEY) @Nullable List<AccessType> permissionList) {
@JsonProperty(value = PERMISSIONS_KEY) @Nullable List<AccessType> permissionList,
@JsonProperty(value = RLS_FILTERS_KEY) @Nullable Map<String, List<String>> rlsFilters
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just for ease of reviewers: changes for rest api

@NihalJain
Copy link
Copy Markdown
Contributor Author

Hello everyone, just bumping this PR. Please let me know if you need me to make any changes or add more context.

@Jackie-Jiang Jackie-Jiang added security Related to security hardening feature New functionality labels Apr 15, 2026
@Jackie-Jiang
Copy link
Copy Markdown
Contributor

@9aman Could you help take a look at this PR?

if (rlsFiltersJson != null && !rlsFiltersJson.isEmpty()) {
try {
rlsFilters = JsonUtils.stringToObject(
rlsFiltersJson, new TypeReference<Map<String, List<String>>>() {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why nothing inside

public TableRowColAccessResult getRowColFilters(RequesterIdentity requesterIdentity, String table) {
Optional<ZkBasicAuthPrincipal> principalOpt = getPrincipalAuth(requesterIdentity);

Preconditions.checkState(principalOpt.isPresent(), "Principal is not authorized");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AccessControl.java:130 explicitly promises the method accepts table names with or without type.
Let's use

principal.hasTable(TableNameBuilder.extractRawTableName(table))

UserConfig userConfig = AccessControlUserConfigUtils.fromZNRecord(znRecord);

Assert.assertNotNull(userConfig, "UserConfig should not be null");
Assert.assertNull(userConfig.getRlsFilters(), "RLS filters should be null due to malformed JSON");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NihalJain Is this a desired behavior. A malformed json can allow the user to access everything within a table.

BasicAuthPrincipalUtils.extractBasicAuthPrincipals converts that null to Collections.emptyMap() and hence no filter is applied.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New functionality security Related to security hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Row-Level Security (RLS) Support for ZooKeeper-Based Authentication

5 participants