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