Repository navigation
Fix dspace-customers 1083 finding 11: check READ and anonymous licence confirmation in clarinusermetadata manage and manage/zip - #1558
Merged
Conversation
…e confirmation in clarinusermetadata manage and manage/zip Both endpoints are permitAll() and stored user metadata, created a download token and e-mailed the item or file name without checking READ. manage/zip also skipped the ALLOW_ANONYMOUS test of the single-file method, so an anonymous caller got a token for files whose licence requires login. Check READ on the item and on every file before anything is written or sent. In manage/zip, run the anonymous licence test for every file, so the strictest licence wins. The bitstream READ check ignores the CLARIN licence-check exceptions, because confirming the licence is the purpose of these endpoints.
There was a problem hiding this comment.
🟡 Changes recommended
Add single-file coverage for denied parent-item READ access and avoid resolving ZIP licence mappings twice per bitstream.
1 open issue
What changed in this PR
This pull request secures the CLARIN user-metadata endpoints by enforcing READ authorization and anonymous licence restrictions before metadata, token, allowance, or e-mail side effects occur.
Changes:
- Added item and bitstream READ checks to single-file and ZIP requests.
- Added per-file ZIP preflight validation, including anonymous licence enforcement.
- Updated integration fixtures and added authorization and licence regression tests.
| File | Summary |
|---|---|
dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ClarinUserMetadataRestController.java |
Adds item/bitstream authorization and anonymous licence checks before processing requests. |
dspace-server-webapp/src/test/java/org/dspace/app/rest/ClarinUserMetadataRestControllerIT.java |
Archives test fixtures and adds coverage for READ authorization, licence restrictions, and token behavior. |
Review effort: Lite
jr-rk
requested review from
milanmajchrak
and removed request for
milanmajchrak
October 27, 2026 16:11
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

References
dspace-customers issue 1083, finding 11. Related: #924 (CLARIN user-data endpoints), #1469 (v9 request-a-copy token in the same controller).
Description
POST /api/core/clarinusermetadata/manage?bitstreamUUID=andPOST /api/core/clarinusermetadata/manage/zip?itemUUID=arepermitAll(). They load the bitstream or item, store the submitted user metadata, create a download token and, forSEND_TOKENlicences, e-mail the item or file name to the address in the request body. Neither method checked READ. Any caller who knew a UUID could have the name of a private, withdrawn or embargoed object sent to any address and could create user metadata and token rows for it.The zip method also lacked the anonymous check that the single-file method has: an anonymous caller is refused unless the licence is
ALLOW_ANONYMOUS. Without it, an anonymous caller got a token for every file of the item, including files whose licence requires login. A valid token skips the CLARIN licence check on download, so those files could be downloaded without logging in.Root cause: the controller never calls
AuthorizeServicebefore its side effects. The zip loop checks nothing per file before it writes the rows for that file.Instructions for Reviewers
List of changes in this PR:
ClarinUserMetadataRestController.manageUserMetadata: checks READ on the parent item and on the bitstream right after loading the bitstream, before anything is written or sent.ClarinUserMetadataRestController.manageUserMetadataForZIP: checks READ on the item. Before the existing processing loop, a new loop checks every ORIGINAL file: READ on the bitstream, and for an anonymous caller the sameALLOW_ANONYMOUStest as the single-file method. One file that fails refuses the whole request. The strictest licence of the item wins, and no row or token is created. This matchesallzip, which also fails as a whole when one file cannot be read.New private
authorizeBitstreamRead:authorizeAction(bitstream, READ)also runs the CLARIN licence check, which throwsMissingLicenseAgreementExceptionuntil the user has confirmed the licence. Confirming the licence is the purpose of this endpoint. The helper therefore ignores that exception andDownloadTokenExpiredException.AuthorizeServiceImpl.authorizeActionthrows both only after the READ policy check has passed. A missing READ policy still throws a plainAuthorizeException.Denials use the usual DSpace mapping: 401 for anonymous callers, 403 for authenticated ones.
ClarinUserMetadataRestControllerIT: 6 new tests on an archived item:manage): 401 anonymous, 403 authenticated;manage): 401;manage/zip): 401 anonymous, 403 authenticated;manage/zip): 401;ALLOW_ANONYMOUS+ASK_ALWAYS(manage/zip): 401;ALLOW_ANONYMOUSlicence (manage/zip): still gets a token, one allowance row per file.Every refused case also asserts that no download token row was created.
ClarinUserMetadataRestControllerIT.prepareEnvironment: the existing tests now archive their workspace items before calling the endpoint. Before this change they called it as an anonymous user on workspace items, which anonymous users cannot read. With the fix that is refused, as it should be. All 17 tests in the class pass.How to test:
mvn verify -pl dspace-server-webapp,dspace -am -DskipUnitTests=true -DskipIntegrationTests=false -Dit.test=ClarinUserMetadataRestControllerIT -Dtest=none -Dsurefire.failIfNoSpecifiedTests=false -DfailIfNoTests=false. Thedspacemodule must be in the reactor. Without it, the test environment zip has no default config.Not in scope:
manage/zipdecidesSEND_TOKENand the licence text for the e-mail from the first file only. If the first file's licence does not requireSEND_TOKENand a later one does, the token is returned in the response instead of by e-mail.EXTRA_EMAIL), and there is no rate limit.bitstreamUUIDreturns an empty 200; unknownitemUUIDthrowsjavax.ws.rs.NotFoundException.allzipdownload endpoint (finding 9).AI Disclosure (REQUIRED)
Were AI tools used in the creation of this PR?
Claude Code wrote the patch and the tests. The targeted IT and Checkstyle pass locally. The tests were not run against the base branch.
Checklist
mainbranch of code (unless it is a backport or is fixing an issue specific to an older branch).pom.xml), I've made sure their licenses align with the DSpace BSD License based on the Licensing of Contributions documentation.Written on
dtq-dev(f9955da5f2). Cherry-picks cleanly ontocustomer/lindatandcustomer/vsb-tuo.dtq-dev-9-baseconflicts because the request-a-copyaccessTokenthere (#1469) needs its own port: a caller with a valid access token must pass the READ check.