Merge pull request #1976 from atmire/DS-3542_Spring-permission-evaluator

DS-3542 Spring security authorizations 2
This commit is contained in:
Tim Donohue
2018-08-23 14:26:30 -05:00
committed by GitHub
57 changed files with 1203 additions and 153 deletions

View File

@@ -164,7 +164,7 @@ public class EPersonDAOImpl extends AbstractHibernateDSODAO<EPerson> implements
addMetadataValueWhereQuery(queryBuilder, queryFields, "like",
EPerson.class.getSimpleName().toLowerCase() + ".email like :queryParam");
}
if (!CollectionUtils.isEmpty(sortFields)) {
if (!CollectionUtils.isEmpty(sortFields) || StringUtils.isNotBlank(sortField)) {
addMetadataSortQuery(queryBuilder, sortFields, Collections.singletonList(sortField));
}

View File

@@ -212,6 +212,11 @@
<artifactId>spring-boot-starter-data-rest</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>

View File

@@ -22,16 +22,15 @@ import org.dspace.app.rest.model.BitstreamRest;
import org.dspace.app.rest.utils.ContextUtil;
import org.dspace.app.rest.utils.MultipartFileSender;
import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.content.Bitstream;
import org.dspace.content.BitstreamFormat;
import org.dspace.content.service.BitstreamService;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.disseminate.service.CitationDocumentService;
import org.dspace.services.EventService;
import org.dspace.usage.UsageEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@@ -68,12 +67,10 @@ public class BitstreamContentRestController {
@Autowired
private EventService eventService;
@Autowired
private AuthorizeService authorizeService;
@Autowired
private CitationDocumentService citationDocumentService;
@PreAuthorize("hasPermission(#uuid, 'BITSTREAM', 'READ')")
@RequestMapping(method = {RequestMethod.GET, RequestMethod.HEAD})
public void retrieve(@PathVariable UUID uuid, HttpServletResponse response,
HttpServletRequest request) throws IOException, SQLException, AuthorizeException {
@@ -81,9 +78,9 @@ public class BitstreamContentRestController {
Context context = ContextUtil.obtainContext(request);
Bitstream bit = getBitstream(context, uuid, response);
Bitstream bit = bitstreamService.find(context, uuid);
if (bit == null) {
//The bitstream was not found or we're not authorized to read it.
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
@@ -169,18 +166,6 @@ public class BitstreamContentRestController {
return name;
}
private Bitstream getBitstream(Context context, @PathVariable UUID uuid, HttpServletResponse response)
throws SQLException, IOException, AuthorizeException {
Bitstream bit = bitstreamService.find(context, uuid);
if (bit == null) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
} else {
authorizeService.authorizeAction(context, bit, Constants.READ);
}
return bit;
}
private boolean isNotAnErrorResponse(HttpServletResponse response) {
Response.Status.Family responseCode = Response.Status.Family.familyOf(response.getStatus());
return responseCode.equals(Response.Status.Family.SUCCESSFUL)

View File

@@ -21,6 +21,7 @@ import org.dspace.app.rest.model.RestModel;
import org.dspace.app.rest.model.hateoas.DSpaceResource;
import org.dspace.app.rest.utils.Utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.domain.Pageable;
import org.springframework.hateoas.Link;
import org.springframework.stereotype.Component;
@@ -45,7 +46,7 @@ public class DSpaceResourceHalLinkFactory extends HalLinkFactory<DSpaceResource,
Method readMethod = pd.getReadMethod();
String name = pd.getName();
if (readMethod != null && !"class".equals(name)) {
LinkRest linkAnnotation = readMethod.getAnnotation(LinkRest.class);
LinkRest linkAnnotation = AnnotationUtils.findAnnotation(readMethod, LinkRest.class);
if (linkAnnotation != null) {
if (StringUtils.isNotBlank(linkAnnotation.name())) {
@@ -57,10 +58,9 @@ public class DSpaceResourceHalLinkFactory extends HalLinkFactory<DSpaceResource,
if (StringUtils.isBlank(linkAnnotation.method())) {
Object linkedObject = readMethod.invoke(data);
if (linkedObject instanceof RestAddressableModel && linkAnnotation.linkClass()
.isAssignableFrom(
linkedObject
.getClass())) {
if (linkedObject instanceof RestAddressableModel
&& linkAnnotation.linkClass().isAssignableFrom(linkedObject.getClass())) {
linkToSubResource = utils
.linkToSingleResource((RestAddressableModel) linkedObject, name);
}

View File

@@ -8,6 +8,7 @@
package org.dspace.app.rest.model.hateoas;
import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.hateoas.core.EvoInflectorRelProvider;
/**
@@ -20,7 +21,7 @@ public class DSpaceRelProvider extends EvoInflectorRelProvider {
@Override
public String getItemResourceRelFor(Class<?> type) {
RelNameDSpaceResource nameAnnotation = type.getAnnotation(RelNameDSpaceResource.class);
RelNameDSpaceResource nameAnnotation = AnnotationUtils.findAnnotation(type, RelNameDSpaceResource.class);
if (nameAnnotation != null) {
return nameAnnotation.value();
}

View File

@@ -25,6 +25,7 @@ import org.dspace.app.rest.model.RestAddressableModel;
import org.dspace.app.rest.repository.DSpaceRestRepository;
import org.dspace.app.rest.repository.LinkRestRepository;
import org.dspace.app.rest.utils.Utils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.hateoas.Link;
@@ -91,7 +92,7 @@ public abstract class DSpaceResource<T extends RestAddressableModel> extends HAL
Method readMethod = pd.getReadMethod();
String name = pd.getName();
if (readMethod != null && !"class".equals(name)) {
LinkRest linkAnnotation = readMethod.getAnnotation(LinkRest.class);
LinkRest linkAnnotation = AnnotationUtils.findAnnotation(readMethod, LinkRest.class);
if (linkAnnotation != null) {
if (StringUtils.isNotBlank(linkAnnotation.name())) {

View File

@@ -29,6 +29,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
/**
@@ -54,6 +55,7 @@ public class AuthorityEntryLinkRepository extends AbstractDSpaceRestRepository
return new AuthorityEntryResource(model);
}
@PreAuthorize("hasAuthority('AUTHENTICATED')")
public Page<AuthorityEntryRest> query(HttpServletRequest request, String name,
Pageable pageable, String projection) {
Context context = obtainContext();

View File

@@ -20,6 +20,7 @@ import org.dspace.content.authority.service.ChoiceAuthorityService;
import org.dspace.core.Context;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
/**
@@ -42,6 +43,7 @@ public class AuthorityEntryValueLinkRepository extends AbstractDSpaceRestReposit
return new AuthorityEntryResource(model);
}
@PreAuthorize("hasAuthority('AUTHENTICATED')")
public AuthorityEntryRest getResource(HttpServletRequest request, String name, String relId,
Pageable pageable, String projection) {
Context context = obtainContext();

View File

@@ -21,6 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
/**
@@ -37,6 +38,7 @@ public class AuthorityRestRepository extends DSpaceRestRepository<AuthorityRest,
@Autowired
private AuthorityUtils authorityUtils;
@PreAuthorize("hasAuthority('AUTHENTICATED')")
@Override
public AuthorityRest findOne(Context context, String name) {
ChoiceAuthority source = cas.getChoiceAuthorityByAuthorityName(name);
@@ -44,6 +46,7 @@ public class AuthorityRestRepository extends DSpaceRestRepository<AuthorityRest,
return result;
}
@PreAuthorize("hasAuthority('AUTHENTICATED')")
@Override
public Page<AuthorityRest> findAll(Context context, Pageable pageable) {
Set<String> authoritiesName = cas.getChoiceAuthoritiesNames();

View File

@@ -28,6 +28,7 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
@@ -51,6 +52,7 @@ public class BitstreamRestRepository extends DSpaceRestRepository<BitstreamRest,
}
@Override
@PreAuthorize("hasPermission(#id, 'BITSTREAM', 'READ')")
public BitstreamRest findOne(Context context, UUID id) {
Bitstream bit = null;
try {
@@ -72,6 +74,7 @@ public class BitstreamRestRepository extends DSpaceRestRepository<BitstreamRest,
}
@Override
@PreAuthorize("hasAuthority('ADMIN')")
public Page<BitstreamRest> findAll(Context context, Pageable pageable) {
List<Bitstream> bit = new ArrayList<Bitstream>();
Iterator<Bitstream> it = null;

View File

@@ -29,6 +29,7 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
/**
@@ -55,6 +56,7 @@ public class CollectionRestRepository extends DSpaceRestRepository<CollectionRes
}
@Override
@PreAuthorize("hasPermission(#id, 'COLLECTION', 'READ')")
public CollectionRest findOne(Context context, UUID id) {
Collection collection = null;
try {

View File

@@ -25,6 +25,7 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
/**
@@ -47,6 +48,7 @@ public class CommunityRestRepository extends DSpaceRestRepository<CommunityRest,
}
@Override
@PreAuthorize("hasPermission(#id, 'COMMUNITY', 'READ')")
public CommunityRest findOne(Context context, UUID id) {
Community community = null;
try {

View File

@@ -23,6 +23,7 @@ import org.dspace.app.rest.model.step.UploadStatusResponse;
import org.dspace.app.util.DCInputsReaderException;
import org.dspace.authorize.AuthorizeException;
import org.dspace.core.Context;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
@@ -43,6 +44,13 @@ public abstract class DSpaceRestRepository<T extends RestAddressableModel, ID ex
private static final Logger log = Logger.getLogger(DSpaceRestRepository.class);
//Trick to make inner-calls to ourselves that are checked by Spring security
//See:
// https://stackoverflow.com/questions/13564627/spring-aop-not-working-for-method-call-inside-another-method
// https://docs.spring.io/spring/docs/4.3.18.RELEASE/spring-framework-reference/htmlsingle/#aop-understanding-aop-proxies
@Autowired
private DSpaceRestRepository<T, ID> thisRepository;
@Override
public <S extends T> S save(S entity) {
Context context = null;
@@ -72,7 +80,7 @@ public abstract class DSpaceRestRepository<T extends RestAddressableModel, ID ex
@Override
public T findOne(ID id) {
Context context = obtainContext();
return findOne(context, id);
return thisRepository.findOne(context, id);
}
public abstract T findOne(Context context, ID id);
@@ -103,7 +111,7 @@ public abstract class DSpaceRestRepository<T extends RestAddressableModel, ID ex
public void delete(ID id) {
Context context = obtainContext();
try {
delete(context, id);
thisRepository.delete(context, id);
context.commit();
} catch (AuthorizeException e) {
throw new RESTAuthorizationException(e);
@@ -141,7 +149,7 @@ public abstract class DSpaceRestRepository<T extends RestAddressableModel, ID ex
@Override
public Page<T> findAll(Pageable pageable) {
Context context = obtainContext();
return findAll(context, pageable);
return thisRepository.findAll(context, pageable);
}
public abstract Page<T> findAll(Context context, Pageable pageable);
@@ -154,7 +162,7 @@ public abstract class DSpaceRestRepository<T extends RestAddressableModel, ID ex
Context context = null;
try {
context = obtainContext();
T entity = createAndReturn(context);
T entity = thisRepository.createAndReturn(context);
context.commit();
return entity;
} catch (AuthorizeException e) {
@@ -177,7 +185,7 @@ public abstract class DSpaceRestRepository<T extends RestAddressableModel, ID ex
throws HttpRequestMethodNotSupportedException, UnprocessableEntityException, PatchBadRequestException {
Context context = obtainContext();
try {
patch(context, request, apiCategory, model, id, patch);
thisRepository.patch(context, request, apiCategory, model, id, patch);
context.commit();
} catch (AuthorizeException ae) {
throw new RESTAuthorizationException(ae);

View File

@@ -34,6 +34,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
@@ -94,6 +95,7 @@ public class EPersonRestRepository extends DSpaceRestRepository<EPersonRest, UUI
}
@Override
@PreAuthorize("hasPermission(#id, 'EPERSON', 'READ')")
public EPersonRest findOne(Context context, UUID id) {
EPerson eperson = null;
try {
@@ -108,6 +110,7 @@ public class EPersonRestRepository extends DSpaceRestRepository<EPersonRest, UUI
}
@Override
@PreAuthorize("hasAuthority('ADMIN')")
public Page<EPersonRest> findAll(Context context, Pageable pageable) {
List<EPerson> epersons = null;
int total = 0;
@@ -117,7 +120,7 @@ public class EPersonRestRepository extends DSpaceRestRepository<EPersonRest, UUI
"The EPerson collection endpoint is reserved to system administrators");
}
total = es.countTotal(context);
epersons = es.findAll(context, EPerson.ID, pageable.getPageSize(), pageable.getOffset());
epersons = es.findAll(context, EPerson.EMAIL, pageable.getPageSize(), pageable.getOffset());
} catch (SQLException e) {
throw new RuntimeException(e.getMessage(), e);
}

View File

@@ -22,6 +22,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
/**
@@ -38,6 +39,7 @@ public class GroupRestRepository extends DSpaceRestRepository<GroupRest, UUID> {
GroupConverter converter;
@Override
@PreAuthorize("hasPermission(#id, 'GROUP', 'READ')")
public GroupRest findOne(Context context, UUID id) {
Group group = null;
try {
@@ -51,6 +53,7 @@ public class GroupRestRepository extends DSpaceRestRepository<GroupRest, UUID> {
return converter.fromModel(group);
}
@PreAuthorize("hasAuthority('ADMIN')")
@Override
public Page<GroupRest> findAll(Context context, Pageable pageable) {
List<Group> groups = null;

View File

@@ -32,6 +32,7 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
/**
@@ -63,6 +64,7 @@ public class ItemRestRepository extends DSpaceRestRepository<ItemRest, UUID> {
}
@Override
@PreAuthorize("hasPermission(#id, 'ITEM', 'READ')")
public ItemRest findOne(Context context, UUID id) {
Item item = null;
try {
@@ -77,6 +79,7 @@ public class ItemRestRepository extends DSpaceRestRepository<ItemRest, UUID> {
}
@Override
@PreAuthorize("hasAuthority('ADMIN')")
public Page<ItemRest> findAll(Context context, Pageable pageable) {
Iterator<Item> it = null;
List<Item> items = new ArrayList<Item>();

View File

@@ -21,6 +21,7 @@ import org.dspace.core.Context;
import org.dspace.core.service.LicenseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
/**
@@ -43,6 +44,7 @@ public class LicenseRestLinkRepository extends AbstractDSpaceRestRepository
return new LicenseResource(model);
}
@PreAuthorize("hasAuthority('AUTHENTICATED')")
public LicenseRest getLicenseCollection(HttpServletRequest request, UUID uuid, Pageable pageable, String projection)
throws Exception {
Context context = obtainContext();

View File

@@ -20,6 +20,7 @@ import org.dspace.core.Context;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
/**
@@ -39,6 +40,7 @@ public class ResourcePolicyRestRepository extends DSpaceRestRepository<ResourceP
@Autowired
Utils utils;
@PreAuthorize("hasAuthority('AUTHENTICATED')")
@Override
public ResourcePolicyRest findOne(Context context, Integer id) {
ResourcePolicy source = null;
@@ -53,6 +55,7 @@ public class ResourcePolicyRestRepository extends DSpaceRestRepository<ResourceP
return resourcePolicyConverter.convert(source);
}
@PreAuthorize("hasAuthority('AUTHENTICATED')")
@Override
public Page<ResourcePolicyRest> findAll(Context context, Pageable pageable) {
throw new RepositoryMethodNotImplementedException(ResourcePolicyRest.NAME, "findAll");

View File

@@ -28,6 +28,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
/**
@@ -48,6 +49,7 @@ public class SubmissionDefinitionRestRepository extends DSpaceRestRepository<Sub
submissionConfigReader = new SubmissionConfigReader();
}
@PreAuthorize("hasAuthority('AUTHENTICATED')")
@Override
public SubmissionDefinitionRest findOne(Context context, String submitName) {
SubmissionConfig subConfig = submissionConfigReader.getSubmissionConfigByName(submitName);
@@ -57,6 +59,7 @@ public class SubmissionDefinitionRestRepository extends DSpaceRestRepository<Sub
return converter.convert(subConfig);
}
@PreAuthorize("hasAuthority('AUTHENTICATED')")
@Override
public Page<SubmissionDefinitionRest> findAll(Context context, Pageable pageable) {
List<SubmissionConfig> subConfs = new ArrayList<SubmissionConfig>();
@@ -66,6 +69,7 @@ public class SubmissionDefinitionRestRepository extends DSpaceRestRepository<Sub
return page;
}
@PreAuthorize("hasAuthority('AUTHENTICATED')")
@SearchRestMethod(name = "findByCollection")
public SubmissionDefinitionRest findByCollection(@Parameter(value = "uuid", required = true) UUID collectionUuid)
throws SQLException {

View File

@@ -21,6 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
/**
@@ -41,6 +42,7 @@ public class SubmissionFormRestRepository extends DSpaceRestRepository<Submissio
inputReader = new DCInputsReader();
}
@PreAuthorize("hasAuthority('AUTHENTICATED')")
@Override
public SubmissionFormRest findOne(Context context, String submitName) {
DCInputSet inputConfig;
@@ -55,6 +57,7 @@ public class SubmissionFormRestRepository extends DSpaceRestRepository<Submissio
return converter.convert(inputConfig);
}
@PreAuthorize("hasAuthority('AUTHENTICATED')")
@Override
public Page<SubmissionFormRest> findAll(Context context, Pageable pageable) {
List<DCInputSet> subConfs = new ArrayList<DCInputSet>();

View File

@@ -23,6 +23,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
/**
@@ -42,6 +43,7 @@ public class SubmissionPanelRestRepository extends DSpaceRestRepository<Submissi
submissionConfigReader = new SubmissionConfigReader();
}
@PreAuthorize("hasAuthority('AUTHENTICATED')")
@Override
public SubmissionSectionRest findOne(Context context, String id) {
try {
@@ -53,6 +55,7 @@ public class SubmissionPanelRestRepository extends DSpaceRestRepository<Submissi
}
}
@PreAuthorize("hasAuthority('AUTHENTICATED')")
@Override
public Page<SubmissionSectionRest> findAll(Context context, Pageable pageable) {
List<SubmissionConfig> subConfs = new ArrayList<SubmissionConfig>();

View File

@@ -30,6 +30,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
/**
@@ -61,6 +62,7 @@ public class SubmissionUploadRestRepository extends DSpaceRestRepository<Submiss
submissionConfigReader = new SubmissionConfigReader();
}
@PreAuthorize("hasAuthority('AUTHENTICATED')")
@Override
public SubmissionUploadRest findOne(Context context, String submitName) {
UploadConfiguration config = uploadConfigurationService.getMap().get(submitName);
@@ -72,6 +74,7 @@ public class SubmissionUploadRestRepository extends DSpaceRestRepository<Submiss
return null;
}
@PreAuthorize("hasAuthority('AUTHENTICATED')")
@Override
public Page<SubmissionUploadRest> findAll(Context context, Pageable pageable) {
List<SubmissionConfig> subConfs = new ArrayList<SubmissionConfig>();

View File

@@ -93,6 +93,7 @@ public class WorkspaceItemRestRepository extends DSpaceRestRepository<WorkspaceI
submissionConfigReader = new SubmissionConfigReader();
}
//TODO @PreAuthorize("hasPermission(#id, 'WORKSPACEITEM', 'READ')")
@Override
public WorkspaceItemRest findOne(Context context, Integer id) {
WorkspaceItem witem = null;
@@ -107,6 +108,7 @@ public class WorkspaceItemRestRepository extends DSpaceRestRepository<WorkspaceI
return converter.fromModel(witem);
}
//TODO @PreAuthorize("hasAuthority('ADMIN')")
@Override
public Page<WorkspaceItemRest> findAll(Context context, Pageable pageable) {
List<WorkspaceItem> witems = null;
@@ -121,6 +123,7 @@ public class WorkspaceItemRestRepository extends DSpaceRestRepository<WorkspaceI
return page;
}
//TODO @PreAuthorize("hasPermission(#submitterID, 'EPERSON', 'READ')")
@SearchRestMethod(name = "findBySubmitter")
public Page<WorkspaceItemRest> findBySubmitter(@Parameter(value = "uuid", required = true) UUID submitterID,
Pageable pageable) {
@@ -194,6 +197,7 @@ public class WorkspaceItemRestRepository extends DSpaceRestRepository<WorkspaceI
return new WorkspaceItemResource(witem, utils, rels);
}
//TODO @PreAuthorize("hasPermission(#id, 'WORKSPACEITEM', 'WRITE')")
@Override
public UploadBitstreamRest upload(HttpServletRequest request, String apiCategory, String model, Integer id,
String extraField, MultipartFile file) throws Exception {
@@ -243,6 +247,7 @@ public class WorkspaceItemRestRepository extends DSpaceRestRepository<WorkspaceI
return result;
}
//TODO @PreAuthorize("hasPermission(#id, 'WORKSPACEITEM', 'WRITE')")
@Override
public void patch(Context context, HttpServletRequest request, String apiCategory, String model, Integer id,
Patch patch) throws SQLException, AuthorizeException {
@@ -304,6 +309,7 @@ public class WorkspaceItemRestRepository extends DSpaceRestRepository<WorkspaceI
}
}
//TODO @PreAuthorize("hasPermission(#id, 'WORKSPACEITEM', 'DELETE')")
@Override
protected void delete(Context context, Integer id) throws AuthorizeException {
WorkspaceItem witem = null;

View File

@@ -0,0 +1,74 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.security;
import java.io.Serializable;
import java.sql.SQLException;
import org.dspace.app.rest.utils.ContextUtil;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.service.EPersonService;
import org.dspace.services.RequestService;
import org.dspace.services.model.Request;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
/**
* Administrators are always allowed to perform any action on any DSpace object. This plugin will check if
* the authenticated EPerson is an administrator of the provided target DSpace Object. If that is the case,
* the authenticated EPerson is allowed to perform the requested action.
*/
@Component
public class AdminRestPermissionEvaluatorPlugin extends DSpaceObjectPermissionEvaluatorPlugin {
private static final Logger log = LoggerFactory.getLogger(DSpaceObjectPermissionEvaluatorPlugin.class);
@Autowired
private AuthorizeService authorizeService;
@Autowired
private RequestService requestService;
@Autowired
private EPersonService ePersonService;
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType,
Object permission) {
//We do not check the "permission" object here because administrators are allowed to do everything
Request request = requestService.getCurrentRequest();
Context context = ContextUtil.obtainContext(request.getServletRequest());
EPerson ePerson = null;
try {
ePerson = ePersonService.findByEmail(context, (String) authentication.getPrincipal());
if (ePerson != null) {
//Check if user is a repository admin
if (authorizeService.isAdmin(context, ePerson)) {
return true;
}
//We don't check the DSO Admin level here as this is action specific.
//For example see org.dspace.authorize.AuthorizeConfiguration.canCollectionAdminPerformItemDeletion
}
} catch (SQLException e) {
log.error(e.getMessage(), e);
}
return false;
}
}

View File

@@ -0,0 +1,91 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.security;
import java.io.Serializable;
import java.sql.SQLException;
import java.util.UUID;
import org.dspace.app.rest.utils.ContextUtil;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.content.DSpaceObject;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.DSpaceObjectService;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.service.EPersonService;
import org.dspace.services.RequestService;
import org.dspace.services.model.Request;
import org.dspace.util.UUIDUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
/**
* DSpaceObjectPermissionEvaluatorPlugin will check persmissions based on the DSpace {@link AuthorizeService}.
* This service will validate if the authenticated user is allowed to perform an action on the given DSpace Object
* based on the resource policies attached to that DSpace object.
*/
@Component
public class AuthorizeServicePermissionEvaluatorPlugin extends DSpaceObjectPermissionEvaluatorPlugin {
private static final Logger log = LoggerFactory.getLogger(AuthorizeServicePermissionEvaluatorPlugin.class);
@Autowired
private AuthorizeService authorizeService;
@Autowired
private RequestService requestService;
@Autowired
private EPersonService ePersonService;
@Autowired
private ContentServiceFactory contentServiceFactory;
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType,
Object permission) {
DSpaceRestPermission restPermission = DSpaceRestPermission.convert(permission);
if (restPermission == null) {
return false;
}
Request request = requestService.getCurrentRequest();
Context context = ContextUtil.obtainContext(request.getServletRequest());
EPerson ePerson = null;
try {
ePerson = ePersonService.findByEmail(context, (String) authentication.getPrincipal());
UUID dsoId = UUIDUtils.fromString(targetId.toString());
DSpaceObjectService dSpaceObjectService =
contentServiceFactory.getDSpaceObjectService(Constants.getTypeID(targetType));
if (dSpaceObjectService != null && dsoId != null) {
DSpaceObject dSpaceObject = dSpaceObjectService.find(context, dsoId);
//If the dso is null then we give permission so we can throw another status code instead
if (dSpaceObject == null) {
return true;
}
return authorizeService.authorizeActionBoolean(context, ePerson, dSpaceObject,
restPermission.getDspaceApiActionId(), false);
}
} catch (SQLException e) {
log.error(e.getMessage(), e);
}
return false;
}
}

View File

@@ -0,0 +1,41 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.security;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
/**
* Spring security authentication entry point to return a 401 response for unauthorized requests
* This class is used in the {@link WebSecurityConfiguration} class.
*/
public class DSpace401AuthenticationEntryPoint implements AuthenticationEntryPoint {
private RestAuthenticationService restAuthenticationService;
public DSpace401AuthenticationEntryPoint(RestAuthenticationService restAuthenticationService) {
this.restAuthenticationService = restAuthenticationService;
}
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.setHeader("WWW-Authenticate",
restAuthenticationService.getWwwAuthenticateHeaderValue(request, response));
response.sendError(HttpServletResponse.SC_UNAUTHORIZED,
authException.getMessage());
}
}

View File

@@ -0,0 +1,38 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.security;
import org.dspace.app.rest.model.DSpaceObjectRest;
import org.springframework.security.core.Authentication;
/**
* Abstract {@link RestPermissionEvaluatorPlugin} class that contains utility methods to
* evaluate permissions for a DSpace Object.
*/
public abstract class DSpaceObjectPermissionEvaluatorPlugin implements RestPermissionEvaluatorPlugin {
/**
* Utility implementation to make the implementation of DSpace Object Permission evaluator plugins more easy.
*
* @param authentication represents the user in question. Should not be null.
* @param targetDomainObject the DSpace object for which permissions should be
* checked. May be null in which case implementations should return false, as the null
* condition can be checked explicitly in the expression.
* @param permission a representation of the DSpace action as supplied by the
* expression system. This corresponds to the DSpace action. Not null.
* @return true if the permission is granted by one of the plugins, false otherwise
*/
public boolean hasPermission(Authentication authentication, Object targetDomainObject,
Object permission) {
DSpaceObjectRest dSpaceObject = (DSpaceObjectRest) targetDomainObject;
return hasPermission(authentication, dSpaceObject.getId(), dSpaceObject.getType(), permission);
}
}

View File

@@ -0,0 +1,71 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.security;
import java.io.Serializable;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
/**
* DSpace permission evaluator.
* To check if a user has permission to a target object, a list of permissionEvaluatorPlugins will be checked.
*
* The following list of plugins exists: EPersonRestPermissionEvaluatorPlugin, AdminRestPermissionEvaluatorPlugin,
* AuthorizeServicePermissionEvaluatorPlugin, GroupRestPermissionEvaluatorPlugin
*/
@Component
public class DSpacePermissionEvaluator implements PermissionEvaluator {
@Autowired
private List<RestPermissionEvaluatorPlugin> permissionEvaluatorPluginList;
/**
*
* @param authentication represents the user in question. Should not be null.
* @param targetDomainObject the DSpace object for which permissions should be
* checked. May be null in which case implementations should return false, as the null
* condition can be checked explicitly in the expression.
* @param permission a representation of the DSpace action as supplied by the
* expression system. This corresponds to the DSpace action. Not null.
* @return true if the permission is granted by one of the plugins, false otherwise
*/
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
for (RestPermissionEvaluatorPlugin permissionEvaluatorPlugin : permissionEvaluatorPluginList) {
if (permissionEvaluatorPlugin.hasPermission(authentication, targetDomainObject, permission)) {
return true;
}
}
return false;
}
/**
* Alternative method for evaluating a permission where only the identifier of the
* target object is available, rather than the target instance itself.
*
* @param authentication represents the user in question. Should not be null.
* @param targetId the UUID for the DSpace object
* @param targetType represents the DSpace object type of the target object. Not null.
* @param permission a representation of the permission object as supplied by the
* expression system. This corresponds to the DSpace action. Not null.
* @return true if the permission is granted by one of the plugins, false otherwise
*/
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType,
Object permission) {
for (RestPermissionEvaluatorPlugin permissionEvaluatorPlugin : permissionEvaluatorPluginList) {
if (permissionEvaluatorPlugin.hasPermission(authentication, targetId, targetType, permission)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,52 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.security;
import org.dspace.core.Constants;
/**
* Enum that lists all available "permissions" an authenticated user can have on a specific REST endpoint.
*/
public enum DSpaceRestPermission {
READ(Constants.READ),
WRITE(Constants.WRITE),
DELETE(Constants.DELETE);
private int dspaceApiActionId;
DSpaceRestPermission(int dspaceApiActionId) {
this.dspaceApiActionId = dspaceApiActionId;
}
public int getDspaceApiActionId() {
return dspaceApiActionId;
}
/**
* Convert a given object to a {@link DSpaceRestPermission} if possible.
* @param object The object to convert
* @return A DSpaceRestPersmission value if the conversion succeeded, null otherwise
*/
public static DSpaceRestPermission convert(Object object) {
if (object == null) {
return null;
} else if (object instanceof DSpaceRestPermission) {
return (DSpaceRestPermission) object;
} else if (object instanceof String) {
try {
return DSpaceRestPermission.valueOf((String) object);
} catch (IllegalArgumentException ex) {
return null;
}
} else {
return null;
}
}
}

View File

@@ -8,7 +8,7 @@
package org.dspace.app.rest.security;
import static org.dspace.app.rest.security.WebSecurityConfiguration.ADMIN_GRANT;
import static org.dspace.app.rest.security.WebSecurityConfiguration.EPERSON_GRANT;
import static org.dspace.app.rest.security.WebSecurityConfiguration.AUTHENTICATED_GRANT;
import java.sql.SQLException;
import java.util.LinkedList;
@@ -150,7 +150,7 @@ public class EPersonRestAuthenticationProvider implements AuthenticationProvider
authorities.add(new SimpleGrantedAuthority(ADMIN_GRANT));
}
authorities.add(new SimpleGrantedAuthority(EPERSON_GRANT));
authorities.add(new SimpleGrantedAuthority(AUTHENTICATED_GRANT));
}
return authorities;

View File

@@ -0,0 +1,73 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.security;
import java.io.Serializable;
import java.sql.SQLException;
import java.util.UUID;
import org.dspace.app.rest.utils.ContextUtil;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.service.EPersonService;
import org.dspace.services.RequestService;
import org.dspace.services.model.Request;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
/**
* An authenicated user is allowed to view, update or delete his or her own data. This {@link RestPermissionEvaluatorPlugin}
* implemenents that requirement.
*/
@Component
public class EPersonRestPermissionEvaluatorPlugin extends DSpaceObjectPermissionEvaluatorPlugin {
private static final Logger log = LoggerFactory.getLogger(EPersonRestPermissionEvaluatorPlugin.class);
@Autowired
private RequestService requestService;
@Autowired
private EPersonService ePersonService;
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId,
String targetType, Object permission) {
//For now this plugin only evaluates READ access
DSpaceRestPermission restPermission = DSpaceRestPermission.convert(permission);
if (!DSpaceRestPermission.READ.equals(restPermission)
&& !DSpaceRestPermission.WRITE.equals(restPermission)
&& !DSpaceRestPermission.DELETE.equals(restPermission)) {
return false;
}
if (Constants.getTypeID(targetType) != Constants.EPERSON) {
return false;
}
Request request = requestService.getCurrentRequest();
Context context = ContextUtil.obtainContext(request.getServletRequest());
EPerson ePerson = null;
try {
ePerson = ePersonService.findByEmail(context, (String) authentication.getPrincipal());
UUID dsoId = UUID.fromString(targetId.toString());
if (dsoId.equals(ePerson.getID())) {
return true;
}
} catch (SQLException e) {
log.error(e.getMessage(), e);
}
return false;
}
}

View File

@@ -0,0 +1,76 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.security;
import java.io.Serializable;
import java.sql.SQLException;
import java.util.UUID;
import org.dspace.app.rest.utils.ContextUtil;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;
import org.dspace.eperson.service.EPersonService;
import org.dspace.eperson.service.GroupService;
import org.dspace.services.RequestService;
import org.dspace.services.model.Request;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
/**
* An authenticated user is allowed to view information on all the groups he or she is a member of (READ permission).
* This {@link RestPermissionEvaluatorPlugin} implements that requirement by validating the group membership.
*/
@Component
public class GroupRestPermissionEvaluatorPlugin extends DSpaceObjectPermissionEvaluatorPlugin {
private static final Logger log = LoggerFactory.getLogger(GroupRestPermissionEvaluatorPlugin.class);
@Autowired
private RequestService requestService;
@Autowired
private GroupService groupService;
@Autowired
private EPersonService ePersonService;
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId,
String targetType, Object permission) {
//This plugin only evaluates READ access
DSpaceRestPermission restPermission = DSpaceRestPermission.convert(permission);
if (!DSpaceRestPermission.READ.equals(restPermission)
|| Constants.getTypeID(targetType) != Constants.GROUP) {
return false;
}
Request request = requestService.getCurrentRequest();
Context context = ContextUtil.obtainContext(request.getServletRequest());
EPerson ePerson = null;
try {
ePerson = ePersonService.findByEmail(context, (String) authentication.getPrincipal());
UUID dsoId = UUID.fromString(targetId.toString());
Group group = groupService.find(context, dsoId);
if (groupService.isMember(context, ePerson, group)) {
return true;
}
} catch (SQLException e) {
log.error(e.getMessage(), e);
}
return false;
}
}

View File

@@ -0,0 +1,32 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Autowired
private PermissionEvaluator dSpacePermissionEvaluator;
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler =
new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(dSpacePermissionEvaluator);
return expressionHandler;
}
}

View File

@@ -35,4 +35,13 @@ public interface RestAuthenticationService {
void invalidateAuthenticationData(HttpServletRequest request, Context context) throws Exception;
AuthenticationService getAuthenticationService();
/**
* Return the value that should be passed in the WWWW-Authenticate header for 4xx responses to the client
* @param request The current client request
* @param response The response being build for the client
* @return A string value that should be set in the WWWW-Authenticate header
*/
String getWwwAuthenticateHeaderValue(HttpServletRequest request, HttpServletResponse response);
}

View File

@@ -0,0 +1,47 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.security;
import java.io.Serializable;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
/**
* Interface to define a permission evaluator plugin. These plugins are used in the DSpace {@link PermissionEvaluator}
* implementation {@link DSpacePermissionEvaluator} to check if an authenticated user has permission to perform a
* certain action on a certain object.
*
* If you implement a this interface in a Spring bean, it will be automatically taken into account when evaluating
* permissions.
*/
public interface RestPermissionEvaluatorPlugin {
/**
* Check in the authenticated user (provided by the {@link Authentication} object) has the specified permission on
* the provided target object.
* @param authentication Authentication object providing user details of the authenticated user
* @param targetDomainObject The target object that the authenticated user wants to see or manipulate
* @param permission Permission object that describes the action the user wants to perform on the target object
* @return true if the user is allowed to perform the action described by the permission. False otherwise.
*/
boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission);
/**
* Check in the authenticated user (provided by the {@link Authentication} object) has the specified permission on
* the target object with the provided identifier.
* @param authentication Authentication object providing user details of the authenticated user
* @param targetId Unique identifier of the target object the user wants to view or manipulate
* @param targetType Type of the target object the users wants to view or manipulate
* @param permission Permission object that describes the action the user wants to perform on the target object
* @return true if the user is allowed to perform the action described by the permission. False otherwise.
*/
boolean hasPermission(Authentication authentication, Serializable targetId, String targetType,
Object permission);
}

View File

@@ -9,17 +9,11 @@ package org.dspace.app.rest.security;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.dspace.app.rest.utils.ContextUtil;
import org.dspace.authenticate.AuthenticationMethod;
import org.dspace.authenticate.service.AuthenticationService;
import org.dspace.core.Context;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
@@ -77,31 +71,10 @@ public class StatelessLoginFilter extends AbstractAuthenticationProcessingFilter
HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
AuthenticationService authenticationService = restAuthenticationService.getAuthenticationService();
String authenticateHeaderValue = restAuthenticationService.getWwwAuthenticateHeaderValue(request, response);
Iterator<AuthenticationMethod> authenticationMethodIterator
= authenticationService.authenticationMethodIterator();
Context context = ContextUtil.obtainContext(request);
StringBuilder wwwAuthenticate = new StringBuilder();
while (authenticationMethodIterator.hasNext()) {
AuthenticationMethod authenticationMethod = authenticationMethodIterator.next();
if (wwwAuthenticate.length() > 0) {
wwwAuthenticate.append(", ");
}
wwwAuthenticate.append(authenticationMethod.getName()).append(" realm=\"DSpace REST API\"");
String loginPageURL = authenticationMethod.loginPageURL(context, request, response);
if (StringUtils.isNotBlank(loginPageURL)) {
// We cannot reply with a 303 code because may browsers handle 3xx response codes transparently. This
// means that the JavaScript client code is not aware of the 303 status and fails to react accordingly.
wwwAuthenticate.append(", location=\"").append(loginPageURL).append("\"");
}
}
response.setHeader("WWW-Authenticate", wwwAuthenticate.toString());
response.setHeader("WWW-Authenticate", authenticateHeaderValue);
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, failed.getMessage());
}
}

