Repository: usergrid Updated Branches: refs/heads/master bf3d4263f -> 40faf7ec0
Cherry-pick the SSO simplifications from 1.x -> master (eliminates the external token validation end-point). Project: http://git-wip-us.apache.org/repos/asf/usergrid/repo Commit: http://git-wip-us.apache.org/repos/asf/usergrid/commit/7f93739e Tree: http://git-wip-us.apache.org/repos/asf/usergrid/tree/7f93739e Diff: http://git-wip-us.apache.org/repos/asf/usergrid/diff/7f93739e Branch: refs/heads/master Commit: 7f93739ee5c584685867847801cd73ad93bd0efc Parents: 5dee6c8 Author: Dave Johnson <[email protected]> Authored: Tue Feb 16 12:42:59 2016 -0500 Committer: Michael Russo <[email protected]> Committed: Wed Mar 2 12:33:40 2016 -0800 ---------------------------------------------------------------------- .../rest/management/ManagementResource.java | 308 ------------------ .../rest/management/ManagementResourceIT.java | 71 ---- stack/services/pom.xml | 17 + .../usergrid/management/ManagementService.java | 7 +- .../cassandra/ManagementServiceImpl.java | 22 +- .../tokens/cassandra/TokenServiceImpl.java | 322 +++++++++++++++++-- 6 files changed, 325 insertions(+), 422 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/usergrid/blob/7f93739e/stack/rest/src/main/java/org/apache/usergrid/rest/management/ManagementResource.java ---------------------------------------------------------------------- diff --git a/stack/rest/src/main/java/org/apache/usergrid/rest/management/ManagementResource.java b/stack/rest/src/main/java/org/apache/usergrid/rest/management/ManagementResource.java index 035cf69..f6aa001 100644 --- a/stack/rest/src/main/java/org/apache/usergrid/rest/management/ManagementResource.java +++ b/stack/rest/src/main/java/org/apache/usergrid/rest/management/ManagementResource.java @@ -17,39 +17,24 @@ package org.apache.usergrid.rest.management; -import com.codahale.metrics.Counter; -import com.codahale.metrics.Timer; -import com.fasterxml.jackson.databind.JsonNode; -import com.google.inject.Injector; import org.apache.amber.oauth2.common.error.OAuthError; import org.apache.amber.oauth2.common.exception.OAuthProblemException; import org.apache.amber.oauth2.common.message.OAuthResponse; import org.apache.amber.oauth2.common.message.types.GrantType; -import org.apache.commons.lang.RandomStringUtils; import org.apache.commons.lang.StringUtils; -import org.apache.http.impl.conn.PoolingClientConnectionManager; import org.apache.shiro.codec.Base64; -import org.apache.usergrid.exception.NotImplementedException; import org.apache.usergrid.management.ApplicationCreator; -import org.apache.usergrid.management.OrganizationInfo; -import org.apache.usergrid.management.OrganizationOwnerInfo; import org.apache.usergrid.management.UserInfo; import org.apache.usergrid.management.exceptions.DisabledAdminUserException; import org.apache.usergrid.management.exceptions.UnactivatedAdminUserException; import org.apache.usergrid.management.exceptions.UnconfirmedAdminUserException; import org.apache.usergrid.persistence.core.metrics.MetricsFactory; -import org.apache.usergrid.persistence.exceptions.EntityNotFoundException; import org.apache.usergrid.rest.AbstractContextResource; import org.apache.usergrid.rest.exceptions.RedirectionException; import org.apache.usergrid.rest.management.organizations.OrganizationsResource; import org.apache.usergrid.rest.management.users.UsersResource; import org.apache.usergrid.security.oauth.AccessInfo; import org.apache.usergrid.security.shiro.utils.SubjectUtils; -import org.glassfish.jersey.apache.connector.ApacheClientProperties; -import org.glassfish.jersey.apache.connector.ApacheConnectorProvider; -import org.glassfish.jersey.client.ClientConfig; -import org.glassfish.jersey.client.ClientProperties; -import org.glassfish.jersey.jackson.JacksonFeature; import org.glassfish.jersey.server.mvc.Viewable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,17 +43,12 @@ import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; import javax.ws.rs.*; -import javax.ws.rs.client.Client; -import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import java.net.URLEncoder; -import java.util.Collections; -import java.util.Iterator; import java.util.Map; -import java.util.UUID; import static javax.servlet.http.HttpServletResponse.*; import static javax.ws.rs.core.MediaType.*; @@ -106,25 +86,9 @@ public class ManagementResource extends AbstractContextResource { @Autowired private ApplicationCreator applicationCreator; - @Autowired - Injector injector; - - - private static Client jerseyClient = null; - - - // names for metrics to be collected - private static final String SSO_TOKENS_REJECTED = "sso.tokens_rejected"; - private static final String SSO_TOKENS_VALIDATED = "sso.tokens_validated"; - private static final String SSO_CREATED_LOCAL_ADMINS = "sso.created_local_admins"; - private static final String SSO_PROCESSING_TIME = "sso.processing_time"; - // usergrid configuration property names needed public static final String USERGRID_SYSADMIN_LOGIN_NAME = "usergrid.sysadmin.login.name"; public static final String USERGRID_CENTRAL_URL = "usergrid.central.url"; - public static final String CENTRAL_CONNECTION_POOL_SIZE = "usergrid.central.connection.pool.size"; - public static final String CENTRAL_CONNECTION_TIMEOUT = "usergrid.central.connection.timeout"; - public static final String CENTRAL_READ_TIMEOUT = "usergrid.central.read.timeout"; MetricsFactory metricsFactory = null; @@ -509,278 +473,6 @@ public class ManagementResource extends AbstractContextResource { /** - * <p> - * Allows call to validateExternalToken() (see below) with a POST of a JSON object. - * </p> - * - * @param ui Information about calling URI. - * @param json JSON object with fields: ext_access_token, ttl - * @param callback For JSONP support. - * @return Returns JSON object with access_token field. - * @throws Exception Returns 401 if access token cannot be validated - */ - @POST - @Path( "/externaltoken" ) - public Response validateExternalToken( - @Context UriInfo ui, - Map<String, Object> json, - @QueryParam( "callback" ) @DefaultValue( "" ) String callback ) throws Exception { - - if ( StringUtils.isEmpty( properties.getProperty( USERGRID_CENTRAL_URL ))) { - throw new NotImplementedException( "External Token Validation Service is not configured" ); - } - - Object extAccessTokenObj = json.get( "ext_access_token" ); - if ( extAccessTokenObj == null ) { - throw new IllegalArgumentException("ext_access_token must be specified"); - } - String extAccessToken = json.get("ext_access_token").toString(); - - Object ttlObj = json.get( "ttl" ); - if ( ttlObj == null ) { - throw new IllegalArgumentException("ttl must be specified"); - } - long ttl; - try { - ttl = Long.parseLong(ttlObj.toString()); - } catch ( NumberFormatException e ) { - throw new IllegalArgumentException("ttl must be specified as a long"); - } - - return validateExternalToken( ui, extAccessToken, ttl, callback ); - } - - - /** - * <p> - * Validates access token from other or "external" Usergrid system. - * Calls other system's /management/me endpoint to get the User - * associated with the access token. If user does not exist locally, - * then user and organizations will be created. If no user is returned - * from the other cluster, then this endpoint will return 401. - * </p> - * - * <p> Part of Usergrid Central SSO feature. - * See <a href="https://issues.apache.org/jira/browse/USERGRID-567">USERGRID-567</a> - * for details about Usergrid Central SSO. - * </p> - * - * @param ui Information about calling URI. - * @param extAccessToken Access token from external Usergrid system. - * @param ttl Time to live for token. - * @param callback For JSONP support. - * @return Returns JSON object with access_token field. - * @throws Exception Returns 401 if access token cannot be validated - */ - @GET - @Path( "/externaltoken" ) - public Response validateExternalToken( - @Context UriInfo ui, - @QueryParam( "ext_access_token" ) String extAccessToken, - @QueryParam( "ttl" ) @DefaultValue("-1") long ttl, - @QueryParam( "callback" ) @DefaultValue( "" ) String callback ) - throws Exception { - - - if ( StringUtils.isEmpty( properties.getProperty( USERGRID_CENTRAL_URL ))) { - throw new NotImplementedException( "External Token Validation Service is not configured" ); - } - - if ( extAccessToken == null ) { - throw new IllegalArgumentException("ext_access_token must be specified"); - } - - if ( ttl == -1 ) { - throw new IllegalArgumentException("ttl must be specified"); - } - AccessInfo accessInfo; - - Timer processingTimer = getMetricsFactory().getTimer( - ManagementResource.class, SSO_PROCESSING_TIME ); - - Timer.Context timerContext = processingTimer.time(); - - try { - // look up user via UG Central's /management/me endpoint. - - JsonNode accessInfoNode = getMeFromUgCentral( extAccessToken ); - - JsonNode userNode = accessInfoNode.get( "user" ); - String username = userNode.get( "username" ).textValue(); - - // if user does not exist locally then we need to fix that - - UserInfo userInfo = management.getAdminUserByUsername( username ); - UUID userId = userInfo == null ? null : userInfo.getUuid(); - - if ( userId == null ) { - - // create local user and and organizations they have on the central Usergrid instance - logger.info("User {} does not exist locally, creating", username ); - - String name = userNode.get( "name" ).textValue(); - String email = userNode.get( "email" ).textValue(); - String dummyPassword = RandomStringUtils.randomAlphanumeric( 40 ); - - JsonNode orgsNode = userNode.get( "organizations" ); - Iterator<String> fieldNames = orgsNode.fieldNames(); - - if ( !fieldNames.hasNext() ) { - // no organizations for user exist in response from central Usergrid SSO - // so create user's personal organization and use username as organization name - fieldNames = Collections.singletonList( username ).iterator(); - } - - // create user and any organizations that user is supposed to have - - while ( fieldNames.hasNext() ) { - - String orgName = fieldNames.next(); - - if ( userId == null ) { - - // haven't created user yet so do that now - OrganizationOwnerInfo ownerOrgInfo = management.createOwnerAndOrganization( - orgName, username, name, email, dummyPassword, true, false ); - - applicationCreator.createSampleFor( ownerOrgInfo.getOrganization() ); - - userId = ownerOrgInfo.getOwner().getUuid(); - userInfo = ownerOrgInfo.getOwner(); - - Counter createdAdminsCounter = getMetricsFactory().getCounter( - ManagementResource.class, SSO_CREATED_LOCAL_ADMINS ); - createdAdminsCounter.inc(); - - logger.info( "Created user {} and org {}", username, orgName ); - - } else { - - // already created user, so just create an org - final OrganizationInfo organization = - management.createOrganization( orgName, userInfo, true ); - - applicationCreator.createSampleFor( organization ); - - logger.info( "Created user {}'s other org {}", username, orgName ); - } - } - - } - - // store the external access_token as if it were one of our own - management.importTokenForAdminUser( userId, extAccessToken, ttl ); - - // success! return JSON object with access_token field - accessInfo = new AccessInfo() - .withExpiresIn( tokens.getMaxTokenAgeInSeconds( extAccessToken ) ) - .withAccessToken( extAccessToken ); - - } catch (Exception e) { - timerContext.stop(); - logger.error("Error validating external token", e); - throw e; - } - - final Response response = Response.status( SC_OK ) - .type( jsonMediaType( callback ) ).entity( accessInfo ).build(); - - timerContext.stop(); - - return response; - } - - /** - * Look up Admin User via UG Central's /management/me endpoint. - * - * @param extAccessToken Access token issued by UG Central of Admin User - * @return JsonNode representation of AccessInfo object for Admin User - * @throws EntityNotFoundException if access_token is not valid. - */ - private JsonNode getMeFromUgCentral( String extAccessToken ) throws EntityNotFoundException { - - // prepare to count tokens validated and rejected - - Counter tokensRejectedCounter = getMetricsFactory().getCounter( - ManagementResource.class, SSO_TOKENS_REJECTED ); - Counter tokensValidatedCounter = getMetricsFactory().getCounter( - ManagementResource.class, SSO_TOKENS_VALIDATED ); - - // create URL of central Usergrid's /management/me endpoint - - String externalUrl = properties.getProperty( USERGRID_CENTRAL_URL ).trim(); - - // be lenient about trailing slash - externalUrl = !externalUrl.endsWith( "/" ) ? externalUrl + "/" : externalUrl; - String me = externalUrl + "management/me?access_token=" + extAccessToken; - - // use our favorite HTTP client to GET /management/me - - Client client = getJerseyClient(); - final JsonNode accessInfoNode; - try { - accessInfoNode = client.target( me ).request() - .accept( MediaType.APPLICATION_JSON_TYPE ) - .get(JsonNode.class); - - tokensValidatedCounter.inc(); - - } catch ( Exception e ) { - // user not found 404 - tokensRejectedCounter.inc(); - String msg = "Cannot find Admin User associated with " + extAccessToken; - throw new EntityNotFoundException( msg, e ); - } - - return accessInfoNode; - } - - - private Client getJerseyClient() { - - if ( jerseyClient == null ) { - - synchronized ( this ) { - - // create HTTPClient and with configured connection pool - - int poolSize = 100; // connections - final String poolSizeStr = properties.getProperty( CENTRAL_CONNECTION_POOL_SIZE ); - if ( poolSizeStr != null ) { - poolSize = Integer.parseInt( poolSizeStr ); - } - - PoolingClientConnectionManager connectionManager = new PoolingClientConnectionManager(); - connectionManager.setMaxTotal(poolSize); - - int timeout = 20000; // ms - final String timeoutStr = properties.getProperty( CENTRAL_CONNECTION_TIMEOUT ); - if ( timeoutStr != null ) { - timeout = Integer.parseInt( timeoutStr ); - } - - int readTimeout = 20000; // ms - final String readTimeoutStr = properties.getProperty( CENTRAL_READ_TIMEOUT ); - if ( readTimeoutStr != null ) { - readTimeout = Integer.parseInt( readTimeoutStr ); - } - - ClientConfig clientConfig = new ClientConfig(); - clientConfig.register( new JacksonFeature() ); - clientConfig.property( ApacheClientProperties.CONNECTION_MANAGER, connectionManager ); - clientConfig.connectorProvider( new ApacheConnectorProvider() ); - - jerseyClient = ClientBuilder.newClient( clientConfig ); - jerseyClient.property( ClientProperties.CONNECT_TIMEOUT, timeout ); - jerseyClient.property( ClientProperties.READ_TIMEOUT, readTimeout ); - } - } - - return jerseyClient; - } - - - /** * Check that authentication is allowed. If external token validation is enabled (Central Usergrid SSO) * then only superusers should be allowed to login directly to this Usergrid instance. */ http://git-wip-us.apache.org/repos/asf/usergrid/blob/7f93739e/stack/rest/src/test/java/org/apache/usergrid/rest/management/ManagementResourceIT.java ---------------------------------------------------------------------- diff --git a/stack/rest/src/test/java/org/apache/usergrid/rest/management/ManagementResourceIT.java b/stack/rest/src/test/java/org/apache/usergrid/rest/management/ManagementResourceIT.java index c54fa0c..f29edcf 100644 --- a/stack/rest/src/test/java/org/apache/usergrid/rest/management/ManagementResourceIT.java +++ b/stack/rest/src/test/java/org/apache/usergrid/rest/management/ManagementResourceIT.java @@ -609,77 +609,6 @@ public class ManagementResourceIT extends AbstractRestIT { @Test - public void testValidateExternalToken() throws Exception { - - // create a new admin user, get access token - - String rand = RandomStringUtils.randomAlphanumeric(10); - final String username = "user_" + rand; - management().orgs().post( - new Organization( username, username, username+"@example.com", username, "password", null ) ); - - refreshIndex(); - - refreshIndex(); - QueryParameters queryParams = new QueryParameters() - .addParam( "username", username ) - .addParam( "password", "password" ) - .addParam( "grant_type", "password" ); - Token accessInfoNode = management.token().get(queryParams); - String accessToken = accessInfoNode.getAccessToken(); - - // set the Usergrid Central SSO URL because Tomcat port is dynamically assigned - - String suToken = clientSetup.getSuperuserToken().getAccessToken(); - Map<String, String> props = new HashMap<String, String>(); - props.put( USERGRID_CENTRAL_URL, getBaseURI().toURL().toExternalForm() ); - pathResource( "testproperties" ).post( props ); - - try { - - // attempt to validate the token, must be valid - queryParams = new QueryParameters() - .addParam( "ext_access_token", accessToken ) - .addParam( "ttl", "1000" ); - - Entity validatedNode = management.externaltoken().get( Entity.class, queryParams ); - String validatedAccessToken = validatedNode.get( "access_token" ).toString(); - assertEquals( accessToken, validatedAccessToken ); - - // attempt to validate an invalid token, must fail - - try { - queryParams = new QueryParameters() - .addParam( "access_token", suToken ) - .addParam( "ext_access_token", "rubbish_token" ) - .addParam( "ttl", "1000" ); - - validatedNode = management.externaltoken().get( Entity.class, queryParams ); - - fail( "Validation should have failed" ); - - } catch (ClientErrorException actual) { - assertEquals( 404, actual.getResponse().getStatus() ); - String errorMsg = actual.getResponse().readEntity( JsonNode.class ) - .get( "error_description" ).toString(); - logger.error( "ERROR: " + errorMsg ); - assertTrue( errorMsg.contains( "Cannot find Admin User" ) ); - } - - // TODO: how do we test the create new user and organization case? - - } finally { - - // unset the Usergrid Central SSO URL so it does not interfere with other tests - - props.put( USERGRID_CENTRAL_URL, "" ); - pathResource( "testproperties" ).post( props ); - } - - } - - - @Test public void testSuperuserOnlyWhenValidateExternalTokensEnabled() throws Exception { // create an org and an admin user http://git-wip-us.apache.org/repos/asf/usergrid/blob/7f93739e/stack/services/pom.xml ---------------------------------------------------------------------- diff --git a/stack/services/pom.xml b/stack/services/pom.xml index c791721..c80c58c 100644 --- a/stack/services/pom.xml +++ b/stack/services/pom.xml @@ -446,6 +446,23 @@ <artifactId>java-wns</artifactId> <version>1.3</version> </dependency> + + <!-- needed for central ssso in TokenServiceImpl --> + + <!-- needed for central ssso in TokenServiceImpl --> + <dependency> + <groupId>commons-httpclient</groupId> + <artifactId>commons-httpclient</artifactId> + <version>3.1</version> + </dependency> + + <!-- needed for central ssso in TokenServiceImpl --> + <dependency> + <groupId>org.glassfish.jersey.connectors</groupId> + <artifactId>jersey-apache-connector</artifactId> + <version>2.21</version> + </dependency> + </dependencies> <!-- http://git-wip-us.apache.org/repos/asf/usergrid/blob/7f93739e/stack/services/src/main/java/org/apache/usergrid/management/ManagementService.java ---------------------------------------------------------------------- diff --git a/stack/services/src/main/java/org/apache/usergrid/management/ManagementService.java b/stack/services/src/main/java/org/apache/usergrid/management/ManagementService.java index 2f2d10f..1d74ec3 100644 --- a/stack/services/src/main/java/org/apache/usergrid/management/ManagementService.java +++ b/stack/services/src/main/java/org/apache/usergrid/management/ManagementService.java @@ -139,9 +139,6 @@ public interface ManagementService { String getActivationTokenForOrganization( UUID organizationId, long ttl ) throws Exception; - /** Import an Admin User token generated by some other system */ - void importTokenForAdminUser( UUID userId, String token, long ttl ) throws Exception; - ServiceResults getAdminUserActivities( UserInfo user ) throws Exception; ServiceResults getAdminUserActivity( UserInfo user ) throws Exception; @@ -174,7 +171,9 @@ public interface ManagementService { ApplicationInfo getApplicationInfo( Identifier id ) throws Exception; - ApplicationInfo getApplicationInfoFromAccessToken( String token ) throws Exception; + void removeAdminUserFromOrganization( UUID userId, UUID organizationId, boolean force ) throws Exception; + + ApplicationInfo getApplicationInfoFromAccessToken( String token ) throws Exception; ServiceResults getApplicationMetadata( UUID applicationId ) throws Exception; http://git-wip-us.apache.org/repos/asf/usergrid/blob/7f93739e/stack/services/src/main/java/org/apache/usergrid/management/cassandra/ManagementServiceImpl.java ---------------------------------------------------------------------- diff --git a/stack/services/src/main/java/org/apache/usergrid/management/cassandra/ManagementServiceImpl.java b/stack/services/src/main/java/org/apache/usergrid/management/cassandra/ManagementServiceImpl.java index 622cecb..65f4744 100644 --- a/stack/services/src/main/java/org/apache/usergrid/management/cassandra/ManagementServiceImpl.java +++ b/stack/services/src/main/java/org/apache/usergrid/management/cassandra/ManagementServiceImpl.java @@ -1476,19 +1476,11 @@ public class ManagementServiceImpl implements ManagementService { @Override public String getAccessTokenForAdminUser( UUID userId, long duration ) throws Exception { - return getTokenForPrincipal( ACCESS, null, smf.getManagementAppId(), ADMIN_USER, userId, duration ); } - @Override - public void importTokenForAdminUser(UUID userId, String token, long ttl) throws Exception { - tokens.importToken( token, TokenCategory.ACCESS, null, - new AuthPrincipalInfo( ADMIN_USER, userId, smf.getManagementAppId() ), null, ttl ); - } - - - /* + /* * (non-Javadoc) * * @see @@ -1676,6 +1668,12 @@ public class ManagementServiceImpl implements ManagementService { @Override public void removeAdminUserFromOrganization( UUID userId, UUID organizationId ) throws Exception { + removeAdminUserFromOrganization( userId, organizationId, false ); + } + + + @Override + public void removeAdminUserFromOrganization( UUID userId, UUID organizationId, boolean force ) throws Exception { if ( ( userId == null ) || ( organizationId == null ) ) { return; @@ -1684,8 +1682,10 @@ public class ManagementServiceImpl implements ManagementService { EntityManager em = emf.getEntityManager( smf.getManagementAppId() ); try { - if ( em.getCollection( new SimpleEntityRef( Group.ENTITY_TYPE, organizationId ), "users", null, 2, - Level.IDS, false ).size() <= 1 ) { + int size = em.getCollection( new SimpleEntityRef( Group.ENTITY_TYPE, organizationId ), + "users", null, 2, Level.IDS, false ).size(); + + if ( !force && size <= 1 ) { throw new Exception(); } } http://git-wip-us.apache.org/repos/asf/usergrid/blob/7f93739e/stack/services/src/main/java/org/apache/usergrid/security/tokens/cassandra/TokenServiceImpl.java ---------------------------------------------------------------------- diff --git a/stack/services/src/main/java/org/apache/usergrid/security/tokens/cassandra/TokenServiceImpl.java b/stack/services/src/main/java/org/apache/usergrid/security/tokens/cassandra/TokenServiceImpl.java index c0729ff..8bbb60f 100644 --- a/stack/services/src/main/java/org/apache/usergrid/security/tokens/cassandra/TokenServiceImpl.java +++ b/stack/services/src/main/java/org/apache/usergrid/security/tokens/cassandra/TokenServiceImpl.java @@ -17,35 +17,55 @@ package org.apache.usergrid.security.tokens.cassandra; -import java.nio.ByteBuffer; -import java.util.*; - -import org.apache.usergrid.utils.ConversionUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.util.Assert; +import com.codahale.metrics.Counter; +import com.google.inject.Injector; +import me.prettyprint.hector.api.Keyspace; +import me.prettyprint.hector.api.beans.HColumn; +import me.prettyprint.hector.api.mutation.Mutator; +import org.apache.commons.lang.RandomStringUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.http.impl.conn.PoolingClientConnectionManager; +import org.apache.usergrid.corepersistence.CpEntityManagerFactory; +import org.apache.usergrid.corepersistence.util.CpNamingUtils; +import org.apache.usergrid.exception.NotImplementedException; +import org.apache.usergrid.management.*; import org.apache.usergrid.persistence.EntityManagerFactory; import org.apache.usergrid.persistence.cassandra.CassandraService; +import org.apache.usergrid.persistence.core.metrics.MetricsFactory; import org.apache.usergrid.persistence.entities.Application; +import org.apache.usergrid.persistence.exceptions.EntityNotFoundException; import org.apache.usergrid.security.AuthPrincipalInfo; import org.apache.usergrid.security.AuthPrincipalType; import org.apache.usergrid.security.tokens.TokenCategory; import org.apache.usergrid.security.tokens.TokenInfo; +import org.apache.usergrid.security.tokens.TokenInfo; import org.apache.usergrid.security.tokens.TokenService; import org.apache.usergrid.security.tokens.exceptions.BadTokenException; import org.apache.usergrid.security.tokens.exceptions.ExpiredTokenException; import org.apache.usergrid.security.tokens.exceptions.InvalidTokenException; +import org.apache.usergrid.services.ServiceManager; +import org.apache.usergrid.utils.ConversionUtils; import org.apache.usergrid.utils.JsonUtils; import org.apache.usergrid.utils.UUIDUtils; +import org.codehaus.jackson.JsonNode; +import org.glassfish.jersey.apache.connector.ApacheClientProperties; +import org.glassfish.jersey.apache.connector.ApacheConnectorProvider; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.jackson.JacksonFeature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.util.Assert; -import me.prettyprint.hector.api.Keyspace; -import me.prettyprint.hector.api.beans.HColumn; -import me.prettyprint.hector.api.mutation.Mutator; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.MediaType; +import java.nio.ByteBuffer; +import java.util.*; import static java.lang.System.currentTimeMillis; - import static me.prettyprint.hector.api.factory.HFactory.createColumn; import static me.prettyprint.hector.api.factory.HFactory.createMutator; import static org.apache.commons.codec.binary.Base64.decodeBase64; @@ -54,20 +74,13 @@ import static org.apache.commons.codec.digest.DigestUtils.sha; import static org.apache.usergrid.persistence.cassandra.CassandraPersistenceUtils.getColumnMap; import static org.apache.usergrid.persistence.cassandra.CassandraService.PRINCIPAL_TOKEN_CF; import static org.apache.usergrid.persistence.cassandra.CassandraService.TOKENS_CF; -import static org.apache.usergrid.security.tokens.TokenCategory.ACCESS; -import static org.apache.usergrid.security.tokens.TokenCategory.EMAIL; -import static org.apache.usergrid.security.tokens.TokenCategory.OFFLINE; -import static org.apache.usergrid.security.tokens.TokenCategory.REFRESH; -import static org.apache.usergrid.utils.ConversionUtils.HOLDER; -import static org.apache.usergrid.utils.ConversionUtils.bytebuffer; -import static org.apache.usergrid.utils.ConversionUtils.bytes; -import static org.apache.usergrid.utils.ConversionUtils.getLong; -import static org.apache.usergrid.utils.ConversionUtils.string; -import static org.apache.usergrid.utils.ConversionUtils.uuid; +import static org.apache.usergrid.persistence.cassandra.Serializers.*; +import static org.apache.usergrid.security.AuthPrincipalType.ADMIN_USER; +import static org.apache.usergrid.security.tokens.TokenCategory.*; +import static org.apache.usergrid.utils.ConversionUtils.*; import static org.apache.usergrid.utils.MapUtils.hasKeys; import static org.apache.usergrid.utils.MapUtils.hashMap; import static org.apache.usergrid.utils.UUIDUtils.getTimestampInMillis; -import static org.apache.usergrid.persistence.cassandra.Serializers.*; public class TokenServiceImpl implements TokenService { @@ -155,9 +168,10 @@ public class TokenServiceImpl implements TokenService { protected EntityManagerFactory emf; + protected MetricsFactory metricsFactory; - public TokenServiceImpl() { + public TokenServiceImpl() { } @@ -310,14 +324,16 @@ public class TokenServiceImpl implements TokenService { UUID uuid = getUUIDForToken( token ); + long ssoTtl = 1000000L; // TODO: property for this + if ( uuid == null ) { - return null; + return isSSOEnabled() ? validateExternalToken( token, ssoTtl ) : null; } TokenInfo tokenInfo = getTokenInfo( uuid ); if ( tokenInfo == null ) { - return null; + return isSSOEnabled() ? validateExternalToken( token, ssoTtl ) : null; } //update the token @@ -573,7 +589,7 @@ public class TokenServiceImpl implements TokenService { } - private UUID getUUIDForToken( String token ) throws ExpiredTokenException, BadTokenException { + private UUID getUUIDForToken(String token ) throws ExpiredTokenException, BadTokenException { TokenCategory tokenCategory = TokenCategory.getFromBase64String( token ); byte[] bytes = decodeBase64( token.substring( TokenCategory.BASE64_PREFIX_LENGTH ) ); UUID uuid = uuid( bytes ); @@ -651,6 +667,8 @@ public class TokenServiceImpl implements TokenService { @Autowired public void setEntityManagerFactory( EntityManagerFactory emf ) { this.emf = emf; + final Injector injector = ((CpEntityManagerFactory)emf).getApplicationContext().getBean( Injector.class ); + metricsFactory = injector.getInstance(MetricsFactory.class); } @@ -707,4 +725,252 @@ public class TokenServiceImpl implements TokenService { private static final int MAX_TTL = 20 * 365 * 24 * 60 * 60; + + + //------------------------------------------------------------------------------------------------------- + // + // Central SSO implementation + + public static final String USERGRID_CENTRAL_URL = "usergrid.central.url"; + public static final String CENTRAL_CONNECTION_POOL_SIZE = "usergrid.central.connection.pool.size"; + public static final String CENTRAL_CONNECTION_TIMEOUT = "usergrid.central.connection.timeout"; + public static final String CENTRAL_READ_TIMEOUT = "usergrid.central.read.timeout"; + + // names for metrics to be collected + private static final String SSO_TOKENS_REJECTED = "sso.tokens_rejected"; + private static final String SSO_TOKENS_VALIDATED = "sso.tokens_validated"; + private static final String SSO_CREATED_LOCAL_ADMINS = "sso.created_local_admins"; + private static final String SSO_PROCESSING_TIME = "sso.processing_time"; + + private static Client jerseyClient = null; + + @Autowired + private ApplicationCreator applicationCreator; + + @Autowired + protected ManagementService management; + + MetricsFactory getMetricsFactory() { + return metricsFactory; + } + + private boolean isSSOEnabled() { + return !StringUtils.isEmpty( properties.getProperty( USERGRID_CENTRAL_URL )); + } + + + /** + * <p> + * Validates access token from other or "external" Usergrid system. + * Calls other system's /management/me endpoint to get the User + * associated with the access token. If user does not exist locally, + * then user and organizations will be created. If no user is returned + * from the other cluster, then return null. + * </p> + * <p/> + * <p> Part of Usergrid Central SSO feature. + * See <a href="https://issues.apache.org/jira/browse/USERGRID-567">USERGRID-567</a> + * for details about Usergrid Central SSO. + * </p> + * + * @param extAccessToken Access token from external Usergrid system. + * @param ttl Time to live for token. + */ + public TokenInfo validateExternalToken(String extAccessToken, long ttl) throws Exception { + + TokenInfo tokenInfo = null; + + if (!isSSOEnabled()) { + throw new NotImplementedException( "External Token Validation Service not enabled" ); + } + + if (extAccessToken == null) { + throw new IllegalArgumentException( "ext_access_token must be specified" ); + } + + if (ttl == -1) { + throw new IllegalArgumentException( "ttl must be specified" ); + } + + com.codahale.metrics.Timer processingTimer = getMetricsFactory().getTimer( + TokenServiceImpl.class, SSO_PROCESSING_TIME ); + + com.codahale.metrics.Timer.Context timerContext = processingTimer.time(); + + try { + // look up user via UG Central's /management/me endpoint. + + JsonNode accessInfoNode = getMeFromUgCentral( extAccessToken ); + + JsonNode userNode = accessInfoNode.get( "user" ); + + String username = userNode.get( "username" ).asText(); + + // if user does not exist locally then we need to fix that + + UserInfo userInfo = management.getAdminUserByUsername( username ); + UUID userId = userInfo == null ? null : userInfo.getUuid(); + + if (userId == null) { + + // create local user and and organizations they have on the central Usergrid instance + logger.info( "User {} does not exist locally, creating", username ); + + String name = userNode.get( "name" ).asText(); + String email = userNode.get( "email" ).asText(); + String dummyPassword = RandomStringUtils.randomAlphanumeric( 40 ); + + JsonNode orgsNode = userNode.get( "organizations" ); + Iterator<String> fieldNames = orgsNode.getFieldNames(); + + if (!fieldNames.hasNext()) { + // no organizations for user exist in response from central Usergrid SSO + // so create user's personal organization and use username as organization name + fieldNames = Collections.singletonList( username ).iterator(); + } + + // create user and any organizations that user is supposed to have + + while (fieldNames.hasNext()) { + + String orgName = fieldNames.next(); + + if (userId == null) { + + // haven't created user yet so do that now + OrganizationOwnerInfo ownerOrgInfo = management.createOwnerAndOrganization( + orgName, username, name, email, dummyPassword, true, false ); + + applicationCreator.createSampleFor( ownerOrgInfo.getOrganization() ); + + userId = ownerOrgInfo.getOwner().getUuid(); + userInfo = ownerOrgInfo.getOwner(); + + Counter createdAdminsCounter = getMetricsFactory().getCounter( + TokenServiceImpl.class, SSO_CREATED_LOCAL_ADMINS ); + createdAdminsCounter.inc(); + + logger.info( "Created user {} and org {}", username, orgName ); + + } else { + + // already created user, so just create an org + final OrganizationInfo organization = + management.createOrganization( orgName, userInfo, true ); + + applicationCreator.createSampleFor( organization ); + + logger.info( "Created user {}'s other org {}", username, orgName ); + } + } + } + + // store the external access_token as if it were one of our own + importToken( extAccessToken, TokenCategory.ACCESS, null, new AuthPrincipalInfo( + ADMIN_USER, userId, CpNamingUtils.MANAGEMENT_APPLICATION_ID), null, ttl ); + + tokenInfo = getTokenInfo( extAccessToken ); + + } catch (Exception e) { + timerContext.stop(); + logger.debug( "Error validating external token", e ); + throw e; + } + + return tokenInfo; + } + + + /** + * Look up Admin User via UG Central's /management/me endpoint. + * + * @param extAccessToken Access token issued by UG Central of Admin User + * @return JsonNode representation of AccessInfo object for Admin User + * @throws EntityNotFoundException if access_token is not valid. + */ + private JsonNode getMeFromUgCentral( String extAccessToken ) throws EntityNotFoundException { + + // prepare to count tokens validated and rejected + + Counter tokensRejectedCounter = getMetricsFactory().getCounter( + TokenServiceImpl.class, SSO_TOKENS_REJECTED ); + Counter tokensValidatedCounter = getMetricsFactory().getCounter( + TokenServiceImpl.class, SSO_TOKENS_VALIDATED ); + + // create URL of central Usergrid's /management/me endpoint + + String externalUrl = properties.getProperty( USERGRID_CENTRAL_URL ).trim(); + + // be lenient about trailing slash + externalUrl = !externalUrl.endsWith( "/" ) ? externalUrl + "/" : externalUrl; + String me = externalUrl + "management/me?access_token=" + extAccessToken; + + // use our favorite HTTP client to GET /management/me + + Client client = getJerseyClient(); + final JsonNode accessInfoNode; + try { + accessInfoNode = client.target( me ).request() + .accept( MediaType.APPLICATION_JSON_TYPE ) + .get(JsonNode.class); + + tokensValidatedCounter.inc(); + + } catch ( Exception e ) { + // user not found 404 + tokensRejectedCounter.inc(); + String msg = "Cannot find Admin User associated with " + extAccessToken; + throw new EntityNotFoundException( msg, e ); + } + + return accessInfoNode; + } + + + + private Client getJerseyClient() { + + if ( jerseyClient == null ) { + + synchronized ( this ) { + + // create HTTPClient and with configured connection pool + + int poolSize = 100; // connections + final String poolSizeStr = properties.getProperty( CENTRAL_CONNECTION_POOL_SIZE ); + if ( poolSizeStr != null ) { + poolSize = Integer.parseInt( poolSizeStr ); + } + + PoolingClientConnectionManager connectionManager = new PoolingClientConnectionManager(); + connectionManager.setMaxTotal(poolSize); + + int timeout = 20000; // ms + final String timeoutStr = properties.getProperty( CENTRAL_CONNECTION_TIMEOUT ); + if ( timeoutStr != null ) { + timeout = Integer.parseInt( timeoutStr ); + } + + int readTimeout = 20000; // ms + final String readTimeoutStr = properties.getProperty( CENTRAL_READ_TIMEOUT ); + if ( readTimeoutStr != null ) { + readTimeout = Integer.parseInt( readTimeoutStr ); + } + + ClientConfig clientConfig = new ClientConfig(); + clientConfig.register( new JacksonFeature() ); + clientConfig.property( ApacheClientProperties.CONNECTION_MANAGER, connectionManager ); + clientConfig.connectorProvider( new ApacheConnectorProvider() ); + + jerseyClient = ClientBuilder.newClient( clientConfig ); + jerseyClient.property( ClientProperties.CONNECT_TIMEOUT, timeout ); + jerseyClient.property( ClientProperties.READ_TIMEOUT, readTimeout ); + } + } + + return jerseyClient; + + } + + }
