Skip to content

[DT-3107, DT-3108, DT-3110, DT-3112, DT-3113] Add entity-scoped FSO endpoints and access-controlled FileStorageObjectService#2867

Open
kevinmarete wants to merge 7 commits intodevelopfrom
km-dt-3113-file-storage-apis
Open

[DT-3107, DT-3108, DT-3110, DT-3112, DT-3113] Add entity-scoped FSO endpoints and access-controlled FileStorageObjectService#2867
kevinmarete wants to merge 7 commits intodevelopfrom
km-dt-3113-file-storage-apis

Conversation

@kevinmarete
Copy link
Copy Markdown
Contributor

@kevinmarete kevinmarete commented Apr 16, 2026

Addresses

https://broadworkbench.atlassian.net/browse/DT-3107
https://broadworkbench.atlassian.net/browse/DT-3108
https://broadworkbench.atlassian.net/browse/DT-3110
https://broadworkbench.atlassian.net/browse/DT-3112
https://broadworkbench.atlassian.net/browse/DT-3113

Summary

This PR introduces core API functionality for managing entity-scoped File Storage Objects (FSOs). It includes endpoints for uploading files, streaming file content, soft-deleting FSOs, and updating file categories. Additionally, it adds a centralized FileStorageObjectService to enforce consistent access control policies based on entity, category, and user roles across all FSO operations.

After

Have you read CONTRIBUTING.md lately? If not, do that first.

  • Label PR with a Jira ticket number and include a link to the ticket
  • Label PR with a security risk modifier [no, low, medium, high]
  • PR describes scope of changes
  • Get a minimum of one thumbs worth of review, preferably two if enough team members are available
  • Get PO sign-off for all non-trivial UI or workflow changes
  • Verify all tests go green
  • Test this change deployed correctly and works on dev environment after deployment

@kevinmarete kevinmarete self-assigned this Apr 16, 2026
@kevinmarete kevinmarete changed the title Km dt 3113 file storage apis [DT-3107, DT-3108, DT-3110, DT-3112, DT-3113] Add entity-scoped FSO endpoints and access-controlled FileStorageObjectService Apr 16, 2026
@sonarqubecloud
Copy link
Copy Markdown

@kevinmarete kevinmarete marked this pull request as ready for review April 17, 2026 18:35
@kevinmarete kevinmarete requested a review from a team as a code owner April 17, 2026 18:35
@kevinmarete kevinmarete requested review from Copilot, eweitz, otchet-broad and rushtong and removed request for a team, Copilot and eweitz April 17, 2026 18:35
@kevinmarete kevinmarete requested a review from eweitz April 17, 2026 18:35
Copy link
Copy Markdown
Member

@eweitz eweitz left a comment

Choose a reason for hiding this comment

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

Looks mostly good!

The only substantive question I have is in the comment beginning "Above notes".

Beyond that, I suggest some potential refinements for code clarity, but defer to you. I also flag code for "update" operations, which IIUC we'll remove pending Greg's confirmation.

Comment on lines +123 to +132
/**
* Downloads the binary content of a document.
*
* <ol>
* <li>Resolves entity ID.
* <li>Fetches FSO metadata (including category).
* <li>Calls {@link #checkAccess} with the file's actual category (READ).
* <li>Streams file from GCS.
* </ol>
*/
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Incidentally, I'm curious about the pros / cons of Consent's pre-existing bucket-to-server-to-client approach vs. a the bucket-to-cilent approach used in Terra and SCP. Added as a parking lot topic for Tuesday!

Comment on lines +173 to +182
/**
* Updates the category of an existing document.
*
* <ol>
* <li>Parses and validates the new category for the entity.
* <li>Calls {@link #checkAccess} with the new category (CRUD).
* <li>Resolves entity ID, fetches FSO, updates category.
* </ol>
*/
public FileStorageObject updateDocumentCategory(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

IIUC, undesired pending Greg's confirmation.

Comment on lines +256 to +272
* <li>CRUD: DAR creator only.
* <li>READ: ADMIN, CHAIRPERSON, MEMBER, or DAR creator.
* </ul>
* <li><b>DAC</b> – {@link FileCategory#DATA_ACCESS_AGREEMENT}:
* <ul>
* <li>CRUD: CHAIRPERSON of the specific DAC (entity-scoped) or ADMIN.
* <li>READ: any authenticated user.
* </ul>
* <li><b>DATASET</b> – {@link FileCategory#NIH_INSTITUTIONAL_CERTIFICATION}:
* <ul>
* <li>CRUD: ADMIN, DATASUBMITTER, or CHAIRPERSON.
* <li>READ: ADMIN, DATASUBMITTER, CHAIRPERSON, or MEMBER.
* </ul>
* <li><b>STUDY</b> – {@link FileCategory#ALTERNATIVE_DATA_SHARING_PLAN}:
* <ul>
* <li>CRUD: ADMIN, DATASUBMITTER, or CHAIRPERSON.
* <li>READ: ADMIN, DATASUBMITTER, or CHAIRPERSON (study must be DAC-linked; enforced by
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Changing CRUD to CREATE, DELETE seems like it would be clearer, since we'll likely not allow "UPDATE" via API and "READ" is cover in the line below.

Comment on lines +321 to +322
ensureHasAnyRole(
user, UserRoles.ADMIN, UserRoles.DATASUBMITTER, UserRoles.CHAIRPERSON, UserRoles.MEMBER);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Above notes:

  *   <li><b>DATASET</b> – {@link FileCategory#NIH_INSTITUTIONAL_CERTIFICATION}:
   *       <ul>
   *         <li>CRUD: ADMIN, DATASUBMITTER, or CHAIRPERSON.
   *         <li>READ: ADMIN, DATASUBMITTER, CHAIRPERSON, or MEMBER.
   *       </ul>
   *   <li><b>STUDY</b> – {@link FileCategory#ALTERNATIVE_DATA_SHARING_PLAN}:
   *       <ul>
   *         <li>CRUD: ADMIN, DATASUBMITTER, or CHAIRPERSON.
   *         <li>READ: ADMIN, DATASUBMITTER, or CHAIRPERSON (study must be DAC-linked; enforced by 
   *             entity resolution via DatasetService).
   *       </ul>
   * </ul>

I interpret that to mean alternative data sharing plans should not permit READ to MEMBER. Naively, the UserRoles.MEMBER argument on this line seems like would permit READ to MEMBER.

private void checkDarAccess(User user, String entityId, OperationType op) {
boolean isCreator = isDarCreator(user, entityId);

if (op == OperationType.CRUD && !isCreator) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Following my other suggestion to refine "CRUD" (i.e. CREATE, READ, UPDATE, DELETE) to "CREATE, DELETE", I think refining this operation type to something like CREATE_OR_DELETE would help avoid maintainer confusion.

description: Not Found

put:
summary: Update document category by entity and file id
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

IIUC we'll presumably remove this, pending Greg's confirmation.

@@ -0,0 +1,15 @@
type: object
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

IIUC we'll presumably remove this, pending Greg's approval.

}

@Test
void testUpdateCategory() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Flagging for removal pending Greg's confirmation.

private static final String DAR_ID = "DAR-100";

@Test
void crudAllowedForCreator() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Similar to my other suggestions to refine usage of "CRUD" -- I recommend changing crud to e.g. createAndDelete throughout this PR's tests.

ForbiddenException.class,
() -> service.checkAccess(researcher, "study", "1", null, OperationType.READ));
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Depending on the answer to my question in the comment beginning "Above notes", it might be worth testing that case here.

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.

2 participants