fix(multi-tenancy): fix organization access validation across tenants (#300) - #8002
Gaetan Santucci (GaetanSantucci) wants to merge 4 commits into
Conversation
|
📖 Documentation check — ✅ Passed 3 functional file(s), 0 doc file(s) changed. No documentation gaps detected. |
There was a problem hiding this comment.
Pull request overview
Improves organization tenant isolation by threading TxCtx through organization access paths and adding extensive HTTP isolation tests.
Changes:
- Adds tenant-aware organization CRUD, search, options, and bulk operations.
- Restricts related tags, teams, and injects by tenant.
- Adds multi-tenant integration coverage.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| openaev-model/src/main/java/io/openaev/database/repository/OrganizationRepository.java | Updated as part of this pull request. |
| openaev-api/src/test/java/io/openaev/rest/organization/OrganizationHttpIsolationTest.java | Updated as part of this pull request. |
| openaev-api/src/main/java/io/openaev/service/organization/OrganizationService.java | Updated as part of this pull request. |
| openaev-api/src/main/java/io/openaev/rest/organization/OrganizationApi.java | Updated as part of this pull request. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| private Specification<Organization> inScope(TxCtx ctx) { | ||
| Set<String> tenantIds = TxCtxScopeUtils.tenantIdsFromHTTPCtx(ctx); | ||
| return (root, query, cb) -> | ||
| tenantIds.isEmpty() ? cb.disjunction() : root.get("tenant").get("id").in(tenantIds); |
| return injectSearchService.getPageOfInjectResultsForOrganization( | ||
| organizationId, searchPaginationInput); |
| @Test | ||
| @DisplayName("Tenant-prefixed collections and options contain only owned organizations") | ||
| void given_tenantPath_should_filterAllCollections() throws Exception { | ||
| // Arrange | ||
| tenantHelper.attachCurrentUserToTenant(tenantA.getId()); |
| @TestInstance(TestInstance.Lifecycle.PER_CLASS) | ||
| @TestPropertySource(properties = "openaev.tenant.active-tables=tags") | ||
| @DisplayName("Organization HTTP tenant isolation") | ||
| class OrganizationHttpIsolationTest extends IntegrationTest { |
Codecov Report❌ Patch coverage is
❌ Your project check has failed because the head coverage (11.57%) is below the target coverage (80.00%). You can increase the head coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## main #8002 +/- ##
============================================
- Coverage 46.82% 46.30% -0.52%
- Complexity 12406 12482 +76
============================================
Files 3039 3020 -19
Lines 101275 101141 -134
Branches 16390 16281 -109
============================================
- Hits 47420 46833 -587
- Misses 50039 50567 +528
+ Partials 3816 3741 -75
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
8577344 to
c231ab6
Compare
| private final EntityManager entityManager; | ||
|
|
||
| /** Lists organizations in the legacy tenant scope, bounded by the caller's authorized scope. */ | ||
| @Transactional(readOnly = true) |
There was a problem hiding this comment.
Seeing as these functions are all called from the API layer where all methods are all already under a transactional, so it's not necessary to add it here. (same for following methods)
| private final TagService tagService; | ||
| private final EntityManager entityManager; | ||
|
|
||
| /** Lists organizations in the legacy tenant scope, bounded by the caller's authorized scope. */ |
There was a problem hiding this comment.
Javadoc is incorrect (missing parameters, etc.)
| /** Creates an organization in the tenant explicitly resolved by the API write-scope resolver. */ | ||
| @Transactional(rollbackFor = Exception.class) | ||
| public Organization createOrganization( | ||
| TxCtx ctx, OrganizationCreateInput input, String tenantId) { |
There was a problem hiding this comment.
ctx isn't useful here as the check was already done on the API layer
| tenantIds.isEmpty() ? cb.disjunction() : root.get("tenant").get("id").in(tenantIds); | ||
| } | ||
|
|
||
| private Set<Tag> resolveTags(List<String> tagIds, String tenantId) { |
There was a problem hiding this comment.
This function should not exist here. Instead, it whould call the tagSet function from TagService. But tagSet doesn't seem to support the tenant filter. So a new function doing this should be created in the tagService class.
Also, this class still fetches all tags requested by their ID and then filter them in memory instead of filtering directly in the select in the database which would be more efficient.
| private Organization findAccessibleById(TxCtx ctx, String organizationId) { | ||
| Specification<Organization> specification = | ||
| inTenantScope(ctx).and((root, query, cb) -> cb.equal(root.get("id"), organizationId)); | ||
| Session session = entityManager.unwrap(Session.class); |
There was a problem hiding this comment.
Not sure why we need to disable the filtering here before making our query
Proposed changes
Testing Instructions
Related issues