diff --git a/core/roboconf-dm-rest-commons/src/main/java/net/roboconf/dm/rest/commons/UrlConstants.java b/core/roboconf-dm-rest-commons/src/main/java/net/roboconf/dm/rest/commons/UrlConstants.java
index a2a00bcf..de3549d0 100644
--- a/core/roboconf-dm-rest-commons/src/main/java/net/roboconf/dm/rest/commons/UrlConstants.java
+++ b/core/roboconf-dm-rest-commons/src/main/java/net/roboconf/dm/rest/commons/UrlConstants.java
@@ -36,4 +36,5 @@ public interface UrlConstants {
String TARGETS = "targets";
String PREFERENCES = "preferences";
String SCHEDULER = "scheduler";
+ String AUTHENTICATION = "auth";
}
diff --git a/core/roboconf-dm-rest-commons/src/main/java/net/roboconf/dm/rest/commons/security/AuthenticationManager.java b/core/roboconf-dm-rest-commons/src/main/java/net/roboconf/dm/rest/commons/security/AuthenticationManager.java
index 04dff006..d257bc92 100644
--- a/core/roboconf-dm-rest-commons/src/main/java/net/roboconf/dm/rest/commons/security/AuthenticationManager.java
+++ b/core/roboconf-dm-rest-commons/src/main/java/net/roboconf/dm/rest/commons/security/AuthenticationManager.java
@@ -120,10 +120,10 @@ public String login( String user, String pwd ) {
/**
* Determines whether a session is valid.
* @param token a token
- * @param validityPeriod the validity period for a session (in seconds)
+ * @param validityPeriod the validity period for a session (in seconds, < 0 for unbound)
* @return true if the session is valid, false otherwise
*/
- public boolean isSessionValid( final String token, int validityPeriod ) {
+ public boolean isSessionValid( final String token, long validityPeriod ) {
boolean valid = false;
Long loginTime = null;
@@ -152,7 +152,7 @@ public boolean isSessionValid( final String token, int validityPeriod ) {
* No error is thrown if the session was already invalid.
*
*
- * @param token a token
+ * @param token a token (can be null)
*/
public void logout( String token ) {
if( token != null )
diff --git a/core/roboconf-dm-rest-services/metadata.xml b/core/roboconf-dm-rest-services/metadata.xml
index b5470548..93b02a0a 100644
--- a/core/roboconf-dm-rest-services/metadata.xml
+++ b/core/roboconf-dm-rest-services/metadata.xml
@@ -54,6 +54,9 @@
+
+
+
diff --git a/core/roboconf-dm-rest-services/pom.xml b/core/roboconf-dm-rest-services/pom.xml
index d63cb944..e43aff5e 100644
--- a/core/roboconf-dm-rest-services/pom.xml
+++ b/core/roboconf-dm-rest-services/pom.xml
@@ -122,9 +122,9 @@
- org.apache.felix
+ org.osgi
org.osgi.core
- 1.4.0
+ 6.0.0
provided
diff --git a/core/roboconf-dm-rest-services/src/main/java/net/roboconf/dm/rest/services/internal/RestApplication.java b/core/roboconf-dm-rest-services/src/main/java/net/roboconf/dm/rest/services/internal/RestApplication.java
index 424b8df2..ee5ad725 100644
--- a/core/roboconf-dm-rest-services/src/main/java/net/roboconf/dm/rest/services/internal/RestApplication.java
+++ b/core/roboconf-dm-rest-services/src/main/java/net/roboconf/dm/rest/services/internal/RestApplication.java
@@ -34,12 +34,14 @@
import com.sun.jersey.api.core.ResourceConfig;
import net.roboconf.dm.management.Manager;
+import net.roboconf.dm.rest.commons.security.AuthenticationManager;
import net.roboconf.dm.rest.services.cors.ResponseCorsFilter;
import net.roboconf.dm.rest.services.internal.resources.IApplicationResource;
import net.roboconf.dm.rest.services.internal.resources.IDebugResource;
import net.roboconf.dm.rest.services.internal.resources.IPreferencesResource;
import net.roboconf.dm.rest.services.internal.resources.ITargetResource;
import net.roboconf.dm.rest.services.internal.resources.impl.ApplicationResource;
+import net.roboconf.dm.rest.services.internal.resources.impl.AuthenticationResource;
import net.roboconf.dm.rest.services.internal.resources.impl.DebugResource;
import net.roboconf.dm.rest.services.internal.resources.impl.ManagementResource;
import net.roboconf.dm.rest.services.internal.resources.impl.PreferencesResource;
@@ -58,6 +60,7 @@ public class RestApplication extends DefaultResourceConfig {
private final IPreferencesResource preferencesResource;
private final ManagementResource managementResource;
private final SchedulerResource schedulerResource;
+ private final AuthenticationResource authenticationResource;
/**
@@ -73,6 +76,7 @@ public RestApplication( Manager manager ) {
this.targetResource = new TargetResource( manager );
this.preferencesResource = new PreferencesResource( manager );
this.schedulerResource = new SchedulerResource();
+ this.authenticationResource = new AuthenticationResource();
getFeatures().put( "com.sun.jersey.api.json.POJOMappingFeature", Boolean.TRUE );
getFeatures().put( ResourceConfig.FEATURE_DISABLE_WADL, Boolean.TRUE );
@@ -100,6 +104,7 @@ public Set getSingletons() {
set.add( this.targetResource );
set.add( this.preferencesResource );
set.add( this.schedulerResource );
+ set.add( this.authenticationResource );
return set;
}
@@ -123,6 +128,15 @@ public void setMavenResolver( MavenResolver mavenResolver ) {
}
+ /**
+ * Sets the authentication manager.
+ * @param authenticationMngr the authentication manager (can be null)
+ */
+ public void setAuthenticationManager( AuthenticationManager authenticationMngr ) {
+ this.authenticationResource.setAuthenticationManager( authenticationMngr );
+ }
+
+
/**
* Enables or disables CORS.
* @param enableCors true to enable it
diff --git a/core/roboconf-dm-rest-services/src/main/java/net/roboconf/dm/rest/services/internal/ServletRegistrationComponent.java b/core/roboconf-dm-rest-services/src/main/java/net/roboconf/dm/rest/services/internal/ServletRegistrationComponent.java
index 7e85fe88..161012a9 100644
--- a/core/roboconf-dm-rest-services/src/main/java/net/roboconf/dm/rest/services/internal/ServletRegistrationComponent.java
+++ b/core/roboconf-dm-rest-services/src/main/java/net/roboconf/dm/rest/services/internal/ServletRegistrationComponent.java
@@ -29,13 +29,19 @@
import java.util.Hashtable;
import java.util.logging.Logger;
+import javax.servlet.Filter;
+
import org.ops4j.pax.url.mvn.MavenResolver;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
import org.osgi.service.http.HttpService;
import com.sun.jersey.spi.container.servlet.ServletContainer;
import net.roboconf.core.utils.Utils;
import net.roboconf.dm.management.Manager;
+import net.roboconf.dm.rest.commons.security.AuthenticationManager;
+import net.roboconf.dm.rest.services.internal.filters.AuthenticationFilter;
import net.roboconf.dm.rest.services.internal.icons.IconServlet;
import net.roboconf.dm.rest.services.internal.websocket.RoboconfWebSocketServlet;
import net.roboconf.dm.scheduler.IScheduler;
@@ -61,13 +67,31 @@ public class ServletRegistrationComponent {
private Manager manager;
private IScheduler scheduler;
private MavenResolver mavenResolver;
+
private boolean enableCors = false;
+ private boolean enableAuthentication = false;
+ private long sessionPeriod;
// Internal fields
private final Logger logger = Logger.getLogger( getClass().getName());
+ private final BundleContext bundleContext;
+
RestApplication app;
ServletContainer jerseyServlet;
+ ServiceRegistration filterServiceRegistration;
+ AuthenticationFilter authenticationFilter;
+ AuthenticationManager authenticationMngr;
+
+
+ /**
+ * Constructor.
+ * @param bundleContext
+ */
+ public ServletRegistrationComponent( BundleContext bundleContext ) {
+ this.bundleContext = bundleContext;
+ }
+
/**
* The method to use when all the dependencies are resolved.
@@ -107,6 +131,16 @@ public void starting() throws Exception {
RoboconfWebSocketServlet websocketServlet = new RoboconfWebSocketServlet();
this.httpService.registerServlet( WEBSOCKET_CONTEXT, websocketServlet, initParams, null );
+
+ // Register a filter for authentication
+ this.authenticationFilter = new AuthenticationFilter();
+ this.authenticationFilter.setAuthenticationEnabled( this.enableAuthentication );
+ this.authenticationFilter.setAuthenticationMngr( this.authenticationMngr );
+ this.authenticationFilter.setSessionPeriod( this.sessionPeriod );
+
+ initParams = new Hashtable<> ();
+ initParams.put( "urlPatterns", "*" );
+ this.filterServiceRegistration = this.bundleContext.registerService( Filter.class, this.authenticationFilter, initParams );
}
@@ -116,6 +150,10 @@ public void starting() throws Exception {
*/
public void stopping() throws Exception {
+ // Remove the filter
+ if( this.filterServiceRegistration != null )
+ this.filterServiceRegistration.unregister();
+
// Update the HTTP service
this.logger.fine( "iPojo unregisters REST and icons servlets related to Roboconf's DM." );
if( this.httpService != null ) {
@@ -130,6 +168,8 @@ public void stopping() throws Exception {
// Reset the application
this.app = null;
this.jerseyServlet = null;
+ this.filterServiceRegistration = null;
+ this.authenticationFilter = null;
}
@@ -223,6 +263,52 @@ public void setEnableCors( boolean enableCors ) {
}
+ /**
+ * Invoked by iPojo.
+ * @param enableAuthentication the enableAuthentication to set
+ */
+ public void setEnableAuthentication( boolean enableAuthentication ) {
+
+ this.logger.fine( "Authentication is now " + (enableAuthentication ? "enabled" : "disabled") + ". Updating the REST resource." );
+ this.enableAuthentication = enableAuthentication;
+
+ if( this.authenticationFilter != null )
+ this.authenticationFilter.setAuthenticationEnabled( enableAuthentication );
+ }
+
+
+ /**
+ * @param authenticationRealm the authenticationRealm to set
+ */
+ public void setAuthenticationRealm( String authenticationRealm ) {
+
+ // Given the way sessions are stored in AuthenticationManager (private map),
+ // changing the realm will invalidate all the current sessions
+ this.logger.fine( "New authentication realm: " + authenticationRealm );
+ this.authenticationMngr = new AuthenticationManager( authenticationRealm );
+
+ // Propagate the change
+ if( this.authenticationFilter != null )
+ this.authenticationFilter.setAuthenticationMngr( this.authenticationMngr );
+
+ if( this.app != null )
+ this.app.setAuthenticationManager( this.authenticationMngr );
+ }
+
+
+ /**
+ * @param sessionPeriod the sessionPeriod to set
+ */
+ public void setSessionPeriod( long sessionPeriod ) {
+
+ this.logger.fine( "New session period: " + sessionPeriod );
+ this.sessionPeriod = sessionPeriod;
+
+ if( this.authenticationFilter != null )
+ this.authenticationFilter.setSessionPeriod( sessionPeriod );
+ }
+
+
// These setters are not used by iPojo.
// But they may be useful when using this class outside OSGi.
diff --git a/core/roboconf-dm-rest-services/src/main/java/net/roboconf/dm/rest/services/internal/filters/AuthenticationFilter.java b/core/roboconf-dm-rest-services/src/main/java/net/roboconf/dm/rest/services/internal/filters/AuthenticationFilter.java
new file mode 100644
index 00000000..a44a6868
--- /dev/null
+++ b/core/roboconf-dm-rest-services/src/main/java/net/roboconf/dm/rest/services/internal/filters/AuthenticationFilter.java
@@ -0,0 +1,146 @@
+/**
+ * Copyright 2017 Linagora, Université Joseph Fourier, Floralis
+ *
+ * The present code is developed in the scope of the joint LINAGORA -
+ * Université Joseph Fourier - Floralis research program and is designated
+ * as a "Result" pursuant to the terms and conditions of the LINAGORA
+ * - Université Joseph Fourier - Floralis research program. Each copyright
+ * holder of Results enumerated here above fully & independently holds complete
+ * ownership of the complete Intellectual Property rights applicable to the whole
+ * of said Results, and may freely exploit it in any manner which does not infringe
+ * the moral rights of the other copyright holders.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.roboconf.dm.rest.services.internal.filters;
+
+import java.io.IOException;
+import java.util.logging.Logger;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import net.roboconf.core.utils.Utils;
+import net.roboconf.dm.rest.commons.security.AuthenticationManager;
+import net.roboconf.dm.rest.services.internal.resources.IAuthenticationResource;
+
+/**
+ * A filter to determine and request (if necessary) authentication.
+ *
+ * This filter is registered as an OSGi service. PAX's web extender automatically
+ * binds it to the web server (Karaf's Jetty). This filter is only applied to the
+ * resources in this bundle, which means the REST API and the web socket. Other web
+ * applications are not impacted. As an example, Karaf and Roboconf web administrations
+ * are served by other bundles, this filter cannot be applied to them.
+ *
+ * @author Vincent Zurczak - Linagora
+ */
+public class AuthenticationFilter implements Filter {
+
+ public static final String SESSION_ID = "sid";
+ private final Logger logger = Logger.getLogger( getClass().getName());
+
+ private AuthenticationManager authenticationMngr;
+ private boolean authenticationEnabled;
+ private long sessionPeriod;
+
+
+ @Override
+ public void doFilter( ServletRequest req, ServletResponse resp, FilterChain chain )
+ throws IOException, ServletException {
+
+ if( ! this.authenticationEnabled ) {
+ chain.doFilter( req, resp );
+
+ } else {
+ HttpServletRequest request = (HttpServletRequest) req;
+ HttpServletResponse response = (HttpServletResponse) resp;
+ String requestedPath = request.getRequestURI();
+ this.logger.info( "Path for auth: " + requestedPath );
+
+ // Find the session ID in the cookies
+ String sessionId = null;
+ Cookie[] cookies = request.getCookies();
+ if( cookies != null ) {
+ for( Cookie cookie : cookies ) {
+ if( SESSION_ID.equals( cookie.getName())) {
+ sessionId = cookie.getValue();
+ break;
+ }
+ }
+ }
+
+ // Is there a valid session?
+ boolean loggedIn = false;
+ if( ! Utils.isEmptyOrWhitespaces( sessionId )) {
+ loggedIn = this.authenticationMngr.isSessionValid( sessionId, this.sessionPeriod );
+ this.logger.finest( "Session " + sessionId + (loggedIn ? " was successfully " : " failed to be ") + "validated." );
+ } else {
+ this.logger.finest( "No session ID was found in the cookie. Authentication cannot be performed." );
+ }
+
+ // Valid session, go on. Send an error otherwise.
+ // No redirection, we mainly deal with our web socket and REST API.
+ boolean loginRequest = IAuthenticationResource.LOGIN_PATH.equals( requestedPath );
+ if( loggedIn || loginRequest ) {
+ chain.doFilter( request, response );
+ } else {
+ response.sendError( 403, "Authentication is required." );
+ }
+ }
+ }
+
+
+ @Override
+ public void destroy() {
+ // nothing
+ }
+
+
+ @Override
+ public void init( FilterConfig filterConfig ) throws ServletException {
+ // nothing
+ }
+
+
+ /**
+ * @param authenticationEnabled the authenticationEnabled to set
+ */
+ public void setAuthenticationEnabled( boolean authenticationEnabled ) {
+ this.authenticationEnabled = authenticationEnabled;
+ }
+
+
+ /**
+ * @param authenticationMngr the authenticationMngr to set
+ */
+ public void setAuthenticationMngr( AuthenticationManager authenticationMngr ) {
+ this.authenticationMngr = authenticationMngr;
+ }
+
+
+ /**
+ * @param sessionPeriod the sessionPeriod to set
+ */
+ public void setSessionPeriod( long sessionPeriod ) {
+ this.sessionPeriod = sessionPeriod;
+ }
+}
diff --git a/core/roboconf-dm-rest-services/src/main/java/net/roboconf/dm/rest/services/internal/resources/IAuthenticationResource.java b/core/roboconf-dm-rest-services/src/main/java/net/roboconf/dm/rest/services/internal/resources/IAuthenticationResource.java
new file mode 100644
index 00000000..a8fd4f4f
--- /dev/null
+++ b/core/roboconf-dm-rest-services/src/main/java/net/roboconf/dm/rest/services/internal/resources/IAuthenticationResource.java
@@ -0,0 +1,71 @@
+/**
+ * Copyright 2017 Linagora, Université Joseph Fourier, Floralis
+ *
+ * The present code is developed in the scope of the joint LINAGORA -
+ * Université Joseph Fourier - Floralis research program and is designated
+ * as a "Result" pursuant to the terms and conditions of the LINAGORA
+ * - Université Joseph Fourier - Floralis research program. Each copyright
+ * holder of Results enumerated here above fully & independently holds complete
+ * ownership of the complete Intellectual Property rights applicable to the whole
+ * of said Results, and may freely exploit it in any manner which does not infringe
+ * the moral rights of the other copyright holders.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.roboconf.dm.rest.services.internal.resources;
+
+import javax.ws.rs.CookieParam;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.core.Response;
+
+import net.roboconf.dm.rest.commons.UrlConstants;
+import net.roboconf.dm.rest.services.internal.filters.AuthenticationFilter;
+
+/**
+ * @author Vincent Zurczak - Linagora
+ */
+public interface IAuthenticationResource {
+
+ String PATH = "/" + UrlConstants.AUTHENTICATION;
+ String LOGIN_PATH = PATH + "/e";
+
+
+ /**
+ * Authenticates a user.
+ * @param username a user name
+ * @param password a password
+ * @return a response (the session ID is returned as a cookie)
+ * @see AuthenticationFilter#SESSION_ID for the cookie's name
+ *
+ * @HTTP 200 Login succeeded.
+ * @HTTP 403 Login failed.
+ * @HTTP 500 Invalid server configuration.
+ */
+ @POST
+ @Path( LOGIN_PATH )
+ Response login( @HeaderParam("u") String username, @HeaderParam("p") String password );
+
+
+ /**
+ * Terminates a user session.
+ * @param sessionId a session ID
+ * @return a response
+ * @HTTP 200 Always successful.
+ */
+ @POST
+ @Path("/s")
+ Response logout( @CookieParam( AuthenticationFilter.SESSION_ID ) String sessionId );
+}
diff --git a/core/roboconf-dm-rest-services/src/main/java/net/roboconf/dm/rest/services/internal/resources/impl/AuthenticationResource.java b/core/roboconf-dm-rest-services/src/main/java/net/roboconf/dm/rest/services/internal/resources/impl/AuthenticationResource.java
new file mode 100644
index 00000000..0c119341
--- /dev/null
+++ b/core/roboconf-dm-rest-services/src/main/java/net/roboconf/dm/rest/services/internal/resources/impl/AuthenticationResource.java
@@ -0,0 +1,88 @@
+/**
+ * Copyright 2017 Linagora, Université Joseph Fourier, Floralis
+ *
+ * The present code is developed in the scope of the joint LINAGORA -
+ * Université Joseph Fourier - Floralis research program and is designated
+ * as a "Result" pursuant to the terms and conditions of the LINAGORA
+ * - Université Joseph Fourier - Floralis research program. Each copyright
+ * holder of Results enumerated here above fully & independently holds complete
+ * ownership of the complete Intellectual Property rights applicable to the whole
+ * of said Results, and may freely exploit it in any manner which does not infringe
+ * the moral rights of the other copyright holders.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.roboconf.dm.rest.services.internal.resources.impl;
+
+import java.util.logging.Logger;
+
+import javax.ws.rs.Path;
+import javax.ws.rs.core.NewCookie;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+import net.roboconf.dm.rest.commons.security.AuthenticationManager;
+import net.roboconf.dm.rest.services.internal.filters.AuthenticationFilter;
+import net.roboconf.dm.rest.services.internal.resources.IAuthenticationResource;
+
+/**
+ * @author Vincent Zurczak - Linagora
+ */
+@Path( IAuthenticationResource.PATH )
+public class AuthenticationResource implements IAuthenticationResource {
+
+ private final Logger logger = Logger.getLogger( getClass().getName());
+ private AuthenticationManager authenticationManager;
+
+
+ @Override
+ public Response login( String username, String password ) {
+ this.logger.fine( "Authenticating user " + username + "..." );
+
+ String sessionId;
+ Response response;
+ if( this.authenticationManager == null )
+ response = Response.status( Status.INTERNAL_SERVER_ERROR ).entity( "No authentication manager was available." ).build();
+ else if(( sessionId = this.authenticationManager.login( username, password )) == null )
+ response = Response.status( Status.FORBIDDEN ).entity( "Authentication failed." ).build();
+ else
+ response = Response.ok().cookie( new NewCookie( AuthenticationFilter.SESSION_ID, sessionId )).build();
+
+ // NewCookie's implementation uses NewCookie.DEFAULT_MAX_AGE as the default
+ // validity for a cookie, which means it is valid until the browser is closed.
+ // That's fine for us. In addition, we maintain a validity period on the server, in memory.
+ // This last one is managed by the authentication manager, itself bound to a REALM.
+
+ return response;
+ }
+
+
+ @Override
+ public Response logout( String sessionId ) {
+
+ this.logger.fine( "Terminating session " + sessionId + "..." );
+ if( this.authenticationManager != null )
+ this.authenticationManager.logout( sessionId );
+
+ return Response.ok().build();
+ }
+
+
+ /**
+ * @param authenticationManager the authenticationManager to set
+ */
+ public void setAuthenticationManager( AuthenticationManager authenticationManager ) {
+ this.authenticationManager = authenticationManager;
+ }
+}
diff --git a/core/roboconf-dm-rest-services/src/test/java/net/roboconf/dm/rest/services/internal/ServletRegistrationComponentTest.java b/core/roboconf-dm-rest-services/src/test/java/net/roboconf/dm/rest/services/internal/ServletRegistrationComponentTest.java
index 1efaeddd..b92a282a 100644
--- a/core/roboconf-dm-rest-services/src/test/java/net/roboconf/dm/rest/services/internal/ServletRegistrationComponentTest.java
+++ b/core/roboconf-dm-rest-services/src/test/java/net/roboconf/dm/rest/services/internal/ServletRegistrationComponentTest.java
@@ -31,6 +31,7 @@
import java.util.HashMap;
import java.util.Map;
+import javax.servlet.Filter;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
@@ -44,6 +45,8 @@
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.Mockito;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
import org.osgi.service.http.HttpContext;
import org.osgi.service.http.HttpService;
import org.osgi.service.http.NamespaceException;
@@ -58,6 +61,8 @@
import net.roboconf.dm.management.Manager;
import net.roboconf.dm.rest.commons.UrlConstants;
import net.roboconf.dm.rest.commons.json.JSonBindingUtils;
+import net.roboconf.dm.rest.commons.security.AuthenticationManager;
+import net.roboconf.dm.rest.services.internal.filters.AuthenticationFilter;
import net.roboconf.messaging.api.MessagingConstants;
/**
@@ -72,8 +77,12 @@ public class ServletRegistrationComponentTest {
private TestManagerWrapper managerWrapper;
private TestApplication app;
+ private ServletRegistrationComponent register;
+ private BundleContext bundleContext;
+
@Before
+ @SuppressWarnings( "unchecked" )
public void initializeManager() throws Exception {
this.manager = new Manager();
this.manager.setMessagingType(MessagingConstants.FACTORY_TEST);
@@ -85,6 +94,14 @@ public void initializeManager() throws Exception {
this.managerWrapper = new TestManagerWrapper( this.manager );
this.managerWrapper.addManagedApplication( new ManagedApplication( this.app ));
+
+ this.bundleContext = Mockito.mock( BundleContext.class );
+ Mockito.when( this.bundleContext.registerService(
+ Mockito.eq( Filter.class ),
+ Mockito.any( Filter.class ),
+ Mockito.any( Dictionary.class ))).thenReturn( Mockito.mock( ServiceRegistration.class ));
+
+ this.register = new ServletRegistrationComponent( this.bundleContext );
}
@@ -97,27 +114,35 @@ public void stopManager() {
@Test
public void testStop_httpServiceIsNull() throws Exception {
- ServletRegistrationComponent register = new ServletRegistrationComponent();
- register.stopping();
+ this.register.stopping();
+ Mockito.verifyZeroInteractions( this.bundleContext );
}
@Test
+ @SuppressWarnings( "unchecked" )
public void testStartAndStop() throws Exception {
- ServletRegistrationComponent register = new ServletRegistrationComponent();
-
// No error if these methods are called before the component is started.
- register.schedulerAppears();
- register.schedulerDisappears();
+ this.register.schedulerAppears();
+ this.register.schedulerDisappears();
+
+ this.register.mavenResolverAppears();
+ this.register.mavenResolverDisappears();
// Deal with the HTTP service.
HttpServiceForTest httpService = new HttpServiceForTest();
- register.setHttpService( httpService );
+ this.register.setHttpService( httpService );
Assert.assertEquals( 0, httpService.pathToServlet.size());
- register.starting();
+ Mockito.verifyZeroInteractions( this.bundleContext );
+ this.register.starting();
+
Assert.assertEquals( 3, httpService.pathToServlet.size());
+ Mockito.verify( this.bundleContext, Mockito.only()).registerService(
+ Mockito.eq( Filter.class ),
+ Mockito.any( AuthenticationFilter.class ),
+ Mockito.any( Dictionary.class ));
ServletContainer jerseyServlet = (ServletContainer) httpService.pathToServlet.get( ServletRegistrationComponent.REST_CONTEXT );
Assert.assertNotNull( jerseyServlet );
@@ -128,17 +153,28 @@ public void testStartAndStop() throws Exception {
HttpServlet websocketServlet = (HttpServlet) httpService.pathToServlet.get( ServletRegistrationComponent.WEBSOCKET_CONTEXT );
Assert.assertNotNull( websocketServlet );
+ // Check there is no authentication manager
+ Assert.assertNull( this.register.authenticationMngr );
+ this.register.setAuthenticationRealm( "realm" );
+ Assert.assertNotNull( this.register.authenticationMngr );
+
// Update the scheduler...
- register.schedulerAppears();
- register.schedulerDisappears();
+ this.register.schedulerAppears();
+ this.register.schedulerDisappears();
// Update the URL resolver
- register.mavenResolverAppears();
- register.mavenResolverDisappears();
+ this.register.mavenResolverAppears();
+ this.register.mavenResolverDisappears();
// Stop...
- register.stopping();
+ this.register.stopping();
+
Assert.assertEquals( 0, httpService.pathToServlet.size());
+ Assert.assertNull( this.register.app );
+ Assert.assertNull( this.register.authenticationFilter );
+ Assert.assertNull( this.register.jerseyServlet );
+ Assert.assertNull( this.register.filterServiceRegistration );
+ Assert.assertNotNull( this.register.authenticationMngr );
}
@@ -205,31 +241,93 @@ public void testJsonSerialization_instance() throws Exception {
public void testSetEnableCors() throws Exception {
// No NPE
- ServletRegistrationComponent register = new ServletRegistrationComponent();
- register.setEnableCors( true );
- register.setEnableCors( false );
+ this.register.setEnableCors( true );
+ this.register.setEnableCors( false );
+
+ // Act like if the component had been started
+ this.register.app = Mockito.spy( new RestApplication( this.manager ));
+ this.register.jerseyServlet = Mockito.mock( ServletContainer.class );
+
+ this.register.setEnableCors( true );
+ Mockito.verify( this.register.app, Mockito.times( 1 )).enableCors( true );
+ Mockito.verify( this.register.jerseyServlet, Mockito.only()).reload();
+
+ Mockito.reset( this.register.app );
+ Mockito.reset( this.register.jerseyServlet );
+
+ this.register.setEnableCors( false );
+ Mockito.verify( this.register.app, Mockito.times( 1 )).enableCors( false );
+ Mockito.verify( this.register.jerseyServlet, Mockito.only()).reload();
+
+ // Stop...
+ this.register.stopping();
+
+ // No NPE
+ this.register.setEnableCors( true );
+ this.register.setEnableCors( false );
+ }
+
+
+ @Test
+ public void testSetSessionPeriod() throws Exception {
+
+ // No NPE
+ this.register.setSessionPeriod( 50 );
// Act like if the component had been started
- register.app = Mockito.spy( new RestApplication( this.manager ));
- register.jerseyServlet = Mockito.mock( ServletContainer.class );
+ this.register.authenticationFilter = Mockito.mock( AuthenticationFilter.class );
- register.setEnableCors( true );
- Mockito.verify( register.app, Mockito.times( 1 )).enableCors( true );
- Mockito.verify( register.jerseyServlet, Mockito.only()).reload();
+ this.register.setSessionPeriod( 500 );
+ Mockito.verify( this.register.authenticationFilter, Mockito.only()).setSessionPeriod( 500 );
- Mockito.reset( register.app );
- Mockito.reset( register.jerseyServlet );
+ // Stop...
+ this.register.stopping();
+
+ // No NPE
+ this.register.setSessionPeriod( -1 );
+ }
+
+
+ @Test
+ public void testSetEnableAuthentication() throws Exception {
+
+ // No NPE
+ this.register.setEnableAuthentication( true );
+ this.register.setEnableAuthentication( false );
+
+ // Act like if the component had been started
+ this.register.authenticationFilter = Mockito.mock( AuthenticationFilter.class );
+
+ this.register.setEnableAuthentication( true );
+ Mockito.verify( this.register.authenticationFilter, Mockito.only()).setAuthenticationEnabled( true );
+
+ // Stop...
+ this.register.stopping();
+
+ // No NPE
+ this.register.setEnableAuthentication( false );
+ }
+
+
+ @Test
+ public void testSetAuthenticationRealm() throws Exception {
+
+ // No NPE
+ Assert.assertNull( this.register.authenticationMngr );
+ this.register.setAuthenticationRealm( "realm" );
+ Assert.assertNotNull( this.register.authenticationMngr );
+
+ // Act like if the component had been started
+ this.register.authenticationFilter = Mockito.mock( AuthenticationFilter.class );
- register.setEnableCors( false );
- Mockito.verify( register.app, Mockito.times( 1 )).enableCors( false );
- Mockito.verify( register.jerseyServlet, Mockito.only()).reload();
+ this.register.setAuthenticationRealm( "realm2" );
+ Mockito.verify( this.register.authenticationFilter, Mockito.only()).setAuthenticationMngr( Mockito.any( AuthenticationManager.class ));
// Stop...
- register.stopping();
+ this.register.stopping();
// No NPE
- register.setEnableCors( true );
- register.setEnableCors( false );
+ this.register.setAuthenticationRealm( "realm" );
}
diff --git a/core/roboconf-dm-rest-services/src/test/java/net/roboconf/dm/rest/services/internal/filters/AuthenticationFilterTest.java b/core/roboconf-dm-rest-services/src/test/java/net/roboconf/dm/rest/services/internal/filters/AuthenticationFilterTest.java
new file mode 100644
index 00000000..cd80d5aa
--- /dev/null
+++ b/core/roboconf-dm-rest-services/src/test/java/net/roboconf/dm/rest/services/internal/filters/AuthenticationFilterTest.java
@@ -0,0 +1,179 @@
+/**
+ * Copyright 2017 Linagora, Université Joseph Fourier, Floralis
+ *
+ * The present code is developed in the scope of the joint LINAGORA -
+ * Université Joseph Fourier - Floralis research program and is designated
+ * as a "Result" pursuant to the terms and conditions of the LINAGORA
+ * - Université Joseph Fourier - Floralis research program. Each copyright
+ * holder of Results enumerated here above fully & independently holds complete
+ * ownership of the complete Intellectual Property rights applicable to the whole
+ * of said Results, and may freely exploit it in any manner which does not infringe
+ * the moral rights of the other copyright holders.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.roboconf.dm.rest.services.internal.filters;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import net.roboconf.dm.rest.commons.security.AuthenticationManager;
+import net.roboconf.dm.rest.services.internal.resources.IAuthenticationResource;
+
+/**
+ * @author Vincent Zurczak - Linagora
+ */
+public class AuthenticationFilterTest {
+
+ @Test
+ public void forCodeCoverage() throws Exception {
+
+ AuthenticationFilter filter = new AuthenticationFilter();
+ filter.init( null );
+ filter.destroy();
+ }
+
+
+ @Test
+ public void testDoFiler_noAuthentication() throws Exception {
+
+ AuthenticationFilter filter = new AuthenticationFilter();
+ filter.setAuthenticationEnabled( false );
+
+ ServletRequest req = Mockito.mock( ServletRequest.class );
+ ServletResponse resp = Mockito.mock( ServletResponse.class );
+ FilterChain chain = Mockito.mock( FilterChain.class );
+
+ filter.doFilter( req, resp, chain );
+ Mockito.verifyZeroInteractions( req );
+ Mockito.verifyZeroInteractions( resp );
+ Mockito.verify( chain, Mockito.only()).doFilter( req, resp );
+ }
+
+
+ @Test
+ public void testDoFiler_withAuthentication_noCookie() throws Exception {
+
+ AuthenticationFilter filter = new AuthenticationFilter();
+ filter.setAuthenticationEnabled( true );
+
+ HttpServletRequest req = Mockito.mock( HttpServletRequest.class );
+ HttpServletResponse resp = Mockito.mock( HttpServletResponse.class );
+ FilterChain chain = Mockito.mock( FilterChain.class );
+
+ filter.doFilter( req, resp, chain );
+ Mockito.verify( req ).getCookies();
+ Mockito.verify( req ).getRequestURI();
+ Mockito.verifyNoMoreInteractions( req );
+
+ Mockito.verify( resp, Mockito.only()).sendError( 403, "Authentication is required." );
+ Mockito.verifyZeroInteractions( chain );
+ }
+
+
+ @Test
+ public void testDoFiler_withAuthentication_withCookie_loggedIn() throws Exception {
+
+ final String sessionId = "a1a2a3a4";
+ final long sessionPeriod = -1;
+
+ AuthenticationFilter filter = new AuthenticationFilter();
+ filter.setAuthenticationEnabled( true );
+ filter.setSessionPeriod( sessionPeriod );
+
+ AuthenticationManager authMngr = Mockito.mock( AuthenticationManager.class );
+ Mockito.when( authMngr.isSessionValid( sessionId, sessionPeriod )).thenReturn( true );
+ filter.setAuthenticationMngr( authMngr );
+
+ FilterChain chain = Mockito.mock( FilterChain.class );
+ HttpServletResponse resp = Mockito.mock( HttpServletResponse.class );
+ HttpServletRequest req = Mockito.mock( HttpServletRequest.class );
+ Mockito.when( req.getCookies()).thenReturn( new Cookie[] {
+ new Cookie( "as", "as" ),
+ new Cookie( AuthenticationFilter.SESSION_ID, sessionId )
+ });
+
+ filter.doFilter( req, resp, chain );
+ Mockito.verify( req ).getCookies();
+ Mockito.verify( req ).getRequestURI();
+ Mockito.verifyNoMoreInteractions( req );
+
+ Mockito.verify( authMngr ).isSessionValid( sessionId, sessionPeriod );
+ Mockito.verify( chain, Mockito.only()).doFilter( req, resp );
+ Mockito.verifyZeroInteractions( resp );
+ }
+
+
+ @Test
+ public void testDoFiler_withAuthentication_withCookie_notLoggedIn() throws Exception {
+
+ final String sessionId = "a1a2a3a4";
+ final long sessionPeriod = -1;
+
+ AuthenticationFilter filter = new AuthenticationFilter();
+ filter.setAuthenticationEnabled( true );
+ filter.setSessionPeriod( sessionPeriod );
+
+ AuthenticationManager authMngr = Mockito.mock( AuthenticationManager.class );
+ Mockito.when( authMngr.isSessionValid( sessionId, sessionPeriod )).thenReturn( false );
+ filter.setAuthenticationMngr( authMngr );
+
+ FilterChain chain = Mockito.mock( FilterChain.class );
+ HttpServletResponse resp = Mockito.mock( HttpServletResponse.class );
+ HttpServletRequest req = Mockito.mock( HttpServletRequest.class );
+ Mockito.when( req.getCookies()).thenReturn( new Cookie[] {
+ new Cookie( "as", "as" ),
+ new Cookie( AuthenticationFilter.SESSION_ID, sessionId )
+ });
+
+ filter.doFilter( req, resp, chain );
+ Mockito.verify( req ).getCookies();
+ Mockito.verify( req ).getRequestURI();
+ Mockito.verifyNoMoreInteractions( req );
+
+ Mockito.verify( authMngr ).isSessionValid( sessionId, sessionPeriod );
+ Mockito.verifyZeroInteractions( chain );
+ Mockito.verify( resp, Mockito.only()).sendError( 403, "Authentication is required." );
+ }
+
+
+ @Test
+ public void testDoFiler_withAuthentication_noCookie_butLoginPageRequested() throws Exception {
+
+ AuthenticationFilter filter = new AuthenticationFilter();
+ filter.setAuthenticationEnabled( true );
+
+ HttpServletRequest req = Mockito.mock( HttpServletRequest.class );
+ Mockito.when( req.getRequestURI()).thenReturn( IAuthenticationResource.LOGIN_PATH );
+ Mockito.when( req.getCookies()).thenReturn( new Cookie[ 0 ]);
+
+ HttpServletResponse resp = Mockito.mock( HttpServletResponse.class );
+ FilterChain chain = Mockito.mock( FilterChain.class );
+
+ filter.doFilter( req, resp, chain );
+ Mockito.verify( req ).getCookies();
+ Mockito.verify( req ).getRequestURI();
+ Mockito.verifyNoMoreInteractions( req );
+
+ Mockito.verify( chain, Mockito.only()).doFilter( req, resp );
+ Mockito.verifyZeroInteractions( resp );
+ }
+}
diff --git a/core/roboconf-dm-rest-services/src/test/java/net/roboconf/dm/rest/services/internal/resources/impl/AuthenticationResourceTest.java b/core/roboconf-dm-rest-services/src/test/java/net/roboconf/dm/rest/services/internal/resources/impl/AuthenticationResourceTest.java
new file mode 100644
index 00000000..a7ddc279
--- /dev/null
+++ b/core/roboconf-dm-rest-services/src/test/java/net/roboconf/dm/rest/services/internal/resources/impl/AuthenticationResourceTest.java
@@ -0,0 +1,84 @@
+/**
+ * Copyright 2017 Linagora, Université Joseph Fourier, Floralis
+ *
+ * The present code is developed in the scope of the joint LINAGORA -
+ * Université Joseph Fourier - Floralis research program and is designated
+ * as a "Result" pursuant to the terms and conditions of the LINAGORA
+ * - Université Joseph Fourier - Floralis research program. Each copyright
+ * holder of Results enumerated here above fully & independently holds complete
+ * ownership of the complete Intellectual Property rights applicable to the whole
+ * of said Results, and may freely exploit it in any manner which does not infringe
+ * the moral rights of the other copyright holders.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.roboconf.dm.rest.services.internal.resources.impl;
+
+import javax.security.auth.login.LoginException;
+import javax.ws.rs.core.NewCookie;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import net.roboconf.dm.rest.commons.security.AuthenticationManager;
+import net.roboconf.dm.rest.commons.security.AuthenticationManager.IAuthService;
+import net.roboconf.dm.rest.services.internal.filters.AuthenticationFilter;
+
+/**
+ * @author Vincent Zurczak - Linagora
+ */
+public class AuthenticationResourceTest {
+
+ @Test
+ public void testLoginAndLogout() throws Exception {
+
+ // No authentication manager
+ AuthenticationResource res = new AuthenticationResource();
+ Response resp = res.login( "kikou", "pwd" );
+ Assert.assertEquals( Status.INTERNAL_SERVER_ERROR.getStatusCode(), resp.getStatus());
+
+ resp = res.logout( null );
+ Assert.assertEquals( Status.OK.getStatusCode(), resp.getStatus());
+
+ // Set one
+ AuthenticationManager authMngr = new AuthenticationManager( "my realm" );
+ IAuthService authService = Mockito.mock( IAuthService.class );
+ authMngr.setAuthService( authService );
+ res.setAuthenticationManager( authMngr );
+
+ // Authentication will work for ANY user, except for "u1"
+ Mockito.doThrow( new LoginException( "for test" )).when( authService ).authenticate( "u1", "p1" );
+
+ resp = res.login( "u2", "p2" );
+ Assert.assertEquals( Status.OK.getStatusCode(), resp.getStatus());
+
+ NewCookie cookie = (NewCookie) resp.getMetadata().getFirst( "Set-Cookie" );
+ Assert.assertNotNull( cookie );
+ Assert.assertEquals( AuthenticationFilter.SESSION_ID, cookie.getName());
+ Assert.assertNotNull( cookie.getValue());
+ Assert.assertTrue( authMngr.isSessionValid( cookie.getValue(), -1 ));
+
+ // Log out
+ res.logout( cookie.getValue());
+ Assert.assertFalse( authMngr.isSessionValid( cookie.getValue(), -1 ));
+
+ // Verify "u1" cannot login
+ resp = res.login( "u1", "p1" );
+ Assert.assertEquals( Status.FORBIDDEN.getStatusCode(), resp.getStatus());
+ Assert.assertEquals( 0, resp.getMetadata().size());
+ }
+}
diff --git a/core/roboconf-dm-rest-services/src/test/java/net/roboconf/dm/rest/services/internal/resources/impl/ManagementResourceTest.java b/core/roboconf-dm-rest-services/src/test/java/net/roboconf/dm/rest/services/internal/resources/impl/ManagementResourceTest.java
index dabd2d7a..f0070e56 100644
--- a/core/roboconf-dm-rest-services/src/test/java/net/roboconf/dm/rest/services/internal/resources/impl/ManagementResourceTest.java
+++ b/core/roboconf-dm-rest-services/src/test/java/net/roboconf/dm/rest/services/internal/resources/impl/ManagementResourceTest.java
@@ -30,6 +30,9 @@
import java.io.InputStream;
import java.util.List;
import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.LogManager;
+import java.util.logging.Logger;
import javax.ws.rs.core.Response.Status;
@@ -153,6 +156,11 @@ public void testListApplicationTemplates() throws Exception {
TestApplicationTemplate tpl = new TestApplicationTemplate();
this.managerWrapper.getApplicationTemplates().put( tpl, Boolean.TRUE );
+ // Improve code coverage by setting the log level to finest
+ Logger logger = Logger.getLogger( ManagementResource.class.getName());
+ LogManager.getLogManager().addLogger( logger );
+ logger.setLevel( Level.FINEST );
+
// Get ALL the templates
templates = this.resource.listApplicationTemplates();
Assert.assertNotNull( templates );
diff --git a/karaf/roboconf-karaf-dist-dm/src/main/resources/etc/net.roboconf.dm.rest.services.configuration.cfg b/karaf/roboconf-karaf-dist-dm/src/main/resources/etc/net.roboconf.dm.rest.services.configuration.cfg
index af436ef3..f9d5bc8f 100644
--- a/karaf/roboconf-karaf-dist-dm/src/main/resources/etc/net.roboconf.dm.rest.services.configuration.cfg
+++ b/karaf/roboconf-karaf-dist-dm/src/main/resources/etc/net.roboconf.dm.rest.services.configuration.cfg
@@ -19,3 +19,15 @@
# CORS = https://en.wikipedia.org/wiki/Cross-origin_resource_sharing
# It should be disabled in production environments.
enable-cors = false
+
+# Enable or disable authentication.
+# Users are authenticated against the specified REALM.
+enable-authentication = false
+
+# The REALM that is used for authentication.
+# We use Karaf realms.
+authentication-realm = karaf
+
+# The period of validity for a session once a user is logged in.
+# Expressed in seconds. Use a negative value for infinite validity.
+session-period = -1
diff --git a/karaf/roboconf-karaf-dist-dm/src/main/resources/etc/org.ops4j.pax.logging.cfg b/karaf/roboconf-karaf-dist-dm/src/main/resources/etc/org.ops4j.pax.logging.cfg
index e38c43e1..3fdd6cee 100644
--- a/karaf/roboconf-karaf-dist-dm/src/main/resources/etc/org.ops4j.pax.logging.cfg
+++ b/karaf/roboconf-karaf-dist-dm/src/main/resources/etc/org.ops4j.pax.logging.cfg
@@ -70,3 +70,4 @@ log4j.logger.net.roboconf.dm.internal.tasks.CheckerForHeartbeatsTask=DEBUG, robo
log4j.logger.net.roboconf.dm.rest.services.internal.resources.impl.ApplicationResource=DEBUG, roboconf
log4j.logger.net.roboconf.target.api.AbstractThreadedTargetHandler$CheckingRunnable=DEBUG, roboconf
log4j.logger.net.roboconf.dm.internal.environment.messaging.DmMessageProcessor=DEBUG, roboconf
+log4j.org.apache.karaf.jaas.modules.audit.LogAuditLoginModule=WARN, karaf