Skip to content

CLARIN-DSpace v9/Fix REST rel information leak: run the authorization check before entity resolution on clarinlruallowances rels - #1438

Merged
milanmajchrak merged 1 commit into
dtq-dev-9-basefrom
ufal/fix-clrua-rel-401-parity-9-base
Sep 10, 2026
Merged

CLARIN-DSpace v9/Fix REST rel information leak: run the authorization check before entity resolution on clarinlruallowances rels#1438
milanmajchrak merged 1 commit into
dtq-dev-9-basefrom
ufal/fix-clrua-rel-401-parity-9-base

Conversation

@milanmajchrak

Copy link
Copy Markdown
Collaborator

References

  • Acceptance card X-11 (_sync3/cards/X-11.md), owner decisions O-6/O-7 (own PR, not bundled)
  • Follows PR CLARIN-DSpace v9/Fix anonymous HTTP 500 on three CLARIN link rels #1431 / card X-05, which fixed the same class of defect on three sibling repositories and
    recorded these two as a leftover
  • Same defect exists on dtq-dev (7.6.x) — see "Not fixed here" below

Description

GET /api/core/clarinlruallowances/{id} answers an anonymous caller 401 whether or not the allowance
exists
, but two of its three rels answered 404 for an unknown id and 401 for an existing one. The
status code was an oracle for entity existence, and the 404 body named the entity. This PR runs the
authorization check before entity resolution so the rels match their parent.

Instructions for Reviewers

The defect

ClarinLicenseResourceUserAllowanceRestRepository.findOne is guarded with
@PreAuthorize("hasAuthority('AUTHENTICATED')"), so anonymous callers never learn anything from it. Two of
the three @LinkRest rels of the same model had no guard at all, and their bodies resolve the entity
first:

clrua = clarinLicenseResourceUserAllowanceService.find(context, clruaID);   // null for a missing row,
                                                                           // returned BEFORE authorizeClruaAction
if (Objects.isNull(clrua)) { throw new ResourceNotFoundException(...); }    // -> 404

So an unknown id produced 404 and an existing id produced 401. Live, before this PR:

URL (anonymous) parent rel
core/clarinlruallowances/1/resourceMapping 401 401
core/clarinlruallowances/1/userRegistration 401 404
core/clarinlruallowances/1/userMetadata 401 404

with the body The ClarinLicenseResourceUserAllowance for id: 1 couldn't be found (and for if: 1 on the
userMetadata rel — a typo present on both branches).

List of changes in this PR

  • CLRUAUUserRegistrationLinkRepository.getUserRegistration and
    CLRUAUserMetadataLinkRepository.getUserMetadata — add
    @PreAuthorize("hasAuthority('AUTHENTICATED')") plus javadoc explaining why the guard has to run before
    entity resolution. This is the idiom the three sibling repositories fixed by CLARIN-DSpace v9/Fix anonymous HTTP 500 on three CLARIN link rels #1431 already use. The
    existing catch (AuthorizeException) -> RESTAuthorizationException translation is kept, so an
    authenticated non-owner still gets 403, not 500.
  • CLRUAUserMetadataLinkRepository — message typo for if: becomes for id: .
  • ClarinResourceMappingCLicenseLinkRepositoryno guard added, on purpose, plus a javadoc saying so:
    its parent ClarinLicenseResourceMappingRestRepository.findOne is permitAll() and the Angular licence
    agreement page follows that rel anonymously (clarin-license-agreement-page.component.ts,
    followLink('clarinLicense')). Guarding it would break the anonymous download flow. Its anonymous 404 is
    parity with its parent, not a leak.
  • ClarinLinkRestRepositoryBeanNameIT — four new tests (10 -> 14).

Deliberately not done: making the rels answer 404 for existing ids too. That would close the leak but
break parity with the parent, which answers 401.

How to test

mvn -o -pl dspace-server-webapp verify -DskipIntegrationTests=false \
    -Dit.test=ClarinLinkRestRepositoryBeanNameIT -Denforcer.skip=true -Dcheckstyle.skip=true \
    -Dlicense.skip=true -Dxml.skip=true