View File

@@ -15,12 +15,12 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@@ -34,10 +34,11 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@EnableWebSecurity
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
public static final String ADMIN_GRANT = "ADMIN";
public static final String EPERSON_GRANT = "EPERSON";
public static final String AUTHENTICATED_GRANT = "AUTHENTICATED";
public static final String ANONYMOUS_GRANT = "ANONYMOUS";
@Autowired
@@ -69,9 +70,6 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
http
//Tell Spring to not create Sessions
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
//Return the login URL when having an access denied error
.exceptionHandling().authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/api/authn/login"))
.and()
//Anonymous requests should have the "ANONYMOUS" security grant
.anonymous().authorities(ANONYMOUS_GRANT).and()
//Wire up the HttpServletRequest with the current SecurityContext values
@@ -79,6 +77,10 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
//Disable CSRF as our API can be used by clients on an other domain, we are also protected against this,
// since we pass the token in a header
.csrf().disable()
//Return 401 on authorization failures with a correct WWWW-Authenticate header
.exceptionHandling().authenticationEntryPoint(
new DSpace401AuthenticationEntryPoint(restAuthenticationService))
.and()
//Logout configuration
.logout()

View File

@@ -10,6 +10,7 @@ package org.dspace.app.rest.security.jwt;
import java.io.IOException;
import java.sql.SQLException;
import java.text.ParseException;
import java.util.Iterator;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -19,6 +20,7 @@ import org.apache.commons.lang.StringUtils;
import org.dspace.app.rest.security.DSpaceAuthentication;
import org.dspace.app.rest.security.RestAuthenticationService;
import org.dspace.app.rest.utils.ContextUtil;
import org.dspace.authenticate.AuthenticationMethod;
import org.dspace.authenticate.service.AuthenticationService;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
@@ -111,6 +113,33 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication
return authenticationService;
}
@Override
public String getWwwAuthenticateHeaderValue(final HttpServletRequest request, final HttpServletResponse response) {
Iterator<AuthenticationMethod> authenticationMethodIterator
= authenticationService.authenticationMethodIterator();
Context context = ContextUtil.obtainContext(request);
StringBuilder wwwAuthenticate = new StringBuilder();
while (authenticationMethodIterator.hasNext()) {
AuthenticationMethod authenticationMethod = authenticationMethodIterator.next();
if (wwwAuthenticate.length() > 0) {
wwwAuthenticate.append(", ");
}
wwwAuthenticate.append(authenticationMethod.getName()).append(" realm=\"DSpace REST API\"");
String loginPageURL = authenticationMethod.loginPageURL(context, request, response);
if (org.apache.commons.lang3.StringUtils.isNotBlank(loginPageURL)) {
// We cannot reply with a 303 code because may browsers handle 3xx response codes transparently. This
// means that the JavaScript client code is not aware of the 303 status and fails to react accordingly.
wwwAuthenticate.append(", location=\"").append(loginPageURL).append("\"");
}
}
return wwwAuthenticate.toString();
}
private void addTokenToResponse(final HttpServletResponse response, final String token) throws IOException {
response.setHeader(AUTHORIZATION_HEADER, String.format("%s %s", AUTHORIZATION_TYPE, token));
}

