Skip to content

Multi tenancy: fix for users without matching roles seeing all tenants data #950

Open
janblok wants to merge 5 commits into
9tigerio:masterfrom
janblok:feature/roleFilterFixForUsersWithoutRole
Open

Multi tenancy: fix for users without matching roles seeing all tenants data #950
janblok wants to merge 5 commits into
9tigerio:masterfrom
janblok:feature/roleFilterFixForUsersWithoutRole

Conversation

@janblok

@janblok janblok commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

When using column based multi tenancy and a user does not have a role matching the roleDataFilters they can see all tenants data in a table having column based multi tenancy. This PR fixes this scenario by allowing them to see only the rows where the tenant id column value is not defined (is null) in case these are present.

Copilot AI left a comment

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.

Pull request overview

This PR addresses a multi-tenancy data-leak in column-based multi-tenancy: when a user has no matching roleDataFilters for a table, the request should no longer return all tenants’ rows, and should instead restrict results to “shared/unscoped” rows (intended as tenant-column IS NULL). The implementation refactors how role data filters are passed from the auth layer into the API layer by attaching both the full filter list and the user’s roles, then deriving per-table filters inside MultiTenancy.

Changes:

  • Refactor role-data-filter propagation to pass (allRoleDataFilters, userRoles) via a request attribute and compute effective filters in MultiTenancy.
  • Add fallback behavior intended to apply an IS NULL tenant filter when a table has a tenant column but the user has no matching role filter.
  • Update REST controllers/DTOs to accept the new Pair<List<RoleDataFilter>, String[]> shape and add an integration test covering the “no matching role filter” scenario.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
pom.xml Adds compiler plugin configuration for Lombok annotation processing.
db2rest-auth/src/main/java/com/homihq/db2rest/auth/provider/AbstractAuthProvider.java Simplifies role data filter retrieval to return all filters from the lookup.
db2rest-auth/src/main/java/com/homihq/db2rest/auth/datalookup/AuthDataLookup.java Changes API to return all role data filters (no longer per-role).
db2rest-auth/src/main/java/com/homihq/db2rest/auth/datalookup/NoAuthdataLookup.java Updates lookup implementation to new no-arg role filter method.
db2rest-auth/src/main/java/com/homihq/db2rest/auth/datalookup/FileAuthDataLookup.java Updates file-based lookup to return all role data filters.
db2rest-auth/src/main/java/com/homihq/db2rest/auth/datalookup/ApiAuthDataLookup.java Updates API-based lookup to return all role data filters.
db2rest-auth/src/main/java/com/homihq/db2rest/auth/data/AuthData.java Ensures roleDataFilters() accessor never returns null.
db2rest-auth/src/main/java/com/homihq/db2rest/auth/AuthFilter.java Stores (roleDataFilters, userRoles) pair as a request attribute after authz.
db2rest-api/rest-common/src/main/java/com/homihq/db2rest/dtos/BulkContext.java Updates bulk context to carry the new Pair-based role filter payload.
db2rest-api/rest-common/src/main/java/com/homihq/db2rest/config/MultiTenancy.java Implements per-table role filter derivation and fallback IS NULL filtering.
db2rest-api/api-rest/src/main/java/com/homihq/db2rest/rest/read/ReadController.java Updates request attribute typing and applies multi-tenancy filtering consistently.
db2rest-api/api-rest/src/main/java/com/homihq/db2rest/rest/read/FindOneController.java Updates request attribute typing to the new Pair payload.
db2rest-api/api-rest/src/main/java/com/homihq/db2rest/rest/read/ExistsQueryController.java Updates request attribute typing to the new Pair payload.
db2rest-api/api-rest/src/main/java/com/homihq/db2rest/rest/read/CountQueryController.java Updates request attribute typing to the new Pair payload.
db2rest-api/api-rest/src/main/java/com/homihq/db2rest/rest/update/UpdateController.java Updates request attribute typing and continues applying tenant column enforcement and filtering.
db2rest-api/api-rest/src/main/java/com/homihq/db2rest/rest/delete/DeleteRestApi.java Updates request attribute typing to the new Pair payload.
db2rest-api/api-rest/src/main/java/com/homihq/db2rest/rest/delete/DeleteController.java Updates method signature to accept the new Pair payload.
db2rest-api/api-rest/src/main/java/com/homihq/db2rest/rest/create/CreateRestApi.java Updates request attribute typing to the new Pair payload.
db2rest-api/api-rest/src/main/java/com/homihq/db2rest/rest/create/CreateController.java Updates method signature to accept the new Pair payload.
db2rest-api/api-rest/src/main/java/com/homihq/db2rest/rest/create/BulkCreateRestApi.java Updates request attribute typing to the new Pair payload.
db2rest-api/api-rest/src/main/java/com/homihq/db2rest/rest/create/BulkCreateController.java Updates bulk context creation to pass the new Pair payload.
db2rest-api/api-rest/src/test/java/com/homihq/db2rest/rest/pg/PgMultiTenancyTest.java Adds coverage for querying as a user without matching role data filters.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 24 to 35
if (dbId.equalsIgnoreCase(roleDataFilter.dbId()) && table.equalsIgnoreCase(roleDataFilter.table())) {
if (!filterBuilder.isEmpty()) filterBuilder.append(";");
if (!filterBuilder.isEmpty())
filterBuilder.append(";");
filterBuilder.append(roleDataFilter.column());
filterBuilder.append("==");
if (roleDataFilter.value() == null) {
filterBuilder.append("=isnull=");
}
else {
filterBuilder.append("==");
}
filterBuilder.append(roleDataFilter.value());
}
Comment on lines +69 to 75
public static void addTenantColumns(Map<String, Object> data, String dbId, String table,
Pair<List<RoleDataFilter>, String[]> roleBasedDataFilters) {
if (roleBasedDataFilters != null) {
for (RoleDataFilter roleDataFilter : roleBasedDataFilters) {
if (dbId.equalsIgnoreCase(roleDataFilter.dbId()) && table.equalsIgnoreCase(roleDataFilter.table())) {
data.put(roleDataFilter.column(), roleDataFilter.value());
}
for (RoleDataFilter roleDataFilter : getUserRoleBasedDataFilters(dbId, table,
roleBasedDataFilters.getLeft(), roleBasedDataFilters.getRight())) {
data.put(roleDataFilter.column(), roleDataFilter.value());
}
Comment thread pom.xml
Comment on lines +173 to +179
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
Comment on lines +51 to +59
List<RoleDataFilter> retval = new ArrayList<>();
for (String role : userRoles) {
retval.addAll(roleDataFilters.stream().filter(rd -> role.equalsIgnoreCase(rd.role())).toList());
}
if (retval.isEmpty() && filterTenantColumn != null) {
// when a tenant column is defined for the table, but the user has no roles, we add a filter to only see null value rows
retval.add(new RoleDataFilter("see_null_values_only", dbId, table, filterTenantColumn, null));
}
return retval;
@RequestParam(name = "sort", required = false, defaultValue = "") List<String> sorts,
@RequestParam(required = false, defaultValue = "-1") int limit,
@RequestParam(required = false, defaultValue = "-1") long offset,
private final ReadService readService;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@janblok This feels shortened in naming especially given the methods. Ideally this would be renamed something like readRoleAttributes ?

@copilot what is this really returning and what do you suggest as a better name and JavaDoc?

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.

3 participants