CLARIN-DSpace v9/Stop the request-a-copy access token from bypassing the CLARIN licence gate - #1437
Merged
milanmajchrak merged 1 commit intoSep 10, 2026
Conversation
…e gate BitstreamRestController.retrieve was annotated @PreAuthorize("#accessToken != null|| hasPermission(#uuid, 'BITSTREAM', 'READ')"), so a non-null request-a-copy access token satisfied authorization on its own: neither the resource policies nor the CLARIN licence gate that AuthorizeServiceImpl.authorizeAction runs for every other bitstream read were consulted. request.item.type is "all", so tokens really are minted, and the fork has no such token path at all. The token is still honoured (request.item.type is unchanged), but only after the same licence check a download without a token goes through. The check is not reimplemented: the new clarinBitstreamAccessTokenSecurity bean and BitstreamResourceAccessByToken both call AuthorizationBitstreamUtils.authorizeBitstream, which is the method the normal download path reaches. That gate reads the CLARIN dtoken from the request, so a caller who holds both an access token and a download token is served. Card X-12, owner decision O-8. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
milanmajchrak
deleted the
ufal/fix-bitstream-accesstoken-bypass-9-base
branch
September 10, 2026 16:02
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
_sync3/cards/X-12.md), owner decision O-8Description
On
dtq-dev-9-base, a valid request-a-copy access token streamed bitstream content past both theresource policies and the CLARIN licence gate. This PR makes the token path go through the same licence
check as an ordinary download, without disabling request-a-copy.
Instructions for Reviewers
The defect
BitstreamRestController.retrievecarried the vanilla DSpace 9 annotation:The operator is
||, so a non-nullaccessTokensatisfied authorization on its own and the right-handside was never evaluated. That right-hand side is the only thing that reaches the CLARIN licence gate:
hasPermission→AuthorizeServicePermissionEvaluatorPlugin→AuthorizeService.authorizeActionBoolean→
AuthorizeServiceImpl.authorizeAction, which callsAuthorizationBitstreamUtils.authorizeBitstream(...)for every non-WRITE bitstream action.Vanilla 9 also ships
BitstreamResourceAccessByToken, which serves the bytes from a context withturnOffAuthorisationSystem(), anddspace/config/modules/requestitem.cfghasrequest.item.type = all,so tokens really are minted.
origin/dtq-devhas neither the token branch nor that class, because CLARINgates restricted downloads behind its own licence flow.
Net effect: anyone holding an access token could download a file behind a CLARIN licence they had never
agreed to.
List of changes in this PR
dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ClarinBitstreamAccessTokenSecurityBean.java—
@Component("clarinBitstreamAccessTokenSecurity")with one method,canDownloadWithAccessToken(UUID, String). It returnstrueonly when request-a-copy is enabled, thetoken is valid for this bitstream (
RequestItemService.authorizeAccessByAccessToken) andAuthorizationBitstreamUtils.authorizeBitstreampasses. It follows the existingVersioningSecurityBeanidiom (
@versioningSecurity.isEnableVersioning()), so no method-security configuration changes.BitstreamRestController.retrieve— the annotation becomes@PreAuthorize("hasPermission(#uuid, 'BITSTREAM', 'READ') or @clarinBitstreamAccessTokenSecurity.canDownloadWithAccessToken(#uuid, #accessToken)").hasPermissionis evaluated first, so a download without a token behaves exactly as before.BitstreamResourceAccessByToken.fetchDocument()— callsauthorizationBitstreamUtils.authorizeBitstream(fileRetrievalContext, bitstream)before building thedocument. That class opens its own context and turns authorisation off in it, so a controller-only guard
would leave it as a second door.
authorizeBitstreamnever consults the authorisation system (submittercheck,
dtokencheck, allowance check), soturnOffAuthorisationSystem()does not weaken it.ClarinBitstreamAccessTokenGateIT— 6 integration tests (below).The licence rules are not duplicated. Both the new bean and the streaming resource call the single
existing implementation,
AuthorizationBitstreamUtils.authorizeBitstream. That method is also what readsthe CLARIN
dtokenfrom the request, which is how the two token kinds are wired together: a caller holdinga valid
accessTokenand adtokenfrom the licence flow is served.request.item.typeis unchanged — request-a-copy stays enabled (O-8).How to test
New IT:
accessTokenDoesNotBypassTheClarinLicenceGateaccessTokenStillServesABitstreamWithoutAClarinLicenceaccessTokenServesTheBitstreamOnceTheClarinLicenceIsSatisfieddtoken→ 200 + body (the wiring)behaviourWithoutAnAccessTokenIsUnchangedbitstreamResourceRefusesToServeLicensedContentOnAnAccessTokenAlonebitstreamResourceStillServesUnlicensedContentOnAnAccessTokenRegression gate — vanilla's own request-a-copy token test lives here and stays green:
Negative control (the tests are real detectors, not decoration):
accessTokenDoesNotBypassTheClarinLicenceGatefailsStatus expected:<401> but was:<500>(Spring Security lets it through; the streaming resource stops it)Status expected:<401> but was:<200>andBitstreamResourceAccessByToken served a CLARIN licence protected bitstream on an access token alone— i.e. the hole itself. The other four methods stay green in both controls.
mvn clean install -P-assembly -DskipTests→ BUILD SUCCESS, 0 Checkstyle violations.Not covered by this PR
than the CLARIN licence page.
/api/authrn/{id}, which the UI uses to decide why a download is refused, does not know aboutaccessToken; for a token holder it answers on the licence state alone. Refusals from this PR thereforesurface as plain 401/403 rather than a
MissingLicenseAgreementExceptionerror name.Checklist
dtq-dev-9-basebranch (fork's DSpace 9 base; notmain).behind
GET /api/core/bitstreams/{uuid}/content?accessToken=is tightened, so no REST Contract PR.🤖 Generated with Claude Code