View File

@@ -21,6 +21,7 @@ import org.dspace.app.rest.repository.LinkRestRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.ConversionException;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
@@ -60,7 +61,11 @@ public class RestRepositoryUtils {
*/
public boolean haveSearchMethods(DSpaceRestRepository repository) {
for (Method method : repository.getClass().getMethods()) {
SearchRestMethod ann = method.getAnnotation(SearchRestMethod.class);
// We need to use AnnotationUtils because the DSpaceRestRepository is possibly enhanced by a Spring AOP
// proxy. The regular "method.getAnnotation()" method would then search the proxy instead of the
// underlying actual class. The proxy does not inherit the annotations.
SearchRestMethod ann =
AnnotationUtils.findAnnotation(method, SearchRestMethod.class);
if (ann != null) {
return true;
}
@@ -75,7 +80,10 @@ public class RestRepositoryUtils {
public List<String> listSearchMethods(DSpaceRestRepository repository) {
List<String> searchMethods = new LinkedList<String>();
for (Method method : repository.getClass().getMethods()) {
SearchRestMethod ann = method.getAnnotation(SearchRestMethod.class);
// We need to use AnnotationUtils because the DSpaceRestRepository is possibly enhanced by a Spring AOP
// proxy. The regular "method.getAnnotation()" method would then search the proxy instead of the
// underlying actual class. The proxy does not inherit the annotations.
SearchRestMethod ann = AnnotationUtils.findAnnotation(method, SearchRestMethod.class);
if (ann != null) {
String name = ann.name();
if (name.isEmpty()) {
@@ -96,8 +104,15 @@ public class RestRepositoryUtils {
*/
public Method getSearchMethod(String searchMethodName, DSpaceRestRepository repository) {
Method searchMethod = null;
for (Method method : repository.getClass().getMethods()) {
SearchRestMethod ann = method.getAnnotation(SearchRestMethod.class);
// DSpaceRestRepository is possibly enhanced with a Spring AOP proxy. Therefor use ClassUtils to determine
// the underlying implementation class.
Method[] methods = ClassUtils.getUserClass(repository.getClass()).getMethods();
for (Method method : methods) {
// We need to use AnnotationUtils because the DSpaceRestRepository is possibly enhanced by a Spring AOP
// proxy. The regular "method.getAnnotation()" method would then search the proxy instead of the
// underlying actual class. The proxy does not inherit the annotations.
SearchRestMethod ann =
AnnotationUtils.findAnnotation(method, SearchRestMethod.class);
if (ann != null) {
String name = ann.name();
if (name.isEmpty()) {

View File

@@ -88,15 +88,15 @@ public class BitstreamRestRepositoryIT extends AbstractControllerIntegrationTest
.build();
}
getClient().perform(get("/api/core/bitstreams/"))
String token = getAuthToken(admin.getEmail(), password);
getClient(token).perform(get("/api/core/bitstreams/"))
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$._embedded.bitstreams", Matchers.containsInAnyOrder(
BitstreamMatcher.matchBitstreamEntry(bitstream),
BitstreamMatcher.matchBitstreamEntry(bitstream1)
)))
;
)));
}
@Test
@@ -145,7 +145,9 @@ public class BitstreamRestRepositoryIT extends AbstractControllerIntegrationTest
.build();
}
getClient().perform(get("/api/core/bitstreams/")
String token = getAuthToken(admin.getEmail(), password);
getClient(token).perform(get("/api/core/bitstreams/")
.param("size", "1"))
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
@@ -160,7 +162,7 @@ public class BitstreamRestRepositoryIT extends AbstractControllerIntegrationTest
;
getClient().perform(get("/api/core/bitstreams/")
getClient(token).perform(get("/api/core/bitstreams/")
.param("size", "1")
.param("page", "1"))
.andExpect(status().isOk())
@@ -172,9 +174,10 @@ public class BitstreamRestRepositoryIT extends AbstractControllerIntegrationTest
Matchers.contains(
BitstreamMatcher.matchBitstreamEntry(bitstream)
)
)))
)));
;
getClient().perform(get("/api/core/bitstreams/"))
.andExpect(status().isUnauthorized());
}
//TODO Re-enable test after https://jira.duraspace.org/browse/DS-3774 is fixed
@@ -413,7 +416,9 @@ public class BitstreamRestRepositoryIT extends AbstractControllerIntegrationTest
.build();
Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build();
getClient().perform(get("/api/core/bitstreams/" + UUID.randomUUID()))
String token = getAuthToken(admin.getEmail(), password);
getClient(token).perform(get("/api/core/bitstreams/" + UUID.randomUUID()))
.andExpect(status().isNotFound())
;
@@ -462,7 +467,7 @@ public class BitstreamRestRepositoryIT extends AbstractControllerIntegrationTest
.andExpect(status().is(204));
// Verify 404 after delete
getClient().perform(get("/api/core/bitstreams/" + bitstream.getID()))
getClient(token).perform(get("/api/core/bitstreams/" + bitstream.getID()))
.andExpect(status().isNotFound());
}

