From c8d4a2ab0123694e4206ad0a297d1c3eaee33b2c Mon Sep 17 00:00:00 2001 From: Vincent Zurczak Date: Wed, 12 Apr 2017 10:53:33 +0200 Subject: [PATCH] #632 User authentication (#773) * #632 User authentication The hardest part is done, integrate with Karaf's JAAS implementation. Next step: implement a servlet filter that uses it * #632 User authentication * #632 User authentication (WTF) --- core/roboconf-dm-rest-commons/pom.xml | 8 +- .../dm/rest/commons/UrlConstants.java | 3 + .../security/AuthenticationManager.java | 250 ++++++++++++++++++ .../security/AuthenticationManagerTest.java | 134 ++++++++++ core/roboconf-dm-rest-services/metadata.xml | 3 + core/roboconf-dm-rest-services/pom.xml | 4 +- .../services/internal/RestApplication.java | 14 + .../ServletRegistrationComponent.java | 92 +++++++ .../filters/AuthenticationFilter.java | 146 ++++++++++ .../resources/IAuthenticationResource.java | 71 +++++ .../impl/AuthenticationResource.java | 94 +++++++ .../ServletRegistrationComponentTest.java | 156 +++++++++-- .../filters/AuthenticationFilterTest.java | 183 +++++++++++++ .../impl/AuthenticationResourceTest.java | 84 ++++++ .../impl/ManagementResourceTest.java | 8 + ...oboconf.dm.rest.services.configuration.cfg | 12 + .../resources/etc/org.ops4j.pax.logging.cfg | 3 +- miscellaneous/roboconf-dm-rest-client/pom.xml | 6 + .../net/roboconf/dm/rest/client/WsClient.java | 57 +++- .../delegates/ApplicationWsDelegate.java | 69 +++-- .../delegates/AuthenticationWsDelegate.java | 118 +++++++++ .../client/delegates/DebugWsDelegate.java | 29 +- .../delegates/ManagementWsDelegate.java | 47 ++-- .../delegates/PreferencesWsDelegate.java | 8 +- .../client/delegates/SchedulerWsDelegate.java | 28 +- .../client/delegates/TargetWsDelegate.java | 21 +- .../AuthenticationWsDelegateTest.java | 105 ++++++++ .../delegates/ManagementWsDelegateTest.java | 6 + .../in/memory/RestSecuredServicesTest.java | 152 +++++++++++ 29 files changed, 1809 insertions(+), 102 deletions(-) create mode 100644 core/roboconf-dm-rest-commons/src/main/java/net/roboconf/dm/rest/commons/security/AuthenticationManager.java create mode 100644 core/roboconf-dm-rest-commons/src/test/java/net/roboconf/dm/rest/commons/security/AuthenticationManagerTest.java create mode 100644 core/roboconf-dm-rest-services/src/main/java/net/roboconf/dm/rest/services/internal/filters/AuthenticationFilter.java create mode 100644 core/roboconf-dm-rest-services/src/main/java/net/roboconf/dm/rest/services/internal/resources/IAuthenticationResource.java create mode 100644 core/roboconf-dm-rest-services/src/main/java/net/roboconf/dm/rest/services/internal/resources/impl/AuthenticationResource.java create mode 100644 core/roboconf-dm-rest-services/src/test/java/net/roboconf/dm/rest/services/internal/filters/AuthenticationFilterTest.java create mode 100644 core/roboconf-dm-rest-services/src/test/java/net/roboconf/dm/rest/services/internal/resources/impl/AuthenticationResourceTest.java create mode 100644 miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/AuthenticationWsDelegate.java create mode 100644 miscellaneous/roboconf-dm-rest-client/src/test/java/net/roboconf/dm/rest/client/delegates/AuthenticationWsDelegateTest.java create mode 100644 miscellaneous/roboconf-integration-tests-dm-with-agents-in-memory/src/test/java/net/roboconf/integration/tests/dm/with/agents/in/memory/RestSecuredServicesTest.java diff --git a/core/roboconf-dm-rest-commons/pom.xml b/core/roboconf-dm-rest-commons/pom.xml index 7fabad1c..82930807 100644 --- a/core/roboconf-dm-rest-commons/pom.xml +++ b/core/roboconf-dm-rest-commons/pom.xml @@ -86,7 +86,13 @@ roboconf-dm ${project.version} test - + + + + org.mockito + mockito-core + test + 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..eff86750 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,7 @@ public interface UrlConstants { String TARGETS = "targets"; String PREFERENCES = "preferences"; String SCHEDULER = "scheduler"; + String AUTHENTICATION = "auth"; + + String SESSION_ID = "sid"; } 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 new file mode 100644 index 00000000..7c4e9d57 --- /dev/null +++ b/core/roboconf-dm-rest-commons/src/main/java/net/roboconf/dm/rest/commons/security/AuthenticationManager.java @@ -0,0 +1,250 @@ +/** + * 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.commons.security; + +import java.io.IOException; +import java.util.Date; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +/** + * A class in charge of managing authentication and sessions. + *

+ * Authentication is delegated to various implementations. + * By default, it is handled by Karaf's JAAS implementation, but it is possible + * to override it by using {@link #setAuthService(IAuthService)}. You will HAVE + * TO use this method if you run the REST services outside Karaf. + *

+ *

+ * When the authentication succeeds, a token is generated by this class + * (a random UUID in fact). The token is stored by this class and associated + * with the login time. + *

+ *

+ * Since sessions can be limited in time (depending on admin preferences), + * we can verify on every action that the session is still valid. + *

+ *

+ * To prevent "man in the middle" "attacks, authentication should be + * used along with HTTPS. + *

