Repository: usergrid Updated Branches: refs/heads/1.x 14d6cdf97 -> bbc5e1a05
Simplify Central SSO by eliminating the external token end-point https://issues.apache.org/jira/browse/USERGRID-1267 Project: http://git-wip-us.apache.org/repos/asf/usergrid/repo Commit: http://git-wip-us.apache.org/repos/asf/usergrid/commit/bd544367 Tree: http://git-wip-us.apache.org/repos/asf/usergrid/tree/bd544367 Diff: http://git-wip-us.apache.org/repos/asf/usergrid/diff/bd544367 Branch: refs/heads/1.x Commit: bd544367ad6f5aa397fa3ab50b560ebab9588726 Parents: 14d6cdf Author: Dave Johnson <[email protected]> Authored: Tue Feb 16 12:42:59 2016 -0500 Committer: Dave Johnson <[email protected]> Committed: Tue Feb 16 12:42:59 2016 -0500 ---------------------------------------------------------------------- .../rest/management/ManagementResource.java | 291 +---------------- .../rest/management/ManagementResourceIT.java | 33 +- stack/services/pom.xml | 21 +- .../usergrid/management/ManagementService.java | 3 - .../cassandra/ManagementServiceImpl.java | 36 +-- .../tokens/cassandra/TokenServiceImpl.java | 317 +++++++++++++++++-- 6 files changed, 333 insertions(+), 368 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/usergrid/blob/bd544367/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 46d6d6b..d71f45d 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 @@ -104,30 +104,15 @@ public class ManagementResource extends AbstractContextResource { * * /management/users/<user-name>/login * /management/users/<user-name>/password - * + * */ @Autowired private ApplicationCreator applicationCreator; - @Autowired - MetricsFactory metricsFactory; - - 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"; public ManagementResource() { logger.info( "ManagementResource initialized" ); @@ -485,280 +470,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 = null; - - Timer processingTimer = metricsFactory.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" ).getTextValue(); - - // 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" ).getTextValue(); - String email = userNode.get( "email" ).getTextValue(); - 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 = metricsFactory.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.debug("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 = metricsFactory.getCounter( - ManagementResource.class, SSO_TOKENS_REJECTED ); - Counter tokensValidatedCounter = metricsFactory.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.resource( me ) - .type( 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 ); - } - - MultiThreadedHttpConnectionManager cm = new MultiThreadedHttpConnectionManager(); - HttpConnectionManagerParams cmParams = cm.getParams(); - cmParams.setMaxTotalConnections( poolSize ); - HttpClient httpClient = new HttpClient( cm ); - - // create Jersey Client using that HTTPClient and with configured timeouts - - 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 DefaultClientConfig(); - clientConfig.getFeatures().put( JSONConfiguration.FEATURE_POJO_MAPPING, Boolean.TRUE ); - clientConfig.getProperties().put( ClientConfig.PROPERTY_CONNECT_TIMEOUT, timeout ); // ms - clientConfig.getProperties().put( ClientConfig.PROPERTY_READ_TIMEOUT, readTimeout ); // ms - - ApacheHttpClientHandler handler = new ApacheHttpClientHandler( httpClient, clientConfig ); - jerseyClient = new ApacheHttpClient( handler ); - - } - } - - 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/bd544367/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 cec172d..3a535c4 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 @@ -676,38 +676,20 @@ public class ManagementResourceIT extends AbstractRestIT { .type( MediaType.APPLICATION_JSON_TYPE ) .post( props ); - // attempt to validate the token, must be valid + // TODO: how do we unit test SSO now that we have no external token end-point? - JsonNode validatedNode = resource().path( "/management/externaltoken" ) - .queryParam( "access_token", suToken ) // as superuser - .queryParam( "ext_access_token", accessToken ) - .queryParam( "ttl", "1000" ) - .get( JsonNode.class ); - String validatedAccessToken = validatedNode.get( "access_token" ).getTextValue(); - assertEquals( accessToken, validatedAccessToken ); + Map<String, String> payload = hashMap( "access_token", accessToken ); - // attempt to validate an invalid token, must fail - - try { - resource().path( "/management/externaltoken" ) - .queryParam( "access_token", suToken ) // as superuser - .queryParam( "ext_access_token", "rubbish_token") - .queryParam( "ttl", "1000" ) - .get( JsonNode.class ); - fail("Validation should have failed"); - } catch ( UniformInterfaceException actual ) { - assertEquals( 404, actual.getResponse().getStatus() ); - String errorMsg = actual.getResponse().getEntity( JsonNode.class ).get( "error_description" ).toString(); - logger.error( "ERROR: " + errorMsg ); - assertTrue( errorMsg.contains( "Cannot find Admin User" ) ); - } + JsonNode node = resource().path( "/management/me" ).accept( MediaType.APPLICATION_JSON ) + .type( MediaType.APPLICATION_JSON_TYPE ).post( JsonNode.class, payload ); + logNode( node ); + String token = node.get( "access_token" ).getTextValue(); + assertNotNull( token ); // TODO: how do we test the create new user and organization case? - - // unset the Usergrid Central SSO URL so it does not interfere with other tests props.put( USERGRID_CENTRAL_URL, "" ); @@ -716,7 +698,6 @@ public class ManagementResourceIT extends AbstractRestIT { .accept( MediaType.APPLICATION_JSON ) .type( MediaType.APPLICATION_JSON_TYPE ) .post( props ); - } http://git-wip-us.apache.org/repos/asf/usergrid/blob/bd544367/stack/services/pom.xml ---------------------------------------------------------------------- diff --git a/stack/services/pom.xml b/stack/services/pom.xml index 75fe4c3..f6edde5 100644 --- a/stack/services/pom.xml +++ b/stack/services/pom.xml @@ -463,7 +463,7 @@ <!-- Testing and Logging Dependencies --> <dependency> <!-- - Do not remove this slf4j-api dependency remove even though pulled + Do not remove this slf4j-api dependency remove even though pulled in transitively. If not present IntelliJ IDEA wigs out. --> <groupId>org.slf4j</groupId> @@ -565,6 +565,25 @@ <scope>test</scope> </dependency> + <!-- needed for central ssso in TokenServiceImpl --> + <dependency> + <groupId>com.sun.jersey</groupId> + <artifactId>jersey-client</artifactId> + </dependency> + + <!-- 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>com.sun.jersey.contribs</groupId> + <artifactId>jersey-apache-client</artifactId> + <version>1.19</version> + </dependency> </dependencies> </project> http://git-wip-us.apache.org/repos/asf/usergrid/blob/bd544367/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 0e0657c..4711a95 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 @@ -127,9 +127,6 @@ public interface ManagementService { public String getActivationTokenForOrganization( UUID organizationId, long ttl ) throws Exception; - /** Import an Admin User token generated by some other system */ - public void importTokenForAdminUser( UUID userId, String token, long ttl ) throws Exception; - public ServiceResults getAdminUserActivities( UserInfo user ) throws Exception; public ServiceResults getAdminUserActivity( UserInfo user ) throws Exception; http://git-wip-us.apache.org/repos/asf/usergrid/blob/bd544367/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 b5eaf25..5f95dea 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 @@ -1416,21 +1416,13 @@ public class ManagementServiceImpl implements ManagementService { @Override public String getAccessTokenForAdminUser( UUID userId, long duration ) throws Exception { - return getTokenForPrincipal( ACCESS, null, MANAGEMENT_APPLICATION_ID, 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, MANAGEMENT_APPLICATION_ID ), null, ttl ); - } - - - /* + /* * (non-Javadoc) - * + * * @see * org.apache.usergrid.management.ManagementService#revokeAccessTokensForAdminUser * (java.util.UUID) @@ -1606,10 +1598,10 @@ public class ManagementServiceImpl implements ManagementService { @Override public void removeAdminUserFromOrganization( UUID userId, UUID organizationId ) throws Exception { - removeAdminUserFromOrganization( userId, organizationId, false ); + removeAdminUserFromOrganization( userId, organizationId, false ); } - + @Override public void removeAdminUserFromOrganization( UUID userId, UUID organizationId, boolean force ) throws Exception { @@ -1620,10 +1612,10 @@ public class ManagementServiceImpl implements ManagementService { EntityManager em = emf.getEntityManager( MANAGEMENT_APPLICATION_ID ); try { - int size = em.getCollection( new SimpleEntityRef( Group.ENTITY_TYPE, organizationId ), + int size = em.getCollection( new SimpleEntityRef( Group.ENTITY_TYPE, organizationId ), "users", null, 2, Level.IDS, false ).size(); - - if ( !force && size <= 1 ) { + + if ( !force && size <= 1 ) { throw new Exception(); } } @@ -1772,7 +1764,7 @@ public class ManagementServiceImpl implements ManagementService { /** - * Remove application from an organization. + * Remove application from an organization. */ @Override public void removeOrganizationApplication( UUID organizationId, UUID applicationId ) throws Exception { @@ -1781,9 +1773,9 @@ public class ManagementServiceImpl implements ManagementService { } EntityManager em = emf.getEntityManager( MANAGEMENT_APPLICATION_ID ); - em.deleteConnection( new ConnectionRefImpl( - "group", // String connectingEntityType - organizationId, // UUID connectingEntityId + em.deleteConnection( new ConnectionRefImpl( + "group", // String connectingEntityType + organizationId, // UUID connectingEntityId "owns", // String connectionType APPLICATION_INFO, // String connectedEntityType applicationId // UUID connectedEntityId @@ -2446,7 +2438,7 @@ public class ManagementServiceImpl implements ManagementService { /* * (non-Javadoc) - * + * * @see * org.apache.usergrid.management.ManagementService#revokeAccessTokensForAappUser * (java.util.UUID, java.util.UUID) @@ -2823,7 +2815,7 @@ public class ManagementServiceImpl implements ManagementService { /* * (non-Javadoc) - * + * * @see * org.apache.usergrid.management.ManagementService#setOrganizationProps(java.util * .UUID, java.util.Map) @@ -2846,7 +2838,7 @@ public class ManagementServiceImpl implements ManagementService { /* * (non-Javadoc) - * + * * @see * org.apache.usergrid.management.ManagementService#getOrganizationProps(java.util * .UUID) http://git-wip-us.apache.org/repos/asf/usergrid/blob/bd544367/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 7ea2af3..970e205 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,17 +17,29 @@ package org.apache.usergrid.security.tokens.cassandra; -import java.nio.ByteBuffer; -import java.util.*; - -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 com.sun.jersey.api.client.Client; +import com.sun.jersey.api.client.config.ClientConfig; +import com.sun.jersey.api.client.config.DefaultClientConfig; +import com.sun.jersey.api.json.JSONConfiguration; +import com.sun.jersey.client.apache.ApacheHttpClient; +import com.sun.jersey.client.apache.ApacheHttpClientHandler; +import me.prettyprint.hector.api.Keyspace; +import me.prettyprint.hector.api.beans.HColumn; +import me.prettyprint.hector.api.mutation.Mutator; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager; +import org.apache.commons.httpclient.params.HttpConnectionManagerParams; +import org.apache.commons.lang.RandomStringUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.usergrid.exception.NotImplementedException; +import org.apache.usergrid.management.*; +import org.apache.usergrid.metrics.MetricsFactory; import org.apache.usergrid.persistence.EntityManagerFactory; import org.apache.usergrid.persistence.cassandra.CassandraService; 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; @@ -38,13 +50,18 @@ import org.apache.usergrid.security.tokens.exceptions.ExpiredTokenException; import org.apache.usergrid.security.tokens.exceptions.InvalidTokenException; import org.apache.usergrid.utils.JsonUtils; import org.apache.usergrid.utils.UUIDUtils; +import org.codehaus.jackson.JsonNode; +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.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; @@ -53,20 +70,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 { @@ -288,14 +298,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 @@ -491,7 +503,7 @@ public class TokenServiceImpl implements TokenService { /* * write to the PRINCIPAL+TOKEN The format is as follow - * + * * appid+principalId+principalType :{ tokenuuid: 0x00} */ @@ -674,4 +686,257 @@ 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 + MetricsFactory metricsFactory; + + @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, CassandraService.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.resource( me ) + .type( 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 ); + } + + MultiThreadedHttpConnectionManager cm = new MultiThreadedHttpConnectionManager(); + HttpConnectionManagerParams cmParams = cm.getParams(); + cmParams.setMaxTotalConnections( poolSize ); + HttpClient httpClient = new HttpClient( cm ); + + // create Jersey Client using that HTTPClient and with configured timeouts + + 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 DefaultClientConfig(); + clientConfig.getFeatures().put( JSONConfiguration.FEATURE_POJO_MAPPING, Boolean.TRUE ); + clientConfig.getProperties().put( ClientConfig.PROPERTY_CONNECT_TIMEOUT, timeout ); // ms + clientConfig.getProperties().put( ClientConfig.PROPERTY_READ_TIMEOUT, readTimeout ); // ms + + ApacheHttpClientHandler handler = new ApacheHttpClientHandler( httpClient, clientConfig ); + jerseyClient = new ApacheHttpClient( handler ); + } + } + + return jerseyClient; + } + + }
