[DT-4061] Centralize study read access in DatasetService - #3049
[DT-4061] Centralize study read access in DatasetService#3049otchet-broad wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
The updated tests introduce a disallowed lenient Mockito stub and add new authorization tests that currently rely on mocked AuthUser email state without stubbing getEmail(), reducing test correctness.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Centralizes study read-visibility enforcement by moving the gating logic out of StudyResource into DatasetService#verifyStudyVisibilityAccess, so downstream endpoints can reuse a single, consistent rule and avoid 500s caused by null unboxing of public_visibility. Also hardens authorization against users whose user_role query returns a null role list, ensuring @RolesAllowed checks deny rather than error.
Changes:
- Added
DatasetService#verifyStudyVisibilityAccess(Study, User)and updatedStudyResourceto delegate to it. - Added null guards in
AuthorizationHelperfor missing users / null role lists, with new tests. - Updated tests and internal AI docs to reflect the new access gate and Guice provider patterns.
File summaries
| File | Description |
|---|---|
| src/main/java/org/broadinstitute/consent/http/service/DatasetService.java | Introduces the centralized study visibility read gate. |
| src/main/java/org/broadinstitute/consent/http/resources/StudyResource.java | Switches resource-level visibility check to call the shared service gate. |
| src/main/java/org/broadinstitute/consent/http/authentication/AuthorizationHelper.java | Prevents NPEs when the user or role list is null during authorization. |
| src/test/java/org/broadinstitute/consent/http/service/DatasetServiceTest.java | Adds unit tests covering public/private/null study visibility behavior. |
| src/test/java/org/broadinstitute/consent/http/resources/StudyResourceTest.java | Updates resource tests to account for the new service-level gate. |
| src/test/java/org/broadinstitute/consent/http/authentication/AuthorizationHelperTest.java | Adds test coverage for null/empty roles and null user authorization cases. |
| docs/ai/CLAUDE.md | Corrects/clarifies ConsentModule singleton/provider guidance. |
Review details
Suppressed comments (2)
src/test/java/org/broadinstitute/consent/http/resources/StudyResourceTest.java:80
- The mock implementation of verifyStudyVisibilityAccess doesn't handle a null Study argument, so a null would cause a NullPointerException via study.getPublicVisibility(). The real DatasetService#verifyStudyVisibilityAccess throws NotFoundException on null; the test stub should mirror that to avoid accidental 500s or brittle tests.
invocation -> {
Study study = invocation.getArgument(0);
User requestingUser = invocation.getArgument(1);
if (!datasetService.isCreatorCustodianOrAdmin(requestingUser, study)
&& !Boolean.TRUE.equals(study.getPublicVisibility())) {
throw new NotFoundException("Study not found");
src/test/java/org/broadinstitute/consent/http/authentication/AuthorizationHelperTest.java:136
- Same issue here: unauthorizedUser/unauthorizedDuosUser are Mockito mocks, so setEmail(...) (used in other tests) won't affect getEmail() unless it is explicitly stubbed. Without stubbing, this test may not be validating the intended behavior.
User user = new User();
user.setEmail("email");
when(userService.findUserByEmail(unauthorizedUser.getEmail())).thenReturn(user);
- Files reviewed: 7/7 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| User user = new User(); | ||
| user.setEmail("email"); | ||
| assertNull(user.getRoles(), "a user with no roles must have a null role list for this test"); | ||
| when(userService.findUserByEmail(unauthorizedUser.getEmail())).thenReturn(user); | ||
|
|
| // If approved role or publicly visible, the user can see the study, otherwise throw | ||
| if (!isCreatorCustodianOrAdmin(user, study) | ||
| && !Boolean.TRUE.equals(study.getPublicVisibility())) { |
| // The read-access gate now lives in DatasetService#verifyStudyVisibilityAccess (shared with | ||
| // the study asset, comment, and metrics endpoints). These tests exercise the resource, so the | ||
| // mock replays the real rule against whatever isCreatorCustodianOrAdmin each test stubs. | ||
| // DatasetServiceTest covers the rule itself. | ||
| lenient() | ||
| .when(datasetService.verifyStudyVisibilityAccess(any(), any())) |
1f57fc7 to
0233a80
Compare
|
kevinmarete
left a comment
There was a problem hiding this comment.
verifyStudyVisibilityAccess treats null visibility as private, but canReadStudy still treats every value except false as public. A null-visibility study can therefore return 404 through the new study gate while remaining readable through dataset routes. Please define this rule once, ideally by changing canReadStudy to require Boolean.TRUE.equals(...) and delegating the new gate to it. Please also cover the dataset path with a null-visibility test.
Extracts the study read-visibility rule out of StudyResource into DatasetService#verifyStudyVisibilityAccess so the study asset, comment, and metrics endpoints added later in this stack share one gate. The rule now has a single definition, in canReadStudy, of which the new gate is just the throwing form. canReadStudy previously read a null public_visibility as public, while the dataset study summaries already required Boolean.TRUE - so a study with an unset flag was hidden on one route and readable on another. It now reads as "not published" everywhere: readable by its creator, its custodians and admins, and by anyone else only once public_visibility is TRUE. That is a behavior change, not just a refactor, and it is worth a look before merging. A study whose public_visibility is NULL is no longer readable by an arbitrary caller through the dataset routes (findDatasetByIdForRead, findStudyByIdForRead, findMinimalDatasetByIdentifier) or the study routes. The column is nullable and registration does write it, so this should only affect rows that predate that or were written directly, but the count is worth checking against the database. Two existing tests asserted the old reading and now assert the new one; the dataset route's null-visibility case is covered in both directions. It also fixes a 500: StudyResource unboxed public_visibility before checking the caller's role, so a null threw rather than denying. Guards AuthorizationHelper against a user with no user_role rows, whose role list is null rather than empty. That NPE surfaced as a 500 on every @RolesAllowed endpoint instead of a plain denial. Also corrects the ConsentModule singleton guidance in docs/ai/CLAUDE.md to describe the provider-parameter pattern the module actually uses. No migration. The PATCH ownership gate that shares this refactor lives on otchet-dt-4061-study-patch-ownership, so it can be reviewed separately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
0233a80 to
68ba657
Compare



Base:
develop· 7 files, +238/-16 · Migration: no · UI impact: noneExtracts the study read-visibility rule out of
StudyResourceintoDatasetService#verifyStudyVisibilityAccess, so the asset, comment, andmetrics endpoints later in the stack all share one gate rather than each
re-deriving it. A study whose
public_visibilityis NULL now reads as "notpublic" — 404 for an unapproved caller — instead of throwing on the unboxing
and returning 500.
Also guards
AuthorizationHelperagainst a user with nouser_rolerows,whose role list is null rather than empty. That NPE surfaced as a 500 on
every
@RolesAllowedendpoint instead of a plain denial. Corrects theConsentModulesingleton guidance indocs/ai/CLAUDE.mdto describe theprovider-parameter pattern the module actually uses.
No behavior change for any caller who could already read a study, and no
migration. The PATCH ownership gate that originally shared this refactor is
now branch 1b.
Depends on: nothing. Every other branch depends on this one.
Where this sits in the DT-4061 stack
otchet-dt-3990-study-ratings-pi-detailswas too large to review meaningfully(95 files, +8168), so it was split into eight PRs. This PR targets
develop,not
develop, so its diff shows only its own work.otchet-dt-4061-study-visibility-authz← this PRdevelopotchet-dt-4061-study-patch-ownershipotchet-dt-4061-study-visibility-authzotchet-dt-4061-study-pi-detailsotchet-dt-4061-study-visibility-authzotchet-dt-4061-study-commentsotchet-dt-4061-study-pi-detailsotchet-dt-4061-study-dar-metricsotchet-dt-4061-study-commentsotchet-dt-4061-study-recommendationsotchet-dt-4061-study-dar-metricsotchet-dt-4061-study-asset-fieldsotchet-dt-4061-study-recommendationsotchet-dt-4061-study-asset-endpointsotchet-dt-4061-study-asset-fieldsBranch 1b is a sibling rather than a link in the chain: nothing depends on it,
so it can be held or dropped without blocking the others. Land the numbered
chain in order — merging out of order, or squash-merging, will require rebasing
the descendants.
The split is verified lossless: branch 7 plus 1b reproduces the original branch
exactly, apart from five
scripts/verify-study-*.shhelper scripts that weredropped at the author's request.
./mvnw test-compilepasses on each branchindependently.
🤖 Generated with Claude Code