Merge pull request #1873 from atmire/POC_stateless_sessions

DS-3542: Stateless sessions authentication
This commit is contained in:
Andrea Bollini
2017-12-02 22:11:03 +01:00
committed by GitHub
69 changed files with 2855 additions and 1143 deletions

View File

@@ -7,7 +7,6 @@
*/
package org.dspace.authenticate;
import javax.servlet.http.HttpServletRequest;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
@@ -15,6 +14,8 @@ import java.util.Date;
import java.util.Iterator;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.dspace.authenticate.service.AuthenticationService;
import org.dspace.authorize.AuthorizeException;
import org.dspace.core.Context;
@@ -113,15 +114,7 @@ public class AuthenticationServiceImpl implements AuthenticationService
ret = AuthenticationMethod.NO_SUCH_USER;
}
if (ret == AuthenticationMethod.SUCCESS) {
EPerson me = context.getCurrentUser();
me.setLastActive(new Date());
try {
ePersonService.update(context, me);
} catch (SQLException ex) {
log.error("Could not update last-active stamp", ex);
} catch (AuthorizeException ex) {
log.error("Could not update last-active stamp", ex);
}
updateLastActiveDate(context);
return ret;
}
if (ret < bestRet) {
@@ -132,6 +125,20 @@ public class AuthenticationServiceImpl implements AuthenticationService
return bestRet;
}
public void updateLastActiveDate(Context context) {
EPerson me = context.getCurrentUser();
if(me != null) {
me.setLastActive(new Date());
try {
ePersonService.update(context, me);
} catch (SQLException ex) {
log.error("Could not update last-active stamp", ex);
} catch (AuthorizeException ex) {
log.error("Could not update last-active stamp", ex);
}
}
}
@Override
public boolean canSelfRegister(Context context,
HttpServletRequest request,

View File

@@ -7,16 +7,17 @@
*/
package org.dspace.authenticate.service;
import java.sql.SQLException;
import java.util.Iterator;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.dspace.authenticate.AuthenticationMethod;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;
import javax.servlet.http.HttpServletRequest;
import java.sql.SQLException;
import java.util.Iterator;
import java.util.List;
/**
* Access point for the stackable authentication methods.
* <p>
@@ -158,6 +159,11 @@ public interface AuthenticationService {
EPerson eperson)
throws SQLException;
/**
* Update the last active (login) timestamp on the current authenticated user
* @param context The authenticated context
*/
public void updateLastActiveDate(Context context);
/**
* Get list of extra groups that user implicitly belongs to.

View File

@@ -92,21 +92,6 @@ public class Context
BATCH_EDIT
}
static
{
// Before initializing a Context object, we need to ensure the database
// is up-to-date. This ensures any outstanding Flyway migrations are run
// PRIOR to Hibernate initializing (occurs when DBConnection is loaded in init() below).
try
{
DatabaseUtils.updateDatabase();
}
catch(SQLException sqle)
{
log.fatal("Cannot initialize database via Flyway!", sqle);
}
}
protected Context(EventService eventService, DBConnection dbConnection) {
this.mode = Mode.READ_WRITE;
this.eventService = eventService;
@@ -145,6 +130,8 @@ public class Context
*/
private void init()
{
updateDatabase();
if(eventService == null)
{
eventService = EventServiceFactory.getInstance().getEventService();
@@ -168,11 +155,22 @@ public class Context
specialGroups = new ArrayList<>();
authStateChangeHistory = new Stack<Boolean>();
authStateClassCallHistory = new Stack<String>();
authStateChangeHistory = new Stack<>();
authStateClassCallHistory = new Stack<>();
setMode(this.mode);
}
protected void updateDatabase() {
// Before initializing a Context object, we need to ensure the database
// is up-to-date. This ensures any outstanding Flyway migrations are run
// PRIOR to Hibernate initializing (occurs when DBConnection is loaded in calling init() method).
try {
DatabaseUtils.updateDatabase();
} catch (SQLException sqle) {
log.fatal("Cannot initialize database via Flyway!", sqle);
}
}
/**
* Get the database connection associated with the context
*

View File

@@ -65,6 +65,9 @@ public class EPerson extends DSpaceObject implements DSpaceObjectLegacySupport
@Column(name="salt", length = 32)
private String salt;
@Column(name="session_salt", length = 32)
private String sessionSalt;
@Column(name="digest_algorithm", length = 16)
private String digestAlgorithm;
@@ -89,6 +92,9 @@ public class EPerson extends DSpaceObject implements DSpaceObjectLegacySupport
@Transient
protected transient EPersonService ePersonService;
@Transient
private Date previousActive;
/**
* Protected constructor, create object using:
* {@link org.dspace.eperson.service.EPersonService#create(Context)}
@@ -370,6 +376,7 @@ public class EPerson extends DSpaceObject implements DSpaceObjectLegacySupport
*/
public void setLastActive(Date when)
{
this.previousActive = lastActive;
this.lastActive = when;
}
@@ -433,4 +440,20 @@ public class EPerson extends DSpaceObject implements DSpaceObjectLegacySupport
}
return ePersonService;
}
public String getSessionSalt() {
return sessionSalt;
}
public void setSessionSalt(String sessionSalt) {
this.sessionSalt = sessionSalt;
}
public Date getPreviousActive() {
if (previousActive == null) {
return new Date(0);
}
return previousActive;
}
}

View File

