mirror of
https://github.com/DSpace/DSpace.git
synced 2025-10-07 01:54:22 +00:00
Merge pull request #1873 from atmire/POC_stateless_sessions
DS-3542: Stateless sessions authentication
This commit is contained in:
@@ -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,
|
||||
|
@@ -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.
|
||||
|
@@ -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
|
||||
*
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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)
|
||||
{
|
||||
|
@@ -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);
|
@@ -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);
|
@@ -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);
|
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -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);
|
||||
|
||||
}
|
||||
|
@@ -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();
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
@@ -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+")");
|
||||
|
@@ -26,8 +26,6 @@ public abstract class DSpaceServicesFactory
|
||||
|
||||
public abstract RequestService getRequestService();
|
||||
|
||||
public abstract SessionService getSessionService();
|
||||
|
||||
public abstract ServiceManager getServiceManager();
|
||||
|
||||
public static DSpaceServicesFactory getInstance()
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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);
|
||||
|
@@ -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" />
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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++;
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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"));
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
@@ -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());
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
@@ -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());
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
|
54
dspace-spring-rest/src/main/webapp/js/hal/http/client.js
Normal file
54
dspace-spring-rest/src/main/webapp/js/hal/http/client.js
Normal 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;
|
||||
};
|
108
dspace-spring-rest/src/main/webapp/login.html
Normal file
108
dspace-spring-rest/src/main/webapp/login.html
Normal 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>
|
@@ -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")));
|
||||
|
||||
|
||||
}
|
||||
}
|
@@ -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)))
|
||||
;
|
||||
}
|
||||
|
||||
|
@@ -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));
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -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" ));
|
||||
}
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
@@ -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)));
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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
|
||||
|
||||
|
7
pom.xml
7
pom.xml
@@ -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>
|
||||
|
Reference in New Issue
Block a user