View File

@@ -322,7 +322,7 @@ public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTe
.withIssueDate("2015-03-12")
.withAuthor("Duck, Donald")
.withSubject("Cartoons").withSubject("Ducks")
.makePrivate()
.makeUnDiscoverable()
.build();
//4. An item with an item-level embargo

View File

@@ -1735,7 +1735,7 @@ public class DiscoveryRestControllerIT extends AbstractControllerIntegrationTest
.withIssueDate("1990-02-13")
.withAuthor("Smith, Maria").withAuthor("Doe, Jane").withAuthor("Testing, Works")
.withSubject("TestingForMore").withSubject("ExtraEntry")
.makePrivate()
.makeUnDiscoverable()
.build();
Item publicItem3 = ItemBuilder.createItem(context, col2)
@@ -1987,7 +1987,7 @@ public class DiscoveryRestControllerIT extends AbstractControllerIntegrationTest
.withAuthor("test2, test2").withAuthor("Maybe, Maybe")
.withSubject("AnotherTest").withSubject("TestingForMore")
.withSubject("ExtraEntry")
.makePrivate()
.makeUnDiscoverable()
.build();
UUID scope = col2.getID();
@@ -2142,7 +2142,7 @@ public class DiscoveryRestControllerIT extends AbstractControllerIntegrationTest
.withIssueDate("2010-02-13")
.withAuthor("Smith, Maria").withAuthor("Doe, Jane")
.withSubject("AnotherTest").withSubject("ExtraEntry")
.makePrivate()
.makeUnDiscoverable()
.build();