# Tests run: 14, Failures: 0, Errors: 0     (10 existing + 4 new)
New test What it pins down
anonymousRelStatusMatchesParentForUnknownId for all six CLARIN rel cells at id = Integer.MAX_VALUE, the anonymous status of the rel equals the anonymous status of its parent. The cells are enumerated from the models' own @LinksRest annotations, so a rel added later is covered automatically
anonymousClruaRelsRevealNothingAboutExistence for the three allowance rels, the anonymous status on a real id equals the status on an unknown id
clruaUserMetadataNotFoundMessageUsesId the not-found message says for id: and not for if:
anonymousResourceMappingClarinLicenseRelStaysPublic anonymous GET of a real clarinlicenseresourcemappings/{id}/clarinLicense is 200 — the guard against over-fixing

Negative control (revert only the two production link repositories to the base):

Tests run: 14, Failures: 3, Errors: 0
  anonymousClruaRelsRevealNothingAboutExistence
  anonymousRelStatusMatchesParentForUnknownId
  clruaUserMetadataNotFoundMessageUsesId
AssertionError: ... expected:<401> but was:<404>                       (x2)
AssertionError: The not-found message should read "for id: ", but was:
                The ClarinLicenseResourceUserAllowance for if: 2147483647 couldn't be found

clruaUserRegistrationAndUserMetadataRelsHonourRoles (the pre-existing test on these two rels) stays
green in that control — it only ever exercised an existing id, which is why it never saw this defect —
and so does anonymousResourceMappingClarinLicenseRelStaysPublic.

mvn clean install -P-assembly -DskipTests -> BUILD SUCCESS, 0 Checkstyle violations.

Not fixed here

  • The same missing guards and the same typo are on origin/dtq-dev (7.6.x, production):
    @PreAuthorize count is 0 in all three classes there. That needs its own issue against the 7.6 line.
  • The live sweep _sync3/sweeps/rest-matrix.sh was recalibrated in the workspace (it filed a bare 404 as
    NO-ID-OR-DROPPED and therefore reported "0 FAIL" while this leak was live). That file is not part of
    this repository, so it is not in this PR.

Checklist

  • My PR is created against the dtq-dev-9-base branch (fork's DSpace 9 base; not main).
  • My PR is small in size.
  • My PR passes Checkstyle validation.
  • My PR includes Javadoc for the modified methods.
  • My PR passes all tests and includes new Integration Tests.
  • My PR includes details on how to test it.
  • My PR includes no new libraries/dependencies.
  • My PR modifies REST API endpoints — no contract change: the same endpoints, only the HTTP status an
    unauthorized caller receives is corrected from 404 to 401/403.
  • My PR includes no new configurations.

🤖 Generated with Claude Code

…ity resolution on clarinlruallowances rels

GET /api/core/clarinlruallowances/{id} answers an anonymous caller 401 whether
or not the allowance exists, but two of its three rels answered 404 for an
unknown id and 401 for an existing one. The status code was therefore an oracle
for entity existence, and the 404 body named the entity.

The cause is ordering, not the check itself: the link methods resolve the entity
first, and ClarinLicenseResourceUserAllowanceService.find returns null for a
missing row before it ever reaches authorizeClruaAction. Adding
@PreAuthorize("hasAuthority('AUTHENTICATED')") to getUserRegistration and
getUserMetadata lets Spring Security reject the caller before the body runs, the
same idiom the three sibling repositories already use, so the rels match their
parent. The AuthorizeException translation stays, so an authenticated non-owner
still gets 403.

Also fixes the message typo "for if: " to "for id: ".

ClarinResourceMappingCLicenseLinkRepository deliberately keeps no guard - its
parent findOne is permitAll() and the Angular licence agreement page follows
that rel anonymously - and now says so in a javadoc, with a test that fails if
someone guards it.

Card X-11, owner decision O-7.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@milanmajchrak
milanmajchrak merged commit 6e81855 into dtq-dev-9-base Sep 10, 2026
12 checks passed
@milanmajchrak
milanmajchrak deleted the ufal/fix-clrua-rel-401-parity-9-base branch September 10, 2026 16:02
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.

1 participant