Skip to content

Fix dspace-customers 1083 finding 11: check READ and anonymous licence confirmation in clarinusermetadata manage and manage/zip - #1558

Merged
milanmajchrak merged 2 commits into
dtq-devfrom
fix/f11-user-metadata-manage-read
Oct 28, 2026
Merged

milanmajchrak merged 2 commits into
dtq-devfrom
fix/f11-user-metadata-manage-read

Conversation

@jr-rk

@jr-rk jr-rk commented Oct 27, 2026 •

Copy link
Copy Markdown

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= and POST /api/core/clarinusermetadata/manage/zip?itemUUID= are permitAll(). They load the bitstream or item, store the submitted user metadata, create a download token and, for SEND_TOKEN licences, 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 AuthorizeService before 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 same ALLOW_ANONYMOUS test 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 matches allzip, 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 throws MissingLicenseAgreementException until the user has confirmed the licence. Confirming the licence is the purpose of this endpoint. The helper therefore ignores that exception and DownloadTokenExpiredException. AuthorizeServiceImpl.authorizeAction throws both only after the READ policy check has passed. A missing READ policy still throws a plain AuthorizeException.

  • Denials use the usual DSpace mapping: 401 for anonymous callers, 403 for authenticated ones.

  • ClarinUserMetadataRestControllerIT: 6 new tests on an archived item:

    • no READ on the bitstream (manage): 401 anonymous, 403 authenticated;
    • no READ on the parent item, READ on the bitstream (manage): 401;
    • no READ on the item (manage/zip): 401 anonymous, 403 authenticated;
    • no READ on one of two files (manage/zip): 401;
    • anonymous caller and mixed licences, ALLOW_ANONYMOUS + ASK_ALWAYS (manage/zip): 401;
    • anonymous caller with an ALLOW_ANONYMOUS licence (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. The dspace module must be in the reactor. Without it, the test environment zip has no default config.

Not in scope:

  • manage/zip decides SEND_TOKEN and the licence text for the e-mail from the first file only. If the first file's licence does not require SEND_TOKEN and a later one does, the token is returned in the response instead of by e-mail.
  • The e-mail recipient comes from the request body (EXTRA_EMAIL), and there is no rate limit.
  • Unknown bitstreamUUID returns an empty 200; unknown itemUUID throws javax.ws.rs.NotFoundException.
  • Download tokens in logs and Matomo URLs (R51), the allzip download endpoint (finding 9).

AI Disclosure (REQUIRED)

Were AI tools used in the creation of this PR?

  • No (This PR does NOT include AI-generated or AI-assisted content that requires disclosure.)
  • Yes (This PR includes AI-generated or AI-assisted content that requires disclosure.)

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

  • My PR is created against the main branch of code (unless it is a backport or is fixing an issue specific to an older branch).
  • My PR is small in size (e.g. less than 1,000 lines of code, not including comments & integration tests). Exceptions may be made if previously agreed upon.
  • My PR follows all coding best practices based on the Code Conventions Guide.
  • My PR passes Checkstyle validation based on the Code Style Guide.
  • My PR includes Javadoc for all new (or modified) public methods and classes. It also includes Javadoc for large or complex private methods.
  • My PR passes all tests and includes new/updated Unit or Integration Tests based on the Code Testing Guide.
  • My PR includes details on how to test it. I've provided clear instructions to reviewers on how to successfully test this fix or feature.
  • If my PR includes new libraries/dependencies (in any pom.xml), I've made sure their licenses align with the DSpace BSD License based on the Licensing of Contributions documentation.
  • If my PR modifies REST API endpoints, I've opened a separate REST Contract PR related to this change.
  • If my PR includes new configurations, I've provided basic technical documentation in the PR itself.
  • If my PR fixes an issue ticket, I've linked them together.

Written on dtq-dev (f9955da5f2). Cherry-picks cleanly onto customer/lindat and customer/vsb-tuo. dtq-dev-9-base conflicts because the request-a-copy accessToken there (#1469) needs its own port: a caller with a valid access token must pass the READ check.

…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.
@jr-rk
jr-rk requested a lite review from Copilot October 27, 2026 14:44
@jr-rk jr-rk self-assigned this Oct 27, 2026
@jr-rk
jr-rk marked this pull request as ready for review October 27, 2026 14:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Denied-request tests should also verify that no ClarinUserMetadata rows are persisted, including for partial or mixed ZIP requests.

No open issues 🎉

1 resolved since last review

Review effort: Lite

@jr-rk
jr-rk requested review from milanmajchrak and removed request for milanmajchrak October 27, 2026 16:11
@milanmajchrak
milanmajchrak merged commit 004c81d into dtq-dev Oct 28, 2026
15 checks passed
@jr-rk
jr-rk deleted the fix/f11-user-metadata-manage-read branch October 28, 2026 14:34
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