Skip to content

Commit 233d87f

Browse files
Fix HTTP 500 on anonymous CLARIN link rels (#1431)
Three CLARIN link rels answered HTTP 500 instead of 401 for an anonymous caller on an id that exists: core/clarinuserregistrations/1/userMetadata 500 (parent: 401) core/clarinuserregistrations/1/clarinLicenses 500 (parent: 401) core/clarinlruallowances/242/resourceMapping 500 (parent: 401) The same rels answer 404 on an id that does not exist, so the route resolves and the repository body runs - the 500 is thrown inside it. Two independent causes, both in fork code: 1. ClarinUserRegistrationServiceImpl.authorizeClarinUserRegistrationAction() dereferenced context.getCurrentUser() with no null check. isAdmin() returns false rather than throwing for an anonymous context, so the authorization helper itself threw NullPointerException before it could throw AuthorizeException. Its sibling authorizeClruaAction() already carries the Objects.nonNull(currentUser) guard, which is why the clarinlruallowances rels answer 401 and these two did not. 2. CLRUAResourceMappingLinkRepository let the checked AuthorizeException escape. RestResourceController.findRelInternal re-throws a RuntimeException target as-is but wraps a checked one in new RuntimeException(e), which surfaces as 500. Its two siblings convert that exception, which is why the same service call produced 401, 401 and 500 from three repositories. Both defects predate the DSpace 9 upgrade; registering the link repositories under the plural model name (#1404) only made the bodies reachable. RestResourceController is byte-identical with dspace-9.3 and vanilla link repositories use @PreAuthorize plus AccessDeniedException, so nothing here belongs upstream. The three link repositories now follow that vanilla pattern: a coarse @PreAuthorize("hasAuthority('AUTHENTICATED')") matching the parent repository's findOne, and a translation of the service's checked AuthorizeException into AccessDeniedException. AccessDeniedException rather than the fork's RESTAuthorizationException because Utils.embedRelFromRepository swallows only the former; the Angular UI embeds userMetadata via followLink(), and any other RuntimeException raised while embedding would fail the parent request with a 500. ClarinLinkRestRepositoryBeanNameIT grows from 3 to 10 tests: the three existing bean-name tests, the fork's one rel on a vanilla model, an automatic check that every registered LinkRestRepository bean names a model repository that exists (so a future model that forgets PLURAL_NAME fails without editing this class), and the anonymous/owner/other-user/ admin matrix for the four rels above. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 11da09b commit 233d87f

5 files changed

Lines changed: 327 additions & 30 deletions

File tree

dspace-api/src/main/java/org/dspace/content/clarin/ClarinUserRegistrationServiceImpl.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,9 @@ private void authorizeClarinUserRegistrationAction(Context context, List<ClarinU
145145
EPerson currentUser = context.getCurrentUser();
146146
ClarinUserRegistration clarinUserRegistration = userRegistrationList.get(0);
147147
UUID userRegistrationEpersonUUID = clarinUserRegistration.getPersonID();
148-
if (currentUser.getID().equals(userRegistrationEpersonUUID)) {
148+
// An anonymous request has no current user. Deny it here instead of throwing a NullPointerException,
149+
// so that the callers can translate the AuthorizeException into 401/403.
150+
if (Objects.nonNull(currentUser) && currentUser.getID().equals(userRegistrationEpersonUUID)) {
149151
return;
150152
}
151153

dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/CLRUAResourceMappingLinkRepository.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import org.springframework.beans.factory.annotation.Autowired;
2424
import org.springframework.data.domain.Pageable;
2525
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
26+
import org.springframework.security.access.AccessDeniedException;
27+
import org.springframework.security.access.prepost.PreAuthorize;
2628
import org.springframework.stereotype.Component;
2729

2830
/**
@@ -36,15 +38,27 @@ public class CLRUAResourceMappingLinkRepository extends AbstractDSpaceRestReposi
3638
@Autowired
3739
ClarinLicenseResourceUserAllowanceService clarinLicenseResourceUserAllowanceService;
3840

41+
/**
42+
* The resource mapping behind a CLARIN license resource user allowance is readable by the user the allowance
43+
* belongs to and by administrators. See
44+
* {@link ClarinUserRegistrationUserMetadataLinkRepository#getUserMetadata} for why the checked
45+
* {@link AuthorizeException} must be translated into an unchecked one here.
46+
*/
47+
@PreAuthorize("hasAuthority('AUTHENTICATED')")
3948
public ClarinLicenseResourceMappingRest getResourceMapping(@Nullable HttpServletRequest request,
4049
Integer clruaID,
4150
@Nullable Pageable optionalPageable,
4251
Projection projection)
43-
throws SQLException, AuthorizeException {
52+
throws SQLException {
4453
Context context = obtainContext();
4554

46-
ClarinLicenseResourceUserAllowance clarinLicenseResourceUserAllowance =
47-
clarinLicenseResourceUserAllowanceService.find(context, clruaID);
55+
ClarinLicenseResourceUserAllowance clarinLicenseResourceUserAllowance;
56+
try {
57+
clarinLicenseResourceUserAllowance = clarinLicenseResourceUserAllowanceService.find(context, clruaID);
58+
} catch (AuthorizeException e) {
59+
throw new AccessDeniedException("The current user is not allowed to read the resource mapping of the "
60+
+ "CLARIN license resource user allowance with id: " + clruaID, e);
61+
}
4862
if (Objects.isNull(clarinLicenseResourceUserAllowance)) {
4963
throw new ResourceNotFoundException("The ClarinLicenseResourceUserAllowance for id: " + clruaID +
5064
" couldn't be found");

dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/CUserRegistrationCLicenseLinkRepository.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import org.springframework.data.domain.Page;
2626
import org.springframework.data.domain.Pageable;
2727
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
28+
import org.springframework.security.access.AccessDeniedException;
29+
import org.springframework.security.access.prepost.PreAuthorize;
2830
import org.springframework.stereotype.Component;
2931

3032
@Component(ClarinUserRegistrationRest.CATEGORY + "." + ClarinUserRegistrationRest.PLURAL_NAME + "." +
@@ -35,12 +37,24 @@ public class CUserRegistrationCLicenseLinkRepository extends AbstractDSpaceRestR
3537
@Autowired
3638
ClarinUserRegistrationService clarinUserRegistrationService;
3739

40+
/**
41+
* The CLARIN licenses a user registration has agreed to are readable by the owner of the registration and by
42+
* administrators. See {@link ClarinUserRegistrationUserMetadataLinkRepository#getUserMetadata} for why the
43+
* checked {@link AuthorizeException} must be translated into an unchecked one here.
44+
*/
45+
@PreAuthorize("hasAuthority('AUTHENTICATED')")
3846
public Page<ClarinLicenseRest> getClarinLicenses(@Nullable HttpServletRequest request,
3947
Integer userRegistrationID,
4048
@Nullable Pageable optionalPageable,
41-
Projection projection) throws SQLException, AuthorizeException {
49+
Projection projection) throws SQLException {
4250
Context context = obtainContext();
43-
ClarinUserRegistration clarinUserRegistration = clarinUserRegistrationService.find(context, userRegistrationID);
51+
ClarinUserRegistration clarinUserRegistration;
52+
try {
53+
clarinUserRegistration = clarinUserRegistrationService.find(context, userRegistrationID);
54+
} catch (AuthorizeException e) {
55+
throw new AccessDeniedException("The current user is not allowed to read the CLARIN licenses of the "
56+
+ "CLARIN user registration with id: " + userRegistrationID, e);
57+
}
4458
if (Objects.isNull(clarinUserRegistration)) {
4559
throw new ResourceNotFoundException("The CLARIN User Registration for id: " + userRegistrationID +
4660
" couldn't be found");

dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ClarinUserRegistrationUserMetadataLinkRepository.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import org.springframework.data.domain.Page;
2424
import org.springframework.data.domain.Pageable;
2525
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
26+
import org.springframework.security.access.AccessDeniedException;
27+
import org.springframework.security.access.prepost.PreAuthorize;
2628
import org.springframework.stereotype.Component;
2729

2830
@Component(ClarinUserRegistrationRest.CATEGORY + "." + ClarinUserRegistrationRest.PLURAL_NAME + "." +
@@ -33,15 +35,30 @@ public class ClarinUserRegistrationUserMetadataLinkRepository extends AbstractDS
3335
@Autowired
3436
ClarinUserRegistrationService clarinUserRegistrationService;
3537

38+
/**
39+
* The user metadata of a CLARIN user registration is readable by the owner of the registration and by
40+
* administrators. Anonymous callers are rejected by {@code @PreAuthorize}; a logged-in caller who does not
41+
* own the registration is rejected by the service. The service reports that with a checked
42+
* {@link AuthorizeException}, which must be translated here: {@code RestResourceController.findRelInternal}
43+
* wraps a checked exception thrown by a link method into a {@code RuntimeException} and the client sees an
44+
* HTTP 500 instead of 401/403.
45+
*/
46+
@PreAuthorize("hasAuthority('AUTHENTICATED')")
3647
public Page<ClarinUserMetadataRest> getUserMetadata(@Nullable HttpServletRequest request,
3748
Integer userRegistrationID,
3849
@Nullable Pageable optionalPageable,
39-
Projection projection) throws SQLException, AuthorizeException {
50+
Projection projection) throws SQLException {
4051
Context context = obtainContext();
4152

42-
ClarinUserRegistration clarinUserRegistration = clarinUserRegistrationService.find(context, userRegistrationID);
53+
ClarinUserRegistration clarinUserRegistration;
54+
try {
55+
clarinUserRegistration = clarinUserRegistrationService.find(context, userRegistrationID);
56+
} catch (AuthorizeException e) {
57+
throw new AccessDeniedException("The current user is not allowed to read the user metadata of the "
58+
+ "CLARIN user registration with id: " + userRegistrationID, e);
59+
}
4360
if (Objects.isNull(clarinUserRegistration)) {
44-
throw new ResourceNotFoundException("The ClarinLicenseResourceUserAllowance for if: " + userRegistrationID +
61+
throw new ResourceNotFoundException("The ClarinUserRegistration for id: " + userRegistrationID +
4562
" couldn't be found");
4663
}
4764

0 commit comments

Comments
 (0)