View File

@@ -95,12 +95,16 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest {
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$._embedded.epersons", Matchers.containsInAnyOrder(
EPersonMatcher.matchEPersonEntry(newUser),
EPersonMatcher.matchDefaultTestEPerson(),
EPersonMatcher.matchDefaultTestEPerson()
EPersonMatcher.matchEPersonOnEmail(admin.getEmail()),
EPersonMatcher.matchEPersonOnEmail(eperson.getEmail())
)))
.andExpect(jsonPath("$.page.size", is(20)))
.andExpect(jsonPath("$.page.totalElements", is(3)))
;
getClient().perform(get("/api/eperson/epersons"))
.andExpect(status().isUnauthorized())
;
}
@Test
@@ -134,24 +138,24 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest {
public void findAllPaginationTest() throws Exception {
context.turnOffAuthorisationSystem();
EPerson ePerson = EPersonBuilder.createEPerson(context)
EPerson testEPerson = EPersonBuilder.createEPerson(context)
.withNameInMetadata("John", "Doe")
.withEmail("Johndoe@fake-email.com")
.build();
String authToken = getAuthToken(admin.getEmail(), password);
// using size = 2 the first page will contains our test user and admin
getClient(authToken).perform(get("/api/eperson/eperson")
getClient(authToken).perform(get("/api/eperson/epersons")
.param("size", "2"))
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$._embedded.epersons", Matchers.containsInAnyOrder(
EPersonMatcher.matchDefaultTestEPerson(),
EPersonMatcher.matchDefaultTestEPerson()
EPersonMatcher.matchEPersonEntry(admin),
EPersonMatcher.matchEPersonEntry(testEPerson)
)))
.andExpect(jsonPath("$._embedded.epersons", Matchers.not(
Matchers.contains(
EPersonMatcher.matchEPersonEntry(ePerson)
EPersonMatcher.matchEPersonEntry(admin)
)
)))
.andExpect(jsonPath("$.page.size", is(2)))
@@ -159,17 +163,22 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest {
;
// using size = 2 the first page will contains our test user and admin
getClient(authToken).perform(get("/api/eperson/eperson")
getClient(authToken).perform(get("/api/eperson/epersons")
.param("size", "2")
.param("page", "1"))
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$._embedded.epersons", Matchers.contains(
EPersonMatcher.matchEPersonEntry(ePerson)
EPersonMatcher.matchEPersonEntry(eperson)
)))
.andExpect(jsonPath("$._embedded.epersons", Matchers.hasSize(1)))
.andExpect(jsonPath("$.page.size", is(2)))
.andExpect(jsonPath("$.page.totalElements", is(3)))
;
getClient().perform(get("/api/eperson/epersons"))
.andExpect(status().isUnauthorized())
;
}
@@ -203,7 +212,7 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest {
}
@Test
public void findOneRelsTest() throws Exception {
public void readEpersonAuthorizationTest() throws Exception {
context.turnOffAuthorisationSystem();
EPerson ePerson = EPersonBuilder.createEPerson(context)
@@ -225,24 +234,41 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest {
)))
.andExpect(jsonPath("$", Matchers.not(
is(
EPersonMatcher.matchEPersonEntry(ePerson)
EPersonMatcher.matchEPersonEntry(eperson)
)
)))
.andExpect(jsonPath("$._links.self.href",
Matchers.containsString("/api/eperson/epersons/" + ePerson2.getID())));
}
//EPerson can only access himself
String epersonToken = getAuthToken(eperson.getEmail(), password);
getClient(epersonToken).perform(get("/api/eperson/epersons/" + ePerson2.getID()))
.andExpect(status().isForbidden());
getClient(epersonToken).perform(get("/api/eperson/epersons/" + eperson.getID()))
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$", is(
EPersonMatcher.matchEPersonOnEmail(eperson.getEmail())
)))
.andExpect(jsonPath("$._links.self.href",
Matchers.containsString("/api/eperson/epersons/" + eperson.getID())));
}
@Test
public void findOneTestWrongUUID() throws Exception {
context.turnOffAuthorisationSystem();
EPerson ePerson = EPersonBuilder.createEPerson(context)
EPerson testEPerson1 = EPersonBuilder.createEPerson(context)
.withNameInMetadata("John", "Doe")
.withEmail("Johndoe@fake-email.com")
.build();
EPerson ePerson2 = EPersonBuilder.createEPerson(context)
EPerson testEPerson2 = EPersonBuilder.createEPerson(context)
.withNameInMetadata("Jane", "Smith")
.withEmail("janesmith@fake-email.com")
.build();

View File

@@ -19,23 +19,25 @@ public class EmptyRestRepositoryIT extends AbstractControllerIntegrationTest {
@Test
public void findAllTest() throws Exception {
String token = getAuthToken(admin.getEmail(), password);
//Test retrieval of all communities while none exist
getClient().perform(get("/api/core/communities"))
getClient(token).perform(get("/api/core/communities"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.page.totalElements", is(0)));
//Test retrieval of all collections while none exist
getClient().perform(get("/api/core/collections"))
getClient(token).perform(get("/api/core/collections"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.page.totalElements", is(0)));
//Test retrieval of all items while none exist
getClient().perform(get("/api/core/items"))
getClient(token).perform(get("/api/core/items"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.page.totalElements", is(0)));
//Test retrieval of all bitstreams while none exist
getClient().perform(get("/api/core/bitstreams"))
getClient(token).perform(get("/api/core/bitstreams"))
. andExpect(status().isOk())
.andExpect(jsonPath("$.page.totalElements", is(0)));
}

View File

@@ -31,8 +31,16 @@ public class GroupRestRepositoryIT extends AbstractControllerIntegrationTest {
@Test
public void findAllTest() throws Exception {
//When we call the root endpoint
getClient().perform(get("/api/eperson/groups"))
//The status has to be 403 Not Authorized
.andExpect(status().isUnauthorized());
String token = getAuthToken(admin.getEmail(), password);
//When we call the root endpoint
getClient(token).perform(get("/api/eperson/groups"))
//The status has to be 200 OK
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
@@ -48,8 +56,16 @@ public class GroupRestRepositoryIT extends AbstractControllerIntegrationTest {
@Test
public void findAllPaginationTest() throws Exception {
context.turnOffAuthorisationSystem();
getClient().perform(get("/api/eperson/groups"))
//The status has to be 403 Not Authorized
.andExpect(status().isUnauthorized());
String token = getAuthToken(admin.getEmail(), password);
//When we call the root endpoint
getClient(token).perform(get("/api/eperson/groups"))
//The status has to be 200 OK
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
@@ -69,9 +85,11 @@ public class GroupRestRepositoryIT extends AbstractControllerIntegrationTest {
.withName(testGroupName)
.build();
String token = getAuthToken(admin.getEmail(), password);
String generatedGroupId = group.getID().toString();
String groupIdCall = "/api/eperson/groups/" + generatedGroupId;
getClient().perform(get(groupIdCall))
getClient(token).perform(get(groupIdCall))
//The status has to be 200 OK
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
@@ -79,7 +97,7 @@ public class GroupRestRepositoryIT extends AbstractControllerIntegrationTest {
GroupMatcher.matchGroupEntry(group.getID(), group.getName())
)))
;
getClient().perform(get("/api/eperson/groups"))
getClient(token).perform(get("/api/eperson/groups"))
//The status has to be 200 OK
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
@@ -89,7 +107,7 @@ public class GroupRestRepositoryIT extends AbstractControllerIntegrationTest {
}
@Test
public void findOneRelsTest() throws Exception {
public void readGroupAuthorizationTest() throws Exception {
context.turnOffAuthorisationSystem();
Group group = GroupBuilder.createGroup(context)
@@ -98,21 +116,42 @@ public class GroupRestRepositoryIT extends AbstractControllerIntegrationTest {
Group group2 = GroupBuilder.createGroup(context)
.withName("Group2")
.addMember(eperson)
.build();
getClient().perform(get("/api/eperson/groups/" + group2.getID()))
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$", Matchers.is(
GroupMatcher.matchGroupEntry(group2.getID(), group2.getName())
)))
.andExpect(jsonPath("$", Matchers.not(
Matchers.is(
GroupMatcher.matchGroupEntry(group.getID(), group.getName())
)
)))
.andExpect(jsonPath("$._links.self.href",
Matchers.containsString("/api/eperson/groups/" + group2.getID())));
//Admin can access
String token = getAuthToken(admin.getEmail(), password);
getClient(token).perform(get("/api/eperson/groups/" + group2.getID()))
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$", Matchers.is(
GroupMatcher.matchGroupEntry(group2.getID(), group2.getName())
)))
.andExpect(jsonPath("$", Matchers.not(
Matchers.is(
GroupMatcher.matchGroupEntry(group.getID(), group.getName())
)
)))
.andExpect(jsonPath("$._links.self.href",
Matchers.containsString("/api/eperson/groups/" + group2.getID())));
//People in group should be able to access token
token = getAuthToken(eperson.getEmail(), password);
getClient(token).perform(get("/api/eperson/groups/" + group2.getID()))
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$", Matchers.is(
GroupMatcher.matchGroupEntry(group2.getID(), group2.getName())
)))
.andExpect(jsonPath("$", Matchers.not(
Matchers.is(
GroupMatcher.matchGroupEntry(group.getID(), group.getName())
)
)))
.andExpect(jsonPath("$._links.self.href",
Matchers.containsString("/api/eperson/groups/" + group2.getID())));
}
@Test

View File

@@ -26,6 +26,8 @@ import org.apache.commons.lang3.CharEncoding;
import org.dspace.app.rest.builder.BitstreamBuilder;
import org.dspace.app.rest.builder.CollectionBuilder;
import org.dspace.app.rest.builder.CommunityBuilder;
import org.dspace.app.rest.builder.EPersonBuilder;
import org.dspace.app.rest.builder.GroupBuilder;
import org.dspace.app.rest.builder.ItemBuilder;
import org.dspace.app.rest.builder.WorkspaceItemBuilder;
import org.dspace.app.rest.matcher.ItemMatcher;
@@ -37,6 +39,8 @@ import org.dspace.content.Collection;
import org.dspace.content.Community;
import org.dspace.content.Item;
import org.dspace.content.WorkspaceItem;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;
import org.hamcrest.Matchers;
import org.junit.Test;
@@ -80,7 +84,9 @@ public class ItemRestRepositoryIT extends AbstractControllerIntegrationTest {
.withSubject("ExtraEntry")
.build();
getClient().perform(get("/api/core/items"))
String token = getAuthToken(admin.getEmail(), password);
getClient(token).perform(get("/api/core/items"))
.andExpect(status().isOk())
.andExpect(jsonPath("$._embedded.items", Matchers.containsInAnyOrder(
ItemMatcher.matchItemWithTitleAndDateIssued(publicItem1, "Public item 1", "2017-10-17"),
@@ -131,7 +137,9 @@ public class ItemRestRepositoryIT extends AbstractControllerIntegrationTest {
.withSubject("ExtraEntry")
.build();
getClient().perform(get("/api/core/items")
String token = getAuthToken(admin.getEmail(), password);
getClient(token).perform(get("/api/core/items")
.param("size", "2"))
.andExpect(status().isOk())
.andExpect(jsonPath("$._embedded.items", Matchers.containsInAnyOrder(
@@ -146,7 +154,7 @@ public class ItemRestRepositoryIT extends AbstractControllerIntegrationTest {
.andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/core/items")))
;
getClient().perform(get("/api/core/items")
getClient(token).perform(get("/api/core/items")
.param("size", "2")
.param("page", "1"))
.andExpect(status().isOk())
@@ -312,7 +320,9 @@ public class ItemRestRepositoryIT extends AbstractControllerIntegrationTest {
Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build();
Collection col2 = CollectionBuilder.createCollection(context, child1).withName("Collection 2").build();
getClient().perform(get("/api/core/items/" + UUID.randomUUID()))
String token = getAuthToken(admin.getEmail(), password);
getClient(token).perform(get("/api/core/items/" + UUID.randomUUID()))
.andExpect(status().isNotFound())
;
@@ -714,7 +724,7 @@ public class ItemRestRepositoryIT extends AbstractControllerIntegrationTest {
.withIssueDate("2017-10-17")
.withAuthor("Smith, Donald").withAuthor("Doe, John")
.withSubject("ExtraEntry")
.makePrivate()
.makeUnDiscoverable()
.build();
String token = getAuthToken(admin.getEmail(), password);
@@ -767,7 +777,7 @@ public class ItemRestRepositoryIT extends AbstractControllerIntegrationTest {
.withIssueDate("2017-10-17")
.withAuthor("Smith, Donald").withAuthor("Doe, John")
.withSubject("ExtraEntry")
.makePrivate()
.makeUnDiscoverable()
.build();
String token = getAuthToken(admin.getEmail(), password);
@@ -810,7 +820,7 @@ public class ItemRestRepositoryIT extends AbstractControllerIntegrationTest {
.withIssueDate("2017-10-17")
.withAuthor("Smith, Donald").withAuthor("Doe, John")
.withSubject("ExtraEntry")
.makePrivate()
.makeUnDiscoverable()
.build();
String token = getAuthToken(eperson.getEmail(), password);
@@ -835,7 +845,7 @@ public class ItemRestRepositoryIT extends AbstractControllerIntegrationTest {
}
@Test
public void makePrivatePatchTest() throws Exception {
public void makeUnDiscoverablePatchTest() throws Exception {
context.turnOffAuthorisationSystem();
//** GIVEN **
@@ -880,7 +890,7 @@ public class ItemRestRepositoryIT extends AbstractControllerIntegrationTest {
}
@Test
public void makePrivatePatchUnauthorizedTest() throws Exception {
public void makeUnDiscoverablePatchUnauthorizedTest() throws Exception {
context.turnOffAuthorisationSystem();
//** GIVEN **
@@ -922,7 +932,8 @@ public class ItemRestRepositoryIT extends AbstractControllerIntegrationTest {
}
public void makePrivatePatchForbiddenTest() throws Exception {
@Test
public void makeUnDiscoverablePatchForbiddenTest() throws Exception {
context.turnOffAuthorisationSystem();
//** GIVEN **
@@ -994,7 +1005,7 @@ public class ItemRestRepositoryIT extends AbstractControllerIntegrationTest {
.withIssueDate("2017-10-17")
.withAuthor("Smith, Donald").withAuthor("Doe, John")
.withSubject("ExtraEntry")
.makePrivate()
.makeUnDiscoverable()
.build();
String token = getAuthToken(admin.getEmail(), password);
@@ -1089,6 +1100,7 @@ public class ItemRestRepositoryIT extends AbstractControllerIntegrationTest {
.andExpect(status().is(404));
}
@Test
public void deleteOneTemplateTest() throws Exception {
context.turnOffAuthorisationSystem();
@@ -1113,10 +1125,11 @@ public class ItemRestRepositoryIT extends AbstractControllerIntegrationTest {
.andExpect(status().is(422));
//Check templateItem is available after failed deletion
getClient().perform(get("/api/core/items/" + templateItem.getID()))
getClient(token).perform(get("/api/core/items/" + templateItem.getID()))
.andExpect(status().isOk());
}
@Test
public void deleteOneWorkspaceTest() throws Exception {
context.turnOffAuthorisationSystem();
@@ -1138,8 +1151,198 @@ public class ItemRestRepositoryIT extends AbstractControllerIntegrationTest {
getClient(token).perform(delete("/api/core/items/" + workspaceItem.getItem().getID()))
.andExpect(status().is(422));
//Check templateItem is available after failed deletion
getClient().perform(get("/api/core/items/" + workspaceItem.getID()))
//Check workspaceItem is available after failed deletion
getClient(token).perform(get("/api/core/items/" + workspaceItem.getItem().getID()))
.andExpect(status().isOk());
}
@Test
public void embargoAccessTest() throws Exception {
context.turnOffAuthorisationSystem();
//** GIVEN **
//1. A community-collection structure with one parent community with sub-community and two collections.
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent Community")
.build();
Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity)
.withName("Sub Community")
.build();
Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build();
//2. An embargoed item
Item embargoedItem1 = ItemBuilder.createItem(context, col1)
.withTitle("embargoed item 1")
.withIssueDate("2017-12-18")
.withAuthor("Smith, Donald").withAuthor("Doe, John")
.withSubject("ExtraEntry")
.withEmbargoPeriod("6 months")
.build();
//3. a public item
Item publicItem1 = ItemBuilder.createItem(context, col1)
.withTitle("Public item 1")
.withIssueDate("2017-10-17")
.withAuthor("Smith, Donald").withAuthor("Doe, John")
.withSubject("ExtraEntry")
.build();
context.restoreAuthSystemState();
//** THEN **
//An anonymous user can view public items
getClient().perform(get("/api/core/items/" + publicItem1.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(
ItemMatcher.matchItemWithTitleAndDateIssued(
publicItem1, "Public item 1", "2017-10-17")
)))
.andExpect(jsonPath("$._links.self.href",
Matchers.containsString("/api/core/items")));
//An anonymous user is not allowed to view embargoed items
getClient().perform(get("/api/core/items/" + embargoedItem1.getID()))
.andExpect(status().isUnauthorized());
//An admin user is allowed to access the embargoed item
String token1 = getAuthToken(admin.getEmail(), password);
getClient(token1).perform(get("/api/core/items/" + embargoedItem1.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(
ItemMatcher.matchItemWithTitleAndDateIssued(
embargoedItem1, "embargoed item 1", "2017-12-18")
)))
.andExpect(jsonPath("$._links.self.href",
Matchers.containsString("/api/core/items")));
}
@Test
public void undiscoverableAccessTest() throws Exception {
context.turnOffAuthorisationSystem();
//** GIVEN **
//1. A community-collection structure with one parent community with sub-community and two collections.
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent Community")
.build();
Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity)
.withName("Sub Community")
.build();
Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build();
//2. An undiscoverable item
Item unDiscoverableYetAccessibleItem1 = ItemBuilder.createItem(context, col1)
.withTitle("Undiscoverable item 1")
.withIssueDate("2017-10-17")
.withAuthor("Smith, Donald").withAuthor("Doe, John")
.withSubject("ExtraEntry")
.makeUnDiscoverable()
.build();
context.restoreAuthSystemState();
//Anonymous users are allowed to access undiscoverable items
getClient().perform(get("/api/core/items/" + unDiscoverableYetAccessibleItem1.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(
ItemMatcher.matchItemWithTitleAndDateIssued(unDiscoverableYetAccessibleItem1,
"Undiscoverable item 1", "2017-10-17")
)));
//Admin users are allowed to acceess undiscoverable items
String token1 = getAuthToken(admin.getEmail(), password);
getClient(token1).perform(get("/api/core/items/" + unDiscoverableYetAccessibleItem1.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(
ItemMatcher.matchItemWithTitleAndDateIssued(unDiscoverableYetAccessibleItem1,
"Undiscoverable item 1", "2017-10-17")
)));
}
@Test
public void privateGroupAccessTest() throws Exception {
context.turnOffAuthorisationSystem();
//** GIVEN **
//1. A community-collection structure with one parent community with sub-community and two collections.
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent Community")
.build();
Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity)
.withName("Sub Community")
.build();
Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build();
//2. An item restricted to a specific internal group
Group staffGroup = GroupBuilder.createGroup(context)
.withName("Staff")
.build();
Item restrictedItem1 = ItemBuilder.createItem(context, col1)
.withTitle("Restricted item 1")
.withIssueDate("2017-12-18")
.withReaderGroup(staffGroup)
.build();
//3. A public item
Item publicItem1 = ItemBuilder.createItem(context, col1)
.withTitle("Public item 1")
.withIssueDate("2017-10-17")
.withAuthor("Smith, Donald").withAuthor("Doe, John")
.withSubject("ExtraEntry")
.build();
//4. A member of the internal group
EPerson staffEPerson = EPersonBuilder.createEPerson(context)
.withEmail("professor@myuni.edu")
.withPassword("s3cr3t")
.withNameInMetadata("Doctor", "Professor")
.withGroupMembership(staffGroup)
.build();
context.restoreAuthSystemState();
//** THEN **
//An anonymous user can view the public item
getClient().perform(get("/api/core/items/" + publicItem1.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(
ItemMatcher.matchItemWithTitleAndDateIssued(
publicItem1, "Public item 1", "2017-10-17")
)))
.andExpect(jsonPath("$._links.self.href",
Matchers.containsString("/api/core/items")));
//An anonymous user is not allowed to the restricted item
getClient().perform(get("/api/core/items/" + restrictedItem1.getID()))
.andExpect(status().isUnauthorized());
//An admin user is allowed to access the restricted item
String token1 = getAuthToken(admin.getEmail(), password);
getClient(token1).perform(get("/api/core/items/" + restrictedItem1.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(
ItemMatcher.matchItemWithTitleAndDateIssued(
restrictedItem1, "Restricted item 1", "2017-12-18")
)))
.andExpect(jsonPath("$._links.self.href",
Matchers.containsString("/api/core/items")));
//A member of the internal group is also allowed to access the restricted item
String token2 = getAuthToken("professor@myuni.edu", "s3cr3t");
getClient(token2).perform(get("/api/core/items/" + restrictedItem1.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(
ItemMatcher.matchItemWithTitleAndDateIssued(
restrictedItem1, "Restricted item 1", "2017-12-18")
)))
.andExpect(jsonPath("$._links.self.href",
Matchers.containsString("/api/core/items")));
}
}

View File

@@ -34,8 +34,16 @@ public class SubmissionDefinitionsControllerIT extends AbstractControllerIntegra
@Test
public void findAll() throws Exception {
//When we call the root endpoint
//When we call the root endpoint as anonymous user
getClient().perform(get("/api/config/submissiondefinitions"))
//The status has to be 403 Not Authorized
.andExpect(status().isUnauthorized());
String token = getAuthToken(admin.getEmail(), password);
getClient(token).perform(get("/api/config/submissiondefinitions"))
//The status has to be 200 OK
.andExpect(status().isOk())
//We expect the content type to be "application/hal+json;charset=UTF-8"
@@ -56,7 +64,14 @@ public class SubmissionDefinitionsControllerIT extends AbstractControllerIntegra
@Test
public void findDefault() throws Exception {
getClient().perform(get("/api/config/submissiondefinitions/traditional"))
//The status has to be 403 Not Authorized
.andExpect(status().isUnauthorized());
String token = getAuthToken(admin.getEmail(), password);
getClient(token).perform(get("/api/config/submissiondefinitions/traditional"))
//The status has to be 200 OK
.andExpect(status().isOk())
//We expect the content type to be "application/hal+json;charset=UTF-8"
@@ -80,6 +95,16 @@ public class SubmissionDefinitionsControllerIT extends AbstractControllerIntegra
Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build();
getClient().perform(get("/api/config/submissiondefinitions/search/findByCollection")
.param("uuid", col1.getID().toString()))
//** THEN **
//The status has to be 200
.andExpect(status().isUnauthorized());
String token = getAuthToken(admin.getEmail(), password);
getClient(token).perform(get("/api/config/submissiondefinitions/search/findByCollection")
.param("uuid", col1.getID().toString()))
//** THEN **
//The status has to be 200
@@ -93,8 +118,17 @@ public class SubmissionDefinitionsControllerIT extends AbstractControllerIntegra
@Test
public void findCollections() throws Exception {
//Match only that a section exists with a submission configuration behind
getClient().perform(get("/api/config/submissiondefinitions/traditional/collections"))
//The status has to be 403 Not Authorized
.andExpect(status().isUnauthorized());
String token = getAuthToken(admin.getEmail(), password);
//Match only that a section exists with a submission configuration behind
getClient(token).perform(get("/api/config/submissiondefinitions/traditional/collections"))
//TODO - this method should return an empty page
.andExpect(status().isNoContent());
//this is the expected result
@@ -104,7 +138,14 @@ public class SubmissionDefinitionsControllerIT extends AbstractControllerIntegra
@Test
public void findSections() throws Exception {
getClient().perform(get("/api/config/submissiondefinitions/traditional/sections"))
//The status has to be 403 Not Authorized
.andExpect(status().isUnauthorized());
String token = getAuthToken(admin.getEmail(), password);
getClient(token).perform(get("/api/config/submissiondefinitions/traditional/sections"))
// The status has to be 200 OK
.andExpect(status().isOk())
// We expect the content type to be "application/hal+json;charset=UTF-8"

View File

@@ -28,8 +28,16 @@ public class SubmissionFormsControllerIT extends AbstractControllerIntegrationTe
@Test
public void findAll() throws Exception {
//When we call the root endpoint
//When we call the root endpoint as anonymous user
getClient().perform(get("/api/config/submissionforms"))
//The status has to be 403 Not Authorized
.andExpect(status().isUnauthorized());
String token = getAuthToken(admin.getEmail(), password);
//When we call the root endpoint
getClient(token).perform(get("/api/config/submissionforms"))
//The status has to be 200 OK
.andExpect(status().isOk())
//We expect the content type to be "application/hal+json;charset=UTF-8"
@@ -51,7 +59,15 @@ public class SubmissionFormsControllerIT extends AbstractControllerIntegrationTe
@Test
public void findTraditionalPageOne() throws Exception {
//When we call the root endpoint as anonymous user
getClient().perform(get("/api/config/submissionforms/traditionalpageone"))
//The status has to be 403 Not Authorized
.andExpect(status().isUnauthorized());
String token = getAuthToken(admin.getEmail(), password);
getClient(token).perform(get("/api/config/submissionforms/traditionalpageone"))
//The status has to be 200 OK
.andExpect(status().isOk())
//We expect the content type to be "application/hal+json;charset=UTF-8"

View File

@@ -27,8 +27,16 @@ public class SubmissionSectionsControllerIT extends AbstractControllerIntegratio
@Test
public void findAll() throws Exception {
//When we call the root endpoint
//When we call the root endpoint as anonymous user
getClient().perform(get("/api/config/submissionsections"))
//The status has to be 403 Not Authorized
.andExpect(status().isUnauthorized());
String token = getAuthToken(admin.getEmail(), password);
//When we call the root endpoint
getClient(token).perform(get("/api/config/submissionsections"))
//The status has to be 200 OK
.andExpect(status().isOk())
//We expect the content type to be "application/hal+json;charset=UTF-8"

View File

@@ -27,8 +27,16 @@ public class SubmissionUploadsControllerIT extends AbstractControllerIntegration
@Test
public void findAll() throws Exception {
//When we call the root endpoint
//When we call the root endpoint as anonymous user
getClient().perform(get("/api/config/submissionuploads"))
//The status has to be 403 Not Authorized
.andExpect(status().isUnauthorized());
String token = getAuthToken(admin.getEmail(), password);
//When we call the root endpoint
getClient(token).perform(get("/api/config/submissionuploads"))
//The status has to be 200 OK
.andExpect(status().isOk())
//We expect the content type to be "application/hal+json;charset=UTF-8"

View File

@@ -14,6 +14,7 @@ import org.dspace.content.service.DSpaceObjectService;
import org.dspace.core.Context;
import org.dspace.discovery.SearchServiceException;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;
public class EPersonBuilder extends AbstractDSpaceObjectBuilder<EPerson> {
@@ -71,4 +72,15 @@ public class EPersonBuilder extends AbstractDSpaceObjectBuilder<EPerson> {
ePerson.setEmail(name);
return this;
}
public EPersonBuilder withGroupMembership(Group group) {
groupService.addMember(context, group, ePerson);
return this;
}
public EPersonBuilder withPassword(final String password) {
ePerson.setCanLogIn(true);
ePersonService.setPassword(ePerson, password);
return this;
}
}

View File

@@ -67,7 +67,7 @@ public class ItemBuilder extends AbstractDSpaceObjectBuilder<Item> {
return addMetadataValue(item, MetadataSchema.DC_SCHEMA, "subject", null, subject);
}
public ItemBuilder makePrivate() {
public ItemBuilder makeUnDiscoverable() {
item.setDiscoverable(false);
return this;
}

View File

@@ -29,7 +29,7 @@ public class EPersonMatcher {
hasJsonPath("$.type", is("eperson")),
hasJsonPath("$.canLogIn", not(empty())),
hasJsonPath("$._links.self.href", containsString("/api/eperson/epersons/" + ePerson.getID().toString())),
hasJsonPath("$.metadata", Matchers.containsInAnyOrder(
hasJsonPath("$.metadata", Matchers.hasItems(
EPersonMetadataMatcher.matchFirstName(ePerson.getFirstName()),
EPersonMetadataMatcher.matchLastName(ePerson.getLastName())
))
@@ -37,9 +37,16 @@ public class EPersonMatcher {
}
public static Matcher<? super Object> matchEPersonOnEmail(String email) {
return allOf(
hasJsonPath("$.type", is("eperson")),
hasJsonPath("$.email", is(email))
);
}
public static Matcher<? super Object> matchDefaultTestEPerson() {
return allOf(
hasJsonPath("$.type", is("eperson"))
hasJsonPath("$.type", is("eperson"))
);
}
}

View File

@@ -31,4 +31,10 @@ public class EPersonMetadataMatcher {
);
}
public static Matcher<? super Object> matchLanguage(String language) {
return allOf(
hasJsonPath("$.key", is("eperson.language")),
hasJsonPath("$.value", is(language))
);
}
}

View File

@@ -47,8 +47,7 @@ public class ItemMatcher {
hasJsonPath("$._links.self.href", startsWith(REST_SERVER_URL)),
hasJsonPath("$._links.bitstreams.href", startsWith(REST_SERVER_URL)),
hasJsonPath("$._links.owningCollection.href", startsWith(REST_SERVER_URL)),
hasJsonPath("$._links.templateItemOf.href", startsWith(REST_SERVER_URL)),
hasJsonPath("$._links.self.href", startsWith(REST_SERVER_URL))
hasJsonPath("$._links.templateItemOf.href", startsWith(REST_SERVER_URL))
);
}

View File

@@ -50,7 +50,7 @@ public class EPersonRestAuthenticationProviderTest {
List<GrantedAuthority> authorities = ePersonRestAuthenticationProvider.getGrantedAuthorities(context, ePerson);
assertThat(authorities.stream().map(a -> a.getAuthority()).collect(Collectors.toList()), containsInAnyOrder(
WebSecurityConfiguration.ADMIN_GRANT, WebSecurityConfiguration.EPERSON_GRANT));
WebSecurityConfiguration.ADMIN_GRANT, WebSecurityConfiguration.AUTHENTICATED_GRANT));
}
@@ -61,7 +61,7 @@ public class EPersonRestAuthenticationProviderTest {
List<GrantedAuthority> authorities = ePersonRestAuthenticationProvider.getGrantedAuthorities(context, ePerson);
assertThat(authorities.stream().map(a -> a.getAuthority()).collect(Collectors.toList()), containsInAnyOrder(
WebSecurityConfiguration.EPERSON_GRANT));
WebSecurityConfiguration.AUTHENTICATED_GRANT));
}

View File

@@ -22,6 +22,7 @@ import org.apache.commons.io.Charsets;
import org.apache.commons.lang.StringUtils;
import org.dspace.app.rest.Application;
import org.dspace.app.rest.model.patch.Operation;
import org.dspace.app.rest.security.MethodSecurityConfig;
import org.dspace.app.rest.security.WebSecurityConfiguration;
import org.dspace.app.rest.utils.ApplicationConfig;
import org.junit.Assert;
@@ -54,7 +55,8 @@ import org.springframework.web.context.WebApplicationContext;
* @author Tom Desair (tom dot desair at atmire dot com)
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = {Application.class, ApplicationConfig.class, WebSecurityConfiguration.class})
@SpringBootTest(classes = {Application.class, ApplicationConfig.class, WebSecurityConfiguration.class,
MethodSecurityConfig.class})
@TestExecutionListeners( {DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class,
TransactionalTestExecutionListener.class})
@DirtiesContext
@@ -62,7 +64,10 @@ import org.springframework.web.context.WebApplicationContext;
public class AbstractControllerIntegrationTest extends AbstractIntegrationTestWithDatabase {
protected static final String AUTHORIZATION_HEADER = "Authorization";
protected static final String AUTHORIZATION_TYPE = "Bearer";
//The Authorization header contains a value like "Bearer TOKENVALUE". This constant string represents the part that
//sits before the actual authentication token and can be used to easily compose or parse the Authorization header.
protected static final String AUTHORIZATION_TYPE = "Bearer ";
public static final String REST_SERVER_URL = "http://localhost/api/";
@@ -105,7 +110,8 @@ public class AbstractControllerIntegrationTest extends AbstractIntegrationTestWi
.addFilters(requestFilters.toArray(new Filter[requestFilters.size()]));
if (StringUtils.isNotBlank(authToken)) {
mockMvcBuilder.defaultRequest(get("").header(AUTHORIZATION_HEADER, AUTHORIZATION_TYPE + " " + authToken));
mockMvcBuilder.defaultRequest(
get("").header(AUTHORIZATION_HEADER, AUTHORIZATION_TYPE + authToken));
}
return mockMvcBuilder
@@ -120,7 +126,9 @@ public class AbstractControllerIntegrationTest extends AbstractIntegrationTestWi
}
public String getAuthToken(String user, String password) throws Exception {
return getAuthResponse(user, password).getHeader(AUTHORIZATION_HEADER);
return StringUtils.substringAfter(
getAuthResponse(user, password).getHeader(AUTHORIZATION_HEADER),
AUTHORIZATION_TYPE);
}
public String getPatchContent(List<Operation> ops) {