+ * + * @author Vincent Zurczak - Linagora + */ +public class AuthenticationManager { + + private final ConcurrentHashMap tokenToLoginTime = new ConcurrentHashMap<> (); + private final Logger logger = Logger.getLogger( getClass().getName()); + private final String realm; + + private IAuthService authService; + + + /** + * @author Vincent Zurczak - Linagora + */ + public static enum REST_ROLES { + ADM, TPL_RW, APP_RW, APP_R + } + + + + /** + * Constructor. + * @param realm + */ + public AuthenticationManager( String realm ) { + this.realm = realm; + this.authService = new KarafAuthService(); + this.authService.setRealm( this.realm ); + } + + + /** + * @param authenticater the authService to set + */ + public void setAuthService( IAuthService authService ) { + this.authService = authService; + authService.setRealm( this.realm ); + } + + + /** + * Authenticates a user and creates a new session. + * @param user a user name + * @param pwd a pass word + * @return a token if authentication worked, null if it failed + */ + public String login( String user, String pwd ) { + + String token = null; + try { + this.authService.authenticate( user, pwd ); + + token = UUID.randomUUID().toString(); + Long now = new Date().getTime(); + this.tokenToLoginTime.put( token, now ); + + } catch( LoginException e ) { + this.logger.severe( "Invalid login attempt by user " + user ); + } + + return token; + } + + + /** + * Determines whether a session is valid. + * @param token a token + * @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, long validityPeriod ) { + + boolean valid = false; + Long loginTime = null; + if( token != null ) + loginTime = this.tokenToLoginTime.get( token ); + + if( validityPeriod < 0 ) { + valid = loginTime != null; + + } else if( loginTime != null ) { + long now = new Date().getTime(); + valid = (now - loginTime) <= validityPeriod * 1000; + + // Invalid sessions should be deleted + if( ! valid ) + logout( token ); + } + + return valid; + } + + + /** + * Invalidates a session. + *

+ * No error is thrown if the session was already invalid. + *

+ * + * @param token a token (can be null) + */ + public void logout( String token ) { + if( token != null ) + this.tokenToLoginTime.remove( token ); + } + + + /** + * An abstraction to manage authentication. + * @author Vincent Zurczak - Linagora + */ + public interface IAuthService { + + /** + * Authenticates someone by user and password. + * @param user a user name + * @param pwd a password + * @throws LoginException if authentication failed + */ + void authenticate( String user, String pwd ) throws LoginException; + + /** + * Sets the REALM to use. + * @param realm a realm name + */ + void setRealm( String realm ); + } + + + /** + * Authentication managed by Apache Karaf. + *

+ * Karaf uses JAAS and by default supports several login modules + * (properties files, databases, LDAP, etc). + *

+ * @author Vincent Zurczak - Linagora + */ + public static class KarafAuthService implements IAuthService { + private String realm; + + + @Override + public void authenticate( String user, String pwd ) throws LoginException { + LoginContext loginCtx = new LoginContext( this.realm, new RoboconfCallbackHandler( user, pwd )); + loginCtx.login(); + } + + @Override + public void setRealm( String realm ) { + this.realm = realm; + } + } + + + /** + * A callback handler for JAAS. + * @author Vincent Zurczak - Linagora + */ + static final class RoboconfCallbackHandler implements CallbackHandler { + private final String username, password; + + + /** + * Constructor. + * @param username + * @param password + */ + public RoboconfCallbackHandler( String username, String password ) { + this.username = username; + this.password = password; + } + + + @Override + public void handle( Callback[] callbacks ) throws IOException, UnsupportedCallbackException { + + for( Callback callback : callbacks ) { + if (callback instanceof NameCallback ) + ((NameCallback) callback).setName( this.username ); + else if( callback instanceof PasswordCallback ) + ((PasswordCallback) callback).setPassword( this.password.toCharArray()); + else + throw new UnsupportedCallbackException( callback ); + } + } + } +} diff --git a/core/roboconf-dm-rest-commons/src/test/java/net/roboconf/dm/rest/commons/security/AuthenticationManagerTest.java b/core/roboconf-dm-rest-commons/src/test/java/net/roboconf/dm/rest/commons/security/AuthenticationManagerTest.java new file mode 100644 index 00000000..028632d0 --- /dev/null +++ b/core/roboconf-dm-rest-commons/src/test/java/net/roboconf/dm/rest/commons/security/AuthenticationManagerTest.java @@ -0,0 +1,134 @@ +/** + * 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.commons.security; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.LanguageCallback; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.LoginException; + +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import net.roboconf.dm.rest.commons.security.AuthenticationManager.IAuthService; +import net.roboconf.dm.rest.commons.security.AuthenticationManager.RoboconfCallbackHandler; + +/** + * @author Vincent Zurczak - Linagora + */ +public class AuthenticationManagerTest { + + @Test + public void testAuthenticationChain_success() { + + AuthenticationManager mngr = new AuthenticationManager( "realm" ); + IAuthService authService = Mockito.mock( IAuthService.class ); + mngr.setAuthService( authService ); + + String token = mngr.login( "me", "my password" ); + Assert.assertNotNull( token ); + Assert.assertTrue( mngr.isSessionValid( token, 1 )); + Assert.assertTrue( mngr.isSessionValid( token, -1 )); + + mngr.logout( token ); + Assert.assertFalse( mngr.isSessionValid( token, 1 )); + Assert.assertFalse( mngr.isSessionValid( token, -1 )); + } + + + @Test + public void testAuthenticationChain_failure() throws Exception { + + AuthenticationManager mngr = new AuthenticationManager( "realm" ); + IAuthService authService = Mockito.mock( IAuthService.class ); + Mockito.doThrow( new LoginException( "for test" )).when( authService ).authenticate( Mockito.anyString(), Mockito.anyString()); + mngr.setAuthService( authService ); + + String token = mngr.login( "me", "my password" ); + Assert.assertNull( token ); + Assert.assertFalse( mngr.isSessionValid( token, 1 )); + Assert.assertFalse( mngr.isSessionValid( token, -1 )); + + mngr.logout( token ); + Assert.assertFalse( mngr.isSessionValid( token, 1 )); + Assert.assertFalse( mngr.isSessionValid( token, -1 )); + } + + + @Test + public void testAuthenticationChain_validityPeriodExpired() throws Exception { + + AuthenticationManager mngr = new AuthenticationManager( "realm" ); + IAuthService authService = Mockito.mock( IAuthService.class ); + mngr.setAuthService( authService ); + + String token = mngr.login( "me", "my password" ); + Assert.assertNotNull( token ); + Assert.assertTrue( mngr.isSessionValid( token, 1 )); + Thread.sleep( 1020 ); + Assert.assertFalse( mngr.isSessionValid( token, 1 )); + + // The session was removed, it should not be marked as valid anymore + Assert.assertFalse( mngr.isSessionValid( token, 10 )); + } + + + @Test + public void testAuthenticationChain_withKaraf_butOutsideKaraf() throws Exception { + + AuthenticationManager mngr = new AuthenticationManager( "realm" ); + + String token = mngr.login( "me", "my password" ); + Assert.assertNull( token ); + Assert.assertFalse( mngr.isSessionValid( token, -1 )); + } + + + @Test + public void testRoboconfCallbackHandler_success() throws Exception { + + RoboconfCallbackHandler handler = new RoboconfCallbackHandler( "user", "password" ); + handler.handle( new Callback[] { + new NameCallback( "Username: " ), + new PasswordCallback( "Password: ", false ) + }); + } + + + @Test( expected = UnsupportedCallbackException.class ) + public void testRoboconfCallbackHandler_failure() throws Exception { + + RoboconfCallbackHandler handler = new RoboconfCallbackHandler( "user", "password" ); + handler.handle( new Callback[] { + new NameCallback( "Username: " ), + new PasswordCallback( "Password: ", false ), + new LanguageCallback() + }); + } +} 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..3ec8389c 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. @@ -87,6 +111,7 @@ public void starting() throws Exception { this.app.setScheduler( this.scheduler ); this.app.setMavenResolver( this.mavenResolver ); this.app.enableCors( this.enableCors ); + this.app.setAuthenticationManager( this.authenticationMngr ); Dictionary initParams = new Hashtable<> (); initParams.put( "servlet-name", "Roboconf DM (REST)" ); @@ -107,6 +132,21 @@ 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.setAuthenticationManager( this.authenticationMngr ); + this.authenticationFilter.setSessionPeriod( this.sessionPeriod ); + + initParams = new Hashtable<> (); + initParams.put( "urlPatterns", "*" ); + + // Consider the bundle context can be null (e.g. when used outside of OSGi) + if( this.bundleContext != null ) + this.filterServiceRegistration = this.bundleContext.registerService( Filter.class, this.authenticationFilter, initParams ); + else + this.logger.warning( "No bundle context was available, the authentication filter was not registered." ); } @@ -116,6 +156,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 +174,8 @@ public void stopping() throws Exception { // Reset the application this.app = null; this.jerseyServlet = null; + this.filterServiceRegistration = null; + this.authenticationFilter = null; } @@ -223,6 +269,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.setAuthenticationManager( 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..37fd8af8 --- /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.UrlConstants; +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 { + + 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( UrlConstants.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 = requestedPath.endsWith( IAuthenticationResource.PATH + IAuthenticationResource.LOGIN_PATH ); + 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 setAuthenticationManager( 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..51407abb --- /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 = "/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( UrlConstants.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..19d8681a --- /dev/null +++ b/core/roboconf-dm-rest-services/src/main/java/net/roboconf/dm/rest/services/internal/resources/impl/AuthenticationResource.java @@ -0,0 +1,94 @@ +/** + * 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.UrlConstants; +import net.roboconf.dm.rest.commons.security.AuthenticationManager; +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(); + this.logger.fine( "No authentication manager was available. User was " + username ); + + } else if(( sessionId = this.authenticationManager.login( username, password )) == null ) { + response = Response.status( Status.FORBIDDEN ).entity( "Authentication failed." ).build(); + this.logger.fine( "Authentication failed. User was " + username ); + + } else { + response = Response.ok().cookie( new NewCookie( UrlConstants.SESSION_ID, sessionId )).build(); + this.logger.fine( "Authentication succeeded. User was " + username ); + } + + // 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..2840eb5a 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()).setAuthenticationManager( 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..9a5a8ff0 --- /dev/null +++ b/core/roboconf-dm-rest-services/src/test/java/net/roboconf/dm/rest/services/internal/filters/AuthenticationFilterTest.java @@ -0,0 +1,183 @@ +/** + * 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.UrlConstants; +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 ); + Mockito.when( req.getRequestURI()).thenReturn( "/whatever" ); + 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.setAuthenticationManager( authMngr ); + + FilterChain chain = Mockito.mock( FilterChain.class ); + HttpServletResponse resp = Mockito.mock( HttpServletResponse.class ); + HttpServletRequest req = Mockito.mock( HttpServletRequest.class ); + Mockito.when( req.getRequestURI()).thenReturn( "/whatever" ); + Mockito.when( req.getCookies()).thenReturn( new Cookie[] { + new Cookie( "as", "as" ), + new Cookie( UrlConstants.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.setAuthenticationManager( authMngr ); + + FilterChain chain = Mockito.mock( FilterChain.class ); + HttpServletResponse resp = Mockito.mock( HttpServletResponse.class ); + HttpServletRequest req = Mockito.mock( HttpServletRequest.class ); + Mockito.when( req.getRequestURI()).thenReturn( "/whatever" ); + Mockito.when( req.getCookies()).thenReturn( new Cookie[] { + new Cookie( "as", "as" ), + new Cookie( UrlConstants.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.PATH + 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..524ec069 --- /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.UrlConstants; +import net.roboconf.dm.rest.commons.security.AuthenticationManager; +import net.roboconf.dm.rest.commons.security.AuthenticationManager.IAuthService; + +/** + * @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( UrlConstants.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..fc1edfcf 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 @@ -63,10 +63,11 @@ log4j.appender.jersey.layout.ConversionPattern=%d{ISO8601} | %-5.5p | %-32.32c{1 log4j.appender.jersey.file=${karaf.data}/log/jersey.log log4j.appender.jersey.append=true -# Silent very verbose loggers but let them associated with the "roboconf" appender +# Silent very verbose loggers but let them associated with various appenders log4j.logger.net.roboconf.dm.internal.tasks.CheckerForStoredMessagesTask=DEBUG, roboconf log4j.logger.net.roboconf.dm.internal.tasks.CheckerForTargetsConfigurationTask=DEBUG, roboconf log4j.logger.net.roboconf.dm.internal.tasks.CheckerForHeartbeatsTask=DEBUG, roboconf 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.logger.org.apache.karaf.jaas.modules.audit.LogAuditLoginModule=WARN, karaf diff --git a/miscellaneous/roboconf-dm-rest-client/pom.xml b/miscellaneous/roboconf-dm-rest-client/pom.xml index 49432ba2..6a4c948f 100644 --- a/miscellaneous/roboconf-dm-rest-client/pom.xml +++ b/miscellaneous/roboconf-dm-rest-client/pom.xml @@ -170,6 +170,12 @@ 4.3.1 provided + + + org.mockito + mockito-core + test + diff --git a/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/WsClient.java b/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/WsClient.java index ff8b5a21..2ca0933a 100644 --- a/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/WsClient.java +++ b/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/WsClient.java @@ -25,6 +25,8 @@ package net.roboconf.dm.rest.client; +import javax.ws.rs.core.Cookie; + import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider; import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.WebResource; @@ -32,11 +34,13 @@ import com.sun.jersey.api.client.config.DefaultClientConfig; import net.roboconf.dm.rest.client.delegates.ApplicationWsDelegate; +import net.roboconf.dm.rest.client.delegates.AuthenticationWsDelegate; import net.roboconf.dm.rest.client.delegates.DebugWsDelegate; import net.roboconf.dm.rest.client.delegates.ManagementWsDelegate; import net.roboconf.dm.rest.client.delegates.PreferencesWsDelegate; import net.roboconf.dm.rest.client.delegates.SchedulerWsDelegate; import net.roboconf.dm.rest.client.delegates.TargetWsDelegate; +import net.roboconf.dm.rest.commons.UrlConstants; import net.roboconf.dm.rest.commons.json.ObjectMapperProvider; /** @@ -80,8 +84,10 @@ public class WsClient { private final TargetWsDelegate targetWsDelegate; private final SchedulerWsDelegate schedulerDelegate; private final PreferencesWsDelegate preferencesWsDelegate; + private final AuthenticationWsDelegate authenticationWsDelegate; private final Client client; + private String sessionId; /** @@ -98,12 +104,13 @@ public WsClient( String rootUrl ) { this.client.setFollowRedirects( true ); WebResource resource = this.client.resource( rootUrl ); - this.applicationDelegate = new ApplicationWsDelegate( resource ); - this.managementDelegate = new ManagementWsDelegate( resource ); - this.debugDelegate = new DebugWsDelegate( resource ); - this.targetWsDelegate = new TargetWsDelegate( resource ); - this.schedulerDelegate = new SchedulerWsDelegate( resource ); - this.preferencesWsDelegate = new PreferencesWsDelegate( resource ); + this.applicationDelegate = new ApplicationWsDelegate( resource, this ); + this.managementDelegate = new ManagementWsDelegate( resource, this ); + this.debugDelegate = new DebugWsDelegate( resource, this ); + this.targetWsDelegate = new TargetWsDelegate( resource, this ); + this.schedulerDelegate = new SchedulerWsDelegate( resource, this ); + this.preferencesWsDelegate = new PreferencesWsDelegate( resource, this ); + this.authenticationWsDelegate = new AuthenticationWsDelegate( resource, this ); } @@ -157,10 +164,48 @@ public PreferencesWsDelegate getPreferencesWsDelegate() { return this.preferencesWsDelegate; } + /** + * @return the authenticationWsDelegate + */ + public AuthenticationWsDelegate getAuthenticationWsDelegate() { + return this.authenticationWsDelegate; + } + /** * @return the Jersey client (useful to configure it) */ public Client getJerseyClient() { return this.client; } + + /** + * Sets the session ID to use when authenticated. + * @param sessionId + */ + public void setSessionId( String sessionId ) { + this.sessionId = sessionId; + } + + /** + * @return the sessionId + */ + public String getSessionId() { + return this.sessionId; + } + + + /** + * @param resource a web resource + * @return a builder with an optional cookie (for authentication) + */ + public WebResource.Builder createBuilder( WebResource resource ) { + + WebResource.Builder result; + if( this.sessionId != null ) + result = resource.cookie( new Cookie( UrlConstants.SESSION_ID, this.sessionId )); + else + result = resource.getRequestBuilder(); + + return result; + } } diff --git a/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/ApplicationWsDelegate.java b/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/ApplicationWsDelegate.java index 7b36ae35..62f41ccb 100644 --- a/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/ApplicationWsDelegate.java +++ b/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/ApplicationWsDelegate.java @@ -39,6 +39,7 @@ import net.roboconf.core.model.beans.Component; import net.roboconf.core.model.beans.Instance; import net.roboconf.core.model.beans.Instance.InstanceStatus; +import net.roboconf.dm.rest.client.WsClient; import net.roboconf.dm.rest.client.exceptions.ApplicationWsException; import net.roboconf.dm.rest.commons.UrlConstants; @@ -49,14 +50,17 @@ public class ApplicationWsDelegate { private final WebResource resource; private final Logger logger; + private final WsClient wsClient; /** * Constructor. * @param resource a web resource + * @param the WS client */ - public ApplicationWsDelegate( WebResource resource ) { + public ApplicationWsDelegate( WebResource resource, WsClient wsClient ) { this.resource = resource; + this.wsClient = wsClient; this.logger = Logger.getLogger( getClass().getName()); } @@ -86,7 +90,10 @@ public void changeInstanceState( String applicationName, InstanceStatus newStatu if( newStatus != null ) path = path.queryParam( "new-state", newStatus.toString()); - ClientResponse response = path.accept( MediaType.APPLICATION_JSON ).post( ClientResponse.class ); + ClientResponse response = this.wsClient.createBuilder( path ) + .accept( MediaType.APPLICATION_JSON ) + .post( ClientResponse.class ); + handleResponse( response ); this.logger.finer( String.valueOf( response.getStatusInfo())); } @@ -104,7 +111,9 @@ public void setDescription( String applicationName, String newDesc ) this.logger.finer( "Updating the description of application " + applicationName + "." ); WebResource path = this.resource.path( UrlConstants.APP ).path( applicationName ).path( "description" ); - ClientResponse response = path.accept( MediaType.TEXT_PLAIN ).post( ClientResponse.class, newDesc ); + ClientResponse response = this.wsClient.createBuilder( path ) + .accept( MediaType.TEXT_PLAIN ) + .post( ClientResponse.class, newDesc ); handleResponse( response ); this.logger.finer( String.valueOf( response.getStatusInfo())); @@ -126,7 +135,10 @@ public void deployAndStartAll( String applicationName, String instancePath ) if( instancePath != null ) path = path.queryParam( "instance-path", instancePath ); - ClientResponse response = path.accept( MediaType.APPLICATION_JSON ).post( ClientResponse.class ); + ClientResponse response = this.wsClient.createBuilder( path ) + .accept( MediaType.APPLICATION_JSON ) + .post( ClientResponse.class ); + handleResponse( response ); this.logger.finer( String.valueOf( response.getStatusInfo())); } @@ -147,7 +159,10 @@ public void stopAll( String applicationName, String instancePath ) if( instancePath != null ) path = path.queryParam( "instance-path", instancePath ); - ClientResponse response = path.accept( MediaType.APPLICATION_JSON ).post( ClientResponse.class ); + ClientResponse response = this.wsClient.createBuilder( path ) + .accept( MediaType.APPLICATION_JSON ) + .post( ClientResponse.class ); + handleResponse( response ); this.logger.finer( String.valueOf( response.getStatusInfo())); } @@ -168,7 +183,10 @@ public void undeployAll( String applicationName, String instancePath ) if( instancePath != null ) path = path.queryParam( "instance-path", instancePath ); - ClientResponse response = path.accept( MediaType.APPLICATION_JSON ).post( ClientResponse.class ); + ClientResponse response = this.wsClient.createBuilder( path ) + .accept( MediaType.APPLICATION_JSON ) + .post( ClientResponse.class ); + handleResponse( response ); this.logger.finer( String.valueOf( response.getStatusInfo())); } @@ -192,7 +210,8 @@ public List listChildrenInstances( String applicationName, String inst path = path.queryParam( "instance-path", instancePath ); List result = - path.accept( MediaType.APPLICATION_JSON ) + this.wsClient.createBuilder( path ) + .accept( MediaType.APPLICATION_JSON ) .type( MediaType.APPLICATION_JSON ) .get( new GenericType> () {}); @@ -221,7 +240,7 @@ public void addInstance( String applicationName, String parentInstancePath, Inst if( parentInstancePath != null ) path = path.queryParam( "instance-path", parentInstancePath ); - ClientResponse response = path + ClientResponse response = this.wsClient.createBuilder( path ) .accept( MediaType.APPLICATION_JSON ).type( MediaType.APPLICATION_JSON ) .post( ClientResponse.class, instance ); @@ -240,13 +259,13 @@ public void removeInstance( String applicationName, String instancePath ) { this.logger.finer( String.format( "Removing instance \"%s\" from application \"%s\"...", instancePath, applicationName ) ); - this.resource + WebResource path = this.resource .path( UrlConstants.APP ) .path( applicationName ) .path( "instances" ) - .queryParam( "instance-path", instancePath ) - .delete(); + .queryParam( "instance-path", instancePath ); + this.wsClient.createBuilder( path ).delete(); this.logger.finer( String.format( "Instance \"%s\" has been removed from application \"%s\"", instancePath, applicationName ) ); } @@ -260,12 +279,12 @@ public void removeInstance( String applicationName, String instancePath ) { public void resynchronize( String applicationName ) { this.logger.finer( String.format( "Resynchronizing application \"%s\"...", applicationName ) ); - this.resource + WebResource path = this.resource .path( UrlConstants.APP ) .path( applicationName ) - .path( "resynchronize" ) - .post(); + .path( "resynchronize" ); + this.wsClient.createBuilder( path ).post(); this.logger.finer( String.format( "Application \"%s\" has been resynchronized", applicationName ) ); } @@ -278,8 +297,9 @@ public void resynchronize( String applicationName ) { public List listAllComponents( String applicationName ) { this.logger.finer( "Listing components for application " + applicationName + "..." ); - List result = this.resource - .path( UrlConstants.APP ).path( applicationName ).path( "components" ) + WebResource path = this.resource.path( UrlConstants.APP ).path( applicationName ).path( "components" ); + List result = + this.wsClient.createBuilder( path ) .accept( MediaType.APPLICATION_JSON ) .get( new GenericType> () {}); @@ -307,7 +327,10 @@ public List findComponentChildren( String applicationName, String com if( componentName != null ) path = path.queryParam( "component-name", componentName ); - List result = path.accept( MediaType.APPLICATION_JSON ).get( new GenericType> () {}); + List result = this.wsClient.createBuilder( path ) + .accept( MediaType.APPLICATION_JSON ) + .get( new GenericType> () {}); + if( result != null ) { this.logger.finer( result.size() + " possible children was or were found for " + componentName + "." ); } else { @@ -332,7 +355,10 @@ public List findComponentAncestors( String applicationName, String co if( componentName != null ) path = path.queryParam( "component-name", componentName ); - List result = path.accept( MediaType.APPLICATION_JSON ).get( new GenericType> () {}); + List result = this.wsClient.createBuilder( path ) + .accept( MediaType.APPLICATION_JSON ) + .get( new GenericType> () {}); + if( result != null ) { this.logger.finer( result.size() + " possible parents was or were found for " + componentName + "." ); } else { @@ -361,7 +387,7 @@ public void bindApplication( String applicationName, String boundTplName, String .queryParam( "bound-tpl", boundTplName ) .queryParam( "bound-app", boundApp ); - ClientResponse response = path.post( ClientResponse.class ); + ClientResponse response = this.wsClient.createBuilder( path ).post( ClientResponse.class ); handleResponse( response ); } @@ -378,7 +404,10 @@ public List listAllCommands( String applicationName ) { WebResource path = this.resource.path( UrlConstants.APP ) .path( applicationName ).path( "commands" ); - List result = path.accept( MediaType.APPLICATION_JSON ).get( new GenericType> () {}); + List result = this.wsClient.createBuilder( path ) + .accept( MediaType.APPLICATION_JSON ) + .get( new GenericType> () {}); + if( result != null ) { this.logger.finer( result.size() + " command(s) were found for " + applicationName + "." ); } else { diff --git a/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/AuthenticationWsDelegate.java b/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/AuthenticationWsDelegate.java new file mode 100644 index 00000000..74385fed --- /dev/null +++ b/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/AuthenticationWsDelegate.java @@ -0,0 +1,118 @@ +/** + * 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.client.delegates; + +import java.util.List; +import java.util.logging.Logger; + +import javax.ws.rs.core.NewCookie; +import javax.ws.rs.core.Response.Status.Family; + +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.WebResource; + +import net.roboconf.dm.rest.client.WsClient; +import net.roboconf.dm.rest.client.exceptions.DebugWsException; +import net.roboconf.dm.rest.commons.UrlConstants; + +/** + * @author Vincent Zurczak - Linagora + */ +public class AuthenticationWsDelegate { + + private final WebResource resource; + private final WsClient wsClient; + private final Logger logger; + + + /** + * Constructor. + * @param resource a web resource + * @param the WS client + */ + public AuthenticationWsDelegate( WebResource resource, WsClient wsClient ) { + this.resource = resource; + this.wsClient = wsClient; + this.logger = Logger.getLogger( getClass().getName()); + } + + + /** + * Logs in with a user name and a password. + * @param username a user name + * @param password a password + * @return a session ID, or null if login failed + * @throws DebugWsException + */ + public String login( String username, String password ) throws DebugWsException { + + this.logger.finer( "Logging in as " + username ); + WebResource path = this.resource.path( UrlConstants.AUTHENTICATION ).path( "e" ); + ClientResponse response = path + .header( "u", username ) + .header( "p", password ) + .post( ClientResponse.class ); + + if( Family.SUCCESSFUL != response.getStatusInfo().getFamily()) { + String value = response.getEntity( String.class ); + this.logger.finer( response.getStatusInfo() + ": " + value ); + throw new DebugWsException( response.getStatusInfo().getStatusCode(), value ); + } + + // Get the session ID from the cookie that should have been returned + String sessionId = null; + List cookies = response.getCookies(); + if( cookies != null ) { + for( NewCookie cookie : cookies ) { + if( UrlConstants.SESSION_ID.equals( cookie.getName())) { + sessionId = cookie.getValue(); + break; + } + } + } + + // Set the session ID + this.wsClient.setSessionId( sessionId ); + this.logger.finer( "Session ID: " + sessionId ); + + return sessionId; + } + + + /** + * Terminates a session. + * @param sessionId a session ID + * @throws DebugWsException + */ + public void logout( String sessionId ) throws DebugWsException { + + this.logger.finer( "Logging out... Session ID = " + sessionId ); + WebResource path = this.resource.path( UrlConstants.AUTHENTICATION ).path( "s" ); + ClientResponse response = this.wsClient.createBuilder( path ).get( ClientResponse.class ); + this.logger.finer( String.valueOf( response.getStatusInfo())); + this.wsClient.setSessionId( null ); + } +} diff --git a/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/DebugWsDelegate.java b/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/DebugWsDelegate.java index f098d37e..beca9995 100644 --- a/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/DebugWsDelegate.java +++ b/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/DebugWsDelegate.java @@ -31,14 +31,15 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response.Status.Family; -import net.roboconf.dm.rest.client.exceptions.DebugWsException; -import net.roboconf.dm.rest.commons.Diagnostic; -import net.roboconf.dm.rest.commons.UrlConstants; - import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.GenericType; import com.sun.jersey.api.client.WebResource; +import net.roboconf.dm.rest.client.WsClient; +import net.roboconf.dm.rest.client.exceptions.DebugWsException; +import net.roboconf.dm.rest.commons.Diagnostic; +import net.roboconf.dm.rest.commons.UrlConstants; + /** * @author Vincent Zurczak - Linagora */ @@ -46,15 +47,17 @@ public class DebugWsDelegate { private final WebResource resource; private final Logger logger; + private final WsClient wsClient; /** * Constructor. - * * @param resource a web resource + * @param the WS client */ - public DebugWsDelegate( WebResource resource ) { + public DebugWsDelegate( WebResource resource, WsClient wsClient ) { this.resource = resource; + this.wsClient = wsClient; this.logger = Logger.getLogger( getClass().getName()); } @@ -73,7 +76,7 @@ public String checkMessagingConnectionForTheDm( String message ) if( message != null ) path = path.queryParam( "message", message ); - ClientResponse response = path.get( ClientResponse.class ); + ClientResponse response = this.wsClient.createBuilder( path ).get( ClientResponse.class ); if( Family.SUCCESSFUL != response.getStatusInfo().getFamily()) { String value = response.getEntity( String.class ); this.logger.finer( response.getStatusInfo() + ": " + value ); @@ -105,7 +108,7 @@ public String checkMessagingConnectionWithAgent( String applicationName, String if( message != null ) path = path.queryParam( "message", message ); - ClientResponse response = path.get( ClientResponse.class ); + ClientResponse response = this.wsClient.createBuilder( path ).get( ClientResponse.class ); if( Family.SUCCESSFUL != response.getStatusInfo().getFamily()) { String value = response.getEntity( String.class ); this.logger.finer( response.getStatusInfo() + ": " + value ); @@ -131,7 +134,10 @@ public Diagnostic diagnoseInstance( String applicationName, String instancePath path = path.queryParam( "application-name", applicationName ); path = path.queryParam( "instance-path", instancePath ); - ClientResponse response = path.accept( MediaType.APPLICATION_JSON ).get( ClientResponse.class ); + ClientResponse response = this.wsClient.createBuilder( path ) + .accept( MediaType.APPLICATION_JSON ) + .get( ClientResponse.class ); + if( Family.SUCCESSFUL != response.getStatusInfo().getFamily()) { String value = response.getEntity( String.class ); this.logger.finer( response.getStatusInfo() + ": " + value ); @@ -153,7 +159,10 @@ public List diagnoseApplication( String applicationName ) { WebResource path = this.resource.path( UrlConstants.DEBUG ).path( "diagnose-application" ); path = path.queryParam( "application-name", applicationName ); - List result = path.accept( MediaType.APPLICATION_JSON ).get( new GenericType> () {}); + List result = this.wsClient.createBuilder( path ) + .accept( MediaType.APPLICATION_JSON ) + .get( new GenericType> () {}); + if( result == null ) this.logger.finer( "No diagnostic was returned for application " + applicationName ); diff --git a/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/ManagementWsDelegate.java b/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/ManagementWsDelegate.java index 666e368c..9949ead8 100644 --- a/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/ManagementWsDelegate.java +++ b/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/ManagementWsDelegate.java @@ -43,6 +43,7 @@ import net.roboconf.core.model.beans.Application; import net.roboconf.core.model.beans.ApplicationTemplate; +import net.roboconf.dm.rest.client.WsClient; import net.roboconf.dm.rest.client.exceptions.ManagementWsException; import net.roboconf.dm.rest.commons.UrlConstants; @@ -53,14 +54,17 @@ public class ManagementWsDelegate { private final WebResource resource; private final Logger logger; + private final WsClient wsClient; /** * Constructor. * @param resource a web resource + * @param the WS client */ - public ManagementWsDelegate( WebResource resource ) { + public ManagementWsDelegate( WebResource resource, WsClient wsClient ) { this.resource = resource; + this.wsClient = wsClient; this.logger = Logger.getLogger( getClass().getName()); } @@ -86,8 +90,8 @@ public void uploadZippedApplicationTemplate( File applicationFile ) throws Manag FormDataMultiPart part = new FormDataMultiPart(); part.bodyPart( new FileDataBodyPart( "file", applicationFile, MediaType.APPLICATION_OCTET_STREAM_TYPE)); - ClientResponse response = this.resource - .path( UrlConstants.APPLICATIONS ).path( "templates" ) + WebResource path = this.resource.path( UrlConstants.APPLICATIONS ).path( "templates" ); + ClientResponse response = this.wsClient.createBuilder( path ) .type( MediaType.MULTIPART_FORM_DATA_TYPE ) .post( ClientResponse.class, part ); @@ -113,7 +117,10 @@ public void loadUnzippedApplicationTemplate( String localFilePath ) throws Manag if( localFilePath != null ) path = path.queryParam( "local-file-path", localFilePath ); - ClientResponse response = path.type( MediaType.APPLICATION_JSON ).post( ClientResponse.class ); + ClientResponse response = this.wsClient.createBuilder( path ) + .type( MediaType.APPLICATION_JSON ) + .post( ClientResponse.class ); + if( Family.SUCCESSFUL != response.getStatusInfo().getFamily()) { String value = response.getEntity( String.class ); this.logger.finer( response.getStatusInfo() + ": " + value ); @@ -136,7 +143,10 @@ public void loadZippedApplicationTemplate( String url ) throws ManagementWsExcep if( url != null ) path = path.queryParam( "url", url ); - ClientResponse response = path.type( MediaType.APPLICATION_JSON ).post( ClientResponse.class ); + ClientResponse response = this.wsClient.createBuilder( path ) + .type( MediaType.APPLICATION_JSON ) + .post( ClientResponse.class ); + if( Family.SUCCESSFUL != response.getStatusInfo().getFamily()) { String value = response.getEntity( String.class ); this.logger.finer( response.getStatusInfo() + ": " + value ); @@ -195,7 +205,7 @@ public List listApplicationTemplates( String exactName, Str if( exactQualifier != null ) path = path.queryParam( "qualifier", exactQualifier ); - List result = path + List result = this.wsClient.createBuilder( path ) .accept( MediaType.APPLICATION_JSON ) .get( new GenericType> () {}); @@ -231,11 +241,11 @@ public List listApplicationTemplates() throws ManagementWsE public void deleteApplicationTemplate( String templateName, String templateQualifier ) throws ManagementWsException { this.logger.finer( "Removing application template " + templateName + "..." ); - ClientResponse response = this.resource + WebResource path = this.resource .path( UrlConstants.APPLICATIONS ).path( "templates" ) - .path( templateName ).path( templateQualifier ) - .delete( ClientResponse.class ); + .path( templateName ).path( templateQualifier ); + ClientResponse response = this.wsClient.createBuilder( path ).delete( ClientResponse.class ); String text = response.getEntity( String.class ); this.logger.finer( text ); if( Family.SUCCESSFUL != response.getStatusInfo().getFamily()) @@ -261,8 +271,8 @@ public Application createApplication( String applicationName, String templateNam ApplicationTemplate tpl = new ApplicationTemplate( templateName ).qualifier( templateQualifier ); Application app = new Application( applicationName, tpl ); - ClientResponse response = this.resource - .path( UrlConstants.APPLICATIONS ) + WebResource path = this.resource.path( UrlConstants.APPLICATIONS ); + ClientResponse response = this.wsClient.createBuilder( path ) .type( MediaType.APPLICATION_JSON ) .post( ClientResponse.class, app ); @@ -291,7 +301,7 @@ public List listApplications( String exactName ) throws ManagementW if( exactName != null ) path = path.queryParam( "name", exactName ); - List result = path + List result = this.wsClient.createBuilder( path ) .accept( MediaType.APPLICATION_JSON ) .get( new GenericType> () {}); @@ -326,10 +336,12 @@ public List listApplications() throws ManagementWsException { public void shutdownApplication( String applicationName ) throws ManagementWsException { this.logger.finer( "Removing application " + applicationName + "..." ); - ClientResponse response = this.resource - .path( UrlConstants.APPLICATIONS ).path( applicationName ).path( "shutdown" ) - .post( ClientResponse.class ); + WebResource path = this.resource + .path( UrlConstants.APPLICATIONS ) + .path( applicationName ) + .path( "shutdown" ); + ClientResponse response = this.wsClient.createBuilder( path ).post( ClientResponse.class ); String text = response.getEntity( String.class ); this.logger.finer( text ); if( Family.SUCCESSFUL != response.getStatusInfo().getFamily()) @@ -345,9 +357,8 @@ public void shutdownApplication( String applicationName ) throws ManagementWsExc public void deleteApplication( String applicationName ) throws ManagementWsException { this.logger.finer( "Removing application " + applicationName + "..." ); - ClientResponse response = this.resource - .path( UrlConstants.APPLICATIONS ).path( applicationName ) - .delete( ClientResponse.class ); + WebResource path = this.resource.path( UrlConstants.APPLICATIONS ).path( applicationName ); + ClientResponse response = this.wsClient.createBuilder( path ).delete( ClientResponse.class ); String text = response.getEntity( String.class ); this.logger.finer( text ); diff --git a/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/PreferencesWsDelegate.java b/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/PreferencesWsDelegate.java index 73bd7404..58096412 100644 --- a/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/PreferencesWsDelegate.java +++ b/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/PreferencesWsDelegate.java @@ -35,6 +35,7 @@ import com.sun.jersey.api.client.WebResource; import net.roboconf.core.model.runtime.Preference; +import net.roboconf.dm.rest.client.WsClient; import net.roboconf.dm.rest.commons.UrlConstants; /** @@ -44,14 +45,17 @@ public class PreferencesWsDelegate { private final WebResource resource; private final Logger logger; + private final WsClient wsClient; /** * Constructor. * @param resource a web resource + * @param the WS client */ - public PreferencesWsDelegate( WebResource resource ) { + public PreferencesWsDelegate( WebResource resource, WsClient wsClient ) { this.resource = resource; + this.wsClient = wsClient; this.logger = Logger.getLogger( getClass().getName()); } @@ -64,7 +68,7 @@ public List listPreferences() { this.logger.finer( "Getting all the preferences." ); WebResource path = this.resource.path( UrlConstants.PREFERENCES ); - List result = path + List result = this.wsClient.createBuilder( path ) .accept( MediaType.APPLICATION_JSON ) .get( new GenericType> () {}); diff --git a/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/SchedulerWsDelegate.java b/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/SchedulerWsDelegate.java index b26b06a5..25a6dc46 100644 --- a/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/SchedulerWsDelegate.java +++ b/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/SchedulerWsDelegate.java @@ -37,6 +37,7 @@ import com.sun.jersey.api.client.WebResource; import net.roboconf.core.model.runtime.ScheduledJob; +import net.roboconf.dm.rest.client.WsClient; import net.roboconf.dm.rest.client.exceptions.SchedulerWsException; import net.roboconf.dm.rest.commons.UrlConstants; import net.roboconf.dm.rest.commons.json.StringWrapper; @@ -48,14 +49,17 @@ public class SchedulerWsDelegate { private final WebResource resource; private final Logger logger; + private final WsClient wsClient; /** * Constructor. * @param resource a web resource + * @param the WS client */ - public SchedulerWsDelegate( WebResource resource ) { + public SchedulerWsDelegate( WebResource resource, WsClient wsClient ) { this.resource = resource; + this.wsClient = wsClient; this.logger = Logger.getLogger( getClass().getName()); } @@ -98,7 +102,8 @@ public List listAllJobs( String appName, String cmdName ) { path = path.queryParam( "cmd-name", cmdName ); List result = - path.accept( MediaType.APPLICATION_JSON ) + this.wsClient.createBuilder( path ) + .accept( MediaType.APPLICATION_JSON ) .type( MediaType.APPLICATION_JSON ) .get( new GenericType> () {}); @@ -106,7 +111,7 @@ public List listAllJobs( String appName, String cmdName ) { this.logger.finer( result.size() + " jobs were found." ); } else { this.logger.finer( "No scheduled job was found." ); - result = new ArrayList( 0 ); + result = new ArrayList<>( 0 ); } return result; @@ -140,9 +145,11 @@ public String createOrUpdateJob( String jobId, String jobName, String appName, S path = path.queryParam( "cmd-name", cmdName ); path = path.queryParam( "cron", cron ); - ClientResponse response = path.accept( MediaType.APPLICATION_JSON ).post( ClientResponse.class ); - handleResponse( response ); + ClientResponse response = this.wsClient.createBuilder( path ) + .accept( MediaType.APPLICATION_JSON ) + .post( ClientResponse.class ); + handleResponse( response ); StringWrapper wrapper = response.getEntity( StringWrapper.class ); return wrapper.toString(); } @@ -158,7 +165,10 @@ public void deleteJob( String jobId ) throws SchedulerWsException { this.logger.finer( "Deleting scheduled job: " + jobId ); WebResource path = this.resource.path( UrlConstants.SCHEDULER ).path( jobId ); - ClientResponse response = path.accept( MediaType.APPLICATION_JSON ).delete( ClientResponse.class ); + ClientResponse response = this.wsClient.createBuilder( path ) + .accept( MediaType.APPLICATION_JSON ) + .delete( ClientResponse.class ); + handleResponse( response ); } @@ -173,9 +183,11 @@ public ScheduledJob getJobProperties( String jobId ) throws SchedulerWsException this.logger.finer( "Getting the properties of a scheduled job: " + jobId ); WebResource path = this.resource.path( UrlConstants.SCHEDULER ).path( jobId ); - ClientResponse response = path.accept( MediaType.APPLICATION_JSON ).get( ClientResponse.class ); - handleResponse( response ); + ClientResponse response = this.wsClient.createBuilder( path ) + .accept( MediaType.APPLICATION_JSON ) + .get( ClientResponse.class ); + handleResponse( response ); return response.getEntity( ScheduledJob.class ); } diff --git a/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/TargetWsDelegate.java b/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/TargetWsDelegate.java index 647c12e1..30ae6539 100644 --- a/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/TargetWsDelegate.java +++ b/miscellaneous/roboconf-dm-rest-client/src/main/java/net/roboconf/dm/rest/client/delegates/TargetWsDelegate.java @@ -39,6 +39,7 @@ import net.roboconf.core.model.beans.AbstractApplication; import net.roboconf.core.model.beans.ApplicationTemplate; import net.roboconf.core.model.runtime.TargetWrapperDescriptor; +import net.roboconf.dm.rest.client.WsClient; import net.roboconf.dm.rest.client.exceptions.TargetWsException; import net.roboconf.dm.rest.commons.UrlConstants; @@ -50,14 +51,17 @@ public class TargetWsDelegate { private final WebResource resource; private final Logger logger; + private final WsClient wsClient; /** * Constructor. * @param resource a web resource + * @param the WS client */ - public TargetWsDelegate( WebResource resource ) { + public TargetWsDelegate( WebResource resource, WsClient wsClient ) { this.resource = resource; + this.wsClient = wsClient; this.logger = Logger.getLogger( getClass().getName()); } @@ -71,7 +75,8 @@ public List listAllTargets() { this.logger.finer( "Listing all the available targets." ); WebResource path = this.resource.path( UrlConstants.TARGETS ); List result = - path.accept( MediaType.APPLICATION_JSON ) + this.wsClient.createBuilder( path ) + .accept( MediaType.APPLICATION_JSON ) .type( MediaType.APPLICATION_JSON ) .get( new GenericType> () {}); @@ -95,7 +100,7 @@ public String createTarget( String targetContent ) throws TargetWsException { this.logger.finer( "Creating a new target." ); WebResource path = this.resource.path( UrlConstants.TARGETS ); - ClientResponse response = path.post( ClientResponse.class, targetContent ); + ClientResponse response = this.wsClient.createBuilder( path ).post( ClientResponse.class, targetContent ); handleResponse( response ); return response.getEntity( String.class ); @@ -112,7 +117,10 @@ public void deleteTarget( String targetId ) throws TargetWsException { this.logger.finer( "Deleting target " + targetId ); WebResource path = this.resource.path( UrlConstants.TARGETS ).path( targetId ); - ClientResponse response = path.accept( MediaType.APPLICATION_JSON ).delete( ClientResponse.class ); + ClientResponse response = this.wsClient.createBuilder( path ) + .accept( MediaType.APPLICATION_JSON ) + .delete( ClientResponse.class ); + handleResponse( response ); } @@ -144,7 +152,10 @@ public void associateTarget( AbstractApplication app, String instancePathOrCompo if( app instanceof ApplicationTemplate ) path = path.queryParam( "qualifier", ((ApplicationTemplate) app).getQualifier()); - ClientResponse response = path.accept( MediaType.APPLICATION_JSON ).post( ClientResponse.class ); + ClientResponse response = this.wsClient.createBuilder( path ) + .accept( MediaType.APPLICATION_JSON ) + .post( ClientResponse.class ); + handleResponse( response ); } diff --git a/miscellaneous/roboconf-dm-rest-client/src/test/java/net/roboconf/dm/rest/client/delegates/AuthenticationWsDelegateTest.java b/miscellaneous/roboconf-dm-rest-client/src/test/java/net/roboconf/dm/rest/client/delegates/AuthenticationWsDelegateTest.java new file mode 100644 index 00000000..b6f6fe6d --- /dev/null +++ b/miscellaneous/roboconf-dm-rest-client/src/test/java/net/roboconf/dm/rest/client/delegates/AuthenticationWsDelegateTest.java @@ -0,0 +1,105 @@ +/** + * Copyright 2014-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.client.delegates; + +import java.net.URI; + +import javax.security.auth.login.LoginException; +import javax.ws.rs.core.UriBuilder; + +import org.glassfish.grizzly.http.server.HttpServer; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import com.sun.jersey.api.container.grizzly2.GrizzlyServerFactory; + +import net.roboconf.dm.rest.client.WsClient; +import net.roboconf.dm.rest.client.exceptions.DebugWsException; +import net.roboconf.dm.rest.commons.security.AuthenticationManager; +import net.roboconf.dm.rest.commons.security.AuthenticationManager.IAuthService; +import net.roboconf.dm.rest.services.internal.RestApplication; + +/** + * @author Vincent Zurczak - Linagora + */ +public class AuthenticationWsDelegateTest { + + private static final String REST_URI = "http://localhost:8090"; + + private WsClient client; + private HttpServer httpServer; + private IAuthService authService; + + + @After + public void after() { + + if( this.httpServer != null ) + this.httpServer.stop(); + + if( this.client != null ) + this.client.destroy(); + } + + + @Before + public void before() throws Exception { + + URI uri = UriBuilder.fromUri( REST_URI ).build(); + RestApplication restApp = new RestApplication( null ); + + AuthenticationManager authenticationMngr = new AuthenticationManager( "whatever" ); + this.authService = Mockito.mock( IAuthService.class ); + authenticationMngr.setAuthService( this.authService ); + restApp.setAuthenticationManager( authenticationMngr ); + + this.httpServer = GrizzlyServerFactory.createHttpServer( uri, restApp ); + this.client = new WsClient( REST_URI ); + } + + + @Test( expected = DebugWsException.class ) + public void testLogin_failure() throws Exception { + + Mockito.doThrow( new LoginException( "for test" )).when( this.authService ).authenticate( "u", "p" ); + this.client.getAuthenticationWsDelegate().login( "u", "p" ); + } + + + @Test + public void testLogin_success() throws Exception { + + String sessionId = this.client.getAuthenticationWsDelegate().login( "u", "p" ); + Assert.assertNotNull( sessionId ); + this.client.getAuthenticationWsDelegate().logout( sessionId ); + + // Log out twice does not result in any error + this.client.getAuthenticationWsDelegate().logout( sessionId ); + } +} diff --git a/miscellaneous/roboconf-dm-rest-client/src/test/java/net/roboconf/dm/rest/client/delegates/ManagementWsDelegateTest.java b/miscellaneous/roboconf-dm-rest-client/src/test/java/net/roboconf/dm/rest/client/delegates/ManagementWsDelegateTest.java index 62121ad6..5a43b35e 100644 --- a/miscellaneous/roboconf-dm-rest-client/src/test/java/net/roboconf/dm/rest/client/delegates/ManagementWsDelegateTest.java +++ b/miscellaneous/roboconf-dm-rest-client/src/test/java/net/roboconf/dm/rest/client/delegates/ManagementWsDelegateTest.java @@ -152,6 +152,12 @@ public void testListApplications() throws Exception { apps = this.client.getManagementDelegate().listApplications( app.getName() + "0" ); Assert.assertNotNull( apps ); Assert.assertEquals( 0, apps.size()); + + // Get when a session ID was set + this.client.setSessionId( "whatever, filtering is not enabled during these tests" ); + apps = this.client.getManagementDelegate().listApplications(); + Assert.assertNotNull( apps ); + Assert.assertEquals( 1, apps.size()); } diff --git a/miscellaneous/roboconf-integration-tests-dm-with-agents-in-memory/src/test/java/net/roboconf/integration/tests/dm/with/agents/in/memory/RestSecuredServicesTest.java b/miscellaneous/roboconf-integration-tests-dm-with-agents-in-memory/src/test/java/net/roboconf/integration/tests/dm/with/agents/in/memory/RestSecuredServicesTest.java new file mode 100644 index 00000000..72897e5b --- /dev/null +++ b/miscellaneous/roboconf-integration-tests-dm-with-agents-in-memory/src/test/java/net/roboconf/integration/tests/dm/with/agents/in/memory/RestSecuredServicesTest.java @@ -0,0 +1,152 @@ +/** + * Copyright 2014-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.integration.tests.dm.with.agents.in.memory; + +import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.editConfigurationFilePut; + +import java.io.File; +import java.util.List; + +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Test; +import org.ops4j.pax.exam.ExamSystem; +import org.ops4j.pax.exam.Option; +import org.ops4j.pax.exam.TestContainer; +import org.ops4j.pax.exam.karaf.container.internal.KarafTestContainer; +import org.ops4j.pax.exam.spi.PaxExamRuntime; + +import net.roboconf.core.internal.tests.TestUtils; +import net.roboconf.core.model.beans.ApplicationTemplate; +import net.roboconf.dm.rest.client.WsClient; +import net.roboconf.integration.tests.commons.internal.ItUtils; +import net.roboconf.integration.tests.dm.with.agents.in.memory.probes.DmWithAgentInMemoryTest; +import net.roboconf.messaging.rabbitmq.internal.utils.RabbitMqTestUtils; + +/** + * This test verifies that a client can interact with the DM's REST services. + * @author Vincent Zurczak - Linagora + */ +public class RestSecuredServicesTest extends DmWithAgentInMemoryTest { + + private File karafDirectory; + + + @Override + public Option[] config() throws Exception { + + List