@@ -21,9 +21,11 @@ import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.sql.DataSource;
import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.dspace.core.Context;
@@ -464,62 +466,63 @@ public class DatabaseUtils
* DataSource object initialized by DatabaseManager
* @return initialized Flyway object
*/
private synchronized static Flyway setupFlyway(DataSource datasource)
{
private static Flyway setupFlyway(DataSource datasource) {
ConfigurationService config = DSpaceServicesFactory.getInstance().getConfigurationService();
if (flywaydb==null)
{
try(Connection connection = datasource.getConnection())
{
// Initialize Flyway DB API (http://flywaydb.org/), used to perform DB migrations
flywaydb = new Flyway();
flywaydb.setDataSource(datasource);
flywaydb.setEncoding("UTF-8");
// Default cleanDisabled to "true" (which disallows the ability to run 'database clean')
flywaydb.setCleanDisabled(config.getBooleanProperty("db.cleanDisabled", true));
// Migration scripts are based on DBMS Keyword (see full path below)
String dbType = getDbType(connection);
connection.close();
// Determine location(s) where Flyway will load all DB migrations
ArrayList<String> scriptLocations = new ArrayList<String>();
// First, add location for custom SQL migrations, if any (based on DB Type)
// e.g. [dspace.dir]/etc/[dbtype]/
// (We skip this for H2 as it's only used for unit testing)
if (!dbType.equals(DBMS_H2))
{
scriptLocations.add("filesystem:" + config.getProperty("dspace.dir") +
"/etc/" + dbType);
if (flywaydb == null) {
synchronized (log) {
if (flywaydb != null) {
return flywaydb;
}
// Also add the Java package where Flyway will load SQL migrations from (based on DB Type)
scriptLocations.add("classpath:org.dspace.storage.rdbms.sqlmigration." + dbType);
try (Connection connection = datasource.getConnection()) {
// Initialize Flyway DB API (http://flywaydb.org/), used to perform DB migrations
flywaydb = new Flyway();
flywaydb.setDataSource(datasource);
flywaydb.setEncoding("UTF-8");
// Also add the Java package where Flyway will load Java migrations from
// NOTE: this also loads migrations from any sub-package
scriptLocations.add("classpath:org.dspace.storage.rdbms.migration");
// Default cleanDisabled to "true" (which disallows the ability to run 'database clean')
flywaydb.setCleanDisabled(config.getBooleanProperty("db.cleanDisabled", true));
//Add all potential workflow migration paths
List<String> workflowFlywayMigrationLocations = WorkflowServiceFactory.getInstance().getWorkflowService().getFlywayMigrationLocations();
scriptLocations.addAll(workflowFlywayMigrationLocations);
// Migration scripts are based on DBMS Keyword (see full path below)
String dbType = getDbType(connection);
connection.close();
// Now tell Flyway which locations to load SQL / Java migrations from
log.info("Loading Flyway DB migrations from: " + StringUtils.join(scriptLocations, ", "));
flywaydb.setLocations(scriptLocations.toArray(new String[scriptLocations.size()]));
// Determine location(s) where Flyway will load all DB migrations
ArrayList<String> scriptLocations = new ArrayList<String>();
// Set flyway callbacks (i.e. classes which are called post-DB migration and similar)
// In this situation, we have a Registry Updater that runs PRE-migration
// NOTE: DatabaseLegacyReindexer only indexes in Legacy Lucene & RDBMS indexes. It can be removed once those are obsolete.
List<FlywayCallback> flywayCallbacks = DSpaceServicesFactory.getInstance().getServiceManager().getServicesByType(FlywayCallback.class);
flywaydb.setCallbacks(flywayCallbacks.toArray(new FlywayCallback[flywayCallbacks.size()]));
}
catch(SQLException e)
{
log.error("Unable to setup Flyway against DSpace database", e);
// First, add location for custom SQL migrations, if any (based on DB Type)
// e.g. [dspace.dir]/etc/[dbtype]/
// (We skip this for H2 as it's only used for unit testing)
if (!dbType.equals(DBMS_H2)) {
scriptLocations.add("filesystem:" + config.getProperty("dspace.dir") +
"/etc/" + dbType);
}
// Also add the Java package where Flyway will load SQL migrations from (based on DB Type)
scriptLocations.add("classpath:org.dspace.storage.rdbms.sqlmigration." + dbType);
// Also add the Java package where Flyway will load Java migrations from
// NOTE: this also loads migrations from any sub-package
scriptLocations.add("classpath:org.dspace.storage.rdbms.migration");
//Add all potential workflow migration paths
List<String> workflowFlywayMigrationLocations = WorkflowServiceFactory.getInstance().getWorkflowService().getFlywayMigrationLocations();
scriptLocations.addAll(workflowFlywayMigrationLocations);
// Now tell Flyway which locations to load SQL / Java migrations from
log.info("Loading Flyway DB migrations from: " + StringUtils.join(scriptLocations, ", "));
flywaydb.setLocations(scriptLocations.toArray(new String[scriptLocations.size()]));
// Set flyway callbacks (i.e. classes which are called post-DB migration and similar)
// In this situation, we have a Registry Updater that runs PRE-migration
// NOTE: DatabaseLegacyReindexer only indexes in Legacy Lucene & RDBMS indexes. It can be removed once those are obsolete.
List<FlywayCallback> flywayCallbacks = DSpaceServicesFactory.getInstance().getServiceManager().getServicesByType(FlywayCallback.class);
flywaydb.setCallbacks(flywayCallbacks.toArray(new FlywayCallback[flywayCallbacks.size()]));
} catch (SQLException e) {
log.error("Unable to setup Flyway against DSpace database", e);
}
}
}
@@ -538,7 +541,7 @@ public class DatabaseUtils
* @throws SQLException if database error
* If database cannot be upgraded.
*/
public static synchronized void updateDatabase()
public static void updateDatabase()
throws SQLException
{
// Get our configured dataSource
@@ -604,17 +607,6 @@ public class DatabaseUtils
// Setup Flyway API against our database
Flyway flyway = setupFlyway(datasource);
// Set whethe Flyway will run migrations "out of order". By default, this is false,
// and Flyway ONLY runs migrations that have a higher version number.
flyway.setOutOfOrder(outOfOrder);
// If a target version was specified, tell Flyway to ONLY migrate to that version
// (i.e. all later migrations are left as "pending"). By default we always migrate to latest version.
if (!StringUtils.isBlank(targetVersion))
{
flyway.setTargetAsString(targetVersion);
}
// Does the necessary Flyway table ("schema_version") exist in this database?
// If not, then this is the first time Flyway has run, and we need to initialize
// NOTE: search is case sensitive, as flyway table name is ALWAYS lowercase,
@@ -639,26 +631,40 @@ public class DatabaseUtils
}
}
// Determine pending Database migrations
MigrationInfo[] pending = flyway.info().pending();
// As long as there are pending migrations, log them and run migrate()
if (pending!=null && pending.length>0)
{
log.info("Pending DSpace database schema migrations:");
for (MigrationInfo info : pending)
{
log.info("\t" + info.getVersion() + " " + info.getDescription() + " " + info.getType() + " " + info.getState());
}
// Run all pending Flyway migrations to ensure the DSpace Database is up to date
flyway.migrate();
// Flag that Discovery will need reindexing, since database was updated
setReindexDiscovery(true);
}
else
if(ArrayUtils.isEmpty(pending)) {
log.info("DSpace database schema is up to date");
} else {
synchronized (log) {
// Set whethe Flyway will run migrations "out of order". By default, this is false,
// and Flyway ONLY runs migrations that have a higher version number.
flyway.setOutOfOrder(outOfOrder);
// If a target version was specified, tell Flyway to ONLY migrate to that version
// (i.e. all later migrations are left as "pending"). By default we always migrate to latest version.
if (!StringUtils.isBlank(targetVersion)) {
flyway.setTargetAsString(targetVersion);
}
// Determine pending Database migrations
pending = flyway.info().pending();
// As long as there are pending migrations, log them and run migrate()
if (pending != null && pending.length > 0) {
log.info("Pending DSpace database schema migrations:");
for (MigrationInfo info : pending) {
log.info("\t" + info.getVersion() + " " + info.getDescription() + " " + info.getType() + " " + info.getState());
}
// Run all pending Flyway migrations to ensure the DSpace Database is up to date
flyway.migrate();
// Flag that Discovery will need reindexing, since database was updated
setReindexDiscovery(true);
} else
log.info("DSpace database schema is up to date");
}
}
}
catch(FlywayException fe)
{

View File

@@ -0,0 +1,20 @@
--
-- 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/
--
-- ===============================================================
-- WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
--
-- DO NOT MANUALLY RUN THIS DATABASE MIGRATION. IT WILL BE EXECUTED
-- AUTOMATICALLY (IF NEEDED) BY "FLYWAY" WHEN YOU STARTUP DSPACE.
-- http://flywaydb.org/
-- ===============================================================
------------------------------------------------------------------------------------------------------------
-- This adds an extra column to the eperson table where we save a salt for stateless authentication
------------------------------------------------------------------------------------------------------------
ALTER TABLE eperson ADD session_salt varchar(32);

View File

@@ -0,0 +1,20 @@
--
-- 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/
--
-- ===============================================================
-- WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
--
-- DO NOT MANUALLY RUN THIS DATABASE MIGRATION. IT WILL BE EXECUTED
-- AUTOMATICALLY (IF NEEDED) BY "FLYWAY" WHEN YOU STARTUP DSPACE.
-- http://flywaydb.org/
-- ===============================================================
------------------------------------------------------------------------------------------------------------
-- This adds an extra column to the eperson table where we save a salt for stateless authentication
------------------------------------------------------------------------------------------------------------
ALTER TABLE eperson ADD session_salt varchar(32);

View File

@@ -0,0 +1,20 @@
--
-- 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/
--
-- ===============================================================
-- WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
--
-- DO NOT MANUALLY RUN THIS DATABASE MIGRATION. IT WILL BE EXECUTED
-- AUTOMATICALLY (IF NEEDED) BY "FLYWAY" WHEN YOU STARTUP DSPACE.
-- http://flywaydb.org/
-- ===============================================================
------------------------------------------------------------------------------------------------------------
-- This adds an extra column to the eperson table where we save a salt for stateless authentication
------------------------------------------------------------------------------------------------------------
ALTER TABLE eperson ADD session_salt varchar(32);

View File

@@ -7,12 +7,14 @@
*/
package org.dspace.rest.authentication;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.dspace.authenticate.AuthenticationMethod;
import org.dspace.authenticate.factory.AuthenticateServiceFactory;
import org.dspace.authenticate.service.AuthenticationService;
import org.dspace.core.Context;
import org.dspace.core.LogManager;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;
import org.dspace.utils.DSpace;
import org.springframework.security.authentication.AuthenticationProvider;
@@ -28,15 +30,14 @@ import java.util.ArrayList;
import java.util.List;
/**
* The core authentication and authorization provider. This provider is called when logging in.
* The core authentication & authorization provider, this provider is called when logging in & will process
*
* @author Roeland Dillen (roeland at atmire dot com)
* @author kevinvandevelde at atmire.com
*
* @deprecated This provider handles both the authorization as well as the authentication,
* FIXME This provider handles both the authorization as well as the authentication,
* due to the way that the DSpace authentication is implemented there is currently no other way to do this.
*/
@Deprecated
public class DSpaceAuthenticationProvider implements AuthenticationProvider {
private static Logger log = Logger.getLogger(DSpaceAuthenticationProvider.class);
@@ -60,7 +61,8 @@ public class DSpaceAuthenticationProvider implements AuthenticationProvider {
if (implicitStatus == AuthenticationMethod.SUCCESS) {
log.info(LogManager.getHeader(context, "login", "type=implicit"));
addSpecialGroupsToGrantedAuthorityList(context, httpServletRequest, grantedAuthorities);
return new UsernamePasswordAuthenticationToken(name, password, grantedAuthorities);
return createAuthenticationToken(password, context, grantedAuthorities);
} else {
int authenticateResult = authenticationService.authenticate(context, name, password, null, httpServletRequest);
if (AuthenticationMethod.SUCCESS == authenticateResult) {
@@ -69,7 +71,8 @@ public class DSpaceAuthenticationProvider implements AuthenticationProvider {
log.info(LogManager
.getHeader(context, "login", "type=explicit"));
return new UsernamePasswordAuthenticationToken(name, password, grantedAuthorities);
return createAuthenticationToken(password, context, grantedAuthorities);
} else {
log.info(LogManager.getHeader(context, "failed_login", "email="
+ name + ", result="
@@ -102,8 +105,19 @@ public class DSpaceAuthenticationProvider implements AuthenticationProvider {
}
}
private Authentication createAuthenticationToken(final String password, final Context context, final List<SimpleGrantedAuthority> grantedAuthorities) {
EPerson ePerson = context.getCurrentUser();
if(ePerson != null && StringUtils.isNotBlank(ePerson.getEmail())) {
return new UsernamePasswordAuthenticationToken(ePerson.getEmail(), password, grantedAuthorities);
} else {
log.info(LogManager.getHeader(context, "failed_login", "No eperson with an non-blank e-mail address found"));
throw new BadCredentialsException("Login failed");
}
}
@Override
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
}
}

View File

@@ -9,7 +9,6 @@ package org.dspace.servicemanager.example;
import org.dspace.services.RequestService;
import org.dspace.services.model.RequestInterceptor;
import org.dspace.services.model.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -36,14 +35,14 @@ public final class RequestInterceptorExample implements RequestInterceptor {
}
@Override
public void onEnd(String requestId, Session session, boolean succeeded,
public void onEnd(String requestId, boolean succeeded,
Exception failure) {
log.info("Intercepting End of Request: id=" + requestId + ", session=" + session.getId() + ", succeeded=" + succeeded);
log.info("Intercepting End of Request: id=" + requestId + ", succeeded=" + succeeded);
}
@Override
public void onStart(String requestId, Session session) {
log.info("Intercepting Start of Request: id=" + requestId + ", session=" + session.getId());
public void onStart(String requestId) {
log.info("Intercepting Start of Request: id=" + requestId);
}
@Override

View File

@@ -7,6 +7,8 @@
*/
package org.dspace.services;
import java.util.UUID;
import org.dspace.services.model.Request;
import org.dspace.services.model.RequestInterceptor;
@@ -21,6 +23,11 @@ import javax.servlet.ServletResponse;
*/
public interface RequestService {
/**
* Request attribute name for the current authenticated user
*/
static final String AUTHENTICATED_EPERSON = "authenticated_eperson";
/**
* Initiates a request in the system.
* Normally this would be triggered by a servlet request starting.
@@ -94,4 +101,19 @@ public interface RequestService {
*/
public void registerRequestInterceptor(RequestInterceptor interceptor);
/**
* Access the current user id for the current session.
* (also available from the current session)
*
* @return the id of the user associated with the current thread OR null if there is no user
*/
public String getCurrentUserId();
/**
* Set the ID of the current authenticated user
*
* @return the id of the user associated with the current thread OR null if there is no user
*/
public void setCurrentUserId(UUID epersonId);
}

View File

@@ -1,51 +0,0 @@
/**
* 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.services;
import org.dspace.services.model.Session;
/**
* Provides access to user sessions and allows for initializing user
* sessions.
*
* @author Aaron Zeckoski (azeckoski @ gmail.com)
*/
public interface SessionService {
/**
* Start a new session and destroy any existing session that is
* known for this thread.
* Will bind this to the current request and capture all required
* session information
* <p>
* WARNING: there is normally no need to call this method as the
* session is created for you when using webapps. This is only
* needed when there is a requirement to create a session operating
* outside a servlet container or manually handling sessions.
*
* @return the Session object associated with the current request or processing thread OR null if there is not one
*/
public Session getCurrentSession();
/**
* Access the current session id for the current thread
* (also available from the current session).
*
* @return the id of the session associated with the current thread OR null if there is no session
*/
public String getCurrentSessionId();
/**
* Access the current user id for the current session.
* (also available from the current session)
*
* @return the id of the user associated with the current thread OR null if there is no user
*/
public String getCurrentUserId();
}

View File

@@ -22,7 +22,6 @@ import java.util.concurrent.ConcurrentHashMap;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Ehcache;
import net.sf.ehcache.Statistics;
import org.dspace.kernel.ServiceManager;
import org.dspace.kernel.mixins.ConfigChangeListener;
import org.dspace.kernel.mixins.InitializedService;
@@ -34,8 +33,10 @@ import org.dspace.services.ConfigurationService;
import org.dspace.services.RequestService;
import org.dspace.services.caching.model.EhcacheCache;
import org.dspace.services.caching.model.MapCache;
import org.dspace.services.model.*;
import org.dspace.services.model.Cache;
import org.dspace.services.model.CacheConfig;
import org.dspace.services.model.CacheConfig.CacheScope;
import org.dspace.services.model.RequestInterceptor;
import org.dspace.utils.servicemanager.ProviderHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -626,7 +627,7 @@ public final class CachingServiceImpl implements CachingService, InitializedServ
private class CachingServiceRequestInterceptor implements RequestInterceptor {
public void onStart(String requestId, Session session) {
public void onStart(String requestId) {
if (requestId != null) {
Map<String, MapCache> requestCaches = requestCachesMap.get(requestId);
if (requestCaches == null) {
@@ -636,7 +637,7 @@ public final class CachingServiceImpl implements CachingService, InitializedServ
}
}
public void onEnd(String requestId, Session session, boolean succeeded, Exception failure) {
public void onEnd(String requestId, boolean succeeded, Exception failure) {
if (requestId != null) {
requestCachesMap.remove(requestId);
}

View File

@@ -7,7 +7,10 @@
*/
package org.dspace.services.events;
import java.util.*;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.lang.ArrayUtils;
@@ -15,15 +18,13 @@ import org.dspace.kernel.mixins.ShutdownService;
import org.dspace.services.CachingService;
import org.dspace.services.EventService;
import org.dspace.services.RequestService;
import org.dspace.services.SessionService;
import org.dspace.services.model.Cache;
import org.dspace.services.model.CacheConfig;
import org.dspace.services.model.CacheConfig.CacheScope;
import org.dspace.services.model.Event;
import org.dspace.services.model.Event.Scope;
import org.dspace.services.model.EventListener;
import org.dspace.services.model.RequestInterceptor;
import org.dspace.services.model.Session;
import org.dspace.services.model.CacheConfig.CacheScope;
import org.dspace.services.model.Event.Scope;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -47,17 +48,15 @@ public final class SystemEventService implements EventService, ShutdownService {
private Map<String, EventListener> listenersMap = new ConcurrentHashMap<String, EventListener>();
private final RequestService requestService;
private final SessionService sessionService;
private final CachingService cachingService;
private EventRequestInterceptor requestInterceptor;
@Autowired(required=true)
public SystemEventService(RequestService requestService, SessionService sessionService, CachingService cachingService) {
if (requestService == null || cachingService == null || sessionService == null) {
public SystemEventService(RequestService requestService, CachingService cachingService) {
if (requestService == null || cachingService == null) {
throw new IllegalArgumentException("requestService, cachingService, and all inputs must not be null");
}
this.requestService = requestService;
this.sessionService = sessionService;
this.cachingService = cachingService;
// register interceptor
@@ -222,7 +221,7 @@ public final class SystemEventService implements EventService, ShutdownService {
}
if (event.getUserId() == null || "".equals(event.getUserId()) ) {
// set to the current user
String userId = this.sessionService.getCurrentUserId();
String userId = this.requestService.getCurrentUserId();
event.setUserId(userId);
}
if (event.getScopes() == null) {
@@ -303,14 +302,14 @@ public final class SystemEventService implements EventService, ShutdownService {
/* (non-Javadoc)
* @see org.dspace.services.model.RequestInterceptor#onStart(java.lang.String, org.dspace.services.model.Session)
*/
public void onStart(String requestId, Session session) {
public void onStart(String requestId) {
// nothing to really do here unless we decide we should purge out any existing events? -AZ
}
/* (non-Javadoc)
* @see org.dspace.services.model.RequestInterceptor#onEnd(java.lang.String, org.dspace.services.model.Session, boolean, java.lang.Exception)
*/
public void onEnd(String requestId, Session session, boolean succeeded, Exception failure) {
public void onEnd(String requestId, boolean succeeded, Exception failure) {
if (succeeded) {
int fired = fireQueuedEvents();
log.debug("Fired "+fired+" events at the end of the request ("+requestId+")");

View File

@@ -26,8 +26,6 @@ public abstract class DSpaceServicesFactory
public abstract RequestService getRequestService();
public abstract SessionService getSessionService();
public abstract ServiceManager getServiceManager();
public static DSpaceServicesFactory getInstance()

View File

@@ -32,9 +32,6 @@ public class DSpaceServicesFactoryImpl extends DSpaceServicesFactory {
@Autowired(required = true)
private RequestService requestService;
@Autowired(required = true)
private SessionService sessionService;
@Autowired(required = true)
private ServiceManager serviceManager;
@@ -63,11 +60,6 @@ public class DSpaceServicesFactoryImpl extends DSpaceServicesFactory {
return requestService;
}
@Override
public SessionService getSessionService() {
return sessionService;
}
@Override
public ServiceManager getServiceManager() {
return serviceManager;

View File

@@ -15,8 +15,6 @@ import javax.servlet.http.HttpServletResponse;
public interface Request {
public String getRequestId();
public Session getSession();
public Object getAttribute(String name);
public void setAttribute(String name, Object o);

View File

@@ -37,10 +37,9 @@ public interface RequestInterceptor extends OrderedService {
* then throw a {@link RequestInterruptionException}.
*
* @param requestId the unique id of the request
* @param session the session associated with this request
* @throws RequestInterruptionException if this interceptor wants to stop the request
*/
public void onStart(String requestId, Session session);
public void onStart(String requestId);
/**
* Take actions after the request is handled for an operation.
@@ -57,11 +56,10 @@ public interface RequestInterceptor extends OrderedService {
* cue to rollback or commit, for example.
*
* @param requestId the unique id of the request
* @param session the session associated with this request
* @param succeeded true if the request operations were successful, false if there was a failure
* @param failure this is the exception associated with the failure, it is null if there is no associated exception
*/
public void onEnd(String requestId, Session session, boolean succeeded, Exception failure);
public void onEnd(String requestId, boolean succeeded, Exception failure);
/**
* Indicate that request processing should be halted. This should

View File

@@ -7,16 +7,26 @@
*/
package org.dspace.services.sessions;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.apache.commons.lang.StringUtils;
import org.dspace.kernel.mixins.InitializedService;
import org.dspace.kernel.mixins.ShutdownService;
import org.dspace.services.ConfigurationService;
import org.dspace.services.RequestService;
import org.dspace.services.SessionService;
import org.dspace.services.model.Request;
import org.dspace.services.model.RequestInterceptor;
import org.dspace.services.model.RequestInterceptor.RequestInterruptionException;
import org.dspace.services.model.Session;
import org.dspace.services.sessions.model.HttpRequestImpl;
import org.dspace.services.sessions.model.InternalRequestImpl;
import org.dspace.utils.servicemanager.OrderedServiceComparator;
@@ -25,27 +35,23 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Required;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* Implementation of the session service.
* <p>
* This depends on having something (a filter typically) which is
* This depends on having something (a filter typically) which is
* placing the current requests into a request storage cache.
* <p>
* TODO use a HttpSessionListener to keep track of all sessions?
*
*
* @author Aaron Zeckoski (azeckoski @ gmail.com)
* @author Atmire
*/
public final class SessionRequestServiceImpl implements SessionService, RequestService, InitializedService, ShutdownService {
public final class StatelessRequestServiceImpl implements RequestService, InitializedService, ShutdownService {
private static Logger log = LoggerFactory.getLogger(SessionRequestServiceImpl.class);
private static Logger log = LoggerFactory.getLogger(StatelessRequestServiceImpl.class);
private ConfigurationService configurationService;
@Autowired
@Required
public void setConfigurationService(ConfigurationService configurationService) {
@@ -102,7 +108,7 @@ public final class SessionRequestServiceImpl implements SessionService, RequestS
for (RequestInterceptor requestInterceptor : interceptors) {
if (requestInterceptor != null) {
try {
requestInterceptor.onStart(req.getRequestId(), req.getSession());
requestInterceptor.onStart(req.getRequestId());
} catch (RequestInterruptionException e) {
String message = "Request stopped from starting by exception from the interceptor ("+requestInterceptor+"): " + e.getMessage();
log.warn(message);
@@ -138,17 +144,11 @@ public final class SessionRequestServiceImpl implements SessionService, RequestS
private void endRequest(String requestId, Exception failure) {
if (requestId != null) {
Session session = null;
Request req = requests.get(requestId);
if (req != null) {
session = req.getSession();
}
List<RequestInterceptor> interceptors = getInterceptors(true); // reverse
for (RequestInterceptor requestInterceptor : interceptors) {
if (requestInterceptor != null) {
try {
requestInterceptor.onEnd(requestId, session, (failure == null), failure);
requestInterceptor.onEnd(requestId, (failure == null), failure);
} catch (RequestInterruptionException e) {
log.warn("Attempt to stop request from ending by an exception from the interceptor ("+requestInterceptor+"), cannot stop requests from ending though so request end continues, this may be an error: " + e.getMessage());
} catch (Exception e) {
@@ -189,48 +189,26 @@ public final class SessionRequestServiceImpl implements SessionService, RequestS
this.interceptorsMap.put(key, interceptor);
}
/**
* Makes a session from the existing HTTP session stuff in the
* current request, or creates a new session of non-HTTP related
* sessions.
*
* @return the new session object which is placed into the request
* @throws IllegalStateException if not session can be created
*/
public Session getCurrentSession() {
Request req = requests.getCurrent();
if (req != null) {
return req.getSession();
}
return null;
}
/* (non-Javadoc)
* @see org.dspace.services.SessionService#getCurrentSessionId()
*/
public String getCurrentSessionId() {
Request req = requests.getCurrent();
if (req != null) {
Session session = req.getSession();
if (session != null) {
return session.getSessionId();
}
}
return null;
}
/* (non-Javadoc)
* @see org.dspace.services.SessionService#getCurrentUserId()
/** (non-Javadoc)
* @see org.dspace.services.RequestService#getCurrentUserId()
*/
public String getCurrentUserId() {
String userId = null;
Session session = getCurrentSession();
if (session != null) {
userId = session.getUserId();
Request currentRequest = getCurrentRequest();
if(currentRequest == null) {
return null;
} else {
return Objects.toString(currentRequest.getAttribute(AUTHENTICATED_EPERSON));
}
}
/** (non-Javadoc)
* @see org.dspace.services.RequestService#setCurrentUserId()
*/
public void setCurrentUserId(UUID epersonId) {
Request currentRequest = getCurrentRequest();
if(currentRequest != null) {
getCurrentRequest().setAttribute(AUTHENTICATED_EPERSON, epersonId);
}
return userId;
}
/* (non-Javadoc)

View File

@@ -7,21 +7,18 @@
*/
package org.dspace.services.sessions.model;
import org.dspace.services.model.Request;
import org.dspace.services.model.Session;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.dspace.services.model.Request;
public final class HttpRequestImpl extends AbstractRequestImpl implements Request {
private transient ServletRequest servletRequest = null;
private transient ServletResponse servletResponse = null;
private Session session = null;
public HttpRequestImpl(ServletRequest request, ServletResponse response) {
if (request == null || response == null) {
throw new IllegalArgumentException("Cannot create a request without an http request or response");
@@ -29,11 +26,6 @@ public final class HttpRequestImpl extends AbstractRequestImpl implements Reques
this.servletRequest = request;
this.servletResponse = response;
if (servletRequest instanceof HttpServletRequest) {
this.session = new SessionImpl((HttpServletRequest)servletRequest);
} else {
this.session = new SessionImpl();
}
}
public ServletRequest getServletRequest() {
@@ -48,10 +40,6 @@ public final class HttpRequestImpl extends AbstractRequestImpl implements Reques
return null;
}
public Session getSession() {
return session;
}
public ServletResponse getServletResponse() {
return servletResponse;
}

View File

@@ -1,198 +0,0 @@
/**
* 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.services.sessions.model;
import java.util.Enumeration;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionContext;
/**
* This is a special HTTP session object that stands in for a real one
* when sessions are not initiated or associated by HTTP requests.
*
* @author Aaron Zeckoski (azeckoski @ gmail.com)
*/
@SuppressWarnings("deprecation")
public final class InternalHttpSession implements HttpSession {
private final String id;
private long lastAccessedTime = System.currentTimeMillis();
private long creationTime = System.currentTimeMillis();
private int maxInactiveInternal = 1800;
private boolean invalidated = false;
private ConcurrentHashMap<String, Object> attributes = null;
private ConcurrentHashMap<String, Object> getAttributes() {
if (this.attributes == null) {
this.attributes = new ConcurrentHashMap<String, Object>();
}
return this.attributes;
}
public InternalHttpSession() {
this.id = UUID.randomUUID().toString();
}
private void checkInvalidated() {
if (invalidated) {
throw new IllegalStateException("This session is no longer valid");
}
}
@Override
public String toString() {
return "internalSession:" + this.id + ":" + this.creationTime + ":" + this.invalidated + ":" + super.toString();
}
/* (non-Javadoc)
* @see javax.servlet.http.HttpSession#getAttribute(java.lang.String)
*/
public Object getAttribute(String name) {
checkInvalidated();
return getAttributes().get(name);
}
/* (non-Javadoc)
* @see javax.servlet.http.HttpSession#getAttributeNames()
*/
@SuppressWarnings("unchecked")
public Enumeration getAttributeNames() {
checkInvalidated();
return getAttributes().keys();
}
/* (non-Javadoc)
* @see javax.servlet.http.HttpSession#getCreationTime()
*/
public long getCreationTime() {
checkInvalidated();
return creationTime;
}
/* (non-Javadoc)
* @see javax.servlet.http.HttpSession#getId()
*/
public String getId() {
checkInvalidated();
return id;
}
/* (non-Javadoc)
* @see javax.servlet.http.HttpSession#getLastAccessedTime()
*/
public long getLastAccessedTime() {
checkInvalidated();
return lastAccessedTime;
}
/* (non-Javadoc)
* @see javax.servlet.http.HttpSession#getMaxInactiveInterval()
*/
public int getMaxInactiveInterval() {
checkInvalidated();
return maxInactiveInternal;
}
/* (non-Javadoc)
* @see javax.servlet.http.HttpSession#getServletContext()
*/
public ServletContext getServletContext() {
checkInvalidated();
return null;
}
/* (non-Javadoc)
* @see javax.servlet.http.HttpSession#getSessionContext()
*/
public HttpSessionContext getSessionContext() {
checkInvalidated();
return null;
}
/* (non-Javadoc)
* @see javax.servlet.http.HttpSession#getValue(java.lang.String)
*/
public Object getValue(String name) {
checkInvalidated();
return getAttributes().get(name);
}
/* (non-Javadoc)
* @see javax.servlet.http.HttpSession#getValueNames()
*/
public String[] getValueNames() {
checkInvalidated();
Set<String> names = getAttributes().keySet();
return names.toArray(new String[names.size()]);
}
/* (non-Javadoc)
* @see javax.servlet.http.HttpSession#invalidate()
*/
public void invalidate() {
invalidated = true;
if (this.attributes != null) {
this.attributes.clear();
this.attributes = null;
}
}
/* (non-Javadoc)
* @see javax.servlet.http.HttpSession#isNew()
*/
public boolean isNew() {
return false;
}
/* (non-Javadoc)
* @see javax.servlet.http.HttpSession#putValue(java.lang.String, java.lang.Object)
*/
public void putValue(String name, Object value) {
checkInvalidated();
getAttributes().put(name, value);
}
/* (non-Javadoc)
* @see javax.servlet.http.HttpSession#removeAttribute(java.lang.String)
*/
public void removeAttribute(String name) {
checkInvalidated();
getAttributes().remove(name);
}
/* (non-Javadoc)
* @see javax.servlet.http.HttpSession#removeValue(java.lang.String)
*/
public void removeValue(String name) {
checkInvalidated();
getAttributes().remove(name);
}
/* (non-Javadoc)
* @see javax.servlet.http.HttpSession#setAttribute(java.lang.String, java.lang.Object)
*/
public void setAttribute(String name, Object value) {
checkInvalidated();
getAttributes().put(name, value);
}
/* (non-Javadoc)
* @see javax.servlet.http.HttpSession#setMaxInactiveInterval(int)
*/
public void setMaxInactiveInterval(int interval) {
checkInvalidated();
this.maxInactiveInternal = interval;
}
}

View File

@@ -7,22 +7,20 @@
*/
package org.dspace.services.sessions.model;
import org.dspace.services.model.Request;
import org.dspace.services.model.Session;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
import org.dspace.services.model.Request;
public final class InternalRequestImpl extends AbstractRequestImpl implements Request {
private Map<String, Object> attributes = new HashMap<String, Object>();
private Session session = new SessionImpl();
public InternalRequestImpl() {
}
@@ -34,10 +32,6 @@ public final class InternalRequestImpl extends AbstractRequestImpl implements Re
return null;
}
public Session getSession() {
return session;
}
public ServletResponse getServletResponse() {
return null;
}

View File

@@ -1,515 +0,0 @@
/**
* 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.services.sessions.model;
import java.io.Serializable;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionContext;
import org.dspace.services.model.Session;
/**
* Represents a users session (login session) in the system. Can hold
* some additional attributes as needed, but the underlying
* implementation may limit the number and size of attributes to ensure
* that session replication is not impacted negatively.
*
* @author Aaron Zeckoski (azeckoski @ gmail.com)
*/
@SuppressWarnings("deprecation")
public final class SessionImpl implements Session {
// keys for things stored in the session
public static final String SESSION_ID = "dspaceSessionId";
public static final String USER_ID = "userId";
public static final String USER_EID = "userEid";
public static final String SERVER_ID = "serverId";
public static final String HOST_IP = "originatingHostIP";
public static final String HOST_NAME = "originatingHostName";
/**
* This is the only thing that is actually replicated across the cluster.
*/
private transient HttpSession httpSession;
/**
* Make a session that is associated with the current HTTP request.
*
* @param request current request
*/
public SessionImpl(HttpServletRequest request) {
if (request == null) {
throw new IllegalArgumentException("Cannot create a session without an http request");
}
this.httpSession = request.getSession(); // establish the session
setKeyAttribute(HOST_IP, request.getRemoteAddr());
setKeyAttribute(HOST_NAME, request.getRemoteHost());
}
/**
* Make a session which is not associated with the current HTTP request.
*/
public SessionImpl() {
// creates a new internal http session that is not cached anywhere
this.httpSession = new InternalHttpSession();
try {
InetAddress i4 = Inet4Address.getLocalHost();
setKeyAttribute(HOST_IP, i4.getHostAddress()); // IP address
setKeyAttribute(HOST_NAME, i4.getHostName());
} catch (UnknownHostException e) {
// could not get address
setKeyAttribute(HOST_IP, "10.0.0.1"); // set a fake one I guess
}
}
/**
* Set the sessionId. Normally this should not probably happen much.
* @param sessionId the session ID
*/
public void setSessionId(String sessionId) {
if (! isAttributeSet(SESSION_ID)) {
if (isBlank(sessionId)) {
// just use the http session id
setKeyAttribute(SESSION_ID, this.httpSession.getId());
} else {
setKeyAttribute(SESSION_ID, sessionId);
}
}
}
/**
* Set the userId and userEid. This should only happen when
* re-binding the session or clearing the associated user.
* If userId is null then user is cleared.
*
* @param userId the user ID
* @param userEid the user EID
*/
public void setUserId(String userId, String userEid) {
if (isBlank(userId)) {
removeKeyAttribute(USER_ID);
removeKeyAttribute(USER_EID);
} else {
setKeyAttribute(USER_ID, userId);
setKeyAttribute(USER_EID, userEid);
}
}
/**
* Set the DSpace serverId which originated this session.
*
* @param serverId the serverId
*/
public void setServerId(String serverId) {
setKeyAttribute(SERVER_ID, serverId);
}
/**
* Are all required values set? Notice that the sense of the test
* is the complement of what the name of this method implies.
*
* @return true if this session already has all the required values
* needed to complete it. This means the serverId and other values
* in the session are already set.
*/
public boolean isIncomplete() {
boolean complete = false;
if (isAttributeSet(SERVER_ID)
&& isAttributeSet(SESSION_ID)
&& isAttributeSet(HOST_IP)) {
complete = true;
}
return ! complete;
}
/**
* @param key the attribute key
* @return true if the attribute is set
*/
public boolean isAttributeSet(String key) {
return getKeyAttribute(key) != null;
}
/**
* @return true if this session is invalidated
*/
public boolean isInvalidated() {
boolean invalid = true;
if (this.httpSession != null) {
try {
this.httpSession.getCreationTime();
invalid = false;
} catch (IllegalStateException e) {
invalid = true;
}
} else {
// no httpsession
invalid = false;
}
return invalid;
}
/**
* Handles the general setting of things in the session.
* Use this to build other set methods.
* Handles checking the session is still valid, and
* the checking for null values in the value and key.
*
* @param key the key to use
* @param value the value to set
* @return true if the value was set, false if cleared or failure
* @throws IllegalArgumentException if the key is null
*/
protected boolean setKeyAttribute(String key, String value) {
if (key == null) {
throw new IllegalArgumentException("session attribute key cannot be null");
}
boolean wasSet = false;
if (! isInvalidated()) {
if (isBlank(value)) {
this.httpSession.removeAttribute(key);
} else {
this.httpSession.setAttribute(key, value);
wasSet = true;
}
}
return wasSet;
}
/**
* Handles the general getting of things from the session.
* Use this to build other set methods.
* Checks that the session is still valid.
*
* @param key the key to use
* @return the value OR null if not found
* @throws IllegalArgumentException if the key is null
*/
protected String getKeyAttribute(String key) {
if (key == null) {
throw new IllegalArgumentException("session attribute key cannot be null");
}
String value = null;
if (! isInvalidated()) {
value = (String) this.httpSession.getAttribute(key);
}
return value;
}
/**
* Handles removal of attributes and related checks.
*
* @param key the key to use
* @throws IllegalArgumentException if the key is null
*/
protected void removeKeyAttribute(String key) {
if (key == null) {
throw new IllegalArgumentException("session attribute key cannot be null");
}
if (! isInvalidated()) {
this.httpSession.removeAttribute(key);
}
}
@Override
public boolean equals(Object obj) {
// sessions are equal if the ids are the same, allows comparison across reloaded items
if (null == obj) {
return false;
}
if (!(obj instanceof SessionImpl)) {
return false;
} else {
SessionImpl castObj = (SessionImpl) obj;
boolean eq;
try {
eq = this.getId().equals(castObj.getId());
} catch (IllegalStateException e) {
eq = false;
}
return eq;
}
}
@Override
public int hashCode() {
String hashStr = this.getClass().getName() + ":" + this.httpSession.toString();
return hashStr.hashCode();
}
@Override
public String toString() {
String str;
if (isInvalidated()) {
str = "invalidated:" + this.httpSession.toString() + ":" + super.toString();
} else {
str = "active:"+getId()+":user="+getUserId()+"("+getUserEID()+"):sid="+getSessionId()+":server="+getServerId()+":created="+getCreationTime()+":accessed="+getLastAccessedTime()+":maxInactiveSecs="+getMaxInactiveInterval()+":hostIP="+getOriginatingHostIP()+":hostName="+getOriginatingHostName()+":"+super.toString();
}
return "Session:" + str;
}
// INTERFACE methods
/* (non-Javadoc)
* @see org.dspace.services.model.Session#getAttribute(java.lang.String)
*/
@Override
public String getAttribute(String key) {
return getKeyAttribute(key);
}
/* (non-Javadoc)
* @see org.dspace.services.model.Session#setAttribute(java.lang.String, java.lang.String)
*/
@Override
public void setAttribute(String key, String value) {
setKeyAttribute(key, value);
}
/* (non-Javadoc)
* @see javax.servlet.http.HttpSession#removeAttribute(java.lang.String)
*/
@Override
public void removeAttribute(String name) {
removeKeyAttribute(name);
}
/* (non-Javadoc)
* @see javax.servlet.http.HttpSession#setAttribute(java.lang.String, java.lang.Object)
*/
@Override
public void setAttribute(String name, Object value) {
if (value != null && !(value instanceof String)) {
throw new UnsupportedOperationException("Invalid session attribute ("+name+","+value+"), Only strings can be stored in the session");
}
setKeyAttribute(name, (String) value);
}
/**
* @return a copy of the attributes in this session.
* Modifying it has no effect on the session attributes.
*/
@SuppressWarnings("unchecked")
@Override
public Map<String, String> getAttributes() {
Map<String, String> map = new HashMap<String, String>();
if (! isInvalidated()) {
Enumeration<String> names = this.httpSession.getAttributeNames();
while (names.hasMoreElements()) {
String key = names.nextElement();
String value = (String) this.httpSession.getAttribute(key);
map.put(key, value);
}
}
return map;
}
/* (non-Javadoc)
* @see javax.servlet.http.HttpSession#getAttributeNames()
*/
@SuppressWarnings("unchecked")
@Override
public Enumeration getAttributeNames() {
return this.httpSession.getAttributeNames();
}
/* (non-Javadoc)
* @see org.dspace.services.model.Session#clear()
*/
@SuppressWarnings("unchecked")
@Override
public void clear() {
if (! isInvalidated()) {
Enumeration<String> names = this.httpSession.getAttributeNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
this.httpSession.removeAttribute(name);
}
}
}
@Override
public String getOriginatingHostIP() {
return getKeyAttribute(HOST_IP);
}
@Override
public String getOriginatingHostName() {
return getKeyAttribute(HOST_NAME);
}
@Override
public String getServerId() {
return getKeyAttribute(SERVER_ID);
}
@Override
public String getSessionId() {
return getKeyAttribute(SESSION_ID);
}
@Override
public String getUserEID() {
return getKeyAttribute(USER_EID);
}
@Override
public String getUserId() {
return getKeyAttribute(USER_ID);
}
@Override
public boolean isActive() {
return ! isInvalidated();
}
// HTTP SESSION passthroughs
@Override
public long getCreationTime() {
return this.httpSession.getCreationTime();
}
@Override
public String getId() {
String id = null;
if (isAttributeSet(SESSION_ID)) {
id = getKeyAttribute(SESSION_ID);
} else {
id = this.httpSession.getId();
}
return id;
}
@Override
public long getLastAccessedTime() {
return this.httpSession.getLastAccessedTime();
}
@Override
public int getMaxInactiveInterval() {
return this.httpSession.getMaxInactiveInterval();
}
@Override
public void setMaxInactiveInterval(int interval) {
this.httpSession.setMaxInactiveInterval(interval);
}
@Override
public ServletContext getServletContext() {
if (this.httpSession != null) {
return this.httpSession.getServletContext();
}
throw new UnsupportedOperationException("No http session available for this operation");
}
@Override
public void invalidate() {
if (! isInvalidated()) {
this.httpSession.invalidate();
}
// TODO nothing otherwise?
}
@Override
public boolean isNew() {
if (! isInvalidated()) {
return this.httpSession.isNew();
}
return false;
}
// DEPRECATED
/* (non-Javadoc)
* @see javax.servlet.http.HttpSession#getValue(java.lang.String)
*/
@Override
public Object getValue(String name) {
return getKeyAttribute(name);
}
/* (non-Javadoc)
* @see javax.servlet.http.HttpSession#getValueNames()
*/
@Override
public String[] getValueNames() {
Set<String> keys = getAttributes().keySet();
return keys.toArray(new String[keys.size()]);
}
/* (non-Javadoc)
* @see javax.servlet.http.HttpSession#removeValue(java.lang.String)
*/
@Override
public void removeValue(String name) {
removeAttribute(name);
}
/* (non-Javadoc)
* @see javax.servlet.http.HttpSession#putValue(java.lang.String, java.lang.Object)
*/
@Override
public void putValue(String name, Object value) {
setAttribute(name, value);
}
@Override
public HttpSessionContext getSessionContext() {
if (this.httpSession != null) {
return this.httpSession.getSessionContext();
}
throw new UnsupportedOperationException("No http session available for this operation");
}
// END DEPRECATED
/**
* Check if something is blank (null or "").
*
* @param string string to check
* @return true if is blank
*/
public static boolean isBlank(String string) {
return (string == null) || ("".equals(string));
}
/**
* Compares sessions by the last time they were accessed, with more
* recent first.
*/
public static final class SessionLastAccessedComparator implements Comparator<Session>, Serializable {
public static final long serialVersionUID = 1l;
public int compare(Session o1, Session o2) {
try {
Long lat1 = Long.valueOf(o1.getLastAccessedTime());
Long lat2 = Long.valueOf(o2.getLastAccessedTime());
return lat2.compareTo(lat1); // reverse
} catch (Exception e) {
return 0;
}
}
}
}

View File

@@ -13,7 +13,6 @@ import org.dspace.kernel.ServiceManager;
import org.dspace.services.ConfigurationService;
import org.dspace.services.EventService;
import org.dspace.services.RequestService;
import org.dspace.services.SessionService;
/**
@@ -74,10 +73,6 @@ public final class DSpace {
public EventService getEventService() {
return getServiceManager().getServiceByName(EventService.class.getName(), EventService.class);
}
public SessionService getSessionService() {
return getServiceManager().getServiceByName(SessionService.class.getName(), SessionService.class);
}
public RequestService getRequestService() {
return getServiceManager().getServiceByName(RequestService.class.getName(), RequestService.class);

View File

@@ -53,10 +53,9 @@
</bean>
<!-- CACHING end beans -->
<!-- SESSION - session and request services (implemented as a single bean) -->
<bean id="org.dspace.services.SessionService" class="org.dspace.services.sessions.SessionRequestServiceImpl" />
<alias alias="org.dspace.services.RequestService" name="org.dspace.services.SessionService" />
<!-- SESSION end beans -->
<!-- REQUEST - request service (implemented as a single bean) -->
<bean id="org.dspace.services.RequestService" class="org.dspace.services.sessions.StatelessRequestServiceImpl" />
<!-- REQUEST end beans -->
<!-- EVENTS -->
<bean id="org.dspace.services.EventService" class="org.dspace.services.events.SystemEventService" />

View File

@@ -19,7 +19,6 @@ import org.dspace.kernel.DSpaceKernel;
import org.dspace.kernel.DSpaceKernelManager;
import org.dspace.kernel.ServiceManager;
import org.dspace.services.RequestService;
import org.dspace.services.SessionService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -34,7 +33,6 @@ public class SampleServlet extends HttpServlet {
private static Logger log = LoggerFactory.getLogger(SampleServlet.class);
private transient SessionService sessionService;
private transient RequestService requestService;
@Override
@@ -49,10 +47,6 @@ public class SampleServlet extends HttpServlet {
throw new IllegalStateException("DSpace Kernel is not running, cannot startup the DirectServlet");
}
ServiceManager serviceManager = kernel.getServiceManager();
sessionService = serviceManager.getServiceByName(SessionService.class.getName(), SessionService.class);
if (sessionService == null) {
throw new IllegalStateException("Could not get the DSpace SessionService");
}
requestService = serviceManager.getServiceByName(RequestService.class.getName(), RequestService.class);
if (requestService == null) {
throw new IllegalStateException("Could not get the DSpace RequestService");
@@ -77,7 +71,7 @@ public class SampleServlet extends HttpServlet {
PrintWriter writer = res.getWriter();
writer.print(XML_HEADER);
writer.print(XHTML_HEADER);
writer.print("DSpaceTest:session=" + sessionService.getCurrentSessionId() + ":request=" + requestService.getCurrentRequestId());
writer.print("DSpaceTest: request=" + requestService.getCurrentRequestId());
writer.print(XHTML_FOOTER);
res.setStatus(HttpServletResponse.SC_OK);

View File

@@ -8,7 +8,6 @@
package org.dspace.services.session;
import org.dspace.services.model.RequestInterceptor;
import org.dspace.services.model.Session;
/**
@@ -24,7 +23,7 @@ public class MockRequestInterceptor implements RequestInterceptor {
/* (non-Javadoc)
* @see org.dspace.services.model.RequestInterceptor#onEnd(java.lang.String, org.dspace.services.model.Session, boolean, java.lang.Exception)
*/
public void onEnd(String requestId, Session session, boolean succeeded, Exception failure) {
public void onEnd(String requestId, boolean succeeded, Exception failure) {
if (succeeded) {
state = "end:success:" + requestId;
} else {
@@ -36,7 +35,7 @@ public class MockRequestInterceptor implements RequestInterceptor {
/* (non-Javadoc)
* @see org.dspace.services.model.RequestInterceptor#onStart(java.lang.String, org.dspace.services.model.Session)
*/
public void onStart(String requestId, Session session) {
public void onStart(String requestId) {
state = "start:" + requestId;
hits++;
}

View File

@@ -7,103 +7,100 @@
*/
package org.dspace.services.session;
import static org.junit.Assert.*;
import java.util.List;
import org.dspace.services.CachingService;
import org.dspace.services.model.Cache;
import org.dspace.services.model.CacheConfig;
import org.dspace.services.model.Session;
import org.dspace.services.model.CacheConfig.CacheScope;
import org.dspace.services.sessions.SessionRequestServiceImpl;
import org.dspace.services.sessions.StatelessRequestServiceImpl;
import org.dspace.test.DSpaceAbstractKernelTest;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Testing the request and session services
*
* @author Aaron Zeckoski (azeckoski @ gmail.com)
*/
public class SessionRequestServiceImplTest extends DSpaceAbstractKernelTest {
public class StatelessRequestServiceImplTest extends DSpaceAbstractKernelTest {
private SessionRequestServiceImpl sessionRequestService;
private StatelessRequestServiceImpl statelessRequestService;
private CachingService cachingService;
@Before
public void before() {
sessionRequestService = getService(SessionRequestServiceImpl.class);
statelessRequestService = getService(StatelessRequestServiceImpl.class);
cachingService = getService(CachingService.class);
}
@After
public void after() {
sessionRequestService.clear();
statelessRequestService.clear();
cachingService.resetCaches();
sessionRequestService = null;
statelessRequestService = null;
cachingService = null;
}
/**
* Test method for {@link org.dspace.services.sessions.SessionRequestServiceImpl#startRequest()}.
* Test method for {@link org.dspace.services.sessions.StatelessRequestServiceImpl#startRequest()}.
*/
@Test
public void testStartRequest() {
String requestId = sessionRequestService.startRequest();
String requestId = statelessRequestService.startRequest();
assertNotNull(requestId);
sessionRequestService.endRequest(null);
statelessRequestService.endRequest(null);
}
/**
* Test method for {@link org.dspace.services.sessions.SessionRequestServiceImpl#endRequest(java.lang.Exception)}.
* Test method for {@link org.dspace.services.sessions.StatelessRequestServiceImpl#endRequest(java.lang.Exception)}.
*/
@Test
public void testEndRequest() {
String requestId = sessionRequestService.startRequest();
String requestId = statelessRequestService.startRequest();
assertNotNull(requestId);
sessionRequestService.endRequest(null);
statelessRequestService.endRequest(null);
assertNull( getRequestCache() );
}
/**
* Test method for {@link org.dspace.services.sessions.SessionRequestServiceImpl#registerRequestInterceptor(org.dspace.services.model.RequestInterceptor)}.
* Test method for {@link org.dspace.services.sessions.StatelessRequestServiceImpl#registerRequestInterceptor(org.dspace.services.model.RequestInterceptor)}.
*/
@Test
public void testRegisterRequestListener() {
MockRequestInterceptor mri = new MockRequestInterceptor();
sessionRequestService.registerRequestInterceptor(mri);
statelessRequestService.registerRequestInterceptor(mri);
assertEquals("", mri.state);
assertEquals(0, mri.hits);
String requestId = sessionRequestService.startRequest();
String requestId = statelessRequestService.startRequest();
assertEquals(1, mri.hits);
assertTrue( mri.state.startsWith("start") );
assertTrue( mri.state.contains(requestId));
sessionRequestService.endRequest(null);
statelessRequestService.endRequest(null);
assertEquals(2, mri.hits);
assertTrue( mri.state.startsWith("end") );
assertTrue( mri.state.contains("success"));
assertTrue( mri.state.contains(requestId));
requestId = sessionRequestService.startRequest();
requestId = statelessRequestService.startRequest();
assertEquals(3, mri.hits);
assertTrue( mri.state.startsWith("start") );
assertTrue( mri.state.contains(requestId));
sessionRequestService.endRequest( new RuntimeException("Oh Noes!") );
statelessRequestService.endRequest( new RuntimeException("Oh Noes!") );
assertEquals(4, mri.hits);
assertTrue( mri.state.startsWith("end") );
assertTrue( mri.state.contains("fail"));
assertTrue( mri.state.contains(requestId));
try {
sessionRequestService.registerRequestInterceptor(null);
statelessRequestService.registerRequestInterceptor(null);
fail("should have thrown exception");
} catch (IllegalArgumentException e) {
assertNotNull(e.getMessage());
@@ -111,49 +108,31 @@ public class SessionRequestServiceImplTest extends DSpaceAbstractKernelTest {
}
/**
* Test method for {@link org.dspace.services.sessions.SessionRequestServiceImpl#getCurrentSession()}.
*/
@Test
public void testGetCurrentSession() {
Session current = sessionRequestService.getCurrentSession();
assertNull(current);
}
/**
* Test method for {@link org.dspace.services.sessions.SessionRequestServiceImpl#getCurrentSessionId()}.
*/
@Test
public void testGetCurrentSessionId() {
String current = sessionRequestService.getCurrentSessionId();
assertNull(current);
}
/**
* Test method for {@link org.dspace.services.sessions.SessionRequestServiceImpl#getCurrentUserId()}.
* Test method for {@link org.dspace.services.sessions.StatelessRequestServiceImpl#getCurrentUserId()}.
*/
@Test
public void testGetCurrentUserId() {
String current = sessionRequestService.getCurrentUserId();
String current = statelessRequestService.getCurrentUserId();
assertNull(current);
}
/**
* Test method for {@link org.dspace.services.sessions.SessionRequestServiceImpl#getCurrentRequestId()}.
* Test method for {@link org.dspace.services.sessions.StatelessRequestServiceImpl#getCurrentRequestId()}.
*/
@Test
public void testGetCurrentRequestId() {
String requestId = sessionRequestService.getCurrentRequestId();
String requestId = statelessRequestService.getCurrentRequestId();
assertNull(requestId); // no request yet
String rid = sessionRequestService.startRequest();
String rid = statelessRequestService.startRequest();
requestId = sessionRequestService.getCurrentRequestId();
requestId = statelessRequestService.getCurrentRequestId();
assertNotNull(requestId);
assertEquals(rid, requestId);
sessionRequestService.endRequest(null);
statelessRequestService.endRequest(null);
requestId = sessionRequestService.getCurrentRequestId();
requestId = statelessRequestService.getCurrentRequestId();
assertNull(requestId); // no request yet
}

View File

@@ -181,18 +181,24 @@
<artifactId>spring-boot-starter-data-rest</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
<!-- <dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<scope>test</scope>
<version>${json-path.version}</version>
</dependency> -->
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path-assert</artifactId>
<version>${json-path.version}</version>
<scope>test</scope>
</dependency>
<!-- The HAL Browser -->
<dependency>
@@ -206,10 +212,11 @@
</dependency>
<!-- Add in Spring Security for AuthN and AuthZ -->
<!-- <dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency> -->
<version>${spring-boot.version}</version>
</dependency>
<!-- Add in log4j support by excluding default logging, and using starter-log4j -->
<!-- See: http://docs.spring.io/spring-boot/docs/current/reference/html/howto-logging.html#howto-configure-log4j-for-logging -->
@@ -256,6 +263,11 @@
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>4.23</version>
</dependency>
<!-- TEST DEPENDENCIES -->
<dependency> <!-- Keep jmockit before junit -->
@@ -288,11 +300,6 @@
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path-assert</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.dspace</groupId>
<artifactId>dspace-solr</artifactId>

View File

@@ -8,11 +8,13 @@
package org.dspace.app.rest;
import java.io.File;
import java.sql.SQLException;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.servlet.Filter;
import org.apache.commons.lang3.StringUtils;
import org.dspace.app.rest.filter.DSpaceRequestContextFilter;
import org.dspace.app.rest.model.hateoas.DSpaceRelProvider;
import org.dspace.app.rest.utils.ApplicationConfig;
@@ -79,7 +81,9 @@ public class Application extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class)
.initializers(new DSpaceKernelInitializer());
.initializers(
new DSpaceKernelInitializer(),
new FlywayDatabaseMigrationInitializer());
}
@Bean
@@ -126,7 +130,7 @@ public class Application extends SpringBootServletInitializer {
}
@Bean
public RequestContextListener requestContextListener() {
public RequestContextListener requestContextListener(){
return new RequestContextListener();
}
@@ -235,4 +239,28 @@ public class Application extends SpringBootServletInitializer {
return providedHome;
}
}
private class FlywayDatabaseMigrationInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
//Run the Flyway migrations by create a Context and Database connection
org.dspace.core.Context context = new org.dspace.core.Context();
boolean failed = false;
try {
if(context.getDBConfig() == null || StringUtils.isBlank(context.getDBConfig().getDatabaseUrl())) {
failed = true;
}
context.complete();
} catch (SQLException e) {
failed = true;
}
if(failed) {
throw new RuntimeException("Unable to initialize the database");
}
}
}
}

View File

@@ -0,0 +1,132 @@
/**
* 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;
import java.sql.SQLException;
import java.util.Arrays;
import javax.servlet.http.HttpServletRequest;
import org.dspace.app.rest.converter.EPersonConverter;
import org.dspace.app.rest.model.AuthnRest;
import org.dspace.app.rest.model.EPersonRest;
import org.dspace.app.rest.model.AuthenticationStatusRest;
import org.dspace.app.rest.model.hateoas.AuthenticationStatusResource;
import org.dspace.app.rest.model.hateoas.AuthnResource;
import org.dspace.app.rest.utils.ContextUtil;
import org.dspace.app.rest.utils.Utils;
import org.dspace.core.Context;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.Link;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* Rest controller that handles authentication on the REST API together with the Spring Security filters
* configured in {@link org.dspace.app.rest.security.WebSecurityConfiguration}
*
* @author Atmire NV (info at atmire dot com)
*/
@RequestMapping(value = "/api/" + AuthnRest.CATEGORY)
@RestController
public class AuthenticationRestController implements InitializingBean {
private static final Logger log = LoggerFactory.getLogger(AuthenticationRestController.class);
@Autowired
DiscoverableEndpointsService discoverableEndpointsService;
@Autowired
private EPersonConverter ePersonConverter;
@Autowired
private Utils utils;
@Override
public void afterPropertiesSet() {
discoverableEndpointsService.register(this, Arrays.asList(new Link("/api/" + AuthnRest.CATEGORY, AuthnRest.NAME)));
}
@RequestMapping(method = RequestMethod.GET)
public AuthnResource authn() throws SQLException {
return new AuthnResource(new AuthnRest(), utils);
}
@RequestMapping(value = "/status", method = RequestMethod.GET)
public AuthenticationStatusResource status(HttpServletRequest request) throws SQLException {
Context context = ContextUtil.obtainContext(request);
EPersonRest ePersonRest = null;
if (context.getCurrentUser() != null) {
ePersonRest = ePersonConverter.fromModel(context.getCurrentUser());
}
AuthenticationStatusResource authenticationStatusResource = new AuthenticationStatusResource( new AuthenticationStatusRest(ePersonRest), utils);
return authenticationStatusResource;
}
@RequestMapping(value = "/login", method = {RequestMethod.GET, RequestMethod.POST})
public ResponseEntity login(HttpServletRequest request, @RequestParam(name = "user", required = false) String user,
@RequestParam(name = "password", required = false) String password) {
//If you can get here, you should be authenticated, the actual login is handled by spring security
//see org.dspace.app.rest.security.StatelessLoginFilter
//If we don't have an EPerson here, this means authentication failed and we should return an error message.
return getLoginResponse(request, "Authentication failed for user " + user + ": The credentials you provided are not valid.");
}
//TODO This should be moved under API, but then we also need to update org.dspace.authenticate.ShibAuthentication
//This is also not gonna work until it is moved
@RequestMapping(value = "/shibboleth-login", method = {RequestMethod.GET, RequestMethod.POST})
public ResponseEntity shibbolethLogin(HttpServletRequest request) {
//If you can get here, you should be authenticated, the actual login is handled by spring security.
//If not, no valid Shibboleth session is present or Shibboleth config is missing.
/* Make sure to apply
- AuthType shibboleth
- ShibRequireSession On
- ShibUseHeaders On
- require valid-user
to this endpoint. The Shibboleth daemon will then take care of redirecting you to the login page if
necessary.
*/
//TODO we should redirect the user to a correct page in the UI. These could be provided as optional parameters.
return getLoginResponse(request, "Shibboleth authentication failed: No valid Shibboleth session could be found.");
}
@RequestMapping(value = "/logout", method = {RequestMethod.GET, RequestMethod.POST})
public ResponseEntity logout() {
//This is handled by org.dspace.app.rest.security.CustomLogoutHandler
return ResponseEntity.ok().build();
}
protected ResponseEntity getLoginResponse(HttpServletRequest request, String failedMessage) {
//Get the context and check if we have an authenticated eperson
org.dspace.core.Context context = null;
context = ContextUtil.obtainContext(request);
if(context == null || context.getCurrentUser() == null) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(failedMessage);
} else {
//We have a user, so the login was successful.
return ResponseEntity.ok().build();
}
}
}

View File

@@ -7,6 +7,8 @@
*/
package org.dspace.app.rest;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.ResourceSupport;
@@ -14,8 +16,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* This is the main entry point of the new REST API. Its responsibility is to
* provide a consistent behaviors for all the exposed resources in terms of

View File

@@ -11,8 +11,8 @@ import java.util.ArrayList;
import java.util.List;
import org.apache.log4j.Logger;
import org.dspace.app.rest.model.GroupRest;
import org.dspace.app.rest.model.EPersonRest;
import org.dspace.app.rest.model.GroupRest;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;
import org.springframework.beans.factory.annotation.Autowired;

View File

@@ -0,0 +1,86 @@
/**
* 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.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.dspace.app.rest.RestResourceController;
/**
* Find out your authentication status.
*
*/
public class AuthenticationStatusRest extends BaseObjectRest<Integer>
{
private boolean okay;
private boolean authenticated;
public static final String NAME = "status";
public static final String CATEGORY = "authn";
@Override
public String getCategory() {
return CATEGORY;
}
@Override
public String getType() {
return NAME;
}
@Override
@JsonIgnore
public String getTypePlural() {
return getType();
}
public Class getController() {
return RestResourceController.class;
}
private EPersonRest ePersonRest;
public AuthenticationStatusRest() {
setOkay(true);
setAuthenticated(false);
}
public AuthenticationStatusRest(EPersonRest eperson) {
setOkay(true);
if(eperson != null) {
setAuthenticated(true);
this.ePersonRest = eperson;
}
}
@LinkRest(linkClass = EPersonRest.class, name = "eperson", optional = true)
@JsonIgnore
public EPersonRest getEPersonRest() {
return ePersonRest;
}
public void setEPersonRest(EPersonRest ePersonRest) {
this.ePersonRest = ePersonRest;
}
public boolean isAuthenticated() {
return authenticated;
}
public void setAuthenticated(boolean authenticated) {
this.authenticated = authenticated;
}
public boolean isOkay() {
return okay;
}
public void setOkay(boolean okay) {
this.okay = okay;
}
}

View File

@@ -0,0 +1,33 @@
/**
* 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.model;
import org.dspace.app.rest.AuthenticationRestController;
/**
* Root rest object for the /api/authn endpoint
*
* @author Atmire NV (info at atmire dot com)
*/
public class AuthnRest extends BaseObjectRest<Integer>{
public static final String NAME = "authn";
public static final String CATEGORY = "authn";
public String getCategory() {
return CATEGORY;
}
public String getType() {
return NAME;
}
public Class getController() {
return AuthenticationRestController.class;
}
}

View File

@@ -9,9 +9,8 @@ package org.dspace.app.rest.model;
import java.util.List;
import org.dspace.app.rest.RestResourceController;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.dspace.app.rest.RestResourceController;
/**
* The BitstreamFormat REST Resource

View File

@@ -10,6 +10,7 @@ package org.dspace.app.rest.model;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.atteo.evo.inflector.English;
/**
* Methods to implement to make a REST resource addressable
@@ -29,6 +30,11 @@ public interface RestModel extends Serializable {
public String getType();
@JsonIgnore
default public String getTypePlural() {
return English.plural(getType());
}
@JsonIgnore
public Class getController();
}

View File

@@ -0,0 +1,24 @@
/**
* 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.model.hateoas;
import org.dspace.app.rest.model.AuthenticationStatusRest;
import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource;
import org.dspace.app.rest.utils.Utils;
/**
* Status Resource, wraps the status object and the authenticated EPerson
*
* @author Atmire NV (info at atmire dot com)
*/
@RelNameDSpaceResource(AuthenticationStatusRest.NAME)
public class AuthenticationStatusResource extends DSpaceResource<AuthenticationStatusRest> {
public AuthenticationStatusResource(AuthenticationStatusRest data, Utils utils, String... rels) {
super(data, utils, rels);
}
}

View File

@@ -0,0 +1,46 @@
/**
* 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.model.hateoas;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;
import java.sql.SQLException;
import org.dspace.app.rest.AuthenticationRestController;
import org.dspace.app.rest.model.AuthnRest;
import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource;
import org.dspace.app.rest.utils.Utils;
import org.springframework.hateoas.Link;
/**
* Authn Rest Resource, used to link to login, logout, status, ...
*
* @author Atmire NV (info at atmire dot com)
*/
@RelNameDSpaceResource(AuthnRest.NAME)
public class AuthnResource extends DSpaceResource<AuthnRest> {
public AuthnResource(AuthnRest data, Utils utils, String... rels) throws SQLException {
super(data, utils, rels);
AuthenticationRestController methodOn = methodOn(AuthenticationRestController.class);
add(new Link(linkTo(methodOn
.login(null, null, null))
.toUriComponentsBuilder().build().toString(), "login"));
add(new Link(linkTo(methodOn
.logout())
.toUriComponentsBuilder().build().toString(), "logout"));
add(new Link(linkTo(methodOn
.status(null))
.toUriComponentsBuilder().build().toString(), "status"));
}
}

View File

@@ -10,7 +10,7 @@ package org.dspace.app.rest.model.hateoas;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;
import org.atteo.evo.inflector.English;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import org.dspace.app.rest.RestResourceController;
import org.dspace.app.rest.model.BrowseEntryRest;
import org.dspace.app.rest.model.BrowseIndexRest;
@@ -19,8 +19,6 @@ import org.springframework.hateoas.Link;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.web.util.UriComponentsBuilder;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
/**
* Browse Entry Rest HAL Resource. The HAL Resource wraps the REST Resource
* adding support for the links and embedded resources
@@ -30,6 +28,7 @@ import com.fasterxml.jackson.annotation.JsonUnwrapped;
*/
@RelNameDSpaceResource(BrowseEntryRest.NAME)
public class BrowseEntryResource extends ResourceSupport {
@JsonUnwrapped
private final BrowseEntryRest data;
@@ -42,7 +41,7 @@ public class BrowseEntryResource extends ResourceSupport {
BrowseIndexRest bix = entry.getBrowseIndex();
RestResourceController methodOn = methodOn(RestResourceController.class, bix.getCategory(), bix.getType());
UriComponentsBuilder uriComponentsBuilder = linkTo(methodOn
.findRel(null, bix.getCategory(), English.plural(bix.getType()), bix.getId(), BrowseIndexRest.ITEMS, null, null, null))
.findRel(null, bix.getCategory(), bix.getTypePlural(), bix.getId(), BrowseIndexRest.ITEMS, null, null, null))
.toUriComponentsBuilder();
Link link = new Link(addFilterParams(uriComponentsBuilder).build().toString(), BrowseIndexRest.ITEMS);
add(link);

View File

@@ -7,23 +7,22 @@
*/
package org.dspace.app.rest.model.hateoas;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import org.apache.commons.lang3.StringUtils;
import org.atteo.evo.inflector.English;
import org.dspace.app.rest.model.BaseObjectRest;
import org.dspace.app.rest.model.LinkRest;
import org.dspace.app.rest.model.LinksRest;
@@ -33,17 +32,8 @@ import org.dspace.app.rest.repository.LinkRestRepository;
import org.dspace.app.rest.utils.Utils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.rest.webmvc.EmbeddedResourcesAssembler;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.PagedResources;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.core.EmbeddedWrappers;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
/**
* A base class for DSpace Rest HAL Resource. The HAL Resource wraps the REST
@@ -84,7 +74,6 @@ public abstract class DSpaceResource<T extends RestModel> extends ResourceSuppor
continue;
}
try {
//RestModel linkClass = linkAnnotation.linkClass().newInstance();
Method[] methods = linkRepository.getClass().getMethods();
boolean found = false;
for (Method m : methods) {
@@ -119,13 +108,16 @@ public abstract class DSpaceResource<T extends RestModel> extends ResourceSuppor
Link linkToSubResource = utils.linkToSubResource(data, name);
// no method is specified to retrieve the linked object(s) so check if it is already here
if (StringUtils.isBlank(linkAnnotation.method())) {
this.add(linkToSubResource);
Object linkedObject = readMethod.invoke(data);
Object wrapObject = linkedObject;
if (linkedObject instanceof RestModel) {
RestModel linkedRM = (RestModel) linkedObject;
wrapObject = utils.getResourceRepository(linkedRM.getCategory(), linkedRM.getType())
.wrapResource(linkedRM);
if(linkAnnotation.linkClass() != null && linkAnnotation.linkClass().isAssignableFrom(linkedRM.getClass())) {
linkToSubResource = utils.linkToSingleResource(linkedRM, name);
}
}
else {
if (linkedObject instanceof List) {
@@ -149,16 +141,18 @@ public abstract class DSpaceResource<T extends RestModel> extends ResourceSuppor
}
if (linkedObject != null) {
embedded.put(name, wrapObject);
} else {
this.add(linkToSubResource);
} else if(!linkAnnotation.optional()) {
embedded.put(name, null);
this.add(linkToSubResource);
}
Method writeMethod = pd.getWriteMethod();
writeMethod.invoke(data, new Object[] { null });
}
else {
// call the link repository
try {
//RestModel linkClass = linkAnnotation.linkClass().newInstance();
String apiCategory = data.getCategory();
String model = data.getType();
LinkRestRepository linkRepository = utils.getLinkResourceRepository(apiCategory, model, linkAnnotation.name());

View File

@@ -23,6 +23,7 @@ import org.springframework.beans.factory.annotation.Autowired;
*
*/
public abstract class AbstractDSpaceRestRepository {
@Autowired
protected Utils utils;
@@ -30,12 +31,6 @@ public abstract class AbstractDSpaceRestRepository {
protected Context obtainContext() {
Request currentRequest = requestService.getCurrentRequest();
Context context = (Context) currentRequest.getAttribute(ContextUtil.DSPACE_CONTEXT);
if (context != null && context.isValid()) {
return context;
}
context = new Context();
currentRequest.setAttribute(ContextUtil.DSPACE_CONTEXT, context);
return context;
return ContextUtil.obtainContext(currentRequest.getServletRequest());
}
}

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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.dspace.app.rest.security.jwt.JWTTokenHandler;
import org.dspace.app.rest.utils.ContextUtil;
import org.dspace.core.Context;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.stereotype.Component;
/**
* Custom logout handler to support stateless sessions
*
* @author Atmire NV (info at atmire dot com)
*/
@Component
public class CustomLogoutHandler implements LogoutHandler {
private static final Logger log = LoggerFactory.getLogger(JWTTokenHandler.class);
@Autowired
private RestAuthenticationService restAuthenticationService;
/**
* This method removes the session salt from an eperson, this way the token won't be verified anymore
* @param httpServletRequest
* @param httpServletResponse
* @param authentication
*/
public void logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) {
try {
Context context = ContextUtil.obtainContext(httpServletRequest);
restAuthenticationService.invalidateAuthenticationData(httpServletRequest, context);
context.commit();
} catch (Exception e) {
log.error("Unable to logout", e);
}
}
}

View File

@@ -0,0 +1,80 @@
/**
* 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.util.Collection;
import java.util.Date;
import java.util.List;
import org.dspace.eperson.EPerson;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
/**
* Custom Authentication for use with DSpace
*
* @author Atmire NV (info at atmire dot com)
*/
public class DSpaceAuthentication implements Authentication {
private Date previousLoginDate;
private String username;
private String password;
private List<GrantedAuthority> authorities;
private boolean authenticated = true;
public DSpaceAuthentication (EPerson ePerson, List<GrantedAuthority> authorities) {
this.previousLoginDate = ePerson.getPreviousActive();
this.username = ePerson.getEmail();
this.authorities = authorities;
}
public DSpaceAuthentication (String username, String password, List<GrantedAuthority> authorities) {
this.username = username;
this.password = password;
this.authorities = authorities;
}
public DSpaceAuthentication (String username, List<GrantedAuthority> authorities) {
this(username, null, authorities);
}
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
public Object getCredentials() {
return password;
}
public Object getDetails() {
return null;
}
public Object getPrincipal() {
return username;
}
public boolean isAuthenticated() {
return authenticated;
}
public void setAuthenticated(boolean authenticated) throws IllegalArgumentException {
this.authenticated = authenticated;
}
public String getName() {
return username;
}
public Date getPreviousLoginDate() {
return previousLoginDate;
}
}

View File

@@ -0,0 +1,161 @@
/**
* 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 static org.dspace.app.rest.security.WebSecurityConfiguration.ADMIN_GRANT;
import static org.dspace.app.rest.security.WebSecurityConfiguration.EPERSON_GRANT;
import java.sql.SQLException;
import java.util.LinkedList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
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.authorize.service.AuthorizeService;
import org.dspace.core.Context;
import org.dspace.core.LogManager;
import org.dspace.eperson.EPerson;
import org.dspace.services.RequestService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
/**
* This class is reponsible for authenticating a user via REST
*
* @author Atmire NV (info at atmire dot com)
*/
@Component
public class EPersonRestAuthenticationProvider implements AuthenticationProvider{
private static final Logger log = LoggerFactory.getLogger(EPersonRestAuthenticationProvider.class);
@Autowired
private AuthenticationService authenticationService;
@Autowired
private AuthorizeService authorizeService;
@Autowired
private RequestService requestService;
@Autowired
private HttpServletRequest request;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Context context = ContextUtil.obtainContext(request);
if(context != null && context.getCurrentUser() != null) {
return authenticateRefreshTokenRequest(context);
} else {
return authenticateNewLogin(authentication);
}
}
private Authentication authenticateRefreshTokenRequest(Context context) {
authenticationService.updateLastActiveDate(context);
return createAuthentication(null, context);
}
private Authentication authenticateNewLogin(Authentication authentication) {
Context newContext = null;
Authentication output = null;
if(authentication != null && authentication.getCredentials() != null) {
try {
newContext = new Context();
String name = authentication.getName();
String password = authentication.getCredentials().toString();
int implicitStatus = authenticationService.authenticateImplicit(newContext, null, null, null, request);
if (implicitStatus == AuthenticationMethod.SUCCESS) {
log.info(LogManager.getHeader(newContext, "login", "type=implicit"));
output = createAuthentication(password, newContext);
} else {
int authenticateResult = authenticationService.authenticate(newContext, name, password, null, request);
if (AuthenticationMethod.SUCCESS == authenticateResult) {
log.info(LogManager
.getHeader(newContext, "login", "type=explicit"));
output = createAuthentication(password, newContext);
} else {
log.info(LogManager.getHeader(newContext, "failed_login", "email="
+ name + ", result="
+ authenticateResult));
throw new BadCredentialsException("Login failed");
}
}
} catch (Exception e) {
log.error("Error while authenticating in the rest api", e);
} finally {
if (newContext != null && newContext.isValid()) {
try {
newContext.complete();
} catch (SQLException e) {
log.error(e.getMessage() + " occurred while trying to close", e);
}
}
}
}
return output;
}
private Authentication createAuthentication(final String password, final Context context) {
EPerson ePerson = context.getCurrentUser();
if(ePerson != null && StringUtils.isNotBlank(ePerson.getEmail())) {
//Pass the eperson ID to the request service
requestService.setCurrentUserId(ePerson.getID());
return new DSpaceAuthentication(ePerson, getGrantedAuthorities(context, ePerson));
} else {
log.info(LogManager.getHeader(context, "failed_login", "No eperson with an non-blank e-mail address found"));
throw new BadCredentialsException("Login failed");
}
}
public List<GrantedAuthority> getGrantedAuthorities(Context context, EPerson eperson) {
List<GrantedAuthority> authorities = new LinkedList<>();
if(eperson != null) {
boolean isAdmin = false;
try {
isAdmin = authorizeService.isAdmin(context, eperson);
} catch (SQLException e) {
log.error("SQL error while checking for admin rights", e);
}
if (isAdmin) {
authorities.add(new SimpleGrantedAuthority(ADMIN_GRANT));
}
authorities.add(new SimpleGrantedAuthority(EPERSON_GRANT));
}
return authorities;
}
public boolean supports(Class<?> authentication) {
return authentication.equals(DSpaceAuthentication.class);
}
}

View File

@@ -0,0 +1,35 @@
/**
* 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.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.springframework.stereotype.Service;
/**
* Interface for a service that can provide authentication for the REST API
*
* @author Atmire NV (info at atmire dot com)
*/
@Service
public interface RestAuthenticationService {
void addAuthenticationDataForUser(HttpServletRequest request, HttpServletResponse response, DSpaceAuthentication authentication) throws IOException;
EPerson getAuthenticatedEPerson(HttpServletRequest request, Context context);
boolean hasAuthenticationData(HttpServletRequest request);
void invalidateAuthenticationData(HttpServletRequest request, Context context) throws Exception;
}

View File

@@ -0,0 +1,94 @@
/**
* 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 java.util.List;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.dspace.app.rest.utils.ContextUtil;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.services.RequestService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
/**
* Custom Spring authentication filter for Stateless authentication, intercepts requests to check for valid
* authentication
*
* @author Atmire NV (info at atmire dot com)
*/
public class StatelessAuthenticationFilter extends BasicAuthenticationFilter{
private static final Logger log = LoggerFactory.getLogger(StatelessAuthenticationFilter.class);
private RestAuthenticationService restAuthenticationService;
private EPersonRestAuthenticationProvider authenticationProvider;
private RequestService requestService;
public StatelessAuthenticationFilter(AuthenticationManager authenticationManager,
RestAuthenticationService restAuthenticationService,
EPersonRestAuthenticationProvider authenticationProvider,
RequestService requestService) {
super(authenticationManager);
this.requestService = requestService;
this.restAuthenticationService = restAuthenticationService;
this.authenticationProvider = authenticationProvider;
}
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain) throws IOException, ServletException {
Authentication authentication = getAuthentication(req);
if (authentication != null ) {
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(req, res);
}
private Authentication getAuthentication(HttpServletRequest request) {
if (restAuthenticationService.hasAuthenticationData(request)) {
// parse the token.
Context context = ContextUtil.obtainContext(request);
EPerson eperson = restAuthenticationService.getAuthenticatedEPerson(request, context);
if (eperson != null) {
//Pass the eperson ID to the request service
requestService.setCurrentUserId(eperson.getID());
//Get the Spring authorities for this eperson
List<GrantedAuthority> authorities = authenticationProvider.getGrantedAuthorities(context, eperson);
//Return the Spring authentication object
return new DSpaceAuthentication(eperson.getEmail(), authorities);
} else {
return null;
}
}
return null;
}
}

View File

@@ -0,0 +1,66 @@
/**
* 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 java.util.ArrayList;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
/**
* This class will filter login requests to try and authenticate them
*
* @author Atmire NV (info at atmire dot com)
*/
public class StatelessLoginFilter extends AbstractAuthenticationProcessingFilter {
private AuthenticationManager authenticationManager;
private RestAuthenticationService restAuthenticationService;
public StatelessLoginFilter(String url, AuthenticationManager authenticationManager, RestAuthenticationService restAuthenticationService) {
super(new AntPathRequestMatcher(url));
this.authenticationManager = authenticationManager;
this.restAuthenticationService = restAuthenticationService;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res) throws AuthenticationException {
String user = req.getParameter("user");
String password = req.getParameter("password");
return authenticationManager.authenticate(
new DSpaceAuthentication(
user,
password,
new ArrayList<>())
);
}
@Override
protected void successfulAuthentication(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain,
Authentication auth) throws IOException, ServletException {
DSpaceAuthentication dSpaceAuthentication = (DSpaceAuthentication) auth;
restAuthenticationService.addAuthenticationDataForUser(req, res, dSpaceAuthentication);
}
}

View File

@@ -0,0 +1,102 @@
/**
* 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.services.RequestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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;
/**
* Spring Security configuration for DSpace Spring Rest
*
* @author Atmire NV (info at atmire dot com)
*/
@EnableWebSecurity
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
public static final String ADMIN_GRANT = "ADMIN";
public static final String EPERSON_GRANT = "EPERSON";
public static final String ANONYMOUS_GRANT = "ANONYMOUS";
@Autowired
private EPersonRestAuthenticationProvider ePersonRestAuthenticationProvider;
@Autowired
private RestAuthenticationService restAuthenticationService;
@Autowired
private RequestService requestService;
@Autowired
private CustomLogoutHandler customLogoutHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().cacheControl();
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
.servletApi().and()
//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()
//Logout configuration
.logout()
//On logout, clear the "session" salt
.addLogoutHandler(customLogoutHandler)
//Configure the logout entry point
.logoutRequestMatcher(new AntPathRequestMatcher("/api/authn/logout"))
//When logout is successful, return OK (200) status
.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
//Everyone can call this endpoint
.permitAll()
.and()
//Configure the URL patterns with their authentication requirements
.authorizeRequests()
//Allow GET and POST by anyone on the login endpoint
.antMatchers( "/api/authn/login").permitAll()
//Everyone can call GET on the status endpoint
.antMatchers(HttpMethod.GET, "/api/authn/status").permitAll()
.and()
//Add a filter before our login endpoints to do the authentication based on the data in the HTTP request
.addFilterBefore(new StatelessLoginFilter("/api/authn/login", authenticationManager(), restAuthenticationService), LogoutFilter.class)
//TODO see comment at org.dspace.app.rest.AuthenticationRestController.shibbolethLogin()
.addFilterBefore(new StatelessLoginFilter("/shibboleth-login", authenticationManager(), restAuthenticationService), LogoutFilter.class)
// Add a custom Token based authentication filter based on the token previously given to the client before each URL
.addFilterBefore(new StatelessAuthenticationFilter(authenticationManager(), restAuthenticationService,
ePersonRestAuthenticationProvider, requestService), StatelessLoginFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(ePersonRestAuthenticationProvider);
}
}

View File

@@ -0,0 +1,56 @@
/**
* 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.jwt;
import java.sql.SQLException;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import com.nimbusds.jwt.JWTClaimsSet;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.service.EPersonService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Provides a claim for a JSON Web Token, this claim is responsible for adding the EPerson ID to it
*
* @author Atmire NV (info at atmire dot com)
*/
@Component
public class EPersonClaimProvider implements JWTClaimProvider{
public static final String EPERSON_ID = "eid";
@Autowired
private EPersonService ePersonService;
public String getKey() {
return EPERSON_ID;
}
public Object getValue(Context context, HttpServletRequest request) {
return context.getCurrentUser().getID().toString();
}
public void parseClaim(Context context, HttpServletRequest request, JWTClaimsSet jwtClaimsSet) throws SQLException {
EPerson ePerson = getEPerson(context, jwtClaimsSet);
context.setCurrentUser(ePerson);
}
public EPerson getEPerson(Context context, JWTClaimsSet jwtClaimsSet) throws SQLException {
return ePersonService.find(context, getEPersonId(jwtClaimsSet));
}
private UUID getEPersonId(JWTClaimsSet jwtClaimsSet) {
return UUID.fromString(jwtClaimsSet.getClaim(EPERSON_ID).toString());
}
}

View File

@@ -0,0 +1,29 @@
/**
* 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.jwt;
import java.sql.SQLException;
import javax.servlet.http.HttpServletRequest;
import com.nimbusds.jwt.JWTClaimsSet;
import org.dspace.core.Context;
/**
* Interface to be implemented if you want to add a custom claim to a JSON Web Token, annotate with @Component
* to include it's implementation in the token
*
* @author Atmire NV (info at atmire dot com)
*/
public interface JWTClaimProvider {
String getKey();
Object getValue(Context context, HttpServletRequest request);
void parseClaim(Context context, HttpServletRequest request, JWTClaimsSet jwtClaimsSet) throws SQLException;
}

View File

@@ -0,0 +1,325 @@
/**
* 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.jwt;
import java.sql.SQLException;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import com.nimbusds.jose.CompressionAlgorithm;
import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWEHeader;
import com.nimbusds.jose.JWEObject;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.Payload;
import com.nimbusds.jose.crypto.DirectDecrypter;
import com.nimbusds.jose.crypto.DirectEncrypter;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.MACVerifier;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.jwt.util.DateUtils;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.dspace.authorize.AuthorizeException;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;
import org.dspace.eperson.service.EPersonService;
import org.dspace.services.ConfigurationService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
import org.springframework.security.crypto.keygen.KeyGenerators;
import org.springframework.stereotype.Component;
/**
* Class responsible for creating and parsing JWTs, supports both JWS and JWE
*
* @author Atmire NV (info at atmire dot com)
*/
@Component
public class JWTTokenHandler implements InitializingBean {
private static final int MAX_CLOCK_SKEW_SECONDS = 60;
private static final Logger log = LoggerFactory.getLogger(JWTTokenHandler.class);
@Autowired
private List<JWTClaimProvider> jwtClaimProviders;
@Autowired
private ConfigurationService configurationService;
@Autowired
private EPersonClaimProvider ePersonClaimProvider;
@Autowired
private EPersonService ePersonService;
private String jwtKey;
private long expirationTime;
private boolean includeIP;
private boolean encryptionEnabled;
private boolean compressionEnabled;
private byte[] encryptionKey;
@Override
public void afterPropertiesSet() throws Exception {
this.jwtKey = getSecret("jwt.token.secret");
this.encryptionKey = getSecret("jwt.encryption.secret").getBytes();
this.expirationTime = configurationService.getLongProperty("jwt.token.expiration", 30) * 60 * 1000;
this.includeIP = configurationService.getBooleanProperty("jwt.token.include.ip", true);
this.encryptionEnabled = configurationService.getBooleanProperty("jwt.encryption.enabled", false);
this.compressionEnabled = configurationService.getBooleanProperty("jwt.compression.enabled", false);
}
/**
* Retrieve EPerson from a jwt
*
* @param token
* @param request
* @param context
* @return
* @throws JOSEException
* @throws ParseException
* @throws SQLException
*/
public EPerson parseEPersonFromToken(String token, HttpServletRequest request, Context context) throws JOSEException, ParseException, SQLException {
if (StringUtils.isBlank(token)) {
return null;
}
SignedJWT signedJWT = getSignedJWT(token);
JWTClaimsSet jwtClaimsSet = signedJWT.getJWTClaimsSet();
EPerson ePerson = getEPerson(context, jwtClaimsSet);
if (isValidToken(request, signedJWT, jwtClaimsSet, ePerson)) {
log.debug("Received valid token for username: " + ePerson.getEmail());
for (JWTClaimProvider jwtClaimProvider : jwtClaimProviders) {
jwtClaimProvider.parseClaim(context, request, jwtClaimsSet);
}
return ePerson;
} else {
log.warn(getIpAddress(request) + " tried to use an expired or non-valid token");
return null;
}
}
/**
* Create a jwt with the EPerson details in it
*
* @param context
* @param request
* @param previousLoginDate
* @param groups
* @return
* @throws JOSEException
*/
public String createTokenForEPerson(Context context, HttpServletRequest request, Date previousLoginDate, List<Group> groups) throws JOSEException, SQLException {
EPerson ePerson = updateSessionSalt(context, previousLoginDate);
JWTClaimsSet claimsSet = buildJwtClaimsSet(context, request);
SignedJWT signedJWT = createSignedJWT(request, ePerson, claimsSet);
String token;
if (isEncryptionEnabled()) {
token = encryptJWT(signedJWT).serialize();
} else {
token = signedJWT.serialize();
}
return token;
}
public void invalidateToken(String token, HttpServletRequest request, Context context) throws Exception {
if (StringUtils.isNotBlank(token)) {
EPerson ePerson = parseEPersonFromToken(token, request, context);
if (ePerson != null) {
ePerson.setSessionSalt("");
}
}
}
public long getExpirationPeriod() {
return expirationTime;
}
public boolean isEncryptionEnabled() {
return encryptionEnabled;
}
public byte[] getEncryptionKey() {
return encryptionKey;
}
private JWEObject encryptJWT(SignedJWT signedJWT) throws JOSEException {
JWEObject jweObject = new JWEObject(
compression(new JWEHeader.Builder(JWEAlgorithm.DIR, EncryptionMethod.A128GCM)
.contentType("JWT"))
.build(), new Payload(signedJWT)
);
jweObject.encrypt(new DirectEncrypter(getEncryptionKey()));
return jweObject;
}
private boolean isValidToken(HttpServletRequest request, SignedJWT signedJWT, JWTClaimsSet jwtClaimsSet, EPerson ePerson) throws JOSEException {
if(StringUtils.isBlank(ePerson.getSessionSalt())) {
return false;
} else {
JWSVerifier verifier = new MACVerifier(buildSigningKey(request, ePerson));
//If token is valid and not expired return eperson in token
Date expirationTime = jwtClaimsSet.getExpirationTime();
return signedJWT.verify(verifier)
&& expirationTime != null
//Ensure expiration timestamp is after the current time, with a minute of acceptable clock skew.
&& DateUtils.isAfter(expirationTime, new Date(), MAX_CLOCK_SKEW_SECONDS);
}
}
private SignedJWT getSignedJWT(String token) throws ParseException, JOSEException {
SignedJWT signedJWT;
if (isEncryptionEnabled()) {
JWEObject jweObject = JWEObject.parse(token);
jweObject.decrypt(new DirectDecrypter(getEncryptionKey()));
signedJWT = jweObject.getPayload().toSignedJWT();
} else {
signedJWT = SignedJWT.parse(token);
}
return signedJWT;
}
private EPerson getEPerson(Context context, JWTClaimsSet jwtClaimsSet) throws SQLException {
return ePersonClaimProvider.getEPerson(context, jwtClaimsSet);
}
private SignedJWT createSignedJWT(HttpServletRequest request, EPerson ePerson, JWTClaimsSet claimsSet) throws JOSEException {
SignedJWT signedJWT = new SignedJWT(
new JWSHeader(JWSAlgorithm.HS256), claimsSet);
JWSSigner signer = new MACSigner(buildSigningKey(request, ePerson));
signedJWT.sign(signer);
return signedJWT;
}
private JWTClaimsSet buildJwtClaimsSet(Context context, HttpServletRequest request) {
JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder();
for (JWTClaimProvider jwtClaimProvider : jwtClaimProviders) {
builder = builder.claim(jwtClaimProvider.getKey(), jwtClaimProvider.getValue(context, request));
}
return builder
.expirationTime(new Date(System.currentTimeMillis() + getExpirationPeriod()))
.build();
}
//This method makes compression configurable
private JWEHeader.Builder compression(JWEHeader.Builder builder) {
if (compressionEnabled) {
return builder.compressionAlgorithm(CompressionAlgorithm.DEF);
}
return builder;
}
/**
* This returns the key used for signing the token. This key is at least 256 bits/32 bytes (server key has minimum length of 1 byte and the eperson session salt is always 32 bytes),
* this way the key is always long enough for the HMAC using SHA-256 algorithm.
* More information: https://tools.ietf.org/html/rfc7518#section-3.2
*
* @param request
* @param ePerson
* @return
*/
private String buildSigningKey(HttpServletRequest request, EPerson ePerson) {
String ipAddress = "";
if (includeIP) {
ipAddress = getIpAddress(request);
}
return jwtKey + ePerson.getSessionSalt() + ipAddress;
}
private String getIpAddress(HttpServletRequest request) {
String ipAddress = request.getHeader("X-FORWARDED-FOR");
if (ipAddress == null) {
ipAddress = request.getRemoteAddr();
}
return ipAddress;
}
private EPerson updateSessionSalt(final Context context, final Date previousLoginDate) throws SQLException {
EPerson ePerson;
try {
ePerson = context.getCurrentUser();
//If the previous login was within the configured token expiration time, we reuse the session salt.
//This allows a user to login on multiple devices/browsers at the same time.
if (StringUtils.isBlank(ePerson.getSessionSalt())
|| previousLoginDate == null
|| (ePerson.getLastActive().getTime() - previousLoginDate.getTime() > expirationTime)) {
ePerson.setSessionSalt(generateRandomKey());
ePersonService.update(context, ePerson);
}
} catch (AuthorizeException e) {
ePerson = null;
}
return ePerson;
}
private String getSecret(String property) {
String secret = configurationService.getProperty(property);
if (StringUtils.isBlank(secret)) {
secret = generateRandomKey();
}
return secret;
}
/**
* Generate a random 32 bytes key
*/
private String generateRandomKey() {
//24 bytes because BASE64 encoding makes this 32 bytes
//Base64 takes 4 characters for every 3 bytes
BytesKeyGenerator bytesKeyGenerator = KeyGenerators.secureRandom(24);
byte[] secretKey = bytesKeyGenerator.generateKey();
return Base64.encodeBase64String(secretKey);
}
}

View File

@@ -0,0 +1,123 @@
/**
* 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.jwt;
import java.io.IOException;
import java.sql.SQLException;
import java.text.ParseException;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.nimbusds.jose.JOSEException;
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.service.AuthenticationService;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;
import org.dspace.eperson.service.EPersonService;
import org.dspace.services.ConfigurationService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Rest Authentication implementation for JSON Web Tokens
*
* @author Atmire NV (info at atmire dot com)
*/
@Component
public class JWTTokenRestAuthenticationServiceImpl implements RestAuthenticationService, InitializingBean {
private static final Logger log = LoggerFactory.getLogger(RestAuthenticationService.class);
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String AUTHORIZATION_TYPE = "Bearer";
@Autowired
private JWTTokenHandler jwtTokenHandler;
@Autowired
private EPersonService ePersonService;
@Autowired
private AuthenticationService authenticationService;
@Override
public void afterPropertiesSet() throws Exception {
}
@Override
public void addAuthenticationDataForUser(HttpServletRequest request, HttpServletResponse response, DSpaceAuthentication authentication) throws IOException {
try {
Context context = ContextUtil.obtainContext(request);
context.setCurrentUser(ePersonService.findByEmail(context, authentication.getName()));
List<Group> groups = authenticationService.getSpecialGroups(context, request);
String token = jwtTokenHandler.createTokenForEPerson(context, request,
authentication.getPreviousLoginDate(), groups);
addTokenToResponse(response, token);
context.commit();
} catch (JOSEException e) {
log.error("JOSE Exception", e);
} catch (SQLException e) {
log.error("SQL error when adding authentication", e);
}
}
@Override
public EPerson getAuthenticatedEPerson(HttpServletRequest request, Context context) {
String token = getToken(request);
try {
EPerson ePerson = jwtTokenHandler.parseEPersonFromToken(token, request, context);
return ePerson;
} catch (JOSEException e) {
log.error("Jose error", e);
} catch (ParseException e) {
log.error("Error parsing EPerson from token", e);
} catch (SQLException e) {
log.error("SQL error while retrieving EPerson from token", e);
}
return null;
}
@Override
public boolean hasAuthenticationData(HttpServletRequest request) {
return StringUtils.isNotBlank(request.getHeader(AUTHORIZATION_HEADER));
}
@Override
public void invalidateAuthenticationData(HttpServletRequest request, Context context) throws Exception {
String token = getToken(request);
jwtTokenHandler.invalidateToken(token, request, context);
}
private void addTokenToResponse(final HttpServletResponse response, final String token) throws IOException {
response.setHeader(AUTHORIZATION_HEADER, String.format("%s %s", AUTHORIZATION_TYPE, token));
}
private String getToken(HttpServletRequest request) {
String authHeader = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.isNotBlank(authHeader)) {
String tokenValue = authHeader.replace(AUTHORIZATION_TYPE, "").trim();
return tokenValue;
} else {
return null;
}
}
}

View File

@@ -0,0 +1,72 @@
/**
* 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.jwt;
import java.sql.SQLException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import com.nimbusds.jwt.JWTClaimsSet;
import org.apache.commons.collections4.CollectionUtils;
import org.dspace.authenticate.service.AuthenticationService;
import org.dspace.core.Context;
import org.dspace.eperson.Group;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* JWT claim provider to read and set the special groups of an eperson on a JWT token
*
* @author Atmire NV (info at atmire dot com)
*/
@Component
public class SpecialGroupClaimProvider implements JWTClaimProvider {
private static final Logger log = LoggerFactory.getLogger(SpecialGroupClaimProvider.class);
public static final String SPECIAL_GROUPS = "sg";
@Autowired
private AuthenticationService authenticationService;
public String getKey() {
return SPECIAL_GROUPS;
}
public Object getValue(Context context, HttpServletRequest request) {
List<Group> groups = new ArrayList<>();
try {
groups = authenticationService.getSpecialGroups(context, request);
} catch (SQLException e) {
log.error("SQLException while retrieving special groups", e);
return null;
}
List<String> groupIds = groups.stream().map(group -> group.getID().toString()).collect(Collectors.toList());
return groupIds;
}
public void parseClaim(Context context, HttpServletRequest request, JWTClaimsSet jwtClaimsSet) {
try {
List<String> groupIds = jwtClaimsSet.getStringListClaim(SPECIAL_GROUPS);
for (String groupId : CollectionUtils.emptyIfNull(groupIds)) {
context.setSpecialGroup(UUID.fromString(groupId));
}
} catch (ParseException e) {
log.error("Error while trying to access specialgroups from ClaimSet", e);
}
}
}

View File

@@ -7,12 +7,13 @@
*/
package org.dspace.app.rest.utils;
import java.sql.SQLException;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import org.apache.log4j.Logger;
import org.dspace.core.Context;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import java.sql.SQLException;
/**
* Miscellaneous UI utility methods methods for managing DSpace context.
*
@@ -53,14 +54,19 @@ public class ContextUtil
*
* @return a context object
*/
public static Context obtainContext(ServletRequest request) throws SQLException
public static Context obtainContext(ServletRequest request)
{
Context context = (Context) request.getAttribute(DSPACE_CONTEXT);
if (context == null)
{
context = ContextUtil.intializeContext();
try {
context = ContextUtil.intializeContext();
} catch (SQLException e) {
log.error("Unable to initialize context", e);
return null;
}
// Store the context in the request
request.setAttribute(DSPACE_CONTEXT, context);
}

View File

@@ -11,8 +11,9 @@ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.atteo.evo.inflector.English;
import org.dspace.app.rest.exception.PaginationException;
import org.dspace.app.rest.exception.RepositoryNotFoundException;
import org.dspace.app.rest.model.AuthorityRest;
@@ -65,7 +66,7 @@ public class Utils {
}
public Link linkToSingleResource(RestModel data, String rel) {
return linkTo(data.getController(), data.getCategory(), English.plural(data.getType())).slash(data)
return linkTo(data.getController(), data.getCategory(), data.getTypePlural()).slash(data)
.withRel(rel);
}
@@ -74,7 +75,7 @@ public class Utils {
}
public Link linkToSubResource(RestModel data, String rel, String path) {
return linkTo(data.getController(), data.getCategory(), English.plural(data.getType())).slash(data).slash(path)
return linkTo(data.getController(), data.getCategory(), data.getTypePlural()).slash(data).slash(path)
.withRel(rel);
}
@@ -149,7 +150,7 @@ public class Utils {
/**
* Build the canonical representation of a metadata key in DSpace. ie
* <schema>.<element>[.<qualifier>]
*
*
* @param schema
* @param element
* @param object

View File

@@ -36,6 +36,7 @@
<ul class="nav">
<li><a href="#/" id="entryPointLink">Go To Entry Point</a></li>
<li><a href="https://github.com/mikekelly/hal-browser">About The HAL Browser</a></li>
<li><a href="login.html">Login</a></li>
</ul>
</div>
</div>
@@ -259,7 +260,7 @@ Content-Type: application/json
<script src="browser/js/hal.js"></script>
<script src="browser/js/hal/browser.js"></script>
<script src="browser/js/hal/http/client.js"></script>
<script src="js/hal/http/client.js"></script>
<script src="browser/js/hal/resource.js"></script>
<script src="browser/js/hal/views/browser.js"></script>

View File

@@ -0,0 +1,54 @@
/*
* 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/
*/
HAL.Http.Client = function(opts) {
this.vent = opts.vent;
this.defaultHeaders = { 'Accept': 'application/hal+json, application/json, */*; q=0.01' };
cookie = document.cookie.match('(^|;)\\s*' + 'MyHalBrowserToken' + '\\s*=\\s*([^;]+)');
cookie ? this.defaultHeaders.Authorization = 'Bearer ' + cookie.pop() : '';
console.log(this.defaultHeaders);
this.headers = this.defaultHeaders;
};
HAL.Http.Client.prototype.get = function(url) {
var self = this;
this.vent.trigger('location-change', { url: url });
var jqxhr = $.ajax({
url: url,
dataType: 'json',
xhrFields: {
withCredentials: true
},
headers: this.headers,
success: function(resource, textStatus, jqXHR) {
self.vent.trigger('response', {
resource: resource,
jqxhr: jqXHR,
headers: jqXHR.getAllResponseHeaders()
});
}
}).error(function() {
self.vent.trigger('fail-response', { jqxhr: jqxhr });
});
};
HAL.Http.Client.prototype.request = function(opts) {
var self = this;
opts.dataType = 'json';
opts.xhrFields = opts.xhrFields || {};
opts.xhrFields.withCredentials = opts.xhrFields.withCredentials || true;
self.vent.trigger('location-change', { url: opts.url });
return jqxhr = $.ajax(opts);
};
HAL.Http.Client.prototype.updateHeaders = function(headers) {
this.headers = headers;
};
HAL.Http.Client.prototype.getHeaders = function() {
return this.headers;
};

View File

@@ -0,0 +1,108 @@
<!--
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/
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Sign in - HAL Browser</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css" rel="stylesheet"/>
<link href="browser/vendor/css/bootstrap.css" rel="stylesheet">
<link href="browser/vendor/css/bootstrap-responsive.css" rel="stylesheet">
<style type="text/css">
body {
padding-top: 40px;
padding-bottom: 40px;
background-color: #f5f5f5;
}
.form-signin {
max-width: 300px;
padding: 19px 29px 29px;
margin: 0 auto 20px;
background-color: #fff;
border: 1px solid #e5e5e5;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
-moz-box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
}
.form-signin .form-signin-heading, .form-signin .checkbox {
margin-bottom: 10px;
}
.form-signin input[type="text"], .form-signin input[type="password"] {
font-size: 16px;
height: auto;
margin-bottom: 15px;
padding: 7px 9px;
}
</style>
<script src="browser/vendor/js/jquery-1.10.2.min.js"></script>
<script src="browser/vendor/js/bootstrap.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js"></script>
</head>
<body>
<div class="container">
<form class="form-signin">
<h2 class="form-signin-heading">HAL Browser</h2>
<input type="text" class="input-block-level" placeholder="Username" id="username">
<input type="password" class="input-block-level" placeholder="Password" id="password">
<button type="button" class="btn btn-large btn-primary" id="login">Sign in</button>
</form>
</div>
<script>
$(document).ready(function() {
toastr.options = {
"closeButton": false,
"debug": false,
"newestOnTop": false,
"progressBar": false,
"positionClass": "toast-top-center",
"preventDuplicates": false,
"showDuration": "300",
"hideDuration": "1000",
"timeOut": "3000",
"extendedTimeOut": "1000",
"showEasing": "swing",
"hideEasing": "linear",
"showMethod": "fadeIn",
"hideMethod": "fadeOut",
"onclick" : function() { toastr.remove(); }
}
$("#login").click(function() {
$.ajax({
//This depends on this file to be called login.html
url : window.location.href.replace("login.html", "") + 'api/authn/login',
type : 'POST',
async : false,
data : 'password='+$("#password").val()+'&user='+$("#username").val() ,
headers : {
"Content-Type" : 'application/x-www-form-urlencoded',
"Accept:" : '*/*'
},
success : function(result, status, xhr) {
document.cookie = "MyHalBrowserToken=" + xhr.getResponseHeader('Authorization').split(" ")[1];
toastr.success('You are now logged in. Please wait while we redirect you...', 'Login Successful');
setTimeout(function() {
window.location.href = window.location.pathname.replace("login.html", "");
}, 2000);
},
error : function() {
toastr.error('The credentials you entered are invalid. Please try again.', 'Login Failed');
}
});
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,287 @@
/**
* 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;
import static java.lang.Thread.sleep;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertNotEquals;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.util.Base64;
import org.dspace.app.rest.builder.GroupBuilder;
import org.dspace.app.rest.test.AbstractControllerIntegrationTest;
import org.dspace.eperson.Group;
import org.junit.Test;
/**
* Integration test that covers various authentication scenarios
*
* @author Atmire NV (info at atmire dot com)
*/
public class AuthenticationRestControllerIT extends AbstractControllerIntegrationTest {
@Test
public void testStatusAuthenticated() throws Exception {
String token = getAuthToken(eperson.getEmail(), password);
getClient(token).perform(get("/api/authn/status"))
.andExpect(status().isOk())
//We expect the content type to be "application/hal+json;charset=UTF-8"
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(true)))
.andExpect(jsonPath("$.type", is("status")))
.andExpect(jsonPath("$._links.eperson.href", startsWith(REST_SERVER_URL)))
.andExpect(jsonPath("$._embedded.eperson.email", is(eperson.getEmail())));
}
@Test
public void testStatusNotAuthenticated() throws Exception {
getClient().perform(get("/api/authn/status"))
.andExpect(status().isOk())
//We expect the content type to be "application/hal+json;charset=UTF-8"
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(false)))
.andExpect(jsonPath("$.type", is("status")));
}
@Test
public void testTwoAuthenticationTokens() throws Exception {
String token1 = getAuthToken(eperson.getEmail(), password);
//Sleep so tokens are different
sleep(1200);
String token2 = getAuthToken(eperson.getEmail(), password);
assertNotEquals(token1, token2);
getClient(token1).perform(get("/api/authn/status"))
.andExpect(status().isOk())
//We expect the content type to be "application/hal+json;charset=UTF-8"
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(true)))
.andExpect(jsonPath("$.type", is("status")))
.andExpect(jsonPath("$._links.eperson.href", startsWith(REST_SERVER_URL)))
.andExpect(jsonPath("$._embedded.eperson.email", is(eperson.getEmail())));
getClient(token2).perform(get("/api/authn/status"))
.andExpect(status().isOk())
//We expect the content type to be "application/hal+json;charset=UTF-8"
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(true)))
.andExpect(jsonPath("$.type", is("status")))
.andExpect(jsonPath("$._links.eperson.href", startsWith(REST_SERVER_URL)))
.andExpect(jsonPath("$._embedded.eperson.email", is(eperson.getEmail())));
}
@Test
public void testTamperingWithToken() throws Exception {
//Receive a valid token
String token = getAuthToken(eperson.getEmail(), password);
//Check it is really valid
getClient(token).perform(get("/api/authn/status"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(true)))
.andExpect(jsonPath("$.type", is("status")));
//The group we try to add to our token
context.turnOffAuthorisationSystem();
Group internalGroup = new GroupBuilder().createGroup(context)
.withName("Internal Group")
.build();
context.restoreAuthSystemState();
//Tamper with the token, insert id of group we don't belong to
String[] jwtSplit = token.split("\\.");
//We try to inject a special group ID to spoof membership
String tampered = new String(Base64.getUrlEncoder().encode(
new String(Base64.getUrlDecoder().decode(
token.split("\\.")[1]))
.replaceAll("\\[]", "[\"" + internalGroup.getID() + "\"]")
.getBytes()));
String tamperedToken = jwtSplit[0] + "." + tampered + "." + jwtSplit[2];
//Try to get authenticated with the tampered token
getClient(tamperedToken).perform(get("/api/authn/status"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(false)))
.andExpect(jsonPath("$.type", is("status")));
}
@Test
public void testLogout() throws Exception {
String token = getAuthToken(eperson.getEmail(), password);
getClient(token).perform(get("/api/authn/status"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(true)))
.andExpect(jsonPath("$.type", is("status")));
getClient(token).perform(get("/api/authn/logout"))
.andExpect(status().isOk());
getClient(token).perform(get("/api/authn/status"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(false)))
.andExpect(jsonPath("$.type", is("status")));
}
@Test
public void testLogoutInvalidatesAllTokens() throws Exception {
String token1 = getAuthToken(eperson.getEmail(), password);
//Sleep so tokens are different
sleep(1200);
String token2 = getAuthToken(eperson.getEmail(), password);
assertNotEquals(token1, token2);
getClient(token1).perform(get("/api/authn/logout"));
getClient(token1).perform(get("/api/authn/status"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(false)))
.andExpect(jsonPath("$.type", is("status")));
getClient(token2).perform(get("/api/authn/status"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(false)))
.andExpect(jsonPath("$.type", is("status")));
}
@Test
public void testRefreshToken() throws Exception {
String token = getAuthToken(eperson.getEmail(), password);
//Sleep so tokens are different
sleep(1200);
String newToken = getClient(token).perform(get("/api/authn/login"))
.andExpect(status().isOk())
.andReturn().getResponse().getHeader("Authorization");
assertNotEquals(token, newToken);
getClient(newToken).perform(get("/api/authn/status"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(true)))
.andExpect(jsonPath("$.type", is("status")));
}
@Test
public void testReuseTokenWithDifferentIP() throws Exception {
String token = getAuthToken(eperson.getEmail(), password);
getClient(token).perform(get("/api/authn/status")
.header("X-FORWARDED-FOR", "1.1.1.1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(false)))
.andExpect(jsonPath("$.type", is("status")));
}
@Test
public void testFailedLoginResponseCode() throws Exception {
getClient().perform(get("/api/authn/login")
.param("user", eperson.getEmail()).param("password", "fakePassword"))
.andExpect(status().isUnauthorized());
}
@Test
public void testLoginLogoutStatusLink() throws Exception {
getClient().perform(get("/api/authn"))
.andExpect(status().isOk())
.andExpect(jsonPath("$._links.login.href", endsWith("login")))
.andExpect(jsonPath("$._links.logout.href", endsWith("logout")))
.andExpect(jsonPath("$._links.status.href", endsWith("status")));
}
/**
* Check if we can just request a new token after we logged out
* @throws Exception
*/
@Test
public void testLoginAgainAfterLogout() throws Exception {
String token = getAuthToken(eperson.getEmail(), password);
//Check if we have a valid token
getClient(token).perform(get("/api/authn/status"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(true)))
.andExpect(jsonPath("$.type", is("status")));
//Logout
getClient(token).perform(get("/api/authn/logout"))
.andExpect(status().isOk());
//Check if we are actually logged out
getClient(token).perform(get("/api/authn/status"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(false)))
.andExpect(jsonPath("$.type", is("status")));
//request a new token
token = getAuthToken(eperson.getEmail(), password);
//Check if we succesfully authenticated again
getClient(token).perform(get("/api/authn/status"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(true)))
.andExpect(jsonPath("$.type", is("status")));
}
}

View File

@@ -14,7 +14,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import org.dspace.app.rest.test.AbstractControllerIntegrationTest;
import org.junit.Test;
/**
* Integration test for the {@link RootRestResourceController}
*/
@@ -41,6 +40,7 @@ public class RootRestResourceControllerIT extends AbstractControllerIntegrationT
.andExpect(jsonPath("$._links.metadatafields.href", startsWith(REST_SERVER_URL)))
.andExpect(jsonPath("$._links.metadataschemas.href", startsWith(REST_SERVER_URL)))
.andExpect(jsonPath("$._links.sites.href", startsWith(REST_SERVER_URL)))
.andExpect(jsonPath("$._links.authn.href", startsWith(REST_SERVER_URL)))
;
}

View File

@@ -0,0 +1,67 @@
/**
* 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 static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.when;
import java.util.List;
import java.util.stream.Collectors;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.security.core.GrantedAuthority;
/**
* @author Atmire NV (info at atmire dot com)
*/
@RunWith(MockitoJUnitRunner.class)
public class EPersonRestAuthenticationProviderTest {
@InjectMocks
private EPersonRestAuthenticationProvider ePersonRestAuthenticationProvider;
@Mock
private EPerson ePerson;
@Mock
private Context context;
@Mock
private AuthorizeService authorizeService;
@Test
public void testGetGrantedAuthoritiesAdmin() throws Exception {
when(authorizeService.isAdmin(context, ePerson)).thenReturn(true);
List<GrantedAuthority> authorities = ePersonRestAuthenticationProvider.getGrantedAuthorities(context, ePerson);
assertThat(authorities.stream().map(a -> a.getAuthority()).collect(Collectors.toList()), containsInAnyOrder(
WebSecurityConfiguration.ADMIN_GRANT, WebSecurityConfiguration.EPERSON_GRANT));
}
@Test
public void testGetGrantedAuthoritiesEPerson() throws Exception {
when(authorizeService.isAdmin(context, ePerson)).thenReturn(false);
List<GrantedAuthority> authorities = ePersonRestAuthenticationProvider.getGrantedAuthorities(context, ePerson);
assertThat(authorities.stream().map(a -> a.getAuthority()).collect(Collectors.toList()), containsInAnyOrder(
WebSecurityConfiguration.EPERSON_GRANT));
}
}

View File

@@ -0,0 +1,84 @@
/**
* 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.jwt;
import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import com.nimbusds.jwt.JWTClaimsSet;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.service.EPersonService;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
/**
* @author Atmire NV (info at atmire dot com)
*/
@RunWith(MockitoJUnitRunner.class)
public class EPersonClaimProviderTest {
@InjectMocks
private EPersonClaimProvider ePersonClaimProvider;
@Mock
private EPerson ePerson;
private Context context;
@Mock
private HttpServletRequest httpServletRequest;
@Mock
private EPersonService ePersonService;
private JWTClaimsSet jwtClaimsSet;
@Before
public void setUp() throws Exception {
context = Mockito.mock(Context.class);
Mockito.doCallRealMethod().when(context).setCurrentUser(any(EPerson.class));
Mockito.doCallRealMethod().when(context).getCurrentUser();
when(ePerson.getID()).thenReturn(UUID.fromString("c3bae216-a481-496b-a524-7df5aabdb609"));
jwtClaimsSet = new JWTClaimsSet.Builder()
.claim(EPersonClaimProvider.EPERSON_ID, "c3bae216-a481-496b-a524-7df5aabdb609")
.build();
when(ePersonService.find(any(), any(UUID.class))).thenReturn(ePerson);
}
@After
public void tearDown() throws Exception {
}
@Test
public void testParseClaim() throws Exception {
ePersonClaimProvider.parseClaim(context, httpServletRequest, jwtClaimsSet);
verify(context).setCurrentUser(ePerson);
}
@Test
public void testGetEPerson() throws Exception {
EPerson parsed = ePersonClaimProvider.getEPerson(context, jwtClaimsSet);
assertEquals(parsed.getID(), UUID.fromString("c3bae216-a481-496b-a524-7df5aabdb609" ));
}
}

View File

@@ -0,0 +1,129 @@
/**
* 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.jwt;
import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.when;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Date;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.service.EPersonService;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.crypto.keygen.KeyGenerators;
import org.springframework.security.crypto.keygen.StringKeyGenerator;
/**
* @author Atmire NV (info at atmire dot com)
*/
@RunWith(MockitoJUnitRunner.class)
public class JWTTokenHandlerTest {
@InjectMocks
@Spy
JWTTokenHandler jwtTokenHandler;
@Mock
private Context context;
@Mock
private EPerson ePerson;
@Mock
private HttpServletRequest httpServletRequest;
@Mock
private EPersonService ePersonService;
@Mock
private EPersonClaimProvider ePersonClaimProvider;
@Spy
private List<JWTClaimProvider> jwtClaimProviders = new ArrayList<>();
@Before
public void setUp() throws Exception {
when(ePerson.getEmail()).thenReturn("test@dspace.org");
when(ePerson.getSessionSalt()).thenReturn("01234567890123456789012345678901");
when(ePerson.getLastActive()).thenReturn(new Date());
when(context.getCurrentUser()).thenReturn(ePerson);
when(ePersonClaimProvider.getKey()).thenReturn( "eid");
when(ePersonClaimProvider.getValue(any(), Mockito.any(HttpServletRequest.class))).thenReturn("epersonID");
jwtClaimProviders.add(ePersonClaimProvider);
}
@After
public void tearDown() throws Exception {
}
@Test
public void testJWTNoEncryption() throws Exception {
Date previous = new Date(System.currentTimeMillis() - 10000000000L);
String token = jwtTokenHandler.createTokenForEPerson(context, new MockHttpServletRequest(), previous, new ArrayList<>());
SignedJWT signedJWT = SignedJWT.parse(token);
String personId = (String) signedJWT.getJWTClaimsSet().getClaim(EPersonClaimProvider.EPERSON_ID);
assertEquals("epersonID", personId);
}
@Test(expected = ParseException.class)
public void testJWTEncrypted() throws Exception {
when(jwtTokenHandler.isEncryptionEnabled()).thenReturn(true);
Date previous = new Date(System.currentTimeMillis() - 10000000000L);
StringKeyGenerator keyGenerator = KeyGenerators.string();
when(jwtTokenHandler.getEncryptionKey()).thenReturn(keyGenerator.generateKey().getBytes());
String token = jwtTokenHandler.createTokenForEPerson(context, new MockHttpServletRequest(), previous, new ArrayList<>());
SignedJWT signedJWT = SignedJWT.parse(token);
}
//temporary set a negative expiration time so the token is invalid immediately
@Test
public void testExpiredToken() throws Exception {
when(jwtTokenHandler.getExpirationPeriod()).thenReturn(-99999999L);
when(ePersonClaimProvider.getEPerson(any(Context.class), any(JWTClaimsSet.class))).thenReturn(ePerson);
Date previous = new Date(new Date().getTime() - 10000000000L);
String token = jwtTokenHandler.createTokenForEPerson(context, new MockHttpServletRequest(), previous, new ArrayList<>());
EPerson parsed = jwtTokenHandler.parseEPersonFromToken(token, httpServletRequest, context);
assertEquals(null, parsed);
}
//Try if we can change the expiration date
@Test
public void testTokenTampering() throws Exception {
when(jwtTokenHandler.getExpirationPeriod()).thenReturn(-99999999L);
when(ePersonClaimProvider.getEPerson(any(Context.class), any(JWTClaimsSet.class))).thenReturn(ePerson);
Date previous = new Date(new Date().getTime() - 10000000000L);
String token = jwtTokenHandler.createTokenForEPerson(context, new MockHttpServletRequest(), previous, new ArrayList<>());
JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().claim("eid", "epersonID").expirationTime(new Date(System.currentTimeMillis() + 99999999)).build();
String tamperedPayload = new String(Base64.getUrlEncoder().encode(jwtClaimsSet.toString().getBytes()));
String[] splitToken = token.split("\\.");
String tamperedToken = splitToken[0] + "." + tamperedPayload + "." + splitToken[2];
EPerson parsed = jwtTokenHandler.parseEPersonFromToken(tamperedToken, httpServletRequest, context);
assertEquals(null, parsed);
}
}

View File

@@ -0,0 +1,87 @@
/**
* 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.jwt;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import com.nimbusds.jwt.JWTClaimsSet;
import org.dspace.core.Context;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
/**
* @author Atmire NV (info at atmire dot com)
*/
@RunWith(MockitoJUnitRunner.class)
public class SpecialGroupClaimProviderTest {
@InjectMocks
private SpecialGroupClaimProvider specialGroupClaimProvider;
private List<UUID> specialGroups = new ArrayList<>();
private Context context;
private String id1 = "02af436f-a531-4934-9b36-a21cd8fdcc57";
private String id2 = "f39d3947-c75d-4d09-86ef-f732cfae7d88";
private String id3 = "2262d8ad-8bb6-4330-9cee-06da30f3feae";
@Mock
private HttpServletRequest httpServletRequest;
private JWTClaimsSet jwtClaimsSet;
@Before
public void setUp() throws Exception {
context = Mockito.mock(Context.class);
//Stub the specialgroups list that is normally kept in the context class
Mockito.doAnswer(invocation -> {
UUID uuid = invocation.getArgumentAt(0, UUID.class);
specialGroups.add(uuid);
return "done";
}).when(context).setSpecialGroup(any(UUID.class));
List<String> groupIds = new ArrayList<>();
groupIds.add(id1);
groupIds.add(id2);
groupIds.add(id3);
jwtClaimsSet = new JWTClaimsSet.Builder()
.claim(SpecialGroupClaimProvider.SPECIAL_GROUPS, groupIds)
.build();
}
@After
public void tearDown() throws Exception {
specialGroups.clear();
}
@Test
public void parseClaim() throws Exception {
specialGroupClaimProvider.parseClaim(context, httpServletRequest, jwtClaimsSet);
assertThat(specialGroups, containsInAnyOrder(
UUID.fromString(id1), UUID.fromString(id2), UUID.fromString(id3)));
}
}

View File

@@ -7,6 +7,8 @@
*/
package org.dspace.app.rest.test;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;
import java.sql.SQLException;
@@ -16,7 +18,9 @@ import java.util.List;
import javax.servlet.Filter;
import org.apache.commons.io.Charsets;
import org.apache.commons.lang.StringUtils;
import org.dspace.app.rest.Application;
import org.dspace.app.rest.security.WebSecurityConfiguration;
import org.dspace.app.rest.utils.ApplicationConfig;
import org.junit.Assert;
import org.junit.runner.RunWith;
@@ -26,6 +30,7 @@ import org.springframework.hateoas.MediaTypes;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -35,6 +40,7 @@ import org.springframework.test.context.transaction.TransactionalTestExecutionLi
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
import org.springframework.web.context.WebApplicationContext;
/**
@@ -42,13 +48,16 @@ import org.springframework.web.context.WebApplicationContext;
* environment to run the integration test
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = {Application.class, ApplicationConfig.class})
@SpringBootTest(classes = {Application.class, ApplicationConfig.class, WebSecurityConfiguration.class})
@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class,
TransactionalTestExecutionListener.class})
@DirtiesContext
@WebAppConfiguration
public class AbstractControllerIntegrationTest extends AbstractIntegrationTestWithDatabase {
protected static final String AUTHORIZATION_HEADER = "Authorization";
protected static final String AUTHORIZATION_TYPE = "Bearer";
public static final String REST_SERVER_URL = "http://localhost/api/";
protected MediaType contentType = new MediaType(MediaTypes.HAL_JSON.getType(),
@@ -74,17 +83,38 @@ public class AbstractControllerIntegrationTest extends AbstractIntegrationTestWi
}
public MockMvc getClient() throws SQLException {
return getClient(null);
}
public MockMvc getClient(String authToken) throws SQLException {
if(context != null && context.isValid()) {
context.commit();
}
return webAppContextSetup(webApplicationContext)
DefaultMockMvcBuilder mockMvcBuilder = webAppContextSetup(webApplicationContext)
//Always log the repsonse to debug
.alwaysDo(MockMvcResultHandlers.log())
//Add all filter implementations
.addFilters(requestFilters.toArray(new Filter[requestFilters.size()]))
.addFilters(requestFilters.toArray(new Filter[requestFilters.size()]));
if(StringUtils.isNotBlank(authToken)) {
mockMvcBuilder.defaultRequest(get("").header(AUTHORIZATION_HEADER, AUTHORIZATION_TYPE + " " + authToken));
}
return mockMvcBuilder
.build();
}
public MockHttpServletResponse getAuthResponse(String user, String password) throws Exception {
return getClient().perform(post("/api/authn/login")
.param("user", user)
.param("password", password))
.andReturn().getResponse();
}
public String getAuthToken(String user, String password) throws Exception {
return getAuthResponse(user, password).getHeader(AUTHORIZATION_HEADER);
}
}

View File

@@ -42,6 +42,11 @@ public class AbstractIntegrationTestWithDatabase extends AbstractDSpaceIntegrati
*/
protected EPerson eperson;
/**
* The password of our test eperson
*/
protected String password = "mySuperS3cretP4ssW0rd";
/**
* The test Parent Community
*/
@@ -106,6 +111,7 @@ public class AbstractIntegrationTestWithDatabase extends AbstractDSpaceIntegrati
eperson.setEmail("test@email.com");
eperson.setCanLogIn(true);
eperson.setLanguage(context, I18nUtil.getDefaultLocale().getLanguage());
ePersonService.setPassword(eperson, password);
// actually save the eperson to unit testing DB
ePersonService.update(context, eperson);
}

View File

@@ -47,4 +47,36 @@
# Authentication by Password (encrypted in DSpace's database). See authentication-password.cfg for default configuration.
# Enabled by default (to disable, either comment out, or define a new list of AuthenticationMethod plugins in your local.cfg)
plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.PasswordAuthentication
plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.PasswordAuthentication
#---------------------------------------------------------------#
#---------------Stateless JWT Authentication--------------------#
#---------------------------------------------------------------#
# Server key part that is a part of the key used to sign the authentication tokens.
# If this property is not set or empty, DSpace will generate a random key on startup.
# IF YOU ARE RUNNING DSPACE IN A CLUSTER, you need to set a value for this property here or as an environment variable
# jwt.token.secret =
# This property enables/disables encryption of the payload in a stateless token. Enabling this makes the data encrypted
# and unreadable by the receiver, but makes the token larger in size. false by default
jwt.encryption.enabled = false
# Encryption key to use when JWT token encryption is enabled (JWE). Note that encrypting tokens might required additional
# configuration in the REST clients
# jwt.encryption.secret =
# This enables compression of the payload of a jwt, enabling this will make the jwt token a little smaller at the cost
# of some performance, this setting WILL ONLY BE used when encrypting the jwt.
jwt.compression.enabled = true
# Expiration time of a token in minutes
jwt.token.expiration = 30
# Restrict tokens to a specific ip-address to prevent theft/session hijacking. This is achieved by making the ip-address
# a part of the JWT siging key. If this property is set to false then the ip-address won't be used as part of
# the signing key of a jwt token and tokens can be shared over multiple ip-addresses.
# For security reasons, this defaults to true
jwt.token.include.ip = true

View File

@@ -1356,13 +1356,6 @@
<version>1.3</version>
<scope>test</scope>
</dependency>
<!-- https://github.com/json-path/JsonPath/tree/master/json-path-assert -->
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path-assert</artifactId>
<version>2.2.0</version>
<scope>test</scope>
</dependency>
<!-- H2 is an in-memory database used for Unit/Integration tests -->
<dependency>
<groupId>com.h2database</groupId>