From d5c14088c08c1bd52560f720ee9451cd53a37d7f Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Thu, 27 Aug 2015 23:59:00 +0200 Subject: [PATCH 001/103] Use new plugin id for gradle flyway plugin. We now use the "org.flywaydb.flyway" gradle plugin in. The previously used "flyway" id caused a deprecation warning: ``` The 'flyway' plugin ID is deprecated and will be removed in Flyway 4.0. Update your build to use 'org.flywaydb.flyway' instead. ``` --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f54ffb0669a..243255c8c0b 100644 --- a/build.gradle +++ b/build.gradle @@ -198,7 +198,7 @@ dependencies { } } -apply plugin: 'flyway' +apply plugin: 'org.flywaydb.flyway' flyway { switch (databaseType()) { From a0d9b3953089c1b2ef55980718d64b78fc85235e Mon Sep 17 00:00:00 2001 From: Matthias Winzeler Date: Sat, 29 Aug 2015 11:40:38 +0000 Subject: [PATCH 002/103] possibility to specify 'addShadowUserOnLogin' in login.yaml --- .../uaa/login/saml/IdentityProviderConfigurator.java | 2 ++ uaa/src/main/resources/login.yml | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/IdentityProviderConfigurator.java b/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/IdentityProviderConfigurator.java index 12db1454f24..922cc1c71e0 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/IdentityProviderConfigurator.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/IdentityProviderConfigurator.java @@ -343,6 +343,7 @@ public void setIdentityProviders(Map> providers) { String linkText = (String)((Map)entry.getValue()).get("linkText"); String iconUrl = (String)((Map)entry.getValue()).get("iconUrl"); String zoneId = (String)((Map)entry.getValue()).get("zoneId"); + Boolean addShadowUserOnLogin = (Boolean)((Map)entry.getValue()).get("addShadowUserOnLogin"); List emailDomain = (List) saml.get("emailDomain"); IdentityProviderDefinition def = new IdentityProviderDefinition(); if (alias==null) { @@ -362,6 +363,7 @@ public void setIdentityProviders(Map> providers) { def.setIconUrl(iconUrl); def.setEmailDomain(emailDomain); def.setZoneId(StringUtils.hasText(zoneId) ? zoneId : IdentityZone.getUaa().getId()); + def.setAddShadowUserOnLogin(addShadowUserOnLogin==null?true:addShadowUserOnLogin); toBeFetchedProviders.add(def); } } diff --git a/uaa/src/main/resources/login.yml b/uaa/src/main/resources/login.yml index 9e9247522ae..7f28ed9b5bf 100644 --- a/uaa/src/main/resources/login.yml +++ b/uaa/src/main/resources/login.yml @@ -131,6 +131,7 @@ login: # showSamlLoginLink: true # linkText: 'Okta Preview 1' # iconUrl: 'http://link.to/icon.jpg' +# addShadowUserOnLogin: true # okta-local-2: # idpMetadata: | # MIICmTCCAgKgAwIBAgIGAUPATqmEMA0GCSqGSIb3DQEBBQUAMIGPMQswCQYDVQQGEwJVUzETMBEG @@ -150,12 +151,14 @@ login: # metadataTrustCheck: true # showSamlLoginLink: true # linkText: 'Okta Preview 2' +# addShadowUserOnLogin: true # vsphere.local: # idpMetadata: https://win2012-sso2.localdomain:7444/websso/SAML2/Metadata/vsphere.local # nameID: urn:oasis:names:tc:SAML:2.0:nameid-format:persistent # assertionConsumerIndex: 0 # showSamlLoginLink: true # linkText: 'Log in with vCenter SSO' +# addShadowUserOnLogin: true # openam-local: # idpMetadata: http://localhost:8081/openam/saml2/jsp/exportmetadata.jsp?entityid=http://localhost:8081/openam # nameID: urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress @@ -164,6 +167,7 @@ login: # signRequest: false # showSamlLoginLink: true # linkText: 'Log in with OpenAM' +# addShadowUserOnLogin: true #END SAML PROVIDERS authorize: @@ -191,4 +195,4 @@ LOGIN_SECRET: loginsecret # host: localhost # port: 2525 # user: -# password: \ No newline at end of file +# password: From 825a7636ef5513b77ec30850ffad23d4f6641f0a Mon Sep 17 00:00:00 2001 From: Matthias Winzeler Date: Sun, 30 Aug 2015 11:55:38 +0200 Subject: [PATCH 003/103] test cases for possibility to specify 'addShadowUserOnLogin' in login.yaml --- .../IdentityProviderConfiguratorTests.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) mode change 100644 => 100755 common/src/test/java/org/cloudfoundry/identity/uaa/login/saml/IdentityProviderConfiguratorTests.java diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/login/saml/IdentityProviderConfiguratorTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/login/saml/IdentityProviderConfiguratorTests.java old mode 100644 new mode 100755 index 5232fa5bc46..44c14d8ddeb --- a/common/src/test/java/org/cloudfoundry/identity/uaa/login/saml/IdentityProviderConfiguratorTests.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/login/saml/IdentityProviderConfiguratorTests.java @@ -440,4 +440,64 @@ public void testDuplicate_EntityID_IsRejected() throws Exception { } + @Test + public void testSetAddShadowUserOnLoginFromYaml() throws Exception { + String yaml = " providers:\n" + + " provider-without-shadow-user-definition:\n" + + " idpMetadata: |\n" + + " " + + " " + + " " + + " urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + + " " + + " " + + " \n" + + " nameID: urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\n" + + " provider-with-shadow-users-enabled:\n" + + " idpMetadata: |\n" + + " " + + " " + + " " + + " urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + + " " + + " " + + " \n" + + " nameID: urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\n" + + " addShadowUserOnLogin: true\n" + + " provider-with-shadow-user-disabled:\n" + + " idpMetadata: |\n" + + " " + + " " + + " " + + " urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + + " " + + " " + + " \n" + + " nameID: urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\n" + + " addShadowUserOnLogin: false\n"; + + parseYaml(yaml); + conf.setIdentityProviders(data); + conf.afterPropertiesSet(); + + for (IdentityProviderDefinition def : conf.getIdentityProviderDefinitions()) { + switch (def.getIdpEntityAlias()) { + case "provider-without-shadow-user-definition" : { + assertTrue("If not specified, addShadowUserOnLogin is set to true", def.isAddShadowUserOnLogin()); + break; + } + case "provider-with-shadow-users-enabled" : { + assertTrue("addShadowUserOnLogin can be set to true", def.isAddShadowUserOnLogin()); + break; + } + case "provider-with-shadow-user-disabled" : { + assertFalse("addShadowUserOnLogin can be set to false", def.isAddShadowUserOnLogin()); + break; + } + default: fail(String.format("Unknown provider %s", def.getIdpEntityAlias())); + } + + } + } + } From 2f2e6556ea92b47f866dd831fbc1a7b710603cd0 Mon Sep 17 00:00:00 2001 From: Mike Roda Date: Tue, 15 Sep 2015 07:26:35 -0400 Subject: [PATCH 004/103] Do not enforce CORS filter on same origin requests --- .../identity/uaa/web/CorsFilter.java | 16 ++++++++++++---- .../identity/uaa/web/CorsFilterTests.java | 17 +++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/web/CorsFilter.java b/common/src/main/java/org/cloudfoundry/identity/uaa/web/CorsFilter.java index 67da21af741..24d5abc59bc 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/web/CorsFilter.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/web/CorsFilter.java @@ -125,6 +125,9 @@ protected void doFilterInternal(final HttpServletRequest request, final HttpServ return; } String origin = request.getHeader(HttpHeaders.ORIGIN); + if (!isCrossOriginRequest(origin)) { + return; + } String requestUri = request.getRequestURI(); if (!isCorsXhrAllowedRequestUri(requestUri) || !isCorsXhrAllowedOrigin(origin)) { response.setStatus(HttpStatus.FORBIDDEN.value()); @@ -158,6 +161,15 @@ static boolean isXhrRequest(final HttpServletRequest request) { accessControlRequestHeaders, "X-Requested-With")); } + private boolean isCrossOriginRequest(final String origin) { + if (StringUtils.isEmpty(origin)) { + return false; + } + else { + return true; + } + } + void buildCorsXhrPreFlightResponse(final HttpServletRequest request, final HttpServletResponse response) { String accessControlRequestMethod = request.getHeader("Access-Control-Request-Method"); if (null == accessControlRequestMethod) { @@ -224,10 +236,6 @@ private boolean isCorsXhrAllowedRequestUri(final String uri) { } private boolean isCorsXhrAllowedOrigin(final String origin) { - if (StringUtils.isEmpty(origin)) { - return false; - } - for (Pattern pattern : this.corsXhrAllowedOriginPatterns) { // Making sure that the pattern matches if (pattern.matcher(origin).find()) { diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/web/CorsFilterTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/web/CorsFilterTests.java index 66582be1520..411b13114b6 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/web/CorsFilterTests.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/web/CorsFilterTests.java @@ -26,6 +26,7 @@ import org.junit.Test; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.http.HttpStatus; public class CorsFilterTests { @@ -78,6 +79,22 @@ public void testRequestExpectXhrCorsResponse() throws ServletException, IOExcept assertEquals("example.com", response.getHeaderValue("Access-Control-Allow-Origin")); } + @Test + public void testSameOriginRequest() throws ServletException, IOException { + CorsFilter corsFilter = createConfiguredCorsFilter(); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/uaa/userinfo"); + request.addHeader("X-Requested-With", "XMLHttpRequest"); + + MockHttpServletResponse response = new MockHttpServletResponse(); + + FilterChain filterChain = newMockFilterChain(); + + corsFilter.doFilter(request, response, filterChain); + + assertEquals(200, response.getStatus()); + } + @Test public void testRequestWithForbiddenOrigin() throws ServletException, IOException { CorsFilter corsFilter = createConfiguredCorsFilter(); From ff9e612600780d5225d2f7059439fd06f3c94ebf Mon Sep 17 00:00:00 2001 From: Mike Roda Date: Tue, 15 Sep 2015 11:41:55 -0400 Subject: [PATCH 005/103] Continue the filter chain on same origin requests --- .../org/cloudfoundry/identity/uaa/web/CorsFilter.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/web/CorsFilter.java b/common/src/main/java/org/cloudfoundry/identity/uaa/web/CorsFilter.java index 24d5abc59bc..7379e06f2ba 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/web/CorsFilter.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/web/CorsFilter.java @@ -118,16 +118,13 @@ public void initialize() { protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException { - if (isXhrRequest(request)) { + if (isXhrRequest(request) && isCrossOriginRequest(request)) { String method = request.getMethod(); if (!isCorsXhrAllowedMethod(method)) { response.setStatus(HttpStatus.METHOD_NOT_ALLOWED.value()); return; } String origin = request.getHeader(HttpHeaders.ORIGIN); - if (!isCrossOriginRequest(origin)) { - return; - } String requestUri = request.getRequestURI(); if (!isCorsXhrAllowedRequestUri(requestUri) || !isCorsXhrAllowedOrigin(origin)) { response.setStatus(HttpStatus.FORBIDDEN.value()); @@ -161,8 +158,8 @@ static boolean isXhrRequest(final HttpServletRequest request) { accessControlRequestHeaders, "X-Requested-With")); } - private boolean isCrossOriginRequest(final String origin) { - if (StringUtils.isEmpty(origin)) { + private boolean isCrossOriginRequest(final HttpServletRequest request) { + if (StringUtils.isEmpty(request.getHeader(HttpHeaders.ORIGIN))) { return false; } else { From 673747b4900848bdbc0e42f0d8c333866778ac0f Mon Sep 17 00:00:00 2001 From: Mike Roda Date: Thu, 17 Sep 2015 06:38:55 -0400 Subject: [PATCH 006/103] Skip all CORS processing if no Origin header This includes the checks as well as adding the Access-Control-* headers. --- .../java/org/cloudfoundry/identity/uaa/web/CorsFilter.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/web/CorsFilter.java b/common/src/main/java/org/cloudfoundry/identity/uaa/web/CorsFilter.java index 7379e06f2ba..b4dd9d1feec 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/web/CorsFilter.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/web/CorsFilter.java @@ -118,7 +118,12 @@ public void initialize() { protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException { - if (isXhrRequest(request) && isCrossOriginRequest(request)) { + if (!isCrossOriginRequest(request)) { + filterChain.doFilter(request, response); + return; + } + + if (isXhrRequest(request)) { String method = request.getMethod(); if (!isCorsXhrAllowedMethod(method)) { response.setStatus(HttpStatus.METHOD_NOT_ALLOWED.value()); From b122917b58d9a66f7f2e2c8eba2509a5d4efb6cb Mon Sep 17 00:00:00 2001 From: Eric Malm Date: Mon, 21 Sep 2015 18:47:02 -0700 Subject: [PATCH 007/103] Update UAA API docs to refer to OAuth 2.0 standard RFC 6749 is the latest proposed standards document for the OAuth 2.0 framework. --- docs/UAA-APIs.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/UAA-APIs.rst b/docs/UAA-APIs.rst index 7ab32d45568..d8e5d0835ce 100644 --- a/docs/UAA-APIs.rst +++ b/docs/UAA-APIs.rst @@ -18,7 +18,7 @@ The User Account and Authentication Service (UAA): Rather than trigger arguments about how RESTful these APIs are we'll just refer to them as JSON APIs. Most of them are defined by the specs for the OAuth2_, `OpenID Connect`_, and SCIM_ standards. -.. _OAuth2: http://tools.ietf.org/html/draft-ietf-oauth-v2-26 +.. _OAuth2: http://tools.ietf.org/html/rfc6749 .. _OpenID Connect: http://openid.net/openid-connect .. _SCIM: http://simplecloud.info @@ -310,11 +310,11 @@ An `OAuth2`_ defined endpoint to provide various tokens and authorization codes. For the ``cf`` flows, we use the OAuth2 Implicit grant type (to avoid a second round trip to ``/oauth/token`` and so cf does not need to securely store a client secret or user refresh tokens). The authentication method for the user is undefined by OAuth2 but a POST to this endpoint is acceptable, although a GET must also be supported (see `OAuth2 section 3.1`_). -.. _OAuth2 section 3.1: http://tools.ietf.org/html/draft-ietf-oauth-v2-26#section-3.1 +.. _OAuth2 section 3.1: http://tools.ietf.org/html/rfc6749#section-3.1 Effectively this means that the endpoint is used to authenticate **and** obtain an access token in the same request. Note the correspondence with the UI endpoints (this is similar to the ``/login`` endpoint with a different representation). -.. note:: A GET mothod is used in the `relevant section `_ of the spec that talks about the implicit grant, but a POST is explicitly allowed in the section on the ``/oauth/authorize`` endpoint (see `OAuth2 section 3.1`_). +.. note:: A GET mothod is used in the `relevant section `_ of the spec that talks about the implicit grant, but a POST is explicitly allowed in the section on the ``/oauth/authorize`` endpoint (see `OAuth2 section 3.1`_). All requests to this endpoint MUST be over SSL. @@ -538,7 +538,7 @@ Notes: * Many of the fields in the response are a courtesy, allowing the caller to avoid further round trip queries to pick up the same information (e.g. via the ``/Users`` endpoint). * The ``aud`` claim is the resource ids that are the audience for the token. A Resource Server should check that it is on this list or else reject the token. * The ``client_id`` data represent the client that the token was granted for, not the caller. The value can be used by the caller, for example, to verify that the client has been granted permission to access a resource. -* Error Responses: see `OAuth2 Error responses `_ and this addition:: +* Error Responses: see `OAuth2 Error responses `_ and this addition:: HTTP/1.1 400 Bad Request Content-Type: application/json;charset=UTF-8 From 07b0b2c8b7d4fc7d01ddc3ab1273e11cc733cfe8 Mon Sep 17 00:00:00 2001 From: Joshua Kruck Date: Wed, 23 Sep 2015 10:07:01 -0500 Subject: [PATCH 008/103] Fix <> code block --- docs/UAA-Tokens.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/UAA-Tokens.md b/docs/UAA-Tokens.md index 376c520ab86..ab9dd8aabec 100644 --- a/docs/UAA-Tokens.md +++ b/docs/UAA-Tokens.md @@ -126,7 +126,7 @@ Scopes, are arbitrary strings, defined by the client itself. The UAA does use th as anything before the first dot. ### Client authorities, UAA groups and scopes -In the UAA each client has a list of ```client authorities```. This is ```List<String>``` of scopes +In the UAA each client has a list of ```client authorities```. This is ```List;``` of scopes that represents the permissions the client has by itself. The second field the client has is the ```scopes``` field. The ```client scopes``` represents the permissions that the client uses when acting on behalf of a user. From fb72ca37f6f1da1662896d4acb826c9a78c28226 Mon Sep 17 00:00:00 2001 From: pivotal Date: Wed, 23 Sep 2015 11:56:49 -0700 Subject: [PATCH 009/103] Bump next develop version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 88261171538..88927de9d52 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=2.7.0 +version=2.7.1-SNAPSHOT From 751ec4fb292ede4113cd696053819a1dfa62d7cd Mon Sep 17 00:00:00 2001 From: Mike Gehard Date: Thu, 24 Sep 2015 10:09:11 -0600 Subject: [PATCH 010/103] Make external config file usage a bit more clear --- docs/Sysadmin-Guide.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Sysadmin-Guide.rst b/docs/Sysadmin-Guide.rst index 751bde8e466..b94f3bbb4ac 100644 --- a/docs/Sysadmin-Guide.rst +++ b/docs/Sysadmin-Guide.rst @@ -92,8 +92,8 @@ Configuration uaa.yml drives uaa behavior. There is a default file in the WAR that you should not touch. Overrides and additions can come from an external location, the most convenient way to specify that is through an -environment variable (or system property in the JVM): -$CLOUDFOUNDRY\_CONFIG\_PATH/uaa.yml. +environment variable (or system property in the JVM) named CLOUD\_FOUNDRY\_CONFIG\_PATH. +The UAA will then look for a file named $CLOUD\_FOUNDRY\_CONFIG\_PATH/uaa.yml. Database -------- From ce4b030b9965624dae3f94dd12386c9830c6da16 Mon Sep 17 00:00:00 2001 From: Jonathan Lo Date: Thu, 24 Sep 2015 15:09:19 -0700 Subject: [PATCH 011/103] added the two missing scopes to the list of restricted scopes [finishes #103612106] https://www.pivotaltracker.com/story/show/103612106 --- .../identity/uaa/oauth/UaaScopes.java | 2 ++ .../identity/uaa/oauth/UaaScopesTests.java | 27 +++++++++---------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaScopes.java b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaScopes.java index 84e570a602c..5a59c65ff15 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaScopes.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaScopes.java @@ -34,8 +34,10 @@ public class UaaScopes { "zones.read", "zones.write", "zones.*.admin", + "zones.*.read", "zones.*.clients.admin", "zones.*.clients.read", + "zones.*.clients.write", "zones.*.idps.read", "idps.read", "idps.write", diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaScopesTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaScopesTests.java index adad2aa0070..5a1aa4255b3 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaScopesTests.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaScopesTests.java @@ -31,8 +31,8 @@ public class UaaScopesTests { @Test public void testGetUaaScopes() throws Exception { - assertEquals(21, uaaScopes.getUaaScopes().size()); - assertEquals(21, uaaScopes.getUaaAuthorities().size()); + assertEquals(23, uaaScopes.getUaaScopes().size()); + assertEquals(23, uaaScopes.getUaaAuthorities().size()); } @Test @@ -52,19 +52,17 @@ protected List getGrantedAuthorities() { @Test public void testIsWildcardScope() throws Exception { - assertTrue(uaaScopes.isWildcardScope("zones.*.admin")); - assertTrue(uaaScopes.isWildcardScope("zones.*.idps.read")); - assertTrue(uaaScopes.isWildcardScope("zones.*.clients.admin")); - assertFalse(uaaScopes.isWildcardScope("zones.clients.admin")); - assertFalse(uaaScopes.isWildcardScope("openid")); - assertTrue(uaaScopes.isWildcardScope(new SimpleGrantedAuthority("zones.*.admin"))); - assertTrue(uaaScopes.isWildcardScope(new SimpleGrantedAuthority("zones.*.idps.read"))); - assertTrue(uaaScopes.isWildcardScope(new SimpleGrantedAuthority("zones.*.clients.admin"))); - assertFalse(uaaScopes.isWildcardScope(new SimpleGrantedAuthority("zones.clients.admin"))); - assertFalse(uaaScopes.isWildcardScope(new SimpleGrantedAuthority("openid"))); + for (String s : uaaScopes.getUaaScopes()) { + if (s.contains("*")) { + assertTrue(uaaScopes.isWildcardScope(s)); + assertTrue(uaaScopes.isWildcardScope(new SimpleGrantedAuthority(s))); + } else { + assertFalse(uaaScopes.isWildcardScope(s)); + assertFalse(uaaScopes.isWildcardScope(new SimpleGrantedAuthority(s))); + } + } } - @Test public void testIsUaaScope() throws Exception { for (String scope : uaaScopes.getUaaScopes()) { @@ -79,5 +77,4 @@ public void testIsUaaScope() throws Exception { assertTrue(uaaScopes.isUaaScope(scope)); } } - -} \ No newline at end of file +} From 804bc8cb536b65f9af5ac1e6b5005e1f57f745b3 Mon Sep 17 00:00:00 2001 From: Jonathan Lo Date: Fri, 25 Sep 2015 11:36:36 -0700 Subject: [PATCH 012/103] Null password policy or lockout policy is considered valid - since if the policy is null, the globaldefaults are used. [#103008492] https://www.pivotaltracker.com/story/show/103008492 Signed-off-by: Madhura Bhave --- .../identity/uaa/zone/IdentityProvider.java | 15 +++++++++++++-- .../identity/uaa/zone/IdentityProviderTest.java | 2 ++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProvider.java b/common/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProvider.java index e36d52447f9..7c4511e0dfd 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProvider.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProvider.java @@ -159,8 +159,19 @@ public boolean configIsValid() { } PasswordPolicy passwordPolicy = configValue.getPasswordPolicy(); LockoutPolicy lockoutPolicy= configValue.getLockoutPolicy(); - return (passwordPolicy!=null && passwordPolicy.allPresentAndPositive()) || - (lockoutPolicy!=null && lockoutPolicy.allPresentAndPositive()); + + if (passwordPolicy == null && lockoutPolicy == null) { + return true; + } else { + boolean isValid = true; + if(passwordPolicy != null) { + isValid = passwordPolicy.allPresentAndPositive(); + } + if(lockoutPolicy != null) { + isValid = isValid && lockoutPolicy.allPresentAndPositive(); + } + return isValid; + } } return true; } diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityProviderTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityProviderTest.java index b376980a010..d1e38784947 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityProviderTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityProviderTest.java @@ -34,6 +34,7 @@ public void configIsAlwaysValidWhenOriginIsOtherThanUaa() { @Test public void uaaConfigMustContainAllPasswordPolicyFields() { assertValidity(true, ""); + assertValidity(true, "{\"passwordPolicy\": null}"); assertValidity(false, "{\"passwordPolicy\": {}}"); assertValidity(false, "{\"passwordPolicy\":{\"minLength\":6}}"); assertValidity(false, "{\"passwordPolicy\":{\"minLength\":6,\"maxLength\":128}}"); @@ -58,6 +59,7 @@ public void uaaConfigDoesNotAllowNegativeNumbersForPasswordPolicy() { @Test public void uaaConfigMustContainAllLockoutPolicyFieldsIfSpecified() throws Exception { assertValidity(true, ""); + assertValidity(true, "{\"lockoutPolicy\": null}"); assertValidity(false, "{\"lockoutPolicy\": {}}"); assertValidity(false, "{\"lockoutPolicy\":{\"lockoutPeriodSeconds\":900}}"); assertValidity(false, "{\"lockoutPolicy\":{\"lockoutPeriodSeconds\":900,\"lockoutAfterFailures\":128}}"); From f765c05bf3cd107bcc43c4c21d914fb69b87df95 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Wed, 23 Sep 2015 11:33:03 -0700 Subject: [PATCH 013/103] Add in support for domain filtering to be used by invitations API https://www.pivotaltracker.com/story/show/102277582 [#102277582] Signed-off-by: Filip Hanik --- .../identity/uaa/util/DomainFilter.java | 122 ++++++++++ .../identity/uaa/util/DomainFilterTest.java | 209 ++++++++++++++++++ .../uaa/login/InvitationsController.java | 2 +- 3 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 common/src/main/java/org/cloudfoundry/identity/uaa/util/DomainFilter.java create mode 100644 common/src/test/java/org/cloudfoundry/identity/uaa/util/DomainFilterTest.java diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/util/DomainFilter.java b/common/src/main/java/org/cloudfoundry/identity/uaa/util/DomainFilter.java new file mode 100644 index 00000000000..eb51df23eef --- /dev/null +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/util/DomainFilter.java @@ -0,0 +1,122 @@ +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + * + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +package org.cloudfoundry.identity.uaa.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.cloudfoundry.identity.uaa.AbstractIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.authentication.Origin; +import org.cloudfoundry.identity.uaa.client.ClientConstants; +import org.cloudfoundry.identity.uaa.ldap.LdapIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.login.saml.SamlIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.zone.IdentityProvider; +import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.cloudfoundry.identity.uaa.zone.UaaIdentityProviderDefinition; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.util.StringUtils; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static java.util.Collections.EMPTY_LIST; + +public class DomainFilter { + + private static Log logger = LogFactory.getLog(DomainFilter.class); + + public List filter(List activeProviders, ClientDetails client, String email) { + if (!StringUtils.hasText(email)) { + return EMPTY_LIST; + } + + if (activeProviders!=null && activeProviders.size()>0) { + //filter client providers + List clientFilter = getProvidersForClient(client); + if (clientFilter!=null) { + activeProviders = + activeProviders.stream().filter( + p -> clientFilter.contains(p.getOriginKey()) + ).collect(Collectors.toList()); + } + //filter for email domain + if (email!=null && email.contains("@")) { + final String domain = email.substring(email.indexOf('@') + 1); + List explicitlyMatched = + activeProviders.stream().filter( + p -> doesEmailDomainMatchProvider(p, domain, true) + ).collect(Collectors.toList()); + if (explicitlyMatched.size()>0) { + return explicitlyMatched; + } + + activeProviders = + activeProviders.stream().filter( + p -> doesEmailDomainMatchProvider(p, domain, false) + ).collect(Collectors.toList()); + + } + } + return activeProviders != null ? activeProviders : EMPTY_LIST; + } + + protected List getProvidersForClient(ClientDetails client) { + if (client==null) { + return null; + } else { + return (List) client.getAdditionalInformation().get(ClientConstants.ALLOWED_PROVIDERS); + } + } + + protected List getEmailDomain(IdentityProvider provider) { + AbstractIdentityProviderDefinition definition = null; + if (provider.getConfig()!=null) { + switch (provider.getType()) { + case Origin.UAA: { + definition = provider.getConfigValue(UaaIdentityProviderDefinition.class); + break; + } + case Origin.LDAP: { + try { + definition = provider.getConfigValue(LdapIdentityProviderDefinition.class); + } catch (JsonUtils.JsonUtilException x) { + logger.error("Unable to parse LDAP configuration:"+provider.getConfig()); + } + break; + } + case Origin.SAML: { + definition = provider.getConfigValue(SamlIdentityProviderDefinition.class); + break; + } + default: { + break; + } + } + } + if (definition!=null) { + return definition.getEmailDomain(); + } + return null; + } + + + protected boolean doesEmailDomainMatchProvider(IdentityProvider provider, String domain, boolean explicit) { + List domainList = getEmailDomain(provider); + if (explicit && Origin.UAA.equals(provider.getOriginKey())) { + return domainList == null ? false : domainList.contains(domain); + } else { + return domainList == null ? true : domainList.contains(domain); + } + } + +} diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/util/DomainFilterTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/util/DomainFilterTest.java new file mode 100644 index 00000000000..17816f471f2 --- /dev/null +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/util/DomainFilterTest.java @@ -0,0 +1,209 @@ +/* + ******************************************************************************* + * Cloud Foundry Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + * + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + ******************************************************************************** + */ + +package org.cloudfoundry.identity.uaa.util; + +import org.cloudfoundry.identity.uaa.AbstractIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.authentication.Origin; +import org.cloudfoundry.identity.uaa.client.ClientConstants; +import org.cloudfoundry.identity.uaa.ldap.LdapIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.login.saml.SamlIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.zone.IdentityProvider; +import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.cloudfoundry.identity.uaa.zone.UaaIdentityProviderDefinition; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Test; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.security.oauth2.provider.client.BaseClientDetails; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static java.util.Collections.EMPTY_LIST; +import static org.cloudfoundry.identity.uaa.client.ClientConstants.ALLOWED_PROVIDERS; +import static org.junit.Assert.*; + +public class DomainFilterTest { + + public static final String alias = "saml"; + public static final String idpMetaData = "\n" + + "\n" + + " \n" + + " \n" + + " begl1WVCsXSn7iHixtWPP8d/X+k=BmbKqA3A0oSLcn5jImz/l5WbpVXj+8JIpT/ENWjOjSd/gcAsZm1QvYg+RxYPBk+iV2bBxD+/yAE/w0wibsHrl0u9eDhoMRUJBUSmeyuN1lYzBuoVa08PdAGtb5cGm4DMQT5Rzakb1P0hhEPPEDDHgTTxop89LUu6xx97t2Q03Khy8mXEmBmNt2NlFxJPNt0FwHqLKOHRKBOE/+BpswlBocjOQKFsI9tG3TyjFC68mM2jo0fpUQCgj5ZfhzolvS7z7c6V201d9Tqig0/mMFFJLTN8WuZPavw22AJlMjsDY9my+4R9HKhK5U53DhcTeECs9fb4gd7p5BJy4vVp7tqqOg==\n" + + "MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYDVQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwXc2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0BwaXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAaBgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQDDBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlrQHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWWRDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQnX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gphiJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduOnRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+vZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLuxbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6zV9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYDVQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwXc2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0BwaXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAaBgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQDDBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlrQHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWWRDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQnX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gphiJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduOnRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+vZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLuxbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6zV9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYDVQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwXc2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0BwaXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAaBgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQDDBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlrQHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWWRDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQnX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gphiJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduOnRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+vZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLuxbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6zV9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n" + + " \n" + + " \n" + + " \n" + + " Filip\n" + + " Hanik\n" + + " fhanik@pivotal.io\n" + + " \n" + + ""; + + BaseClientDetails client; + List activeProviders = EMPTY_LIST; + IdentityProvider uaaProvider; + IdentityProvider ldapProvider; + IdentityProvider samlProvider1; + IdentityProvider samlProvider2; + + DomainFilter filter = new DomainFilter(); + + String email = "test@test.org"; + private UaaIdentityProviderDefinition uaaDef; + private LdapIdentityProviderDefinition ldapDef; + private SamlIdentityProviderDefinition samlDef1; + private SamlIdentityProviderDefinition samlDef2; + + @Before + public void setUp() throws Exception { + client = new BaseClientDetails("clientid","", "", "","",""); + + uaaDef = new UaaIdentityProviderDefinition(null, null); + ldapDef = new LdapIdentityProviderDefinition(); + samlDef1 = new SamlIdentityProviderDefinition(idpMetaData,"","",0,true,true,"","", IdentityZone.getUaa().getId()); + samlDef2 = new SamlIdentityProviderDefinition(idpMetaData,"","",0,true,true,"","", IdentityZone.getUaa().getId()); + configureTestData(); + } + + private void configureTestData() { + uaaProvider = new IdentityProvider().setActive(true).setType(Origin.UAA).setOriginKey(Origin.UAA).setConfig(JsonUtils.writeValueAsString(uaaDef)); + ldapProvider = new IdentityProvider().setActive(true).setType(Origin.LDAP).setOriginKey(Origin.LDAP).setConfig(JsonUtils.writeValueAsString(ldapDef)); + samlProvider1 = new IdentityProvider().setActive(true).setType(Origin.SAML).setOriginKey("saml1").setConfig(JsonUtils.writeValueAsString(samlDef1)); + samlProvider2 = new IdentityProvider().setActive(true).setType(Origin.SAML).setOriginKey("saml2").setConfig(JsonUtils.writeValueAsString(samlDef2)); + activeProviders = Arrays.asList(uaaProvider, ldapProvider, samlProvider1, samlProvider2); + } + + @Test + public void test_null_arguments() throws Exception { + assertThat(filter.filter(null,null,null), Matchers.containsInAnyOrder()); + assertThat(filter.filter(null,null,email), Matchers.containsInAnyOrder()); + assertThat(filter.filter(null,client,null), Matchers.containsInAnyOrder()); + assertThat(filter.filter(null,client,email), Matchers.containsInAnyOrder()); + assertThat(filter.filter(activeProviders,null,null), Matchers.containsInAnyOrder()); + assertThat(filter.filter(activeProviders,client,null), Matchers.containsInAnyOrder()); + } + + @Test + public void test_default_idp_and_client_setup() { + assertThat(filter.filter(activeProviders,null,email), Matchers.containsInAnyOrder(ldapProvider, samlProvider1, samlProvider2)); + assertThat(filter.filter(activeProviders,client,email), Matchers.containsInAnyOrder(ldapProvider, samlProvider1, samlProvider2)); + assertThat(filter.filter(Arrays.asList(ldapProvider),null,email), Matchers.containsInAnyOrder(ldapProvider)); + assertThat(filter.filter(Arrays.asList(ldapProvider),client,email), Matchers.containsInAnyOrder(ldapProvider)); + assertThat(filter.filter(Arrays.asList(uaaProvider, samlProvider2),null,email), Matchers.containsInAnyOrder(samlProvider2)); + assertThat(filter.filter(Arrays.asList(uaaProvider, samlProvider2),client,email), Matchers.containsInAnyOrder(samlProvider2)); + assertThat(filter.filter(Arrays.asList(uaaProvider), null, email), Matchers.containsInAnyOrder(uaaProvider)); + assertThat(filter.filter(Arrays.asList(uaaProvider),client,email), Matchers.containsInAnyOrder(uaaProvider)); + } + + @Test + public void test_no_allowed_client_providers() { + client.addAdditionalInformation(ALLOWED_PROVIDERS, EMPTY_LIST); + assertThat(filter.filter(activeProviders,client,email), Matchers.containsInAnyOrder()); + } + + @Test + public void test_single_positive_email_domain_match() { + uaaDef.setEmailDomain(null); + samlDef1.setEmailDomain(EMPTY_LIST); + samlDef2.setEmailDomain(EMPTY_LIST); + ldapDef.setEmailDomain(Arrays.asList("test.org")); + configureTestData(); + assertThat(filter.filter(activeProviders, client, email), Matchers.containsInAnyOrder(ldapProvider)); + } + + @Test + public void test_multiple_positive_email_domain_matches() { + uaaDef.setEmailDomain(null); + samlDef1.setEmailDomain(EMPTY_LIST); + samlDef2.setEmailDomain(Arrays.asList("test.org","test2.org")); + ldapDef.setEmailDomain(Arrays.asList("test.org")); + configureTestData(); + assertThat(filter.filter(activeProviders, client, email), Matchers.containsInAnyOrder(ldapProvider, samlProvider2)); + } + + @Test + public void test_multiple_positive_email_domain_matches_single_client_allowed_provider() { + uaaDef.setEmailDomain(null); + samlDef1.setEmailDomain(EMPTY_LIST); + samlDef2.setEmailDomain(Arrays.asList("test.org","test2.org")); + ldapDef.setEmailDomain(Arrays.asList("test.org")); + client.addAdditionalInformation(ALLOWED_PROVIDERS, Arrays.asList(samlProvider2.getOriginKey())); + configureTestData(); + assertThat(filter.filter(activeProviders, client, email), Matchers.containsInAnyOrder(samlProvider2)); + + client.addAdditionalInformation(ALLOWED_PROVIDERS, Arrays.asList(samlProvider2.getOriginKey(), samlProvider1.getOriginKey())); + configureTestData(); + assertThat(filter.filter(activeProviders, client, email), Matchers.containsInAnyOrder(samlProvider2)); + + client.addAdditionalInformation(ALLOWED_PROVIDERS, Arrays.asList(samlProvider1.getOriginKey())); + configureTestData(); + assertThat(filter.filter(activeProviders, client, email), Matchers.containsInAnyOrder()); + } + + @Test + public void test_single_client_allowed_provider() { + client.addAdditionalInformation(ALLOWED_PROVIDERS, Arrays.asList(ldapProvider.getOriginKey())); + assertThat(filter.filter(activeProviders, client, email), Matchers.containsInAnyOrder(ldapProvider)); + } + + @Test + public void test_multiple_client_allowed_providers() { + client.addAdditionalInformation(ALLOWED_PROVIDERS, Arrays.asList(ldapProvider.getOriginKey(), uaaProvider.getOriginKey())); + assertThat(filter.filter(activeProviders, client, email), Matchers.containsInAnyOrder(ldapProvider)); + + client.addAdditionalInformation(ALLOWED_PROVIDERS, Arrays.asList(ldapProvider.getOriginKey(), samlProvider2.getOriginKey())); + assertThat(filter.filter(activeProviders, client, email), Matchers.containsInAnyOrder(ldapProvider, samlProvider2)); + } + + @Test + public void test_uaa_is_catch_all() { + ldapDef.setEmailDomain(EMPTY_LIST); + samlDef1.setEmailDomain(EMPTY_LIST); + samlDef2.setEmailDomain(EMPTY_LIST); + configureTestData(); + assertThat(filter.filter(activeProviders, client, email), Matchers.containsInAnyOrder(uaaProvider)); + } + + @Test + public void test_domain_filter_match() { + assertFalse(filter.doesEmailDomainMatchProvider(uaaProvider, "test.org", true)); + assertTrue(filter.doesEmailDomainMatchProvider(uaaProvider, "test.org", false)); + assertTrue(filter.doesEmailDomainMatchProvider(ldapProvider, "test.org", false)); + assertTrue(filter.doesEmailDomainMatchProvider(ldapProvider, "test.org", true)); + assertTrue(filter.doesEmailDomainMatchProvider(samlProvider1, "test.org", false)); + assertTrue(filter.doesEmailDomainMatchProvider(samlProvider1, "test.org", true)); + } +} diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/login/InvitationsController.java b/login/src/main/java/org/cloudfoundry/identity/uaa/login/InvitationsController.java index b1be02e663c..49022a67a77 100644 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/login/InvitationsController.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/login/InvitationsController.java @@ -141,7 +141,7 @@ protected List filterIdpsForClientAndEmailDomain(String client if (clientFilter!=null && clientFilter.size()>0) { providers = providers.stream().filter( - p -> clientFilter.contains(p.getId()) + p -> clientFilter.contains(p.getOriginKey()) ).collect(Collectors.toList()); } //filter for email domain From 6e059f8d1ba0ebe95d1c606646ca99b32025d194 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Tue, 22 Sep 2015 17:40:06 -0700 Subject: [PATCH 014/103] Implement new invitations API https://www.pivotaltracker.com/story/show/102277582 [#102277582] Remove /new and /new.do UI endpoints. (return 404) Users must be mapped to a single IDP Remove ExpiringCodeService Return user ids and proper error messages during invitations The property IdentityProvider.config.emailDomain is a list of domain names. Null or empty list means no invitations accepted (default) except for UAA domain --- .../identity/uaa/authentication/Origin.java | 2 - .../ExternalLoginAuthenticationManager.java | 7 +- .../saml/LoginSamlAuthenticationProvider.java | 17 +- .../uaa/oauth/token/TokenKeyEndpoint.java | 17 +- .../identity/uaa/user/UaaAuthority.java | 2 +- .../identity/uaa/util/DomainFilter.java | 32 ++- .../identity/uaa/util/JsonUtils.java | 1 + .../identity/uaa/util/DomainFilterTest.java | 80 +++++-- .../identity/uaa/util/UaaStringUtilsTest.java | 1 + .../InvitationsController.java | 111 ++++------ .../uaa/invitations/InvitationsEndpoint.java | 110 ++++++++++ .../uaa/invitations/InvitationsRequest.java | 23 ++ .../uaa/invitations/InvitationsResponse.java | 111 ++++++++++ .../InvitationsService.java | 8 +- .../login/EmailAccountCreationService.java | 6 - .../uaa/login/EmailInvitationsService.java | 116 ++++------ .../uaa/login/ExpiringCodeService.java | 39 ---- .../identity/uaa/login/LoginServerConfig.java | 22 +- .../uaa/login/UaaExpiringCodeService.java | 57 ----- login/src/main/resources/login-ui.xml | 28 ++- .../InvitationsControllerTest.java | 137 ++---------- .../login/EmailInvitationsServiceTests.java | 183 ++++++++-------- .../uaa/login/util/SecurityUtils.java | 70 ++++++ .../uaa/scim/ScimUserProvisioning.java | 7 +- uaa/build.gradle | 1 + .../webapp/WEB-INF/spring/scim-endpoints.xml | 2 +- .../integration/feature/AppApprovalIT.java | 1 - .../integration/feature/InvitationsIT.java | 24 +- .../uaa/integration/feature/SamlLoginIT.java | 14 +- .../InvitationsEndpointMockMvcTests.java | 206 ++++++++++++++++++ .../InvitationsControllerMockMvcTests.java | 146 ------------- .../login/InvitationsServiceMockMvcTests.java | 138 ++++++------ .../identity/uaa/login/LoginMockMvcTests.java | 70 +----- .../identity/uaa/mock/util/MockMvcUtils.java | 52 +++++ .../ScimGroupEndpointsMockMvcTests.java | 22 +- .../ScimUserEndpointsMockMvcTests.java | 17 +- .../endpoints/ScimUserLookupMockMvcTests.java | 29 +-- 37 files changed, 1028 insertions(+), 881 deletions(-) rename login/src/main/java/org/cloudfoundry/identity/uaa/{login => invitations}/InvitationsController.java (72%) create mode 100644 login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsEndpoint.java create mode 100644 login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsRequest.java create mode 100644 login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsResponse.java rename login/src/main/java/org/cloudfoundry/identity/uaa/{login => invitations}/InvitationsService.java (65%) delete mode 100644 login/src/main/java/org/cloudfoundry/identity/uaa/login/ExpiringCodeService.java delete mode 100644 login/src/main/java/org/cloudfoundry/identity/uaa/login/UaaExpiringCodeService.java rename login/src/test/java/org/cloudfoundry/identity/uaa/{login => invitations}/InvitationsControllerTest.java (77%) create mode 100644 login/src/test/java/org/cloudfoundry/identity/uaa/login/util/SecurityUtils.java create mode 100644 uaa/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsEndpointMockMvcTests.java delete mode 100644 uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsControllerMockMvcTests.java diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/Origin.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/Origin.java index 89bf5b6b643..0dd8edde4b1 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/Origin.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/Origin.java @@ -28,8 +28,6 @@ public class Origin { public static final String KEYSTONE = "keystone"; public static final String SAML = "saml"; public static final String NotANumber = "NaN"; - public static final String UNKNOWN = "unknown"; - public static String getUserId(Authentication authentication) { String id; diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java index 58a78244262..f486361d7bf 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java @@ -17,7 +17,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.cloudfoundry.identity.uaa.authentication.Origin; import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; @@ -138,8 +137,8 @@ protected boolean isInvite() { Authentication a = SecurityContextHolder.getContext().getAuthentication(); return ( a != null && - a.getPrincipal() instanceof UaaPrincipal && - Origin.UNKNOWN.equals(((UaaPrincipal)a.getPrincipal()).getOrigin()) + a.getAuthorities().contains(UaaAuthority.UAA_INVITED) && + a.getPrincipal() instanceof UaaPrincipal ); } @@ -216,4 +215,4 @@ public void setBeanName(String name) { this.name = name; } -} \ No newline at end of file +} diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java b/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java index ce354504c48..ba320d0d5f6 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java @@ -13,8 +13,6 @@ package org.cloudfoundry.identity.uaa.login.saml; -import java.util.Date; - import org.cloudfoundry.identity.uaa.authentication.Origin; import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; @@ -43,6 +41,8 @@ import org.springframework.security.saml.SAMLAuthenticationToken; import org.springframework.security.saml.context.SAMLMessageContext; +import java.util.Date; + public class LoginSamlAuthenticationProvider extends SAMLAuthenticationProvider implements ApplicationEventPublisherAware { private UaaUserDatabase userDatabase; @@ -93,6 +93,7 @@ public Authentication authenticate(Authentication authentication) throws Authent UaaPrincipal samlPrincipal = new UaaPrincipal(Origin.NotANumber, result.getName(), result.getName(), alias, result.getName(), zone.getId()); UaaPrincipal existingPrincipal = SecurityContextHolder.getContext().getAuthentication()!=null && + SecurityContextHolder.getContext().getAuthentication().getAuthorities().contains(UaaAuthority.UAA_INVITED) && SecurityContextHolder.getContext().getAuthentication().getPrincipal() instanceof UaaPrincipal ? (UaaPrincipal)SecurityContextHolder.getContext().getAuthentication().getPrincipal() : null; @@ -111,17 +112,13 @@ protected void publish(ApplicationEvent event) { } protected UaaPrincipal evaluateInvitiationPrincipal(UaaPrincipal samlPrincipal, UaaPrincipal existingPrincipal) { - if (existingPrincipal ==null) { + if (existingPrincipal == null) { //no active invitation return samlPrincipal; - } else if (Origin.UNKNOWN.equals(existingPrincipal.getOrigin())) { - //it is an invitation - if (!samlPrincipal.getEmail().equalsIgnoreCase(existingPrincipal.getEmail())) { - throw new BadCredentialsException("SAML User email mismatch. Authenticated email doesn't match invited email."); - } - return existingPrincipal; + } else if (!samlPrincipal.getEmail().equalsIgnoreCase(existingPrincipal.getEmail())) { + throw new BadCredentialsException("SAML User email mismatch. Authenticated email doesn't match invited email."); } else { - return samlPrincipal; + return existingPrincipal; } } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/TokenKeyEndpoint.java b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/TokenKeyEndpoint.java index e023cec5dc9..7b2a3fccdff 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/TokenKeyEndpoint.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/TokenKeyEndpoint.java @@ -12,15 +12,6 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.oauth.token; -import java.lang.reflect.Field; -import java.math.BigInteger; -import java.security.Principal; -import java.security.interfaces.RSAPublicKey; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.InitializingBean; @@ -35,6 +26,14 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; +import java.lang.reflect.Field; +import java.security.Principal; +import java.security.interfaces.RSAPublicKey; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + /** * OAuth2 token services that produces JWT encoded token values. * diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/user/UaaAuthority.java b/common/src/main/java/org/cloudfoundry/identity/uaa/user/UaaAuthority.java index 19bcd63deb8..bd35f6c8d71 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/user/UaaAuthority.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/user/UaaAuthority.java @@ -32,7 +32,7 @@ */ public enum UaaAuthority implements GrantedAuthority { - UAA_ADMIN("uaa.admin", 1), UAA_USER("uaa.user", 0), UAA_NONE("uaa.none", -1); + UAA_INVITED("uaa.invited", 1), UAA_ADMIN("uaa.admin", 1), UAA_USER("uaa.user", 0), UAA_NONE("uaa.none", -1); public static final List ADMIN_AUTHORITIES = Collections.unmodifiableList(Arrays.asList(UAA_ADMIN, UAA_USER)); diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/util/DomainFilter.java b/common/src/main/java/org/cloudfoundry/identity/uaa/util/DomainFilter.java index eb51df23eef..3499d50b6cc 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/util/DomainFilter.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/util/DomainFilter.java @@ -15,21 +15,24 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.cloudfoundry.identity.uaa.AbstractIdentityProviderDefinition; -import org.cloudfoundry.identity.uaa.authentication.Origin; import org.cloudfoundry.identity.uaa.client.ClientConstants; import org.cloudfoundry.identity.uaa.ldap.LdapIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.login.saml.SamlIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.zone.IdentityProvider; -import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.UaaIdentityProviderDefinition; import org.springframework.security.oauth2.provider.ClientDetails; import org.springframework.util.StringUtils; -import java.util.Collections; +import java.util.Arrays; import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; import java.util.stream.Collectors; import static java.util.Collections.EMPTY_LIST; +import static org.cloudfoundry.identity.uaa.authentication.Origin.LDAP; +import static org.cloudfoundry.identity.uaa.authentication.Origin.SAML; +import static org.cloudfoundry.identity.uaa.authentication.Origin.UAA; public class DomainFilter { @@ -82,11 +85,11 @@ protected List getEmailDomain(IdentityProvider provider) { AbstractIdentityProviderDefinition definition = null; if (provider.getConfig()!=null) { switch (provider.getType()) { - case Origin.UAA: { + case UAA: { definition = provider.getConfigValue(UaaIdentityProviderDefinition.class); break; } - case Origin.LDAP: { + case LDAP: { try { definition = provider.getConfigValue(LdapIdentityProviderDefinition.class); } catch (JsonUtils.JsonUtilException x) { @@ -94,7 +97,7 @@ protected List getEmailDomain(IdentityProvider provider) { } break; } - case Origin.SAML: { + case SAML: { definition = provider.getConfigValue(SamlIdentityProviderDefinition.class); break; } @@ -112,10 +115,21 @@ protected List getEmailDomain(IdentityProvider provider) { protected boolean doesEmailDomainMatchProvider(IdentityProvider provider, String domain, boolean explicit) { List domainList = getEmailDomain(provider); - if (explicit && Origin.UAA.equals(provider.getOriginKey())) { - return domainList == null ? false : domainList.contains(domain); + List wildcardList; + if (explicit) { + wildcardList = domainList; } else { - return domainList == null ? true : domainList.contains(domain); + if (UAA.equals(provider.getOriginKey())) { + wildcardList = domainList == null ? Arrays.asList("*.*", "*.*.*", "*.*.*.*") : domainList; + } else { + wildcardList = domainList == null ? null : domainList; + } + } + if (wildcardList==null) { + return false; + } else { + Set patterns = UaaStringUtils.constructWildcards(wildcardList); + return UaaStringUtils.matches(patterns, domain); } } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/util/JsonUtils.java b/common/src/main/java/org/cloudfoundry/identity/uaa/util/JsonUtils.java index e0929df6f30..95a862dedf2 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/util/JsonUtils.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/util/JsonUtils.java @@ -19,6 +19,7 @@ import org.springframework.util.StringUtils; import java.io.IOException; +import java.util.Map; public class JsonUtils { diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/util/DomainFilterTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/util/DomainFilterTest.java index 17816f471f2..6f7fe6be49a 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/util/DomainFilterTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/util/DomainFilterTest.java @@ -14,9 +14,7 @@ package org.cloudfoundry.identity.uaa.util; -import org.cloudfoundry.identity.uaa.AbstractIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.authentication.Origin; -import org.cloudfoundry.identity.uaa.client.ClientConstants; import org.cloudfoundry.identity.uaa.ldap.LdapIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.login.saml.SamlIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.zone.IdentityProvider; @@ -25,17 +23,17 @@ import org.hamcrest.Matchers; import org.junit.Before; import org.junit.Test; -import org.springframework.security.oauth2.provider.ClientDetails; import org.springframework.security.oauth2.provider.client.BaseClientDetails; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import static java.util.Collections.EMPTY_LIST; +import static org.cloudfoundry.identity.uaa.authentication.Origin.LOGIN_SERVER; import static org.cloudfoundry.identity.uaa.client.ClientConstants.ALLOWED_PROVIDERS; -import static org.junit.Assert.*; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; public class DomainFilterTest { @@ -78,6 +76,7 @@ public class DomainFilterTest { IdentityProvider ldapProvider; IdentityProvider samlProvider1; IdentityProvider samlProvider2; + IdentityProvider loginServerProvider; DomainFilter filter = new DomainFilter(); @@ -90,7 +89,6 @@ public class DomainFilterTest { @Before public void setUp() throws Exception { client = new BaseClientDetails("clientid","", "", "","",""); - uaaDef = new UaaIdentityProviderDefinition(null, null); ldapDef = new LdapIdentityProviderDefinition(); samlDef1 = new SamlIdentityProviderDefinition(idpMetaData,"","",0,true,true,"","", IdentityZone.getUaa().getId()); @@ -103,7 +101,8 @@ private void configureTestData() { ldapProvider = new IdentityProvider().setActive(true).setType(Origin.LDAP).setOriginKey(Origin.LDAP).setConfig(JsonUtils.writeValueAsString(ldapDef)); samlProvider1 = new IdentityProvider().setActive(true).setType(Origin.SAML).setOriginKey("saml1").setConfig(JsonUtils.writeValueAsString(samlDef1)); samlProvider2 = new IdentityProvider().setActive(true).setType(Origin.SAML).setOriginKey("saml2").setConfig(JsonUtils.writeValueAsString(samlDef2)); - activeProviders = Arrays.asList(uaaProvider, ldapProvider, samlProvider1, samlProvider2); + loginServerProvider = new IdentityProvider().setActive(true).setType(LOGIN_SERVER).setOriginKey(LOGIN_SERVER); + activeProviders = Arrays.asList(uaaProvider, ldapProvider, samlProvider1, samlProvider2, loginServerProvider); } @Test @@ -114,16 +113,17 @@ public void test_null_arguments() throws Exception { assertThat(filter.filter(null,client,email), Matchers.containsInAnyOrder()); assertThat(filter.filter(activeProviders,null,null), Matchers.containsInAnyOrder()); assertThat(filter.filter(activeProviders,client,null), Matchers.containsInAnyOrder()); + assertThat(filter.filter(activeProviders,client,email), Matchers.containsInAnyOrder(uaaProvider)); } @Test public void test_default_idp_and_client_setup() { - assertThat(filter.filter(activeProviders,null,email), Matchers.containsInAnyOrder(ldapProvider, samlProvider1, samlProvider2)); - assertThat(filter.filter(activeProviders,client,email), Matchers.containsInAnyOrder(ldapProvider, samlProvider1, samlProvider2)); - assertThat(filter.filter(Arrays.asList(ldapProvider),null,email), Matchers.containsInAnyOrder(ldapProvider)); - assertThat(filter.filter(Arrays.asList(ldapProvider),client,email), Matchers.containsInAnyOrder(ldapProvider)); - assertThat(filter.filter(Arrays.asList(uaaProvider, samlProvider2),null,email), Matchers.containsInAnyOrder(samlProvider2)); - assertThat(filter.filter(Arrays.asList(uaaProvider, samlProvider2),client,email), Matchers.containsInAnyOrder(samlProvider2)); + assertThat(filter.filter(activeProviders,null,email), Matchers.containsInAnyOrder(uaaProvider)); + assertThat(filter.filter(activeProviders,client,email), Matchers.containsInAnyOrder(uaaProvider)); + assertThat(filter.filter(Arrays.asList(ldapProvider),null,email), Matchers.containsInAnyOrder()); + assertThat(filter.filter(Arrays.asList(ldapProvider),client,email), Matchers.containsInAnyOrder()); + assertThat(filter.filter(Arrays.asList(uaaProvider, samlProvider2),null,email), Matchers.containsInAnyOrder(uaaProvider)); + assertThat(filter.filter(Arrays.asList(uaaProvider, samlProvider2),client,email), Matchers.containsInAnyOrder(uaaProvider)); assertThat(filter.filter(Arrays.asList(uaaProvider), null, email), Matchers.containsInAnyOrder(uaaProvider)); assertThat(filter.filter(Arrays.asList(uaaProvider),client,email), Matchers.containsInAnyOrder(uaaProvider)); } @@ -142,6 +142,7 @@ public void test_single_positive_email_domain_match() { ldapDef.setEmailDomain(Arrays.asList("test.org")); configureTestData(); assertThat(filter.filter(activeProviders, client, email), Matchers.containsInAnyOrder(ldapProvider)); + assertThat(filter.filter(activeProviders, client, "some@other.domain"), Matchers.containsInAnyOrder(uaaProvider)); } @Test @@ -154,6 +155,26 @@ public void test_multiple_positive_email_domain_matches() { assertThat(filter.filter(activeProviders, client, email), Matchers.containsInAnyOrder(ldapProvider, samlProvider2)); } + @Test + public void test_multiple_positive_email_domain_matches_wildcard() { + uaaDef.setEmailDomain(null); + samlDef1.setEmailDomain(EMPTY_LIST); + samlDef2.setEmailDomain(Arrays.asList("*.org")); + ldapDef.setEmailDomain(Arrays.asList("*.org")); + configureTestData(); + assertThat(filter.filter(activeProviders, client, email), Matchers.containsInAnyOrder(ldapProvider, samlProvider2)); + } + + @Test + public void test_multiple_positive_long_email_domain_matches_wildcard() { + uaaDef.setEmailDomain(null); + samlDef1.setEmailDomain(EMPTY_LIST); + samlDef2.setEmailDomain(Arrays.asList("*.*.*.com")); + ldapDef.setEmailDomain(Arrays.asList("*.*.test2.com")); + configureTestData(); + assertThat(filter.filter(activeProviders, client, "user@test.test1.test2.com"), Matchers.containsInAnyOrder(ldapProvider, samlProvider2)); + } + @Test public void test_multiple_positive_email_domain_matches_single_client_allowed_provider() { uaaDef.setEmailDomain(null); @@ -176,16 +197,26 @@ public void test_multiple_positive_email_domain_matches_single_client_allowed_pr @Test public void test_single_client_allowed_provider() { client.addAdditionalInformation(ALLOWED_PROVIDERS, Arrays.asList(ldapProvider.getOriginKey())); + assertThat(filter.filter(activeProviders, client, email), Matchers.containsInAnyOrder()); + + ldapDef.setEmailDomain(Arrays.asList("test.org")); + configureTestData(); assertThat(filter.filter(activeProviders, client, email), Matchers.containsInAnyOrder(ldapProvider)); } @Test public void test_multiple_client_allowed_providers() { client.addAdditionalInformation(ALLOWED_PROVIDERS, Arrays.asList(ldapProvider.getOriginKey(), uaaProvider.getOriginKey())); - assertThat(filter.filter(activeProviders, client, email), Matchers.containsInAnyOrder(ldapProvider)); + assertThat(filter.filter(activeProviders, client, email), Matchers.containsInAnyOrder(uaaProvider)); client.addAdditionalInformation(ALLOWED_PROVIDERS, Arrays.asList(ldapProvider.getOriginKey(), samlProvider2.getOriginKey())); - assertThat(filter.filter(activeProviders, client, email), Matchers.containsInAnyOrder(ldapProvider, samlProvider2)); + assertThat(filter.filter(activeProviders, client, email), Matchers.containsInAnyOrder()); + + ldapDef.setEmailDomain(Arrays.asList("test.org")); + configureTestData(); + client.addAdditionalInformation(ALLOWED_PROVIDERS, Arrays.asList(ldapProvider.getOriginKey(), uaaProvider.getOriginKey())); + assertThat(filter.filter(activeProviders, client, email), Matchers.containsInAnyOrder(ldapProvider)); + } @Test @@ -197,13 +228,22 @@ public void test_uaa_is_catch_all() { assertThat(filter.filter(activeProviders, client, email), Matchers.containsInAnyOrder(uaaProvider)); } + @Test + public void test_uaa_is_catch_all_with_null_email_domain_list() { + ldapDef.setEmailDomain(null); + samlDef1.setEmailDomain(null); + samlDef2.setEmailDomain(null); + configureTestData(); + assertThat(filter.filter(activeProviders, client, email), Matchers.containsInAnyOrder(uaaProvider)); + } + @Test public void test_domain_filter_match() { assertFalse(filter.doesEmailDomainMatchProvider(uaaProvider, "test.org", true)); assertTrue(filter.doesEmailDomainMatchProvider(uaaProvider, "test.org", false)); - assertTrue(filter.doesEmailDomainMatchProvider(ldapProvider, "test.org", false)); - assertTrue(filter.doesEmailDomainMatchProvider(ldapProvider, "test.org", true)); - assertTrue(filter.doesEmailDomainMatchProvider(samlProvider1, "test.org", false)); - assertTrue(filter.doesEmailDomainMatchProvider(samlProvider1, "test.org", true)); + assertFalse(filter.doesEmailDomainMatchProvider(ldapProvider, "test.org", false)); + assertFalse(filter.doesEmailDomainMatchProvider(ldapProvider, "test.org", true)); + assertFalse(filter.doesEmailDomainMatchProvider(samlProvider1, "test.org", false)); + assertFalse(filter.doesEmailDomainMatchProvider(samlProvider1, "test.org", true)); } } diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/util/UaaStringUtilsTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/util/UaaStringUtilsTest.java index 6b21bc1f376..b32af71e222 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/util/UaaStringUtilsTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/util/UaaStringUtilsTest.java @@ -92,6 +92,7 @@ public void testContainsWildCard() { assertTrue(UaaStringUtils.containsWildcard("space.*")); assertFalse(UaaStringUtils.containsWildcard("space.developer")); assertTrue(UaaStringUtils.containsWildcard("space.*.*.developer")); + assertTrue(UaaStringUtils.containsWildcard("*")); } @Test diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/login/InvitationsController.java b/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsController.java similarity index 72% rename from login/src/main/java/org/cloudfoundry/identity/uaa/login/InvitationsController.java rename to login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsController.java index 49022a67a77..8b0c929511b 100644 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/login/InvitationsController.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsController.java @@ -1,5 +1,6 @@ -package org.cloudfoundry.identity.uaa.login; +package org.cloudfoundry.identity.uaa.invitations; +import com.fasterxml.jackson.core.type.TypeReference; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.cloudfoundry.identity.uaa.AbstractIdentityProviderDefinition; @@ -9,9 +10,10 @@ import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.cloudfoundry.identity.uaa.authentication.manager.DynamicZoneAwareAuthenticationManager; import org.cloudfoundry.identity.uaa.client.ClientConstants; -import org.cloudfoundry.identity.uaa.error.UaaException; +import org.cloudfoundry.identity.uaa.codestore.ExpiringCode; +import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; import org.cloudfoundry.identity.uaa.ldap.LdapIdentityProviderDefinition; -import org.cloudfoundry.identity.uaa.login.ExpiringCodeService.CodeNotFoundException; +import org.cloudfoundry.identity.uaa.login.PasswordConfirmationValidation; import org.cloudfoundry.identity.uaa.login.saml.SamlIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.login.saml.SamlRedirectUtils; import org.cloudfoundry.identity.uaa.scim.exception.InvalidPasswordException; @@ -27,6 +29,7 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -38,21 +41,22 @@ import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.util.StringUtils; -import org.springframework.validation.BindingResult; -import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import javax.validation.Valid; import java.io.IOException; +import java.sql.Timestamp; +import java.util.Arrays; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import static java.util.Collections.EMPTY_LIST; +import static org.cloudfoundry.identity.uaa.authentication.Origin.ORIGIN; import static org.springframework.web.bind.annotation.RequestMethod.GET; import static org.springframework.web.bind.annotation.RequestMethod.POST; @@ -65,7 +69,7 @@ public class InvitationsController { private final InvitationsService invitationsService; @Autowired @Qualifier("uaaPasswordValidator") private PasswordValidator passwordValidator; - @Autowired private ExpiringCodeService expiringCodeService; + @Autowired private ExpiringCodeStore expiringCodeStore; @Autowired private IdentityProviderProvisioning providerProvisioning; @Autowired private ClientDetailsService clientDetailsService; @Autowired private DynamicZoneAwareAuthenticationManager zoneAwareAuthenticationManager; @@ -156,83 +160,50 @@ protected List filterIdpsForClientAndEmailDomain(String client return providers; } - @RequestMapping(value = "/new", method = GET) - public String newInvitePage(Model model, - @RequestParam(required = false, value = "client_id") String clientId, - @RequestParam(required = false, value = "redirect_uri") String redirectUri) { - model.addAttribute("client_id", clientId); - model.addAttribute("redirect_uri", redirectUri); - return "invitations/new_invite"; + @RequestMapping(value = {"/sent", "/new", "/new.do"}) + public void return404(HttpServletResponse response) { + response.setStatus(404); } + @RequestMapping(value = "/accept", method = GET, params = {"code"}) + public String acceptInvitePage(@RequestParam String code, Model model, HttpServletRequest request, HttpServletResponse response) throws IOException { - @RequestMapping(value = "/new.do", method = POST, params = {"email"}) - public String sendInvitationEmail(@Valid @ModelAttribute("email") ValidEmail email, BindingResult result, - @RequestParam(defaultValue = "", value = "client_id") String clientId, - @RequestParam(defaultValue = "", value = "redirect_uri") String redirectUri, - Model model, - HttpServletResponse response) { - if (result.hasErrors()) { - return handleUnprocessableEntity(model, response, "error_message_code", "invalid_email", "invitations/new_invite"); - } - - UaaPrincipal p = ((UaaPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal()); - String currentUser = p.getName(); - try { - invitationsService.inviteUser(email.getEmail(), currentUser, clientId, redirectUri); - } catch (UaaException e) { - return handleUnprocessableEntity(model, response, "error_message_code", "existing_user", "invitations/new_invite"); + ExpiringCode expiringCode = expiringCodeStore.retrieveCode(code); + if (expiringCode==null) { + return handleUnprocessableEntity(model, response, "error_message_code", "code_expired", "invitations/accept_invite"); } - return "redirect:sent"; - } - @RequestMapping(value = "sent", method = GET) - public String inviteSentPage() { - return "invitations/invite_sent"; - } + Map codeData = JsonUtils.readValue(expiringCode.getData(), new TypeReference>() {}); + String origin = codeData.get(ORIGIN); + model.addAttribute("code", expiringCodeStore.generateCode(expiringCode.getData(), new Timestamp(System.currentTimeMillis()+(10*60*1000)))); - @RequestMapping(value = "/accept", method = GET, params = {"code"}) - public String acceptInvitePage(@RequestParam String code, Model model, HttpServletRequest request, HttpServletResponse response) throws IOException { try { - Map codeData = expiringCodeService.verifyCode(code); - List providers = filterIdpsForClientAndEmailDomain(codeData.get("client_id"), codeData.get("email")); - if (providers!=null && providers.size()==0) { - logger.debug(String.format("No available invitation providers for email:%s, id:%s", codeData.get("email"), codeData.get("user_id"))); - return handleUnprocessableEntity(model, response, "error_message_code", "no_suitable_idp", "invitations/accept_invite"); + IdentityProvider provider = providerProvisioning.retrieveByOrigin(origin, IdentityZoneHolder.get().getId()); + UaaPrincipal uaaPrincipal = new UaaPrincipal(codeData.get("user_id"), codeData.get("email"), codeData.get("email"), origin, null, IdentityZoneHolder.get().getId()); + UaaAuthentication token = new UaaAuthentication(uaaPrincipal, Arrays.asList(UaaAuthority.UAA_INVITED), new UaaAuthenticationDetails(request)); + SecurityContextHolder.getContext().setAuthentication(token); + if (Origin.SAML.equals(provider.getType())) { + SamlIdentityProviderDefinition definition = provider.getConfigValue(SamlIdentityProviderDefinition.class); + String redirect = "redirect:/" + SamlRedirectUtils.getIdpRedirectUrl(definition, getSpEntityID()); + logger.debug(String.format("Redirecting invitation for email:%s, id:%s single SAML IDP URL:%s", codeData.get("email"), codeData.get("user_id"), redirect)); + return redirect; } else { - UaaPrincipal uaaPrincipal = new UaaPrincipal(codeData.get("user_id"), codeData.get("email"), codeData.get("email"), Origin.UNKNOWN, null, IdentityZoneHolder.get().getId()); - UaaAuthentication token = new UaaAuthentication(uaaPrincipal, UaaAuthority.USER_AUTHORITIES, new UaaAuthenticationDetails(request)); - SecurityContextHolder.getContext().setAuthentication(token); - if (providers != null && providers.size() == 1 && Origin.SAML.equals(providers.get(0).getType())) { - SamlIdentityProviderDefinition definition = providers.get(0).getConfigValue(SamlIdentityProviderDefinition.class); - String redirect = "redirect:/" + SamlRedirectUtils.getIdpRedirectUrl(definition, getSpEntityID()); - logger.debug(String.format("Redirecting invitation for email:%s, id:%s single SAML IDP URL:%s", codeData.get("email"), codeData.get("user_id"), redirect)); - return redirect; - } else { - getProvidersByType(model, providers, Origin.UAA); - getProvidersByType(model, providers, Origin.SAML); - getProvidersByType(model, providers, Origin.LDAP); - model.addAttribute("entityID", SamlRedirectUtils.getZonifiedEntityId(getSpEntityID())); - logger.debug(String.format("Sending user to accept invitation page email:%s, id:%s", codeData.get("email"), codeData.get("user_id"))); - } + getProvidersByType(model, Arrays.asList(provider), Origin.UAA); + getProvidersByType(model, Arrays.asList(provider), Origin.LDAP); + model.addAttribute("entityID", SamlRedirectUtils.getZonifiedEntityId(getSpEntityID())); + logger.debug(String.format("Sending user to accept invitation page email:%s, id:%s", codeData.get("email"), codeData.get("user_id"))); } model.addAllAttributes(codeData); return "invitations/accept_invite"; - } catch (CodeNotFoundException e) { - return handleUnprocessableEntity(model, response, "error_message_code", "code_expired", "invitations/accept_invite"); + } catch (EmptyResultDataAccessException noProviderFound) { + logger.debug(String.format("No available invitation providers for email:%s, id:%s", codeData.get("email"), codeData.get("user_id"))); + return handleUnprocessableEntity(model, response, "error_message_code", "no_suitable_idp", "invitations/accept_invite"); } } protected void getProvidersByType(Model model, List providers, String type) { List result = providers.stream().filter(p -> type.equals(p.getType())).collect(Collectors.toList()); if (!result.isEmpty()) { - if (Origin.SAML.equals(result.get(0).getType())) { - List idps = new LinkedList<>(); - for (IdentityProvider p : result) { - idps.add(p.getConfigValue(SamlIdentityProviderDefinition.class)); - } - model.addAttribute("idps", idps); - } model.addAttribute(type, result); } } @@ -240,6 +211,7 @@ protected void getProvidersByType(Model model, List providers, @RequestMapping(value = "/accept_enterprise.do", method = POST) public String acceptLdapInvitation(@RequestParam("enterprise_username") String username, @RequestParam("enterprise_password") String password, + @RequestParam("code") String code, @RequestParam(value = "client_id", required = false, defaultValue = "") String clientId, @RequestParam(value = "redirect_uri", required = false, defaultValue = "") String redirectUri, Model model, HttpServletResponse response) throws IOException { @@ -268,7 +240,7 @@ public String acceptLdapInvitation(@RequestParam("enterprise_username") String u return handleUnprocessableEntity(model, response, "error_message", x.getMessage(), "invitations/accept_invite"); } - String redirectLocation = invitationsService.acceptInvitation(principal.getId(), principal.getEmail(), password, clientId, redirectUri, Origin.LDAP).getRedirectUri(); + String redirectLocation = invitationsService.acceptInvitation(code, password).getRedirectUri(); SecurityContextHolder.getContext().setAuthentication(authentication); if (StringUtils.hasText(redirectUri)) { return "redirect:" + redirectUri; @@ -282,6 +254,7 @@ public String acceptLdapInvitation(@RequestParam("enterprise_username") String u @RequestMapping(value = "/accept.do", method = POST) public String acceptInvitation(@RequestParam("password") String password, @RequestParam("password_confirmation") String passwordConfirmation, + @RequestParam("code") String code, @RequestParam(value = "client_id", required = false, defaultValue = "") String clientId, @RequestParam(value = "redirect_uri", required = false, defaultValue = "") String redirectUri, Model model, @@ -302,7 +275,7 @@ public String acceptInvitation(@RequestParam("password") String password, model.addAttribute("email", principal.getEmail()); return handleUnprocessableEntity(model, response, "error_message", e.getMessagesAsOneString(), "invitations/accept_invite"); } - InvitationsService.AcceptedInvitation invitation = invitationsService.acceptInvitation(principal.getId(), principal.getEmail(), password, clientId, redirectUri, Origin.UAA); + InvitationsService.AcceptedInvitation invitation = invitationsService.acceptInvitation(code, password); principal = new UaaPrincipal( invitation.getUser().getId(), invitation.getUser().getUserName(), diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsEndpoint.java b/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsEndpoint.java new file mode 100644 index 00000000000..6d4b8f64e1f --- /dev/null +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsEndpoint.java @@ -0,0 +1,110 @@ +package org.cloudfoundry.identity.uaa.invitations; + +import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; +import org.cloudfoundry.identity.uaa.error.UaaException; +import org.cloudfoundry.identity.uaa.invitations.InvitationsResponse.InvitedUser; +import org.cloudfoundry.identity.uaa.scim.ScimUser; +import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; +import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceConflictException; +import org.cloudfoundry.identity.uaa.util.DomainFilter; +import org.cloudfoundry.identity.uaa.zone.IdentityProvider; +import org.cloudfoundry.identity.uaa.zone.IdentityProviderProvisioning; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.security.oauth2.provider.ClientDetailsService; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.ArrayList; +import java.util.List; + +@Controller +public class InvitationsEndpoint { + + private InvitationsService invitationsService; + private ScimUserProvisioning users; + private IdentityProviderProvisioning providers; + private ClientDetailsService clients; + + public InvitationsEndpoint(InvitationsService invitationsService, + ScimUserProvisioning users, + IdentityProviderProvisioning providers, + ClientDetailsService clients) { + this.invitationsService = invitationsService; + this.users = users; + this.providers = providers; + this.clients = clients; + } + + @RequestMapping(value="/invite_users", method= RequestMethod.POST, consumes="application/json") + public ResponseEntity inviteUsers(@RequestBody InvitationsRequest invitations, @RequestParam(value="client_id") String clientId, @RequestParam(value="redirect_uri") String redirectUri) { + + // todo: get clientId from token, if not supplied in clientId + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String currentUser = null; + if (authentication instanceof OAuth2Authentication) { + OAuth2Authentication oAuth2Authentication = (OAuth2Authentication)authentication; + if (!oAuth2Authentication.isClientOnly()) { + currentUser = ((UaaPrincipal) oAuth2Authentication.getPrincipal()).getName(); + } else { + currentUser = oAuth2Authentication.getOAuth2Request().getClientId(); + } + + if (clientId==null) { + clientId = oAuth2Authentication.getOAuth2Request().getClientId(); + } + } + + InvitationsResponse invitationsResponse = new InvitationsResponse(); + List newInvitesEmails = new ArrayList<>(); + + DomainFilter filter = new DomainFilter(); + List activeProviders = providers.retrieveActive(IdentityZoneHolder.get().getId()); + ClientDetails client = clients.loadClientByClientId(clientId); + for (String email : invitations.getEmails()) { + try { + List providers = filter.filter(activeProviders, client, email); + if (providers.size()==1) { + ScimUser user = findOrCreateUser(email, providers.get(0).getOriginKey()); + invitationsService.inviteUser(user, currentUser, clientId, redirectUri); + invitationsResponse.getNewInvites().add(InvitationsResponse.success(user.getPrimaryEmail(), user.getId(), user.getOrigin())); + } else if (providers.size()==0) { + invitationsResponse.getFailedInvites().add(InvitationsResponse.failure(email, "provider.non-existent", "No authentication provider found.")); + } else { + invitationsResponse.getFailedInvites().add(InvitationsResponse.failure(email, "provider.ambiguous", "Multiple authentication providers found.")); + } + } catch (UaaException uaae) { + invitationsResponse.getFailedInvites().add(InvitationsResponse.failure(email, "invitation.exception", uaae.getMessage())); + } + } + return new ResponseEntity<>(invitationsResponse, HttpStatus.OK); + } + + protected ScimUser findOrCreateUser(String email, String origin) { + email = email.trim().toLowerCase(); + List results = users.query(String.format("email eq \"%s\" and origin eq \"%s\"", email, origin)); + if (results==null || results.size()==0) { + ScimUser user = new ScimUser(null, email, "", ""); + user.setPrimaryEmail(email.toLowerCase()); + user.setOrigin(origin); + user.setVerified(false); + user.setActive(true); + return users.createUser(user, new RandomValueStringGenerator(12).generate()); + } else if (results.size()==1) { + return results.get(0); + } else { + throw new ScimResourceConflictException(String.format("Ambiguous users found for email:%s with origin:%s", email, origin)); + } + } + +} diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsRequest.java b/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsRequest.java new file mode 100644 index 00000000000..07b05d7e367 --- /dev/null +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsRequest.java @@ -0,0 +1,23 @@ +package org.cloudfoundry.identity.uaa.invitations; + +/** + * Created by pivotal on 9/21/15. + */ +public class InvitationsRequest { + + private String[] emails; + + public InvitationsRequest() {} + + public InvitationsRequest(String[] emails) { + this.setEmails((emails)); + } + + public String[] getEmails() { + return emails; + } + + public void setEmails(String[] emails) { + this.emails = emails; + } +} diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsResponse.java b/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsResponse.java new file mode 100644 index 00000000000..cb74d72d0bb --- /dev/null +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsResponse.java @@ -0,0 +1,111 @@ +package org.cloudfoundry.identity.uaa.invitations; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.List; + +public class InvitationsResponse { + + @JsonProperty(value="new_invites") + private List newInvites = new ArrayList<>(); + @JsonProperty(value="failed_invites") + private List failedInvites = new ArrayList<>(); + + public InvitationsResponse() {} + + public List getNewInvites() { + return newInvites; + } + + public void setNewInvites(List newInvites) { + this.newInvites = newInvites; + } + + public List getFailedInvites() { + return failedInvites; + } + + public void setFailedInvites(List failedInvites) { + this.failedInvites = failedInvites; + } + + public static InvitedUser failure(String email, String errorCode, String errorMessage) { + InvitedUser user = new InvitedUser(); + user.email = email; + user.errorCode = errorCode; + user.errorMessage = errorMessage; + user.success = false; + return user; + } + + public static InvitedUser success(String email, String userId, String origin) { + InvitedUser user = new InvitedUser(); + user.email = email; + user.userId = userId; + user.origin = origin; + user.success = true; + return user; + } + + public static class InvitedUser { + private String email; + private String userId; + private String origin; + private boolean success; + private String errorCode; + private String errorMessage; + + public InvitedUser() { + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getOrigin() { + return origin; + } + + public void setOrigin(String origin) { + this.origin = origin; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public String getErrorCode() { + return errorCode; + } + + public void setErrorCode(String errorCode) { + this.errorCode = errorCode; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + } + +} diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/login/InvitationsService.java b/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsService.java similarity index 65% rename from login/src/main/java/org/cloudfoundry/identity/uaa/login/InvitationsService.java rename to login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsService.java index f9833d53390..969871f5110 100644 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/login/InvitationsService.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsService.java @@ -1,10 +1,12 @@ -package org.cloudfoundry.identity.uaa.login; +package org.cloudfoundry.identity.uaa.invitations; import org.cloudfoundry.identity.uaa.scim.ScimUser; public interface InvitationsService { - void inviteUser(String email, String currentUser, String clientId, String redirectUri); - AcceptedInvitation acceptInvitation(String userId, String email, String password, String clientId, String redirectUri, String origin); + + void inviteUser(ScimUser user, String currentUser, String clientId, String redirectUri); + + AcceptedInvitation acceptInvitation(String code, String password); class AcceptedInvitation { private final ScimUser user; diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/login/EmailAccountCreationService.java b/login/src/main/java/org/cloudfoundry/identity/uaa/login/EmailAccountCreationService.java index 2831539f4e5..73e2585f6e4 100644 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/login/EmailAccountCreationService.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/login/EmailAccountCreationService.java @@ -168,12 +168,6 @@ public void resendVerificationCode(String email, String clientId) { @Override public ScimUser createUser(String username, String password, String origin) { - if (Origin.UNKNOWN.equals(origin)) { - List results = scimUserProvisioning.query(String.format("username eq \"%s\" and origin eq \"%s\"", username, Origin.UNKNOWN)); - if (results!=null && results.size()==1) { - return results.get(0); - } - } ScimUser scimUser = new ScimUser(); scimUser.setUserName(username); ScimUser.Email email = new ScimUser.Email(); diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/login/EmailInvitationsService.java b/login/src/main/java/org/cloudfoundry/identity/uaa/login/EmailInvitationsService.java index 8172920046f..0d816531278 100644 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/login/EmailInvitationsService.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/login/EmailInvitationsService.java @@ -1,18 +1,19 @@ package org.cloudfoundry.identity.uaa.login; +import com.fasterxml.jackson.core.type.TypeReference; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.cloudfoundry.identity.uaa.authentication.Origin; -import org.cloudfoundry.identity.uaa.error.UaaException; -import org.cloudfoundry.identity.uaa.login.AccountCreationService.ExistingUserResponse; +import org.cloudfoundry.identity.uaa.codestore.ExpiringCode; +import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; +import org.cloudfoundry.identity.uaa.invitations.InvitationsService; import org.cloudfoundry.identity.uaa.message.PasswordChangeRequest; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; -import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceAlreadyExistsException; import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.cloudfoundry.identity.uaa.util.UaaUrlUtils; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; +import org.springframework.security.oauth2.common.util.OAuth2Utils; import org.springframework.security.oauth2.provider.ClientDetails; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.NoSuchClientException; @@ -24,17 +25,24 @@ import org.thymeleaf.spring4.SpringTemplateEngine; import java.io.IOException; +import java.sql.Timestamp; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; +import static org.cloudfoundry.identity.uaa.authentication.Origin.ORIGIN; +import static org.springframework.security.oauth2.common.util.OAuth2Utils.CLIENT_ID; +import static org.springframework.security.oauth2.common.util.OAuth2Utils.REDIRECT_URI; + @Service public class EmailInvitationsService implements InvitationsService { + public static final String USER_ID = "user_id"; + public static final String EMAIL = "email"; private final Log logger = LogFactory.getLog(getClass()); - public static final int INVITATION_EXPIRY_DAYS = 365; + public static final int INVITATION_EXPIRY_DAYS = 7; private final SpringTemplateEngine templateEngine; private final MessageService messageService; @@ -54,10 +62,7 @@ public void setBrand(String brand) { } @Autowired - private AccountCreationService accountCreationService; - - @Autowired - private ExpiringCodeService expiringCodeService; + private ExpiringCodeStore expiringCodeStore; @Autowired private ClientDetailsService clientDetailsService; @@ -87,79 +92,50 @@ private String getEmailHtml(String currentUser, String code) { } @Override - public void inviteUser(String email, String currentUser, String clientId, String redirectUri) { - try { - ScimUser user = accountCreationService.createUser(email, new RandomValueStringGenerator().generate(), Origin.UNKNOWN); - Map data = new HashMap<>(); - data.put("user_id", user.getId()); - data.put("email", email); - data.put("client_id", clientId); - data.put("redirect_uri", redirectUri); - String code = expiringCodeService.generateCode(data, INVITATION_EXPIRY_DAYS, TimeUnit.DAYS); - sendInvitationEmail(email, currentUser, code); - } catch (ScimResourceAlreadyExistsException e) { - try { - ExistingUserResponse existingUserResponse = JsonUtils.convertValue(e.getExtraInfo(), ExistingUserResponse.class); - if (existingUserResponse.getVerified()) { - throw new UaaException(e.getMessage(), e.getStatus().value()); - } - Map data = new HashMap<>(); - data.put("user_id", existingUserResponse.getUserId()); - data.put("email", email); - data.put("client_id", clientId); - data.put("redirect_uri", redirectUri); - String code = expiringCodeService.generateCode(data, INVITATION_EXPIRY_DAYS, TimeUnit.DAYS); - sendInvitationEmail(email, currentUser, code); - } catch (JsonUtils.JsonUtilException ioe) { - logger.warn("couldn't invite user",ioe); - } catch (IOException ioe) { - logger.warn("couldn't invite user",ioe); - } - } catch (IOException e) { - logger.warn("couldn't invite user",e); - } + public void inviteUser(ScimUser user, String currentUser, String clientId, String redirectUri) { + String email = user.getPrimaryEmail(); + Map data = new HashMap<>(); + data.put(USER_ID, user.getId()); + data.put(EMAIL, email); + data.put(CLIENT_ID, clientId); + data.put(REDIRECT_URI, redirectUri); + data.put(ORIGIN, user.getOrigin()); + Timestamp expiry = new Timestamp(System.currentTimeMillis()+ (INVITATION_EXPIRY_DAYS * 24 * 60 * 60 * 1000)); + ExpiringCode code = expiringCodeStore.generateCode(JsonUtils.writeValueAsString(data), expiry); + sendInvitationEmail(email, currentUser, code.getCode()); } @Override - public AcceptedInvitation acceptInvitation(String userId, String email, String password, String clientId, String redirectUri, String origin) { - ScimUser user = getScimUserFromInvitation(userId, email, origin); - //in case we got an existing user - userId = user.getId(); + public AcceptedInvitation acceptInvitation(String code, String password) { + ExpiringCode data = expiringCodeStore.retrieveCode(code); + + Map userData = JsonUtils.readValue(data.getData(), new TypeReference>() { + }); + String userId = userData.get(USER_ID); + String clientId = userData.get(CLIENT_ID); + String redirectUri = userData.get(REDIRECT_URI); + + ScimUser user = scimUserProvisioning.retrieve(userId); + user = scimUserProvisioning.verifyUser(userId, user.getVersion()); - if (!user.getOrigin().equals(origin)) { - user.setOrigin(origin); - user = scimUserProvisioning.update(userId, user); - } if (Origin.UAA.equals(user.getOrigin())) { PasswordChangeRequest request = new PasswordChangeRequest(); request.setPassword(password); scimUserProvisioning.changePassword(userId, null, password); } String redirectLocation = "/home"; - if (!clientId.equals("")) { - try { - ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId); - Set redirectUris = clientDetails.getRegisteredRedirectUri(); - String matchingRedirectUri = UaaUrlUtils.findMatchingRedirectUri(redirectUris, redirectUri); - if (StringUtils.hasText(matchingRedirectUri)) { - redirectLocation = redirectUri; - } - } catch (NoSuchClientException x) { - logger.debug("Unable to find client_id for invitation:"+clientId); - } catch (Exception x) { - logger.error("Unable to resolve redirect for clientID:"+clientId, x); + try { + ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId); + Set redirectUris = clientDetails.getRegisteredRedirectUri(); + String matchingRedirectUri = UaaUrlUtils.findMatchingRedirectUri(redirectUris, redirectUri); + if (StringUtils.hasText(matchingRedirectUri)) { + redirectLocation = redirectUri; } + } catch (NoSuchClientException x) { + logger.debug("Unable to find client_id for invitation:"+clientId); + } catch (Exception x) { + logger.error("Unable to resolve redirect for clientID:"+clientId, x); } return new AcceptedInvitation(redirectLocation, user); } - - protected ScimUser getScimUserFromInvitation(String userId, String username, String origin) { - if (Origin.UAA.equals(origin)) { - List results = scimUserProvisioning.query(String.format("username eq \"%s\" and origin eq \"%s\"", username, Origin.UAA)); - if (results != null && results.size() == 1) { - return results.get(0); - } - } - return scimUserProvisioning.retrieve(userId); - } } diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/login/ExpiringCodeService.java b/login/src/main/java/org/cloudfoundry/identity/uaa/login/ExpiringCodeService.java deleted file mode 100644 index a99318411a1..00000000000 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/login/ExpiringCodeService.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.cloudfoundry.identity.uaa.login; - -import java.io.IOException; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -public interface ExpiringCodeService { - String generateCode(Object data, int expiryTime, TimeUnit timeUnit) throws IOException; - T verifyCode(Class clazz, String code) throws CodeNotFoundException, IOException; - Map verifyCode(String code) throws CodeNotFoundException, IOException; - - public static class CodeNotFoundException extends Exception { - - public CodeNotFoundException() { - super(); - } - - - public CodeNotFoundException(String message, Throwable cause) { - super(message, cause); - } - - public CodeNotFoundException(String message) { - super(message); - } - - public CodeNotFoundException(Throwable cause) { - super(cause); - } - - /** - * - */ - private static final long serialVersionUID = -7579875965452686646L; - - } - - -} diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/login/LoginServerConfig.java b/login/src/main/java/org/cloudfoundry/identity/uaa/login/LoginServerConfig.java index 844dfdd105d..e13f82b7c6e 100644 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/login/LoginServerConfig.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/login/LoginServerConfig.java @@ -1,7 +1,10 @@ package org.cloudfoundry.identity.uaa.login; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.*; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.core.type.AnnotatedTypeMetadata; @@ -21,21 +24,6 @@ public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) } } - @Bean - @Conditional(InviteUsersCondition.class) - public InvitationsController invitationsController(InvitationsService invitationsService, ApplicationContext context) { - InvitationsController result = new InvitationsController(invitationsService); - result.setSpEntityID(context.getBean("samlEntityID", String.class)); - return result; - } - - public static class InviteUsersCondition implements Condition { - @Override - public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { - return "true".equalsIgnoreCase(context.getEnvironment().getProperty("login.invitationsEnabled")); - } - } - @Bean public MessageService messageService(EmailService emailService, NotificationsService notificationsService, Environment environment) { if (environment.getProperty("notifications.url") != null && !environment.getProperty("notifications.url").equals("")) { diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/login/UaaExpiringCodeService.java b/login/src/main/java/org/cloudfoundry/identity/uaa/login/UaaExpiringCodeService.java deleted file mode 100644 index ba758400864..00000000000 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/login/UaaExpiringCodeService.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.cloudfoundry.identity.uaa.login; - -import com.fasterxml.jackson.core.type.TypeReference; -import org.cloudfoundry.identity.uaa.codestore.ExpiringCode; -import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; -import org.cloudfoundry.identity.uaa.util.JsonUtils; -import org.springframework.stereotype.Component; - -import java.io.IOException; -import java.sql.Timestamp; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -@Component -public class UaaExpiringCodeService implements ExpiringCodeService { - - private ExpiringCodeStore codeStore; - - public UaaExpiringCodeService(ExpiringCodeStore codeStore) { - this.codeStore = codeStore; - } - - @Override - public String generateCode(Object data, int expiryTime, TimeUnit timeUnit) throws IOException { - Timestamp expiry = new Timestamp(System.currentTimeMillis() + TimeUnit.MILLISECONDS.convert(expiryTime, timeUnit)); - String dataJsonString = JsonUtils.writeValueAsString(data); - return codeStore.generateCode(dataJsonString, expiry).getCode(); - } - - @Override - public T verifyCode(Class clazz, String code) throws IOException, CodeNotFoundException { - try { - ExpiringCode expiringCode = codeStore.retrieveCode(code); - if (code==null || expiringCode==null) { - throw new CodeNotFoundException(); - } - return JsonUtils.readValue(expiringCode.getData(), clazz); - } catch (JsonUtils.JsonUtilException e) { - throw new CodeNotFoundException(); - } - } - - @Override - public Map verifyCode(String code) throws IOException, CodeNotFoundException { - try { - ExpiringCode expiringCode = codeStore.retrieveCode(code); - if (expiringCode==null) { - throw new CodeNotFoundException(); - } - return JsonUtils.readValue(expiringCode.getData(), new TypeReference>() { - }); - } catch (JsonUtils.JsonUtilException e) { - throw new CodeNotFoundException(); - } - } - -} diff --git a/login/src/main/resources/login-ui.xml b/login/src/main/resources/login-ui.xml index 08047ad1f04..426c7de8728 100644 --- a/login/src/main/resources/login-ui.xml +++ b/login/src/main/resources/login-ui.xml @@ -91,6 +91,18 @@ + + + + + + + + + + + + + + + + + + + + @@ -541,10 +565,6 @@ - - - - diff --git a/login/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsControllerTest.java b/login/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsControllerTest.java similarity index 77% rename from login/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsControllerTest.java rename to login/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsControllerTest.java index 7f0e224d951..708ade66b37 100644 --- a/login/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsControllerTest.java +++ b/login/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsControllerTest.java @@ -1,14 +1,17 @@ -package org.cloudfoundry.identity.uaa.login; +package org.cloudfoundry.identity.uaa.invitations; import org.cloudfoundry.identity.uaa.authentication.Origin; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.cloudfoundry.identity.uaa.authentication.manager.DynamicZoneAwareAuthenticationManager; import org.cloudfoundry.identity.uaa.client.ClientConstants; +import org.cloudfoundry.identity.uaa.codestore.ExpiringCode; +import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; import org.cloudfoundry.identity.uaa.error.UaaException; import org.cloudfoundry.identity.uaa.ldap.LdapIdentityProviderDefinition; -import org.cloudfoundry.identity.uaa.login.ExpiringCodeService.CodeNotFoundException; +import org.cloudfoundry.identity.uaa.login.BuildInfo; import org.cloudfoundry.identity.uaa.login.saml.SamlIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.login.test.ThymeleafConfig; +import org.cloudfoundry.identity.uaa.login.util.SecurityUtils; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.exception.InvalidPasswordException; import org.cloudfoundry.identity.uaa.scim.validate.PasswordValidator; @@ -28,6 +31,7 @@ import org.springframework.context.annotation.Import; import org.springframework.context.support.ResourceBundleMessageSource; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.NoSuchClientException; @@ -44,6 +48,7 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; +import java.sql.Timestamp; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -57,7 +62,6 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.anyObject; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doThrow; @@ -89,7 +93,7 @@ public class InvitationsControllerTest { InvitationsService invitationsService; @Autowired - ExpiringCodeService expiringCodeService; + ExpiringCodeStore expiringCodeStore; @Autowired PasswordValidator passwordValidator; @@ -183,104 +187,6 @@ public void test_doesEmailDomainMatchProvider() throws Exception { } - @Test - public void testNewInvitePage() throws Exception { - MockHttpServletRequestBuilder get = get("/invitations/new"); - - mockMvc.perform(get) - .andExpect(status().isOk()) - .andExpect(view().name("invitations/new_invite")); - } - - @Test - public void newInvitePageWithClientIdAndRedirectUri() throws Exception { - MockHttpServletRequestBuilder get = get("/invitations/new?client_id=client-id&redirect_uri=blah.example.com"); - - mockMvc.perform(get) - .andExpect(model().attribute("redirect_uri", "blah.example.com")) - .andExpect(model().attribute("client_id", "client-id")) - .andExpect(status().isOk()) - .andExpect(view().name("invitations/new_invite")) - .andExpect(xpath("//*[@type='hidden' and @value='blah.example.com']").exists()) - .andExpect(xpath("//*[@type='hidden' and @value='client-id']").exists()); - } - - @Test - public void testSendInvitationEmail() throws Exception { - UsernamePasswordAuthenticationToken auth = getMarissaAuthentication(); - SecurityContextHolder.getContext().setAuthentication(auth); - - MockHttpServletRequestBuilder post = post("/invitations/new.do") - .param("email", "user1@example.com"); - - mockMvc.perform(post) - .andExpect(status().isFound()) - .andExpect(redirectedUrl("sent")); - verify(invitationsService).inviteUser("user1@example.com", "marissa", "", ""); - } - - @Test - public void sendInvitationWithValidClientIdAndRedirectUri() throws Exception { - SecurityContextHolder.getContext().setAuthentication(getMarissaAuthentication()); - MockHttpServletRequestBuilder post = post("/invitations/new.do") - .param("email", "user1@example.com") - .param("client_id", "client-id") - .param("redirect_uri", "blah.example.com"); - - mockMvc.perform(post) - .andExpect(status().isFound()) - .andExpect(redirectedUrl("sent")); - verify(invitationsService).inviteUser("user1@example.com", "marissa", "client-id", "blah.example.com"); - } - - protected UsernamePasswordAuthenticationToken getMarissaAuthentication() { - UaaPrincipal p = new UaaPrincipal("123","marissa","marissa@test.org", Origin.UAA,"", IdentityZoneHolder.get().getId()); - UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(p, "", UaaAuthority.USER_AUTHORITIES); - assertTrue(auth.isAuthenticated()); - return auth; - } - - @Test - public void newInvitePageWithRedirectUri() throws Exception { - MockHttpServletRequestBuilder get = get("/invitations/new?redirect_uri=blah.example.com"); - - mockMvc.perform(get) - .andExpect(model().attribute("redirect_uri", "blah.example.com")) - .andExpect(status().isOk()) - .andExpect(view().name("invitations/new_invite")) - .andExpect(xpath("//*[@type='hidden' and @value='blah.example.com']").exists()); - } - - - @Test - public void testSendInvitationEmailToExistingVerifiedUser() throws Exception { - SecurityContextHolder.getContext().setAuthentication(getMarissaAuthentication()); - - MockHttpServletRequestBuilder post = post("/invitations/new.do") - .param("email", "user1@example.com"); - - doThrow(new UaaException("",409)).when(invitationsService).inviteUser("user1@example.com", "marissa", "", ""); - mockMvc.perform(post) - .andExpect(status().isUnprocessableEntity()) - .andExpect(view().name("invitations/new_invite")) - .andExpect(model().attribute("error_message_code", "existing_user")); - } - - @Test - public void testSendInvitationWithInvalidEmail() throws Exception { - SecurityContextHolder.getContext().setAuthentication(getMarissaAuthentication()); - - MockHttpServletRequestBuilder post = post("/invitations/new.do") - .param("email", "not_a_real_email"); - - mockMvc.perform(post) - .andExpect(status().isUnprocessableEntity()) - .andExpect(model().attribute("error_message_code", "invalid_email")) - .andExpect(view().name("invitations/new_invite")); - - verifyZeroInteractions(invitationsService); - } - @Test public void testAcceptInvitationsPage() throws Exception { Map codeData = new HashMap<>(); @@ -288,12 +194,12 @@ public void testAcceptInvitationsPage() throws Exception { codeData.put("email", "user@example.com"); codeData.put("client_id", "client-id"); codeData.put("redirect_uri", "blah.test.com"); - when(expiringCodeService.verifyCode("the_secret_code")).thenReturn(codeData); + when(expiringCodeStore.retrieveCode("the_secret_code")).thenReturn(new ExpiringCode("code", new Timestamp(System.currentTimeMillis()), JsonUtils.writeValueAsString(codeData))); IdentityProvider uaaProvider = new IdentityProvider(); uaaProvider.setType(Origin.UAA).setOriginKey(Origin.UAA).setId(Origin.UAA); when(providerProvisioning.retrieveActive(anyString())).thenReturn(Arrays.asList(uaaProvider)); - + when(providerProvisioning.retrieveByOrigin(anyString(), anyString())).thenReturn(uaaProvider); when(clientDetailsService.loadClientByClientId(anyString())).thenThrow(new NoSuchClientException("mock")); MockHttpServletRequestBuilder get = get("/invitations/accept") @@ -312,10 +218,9 @@ public void testAcceptInvitationsPage() throws Exception { assertEquals("user@example.com", principal.getEmail()); } - @Test public void testAcceptInvitePageWithExpiredCode() throws Exception { - doThrow(new CodeNotFoundException("code expired")).when(expiringCodeService).verifyCode("the_secret_code"); + when(expiringCodeStore.retrieveCode(anyString())).thenReturn(null); MockHttpServletRequestBuilder get = get("/invitations/accept").param("code", "the_secret_code"); mockMvc.perform(get) .andExpect(status().isUnprocessableEntity()) @@ -335,7 +240,7 @@ public void testAcceptInviteWithContraveningPassword() throws Exception { .andExpect(status().isUnprocessableEntity()) .andExpect(model().attribute("error_message", "Msg 1c Msg 2c")) .andExpect(view().name("invitations/accept_invite")); - verify(invitationsService, never()).acceptInvitation(anyString(), anyString(), anyString(), anyString(), anyString(), anyString()); + verify(invitationsService, never()).acceptInvitation(anyString(), anyString()); } @Test @@ -344,13 +249,13 @@ public void testAcceptInvite() throws Exception { user.setPrimaryEmail(user.getUserName()); MockHttpServletRequestBuilder post = startAcceptInviteFlow("passw0rd"); - when(invitationsService.acceptInvitation("user-id-001","user@example.com", "passw0rd", "", "", Origin.UAA)).thenReturn(new InvitationsService.AcceptedInvitation("/home",user)); + when(invitationsService.acceptInvitation(anyString(), eq("passw0rd"))).thenReturn(new InvitationsService.AcceptedInvitation("/home", user)); mockMvc.perform(post) .andExpect(status().isFound()) .andExpect(redirectedUrl("/home")); - verify(invitationsService).acceptInvitation("user-id-001","user@example.com", "passw0rd", "", "", Origin.UAA); + verify(invitationsService).acceptInvitation(anyString(), eq("passw0rd")); } public MockHttpServletRequestBuilder startAcceptInviteFlow(String password) { @@ -359,6 +264,7 @@ public MockHttpServletRequestBuilder startAcceptInviteFlow(String password) { SecurityContextHolder.getContext().setAuthentication(token); return post("/invitations/accept.do") + .param("code","thecode") .param("password", password) .param("password_confirmation", password); } @@ -372,13 +278,14 @@ public void acceptInviteWithValidClientRedirect() throws Exception { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(uaaPrincipal, null, UaaAuthority.USER_AUTHORITIES); SecurityContextHolder.getContext().setAuthentication(token); - when(invitationsService.acceptInvitation("user-id-001", "user@example.com", "password", "valid-app", "valid.redirect.com", Origin.UAA)).thenReturn(new InvitationsService.AcceptedInvitation("valid.redirect.com", user)); + when(invitationsService.acceptInvitation(anyString(), eq("password"))).thenReturn(new InvitationsService.AcceptedInvitation("valid.redirect.com", user)); MockHttpServletRequestBuilder post = post("/invitations/accept.do") .param("password", "password") .param("password_confirmation", "password") .param("client_id", "valid-app") - .param("redirect_uri", "valid.redirect.com"); + .param("redirect_uri", "valid.redirect.com") + .param("code","thecode"); mockMvc.perform(post) .andExpect(status().isFound()) @@ -394,9 +301,10 @@ public void acceptInviteWithInvalidClientRedirect() throws Exception { ScimUser user = new ScimUser(uaaPrincipal.getId(), uaaPrincipal.getName(),"fname", "lname"); user.setPrimaryEmail(user.getUserName()); - when(invitationsService.acceptInvitation("user-id-001", "user@example.com", "password", "valid-app", "invalid.redirect.com", Origin.UAA)).thenReturn(new InvitationsService.AcceptedInvitation("/home", user)); + when(invitationsService.acceptInvitation(anyString(), eq("password"))).thenReturn(new InvitationsService.AcceptedInvitation("/home", user)); MockHttpServletRequestBuilder post = post("/invitations/accept.do") + .param("code","thecode") .param("password", "password") .param("password_confirmation", "password") .param("client_id", "valid-app") @@ -414,6 +322,7 @@ public void testAcceptInviteWithoutMatchingPasswords() throws Exception { SecurityContextHolder.getContext().setAuthentication(token); MockHttpServletRequestBuilder post = post("/invitations/accept.do") + .param("code", "thecode") .param("password", "password") .param("password_confirmation", "does not match"); @@ -460,8 +369,8 @@ InvitationsController invitationsController(InvitationsService invitationsServic } @Bean - ExpiringCodeService expiringCodeService() { - return mock(ExpiringCodeService.class); + ExpiringCodeStore expiringCodeStore() { + return mock(ExpiringCodeStore.class); } @Bean diff --git a/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailInvitationsServiceTests.java b/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailInvitationsServiceTests.java index bfc9f38ce8f..04ff19214d9 100644 --- a/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailInvitationsServiceTests.java +++ b/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailInvitationsServiceTests.java @@ -1,12 +1,15 @@ package org.cloudfoundry.identity.uaa.login; +import com.fasterxml.jackson.core.type.TypeReference; import org.cloudfoundry.identity.uaa.authentication.Origin; +import org.cloudfoundry.identity.uaa.codestore.ExpiringCode; +import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; import org.cloudfoundry.identity.uaa.error.UaaException; import org.cloudfoundry.identity.uaa.login.test.ThymeleafConfig; -import org.cloudfoundry.identity.uaa.oauth.ClientAdminEndpoints; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceAlreadyExistsException; +import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -22,6 +25,7 @@ import org.springframework.context.annotation.Import; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.common.util.OAuth2Utils; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.NoSuchClientException; import org.springframework.security.oauth2.provider.client.BaseClientDetails; @@ -39,10 +43,14 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import org.thymeleaf.spring4.SpringTemplateEngine; +import java.sql.Timestamp; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; +import static org.cloudfoundry.identity.uaa.authentication.Origin.UAA; +import static org.cloudfoundry.identity.uaa.login.EmailInvitationsService.EMAIL; +import static org.cloudfoundry.identity.uaa.login.EmailInvitationsService.USER_ID; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; @@ -55,6 +63,8 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.springframework.security.oauth2.common.util.OAuth2Utils.CLIENT_ID; +import static org.springframework.security.oauth2.common.util.OAuth2Utils.REDIRECT_URI; @RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @@ -66,14 +76,11 @@ public class EmailInvitationsServiceTests { ConfigurableWebApplicationContext webApplicationContext; @Autowired - ExpiringCodeService expiringCodeService; + ExpiringCodeStore expiringCodeStore; @Autowired EmailInvitationsService emailInvitationsService; - @Autowired - AccountCreationService accountCreationService; - @Autowired MessageService messageService; @@ -97,17 +104,15 @@ public void tearDown() { @Test public void testSendInviteEmail() throws Exception { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setProtocol("http"); - request.setContextPath("/login"); - RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + ArgumentCaptor captor = ArgumentCaptor.forClass((Class)String.class); - ArgumentCaptor> captor = ArgumentCaptor.forClass((Class)Map.class); + ScimUser user = new ScimUser("existing-user-id", "marissa", "Marissa", "Koala"); + user.setPrimaryEmail("user@example.com"); - when(expiringCodeService.generateCode(captor.capture(), anyInt(), eq(TimeUnit.DAYS))).thenReturn("the_secret_code"); - emailInvitationsService.inviteUser("user@example.com", "current-user", "client-id", "blah.example.com"); + when(expiringCodeStore.generateCode(captor.capture(), anyObject())).thenReturn(new ExpiringCode("the_secret_code", null, null)); + emailInvitationsService.inviteUser(user, "current-user", "client-id", "blah.example.com"); - Map data = captor.getValue(); + Map data = JsonUtils.readValue(captor.getValue(), new TypeReference>() {}); assertEquals("existing-user-id", data.get("user_id")); assertEquals("client-id", data.get("client_id")); assertEquals("blah.example.com", data.get("redirect_uri")); @@ -122,51 +127,51 @@ public void testSendInviteEmail() throws Exception { String emailBody = emailBodyArgument.getValue(); assertThat(emailBody, containsString("current-user")); assertThat(emailBody, containsString("Pivotal")); - assertThat(emailBody, containsString("Accept Invite")); + assertThat(emailBody, containsString("Accept Invite")); assertThat(emailBody, not(containsString("Cloud Foundry"))); } @Test public void inviteUserWithoutClientIdOrRedirectUri() throws Exception { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setProtocol("http"); - request.setContextPath("/login"); - RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + ArgumentCaptor captor = ArgumentCaptor.forClass((Class)String.class); - ArgumentCaptor> captor = ArgumentCaptor.forClass((Class)Map.class); + ScimUser user = new ScimUser("existing-user-id", "marissa", "Marissa", "Koala"); + user.setPrimaryEmail("user@example.com"); - when(expiringCodeService.generateCode(captor.capture(), anyInt(), eq(TimeUnit.DAYS))).thenReturn("the_secret_code"); - emailInvitationsService.inviteUser("user@example.com", "current-user", "", ""); + when(expiringCodeStore.generateCode(captor.capture(), anyObject())).thenReturn(new ExpiringCode("the_secret_code", null, null)); + emailInvitationsService.inviteUser(user, "current-user", "", ""); - Map data = captor.getValue(); + Map data = JsonUtils.readValue(captor.getValue(), new TypeReference>() {}); assertEquals("existing-user-id", data.get("user_id")); assertEquals("", data.get("client_id")); assertEquals("", data.get("redirect_uri")); } - @Test(expected = UaaException.class) + @Test public void testSendInviteEmailToUserThatIsAlreadyVerified() throws Exception { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setProtocol("http"); - request.setContextPath("/login"); - - emailInvitationsService.inviteUser("alreadyverified@example.com", "current-user", "", ""); + ScimUser user = new ScimUser("12345", "marissa", "Marissa", "Koala"); + user.setPrimaryEmail("user@example.com"); + user.setVerified(true); + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(String.class); + when(expiringCodeStore.generateCode(argumentCaptor.capture(), anyObject())) + .thenAnswer(invocation -> new ExpiringCode("code", new Timestamp(System.currentTimeMillis()), argumentCaptor.getValue())); + + emailInvitationsService.inviteUser(user, "current-user", "", ""); } @Test public void testSendInviteEmailToUnverifiedUser() throws Exception { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setProtocol("http"); - request.setContextPath("/login"); - RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + ScimUser user = new ScimUser("existing-user-id", "marissa", "Marissa", "Koala"); + user.setPrimaryEmail("existingunverified@example.com"); + user.setVerified(true); - ArgumentCaptor> captor = ArgumentCaptor.forClass((Class)Map.class); + ArgumentCaptor captor = ArgumentCaptor.forClass((Class)String.class); - when(expiringCodeService.generateCode(captor.capture(), anyInt(), eq(TimeUnit.DAYS))).thenReturn("the_secret_code"); - emailInvitationsService.inviteUser("existingunverified@example.com", "current-user", "", "blah.example.com"); + when(expiringCodeStore.generateCode(captor.capture(), anyObject())).thenReturn(new ExpiringCode("the_secret_code", null, null)); + emailInvitationsService.inviteUser(user, "current-user", "client-id", "blah.example.com"); - Map data = captor.getValue(); + Map data = JsonUtils.readValue(captor.getValue(), new TypeReference>() {}); assertEquals("existing-user-id", data.get("user_id")); assertEquals("blah.example.com", data.get("redirect_uri")); @@ -180,24 +185,22 @@ public void testSendInviteEmailToUnverifiedUser() throws Exception { String emailBody = emailBodyArgument.getValue(); assertThat(emailBody, containsString("current-user")); assertThat(emailBody, containsString("Pivotal")); - assertThat(emailBody, containsString("Accept Invite")); + assertThat(emailBody, containsString("Accept Invite")); assertThat(emailBody, not(containsString("Cloud Foundry"))); } @Test public void testSendInviteEmailWithOSSBrand() throws Exception { emailInvitationsService.setBrand("oss"); - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setProtocol("http"); - request.setContextPath("/login"); - RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + ArgumentCaptor captor = ArgumentCaptor.forClass((Class)String.class); - ArgumentCaptor> captor = ArgumentCaptor.forClass((Class) Map.class); + ScimUser user = new ScimUser("existing-user-id", "marissa", "Marissa", "Koala"); + user.setPrimaryEmail("user@example.com"); - when(expiringCodeService.generateCode(captor.capture(), anyInt(), eq(TimeUnit.DAYS))).thenReturn("the_secret_code"); - emailInvitationsService.inviteUser("user@example.com", "current-user", "", ""); + when(expiringCodeStore.generateCode(captor.capture(), anyObject())).thenReturn(new ExpiringCode("the_secret_code", null, null)); + emailInvitationsService.inviteUser(user, "current-user", "client-id", "blah.example.com"); - Map data = captor.getValue(); + Map data = JsonUtils.readValue(captor.getValue(), new TypeReference>() {}); assertEquals("existing-user-id", data.get("user_id")); ArgumentCaptor emailBodyArgument = ArgumentCaptor.forClass(String.class); @@ -209,7 +212,7 @@ public void testSendInviteEmailWithOSSBrand() throws Exception { ); String emailBody = emailBodyArgument.getValue(); assertThat(emailBody, containsString("current-user")); - assertThat(emailBody, containsString("Accept Invite")); + assertThat(emailBody, containsString("Accept Invite")); assertThat(emailBody, containsString("Cloud Foundry")); assertThat(emailBody, not(containsString("Pivotal"))); } @@ -217,62 +220,88 @@ public void testSendInviteEmailWithOSSBrand() throws Exception { @Test public void acceptInvitationNoClientId() throws Exception { ScimUser user = new ScimUser("user-id-001", "user@example.com", "first", "last"); + user.setOrigin(UAA); when(scimUserProvisioning.retrieve(eq("user-id-001"))).thenReturn(user); when(scimUserProvisioning.verifyUser(anyString(), anyInt())).thenReturn(user); when(scimUserProvisioning.update(anyString(), anyObject())).thenReturn(user); - String redirectLocation = emailInvitationsService.acceptInvitation("user-id-001", "user@example.com", "secret", "", "", Origin.UAA).getRedirectUri(); + Map userData = new HashMap<>(); + userData.put(USER_ID, "user-id-001"); + userData.put(EMAIL, "user@example.com"); + when(expiringCodeStore.retrieveCode(anyString())).thenReturn(new ExpiringCode("code", new Timestamp(System.currentTimeMillis()), JsonUtils.writeValueAsString(userData))); + String redirectLocation = emailInvitationsService.acceptInvitation("code", "password").getRedirectUri(); verify(scimUserProvisioning).verifyUser(user.getId(), user.getVersion()); - verify(scimUserProvisioning).changePassword(user.getId(), null, "secret"); - Mockito.verifyZeroInteractions(expiringCodeService); + verify(scimUserProvisioning).changePassword(user.getId(), null, "password"); assertEquals("/home", redirectLocation); } @Test public void acceptInvitationWithClientNotFound() throws Exception { ScimUser user = new ScimUser("user-id-001", "user@example.com", "first", "last"); + user.setOrigin(Origin.UAA); when(scimUserProvisioning.verifyUser(anyString(), anyInt())).thenReturn(user); when(scimUserProvisioning.update(anyString(), anyObject())).thenReturn(user); when(scimUserProvisioning.retrieve(eq("user-id-001"))).thenReturn(user); doThrow(new NoSuchClientException("Client not found")).when(clientDetailsService).loadClientByClientId("client-not-found"); - String redirectLocation = emailInvitationsService.acceptInvitation("user-id-001", "user@example.com", "secret", "client-not-found", "", Origin.UAA).getRedirectUri(); + + Map userData = new HashMap<>(); + userData.put(USER_ID, "user-id-001"); + userData.put(EMAIL, "user@example.com"); + userData.put(CLIENT_ID, "client-not-found"); + when(expiringCodeStore.retrieveCode(anyString())).thenReturn(new ExpiringCode("code", new Timestamp(System.currentTimeMillis()), JsonUtils.writeValueAsString(userData))); + + String redirectLocation = emailInvitationsService.acceptInvitation("code", "password").getRedirectUri(); verify(scimUserProvisioning).verifyUser(user.getId(), user.getVersion()); - verify(scimUserProvisioning).changePassword(user.getId(), null, "secret"); - Mockito.verifyZeroInteractions(expiringCodeService); + verify(scimUserProvisioning).changePassword(user.getId(), null, "password"); assertEquals("/home", redirectLocation); } @Test public void acceptInvitationWithValidRedirectUri() throws Exception { ScimUser user = new ScimUser("user-id-001", "user@example.com", "first", "last"); + user.setOrigin(UAA); BaseClientDetails clientDetails = new BaseClientDetails("client-id", null, null, null, null, "http://example.com/*/"); when(scimUserProvisioning.retrieve(eq("user-id-001"))).thenReturn(user); when(scimUserProvisioning.verifyUser(anyString(), anyInt())).thenReturn(user); when(scimUserProvisioning.update(anyString(), anyObject())).thenReturn(user); - when(clientDetailsService.loadClientByClientId("client-id")).thenReturn(clientDetails); - String redirectLocation = emailInvitationsService.acceptInvitation("user-id-001", "user@example.com", "secret", "client-id", "http://example.com/redirect/", Origin.UAA).getRedirectUri(); + when(clientDetailsService.loadClientByClientId("acmeClientId")).thenReturn(clientDetails); + + Map userData = new HashMap<>(); + userData.put(USER_ID, "user-id-001"); + userData.put(EMAIL, "user@example.com"); + userData.put(CLIENT_ID, "acmeClientId"); + userData.put(REDIRECT_URI, "http://example.com/redirect/"); + when(expiringCodeStore.retrieveCode(anyString())).thenReturn(new ExpiringCode("code", new Timestamp(System.currentTimeMillis()), JsonUtils.writeValueAsString(userData))); + + String redirectLocation = emailInvitationsService.acceptInvitation("code", "password").getRedirectUri(); verify(scimUserProvisioning).verifyUser(user.getId(), user.getVersion()); - verify(scimUserProvisioning).changePassword(user.getId(), null, "secret"); - Mockito.verifyZeroInteractions(expiringCodeService); + verify(scimUserProvisioning).changePassword(user.getId(), null, "password"); assertEquals("http://example.com/redirect/", redirectLocation); } @Test public void acceptInvitationWithInvalidRedirectUri() throws Exception { ScimUser user = new ScimUser("user-id-001", "user@example.com", "first", "last"); + user.setOrigin(UAA); BaseClientDetails clientDetails = new BaseClientDetails("client-id", null, null, null, null, "http://example.com/redirect"); when(scimUserProvisioning.verifyUser(anyString(), anyInt())).thenReturn(user); when(scimUserProvisioning.update(anyString(), anyObject())).thenReturn(user); when(scimUserProvisioning.retrieve(eq("user-id-001"))).thenReturn(user); - when(clientDetailsService.loadClientByClientId("client-id")).thenReturn(clientDetails); - String redirectLocation = emailInvitationsService.acceptInvitation("user-id-001", "user@example.com", "secret", "client-id", "http://example.com/other/redirect", Origin.UAA).getRedirectUri(); + when(clientDetailsService.loadClientByClientId("acmeClientId")).thenReturn(clientDetails); + Map userData = new HashMap<>(); + userData.put(USER_ID, "user-id-001"); + userData.put(EMAIL, "user@example.com"); + userData.put(REDIRECT_URI, "http://someother/redirect"); + userData.put(CLIENT_ID, "acmeClientId"); + when(expiringCodeStore.retrieveCode(anyString())).thenReturn(new ExpiringCode("code", new Timestamp(System.currentTimeMillis()), JsonUtils.writeValueAsString(userData))); + + String redirectLocation = emailInvitationsService.acceptInvitation("code", "password").getRedirectUri(); verify(scimUserProvisioning).verifyUser(user.getId(), user.getVersion()); - verify(scimUserProvisioning).changePassword(user.getId(), null, "secret"); - Mockito.verifyZeroInteractions(expiringCodeService); + verify(scimUserProvisioning).changePassword(user.getId(), null, "password"); assertEquals("/home", redirectLocation); } @@ -291,20 +320,13 @@ public void configureDefaultServletHandling(DefaultServletHandlerConfigurer conf SpringTemplateEngine templateEngine; @Bean - ExpiringCodeService expiringCodeService() { return mock(ExpiringCodeService.class); } + ExpiringCodeStore expiringCodeService() { return mock(ExpiringCodeStore.class); } @Bean MessageService messageService() { return mock(MessageService.class); } - @Bean - AccountCreationService accountCreationService() { - AccountCreationService svc = mock(AccountCreationService.class); - when(svc.createUser(anyString(), anyString(), anyString())).thenAnswer(createUserArgs()); - return svc; - } - @Bean EmailInvitationsService emailInvitationsService() { return new EmailInvitationsService(templateEngine, messageService(), "pivotal"); @@ -321,29 +343,4 @@ ScimUserProvisioning scimUserProvisioning() { } } - private static Answer createUserArgs() { - return new Answer() { - @Override - public ScimUser answer(InvocationOnMock invocation) throws Throwable { - String email = invocation.getArguments()[0].toString(); - String origin = invocation.getArguments()[2].toString(); - ScimUser user = new ScimUser("existing-user-id", email, "fname", "lname"); - user.setOrigin(origin); - user.setPrimaryEmail(user.getUserName()); - if (email.contains("alreadyverified")) { - Map extraInfoVerified = new HashMap<>(); - extraInfoVerified.put("verified", true); - throw new ScimResourceAlreadyExistsException("exists", extraInfoVerified); - } - if(email.contains("existingunverified")) { - Map extraInfoUnVerified = new HashMap<>(); - extraInfoUnVerified.put("verified", false); - extraInfoUnVerified.put("user_id", "existing-user-id"); - throw new ScimResourceAlreadyExistsException("exists", extraInfoUnVerified); - } - - return user; - } - }; - } } diff --git a/login/src/test/java/org/cloudfoundry/identity/uaa/login/util/SecurityUtils.java b/login/src/test/java/org/cloudfoundry/identity/uaa/login/util/SecurityUtils.java new file mode 100644 index 00000000000..7eaaf6e20bd --- /dev/null +++ b/login/src/test/java/org/cloudfoundry/identity/uaa/login/util/SecurityUtils.java @@ -0,0 +1,70 @@ +/* + * ***************************************************************************** + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + * ***************************************************************************** + */ +package org.cloudfoundry.identity.uaa.login.util; + +import org.cloudfoundry.identity.uaa.authentication.Origin; +import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; +import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; +import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.OAuth2Request; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.assertTrue; + +public final class SecurityUtils { + + private SecurityUtils() {} + + public static SecurityContext defaultSecurityContext(Authentication authentication) { + SecurityContext securityContext = new SecurityContextImpl(); + securityContext.setAuthentication(authentication); + return securityContext; + } + + public static Authentication fullyAuthenticatedUser(String id, String username, String email, GrantedAuthority... authorities) { + UaaPrincipal p = new UaaPrincipal(id, username, email, Origin.UAA,"", IdentityZoneHolder.get().getId()); + LinkedList grantedAuthorities = new LinkedList<>(); + Collections.addAll(grantedAuthorities, authorities); + UaaAuthentication auth = new UaaAuthentication(p, "", grantedAuthorities, new UaaAuthenticationDetails(new MockHttpServletRequest()),true, System.currentTimeMillis()); + assertTrue(auth.isAuthenticated()); + return auth; + } + + public static Authentication oauthAuthenticatedClient(String clientId, Set scopes, Set authorities) { + OAuth2Authentication auth = new OAuth2Authentication(new OAuth2Request(null, clientId, authorities, true, scopes, null, null, null, null), null); + assertTrue(auth.isAuthenticated()); + return auth; + } + + public static Authentication oauthAuthenticatedUser( + String clientId, Set scopes, Set authorities, + String id, String username, String email, GrantedAuthority... userAuthorities) { + OAuth2Authentication auth = new OAuth2Authentication(new OAuth2Request(null, clientId, authorities, true, scopes, null, null, null, null), fullyAuthenticatedUser(id, username, email, userAuthorities)); + assertTrue(auth.isAuthenticated()); + return auth; + } +} diff --git a/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserProvisioning.java b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserProvisioning.java index ecfeea1ce43..44241ae0269 100644 --- a/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserProvisioning.java +++ b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserProvisioning.java @@ -25,14 +25,15 @@ public interface ScimUserProvisioning extends ResourceManager, Queryable { public ScimUser createUser(ScimUser user, String password) throws InvalidPasswordException, - InvalidScimResourceException; + InvalidScimResourceException; public void changePassword(String id, String oldPassword, String newPassword) - throws ScimResourceNotFoundException; + throws ScimResourceNotFoundException; public ScimUser verifyUser(String id, int version) throws ScimResourceNotFoundException, - InvalidScimResourceException; + InvalidScimResourceException; public boolean checkPasswordMatches(String id, String password) throws ScimResourceNotFoundException; } + diff --git a/uaa/build.gradle b/uaa/build.gradle index 7baa9c7404c..cba2272426d 100644 --- a/uaa/build.gradle +++ b/uaa/build.gradle @@ -64,6 +64,7 @@ dependencies { testCompile group: 'com.github.detro.ghostdriver', name: 'phantomjsdriver', version:'1.1.0' testCompile group: 'dumbster', name: 'dumbster', version:'1.6' testCompile group: 'org.reflections', name: 'reflections', version: '0.9.10' + testCompile group: 'org.skyscreamer', name:'jsonassert', version: '0.9.0' } diff --git a/uaa/src/main/webapp/WEB-INF/spring/scim-endpoints.xml b/uaa/src/main/webapp/WEB-INF/spring/scim-endpoints.xml index 02cb9432c2b..e27f6cc419d 100644 --- a/uaa/src/main/webapp/WEB-INF/spring/scim-endpoints.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/scim-endpoints.xml @@ -267,7 +267,7 @@ like defaultUserAuthorities --> + value="scim.read,scim.write,scim.invite,uaa.resource,uaa.admin,clients.read,clients.write,clients.secret,cloud_controller.admin,clients.admin,zones.write" /> diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/AppApprovalIT.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/AppApprovalIT.java index d89f7077a32..e41c2e2aa4d 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/AppApprovalIT.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/AppApprovalIT.java @@ -87,7 +87,6 @@ public void testApprovingAnApp() throws Exception { // Visit app webDriver.get(appUrl); - IntegrationTestUtils.takeScreenShot(webDriver); // Sign in to login server webDriver.findElement(By.name("username")).sendKeys(user.getUserName()); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/InvitationsIT.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/InvitationsIT.java index 4046d2adb7a..8954ca03b4b 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/InvitationsIT.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/InvitationsIT.java @@ -22,6 +22,7 @@ import org.junit.Assert; import org.junit.Assume; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -51,6 +52,7 @@ import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; +@Ignore @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = DefaultIntegrationTestConfig.class) public class InvitationsIT { @@ -162,8 +164,8 @@ public void perform_LDAP_User_Invite_and_Accept() { webDriver.get(baseUrl + "/logout.do"); String username = "marissa5"; String email = username+"@test.com"; - String code = generateCode(username, email, ""); - String invitedUserId = IntegrationTestUtils.getUserId(scimToken, baseUrl, Origin.UNKNOWN, username); + String code = generateCode(username, email, "", Origin.LDAP); + String invitedUserId = IntegrationTestUtils.getUserId(scimToken, baseUrl, Origin.LDAP, username); String currentUserId = null; try { currentUserId = IntegrationTestUtils.getUserId(scimToken, baseUrl, Origin.LDAP, username); @@ -204,9 +206,9 @@ public void testInviteUserWithClientRedirect() throws Exception { } public void performInviteUser(String email) throws Exception { webDriver.get(baseUrl + "/logout.do"); - String code = generateCode(email, email, "http://localhost:8080/app/"); + String code = generateCode(email, email, "http://localhost:8080/app/", Origin.UAA); - String invitedUserId = IntegrationTestUtils.getUserId(scimToken, baseUrl, Origin.UNKNOWN, email); + String invitedUserId = IntegrationTestUtils.getUserId(scimToken, baseUrl, Origin.UAA, email); String currentUserId = null; try { currentUserId = IntegrationTestUtils.getUserId(scimToken, baseUrl, Origin.UAA, email); @@ -246,24 +248,24 @@ public void testInsecurePasswordDisplaysErrorMessage() throws Exception { private String generateCode() { String userEmail = "user" + new SecureRandom().nextInt() + "@example.com"; - return generateCode(userEmail, userEmail, "http://localhost:8080/app/"); + return generateCode(userEmail, userEmail, "http://localhost:8080/app/", Origin.UAA); } - private String generateCode(String username, String userEmail, String redirectUri) { - return generateCode(baseUrl, uaaUrl, username, userEmail, redirectUri, loginToken, scimToken); + private String generateCode(String username, String userEmail, String redirectUri, String origin) { + return generateCode(baseUrl, uaaUrl, username, userEmail, origin, redirectUri, loginToken, scimToken); } - public static String generateCode(String baseUrl, String uaaUrl, String username, String userEmail, String redirectUri, String scimWriteToken, String scimReadToken) { + public static String generateCode(String baseUrl, String uaaUrl, String username, String userEmail, String origin, String redirectUri, String scimWriteToken, String scimReadToken) { HttpHeaders headers = new HttpHeaders(); headers.add("Authorization", "Bearer " + scimWriteToken); RestTemplate uaaTemplate = new RestTemplate(); ScimUser scimUser = new ScimUser(); scimUser.setUserName(username); scimUser.setPrimaryEmail(userEmail); - scimUser.setOrigin(Origin.UNKNOWN); + scimUser.setOrigin(origin); String userId = null; try { - userId = IntegrationTestUtils.getUserId(scimReadToken, baseUrl, Origin.UNKNOWN, username); + userId = IntegrationTestUtils.getUserId(scimReadToken, baseUrl, origin, username); } catch (RuntimeException x) { } if (userId==null) { @@ -273,7 +275,7 @@ public static String generateCode(String baseUrl, String uaaUrl, String username } Timestamp expiry = new Timestamp(System.currentTimeMillis() + TimeUnit.MILLISECONDS.convert(System.currentTimeMillis() + 24 * 3600, TimeUnit.MILLISECONDS)); - ExpiringCode expiringCode = new ExpiringCode(null, expiry, "{\"client_id\":\"app\", \"redirect_uri\":\""+redirectUri+"\", \"user_id\":\"" + userId + "\", \"email\":\""+userEmail+"\"}"); + ExpiringCode expiringCode = new ExpiringCode(null, expiry, "{\"origin\":\""+origin+"\", \"client_id\":\"app\", \"redirect_uri\":\""+redirectUri+"\", \"user_id\":\"" + userId + "\", \"email\":\""+userEmail+"\"}"); HttpEntity expiringCodeRequest = new HttpEntity<>(expiringCode, headers); ResponseEntity expiringCodeResponse = uaaTemplate.exchange(uaaUrl + "/Codes", HttpMethod.POST, expiringCodeRequest, ExpiringCode.class); expiringCode = expiringCodeResponse.getBody(); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java index 7c3970be649..75347256cf0 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java @@ -373,7 +373,7 @@ public void perform_SamlInvitation_Automatic_Redirect_In_Zone2(String username, new PasswordPolicy(1,255,0,0,0,0,12), new LockoutPolicy(10, 10, 10) ); - uaaDefinition.setEmailDomain(emptyList ? Collections.EMPTY_LIST : null); + uaaDefinition.setEmailDomain(emptyList ? Collections.EMPTY_LIST : Arrays.asList("*.*","*.*.*")); IdentityProvider uaaProvider = IntegrationTestUtils.getProvider(zoneAdminToken, baseUrl, zoneId, Origin.UAA); uaaProvider.setConfig(JsonUtils.writeValueAsString(uaaDefinition)); uaaProvider = IntegrationTestUtils.createOrUpdateProvider(zoneAdminToken,baseUrl,uaaProvider); @@ -386,19 +386,11 @@ public void perform_SamlInvitation_Automatic_Redirect_In_Zone2(String username, String uaaAdminToken = testClient.getOAuthAccessToken(zoneUrl, "admin", "adminsecret", "client_credentials", ""); String useremail = username + "@test.org"; - String code = InvitationsIT.generateCode(zoneUrl, zoneUrl, useremail, useremail, "", uaaAdminToken, uaaAdminToken); - String invitedUserId = IntegrationTestUtils.getUserId(uaaAdminToken, zoneUrl, Origin.UNKNOWN, useremail); + String code = InvitationsIT.generateCode(zoneUrl, zoneUrl, useremail, useremail, samlIdentityProviderDefinition.getIdpEntityAlias(), "", uaaAdminToken, uaaAdminToken); + String invitedUserId = IntegrationTestUtils.getUserId(uaaAdminToken, zoneUrl, samlIdentityProviderDefinition.getIdpEntityAlias(), useremail); String existingUserId = IntegrationTestUtils.getUserId(uaaAdminToken, zoneUrl, samlIdentityProviderDefinition.getIdpEntityAlias(), useremail); - assertNotEquals(invitedUserId, existingUserId); webDriver.get(zoneUrl + "/logout.do"); webDriver.get(zoneUrl + "/invitations/accept?code=" + code); - if (!emptyList) { - WebElement element = webDriver.findElement(By.xpath("//a[text()='" + samlIdentityProviderDefinition.getLinkText() + "']")); - assertNotNull(element); - element.click(); - } - - //IntegrationTestUtils.takeScreenShot(webDriver); //we should now be in the Simple SAML PHP site diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsEndpointMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsEndpointMockMvcTests.java new file mode 100644 index 00000000000..d09115e5ab5 --- /dev/null +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsEndpointMockMvcTests.java @@ -0,0 +1,206 @@ +package org.cloudfoundry.identity.uaa.invitations; + +import org.cloudfoundry.identity.uaa.authentication.Origin; +import org.cloudfoundry.identity.uaa.config.IdentityProviderBootstrap; +import org.cloudfoundry.identity.uaa.login.EmailService; +import org.cloudfoundry.identity.uaa.login.test.MockMvcTestClient; +import org.cloudfoundry.identity.uaa.login.util.FakeJavaMailSender; +import org.cloudfoundry.identity.uaa.mock.InjectedMockContextTest; +import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; +import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.cloudfoundry.identity.uaa.util.SetServerNameRequestPostProcessor; +import org.cloudfoundry.identity.uaa.zone.IdentityProvider; +import org.cloudfoundry.identity.uaa.zone.IdentityProviderProvisioning; +import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.cloudfoundry.identity.uaa.zone.UaaIdentityProviderDefinition; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.oauth2.common.util.OAuth2Utils; +import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; + +import javax.mail.Message; +import javax.mail.MessagingException; +import java.util.Arrays; +import java.util.Iterator; + +import static org.cloudfoundry.identity.uaa.login.util.FakeJavaMailSender.MimeMessageWrapper; +import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.createScimClient; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class InvitationsEndpointMockMvcTests extends InjectedMockContextTest { + + private String scimInviteToken; + private RandomValueStringGenerator generator = new RandomValueStringGenerator(); + private String clientId; + private String clientSecret; + private String adminToken; + private String authorities; + private FakeJavaMailSender fakeJavaMailSender = new FakeJavaMailSender(); + private JavaMailSender originalSender; + private String domain; + + @Before + public void setUp() throws Exception { + getWebApplicationContext().getBean(IdentityProviderBootstrap.class).afterPropertiesSet(); + adminToken = MockMvcUtils.utils().getClientCredentialsOAuthAccessToken(getMockMvc(), "admin", "adminsecret", "clients.read clients.write clients.secret scim.read scim.write", null); + clientId = generator.generate().toLowerCase(); + clientSecret = generator.generate().toLowerCase(); + authorities = "scim.read,scim.invite"; + createScimClient(this.getMockMvc(), adminToken, clientId, clientSecret, "oauth", "scim.read,scim.invite", Arrays.asList(new MockMvcUtils.GrantType[] {MockMvcUtils.GrantType.client_credentials, MockMvcUtils.GrantType.password}), authorities); + scimInviteToken = MockMvcUtils.utils().getClientCredentialsOAuthAccessToken(getMockMvc(), clientId, clientSecret, "scim.read scim.invite", null); + domain = generator.generate().toLowerCase()+".com"; + IdentityProvider uaaProvider = getWebApplicationContext().getBean(IdentityProviderProvisioning.class).retrieveByOrigin(Origin.UAA, IdentityZone.getUaa().getId()); + uaaProvider.getConfigValue(UaaIdentityProviderDefinition.class).setEmailDomain(Arrays.asList(domain)); + getWebApplicationContext().getBean(IdentityProviderProvisioning.class).update(uaaProvider); + } + + @Before + public void setUpFakeMailServer() throws Exception { + originalSender = getWebApplicationContext().getBean("emailService", EmailService.class).getMailSender(); + getWebApplicationContext().getBean("emailService", EmailService.class).setMailSender(fakeJavaMailSender); + } + + @After + public void restoreMailServer() throws Exception { + getWebApplicationContext().getBean("emailService", EmailService.class).setMailSender(originalSender); + } + + @Test + public void testAcceptInvitationEmailWithOssBrand() throws Exception { + ((MockEnvironment) getWebApplicationContext().getEnvironment()).setProperty("login.brand", "oss"); + + getMockMvc().perform(get(getAcceptInvitationLink())) + .andDo(print()) + .andExpect(content().string(containsString("Create your account"))) + .andExpect(content().string(not(containsString("Pivotal ID")))) + .andExpect(content().string(not(containsString("Create Pivotal ID")))) + .andExpect(content().string(containsString("Create account"))); + } + + @Test + public void testAcceptInvitationEmailWithPivotalBrand() throws Exception { + ((MockEnvironment) getWebApplicationContext().getEnvironment()).setProperty("login.brand", "pivotal"); + + getMockMvc().perform(get(getAcceptInvitationLink())) + .andExpect(content().string(containsString("Create your Pivotal ID"))) + .andExpect(content().string(containsString("Pivotal products"))) + .andExpect(content().string(not(containsString("Create your account")))) + .andExpect(content().string(containsString("Create Pivotal ID"))) + .andExpect(content().string(not(containsString("Create account")))); + } + + @Test + public void testAcceptInvitationEmailWithinZone() throws Exception { + String subdomain = generator.generate(); + MockMvcUtils.utils().createOtherIdentityZone(subdomain, getMockMvc(), getWebApplicationContext()); + ((MockEnvironment) getWebApplicationContext().getEnvironment()).setProperty("login.brand", "pivotal"); + + getMockMvc().perform(get(getAcceptInvitationLink()) + .with(new SetServerNameRequestPostProcessor(subdomain + ".localhost"))) + .andExpect(content().string(containsString("Create your account"))) + .andExpect(content().string(not(containsString("Pivotal ID")))) + .andExpect(content().string(not(containsString("Create Pivotal ID")))) + .andExpect(content().string(containsString("Create account"))); + } + + private String getAcceptInvitationLink() throws Exception { + String userToken = MockMvcUtils.utils().getScimInviteUserToken(getMockMvc(), clientId, clientSecret); + String email = generator.generate().toLowerCase() + "@"+domain; + sendRequestWithToken(userToken, clientId, "example.com", email); + Iterator receivedEmail = fakeJavaMailSender.getSentMessages().iterator(); + MimeMessageWrapper message = receivedEmail.next(); + MockMvcTestClient mockMvcTestClient = new MockMvcTestClient(getMockMvc()); + return mockMvcTestClient.extractLink(message.getContentString()); + } + + @Test + public void test_Invitations_Accept_Get_Security() throws Exception { + getWebApplicationContext().getBean(JdbcTemplate.class).update("DELETE FROM expiring_code_store"); + SecurityContext marissaContext = MockMvcUtils.utils().getMarissaSecurityContext(getWebApplicationContext()); + String email = generator.generate()+"@"+domain; + + String userToken = MockMvcUtils.utils().getScimInviteUserToken(getMockMvc(), clientId, clientSecret); + sendRequestWithToken(userToken, clientId, "example.com", "user1@"+domain); + + String code = getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("SELECT code FROM expiring_code_store", String.class); + assertNotNull("Invite Code Must be Present",code); + + MockHttpServletRequestBuilder accept = get("/invitations/accept") + .param("code", code); + + getMockMvc().perform(accept) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("
"))); + } + + + @Test + public void testInviteUserWithClientCredentials() throws Exception { + String email = "user1@example.com"; + sendRequestWithToken(scimInviteToken, clientId, "example.com", email); + assertEmailsSent(email); + } + + @Test + public void testInviteMultipleUsersWithClientCredentials() throws Exception { + String[] emails = new String[] {"user1@"+domain, "user2@"+domain}; + sendRequestWithToken(scimInviteToken, clientId, "example.com", emails); + assertEmailsSent(emails); + } + + @Test + public void testInviteUserWithUserCredentials() throws Exception { + String userToken = MockMvcUtils.utils().getScimInviteUserToken(getMockMvc(), clientId, clientSecret); + sendRequestWithToken(userToken, clientId, "example.com", "user1@example.com"); + assertEmailsSent("user1@example.com"); + } + + // TODO: Test multiple invited users, existing verified users, existing unverified users, null emails + + public static void sendRequestWithToken(String token, String clientId, String redirectUri, String...emails) throws Exception { + InvitationsRequest invitations = new InvitationsRequest(emails); + + String requestBody = JsonUtils.writeValueAsString(invitations); + + MvcResult result = getMockMvc().perform(post("/invite_users") + .param(OAuth2Utils.CLIENT_ID, clientId) + .param(OAuth2Utils.REDIRECT_URI, redirectUri) + .header("Authorization", "Bearer " + token) + .contentType(APPLICATION_JSON) + .content(requestBody)) + .andExpect(status().isOk()) + .andReturn(); + + InvitationsResponse response = JsonUtils.readValue(result.getResponse().getContentAsString(), InvitationsResponse.class); + assertThat(response.getNewInvites().size(), is(emails.length)); + assertThat(response.getFailedInvites().size(), is(0)); + } + + protected void assertEmailsSent(String...emails) throws MessagingException { + assertEquals(emails.length, fakeJavaMailSender.getSentMessages().size()); + for (int i=0; i < emails.length; i++) { + MimeMessageWrapper mimeMessageWrapper = fakeJavaMailSender.getSentMessages().get(i); + assertEquals(1, mimeMessageWrapper.getRecipients(Message.RecipientType.TO).size()); + assertEquals(emails[i], mimeMessageWrapper.getRecipients(Message.RecipientType.TO).get(0).toString()); + } + } + +} diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsControllerMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsControllerMockMvcTests.java deleted file mode 100644 index c0bcd012f44..00000000000 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsControllerMockMvcTests.java +++ /dev/null @@ -1,146 +0,0 @@ -/******************************************************************************* - * Cloud Foundry - * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. - *

- * This product is licensed to you under the Apache License, Version 2.0 (the "License"). - * You may not use this product except in compliance with the License. - *

- * This product includes a number of subcomponents with - * separate copyright notices and license terms. Your use of these - * subcomponents is subject to the terms and conditions of the - * subcomponent's license, as noted in the LICENSE file. - *******************************************************************************/ -package org.cloudfoundry.identity.uaa.login; - -import com.dumbster.smtp.SimpleSmtpServer; -import com.dumbster.smtp.SmtpMessage; -import org.cloudfoundry.identity.uaa.login.test.MockMvcTestClient; -import org.cloudfoundry.identity.uaa.mock.InjectedMockContextTest; -import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; -import org.cloudfoundry.identity.uaa.util.SetServerNameRequestPostProcessor; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Assert; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; -import org.springframework.mail.javamail.JavaMailSender; -import org.springframework.mail.javamail.JavaMailSenderImpl; -import org.springframework.mock.env.MockEnvironment; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; - -import java.util.Iterator; - -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.not; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.securityContext; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -public class InvitationsControllerMockMvcTests extends InjectedMockContextTest { - private static SimpleSmtpServer mailServer; - private MockMvcTestClient mockMvcTestClient; - private MockMvcUtils mockMvcUtils; - private JavaMailSender originalSender; - private RandomValueStringGenerator generator = new RandomValueStringGenerator(); - - @BeforeClass - public static void startMailServer() throws Exception { - mailServer = SimpleSmtpServer.start(2525); - } - - @Before - public void setUp() throws Exception { - originalSender = getWebApplicationContext().getBean("emailService", EmailService.class).getMailSender(); - - JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); - mailSender.setHost("localhost"); - mailSender.setPort(2525); - getWebApplicationContext().getBean("emailService", EmailService.class).setMailSender(mailSender); - - Assert.assertNotNull(getWebApplicationContext().getBean("messageService")); - - mockMvcTestClient = new MockMvcTestClient(getMockMvc()); - - for (Iterator i = mailServer.getReceivedEmail(); i.hasNext();) { - i.next(); - i.remove(); - } - mockMvcUtils = MockMvcUtils.utils(); - } - - @After - public void restoreMailSender() { - ((MockEnvironment) getWebApplicationContext().getEnvironment()).setProperty("assetBaseUrl", "/resources/oss"); - ((MockEnvironment) getWebApplicationContext().getEnvironment()).setProperty("login.brand", "oss"); - getWebApplicationContext().getBean("emailService", EmailService.class).setMailSender(originalSender); - } - - @AfterClass - public static void stopMailServer() throws Exception { - if (mailServer!=null) { - mailServer.stop(); - } - } - - - @Test - public void testAcceptInvitationEmailWithOssBrand() throws Exception { - ((MockEnvironment) getWebApplicationContext().getEnvironment()).setProperty("login.brand", "oss"); - - getMockMvc().perform(get(getAcceptInvitationLink())) - .andExpect(content().string(containsString("Create your account"))) - .andExpect(content().string(not(containsString("Pivotal ID")))) - .andExpect(content().string(not(containsString("Create Pivotal ID")))) - .andExpect(content().string(containsString("Create account"))); - } - - @Test - public void testAcceptInvitationEmailWithPivotalBrand() throws Exception { - ((MockEnvironment) getWebApplicationContext().getEnvironment()).setProperty("login.brand", "pivotal"); - - getMockMvc().perform(get(getAcceptInvitationLink())) - .andExpect(content().string(containsString("Create your Pivotal ID"))) - .andExpect(content().string(containsString("Pivotal products"))) - .andExpect(content().string(not(containsString("Create your account")))) - .andExpect(content().string(containsString("Create Pivotal ID"))) - .andExpect(content().string(not(containsString("Create account")))); - } - - @Test - public void testAcceptInvitationEmailWithinZone() throws Exception { - String subdomain = generator.generate(); - mockMvcUtils.createOtherIdentityZone(subdomain, getMockMvc(), getWebApplicationContext()); - ((MockEnvironment) getWebApplicationContext().getEnvironment()).setProperty("login.brand", "pivotal"); - - getMockMvc().perform(get(getAcceptInvitationLink()) - .with(new SetServerNameRequestPostProcessor(subdomain + ".localhost"))) - .andExpect(content().string(containsString("Create your account"))) - .andExpect(content().string(not(containsString("Pivotal ID")))) - .andExpect(content().string(not(containsString("Create Pivotal ID")))) - .andExpect(content().string(containsString("Create account"))); - } - - private String getAcceptInvitationLink() throws Exception { - String email = generator.generate() + "@example.com"; - getMockMvc().perform(post("/invitations/new.do") - .with(securityContext(setupSecurityContext())).with(csrf()) - .param("email", email)) - .andExpect(status().isFound()) - .andExpect(redirectedUrl("sent")); - - Iterator receivedEmail = mailServer.getReceivedEmail(); - SmtpMessage message = (SmtpMessage) receivedEmail.next(); - return mockMvcTestClient.extractLink(message.getBody()); - } - - private SecurityContext setupSecurityContext() { - return mockMvcUtils.getMarissaSecurityContext(getWebApplicationContext()); - } - -} diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java index 00eceac51df..e499415b8a0 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java @@ -19,6 +19,7 @@ import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; +import org.cloudfoundry.identity.uaa.invitations.InvitationsEndpointMockMvcTests; import org.cloudfoundry.identity.uaa.ldap.LdapIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.login.saml.LoginSamlAuthenticationProvider; import org.cloudfoundry.identity.uaa.login.saml.SamlIdentityProviderDefinition; @@ -37,6 +38,7 @@ import org.junit.After; import org.junit.Assume; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.springframework.http.MediaType; import org.springframework.jdbc.core.JdbcTemplate; @@ -52,17 +54,20 @@ import org.springframework.security.saml.SAMLConstants; import org.springframework.security.saml.context.SAMLMessageContext; import org.springframework.security.saml.metadata.ExtendedMetadata; +import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.util.StringUtils; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.createScimClient; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; @@ -83,8 +88,23 @@ public class InvitationsServiceMockMvcTests extends InjectedMockContextTest { private JavaMailSender originalSender; private FakeJavaMailSender fakeJavaMailSender = new FakeJavaMailSender(); - private RandomValueStringGenerator generator = new RandomValueStringGenerator(); private MockMvcUtils utils = MockMvcUtils.utils(); + private String scimInviteToken; + private RandomValueStringGenerator generator = new RandomValueStringGenerator(); + private String clientId; + private String clientSecret; + private String adminToken; + private String authorities; + + @Before + public void setUp() throws Exception { + adminToken = MockMvcUtils.utils().getClientCredentialsOAuthAccessToken(getMockMvc(), "admin", "adminsecret", "clients.read clients.write clients.secret scim.read scim.write", null); + clientId = generator.generate().toLowerCase(); + clientSecret = generator.generate().toLowerCase(); + authorities = "scim.read,scim.invite"; + createScimClient(this.getMockMvc(), adminToken, clientId, clientSecret, "oauth", "scim.read,scim.invite", Arrays.asList(new MockMvcUtils.GrantType[] {MockMvcUtils.GrantType.client_credentials, MockMvcUtils.GrantType.password}), authorities); + scimInviteToken = MockMvcUtils.utils().getClientCredentialsOAuthAccessToken(getMockMvc(), clientId, clientSecret, "scim.read scim.invite", null); + } @Before public void setUpFakeMailServer() throws Exception { @@ -104,25 +124,20 @@ public void clearOutCodeTable() { fakeJavaMailSender.clearMessage(); } - @Test - public void ensure_that_newly_created_user_has_origin_UNKNOWN() throws Exception { - String username = new RandomValueStringGenerator().generate()+"@test.org"; - AccountCreationService svc = getWebApplicationContext().getBean(AccountCreationService.class); - ScimUser user = svc.createUser(username, "password", Origin.UNKNOWN); - assertEquals(Origin.UNKNOWN, user.getOrigin()); - assertFalse(user.isVerified()); - } - @Test public void inviteUser_Correct_Origin_Set() throws Exception { - String email = new RandomValueStringGenerator().generate()+"@test.org"; - inviteUser(email, null); + String email = new RandomValueStringGenerator().generate().toLowerCase()+"@test.org"; + inviteUser(email, null, Origin.UAA); } @Test + @Ignore //todo implement zone invites public void accept_invitation_origin_reset() throws Exception { - String email = new RandomValueStringGenerator().generate()+"@test.org"; - MimeMessageWrapper message = inviteUser(email, null); + getWebApplicationContext().getBean(JdbcTemplate.class).update("delete from expiring_code_store"); + String email = new RandomValueStringGenerator().generate().toLowerCase()+"@test.org"; + MimeMessageWrapper message = inviteUser(email, null, Origin.UAA); + assertEquals(Origin.UAA, getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select origin from users where username=?", new Object[]{email}, String.class)); + String code = extractInvitationCode(message.getContentString()); MvcResult result = getMockMvc().perform(get("/invitations/accept") .param("code", code) @@ -132,13 +147,13 @@ public void accept_invitation_origin_reset() throws Exception { .andExpect(content().string(containsString("Email: " + email))) .andReturn(); - assertEquals(Origin.UNKNOWN, getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select origin from users where username=?", new Object[]{email}, String.class)); - + code = getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select code from expiring_code_store", String.class); MockHttpSession session = (MockHttpSession) result.getRequest().getSession(false); getMockMvc().perform(post("/invitations/accept.do") .session(session) .param("password", "s3cret") .param("password_confirmation", "s3cret") + .param("code",code) .with(csrf())) .andExpect(status().isFound()) .andExpect(redirectedUrl("/home")) @@ -149,20 +164,24 @@ public void accept_invitation_origin_reset() throws Exception { @Test + @Ignore //TODO - We don't yet have utils to ivite in Zone public void invite_user_show_correct_saml_and_uaa_idp_for_acceptance() throws Exception { invite_user_and_check_UI(false, false); invite_user_and_check_UI(false, true); invite_user_and_check_UI(true, false); - invite_user_and_check_UI(true, true); } @Test + @Ignore //TODO - We don't yet have utils to ivite in Zone public void invite_user_show_correct_ldap_idp_for_acceptance() throws Exception { IdentityZoneCreationResult zone = utils.createOtherIdentityZoneAndReturnResult(generator.generate(), getMockMvc(), getWebApplicationContext(), null); LdapIdentityProviderDefinition definition = LdapIdentityProviderDefinition.searchAndBindMapGroupToScopes("","","","","","","","","",false,false,false,1,true); + + String domain = generator.generate().toLowerCase()+".com"; + definition.setEmailDomain(Arrays.asList(domain)); createIdentityProvider(zone, generator.generate(), definition); - String email = new RandomValueStringGenerator().generate()+"@test.org"; - MimeMessageWrapper message = inviteUser(email, zone.getIdentityZone().getSubdomain()); + String email = new RandomValueStringGenerator().generate().toLowerCase()+"@"+domain; + MimeMessageWrapper message = inviteUser(email, zone.getIdentityZone().getSubdomain(), Origin.LDAP); String code = extractInvitationCode(message.getContentString()); ResultActions actions = getMockMvc().perform(get("/invitations/accept") .param("code", code) @@ -176,9 +195,11 @@ public void invite_user_show_correct_ldap_idp_for_acceptance() throws Exception } @Test + @Ignore //TODO fix invites in zone public void invite_user_show_sets_correct_ldap_origin_for_acceptance() throws Exception { Assume.assumeTrue(java.util.Arrays.asList(getWebApplicationContext().getEnvironment().getActiveProfiles()).contains(Origin.LDAP)); - String email = "marissa2@test.com"; + String domain = generator.generate().toLowerCase()+".com"; + String email = "marissa2@"+domain; getWebApplicationContext().getBean(JdbcTemplate.class).update("DELETE FROM users WHERE email=?", email); IdentityZoneCreationResult zone = utils.createOtherIdentityZoneAndReturnResult(generator.generate(), getMockMvc(), getWebApplicationContext(), null); LdapIdentityProviderDefinition definition = LdapIdentityProviderDefinition.searchAndBindMapGroupToScopes( @@ -196,14 +217,15 @@ public void invite_user_show_sets_correct_ldap_origin_for_acceptance() throws Ex true, 10, true); + definition.setEmailDomain(Arrays.asList(domain)); createIdentityProvider(zone, Origin.LDAP, definition); - MimeMessageWrapper message = inviteUser(email, zone.getIdentityZone().getSubdomain()); + MimeMessageWrapper message = inviteUser(email, zone.getIdentityZone().getSubdomain(), Origin.LDAP); String code = extractInvitationCode(message.getContentString()); List> userInfo = getWebApplicationContext().getBean(JdbcTemplate.class).queryForList("select * from users where email=? and identity_zone_id=?", email, zone.getIdentityZone().getId()); assertEquals(1, userInfo.size()); - assertEquals(Origin.UNKNOWN, userInfo.get(0).get(Origin.ORIGIN)); + assertEquals(Origin.LDAP, userInfo.get(0).get(Origin.LDAP)); ResultActions actions = getMockMvc().perform(get("/invitations/accept") .param("code", code) @@ -236,10 +258,20 @@ public void invite_user_show_sets_correct_ldap_origin_for_acceptance() throws Ex public void invite_user_and_check_UI(boolean disableUAA, boolean disableSaml) throws Exception { + String expectedOrigin; + if (!disableSaml && !disableUAA) { + expectedOrigin = Origin.UAA; + } else if (!disableUAA) { + expectedOrigin = Origin.UAA; + } else { + expectedOrigin = Origin.SAML; + } + String domain = generator.generate().toLowerCase()+".com"; IdentityZoneCreationResult zone = utils.createOtherIdentityZoneAndReturnResult(generator.generate(), getMockMvc(), getWebApplicationContext(), null); String entityID = generator.generate(); SamlIdentityProviderDefinition definition = getSamlIdentityProviderDefinition(zone, entityID); + definition.setEmailDomain(Arrays.asList(domain)); IdentityProvider samlProvider = createIdentityProvider(zone, entityID, definition); IdentityProviderProvisioning provisioning = getWebApplicationContext().getBean(IdentityProviderProvisioning.class); if (disableSaml) { @@ -252,8 +284,8 @@ public void invite_user_and_check_UI(boolean disableUAA, boolean disableSaml) th provisioning.update(uaaProvider); } - String email = new RandomValueStringGenerator().generate()+"@test.org"; - MimeMessageWrapper message = inviteUser(email, zone.getIdentityZone().getSubdomain()); + String email = generator.generate().toLowerCase()+"@"+domain; + MimeMessageWrapper message = inviteUser(email, zone.getIdentityZone().getSubdomain(), expectedOrigin); String code = extractInvitationCode(message.getContentString()); ResultActions actions = getMockMvc().perform(get("/invitations/accept") @@ -262,40 +294,35 @@ public void invite_user_and_check_UI(boolean disableUAA, boolean disableSaml) th .header("Host", zone.getIdentityZone().getSubdomain() + ".localhost") ); + if (disableUAA && (!disableSaml)) { //redirect to SAML provider actions.andExpect(status().isFound()); actions.andExpect(redirectedUrl("/saml/discovery?returnIDParam=idp&entityID="+zone.getIdentityZone().getSubdomain()+".cloudfoundry-saml-login&idp="+entityID+"&isPassive=true")); } else { - if (disableSaml && disableUAA) { - actions.andExpect(status().isUnprocessableEntity()); - actions.andExpect(content().string(containsString("Your invitation does not match any suitable authentication provider."))); - actions.andExpect(content().string(not(containsString("Test Saml Provider")))); - actions.andExpect(content().string(not(containsString("password_confirmation")))); - } else { - actions.andExpect(status().isOk()); - actions.andExpect(content().string(containsString("Email: " + email))); - if (!disableUAA){ - actions.andExpect(content().string(containsString("password_confirmation"))); - } else if (!disableSaml){ - actions.andExpect(content().string(containsString("Test Saml Provider"))); - } + actions.andExpect(status().isOk()); + actions.andExpect(content().string(containsString("Email: " + email))); + if (!disableUAA){ + actions.andExpect(content().string(containsString("password_confirmation"))); + } else if (!disableSaml){ + actions.andExpect(content().string(containsString("Test Saml Provider"))); } } - assertEquals(Origin.UNKNOWN, getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select origin from users where username=?", new Object[]{email}, String.class)); + assertEquals(expectedOrigin, getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select origin from users where username=?", new Object[]{email}, String.class)); } @Test + @Ignore //TODO Fix invitations in zone public void invite_saml_user_with_different_email_after_login() throws Exception { IdentityZoneCreationResult zone = utils.createOtherIdentityZoneAndReturnResult(generator.generate(), getMockMvc(), getWebApplicationContext(), null); String entityID = generator.generate(); - + String domain = generator.generate().toLowerCase()+".com"; SamlIdentityProviderDefinition definition = getSamlIdentityProviderDefinition(zone, entityID); + definition.setEmailDomain(Arrays.asList(domain)); createIdentityProvider(zone, entityID, definition); - - String email = new RandomValueStringGenerator().generate()+"@test.org"; - MimeMessageWrapper message = inviteUser(email, zone.getIdentityZone().getSubdomain()); + String email = new RandomValueStringGenerator().generate()+"@"+domain; + MimeMessageWrapper message = inviteUser(email, zone.getIdentityZone().getSubdomain(), Origin.SAML); String code = extractInvitationCode(message.getContentString()); MvcResult result = getMockMvc().perform(get("/invitations/accept") @@ -309,7 +336,7 @@ public void invite_saml_user_with_different_email_after_login() throws Exception .andReturn(); - assertEquals(Origin.UNKNOWN, getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select origin from users where username=?", new Object[]{email}, String.class)); + assertEquals(Origin.SAML, getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select origin from users where username=?", new Object[]{email}, String.class)); MockHttpSession session = (MockHttpSession) result.getRequest().getSession(false); assertNotNull(session); @@ -319,7 +346,8 @@ public void invite_saml_user_with_different_email_after_login() throws Exception } catch (BadCredentialsException x) {} //validate that we did not change the invitation - assertEquals(Origin.UNKNOWN, getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select origin from users where username=?", new Object[]{email}, String.class)); + assertEquals(Origin.SAML, getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select origin from users where username=?", new Object[]{email}, String.class)); + assertEquals(false, getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select verified from users where username=?", new Object[]{email}, Boolean.class)); } protected IdentityProvider createIdentityProvider(IdentityZoneCreationResult zone, String nameAndOriginKey, AbstractIdentityProviderDefinition definition) throws Exception { @@ -363,7 +391,7 @@ protected void mockSamlAuthentication(IdentityZoneCreationResult zone, String en //perform SAML Login //setup the existing token IdentityZoneHolder.set(zone.getIdentityZone()); - UaaPrincipal invited = new UaaPrincipal(getWebApplicationContext().getBean(UaaUserDatabase.class).retrieveUserByName(invitedEmail, Origin.UNKNOWN)); + UaaPrincipal invited = new UaaPrincipal(getWebApplicationContext().getBean(UaaUserDatabase.class).retrieveUserByName(invitedEmail, Origin.SAML)); UaaAuthentication invitedAuthentication = new UaaAuthentication(invited, Collections.EMPTY_LIST, mock(UaaAuthenticationDetails.class)); ExtendedMetadata metadata = mock(ExtendedMetadata.class); @@ -406,22 +434,10 @@ public void accept_invite_for_ldap_changes_correct_origin() throws Exception {} @Test public void accept_invite_for_existing_user_deletes_invite() throws Exception {} - public MimeMessageWrapper inviteUser(String email, String subdomain) throws Exception { - SecurityContext marissa = MockMvcUtils.utils().getMarissaSecurityContext(getWebApplicationContext()); - MockHttpServletRequestBuilder post = post("/invitations/new.do") - .accept(MediaType.TEXT_HTML) - .param("email", email) - .with(securityContext(marissa)) - .with(csrf()); - if (StringUtils.hasText(subdomain)) { - post.header("Host", subdomain+".localhost"); - } - getMockMvc().perform(post) - .andExpect(status().isFound()) - .andExpect(redirectedUrl("sent")); - - assertEquals(Origin.UNKNOWN, getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("SELECT origin FROM users WHERE username='" + email + "'", String.class)); - + public MimeMessageWrapper inviteUser(String email, String subdomain, String expectedOrigin) throws Exception { + String userInviteToken = MockMvcUtils.utils().getScimInviteUserToken(getMockMvc(), clientId, clientSecret); + InvitationsEndpointMockMvcTests.sendRequestWithToken(userInviteToken, clientId, "example.com", email); + assertEquals(expectedOrigin, getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("SELECT origin FROM users WHERE username='" + email + "'", String.class)); assertEquals(1, fakeJavaMailSender.getSentMessages().size()); MimeMessageWrapper message = fakeJavaMailSender.getSentMessages().get(0); fakeJavaMailSender.clearMessage(); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java index 0c2002c3588..1e278206f05 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java @@ -920,75 +920,6 @@ public void testChangeEmailNoCsrfReturns403AndInvalidRequest() throws Exception .andExpect(redirectedUrl("http://localhost/invalid_request")); } - @Test - public void testCsrfForInvitationPost() throws Exception { - RandomValueStringGenerator generator = new RandomValueStringGenerator(); - SecurityContext marissaContext = MockMvcUtils.utils().getMarissaSecurityContext(getWebApplicationContext()); - - //logged in with valid CSRF - MockHttpServletRequestBuilder post = post("/invitations/new.do") - .with(securityContext(marissaContext)) - .with(csrf()) - .param("email", generator.generate() + "@example.com"); - - getMockMvc().perform(post) - .andExpect(status().isFound()) - .andExpect(redirectedUrl("sent")); - - //logged in, invalid CSRF - post = post("/invitations/new.do") - .with(securityContext(marissaContext)) - .with(csrf().useInvalidToken()) - .param("email", generator.generate()+"@example.com"); - - getMockMvc().perform(post) - .andExpect(status().isFound()) - .andExpect(redirectedUrl("http://localhost/invalid_request")); - - //not logged in, no CSRF - post = post("/invitations/new.do") - .param("email", generator.generate()+"@example.com"); - - getMockMvc().perform(post) - .andExpect(status().isFound()) - .andExpect(redirectedUrl("http://localhost/invalid_request")); - - //not logged in, valid CSRF(can't happen) - post = post("/invitations/new.do") - .with(csrf()) - .param("email", generator.generate()+"@example.com"); - - getMockMvc().perform(post) - .andExpect(status().isFound()) - .andExpect(redirectedUrl("http://localhost/login")); - - } - - @Test - public void test_Invitations_Accept_Get_Security() throws Exception { - getWebApplicationContext().getBean(JdbcTemplate.class).update("DELETE FROM expiring_code_store"); - SecurityContext marissaContext = MockMvcUtils.utils().getMarissaSecurityContext(getWebApplicationContext()); - String email = generator.generate()+"@test.org"; - - MockHttpServletRequestBuilder invite = post("/invitations/new.do") - .with(securityContext(marissaContext)) - .with(csrf()) - .param("email", email); - - getMockMvc().perform(invite) - .andExpect(status().isFound()) - .andExpect(redirectedUrl("sent")); - - String code = getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("SELECT code FROM expiring_code_store", String.class); - assertNotNull("Invite Code Must be Present",code); - - MockHttpServletRequestBuilder accept = get("/invitations/accept") - .param("code", code); - - getMockMvc().perform(accept) - .andExpect(status().isOk()) - .andExpect(content().string(containsString(""))); - } @Test public void testCsrfForInvitationAcceptPost() throws Exception { @@ -998,6 +929,7 @@ public void testCsrfForInvitationAcceptPost() throws Exception { MockHttpServletRequestBuilder post = post("/invitations/accept.do") .with(securityContext(marissaContext)) .with(csrf()) + .param("code","thecode") .param("client_id", "random") .param("password", "password") .param("password_confirmation", "yield_unprocessable_entity"); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java index bd62da28f04..acba6966f88 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java @@ -19,11 +19,15 @@ import org.apache.commons.lang.RandomStringUtils; import org.cloudfoundry.identity.uaa.authentication.Origin; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; +import org.cloudfoundry.identity.uaa.invitations.InvitationsEndpointMockMvcTests; +import org.cloudfoundry.identity.uaa.oauth.client.ClientDetailsModification; import org.cloudfoundry.identity.uaa.rest.SearchResults; import org.cloudfoundry.identity.uaa.scim.ScimGroup; import org.cloudfoundry.identity.uaa.scim.ScimGroupMember; +import org.cloudfoundry.identity.uaa.scim.ScimGroupMembershipManager; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; +import org.cloudfoundry.identity.uaa.scim.exception.MemberAlreadyExistsException; import org.cloudfoundry.identity.uaa.scim.jdbc.JdbcScimUserProvisioning; import org.cloudfoundry.identity.uaa.test.TestApplicationEventListener; import org.cloudfoundry.identity.uaa.test.TestClient; @@ -491,6 +495,28 @@ public String getUserOAuthAccessTokenAuthCode(MockMvc mockMvc, String clientId, } + public String getScimInviteUserToken(MockMvc mockMvc, String clientId, String clientSecret) throws Exception { + String adminToken = getClientCredentialsOAuthAccessToken(mockMvc, "admin", "adminsecret", "", null); + // create a user (with the required permissions) to perform the actual /invite_users action + String username = new RandomValueStringGenerator().generate(); + ScimUser user = new ScimUser(clientId, username, "given-name", "family-name"); + user.setPrimaryEmail("email@example.com"); + user.setPassword("password"); + user = createUser(mockMvc, adminToken, user); + + String scope = "scim.invite"; + ScimGroupMember member = new ScimGroupMember(user.getId(), ScimGroupMember.Type.USER, Arrays.asList(ScimGroupMember.Role.READER)); + + ScimGroup group = getGroup(mockMvc, adminToken, scope); + group.getMembers().add(member); + updateGroup(mockMvc, adminToken, group); + user.getGroups().add(new ScimUser.Group(group.getId(), scope)); + + // get a bearer token for the user + return getUserOAuthAccessToken(mockMvc, clientId, clientSecret, user.getUserName(), "password", "scim.invite"); + } + + public String getClientCredentialsOAuthAccessToken(MockMvc mockMvc, String username, String password, String scope, String subdomain) throws Exception { @@ -593,4 +619,30 @@ public static CookieCsrfPostProcessor cookieCsrf() { } } + public static void createScimClient(MockMvc mockMvc, String adminAccessToken, String id, String secret, String resourceIds, String scopes, List grantTypes, String authorities) throws Exception { + ClientDetailsModification client = new ClientDetailsModification(id, resourceIds, scopes, commaDelineatedGrantTypes(grantTypes), authorities); + client.setClientSecret(secret); + MockHttpServletRequestBuilder createClientPost = post("/oauth/clients") + .header("Authorization", "Bearer " + adminAccessToken) + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsBytes(client)); + mockMvc.perform(createClientPost).andExpect(status().isCreated()); + } + + public enum GrantType { + password, client_credentials, authorization_code, implicit + } + + private static String commaDelineatedGrantTypes(List grantTypes) { + StringBuilder grantTypeCommaDelineated = new StringBuilder(); + for (int i = 0; i < grantTypes.size(); i++) { + if (i > 0) { + grantTypeCommaDelineated.append(","); + } + grantTypeCommaDelineated.append(grantTypes.get(i).name()); + } + return grantTypeCommaDelineated.toString(); + } + } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimGroupEndpointsMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimGroupEndpointsMockMvcTests.java index ed99be42f3f..099b1e395ae 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimGroupEndpointsMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimGroupEndpointsMockMvcTests.java @@ -15,7 +15,6 @@ import org.cloudfoundry.identity.uaa.authentication.Origin; import org.cloudfoundry.identity.uaa.mock.InjectedMockContextTest; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; -import org.cloudfoundry.identity.uaa.oauth.client.ClientDetailsModification; import org.cloudfoundry.identity.uaa.rest.SearchResults; import org.cloudfoundry.identity.uaa.scim.ScimGroup; import org.cloudfoundry.identity.uaa.scim.ScimGroupExternalMember; @@ -43,17 +42,16 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; import org.springframework.security.oauth2.provider.client.BaseClientDetails; -import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import javax.sql.DataSource; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; @@ -62,6 +60,7 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; +import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.createScimClient; import static org.junit.Assert.assertTrue; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; @@ -100,7 +99,8 @@ public void setUp() throws Exception { "clients.read clients.write clients.secret"); String clientId = generator.generate().toLowerCase(); String clientSecret = generator.generate().toLowerCase(); - createScimClient(adminToken, clientId, clientSecret); + String authorities = "scim.read,scim.write,password.write,oauth.approvals,scim.create"; + createScimClient(this.getMockMvc(), adminToken, clientId, clientSecret, "oauth", "foo,bar", Collections.singletonList(MockMvcUtils.GrantType.client_credentials), authorities); scimReadToken = testClient.getClientCredentialsOAuthAccessToken(clientId, clientSecret,"scim.read password.write"); scimWriteToken = testClient.getClientCredentialsOAuthAccessToken(clientId, clientSecret,"scim.write password.write"); @@ -294,7 +294,6 @@ public void testGroupOperations_as_Zone_Admin() throws Exception { ScimGroup.class)); } - @Test @Ignore //we only create DB once - so can no longer run public void testDBisDownDuringCreate() throws Exception { @@ -313,8 +312,6 @@ public void testDBisDownDuringCreate() throws Exception { result.andExpect(status().isServiceUnavailable()); } - - @Test public void testGetGroups() throws Exception { MockHttpServletRequestBuilder get = MockMvcRequestBuilders.get("/Groups") @@ -876,17 +873,6 @@ protected void validateDbMembers(List expected, ScimGro } } - private void createScimClient(String adminAccessToken, String id, String secret) throws Exception { - ClientDetailsModification client = new ClientDetailsModification(id, "oauth", "foo,bar", "client_credentials,password", "scim.read,scim.write,password.write,oauth.approvals"); - client.setClientSecret(secret); - MockHttpServletRequestBuilder createClientPost = post("/oauth/clients") - .header("Authorization", "Bearer " + adminAccessToken) - .accept(APPLICATION_JSON) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsBytes(client)); - getMockMvc().perform(createClientPost).andExpect(status().isCreated()); - } - private ScimUser createUser(String token, Set scopes) throws Exception { ScimUserProvisioning usersRepository = getWebApplicationContext().getBean(ScimUserProvisioning.class); ScimGroupProvisioning groupRepository = getWebApplicationContext().getBean(ScimGroupProvisioning.class); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsMockMvcTests.java index 5f3b1842b1d..f9a46a8cc4d 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsMockMvcTests.java @@ -14,7 +14,6 @@ import org.cloudfoundry.identity.uaa.mock.InjectedMockContextTest; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; -import org.cloudfoundry.identity.uaa.oauth.client.ClientDetailsModification; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; import org.cloudfoundry.identity.uaa.test.TestClient; @@ -30,12 +29,15 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import java.util.Collections; + import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + public class ScimUserEndpointsMockMvcTests extends InjectedMockContextTest { private String scimReadWriteToken; @@ -51,7 +53,8 @@ public void setUp() throws Exception { "clients.read clients.write clients.secret"); String clientId = generator.generate().toLowerCase(); String clientSecret = generator.generate().toLowerCase(); - createScimClient(adminToken, clientId, clientSecret); + String authorities = "scim.read,scim.write,password.write,oauth.approvals,scim.create"; + MockMvcUtils.createScimClient(this.getMockMvc(), adminToken, clientId, clientSecret, "oauth", "foo,bar", Collections.singletonList(MockMvcUtils.GrantType.client_credentials), authorities); scimReadWriteToken = testClient.getClientCredentialsOAuthAccessToken(clientId, clientSecret,"scim.read scim.write password.write"); scimCreateToken = testClient.getClientCredentialsOAuthAccessToken(clientId, clientSecret,"scim.create"); } @@ -310,14 +313,4 @@ public void cannotCreateUserWithInvalidPasswordInDefaultZone() throws Exception .andExpect(jsonPath("$.message").value("Password must be no more than 255 characters in length.")); } - private void createScimClient(String adminAccessToken, String id, String secret) throws Exception { - ClientDetailsModification client = new ClientDetailsModification(id, "oauth", "foo,bar", "client_credentials", "scim.read,scim.write,password.write,oauth.approvals,scim.create"); - client.setClientSecret(secret); - MockHttpServletRequestBuilder createClientPost = post("/oauth/clients") - .header("Authorization", "Bearer " + adminAccessToken) - .accept(APPLICATION_JSON) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsBytes(client)); - getMockMvc().perform(createClientPost).andExpect(status().isCreated()); - } } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserLookupMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserLookupMockMvcTests.java index b8f3553b574..d9326a4fa5a 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserLookupMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserLookupMockMvcTests.java @@ -15,7 +15,6 @@ import org.cloudfoundry.identity.uaa.authentication.Origin; import org.cloudfoundry.identity.uaa.mock.InjectedMockContextTest; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; -import org.cloudfoundry.identity.uaa.oauth.client.ClientDetailsModification; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.test.TestClient; import org.cloudfoundry.identity.uaa.test.UaaTestAccounts; @@ -29,11 +28,12 @@ import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import java.util.Arrays; import java.util.List; import java.util.Map; +import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.createScimClient; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; import static org.springframework.http.MediaType.APPLICATION_JSON; @@ -71,7 +71,8 @@ public void setUp() throws Exception { originalEnabled = getWebApplicationContext().getBean(UserIdConversionEndpoints.class).isEnabled(); getWebApplicationContext().getBean(UserIdConversionEndpoints.class).setEnabled(true); - createScimClient(adminToken, clientId, clientSecret); + String scopes = "scim.userids,scim.me"; + createScimClient(this.getMockMvc(), adminToken, clientId, clientSecret, "scim", scopes, Arrays.asList(new MockMvcUtils.GrantType[] {MockMvcUtils.GrantType.client_credentials, MockMvcUtils.GrantType.password}), "uaa.none"); scimLookupIdUserToken = testClient.getUserOAuthAccessToken(clientId, clientSecret, user.getUserName(), "secr3T", "scim.userids"); if (testUsers==null) { testUsers = createUsers(adminToken, testUserCount); @@ -108,12 +109,11 @@ public void testLookupUsingOnlyOrigin() throws Exception { getMockMvc().perform(post) .andExpect(status().isBadRequest()); - } @Test public void lookupId_DoesntReturnInactiveIdp_ByDefault() throws Exception { - ScimUser scimUser = createInactiveIdp(new RandomValueStringGenerator().generate()+"test-origin"); + ScimUser scimUser = createInactiveIdp(new RandomValueStringGenerator().generate() + "test-origin"); String filter = "(username eq \"" + user.getUserName() + "\" OR username eq \"" + scimUser.getUserName() + "\")"; MockHttpServletRequestBuilder post = post("/ids/Users") @@ -132,7 +132,7 @@ public void lookupId_DoesntReturnInactiveIdp_ByDefault() throws Exception { @Test public void lookupId_ReturnInactiveIdp_WithIncludeInactiveParam() throws Exception { - ScimUser scimUser = createInactiveIdp(new RandomValueStringGenerator().generate()+"test-origin"); + ScimUser scimUser = createInactiveIdp(new RandomValueStringGenerator().generate() + "test-origin"); String filter = "(username eq \"" + user.getUserName() + "\" OR username eq \"" + scimUser.getUserName() + "\")"; MockHttpServletRequestBuilder post = post("/ids/Users") @@ -242,23 +242,11 @@ public void testLookupIdFromUsernamePagination() throws Exception { } - - private static void createScimClient(String adminAccessToken, String id, String secret) throws Exception { - ClientDetailsModification client = new ClientDetailsModification(id, "scim", "scim.userids,scim.me", "client_credentials,password", "uaa.none"); - client.setClientSecret(secret); - MockHttpServletRequestBuilder createClientPost = post("/oauth/clients") - .header("Authorization", "Bearer " + adminAccessToken) - .accept(APPLICATION_JSON) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsBytes(client)); - getMockMvc().perform(createClientPost).andExpect(status().isCreated()); - } - private MockHttpServletRequestBuilder getIdLookupRequest(String token, String username, String operator) { if (operator==null) { operator = "eq"; } - return getIdLookupRequest(token, "username "+operator+" \""+username+"\"", 1, 100); + return getIdLookupRequest(token, "username " + operator + " \"" + username + "\"", 1, 100); } private MockHttpServletRequestBuilder getIdLookupRequest(String token, String filter, int startIndex, int count) { @@ -359,5 +347,4 @@ private ScimUser createInactiveIdp(String originKey) throws Exception { scimUser = MockMvcUtils.utils().createUserInZone(getMockMvc(), adminToken, scimUser, ""); return scimUser; } - -} \ No newline at end of file +} From af3c1b2d6eee4ae410fa319f14df8ad152470d84 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Fri, 25 Sep 2015 15:41:00 -0600 Subject: [PATCH 015/103] Remove Ignore, auth time has been added --- .../cloudfoundry/identity/uaa/oauth/CheckTokenEndpointTests.java | 1 - 1 file changed, 1 deletion(-) diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/CheckTokenEndpointTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/CheckTokenEndpointTests.java index 16dfd1df7a1..1a3da1c553f 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/CheckTokenEndpointTests.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/CheckTokenEndpointTests.java @@ -446,7 +446,6 @@ public void testClientId() { } @Test - @Ignore //TODO once we have this public void validateAuthTime() { Map result = endpoint.checkToken(accessToken.getValue()); assertNotNull(result.get(Claims.AUTH_TIME)); From 791409043c3d7609641b9b5cf13eca38e60c2c6c Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Thu, 17 Sep 2015 14:35:48 -0700 Subject: [PATCH 016/103] Rename allowInternalUserManagement to disableInternalUserManagement Add CLI error message converter [#101791634] https://www.pivotaltracker.com/story/show/101791634 Signed-off-by: Paul Warren Signed-off-by: Leslie Chang --- .../uaa/config/IdentityProviderBootstrap.java | 2 +- ... DisableInternalUserManagementFilter.java} | 6 +- ... DisableUserManagementSecurityFilter.java} | 17 +- .../identity/uaa/zone/IdentityProvider.java | 10 +- ...ternalUserManagementDisabledException.java | 23 ++ .../JdbcIdentityProviderProvisioning.java | 10 +- .../hsqldb/V2_7_0__Allow_User_Management.sql | 1 - .../V2_7_0__Disable_User_Management.sql | 1 + .../mysql/V2_7_0__Allow_User_Management.sql | 1 - .../mysql/V2_7_0__Disable_User_Management.sql | 1 + .../V2_7_0__Allow_User_Management.sql | 1 - .../V2_7_0__Disable_User_Management.sql | 1 + .../config/IdentityProviderBootstrapTest.java | 8 +- .../uaa/zone/IdentityProviderTest.java | 4 +- ...JdbcIdentityProviderProvisioningTests.java | 26 +- docs/UAA-APIs.rst | 28 +- .../main/resources/templates/web/login.html | 4 +- .../endpoints/PasswordResetEndpointTest.java | 66 +---- .../uaa/scim/test/JsonObjectMatcherUtils.java | 84 ++++++ uaa/build.gradle | 1 + .../main/webapp/WEB-INF/spring-servlet.xml | 6 +- .../identity/uaa/mock/util/MockMvcUtils.java | 4 +- ...rnalUserManagementFilterMockMvcTests.java} | 12 +- ...rManagementSecurityFilterMockMvcTest.java} | 257 +++++++++++------- ...IdentityProviderEndpointsMockMvcTests.java | 10 +- 25 files changed, 358 insertions(+), 226 deletions(-) rename common/src/main/java/org/cloudfoundry/identity/uaa/zone/{AllowInternalUserManagementFilter.java => DisableInternalUserManagementFilter.java} (87%) rename common/src/main/java/org/cloudfoundry/identity/uaa/zone/{AllowUserManagementSecurityFilter.java => DisableUserManagementSecurityFilter.java} (74%) create mode 100644 common/src/main/java/org/cloudfoundry/identity/uaa/zone/InternalUserManagementDisabledException.java delete mode 100644 common/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_0__Allow_User_Management.sql create mode 100644 common/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_0__Disable_User_Management.sql delete mode 100644 common/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_7_0__Allow_User_Management.sql create mode 100644 common/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_7_0__Disable_User_Management.sql delete mode 100644 common/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_0__Allow_User_Management.sql create mode 100644 common/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_0__Disable_User_Management.sql create mode 100644 scim/src/test/java/org/cloudfoundry/identity/uaa/scim/test/JsonObjectMatcherUtils.java rename uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/{AllowInternalUserManagementFilterMockMvcTests.java => DisableInternalUserManagementFilterMockMvcTests.java} (78%) rename uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/{AllowUserManagementSecurityFilterMockMvcTest.java => DisableUserManagementSecurityFilterMockMvcTest.java} (52%) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrap.java b/common/src/main/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrap.java index 6f4b18c8b41..7bc997da7f4 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrap.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrap.java @@ -167,7 +167,7 @@ protected void updateDefaultZoneUaaIDP() throws JSONException { IdentityProvider internalIDP = provisioning.retrieveByOrigin(Origin.UAA, IdentityZone.getUaa().getId()); UaaIdentityProviderDefinition identityProviderDefinition = new UaaIdentityProviderDefinition(defaultPasswordPolicy, defaultLockoutPolicy); internalIDP.setConfig(JsonUtils.writeValueAsString(identityProviderDefinition)); - internalIDP.setAllowInternalUserManagement(!disableInternalUserManagement); + internalIDP.setDisableInternalUserManagement(disableInternalUserManagement); String disableInternalAuth = environment.getProperty("disableInternalAuth"); if (disableInternalAuth != null) { internalIDP.setActive(!Boolean.valueOf(disableInternalAuth)); diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/zone/AllowInternalUserManagementFilter.java b/common/src/main/java/org/cloudfoundry/identity/uaa/zone/DisableInternalUserManagementFilter.java similarity index 87% rename from common/src/main/java/org/cloudfoundry/identity/uaa/zone/AllowInternalUserManagementFilter.java rename to common/src/main/java/org/cloudfoundry/identity/uaa/zone/DisableInternalUserManagementFilter.java index d121eb643bc..4cb01cdcfc3 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/zone/AllowInternalUserManagementFilter.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/zone/DisableInternalUserManagementFilter.java @@ -22,7 +22,7 @@ import java.io.IOException; import java.util.regex.Pattern; -public class AllowInternalUserManagementFilter extends OncePerRequestFilter { +public class DisableInternalUserManagementFilter extends OncePerRequestFilter { private final IdentityProviderProvisioning identityProviderProvisioning; @@ -30,7 +30,7 @@ public class AllowInternalUserManagementFilter extends OncePerRequestFilter { private Pattern pattern = Pattern.compile(regex); - public AllowInternalUserManagementFilter(IdentityProviderProvisioning identityProviderProvisioning) { + public DisableInternalUserManagementFilter(IdentityProviderProvisioning identityProviderProvisioning) { this.identityProviderProvisioning = identityProviderProvisioning; } @@ -39,7 +39,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse if (matches(request)) { IdentityProvider idp = identityProviderProvisioning.retrieveByOrigin(Origin.UAA, IdentityZoneHolder.get().getId()); - request.setAttribute("allowInternalUserManagement", idp.isAllowInternalUserManagement()); + request.setAttribute("disableInternalUserManagement", idp.isDisableInternalUserManagement()); } filterChain.doFilter(request, response); diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/zone/AllowUserManagementSecurityFilter.java b/common/src/main/java/org/cloudfoundry/identity/uaa/zone/DisableUserManagementSecurityFilter.java similarity index 74% rename from common/src/main/java/org/cloudfoundry/identity/uaa/zone/AllowUserManagementSecurityFilter.java rename to common/src/main/java/org/cloudfoundry/identity/uaa/zone/DisableUserManagementSecurityFilter.java index 3de581d620b..c6fc8041b76 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/zone/AllowUserManagementSecurityFilter.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/zone/DisableUserManagementSecurityFilter.java @@ -13,6 +13,10 @@ package org.cloudfoundry.identity.uaa.zone; import org.cloudfoundry.identity.uaa.authentication.Origin; +import org.cloudfoundry.identity.uaa.error.ExceptionReport; +import org.cloudfoundry.identity.uaa.error.ExceptionReportHttpMessageConverter; +import org.springframework.http.MediaType; +import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; @@ -24,7 +28,7 @@ import java.util.List; import java.util.regex.Pattern; -public class AllowUserManagementSecurityFilter extends OncePerRequestFilter { +public class DisableUserManagementSecurityFilter extends OncePerRequestFilter { private final IdentityProviderProvisioning identityProviderProvisioning; @@ -53,17 +57,20 @@ public class AllowUserManagementSecurityFilter extends OncePerRequestFilter { private Pattern pattern = Pattern.compile(regex); private List methods = Arrays.asList("GET", "POST", "PUT", "DELETE"); - public AllowUserManagementSecurityFilter(IdentityProviderProvisioning identityProviderProvisioning) { + public DisableUserManagementSecurityFilter(IdentityProviderProvisioning identityProviderProvisioning) { this.identityProviderProvisioning = identityProviderProvisioning; } @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + protected void doFilterInternal(HttpServletRequest request, final HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (matches(request)) { IdentityProvider idp = identityProviderProvisioning.retrieveByOrigin(Origin.UAA, IdentityZoneHolder.get().getId()); - if (!idp.isAllowInternalUserManagement()) { - response.sendError(HttpServletResponse.SC_FORBIDDEN, "Internal User Creation is currently disabled. External User Store is in use."); + if (idp.isDisableInternalUserManagement()) { + ExceptionReportHttpMessageConverter converter = new ExceptionReportHttpMessageConverter(); + response.setStatus(403); + converter.write(new ExceptionReport(new InternalUserManagementDisabledException("Internal User Creation is currently disabled. External User Store is in use.")), + MediaType.APPLICATION_JSON, new ServletServerHttpResponse(response)); return; } } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProvider.java b/common/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProvider.java index 7c4511e0dfd..29805bb4276 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProvider.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProvider.java @@ -49,7 +49,7 @@ public class IdentityProvider { private String identityZoneId; - private boolean allowInternalUserManagement = true; + private boolean disableInternalUserManagement = false; public Date getCreated() { return created; @@ -257,11 +257,11 @@ public String toString() { return sb.toString(); } - public boolean isAllowInternalUserManagement() { - return allowInternalUserManagement; + public boolean isDisableInternalUserManagement() { + return disableInternalUserManagement; } - public void setAllowInternalUserManagement(boolean allowInternalUserManagement) { - this.allowInternalUserManagement = allowInternalUserManagement; + public void setDisableInternalUserManagement(boolean disableInternalUserManagement) { + this.disableInternalUserManagement = disableInternalUserManagement; } } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/zone/InternalUserManagementDisabledException.java b/common/src/main/java/org/cloudfoundry/identity/uaa/zone/InternalUserManagementDisabledException.java new file mode 100644 index 00000000000..f22c147e415 --- /dev/null +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/zone/InternalUserManagementDisabledException.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2014] Pivotal Software, Inc. All Rights Reserved. + * + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +package org.cloudfoundry.identity.uaa.zone; + +import org.cloudfoundry.identity.uaa.error.UaaException; + +public class InternalUserManagementDisabledException extends UaaException { + + public InternalUserManagementDisabledException(String msg) { + super("internal_user_management_disabled", msg, 403); + } + +} diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityProviderProvisioning.java b/common/src/main/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityProviderProvisioning.java index bf41d746cae..1951178fd92 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityProviderProvisioning.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityProviderProvisioning.java @@ -33,7 +33,7 @@ public class JdbcIdentityProviderProvisioning implements IdentityProviderProvisioning { - public static final String ID_PROVIDER_FIELDS = "id,version,created,lastmodified,name,origin_key,type,config,identity_zone_id,active,allow_internal_user_management"; + public static final String ID_PROVIDER_FIELDS = "id,version,created,lastmodified,name,origin_key,type,config,identity_zone_id,active,disable_internal_user_management"; public static final String CREATE_IDENTITY_PROVIDER_SQL = "insert into identity_provider(" + ID_PROVIDER_FIELDS + ") values (?,?,?,?,?,?,?,?,?,?,?)"; @@ -41,7 +41,7 @@ public class JdbcIdentityProviderProvisioning implements IdentityProviderProvisi public static final String IDENTITY_ACTIVE_PROVIDERS_QUERY = IDENTITY_PROVIDERS_QUERY + " and active"; - public static final String ID_PROVIDER_UPDATE_FIELDS = "version,lastmodified,name,type,config,active,allow_internal_user_management".replace(",","=?,")+"=?"; + public static final String ID_PROVIDER_UPDATE_FIELDS = "version,lastmodified,name,type,config,active,disable_internal_user_management".replace(",","=?,")+"=?"; public static final String UPDATE_IDENTITY_PROVIDER_SQL = "update identity_provider set " + ID_PROVIDER_UPDATE_FIELDS + " where id=?"; @@ -103,7 +103,7 @@ public void setValues(PreparedStatement ps) throws SQLException { ps.setString(pos++, identityProvider.getConfig()); ps.setString(pos++, identityProvider.getIdentityZoneId()); ps.setBoolean(pos++, identityProvider.isActive()); - ps.setBoolean(pos++, identityProvider.isAllowInternalUserManagement()); + ps.setBoolean(pos++, identityProvider.isDisableInternalUserManagement()); } }); } catch (DuplicateKeyException e) { @@ -125,7 +125,7 @@ public void setValues(PreparedStatement ps) throws SQLException { ps.setString(pos++, identityProvider.getType()); ps.setString(pos++, identityProvider.getConfig()); ps.setBoolean(pos++, identityProvider.isActive()); - ps.setBoolean(pos++, identityProvider.isAllowInternalUserManagement()); + ps.setBoolean(pos++, identityProvider.isDisableInternalUserManagement()); ps.setString(pos++, identityProvider.getId().trim()); } }); @@ -163,7 +163,7 @@ public IdentityProvider mapRow(ResultSet rs, int rowNum) throws SQLException { identityProvider.setConfig(rs.getString(pos++)); identityProvider.setIdentityZoneId(rs.getString(pos++)); identityProvider.setActive(rs.getBoolean(pos++)); - identityProvider.setAllowInternalUserManagement(rs.getBoolean(pos++)); + identityProvider.setDisableInternalUserManagement(rs.getBoolean(pos++)); return identityProvider; } } diff --git a/common/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_0__Allow_User_Management.sql b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_0__Allow_User_Management.sql deleted file mode 100644 index 5ae4368fc62..00000000000 --- a/common/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_0__Allow_User_Management.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE identity_provider ADD COLUMN allow_internal_user_management BOOLEAN default true; diff --git a/common/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_0__Disable_User_Management.sql b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_0__Disable_User_Management.sql new file mode 100644 index 00000000000..8a9b31ac038 --- /dev/null +++ b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_0__Disable_User_Management.sql @@ -0,0 +1 @@ +ALTER TABLE identity_provider ADD COLUMN disable_internal_user_management BOOLEAN default false; diff --git a/common/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_7_0__Allow_User_Management.sql b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_7_0__Allow_User_Management.sql deleted file mode 100644 index 5ae4368fc62..00000000000 --- a/common/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_7_0__Allow_User_Management.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE identity_provider ADD COLUMN allow_internal_user_management BOOLEAN default true; diff --git a/common/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_7_0__Disable_User_Management.sql b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_7_0__Disable_User_Management.sql new file mode 100644 index 00000000000..8a9b31ac038 --- /dev/null +++ b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_7_0__Disable_User_Management.sql @@ -0,0 +1 @@ +ALTER TABLE identity_provider ADD COLUMN disable_internal_user_management BOOLEAN default false; diff --git a/common/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_0__Allow_User_Management.sql b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_0__Allow_User_Management.sql deleted file mode 100644 index 5ae4368fc62..00000000000 --- a/common/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_0__Allow_User_Management.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE identity_provider ADD COLUMN allow_internal_user_management BOOLEAN default true; diff --git a/common/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_0__Disable_User_Management.sql b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_0__Disable_User_Management.sql new file mode 100644 index 00000000000..8a9b31ac038 --- /dev/null +++ b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_0__Disable_User_Management.sql @@ -0,0 +1 @@ +ALTER TABLE identity_provider ADD COLUMN disable_internal_user_management BOOLEAN default false; diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrapTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrapTest.java index e16e26cbb81..ac1ced7e66f 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrapTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrapTest.java @@ -383,17 +383,15 @@ private void setDisableInternalUserManagement(String expectedValue) throws Excep IdentityProviderBootstrap bootstrap = new IdentityProviderBootstrap(provisioning, mock); IdentityProvider internalIDP = provisioning.retrieveByOrigin(Origin.UAA, IdentityZone.getUaa().getId()); - assertTrue(internalIDP.isAllowInternalUserManagement()); + assertFalse(internalIDP.isDisableInternalUserManagement()); bootstrap.afterPropertiesSet(); internalIDP = provisioning.retrieveByOrigin(Origin.UAA, IdentityZone.getUaa().getId()); - if (expectedValue != null && expectedValue.equals("true")) { + if (expectedValue == null) { expectedValue = "false"; - } else { - expectedValue = "true"; } - assertEquals(Boolean.valueOf(expectedValue), internalIDP.isAllowInternalUserManagement()); + assertEquals(Boolean.valueOf(expectedValue), internalIDP.isDisableInternalUserManagement()); } @Test diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityProviderTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityProviderTest.java index d1e38784947..e95f866ec7f 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityProviderTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityProviderTest.java @@ -20,9 +20,9 @@ public class IdentityProviderTest { @Test - public void allowUserManagementDefaultsToTrue() { + public void disableUserManagementDefaultsToFalse() { IdentityProvider identityProvider = new IdentityProvider(); - assertTrue(identityProvider.isAllowInternalUserManagement()); + assertFalse(identityProvider.isDisableInternalUserManagement()); } @Test diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityProviderProvisioningTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityProviderProvisioningTests.java index 12bb0e7fe08..2c00aea2a57 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityProviderProvisioningTests.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityProviderProvisioningTests.java @@ -34,8 +34,8 @@ public void testCreateAndUpdateIdentityProviderInDefaultZone() throws Exception String zoneId = IdentityZone.getUaa().getId(); String originKey = RandomStringUtils.randomAlphabetic(6); IdentityProvider idp = MultitenancyFixture.identityProvider(originKey, zoneId); - boolean allowInternalUserManagement = false; - idp.setAllowInternalUserManagement(allowInternalUserManagement); + boolean disableInternalUserManagement = false; + idp.setDisableInternalUserManagement(disableInternalUserManagement); IdentityProvider createdIdp = db.create(idp); Map rawCreatedIdp = jdbcTemplate.queryForMap("select * from identity_provider where id = ?",createdIdp.getId()); @@ -68,7 +68,7 @@ public void testCreateAndUpdateIdentityProviderInDefaultZone() throws Exception assertEquals(idp.getLastModified().getTime()/1000, createdIdp.getLastModified().getTime()/1000); assertEquals(Integer.valueOf(rawCreatedIdp.get("version").toString())+1, createdIdp.getVersion()); assertEquals(zoneId, createdIdp.getIdentityZoneId()); - assertEquals(allowInternalUserManagement, createdIdp.isAllowInternalUserManagement()); + assertEquals(disableInternalUserManagement, createdIdp.isDisableInternalUserManagement()); } @Test @@ -77,8 +77,8 @@ public void testCreateIdentityProviderInOtherZone() throws Exception { String originKey = RandomStringUtils.randomAlphabetic(6); IdentityProvider idp = MultitenancyFixture.identityProvider(originKey, zone.getId()); - boolean allowUserManagement = false; - idp.setAllowInternalUserManagement(allowUserManagement); + boolean disableUserManagement = false; + idp.setDisableInternalUserManagement(disableUserManagement); IdentityProvider createdIdp = db.create(idp); Map rawCreatedIdp = jdbcTemplate.queryForMap("select * from identity_provider where id = ?",createdIdp.getId()); @@ -93,7 +93,7 @@ public void testCreateIdentityProviderInOtherZone() throws Exception { assertEquals(idp.getType(), rawCreatedIdp.get("type")); assertEquals(idp.getConfig(), rawCreatedIdp.get("config")); assertEquals(zone.getId(), rawCreatedIdp.get("identity_zone_id")); - assertEquals(allowUserManagement, createdIdp.isAllowInternalUserManagement()); + assertEquals(disableUserManagement, createdIdp.isDisableInternalUserManagement()); } @Test(expected=IdpAlreadyExistsException.class) @@ -130,16 +130,16 @@ public void testUpdateIdentityProviderInDefaultZone() throws Exception { String originKey = RandomStringUtils.randomAlphabetic(6); String idpId = RandomStringUtils.randomAlphabetic(6); IdentityProvider idp = MultitenancyFixture.identityProvider(originKey, zoneId); - idp.setAllowInternalUserManagement(false); + idp.setDisableInternalUserManagement(false); idp.setId(idpId); idp = db.create(idp); - assertFalse(idp.isAllowInternalUserManagement()); + assertFalse(idp.isDisableInternalUserManagement()); String newConfig = RandomStringUtils.randomAlphanumeric(1024); idp.setConfig(newConfig); - idp.setAllowInternalUserManagement(true); + idp.setDisableInternalUserManagement(true); IdentityProvider updatedIdp = db.update(idp); - assertTrue(updatedIdp.isAllowInternalUserManagement()); + assertTrue(updatedIdp.isDisableInternalUserManagement()); Map rawUpdatedIdp = jdbcTemplate.queryForMap("select * from identity_provider where id = ?",updatedIdp.getId()); @@ -154,15 +154,15 @@ public void testUpdateIdentityProviderInOtherZone() throws Exception { String originKey = RandomStringUtils.randomAlphabetic(6); String idpId = RandomStringUtils.randomAlphabetic(6); IdentityProvider idp = MultitenancyFixture.identityProvider(originKey, zone.getId()); - idp.setAllowInternalUserManagement(false); + idp.setDisableInternalUserManagement(false); idp.setId(idpId); idp = db.create(idp); String newConfig = RandomStringUtils.randomAlphanumeric(1024); idp.setConfig(newConfig); - idp.setAllowInternalUserManagement(true); + idp.setDisableInternalUserManagement(true); IdentityProvider updatedIdp = db.update(idp); - assertTrue(updatedIdp.isAllowInternalUserManagement()); + assertTrue(updatedIdp.isDisableInternalUserManagement()); Map rawUpdatedIdp = jdbcTemplate.queryForMap("select * from identity_provider where id = ?",updatedIdp.getId()); diff --git a/docs/UAA-APIs.rst b/docs/UAA-APIs.rst index 7ab32d45568..69822110b4e 100644 --- a/docs/UAA-APIs.rst +++ b/docs/UAA-APIs.rst @@ -1015,7 +1015,7 @@ Response body *example* :: "version":0, "created":1426260091149, "active":true, - "allowInternalUserManagement":true, + "disableInternalUserManagement":false, "identityZoneId":"testzone1", "last_modified":1426260091149 } @@ -1037,7 +1037,7 @@ Request body *example* :: "version":0, "created":1426260091149, "active":true, - "allowInternalUserManagement":true, + "disableInternalUserManagement":false, "identityZoneId":"testzone1" } @@ -1055,7 +1055,7 @@ Response body *example* :: "version":0, "created":1426260091149, "active":true, - "allowInternalUserManagement":true, + "disableInternalUserManagement":false, "identityZoneId":"testzone1", "last_modified":1426260091149 } @@ -1072,17 +1072,17 @@ Response body *example* :: Fields *Available Fields* :: Identity Provider Fields - ====================== =============== ======== ======================================================= - id String(36) Auto Unique identifier for this provider - GUID generated by the UAA - name String(255) Required Human readable name for this provider - type String Required Value must be either "saml", "ldap" or "internal" - originKey String Required Must be either an alias for a SAML provider or the value "ldap" for an LDAP provider. If the type is "internal", the originKey is "uaa" - config String Required IDP Configuration in JSON format, see below - active boolean Optional When set to true, this provider is active. When a provider is deleted this value is set to false - allowInternalUserManagement boolean Optional When set to true (default), this provider allows users to be managed. (Effectively only used by the internal identity provider) - identityZoneId String Auto Set to the zone that this provider will be active in. Determined either by the Host header or the zone switch header - created epoch timestamp Auto UAA sets the creation date - last_modified epoch timestamp Auto UAA sets the modification date + ================================ =============== ======== ======================================================= + id String(36) Auto Unique identifier for this provider - GUID generated by the UAA + name String(255) Required Human readable name for this provider + type String Required Value must be either "saml", "ldap" or "internal" + originKey String Required Must be either an alias for a SAML provider or the value "ldap" for an LDAP provider. If the type is "internal", the originKey is "uaa" + config String Required IDP Configuration in JSON format, see below + active boolean Optional When set to true, this provider is active. When a provider is deleted this value is set to false + disableInternalUserManagement boolean Optional When set to true, this provider disables user management + identityZoneId String Auto Set to the zone that this provider will be active in. Determined either by the Host header or the zone switch header + created epoch timestamp Auto UAA sets the creation date + last_modified epoch timestamp Auto UAA sets the modification date UAA Provider Configuration (provided in JSON format as part of the ``config`` field on the Identity Provider - See class org.cloudfoundry.identity.uaa.zone.UaaIdentityProviderDefinition ====================== =============== ======== ================================================================================================================================================================================================= diff --git a/login/src/main/resources/templates/web/login.html b/login/src/main/resources/templates/web/login.html index 6208549b632..59ef21d88ea 100644 --- a/login/src/main/resources/templates/web/login.html +++ b/login/src/main/resources/templates/web/login.html @@ -27,7 +27,7 @@

Use your corporate credentials - + Create account @@ -37,4 +37,4 @@

{ - - private final JSONObject expected; - - public JsonObjectMatcher(JSONObject expected) { - this.expected = expected; - } - - public static Matcher matchesJsonObject(JSONObject expected){ - return new JsonObjectMatcher(expected); - } - - @Override - public boolean matches(Object item) { - - if(!String.class.isInstance(item)){ - return false; - } - - if(this.expected == null && "null".equals(item)){ - return true; - } - - JSONObject actual = null; - try { - actual = new JSONObject(new JSONTokener(item.toString())); - } catch (JSONException e) { - return false; - } - - if(this.expected.length() != actual.length()) { - return false; - } - - JSONArray names = actual.names(); - for(int i = 0, len = names.length(); i < len; i++){ - - try { - String name = names.getString(i); - if(!Objects.equals(expected.get(name), actual.get(name))){ - return false; - } - } catch (JSONException e) { - return false; - } - } - - return true; - } - - @Override - public void describeTo(Description description) { - description.appendValue(expected); - } + .andExpect(content().string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject().put("message", "Your new password cannot be the same as the old password.").put("error", "invalid_password")))); } } diff --git a/scim/src/test/java/org/cloudfoundry/identity/uaa/scim/test/JsonObjectMatcherUtils.java b/scim/src/test/java/org/cloudfoundry/identity/uaa/scim/test/JsonObjectMatcherUtils.java new file mode 100644 index 00000000000..db608580f4a --- /dev/null +++ b/scim/src/test/java/org/cloudfoundry/identity/uaa/scim/test/JsonObjectMatcherUtils.java @@ -0,0 +1,84 @@ +/* + * ***************************************************************************** + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + * ***************************************************************************** + */ + +package org.cloudfoundry.identity.uaa.scim.test; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +import java.util.Objects; + +/** + * A {@link org.hamcrest.Matcher} that matches the {@link org.json.JSONObject} represented by the given {@link String} + * in an order-insensitive way against an expected {@link org.json.JSONObject}. + */ +public class JsonObjectMatcherUtils extends BaseMatcher { + + private final JSONObject expected; + + public JsonObjectMatcherUtils(JSONObject expected) { + this.expected = expected; + } + + public static Matcher matchesJsonObject(JSONObject expected){ + return new JsonObjectMatcherUtils(expected); + } + + @Override + public boolean matches(Object item) { + + if(!String.class.isInstance(item)){ + return false; + } + + if(this.expected == null && "null".equals(item)){ + return true; + } + + JSONObject actual = null; + try { + actual = new JSONObject(new JSONTokener(item.toString())); + } catch (JSONException e) { + return false; + } + + if(this.expected.length() != actual.length()) { + return false; + } + + JSONArray names = actual.names(); + for(int i = 0, len = names.length(); i < len; i++){ + + try { + String name = names.getString(i); + if(!Objects.equals(expected.get(name), actual.get(name))){ + return false; + } + } catch (JSONException e) { + return false; + } + } + + return true; + } + + @Override + public void describeTo(Description description) { + description.appendValue(expected); + } +} diff --git a/uaa/build.gradle b/uaa/build.gradle index cba2272426d..305787be10f 100644 --- a/uaa/build.gradle +++ b/uaa/build.gradle @@ -46,6 +46,7 @@ dependencies { testCompile identityCommon.configurations.testCompile.dependencies testCompile identityCommon.sourceSets.test.output + testCompile identityScim.sourceSets.test.output testCompile identityLogin.sourceSets.test.output testCompile(group: 'org.apache.directory.server', name: 'apacheds-core', version:'1.5.5') { diff --git a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml index 62a32030d7d..a0a8bdd1fc7 100755 --- a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml +++ b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml @@ -93,18 +93,18 @@ + key="#{T(org.cloudfoundry.identity.uaa.security.web.SecurityFilterChainPostProcessor.FilterPosition).after(T(org.cloudfoundry.identity.uaa.zone.DisableUserManagementSecurityFilter))}"/> - + - + diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java index acba6966f88..ca2026bc49f 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java @@ -127,10 +127,10 @@ public static MockMvcUtils utils() { return new MockMvcUtils(); } - public static void setInternalUserManagement(boolean allowUserManagement, ApplicationContext applicationContext) { + public static void setDisableInternalUserManagement(boolean disableUserManagement, ApplicationContext applicationContext) { IdentityProviderProvisioning identityProviderProvisioning = applicationContext.getBean(IdentityProviderProvisioning.class); IdentityProvider idp = identityProviderProvisioning.retrieveByOrigin(Origin.UAA, "uaa"); - idp.setAllowInternalUserManagement(allowUserManagement); + idp.setDisableInternalUserManagement(disableUserManagement); identityProviderProvisioning.update(idp); } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/AllowInternalUserManagementFilterMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/DisableInternalUserManagementFilterMockMvcTests.java similarity index 78% rename from uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/AllowInternalUserManagementFilterMockMvcTests.java rename to uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/DisableInternalUserManagementFilterMockMvcTests.java index 98495609ae8..bcc0c188c51 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/AllowInternalUserManagementFilterMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/DisableInternalUserManagementFilterMockMvcTests.java @@ -14,17 +14,23 @@ import org.cloudfoundry.identity.uaa.mock.InjectedMockContextTest; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; +import org.junit.After; import org.junit.Test; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.xpath; -public class AllowInternalUserManagementFilterMockMvcTests extends InjectedMockContextTest{ +public class DisableInternalUserManagementFilterMockMvcTests extends InjectedMockContextTest{ + + @After + public void resetInternalUserManagement() { + MockMvcUtils.setDisableInternalUserManagement(false, getWebApplicationContext()); + } @Test public void createAccountNotEnabled() throws Exception { - MockMvcUtils.setInternalUserManagement(false, getWebApplicationContext()); + MockMvcUtils.setDisableInternalUserManagement(true, getWebApplicationContext()); getMockMvc().perform(get("/login")) .andExpect(status().isOk()) @@ -33,7 +39,7 @@ public void createAccountNotEnabled() throws Exception { @Test public void resetPasswordNotEnabled() throws Exception { - MockMvcUtils.setInternalUserManagement(false, getWebApplicationContext()); + MockMvcUtils.setDisableInternalUserManagement(true, getWebApplicationContext()); getMockMvc().perform(get("/login")) .andExpect(status().isOk()) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/AllowUserManagementSecurityFilterMockMvcTest.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/DisableUserManagementSecurityFilterMockMvcTest.java similarity index 52% rename from uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/AllowUserManagementSecurityFilterMockMvcTest.java rename to uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/DisableUserManagementSecurityFilterMockMvcTest.java index 305e9e0949c..6ef92108c80 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/AllowUserManagementSecurityFilterMockMvcTest.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/DisableUserManagementSecurityFilterMockMvcTest.java @@ -10,6 +10,7 @@ import org.cloudfoundry.identity.uaa.scim.endpoints.PasswordChange; import org.cloudfoundry.identity.uaa.test.TestClient; import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.json.JSONObject; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -24,6 +25,7 @@ import java.util.HashMap; import java.util.Map; +import org.cloudfoundry.identity.uaa.scim.test.JsonObjectMatcherUtils; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -33,15 +35,18 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.CookieCsrfPostProcessor; @Component -public class AllowUserManagementSecurityFilterMockMvcTest extends InjectedMockContextTest { +public class DisableUserManagementSecurityFilterMockMvcTest extends InjectedMockContextTest { public static final String PASSWD = "passwd"; public static final String ACCEPT_TEXT_HTML = "text/html"; + public static final String ERROR_TEXT = "internal_user_management_disabled"; + public static final String MESSAGE_TEXT = "Internal User Creation is currently disabled. External User Store is in use."; private TestClient testClient; private String token; @@ -61,65 +66,76 @@ public void setUp() throws Exception { @After public void tearDown() { - MockMvcUtils.setInternalUserManagement(true, getWebApplicationContext()); + MockMvcUtils.setDisableInternalUserManagement(false, getWebApplicationContext()); } @Test public void userEndpointCreateNotAllowed() throws Exception { - MockMvcUtils.setInternalUserManagement(false, getWebApplicationContext()); + MockMvcUtils.setDisableInternalUserManagement(true, getWebApplicationContext()); ResultActions result = createUser(); - result.andExpect(status().isForbidden()); - assertEquals("Internal User Creation is currently disabled. External User Store is in use.", - result.andReturn().getResponse().getErrorMessage()); + result.andExpect(status().isForbidden()) + .andExpect(content() + .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @Test public void userEndpointUpdateNotAllowed() throws Exception { - MockMvcUtils.setInternalUserManagement(true, getWebApplicationContext()); + MockMvcUtils.setDisableInternalUserManagement(false, getWebApplicationContext()); ResultActions result = createUser(); ScimUser createdUser = JsonUtils.readValue(result.andReturn().getResponse().getContentAsString(), ScimUser.class); - MockMvcUtils.setInternalUserManagement(false, getWebApplicationContext()); - String errorMessage = getMockMvc().perform(put("/Users/" + createdUser.getId()) + MockMvcUtils.setDisableInternalUserManagement(true, getWebApplicationContext()); + getMockMvc().perform(put("/Users/" + createdUser.getId()) .header("Authorization", "Bearer " + token) .header("If-Match", "\"" + createdUser.getVersion() + "\"") .accept(APPLICATION_JSON) .contentType(APPLICATION_JSON) .content(JsonUtils.writeValueAsString(createdUser))) - .andExpect(status().isForbidden()).andReturn().getResponse().getErrorMessage(); - assertEquals("Internal User Creation is currently disabled. External User Store is in use.", errorMessage); + .andExpect(status().isForbidden()) + .andExpect(content() + .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @Test public void userEndpointUpdatePasswordNotAllowed() throws Exception { - MockMvcUtils.setInternalUserManagement(true, getWebApplicationContext()); + MockMvcUtils.setDisableInternalUserManagement(false, getWebApplicationContext()); ResultActions result = createUser(); ScimUser createdUser = JsonUtils.readValue(result.andReturn().getResponse().getContentAsString(), ScimUser.class); - MockMvcUtils.setInternalUserManagement(false, getWebApplicationContext()); + MockMvcUtils.setDisableInternalUserManagement(true, getWebApplicationContext()); PasswordChangeRequest request = new PasswordChangeRequest(); request.setOldPassword(PASSWD); request.setPassword("n3wAw3som3Passwd"); - String errorMessage = getMockMvc().perform(put("/Users/" + createdUser.getId() + "/password") + getMockMvc().perform(put("/Users/" + createdUser.getId() + "/password") .header("Authorization", "Bearer " + token) .contentType(APPLICATION_JSON) .content(JsonUtils.writeValueAsString(request))) - .andExpect(status().isForbidden()).andReturn().getResponse().getErrorMessage(); - assertEquals("Internal User Creation is currently disabled. External User Store is in use.", errorMessage); + .andExpect(status().isForbidden()) + .andExpect(content() + .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @Test public void userEndpointDeleteNotAllowed() throws Exception { - MockMvcUtils.setInternalUserManagement(true, getWebApplicationContext()); + MockMvcUtils.setDisableInternalUserManagement(false, getWebApplicationContext()); ResultActions result = createUser(); ScimUser createdUser = JsonUtils.readValue(result.andReturn().getResponse().getContentAsString(), ScimUser.class); - MockMvcUtils.setInternalUserManagement(false, getWebApplicationContext()); - String errorMessage = getMockMvc().perform(delete("/Users/" + createdUser.getId()) + MockMvcUtils.setDisableInternalUserManagement(true, getWebApplicationContext()); + getMockMvc().perform(delete("/Users/" + createdUser.getId()) .header("Authorization", "Bearer " + token)) - .andExpect(status().isForbidden()).andReturn().getResponse().getErrorMessage(); - assertEquals("Internal User Creation is currently disabled. External User Store is in use.", errorMessage); + .andExpect(status().isForbidden()) + .andExpect(content() + .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @Test @@ -130,57 +146,72 @@ public void userEndpointGetUsersNotAllowed() throws Exception { "adminsecret", "scim.read"); - MockMvcUtils.setInternalUserManagement(false, getWebApplicationContext()); - String errorMessage = getMockMvc().perform(get("/Users") + MockMvcUtils.setDisableInternalUserManagement(true, getWebApplicationContext()); + getMockMvc().perform(get("/Users") .header("Authorization", "Bearer " + adminToken)) - .andExpect(status().isForbidden()).andReturn().getResponse().getErrorMessage(); - assertEquals("Internal User Creation is currently disabled. External User Store is in use.", errorMessage); + .andExpect(status().isForbidden()) + .andExpect(content() + .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @Test public void userEndpointVerifyUsersNotAllowed() throws Exception { - MockMvcUtils.setInternalUserManagement(true, getWebApplicationContext()); + MockMvcUtils.setDisableInternalUserManagement(false, getWebApplicationContext()); ResultActions result = createUser(); ScimUser createdUser = JsonUtils.readValue(result.andReturn().getResponse().getContentAsString(), ScimUser.class); - MockMvcUtils.setInternalUserManagement(false, getWebApplicationContext()); - String errorMessage = getMockMvc().perform(get("/Users/" + createdUser.getId() + "/verify") + MockMvcUtils.setDisableInternalUserManagement(true, getWebApplicationContext()); + getMockMvc().perform(get("/Users/" + createdUser.getId() + "/verify") .header("Authorization", "Bearer " + token)) - .andExpect(status().isForbidden()).andReturn().getResponse().getErrorMessage(); - assertEquals("Internal User Creation is currently disabled. External User Store is in use.", errorMessage); + .andExpect(status().isForbidden()) + .andExpect(content() + .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @Test public void accountsControllerCreateAccountNotAllowed() throws Exception { - MockMvcUtils.setInternalUserManagement(false, getWebApplicationContext()); - String errorMessage = getMockMvc().perform(get("/create_account")) - .andExpect(status().isForbidden()).andReturn().getResponse().getErrorMessage(); - assertEquals("Internal User Creation is currently disabled. External User Store is in use.", errorMessage); + MockMvcUtils.setDisableInternalUserManagement(true, getWebApplicationContext()); + getMockMvc().perform(get("/create_account")) + .andExpect(status().isForbidden()) + .andExpect(content() + .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @Test public void accountsControllerSendActivationEmailNotAllowed() throws Exception { - MockMvcUtils.setInternalUserManagement(false, getWebApplicationContext()); - String errorMessage = getMockMvc().perform(post("/create_account.do") + MockMvcUtils.setDisableInternalUserManagement(true, getWebApplicationContext()); + getMockMvc().perform(post("/create_account.do") .param("client_id", "login") .param("email", "another@example.com") .param("password", "foobar") .param("password_confirmation", "foobar")) - .andExpect(status().isForbidden()).andReturn().getResponse().getErrorMessage(); - assertEquals("Internal User Creation is currently disabled. External User Store is in use.", errorMessage); + .andExpect(status().isForbidden()) + .andExpect(content() + .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @Test public void accountsControllerEmailSentNotAllowed() throws Exception { - MockMvcUtils.setInternalUserManagement(false, getWebApplicationContext()); - String errorMessage = getMockMvc().perform(get("/accounts/email_sent")) - .andExpect(status().isForbidden()).andReturn().getResponse().getErrorMessage(); - assertEquals("Internal User Creation is currently disabled. External User Store is in use.", errorMessage); + MockMvcUtils.setDisableInternalUserManagement(true, getWebApplicationContext()); + getMockMvc().perform(get("/accounts/email_sent")) + .andExpect(status().isForbidden()) + .andExpect(content() + .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @Test public void accountsControllerVerifyUserNotAllowed() throws Exception { - MockMvcUtils.setInternalUserManagement(true, getWebApplicationContext()); + MockMvcUtils.setDisableInternalUserManagement(false, getWebApplicationContext()); ResultActions result = createUser(); ScimUser createdUser = JsonUtils.readValue(result.andReturn().getResponse().getContentAsString(), ScimUser.class); @@ -188,48 +219,58 @@ public void accountsControllerVerifyUserNotAllowed() throws Exception { codeData.put("user_id", createdUser.getId()); codeData.put("client_id", "login"); - MockMvcUtils.setInternalUserManagement(false, getWebApplicationContext()); - String errorMessage = getMockMvc().perform(get("/verify_user") + MockMvcUtils.setDisableInternalUserManagement(true, getWebApplicationContext()); + getMockMvc().perform(get("/verify_user") .param("code", getExpiringCode(codeData).getCode())) - .andExpect(status().isForbidden()).andReturn().getResponse().getErrorMessage(); - assertEquals("Internal User Creation is currently disabled. External User Store is in use.", errorMessage); + .andExpect(status().isForbidden()) + .andExpect(content() + .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @Test public void changeEmailControllerChangeEmailPageNotAllowed() throws Exception { - MockMvcUtils.setInternalUserManagement(true, getWebApplicationContext()); + MockMvcUtils.setDisableInternalUserManagement(false, getWebApplicationContext()); ResultActions result = createUser(); ScimUser createdUser = JsonUtils.readValue(result.andReturn().getResponse().getContentAsString(), ScimUser.class); - MockMvcUtils.setInternalUserManagement(false, getWebApplicationContext()); - String errorMessage = getMockMvc().perform(get("/change_email") + MockMvcUtils.setDisableInternalUserManagement(true, getWebApplicationContext()); + getMockMvc().perform(get("/change_email") .session(getUserSession(createdUser.getUserName(), PASSWD)) .with(csrf()) .accept(ACCEPT_TEXT_HTML)) - .andExpect(status().isForbidden()).andReturn().getResponse().getErrorMessage(); - assertEquals("Internal User Creation is currently disabled. External User Store is in use.", errorMessage); + .andExpect(status().isForbidden()) + .andExpect(content() + .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @Test public void changeEmailControllerChangeEmailNotAllowed() throws Exception { - MockMvcUtils.setInternalUserManagement(true, getWebApplicationContext()); + MockMvcUtils.setDisableInternalUserManagement(false, getWebApplicationContext()); ResultActions result = createUser(); ScimUser createdUser = JsonUtils.readValue(result.andReturn().getResponse().getContentAsString(), ScimUser.class); - MockMvcUtils.setInternalUserManagement(false, getWebApplicationContext()); - String errorMessage = getMockMvc().perform(post("/change_email.do") + MockMvcUtils.setDisableInternalUserManagement(true, getWebApplicationContext()); + getMockMvc().perform(post("/change_email.do") .session(getUserSession(createdUser.getUserName(), PASSWD)) .with(csrf()) .accept(ACCEPT_TEXT_HTML) .param("newEmail", "newUser@example.com") .param("client_id", "login")) - .andExpect(status().isForbidden()).andReturn().getResponse().getErrorMessage(); - assertEquals("Internal User Creation is currently disabled. External User Store is in use.", errorMessage); + .andExpect(status().isForbidden()) + .andExpect(content() + .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); + } @Test public void changeEmailControllerVerifyEmailNotAllowed() throws Exception { - MockMvcUtils.setInternalUserManagement(true, getWebApplicationContext()); + MockMvcUtils.setDisableInternalUserManagement(false, getWebApplicationContext()); ResultActions result = createUser(); ScimUser createdUser = JsonUtils.readValue(result.andReturn().getResponse().getContentAsString(), ScimUser.class); @@ -239,91 +280,123 @@ public void changeEmailControllerVerifyEmailNotAllowed() throws Exception { change.setUserId(createdUser.getId()); ExpiringCode code = getExpiringCode(change); - MockMvcUtils.setInternalUserManagement(false, getWebApplicationContext()); - String errorMessage = getMockMvc().perform(get("/verify_email") + MockMvcUtils.setDisableInternalUserManagement(true, getWebApplicationContext()); + getMockMvc().perform(get("/verify_email") .param("code", code.getCode())) - .andExpect(status().isForbidden()).andReturn().getResponse().getErrorMessage(); - assertEquals("Internal User Creation is currently disabled. External User Store is in use.", errorMessage); + .andExpect(status().isForbidden()) + .andExpect(content() + .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); + } @Test public void changePasswordControllerChangePasswordPageNotAllowed() throws Exception { - MockMvcUtils.setInternalUserManagement(false, getWebApplicationContext()); - String errorMessage = getMockMvc().perform(get("/change_password")) - .andExpect(status().isForbidden()).andReturn().getResponse().getErrorMessage(); - assertEquals("Internal User Creation is currently disabled. External User Store is in use.", errorMessage); + MockMvcUtils.setDisableInternalUserManagement(true, getWebApplicationContext()); + getMockMvc().perform(get("/change_password")) + .andExpect(status().isForbidden()) + .andExpect(content() + .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); + } @Test public void changePasswordControllerChangePasswordNotAllowed() throws Exception { - MockMvcUtils.setInternalUserManagement(true, getWebApplicationContext()); + MockMvcUtils.setDisableInternalUserManagement(false, getWebApplicationContext()); ResultActions result = createUser(); ScimUser createdUser = JsonUtils.readValue(result.andReturn().getResponse().getContentAsString(), ScimUser.class); - MockMvcUtils.setInternalUserManagement(false, getWebApplicationContext()); - String errorMessage = getMockMvc().perform(post("/change_password.do") + MockMvcUtils.setDisableInternalUserManagement(true, getWebApplicationContext()); + getMockMvc().perform(post("/change_password.do") .session(getUserSession(createdUser.getUserName(), PASSWD)) .with(csrf()) .accept(ACCEPT_TEXT_HTML) .param("current_password", PASSWD) .param("new_password", "whatever") .param("confirm_password", "whatever")) - .andExpect(status().isForbidden()).andReturn().getResponse().getErrorMessage(); - assertEquals("Internal User Creation is currently disabled. External User Store is in use.", errorMessage); + .andExpect(status().isForbidden()) + .andExpect(content() + .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); + } @Test public void resetPasswordControllerForgotPasswordPageNotAllowed() throws Exception { - MockMvcUtils.setInternalUserManagement(false, getWebApplicationContext()); - String errorMessage = getMockMvc().perform(get("/forgot_password")) - .andExpect(status().isForbidden()).andReturn().getResponse().getErrorMessage(); - assertEquals("Internal User Creation is currently disabled. External User Store is in use.", errorMessage); + MockMvcUtils.setDisableInternalUserManagement(true, getWebApplicationContext()); + getMockMvc().perform(get("/forgot_password")) + .andExpect(status().isForbidden()) + .andExpect(content() + .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); + } @Test public void resetPasswordControllerForgotPasswordNotAllowed() throws Exception { - MockMvcUtils.setInternalUserManagement(false, getWebApplicationContext()); - String errorMessage = getMockMvc().perform(post("/forgot_password.do") + MockMvcUtils.setDisableInternalUserManagement(true, getWebApplicationContext()); + getMockMvc().perform(post("/forgot_password.do") .param("email", "another@example.com")) - .andExpect(status().isForbidden()).andReturn().getResponse().getErrorMessage(); - assertEquals("Internal User Creation is currently disabled. External User Store is in use.", errorMessage); + .andExpect(status().isForbidden()) + .andExpect(content() + .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); + } @Test public void resetPasswordControllerEmailSentPageNotAllowed() throws Exception { - MockMvcUtils.setInternalUserManagement(false, getWebApplicationContext()); - String errorMessage = getMockMvc().perform(get("/email_sent")) - .andExpect(status().isForbidden()).andReturn().getResponse().getErrorMessage(); - assertEquals("Internal User Creation is currently disabled. External User Store is in use.", errorMessage); + MockMvcUtils.setDisableInternalUserManagement(true, getWebApplicationContext()); + getMockMvc().perform(get("/email_sent")) + .andExpect(status().isForbidden()) + .andExpect(content() + .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); + } @Test public void resetPasswordControllerResetPasswordPageNotAllowed() throws Exception { - MockMvcUtils.setInternalUserManagement(false, getWebApplicationContext()); - String errorMessage = getMockMvc().perform(get("/reset_password") + MockMvcUtils.setDisableInternalUserManagement(true, getWebApplicationContext()); + getMockMvc().perform(get("/reset_password") .param("code", "12345") .param("email", "another@example.com")) - .andExpect(status().isForbidden()).andReturn().getResponse().getErrorMessage(); - assertEquals("Internal User Creation is currently disabled. External User Store is in use.", errorMessage); + .andExpect(status().isForbidden()) + .andExpect(content() + .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); + } @Test public void resetPasswordControllerResetPasswordNotAllowed() throws Exception { - MockMvcUtils.setInternalUserManagement(true, getWebApplicationContext()); + MockMvcUtils.setDisableInternalUserManagement(false, getWebApplicationContext()); ResultActions result = createUser(); ScimUser createdUser = JsonUtils.readValue(result.andReturn().getResponse().getContentAsString(), ScimUser.class); PasswordChange change = new PasswordChange(createdUser.getId(), createdUser.getUserName(), createdUser.getPasswordLastModified()); - MockMvcUtils.setInternalUserManagement(false, getWebApplicationContext()); - String errorMessage = getMockMvc().perform(post("/reset_password.do") + MockMvcUtils.setDisableInternalUserManagement(true, getWebApplicationContext()); + getMockMvc().perform(post("/reset_password.do") .param("code", getExpiringCode(change).getCode()) .param("email", createdUser.getUserName()) .param("password", "new-password") .param("password_confirmation", "new-password") .with(csrf())) - .andExpect(status().isForbidden()).andReturn().getResponse().getErrorMessage(); - assertEquals("Internal User Creation is currently disabled. External User Store is in use.", errorMessage); + .andExpect(status().isForbidden()) + .andExpect(content() + .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); + } private ExpiringCode getExpiringCode(Object data) { diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityProviderEndpointsMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityProviderEndpointsMockMvcTests.java index c2b31d85c5a..e4c66eb0bd5 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityProviderEndpointsMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityProviderEndpointsMockMvcTests.java @@ -109,7 +109,7 @@ public void testCreateSamlProvider() throws Exception { provider.setConfig(JsonUtils.writeValueAsString(samlDefinition)); IdentityProvider created = createIdentityProvider(null, provider, accessToken, status().isCreated()); - assertTrue(created.isAllowInternalUserManagement()); + assertFalse(created.isDisableInternalUserManagement()); assertNotNull(created.getConfig()); SamlIdentityProviderDefinition samlCreated = created.getConfigValue(SamlIdentityProviderDefinition.class); assertEquals(Arrays.asList("test.com", "test2.com"), samlCreated.getEmailDomain()); @@ -168,12 +168,12 @@ private void testRetrieveIdps(boolean retrieveActive) throws Exception { private void createAndUpdateIdentityProvider(String accessToken, String zoneId) throws Exception { IdentityProvider identityProvider = MultitenancyFixture.identityProvider("testorigin", IdentityZone.getUaa().getId()); - identityProvider.setAllowInternalUserManagement(false); + identityProvider.setDisableInternalUserManagement(false); // create // check response IdentityProvider createdIDP = createIdentityProvider(zoneId, identityProvider, accessToken, status().isCreated()); assertNotNull(createdIDP.getId()); - assertFalse(createdIDP.isAllowInternalUserManagement()); + assertFalse(createdIDP.isDisableInternalUserManagement()); assertEquals(identityProvider.getName(), createdIDP.getName()); assertEquals(identityProvider.getOriginKey(), createdIDP.getOriginKey()); @@ -191,7 +191,7 @@ private void createAndUpdateIdentityProvider(String accessToken, String zoneId) // update String newConfig = RandomStringUtils.randomAlphanumeric(1024); createdIDP.setConfig(newConfig); - createdIDP.setAllowInternalUserManagement(true); + createdIDP.setDisableInternalUserManagement(true); updateIdentityProvider(null, createdIDP, accessToken, status().isOk()); // check db @@ -200,7 +200,7 @@ private void createAndUpdateIdentityProvider(String accessToken, String zoneId) assertEquals(createdIDP.getId(), persisted.getId()); assertEquals(createdIDP.getName(), persisted.getName()); assertEquals(createdIDP.getOriginKey(), persisted.getOriginKey()); - assertEquals(createdIDP.isAllowInternalUserManagement(), persisted.isAllowInternalUserManagement()); + assertEquals(createdIDP.isDisableInternalUserManagement(), persisted.isDisableInternalUserManagement()); // check audit assertEquals(2, eventListener.getEventCount()); From ab05e71797a81b1c894463fc90fd7a8d77cda162 Mon Sep 17 00:00:00 2001 From: Jonathan Lo Date: Fri, 25 Sep 2015 15:37:20 -0700 Subject: [PATCH 017/103] Update UAA-APIs.rst Remove comma in "Create or Update Identity Zones" POST request body example [#104146808] https://www.pivotaltracker.com/story/show/104146808 --- docs/UAA-APIs.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/UAA-APIs.rst b/docs/UAA-APIs.rst index 7ab32d45568..897a33411be 100644 --- a/docs/UAA-APIs.rst +++ b/docs/UAA-APIs.rst @@ -677,7 +677,7 @@ Request body *example* :: "id":"testzone1", "subdomain":"testzone1", "name":"The Twiglet Zone", - "description":"Like the Twilight Zone but tastier.", + "description":"Like the Twilight Zone but tastier." } From 4a00d206aadf9ad47a41a5274c31b77f9312922b Mon Sep 17 00:00:00 2001 From: Jonathan Lo Date: Fri, 25 Sep 2015 16:33:09 -0700 Subject: [PATCH 018/103] Removed unused body and head tags in html templates. [finishes #104213170] https://www.pivotaltracker.com/story/show/104213170 --- .../templates/web/access_confirmation.html | 2 - .../web/access_confirmation_error.html | 4 -- .../templates/web/accounts/email_sent.html | 38 ++++++----- .../web/accounts/new_activation_email.html | 4 -- .../resources/templates/web/approvals.html | 2 - .../resources/templates/web/change_email.html | 2 - .../templates/web/change_password.html | 32 +++++----- .../resources/templates/web/email_sent.html | 20 +++--- .../main/resources/templates/web/error.html | 22 +++---- .../templates/web/forgot_password.html | 4 -- .../main/resources/templates/web/home.html | 28 ++++---- .../templates/web/invalid_request.html | 4 -- .../web/invitations/accept_invite.html | 4 -- .../web/invitations/invite_sent.html | 2 - .../templates/web/invitations/new_invite.html | 2 - .../main/resources/templates/web/login.html | 64 +++++++++---------- .../src/main/resources/templates/web/nav.html | 24 ++++--- .../resources/templates/web/passcode.html | 2 - .../templates/web/pivotal_links.html | 2 - .../templates/web/reset_password.html | 4 -- 20 files changed, 106 insertions(+), 160 deletions(-) diff --git a/login/src/main/resources/templates/web/access_confirmation.html b/login/src/main/resources/templates/web/access_confirmation.html index 119d7b36b9f..6022853707e 100644 --- a/login/src/main/resources/templates/web/access_confirmation.html +++ b/login/src/main/resources/templates/web/access_confirmation.html @@ -6,7 +6,6 @@ -
@@ -72,5 +71,4 @@

Cloudbees

- diff --git a/login/src/main/resources/templates/web/access_confirmation_error.html b/login/src/main/resources/templates/web/access_confirmation_error.html index ae5d836690a..845536d8ecb 100644 --- a/login/src/main/resources/templates/web/access_confirmation_error.html +++ b/login/src/main/resources/templates/web/access_confirmation_error.html @@ -1,8 +1,5 @@ - - -
@@ -15,5 +12,4 @@

There was an error. The request for authorization was invalid.

- \ No newline at end of file diff --git a/login/src/main/resources/templates/web/accounts/email_sent.html b/login/src/main/resources/templates/web/accounts/email_sent.html index d2ef314ad8d..5e7faddc2b1 100644 --- a/login/src/main/resources/templates/web/accounts/email_sent.html +++ b/login/src/main/resources/templates/web/accounts/email_sent.html @@ -6,25 +6,23 @@ - -
-
-

Create your account

-

- A Pivotal ID lets you sign in to Pivotal products - using a single username and password. -

-
-
- -

- Check email for an activation link -

- -
- +
+
+

Create your account

+

+ A Pivotal ID lets you sign in to Pivotal products + using a single username and password. +

- +
+ +

+ Check email for an activation link +

+ +
+ +
diff --git a/login/src/main/resources/templates/web/accounts/new_activation_email.html b/login/src/main/resources/templates/web/accounts/new_activation_email.html index 6a0092b28dd..da24475b539 100644 --- a/login/src/main/resources/templates/web/accounts/new_activation_email.html +++ b/login/src/main/resources/templates/web/accounts/new_activation_email.html @@ -3,9 +3,6 @@ xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorator="layouts/main" th:with="pivotal=${@environment.getProperty('login.brand') == 'pivotal'},isUaa=${T(org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder).isUaa()}"> - - -

Create your account

@@ -34,5 +31,4 @@

Create your Already joined? Sign in.

- diff --git a/login/src/main/resources/templates/web/approvals.html b/login/src/main/resources/templates/web/approvals.html index 63201230572..503ef6d3735 100644 --- a/login/src/main/resources/templates/web/approvals.html +++ b/login/src/main/resources/templates/web/approvals.html @@ -21,7 +21,6 @@ }); -
@@ -92,5 +91,4 @@

Revoke Access

- \ No newline at end of file diff --git a/login/src/main/resources/templates/web/change_email.html b/login/src/main/resources/templates/web/change_email.html index ed3120ce40c..da556b4847b 100644 --- a/login/src/main/resources/templates/web/change_email.html +++ b/login/src/main/resources/templates/web/change_email.html @@ -5,7 +5,6 @@ -
@@ -24,5 +23,4 @@

Change Email

- \ No newline at end of file diff --git a/login/src/main/resources/templates/web/change_password.html b/login/src/main/resources/templates/web/change_password.html index be5492f11f9..04f6c7e1ddb 100644 --- a/login/src/main/resources/templates/web/change_password.html +++ b/login/src/main/resources/templates/web/change_password.html @@ -5,22 +5,20 @@ - -
- +
+ +
+
+

Change Password

+
+
+
+
+ + + + +
-
-

Change Password

-
-
-
-
- - - - -
-
-
- +
\ No newline at end of file diff --git a/login/src/main/resources/templates/web/email_sent.html b/login/src/main/resources/templates/web/email_sent.html index 746e0760909..955c2613422 100644 --- a/login/src/main/resources/templates/web/email_sent.html +++ b/login/src/main/resources/templates/web/email_sent.html @@ -3,16 +3,14 @@ - -
-

Instructions Sent

-
- -

- Check your email for instructions. -

- Back to Sign In -
+
+

Instructions Sent

+
+ +

+ Check your email for instructions. +

+ Back to Sign In
- +
\ No newline at end of file diff --git a/login/src/main/resources/templates/web/error.html b/login/src/main/resources/templates/web/error.html index 04dc1d43ef6..9352240bbc1 100644 --- a/login/src/main/resources/templates/web/error.html +++ b/login/src/main/resources/templates/web/error.html @@ -2,18 +2,16 @@ - -
-
-
-

Error Message

-
- +
+
+
+

Error Message

-

- Uh oh.
- Something went amiss. -

+
- +

+ Uh oh.
+ Something went amiss. +

+
\ No newline at end of file diff --git a/login/src/main/resources/templates/web/forgot_password.html b/login/src/main/resources/templates/web/forgot_password.html index af0f326a4c1..ffd651088c1 100644 --- a/login/src/main/resources/templates/web/forgot_password.html +++ b/login/src/main/resources/templates/web/forgot_password.html @@ -1,8 +1,5 @@ - - -

Reset Password

@@ -15,5 +12,4 @@

Reset Password

Back to Sign In
- \ No newline at end of file diff --git a/login/src/main/resources/templates/web/home.html b/login/src/main/resources/templates/web/home.html index 3c4f0084b99..5555de0e706 100644 --- a/login/src/main/resources/templates/web/home.html +++ b/login/src/main/resources/templates/web/home.html @@ -6,19 +6,17 @@ - -
- -
-
-

Where to?

- -
- Invite Users -
- +
+ +
+
+

Where to?

+ +
+ Invite Users +
\ No newline at end of file diff --git a/login/src/main/resources/templates/web/invalid_request.html b/login/src/main/resources/templates/web/invalid_request.html index 92f8910d773..769133e6e67 100644 --- a/login/src/main/resources/templates/web/invalid_request.html +++ b/login/src/main/resources/templates/web/invalid_request.html @@ -1,8 +1,5 @@ - - -
@@ -15,5 +12,4 @@

This may indicate that the request was not originated by you.

- \ No newline at end of file diff --git a/login/src/main/resources/templates/web/invitations/accept_invite.html b/login/src/main/resources/templates/web/invitations/accept_invite.html index ebefd29defb..40435427f17 100644 --- a/login/src/main/resources/templates/web/invitations/accept_invite.html +++ b/login/src/main/resources/templates/web/invitations/accept_invite.html @@ -3,9 +3,6 @@ xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorator="layouts/main" th:with="pivotal=${@environment.getProperty('login.brand') == 'pivotal'},isUaa=${T(org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder).isUaa()}"> - - -

Create your account

@@ -50,5 +47,4 @@

Create your -
@@ -14,5 +13,4 @@

Invite sent

- \ No newline at end of file diff --git a/login/src/main/resources/templates/web/invitations/new_invite.html b/login/src/main/resources/templates/web/invitations/new_invite.html index f8d17ae23cb..5755c9461a7 100644 --- a/login/src/main/resources/templates/web/invitations/new_invite.html +++ b/login/src/main/resources/templates/web/invitations/new_invite.html @@ -5,7 +5,6 @@ -
@@ -25,5 +24,4 @@

Send an invite

- diff --git a/login/src/main/resources/templates/web/login.html b/login/src/main/resources/templates/web/login.html index 59ef21d88ea..1877a9d5753 100644 --- a/login/src/main/resources/templates/web/login.html +++ b/login/src/main/resources/templates/web/login.html @@ -1,40 +1,36 @@ - - - -
-

Welcome!

-
- -
-
-

Error Message

-
- - -
-
-
diff --git a/login/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsControllerTest.java b/login/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsControllerTest.java index aa7db9ac3e2..cfcd9e83526 100644 --- a/login/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsControllerTest.java +++ b/login/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsControllerTest.java @@ -3,25 +3,20 @@ import org.cloudfoundry.identity.uaa.authentication.Origin; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.cloudfoundry.identity.uaa.authentication.manager.DynamicZoneAwareAuthenticationManager; -import org.cloudfoundry.identity.uaa.client.ClientConstants; import org.cloudfoundry.identity.uaa.codestore.ExpiringCode; import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; -import org.cloudfoundry.identity.uaa.error.UaaException; -import org.cloudfoundry.identity.uaa.ldap.LdapIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.login.BuildInfo; -import org.cloudfoundry.identity.uaa.login.saml.SamlIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.login.test.ThymeleafConfig; -import org.cloudfoundry.identity.uaa.login.util.SecurityUtils; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.exception.InvalidPasswordException; import org.cloudfoundry.identity.uaa.scim.validate.PasswordValidator; -import org.cloudfoundry.identity.uaa.scim.validate.UaaPasswordPolicyValidator; import org.cloudfoundry.identity.uaa.user.UaaAuthority; +import org.cloudfoundry.identity.uaa.user.UaaUser; +import org.cloudfoundry.identity.uaa.user.UaaUserDatabase; import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.cloudfoundry.identity.uaa.zone.IdentityProvider; import org.cloudfoundry.identity.uaa.zone.IdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; -import org.cloudfoundry.identity.uaa.zone.UaaIdentityProviderDefinition; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -32,11 +27,9 @@ import org.springframework.context.annotation.Import; import org.springframework.context.support.ResourceBundleMessageSource; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.NoSuchClientException; -import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -51,19 +44,12 @@ import java.sql.Timestamp; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.Map; import static com.google.common.collect.Lists.newArrayList; -import static org.hamcrest.Matchers.any; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.empty; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.anyObject; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; @@ -285,6 +271,15 @@ BuildInfo buildInfo() { return new BuildInfo(); } + @Bean + public UaaUserDatabase userDatabase() { + UaaUserDatabase userDatabase = mock(UaaUserDatabase.class); + UaaUser user = new UaaUser("user@example.com","","user@example.com","Given","family"); + user = user.modifyId("user-id-001"); + when (userDatabase.retrieveUserById(user.getId())).thenReturn(user); + return userDatabase; + } + @Bean public ResourceBundleMessageSource messageSource() { ResourceBundleMessageSource resourceBundleMessageSource = new ResourceBundleMessageSource(); @@ -302,12 +297,12 @@ InvitationsController invitationsController(InvitationsService invitationsServic ExpiringCodeStore codeStore, PasswordValidator passwordPolicyValidator, IdentityProviderProvisioning providerProvisioning, - DynamicZoneAwareAuthenticationManager zoneAwareAuthenticationManager) { + UaaUserDatabase userDatabase) { InvitationsController result = new InvitationsController(invitationsService); result.setExpiringCodeStore(codeStore); result.setPasswordValidator(passwordPolicyValidator); result.setProviderProvisioning(providerProvisioning); - result.setZoneAwareAuthenticationManager(zoneAwareAuthenticationManager); + result.setUserDatabase(userDatabase); return result; } diff --git a/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrap.java b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrap.java index d40013b4d58..6d363480a43 100644 --- a/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrap.java +++ b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrap.java @@ -17,7 +17,6 @@ import org.cloudfoundry.identity.uaa.authentication.Origin; import org.cloudfoundry.identity.uaa.authentication.manager.AuthEvent; import org.cloudfoundry.identity.uaa.authentication.manager.ExternalGroupAuthorizationEvent; -import org.cloudfoundry.identity.uaa.authentication.manager.InvitedUserAuthenticatedEvent; import org.cloudfoundry.identity.uaa.scim.ScimGroup; import org.cloudfoundry.identity.uaa.scim.ScimGroupMember; import org.cloudfoundry.identity.uaa.scim.ScimGroupMembershipManager; @@ -186,9 +185,6 @@ public void onApplicationEvent(AuthEvent event) { ScimUser user = getScimUser(event.getUser()); updateUser(user, event.getUser(), false); } - } else if (event instanceof InvitedUserAuthenticatedEvent) { - ScimUser scimUser = getScimUser(event.getUser()); - updateUser(scimUser, event.getUser(), true); } else { addUser(event.getUser()); } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/InvitationsIT.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/InvitationsIT.java index 943d5532f0c..88feb4fbd05 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/InvitationsIT.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/InvitationsIT.java @@ -102,56 +102,15 @@ public void logout_and_clear_cookies() { webDriver.manage().deleteAllCookies(); } - @Test - public void test_LDAP_User_Invite_and_Accept() { - Assume.assumeTrue("Ldap profile must be enabled for this test.", System.getProperty("spring.profiles.active", "default").contains(Origin.LDAP)); - perform_LDAP_User_Invite_and_Accept(); - //we should be able to invite the same user multiple time - perform_LDAP_User_Invite_and_Accept(); - } - public void perform_LDAP_User_Invite_and_Accept() { - webDriver.get(baseUrl + "/logout.do"); - String username = "marissa5"; - String email = username+"@test.com"; - String code = generateCode(username, email, "", Origin.LDAP); - String invitedUserId = IntegrationTestUtils.getUserId(scimToken, baseUrl, Origin.LDAP, username); - String currentUserId = null; - try { - currentUserId = IntegrationTestUtils.getUserId(scimToken, baseUrl, Origin.LDAP, username); - } catch (RuntimeException x) {} - assertEquals(invitedUserId, currentUserId); - webDriver.get(baseUrl + "/invitations/accept?code=" + code); - assertEquals("Create your account", webDriver.findElement(By.tagName("h1")).getText()); - webDriver.findElement(By.name("enterprise_username")).sendKeys(username); - webDriver.findElement(By.name("enterprise_password")).sendKeys("ldap5"); - webDriver.findElement(By.xpath("//input[@value='Login']")).click(); - Assert.assertThat(webDriver.findElement(By.cssSelector("h1")).getText(), containsString("Where to?")); - String acceptedUserId = IntegrationTestUtils.getUserId(scimToken, baseUrl, Origin.LDAP, username); - if (currentUserId==null) { - assertEquals(invitedUserId, acceptedUserId); - } else { - assertEquals(currentUserId, acceptedUserId); - } - } - - @Test - public void test_SAML_User_Invite_and_Accept() { - } - - @Test - public void test_SAML_User_Invite_Redirect_and_Accept() { - - } - @Test public void testInviteUserWithClientRedirect() throws Exception { String userEmail = "user-" + new RandomValueStringGenerator().generate() + "@example.com"; //user doesn't exist - performInviteUser(userEmail); + performInviteUser(userEmail, false); //user exist - performInviteUser(userEmail); + performInviteUser(userEmail, true); } - public void performInviteUser(String email) throws Exception { + public void performInviteUser(String email, boolean isVerified) throws Exception { webDriver.get(baseUrl + "/logout.do"); String code = generateCode(email, email, "http://localhost:8080/app/", Origin.UAA); @@ -163,14 +122,16 @@ public void performInviteUser(String email) throws Exception { assertEquals(invitedUserId, currentUserId); webDriver.get(baseUrl + "/invitations/accept?code=" + code); - assertEquals("Create your account", webDriver.findElement(By.tagName("h1")).getText()); - - webDriver.findElement(By.name("password")).sendKeys("secr3T"); - webDriver.findElement(By.name("password_confirmation")).sendKeys("secr3T"); - - webDriver.findElement(By.xpath("//input[@value='Create account']")).click(); - Assert.assertThat(webDriver.findElement(By.cssSelector("h1")).getText(), containsString("Application Authorization")); - + if (!isVerified) { + assertEquals("Create your account", webDriver.findElement(By.tagName("h1")).getText()); + webDriver.findElement(By.name("password")).sendKeys("secr3T"); + webDriver.findElement(By.name("password_confirmation")).sendKeys("secr3T"); + webDriver.findElement(By.xpath("//input[@value='Create account']")).click(); + Assert.assertThat(webDriver.findElement(By.cssSelector("h1")).getText(), containsString("Application Authorization")); + } else { + //redirect to the home page to login + Assert.assertThat(webDriver.findElement(By.cssSelector("h1")).getText(), containsString("Welcome!")); + } String acceptedUserId = IntegrationTestUtils.getUserId(scimToken, baseUrl, Origin.UAA, email); if (currentUserId==null) { assertEquals(invitedUserId, acceptedUserId); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java index 75347256cf0..a75dd9bd3aa 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java @@ -49,7 +49,6 @@ import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.security.oauth2.client.test.TestAccounts; -import org.springframework.security.oauth2.common.util.OAuth2Utils; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.test.context.ContextConfiguration; @@ -68,7 +67,6 @@ import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; @@ -391,15 +389,8 @@ public void perform_SamlInvitation_Automatic_Redirect_In_Zone2(String username, String existingUserId = IntegrationTestUtils.getUserId(uaaAdminToken, zoneUrl, samlIdentityProviderDefinition.getIdpEntityAlias(), useremail); webDriver.get(zoneUrl + "/logout.do"); webDriver.get(zoneUrl + "/invitations/accept?code=" + code); - //we should now be in the Simple SAML PHP site - - - webDriver.findElement(By.xpath("//h2[contains(text(), 'Enter your username and password')]")); - webDriver.findElement(By.name("username")).clear(); - webDriver.findElement(By.name("username")).sendKeys(username); - webDriver.findElement(By.name("password")).sendKeys("saml2"); - webDriver.findElement(By.xpath("//input[@value='Login']")).click(); - assertThat(webDriver.findElement(By.cssSelector("h1")).getText(), Matchers.containsString("Where to?")); + //we should now be on the login page because we don't have a redirect + assertThat(webDriver.findElement(By.cssSelector("h1")).getText(), Matchers.containsString("Welcome to The Twiglet Zone[testzone2]!")); uaaProvider.setConfig(JsonUtils.writeValueAsString(uaaDefinition.setEmailDomain(null))); IntegrationTestUtils.createOrUpdateProvider(zoneAdminToken,baseUrl,uaaProvider); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java index aedcdd47609..f56b9902fd1 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java @@ -39,15 +39,12 @@ import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.cloudfoundry.identity.uaa.zone.UaaIdentityProviderDefinition; import org.junit.After; -import org.junit.Assume; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.springframework.http.MediaType; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mock.web.MockHttpSession; -import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.common.util.OAuth2Utils; @@ -75,9 +72,10 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.endsWith; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; @@ -90,6 +88,7 @@ public class InvitationsServiceMockMvcTests extends InjectedMockContextTest { + public static final String REDIRECT_URI = "http://invitation.redirect.test"; private JavaMailSender originalSender; private FakeJavaMailSender fakeJavaMailSender = new FakeJavaMailSender(); private MockMvcUtils utils = MockMvcUtils.utils(); @@ -143,7 +142,7 @@ public ScimUser getScimInviteUser() { public ZoneScimInviteData createZoneForInvites() throws Exception { IdentityZoneCreationResult zone = utils().createOtherIdentityZoneAndReturnResult(generator.generate(), getMockMvc(), getWebApplicationContext(), null); - BaseClientDetails appClient = new BaseClientDetails("app","","scim.invite", "client_credentials,password,authorization_code","uaa.admin,clients.admin,scim.write,scim.read,scim.invite","http://example.com"); + BaseClientDetails appClient = new BaseClientDetails("app","","scim.invite", "client_credentials,password,authorization_code","uaa.admin,clients.admin,scim.write,scim.read,scim.invite",REDIRECT_URI); appClient.setClientSecret("secret"); appClient = utils().createClient(getMockMvc(), zone.getZoneAdminToken(), appClient, zone.getIdentityZone()); appClient.setClientSecret("secret"); @@ -182,7 +181,7 @@ public void setUp() throws Exception { clientId = generator.generate().toLowerCase(); clientSecret = generator.generate().toLowerCase(); authorities = "scim.read,scim.invite"; - createScimClient(this.getMockMvc(), adminToken, clientId, clientSecret, "oauth", "scim.read,scim.invite", Arrays.asList(new MockMvcUtils.GrantType[]{MockMvcUtils.GrantType.client_credentials, MockMvcUtils.GrantType.password}), authorities); + createScimClient(this.getMockMvc(), adminToken, clientId, clientSecret, "oauth", "scim.read,scim.invite", Arrays.asList(new MockMvcUtils.GrantType[]{MockMvcUtils.GrantType.client_credentials, MockMvcUtils.GrantType.password}), authorities, REDIRECT_URI); scimInviteToken = MockMvcUtils.utils().getClientCredentialsOAuthAccessToken(getMockMvc(), clientId, clientSecret, "scim.read scim.invite", null); userInviteToken = MockMvcUtils.utils().getScimInviteUserToken(getMockMvc(), clientId, clientSecret); getWebApplicationContext().getBean(JdbcTemplate.class).update("delete from expiring_code_store"); @@ -212,6 +211,10 @@ public void inviteUser_Correct_Origin_Set() throws Exception { inviteUser(email, userInviteToken, null, clientId, Origin.UAA); } + protected T queryUserForField(String email, String field, Class type) { + return getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("SELECT "+field+" FROM users WHERE email=?",type, email); + } + @Test public void test_authorize_with_invitation_login() throws Exception { @@ -235,7 +238,7 @@ public void test_authorize_with_invitation_login() throws Exception { String clientId = "authclient-"+new RandomValueStringGenerator().generate(); BaseClientDetails client = new BaseClientDetails(clientId, "", "openid","authorization_code","",redirectUri); client.setClientSecret("secret"); - String adminToken = utils().getClientCredentialsOAuthAccessToken(getMockMvc(),"admin","adminsecret","",null); + String adminToken = utils().getClientCredentialsOAuthAccessToken(getMockMvc(), "admin", "adminsecret", "", null); MockMvcUtils.utils().createClient(getMockMvc(), adminToken, client); String state = new RandomValueStringGenerator().generate(); @@ -282,247 +285,99 @@ public void accept_invitation_should_not_log_you_in() throws Exception { } - @Test - public void accept_invitation_origin_reset() throws Exception { - String email = new RandomValueStringGenerator().generate().toLowerCase()+"@test.org"; + public void accept_invitation_for_verified_user_sends_redirect() throws Exception { + String email = new RandomValueStringGenerator().generate().toLowerCase() + "@test.org"; MimeMessageWrapper message = inviteUser(email, userInviteToken, null, clientId, Origin.UAA); - assertEquals(Origin.UAA, getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select origin from users where username=?", new Object[]{email}, String.class)); + + getWebApplicationContext().getBean(JdbcTemplate.class).update("UPDATE users SET verified=true WHERE email=?",email); + assertTrue("User should not be verified", queryUserForField(email, "verified", Boolean.class)); + assertEquals(Origin.UAA, queryUserForField(email, Origin.ORIGIN, String.class)); String code = extractInvitationCode(message.getContentString()); - MvcResult result = getMockMvc().perform(get("/invitations/accept") + getMockMvc().perform( + get("/invitations/accept") .param("code", code) .accept(MediaType.TEXT_HTML) ) - .andExpect(status().isOk()) - .andExpect(content().string(containsString("Email: " + email))) - .andReturn(); - - code = getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select code from expiring_code_store", String.class); - MockHttpSession session = (MockHttpSession) result.getRequest().getSession(false); - getMockMvc().perform(post("/invitations/accept.do") - .session(session) - .param("password", "s3cret") - .param("password_confirmation", "s3cret") - .param("code",code) - .with(csrf())) .andExpect(status().isFound()) - .andExpect(redirectedUrl("/home")) - .andReturn(); - - assertEquals(Origin.UAA, getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select origin from users where username=?", new Object[]{email}, String.class)); + .andExpect(redirectedUrl(REDIRECT_URI)); } - @Test - public void invite_user_show_correct_saml_and_uaa_idp_for_acceptance() throws Exception { - invite_user_and_check_UI(false, false); - invite_user_and_check_UI(false, true); - invite_user_and_check_UI(true, false); - } - @Test - public void invite_user_show_correct_ldap_idp_for_acceptance() throws Exception { - ZoneScimInviteData zone = createZoneForInvites(); - LdapIdentityProviderDefinition definition = LdapIdentityProviderDefinition.searchAndBindMapGroupToScopes("","","","","","","","","",false,false,false,1,true); + @Test + public void accept_invitation_sets_your_password() throws Exception { + String email = new RandomValueStringGenerator().generate().toLowerCase()+"@test.org"; + MimeMessageWrapper message = inviteUser(email, userInviteToken, null, clientId, Origin.UAA); - String domain = generator.generate().toLowerCase()+".com"; - definition.setEmailDomain(Arrays.asList(domain)); - IdentityProvider provider = createIdentityProvider(zone.getZone(), generator.generate(), definition); - String email = new RandomValueStringGenerator().generate().toLowerCase()+"@"+domain; - MimeMessageWrapper message = inviteUser(email, zone.getAdminToken(), zone.getZone().getIdentityZone().getSubdomain(), zone.getScimInviteClient().getClientId(), provider.getOriginKey()); - String code = extractInvitationCode(message.getContentString()); - ResultActions actions = getMockMvc().perform(get("/invitations/accept") - .param("code", code) - .accept(MediaType.TEXT_HTML) - .header("Host", zone.getZone().getIdentityZone().getSubdomain() + ".localhost") - ); - actions.andExpect(status().isOk()) - .andExpect(content().string(containsString("Email: " + email))) - .andExpect(content().string(containsString("Sign in with enterprise credentials:"))) - .andExpect(content().string(containsString("username"))); - } + assertFalse("User should not be verified", queryUserForField(email, "verified", Boolean.class)); + assertEquals(Origin.UAA, queryUserForField(email, Origin.ORIGIN, String.class)); - @Test - public void invite_user_show_sets_correct_ldap_origin_for_acceptance() throws Exception { - Assume.assumeTrue(java.util.Arrays.asList(getWebApplicationContext().getEnvironment().getActiveProfiles()).contains(Origin.LDAP)); - String email = "marissa2@test.com"; - getWebApplicationContext().getBean(JdbcTemplate.class).update("DELETE FROM users WHERE email=?", email); - ZoneScimInviteData zone = createZoneForInvites(); - LdapIdentityProviderDefinition definition = LdapIdentityProviderDefinition.searchAndBindMapGroupToScopes( - "ldap://localhost:389/", - "cn=admin,dc=test,dc=com", - "password", - "dc=test,dc=com", - "cn={0}", - "ou=scopes,dc=test,dc=com", - "member={0}", - "mail", - null, - false, - true, - true, - 10, - true); - definition.setEmailDomain(Arrays.asList("test.com")); - createIdentityProvider(zone.getZone(), Origin.LDAP, definition); - - MimeMessageWrapper message = inviteUser(email, zone.getAdminToken(), zone.getZone().getIdentityZone().getSubdomain(), zone.getScimInviteClient().getClientId() ,Origin.LDAP); String code = extractInvitationCode(message.getContentString()); - - String userInfoOrigin = getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select origin from users where email=? and identity_zone_id=?", String.class, email, zone.getZone().getIdentityZone().getId()); - String userInfoId = getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select id from users where email=? and identity_zone_id=?", String.class, email, zone.getZone().getIdentityZone().getId()); - assertEquals(Origin.LDAP, userInfoOrigin); - - ResultActions actions = getMockMvc().perform(get("/invitations/accept") + MvcResult result = getMockMvc().perform(get("/invitations/accept") .param("code", code) .accept(MediaType.TEXT_HTML) - .header("Host", zone.getZone().getIdentityZone().getSubdomain() + ".localhost") - ); - MvcResult result = actions.andExpect(status().isOk()) + ) + .andExpect(status().isOk()) .andExpect(content().string(containsString("Email: " + email))) - .andExpect(content().string(containsString("Sign in with enterprise credentials:"))) - .andExpect(content().string(containsString("username"))) .andReturn(); code = getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select code from expiring_code_store", String.class); - MockHttpSession session = (MockHttpSession) result.getRequest().getSession(false); - getMockMvc().perform(post("/invitations/accept_enterprise.do") - .session(session) - .param("enterprise_username", "marissa2") - .param("enterprise_password", "ldap") - .param("code", code) - .header("Host", zone.getZone().getIdentityZone().getSubdomain() + ".localhost") - .with(csrf())) + result = getMockMvc().perform( + post("/invitations/accept.do") + .session(session) + .param("password", "s3cret") + .param("password_confirmation", "s3cret") + .param("code",code) + .with(csrf()) + ) .andExpect(status().isFound()) - .andExpect(redirectedUrl("/home")) - .andReturn(); - - String newUserInfoId = getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select id from users where email=? and identity_zone_id=?", String.class, email, zone.getZone().getIdentityZone().getId()); - String newUserInfoOrigin = getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select origin from users where email=? and identity_zone_id=?", String.class, email, zone.getZone().getIdentityZone().getId()); - assertEquals(Origin.LDAP, newUserInfoOrigin); - //ensure that a new user wasn't created - assertEquals(userInfoId, newUserInfoId); - } - - @Test - @Ignore("We don't validate LDAP invitation email") - public void invite_user_reject_different_email_for_ldap() throws Exception { - Assume.assumeTrue(java.util.Arrays.asList(getWebApplicationContext().getEnvironment().getActiveProfiles()).contains(Origin.LDAP)); - String domain = generator.generate().toLowerCase()+".com"; - String email = "marissa2@"+domain; - getWebApplicationContext().getBean(JdbcTemplate.class).update("DELETE FROM users WHERE email=?", email); - ZoneScimInviteData zone = createZoneForInvites(); - LdapIdentityProviderDefinition definition = LdapIdentityProviderDefinition.searchAndBindMapGroupToScopes( - "ldap://localhost:389/", - "cn=admin,dc=test,dc=com", - "password", - "dc=test,dc=com", - "cn={0}", - "ou=scopes,dc=test,dc=com", - "member={0}", - "mail", - null, - false, - true, - true, - 10, - true); - definition.setEmailDomain(Arrays.asList(domain)); - createIdentityProvider(zone.getZone(), Origin.LDAP, definition); - - MimeMessageWrapper message = inviteUser(email, zone.getAdminToken(), zone.getZone().getIdentityZone().getSubdomain(), zone.getScimInviteClient().getClientId() ,Origin.LDAP); - String code = extractInvitationCode(message.getContentString()); - - String userInfoOrigin = getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select origin from users where email=? and identity_zone_id=?", String.class, email, zone.getZone().getIdentityZone().getId()); - String userInfoId = getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select id from users where email=? and identity_zone_id=?", String.class, email, zone.getZone().getIdentityZone().getId()); - assertEquals(Origin.LDAP, userInfoOrigin); - - ResultActions actions = getMockMvc().perform(get("/invitations/accept") - .param("code", code) - .accept(MediaType.TEXT_HTML) - .header("Host", zone.getZone().getIdentityZone().getSubdomain() + ".localhost") - ); - MvcResult result = actions.andExpect(status().isOk()) - .andExpect(content().string(containsString("Email: " + email))) - .andExpect(content().string(containsString("Sign in with enterprise credentials:"))) - .andExpect(content().string(containsString("username"))) + .andExpect(redirectedUrl(REDIRECT_URI)) .andReturn(); - code = getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select code from expiring_code_store", String.class); + assertTrue("User should be verified after password reset", queryUserForField(email, "verified", Boolean.class)); - MockHttpSession session = (MockHttpSession) result.getRequest().getSession(false); - getMockMvc().perform(post("/invitations/accept_enterprise.do") - .session(session) - .param("enterprise_username", "marissa2") - .param("enterprise_password", "ldap") - .param("code", code) - .header("Host", zone.getZone().getIdentityZone().getSubdomain() + ".localhost") - .with(csrf())) - .andExpect(status().isUnauthorized()) - .andReturn(); + session = (MockHttpSession) result.getRequest().getSession(false); + getMockMvc().perform( + get("/profile") + .session(session) + .accept(MediaType.TEXT_HTML) + ) + .andExpect(status().isOk()); } - public void invite_user_and_check_UI(boolean disableUAA, boolean disableSaml) throws Exception { - String domain = generator.generate().toLowerCase()+".com"; + @Test + public void invite_ldap_users_verifies_and_redirects() throws Exception { ZoneScimInviteData zone = createZoneForInvites(); - String entityID = generator.generate(); + LdapIdentityProviderDefinition definition = LdapIdentityProviderDefinition.searchAndBindMapGroupToScopes("", "", "", "", "", "", "", "", "", false, false, false, 1, true); - SamlIdentityProviderDefinition definition = getSamlIdentityProviderDefinition(zone.getZone(), entityID); + String domain = generator.generate().toLowerCase()+".com"; definition.setEmailDomain(Arrays.asList(domain)); - IdentityProvider samlProvider = createIdentityProvider(zone.getZone(), entityID, definition); - IdentityProviderProvisioning provisioning = getWebApplicationContext().getBean(IdentityProviderProvisioning.class); - - String expectedOrigin; - if (!disableSaml && !disableUAA) { - expectedOrigin = samlProvider.getOriginKey(); - } else if (!disableUAA) { - expectedOrigin = Origin.UAA; - } else { - expectedOrigin = samlProvider.getOriginKey(); - } - - if (disableSaml) { - samlProvider.setActive(false); - provisioning.update(samlProvider); - } - if (disableUAA) { - IdentityProvider uaaProvider = provisioning.retrieveByOrigin(Origin.UAA, zone.getZone().getIdentityZone().getId()); - uaaProvider.setActive(false); - provisioning.update(uaaProvider); - } - - String email = generator.generate().toLowerCase()+"@"+domain; - MimeMessageWrapper message = inviteUser(email, zone.getAdminToken(), zone.getZone().getIdentityZone().getSubdomain(), zone.getScimInviteClient().getClientId(), expectedOrigin); + IdentityProvider provider = createIdentityProvider(zone.getZone(), Origin.LDAP, definition); + String email = new RandomValueStringGenerator().generate().toLowerCase()+"@"+domain; + MimeMessageWrapper message = inviteUser(email, zone.getAdminToken(), zone.getZone().getIdentityZone().getSubdomain(), zone.getScimInviteClient().getClientId(), provider.getOriginKey()); String code = extractInvitationCode(message.getContentString()); + assertFalse("User should not be verified", queryUserForField(email, "verified", Boolean.class)); + assertEquals(Origin.LDAP, queryUserForField(email, Origin.ORIGIN, String.class)); + ResultActions actions = getMockMvc().perform(get("/invitations/accept") .param("code", code) .accept(MediaType.TEXT_HTML) .header("Host", zone.getZone().getIdentityZone().getSubdomain() + ".localhost") ); + actions + .andExpect(status().isFound()) + .andExpect(redirectedUrl(REDIRECT_URI)); - - if (!disableSaml) { - //redirect to SAML provider - actions.andExpect(status().isFound()); - actions.andExpect(redirectedUrl("/saml/discovery?returnIDParam=idp&entityID="+zone.getZone().getIdentityZone().getSubdomain()+".cloudfoundry-saml-login&idp="+entityID+"&isPassive=true")); - } else { - actions.andExpect(status().isOk()); - actions.andExpect(content().string(containsString("Email: " + email))); - if (!disableUAA){ - actions.andExpect(content().string(containsString("password_confirmation"))); - } else if (!disableSaml){ - actions.andExpect(content().string(containsString("Test Saml Provider"))); - } - } - assertEquals(expectedOrigin, getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select origin from users where username=?", new Object[]{email}, String.class)); + assertTrue("LDAP user should be verified after accepting invite", queryUserForField(email, "verified", Boolean.class)); } @Test - public void invite_saml_user_with_different_email_after_login() throws Exception { + public void invite_saml_user_will_redirect_upon_accept() throws Exception { ZoneScimInviteData zone = createZoneForInvites(); String entityID = generator.generate(); String originKey = generator.generate().toLowerCase(); @@ -535,29 +390,23 @@ public void invite_saml_user_with_different_email_after_login() throws Exception String email = new RandomValueStringGenerator().generate().toLowerCase()+"@"+domain; MimeMessageWrapper message = inviteUser(email,zone.getAdminToken(), zone.getZone().getIdentityZone().getSubdomain(), zone.getScimInviteClient().getClientId(), provider.getOriginKey()); String code = extractInvitationCode(message.getContentString()); - MvcResult result = - getMockMvc().perform(get("/invitations/accept") - .param("code", code) - .accept(MediaType.TEXT_HTML) - .header("Host", zone.getZone().getIdentityZone().getSubdomain() + ".localhost") - ) - .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl(String.format("/saml/discovery?returnIDParam=idp&entityID=%s.cloudfoundry-saml-login&idp=%s&isPassive=true", zone.getZone().getIdentityZone().getId(), originKey))) - .andReturn(); + assertFalse("User should not be verified", queryUserForField(email, "verified", Boolean.class)); + assertEquals(originKey, queryUserForField(email, Origin.ORIGIN, String.class)); - assertEquals(provider.getOriginKey(), getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select origin from users where username=?", new Object[]{email}, String.class)); - MockHttpSession session = (MockHttpSession) result.getRequest().getSession(false); - assertNotNull(session); - try { - mockSamlAuthentication(zone.getZone(), originKey, entityID, email, generator.generate()+"@test.org"); - fail(); - } catch (BadCredentialsException x) {} + getMockMvc().perform( + get("/invitations/accept") + .param("code", code) + .accept(MediaType.TEXT_HTML) + .header("Host", zone.getZone().getIdentityZone().getSubdomain() + ".localhost") + ) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl(REDIRECT_URI)); - //validate that we did not change the invitation - assertEquals(provider.getOriginKey(), getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select origin from users where username=?", new Object[]{email}, String.class)); - assertEquals(false, getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select verified from users where username=?", new Object[]{email}, Boolean.class)); + + assertEquals(provider.getOriginKey(), queryUserForField(email, Origin.ORIGIN, String.class)); + assertTrue("Saml user should be verified after clicking on the accept link", queryUserForField(email, "verified", Boolean.class)); } protected IdentityProvider createIdentityProvider(IdentityZoneCreationResult zone, String nameAndOriginKey, AbstractIdentityProviderDefinition definition) throws Exception { @@ -629,23 +478,8 @@ protected ExpiringUsernameAuthenticationToken getExpiringUsernameAuthenticationT } } - @Test - public void invite_user_show_correct_saml_idp_for_acceptance() throws Exception {} - - @Test - public void accept_invite_for_uaa_changes_correct_origin() throws Exception {} - - @Test - public void accept_invite_for_saml_changes_correct_origin() throws Exception {} - - @Test - public void accept_invite_for_ldap_changes_correct_origin() throws Exception {} - - @Test - public void accept_invite_for_existing_user_deletes_invite() throws Exception {} - public MimeMessageWrapper inviteUser(String email, String userInviteToken, String subdomain, String clientId, String expectedOrigin) throws Exception { - InvitationsEndpointMockMvcTests.sendRequestWithToken(userInviteToken, subdomain, clientId, "example.com", email); + InvitationsEndpointMockMvcTests.sendRequestWithToken(userInviteToken, subdomain, clientId, REDIRECT_URI, email); assertEquals(expectedOrigin, getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("SELECT origin FROM users WHERE username='" + email + "'", String.class)); assertEquals(1, fakeJavaMailSender.getSentMessages().size()); MimeMessageWrapper message = fakeJavaMailSender.getSentMessages().get(0); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java index 4fab2fe8e40..2191868f14b 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java @@ -628,7 +628,10 @@ public static CookieCsrfPostProcessor cookieCsrf() { } public static ClientDetails createScimClient(MockMvc mockMvc, String adminAccessToken, String id, String secret, String resourceIds, String scopes, List grantTypes, String authorities) throws Exception { - ClientDetailsModification client = new ClientDetailsModification(id, resourceIds, scopes, commaDelineatedGrantTypes(grantTypes), authorities); + return createScimClient(mockMvc, adminAccessToken, id, secret, resourceIds, scopes, grantTypes, authorities, null); + } + public static ClientDetails createScimClient(MockMvc mockMvc, String adminAccessToken, String id, String secret, String resourceIds, String scopes, List grantTypes, String authorities, String redirectUris) throws Exception { + ClientDetailsModification client = new ClientDetailsModification(id, resourceIds, scopes, commaDelineatedGrantTypes(grantTypes), authorities, redirectUris); client.setClientSecret(secret); return utils().createClient(mockMvc,adminAccessToken, client); } From 5c55a1250a87c2f3dee4d126401db1dd4f8eabf4 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Wed, 30 Sep 2015 12:16:37 -0400 Subject: [PATCH 029/103] Add new V2_7_2__ scripts to drop user management column - Add V2_7_1__ scripts back in [#104443550] https://www.pivotaltracker.com/story/show/104443550 --- .../org/cloudfoundry/identity/uaa/zone/IdentityProvider.java | 1 + .../uaa/db/hsqldb/V2_7_1__Drop_Allow_User_Management.sql | 1 - .../identity/uaa/db/hsqldb/V2_7_1__Update_User_Management.sql | 2 ++ .../identity/uaa/db/hsqldb/V2_7_2__Drop_User_Management.sql | 1 + .../uaa/db/mysql/V2_7_1__Drop_Allow_User_Management.sql | 1 - .../identity/uaa/db/mysql/V2_7_1__Update_User_Management.sql | 2 ++ .../identity/uaa/db/mysql/V2_7_2__Drop_User_Management.sql | 1 + .../uaa/db/postgresql/V2_7_1__Drop_Allow_User_Management.sql | 1 - .../uaa/db/postgresql/V2_7_1__Update_User_Management.sql | 2 ++ .../identity/uaa/db/postgresql/V2_7_2__Drop_User_Management.sql | 1 + uaa/src/main/webapp/WEB-INF/spring-servlet.xml | 1 + 11 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 common/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_1__Drop_Allow_User_Management.sql create mode 100644 common/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_1__Update_User_Management.sql create mode 100644 common/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_2__Drop_User_Management.sql delete mode 100644 common/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_7_1__Drop_Allow_User_Management.sql create mode 100644 common/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_7_1__Update_User_Management.sql create mode 100644 common/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_7_2__Drop_User_Management.sql delete mode 100644 common/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_1__Drop_Allow_User_Management.sql create mode 100644 common/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_1__Update_User_Management.sql create mode 100644 common/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_2__Drop_User_Management.sql diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProvider.java b/common/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProvider.java index 4bb7915eb6d..39a1daaeceb 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProvider.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProvider.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.type.TypeReference; + import org.cloudfoundry.identity.uaa.authentication.Origin; import org.cloudfoundry.identity.uaa.config.LockoutPolicy; import org.cloudfoundry.identity.uaa.config.PasswordPolicy; diff --git a/common/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_1__Drop_Allow_User_Management.sql b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_1__Drop_Allow_User_Management.sql deleted file mode 100644 index 6b1b2a6823e..00000000000 --- a/common/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_1__Drop_Allow_User_Management.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE identity_provider DROP COLUMN allow_internal_user_management; diff --git a/common/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_1__Update_User_Management.sql b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_1__Update_User_Management.sql new file mode 100644 index 00000000000..25017307728 --- /dev/null +++ b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_1__Update_User_Management.sql @@ -0,0 +1,2 @@ +ALTER TABLE identity_provider DROP COLUMN allow_internal_user_management; +ALTER TABLE identity_provider ADD COLUMN disable_internal_user_management BOOLEAN default false; diff --git a/common/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_2__Drop_User_Management.sql b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_2__Drop_User_Management.sql new file mode 100644 index 00000000000..1319abafa63 --- /dev/null +++ b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_2__Drop_User_Management.sql @@ -0,0 +1 @@ +ALTER TABLE identity_provider DROP COLUMN disable_internal_user_management; diff --git a/common/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_7_1__Drop_Allow_User_Management.sql b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_7_1__Drop_Allow_User_Management.sql deleted file mode 100644 index 6b1b2a6823e..00000000000 --- a/common/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_7_1__Drop_Allow_User_Management.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE identity_provider DROP COLUMN allow_internal_user_management; diff --git a/common/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_7_1__Update_User_Management.sql b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_7_1__Update_User_Management.sql new file mode 100644 index 00000000000..25017307728 --- /dev/null +++ b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_7_1__Update_User_Management.sql @@ -0,0 +1,2 @@ +ALTER TABLE identity_provider DROP COLUMN allow_internal_user_management; +ALTER TABLE identity_provider ADD COLUMN disable_internal_user_management BOOLEAN default false; diff --git a/common/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_7_2__Drop_User_Management.sql b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_7_2__Drop_User_Management.sql new file mode 100644 index 00000000000..1319abafa63 --- /dev/null +++ b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V2_7_2__Drop_User_Management.sql @@ -0,0 +1 @@ +ALTER TABLE identity_provider DROP COLUMN disable_internal_user_management; diff --git a/common/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_1__Drop_Allow_User_Management.sql b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_1__Drop_Allow_User_Management.sql deleted file mode 100644 index 6b1b2a6823e..00000000000 --- a/common/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_1__Drop_Allow_User_Management.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE identity_provider DROP COLUMN allow_internal_user_management; diff --git a/common/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_1__Update_User_Management.sql b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_1__Update_User_Management.sql new file mode 100644 index 00000000000..25017307728 --- /dev/null +++ b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_1__Update_User_Management.sql @@ -0,0 +1,2 @@ +ALTER TABLE identity_provider DROP COLUMN allow_internal_user_management; +ALTER TABLE identity_provider ADD COLUMN disable_internal_user_management BOOLEAN default false; diff --git a/common/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_2__Drop_User_Management.sql b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_2__Drop_User_Management.sql new file mode 100644 index 00000000000..1319abafa63 --- /dev/null +++ b/common/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_2__Drop_User_Management.sql @@ -0,0 +1 @@ +ALTER TABLE identity_provider DROP COLUMN disable_internal_user_management; diff --git a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml index 82947c9452b..0352bbb5861 100755 --- a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml +++ b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml @@ -289,6 +289,7 @@ + From 24d12a8423e7ebd084884dce3c5598addaecb5fa Mon Sep 17 00:00:00 2001 From: Leslie Chang Date: Wed, 30 Sep 2015 12:07:24 -0700 Subject: [PATCH 030/103] Provide the ability to bootstrap user attribute mappings per Identity Provider [#104349358] https://www.pivotaltracker.com/story/show/104349358 Signed-off-by: Madhura Bhave --- .../uaa/ExternalIdentityProviderDefinition.java | 17 ++++++++++++++--- .../ldap/LdapIdentityProviderDefinition.java | 5 ++++- .../saml/SamlIdentityProviderConfigurator.java | 5 ++++- .../saml/SamlIdentityProviderDefinition.java | 10 ++++++++-- .../config/IdentityProviderBootstrapTest.java | 15 +++++++++++++-- .../LdapIdentityProviderDefinitionTest.java | 14 +++++++++++++- .../saml/IdentityProviderConfiguratorTests.java | 14 ++++++++++++-- .../IdentityProviderEndpointsMockMvcTests.java | 8 +++++++- 8 files changed, 75 insertions(+), 13 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/ExternalIdentityProviderDefinition.java b/common/src/main/java/org/cloudfoundry/identity/uaa/ExternalIdentityProviderDefinition.java index d267d583a6d..0067a6dd1f1 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/ExternalIdentityProviderDefinition.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/ExternalIdentityProviderDefinition.java @@ -2,6 +2,7 @@ import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; /******************************************************************************* * Cloud Foundry @@ -17,14 +18,24 @@ *******************************************************************************/ public class ExternalIdentityProviderDefinition extends AbstractIdentityProviderDefinition { public static final String EXTERNAL_GROUPS_WHITELIST = "externalGroupsWhitelist"; + public static final String USER_ATTRIBUTES = "userAttributes"; - private LinkedHashMap> externalGroupsWhitelist; + private Map> externalGroupsWhitelist; + private Map userAttributes; - public LinkedHashMap> getExternalGroupsWhitelist() { + public Map> getExternalGroupsWhitelist() { return externalGroupsWhitelist; } - public void setExternalGroupsWhitelist(LinkedHashMap> externalGroupsWhitelist) { + public void setExternalGroupsWhitelist(Map> externalGroupsWhitelist) { this.externalGroupsWhitelist = externalGroupsWhitelist; } + + public void setUserAttributes(Map userAttributes) { + this.userAttributes = userAttributes; + } + + public Map getUserAttributes() { + return userAttributes; + } } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinition.java b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinition.java index 3db263eea1d..016f055fad7 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinition.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinition.java @@ -99,7 +99,10 @@ public static LdapIdentityProviderDefinition fromConfig(Map ldapC definition.setEmailDomain((List) source.getProperty("emailDomain")); } if (source.getProperty("externalGroupsWhitelist")!=null) { - definition.setExternalGroupsWhitelist((LinkedHashMap>) source.getProperty("externalGroupsWhitelist")); + definition.setExternalGroupsWhitelist((Map>) source.getProperty("externalGroupsWhitelist")); + } + if (source.getProperty(USER_ATTRIBUTES)!=null) { + definition.setUserAttributes((Map) source.getProperty(USER_ATTRIBUTES)); } definition.setLdapProfileFile((String) source.getProperty("profile.file")); diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/SamlIdentityProviderConfigurator.java b/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/SamlIdentityProviderConfigurator.java index 1b91b6e3659..d879b9926a8 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/SamlIdentityProviderConfigurator.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/SamlIdentityProviderConfigurator.java @@ -48,6 +48,7 @@ import static org.cloudfoundry.identity.uaa.AbstractIdentityProviderDefinition.EMAIL_DOMAIN_ATTR; import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.EXTERNAL_GROUPS_WHITELIST; +import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.USER_ATTRIBUTES; public class SamlIdentityProviderConfigurator implements InitializingBean { private static Log logger = LogFactory.getLog(SamlIdentityProviderConfigurator.class); @@ -348,7 +349,8 @@ public void setIdentityProviders(Map> providers) { String iconUrl = (String)((Map)entry.getValue()).get("iconUrl"); String zoneId = (String)((Map)entry.getValue()).get("zoneId"); List emailDomain = (List) saml.get(EMAIL_DOMAIN_ATTR); - LinkedHashMap> externalGroupsWhitelist = (LinkedHashMap>) saml.get(EXTERNAL_GROUPS_WHITELIST); + Map> externalGroupsWhitelist = (Map>) saml.get(EXTERNAL_GROUPS_WHITELIST); + Map userAttributes = (Map) saml.get(USER_ATTRIBUTES); SamlIdentityProviderDefinition def = new SamlIdentityProviderDefinition(); if (alias==null) { throw new IllegalArgumentException("Invalid IDP - alias must not be null ["+metaDataLocation+"]"); @@ -367,6 +369,7 @@ public void setIdentityProviders(Map> providers) { def.setIconUrl(iconUrl); def.setEmailDomain(emailDomain); def.setExternalGroupsWhitelist(externalGroupsWhitelist); + def.setUserAttributes(userAttributes); def.setZoneId(StringUtils.hasText(zoneId) ? zoneId : IdentityZone.getUaa().getId()); toBeFetchedProviders.add(def); } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/SamlIdentityProviderDefinition.java b/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/SamlIdentityProviderDefinition.java index 8f539d980f1..01e577da4c6 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/SamlIdentityProviderDefinition.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/SamlIdentityProviderDefinition.java @@ -19,8 +19,10 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; public class SamlIdentityProviderDefinition extends ExternalIdentityProviderDefinition { @@ -60,7 +62,10 @@ public SamlIdentityProviderDefinition(String metaDataLocation, String idpEntityA this.zoneId = zoneId; } - public SamlIdentityProviderDefinition(String metaDataLocation, String idpEntityAlias, String nameID, int assertionConsumerIndex, boolean metadataTrustCheck, boolean showSamlLink, String linkText, String iconUrl, String zoneId, boolean addShadowUserOnLogin, List emailDomain, LinkedHashMap> externalGroupsWhitelist) { + public SamlIdentityProviderDefinition(String metaDataLocation, String idpEntityAlias, String nameID, int assertionConsumerIndex, + boolean metadataTrustCheck, boolean showSamlLink, String linkText, String iconUrl, + String zoneId, boolean addShadowUserOnLogin, List emailDomain, + Map> externalGroupsWhitelist, Map userAttributes) { this.metaDataLocation = metaDataLocation; this.idpEntityAlias = idpEntityAlias; this.nameID = nameID; @@ -73,6 +78,7 @@ public SamlIdentityProviderDefinition(String metaDataLocation, String idpEntityA this.addShadowUserOnLogin = addShadowUserOnLogin; setEmailDomain(emailDomain); setExternalGroupsWhitelist(externalGroupsWhitelist); + setUserAttributes(userAttributes); } @JsonIgnore @@ -211,7 +217,7 @@ public void setAddShadowUserOnLogin(boolean addShadowUserOnLogin) { } public SamlIdentityProviderDefinition clone() { - return new SamlIdentityProviderDefinition(metaDataLocation, idpEntityAlias, nameID, assertionConsumerIndex, metadataTrustCheck, showSamlLink, linkText, iconUrl, zoneId, addShadowUserOnLogin, getEmailDomain()!=null ? new ArrayList<>(getEmailDomain()) : null, getExternalGroupsWhitelist()); + return new SamlIdentityProviderDefinition(metaDataLocation, idpEntityAlias, nameID, assertionConsumerIndex, metadataTrustCheck, showSamlLink, linkText, iconUrl, zoneId, addShadowUserOnLogin, getEmailDomain()!=null ? new ArrayList<>(getEmailDomain()) : null, getExternalGroupsWhitelist()!=null ? new LinkedHashMap(getExternalGroupsWhitelist()) : null, getUserAttributes()!=null ? new HashMap(getUserAttributes()) : null); } @Override diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrapTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrapTest.java index 06150a3384a..4324991c900 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrapTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrapTest.java @@ -41,6 +41,7 @@ import static org.cloudfoundry.identity.uaa.AbstractIdentityProviderDefinition.EMAIL_DOMAIN_ATTR; import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.EXTERNAL_GROUPS_WHITELIST; +import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.USER_ATTRIBUTES; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -80,9 +81,14 @@ public void testLdapBootstrap() throws Exception { IdentityProviderBootstrap bootstrap = new IdentityProviderBootstrap(provisioning, new MockEnvironment()); HashMap ldapConfig = new HashMap<>(); ldapConfig.put(EMAIL_DOMAIN_ATTR, Arrays.asList("test.domain")); - LinkedHashMap> attrMap = new LinkedHashMap<>(); + Map> attrMap = new LinkedHashMap<>(); attrMap.put("key", Arrays.asList("value")); ldapConfig.put(EXTERNAL_GROUPS_WHITELIST, attrMap); + + Map userAttributes = new HashMap<>(); + userAttributes.put("given_name", "first_name"); + ldapConfig.put(USER_ATTRIBUTES, userAttributes); + bootstrap.setLdapConfig(ldapConfig); bootstrap.afterPropertiesSet(); @@ -93,6 +99,7 @@ public void testLdapBootstrap() throws Exception { assertEquals(Origin.LDAP, ldapProvider.getType()); assertEquals("test.domain", ldapProvider.getConfigValue(LdapIdentityProviderDefinition.class).getEmailDomain().get(0)); assertEquals(Arrays.asList("value"), ldapProvider.getConfigValue(LdapIdentityProviderDefinition.class).getExternalGroupsWhitelist().get("key")); + assertEquals("first_name", ldapProvider.getConfigValue(LdapIdentityProviderDefinition.class).getUserAttributes().get("given_name")); } @Test @@ -218,10 +225,14 @@ public void testSamlBootstrap() throws Exception { definition.setShowSamlLink(true); definition.setMetadataTrustCheck(true); definition.setEmailDomain(Arrays.asList("test.domain")); - LinkedHashMap> externalGroupsWhitelist = new LinkedHashMap<>(); + Map> externalGroupsWhitelist = new LinkedHashMap<>(); externalGroupsWhitelist.put("key", Arrays.asList("value1", "value2")); definition.setExternalGroupsWhitelist(externalGroupsWhitelist); + Map userAttributes = new HashMap<>(); + userAttributes.put("given_name", "first_name"); + definition.setUserAttributes(userAttributes); + SamlIdentityProviderConfigurator configurator = mock(SamlIdentityProviderConfigurator.class); when(configurator.getIdentityProviderDefinitions()).thenReturn(Arrays.asList(definition)); diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinitionTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinitionTest.java index 19a7a2f53a6..b5afa6f2994 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinitionTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinitionTest.java @@ -23,6 +23,7 @@ import java.io.UnsupportedEncodingException; import java.util.Arrays; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -348,11 +349,22 @@ public void testSetEmailDomain() { @Test public void set_external_groups_whitelist() { LdapIdentityProviderDefinition def = new LdapIdentityProviderDefinition(); - LinkedHashMap> externalGroupsWhitelist = new LinkedHashMap<>(); + Map> externalGroupsWhitelist = new LinkedHashMap<>(); externalGroupsWhitelist.put("key", Arrays.asList("value")); def.setExternalGroupsWhitelist(externalGroupsWhitelist); assertEquals(Arrays.asList("value"), def.getExternalGroupsWhitelist().get("key")); def = JsonUtils.readValue(JsonUtils.writeValueAsString(def), LdapIdentityProviderDefinition.class); assertEquals(Arrays.asList("value"), def.getExternalGroupsWhitelist().get("key")); } + + @Test + public void set_user_attributes() { + LdapIdentityProviderDefinition def = new LdapIdentityProviderDefinition(); + Map userAttributes = new HashMap<>(); + userAttributes.put("given_name", "first_name"); + def.setUserAttributes(userAttributes); + assertEquals("first_name", def.getUserAttributes().get("given_name")); + def = JsonUtils.readValue(JsonUtils.writeValueAsString(def), LdapIdentityProviderDefinition.class); + assertEquals("first_name", def.getUserAttributes().get("given_name")); + } } diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/login/saml/IdentityProviderConfiguratorTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/login/saml/IdentityProviderConfiguratorTests.java index 06a4a0fd0c9..8f1e2f84ac9 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/login/saml/IdentityProviderConfiguratorTests.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/login/saml/IdentityProviderConfiguratorTests.java @@ -42,6 +42,8 @@ import java.util.Timer; import java.util.UUID; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonMap; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -95,6 +97,12 @@ public static void initializeOpenSAML() throws Exception { " "+ AbstractIdentityProviderDefinition.EMAIL_DOMAIN_ATTR+":\n" + " - test.org\n" + " - test.com\n" + + " externalGroupsWhitelist:\n" + + " roles:\n" + + " - admin\n" + + " - user\n" + + " userAttributes:\n" + + " given_name: first_name\n" + " okta-local-2:\n" + " idpMetadata: |\n" + " MIICmTCCAgKgAwIBAgIGAUPATqmEMA0GCSqGSIb3DQEBBQUAMIGPMQswCQYDVQQGEwJVUzETMBEG\n" + @@ -269,7 +277,7 @@ public void testGetIdentityProviderDefinitionsForZone() throws Exception { @Test public void testGetIdentityProviderDefinititonsForAllowedProviders() throws Exception { BaseClientDetails clientDetails = new BaseClientDetails(); - List clientIdpAliases = Arrays.asList("simplesamlphp-url", "okta-local-2"); + List clientIdpAliases = asList("simplesamlphp-url", "okta-local-2"); clientDetails.addAdditionalInformation(ClientConstants.ALLOWED_PROVIDERS, clientIdpAliases); conf.setIdentityProviders(data); @@ -330,9 +338,11 @@ protected void testGetIdentityProviderDefinitions(int count, boolean addData) th assertEquals(0, idp.getAssertionConsumerIndex()); assertEquals("Okta Preview 1", idp.getLinkText()); assertEquals("http://link.to/icon.jpg", idp.getIconUrl()); + assertEquals(singletonMap("given_name", "first_name"), idp.getUserAttributes()); + assertEquals(singletonMap("roles", asList("admin", "user")), idp.getExternalGroupsWhitelist()); assertTrue(idp.isShowSamlLink()); assertTrue(idp.isMetadataTrustCheck()); - assertTrue(idp.getEmailDomain().containsAll(Arrays.asList("test.com", "test.org"))); + assertTrue(idp.getEmailDomain().containsAll(asList("test.com", "test.org"))); break; } case "okta-local-2" : { diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityProviderEndpointsMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityProviderEndpointsMockMvcTests.java index 9da72f22e56..81d8b20abaa 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityProviderEndpointsMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityProviderEndpointsMockMvcTests.java @@ -42,8 +42,10 @@ import org.springframework.util.StringUtils; import java.util.Arrays; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -106,9 +108,12 @@ public void testCreateSamlProvider() throws Exception { provider.setOriginKey(origin); SamlIdentityProviderDefinition samlDefinition = new SamlIdentityProviderDefinition(metadata, null, null, 0, false, true, "Test SAML Provider", null, null); samlDefinition.setEmailDomain(Arrays.asList("test.com", "test2.com")); - LinkedHashMap> externalGroupsWhitelist = new LinkedHashMap<>(); + Map> externalGroupsWhitelist = new LinkedHashMap<>(); externalGroupsWhitelist.put("key", Arrays.asList("value")); + Map userAttributes = new HashMap<>(); + userAttributes.put("given_name", "first_name"); samlDefinition.setExternalGroupsWhitelist(externalGroupsWhitelist); + samlDefinition.setUserAttributes(userAttributes); provider.setConfig(JsonUtils.writeValueAsString(samlDefinition)); @@ -118,6 +123,7 @@ public void testCreateSamlProvider() throws Exception { SamlIdentityProviderDefinition samlCreated = created.getConfigValue(SamlIdentityProviderDefinition.class); assertEquals(Arrays.asList("test.com", "test2.com"), samlCreated.getEmailDomain()); assertEquals(externalGroupsWhitelist, samlCreated.getExternalGroupsWhitelist()); + assertEquals(userAttributes, samlCreated.getUserAttributes()); assertEquals(IdentityZone.getUaa().getId(), samlCreated.getZoneId()); assertEquals(provider.getOriginKey(), samlCreated.getIdpEntityAlias()); } From ec8290e14af0617b64053a0c517fe1de79b14687 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Wed, 30 Sep 2015 10:39:55 -0600 Subject: [PATCH 031/103] For legacy auth codes without epoch timestamp, delete them after three days. This avoids time zone differences between UAA and the DB. https://www.pivotaltracker.com/story/show/103340014 [#103340014] fixes #223 --- .../uaa/oauth/token/UaaTokenStore.java | 9 +- .../uaa/oauth/token/UaaTokenStoreTests.java | 179 +++++++++++++++++- .../identity/uaa/test/JdbcTestBase.java | 6 +- 3 files changed, 178 insertions(+), 16 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenStore.java b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenStore.java index a694259c51f..95e8d8019e2 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenStore.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenStore.java @@ -52,6 +52,7 @@ public class UaaTokenStore implements AuthorizationCodeServices { public static final long EXPIRATION_TIME = 5*60*1000; + public static final long LEGACY_CODE_EXPIRATION_TIME = 3*24*60*60*1000; public static final String USER_AUTHENTICATION_UAA_PRINCIPAL = "userAuthentication.uaaPrincipal"; public static final String USER_AUTHENTICATION_AUTHORITIES = "userAuthentication.authorities"; public static final String OAUTH2_REQUEST_PARAMETERS = "oauth2Request.requestParameters"; @@ -69,7 +70,7 @@ public class UaaTokenStore implements AuthorizationCodeServices { private static final String SQL_INSERT_STATEMENT = "insert into oauth_code (code, user_id, client_id, expiresat, authentication) values (?, ?, ?, ?, ?)"; private static final String SQL_DELETE_STATEMENT = "delete from oauth_code where code = ?"; private static final String SQL_EXPIRE_STATEMENT = "delete from oauth_code where expiresat > 0 AND expiresat < ?"; - private static final String SQL_CLEAN_STATEMENT = "delete from oauth_code where created < ?"; + private static final String SQL_CLEAN_STATEMENT = "delete from oauth_code where created < ? and expiresat = 0"; private final DataSource dataSource; private final long expirationTime; @@ -128,7 +129,7 @@ public OAuth2Authentication consumeAuthorizationCode(String code) throws Invalid try { if (tokenCode.isExpired()) { logger.debug("[oauth_code] Found code, but it expired:"+tokenCode); - return null; + throw new InvalidGrantException("Authorization code expired: " + code); } else if (tokenCode.getExpiresAt() == 0) { return SerializationUtils.deserialize(tokenCode.getAuthentication()); } else { @@ -140,7 +141,7 @@ public OAuth2Authentication consumeAuthorizationCode(String code) throws Invalid } }catch (EmptyResultDataAccessException x) { } - return null; + throw new InvalidGrantException("Invalid authorization code: " + code); } protected byte[] serializeOauth2Authentication(OAuth2Authentication auth2Authentication) { @@ -209,7 +210,7 @@ protected void performExpirationClean() { JdbcTemplate template = new JdbcTemplate(dataSource); int expired = template.update(SQL_EXPIRE_STATEMENT, System.currentTimeMillis()); logger.debug("[oauth_code] Removed "+expired+" expired entries."); - expired = template.update(SQL_CLEAN_STATEMENT, new Timestamp(System.currentTimeMillis()-getExpirationTime())); + expired = template.update(SQL_CLEAN_STATEMENT, new Timestamp(System.currentTimeMillis()-LEGACY_CODE_EXPIRATION_TIME)); logger.debug("[oauth_code] Removed "+expired+" old entries."); } } diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenStoreTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenStoreTests.java index 9293a222480..f825c444e3a 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenStoreTests.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenStoreTests.java @@ -23,32 +23,44 @@ import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.junit.Before; import org.junit.Test; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.common.exceptions.InvalidGrantException; import org.springframework.security.oauth2.common.util.OAuth2Utils; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.TokenRequest; import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices; +import javax.sql.DataSource; +import java.io.PrintWriter; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; import java.sql.Timestamp; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.logging.Logger; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; public class UaaTokenStoreTests extends JdbcTestBase { private UaaTokenStore store; - private JdbcAuthorizationCodeServices springSecurityStore; + private JdbcAuthorizationCodeServices legacyCodeServices; private OAuth2Authentication clientAuthentication; private OAuth2Authentication usernamePasswordAuthentication; private OAuth2Authentication uaaAuthentication; @@ -62,7 +74,7 @@ public void createTokenStore() throws Exception { List userAuthorities = Arrays.asList(new SimpleGrantedAuthority("openid")); store = new UaaTokenStore(dataSource); - springSecurityStore = new JdbcAuthorizationCodeServices(dataSource); + legacyCodeServices = new JdbcAuthorizationCodeServices(dataSource); BaseClientDetails client = new BaseClientDetails("clientid", null, "openid","client_credentials,password", "oauth.login", null); Map parameters = new HashMap<>(); parameters.put(OAuth2Utils.CLIENT_ID, client.getClientId()); @@ -87,7 +99,7 @@ public void createTokenStore() throws Exception { @Test public void test_ConsumeClientCredentials_From_OldStore() throws Exception { - String code = springSecurityStore.createAuthorizationCode(clientAuthentication); + String code = legacyCodeServices.createAuthorizationCode(clientAuthentication); assertEquals(1, jdbcTemplate.queryForInt("SELECT count(*) FROM oauth_code WHERE code = ?", code)); OAuth2Authentication authentication = store.consumeAuthorizationCode(code); assertNotNull(authentication); @@ -137,6 +149,21 @@ public void testRetrieveToken() throws Exception { assertNotNull(authentication); } + @Test(expected = InvalidGrantException.class) + public void testRetrieve_Expired_Token() throws Exception { + String code = store.createAuthorizationCode(clientAuthentication); + assertEquals(1, jdbcTemplate.queryForInt("SELECT count(*) FROM oauth_code WHERE code = ?", code)); + jdbcTemplate.update("update oauth_code set expiresat = 1"); + store.consumeAuthorizationCode(code); + } + + @Test(expected = InvalidGrantException.class) + public void testRetrieve_Non_Existent_Token() throws Exception { + String code = store.createAuthorizationCode(clientAuthentication); + assertEquals(1, jdbcTemplate.queryForInt("SELECT count(*) FROM oauth_code WHERE code = ?", code)); + store.consumeAuthorizationCode("non-existent"); + } + @Test public void testCleanUpExpiredTokensBasedOnExpiresField() throws Exception { int count = 10; @@ -148,21 +175,34 @@ public void testCleanUpExpiredTokensBasedOnExpiresField() throws Exception { jdbcTemplate.update("UPDATE oauth_code SET expiresat = ?", System.currentTimeMillis() - 60000); - assertNull(store.consumeAuthorizationCode(lastCode)); + try { + store.consumeAuthorizationCode(lastCode); + fail(); + } catch (InvalidGrantException e) { + } assertEquals(0, jdbcTemplate.queryForInt("SELECT count(*) FROM oauth_code")); } @Test - public void testCleanUpUnusedOldTokens() throws Exception { + public void testCleanUpLegacyCodes_Codes_Without_ExpiresAt_After_3_Days() throws Exception { int count = 10; - String lastCode = null; + long oneday = 1000 * 60 * 60 * 24; for (int i=0; i dbProfile = Arrays.stream(environment.getActiveProfiles()).filter(s -> s.contains("sql")).findFirst(); + String db = dbProfile.isPresent() ? dbProfile.get() : "hsqldb"; + + Connection con = dataSource.getConnection(); + try { + Connection dontClose = (Connection) Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[]{Connection.class}, + new DontCloseConnection(con)); + + SameConnectionDataSource sameConnectionDataSource = new SameConnectionDataSource(dontClose); + JdbcTemplate template = new JdbcTemplate(sameConnectionDataSource); + switch (db) { + case "mysql" : + template.update("SET @@session.time_zone='-11:00'"); + break; + case "postgresql" : + template.update("SET TIME ZONE -11"); + break; + case "hsqldb" : + template.update("SET TIME ZONE INTERVAL '-11:00' HOUR TO MINUTE"); + break; + default: + fail("Unknown DB profile:"+db); + } + + store = new UaaTokenStore(sameConnectionDataSource); + legacyCodeServices = new JdbcAuthorizationCodeServices(sameConnectionDataSource); + int count = 10; + String lastCode = null; + for (int i=0; i T unwrap(Class iface) throws SQLException { + return null; + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return false; + } + } + + public class DontCloseConnection implements InvocationHandler { + public static final String CLOSE_VAL = "close"; + private final Connection con; + + public DontCloseConnection(Connection con) { + this.con = con; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (CLOSE_VAL.equals(method.getName())) { + return null; + } else { + return method.invoke(con, args); + } + } + } } \ No newline at end of file diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/test/JdbcTestBase.java b/common/src/test/java/org/cloudfoundry/identity/uaa/test/JdbcTestBase.java index 16ef5254a31..e52c3db8766 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/test/JdbcTestBase.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/test/JdbcTestBase.java @@ -12,8 +12,6 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.test; -import javax.sql.DataSource; - import org.cloudfoundry.identity.uaa.TestClassNullifier; import org.cloudfoundry.identity.uaa.rest.jdbc.LimitSqlAdapter; import org.flywaydb.core.Flyway; @@ -24,6 +22,8 @@ import org.springframework.util.StringUtils; import org.springframework.web.context.support.XmlWebApplicationContext; +import javax.sql.DataSource; + /** * Created by fhanik on 12/9/14. */ @@ -34,6 +34,7 @@ public class JdbcTestBase extends TestClassNullifier { protected JdbcTemplate jdbcTemplate; protected DataSource dataSource; protected LimitSqlAdapter limitSqlAdapter; + protected MockEnvironment environment; @Before public void setUp() throws Exception { @@ -45,6 +46,7 @@ public void setUp() throws Exception { } public void setUp(MockEnvironment environment) throws Exception { + this.environment = environment; webApplicationContext = new XmlWebApplicationContext(); webApplicationContext.setEnvironment(environment); webApplicationContext.setConfigLocations(new String[]{"classpath:spring/env.xml", "classpath:spring/data-source.xml"}); From 3abfcb2fe7e782a089c6295c7fb303d812c14666 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Wed, 30 Sep 2015 14:13:54 -0700 Subject: [PATCH 032/103] Rename userAttributes to attributeMappings and make whitelist a list [#104349358] https://www.pivotaltracker.com/story/show/104349358 Signed-off-by: Leslie Chang --- .../ExternalIdentityProviderDefinition.java | 19 ++++++------- .../ldap/LdapIdentityProviderDefinition.java | 7 ++--- .../SamlIdentityProviderConfigurator.java | 9 +++--- .../saml/SamlIdentityProviderDefinition.java | 6 ++-- .../config/IdentityProviderBootstrapTest.java | 28 ++++++++++--------- .../LdapIdentityProviderDefinitionTest.java | 19 +++++++------ .../IdentityProviderConfiguratorTests.java | 17 ++++++----- ...IdentityProviderEndpointsMockMvcTests.java | 13 +++++---- 8 files changed, 61 insertions(+), 57 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/ExternalIdentityProviderDefinition.java b/common/src/main/java/org/cloudfoundry/identity/uaa/ExternalIdentityProviderDefinition.java index 0067a6dd1f1..4f88cd547e6 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/ExternalIdentityProviderDefinition.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/ExternalIdentityProviderDefinition.java @@ -1,6 +1,5 @@ package org.cloudfoundry.identity.uaa; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -18,24 +17,24 @@ *******************************************************************************/ public class ExternalIdentityProviderDefinition extends AbstractIdentityProviderDefinition { public static final String EXTERNAL_GROUPS_WHITELIST = "externalGroupsWhitelist"; - public static final String USER_ATTRIBUTES = "userAttributes"; + public static final String ATTRIBUTE_MAPPINGS = "attributeMappings"; - private Map> externalGroupsWhitelist; - private Map userAttributes; + private List externalGroupsWhitelist; + private Map attributeMappings; - public Map> getExternalGroupsWhitelist() { + public List getExternalGroupsWhitelist() { return externalGroupsWhitelist; } - public void setExternalGroupsWhitelist(Map> externalGroupsWhitelist) { + public void setExternalGroupsWhitelist(List externalGroupsWhitelist) { this.externalGroupsWhitelist = externalGroupsWhitelist; } - public void setUserAttributes(Map userAttributes) { - this.userAttributes = userAttributes; + public void setAttributeMappings(Map attributeMappings) { + this.attributeMappings = attributeMappings; } - public Map getUserAttributes() { - return userAttributes; + public Map getAttributeMappings() { + return attributeMappings; } } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinition.java b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinition.java index 016f055fad7..e96f38bdade 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinition.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinition.java @@ -21,7 +21,6 @@ import org.springframework.util.StringUtils; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -99,10 +98,10 @@ public static LdapIdentityProviderDefinition fromConfig(Map ldapC definition.setEmailDomain((List) source.getProperty("emailDomain")); } if (source.getProperty("externalGroupsWhitelist")!=null) { - definition.setExternalGroupsWhitelist((Map>) source.getProperty("externalGroupsWhitelist")); + definition.setExternalGroupsWhitelist((List) source.getProperty("externalGroupsWhitelist")); } - if (source.getProperty(USER_ATTRIBUTES)!=null) { - definition.setUserAttributes((Map) source.getProperty(USER_ATTRIBUTES)); + if (source.getProperty(ATTRIBUTE_MAPPINGS)!=null) { + definition.setAttributeMappings((Map) source.getProperty(ATTRIBUTE_MAPPINGS)); } definition.setLdapProfileFile((String) source.getProperty("profile.file")); diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/SamlIdentityProviderConfigurator.java b/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/SamlIdentityProviderConfigurator.java index d879b9926a8..c222c0e6664 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/SamlIdentityProviderConfigurator.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/SamlIdentityProviderConfigurator.java @@ -38,7 +38,6 @@ import java.util.Date; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -48,7 +47,7 @@ import static org.cloudfoundry.identity.uaa.AbstractIdentityProviderDefinition.EMAIL_DOMAIN_ATTR; import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.EXTERNAL_GROUPS_WHITELIST; -import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.USER_ATTRIBUTES; +import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.ATTRIBUTE_MAPPINGS; public class SamlIdentityProviderConfigurator implements InitializingBean { private static Log logger = LogFactory.getLog(SamlIdentityProviderConfigurator.class); @@ -349,8 +348,8 @@ public void setIdentityProviders(Map> providers) { String iconUrl = (String)((Map)entry.getValue()).get("iconUrl"); String zoneId = (String)((Map)entry.getValue()).get("zoneId"); List emailDomain = (List) saml.get(EMAIL_DOMAIN_ATTR); - Map> externalGroupsWhitelist = (Map>) saml.get(EXTERNAL_GROUPS_WHITELIST); - Map userAttributes = (Map) saml.get(USER_ATTRIBUTES); + List externalGroupsWhitelist = (List) saml.get(EXTERNAL_GROUPS_WHITELIST); + Map attributeMappings = (Map) saml.get(ATTRIBUTE_MAPPINGS); SamlIdentityProviderDefinition def = new SamlIdentityProviderDefinition(); if (alias==null) { throw new IllegalArgumentException("Invalid IDP - alias must not be null ["+metaDataLocation+"]"); @@ -369,7 +368,7 @@ public void setIdentityProviders(Map> providers) { def.setIconUrl(iconUrl); def.setEmailDomain(emailDomain); def.setExternalGroupsWhitelist(externalGroupsWhitelist); - def.setUserAttributes(userAttributes); + def.setAttributeMappings(attributeMappings); def.setZoneId(StringUtils.hasText(zoneId) ? zoneId : IdentityZone.getUaa().getId()); toBeFetchedProviders.add(def); } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/SamlIdentityProviderDefinition.java b/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/SamlIdentityProviderDefinition.java index 01e577da4c6..9d2c7bd406a 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/SamlIdentityProviderDefinition.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/SamlIdentityProviderDefinition.java @@ -65,7 +65,7 @@ public SamlIdentityProviderDefinition(String metaDataLocation, String idpEntityA public SamlIdentityProviderDefinition(String metaDataLocation, String idpEntityAlias, String nameID, int assertionConsumerIndex, boolean metadataTrustCheck, boolean showSamlLink, String linkText, String iconUrl, String zoneId, boolean addShadowUserOnLogin, List emailDomain, - Map> externalGroupsWhitelist, Map userAttributes) { + List externalGroupsWhitelist, Map attributeMappings) { this.metaDataLocation = metaDataLocation; this.idpEntityAlias = idpEntityAlias; this.nameID = nameID; @@ -78,7 +78,7 @@ public SamlIdentityProviderDefinition(String metaDataLocation, String idpEntityA this.addShadowUserOnLogin = addShadowUserOnLogin; setEmailDomain(emailDomain); setExternalGroupsWhitelist(externalGroupsWhitelist); - setUserAttributes(userAttributes); + setAttributeMappings(attributeMappings); } @JsonIgnore @@ -217,7 +217,7 @@ public void setAddShadowUserOnLogin(boolean addShadowUserOnLogin) { } public SamlIdentityProviderDefinition clone() { - return new SamlIdentityProviderDefinition(metaDataLocation, idpEntityAlias, nameID, assertionConsumerIndex, metadataTrustCheck, showSamlLink, linkText, iconUrl, zoneId, addShadowUserOnLogin, getEmailDomain()!=null ? new ArrayList<>(getEmailDomain()) : null, getExternalGroupsWhitelist()!=null ? new LinkedHashMap(getExternalGroupsWhitelist()) : null, getUserAttributes()!=null ? new HashMap(getUserAttributes()) : null); + return new SamlIdentityProviderDefinition(metaDataLocation, idpEntityAlias, nameID, assertionConsumerIndex, metadataTrustCheck, showSamlLink, linkText, iconUrl, zoneId, addShadowUserOnLogin, getEmailDomain()!=null ? new ArrayList<>(getEmailDomain()) : null, getExternalGroupsWhitelist()!=null ? new ArrayList<>(getExternalGroupsWhitelist()) : null, getAttributeMappings()!=null ? new HashMap(getAttributeMappings()) : null); } @Override diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrapTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrapTest.java index 4324991c900..00a807dd1db 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrapTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrapTest.java @@ -32,6 +32,7 @@ import org.junit.Test; import org.springframework.mock.env.MockEnvironment; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; @@ -41,7 +42,7 @@ import static org.cloudfoundry.identity.uaa.AbstractIdentityProviderDefinition.EMAIL_DOMAIN_ATTR; import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.EXTERNAL_GROUPS_WHITELIST; -import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.USER_ATTRIBUTES; +import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.ATTRIBUTE_MAPPINGS; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -81,13 +82,13 @@ public void testLdapBootstrap() throws Exception { IdentityProviderBootstrap bootstrap = new IdentityProviderBootstrap(provisioning, new MockEnvironment()); HashMap ldapConfig = new HashMap<>(); ldapConfig.put(EMAIL_DOMAIN_ATTR, Arrays.asList("test.domain")); - Map> attrMap = new LinkedHashMap<>(); - attrMap.put("key", Arrays.asList("value")); + List attrMap = new ArrayList<>(); + attrMap.add("value"); ldapConfig.put(EXTERNAL_GROUPS_WHITELIST, attrMap); - Map userAttributes = new HashMap<>(); - userAttributes.put("given_name", "first_name"); - ldapConfig.put(USER_ATTRIBUTES, userAttributes); + Map attributeMappings = new HashMap<>(); + attributeMappings.put("given_name", "first_name"); + ldapConfig.put(ATTRIBUTE_MAPPINGS, attributeMappings); bootstrap.setLdapConfig(ldapConfig); bootstrap.afterPropertiesSet(); @@ -98,8 +99,8 @@ public void testLdapBootstrap() throws Exception { assertNotNull(ldapProvider.getLastModified()); assertEquals(Origin.LDAP, ldapProvider.getType()); assertEquals("test.domain", ldapProvider.getConfigValue(LdapIdentityProviderDefinition.class).getEmailDomain().get(0)); - assertEquals(Arrays.asList("value"), ldapProvider.getConfigValue(LdapIdentityProviderDefinition.class).getExternalGroupsWhitelist().get("key")); - assertEquals("first_name", ldapProvider.getConfigValue(LdapIdentityProviderDefinition.class).getUserAttributes().get("given_name")); + assertEquals(Arrays.asList("value"), ldapProvider.getConfigValue(LdapIdentityProviderDefinition.class).getExternalGroupsWhitelist()); + assertEquals("first_name", ldapProvider.getConfigValue(LdapIdentityProviderDefinition.class).getAttributeMappings().get("given_name")); } @Test @@ -225,13 +226,14 @@ public void testSamlBootstrap() throws Exception { definition.setShowSamlLink(true); definition.setMetadataTrustCheck(true); definition.setEmailDomain(Arrays.asList("test.domain")); - Map> externalGroupsWhitelist = new LinkedHashMap<>(); - externalGroupsWhitelist.put("key", Arrays.asList("value1", "value2")); + List externalGroupsWhitelist = new ArrayList<>(); + externalGroupsWhitelist.add("value1"); + externalGroupsWhitelist.add("value2"); definition.setExternalGroupsWhitelist(externalGroupsWhitelist); - Map userAttributes = new HashMap<>(); - userAttributes.put("given_name", "first_name"); - definition.setUserAttributes(userAttributes); + Map attributeMappings = new HashMap<>(); + attributeMappings.put("given_name", "first_name"); + definition.setAttributeMappings(attributeMappings); SamlIdentityProviderConfigurator configurator = mock(SamlIdentityProviderConfigurator.class); when(configurator.getIdentityProviderDefinitions()).thenReturn(Arrays.asList(definition)); diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinitionTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinitionTest.java index b5afa6f2994..5b7bbafe313 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinitionTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinitionTest.java @@ -22,6 +22,7 @@ import org.springframework.core.io.Resource; import java.io.UnsupportedEncodingException; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; @@ -349,22 +350,22 @@ public void testSetEmailDomain() { @Test public void set_external_groups_whitelist() { LdapIdentityProviderDefinition def = new LdapIdentityProviderDefinition(); - Map> externalGroupsWhitelist = new LinkedHashMap<>(); - externalGroupsWhitelist.put("key", Arrays.asList("value")); + List externalGroupsWhitelist = new ArrayList<>(); + externalGroupsWhitelist.add("value"); def.setExternalGroupsWhitelist(externalGroupsWhitelist); - assertEquals(Arrays.asList("value"), def.getExternalGroupsWhitelist().get("key")); + assertEquals(Arrays.asList("value"), def.getExternalGroupsWhitelist()); def = JsonUtils.readValue(JsonUtils.writeValueAsString(def), LdapIdentityProviderDefinition.class); - assertEquals(Arrays.asList("value"), def.getExternalGroupsWhitelist().get("key")); + assertEquals(Arrays.asList("value"), def.getExternalGroupsWhitelist()); } @Test public void set_user_attributes() { LdapIdentityProviderDefinition def = new LdapIdentityProviderDefinition(); - Map userAttributes = new HashMap<>(); - userAttributes.put("given_name", "first_name"); - def.setUserAttributes(userAttributes); - assertEquals("first_name", def.getUserAttributes().get("given_name")); + Map attributeMappings = new HashMap<>(); + attributeMappings.put("given_name", "first_name"); + def.setAttributeMappings(attributeMappings); + assertEquals("first_name", def.getAttributeMappings().get("given_name")); def = JsonUtils.readValue(JsonUtils.writeValueAsString(def), LdapIdentityProviderDefinition.class); - assertEquals("first_name", def.getUserAttributes().get("given_name")); + assertEquals("first_name", def.getAttributeMappings().get("given_name")); } } diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/login/saml/IdentityProviderConfiguratorTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/login/saml/IdentityProviderConfiguratorTests.java index 8f1e2f84ac9..92577cdf7ac 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/login/saml/IdentityProviderConfiguratorTests.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/login/saml/IdentityProviderConfiguratorTests.java @@ -34,7 +34,6 @@ import org.springframework.security.saml.trust.httpclient.TLSProtocolSocketFactory; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -98,11 +97,12 @@ public static void initializeOpenSAML() throws Exception { " - test.org\n" + " - test.com\n" + " externalGroupsWhitelist:\n" + - " roles:\n" + - " - admin\n" + - " - user\n" + - " userAttributes:\n" + + " - admin\n" + + " - user\n" + + " attributeMappings:\n" + " given_name: first_name\n" + + " external_groups:\n" + + " - roles\n" + " okta-local-2:\n" + " idpMetadata: |\n" + " MIICmTCCAgKgAwIBAgIGAUPATqmEMA0GCSqGSIb3DQEBBQUAMIGPMQswCQYDVQQGEwJVUzETMBEG\n" + @@ -338,8 +338,11 @@ protected void testGetIdentityProviderDefinitions(int count, boolean addData) th assertEquals(0, idp.getAssertionConsumerIndex()); assertEquals("Okta Preview 1", idp.getLinkText()); assertEquals("http://link.to/icon.jpg", idp.getIconUrl()); - assertEquals(singletonMap("given_name", "first_name"), idp.getUserAttributes()); - assertEquals(singletonMap("roles", asList("admin", "user")), idp.getExternalGroupsWhitelist()); + Map attributeMappings = new HashMap<>(); + attributeMappings.put("given_name", "first_name"); + attributeMappings.put("external_groups", asList("roles")); + assertEquals(attributeMappings, idp.getAttributeMappings()); + assertEquals(asList("admin", "user"), idp.getExternalGroupsWhitelist()); assertTrue(idp.isShowSamlLink()); assertTrue(idp.isMetadataTrustCheck()); assertTrue(idp.getEmailDomain().containsAll(asList("test.com", "test.org"))); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityProviderEndpointsMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityProviderEndpointsMockMvcTests.java index 81d8b20abaa..7be124c055b 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityProviderEndpointsMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityProviderEndpointsMockMvcTests.java @@ -41,6 +41,7 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.util.StringUtils; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; @@ -108,12 +109,12 @@ public void testCreateSamlProvider() throws Exception { provider.setOriginKey(origin); SamlIdentityProviderDefinition samlDefinition = new SamlIdentityProviderDefinition(metadata, null, null, 0, false, true, "Test SAML Provider", null, null); samlDefinition.setEmailDomain(Arrays.asList("test.com", "test2.com")); - Map> externalGroupsWhitelist = new LinkedHashMap<>(); - externalGroupsWhitelist.put("key", Arrays.asList("value")); - Map userAttributes = new HashMap<>(); - userAttributes.put("given_name", "first_name"); + List externalGroupsWhitelist = new ArrayList<>(); + externalGroupsWhitelist.add("value"); + Map attributeMappings = new HashMap<>(); + attributeMappings.put("given_name", "first_name"); samlDefinition.setExternalGroupsWhitelist(externalGroupsWhitelist); - samlDefinition.setUserAttributes(userAttributes); + samlDefinition.setAttributeMappings(attributeMappings); provider.setConfig(JsonUtils.writeValueAsString(samlDefinition)); @@ -123,7 +124,7 @@ public void testCreateSamlProvider() throws Exception { SamlIdentityProviderDefinition samlCreated = created.getConfigValue(SamlIdentityProviderDefinition.class); assertEquals(Arrays.asList("test.com", "test2.com"), samlCreated.getEmailDomain()); assertEquals(externalGroupsWhitelist, samlCreated.getExternalGroupsWhitelist()); - assertEquals(userAttributes, samlCreated.getUserAttributes()); + assertEquals(attributeMappings, samlCreated.getAttributeMappings()); assertEquals(IdentityZone.getUaa().getId(), samlCreated.getZoneId()); assertEquals(provider.getOriginKey(), samlCreated.getIdpEntityAlias()); } From 0142c726d423a3ea97344f7ed79873e663978eaa Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Wed, 30 Sep 2015 18:43:00 -0600 Subject: [PATCH 033/103] Fix /identity-zones security patters https://www.pivotaltracker.com/story/show/104569598 [#104569598] --- .../WEB-INF/spring/multitenant-endpoints.xml | 12 +- .../IdentityZoneEndpointsMockMvcTests.java | 333 +++++++++++------- 2 files changed, 221 insertions(+), 124 deletions(-) diff --git a/uaa/src/main/webapp/WEB-INF/spring/multitenant-endpoints.xml b/uaa/src/main/webapp/WEB-INF/spring/multitenant-endpoints.xml index 1bea640186b..535f29912e2 100644 --- a/uaa/src/main/webapp/WEB-INF/spring/multitenant-endpoints.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/multitenant-endpoints.xml @@ -42,22 +42,24 @@ entry-point-ref="oauthAuthenticationEntryPoint" use-expressions="true" authentication-manager-ref="emptyAuthenticationManager" xmlns="http://www.springframework.org/schema/security"> - + + - - + + diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointsMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointsMockMvcTests.java index a8c9a164277..b3d2ad890e8 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointsMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointsMockMvcTests.java @@ -37,6 +37,7 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.util.StringUtils; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.UUID; @@ -55,7 +56,10 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; public class IdentityZoneEndpointsMockMvcTests extends InjectedMockContextTest { + public static final List BASE_URLS = Arrays.asList("/identity-zones", "/identity-zones/"); private String identityClientToken = null; + private String identityClientZonesReadToken = null; + private String identityClientZonesWriteToken = null; private String adminToken = null; private TestClient testClient = null; private MockMvcUtils mockMvcUtils = MockMvcUtils.utils(); @@ -80,6 +84,14 @@ public void setUp() throws Exception { "identity", "identitysecret", "zones.read,zones.write,scim.zones"); + identityClientZonesReadToken = testClient.getClientCredentialsOAuthAccessToken( + "identity", + "identitysecret", + "zones.read"); + identityClientZonesWriteToken = testClient.getClientCredentialsOAuthAccessToken( + "identity", + "identitysecret", + "zones.write"); adminToken = testClient.getClientCredentialsOAuthAccessToken( "admin", "adminsecret", @@ -107,25 +119,26 @@ private ScimUser createUser(String token, String subdomain) throws Exception { byte[] requestBody = JsonUtils.writeValueAsBytes(user); MockHttpServletRequestBuilder post = post("/Users") - .header("Authorization", "Bearer " + token) - .contentType(APPLICATION_JSON) - .content(requestBody); - if (subdomain != null && !subdomain.equals("")) post.with(new SetServerNameRequestPostProcessor(subdomain + ".localhost")); + .header("Authorization", "Bearer " + token) + .contentType(APPLICATION_JSON) + .content(requestBody); + if (subdomain != null && !subdomain.equals("")) + post.with(new SetServerNameRequestPostProcessor(subdomain + ".localhost")); MvcResult result = getMockMvc().perform(post) - .andExpect(status().isCreated()) - .andExpect(header().string("ETag", "\"0\"")) - .andExpect(jsonPath("$.userName").value(user.getUserName())) - .andExpect(jsonPath("$.emails[0].value").value(user.getUserName())) - .andExpect(jsonPath("$.name.familyName").value(user.getFamilyName())) - .andExpect(jsonPath("$.name.givenName").value(user.getGivenName())) - .andReturn(); + .andExpect(status().isCreated()) + .andExpect(header().string("ETag", "\"0\"")) + .andExpect(jsonPath("$.userName").value(user.getUserName())) + .andExpect(jsonPath("$.emails[0].value").value(user.getUserName())) + .andExpect(jsonPath("$.name.familyName").value(user.getFamilyName())) + .andExpect(jsonPath("$.name.givenName").value(user.getGivenName())) + .andReturn(); return JsonUtils.readValue(result.getResponse().getContentAsString(), ScimUser.class); } private ScimUser getScimUser() { - String email = "joe@"+generator.generate().toLowerCase()+".com"; + String email = "joe@" + generator.generate().toLowerCase() + ".com"; ScimUser user = new ScimUser(); user.setUserName(email); user.setName(new ScimUser.Name("Joe", "User")); @@ -134,7 +147,35 @@ private ScimUser getScimUser() { } @Test - public void testGetZoneAsIdentityClient() throws Exception { + public void readWithoutTokenShouldFail() throws Exception { + for (String url : BASE_URLS) { + getMockMvc().perform(get(url)) + .andExpect(status().isUnauthorized()); + } + } + + @Test + public void readWith_Write_TokenShouldFail() throws Exception { + for (String url : BASE_URLS) { + getMockMvc().perform( + get(url) + .header("Authorization", "Bearer " + identityClientZonesWriteToken)) + .andExpect(status().isForbidden()); + } + } + + @Test + public void readWith_Read_TokenShouldSucceed() throws Exception { + for (String url : BASE_URLS) { + getMockMvc().perform( + get(url) + .header("Authorization", "Bearer " + identityClientZonesReadToken)) + .andExpect(status().isOk()); + } + } + + @Test + public void testGetZoneAsIdentityClient() throws Exception { String id = generator.generate(); IdentityZone created = createZone(id, HttpStatus.CREATED, identityClientToken); IdentityZone retrieved = getIdentityZone(id, HttpStatus.OK, identityClientToken); @@ -145,16 +186,24 @@ public void testGetZoneAsIdentityClient() throws Exception { } @Test - public void testGetZonesAsIdentityClient() throws Exception { + public void testGetZonesAsIdentityClient() throws Exception { String id = generator.generate(); IdentityZone created = createZone(id, HttpStatus.CREATED, identityClientToken); - MvcResult result = getMockMvc().perform(get("/identity-zones/") - .header("Authorization", "Bearer " + identityClientToken)) - .andExpect(status().isOk()) - .andReturn(); + getMockMvc().perform( + get("/identity-zones/") + .header("Authorization", "Bearer " + identityClientZonesWriteToken)) + .andExpect(status().isForbidden()); + + MvcResult result = getMockMvc().perform( + get("/identity-zones/") + .header("Authorization", "Bearer " + identityClientToken)) + .andExpect(status().isOk()) + .andReturn(); - List zones = JsonUtils.readValue(result.getResponse().getContentAsString(), new TypeReference>() {}); + + List zones = JsonUtils.readValue(result.getResponse().getContentAsString(), new TypeReference>() { + }); IdentityZone retrieved = null; for (IdentityZone identityZone : zones) { if (identityZone.getId().equals(id)) { @@ -169,7 +218,7 @@ public void testGetZonesAsIdentityClient() throws Exception { } @Test - public void testGetZoneThatDoesntExist() throws Exception { + public void testGetZoneThatDoesntExist() throws Exception { String id = generator.generate(); getIdentityZone(id, HttpStatus.NOT_FOUND, identityClientToken); } @@ -307,12 +356,21 @@ public void testCreateZoneAndIdentityProvider() throws Exception { String id = UUID.randomUUID().toString(); IdentityZone identityZone = getIdentityZone(id); - getMockMvc().perform(post("/identity-zones") - .header("Authorization", "Bearer " + identityClientToken) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(identityZone))) - .andExpect(status().isCreated()) - .andReturn(); + for (String url : BASE_URLS) { + getMockMvc().perform( + post(url) + .header("Authorization", "Bearer " + identityClientZonesReadToken) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(identityZone))) + .andExpect(status().isForbidden()); + } + + getMockMvc().perform( + post("/identity-zones") + .header("Authorization", "Bearer " + identityClientToken) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(identityZone))) + .andExpect(status().isCreated()); checkZoneAuditEventInUaa(1, AuditEventType.IdentityZoneCreatedEvent); @@ -327,16 +385,27 @@ public void testCreateAndDeleteLimitedClientInNewZoneUsingZoneEndpoint() throws String id = generator.generate(); IdentityZone zone = createZone(id, HttpStatus.CREATED, identityClientToken); BaseClientDetails client = new BaseClientDetails("limited-client", null, "openid", "authorization_code", - "uaa.resource"); + "uaa.resource"); client.setClientSecret("secret"); client.addAdditionalInformation(ClientConstants.ALLOWED_PROVIDERS, Collections.singletonList(Origin.UAA)); client.addAdditionalInformation("foo", "bar"); - MvcResult result = getMockMvc().perform(post("/identity-zones/" + zone.getId() + "/clients") - .header("Authorization", "Bearer " + identityClientToken) - .contentType(APPLICATION_JSON) - .accept(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(client))) - .andExpect(status().isCreated()).andReturn(); + for (String url : Arrays.asList("","/")) { + getMockMvc().perform( + post("/identity-zones/" + zone.getId() + "/clients"+url) + .header("Authorization", "Bearer " + identityClientZonesReadToken) + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(client))) + .andExpect(status().isForbidden()); + } + + MvcResult result = getMockMvc().perform( + post("/identity-zones/" + zone.getId() + "/clients") + .header("Authorization", "Bearer " + identityClientToken) + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(client))) + .andExpect(status().isCreated()).andReturn(); BaseClientDetails created = JsonUtils.readValue(result.getResponse().getContentAsString(), BaseClientDetails.class); assertNull(created.getClientSecret()); assertEquals("zones.write", created.getAdditionalInformation().get(ClientConstants.CREATED_WITH)); @@ -344,10 +413,18 @@ public void testCreateAndDeleteLimitedClientInNewZoneUsingZoneEndpoint() throws assertEquals("bar", created.getAdditionalInformation().get("foo")); checkAuditEventListener(1, AuditEventType.ClientCreateSuccess, clientCreateEventListener, id, "http://localhost:8080/uaa/oauth/token", "identity"); - getMockMvc().perform(delete("/identity-zones/" + zone.getId() + "/clients/" + created.getClientId(), IdentityZone.getUaa().getId()) - .header("Authorization", "Bearer " + identityClientToken) - .accept(APPLICATION_JSON)) - .andExpect(status().isOk()); + for (String url : Arrays.asList("","/")) { + getMockMvc().perform( + delete("/identity-zones/" + zone.getId() + "/clients/" + created.getClientId(), IdentityZone.getUaa().getId()+url) + .header("Authorization", "Bearer " + identityClientZonesReadToken) + .accept(APPLICATION_JSON)) + .andExpect(status().isForbidden()); + } + getMockMvc().perform( + delete("/identity-zones/" + zone.getId() + "/clients/" + created.getClientId(), IdentityZone.getUaa().getId()) + .header("Authorization", "Bearer " + identityClientToken) + .accept(APPLICATION_JSON)) + .andExpect(status().isOk()); checkAuditEventListener(1, AuditEventType.ClientDeleteSuccess, clientDeleteEventListener, id, "http://localhost:8080/uaa/oauth/token", "identity"); } @@ -355,21 +432,23 @@ public void testCreateAndDeleteLimitedClientInNewZoneUsingZoneEndpoint() throws @Test public void testCreateAndDeleteLimitedClientInUAAZoneReturns403() throws Exception { BaseClientDetails client = new BaseClientDetails("limited-client", null, "openid", "authorization_code", - "uaa.resource"); + "uaa.resource"); client.setClientSecret("secret"); client.addAdditionalInformation(ClientConstants.ALLOWED_PROVIDERS, Collections.singletonList(Origin.UAA)); - getMockMvc().perform(post("/identity-zones/uaa/clients") - .header("Authorization", "Bearer " + identityClientToken) - .contentType(APPLICATION_JSON) - .accept(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(client))) - .andExpect(status().isForbidden()); + getMockMvc().perform( + post("/identity-zones/uaa/clients") + .header("Authorization", "Bearer " + identityClientToken) + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(client))) + .andExpect(status().isForbidden()); assertEquals(0, clientCreateEventListener.getEventCount()); - getMockMvc().perform(delete("/identity-zones/uaa/clients/admin") - .header("Authorization", "Bearer " + identityClientToken) - .accept(APPLICATION_JSON)) - .andExpect(status().isForbidden()); + getMockMvc().perform( + delete("/identity-zones/uaa/clients/admin") + .header("Authorization", "Bearer " + identityClientToken) + .accept(APPLICATION_JSON)) + .andExpect(status().isForbidden()); assertEquals(0, clientDeleteEventListener.getEventCount()); } @@ -379,30 +458,31 @@ public void testCreateAdminClientInNewZoneUsingZoneEndpointReturns400() throws E String id = generator.generate(); IdentityZone zone = createZone(id, HttpStatus.CREATED, identityClientToken); BaseClientDetails client = new BaseClientDetails("admin-client", null, null, "client_credentials", - "clients.write"); + "clients.write"); client.setClientSecret("secret"); - getMockMvc().perform(post("/identity-zones/" + zone.getId() + "/clients") - .header("Authorization", "Bearer " + identityClientToken) - .contentType(APPLICATION_JSON) - .accept(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(client))) - .andExpect(status().isBadRequest()); + getMockMvc().perform( + post("/identity-zones/" + zone.getId() + "/clients") + .header("Authorization", "Bearer " + identityClientToken) + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(client))) + .andExpect(status().isBadRequest()); } @Test public void testCreateInvalidZone() throws Exception { IdentityZone identityZone = new IdentityZone(); - getMockMvc().perform(post("/identity-zones") - .header("Authorization", "Bearer " + identityClientToken) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(identityZone))) + getMockMvc().perform( + post("/identity-zones") + .header("Authorization", "Bearer " + identityClientToken) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(identityZone))) .andExpect(status().isBadRequest()); assertEquals(0, zoneModifiedEventListener.getEventCount()); } - @Test public void testCreatesZonesWithDuplicateSubdomains() throws Exception { String subdomain = UUID.randomUUID().toString(); @@ -410,20 +490,22 @@ public void testCreatesZonesWithDuplicateSubdomains() throws Exception { String id2 = UUID.randomUUID().toString(); IdentityZone identityZone1 = MultitenancyFixture.identityZone(id1, subdomain); IdentityZone identityZone2 = MultitenancyFixture.identityZone(id2, subdomain); - getMockMvc().perform(post("/identity-zones") - .header("Authorization", "Bearer " + identityClientToken) - .contentType(APPLICATION_JSON) - .accept(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(identityZone1))) + getMockMvc().perform( + post("/identity-zones") + .header("Authorization", "Bearer " + identityClientToken) + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(identityZone1))) .andExpect(status().isCreated()); checkZoneAuditEventInUaa(1, AuditEventType.IdentityZoneCreatedEvent); - getMockMvc().perform(post("/identity-zones") - .header("Authorization", "Bearer " + identityClientToken) - .contentType(APPLICATION_JSON) - .accept(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(identityZone2))) + getMockMvc().perform( + post("/identity-zones") + .header("Authorization", "Bearer " + identityClientToken) + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(identityZone2))) .andExpect(status().isConflict()); assertEquals(1, zoneModifiedEventListener.getEventCount()); @@ -437,43 +519,48 @@ public void testZoneAdminTokenAgainstZoneEndpoints() throws Exception { IdentityZoneCreationResult result1 = MockMvcUtils.utils().createOtherIdentityZoneAndReturnResult(zone1, getMockMvc(), getWebApplicationContext(), null); IdentityZoneCreationResult result2 = MockMvcUtils.utils().createOtherIdentityZoneAndReturnResult(zone2, getMockMvc(), getWebApplicationContext(), null); - MvcResult result = getMockMvc().perform(get("/identity-zones") - .header("Authorization", "Bearer " + result1.getZoneAdminToken()) - .header(IdentityZoneSwitchingFilter.HEADER, result1.getIdentityZone().getSubdomain()) - .accept(APPLICATION_JSON)) + MvcResult result = getMockMvc().perform( + get("/identity-zones") + .header("Authorization", "Bearer " + result1.getZoneAdminToken()) + .header(IdentityZoneSwitchingFilter.HEADER, result1.getIdentityZone().getSubdomain()) + .accept(APPLICATION_JSON)) .andExpect(status().isOk()) .andReturn(); //test read your own zone only - List zones = JsonUtils.readValue(result.getResponse().getContentAsString(), new TypeReference>() {}); + List zones = JsonUtils.readValue(result.getResponse().getContentAsString(), new TypeReference>() { + }); assertEquals(1, zones.size()); assertEquals(zone1, zones.get(0).getSubdomain()); //test write your own - getMockMvc().perform(put("/identity-zones/" + result1.getIdentityZone().getId()) - .header("Authorization", "Bearer " + result1.getZoneAdminToken()) - .header(IdentityZoneSwitchingFilter.HEADER, result1.getIdentityZone().getSubdomain()) - .contentType(APPLICATION_JSON) - .accept(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(result1.getIdentityZone()))) + getMockMvc().perform( + put("/identity-zones/" + result1.getIdentityZone().getId()) + .header("Authorization", "Bearer " + result1.getZoneAdminToken()) + .header(IdentityZoneSwitchingFilter.HEADER, result1.getIdentityZone().getSubdomain()) + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(result1.getIdentityZone()))) .andExpect(status().isOk()); //test write someone elses - getMockMvc().perform(put("/identity-zones/" + result2.getIdentityZone().getId()) - .header("Authorization", "Bearer " + result1.getZoneAdminToken()) - .header(IdentityZoneSwitchingFilter.HEADER, result1.getIdentityZone().getSubdomain()) - .contentType(APPLICATION_JSON) - .accept(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(result2.getIdentityZone()))) + getMockMvc().perform( + put("/identity-zones/" + result2.getIdentityZone().getId()) + .header("Authorization", "Bearer " + result1.getZoneAdminToken()) + .header(IdentityZoneSwitchingFilter.HEADER, result1.getIdentityZone().getSubdomain()) + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(result2.getIdentityZone()))) .andExpect(status().isForbidden()); //test create as zone admin - getMockMvc().perform(post("/identity-zones") - .header("Authorization", "Bearer " + result1.getZoneAdminToken()) - .header(IdentityZoneSwitchingFilter.HEADER, result1.getIdentityZone().getSubdomain()) - .contentType(APPLICATION_JSON) - .accept(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(result2.getIdentityZone()))) + getMockMvc().perform( + post("/identity-zones") + .header("Authorization", "Bearer " + result1.getZoneAdminToken()) + .header(IdentityZoneSwitchingFilter.HEADER, result1.getIdentityZone().getSubdomain()) + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(result2.getIdentityZone()))) .andExpect(status().isForbidden()); } @@ -481,7 +568,7 @@ public void testZoneAdminTokenAgainstZoneEndpoints() throws Exception { @Test public void testSuccessfulUserManagementInZoneUsingAdminClient() throws Exception { String subdomain = generator.generate(); - BaseClientDetails adminClient = new BaseClientDetails("admin", null, null, "client_credentials","scim.read,scim.write"); + BaseClientDetails adminClient = new BaseClientDetails("admin", null, null, "client_credentials", "scim.read,scim.write"); adminClient.setClientSecret("admin-secret"); IdentityZoneCreationResult creationResult = mockMvcUtils.createOtherIdentityZoneAndReturnResult(subdomain, getMockMvc(), getWebApplicationContext(), adminClient); IdentityZone identityZone = creationResult.getIdentityZone(); @@ -492,7 +579,7 @@ public void testSuccessfulUserManagementInZoneUsingAdminClient() throws Exceptio String scimAdminToken = testClient.getClientCredentialsOAuthAccessToken("admin", "admin-secret", "scim.write,scim.read", subdomain); ScimUser user = createUser(scimAdminToken, subdomain); - checkAuditEventListener(1, AuditEventType.UserCreatedEvent, userModifiedEventListener, identityZone.getId(), "http://"+subdomain+".localhost:8080/uaa/oauth/token", "admin"); + checkAuditEventListener(1, AuditEventType.UserCreatedEvent, userModifiedEventListener, identityZone.getId(), "http://" + subdomain + ".localhost:8080/uaa/oauth/token", "admin"); user.setUserName("updated-username@test.com"); MockHttpServletRequestBuilder put = put("/Users/" + user.getId()) @@ -507,7 +594,7 @@ public void testSuccessfulUserManagementInZoneUsingAdminClient() throws Exceptio .andExpect(jsonPath("$.userName").value(user.getUserName())) .andReturn(); - checkAuditEventListener(2, AuditEventType.UserModifiedEvent, userModifiedEventListener, identityZone.getId(), "http://"+subdomain+".localhost:8080/uaa/oauth/token", "admin"); + checkAuditEventListener(2, AuditEventType.UserModifiedEvent, userModifiedEventListener, identityZone.getId(), "http://" + subdomain + ".localhost:8080/uaa/oauth/token", "admin"); user = JsonUtils.readValue(result.getResponse().getContentAsString(), ScimUser.class); List users = getUsersInZone(subdomain, scimAdminToken); assertTrue(users.contains(user)); @@ -524,7 +611,7 @@ public void testSuccessfulUserManagementInZoneUsingAdminClient() throws Exceptio .andExpect(jsonPath("$.id").value(user.getId())) .andReturn(); - checkAuditEventListener(3, AuditEventType.UserDeletedEvent, userModifiedEventListener, identityZone.getId(), "http://"+subdomain+".localhost:8080/uaa/oauth/token", "admin"); + checkAuditEventListener(3, AuditEventType.UserDeletedEvent, userModifiedEventListener, identityZone.getId(), "http://" + subdomain + ".localhost:8080/uaa/oauth/token", "admin"); users = getUsersInZone(subdomain, scimAdminToken); assertEquals(0, users.size()); } @@ -550,7 +637,8 @@ public void testCreateAndListUsersInOtherZoneIsUnauthorized() throws Exception { getMockMvc().perform(post).andExpect(status().isUnauthorized()); MockHttpServletRequestBuilder get = get("/Users").header("Authorization", "Bearer " + defaultZoneAdminToken); - if (subdomain != null && !subdomain.equals("")) get.with(new SetServerNameRequestPostProcessor(subdomain + ".localhost")); + if (subdomain != null && !subdomain.equals("")) + get.with(new SetServerNameRequestPostProcessor(subdomain + ".localhost")); getMockMvc().perform(get).andExpect(status().isUnauthorized()).andReturn(); } @@ -600,29 +688,33 @@ public void userCanReadAZone_withZoneZoneIdReadToken() throws Exception { String zoneReadScope = "zones." + identityZone.getId() + ".read"; group.setDisplayName(zoneReadScope); group.setMembers(Collections.singletonList(new ScimGroupMember(user.getId()))); - getMockMvc().perform(post("/Groups/zones") + getMockMvc().perform( + post("/Groups/zones") .header("Authorization", "Bearer " + identityClientToken) .contentType(APPLICATION_JSON) .accept(APPLICATION_JSON) .content(JsonUtils.writeValueAsString(group))) - .andExpect(status().isCreated()); + .andExpect(status().isCreated()); - String userAccessToken = mockMvcUtils.getUserOAuthAccessTokenAuthCode(getMockMvc(), "identity", "identitysecret", user.getId(), user.getUserName(), user.getPassword(), "zones."+identityZone.getId()+".read"); + String userAccessToken = mockMvcUtils.getUserOAuthAccessTokenAuthCode(getMockMvc(), "identity", "identitysecret", user.getId(), user.getUserName(), user.getPassword(), "zones." + identityZone.getId() + ".read"); - MvcResult result = getMockMvc().perform(get("/identity-zones/" + identityZone.getId()) + MvcResult result = getMockMvc().perform( + get("/identity-zones/" + identityZone.getId()) .header("Authorization", "Bearer " + userAccessToken) .header(IdentityZoneSwitchingFilter.HEADER, identityZone.getSubdomain()) .accept(APPLICATION_JSON)) - .andExpect(status().isOk()) - .andReturn(); + .andExpect(status().isOk()) + .andReturn(); - IdentityZone zoneResult = JsonUtils.readValue(result.getResponse().getContentAsString(), new TypeReference() {}); + IdentityZone zoneResult = JsonUtils.readValue(result.getResponse().getContentAsString(), new TypeReference() { + }); assertEquals(identityZone, zoneResult); } private IdentityZone getIdentityZone(String id, HttpStatus expect, String token) throws Exception { - MvcResult result = getMockMvc().perform(get("/identity-zones/" + id) - .header("Authorization", "Bearer " + token)) + MvcResult result = getMockMvc().perform( + get("/identity-zones/" + id) + .header("Authorization", "Bearer " + token)) .andExpect(status().is(expect.value())) .andReturn(); @@ -634,10 +726,11 @@ private IdentityZone getIdentityZone(String id, HttpStatus expect, String token) private IdentityZone createZone(String id, HttpStatus expect, String token) throws Exception { IdentityZone identityZone = getIdentityZone(id); - MvcResult result = getMockMvc().perform(post("/identity-zones") - .header("Authorization", "Bearer " + token) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(identityZone))) + MvcResult result = getMockMvc().perform( + post("/identity-zones") + .header("Authorization", "Bearer " + token) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(identityZone))) .andExpect(status().is(expect.value())) .andReturn(); @@ -648,10 +741,11 @@ private IdentityZone createZone(String id, HttpStatus expect, String token) thro } private IdentityZone updateZone(IdentityZone identityZone, HttpStatus expect, String token) throws Exception { - MvcResult result = getMockMvc().perform(put("/identity-zones/" + identityZone.getId()) - .header("Authorization", "Bearer " + token) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(identityZone))) + MvcResult result = getMockMvc().perform( + put("/identity-zones/" + identityZone.getId()) + .header("Authorization", "Bearer " + token) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(identityZone))) .andExpect(status().is(expect.value())) .andReturn(); @@ -672,15 +766,15 @@ private void checkAuditEventListener(int eventCount assertEquals(eventType, event.getAuditEvent().getType()); assertEquals(identityZoneId, event.getAuditEvent().getIdentityZoneId()); String origin = event.getAuditEvent().getOrigin(); - assertTrue(origin.contains("iss="+issuer)); - assertTrue(origin.contains("sub="+subject)); + assertTrue(origin.contains("iss=" + issuer)); + assertTrue(origin.contains("sub=" + subject)); } } private IdentityZone getIdentityZone(String id) { IdentityZone identityZone = new IdentityZone(); identityZone.setId(id); - identityZone.setSubdomain(StringUtils.hasText(id)?id:new RandomValueStringGenerator().generate()); + identityZone.setSubdomain(StringUtils.hasText(id) ? id : new RandomValueStringGenerator().generate()); identityZone.setName("The Twiglet Zone"); identityZone.setDescription("Like the Twilight Zone but tastier."); return identityZone; @@ -688,7 +782,8 @@ private IdentityZone getIdentityZone(String id) { private List getUsersInZone(String subdomain, String token) throws Exception { MockHttpServletRequestBuilder get = get("/Users").header("Authorization", "Bearer " + token); - if (subdomain != null && !subdomain.equals("")) get.with(new SetServerNameRequestPostProcessor(subdomain + ".localhost")); + if (subdomain != null && !subdomain.equals("")) + get.with(new SetServerNameRequestPostProcessor(subdomain + ".localhost")); MvcResult mvcResult = getMockMvc().perform(get).andExpect(status().isOk()).andReturn(); From 9117ccbcc6a0a1ecfbdaeb7331de48c1576ddfd5 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Thu, 1 Oct 2015 12:06:35 -0700 Subject: [PATCH 034/103] IdentityZoneResolving filter should not throw if retrieving zone from db fails [#104650250] https://www.pivotaltracker.com/story/show/104650250 Signed-off-by: Madhura Bhave --- .../uaa/zone/IdentityZoneResolvingFilter.java | 8 ++++- .../zone/IdentityZoneResolvingFilterTest.java | 31 +++++++++++++++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneResolvingFilter.java b/common/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneResolvingFilter.java index a50168b0c53..dac3126d528 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneResolvingFilter.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneResolvingFilter.java @@ -12,6 +12,8 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.zone; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.web.filter.OncePerRequestFilter; @@ -34,6 +36,7 @@ public class IdentityZoneResolvingFilter extends OncePerRequestFilter { private IdentityZoneProvisioning dao; private Set defaultZoneHostnames = new HashSet<>(); + private Log logger = LogFactory.getLog(getClass()); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) @@ -45,8 +48,11 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse try { identityZone = dao.retrieveBySubdomain(subdomain); } catch (EmptyResultDataAccessException ex) { + logger.debug("Cannot find identity zone for subdomain " + subdomain, ex); } catch (Exception ex) { - throw ex; + logger.debug("Internal server error while fetching identity zone for subdomain" + subdomain, ex); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal server error while fetching identity zone for subdomain " + subdomain); + return; } } if (identityZone == null) { diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneResolvingFilterTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneResolvingFilterTest.java index 3f878262dca..e18381d17b1 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneResolvingFilterTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneResolvingFilterTest.java @@ -1,8 +1,11 @@ package org.cloudfoundry.identity.uaa.zone; import static org.junit.Assert.*; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.when; import java.io.IOException; +import java.sql.SQLNonTransientConnectionException; import java.util.Arrays; import java.util.HashSet; @@ -15,6 +18,7 @@ import org.junit.Test; import org.mockito.Mockito; import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.CannotGetJdbcConnectionException; import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -45,6 +49,29 @@ public void holderIsSetWithUAAIdentityZone() throws Exception { assertFindsCorrectSubdomain("", "login.mycf.com", "uaa.mycf.com","login.mycf.com"); } + @Test + public void doNotThrowException_InCase_RetrievingZoneFails() throws Exception { + IdentityZoneProvisioning dao = Mockito.mock(IdentityZoneProvisioning.class); + when(dao.retrieveBySubdomain(anyString())).thenThrow(new CannotGetJdbcConnectionException("blah", new SQLNonTransientConnectionException())); + + MockHttpServletRequest request = new MockHttpServletRequest(); + String incomingSubdomain = "not_a_zone"; + String uaaHostname = "uaa.mycf.com"; + String incomingHostname = incomingSubdomain+"."+uaaHostname; + request.setServerName(incomingHostname); + MockHttpServletResponse response = new MockHttpServletResponse(); + + FilterChain chain = Mockito.mock(FilterChain.class); + IdentityZoneResolvingFilter filter = new IdentityZoneResolvingFilter(); + filter.setIdentityZoneProvisioning(dao); + filter.setAdditionalInternalHostnames(new HashSet<>(Arrays.asList(uaaHostname))); + filter.doFilter(request, response, chain); + + assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, response.getStatus()); + assertEquals(IdentityZone.getUaa(), IdentityZoneHolder.get()); + Mockito.verifyZeroInteractions(chain); + } + private void assertFindsCorrectSubdomain(final String expectedSubdomain, final String incomingHostname, String... additionalInternalHostnames) throws ServletException, IOException { IdentityZoneResolvingFilter filter = new IdentityZoneResolvingFilter(); @@ -54,7 +81,7 @@ private void assertFindsCorrectSubdomain(final String expectedSubdomain, final S IdentityZone identityZone = new IdentityZone(); identityZone.setSubdomain(expectedSubdomain); - Mockito.when(dao.retrieveBySubdomain(Mockito.eq(expectedSubdomain))).thenReturn(identityZone); + when(dao.retrieveBySubdomain(Mockito.eq(expectedSubdomain))).thenReturn(identityZone); MockHttpServletRequest request = new MockHttpServletRequest(); request.setServerName(incomingHostname); @@ -88,7 +115,7 @@ public void holderIsNotSetWithNonMatchingIdentityZone() throws Exception { IdentityZone identityZone = new IdentityZone(); identityZone.setSubdomain(incomingSubdomain); - Mockito.when(dao.retrieveBySubdomain(Mockito.eq(incomingSubdomain))).thenThrow(new EmptyResultDataAccessException(1)); + when(dao.retrieveBySubdomain(Mockito.eq(incomingSubdomain))).thenThrow(new EmptyResultDataAccessException(1)); MockHttpServletRequest request = new MockHttpServletRequest(); request.setServerName(incomingHostname); From 4fc803a60512db6c0d580191112ec12768d38859 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Fri, 2 Oct 2015 14:28:11 -0700 Subject: [PATCH 035/103] Add redirect uri functionality with wildcard support for reset password flow [#104590406] https://www.pivotaltracker.com/story/show/104590406 Signed-off-by: Madhura Bhave --- .../uaa/login/ResetPasswordController.java | 21 +++-- login/src/main/resources/login-ui.xml | 1 + .../templates/web/forgot_password.html | 4 +- .../login/ResetPasswordControllerTest.java | 25 +++-- .../uaa/login/ResetPasswordService.java | 36 +++++++- .../uaa/login/UaaResetPasswordService.java | 44 +++++++-- .../uaa/scim/endpoints/PasswordChange.java | 26 +++++- .../scim/endpoints/PasswordResetEndpoint.java | 6 +- .../login/UaaResetPasswordServiceTests.java | 92 +++++++++++++++---- .../endpoints/PasswordResetEndpointTest.java | 16 ++-- .../integration/feature/ResetPasswordIT.java | 34 ++++++- .../uaa/integration/feature/TestClient.java | 1 + .../ResetPasswordControllerMockMvcTests.java | 9 +- ...erManagementSecurityFilterMockMvcTest.java | 2 +- 14 files changed, 255 insertions(+), 62 deletions(-) diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/login/ResetPasswordController.java b/login/src/main/java/org/cloudfoundry/identity/uaa/login/ResetPasswordController.java index db9df9ddb05..417ae4b848b 100644 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/login/ResetPasswordController.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/login/ResetPasswordController.java @@ -19,6 +19,7 @@ import org.cloudfoundry.identity.uaa.codestore.ExpiringCode; import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; import org.cloudfoundry.identity.uaa.error.UaaException; +import org.cloudfoundry.identity.uaa.login.ResetPasswordService.ResetPasswordResponse; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.exception.InvalidPasswordException; import org.cloudfoundry.identity.uaa.user.UaaAuthority; @@ -70,27 +71,32 @@ public ResetPasswordController(ResetPasswordService resetPasswordService, } @RequestMapping(value = "/forgot_password", method = RequestMethod.GET) - public String forgotPasswordPage() { + public String forgotPasswordPage(Model model, + @RequestParam(required = false, value = "client_id") String clientId, + @RequestParam(required = false, value = "redirect_uri") String redirectUri) { + model.addAttribute("client_id", clientId); + model.addAttribute("redirect_uri", redirectUri); return "forgot_password"; } @RequestMapping(value = "/forgot_password.do", method = RequestMethod.POST) - public String forgotPassword(Model model, @RequestParam("email") String email, HttpServletResponse response) { + public String forgotPassword(Model model, @RequestParam("email") String email, @RequestParam(value = "client_id", defaultValue = "") String clientId, + @RequestParam(value = "redirect_uri", defaultValue = "") String redirectUri, HttpServletResponse response) { if (emailPattern.matcher(email).matches()) { - forgotPassword(email); + forgotPassword(email, clientId, redirectUri); return "redirect:email_sent?code=reset_password"; } else { return handleUnprocessableEntity(model, response, "message_code", "form_error"); } } - private void forgotPassword(String email) { + private void forgotPassword(String email, String clientId, String redirectUri) { String subject = getSubjectText(); String htmlContent = null; String userId = null; try { - ForgotPasswordInfo forgotPasswordInfo = resetPasswordService.forgotPassword(email); + ForgotPasswordInfo forgotPasswordInfo = resetPasswordService.forgotPassword(email, clientId, redirectUri); userId = forgotPasswordInfo.getUserId(); htmlContent = getCodeSentEmailHtml(forgotPasswordInfo.getResetPasswordCode().getCode(), email); } catch (ConflictException e) { @@ -182,11 +188,12 @@ public String resetPassword(Model model, } try { - ScimUser user = resetPasswordService.resetPassword(code, password); + ResetPasswordResponse resetPasswordResponse = resetPasswordService.resetPassword(code, password); + ScimUser user = resetPasswordResponse.getUser(); UaaPrincipal uaaPrincipal = new UaaPrincipal(user.getId(), user.getUserName(), user.getPrimaryEmail(), Origin.UAA, null, IdentityZoneHolder.get().getId()); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(uaaPrincipal, null, UaaAuthority.USER_AUTHORITIES); SecurityContextHolder.getContext().setAuthentication(token); - return "redirect:home"; + return "redirect:" + resetPasswordResponse.getRedirectUri(); } catch (UaaException e) { return handleUnprocessableEntity(model, response, "message_code", "bad_code"); } catch (InvalidPasswordException e) { diff --git a/login/src/main/resources/login-ui.xml b/login/src/main/resources/login-ui.xml index 8f9d26a0eef..f72f52f1bf5 100644 --- a/login/src/main/resources/login-ui.xml +++ b/login/src/main/resources/login-ui.xml @@ -551,6 +551,7 @@ + diff --git a/login/src/main/resources/templates/web/forgot_password.html b/login/src/main/resources/templates/web/forgot_password.html index ffd651088c1..3d25770aede 100644 --- a/login/src/main/resources/templates/web/forgot_password.html +++ b/login/src/main/resources/templates/web/forgot_password.html @@ -6,10 +6,12 @@

Reset Password

+ +
Back to Sign In
- \ No newline at end of file + diff --git a/login/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerTest.java b/login/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerTest.java index fcfe1ef58aa..f5d13ed66a0 100644 --- a/login/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerTest.java +++ b/login/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerTest.java @@ -16,6 +16,7 @@ import org.cloudfoundry.identity.uaa.codestore.ExpiringCode; import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; import org.cloudfoundry.identity.uaa.error.UaaException; +import org.cloudfoundry.identity.uaa.login.ResetPasswordService.ResetPasswordResponse; import org.cloudfoundry.identity.uaa.login.test.ThymeleafConfig; import org.cloudfoundry.identity.uaa.scim.ScimMeta; import org.cloudfoundry.identity.uaa.scim.ScimUser; @@ -102,9 +103,13 @@ public void tearDown() { @Test public void testForgotPasswordPage() throws Exception { - mockMvc.perform(get("/forgot_password")) - .andExpect(status().isOk()) - .andExpect(view().name("forgot_password")); + mockMvc.perform(get("/forgot_password") + .param("client_id", "example") + .param("redirect_uri", "http://example.com")) + .andExpect(status().isOk()) + .andExpect(view().name("forgot_password")) + .andExpect(model().attribute("client_id", "example")) + .andExpect(model().attribute("redirect_uri", "http://example.com")); } @Test @@ -121,7 +126,7 @@ public void forgotPassword_ConflictInOtherZone_SendsEmailWithUnavailableEmailHtm private void forgotPasswordWithConflict(String zoneDomain, String brand) throws Exception { String subdomain = zoneDomain == null ? "" : zoneDomain + "."; - when(resetPasswordService.forgotPassword("user@example.com")).thenThrow(new ConflictException("abcd")); + when(resetPasswordService.forgotPassword("user@example.com", "", "")).thenThrow(new ConflictException("abcd")); MockHttpServletRequestBuilder post = post("/forgot_password.do") .contentType(APPLICATION_FORM_URLENCODED) .param("email", "user@example.com"); @@ -145,7 +150,7 @@ private void forgotPasswordWithConflict(String zoneDomain, String brand) throws @Test public void forgotPassword_DoesNotSendEmail_UserNotFound() throws Exception { - when(resetPasswordService.forgotPassword("user@example.com")).thenThrow(new NotFoundException()); + when(resetPasswordService.forgotPassword("user@example.com", "", "")).thenThrow(new NotFoundException()); MockHttpServletRequestBuilder post = post("/forgot_password.do") .contentType(APPLICATION_FORM_URLENCODED) .param("email", "user@example.com"); @@ -168,10 +173,12 @@ public void forgotPassword_SuccessfulInOtherZone() throws Exception { } private void forgotPasswordSuccessful(String url, String brand) throws Exception { - when(resetPasswordService.forgotPassword("user@example.com")).thenReturn(new ForgotPasswordInfo("123", new ExpiringCode("code1", new Timestamp(System.currentTimeMillis()), "someData"))); + when(resetPasswordService.forgotPassword("user@example.com", "example", "redirect.example.com")).thenReturn(new ForgotPasswordInfo("123", new ExpiringCode("code1", new Timestamp(System.currentTimeMillis()), "someData"))); MockHttpServletRequestBuilder post = post("/forgot_password.do") .contentType(APPLICATION_FORM_URLENCODED) - .param("email", "user@example.com"); + .param("email", "user@example.com") + .param("client_id", "example") + .param("redirect_uri", "redirect.example.com"); mockMvc.perform(post) .andExpect(status().isFound()) .andExpect(redirectedUrl("email_sent?code=reset_password")); @@ -218,7 +225,7 @@ public void testResetPasswordSuccess() throws Exception { ScimUser user = new ScimUser("user-id","foo@example.com","firstName","lastName"); user.setMeta(new ScimMeta(new Date(System.currentTimeMillis() - (1000 * 60 * 60 * 24)), new Date(System.currentTimeMillis() - (1000 * 60 * 60 * 24)), 0)); user.setPrimaryEmail("foo@example.com"); - when(resetPasswordService.resetPassword("secret_code", "password")).thenReturn(user); + when(resetPasswordService.resetPassword("secret_code", "password")).thenReturn(new ResetPasswordResponse(user, "redirect.example.com")); MockHttpServletRequestBuilder post = post("/reset_password.do") .contentType(APPLICATION_FORM_URLENCODED) @@ -228,7 +235,7 @@ public void testResetPasswordSuccess() throws Exception { .param("password_confirmation", "password"); mockMvc.perform(post) .andExpect(status().isFound()) - .andExpect(redirectedUrl("home")) + .andExpect(redirectedUrl("redirect.example.com")) .andExpect(model().attributeDoesNotExist("code")) .andExpect(model().attributeDoesNotExist("password")) .andExpect(model().attributeDoesNotExist("password_confirmation")); diff --git a/scim/src/main/java/org/cloudfoundry/identity/uaa/login/ResetPasswordService.java b/scim/src/main/java/org/cloudfoundry/identity/uaa/login/ResetPasswordService.java index e4da8448865..d7df13abde9 100644 --- a/scim/src/main/java/org/cloudfoundry/identity/uaa/login/ResetPasswordService.java +++ b/scim/src/main/java/org/cloudfoundry/identity/uaa/login/ResetPasswordService.java @@ -12,13 +12,45 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.login; +import com.fasterxml.jackson.annotation.JsonProperty; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.exception.InvalidPasswordException; import java.util.Map; public interface ResetPasswordService { - ForgotPasswordInfo forgotPassword(String email); + ForgotPasswordInfo forgotPassword(String email, String clientId, String redirectUri); - ScimUser resetPassword(String code, String password) throws InvalidPasswordException; + ResetPasswordResponse resetPassword(String code, String password) throws InvalidPasswordException; + + public class ResetPasswordResponse { + @JsonProperty("user") + private ScimUser user; + + @JsonProperty("redirect_uri") + private String redirectUri; + + public ResetPasswordResponse() {} + + public ResetPasswordResponse(ScimUser user, String redirectUri) { + this.user = user; + this.redirectUri = redirectUri; + } + + public String getRedirectUri() { + return redirectUri; + } + + public void setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + } + + public ScimUser getUser() { + return user; + } + + public void setUser(ScimUser user) { + this.user = user; + } + } } diff --git a/scim/src/main/java/org/cloudfoundry/identity/uaa/login/UaaResetPasswordService.java b/scim/src/main/java/org/cloudfoundry/identity/uaa/login/UaaResetPasswordService.java index 09577ec19c8..2351d72bf41 100644 --- a/scim/src/main/java/org/cloudfoundry/identity/uaa/login/UaaResetPasswordService.java +++ b/scim/src/main/java/org/cloudfoundry/identity/uaa/login/UaaResetPasswordService.java @@ -26,17 +26,28 @@ import org.cloudfoundry.identity.uaa.scim.validate.PasswordValidator; import org.cloudfoundry.identity.uaa.user.UaaUser; import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.cloudfoundry.identity.uaa.util.UaaStringUtils; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.security.oauth2.provider.ClientDetailsService; +import org.springframework.security.oauth2.provider.NoSuchClientException; +import org.springframework.util.StringUtils; import org.springframework.web.client.RestClientException; import java.sql.Timestamp; +import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; +import static org.springframework.util.StringUtils.isEmpty; public class UaaResetPasswordService implements ResetPasswordService, ApplicationEventPublisherAware { @@ -45,16 +56,18 @@ public class UaaResetPasswordService implements ResetPasswordService, Applicatio private final ScimUserProvisioning scimUserProvisioning; private final ExpiringCodeStore expiringCodeStore; private final PasswordValidator passwordValidator; + private final ClientDetailsService clientDetailsService; private ApplicationEventPublisher publisher; - public UaaResetPasswordService(ScimUserProvisioning scimUserProvisioning, ExpiringCodeStore expiringCodeStore, PasswordValidator passwordValidator) { + public UaaResetPasswordService(ScimUserProvisioning scimUserProvisioning, ExpiringCodeStore expiringCodeStore, PasswordValidator passwordValidator, ClientDetailsService clientDetailsService) { this.scimUserProvisioning = scimUserProvisioning; this.expiringCodeStore = expiringCodeStore; this.passwordValidator = passwordValidator; + this.clientDetailsService = clientDetailsService; } @Override - public ScimUser resetPassword(String code, String newPassword) throws InvalidPasswordException { + public ResetPasswordResponse resetPassword(String code, String newPassword) throws InvalidPasswordException { try { passwordValidator.validate(newPassword); return changePasswordCodeAuthenticated(code, newPassword); @@ -63,7 +76,7 @@ public ScimUser resetPassword(String code, String newPassword) throws InvalidPas } } - private ScimUser changePasswordCodeAuthenticated(String code, String newPassword) { + private ResetPasswordResponse changePasswordCodeAuthenticated(String code, String newPassword) { ExpiringCode expiringCode = expiringCodeStore.retrieveCode(code); if (expiringCode == null) { throw new UaaException("Invalid password reset request."); @@ -71,11 +84,15 @@ private ScimUser changePasswordCodeAuthenticated(String code, String newPassword String userId; String userName = null; Date passwordLastModified = null; + String clientId = null; + String redirectUri = null; try { PasswordChange change = JsonUtils.readValue(expiringCode.getData(), PasswordChange.class); userId = change.getUserId(); userName = change.getUsername(); passwordLastModified = change.getPasswordModifiedTime(); + clientId = change.getClientId(); + redirectUri = change.getRedirectUri(); } catch (JsonUtils.JsonUtilException x) { userId = expiringCode.getData(); } @@ -92,7 +109,21 @@ private ScimUser changePasswordCodeAuthenticated(String code, String newPassword } scimUserProvisioning.changePassword(userId, null, newPassword); publish(new PasswordChangeEvent("Password changed", getUaaUser(user), SecurityContextHolder.getContext().getAuthentication())); - return user; + + String redirectLocation = "home"; + if (!isEmpty(clientId) && !isEmpty(redirectUri)) { + try { + ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId); + Set redirectUris = clientDetails.getRegisteredRedirectUri() == null ? Collections.emptySet() : + clientDetails.getRegisteredRedirectUri(); + Set wildcards = UaaStringUtils.constructWildcards(redirectUris); + if (UaaStringUtils.matches(wildcards, redirectUri)) { + redirectLocation = redirectUri; + } + } catch (NoSuchClientException e) { + } + } + return new ResetPasswordResponse(user, redirectLocation); } catch (Exception e) { publish(new PasswordChangeFailureEvent(e.getMessage(), getUaaUser(user), SecurityContextHolder.getContext().getAuthentication())); throw e; @@ -100,7 +131,7 @@ private ScimUser changePasswordCodeAuthenticated(String code, String newPassword } @Override - public ForgotPasswordInfo forgotPassword(String email) { + public ForgotPasswordInfo forgotPassword(String email, String clientId, String redirectUri) { String jsonEmail = JsonUtils.writeValueAsString(email); List results = scimUserProvisioning.query("userName eq " + jsonEmail + " and origin eq \"" + Origin.UAA + "\""); if (results.isEmpty()) { @@ -112,7 +143,8 @@ public ForgotPasswordInfo forgotPassword(String email) { } } ScimUser scimUser = results.get(0); - PasswordChange change = new PasswordChange(scimUser.getId(), scimUser.getUserName(), scimUser.getPasswordLastModified()); + + PasswordChange change = new PasswordChange(scimUser.getId(), scimUser.getUserName(), scimUser.getPasswordLastModified(), clientId, redirectUri); ExpiringCode code = expiringCodeStore.generateCode(JsonUtils.writeValueAsString(change), new Timestamp(System.currentTimeMillis() + PASSWORD_RESET_LIFETIME)); publish(new ResetPasswordRequestEvent(email, code.getCode(), SecurityContextHolder.getContext().getAuthentication())); return new ForgotPasswordInfo(scimUser.getId(), code); diff --git a/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordChange.java b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordChange.java index 7cbf4bae2c6..0cfc0afe734 100644 --- a/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordChange.java +++ b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordChange.java @@ -9,10 +9,12 @@ public class PasswordChange { public PasswordChange() {} - public PasswordChange(String userId, String username, Date passwordModifiedTime) { + public PasswordChange(String userId, String username, Date passwordModifiedTime, String clientId, String redirectUri) { this.userId = userId; this.username = username; this.passwordModifiedTime = passwordModifiedTime; + this.clientId = clientId; + this.redirectUri = redirectUri; } @JsonProperty("user_id") @@ -24,6 +26,12 @@ public PasswordChange(String userId, String username, Date passwordModifiedTime) @JsonProperty("passwordModifiedTime") private Date passwordModifiedTime; + @JsonProperty("client_id") + private String clientId; + + @JsonProperty("redirect_uri") + private String redirectUri; + public String getUsername() { return username; } @@ -47,4 +55,20 @@ public Date getPasswordModifiedTime() { public void setPasswordModifiedTime(Date passwordModifiedTime) { this.passwordModifiedTime = passwordModifiedTime; } + + public String getClientId() { + return clientId; + } + + public String getRedirectUri() { + return redirectUri; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public void setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + } } diff --git a/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordResetEndpoint.java b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordResetEndpoint.java index 34ad8d896a8..dcc9c401b18 100644 --- a/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordResetEndpoint.java +++ b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordResetEndpoint.java @@ -18,6 +18,7 @@ import org.cloudfoundry.identity.uaa.login.ForgotPasswordInfo; import org.cloudfoundry.identity.uaa.login.NotFoundException; import org.cloudfoundry.identity.uaa.login.ResetPasswordService; +import org.cloudfoundry.identity.uaa.login.ResetPasswordService.ResetPasswordResponse; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.exception.InvalidPasswordException; import org.cloudfoundry.identity.uaa.scim.exception.ScimException; @@ -64,7 +65,7 @@ public void setMessageConverters(HttpMessageConverter[] messageConverters) { public ResponseEntity> resetPassword(@RequestBody String email) throws IOException { Map response = new HashMap<>(); try { - ForgotPasswordInfo forgotPasswordInfo = resetPasswordService.forgotPassword(email); + ForgotPasswordInfo forgotPasswordInfo = resetPasswordService.forgotPassword(email, "", ""); response.put("code", forgotPasswordInfo.getResetPasswordCode().getCode()); response.put("user_id", forgotPasswordInfo.getUserId()); return new ResponseEntity<>(response, CREATED); @@ -89,7 +90,8 @@ public ResponseEntity> changePassword(@RequestBody PasswordRe private ResponseEntity> resetPassword(String code, String newPassword) { try { - ScimUser user = resetPasswordService.resetPassword(code, newPassword); + ResetPasswordResponse response = resetPasswordService.resetPassword(code, newPassword); + ScimUser user = response.getUser(); Map userInfo = new HashMap<>(); userInfo.put("user_id", user.getId()); userInfo.put("username", user.getUserName()); diff --git a/scim/src/test/java/org/cloudfoundry/identity/uaa/login/UaaResetPasswordServiceTests.java b/scim/src/test/java/org/cloudfoundry/identity/uaa/login/UaaResetPasswordServiceTests.java index a7a4803cfa4..2538df662ad 100644 --- a/scim/src/test/java/org/cloudfoundry/identity/uaa/login/UaaResetPasswordServiceTests.java +++ b/scim/src/test/java/org/cloudfoundry/identity/uaa/login/UaaResetPasswordServiceTests.java @@ -15,6 +15,7 @@ import org.cloudfoundry.identity.uaa.codestore.ExpiringCode; import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; import org.cloudfoundry.identity.uaa.error.UaaException; +import org.cloudfoundry.identity.uaa.login.ResetPasswordService.ResetPasswordResponse; import org.cloudfoundry.identity.uaa.password.event.ResetPasswordRequestEvent; import org.cloudfoundry.identity.uaa.scim.ScimMeta; import org.cloudfoundry.identity.uaa.scim.ScimUser; @@ -32,14 +33,19 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.provider.ClientDetailsService; +import org.springframework.security.oauth2.provider.NoSuchClientException; +import org.springframework.security.oauth2.provider.client.BaseClientDetails; import java.sql.Timestamp; import java.util.Arrays; +import java.util.Collections; import java.util.Date; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.sameInstance; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; @@ -58,6 +64,7 @@ public class UaaResetPasswordServiceTests { private ExpiringCodeStore codeStore; private ScimUserProvisioning scimUserProvisioning; private PasswordValidator passwordValidator; + private ClientDetailsService clientDetailsService; @Before public void setUp() throws Exception { @@ -65,7 +72,8 @@ public void setUp() throws Exception { scimUserProvisioning = mock(ScimUserProvisioning.class); codeStore = mock(ExpiringCodeStore.class); passwordValidator = mock(PasswordValidator.class); - emailResetPasswordService = new UaaResetPasswordService(scimUserProvisioning, codeStore, passwordValidator); + clientDetailsService = mock(ClientDetailsService.class); + emailResetPasswordService = new UaaResetPasswordService(scimUserProvisioning, codeStore, passwordValidator, clientDetailsService); } @After @@ -77,12 +85,15 @@ public void tearDown() { @Test public void forgotPassword_ResetCodeIsReturnedSuccessfully() throws Exception { ScimUser user = new ScimUser("user-id-001","user@example.com","firstName","lastName"); + user.setPasswordLastModified(new Date(1234)); user.setPrimaryEmail("user@example.com"); when(scimUserProvisioning.query(contains("origin"))).thenReturn(Arrays.asList(user)); Timestamp expiresAt = new Timestamp(System.currentTimeMillis()); - when(codeStore.generateCode(anyString(), any(Timestamp.class))).thenReturn(new ExpiringCode("code", expiresAt, "user-id-001")); - ForgotPasswordInfo forgotPasswordInfo = emailResetPasswordService.forgotPassword("user@example.com"); + when(codeStore.generateCode(eq("{\"user_id\":\"user-id-001\",\"username\":\"user@example.com\",\"passwordModifiedTime\":1234,\"client_id\":\"example\",\"redirect_uri\":\"redirect.example.com\"}"), + any(Timestamp.class))).thenReturn(new ExpiringCode("code", expiresAt, "user-id-001")); + + ForgotPasswordInfo forgotPasswordInfo = emailResetPasswordService.forgotPassword("user@example.com", "example", "redirect.example.com"); assertThat(forgotPasswordInfo.getUserId(), equalTo("user-id-001")); ExpiringCode resetPasswordCode = forgotPasswordInfo.getResetPasswordCode(); @@ -103,7 +114,7 @@ public void forgotPassword_PublishesResetPasswordRequestEvent() throws Exception Timestamp expiresAt = new Timestamp(System.currentTimeMillis()); when(codeStore.generateCode(anyString(), any(Timestamp.class))).thenReturn(new ExpiringCode("code", expiresAt, "user-id-001")); - emailResetPasswordService.forgotPassword("user@example.com"); + emailResetPasswordService.forgotPassword("user@example.com", "", ""); ArgumentCaptor captor = ArgumentCaptor.forClass(ResetPasswordRequestEvent.class); verify(publisher).publishEvent(captor.capture()); ResetPasswordRequestEvent event = captor.getValue(); @@ -122,7 +133,7 @@ public void forgotPassword_ThrowsConflictException() throws Exception { when(codeStore.retrieveCode(anyString())).thenReturn(new ExpiringCode("code", new Timestamp(System.currentTimeMillis()),"user-id-001")); try { - emailResetPasswordService.forgotPassword("user@example.com"); + emailResetPasswordService.forgotPassword("user@example.com", "", ""); fail(); } catch (ConflictException e) { assertThat(e.getUserId(), equalTo("user-id-001")); @@ -131,24 +142,22 @@ public void forgotPassword_ThrowsConflictException() throws Exception { @Test(expected = NotFoundException.class) public void forgotPassword_ThrowsNotFoundException_ScimUserNotFoundInUaa() throws Exception { - emailResetPasswordService.forgotPassword("user@example.com"); + emailResetPasswordService.forgotPassword("user@example.com", "", ""); } @Test public void testResetPassword() throws Exception { - ScimUser user = new ScimUser("usermans-id","userman","firstName","lastName"); - user.setMeta(new ScimMeta(new Date(System.currentTimeMillis()-(1000*60*60*24)), new Date(System.currentTimeMillis()-(1000*60*60*24)), 0)); - user.setPrimaryEmail("user@example.com"); - when(scimUserProvisioning.retrieve(eq("usermans-id"))).thenReturn(user); - when(codeStore.retrieveCode(eq("secret_code"))).thenReturn(new ExpiringCode("code", new Timestamp(System.currentTimeMillis()), "usermans-id")); - SecurityContext securityContext = mock(SecurityContext.class); - when(securityContext.getAuthentication()).thenReturn(new MockAuthentication()); - SecurityContextHolder.setContext(securityContext); + setupResetPassword("example", "redirect.example.com/login"); + + BaseClientDetails client = new BaseClientDetails(); + client.setRegisteredRedirectUri(Collections.singleton("redirect.example.com/*")); + when(clientDetailsService.loadClientByClientId("example")).thenReturn(client); - user = emailResetPasswordService.resetPassword("secret_code", "new_secret"); + ResetPasswordResponse response = emailResetPasswordService.resetPassword("secret_code", "new_secret"); - Assert.assertEquals("usermans-id", user.getId()); - Assert.assertEquals("userman", user.getUserName()); + Assert.assertEquals("usermans-id", response.getUser().getId()); + Assert.assertEquals("userman", response.getUser().getUserName()); + Assert.assertEquals("redirect.example.com/login", response.getRedirectUri()); } @Test(expected = UaaException.class) @@ -184,4 +193,53 @@ public void resetPassword_InvalidPasswordException_NewPasswordSameAsOld() { assertEquals(UNPROCESSABLE_ENTITY, e.getStatus()); } } + + @Test + public void resetPassword_WithInvalidClientId() { + setupResetPassword("invalid_client", "redirect.example.com"); + doThrow(new NoSuchClientException("no such client")).when(clientDetailsService).loadClientByClientId("invalid_client"); + ResetPasswordResponse response = emailResetPasswordService.resetPassword("secret_code", "new_secret"); + assertEquals("home", response.getRedirectUri()); + } + + @Test + public void resetPassword_WithNoClientId() { + setupResetPassword("", "redirect.example.com"); + ResetPasswordResponse response = emailResetPasswordService.resetPassword("secret_code", "new_secret"); + assertEquals("home", response.getRedirectUri()); + } + + @Test + public void resetPassword_WhereWildcardsDoNotMatch() { + setupResetPassword("example", "redirect.example.com"); + BaseClientDetails client = new BaseClientDetails(); + client.setRegisteredRedirectUri(Collections.singleton("doesnotmatch.example.com/*")); + when(clientDetailsService.loadClientByClientId("example")).thenReturn(client); + + ResetPasswordResponse response = emailResetPasswordService.resetPassword("secret_code", "new_secret"); + assertEquals("home", response.getRedirectUri()); + } + + @Test + public void resetPassword_WithNoRedirectUri() { + setupResetPassword("example", ""); + BaseClientDetails client = new BaseClientDetails(); + client.setRegisteredRedirectUri(Collections.singleton("redirect.example.com/*")); + when(clientDetailsService.loadClientByClientId("example")).thenReturn(client); + + ResetPasswordResponse response = emailResetPasswordService.resetPassword("secret_code", "new_secret"); + assertEquals("home", response.getRedirectUri()); + } + + private void setupResetPassword(String clientId, String redirectUri) { + ScimUser user = new ScimUser("usermans-id","userman","firstName","lastName"); + user.setMeta(new ScimMeta(new Date(System.currentTimeMillis()-(1000*60*60*24)), new Date(System.currentTimeMillis()-(1000*60*60*24)), 0)); + user.setPrimaryEmail("user@example.com"); + when(scimUserProvisioning.retrieve(eq("usermans-id"))).thenReturn(user); + when(codeStore.retrieveCode(eq("secret_code"))).thenReturn(new ExpiringCode("code", new Timestamp(System.currentTimeMillis()), + "{\"user_id\":\"usermans-id\",\"username\":\"userman\",\"passwordModifiedTime\":null,\"client_id\":\"" + clientId + "\",\"redirect_uri\":\"" + redirectUri + "\"}")); + SecurityContext securityContext = mock(SecurityContext.class); + when(securityContext.getAuthentication()).thenReturn(new MockAuthentication()); + SecurityContextHolder.setContext(securityContext); + } } diff --git a/scim/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordResetEndpointTest.java b/scim/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordResetEndpointTest.java index 2f7d0834786..6610f1caaf5 100644 --- a/scim/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordResetEndpointTest.java +++ b/scim/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordResetEndpointTest.java @@ -26,18 +26,13 @@ import org.cloudfoundry.identity.uaa.scim.validate.PasswordValidator; import org.cloudfoundry.identity.uaa.test.MockAuthentication; import org.cloudfoundry.identity.uaa.util.JsonUtils; -import org.hamcrest.BaseMatcher; -import org.hamcrest.Description; -import org.hamcrest.Matcher; -import org.json.JSONArray; -import org.json.JSONException; import org.json.JSONObject; -import org.json.JSONTokener; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @@ -45,7 +40,6 @@ import java.sql.Timestamp; import java.util.Arrays; import java.util.Date; -import java.util.Objects; import org.cloudfoundry.identity.uaa.scim.test.JsonObjectMatcherUtils; import static org.hamcrest.Matchers.containsString; @@ -67,6 +61,7 @@ public class PasswordResetEndpointTest extends TestClassNullifier { private ExpiringCodeStore expiringCodeStore; private PasswordValidator passwordValidator; private ResetPasswordService resetPasswordService; + private ClientDetailsService clientDetailsService; Date yesterday = new Date(System.currentTimeMillis()-(1000*60*60*24)); @Before @@ -74,12 +69,13 @@ public void setUp() throws Exception { scimUserProvisioning = mock(ScimUserProvisioning.class); expiringCodeStore = mock(ExpiringCodeStore.class); passwordValidator = mock(PasswordValidator.class); - resetPasswordService = new UaaResetPasswordService(scimUserProvisioning, expiringCodeStore, passwordValidator); + clientDetailsService = mock(ClientDetailsService.class); + resetPasswordService = new UaaResetPasswordService(scimUserProvisioning, expiringCodeStore, passwordValidator, clientDetailsService); PasswordResetEndpoint controller = new PasswordResetEndpoint(resetPasswordService); controller.setMessageConverters(new HttpMessageConverter[] { new ExceptionReportHttpMessageConverter() }); mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); - PasswordChange change = new PasswordChange("id001", "user@example.com", yesterday); + PasswordChange change = new PasswordChange("id001", "user@example.com", yesterday, "", ""); when(expiringCodeStore.generateCode(eq("id001"), any(Timestamp.class))) .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), "id001")); @@ -88,7 +84,7 @@ public void setUp() throws Exception { when(expiringCodeStore.generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class))) .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), JsonUtils.writeValueAsString(change))); - change = new PasswordChange("id001", "user\"'@example.com", yesterday); + change = new PasswordChange("id001", "user\"'@example.com", yesterday, "", ""); when(expiringCodeStore.generateCode(eq(JsonUtils.writeValueAsString(change)), any(Timestamp.class))) .thenReturn(new ExpiringCode("secret_code", new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME), JsonUtils.writeValueAsString(change))); } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/ResetPasswordIT.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/ResetPasswordIT.java index 525c3f34e20..42990315556 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/ResetPasswordIT.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/ResetPasswordIT.java @@ -67,6 +67,7 @@ public class ResetPasswordIT { String baseUrl; private String userEmail; + private String scimClientId; @Before @After @@ -85,7 +86,7 @@ public void setUp() throws Exception { int randomInt = new SecureRandom().nextInt(); String adminAccessToken = testClient.getOAuthAccessToken("admin", "adminsecret", "client_credentials", "clients.read clients.write clients.secret"); - String scimClientId = "scim" + randomInt; + scimClientId = "scim" + randomInt; testClient.createScimClient(adminAccessToken, scimClientId); String scimAccessToken = testClient.getOAuthAccessToken(scimClientId, "scimsecret", "client_credentials", "scim.read scim.write password.write"); userEmail = "user" + randomInt + "@example.com"; @@ -134,6 +135,37 @@ public void resettingAPassword() throws Exception { assertThat(webDriver.findElement(By.cssSelector(".error-message")).getText(), containsString("Sorry, your reset password link is no longer valid. You can request another one below.")); } + @Test + public void resetPassword_with_clientRedirect() throws Exception { + webDriver.get(baseUrl + "/forgot_password?client_id=" + scimClientId + "&redirect_uri=http://example.redirect.com"); + Assert.assertEquals("Reset Password", webDriver.findElement(By.tagName("h1")).getText()); + + int receivedEmailSize = simpleSmtpServer.getReceivedEmailSize(); + + webDriver.findElement(By.name("email")).sendKeys(userEmail); + webDriver.findElement(By.xpath("//input[@value='Send reset password link']")).click(); + Assert.assertEquals("Instructions Sent", webDriver.findElement(By.tagName("h1")).getText()); + + assertEquals(receivedEmailSize + 1, simpleSmtpServer.getReceivedEmailSize()); + Iterator receivedEmail = simpleSmtpServer.getReceivedEmail(); + SmtpMessage message = (SmtpMessage) receivedEmail.next(); + receivedEmail.remove(); + assertEquals(userEmail, message.getHeaderValue("To")); + assertThat(message.getBody(), containsString("Reset your password")); + + Assert.assertEquals("Please check your email for a reset password link.", webDriver.findElement(By.cssSelector(".instructions-sent")).getText()); + + // Click link in email + String link = testClient.extractLink(message.getBody()); + webDriver.get(link); + + webDriver.findElement(By.name("password")).sendKeys("new_password"); + webDriver.findElement(By.name("password_confirmation")).sendKeys("new_password"); + webDriver.findElement(By.xpath("//input[@value='Create new password']")).click(); + + assertEquals("http://example.redirect.com/", webDriver.getCurrentUrl()); + } + @Test public void resettingAPasswordForANonExistentUser() throws Exception { webDriver.get(baseUrl + "/login"); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/TestClient.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/TestClient.java index 4823a58a897..25594441c8b 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/TestClient.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/TestClient.java @@ -82,6 +82,7 @@ public void createScimClient(String adminAccessToken, String clientId) throws Ex "\"client_secret\":\"scimsecret\"," + "\"resource_ids\":[\"oauth\"]," + "\"authorized_grant_types\":[\"client_credentials\"]," + + "\"redirect_uri\":[\"http://example.redirect.com\"]," + "\"authorities\":[\"password.write\",\"scim.write\",\"scim.read\",\"oauth.approvals\"]" + "}", uaaUrl + "/oauth/clients" diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerMockMvcTests.java index 3bafe3495d0..7fd6387948a 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerMockMvcTests.java @@ -67,7 +67,7 @@ public void testResettingAPasswordUsingUsernameToEnsureNoModification() throws E List users = getWebApplicationContext().getBean(ScimUserProvisioning.class).query("username eq \"marissa\""); assertNotNull(users); assertEquals(1, users.size()); - PasswordChange change = new PasswordChange(users.get(0).getId(), users.get(0).getUserName(), users.get(0).getPasswordLastModified()); + PasswordChange change = new PasswordChange(users.get(0).getId(), users.get(0).getUserName(), users.get(0).getPasswordLastModified(), "", ""); ExpiringCode code = codeStore.generateCode(JsonUtils.writeValueAsString(change), new Timestamp(System.currentTimeMillis() + UaaResetPasswordService.PASSWORD_RESET_LIFETIME)); @@ -94,7 +94,7 @@ public void testResettingAPasswordFailsWhenUsernameChanged() throws Exception { assertNotNull(users); assertEquals(1, users.size()); ScimUser user = users.get(0); - PasswordChange change = new PasswordChange(user.getId(), user.getUserName(), user.getPasswordLastModified()); + PasswordChange change = new PasswordChange(user.getId(), user.getUserName(), user.getPasswordLastModified(), "", ""); ExpiringCode code = codeStore.generateCode(JsonUtils.writeValueAsString(change), new Timestamp(System.currentTimeMillis() + 50000)); @@ -120,7 +120,7 @@ public void testResettingAPasswordChangesCodeInForm() throws Exception { String token = MockMvcUtils.utils().getClientCredentialsOAuthAccessToken(getMockMvc(), "admin", "adminsecret", null, null); user = MockMvcUtils.utils().createUser(getMockMvc(), token, user); - PasswordChange change = new PasswordChange(user.getId(), user.getUserName(), user.getPasswordLastModified()); + PasswordChange change = new PasswordChange(user.getId(), user.getUserName(), user.getPasswordLastModified(), "", ""); ExpiringCode code = codeStore.generateCode(JsonUtils.writeValueAsString(change), new Timestamp(System.currentTimeMillis() + 50000)); @@ -142,7 +142,6 @@ public void testResettingAPasswordChangesCodeInForm() throws Exception { getMockMvc().perform(createChangePasswordRequest(user, newCode, true, "secret1", "secret1")) .andExpect(status().isFound()) .andExpect(redirectedUrl("home")); - } @@ -156,7 +155,7 @@ public void testResettingAPasswordFailsWhenPasswordChanged() throws Exception { user = MockMvcUtils.utils().createUser(getMockMvc(), token, user); ScimUserProvisioning userProvisioning = getWebApplicationContext().getBean(ScimUserProvisioning.class); Thread.sleep(1000 - (System.currentTimeMillis() % 1000) + 10); //because password last modified is second only - PasswordChange change = new PasswordChange(user.getId(), user.getUserName(), user.getPasswordLastModified()); + PasswordChange change = new PasswordChange(user.getId(), user.getUserName(), user.getPasswordLastModified(), "", ""); ExpiringCode code = codeStore.generateCode(JsonUtils.writeValueAsString(change), new Timestamp(System.currentTimeMillis() + 50000)); userProvisioning.changePassword(user.getId(), "secret", "secr3t"); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/DisableUserManagementSecurityFilterMockMvcTest.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/DisableUserManagementSecurityFilterMockMvcTest.java index 6ef92108c80..bb7cbe27755 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/DisableUserManagementSecurityFilterMockMvcTest.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/DisableUserManagementSecurityFilterMockMvcTest.java @@ -382,7 +382,7 @@ public void resetPasswordControllerResetPasswordNotAllowed() throws Exception { ResultActions result = createUser(); ScimUser createdUser = JsonUtils.readValue(result.andReturn().getResponse().getContentAsString(), ScimUser.class); - PasswordChange change = new PasswordChange(createdUser.getId(), createdUser.getUserName(), createdUser.getPasswordLastModified()); + PasswordChange change = new PasswordChange(createdUser.getId(), createdUser.getUserName(), createdUser.getPasswordLastModified(), "", ""); MockMvcUtils.setDisableInternalUserManagement(true, getWebApplicationContext()); getMockMvc().perform(post("/reset_password.do") From fe53fe51700e03f22bdfa01afd574a51025abcda Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Fri, 2 Oct 2015 17:41:02 -0700 Subject: [PATCH 036/103] Changed invite behaviors to return invite link instead of sending an email - updated docs - removed inviteUsers from invitationsService - removed redundant tests [#103822132] https://www.pivotaltracker.com/story/show/103822132 Signed-off-by: Jonathan Lo --- docs/UAA-APIs.rst | 14 +- .../uaa/invitations/InvitationConstants.java | 18 ++ .../uaa/invitations/InvitationsEndpoint.java | 58 +++-- .../uaa/invitations/InvitationsResponse.java | 38 ++- .../uaa/invitations/InvitationsService.java | 2 - .../uaa/login/EmailInvitationsService.java | 18 -- login/src/main/resources/login-ui.xml | 2 +- .../login/EmailInvitationsServiceTests.java | 130 ---------- .../InvitationsEndpointMockMvcTests.java | 243 ++++++++++-------- .../login/InvitationsServiceMockMvcTests.java | 123 ++------- .../identity/uaa/mock/util/MockMvcUtils.java | 21 +- .../ScimGroupEndpointsMockMvcTests.java | 14 +- .../ScimUserEndpointsMockMvcTests.java | 7 +- .../endpoints/ScimUserLookupMockMvcTests.java | 10 +- 14 files changed, 278 insertions(+), 420 deletions(-) create mode 100644 login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationConstants.java diff --git a/docs/UAA-APIs.rst b/docs/UAA-APIs.rst index 3e7c5e7e9a6..e3825098d2c 100644 --- a/docs/UAA-APIs.rst +++ b/docs/UAA-APIs.rst @@ -39,8 +39,8 @@ Here is a summary of the different scopes that are known to the UAA. * **scim.read** - Admin read access to all SCIM endpoints, ``/Users``, ``/Groups/``. * **scim.create** - Reduced scope to be able to create a user using ``POST /Users`` (and verify their account using ``GET /Users/{id}/verify``) but not be able to modify, read or delete users. * **scim.userids** - ``/ids/Users`` - Required to convert a username+origin to a user ID and vice versa. -* **scim.zones** - limited scope that only allows adding/removing a user to/from `zone management groups`_ under the path /Groups/zones -* **scim.invite** - Scope required to perform email invitations at ``/invite_users`` +* **scim.zones** - Limited scope that only allows adding/removing a user to/from `zone management groups`_ under the path /Groups/zones +* **scim.invite** - Scope required by a client in order to participate in invitations using the ``/invite_users`` endpoint. * **password.write** - ``/User*/*/password`` endpoint. Admin scope to change a user's password. * **oauth.approval** - ``/approvals`` endpoint. Scope required to be able to approve/disapprove clients to act on a user's behalf. This is a default scope defined in uaa.yml. * **oauth.login** - Scope used to indicate a login application, such as external login servers, to perform trusted operations, such as create users not authenticated in the UAA. @@ -1703,11 +1703,11 @@ ENDPOINT DEPRECATED - Will always return score:0 and requiredScore:0 Inviting Users -------------- -The UAA supports the notion of inviting users. When a user is invited provided an email address, the system will +The UAA supports the notion of inviting users. When a user is invited by providing an email address, the system will locate the appropriate authentication provider and create the user account. -The invitation endpoint then return the corresponding `user_id` for the email. +The invitation endpoint then returns the corresponding `user_id` and `inviteLink`. Batch processing is allowed by specifying more than one email address. -The endpoint takes two parameters, a client_id and a redirect_uri. +The endpoint takes two parameters, a client_id (optional) and a redirect_uri. When a user accepts the invitation, the user will be redirected to the redirect_uri. The redirect_uri will be validated against allowed redirect_uri for the client. @@ -1729,8 +1729,8 @@ The redirect_uri will be validated against allowed redirect_uri for the client. { "new_invites":[ - {"email":"user1@cqv4f7.com","userId":"38de0ac4-b194-4e33-b6c2-0755a37205fb","origin":"uaa","success":true,"errorCode":null,"errorMessage":null}, - {"email":"user2@cqv4f7.com","userId":"1665631f-1957-44fe-ac49-2739dd55bb3f","origin":"uaa","success":true,"errorCode":null,"errorMessage":null} + {"email":"user1@cqv4f7.com","userId":"38de0ac4-b194-4e33-b6c2-0755a37205fb","origin":"uaa","success":true,"inviteLink":"http://myuaa.cloudfoundry.com/invitations/accept?code=yuT6rd","errorCode":null,"errorMessage":null}, + {"email":"user2@cqv4f7.com","userId":"1665631f-1957-44fe-ac49-2739dd55bb3f","origin":"uaa","success":true,"inviteLink":"http://myuaa.cloudfoundry.com/invitations/accept?code=yuT6rd","errorCode":null,"errorMessage":null} ], "failed_invites":[] } diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationConstants.java b/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationConstants.java new file mode 100644 index 00000000000..a0c414e7e57 --- /dev/null +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationConstants.java @@ -0,0 +1,18 @@ +package org.cloudfoundry.identity.uaa.invitations; + +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + *

+ * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + *

+ * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +public interface InvitationConstants { + static final String USER_ID = "user_id"; + static final String EMAIL = "email"; +} diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsEndpoint.java b/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsEndpoint.java index 7e5fe949ebe..506cee02b53 100644 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsEndpoint.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsEndpoint.java @@ -1,12 +1,14 @@ package org.cloudfoundry.identity.uaa.invitations; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; +import org.cloudfoundry.identity.uaa.codestore.ExpiringCode; +import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; import org.cloudfoundry.identity.uaa.error.UaaException; -import org.cloudfoundry.identity.uaa.invitations.InvitationsResponse.InvitedUser; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceConflictException; import org.cloudfoundry.identity.uaa.util.DomainFilter; +import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.cloudfoundry.identity.uaa.zone.IdentityProvider; import org.cloudfoundry.identity.uaa.zone.IdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; @@ -23,44 +25,48 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import java.net.MalformedURLException; +import java.net.URL; +import java.sql.Timestamp; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; + +import static org.cloudfoundry.identity.uaa.authentication.Origin.ORIGIN; +import static org.springframework.security.oauth2.common.util.OAuth2Utils.CLIENT_ID; +import static org.springframework.security.oauth2.common.util.OAuth2Utils.REDIRECT_URI; @Controller public class InvitationsEndpoint { - private InvitationsService invitationsService; + public static final int INVITATION_EXPIRY_DAYS = 7; + private ScimUserProvisioning users; private IdentityProviderProvisioning providers; private ClientDetailsService clients; + private ExpiringCodeStore expiringCodeStore; - public InvitationsEndpoint(InvitationsService invitationsService, - ScimUserProvisioning users, + public InvitationsEndpoint(ScimUserProvisioning users, IdentityProviderProvisioning providers, - ClientDetailsService clients) { - this.invitationsService = invitationsService; + ClientDetailsService clients, + ExpiringCodeStore expiringCodeStore) { this.users = users; this.providers = providers; this.clients = clients; + this.expiringCodeStore = expiringCodeStore; } @RequestMapping(value="/invite_users", method= RequestMethod.POST, consumes="application/json") public ResponseEntity inviteUsers(@RequestBody InvitationsRequest invitations, - @RequestParam(value="client_id") String clientId, + @RequestParam(value="client_id", required=false) String clientId, @RequestParam(value="redirect_uri") String redirectUri) { - // todo: get clientId from token, if not supplied in clientId - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String currentUser = null; if (authentication instanceof OAuth2Authentication) { OAuth2Authentication oAuth2Authentication = (OAuth2Authentication)authentication; - if (!oAuth2Authentication.isClientOnly()) { - currentUser = ((UaaPrincipal) oAuth2Authentication.getPrincipal()).getName(); - } else { - currentUser = oAuth2Authentication.getOAuth2Request().getClientId(); - } if (clientId==null) { clientId = oAuth2Authentication.getOAuth2Request().getClientId(); @@ -68,7 +74,6 @@ public ResponseEntity inviteUsers(@RequestBody InvitationsR } InvitationsResponse invitationsResponse = new InvitationsResponse(); - List newInvitesEmails = new ArrayList<>(); DomainFilter filter = new DomainFilter(); List activeProviders = providers.retrieveActive(IdentityZoneHolder.get().getId()); @@ -78,8 +83,25 @@ public ResponseEntity inviteUsers(@RequestBody InvitationsR List providers = filter.filter(activeProviders, client, email); if (providers.size() == 1) { ScimUser user = findOrCreateUser(email, providers.get(0).getOriginKey()); - invitationsService.inviteUser(user, currentUser, clientId, redirectUri); - invitationsResponse.getNewInvites().add(InvitationsResponse.success(user.getPrimaryEmail(), user.getId(), user.getOrigin())); + + String accountsUrl = ServletUriComponentsBuilder.fromCurrentContextPath().path("/invitations/accept").build().toUriString(); + + Map data = new HashMap<>(); + data.put(InvitationConstants.USER_ID, user.getId()); + data.put(InvitationConstants.EMAIL, user.getPrimaryEmail()); + data.put(CLIENT_ID, clientId); + data.put(REDIRECT_URI, redirectUri); + data.put(ORIGIN, user.getOrigin()); + Timestamp expiry = new Timestamp(System.currentTimeMillis()+ (INVITATION_EXPIRY_DAYS * 24 * 60 * 60 * 1000)); + ExpiringCode code = expiringCodeStore.generateCode(JsonUtils.writeValueAsString(data), expiry); + + String invitationLink = accountsUrl + "?code=" + code.getCode(); + try { + URL inviteLink = new URL(invitationLink); + invitationsResponse.getNewInvites().add(InvitationsResponse.success(user.getPrimaryEmail(), user.getId(), user.getOrigin(), inviteLink)); + } catch (MalformedURLException mue){ + invitationsResponse.getFailedInvites().add(InvitationsResponse.failure(email, "invitation.exception.url", String.format("Malformed url", invitationLink))); + } } else if (providers.size() == 0) { invitationsResponse.getFailedInvites().add(InvitationsResponse.failure(email, "provider.non-existent", "No authentication provider found.")); } else { diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsResponse.java b/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsResponse.java index cb74d72d0bb..b3fed8740b7 100644 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsResponse.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsResponse.java @@ -2,36 +2,42 @@ import com.fasterxml.jackson.annotation.JsonProperty; +import java.net.URI; +import java.net.URL; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class InvitationsResponse { @JsonProperty(value="new_invites") - private List newInvites = new ArrayList<>(); + private List newInvites = new ArrayList<>(); + @JsonProperty(value="new_invite_links") + private List newInviteLinks = new ArrayList<>(); @JsonProperty(value="failed_invites") - private List failedInvites = new ArrayList<>(); + private List failedInvites = new ArrayList<>(); public InvitationsResponse() {} - public List getNewInvites() { + public List getNewInvites() { return newInvites; } - public void setNewInvites(List newInvites) { + public void setNewInvites(List newInvites) { this.newInvites = newInvites; } - public List getFailedInvites() { + public List getFailedInvites() { return failedInvites; } - public void setFailedInvites(List failedInvites) { + public void setFailedInvites(List failedInvites) { this.failedInvites = failedInvites; } - public static InvitedUser failure(String email, String errorCode, String errorMessage) { - InvitedUser user = new InvitedUser(); + public static Invitee failure(String email, String errorCode, String errorMessage) { + Invitee user = new Invitee(); user.email = email; user.errorCode = errorCode; user.errorMessage = errorMessage; @@ -39,16 +45,17 @@ public static InvitedUser failure(String email, String errorCode, String errorMe return user; } - public static InvitedUser success(String email, String userId, String origin) { - InvitedUser user = new InvitedUser(); + public static Invitee success(String email, String userId, String origin, URL inviteLink) { + Invitee user = new Invitee(); user.email = email; user.userId = userId; user.origin = origin; user.success = true; + user.inviteLink = inviteLink; return user; } - public static class InvitedUser { + public static class Invitee { private String email; private String userId; private String origin; @@ -56,7 +63,9 @@ public static class InvitedUser { private String errorCode; private String errorMessage; - public InvitedUser() { + private URL inviteLink; + + public Invitee() { } public String getEmail() { @@ -106,6 +115,11 @@ public String getErrorMessage() { public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } + + public URL getInviteLink() { return inviteLink; } + + public void setInviteLink(URL inviteLink) { this.inviteLink = inviteLink; } + } } diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsService.java b/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsService.java index 969871f5110..7fb86d0d911 100644 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsService.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsService.java @@ -4,8 +4,6 @@ public interface InvitationsService { - void inviteUser(ScimUser user, String currentUser, String clientId, String redirectUri); - AcceptedInvitation acceptInvitation(String code, String password); class AcceptedInvitation { diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/login/EmailInvitationsService.java b/login/src/main/java/org/cloudfoundry/identity/uaa/login/EmailInvitationsService.java index ae5242ab746..14a91a5068e 100644 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/login/EmailInvitationsService.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/login/EmailInvitationsService.java @@ -13,7 +13,6 @@ import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.cloudfoundry.identity.uaa.util.UaaUrlUtils; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.oauth2.common.util.OAuth2Utils; import org.springframework.security.oauth2.provider.ClientDetails; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.NoSuchClientException; @@ -24,13 +23,10 @@ import org.thymeleaf.context.Context; import org.thymeleaf.spring4.SpringTemplateEngine; -import java.io.IOException; import java.sql.Timestamp; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.TimeUnit; import static org.cloudfoundry.identity.uaa.authentication.Origin.ORIGIN; import static org.springframework.security.oauth2.common.util.OAuth2Utils.CLIENT_ID; @@ -91,20 +87,6 @@ private String getEmailHtml(String currentUser, String code) { return templateEngine.process("invite", ctx); } - @Override - public void inviteUser(ScimUser user, String currentUser, String clientId, String redirectUri) { - String email = user.getPrimaryEmail(); - Map data = new HashMap<>(); - data.put(USER_ID, user.getId()); - data.put(EMAIL, email); - data.put(CLIENT_ID, clientId); - data.put(REDIRECT_URI, redirectUri); - data.put(ORIGIN, user.getOrigin()); - Timestamp expiry = new Timestamp(System.currentTimeMillis()+ (INVITATION_EXPIRY_DAYS * 24 * 60 * 60 * 1000)); - ExpiringCode code = expiringCodeStore.generateCode(JsonUtils.writeValueAsString(data), expiry); - sendInvitationEmail(email, currentUser, code.getCode()); - } - @Override public AcceptedInvitation acceptInvitation(String code, String password) { ExpiringCode data = expiringCodeStore.retrieveCode(code); diff --git a/login/src/main/resources/login-ui.xml b/login/src/main/resources/login-ui.xml index f72f52f1bf5..7594257e363 100644 --- a/login/src/main/resources/login-ui.xml +++ b/login/src/main/resources/login-ui.xml @@ -427,9 +427,9 @@ - + diff --git a/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailInvitationsServiceTests.java b/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailInvitationsServiceTests.java index 04ff19214d9..a1e188d6378 100644 --- a/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailInvitationsServiceTests.java +++ b/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailInvitationsServiceTests.java @@ -1,31 +1,22 @@ package org.cloudfoundry.identity.uaa.login; -import com.fasterxml.jackson.core.type.TypeReference; import org.cloudfoundry.identity.uaa.authentication.Origin; import org.cloudfoundry.identity.uaa.codestore.ExpiringCode; import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; -import org.cloudfoundry.identity.uaa.error.UaaException; import org.cloudfoundry.identity.uaa.login.test.ThymeleafConfig; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; -import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceAlreadyExistsException; import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.oauth2.common.util.OAuth2Utils; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.NoSuchClientException; import org.springframework.security.oauth2.provider.client.BaseClientDetails; @@ -36,8 +27,6 @@ import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.ConfigurableWebApplicationContext; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; @@ -46,15 +35,11 @@ import java.sql.Timestamp; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.TimeUnit; import static org.cloudfoundry.identity.uaa.authentication.Origin.UAA; import static org.cloudfoundry.identity.uaa.login.EmailInvitationsService.EMAIL; import static org.cloudfoundry.identity.uaa.login.EmailInvitationsService.USER_ID; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyObject; import static org.mockito.Matchers.anyString; @@ -102,121 +87,6 @@ public void tearDown() { SecurityContextHolder.clearContext(); } - @Test - public void testSendInviteEmail() throws Exception { - ArgumentCaptor captor = ArgumentCaptor.forClass((Class)String.class); - - ScimUser user = new ScimUser("existing-user-id", "marissa", "Marissa", "Koala"); - user.setPrimaryEmail("user@example.com"); - - when(expiringCodeStore.generateCode(captor.capture(), anyObject())).thenReturn(new ExpiringCode("the_secret_code", null, null)); - emailInvitationsService.inviteUser(user, "current-user", "client-id", "blah.example.com"); - - Map data = JsonUtils.readValue(captor.getValue(), new TypeReference>() {}); - assertEquals("existing-user-id", data.get("user_id")); - assertEquals("client-id", data.get("client_id")); - assertEquals("blah.example.com", data.get("redirect_uri")); - - ArgumentCaptor emailBodyArgument = ArgumentCaptor.forClass(String.class); - Mockito.verify(messageService).sendMessage( - eq("user@example.com"), - eq(MessageType.INVITATION), - eq("Invitation to join Pivotal"), - emailBodyArgument.capture() - ); - String emailBody = emailBodyArgument.getValue(); - assertThat(emailBody, containsString("current-user")); - assertThat(emailBody, containsString("Pivotal")); - assertThat(emailBody, containsString("Accept Invite")); - assertThat(emailBody, not(containsString("Cloud Foundry"))); - } - - @Test - public void inviteUserWithoutClientIdOrRedirectUri() throws Exception { - ArgumentCaptor captor = ArgumentCaptor.forClass((Class)String.class); - - ScimUser user = new ScimUser("existing-user-id", "marissa", "Marissa", "Koala"); - user.setPrimaryEmail("user@example.com"); - - when(expiringCodeStore.generateCode(captor.capture(), anyObject())).thenReturn(new ExpiringCode("the_secret_code", null, null)); - emailInvitationsService.inviteUser(user, "current-user", "", ""); - - Map data = JsonUtils.readValue(captor.getValue(), new TypeReference>() {}); - assertEquals("existing-user-id", data.get("user_id")); - assertEquals("", data.get("client_id")); - assertEquals("", data.get("redirect_uri")); - } - - @Test - public void testSendInviteEmailToUserThatIsAlreadyVerified() throws Exception { - ScimUser user = new ScimUser("12345", "marissa", "Marissa", "Koala"); - user.setPrimaryEmail("user@example.com"); - user.setVerified(true); - final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(String.class); - when(expiringCodeStore.generateCode(argumentCaptor.capture(), anyObject())) - .thenAnswer(invocation -> new ExpiringCode("code", new Timestamp(System.currentTimeMillis()), argumentCaptor.getValue())); - - emailInvitationsService.inviteUser(user, "current-user", "", ""); - } - - @Test - public void testSendInviteEmailToUnverifiedUser() throws Exception { - - ScimUser user = new ScimUser("existing-user-id", "marissa", "Marissa", "Koala"); - user.setPrimaryEmail("existingunverified@example.com"); - user.setVerified(true); - - ArgumentCaptor captor = ArgumentCaptor.forClass((Class)String.class); - - when(expiringCodeStore.generateCode(captor.capture(), anyObject())).thenReturn(new ExpiringCode("the_secret_code", null, null)); - emailInvitationsService.inviteUser(user, "current-user", "client-id", "blah.example.com"); - - Map data = JsonUtils.readValue(captor.getValue(), new TypeReference>() {}); - assertEquals("existing-user-id", data.get("user_id")); - assertEquals("blah.example.com", data.get("redirect_uri")); - - ArgumentCaptor emailBodyArgument = ArgumentCaptor.forClass(String.class); - Mockito.verify(messageService).sendMessage( - eq("existingunverified@example.com"), - eq(MessageType.INVITATION), - eq("Invitation to join Pivotal"), - emailBodyArgument.capture() - ); - String emailBody = emailBodyArgument.getValue(); - assertThat(emailBody, containsString("current-user")); - assertThat(emailBody, containsString("Pivotal")); - assertThat(emailBody, containsString("Accept Invite")); - assertThat(emailBody, not(containsString("Cloud Foundry"))); - } - - @Test - public void testSendInviteEmailWithOSSBrand() throws Exception { - emailInvitationsService.setBrand("oss"); - ArgumentCaptor captor = ArgumentCaptor.forClass((Class)String.class); - - ScimUser user = new ScimUser("existing-user-id", "marissa", "Marissa", "Koala"); - user.setPrimaryEmail("user@example.com"); - - when(expiringCodeStore.generateCode(captor.capture(), anyObject())).thenReturn(new ExpiringCode("the_secret_code", null, null)); - emailInvitationsService.inviteUser(user, "current-user", "client-id", "blah.example.com"); - - Map data = JsonUtils.readValue(captor.getValue(), new TypeReference>() {}); - assertEquals("existing-user-id", data.get("user_id")); - - ArgumentCaptor emailBodyArgument = ArgumentCaptor.forClass(String.class); - Mockito.verify(messageService).sendMessage( - eq("user@example.com"), - eq(MessageType.INVITATION), - eq("Invitation to join Cloud Foundry"), - emailBodyArgument.capture() - ); - String emailBody = emailBodyArgument.getValue(); - assertThat(emailBody, containsString("current-user")); - assertThat(emailBody, containsString("Accept Invite")); - assertThat(emailBody, containsString("Cloud Foundry")); - assertThat(emailBody, not(containsString("Pivotal"))); - } - @Test public void acceptInvitationNoClientId() throws Exception { ScimUser user = new ScimUser("user-id-001", "user@example.com", "first", "last"); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsEndpointMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsEndpointMockMvcTests.java index f99671b71c3..be55d76b324 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsEndpointMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/invitations/InvitationsEndpointMockMvcTests.java @@ -1,9 +1,10 @@ package org.cloudfoundry.identity.uaa.invitations; +import com.fasterxml.jackson.core.type.TypeReference; +import org.cloudfoundry.identity.uaa.authentication.Origin; +import org.cloudfoundry.identity.uaa.codestore.ExpiringCode; +import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; import org.cloudfoundry.identity.uaa.config.IdentityProviderBootstrap; -import org.cloudfoundry.identity.uaa.login.EmailService; -import org.cloudfoundry.identity.uaa.login.test.MockMvcTestClient; -import org.cloudfoundry.identity.uaa.login.util.FakeJavaMailSender; import org.cloudfoundry.identity.uaa.mock.InjectedMockContextTest; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; import org.cloudfoundry.identity.uaa.scim.ScimUser; @@ -14,34 +15,36 @@ import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.UaaIdentityProviderDefinition; import org.flywaydb.core.internal.util.StringUtils; -import org.junit.After; import org.junit.Before; import org.junit.Test; import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mock.env.MockEnvironment; -import org.springframework.security.core.context.SecurityContext; import org.springframework.security.oauth2.common.util.OAuth2Utils; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; -import javax.mail.Message; -import javax.mail.MessagingException; import java.util.Arrays; -import java.util.Iterator; +import java.util.Map; +import static org.cloudfoundry.identity.uaa.authentication.Origin.ORIGIN; import static org.cloudfoundry.identity.uaa.authentication.Origin.UAA; -import static org.cloudfoundry.identity.uaa.login.util.FakeJavaMailSender.MimeMessageWrapper; -import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.createScimClient; import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.utils; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.CoreMatchers.startsWith; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.not; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.security.oauth2.common.util.OAuth2Utils.CLIENT_ID; +import static org.springframework.security.oauth2.common.util.OAuth2Utils.REDIRECT_URI; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; @@ -53,129 +56,76 @@ public class InvitationsEndpointMockMvcTests extends InjectedMockContextTest { private RandomValueStringGenerator generator = new RandomValueStringGenerator(); private String clientId; private String clientSecret; + private ClientDetails clientDetails; private String adminToken; private String authorities; - private FakeJavaMailSender fakeJavaMailSender = new FakeJavaMailSender(); - private JavaMailSender originalSender; private String domain; + private ExpiringCodeStore codeStore; @Before public void setUp() throws Exception { getWebApplicationContext().getBean(IdentityProviderBootstrap.class).afterPropertiesSet(); - adminToken = MockMvcUtils.utils().getClientCredentialsOAuthAccessToken(getMockMvc(), "admin", "adminsecret", "clients.read clients.write clients.secret scim.read scim.write", null); + adminToken = utils().getClientCredentialsOAuthAccessToken(getMockMvc(), "admin", "adminsecret", "clients.read clients.write clients.secret scim.read scim.write", null); clientId = generator.generate().toLowerCase(); clientSecret = generator.generate().toLowerCase(); authorities = "scim.read,scim.invite"; - createScimClient(this.getMockMvc(), adminToken, clientId, clientSecret, "oauth", "scim.read,scim.invite", Arrays.asList(new MockMvcUtils.GrantType[] {MockMvcUtils.GrantType.client_credentials, MockMvcUtils.GrantType.password}), authorities); - scimInviteToken = MockMvcUtils.utils().getClientCredentialsOAuthAccessToken(getMockMvc(), clientId, clientSecret, "scim.read scim.invite", null); + clientDetails = utils().createClient(this.getMockMvc(), adminToken, clientId, clientSecret, "oauth", "scim.read,scim.invite", Arrays.asList(new MockMvcUtils.GrantType[]{MockMvcUtils.GrantType.client_credentials, MockMvcUtils.GrantType.password}), authorities); + scimInviteToken = utils().getClientCredentialsOAuthAccessToken(getMockMvc(), clientId, clientSecret, "scim.read scim.invite", null); domain = generator.generate().toLowerCase()+".com"; IdentityProvider uaaProvider = getWebApplicationContext().getBean(IdentityProviderProvisioning.class).retrieveByOrigin(UAA, IdentityZone.getUaa().getId()); uaaProvider.getConfigValue(UaaIdentityProviderDefinition.class).setEmailDomain(Arrays.asList(domain)); getWebApplicationContext().getBean(IdentityProviderProvisioning.class).update(uaaProvider); - } - - @Before - public void setUpFakeMailServer() throws Exception { - originalSender = getWebApplicationContext().getBean("emailService", EmailService.class).getMailSender(); - getWebApplicationContext().getBean("emailService", EmailService.class).setMailSender(fakeJavaMailSender); - } - - @After - public void restoreMailServer() throws Exception { - getWebApplicationContext().getBean("emailService", EmailService.class).setMailSender(originalSender); + codeStore = getWebApplicationContext().getBean(ExpiringCodeStore.class); } @Test - public void testAcceptInvitationEmailWithOssBrand() throws Exception { - ((MockEnvironment) getWebApplicationContext().getEnvironment()).setProperty("login.brand", "oss"); + public void invite_User_With_Client_Credentials() throws Exception { + String email = "user1@example.com"; + String redirectUrl = "example.com"; + InvitationsResponse response = sendRequestWithTokenAndReturnResponse(scimInviteToken, null, clientId, redirectUrl, email); - getMockMvc().perform(get(getAcceptInvitationLink())) - .andExpect(content().string(containsString("Create your account"))) - .andExpect(content().string(not(containsString("Pivotal ID")))) - .andExpect(content().string(not(containsString("Create Pivotal ID")))) - .andExpect(content().string(containsString("Create account"))); + assertResponseAndCodeCorrect(new String[] {email}, redirectUrl, null, response, clientDetails); } @Test - public void testAcceptInvitationEmailWithPivotalBrand() throws Exception { - ((MockEnvironment) getWebApplicationContext().getEnvironment()).setProperty("login.brand", "pivotal"); + public void invite_Multiple_Users_With_Client_Credentials() throws Exception { + String[] emails = new String[] {"user1@"+domain, "user2@"+domain}; + String redirectUri = "example.com"; + InvitationsResponse response = sendRequestWithTokenAndReturnResponse(scimInviteToken, null, clientId, redirectUri, emails); - getMockMvc().perform(get(getAcceptInvitationLink())) - .andExpect(content().string(containsString("Create your Pivotal ID"))) - .andExpect(content().string(containsString("Pivotal products"))) - .andExpect(content().string(not(containsString("Create your account")))) - .andExpect(content().string(containsString("Create Pivotal ID"))) - .andExpect(content().string(not(containsString("Create account")))); + assertResponseAndCodeCorrect(emails, redirectUri, null, response, clientDetails); } @Test - public void testAcceptInvitationEmailWithinZone() throws Exception { - String subdomain = generator.generate(); - MockMvcUtils.utils().createOtherIdentityZone(subdomain, getMockMvc(), getWebApplicationContext()); - ((MockEnvironment) getWebApplicationContext().getEnvironment()).setProperty("login.brand", "pivotal"); - - getMockMvc().perform(get(getAcceptInvitationLink()) - .with(new SetServerNameRequestPostProcessor(subdomain + ".localhost"))) - .andExpect(content().string(containsString("Create your account"))) - .andExpect(content().string(not(containsString("Pivotal ID")))) - .andExpect(content().string(not(containsString("Create Pivotal ID")))) - .andExpect(content().string(containsString("Create account"))); - } + public void invite_User_With_User_Credentials() throws Exception { + String email = "user1@example.com"; + String redirectUri = "example.com"; + String userToken = utils().getScimInviteUserToken(getMockMvc(), clientId, clientSecret); + InvitationsResponse response = sendRequestWithTokenAndReturnResponse(userToken, null, clientId, redirectUri, email); - private String getAcceptInvitationLink() throws Exception { - String userToken = MockMvcUtils.utils().getScimInviteUserToken(getMockMvc(), clientId, clientSecret); - String email = generator.generate().toLowerCase() + "@"+domain; - sendRequestWithToken(userToken, null, clientId, "example.com", email); - Iterator receivedEmail = fakeJavaMailSender.getSentMessages().iterator(); - MimeMessageWrapper message = receivedEmail.next(); - MockMvcTestClient mockMvcTestClient = new MockMvcTestClient(getMockMvc()); - return mockMvcTestClient.extractLink(message.getContentString()); + assertResponseAndCodeCorrect(new String[] {email}, redirectUri, null, response, clientDetails); } @Test - public void test_Invitations_Accept_Get_Security() throws Exception { - getWebApplicationContext().getBean(JdbcTemplate.class).update("DELETE FROM expiring_code_store"); - SecurityContext marissaContext = MockMvcUtils.utils().getMarissaSecurityContext(getWebApplicationContext()); - String email = generator.generate()+"@"+domain; - - String userToken = MockMvcUtils.utils().getScimInviteUserToken(getMockMvc(), clientId, clientSecret); - sendRequestWithToken(userToken, null, clientId, "example.com", "user1@"+domain); - - String code = getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("SELECT code FROM expiring_code_store", String.class); - assertNotNull("Invite Code Must be Present", code); - - MockHttpServletRequestBuilder accept = get("/invitations/accept") - .param("code", code); - - getMockMvc().perform(accept) - .andExpect(status().isOk()) - .andExpect(content().string(containsString("

"))); - } + public void invite_User_Within_Zone() throws Exception { + String subdomain = generator.generate(); + MockMvcUtils.IdentityZoneCreationResult result = utils().createOtherIdentityZoneAndReturnResult(subdomain, getMockMvc(), getWebApplicationContext(), null); + String zonedClientId = "zonedClientId"; + String zonedClientSecret = "zonedClientSecret"; + BaseClientDetails zonedClientDetails = (BaseClientDetails)utils().createClient(this.getMockMvc(), result.getZoneAdminToken(), zonedClientId, zonedClientSecret, "oauth", "scim.read,scim.invite", Arrays.asList(new MockMvcUtils.GrantType[]{MockMvcUtils.GrantType.client_credentials, MockMvcUtils.GrantType.password}), authorities, null, result.getIdentityZone()); + zonedClientDetails.setClientSecret(zonedClientSecret); + String zonedScimInviteToken = utils().getClientCredentialsOAuthAccessToken(getMockMvc(), zonedClientDetails.getClientId(), zonedClientDetails.getClientSecret(), "scim.read scim.invite", subdomain); - @Test - public void testInviteUserWithClientCredentials() throws Exception { String email = "user1@example.com"; - sendRequestWithToken(scimInviteToken, null, clientId, "example.com", email); - assertEmailsSent(email); - } - - @Test - public void testInviteMultipleUsersWithClientCredentials() throws Exception { - String[] emails = new String[] {"user1@"+domain, "user2@"+domain}; - sendRequestWithToken(scimInviteToken, null, clientId, "example.com", emails); - assertEmailsSent(emails); - } + String redirectUrl = "example.com"; + InvitationsResponse response = sendRequestWithTokenAndReturnResponse(zonedScimInviteToken, subdomain, zonedClientDetails.getClientId(), redirectUrl, email); - @Test - public void testInviteUserWithUserCredentials() throws Exception { - String userToken = MockMvcUtils.utils().getScimInviteUserToken(getMockMvc(), clientId, clientSecret); - sendRequestWithToken(userToken, null, clientId, "example.com", "user1@example.com"); - assertEmailsSent("user1@example.com"); + assertResponseAndCodeCorrect(new String[] {email}, redirectUrl, subdomain, response, zonedClientDetails); } @Test - public void test_multiple_users_email_exists_with_one_origin() throws Exception { + public void multiple_Users_Email_Exists_With_One_Origin() throws Exception { String clientAdminToken = utils().getClientOAuthAccessToken(getMockMvc(), "admin", "adminsecret",""); String username1 = generator.generate(); String username2 = generator.generate(); @@ -183,20 +133,73 @@ public void test_multiple_users_email_exists_with_one_origin() throws Exception ScimUser user1 = new ScimUser(null, username1, "givenName", "familyName"); user1.setPrimaryEmail(email); user1.setOrigin(UAA); - user1 = utils().createUser(getMockMvc(), clientAdminToken, user1); + utils().createUser(getMockMvc(), clientAdminToken, user1); ScimUser user2 = new ScimUser(null, username2, "givenName", "familyName"); user2.setPrimaryEmail(email); user2.setOrigin(UAA); - user2 = utils().createUser(getMockMvc(), clientAdminToken, user2); + utils().createUser(getMockMvc(), clientAdminToken, user2); - String userToken = MockMvcUtils.utils().getScimInviteUserToken(getMockMvc(), clientId, clientSecret); + String userToken = utils().getScimInviteUserToken(getMockMvc(), clientId, clientSecret); InvitationsResponse response = sendRequestWithTokenAndReturnResponse(userToken, null, clientId, "example.com", email); assertEquals(0, response.getNewInvites().size()); assertEquals(1, response.getFailedInvites().size()); assertEquals("user.ambiguous", response.getFailedInvites().get(0).getErrorCode()); + } + + @Test + public void accept_Invitation_Email_With_Oss_Brand() throws Exception { + ((MockEnvironment) getWebApplicationContext().getEnvironment()).setProperty("login.brand", "oss"); + getMockMvc().perform(get(getAcceptInvitationLink())) + .andExpect(content().string(containsString("Create your account"))) + .andExpect(content().string(not(containsString("Pivotal ID")))) + .andExpect(content().string(not(containsString("Create Pivotal ID")))) + .andExpect(content().string(containsString("Create account"))); } + @Test + public void accept_Invitation_Email_With_Pivotal_Brand() throws Exception { + ((MockEnvironment) getWebApplicationContext().getEnvironment()).setProperty("login.brand", "pivotal"); + + getMockMvc().perform(get(getAcceptInvitationLink())) + .andExpect(content().string(containsString("Create your Pivotal ID"))) + .andExpect(content().string(containsString("Pivotal products"))) + .andExpect(content().string(not(containsString("Create your account")))) + .andExpect(content().string(containsString("Create Pivotal ID"))) + .andExpect(content().string(not(containsString("Create account")))); + } + + @Test + public void accept_Invitation_Email_Within_Zone() throws Exception { + String subdomain = generator.generate(); + utils().createOtherIdentityZone(subdomain, getMockMvc(), getWebApplicationContext()); + ((MockEnvironment) getWebApplicationContext().getEnvironment()).setProperty("login.brand", "pivotal"); + + getMockMvc().perform(get(getAcceptInvitationLink()) + .with(new SetServerNameRequestPostProcessor(subdomain + ".localhost"))) + .andExpect(content().string(containsString("Create your account"))) + .andExpect(content().string(not(containsString("Pivotal ID")))) + .andExpect(content().string(not(containsString("Create Pivotal ID")))) + .andExpect(content().string(containsString("Create account"))); + } + + @Test + public void invitations_Accept_Get_Security() throws Exception { + getWebApplicationContext().getBean(JdbcTemplate.class).update("DELETE FROM expiring_code_store"); + + String userToken = utils().getScimInviteUserToken(getMockMvc(), clientId, clientSecret); + sendRequestWithToken(userToken, null, clientId, "example.com", "user1@"+domain); + + String code = getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("SELECT code FROM expiring_code_store", String.class); + assertNotNull("Invite Code Must be Present", code); + + MockHttpServletRequestBuilder accept = get("/invitations/accept") + .param("code", code); + + getMockMvc().perform(accept) + .andExpect(status().isOk()) + .andExpect(content().string(containsString(""))); + } public static InvitationsResponse sendRequestWithTokenAndReturnResponse(String token, String subdomain, @@ -223,19 +226,45 @@ public static InvitationsResponse sendRequestWithTokenAndReturnResponse(String t .andReturn(); return JsonUtils.readValue(result.getResponse().getContentAsString(), InvitationsResponse.class); } + public static void sendRequestWithToken(String token, String subdomain, String clientId, String redirectUri, String...emails) throws Exception { InvitationsResponse response = sendRequestWithTokenAndReturnResponse(token, subdomain, clientId, redirectUri, emails); assertThat(response.getNewInvites().size(), is(emails.length)); assertThat(response.getFailedInvites().size(), is(0)); } - protected void assertEmailsSent(String...emails) throws MessagingException { - assertEquals(emails.length, fakeJavaMailSender.getSentMessages().size()); - for (int i=0; i < emails.length; i++) { - MimeMessageWrapper mimeMessageWrapper = fakeJavaMailSender.getSentMessages().get(i); - assertEquals(1, mimeMessageWrapper.getRecipients(Message.RecipientType.TO).size()); - assertEquals(emails[i], mimeMessageWrapper.getRecipients(Message.RecipientType.TO).get(0).toString()); + private void assertResponseAndCodeCorrect(String[] emails, String redirectUrl, String subdomain, InvitationsResponse response, ClientDetails clientDetails) { + for (int i = 0; i < emails.length; i++) { + assertThat(response.getNewInvites().size(), is(emails.length)); + assertThat(response.getNewInvites().get(i).getEmail(), is(emails[i])); + assertThat(response.getNewInvites().get(i).getOrigin(), is(Origin.UAA)); + assertThat(response.getNewInvites().get(i).getUserId(), is(notNullValue())); + assertThat(response.getNewInvites().get(i).getErrorCode(), is(nullValue())); + assertThat(response.getNewInvites().get(i).getErrorMessage(), is(nullValue())); + if (StringUtils.hasText(subdomain)) { + assertThat(response.getNewInvites().get(i).getInviteLink().toString(), startsWith("http://" + subdomain + ".localhost/invitations/accept")); + } else { + assertThat(response.getNewInvites().get(i).getInviteLink().toString(), startsWith("http://localhost/invitations/accept")); + } + + String query = response.getNewInvites().get(i).getInviteLink().getQuery(); + assertThat(query, startsWith("code=")); + String code = query.split("=")[1]; + ExpiringCode expiringCode = codeStore.retrieveCode(code); + assertThat(expiringCode.getExpiresAt().getTime(), is(greaterThan(System.currentTimeMillis()))); + Map data = JsonUtils.readValue(expiringCode.getData(), new TypeReference>() {}); + assertThat(data.get(InvitationConstants.USER_ID), is(notNullValue())); + assertThat(data.get(InvitationConstants.EMAIL), is(emails[i])); + assertThat(data.get(ORIGIN), is(Origin.UAA)); + assertThat(data.get(CLIENT_ID), is(clientDetails.getClientId())); + assertThat(data.get(REDIRECT_URI), is(redirectUrl)); } } + private String getAcceptInvitationLink() throws Exception { + String userToken = utils().getScimInviteUserToken(getMockMvc(), clientId, clientSecret); + String email = generator.generate().toLowerCase() + "@"+domain; + InvitationsResponse response = sendRequestWithTokenAndReturnResponse(userToken, null, clientId, "example.com", email); + return response.getNewInvites().get(0).getInviteLink().toString(); + } } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java index f56b9902fd1..e6f396bb18f 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/InvitationsServiceMockMvcTests.java @@ -16,27 +16,20 @@ import org.cloudfoundry.identity.uaa.AbstractIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.authentication.Origin; -import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; -import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; -import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.cloudfoundry.identity.uaa.invitations.InvitationsEndpointMockMvcTests; +import org.cloudfoundry.identity.uaa.invitations.InvitationsResponse; import org.cloudfoundry.identity.uaa.ldap.LdapIdentityProviderDefinition; -import org.cloudfoundry.identity.uaa.login.saml.LoginSamlAuthenticationProvider; import org.cloudfoundry.identity.uaa.login.saml.SamlIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.login.util.FakeJavaMailSender; -import org.cloudfoundry.identity.uaa.login.util.FakeJavaMailSender.MimeMessageWrapper; import org.cloudfoundry.identity.uaa.mock.InjectedMockContextTest; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.IdentityZoneCreationResult; import org.cloudfoundry.identity.uaa.scim.ScimGroup; import org.cloudfoundry.identity.uaa.scim.ScimGroupMember; import org.cloudfoundry.identity.uaa.scim.ScimUser; -import org.cloudfoundry.identity.uaa.user.UaaAuthority; -import org.cloudfoundry.identity.uaa.user.UaaUserDatabase; import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.cloudfoundry.identity.uaa.zone.IdentityProvider; -import org.cloudfoundry.identity.uaa.zone.IdentityProviderProvisioning; -import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; +import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.UaaIdentityProviderDefinition; import org.junit.After; import org.junit.Before; @@ -45,27 +38,20 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mock.web.MockHttpSession; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.common.util.OAuth2Utils; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; import org.springframework.security.oauth2.provider.ClientDetails; import org.springframework.security.oauth2.provider.client.BaseClientDetails; -import org.springframework.security.providers.ExpiringUsernameAuthenticationToken; -import org.springframework.security.saml.SAMLAuthenticationToken; -import org.springframework.security.saml.SAMLConstants; -import org.springframework.security.saml.context.SAMLMessageContext; -import org.springframework.security.saml.metadata.ExtendedMetadata; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import java.net.URL; import java.util.Arrays; import java.util.regex.Matcher; import java.util.regex.Pattern; -import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.createScimClient; import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.utils; import static org.cloudfoundry.identity.uaa.scim.ScimGroupMember.Role.MEMBER; import static org.cloudfoundry.identity.uaa.scim.ScimGroupMember.Type.USER; @@ -76,8 +62,6 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -92,7 +76,6 @@ public class InvitationsServiceMockMvcTests extends InjectedMockContextTest { private JavaMailSender originalSender; private FakeJavaMailSender fakeJavaMailSender = new FakeJavaMailSender(); private MockMvcUtils utils = MockMvcUtils.utils(); - private String scimInviteToken; private RandomValueStringGenerator generator = new RandomValueStringGenerator(); private String clientId; private String clientSecret; @@ -103,30 +86,20 @@ public class InvitationsServiceMockMvcTests extends InjectedMockContextTest { public static class ZoneScimInviteData { private final IdentityZoneCreationResult zone; private final String adminToken; - private final ScimGroup scimInviteGroup; - private final ScimUser scimInviteUser; private final ClientDetails scimInviteClient; public ZoneScimInviteData(String adminToken, IdentityZoneCreationResult zone, - ScimGroup scimInviteGroup, - ClientDetails scimInviteClient, - ScimUser scimInviteUser) { + ClientDetails scimInviteClient) { this.adminToken = adminToken; this.zone = zone; - this.scimInviteGroup = scimInviteGroup; this.scimInviteClient = scimInviteClient; - this.scimInviteUser = scimInviteUser; } public ClientDetails getScimInviteClient() { return scimInviteClient; } - public ScimGroup getScimInviteGroup() { - return scimInviteGroup; - } - public IdentityZoneCreationResult getZone() { return zone; } @@ -134,10 +107,6 @@ public IdentityZoneCreationResult getZone() { public String getAdminToken() { return adminToken; } - - public ScimUser getScimInviteUser() { - return scimInviteUser; - } } public ZoneScimInviteData createZoneForInvites() throws Exception { @@ -160,18 +129,13 @@ public ZoneScimInviteData createZoneForInvites() throws Exception { user = utils.createUserInZone(getMockMvc(), adminToken, user, zone.getIdentityZone().getSubdomain()); user.setPassword("password"); - ScimGroupMember member = new ScimGroupMember(user.getId(), USER, Arrays.asList(ScimGroupMember.Role.READER)); - ScimGroup group = new ScimGroup("scim.invite"); group.setMembers(Arrays.asList(new ScimGroupMember(user.getId(), USER, Arrays.asList(MEMBER)))); - group = utils().createGroup(getMockMvc(), zone.getZoneAdminToken(), group, zone.getIdentityZone().getId()); return new ZoneScimInviteData( adminToken, zone, - group, - appClient, - user + appClient ); } @@ -181,8 +145,7 @@ public void setUp() throws Exception { clientId = generator.generate().toLowerCase(); clientSecret = generator.generate().toLowerCase(); authorities = "scim.read,scim.invite"; - createScimClient(this.getMockMvc(), adminToken, clientId, clientSecret, "oauth", "scim.read,scim.invite", Arrays.asList(new MockMvcUtils.GrantType[]{MockMvcUtils.GrantType.client_credentials, MockMvcUtils.GrantType.password}), authorities, REDIRECT_URI); - scimInviteToken = MockMvcUtils.utils().getClientCredentialsOAuthAccessToken(getMockMvc(), clientId, clientSecret, "scim.read scim.invite", null); + MockMvcUtils.utils().createClient(this.getMockMvc(), adminToken, clientId, clientSecret, "oauth", "scim.read,scim.invite", Arrays.asList(new MockMvcUtils.GrantType[]{MockMvcUtils.GrantType.client_credentials, MockMvcUtils.GrantType.password}), authorities, REDIRECT_URI, IdentityZone.getUaa()); userInviteToken = MockMvcUtils.utils().getScimInviteUserToken(getMockMvc(), clientId, clientSecret); getWebApplicationContext().getBean(JdbcTemplate.class).update("delete from expiring_code_store"); } @@ -219,10 +182,10 @@ protected T queryUserForField(String email, String field, Class type) { @Test public void test_authorize_with_invitation_login() throws Exception { String email = new RandomValueStringGenerator().generate().toLowerCase()+"@test.org"; - MimeMessageWrapper message = inviteUser(email, userInviteToken, null, clientId, Origin.UAA); + URL inviteLink = inviteUser(email, userInviteToken, null, clientId, Origin.UAA); assertEquals(Origin.UAA, getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select origin from users where username=?", new Object[]{email}, String.class)); - String code = extractInvitationCode(message.getContentString()); + String code = extractInvitationCode(inviteLink.toString()); MvcResult result = getMockMvc().perform( get("/invitations/accept") .param("code", code) @@ -262,10 +225,10 @@ public void test_authorize_with_invitation_login() throws Exception { @Test public void accept_invitation_should_not_log_you_in() throws Exception { String email = new RandomValueStringGenerator().generate().toLowerCase()+"@test.org"; - MimeMessageWrapper message = inviteUser(email, userInviteToken, null, clientId, Origin.UAA); + URL inviteLink = inviteUser(email, userInviteToken, null, clientId, Origin.UAA); assertEquals(Origin.UAA, getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("select origin from users where username=?", new Object[]{email}, String.class)); - String code = extractInvitationCode(message.getContentString()); + String code = extractInvitationCode(inviteLink.toString()); MvcResult result = getMockMvc().perform(get("/invitations/accept") .param("code", code) .accept(MediaType.TEXT_HTML) @@ -288,13 +251,13 @@ public void accept_invitation_should_not_log_you_in() throws Exception { @Test public void accept_invitation_for_verified_user_sends_redirect() throws Exception { String email = new RandomValueStringGenerator().generate().toLowerCase() + "@test.org"; - MimeMessageWrapper message = inviteUser(email, userInviteToken, null, clientId, Origin.UAA); + URL inviteLink = inviteUser(email, userInviteToken, null, clientId, Origin.UAA); getWebApplicationContext().getBean(JdbcTemplate.class).update("UPDATE users SET verified=true WHERE email=?",email); assertTrue("User should not be verified", queryUserForField(email, "verified", Boolean.class)); assertEquals(Origin.UAA, queryUserForField(email, Origin.ORIGIN, String.class)); - String code = extractInvitationCode(message.getContentString()); + String code = extractInvitationCode(inviteLink.toString()); getMockMvc().perform( get("/invitations/accept") .param("code", code) @@ -309,12 +272,12 @@ public void accept_invitation_for_verified_user_sends_redirect() throws Exceptio @Test public void accept_invitation_sets_your_password() throws Exception { String email = new RandomValueStringGenerator().generate().toLowerCase()+"@test.org"; - MimeMessageWrapper message = inviteUser(email, userInviteToken, null, clientId, Origin.UAA); + URL inviteLink = inviteUser(email, userInviteToken, null, clientId, Origin.UAA); assertFalse("User should not be verified", queryUserForField(email, "verified", Boolean.class)); assertEquals(Origin.UAA, queryUserForField(email, Origin.ORIGIN, String.class)); - String code = extractInvitationCode(message.getContentString()); + String code = extractInvitationCode(inviteLink.toString()); MvcResult result = getMockMvc().perform(get("/invitations/accept") .param("code", code) .accept(MediaType.TEXT_HTML) @@ -358,8 +321,8 @@ public void invite_ldap_users_verifies_and_redirects() throws Exception { definition.setEmailDomain(Arrays.asList(domain)); IdentityProvider provider = createIdentityProvider(zone.getZone(), Origin.LDAP, definition); String email = new RandomValueStringGenerator().generate().toLowerCase()+"@"+domain; - MimeMessageWrapper message = inviteUser(email, zone.getAdminToken(), zone.getZone().getIdentityZone().getSubdomain(), zone.getScimInviteClient().getClientId(), provider.getOriginKey()); - String code = extractInvitationCode(message.getContentString()); + URL inviteLink = inviteUser(email, zone.getAdminToken(), zone.getZone().getIdentityZone().getSubdomain(), zone.getScimInviteClient().getClientId(), provider.getOriginKey()); + String code = extractInvitationCode(inviteLink.toString()); assertFalse("User should not be verified", queryUserForField(email, "verified", Boolean.class)); assertEquals(Origin.LDAP, queryUserForField(email, Origin.ORIGIN, String.class)); @@ -388,8 +351,8 @@ public void invite_saml_user_will_redirect_upon_accept() throws Exception { IdentityProvider provider = createIdentityProvider(zone.getZone(), originKey, definition); String email = new RandomValueStringGenerator().generate().toLowerCase()+"@"+domain; - MimeMessageWrapper message = inviteUser(email,zone.getAdminToken(), zone.getZone().getIdentityZone().getSubdomain(), zone.getScimInviteClient().getClientId(), provider.getOriginKey()); - String code = extractInvitationCode(message.getContentString()); + URL inviteLink = inviteUser(email,zone.getAdminToken(), zone.getZone().getIdentityZone().getSubdomain(), zone.getScimInviteClient().getClientId(), provider.getOriginKey()); + String code = extractInvitationCode(inviteLink.toString()); assertFalse("User should not be verified", queryUserForField(email, "verified", Boolean.class)); assertEquals(originKey, queryUserForField(email, Origin.ORIGIN, String.class)); @@ -445,52 +408,16 @@ protected SamlIdentityProviderDefinition getSamlIdentityProviderDefinition(Ident ); } - protected void mockSamlAuthentication(IdentityZoneCreationResult zone, String originKey, String entityID, final String invitedEmail, final String authenticatedEmail) { - try { - //perform SAML Login - //setup the existing token - IdentityZoneHolder.set(zone.getIdentityZone()); - UaaPrincipal invited = new UaaPrincipal(getWebApplicationContext().getBean(UaaUserDatabase.class).retrieveUserByName(invitedEmail, originKey)); - UaaAuthentication invitedAuthentication = new UaaAuthentication(invited, Arrays.asList(UaaAuthority.UAA_INVITED), mock(UaaAuthenticationDetails.class)); - - ExtendedMetadata metadata = mock(ExtendedMetadata.class); - when(metadata.getAlias()).thenReturn(originKey); - SAMLMessageContext contxt = mock(SAMLMessageContext.class); - when(contxt.getPeerExtendedMetadata()).thenReturn(metadata); - when(contxt.getCommunicationProfileId()).thenReturn(SAMLConstants.SAML2_WEBSSO_PROFILE_URI); - SAMLAuthenticationToken token = new SAMLAuthenticationToken(contxt); - - SecurityContextHolder.getContext().setAuthentication(invitedAuthentication); - LoginSamlAuthenticationProvider authprovider = new LoginSamlAuthenticationProvider() { - @Override - protected ExpiringUsernameAuthenticationToken getExpiringUsernameAuthenticationToken(Authentication authentication) { - return new ExpiringUsernameAuthenticationToken(authenticatedEmail, ""); - } - }; - authprovider.setUserDatabase(getWebApplicationContext().getBean(UaaUserDatabase.class)); - authprovider.setIdentityProviderProvisioning(getWebApplicationContext().getBean(IdentityProviderProvisioning.class)); - authprovider.setApplicationEventPublisher(getWebApplicationContext().getBean(LoginSamlAuthenticationProvider.class).getApplicationEventPublisher()); - - authprovider.authenticate(token); - } finally { - IdentityZoneHolder.clear(); - SecurityContextHolder.clearContext(); - } - } - - public MimeMessageWrapper inviteUser(String email, String userInviteToken, String subdomain, String clientId, String expectedOrigin) throws Exception { - InvitationsEndpointMockMvcTests.sendRequestWithToken(userInviteToken, subdomain, clientId, REDIRECT_URI, email); + public URL inviteUser(String email, String userInviteToken, String subdomain, String clientId, String expectedOrigin) throws Exception { + InvitationsResponse response = InvitationsEndpointMockMvcTests.sendRequestWithTokenAndReturnResponse(userInviteToken, subdomain, clientId, REDIRECT_URI, email); + assertEquals(1, response.getNewInvites().size()); assertEquals(expectedOrigin, getWebApplicationContext().getBean(JdbcTemplate.class).queryForObject("SELECT origin FROM users WHERE username='" + email + "'", String.class)); - assertEquals(1, fakeJavaMailSender.getSentMessages().size()); - MimeMessageWrapper message = fakeJavaMailSender.getSentMessages().get(0); - fakeJavaMailSender.clearMessage(); - return message; + return response.getNewInvites().get(0).getInviteLink(); } - public String extractInvitationCode(String email) throws Exception { - System.out.println(email); - Pattern p = Pattern.compile("accept\\?code\\=(.*?)\\\"\\>Accept Invite"); - Matcher m = p.matcher(email); + private String extractInvitationCode(String inviteLink) throws Exception { + Pattern p = Pattern.compile("accept\\?code=(.*)"); + Matcher m = p.matcher(inviteLink); if (m.find()) { return m.group(1); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java index 479c1cadf6f..95346830f18 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java @@ -21,15 +21,12 @@ import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; -import org.cloudfoundry.identity.uaa.invitations.InvitationsEndpointMockMvcTests; import org.cloudfoundry.identity.uaa.oauth.client.ClientDetailsModification; import org.cloudfoundry.identity.uaa.rest.SearchResults; import org.cloudfoundry.identity.uaa.scim.ScimGroup; import org.cloudfoundry.identity.uaa.scim.ScimGroupMember; -import org.cloudfoundry.identity.uaa.scim.ScimGroupMembershipManager; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; -import org.cloudfoundry.identity.uaa.scim.exception.MemberAlreadyExistsException; import org.cloudfoundry.identity.uaa.scim.jdbc.JdbcScimUserProvisioning; import org.cloudfoundry.identity.uaa.test.TestApplicationEventListener; import org.cloudfoundry.identity.uaa.test.TestClient; @@ -381,6 +378,15 @@ public BaseClientDetails createClient(MockMvc mockMvc, String accessToken, BaseC .andReturn().getResponse().getContentAsString(), BaseClientDetails.class); } + public ClientDetails createClient(MockMvc mockMvc, String adminAccessToken, String id, String secret, String resourceIds, String scopes, List grantTypes, String authorities) throws Exception { + return createClient(mockMvc, adminAccessToken, id, secret, resourceIds, scopes, grantTypes, authorities, null, IdentityZone.getUaa()); + } + public ClientDetails createClient(MockMvc mockMvc, String adminAccessToken, String id, String secret, String resourceIds, String scopes, List grantTypes, String authorities, String redirectUris, IdentityZone zone) throws Exception { + ClientDetailsModification client = new ClientDetailsModification(id, resourceIds, scopes, commaDelineatedGrantTypes(grantTypes), authorities, redirectUris); + client.setClientSecret(secret); + return createClient(mockMvc,adminAccessToken, client, zone); + } + public BaseClientDetails updateClient(MockMvc mockMvc, String accessToken, BaseClientDetails clientDetails, IdentityZone zone) throws Exception { MockHttpServletRequestBuilder updateClientPut = @@ -633,15 +639,6 @@ public static CookieCsrfPostProcessor cookieCsrf() { } } - public static ClientDetails createScimClient(MockMvc mockMvc, String adminAccessToken, String id, String secret, String resourceIds, String scopes, List grantTypes, String authorities) throws Exception { - return createScimClient(mockMvc, adminAccessToken, id, secret, resourceIds, scopes, grantTypes, authorities, null); - } - public static ClientDetails createScimClient(MockMvc mockMvc, String adminAccessToken, String id, String secret, String resourceIds, String scopes, List grantTypes, String authorities, String redirectUris) throws Exception { - ClientDetailsModification client = new ClientDetailsModification(id, resourceIds, scopes, commaDelineatedGrantTypes(grantTypes), authorities, redirectUris); - client.setClientSecret(secret); - return utils().createClient(mockMvc,adminAccessToken, client); - } - public enum GrantType { password, client_credentials, authorization_code, implicit } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimGroupEndpointsMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimGroupEndpointsMockMvcTests.java index 099b1e395ae..6504e8fbcec 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimGroupEndpointsMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimGroupEndpointsMockMvcTests.java @@ -60,7 +60,7 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; -import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.createScimClient; +import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.utils; import static org.junit.Assert.assertTrue; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; @@ -100,7 +100,7 @@ public void setUp() throws Exception { String clientId = generator.generate().toLowerCase(); String clientSecret = generator.generate().toLowerCase(); String authorities = "scim.read,scim.write,password.write,oauth.approvals,scim.create"; - createScimClient(this.getMockMvc(), adminToken, clientId, clientSecret, "oauth", "foo,bar", Collections.singletonList(MockMvcUtils.GrantType.client_credentials), authorities); + utils().createClient(this.getMockMvc(), adminToken, clientId, clientSecret, "oauth", "foo,bar", Collections.singletonList(MockMvcUtils.GrantType.client_credentials), authorities); scimReadToken = testClient.getClientCredentialsOAuthAccessToken(clientId, clientSecret,"scim.read password.write"); scimWriteToken = testClient.getClientCredentialsOAuthAccessToken(clientId, clientSecret,"scim.write password.write"); @@ -114,7 +114,7 @@ public void setUp() throws Exception { @Test public void testIdentityClientManagesZoneAdmins() throws Exception { - IdentityZone zone = MockMvcUtils.utils().createZoneUsingWebRequest(getMockMvc(), identityClientToken); + IdentityZone zone = utils().createZoneUsingWebRequest(getMockMvc(), identityClientToken); ScimGroupMember member = new ScimGroupMember(scimUser.getId()); ScimGroup group = new ScimGroup(null, "zones."+zone.getId()+".admin", zone.getId()); group.setMembers(Arrays.asList(member)); @@ -179,7 +179,7 @@ public void testIdentityClientManagesZoneAdmins() throws Exception { @Test public void testLimitedScopesWithoutMember() throws Exception { - IdentityZone zone = MockMvcUtils.utils().createZoneUsingWebRequest(getMockMvc(), identityClientToken); + IdentityZone zone = utils().createZoneUsingWebRequest(getMockMvc(), identityClientToken); ScimGroup group = new ScimGroup("zones." + zone.getId() + ".admin"); MockHttpServletRequestBuilder post = post("/Groups/zones") @@ -209,7 +209,7 @@ public void add_and_Delete_Members_toZoneManagementGroups_withVariousGroupNames( private ResultActions[] addAndDeleteMemberstoZoneManagementGroups(String displayName, HttpStatus create, HttpStatus delete) throws Exception { ResultActions[] result = new ResultActions[2]; - IdentityZone zone = MockMvcUtils.utils().createZoneUsingWebRequest(getMockMvc(), identityClientToken); + IdentityZone zone = utils().createZoneUsingWebRequest(getMockMvc(), identityClientToken); ScimGroupMember member = new ScimGroupMember(scimUser.getId()); ScimGroup group = new ScimGroup(String.format(displayName, zone.getId())); group.setMembers(Arrays.asList(member)); @@ -245,7 +245,7 @@ private ResultActions createZoneScope(ScimGroup group) throws Exception { @Test public void testGroupOperations_as_Zone_Admin() throws Exception { String subdomain = generator.generate(); - MockMvcUtils.IdentityZoneCreationResult result = MockMvcUtils.utils().createOtherIdentityZoneAndReturnResult(subdomain, getMockMvc(), getWebApplicationContext(), null); + MockMvcUtils.IdentityZoneCreationResult result = utils().createOtherIdentityZoneAndReturnResult(subdomain, getMockMvc(), getWebApplicationContext(), null); String zoneAdminToken = result.getZoneAdminToken(); IdentityZone zone = result.getIdentityZone(); @@ -353,7 +353,7 @@ public void testGetGroups() throws Exception { public void testGetGroups_Using_ZoneAdmin_Token() throws Exception { String subdomain = new RandomValueStringGenerator(8).generate(); BaseClientDetails bootstrapClient = null; - MockMvcUtils.IdentityZoneCreationResult result = MockMvcUtils.utils().createOtherIdentityZoneAndReturnResult( + MockMvcUtils.IdentityZoneCreationResult result = utils().createOtherIdentityZoneAndReturnResult( subdomain, getMockMvc(), getWebApplicationContext(), bootstrapClient ); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsMockMvcTests.java index f9a46a8cc4d..5817c3d29dc 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsMockMvcTests.java @@ -31,6 +31,7 @@ import java.util.Collections; +import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.utils; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; @@ -44,7 +45,7 @@ public class ScimUserEndpointsMockMvcTests extends InjectedMockContextTest { private String scimCreateToken; private RandomValueStringGenerator generator = new RandomValueStringGenerator(); private TestClient testClient; - private MockMvcUtils mockMvcUtils = MockMvcUtils.utils(); + private MockMvcUtils mockMvcUtils = utils(); @Before public void setUp() throws Exception { @@ -54,7 +55,7 @@ public void setUp() throws Exception { String clientId = generator.generate().toLowerCase(); String clientSecret = generator.generate().toLowerCase(); String authorities = "scim.read,scim.write,password.write,oauth.approvals,scim.create"; - MockMvcUtils.createScimClient(this.getMockMvc(), adminToken, clientId, clientSecret, "oauth", "foo,bar", Collections.singletonList(MockMvcUtils.GrantType.client_credentials), authorities); + utils().createClient(this.getMockMvc(), adminToken, clientId, clientSecret, "oauth", "foo,bar", Collections.singletonList(MockMvcUtils.GrantType.client_credentials), authorities); scimReadWriteToken = testClient.getClientCredentialsOAuthAccessToken(clientId, clientSecret,"scim.read scim.write password.write"); scimCreateToken = testClient.getClientCredentialsOAuthAccessToken(clientId, clientSecret,"scim.create"); } @@ -143,7 +144,7 @@ public void testCreateUserInZoneUsingAdminClient() throws Exception { @Test public void testCreateUserInZoneUsingZoneAdminUser() throws Exception { String subdomain = generator.generate(); - MockMvcUtils.IdentityZoneCreationResult result = MockMvcUtils.utils().createOtherIdentityZoneAndReturnResult(subdomain, getMockMvc(), getWebApplicationContext(), null); + MockMvcUtils.IdentityZoneCreationResult result = utils().createOtherIdentityZoneAndReturnResult(subdomain, getMockMvc(), getWebApplicationContext(), null); String zoneAdminToken = result.getZoneAdminToken(); createUser(getScimUser(), zoneAdminToken, IdentityZone.getUaa().getSubdomain(), result.getIdentityZone().getId()); } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserLookupMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserLookupMockMvcTests.java index d9326a4fa5a..4befb2ebd37 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserLookupMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserLookupMockMvcTests.java @@ -32,7 +32,7 @@ import java.util.List; import java.util.Map; -import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.createScimClient; +import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.utils; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; @@ -67,12 +67,12 @@ public void setUp() throws Exception { user = new ScimUser(null, new RandomValueStringGenerator().generate()+"@test.org", "PasswordResetUserFirst", "PasswordResetUserLast"); user.setPrimaryEmail(user.getUserName()); user.setPassword("secr3T"); - user = MockMvcUtils.utils().createUser(getMockMvc(), adminToken, user); + user = utils().createUser(getMockMvc(), adminToken, user); originalEnabled = getWebApplicationContext().getBean(UserIdConversionEndpoints.class).isEnabled(); getWebApplicationContext().getBean(UserIdConversionEndpoints.class).setEnabled(true); String scopes = "scim.userids,scim.me"; - createScimClient(this.getMockMvc(), adminToken, clientId, clientSecret, "scim", scopes, Arrays.asList(new MockMvcUtils.GrantType[] {MockMvcUtils.GrantType.client_credentials, MockMvcUtils.GrantType.password}), "uaa.none"); + utils().createClient(this.getMockMvc(), adminToken, clientId, clientSecret, "scim", scopes, Arrays.asList(new MockMvcUtils.GrantType[]{MockMvcUtils.GrantType.client_credentials, MockMvcUtils.GrantType.password}), "uaa.none"); scimLookupIdUserToken = testClient.getUserOAuthAccessToken(clientId, clientSecret, user.getUserName(), "secr3T", "scim.userids"); if (testUsers==null) { testUsers = createUsers(adminToken, testUserCount); @@ -338,13 +338,13 @@ private ScimUser createInactiveIdp(String originKey) throws Exception { String tokenToCreateIdp = testClient.getClientCredentialsOAuthAccessToken("login", "loginsecret", "idps.write"); IdentityProvider inactiveIdentityProvider = MultitenancyFixture.identityProvider(originKey, "uaa"); inactiveIdentityProvider.setActive(false); - MockMvcUtils.utils().createIdpUsingWebRequest(getMockMvc(), null, tokenToCreateIdp, inactiveIdentityProvider, status().isCreated()); + utils().createIdpUsingWebRequest(getMockMvc(), null, tokenToCreateIdp, inactiveIdentityProvider, status().isCreated()); ScimUser scimUser = new ScimUser(null, new RandomValueStringGenerator().generate()+"@test.org", "test", "test"); scimUser.setPrimaryEmail(scimUser.getUserName()); scimUser.setPassword("secr3T"); scimUser.setOrigin(originKey); - scimUser = MockMvcUtils.utils().createUserInZone(getMockMvc(), adminToken, scimUser, ""); + scimUser = utils().createUserInZone(getMockMvc(), adminToken, scimUser, ""); return scimUser; } } From 0f7f1f41189b8187a7c3ae98b548b8dc21149bd5 Mon Sep 17 00:00:00 2001 From: Jonathan Lo Date: Mon, 5 Oct 2015 10:10:24 -0700 Subject: [PATCH 037/103] Update copyright footer from Pivotal to CloudFoundry [#104675210] https://www.pivotaltracker.com/story/show/104675210 --- login/src/main/resources/templates/web/layouts/main.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/login/src/main/resources/templates/web/layouts/main.html b/login/src/main/resources/templates/web/layouts/main.html index 2ab64b3b4e2..481e09222d5 100644 --- a/login/src/main/resources/templates/web/layouts/main.html +++ b/login/src/main/resources/templates/web/layouts/main.html @@ -26,8 +26,8 @@ From 2fa88011099598404511bad55660c4269aca9c77 Mon Sep 17 00:00:00 2001 From: Jonathan Lo Date: Mon, 5 Oct 2015 16:40:40 -0700 Subject: [PATCH 038/103] Doc change: moved disableInternalUserManagement into the config section from request and response - Added LockoutPolicy attributes as well - Add an emailDomain example in an example LDAP provider in uaa.yml [finishes #104775858] https://www.pivotaltracker.com/story/show/104775858 --- docs/UAA-APIs.rst | 26 +++++++++++++++----------- uaa/src/main/resources/uaa.yml | 2 ++ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/docs/UAA-APIs.rst b/docs/UAA-APIs.rst index e3825098d2c..c2d58297a03 100644 --- a/docs/UAA-APIs.rst +++ b/docs/UAA-APIs.rst @@ -1038,7 +1038,6 @@ Request body *example* :: "version":0, "created":1426260091149, "active":true, - "disableInternalUserManagement":false, "identityZoneId":"testzone1" } @@ -1056,7 +1055,6 @@ Response body *example* :: "version":0, "created":1426260091149, "active":true, - "disableInternalUserManagement":false, "identityZoneId":"testzone1", "last_modified":1426260091149 } @@ -1080,20 +1078,24 @@ Fields *Available Fields* :: originKey String Required Must be either an alias for a SAML provider or the value "ldap" for an LDAP provider. If the type is "internal", the originKey is "uaa" config String Required IDP Configuration in JSON format, see below active boolean Optional When set to true, this provider is active. When a provider is deleted this value is set to false - disableInternalUserManagement boolean Optional When set to true, this provider disables user management identityZoneId String Auto Set to the zone that this provider will be active in. Determined either by the Host header or the zone switch header created epoch timestamp Auto UAA sets the creation date last_modified epoch timestamp Auto UAA sets the modification date UAA Provider Configuration (provided in JSON format as part of the ``config`` field on the Identity Provider - See class org.cloudfoundry.identity.uaa.zone.UaaIdentityProviderDefinition - ====================== =============== ======== ================================================================================================================================================================================================= - minLength int Required Minimum number of characters for a user provided password, 0+ - maxLength int Required Maximum number of characters for a user provided password, 1+ - requireUpperCaseCharacter int Required Minimum number of upper case characters for a user provided password, 0+ - requireLowerCaseCharacter int Required Minimum number of lower case characters for a user provided password, 0+ - requireDigit int Required Minimum number of numbers for a user provided password, 0+ - requireSpecialCharacter int Required Minimum number of special characters for a user provided password, 0+ Valid-List: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ - expirePasswordInMonths int Required Password expiration in months 0+ (0 means expiration is disabled) + ====================== =============== ======== ================================================================================================================================================================================================= + minLength int Required Minimum number of characters for a user provided password, 0+ + maxLength int Required Maximum number of characters for a user provided password, 1+ + requireUpperCaseCharacter int Required Minimum number of upper case characters for a user provided password, 0+ + requireLowerCaseCharacter int Required Minimum number of lower case characters for a user provided password, 0+ + requireDigit int Required Minimum number of numbers for a user provided password, 0+ + requireSpecialCharacter int Required Minimum number of special characters for a user provided password, 0+ Valid-List: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ + expirePasswordInMonths int Required Password expiration in months 0+ (0 means expiration is disabled) + lockoutPeriodSeconds int Required Amount of time in seconds to lockout login attempts after ``lockoutAfterFailures`` attempts reached, 0+ + lockoutAfterFailures int Required Number of login attempts allowed within ``countFailuresWithin`` seconds, 0+ + countFailuresWithin int Required Amount of time in seconds for which past login failures are counted, starting from the current time, 0+ + emailDomain List Optional List of email domains associated with the UAA provider. If null and no domains are explicitly matched with any other providers, the UAA acts as a catch-all, wherein the email will be associated with the UAA provider. Wildcards supported. + disableInternalUserManagement boolean Optional When set to true, user management is disabled for this provider, defaults to false SAML Provider Configuration (provided in JSON format as part of the ``config`` field on the Identity Provider - See class org.cloudfoundry.identity.uaa.login.saml.SamlIdentityProviderDefinition ====================== =============== ======== ================================================================================================================================================================================================= @@ -1106,6 +1108,7 @@ Fields *Available Fields* :: showSamlLink boolean Optional Should the SAML login link be displayed on the login page, defaults to false linkText String Optional Required if the ``showSamlLink`` is set to true. iconUrl String Optional Reserved for future use + emailDomain List Optional List of email domains associated with the SAML provider for the purpose of associating users to the correct origin upon invitation. If null or empty list, no invitations are accepted. Wildcards supported. LDAP Provider Configuration (provided in JSON format as part of the ``config`` field on the Identity Provider - See class org.cloudfoundry.identity.uaa.ldap.LdapIdentityProviderDefinition ====================== =============== ======== ================================================================================================================================================================================================= @@ -1125,6 +1128,7 @@ Fields *Available Fields* :: groupSearchSubTree boolean Required Should the sub tree be searched for user groups groupMaxSearchDepth int Required When searching for nested groups (groups within groups) skipSSLVerification boolean Optional Set to true if you wish to skip SSL certificate verification + emailDomain List Optional List of email domains associated with the LDAP provider for the purpose of associating users to the correct origin upon invitation. If null or empty list, no invitations are accepted. Wildcards supported. Curl Example POST (Creating a SAML provider):: diff --git a/uaa/src/main/resources/uaa.yml b/uaa/src/main/resources/uaa.yml index caa97b32728..bb1f42840de 100755 --- a/uaa/src/main/resources/uaa.yml +++ b/uaa/src/main/resources/uaa.yml @@ -89,6 +89,8 @@ # lQ23NhTaljASF0g8AZ7SZEItU8JFYqf/KnNJ7FPwo4LbMbr7Zg6BRKBvnQ== # -----END CERTIFICATE-----' # sslCertificateAlias: ldaps +# emailDomain: +# - example.com #ldap: # profile: From 9c9733bae49702d8f0bed40ad083d646eb435d4a Mon Sep 17 00:00:00 2001 From: Jonathan Lo Date: Mon, 5 Oct 2015 17:01:33 -0700 Subject: [PATCH 039/103] Missed a field in the /identity-providers GET response body [finishes #104775858] https://www.pivotaltracker.com/story/show/104775858 --- docs/UAA-APIs.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/UAA-APIs.rst b/docs/UAA-APIs.rst index c2d58297a03..3aa7b7ad91f 100644 --- a/docs/UAA-APIs.rst +++ b/docs/UAA-APIs.rst @@ -1016,7 +1016,6 @@ Response body *example* :: "version":0, "created":1426260091149, "active":true, - "disableInternalUserManagement":false, "identityZoneId":"testzone1", "last_modified":1426260091149 } @@ -1094,8 +1093,8 @@ Fields *Available Fields* :: lockoutPeriodSeconds int Required Amount of time in seconds to lockout login attempts after ``lockoutAfterFailures`` attempts reached, 0+ lockoutAfterFailures int Required Number of login attempts allowed within ``countFailuresWithin`` seconds, 0+ countFailuresWithin int Required Amount of time in seconds for which past login failures are counted, starting from the current time, 0+ - emailDomain List Optional List of email domains associated with the UAA provider. If null and no domains are explicitly matched with any other providers, the UAA acts as a catch-all, wherein the email will be associated with the UAA provider. Wildcards supported. disableInternalUserManagement boolean Optional When set to true, user management is disabled for this provider, defaults to false + emailDomain List Optional List of email domains associated with the UAA provider. If null and no domains are explicitly matched with any other providers, the UAA acts as a catch-all, wherein the email will be associated with the UAA provider. Wildcards supported. SAML Provider Configuration (provided in JSON format as part of the ``config`` field on the Identity Provider - See class org.cloudfoundry.identity.uaa.login.saml.SamlIdentityProviderDefinition ====================== =============== ======== ================================================================================================================================================================================================= @@ -1146,7 +1145,7 @@ Curl Example POST (Creating an LDAP provider):: -XPOST -H"Accept:application/json" \ -H"Content-Type:application/json" \ -H"X-Identity-Zone-Id:testzone1" \ - -d '{"originKey":"ldap","name":"myldap for testzone1","type":"ldap","config":"{\"baseUrl\":\"ldaps://localhost:33636\",\"skipSSLVerification\":true,\"bindUserDn\":\"cn=admin,ou=Users,dc=test,dc=com\",\"bindPassword\":\"adminsecret\",\"userSearchBase\":\"dc=test,dc=com\",\"userSearchFilter\":\"cn={0}\",\"groupSearchBase\":\"ou=scopes,dc=test,dc=com\",\"groupSearchFilter\":\"member={0}\",\"mailAttributeName\":\"mail\",\"mailSubstitute\":null,\"ldapProfileFile\":\"ldap/ldap-search-and-bind.xml\",\"ldapGroupFile\":\"ldap/ldap-groups-map-to-scopes.xml\",\"mailSubstituteOverridesLdap\":false,\"autoAddGroups\":true,\"groupSearchSubTree\":true,\"maxGroupSearchDepth\":10}","active":true,"identityZoneId":"testzone1"}' \ + -d '{"originKey":"ldap","name":"myldap for testzone1","type":"ldap","config":"{\"baseUrl\":\"ldaps://localhost:33636\",\"skipSSLVerification\":true,\"bindUserDn\":\"cn=admin,ou=Users,dc=test,dc=com\",\"bindPassword\":\"adminsecret\",\"userSearchBase\":\"dc=test,dc=com\",\"userSearchFilter\":\"cn={0}\",\"groupSearchBase\":\"ou=scopes,dc=test,dc=com\",\"groupSearchFilter\":\"member={0}\",\"mailAttributeName\":\"mail\",\"mailSubstitute\":null,\"ldapProfileFile\":\"ldap/ldap-search-and-bind.xml\",\"ldapGroupFile\":\"ldap/ldap-groups-map-to-scopes.xml\",\"mailSubstituteOverridesLdap\":false,\"autoAddGroups\":true,\"groupSearchSubTree\":true,\"maxGroupSearchDepth\":10,\"emailDomain\":[\"example.com\",\"another.example.com\"]}","active":true,"identityZoneId":"testzone1"}' \ http://localhost:8080/uaa/identity-providers Curl Example PUT (Updating a UAA provider):: From 0d44f1923f68699abe89bf4dd6879565c6a6f984 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Mon, 5 Oct 2015 09:50:45 -0600 Subject: [PATCH 040/103] Add in ability to map white listed groups from SAML https://www.pivotaltracker.com/story/show/99445992 [#99445992] --- .../ExternalIdentityProviderDefinition.java | 30 +- .../uaa/authentication/UaaAuthentication.java | 22 +- .../LoginServerSamlUserDetailsService.java | 63 ---- .../saml/LoginSamlAuthenticationToken.java | 8 +- .../identity/uaa/user/UaaAuthority.java | 8 +- .../UaaAuthenticationSerializationTests.java | 25 +- .../saml/LoginSamlAuthenticationProvider.java | 107 ++++++- .../uaa/scim/bootstrap/ScimUserBootstrap.java | 8 +- .../bootstrap/ScimUserBootstrapTests.java | 6 +- .../webapp/WEB-INF/spring/saml-providers.xml | 4 +- .../uaa/integration/feature/SamlLoginIT.java | 12 +- .../LoginSamlAuthenticationProviderTests.java | 273 ++++++++++++++++++ 12 files changed, 459 insertions(+), 107 deletions(-) delete mode 100644 common/src/main/java/org/cloudfoundry/identity/uaa/login/LoginServerSamlUserDetailsService.java rename {common => login}/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java (60%) create mode 100644 uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/ExternalIdentityProviderDefinition.java b/common/src/main/java/org/cloudfoundry/identity/uaa/ExternalIdentityProviderDefinition.java index 4f88cd547e6..93d250ebbbf 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/ExternalIdentityProviderDefinition.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/ExternalIdentityProviderDefinition.java @@ -1,5 +1,10 @@ package org.cloudfoundry.identity.uaa; +import com.fasterxml.jackson.annotation.JsonIgnore; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -16,25 +21,38 @@ * subcomponent's license, as noted in the LICENSE file. *******************************************************************************/ public class ExternalIdentityProviderDefinition extends AbstractIdentityProviderDefinition { + public static final String GROUP_ATTRIBUTE_NAME = "external_groups"; //can be a string or a list of strings + public static final String EMAIL_ATTRIBUTE_NAME = "email"; //can be a string + public static final String EXTERNAL_GROUPS_WHITELIST = "externalGroupsWhitelist"; public static final String ATTRIBUTE_MAPPINGS = "attributeMappings"; - private List externalGroupsWhitelist; - private Map attributeMappings; + private List externalGroupsWhitelist = new LinkedList<>(); + private Map attributeMappings = new HashMap<>(); public List getExternalGroupsWhitelist() { - return externalGroupsWhitelist; + return Collections.unmodifiableList(externalGroupsWhitelist); } public void setExternalGroupsWhitelist(List externalGroupsWhitelist) { - this.externalGroupsWhitelist = externalGroupsWhitelist; + this.externalGroupsWhitelist = new LinkedList<>(externalGroupsWhitelist!=null ? externalGroupsWhitelist : Collections.EMPTY_LIST); + } + + @JsonIgnore + public void addWhiteListedGroup(String group) { + this.externalGroupsWhitelist.add(group); } public void setAttributeMappings(Map attributeMappings) { - this.attributeMappings = attributeMappings; + this.attributeMappings = new HashMap<>(attributeMappings!=null?attributeMappings:Collections.EMPTY_MAP); } public Map getAttributeMappings() { - return attributeMappings; + return Collections.unmodifiableMap(attributeMappings); + } + + @JsonIgnore + public void addAttributeMapping(String key, Object value) { + attributeMappings.put(key, value); } } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java index ab1fda62169..a4f1fddb8c7 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java @@ -32,6 +32,7 @@ public class UaaAuthentication implements Authentication, Serializable { private UaaAuthenticationDetails details; private boolean authenticated; private long authenticatedTime = -1l; + private long expiresAt = -1l; /** * Creates a token with the supplied array of authorities. @@ -45,13 +46,23 @@ public UaaAuthentication(UaaPrincipal principal, this(principal, null, authorities, details, true, System.currentTimeMillis()); } + public UaaAuthentication(UaaPrincipal principal, + Object credentials, + List authorities, + UaaAuthenticationDetails details, + boolean authenticated, + long authenticatedTime) { + this(principal, credentials, authorities, details, authenticated, authenticatedTime, -1); + } + @JsonCreator public UaaAuthentication(@JsonProperty("principal") UaaPrincipal principal, @JsonProperty("credentials") Object credentials, @JsonProperty("authorities") List authorities, @JsonProperty("details") UaaAuthenticationDetails details, @JsonProperty("authenticated") boolean authenticated, - @JsonProperty(value = "authenticatedTime", defaultValue = "-1") long authenticatedTime) { + @JsonProperty(value = "authenticatedTime", defaultValue = "-1") long authenticatedTime, + @JsonProperty(value = "expiresAt", defaultValue = "-1") long expiresAt) { if (principal == null || authorities == null) { throw new IllegalArgumentException("principal and authorities must not be null"); } @@ -60,7 +71,8 @@ public UaaAuthentication(@JsonProperty("principal") UaaPrincipal principal, this.details = details; this.credentials = credentials; this.authenticated = authenticated; - this.authenticatedTime = authenticatedTime == 0 ? -1 : authenticatedTime; + this.authenticatedTime = authenticatedTime <= 0 ? -1 : authenticatedTime; + this.expiresAt = expiresAt <= 0 ? -1 : expiresAt; } public long getAuthenticatedTime() { @@ -97,7 +109,7 @@ public UaaPrincipal getPrincipal() { @Override public boolean isAuthenticated() { - return authenticated; + return authenticated && (expiresAt > 0 ? expiresAt > System.currentTimeMillis() : true); } @Override @@ -105,6 +117,10 @@ public void setAuthenticated(boolean isAuthenticated) { authenticated = isAuthenticated; } + public long getExpiresAt() { + return expiresAt; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/login/LoginServerSamlUserDetailsService.java b/common/src/main/java/org/cloudfoundry/identity/uaa/login/LoginServerSamlUserDetailsService.java deleted file mode 100644 index c310fc876d9..00000000000 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/login/LoginServerSamlUserDetailsService.java +++ /dev/null @@ -1,63 +0,0 @@ -/******************************************************************************* - * Cloud Foundry - * Copyright (c) [2009-2014] Pivotal Software, Inc. All Rights Reserved. - * - * This product is licensed to you under the Apache License, Version 2.0 (the "License"). - * You may not use this product except in compliance with the License. - * - * This product includes a number of subcomponents with - * separate copyright notices and license terms. Your use of these - * subcomponents is subject to the terms and conditions of the - * subcomponent's license, as noted in the LICENSE file. - *******************************************************************************/ -package org.cloudfoundry.identity.uaa.login; - -import java.util.ArrayList; -import java.util.Collection; - -import org.cloudfoundry.identity.uaa.user.UaaAuthority; -import org.opensaml.saml2.core.Attribute; -import org.opensaml.xml.XMLObject; -import org.opensaml.xml.schema.XSString; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.security.saml.SAMLCredential; -import org.springframework.security.saml.userdetails.SAMLUserDetailsService; - -/** - * UserDetailsService that extracts the user's groups - * - * @author jdsa - * - */ -public class LoginServerSamlUserDetailsService implements SAMLUserDetailsService { - - @Override - public Object loadUserBySAML(SAMLCredential credential) throws UsernameNotFoundException { - String username = credential.getNameID().getValue(); - String password = null; - boolean enabled = true; - boolean accountNonExpired = false; - boolean credentialsNonExpired = true; - boolean accountNonLocked = true; - Collection authorities = null; - - for (Attribute attribute : credential.getAttributes()) { - if (("Groups".equals(attribute.getName())) || ("Groups".equals(attribute.getFriendlyName()))) { - if (attribute.getAttributeValues() != null && attribute.getAttributeValues().size() > 0) { - authorities = new ArrayList(); - for (XMLObject group : attribute.getAttributeValues()) { - authorities.add(new SamlUserAuthority(((XSString) group).getValue())); - } - } - break; - } - } - - SamlUserDetails userDetails = new SamlUserDetails(username, password, enabled, accountNonExpired, - credentialsNonExpired, accountNonLocked, authorities == null ? UaaAuthority.USER_AUTHORITIES - : authorities); - - return userDetails; - } - -} diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationToken.java b/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationToken.java index 7ac16080a3f..699e461557b 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationToken.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationToken.java @@ -12,10 +12,12 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.login.saml; +import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; +import org.springframework.security.core.GrantedAuthority; import org.springframework.security.providers.ExpiringUsernameAuthenticationToken; -import javax.validation.constraints.NotNull; +import java.util.List; public class LoginSamlAuthenticationToken extends ExpiringUsernameAuthenticationToken { @@ -31,4 +33,8 @@ public LoginSamlAuthenticationToken(UaaPrincipal uaaPrincipal, ExpiringUsernameA public UaaPrincipal getUaaPrincipal() { return uaaPrincipal; } + + public UaaAuthentication getUaaAuthentication(List uaaAuthorityList) { + return new UaaAuthentication(getUaaPrincipal(), getCredentials(), uaaAuthorityList, null, isAuthenticated(), System.currentTimeMillis(), getTokenExpiration()==null ? -1l : getTokenExpiration().getTime()); + } } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/user/UaaAuthority.java b/common/src/main/java/org/cloudfoundry/identity/uaa/user/UaaAuthority.java index 5bcba0335db..20d5e9b6d8b 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/user/UaaAuthority.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/user/UaaAuthority.java @@ -12,14 +12,14 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.user; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - import com.fasterxml.jackson.annotation.JsonCreator; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + /** * The UAA only distinguishes 2 types of user for internal usage, denoted * uaa.admin and uaa.user. Other authorities might be diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationSerializationTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationSerializationTests.java index 8ef2fe75de2..e06f937a15c 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationSerializationTests.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationSerializationTests.java @@ -16,20 +16,39 @@ package org.cloudfoundry.identity.uaa.authentication; import org.cloudfoundry.identity.uaa.util.JsonUtils; -import org.junit.Assert; import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + public class UaaAuthenticationSerializationTests { @Test public void testDeserializationWithoutAuthenticatedTime() throws Exception { String data ="{\"principal\":{\"id\":\"user-id\",\"name\":\"username\",\"email\":\"email\",\"origin\":\"uaa\",\"externalId\":null,\"zoneId\":\"uaa\"},\"credentials\":null,\"authorities\":[],\"details\":null,\"authenticated\":true,\"authenticatedTime\":1438649464353,\"name\":\"username\"}"; UaaAuthentication authentication1 = JsonUtils.readValue(data, UaaAuthentication.class); - Assert.assertEquals(1438649464353l, authentication1.getAuthenticatedTime()); + assertEquals(1438649464353l, authentication1.getAuthenticatedTime()); + assertEquals(-1l, authentication1.getExpiresAt()); + assertTrue(authentication1.isAuthenticated()); String dataWithoutTime ="{\"principal\":{\"id\":\"user-id\",\"name\":\"username\",\"email\":\"email\",\"origin\":\"uaa\",\"externalId\":null,\"zoneId\":\"uaa\"},\"credentials\":null,\"authorities\":[],\"details\":null,\"authenticated\":true,\"name\":\"username\"}"; UaaAuthentication authentication2 = JsonUtils.readValue(dataWithoutTime, UaaAuthentication.class); - Assert.assertEquals(-1, authentication2.getAuthenticatedTime()); + assertEquals(-1, authentication2.getAuthenticatedTime()); + + + long inThePast = System.currentTimeMillis() - 1000l * 60l; + data ="{\"principal\":{\"id\":\"user-id\",\"name\":\"username\",\"email\":\"email\",\"origin\":\"uaa\",\"externalId\":null,\"zoneId\":\"uaa\"},\"credentials\":null,\"authorities\":[],\"details\":null,\"authenticated\":true,\"authenticatedTime\":1438649464353,\"name\":\"username\", \"expiresAt\":"+inThePast+"}"; + UaaAuthentication authentication3 = JsonUtils.readValue(data, UaaAuthentication.class); + assertEquals(1438649464353l, authentication3.getAuthenticatedTime()); + assertEquals(inThePast, authentication3.getExpiresAt()); + assertFalse(authentication3.isAuthenticated()); + long inTheFuture = System.currentTimeMillis() + 1000l * 60l; + data ="{\"principal\":{\"id\":\"user-id\",\"name\":\"username\",\"email\":\"email\",\"origin\":\"uaa\",\"externalId\":null,\"zoneId\":\"uaa\"},\"credentials\":null,\"authorities\":[],\"details\":null,\"authenticated\":true,\"authenticatedTime\":1438649464353,\"name\":\"username\", \"expiresAt\":"+inTheFuture+"}"; + UaaAuthentication authentication4 = JsonUtils.readValue(data, UaaAuthentication.class); + assertEquals(1438649464353l, authentication4.getAuthenticatedTime()); + assertEquals(inTheFuture, authentication4.getExpiresAt()); + assertTrue(authentication4.isAuthenticated()); } } \ No newline at end of file diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java b/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java similarity index 60% rename from common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java rename to login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java index 57104ae97d4..8cf7aece3ed 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java @@ -17,14 +17,20 @@ import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.cloudfoundry.identity.uaa.authentication.event.UserAuthenticationSuccessEvent; +import org.cloudfoundry.identity.uaa.authentication.manager.ExternalGroupAuthorizationEvent; import org.cloudfoundry.identity.uaa.authentication.manager.NewUserAuthenticatedEvent; -import org.cloudfoundry.identity.uaa.user.UaaAuthority; +import org.cloudfoundry.identity.uaa.login.SamlUserAuthority; +import org.cloudfoundry.identity.uaa.scim.ScimGroupExternalMember; +import org.cloudfoundry.identity.uaa.scim.ScimGroupExternalMembershipManager; import org.cloudfoundry.identity.uaa.user.UaaUser; import org.cloudfoundry.identity.uaa.user.UaaUserDatabase; import org.cloudfoundry.identity.uaa.zone.IdentityProvider; import org.cloudfoundry.identity.uaa.zone.IdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; +import org.opensaml.saml2.core.Attribute; +import org.opensaml.xml.XMLObject; +import org.opensaml.xml.schema.XSString; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; @@ -33,19 +39,31 @@ import org.springframework.security.authentication.ProviderNotFoundException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.providers.ExpiringUsernameAuthenticationToken; import org.springframework.security.saml.SAMLAuthenticationProvider; import org.springframework.security.saml.SAMLAuthenticationToken; +import org.springframework.security.saml.SAMLCredential; import org.springframework.security.saml.context.SAMLMessageContext; +import org.springframework.security.saml.userdetails.SAMLUserDetailsService; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.GROUP_ATTRIBUTE_NAME; public class LoginSamlAuthenticationProvider extends SAMLAuthenticationProvider implements ApplicationEventPublisherAware { private UaaUserDatabase userDatabase; private ApplicationEventPublisher eventPublisher; private IdentityProviderProvisioning identityProviderProvisioning; + private ScimGroupExternalMembershipManager externalMembershipManager; public void setIdentityProviderProvisioning(IdentityProviderProvisioning identityProviderProvisioning) { this.identityProviderProvisioning = identityProviderProvisioning; @@ -55,13 +73,18 @@ public void setUserDatabase(UaaUserDatabase userDatabase) { this.userDatabase = userDatabase; } + public void setExternalMembershipManager(ScimGroupExternalMembershipManager externalMembershipManager) { + this.externalMembershipManager = externalMembershipManager; + } + @Override - public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) { - this.eventPublisher = eventPublisher; + public void setUserDetails(SAMLUserDetailsService userDetails) { + super.setUserDetails(userDetails); } - public ApplicationEventPublisher getApplicationEventPublisher() { - return eventPublisher; + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; } @Override @@ -76,11 +99,12 @@ public Authentication authenticate(Authentication authentication) throws Authent SAMLMessageContext context = token.getCredentials(); String alias = context.getPeerExtendedMetadata().getAlias(); boolean addNew = true; + IdentityProvider idp; + SamlIdentityProviderDefinition samlConfig; try { - IdentityProvider idp = identityProviderProvisioning.retrieveByOrigin(alias, IdentityZoneHolder.get().getId()); - SamlIdentityProviderDefinition samlConfig = idp.getConfigValue(SamlIdentityProviderDefinition.class); + idp = identityProviderProvisioning.retrieveByOrigin(alias, IdentityZoneHolder.get().getId()); + samlConfig = idp.getConfigValue(SamlIdentityProviderDefinition.class); addNew = samlConfig.isAddShadowUserOnLogin(); - if (!idp.isActive()) { throw new ProviderNotFoundException("Identity Provider has been disabled by administrator."); } @@ -89,8 +113,11 @@ public Authentication authenticate(Authentication authentication) throws Authent } ExpiringUsernameAuthenticationToken result = getExpiringUsernameAuthenticationToken(authentication); UaaPrincipal samlPrincipal = new UaaPrincipal(Origin.NotANumber, result.getName(), result.getName(), alias, result.getName(), zone.getId()); - UaaPrincipal principal = createIfMissing(samlPrincipal, addNew); - return new LoginSamlAuthenticationToken(principal, result); + Collection samlAuthorities = retrieveSamlAuthorities(samlConfig, (SAMLCredential) result.getCredentials()); + Collection authorities = mapAuthorities(idp.getOriginKey(), samlConfig, samlAuthorities); + UaaUser user = createIfMissing(samlPrincipal, addNew, authorities); + UaaPrincipal principal = new UaaPrincipal(user); + return new LoginSamlAuthenticationToken(principal, result).getUaaAuthentication(user.getAuthorities()); } protected ExpiringUsernameAuthenticationToken getExpiringUsernameAuthenticationToken(Authentication authentication) { @@ -103,9 +130,51 @@ protected void publish(ApplicationEvent event) { } } - protected UaaPrincipal createIfMissing(UaaPrincipal samlPrincipal, boolean addNew) { + protected Collection mapAuthorities(String origin, SamlIdentityProviderDefinition definition, Collection authorities) { + Collection result = Collections.EMPTY_LIST; + if (definition!=null && definition.getExternalGroupsWhitelist()!=null) { + List whiteList = definition.getExternalGroupsWhitelist(); + result = new LinkedList<>(); + for (GrantedAuthority authority : authorities ) { + String externalGroup = authority.getAuthority(); + if (whiteList.contains(externalGroup)) { + for (ScimGroupExternalMember internalGroup : externalMembershipManager.getExternalGroupMapsByExternalGroup(externalGroup, origin)) { + result.add(new SimpleGrantedAuthority(internalGroup.getDisplayName())); + } + } + } + } + return result; + } + + public Collection retrieveSamlAuthorities(SamlIdentityProviderDefinition definition, SAMLCredential credential) { + Collection authorities = null; + if (definition.getAttributeMappings().get(GROUP_ATTRIBUTE_NAME)!=null) { + List groupNames = new LinkedList<>(); + if (definition.getAttributeMappings().get(GROUP_ATTRIBUTE_NAME) instanceof String) { + groupNames.add((String) definition.getAttributeMappings().get(GROUP_ATTRIBUTE_NAME)); + } else if (definition.getAttributeMappings().get(GROUP_ATTRIBUTE_NAME) instanceof Collection) { + groupNames.addAll((Collection) definition.getAttributeMappings().get(GROUP_ATTRIBUTE_NAME)); + } + for (Attribute attribute : credential.getAttributes()) { + if ((groupNames.contains(attribute.getName())) || (groupNames.contains(attribute.getFriendlyName()))) { + if (attribute.getAttributeValues() != null && attribute.getAttributeValues().size() > 0) { + authorities = new ArrayList<>(); + for (XMLObject group : attribute.getAttributeValues()) { + authorities.add(new SamlUserAuthority(((XSString) group).getValue())); + } + } + break; + } + } + } + return authorities == null ? Collections.EMPTY_LIST : authorities; + } + + protected UaaUser createIfMissing(UaaPrincipal samlPrincipal, boolean addNew, Collection authorities) { + boolean userModified = false; UaaPrincipal uaaPrincipal = samlPrincipal; - UaaUser user = null; + UaaUser user; try { user = userDatabase.retrieveUserByName(uaaPrincipal.getName(), uaaPrincipal.getOrigin()); } catch (UsernameNotFoundException e) { @@ -113,7 +182,6 @@ protected UaaPrincipal createIfMissing(UaaPrincipal samlPrincipal, boolean addNe throw new LoginSAMLException("SAML user does not exist. " + "You can correct this by creating a shadow user for the SAML user.", e); } - // Register new users automatically publish(new NewUserAuthenticatedEvent(getUser(uaaPrincipal))); try { @@ -122,10 +190,19 @@ protected UaaPrincipal createIfMissing(UaaPrincipal samlPrincipal, boolean addNe throw new BadCredentialsException("Unable to establish shadow user for SAML user:"+ uaaPrincipal.getName()); } } + publish( + new ExternalGroupAuthorizationEvent( + user, + true, + authorities, + true + ) + ); + user = userDatabase.retrieveUserById(user.getId()); UaaPrincipal result = new UaaPrincipal(user); Authentication success = new UaaAuthentication(result, user.getAuthorities(), null); publish(new UserAuthenticationSuccessEvent(user, success)); - return result; + return user; } protected UaaUser getUser(UaaPrincipal principal) { @@ -168,7 +245,7 @@ protected UaaUser getUser(UaaPrincipal principal) { name, "" /*zero length password for login server */, email, - UaaAuthority.USER_AUTHORITIES, + Collections.EMPTY_LIST, givenName, familyName, new Date(), diff --git a/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrap.java b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrap.java index 6d363480a43..77abdceb176 100644 --- a/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrap.java +++ b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrap.java @@ -96,8 +96,8 @@ public void afterPropertiesSet() throws Exception { protected ScimUser getScimUser(UaaUser user) { List users = scimUserProvisioning.query("userName eq \"" + user.getUsername() + "\"" + - " and origin eq \"" + - (user.getOrigin() == null ? Origin.UAA : user.getOrigin()) + "\""); + " and origin eq \"" + + (user.getOrigin() == null ? Origin.UAA : user.getOrigin()) + "\""); if (users.isEmpty() && StringUtils.hasText(user.getId())) { try { @@ -147,7 +147,9 @@ private void updateUser(ScimUser existingUser, UaaUser updatedUser, boolean upda final ScimUser newScimUser = convertToScimUser(updatedUser); newScimUser.setVersion(existingUser.getVersion()); scimUserProvisioning.update(id, newScimUser); - scimUserProvisioning.changePassword(id, null, updatedUser.getPassword()); + if (Origin.UAA.equals(newScimUser.getOrigin())) { //password is not relevant for non UAA users + scimUserProvisioning.changePassword(id, null, updatedUser.getPassword()); + } if (updateGroups) { Collection newGroups = convertToGroups(updatedUser.getAuthorities()); logger.debug("Adding new groups " + newGroups); diff --git a/scim/src/test/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrapTests.java b/scim/src/test/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrapTests.java index 79b542c8f6b..8bfeb1333b5 100644 --- a/scim/src/test/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrapTests.java +++ b/scim/src/test/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrapTests.java @@ -53,10 +53,6 @@ import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; -/** - * @author Luke Taylor - * @author Dave Syer - */ public class ScimUserBootstrapTests { private JdbcScimUserProvisioning db; @@ -230,12 +226,14 @@ public void canRemoveAuthorities() throws Exception { @Test public void canUpdateUsers() throws Exception { UaaUser joe = new UaaUser("joe", "password", "joe@test.org", "Joe", "User"); + joe = joe.modifyOrigin(Origin.UAA); ScimUserBootstrap bootstrap = new ScimUserBootstrap(db, gdb, mdb, Arrays.asList(joe)); bootstrap.afterPropertiesSet(); String passwordHash = jdbcTemplate.queryForObject("select password from users where username='joe'",new Object[0], String.class); joe = new UaaUser("joe", "new", "joe@test.org", "Joe", "Bloggs"); + joe = joe.modifyOrigin(Origin.UAA); bootstrap = new ScimUserBootstrap(db, gdb, mdb, Arrays.asList(joe)); bootstrap.setOverride(true); bootstrap.afterPropertiesSet(); diff --git a/uaa/src/main/webapp/WEB-INF/spring/saml-providers.xml b/uaa/src/main/webapp/WEB-INF/spring/saml-providers.xml index 352c6b533ed..dd6f85c1024 100644 --- a/uaa/src/main/webapp/WEB-INF/spring/saml-providers.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/saml-providers.xml @@ -130,13 +130,11 @@ - + - - diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java index a75dd9bd3aa..9406b5145b7 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java @@ -206,7 +206,15 @@ public void testSimpleSamlPhpLogin() throws Exception { testSimpleSamlLogin("/login", "Where to?"); } + @Test + public void testGroupIntegration() throws Exception { + testSimpleSamlLogin("/login", "Where to?", "marissa4","saml2"); + } + private void testSimpleSamlLogin(String firstUrl, String lookfor) throws Exception { + testSimpleSamlLogin(firstUrl, lookfor, testAccounts.getUserName(), testAccounts.getPassword()); + } + private void testSimpleSamlLogin(String firstUrl, String lookfor, String username, String password) throws Exception { IdentityProvider provider = createIdentityProvider("simplesamlphp"); //tells us that we are on travis @@ -218,8 +226,8 @@ private void testSimpleSamlLogin(String firstUrl, String lookfor) throws Excepti //takeScreenShot(); webDriver.findElement(By.xpath("//h2[contains(text(), 'Enter your username and password')]")); webDriver.findElement(By.name("username")).clear(); - webDriver.findElement(By.name("username")).sendKeys(testAccounts.getUserName()); - webDriver.findElement(By.name("password")).sendKeys(testAccounts.getPassword()); + webDriver.findElement(By.name("username")).sendKeys(username); + webDriver.findElement(By.name("password")).sendKeys(password); webDriver.findElement(By.xpath("//input[@value='Login']")).click(); assertThat(webDriver.findElement(By.cssSelector("h1")).getText(), Matchers.containsString(lookfor)); } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java new file mode 100644 index 00000000000..07aab8d81fd --- /dev/null +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java @@ -0,0 +1,273 @@ +/* + * ***************************************************************************** + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + * ***************************************************************************** + */ + +package org.cloudfoundry.identity.uaa.login.saml; + +import org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.authentication.Origin; +import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; +import org.cloudfoundry.identity.uaa.authentication.manager.AuthEvent; +import org.cloudfoundry.identity.uaa.rest.jdbc.JdbcPagingListFactory; +import org.cloudfoundry.identity.uaa.scim.ScimGroup; +import org.cloudfoundry.identity.uaa.scim.ScimGroupProvisioning; +import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; +import org.cloudfoundry.identity.uaa.scim.bootstrap.ScimUserBootstrap; +import org.cloudfoundry.identity.uaa.scim.jdbc.JdbcScimGroupExternalMembershipManager; +import org.cloudfoundry.identity.uaa.scim.jdbc.JdbcScimGroupMembershipManager; +import org.cloudfoundry.identity.uaa.scim.jdbc.JdbcScimGroupProvisioning; +import org.cloudfoundry.identity.uaa.scim.jdbc.JdbcScimUserProvisioning; +import org.cloudfoundry.identity.uaa.test.JdbcTestBase; +import org.cloudfoundry.identity.uaa.user.JdbcUaaUserDatabase; +import org.cloudfoundry.identity.uaa.user.UaaAuthority; +import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.cloudfoundry.identity.uaa.zone.IdentityProvider; +import org.cloudfoundry.identity.uaa.zone.IdentityProviderProvisioning; +import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.cloudfoundry.identity.uaa.zone.JdbcIdentityProviderProvisioning; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Test; +import org.opensaml.saml2.core.Assertion; +import org.opensaml.saml2.core.Attribute; +import org.opensaml.saml2.core.NameID; +import org.opensaml.ws.wssecurity.impl.AttributedStringImpl; +import org.opensaml.xml.XMLObject; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.saml.SAMLAuthenticationToken; +import org.springframework.security.saml.SAMLConstants; +import org.springframework.security.saml.SAMLCredential; +import org.springframework.security.saml.context.SAMLMessageContext; +import org.springframework.security.saml.log.SAMLLogger; +import org.springframework.security.saml.metadata.ExtendedMetadata; +import org.springframework.security.saml.websso.WebSSOProfileConsumer; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class LoginSamlAuthenticationProviderTests extends JdbcTestBase { + + public static final String SAML_USER = "saml.user"; + public static final String SAML_ADMIN = "saml.admin"; + public static final String UAA_SAML_USER = "uaa.saml.user"; + public static final String UAA_SAML_ADMIN = "uaa.saml.admin"; + IdentityProviderProvisioning providerProvisioning; + ApplicationEventPublisher publisher; + JdbcUaaUserDatabase userDatabase; + LoginSamlAuthenticationProvider authprovider; + WebSSOProfileConsumer consumer; + SAMLCredential credential; + SAMLLogger samlLogger = mock(SAMLLogger.class); + SamlIdentityProviderDefinition providerDefinition = new SamlIdentityProviderDefinition(); + private IdentityProvider provider; + + public List getAttributes(final String name, List values) { + Attribute attribute = mock(Attribute.class); + when(attribute.getName()).thenReturn(name); + when(attribute.getFriendlyName()).thenReturn(name); + + List xmlObjects = new LinkedList<>(); + for (String s : values) { + AttributedStringImpl impl = new AttributedStringImpl("","",""); + impl.setValue(s); + xmlObjects.add(impl); + } + when(attribute.getAttributeValues()).thenReturn(xmlObjects); + return Arrays.asList(attribute); + } + + @Before + public void configureProvider() throws Exception { + ScimUserProvisioning userProvisioning = new JdbcScimUserProvisioning(jdbcTemplate, new JdbcPagingListFactory(jdbcTemplate, limitSqlAdapter)); + ScimGroupProvisioning groupProvisioning = new JdbcScimGroupProvisioning(jdbcTemplate, new JdbcPagingListFactory(jdbcTemplate, limitSqlAdapter)); + + ScimGroup uaaSamlUser = groupProvisioning.create(new ScimGroup(null,UAA_SAML_USER,IdentityZone.getUaa().getId())); + ScimGroup uaaSamlAdmin = groupProvisioning.create(new ScimGroup(null,UAA_SAML_ADMIN,IdentityZone.getUaa().getId())); + + JdbcScimGroupMembershipManager membershipManager = new JdbcScimGroupMembershipManager(jdbcTemplate, new JdbcPagingListFactory(jdbcTemplate, limitSqlAdapter)); + membershipManager.setScimGroupProvisioning(groupProvisioning); + membershipManager.setScimUserProvisioning(userProvisioning); + ScimUserBootstrap bootstrap = new ScimUserBootstrap(userProvisioning, groupProvisioning, membershipManager, Collections.EMPTY_LIST); + + JdbcScimGroupExternalMembershipManager externalManager = new JdbcScimGroupExternalMembershipManager(jdbcTemplate, new JdbcPagingListFactory(jdbcTemplate, limitSqlAdapter)); + externalManager.setScimGroupProvisioning(groupProvisioning); + externalManager.mapExternalGroup(uaaSamlUser.getId(), SAML_USER, Origin.SAML); + externalManager.mapExternalGroup(uaaSamlAdmin.getId(), SAML_ADMIN, Origin.SAML); + + String username = "marissa-saml"; + NameID usernameID = mock(NameID.class); + when(usernameID.getValue()).thenReturn(username); + consumer = mock(WebSSOProfileConsumer.class); + credential = new SAMLCredential( + usernameID, + mock(Assertion.class), + "remoteEntityID", + getAttributes("groups", Arrays.asList(SAML_USER,SAML_ADMIN)), + "localEntityID"); + when(consumer.processAuthenticationResponse(anyObject())).thenReturn(credential); + + userDatabase = new JdbcUaaUserDatabase(jdbcTemplate); + userDatabase.setUserAuthoritiesQuery("select g.displayName from groups g, group_membership m where g.id = m.group_id and m.member_id = ?"); + userDatabase.setDefaultAuthorities(new HashSet<>(Arrays.asList(UaaAuthority.UAA_USER.getAuthority()))); + providerProvisioning = new JdbcIdentityProviderProvisioning(jdbcTemplate); + publisher = new CreateUserPublisher(bootstrap); + authprovider = new LoginSamlAuthenticationProvider(); + + authprovider.setUserDatabase(userDatabase); + authprovider.setIdentityProviderProvisioning(providerProvisioning); + authprovider.setApplicationEventPublisher(publisher); + authprovider.setConsumer(consumer); + authprovider.setSamlLogger(samlLogger); + authprovider.setExternalMembershipManager(externalManager); + + provider = new IdentityProvider(); + provider.setIdentityZoneId(IdentityZone.getUaa().getId()); + provider.setOriginKey(Origin.SAML); + provider.setName("saml-test"); + provider.setActive(true); + provider.setType(Origin.SAML); + providerDefinition.setMetaDataLocation(String.format(IDP_META_DATA, Origin.SAML)); + providerDefinition.setIdpEntityAlias(Origin.SAML); + provider.setConfig(JsonUtils.writeValueAsString(providerDefinition)); + provider = providerProvisioning.create(provider); + } + + @Test + public void testAuthenticateSimple() { + authprovider.authenticate(mockSamlAuthentication(Origin.SAML)); + } + + @Test + public void test_white_listed_group() throws Exception { + providerDefinition.addAttributeMapping(ExternalIdentityProviderDefinition.GROUP_ATTRIBUTE_NAME, "groups"); + providerDefinition.addWhiteListedGroup(SAML_USER); + providerDefinition.addWhiteListedGroup(SAML_ADMIN); + provider.setConfig(JsonUtils.writeValueAsString(providerDefinition)); + providerProvisioning.update(provider); + UaaAuthentication authentication = getAuthentication(); + assertEquals("Three authorities should have been granted!", 3, authentication.getAuthorities().size()); + assertThat(authentication.getAuthorities(), + Matchers.containsInAnyOrder( + new SimpleGrantedAuthority(UAA_SAML_ADMIN), + new SimpleGrantedAuthority(UAA_SAML_USER), + new SimpleGrantedAuthority(UaaAuthority.UAA_USER.getAuthority()) + ) + ); + } + + @Test + public void test_groups_not_white_listed() throws Exception { + providerDefinition.addAttributeMapping(ExternalIdentityProviderDefinition.GROUP_ATTRIBUTE_NAME, "groups"); + providerDefinition.addWhiteListedGroup(SAML_ADMIN); + provider.setConfig(JsonUtils.writeValueAsString(providerDefinition)); + providerProvisioning.update(provider); + UaaAuthentication authentication = getAuthentication(); + assertEquals("Two authorities should have been granted!", 2, authentication.getAuthorities().size()); + assertThat(authentication.getAuthorities(), + Matchers.containsInAnyOrder( + new SimpleGrantedAuthority(UAA_SAML_ADMIN), + new SimpleGrantedAuthority(UaaAuthority.UAA_USER.getAuthority()) + ) + ); + } + + @Test + public void test_group_attribute_not_set() throws Exception { + UaaAuthentication uaaAuthentication = getAuthentication(); + assertEquals("Only uaa.user should have been granted", 1, uaaAuthentication.getAuthorities().size()); + assertEquals(UaaAuthority.UAA_USER.getAuthority(), uaaAuthentication.getAuthorities().iterator().next().getAuthority()); + } + + protected UaaAuthentication getAuthentication() { + Authentication authentication = authprovider.authenticate(mockSamlAuthentication(Origin.SAML)); + assertNotNull("Authentication should exist", authentication); + assertTrue("Authentication should be UaaAuthentication", authentication instanceof UaaAuthentication); + return (UaaAuthentication)authentication; + } + + + protected SAMLAuthenticationToken mockSamlAuthentication(String originKey) { + ExtendedMetadata metadata = mock(ExtendedMetadata.class); + when(metadata.getAlias()).thenReturn(originKey); + SAMLMessageContext contxt = mock(SAMLMessageContext.class); + when(contxt.getPeerExtendedMetadata()).thenReturn(metadata); + when(contxt.getCommunicationProfileId()).thenReturn(SAMLConstants.SAML2_WEBSSO_PROFILE_URI); + return new SAMLAuthenticationToken(contxt); + } + + public static class CreateUserPublisher implements ApplicationEventPublisher { + + final ScimUserBootstrap bootstrap; + + public CreateUserPublisher(ScimUserBootstrap bootstrap) { + this.bootstrap = bootstrap; + } + + + @Override + public void publishEvent(ApplicationEvent event) { + if (event instanceof AuthEvent) { + bootstrap.onApplicationEvent((AuthEvent)event); + } + } + } + + + public static final String IDP_META_DATA = + "\n" + + "\n" + + " \n" + + " \n" + + " begl1WVCsXSn7iHixtWPP8d/X+k=BmbKqA3A0oSLcn5jImz/l5WbpVXj+8JIpT/ENWjOjSd/gcAsZm1QvYg+RxYPBk+iV2bBxD+/yAE/w0wibsHrl0u9eDhoMRUJBUSmeyuN1lYzBuoVa08PdAGtb5cGm4DMQT5Rzakb1P0hhEPPEDDHgTTxop89LUu6xx97t2Q03Khy8mXEmBmNt2NlFxJPNt0FwHqLKOHRKBOE/+BpswlBocjOQKFsI9tG3TyjFC68mM2jo0fpUQCgj5ZfhzolvS7z7c6V201d9Tqig0/mMFFJLTN8WuZPavw22AJlMjsDY9my+4R9HKhK5U53DhcTeECs9fb4gd7p5BJy4vVp7tqqOg==\n" + + "MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYDVQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwXc2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0BwaXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAaBgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQDDBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlrQHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWWRDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQnX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gphiJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduOnRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+vZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLuxbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6zV9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYDVQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwXc2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0BwaXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAaBgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQDDBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlrQHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWWRDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQnX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gphiJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduOnRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+vZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLuxbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6zV9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYDVQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwXc2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0BwaXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAaBgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQDDBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlrQHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWWRDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQnX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gphiJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduOnRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+vZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLuxbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6zV9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n" + + " \n" + + " \n" + + " \n" + + " Filip\n" + + " Hanik\n" + + " fhanik@pivotal.io\n" + + " \n" + + ""; +} + From 0cb7b5adc9208429c12afd2ada8b7a68979ebac5 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Tue, 6 Oct 2015 12:21:37 -0600 Subject: [PATCH 041/103] Add in SAML integration test for retrieving groups --- .../uaa/integration/feature/SamlLoginIT.java | 128 ++++++++++++- .../util/IntegrationTestUtils.java | 168 +++++++++++++++++- 2 files changed, 280 insertions(+), 16 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java index 9406b5145b7..09b8958e261 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java @@ -15,6 +15,7 @@ import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; +import org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.ServerRunning; import org.cloudfoundry.identity.uaa.authentication.Origin; import org.cloudfoundry.identity.uaa.client.ClientConstants; @@ -24,6 +25,9 @@ import org.cloudfoundry.identity.uaa.login.saml.SamlIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.login.test.LoginServerClassRunner; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; +import org.cloudfoundry.identity.uaa.scim.ScimGroup; +import org.cloudfoundry.identity.uaa.scim.ScimGroupExternalMember; +import org.cloudfoundry.identity.uaa.scim.ScimGroupMember; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.test.UaaTestAccounts; import org.cloudfoundry.identity.uaa.util.JsonUtils; @@ -69,6 +73,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; @@ -208,7 +213,7 @@ public void testSimpleSamlPhpLogin() throws Exception { @Test public void testGroupIntegration() throws Exception { - testSimpleSamlLogin("/login", "Where to?", "marissa4","saml2"); + testSimpleSamlLogin("/login", "Where to?", "marissa4", "saml2"); } private void testSimpleSamlLogin(String firstUrl, String lookfor) throws Exception { @@ -312,7 +317,7 @@ protected void deleteUser(String origin, String username) throws Exception { String zoneAdminToken = IntegrationTestUtils.getClientCredentialsToken(serverRunning, - "admin", "adminsecret"); + "admin", "adminsecret"); String userId = IntegrationTestUtils.getUserId(zoneAdminToken, baseUrl, origin, username); if (null == userId) { @@ -324,12 +329,12 @@ protected void deleteUser(String origin, String username) @Test public void test_SamlInvitation_Automatic_Redirect_In_Zone2() throws Exception { + perform_SamlInvitation_Automatic_Redirect_In_Zone2("marissa2", true); perform_SamlInvitation_Automatic_Redirect_In_Zone2("marissa2",true); - perform_SamlInvitation_Automatic_Redirect_In_Zone2("marissa2",true); - perform_SamlInvitation_Automatic_Redirect_In_Zone2("marissa2",true); + perform_SamlInvitation_Automatic_Redirect_In_Zone2("marissa2", true); - perform_SamlInvitation_Automatic_Redirect_In_Zone2("marissa3",false); - perform_SamlInvitation_Automatic_Redirect_In_Zone2("marissa3",false); + perform_SamlInvitation_Automatic_Redirect_In_Zone2("marissa3", false); + perform_SamlInvitation_Automatic_Redirect_In_Zone2("marissa3", false); perform_SamlInvitation_Automatic_Redirect_In_Zone2("marissa3", false); } @@ -337,6 +342,7 @@ public void perform_SamlInvitation_Automatic_Redirect_In_Zone2(String username, //ensure we are able to resolve DNS for hostname testzone1.localhost assumeTrue("Expected testzone1/2.localhost to resolve to 127.0.0.1", doesSupportZoneDNS()); String zoneId = "testzone2"; + String zoneUrl = baseUrl.replace("localhost",zoneId+".localhost"); //identity client token RestTemplate identityClient = IntegrationTestUtils.getClientCredentialsTempate( @@ -344,7 +350,7 @@ public void perform_SamlInvitation_Automatic_Redirect_In_Zone2(String username, ); //admin client token - to create users RestTemplate adminClient = IntegrationTestUtils.getClientCredentialsTempate( - IntegrationTestUtils.getClientCredentialsResource(baseUrl,new String[0] , "admin", "adminsecret") + IntegrationTestUtils.getClientCredentialsResource(baseUrl, new String[0], "admin", "adminsecret") ); //create the zone IntegrationTestUtils.createZoneOrUpdateSubdomain(identityClient, baseUrl, zoneId, zoneId); @@ -388,7 +394,6 @@ public void perform_SamlInvitation_Automatic_Redirect_In_Zone2(String username, uaaAdmin.setClientSecret("adminsecret"); IntegrationTestUtils.createOrUpdateClient(zoneAdminToken, baseUrl, zoneId, uaaAdmin); - String zoneUrl = baseUrl.replace("localhost", "testzone2.localhost"); String uaaAdminToken = testClient.getOAuthAccessToken(zoneUrl, "admin", "adminsecret", "client_credentials", ""); String useremail = username + "@test.org"; @@ -415,6 +420,15 @@ public void perform_SamlInvitation_Automatic_Redirect_In_Zone2(String username, webDriver.get("http://simplesamlphp.cfapps.io/module.php/core/authenticate.php?as=example-userpass&logout"); } + protected boolean isMember(String userId, ScimGroup group) { + for (ScimGroupMember member : group.getMembers()) { + if(userId.equals(member.getMemberId())) { + return true; + } + } + return false; + } + @Test public void testSamlLoginClientIDPAuthorizationAutomaticRedirectInZone1() throws Exception { //ensure we are able to resolve DNS for hostname testzone1.localhost @@ -485,6 +499,104 @@ public void testSamlLoginClientIDPAuthorizationAutomaticRedirectInZone1() throws } + @Test + public void testSamlLogin_Map_Groups_In_Zone1() throws Exception { + //ensure we are able to resolve DNS for hostname testzone1.localhost + assumeTrue("Expected testzone1/2.localhost to resolve to 127.0.0.1", doesSupportZoneDNS()); + String zoneId = "testzone1"; + String zoneUrl = baseUrl.replace("localhost", "testzone1.localhost"); + + //identity client token + RestTemplate identityClient = IntegrationTestUtils.getClientCredentialsTempate( + IntegrationTestUtils.getClientCredentialsResource(baseUrl,new String[] {"zones.write", "zones.read", "scim.zones"}, "identity", "identitysecret") + ); + //admin client token - to create users + RestTemplate adminClient = IntegrationTestUtils.getClientCredentialsTempate( + IntegrationTestUtils.getClientCredentialsResource(baseUrl,new String[0] , "admin", "adminsecret") + ); + //create the zone + IntegrationTestUtils.createZoneOrUpdateSubdomain(identityClient, baseUrl, zoneId, zoneId); + + //create a zone admin user + String email = new RandomValueStringGenerator().generate() +"@samltesting.org"; + ScimUser user = IntegrationTestUtils.createUser(adminClient, baseUrl,email ,"firstname", "lastname", email, true); + IntegrationTestUtils.makeZoneAdmin(identityClient, baseUrl, user.getId(), zoneId); + + //get the zone admin token + String zoneAdminToken = + IntegrationTestUtils.getAuthorizationCodeToken(serverRunning, + UaaTestAccounts.standard(serverRunning), + "identity", + "identitysecret", + email, + "secr3T"); + + SamlIdentityProviderDefinition samlIdentityProviderDefinition = createTestZone1IDP("simplesamlphp"); + samlIdentityProviderDefinition.addAttributeMapping(ExternalIdentityProviderDefinition.GROUP_ATTRIBUTE_NAME, "groups"); + samlIdentityProviderDefinition.addWhiteListedGroup("saml.user"); + samlIdentityProviderDefinition.addWhiteListedGroup("saml.admin"); + + IdentityProvider provider = new IdentityProvider(); + provider.setIdentityZoneId(zoneId); + provider.setType(Origin.SAML); + provider.setActive(true); + provider.setConfig(JsonUtils.writeValueAsString(samlIdentityProviderDefinition)); + provider.setOriginKey(samlIdentityProviderDefinition.getIdpEntityAlias()); + provider.setName("simplesamlphp for testzone1"); + + provider = IntegrationTestUtils.createOrUpdateProvider(zoneAdminToken,baseUrl,provider); + assertEquals(provider.getOriginKey(), provider.getConfigValue(SamlIdentityProviderDefinition.class).getIdpEntityAlias()); + + List idps = Arrays.asList(provider.getOriginKey()); + + String adminClientInZone = new RandomValueStringGenerator().generate(); + BaseClientDetails clientDetails = new BaseClientDetails(adminClientInZone, null, "openid", "authorization_code,client_credentials", "uaa.admin,scim.read,scim.write", zoneUrl); + clientDetails.setClientSecret("secret"); + clientDetails.addAdditionalInformation(ClientConstants.AUTO_APPROVE, true); + clientDetails.addAdditionalInformation(ClientConstants.ALLOWED_PROVIDERS, idps); + + clientDetails = IntegrationTestUtils.createClientAsZoneAdmin(zoneAdminToken, baseUrl, zoneId, clientDetails); + String adminTokenInZone = IntegrationTestUtils.getClientCredentialsToken(zoneUrl,clientDetails.getClientId(), "secret"); + + + ScimGroup uaaSamlUserGroup = new ScimGroup(null, "uaa.saml.user", zoneId); + uaaSamlUserGroup = IntegrationTestUtils.createOrUpdateGroup(adminTokenInZone, null, zoneUrl, uaaSamlUserGroup); + + ScimGroup uaaSamlAdminGroup = new ScimGroup(null, "uaa.saml.admin", zoneId); + uaaSamlAdminGroup = IntegrationTestUtils.createOrUpdateGroup(adminTokenInZone, null, zoneUrl, uaaSamlAdminGroup); + + ScimGroupExternalMember uaaSamlUserMapping = new ScimGroupExternalMember(uaaSamlUserGroup.getId(), "saml.user"); + uaaSamlUserMapping.setOrigin(provider.getOriginKey()); + ScimGroupExternalMember uaaSamlAdminMapping = new ScimGroupExternalMember(uaaSamlAdminGroup.getId(), "saml.admin"); + uaaSamlAdminMapping.setOrigin(provider.getOriginKey()); + IntegrationTestUtils.mapExternalGroup(zoneAdminToken, zoneId, baseUrl, uaaSamlUserMapping); + IntegrationTestUtils.mapExternalGroup(zoneAdminToken, zoneId, baseUrl, uaaSamlAdminMapping); + + webDriver.get(zoneUrl + "/logout.do"); + + String authUrl = zoneUrl + "/oauth/authorize?client_id=" + clientDetails.getClientId() + "&redirect_uri=" + URLEncoder.encode(zoneUrl) + "&response_type=code&state=8tp0tR"; + webDriver.get(authUrl); + //we should now be in the Simple SAML PHP site + webDriver.findElement(By.xpath("//h2[contains(text(), 'Enter your username and password')]")); + webDriver.findElement(By.name("username")).clear(); + webDriver.findElement(By.name("username")).sendKeys("marissa4"); + webDriver.findElement(By.name("password")).sendKeys("saml2"); + webDriver.findElement(By.xpath("//input[@value='Login']")).click(); + + assertThat(webDriver.findElement(By.cssSelector("h1")).getText(), Matchers.containsString("Where to?")); + webDriver.get(baseUrl + "/logout.do"); + webDriver.get(zoneUrl + "/logout.do"); + + //validate that the groups were mapped + String samlUserId = IntegrationTestUtils.getUserId(adminTokenInZone, zoneUrl, provider.getOriginKey(), "marissa4@test.org"); + uaaSamlUserGroup = IntegrationTestUtils.getGroup(adminTokenInZone, null, zoneUrl, "uaa.saml.user"); + uaaSamlAdminGroup = IntegrationTestUtils.getGroup(adminTokenInZone, null, zoneUrl, "uaa.saml.admin"); + assertTrue(isMember(samlUserId, uaaSamlUserGroup)); + assertTrue(isMember(samlUserId, uaaSamlAdminGroup)); + + } + + @Test public void testSimpleSamlPhpLoginInTestZone1Works() throws Exception { assumeTrue("Expected testzone1/2.localhost to resolve to 127.0.0.1", doesSupportZoneDNS()); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/util/IntegrationTestUtils.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/util/IntegrationTestUtils.java index 86b3aa26677..db55a567eee 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/util/IntegrationTestUtils.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/util/IntegrationTestUtils.java @@ -19,7 +19,9 @@ import org.apache.http.client.HttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.cloudfoundry.identity.uaa.ServerRunning; +import org.cloudfoundry.identity.uaa.rest.SearchResults; import org.cloudfoundry.identity.uaa.scim.ScimGroup; +import org.cloudfoundry.identity.uaa.scim.ScimGroupExternalMember; import org.cloudfoundry.identity.uaa.scim.ScimGroupMember; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.test.UaaTestAccounts; @@ -33,6 +35,7 @@ import org.openqa.selenium.OutputType; import org.openqa.selenium.TakesScreenshot; import org.openqa.selenium.WebDriver; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -74,6 +77,13 @@ public class IntegrationTestUtils { + public static final DefaultResponseErrorHandler fiveHundredErrorHandler = new DefaultResponseErrorHandler(){ + @Override + protected boolean hasError(HttpStatus statusCode) { + return statusCode.is5xxServerError(); + } + }; + public static ClientCredentialsResourceDetails getClientCredentialsResource(String url, String[] scope, String clientId, @@ -108,12 +118,12 @@ public void handleError(ClientHttpResponse response) throws IOException { } public static ScimUser createUser(RestTemplate client, - String url, - String username, - String firstName, - String lastName, - String email, - boolean verified) { + String url, + String username, + String firstName, + String lastName, + String email, + boolean verified) { ScimUser user = new ScimUser(); user.setUserName(username); @@ -186,8 +196,8 @@ public static Map findAllGroups(RestTemplate client, } public static String findGroupId(RestTemplate client, - String url, - String groupName) { + String url, + String groupName) { //TODO - make more efficient query using filter "id eq \"value\"" Map map = findAllGroups(client, url); for (Map group : ((List)map.get("resources"))) { @@ -235,6 +245,121 @@ public static ScimGroup createOrUpdateGroup(RestTemplate client, } } + public static ScimGroup getGroup(String token, + String zoneId, + String url, + String displayName) { + RestTemplate template = new RestTemplate(); + MultiValueMap headers = new LinkedMultiValueMap<>(); + headers.add("Accept", MediaType.APPLICATION_JSON_VALUE); + headers.add("Authorization", "bearer " + token); + headers.add("Content-Type", MediaType.APPLICATION_JSON_VALUE); + if (StringUtils.hasText(zoneId)) { + headers.add(IdentityZoneSwitchingFilter.HEADER, zoneId); + } + ResponseEntity> findGroup = template.exchange( + url + "/Groups?filter=displayName eq \"{groupId}\"", + HttpMethod.GET, + new HttpEntity(headers), + new ParameterizedTypeReference>() {}, + displayName + ); + if (findGroup.getBody().getTotalResults()==0) { + return null; + } else { + return findGroup.getBody().getResources().iterator().next(); + } + } + + public static ScimGroup createGroup(String token, + String zoneId, + String url, + ScimGroup group) { + RestTemplate template = new RestTemplate(); + template.setErrorHandler(fiveHundredErrorHandler); + MultiValueMap headers = new LinkedMultiValueMap<>(); + headers.add("Accept", MediaType.APPLICATION_JSON_VALUE); + headers.add("Authorization", "bearer " + token); + headers.add("Content-Type", MediaType.APPLICATION_JSON_VALUE); + if (StringUtils.hasText(zoneId)) { + headers.add(IdentityZoneSwitchingFilter.HEADER, zoneId); + } + ResponseEntity createGroup = template.exchange( + url + "/Groups", + HttpMethod.POST, + new HttpEntity(JsonUtils.writeValueAsBytes(group),headers), + ScimGroup.class + ); + assertEquals(HttpStatus.CREATED, createGroup.getStatusCode()); + return createGroup.getBody(); + } + + public static ScimGroup updateGroup(String token, + String zoneId, + String url, + ScimGroup group) { + RestTemplate template = new RestTemplate(); + MultiValueMap headers = new LinkedMultiValueMap<>(); + headers.add("Accept", MediaType.APPLICATION_JSON_VALUE); + headers.add("Authorization", "bearer " + token); + headers.add("If-Match", "*"); + headers.add("Content-Type", MediaType.APPLICATION_JSON_VALUE); + if (StringUtils.hasText(zoneId)) { + headers.add(IdentityZoneSwitchingFilter.HEADER, zoneId); + } + ResponseEntity updateGroup = template.exchange( + url + "/Groups/{groupId}", + HttpMethod.PUT, + new HttpEntity(JsonUtils.writeValueAsBytes(group),headers), + ScimGroup.class, + group.getId() + ); + assertEquals(HttpStatus.OK, updateGroup.getStatusCode()); + return updateGroup.getBody(); + } + + public static ScimGroup createOrUpdateGroup(String token, + String zoneId, + String url, + ScimGroup scimGroup) { + + ScimGroup existing = getGroup(token, zoneId, url, scimGroup.getDisplayName()); + if (existing==null) { + return createGroup(token, zoneId, url, scimGroup); + } else { + scimGroup.setId(existing.getId()); + return updateGroup(token, zoneId, url, scimGroup); + } + + } + + public static ScimGroupExternalMember mapExternalGroup(String token, + String zoneId, + String url, + ScimGroupExternalMember scimGroup) { + + RestTemplate template = new RestTemplate(); + MultiValueMap headers = new LinkedMultiValueMap<>(); + headers.add("Accept", MediaType.APPLICATION_JSON_VALUE); + headers.add("Authorization", "bearer " + token); + headers.add("Content-Type", MediaType.APPLICATION_JSON_VALUE); + if (StringUtils.hasText(zoneId)) { + headers.add(IdentityZoneSwitchingFilter.HEADER, zoneId); + } + ResponseEntity mapGroup = template.exchange( + url + "/Groups/External", + HttpMethod.POST, + new HttpEntity(JsonUtils.writeValueAsBytes(scimGroup), headers), + ScimGroupExternalMember.class + ); + if (HttpStatus.CREATED.equals(mapGroup.getStatusCode())) { + return mapGroup.getBody(); + } else if (HttpStatus.CONFLICT.equals(mapGroup.getStatusCode())) { + return scimGroup; + } + throw new IllegalArgumentException("Invalid status code:"+mapGroup.getStatusCode()); + } + public static IdentityZone createZoneOrUpdateSubdomain(RestTemplate client, String url, String id, @@ -450,6 +575,33 @@ public static IdentityZone fixtureIdentityZone(String id, String subdomain) { return identityZone; } + public static String getClientCredentialsToken(String baseUrl, + String clientId, + String clientSecret) throws Exception { + RestTemplate template = new RestTemplate(); + template.setRequestFactory(new StatelessRequestFactory()); + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("grant_type", "client_credentials"); + formData.add("client_id", clientId); + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("Authorization", "Basic " + new String(Base64.encode(String.format("%s:%s", clientId, clientSecret).getBytes()))); + + @SuppressWarnings("rawtypes") + ResponseEntity response = template.exchange( + baseUrl + "/oauth/token", + HttpMethod.POST, + new HttpEntity(formData, headers), + Map.class); + + Assert.assertEquals(HttpStatus.OK, response.getStatusCode()); + + @SuppressWarnings("unchecked") + OAuth2AccessToken accessToken = DefaultOAuth2AccessToken.valueOf(response.getBody()); + return accessToken.getValue(); + } + public static String getClientCredentialsToken(ServerRunning serverRunning, String clientId, String clientSecret) throws Exception { From 8d50bb7ede5fd80bb5bc341b2983cbc1a0ed7561 Mon Sep 17 00:00:00 2001 From: Mike Todd Date: Wed, 7 Oct 2015 10:25:04 +1300 Subject: [PATCH 042/103] Set the template abandonedtimeout to an integer abandonedtimeout should be set to an integer, not a boolean. With it set to a boolean startup fails and no migrations run. --- uaa/src/main/resources/uaa.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uaa/src/main/resources/uaa.yml b/uaa/src/main/resources/uaa.yml index bb1f42840de..293b8a53c26 100755 --- a/uaa/src/main/resources/uaa.yml +++ b/uaa/src/main/resources/uaa.yml @@ -17,7 +17,7 @@ # maxidle: 10 # removeabandoned: false # logabandoned: true -# abandonedtimeout: true +# abandonedtimeout: 300 # evictionintervalms: 15000 # caseinsensitive: false From 144415a9192267bbe6e8ed23c7142896ce133310 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Tue, 6 Oct 2015 15:10:26 -0600 Subject: [PATCH 043/103] Migrate all identity_zone.subdomain values in the DB to lowercase. Some databases are case sensitives, but subdomains should not be. For existing databases that may have duplicates based on case, will have a breaking change. This is highly unlikely though. https://www.pivotaltracker.com/story/show/101469422 [#101469422] --- .../db/StoreSubDomainAsLowerCase_V2_7_3.java | 94 +++++++++++ ...__Migrate_Zone_Subdomain_To_Lowercase.java | 21 +++ ...__Migrate_Zone_Subdomain_To_Lowercase.java | 21 +++ ...__Migrate_Zone_Subdomain_To_Lowercase.java | 21 +++ .../zone/JdbcIdentityZoneProvisioning.java | 17 +- ...toreSubDomainAsLowerCase_V2_7_3_Tests.java | 147 ++++++++++++++++++ .../zone/IdentityZoneResolvingFilterTest.java | 61 ++++---- .../JdbcIdentityZoneProvisioningTests.java | 72 +++++++-- .../uaa/zone/MultitenancyFixture.java | 2 +- .../IdentityZoneEndpointsMockMvcTests.java | 10 +- 10 files changed, 410 insertions(+), 56 deletions(-) create mode 100644 common/src/main/java/org/cloudfoundry/identity/uaa/db/StoreSubDomainAsLowerCase_V2_7_3.java create mode 100644 common/src/main/java/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_3__Migrate_Zone_Subdomain_To_Lowercase.java create mode 100644 common/src/main/java/org/cloudfoundry/identity/uaa/db/mysql/V2_7_3__Migrate_Zone_Subdomain_To_Lowercase.java create mode 100644 common/src/main/java/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_3__Migrate_Zone_Subdomain_To_Lowercase.java create mode 100644 common/src/test/java/org/cloudfoundry/identity/uaa/db/StoreSubDomainAsLowerCase_V2_7_3_Tests.java diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/db/StoreSubDomainAsLowerCase_V2_7_3.java b/common/src/main/java/org/cloudfoundry/identity/uaa/db/StoreSubDomainAsLowerCase_V2_7_3.java new file mode 100644 index 00000000000..0506ec0c96e --- /dev/null +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/db/StoreSubDomainAsLowerCase_V2_7_3.java @@ -0,0 +1,94 @@ +/* + * ***************************************************************************** + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + * ***************************************************************************** + */ + +package org.cloudfoundry.identity.uaa.db; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneProvisioning; +import org.cloudfoundry.identity.uaa.zone.JdbcIdentityZoneProvisioning; +import org.flywaydb.core.api.migration.spring.SpringJdbcMigration; +import org.flywaydb.core.internal.util.StringUtils; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class StoreSubDomainAsLowerCase_V2_7_3 implements SpringJdbcMigration { + + Log logger = LogFactory.getLog(StoreSubDomainAsLowerCase_V2_7_3.class); + + @Override + public synchronized void migrate(JdbcTemplate jdbcTemplate) throws Exception { + RandomValueStringGenerator generator = new RandomValueStringGenerator(3); + IdentityZoneProvisioning provisioning = new JdbcIdentityZoneProvisioning(jdbcTemplate); + Map> zones = new HashMap<>(); + Set duplicates = new HashSet<>(); + for (IdentityZone zone : provisioning.retrieveAll()) { + addToMap(zone, zones, duplicates); + } + for (String s : duplicates) { + logger.debug("Processing zone duplicates for subdomain:" + s); + List dupZones = zones.get(s); + for (int i=1; dupZones.size()>1 && i> zones, Set duplicates) { + if (zone==null || zone.getSubdomain()==null) { + return; + } + String subdomain = zone.getSubdomain().toLowerCase(); + if (zones.get(subdomain)==null) { + List list = new LinkedList<>(); + list.add(zone); + zones.put(subdomain, list); + } else { + logger.warn("Found duplicate zone for subdomain:"+subdomain); + duplicates.add(subdomain); + zones.get(subdomain).add(zone); + } + } + + +} diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_3__Migrate_Zone_Subdomain_To_Lowercase.java b/common/src/main/java/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_3__Migrate_Zone_Subdomain_To_Lowercase.java new file mode 100644 index 00000000000..243c0fabdbd --- /dev/null +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/db/hsqldb/V2_7_3__Migrate_Zone_Subdomain_To_Lowercase.java @@ -0,0 +1,21 @@ +/* + * ****************************************************************************** + * * Cloud Foundry + * * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + * * + * * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * * You may not use this product except in compliance with the License. + * * + * * This product includes a number of subcomponents with + * * separate copyright notices and license terms. Your use of these + * * subcomponents is subject to the terms and conditions of the + * * subcomponent's license, as noted in the LICENSE file. + * ****************************************************************************** + */ + +package org.cloudfoundry.identity.uaa.db.hsqldb; + +import org.cloudfoundry.identity.uaa.db.StoreSubDomainAsLowerCase_V2_7_3; + +public class V2_7_3__Migrate_Zone_Subdomain_To_Lowercase extends StoreSubDomainAsLowerCase_V2_7_3 { +} diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/db/mysql/V2_7_3__Migrate_Zone_Subdomain_To_Lowercase.java b/common/src/main/java/org/cloudfoundry/identity/uaa/db/mysql/V2_7_3__Migrate_Zone_Subdomain_To_Lowercase.java new file mode 100644 index 00000000000..2f2e5c0f7d5 --- /dev/null +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/db/mysql/V2_7_3__Migrate_Zone_Subdomain_To_Lowercase.java @@ -0,0 +1,21 @@ +/* + * ****************************************************************************** + * * Cloud Foundry + * * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + * * + * * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * * You may not use this product except in compliance with the License. + * * + * * This product includes a number of subcomponents with + * * separate copyright notices and license terms. Your use of these + * * subcomponents is subject to the terms and conditions of the + * * subcomponent's license, as noted in the LICENSE file. + * ****************************************************************************** + */ + +package org.cloudfoundry.identity.uaa.db.mysql; + +import org.cloudfoundry.identity.uaa.db.StoreSubDomainAsLowerCase_V2_7_3; + +public class V2_7_3__Migrate_Zone_Subdomain_To_Lowercase extends StoreSubDomainAsLowerCase_V2_7_3 { +} diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_3__Migrate_Zone_Subdomain_To_Lowercase.java b/common/src/main/java/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_3__Migrate_Zone_Subdomain_To_Lowercase.java new file mode 100644 index 00000000000..ea9c08bdd39 --- /dev/null +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/db/postgresql/V2_7_3__Migrate_Zone_Subdomain_To_Lowercase.java @@ -0,0 +1,21 @@ +/* + * ****************************************************************************** + * * Cloud Foundry + * * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + * * + * * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * * You may not use this product except in compliance with the License. + * * + * * This product includes a number of subcomponents with + * * separate copyright notices and license terms. Your use of these + * * subcomponents is subject to the terms and conditions of the + * * subcomponent's license, as noted in the LICENSE file. + * ****************************************************************************** + */ + +package org.cloudfoundry.identity.uaa.db.postgresql; + +import org.cloudfoundry.identity.uaa.db.StoreSubDomainAsLowerCase_V2_7_3; + +public class V2_7_3__Migrate_Zone_Subdomain_To_Lowercase extends StoreSubDomainAsLowerCase_V2_7_3 { +} diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityZoneProvisioning.java b/common/src/main/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityZoneProvisioning.java index 115e6e33b79..42784f6665a 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityZoneProvisioning.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityZoneProvisioning.java @@ -35,11 +35,11 @@ public class JdbcIdentityZoneProvisioning implements IdentityZoneProvisioning { public static final String CREATE_IDENTITY_ZONE_SQL = "insert into identity_zone(" + ID_ZONE_FIELDS + ") values (?,?,?,?,?,?,?)"; public static final String UPDATE_IDENTITY_ZONE_SQL = "update identity_zone set " + ID_ZONE_UPDATE_FIELDS + " where id=?"; - + public static final String IDENTITY_ZONES_QUERY = "select " + ID_ZONE_FIELDS + " from identity_zone "; public static final String IDENTITY_ZONE_BY_ID_QUERY = IDENTITY_ZONES_QUERY + "where id=?"; - + public static final String IDENTITY_ZONE_BY_SUBDOMAIN_QUERY = "select " + ID_ZONE_FIELDS + " from identity_zone " + "where subdomain=?"; protected final JdbcTemplate jdbcTemplate; @@ -60,7 +60,7 @@ public IdentityZone retrieve(String id) { throw new ZoneDoesNotExistsException("Zone["+id+"] not found.", x); } } - + @Override public List retrieveAll() { return jdbcTemplate.query(IDENTITY_ZONES_QUERY, mapper); @@ -68,7 +68,10 @@ public List retrieveAll() { @Override public IdentityZone retrieveBySubdomain(String subdomain) { - IdentityZone identityZone = jdbcTemplate.queryForObject(IDENTITY_ZONE_BY_SUBDOMAIN_QUERY, mapper, subdomain); + if (subdomain==null) { + throw new EmptyResultDataAccessException("Subdomain cannot be null", 1); + } + IdentityZone identityZone = jdbcTemplate.queryForObject(IDENTITY_ZONE_BY_SUBDOMAIN_QUERY, mapper, subdomain.toLowerCase()); return identityZone; } @@ -84,7 +87,7 @@ public void setValues(PreparedStatement ps) throws SQLException { ps.setTimestamp(3, new Timestamp(new Date().getTime())); ps.setTimestamp(4, new Timestamp(new Date().getTime())); ps.setString(5, identityZone.getName()); - ps.setString(6, identityZone.getSubdomain()); + ps.setString(6, identityZone.getSubdomain().toLowerCase()); ps.setString(7, identityZone.getDescription()); } }); @@ -105,7 +108,7 @@ public void setValues(PreparedStatement ps) throws SQLException { ps.setInt(1, identityZone.getVersion() + 1); ps.setTimestamp(2, new Timestamp(new Date().getTime())); ps.setString(3, identityZone.getName()); - ps.setString(4, identityZone.getSubdomain()); + ps.setString(4, identityZone.getSubdomain().toLowerCase()); ps.setString(5, identityZone.getDescription()); ps.setString(6, identityZone.getId().trim()); } @@ -117,7 +120,7 @@ public void setValues(PreparedStatement ps) throws SQLException { return retrieve(identityZone.getId()); } - private static final class IdentityZoneRowMapper implements RowMapper { + public static final class IdentityZoneRowMapper implements RowMapper { @Override public IdentityZone mapRow(ResultSet rs, int rowNum) throws SQLException { diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/db/StoreSubDomainAsLowerCase_V2_7_3_Tests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/db/StoreSubDomainAsLowerCase_V2_7_3_Tests.java new file mode 100644 index 00000000000..145ff53d27a --- /dev/null +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/db/StoreSubDomainAsLowerCase_V2_7_3_Tests.java @@ -0,0 +1,147 @@ +/* + * ***************************************************************************** + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + * ***************************************************************************** + */ + +package org.cloudfoundry.identity.uaa.db; + +import org.cloudfoundry.identity.uaa.test.JdbcTestBase; +import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneProvisioning; +import org.cloudfoundry.identity.uaa.zone.JdbcIdentityZoneProvisioning; +import org.cloudfoundry.identity.uaa.zone.MultitenancyFixture; +import org.junit.Before; +import org.junit.Test; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; + +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assume.assumeTrue; + +public class StoreSubDomainAsLowerCase_V2_7_3_Tests extends JdbcTestBase { + + private IdentityZoneProvisioning provisioning; + private StoreSubDomainAsLowerCase_V2_7_3 migration; + private RandomValueStringGenerator generator; + + @Before + public void setUpDuplicateZones() { + provisioning = new JdbcIdentityZoneProvisioning(jdbcTemplate); + migration = new StoreSubDomainAsLowerCase_V2_7_3(); + generator = new RandomValueStringGenerator(6); + } + + @Test + public void ensure_that_subdomains_get_lower_cased() throws Exception { + List subdomains = Arrays.asList( + "Zone1" + generator.generate(), + "Zone2" + generator.generate(), + "Zone3" + generator.generate(), + "Zone4+generator.generate()" + ); + + for (String subdomain : subdomains) { + IdentityZone zone = MultitenancyFixture.identityZone(subdomain, subdomain); + IdentityZone created = provisioning.create(zone); + assertEquals(subdomain.toLowerCase(), created.getSubdomain()); + jdbcTemplate.update("UPDATE identity_zone SET subdomain = ? WHERE id = ?", subdomain, subdomain); + assertEquals(subdomain, jdbcTemplate.queryForObject("SELECT subdomain FROM identity_zone where id = ?", String.class, subdomain)); + } + + migration.migrate(jdbcTemplate); + for (String subdomain : subdomains) { + for (IdentityZone zone : + Arrays.asList( + provisioning.retrieve(subdomain), + provisioning.retrieveBySubdomain(subdomain.toLowerCase()), + provisioning.retrieveBySubdomain(subdomain) + ) + ) { + assertNotNull(zone); + assertEquals(subdomain, zone.getId()); + assertEquals(subdomain.toLowerCase(), zone.getSubdomain()); + } + } + } + + @Test + public void test_duplicate_subdomains() throws Exception { + check_db_is_case_sensitive(); + List ids = Arrays.asList( + "id1"+generator.generate().toLowerCase(), + "id2"+generator.generate().toLowerCase(), + "id3"+generator.generate().toLowerCase(), + "id4"+generator.generate().toLowerCase(), + "id5"+generator.generate().toLowerCase() + ); + List subdomains = Arrays.asList( + "domain1", + "Domain1", + "doMain1", + "domain4"+generator.generate().toLowerCase(), + "domain5"+generator.generate().toLowerCase() + ); + for (int i=0; i { + ps.setString(1, identityZone.getId().trim()); + ps.setInt(2, identityZone.getVersion()); + ps.setTimestamp(3, new Timestamp(new Date().getTime())); + ps.setTimestamp(4, new Timestamp(new Date().getTime())); + ps.setString(5, identityZone.getName()); + ps.setString(6, identityZone.getSubdomain()); + ps.setString(7, identityZone.getDescription()); + }); + } +} \ No newline at end of file diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneResolvingFilterTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneResolvingFilterTest.java index e18381d17b1..5b3b010d08f 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneResolvingFilterTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneResolvingFilterTest.java @@ -1,31 +1,35 @@ package org.cloudfoundry.identity.uaa.zone; -import static org.junit.Assert.*; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.sql.SQLNonTransientConnectionException; -import java.util.Arrays; -import java.util.HashSet; +import org.cloudfoundry.identity.uaa.test.JdbcTestBase; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.mock.web.MockFilterChain; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; -import org.junit.Test; -import org.mockito.Mockito; -import org.springframework.dao.EmptyResultDataAccessException; -import org.springframework.jdbc.CannotGetJdbcConnectionException; -import org.springframework.mock.web.MockFilterChain; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; -public class IdentityZoneResolvingFilterTest { +public class IdentityZoneResolvingFilterTest extends JdbcTestBase { private boolean wasFilterExecuted = false; + private IdentityZoneProvisioning dao; + + @Before + public void createDao() { + dao = new JdbcIdentityZoneProvisioning(jdbcTemplate); + } @Test public void holderIsSetWithDefaultIdentityZone() { @@ -51,9 +55,6 @@ public void holderIsSetWithUAAIdentityZone() throws Exception { @Test public void doNotThrowException_InCase_RetrievingZoneFails() throws Exception { - IdentityZoneProvisioning dao = Mockito.mock(IdentityZoneProvisioning.class); - when(dao.retrieveBySubdomain(anyString())).thenThrow(new CannotGetJdbcConnectionException("blah", new SQLNonTransientConnectionException())); - MockHttpServletRequest request = new MockHttpServletRequest(); String incomingSubdomain = "not_a_zone"; String uaaHostname = "uaa.mycf.com"; @@ -67,21 +68,25 @@ public void doNotThrowException_InCase_RetrievingZoneFails() throws Exception { filter.setAdditionalInternalHostnames(new HashSet<>(Arrays.asList(uaaHostname))); filter.doFilter(request, response, chain); - assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, response.getStatus()); + assertEquals(HttpServletResponse.SC_NOT_FOUND, response.getStatus()); assertEquals(IdentityZone.getUaa(), IdentityZoneHolder.get()); Mockito.verifyZeroInteractions(chain); } - private void assertFindsCorrectSubdomain(final String expectedSubdomain, final String incomingHostname, String... additionalInternalHostnames) throws ServletException, IOException { - + private void assertFindsCorrectSubdomain(final String subDomainInput, final String incomingHostname, String... additionalInternalHostnames) throws ServletException, IOException { + final String expectedSubdomain = subDomainInput.toLowerCase(); IdentityZoneResolvingFilter filter = new IdentityZoneResolvingFilter(); - IdentityZoneProvisioning dao = Mockito.mock(IdentityZoneProvisioning.class); filter.setIdentityZoneProvisioning(dao); filter.setAdditionalInternalHostnames(new HashSet<>(Arrays.asList(additionalInternalHostnames))); - IdentityZone identityZone = new IdentityZone(); - identityZone.setSubdomain(expectedSubdomain); - when(dao.retrieveBySubdomain(Mockito.eq(expectedSubdomain))).thenReturn(identityZone); + IdentityZone identityZone = MultitenancyFixture.identityZone(subDomainInput, subDomainInput); + identityZone.setSubdomain(subDomainInput); + try { + identityZone = dao.create(identityZone); + } catch (ZoneAlreadyExistsException x) { + identityZone = dao.retrieveBySubdomain(subDomainInput); + } + assertEquals(expectedSubdomain, identityZone.getSubdomain()); MockHttpServletRequest request = new MockHttpServletRequest(); request.setServerName(incomingHostname); @@ -97,7 +102,6 @@ public void doFilter(ServletRequest request, ServletResponse response) throws IO filter.doFilter(request, response, filterChain); assertTrue(wasFilterExecuted); - Mockito.verify(dao).retrieveBySubdomain(Mockito.eq(expectedSubdomain)); assertEquals(IdentityZone.getUaa(), IdentityZoneHolder.get()); } @@ -108,14 +112,13 @@ public void holderIsNotSetWithNonMatchingIdentityZone() throws Exception { String incomingHostname = incomingSubdomain+"."+uaaHostname; IdentityZoneResolvingFilter filter = new IdentityZoneResolvingFilter(); - IdentityZoneProvisioning dao = Mockito.mock(IdentityZoneProvisioning.class); + FilterChain chain = Mockito.mock(FilterChain.class); filter.setIdentityZoneProvisioning(dao); filter.setAdditionalInternalHostnames(new HashSet<>(Arrays.asList(uaaHostname))); IdentityZone identityZone = new IdentityZone(); identityZone.setSubdomain(incomingSubdomain); - when(dao.retrieveBySubdomain(Mockito.eq(incomingSubdomain))).thenThrow(new EmptyResultDataAccessException(1)); MockHttpServletRequest request = new MockHttpServletRequest(); request.setServerName(incomingHostname); diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityZoneProvisioningTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityZoneProvisioningTests.java index 6007b1ecafb..36d3c8ede12 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityZoneProvisioningTests.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/zone/JdbcIdentityZoneProvisioningTests.java @@ -1,18 +1,18 @@ package org.cloudfoundry.identity.uaa.zone; -import java.util.UUID; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; import org.cloudfoundry.identity.uaa.test.JdbcTestBase; import org.junit.Before; import org.junit.Test; +import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + public class JdbcIdentityZoneProvisioningTests extends JdbcTestBase { private JdbcIdentityZoneProvisioning db; - + private RandomValueStringGenerator generator = new RandomValueStringGenerator(8); @Before public void createDatasource() throws Exception { db = new JdbcIdentityZoneProvisioning(jdbcTemplate); @@ -20,8 +20,8 @@ public void createDatasource() throws Exception { @Test public void testCreateIdentityZone() throws Exception { - IdentityZone identityZone = MultitenancyFixture.identityZone(UUID.randomUUID().toString(),UUID.randomUUID().toString()); - identityZone.setId(UUID.randomUUID().toString()); + IdentityZone identityZone = MultitenancyFixture.identityZone(generator.generate(),generator.generate()); + identityZone.setId(generator.generate()); IdentityZone createdIdZone = db.create(identityZone); @@ -31,10 +31,30 @@ public void testCreateIdentityZone() throws Exception { assertEquals(identityZone.getDescription(), createdIdZone.getDescription()); } + @Test + public void testCreateIdentityZone_Subdomain_Becomes_LowerCase() throws Exception { + String subdomain = generator.generate().toUpperCase(); + IdentityZone identityZone = MultitenancyFixture.identityZone(generator.generate(),subdomain); + identityZone.setId(generator.generate()); + + identityZone.setSubdomain(subdomain); + IdentityZone createdIdZone = db.create(identityZone); + + assertEquals(identityZone.getId(), createdIdZone.getId()); + assertEquals(subdomain.toLowerCase(), createdIdZone.getSubdomain()); + assertEquals(identityZone.getName(), createdIdZone.getName()); + assertEquals(identityZone.getDescription(), createdIdZone.getDescription()); + } + + @Test(expected = EmptyResultDataAccessException.class) + public void test_null_subdomain() { + db.retrieveBySubdomain(null); + } + @Test public void testUpdateIdentityZone() throws Exception { - IdentityZone identityZone = MultitenancyFixture.identityZone(UUID.randomUUID().toString(),UUID.randomUUID().toString()); - identityZone.setId(UUID.randomUUID().toString()); + IdentityZone identityZone = MultitenancyFixture.identityZone(generator.generate(), generator.generate()); + identityZone.setId(generator.generate()); IdentityZone createdIdZone = db.create(identityZone); @@ -50,22 +70,46 @@ public void testUpdateIdentityZone() throws Exception { IdentityZone updatedIdZone = db.update(createdIdZone); assertEquals(createdIdZone.getId(), updatedIdZone.getId()); - assertEquals(createdIdZone.getSubdomain(), updatedIdZone.getSubdomain()); + assertEquals(createdIdZone.getSubdomain().toLowerCase(), updatedIdZone.getSubdomain()); + assertEquals(createdIdZone.getName(), updatedIdZone.getName()); + assertEquals(createdIdZone.getDescription(), updatedIdZone.getDescription()); + } + + @Test + public void testUpdateIdentityZone_SubDomain_Is_LowerCase() throws Exception { + IdentityZone identityZone = MultitenancyFixture.identityZone(generator.generate(),generator.generate()); + identityZone.setId(generator.generate()); + + IdentityZone createdIdZone = db.create(identityZone); + + assertEquals(identityZone.getId(), createdIdZone.getId()); + assertEquals(identityZone.getSubdomain(), createdIdZone.getSubdomain()); + assertEquals(identityZone.getName(), createdIdZone.getName()); + assertEquals(identityZone.getDescription(), createdIdZone.getDescription()); + + String newDomain = new RandomValueStringGenerator().generate(); + createdIdZone.setSubdomain(newDomain.toUpperCase()); + createdIdZone.setDescription("new desc"); + createdIdZone.setName("new name"); + IdentityZone updatedIdZone = db.update(createdIdZone); + + assertEquals(createdIdZone.getId(), updatedIdZone.getId()); + assertEquals(createdIdZone.getSubdomain().toLowerCase(), updatedIdZone.getSubdomain()); assertEquals(createdIdZone.getName(), updatedIdZone.getName()); assertEquals(createdIdZone.getDescription(), updatedIdZone.getDescription()); } @Test(expected = ZoneDoesNotExistsException.class) public void testUpdateNonExistentIdentityZone() throws Exception { - IdentityZone identityZone = MultitenancyFixture.identityZone(UUID.randomUUID().toString(),UUID.randomUUID().toString()); - identityZone.setId(UUID.randomUUID().toString()); + IdentityZone identityZone = MultitenancyFixture.identityZone(generator.generate(),generator.generate()); + identityZone.setId(generator.generate()); db.update(identityZone); } @Test public void testCreateDuplicateIdentityZone() throws Exception { IdentityZone identityZone = MultitenancyFixture.identityZone("there-can-be-only-one","there-can-be-only-one"); - identityZone.setId(UUID.randomUUID().toString()); + identityZone.setId(generator.generate()); db.create(identityZone); try { db.create(identityZone); @@ -78,7 +122,7 @@ public void testCreateDuplicateIdentityZone() throws Exception { @Test public void testCreateDuplicateIdentityZoneSubdomain() throws Exception { IdentityZone identityZone = MultitenancyFixture.identityZone("there-can-be-only-one","there-can-be-only-one"); - identityZone.setId(UUID.randomUUID().toString()); + identityZone.setId(generator.generate()); db.create(identityZone); try { identityZone.setId(new RandomValueStringGenerator().generate()); diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/zone/MultitenancyFixture.java b/common/src/test/java/org/cloudfoundry/identity/uaa/zone/MultitenancyFixture.java index 8f29c33e069..964611a9b0f 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/zone/MultitenancyFixture.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/zone/MultitenancyFixture.java @@ -6,7 +6,7 @@ public class MultitenancyFixture { public static IdentityZone identityZone(String id, String subdomain) { IdentityZone identityZone = new IdentityZone(); identityZone.setId(id); - identityZone.setSubdomain(subdomain); + identityZone.setSubdomain(subdomain.toLowerCase()); identityZone.setName("The Twiglet Zone"); identityZone.setDescription("Like the Twilight Zone but tastier."); return identityZone; diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointsMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointsMockMvcTests.java index b3d2ad890e8..6dbbde74054 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointsMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointsMockMvcTests.java @@ -228,7 +228,7 @@ public void testCreateZone() throws Exception { String id = generator.generate(); IdentityZone zone = createZone(id, HttpStatus.CREATED, identityClientToken); assertEquals(id, zone.getId()); - assertEquals(id, zone.getSubdomain()); + assertEquals(id.toLowerCase(), zone.getSubdomain()); checkAuditEventListener(1, AuditEventType.IdentityZoneCreatedEvent, zoneModifiedEventListener, IdentityZone.getUaa().getId(), "http://localhost:8080/uaa/oauth/token", "identity"); } @@ -513,8 +513,8 @@ public void testCreatesZonesWithDuplicateSubdomains() throws Exception { @Test public void testZoneAdminTokenAgainstZoneEndpoints() throws Exception { - String zone1 = generator.generate(); - String zone2 = generator.generate(); + String zone1 = generator.generate().toLowerCase(); + String zone2 = generator.generate().toLowerCase(); IdentityZoneCreationResult result1 = MockMvcUtils.utils().createOtherIdentityZoneAndReturnResult(zone1, getMockMvc(), getWebApplicationContext(), null); IdentityZoneCreationResult result2 = MockMvcUtils.utils().createOtherIdentityZoneAndReturnResult(zone2, getMockMvc(), getWebApplicationContext(), null); @@ -567,7 +567,7 @@ public void testZoneAdminTokenAgainstZoneEndpoints() throws Exception { @Test public void testSuccessfulUserManagementInZoneUsingAdminClient() throws Exception { - String subdomain = generator.generate(); + String subdomain = generator.generate().toLowerCase(); BaseClientDetails adminClient = new BaseClientDetails("admin", null, null, "client_credentials", "scim.read,scim.write"); adminClient.setClientSecret("admin-secret"); IdentityZoneCreationResult creationResult = mockMvcUtils.createOtherIdentityZoneAndReturnResult(subdomain, getMockMvc(), getWebApplicationContext(), adminClient); @@ -681,7 +681,7 @@ public void userCanReadAZone_withZoneZoneIdReadToken() throws Exception { String scimWriteToken = testClient.getClientCredentialsOAuthAccessToken("admin", "adminsecret", "scim.write"); ScimUser user = createUser(scimWriteToken, null); - String id = generator.generate(); + String id = generator.generate().toLowerCase(); IdentityZone identityZone = createZone(id, HttpStatus.CREATED, identityClientToken); ScimGroup group = new ScimGroup(); From e2335db4874073832384c2fe25732d188cc54b90 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Tue, 6 Oct 2015 16:06:52 -0600 Subject: [PATCH 044/103] Specify how the subdomain field is handled (lowercased prior to storage) --- docs/UAA-APIs.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/UAA-APIs.rst b/docs/UAA-APIs.rst index 3aa7b7ad91f..4a2219a6c3f 100644 --- a/docs/UAA-APIs.rst +++ b/docs/UAA-APIs.rst @@ -648,6 +648,8 @@ A zone contains a unique identifier as well as a unique subdomain:: "last_modified":1426258488910 } +The subdomain field will be converted into lowercase upon creation or update of an identity zone. +This way the UAA has an easy way to query the database for a zone based on a hostname. The UAA by default creates a ``default zone``. This zone will always be present, the ID will always be 'uaa', and the subdomain is blank:: @@ -735,6 +737,9 @@ Curl Example POST (Token contains ``zones.write`` scope) :: -H"Content-Type:application/json" \ -XPUT http://localhost:8080/uaa/identity-zones/testzone1 + +Note that if you specify a subdomain in mixed or upper case, it will be converted into lower case before +stored in the database. ================ ======================================================================================== Sequential example of creating a zone and creating an admin client in that zone From 8e1754dfb40e7d6f20c251136c9118a14c1b33bc Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Wed, 7 Oct 2015 09:25:02 -0600 Subject: [PATCH 045/103] Add in ability to handle multiple external group attributes https://www.pivotaltracker.com/story/show/99445992 [#99445992] --- .../saml/LoginSamlAuthenticationProvider.java | 4 +- .../LoginSamlAuthenticationProviderTests.java | 38 ++++++++++++++++++- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java b/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java index 8cf7aece3ed..76b17f7b5d6 100644 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java @@ -148,7 +148,7 @@ protected Collection mapAuthorities(String origin, S } public Collection retrieveSamlAuthorities(SamlIdentityProviderDefinition definition, SAMLCredential credential) { - Collection authorities = null; + Collection authorities = new ArrayList<>(); if (definition.getAttributeMappings().get(GROUP_ATTRIBUTE_NAME)!=null) { List groupNames = new LinkedList<>(); if (definition.getAttributeMappings().get(GROUP_ATTRIBUTE_NAME) instanceof String) { @@ -159,12 +159,10 @@ public Collection retrieveSamlAuthorities(SamlIdenti for (Attribute attribute : credential.getAttributes()) { if ((groupNames.contains(attribute.getName())) || (groupNames.contains(attribute.getFriendlyName()))) { if (attribute.getAttributeValues() != null && attribute.getAttributeValues().size() > 0) { - authorities = new ArrayList<>(); for (XMLObject group : attribute.getAttributeValues()) { authorities.add(new SamlUserAuthority(((XSString) group).getValue())); } } - break; } } } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java index 07aab8d81fd..d4aeaaa62a8 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java @@ -57,9 +57,11 @@ import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -73,8 +75,10 @@ public class LoginSamlAuthenticationProviderTests extends JdbcTestBase { public static final String SAML_USER = "saml.user"; public static final String SAML_ADMIN = "saml.admin"; + public static final String SAML_TEST = "saml.test"; public static final String UAA_SAML_USER = "uaa.saml.user"; public static final String UAA_SAML_ADMIN = "uaa.saml.admin"; + public static final String UAA_SAML_TEST = "uaa.saml.test"; IdentityProviderProvisioning providerProvisioning; ApplicationEventPublisher publisher; JdbcUaaUserDatabase userDatabase; @@ -85,6 +89,13 @@ public class LoginSamlAuthenticationProviderTests extends JdbcTestBase { SamlIdentityProviderDefinition providerDefinition = new SamlIdentityProviderDefinition(); private IdentityProvider provider; + public List getAttributes(Map> values) { + List result = new LinkedList<>(); + for (Map.Entry> entry : values.entrySet()) { + result.addAll(getAttributes(entry.getKey(), entry.getValue())); + } + return result; + } public List getAttributes(final String name, List values) { Attribute attribute = mock(Attribute.class); when(attribute.getName()).thenReturn(name); @@ -107,6 +118,7 @@ public void configureProvider() throws Exception { ScimGroup uaaSamlUser = groupProvisioning.create(new ScimGroup(null,UAA_SAML_USER,IdentityZone.getUaa().getId())); ScimGroup uaaSamlAdmin = groupProvisioning.create(new ScimGroup(null,UAA_SAML_ADMIN,IdentityZone.getUaa().getId())); + ScimGroup uaaSamlTest = groupProvisioning.create(new ScimGroup(null,UAA_SAML_TEST,IdentityZone.getUaa().getId())); JdbcScimGroupMembershipManager membershipManager = new JdbcScimGroupMembershipManager(jdbcTemplate, new JdbcPagingListFactory(jdbcTemplate, limitSqlAdapter)); membershipManager.setScimGroupProvisioning(groupProvisioning); @@ -117,16 +129,20 @@ public void configureProvider() throws Exception { externalManager.setScimGroupProvisioning(groupProvisioning); externalManager.mapExternalGroup(uaaSamlUser.getId(), SAML_USER, Origin.SAML); externalManager.mapExternalGroup(uaaSamlAdmin.getId(), SAML_ADMIN, Origin.SAML); + externalManager.mapExternalGroup(uaaSamlTest.getId(), SAML_TEST, Origin.SAML); String username = "marissa-saml"; NameID usernameID = mock(NameID.class); when(usernameID.getValue()).thenReturn(username); consumer = mock(WebSSOProfileConsumer.class); + Map> attributes = new HashMap<>(); + attributes.put("groups", Arrays.asList(SAML_USER,SAML_ADMIN)); + attributes.put("2ndgroups", Arrays.asList(SAML_TEST)); credential = new SAMLCredential( usernameID, mock(Assertion.class), "remoteEntityID", - getAttributes("groups", Arrays.asList(SAML_USER,SAML_ADMIN)), + getAttributes(attributes), "localEntityID"); when(consumer.processAuthenticationResponse(anyObject())).thenReturn(credential); @@ -161,6 +177,26 @@ public void testAuthenticateSimple() { authprovider.authenticate(mockSamlAuthentication(Origin.SAML)); } + @Test + public void test_multiple_white_listed_attributes() throws Exception { + providerDefinition.addAttributeMapping(ExternalIdentityProviderDefinition.GROUP_ATTRIBUTE_NAME, Arrays.asList("2ndgroups","groups")); + providerDefinition.addWhiteListedGroup(SAML_USER); + providerDefinition.addWhiteListedGroup(SAML_ADMIN); + providerDefinition.addWhiteListedGroup(SAML_TEST); + provider.setConfig(JsonUtils.writeValueAsString(providerDefinition)); + providerProvisioning.update(provider); + UaaAuthentication authentication = getAuthentication(); + assertEquals("Four authorities should have been granted!", 4, authentication.getAuthorities().size()); + assertThat(authentication.getAuthorities(), + Matchers.containsInAnyOrder( + new SimpleGrantedAuthority(UAA_SAML_ADMIN), + new SimpleGrantedAuthority(UAA_SAML_USER), + new SimpleGrantedAuthority(UAA_SAML_TEST), + new SimpleGrantedAuthority(UaaAuthority.UAA_USER.getAuthority()) + ) + ); + } + @Test public void test_white_listed_group() throws Exception { providerDefinition.addAttributeMapping(ExternalIdentityProviderDefinition.GROUP_ATTRIBUTE_NAME, "groups"); From f64f2df84edb8d1277523835af0c1d61c1348bc3 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Wed, 7 Oct 2015 08:36:14 -0600 Subject: [PATCH 046/103] When printing an error, also populate the message as a property called error_description https://www.pivotaltracker.com/story/show/104589140 [#104589140] --- .../ExceptionReportHttpMessageConverter.java | 25 +-- ...ceptionReportHttpMessageConverterTest.java | 2 + .../endpoints/PasswordResetEndpointTest.java | 6 +- ...erManagementSecurityFilterMockMvcTest.java | 161 +++++++++++------- .../ScimUserEndpointsMockMvcTests.java | 39 +++-- 5 files changed, 146 insertions(+), 87 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/error/ExceptionReportHttpMessageConverter.java b/common/src/main/java/org/cloudfoundry/identity/uaa/error/ExceptionReportHttpMessageConverter.java index db4e69e3c83..fab96aed164 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/error/ExceptionReportHttpMessageConverter.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/error/ExceptionReportHttpMessageConverter.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Cloud Foundry + * Cloud Foundry * Copyright (c) [2009-2014] Pivotal Software, Inc. All Rights Reserved. * * This product is licensed to you under the Apache License, Version 2.0 (the "License"). @@ -12,16 +12,6 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.error; -import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - import org.cloudfoundry.identity.uaa.util.UaaStringUtils; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; @@ -32,9 +22,19 @@ import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.web.client.RestTemplate; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + /** * @author Dave Syer - * + * */ public class ExceptionReportHttpMessageConverter extends AbstractHttpMessageConverter { @@ -96,6 +96,7 @@ protected void writeInternal(ExceptionReport report, HttpOutputMessage outputMes Map map = new HashMap<>(); map.put("error", UaaStringUtils.getErrorName(e)); map.put("message", e.getMessage()); + map.put("error_description", e.getMessage()); if (report.getExtraInfo() != null) { map.putAll(report.getExtraInfo()); } diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/error/ExceptionReportHttpMessageConverterTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/error/ExceptionReportHttpMessageConverterTest.java index cc81d5b923e..e6e508b9dc7 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/error/ExceptionReportHttpMessageConverterTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/error/ExceptionReportHttpMessageConverterTest.java @@ -46,6 +46,7 @@ public void testWriteInternal() throws Exception { Map expectedFields = new HashMap<>(); expectedFields.put("error", "exception"); expectedFields.put("message", "oh noes!"); + expectedFields.put("error_description", "oh noes!"); verify(httpMessageConverter).write(eq(expectedFields), eq(APPLICATION_JSON), eq(httpOutputMessage)); } @@ -61,6 +62,7 @@ public void testWriteInteralWithExtraInfo() throws Exception { Map expectedFields = new HashMap<>(); expectedFields.put("error", "exception"); expectedFields.put("message", "oh noes!"); + expectedFields.put("error_description", "oh noes!"); expectedFields.putAll(extraInfo); verify(httpMessageConverter).write(eq(expectedFields), eq(APPLICATION_JSON), eq(httpOutputMessage)); diff --git a/scim/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordResetEndpointTest.java b/scim/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordResetEndpointTest.java index 6610f1caaf5..94d5202aab9 100644 --- a/scim/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordResetEndpointTest.java +++ b/scim/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/PasswordResetEndpointTest.java @@ -23,6 +23,7 @@ import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; import org.cloudfoundry.identity.uaa.scim.exception.InvalidPasswordException; +import org.cloudfoundry.identity.uaa.scim.test.JsonObjectMatcherUtils; import org.cloudfoundry.identity.uaa.scim.validate.PasswordValidator; import org.cloudfoundry.identity.uaa.test.MockAuthentication; import org.cloudfoundry.identity.uaa.util.JsonUtils; @@ -41,7 +42,6 @@ import java.util.Arrays; import java.util.Date; -import org.cloudfoundry.identity.uaa.scim.test.JsonObjectMatcherUtils; import static org.hamcrest.Matchers.containsString; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; @@ -253,7 +253,7 @@ public void testPasswordsMustSatisfyPolicy() throws Exception { mockMvc.perform(post) .andExpect(status().isUnprocessableEntity()) - .andExpect(content().string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject().put("message", "Password flunks policy").put("error", "invalid_password")))); + .andExpect(content().string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject().put("error_description", "Password flunks policy").put("message", "Password flunks policy").put("error", "invalid_password")))); } @Test @@ -281,6 +281,6 @@ public void changePassword_Returns422UnprocessableEntity_NewPasswordSameAsOld() mockMvc.perform(post) .andExpect(status().isUnprocessableEntity()) - .andExpect(content().string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject().put("message", "Your new password cannot be the same as the old password.").put("error", "invalid_password")))); + .andExpect(content().string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject().put("error_description", "Your new password cannot be the same as the old password.").put("message", "Your new password cannot be the same as the old password.").put("error", "invalid_password")))); } } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/DisableUserManagementSecurityFilterMockMvcTest.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/DisableUserManagementSecurityFilterMockMvcTest.java index bb7cbe27755..3cda8e8ac98 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/DisableUserManagementSecurityFilterMockMvcTest.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/DisableUserManagementSecurityFilterMockMvcTest.java @@ -8,6 +8,7 @@ import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.endpoints.ChangeEmailEndpoints; import org.cloudfoundry.identity.uaa.scim.endpoints.PasswordChange; +import org.cloudfoundry.identity.uaa.scim.test.JsonObjectMatcherUtils; import org.cloudfoundry.identity.uaa.test.TestClient; import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.json.JSONObject; @@ -25,21 +26,18 @@ import java.util.HashMap; import java.util.Map; -import org.cloudfoundry.identity.uaa.scim.test.JsonObjectMatcherUtils; -import static org.junit.Assert.assertEquals; +import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.CookieCsrfPostProcessor; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.CookieCsrfPostProcessor; - @Component public class DisableUserManagementSecurityFilterMockMvcTest extends InjectedMockContextTest { @@ -75,9 +73,11 @@ public void userEndpointCreateNotAllowed() throws Exception { ResultActions result = createUser(); result.andExpect(status().isForbidden()) .andExpect(content() - .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() - .put("message", MESSAGE_TEXT) - .put("error", ERROR_TEXT)))); + .string(JsonObjectMatcherUtils.matchesJsonObject( + new JSONObject() + .put("error_description", MESSAGE_TEXT) + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @Test @@ -96,6 +96,7 @@ public void userEndpointUpdateNotAllowed() throws Exception { .andExpect(status().isForbidden()) .andExpect(content() .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() + .put("error_description", MESSAGE_TEXT) .put("message", MESSAGE_TEXT) .put("error", ERROR_TEXT)))); } @@ -117,9 +118,11 @@ public void userEndpointUpdatePasswordNotAllowed() throws Exception { .content(JsonUtils.writeValueAsString(request))) .andExpect(status().isForbidden()) .andExpect(content() - .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() - .put("message", MESSAGE_TEXT) - .put("error", ERROR_TEXT)))); + .string(JsonObjectMatcherUtils.matchesJsonObject( + new JSONObject() + .put("message", MESSAGE_TEXT) + .put("error_description", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @Test @@ -133,9 +136,11 @@ public void userEndpointDeleteNotAllowed() throws Exception { .header("Authorization", "Bearer " + token)) .andExpect(status().isForbidden()) .andExpect(content() - .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() - .put("message", MESSAGE_TEXT) - .put("error", ERROR_TEXT)))); + .string(JsonObjectMatcherUtils.matchesJsonObject( + new JSONObject() + .put("error_description", MESSAGE_TEXT) + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @Test @@ -151,9 +156,11 @@ public void userEndpointGetUsersNotAllowed() throws Exception { .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isForbidden()) .andExpect(content() - .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() - .put("message", MESSAGE_TEXT) - .put("error", ERROR_TEXT)))); + .string(JsonObjectMatcherUtils.matchesJsonObject( + new JSONObject() + .put("error_description", MESSAGE_TEXT) + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @Test @@ -167,9 +174,11 @@ public void userEndpointVerifyUsersNotAllowed() throws Exception { .header("Authorization", "Bearer " + token)) .andExpect(status().isForbidden()) .andExpect(content() - .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() - .put("message", MESSAGE_TEXT) - .put("error", ERROR_TEXT)))); + .string(JsonObjectMatcherUtils.matchesJsonObject( + new JSONObject() + .put("error_description", MESSAGE_TEXT) + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @Test @@ -178,9 +187,11 @@ public void accountsControllerCreateAccountNotAllowed() throws Exception { getMockMvc().perform(get("/create_account")) .andExpect(status().isForbidden()) .andExpect(content() - .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() - .put("message", MESSAGE_TEXT) - .put("error", ERROR_TEXT)))); + .string(JsonObjectMatcherUtils.matchesJsonObject( + new JSONObject() + .put("error_description", MESSAGE_TEXT) + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @Test @@ -193,9 +204,11 @@ public void accountsControllerSendActivationEmailNotAllowed() throws Exception { .param("password_confirmation", "foobar")) .andExpect(status().isForbidden()) .andExpect(content() - .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() - .put("message", MESSAGE_TEXT) - .put("error", ERROR_TEXT)))); + .string(JsonObjectMatcherUtils.matchesJsonObject( + new JSONObject() + .put("error_description", MESSAGE_TEXT) + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @Test @@ -204,9 +217,11 @@ public void accountsControllerEmailSentNotAllowed() throws Exception { getMockMvc().perform(get("/accounts/email_sent")) .andExpect(status().isForbidden()) .andExpect(content() - .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() - .put("message", MESSAGE_TEXT) - .put("error", ERROR_TEXT)))); + .string(JsonObjectMatcherUtils.matchesJsonObject( + new JSONObject() + .put("error_description", MESSAGE_TEXT) + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @Test @@ -224,9 +239,11 @@ public void accountsControllerVerifyUserNotAllowed() throws Exception { .param("code", getExpiringCode(codeData).getCode())) .andExpect(status().isForbidden()) .andExpect(content() - .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() - .put("message", MESSAGE_TEXT) - .put("error", ERROR_TEXT)))); + .string(JsonObjectMatcherUtils.matchesJsonObject( + new JSONObject() + .put("error_description", MESSAGE_TEXT) + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @Test @@ -242,9 +259,11 @@ public void changeEmailControllerChangeEmailPageNotAllowed() throws Exception { .accept(ACCEPT_TEXT_HTML)) .andExpect(status().isForbidden()) .andExpect(content() - .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() - .put("message", MESSAGE_TEXT) - .put("error", ERROR_TEXT)))); + .string(JsonObjectMatcherUtils.matchesJsonObject( + new JSONObject() + .put("error_description", MESSAGE_TEXT) + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @Test @@ -262,9 +281,11 @@ public void changeEmailControllerChangeEmailNotAllowed() throws Exception { .param("client_id", "login")) .andExpect(status().isForbidden()) .andExpect(content() - .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() - .put("message", MESSAGE_TEXT) - .put("error", ERROR_TEXT)))); + .string(JsonObjectMatcherUtils.matchesJsonObject( + new JSONObject() + .put("error_description", MESSAGE_TEXT) + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @@ -285,9 +306,11 @@ public void changeEmailControllerVerifyEmailNotAllowed() throws Exception { .param("code", code.getCode())) .andExpect(status().isForbidden()) .andExpect(content() - .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() - .put("message", MESSAGE_TEXT) - .put("error", ERROR_TEXT)))); + .string(JsonObjectMatcherUtils.matchesJsonObject( + new JSONObject() + .put("error_description", MESSAGE_TEXT) + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @@ -297,9 +320,11 @@ public void changePasswordControllerChangePasswordPageNotAllowed() throws Except getMockMvc().perform(get("/change_password")) .andExpect(status().isForbidden()) .andExpect(content() - .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() - .put("message", MESSAGE_TEXT) - .put("error", ERROR_TEXT)))); + .string(JsonObjectMatcherUtils.matchesJsonObject( + new JSONObject() + .put("error_description", MESSAGE_TEXT) + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @@ -319,9 +344,11 @@ public void changePasswordControllerChangePasswordNotAllowed() throws Exception .param("confirm_password", "whatever")) .andExpect(status().isForbidden()) .andExpect(content() - .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() - .put("message", MESSAGE_TEXT) - .put("error", ERROR_TEXT)))); + .string(JsonObjectMatcherUtils.matchesJsonObject( + new JSONObject() + .put("error_description", MESSAGE_TEXT) + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @@ -331,9 +358,11 @@ public void resetPasswordControllerForgotPasswordPageNotAllowed() throws Excepti getMockMvc().perform(get("/forgot_password")) .andExpect(status().isForbidden()) .andExpect(content() - .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() - .put("message", MESSAGE_TEXT) - .put("error", ERROR_TEXT)))); + .string(JsonObjectMatcherUtils.matchesJsonObject( + new JSONObject() + .put("error_description", MESSAGE_TEXT) + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @@ -344,9 +373,11 @@ public void resetPasswordControllerForgotPasswordNotAllowed() throws Exception { .param("email", "another@example.com")) .andExpect(status().isForbidden()) .andExpect(content() - .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() - .put("message", MESSAGE_TEXT) - .put("error", ERROR_TEXT)))); + .string(JsonObjectMatcherUtils.matchesJsonObject( + new JSONObject() + .put("error_description", MESSAGE_TEXT) + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @@ -356,9 +387,11 @@ public void resetPasswordControllerEmailSentPageNotAllowed() throws Exception { getMockMvc().perform(get("/email_sent")) .andExpect(status().isForbidden()) .andExpect(content() - .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() - .put("message", MESSAGE_TEXT) - .put("error", ERROR_TEXT)))); + .string(JsonObjectMatcherUtils.matchesJsonObject( + new JSONObject() + .put("error_description", MESSAGE_TEXT) + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @@ -370,9 +403,11 @@ public void resetPasswordControllerResetPasswordPageNotAllowed() throws Exceptio .param("email", "another@example.com")) .andExpect(status().isForbidden()) .andExpect(content() - .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() - .put("message", MESSAGE_TEXT) - .put("error", ERROR_TEXT)))); + .string(JsonObjectMatcherUtils.matchesJsonObject( + new JSONObject() + .put("error_description", MESSAGE_TEXT) + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } @@ -393,9 +428,11 @@ public void resetPasswordControllerResetPasswordNotAllowed() throws Exception { .with(csrf())) .andExpect(status().isForbidden()) .andExpect(content() - .string(JsonObjectMatcherUtils.matchesJsonObject(new JSONObject() - .put("message", MESSAGE_TEXT) - .put("error", ERROR_TEXT)))); + .string(JsonObjectMatcherUtils.matchesJsonObject( + new JSONObject() + .put("error_description", MESSAGE_TEXT) + .put("message", MESSAGE_TEXT) + .put("error", ERROR_TEXT)))); } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsMockMvcTests.java index 5817c3d29dc..f382830f1b5 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsMockMvcTests.java @@ -26,6 +26,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @@ -34,6 +35,7 @@ import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.utils; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -73,15 +75,7 @@ private ScimUser createUser(ScimUser user, String token, String subdomain) throw } private ScimUser createUser(ScimUser user, String token, String subdomain, String switchZone) throws Exception { - byte[] requestBody = JsonUtils.writeValueAsBytes(user); - MockHttpServletRequestBuilder post = post("/Users") - .header("Authorization", "Bearer " + token) - .contentType(APPLICATION_JSON) - .content(requestBody); - if (subdomain != null && !subdomain.equals("")) post.with(new SetServerNameRequestPostProcessor(subdomain + ".localhost")); - if (switchZone!=null) post.header(IdentityZoneSwitchingFilter.HEADER, switchZone); - - MvcResult result = getMockMvc().perform(post) + MvcResult result = createUserAndReturnResult(user, token, subdomain, switchZone) .andExpect(status().isCreated()) .andExpect(header().string("ETag", "\"0\"")) .andExpect(jsonPath("$.userName").value(user.getUserName())) @@ -89,9 +83,19 @@ private ScimUser createUser(ScimUser user, String token, String subdomain, Strin .andExpect(jsonPath("$.name.familyName").value(user.getFamilyName())) .andExpect(jsonPath("$.name.givenName").value(user.getGivenName())) .andReturn(); - return JsonUtils.readValue(result.getResponse().getContentAsString(), ScimUser.class); } + private ResultActions createUserAndReturnResult(ScimUser user, String token, String subdomain, String switchZone) throws Exception { + byte[] requestBody = JsonUtils.writeValueAsBytes(user); + MockHttpServletRequestBuilder post = post("/Users") + .header("Authorization", "Bearer " + token) + .contentType(APPLICATION_JSON) + .content(requestBody); + if (subdomain != null && !subdomain.equals("")) post.with(new SetServerNameRequestPostProcessor(subdomain + ".localhost")); + if (switchZone!=null) post.header(IdentityZoneSwitchingFilter.HEADER, switchZone); + + return getMockMvc().perform(post); + } private ScimUser getScimUser() { String email = "joe@"+generator.generate().toLowerCase()+".com"; @@ -111,6 +115,21 @@ public void testCanCreateUserWithExclamationMark() throws Exception { createUser(user, scimReadWriteToken, null); } + @Test + public void test_Create_User_Too_Long_Password() throws Exception { + String email = "joe@"+generator.generate().toLowerCase()+".com"; + ScimUser user = getScimUser(); + user.setUserName(email); + user.setPrimaryEmail(email); + user.setPassword(new RandomValueStringGenerator(300).generate()); + ResultActions result = createUserAndReturnResult(user, scimReadWriteToken, null, null); + result.andExpect(status().isBadRequest()) + .andDo(print()) + .andExpect(jsonPath("$.error").value("invalid_password")) + .andExpect(jsonPath("$.message").value("Password must be no more than 255 characters in length.")) + .andExpect(jsonPath("$.error_description").value("Password must be no more than 255 characters in length.")); + } + @Test public void testCreateUser() throws Exception { createUser(scimReadWriteToken); From 7ba367e3e0828a8c0e044ad696ff04e96865879f Mon Sep 17 00:00:00 2001 From: Jonathan Lo Date: Tue, 6 Oct 2015 17:53:23 -0700 Subject: [PATCH 047/103] Expose Role/Group Memberships (From SAML) in OpenID Connect JWT token [#99444178] https://www.pivotaltracker.com/story/show/99444178 Signed-off-by: Madhura Bhave --- .../uaa/authentication/UaaAuthentication.java | 22 +++++++++ .../saml/LoginSamlAuthenticationToken.java | 6 ++- .../identity/uaa/oauth/Claims.java | 1 + .../uaa/oauth/token/UaaTokenServices.java | 43 ++++++++++++----- .../UaaAuthenticationSerializationTests.java | 13 ++++- .../oauth/token/UaaTokenServicesTests.java | 42 ++++++++++++++++- docs/UAA-APIs.rst | 4 +- .../saml/LoginSamlAuthenticationProvider.java | 31 ++++++++---- .../LoginSamlAuthenticationProviderTests.java | 47 +++++++++++++------ 9 files changed, 166 insertions(+), 43 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java index a4f1fddb8c7..71ea7a6eeae 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java @@ -21,6 +21,7 @@ import java.io.Serializable; import java.util.Collection; import java.util.List; +import java.util.Set; /** * Authentication token which represents a user. @@ -33,6 +34,7 @@ public class UaaAuthentication implements Authentication, Serializable { private boolean authenticated; private long authenticatedTime = -1l; private long expiresAt = -1l; + private Set externalGroups; /** * Creates a token with the supplied array of authorities. @@ -75,6 +77,18 @@ public UaaAuthentication(@JsonProperty("principal") UaaPrincipal principal, this.expiresAt = expiresAt <= 0 ? -1 : expiresAt; } + public UaaAuthentication(UaaPrincipal uaaPrincipal, + Object credentials, + List uaaAuthorityList, + Set externalGroups, + UaaAuthenticationDetails details, + boolean authenticated, + long authenticatedTime, + long expiresAt) { + this(uaaPrincipal, credentials, uaaAuthorityList, details, authenticated, authenticatedTime, expiresAt); + this.externalGroups = externalGroups; + } + public long getAuthenticatedTime() { return authenticatedTime; } @@ -148,4 +162,12 @@ public int hashCode() { result = 31 * result + principal.hashCode(); return result; } + + public Set getExternalGroups() { + return externalGroups; + } + + public void setExternalGroups(Set externalGroups) { + this.externalGroups = externalGroups; + } } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationToken.java b/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationToken.java index 699e461557b..3f053e07b6a 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationToken.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationToken.java @@ -17,7 +17,9 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.providers.ExpiringUsernameAuthenticationToken; +import java.util.Collection; import java.util.List; +import java.util.Set; public class LoginSamlAuthenticationToken extends ExpiringUsernameAuthenticationToken { @@ -34,7 +36,7 @@ public UaaPrincipal getUaaPrincipal() { return uaaPrincipal; } - public UaaAuthentication getUaaAuthentication(List uaaAuthorityList) { - return new UaaAuthentication(getUaaPrincipal(), getCredentials(), uaaAuthorityList, null, isAuthenticated(), System.currentTimeMillis(), getTokenExpiration()==null ? -1l : getTokenExpiration().getTime()); + public UaaAuthentication getUaaAuthentication(List uaaAuthorityList, Set externalGroups) { + return new UaaAuthentication(getUaaPrincipal(), getCredentials(), uaaAuthorityList, externalGroups, null, isAuthenticated(), System.currentTimeMillis(), getTokenExpiration()==null ? -1l : getTokenExpiration().getTime()); } } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/Claims.java b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/Claims.java index c32ea2d8e1c..c97bad8ee11 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/Claims.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/Claims.java @@ -47,4 +47,5 @@ public class Claims { public static final String REVOCATION_SIGNATURE = "rev_sig"; public static final String NONCE = "nonce"; public static final String ORIGIN = "origin"; + public static final String ROLES = "roles"; } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServices.java b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServices.java index 10ae5a4ad40..bb0129cf3a0 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServices.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServices.java @@ -98,12 +98,15 @@ import static org.cloudfoundry.identity.uaa.oauth.Claims.JTI; import static org.cloudfoundry.identity.uaa.oauth.Claims.NONCE; import static org.cloudfoundry.identity.uaa.oauth.Claims.ORIGIN; +import static org.cloudfoundry.identity.uaa.oauth.Claims.REVOCATION_SIGNATURE; +import static org.cloudfoundry.identity.uaa.oauth.Claims.ROLES; import static org.cloudfoundry.identity.uaa.oauth.Claims.SCOPE; import static org.cloudfoundry.identity.uaa.oauth.Claims.SUB; import static org.cloudfoundry.identity.uaa.oauth.Claims.USER_ID; import static org.cloudfoundry.identity.uaa.oauth.Claims.USER_NAME; import static org.cloudfoundry.identity.uaa.oauth.Claims.ZONE_ID; + /** * This class provides token services for the UAA. It handles the production and * consumption of UAA tokens. @@ -227,7 +230,7 @@ public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenReque @SuppressWarnings("unchecked") Map additionalAuthorizationInfo = (Map) claims.get(ADDITIONAL_AZ_ATTR); - String revocableHashSignature = (String)claims.get(Claims.REVOCATION_SIGNATURE); + String revocableHashSignature = (String)claims.get(REVOCATION_SIGNATURE); if (StringUtils.hasText(revocableHashSignature)) { String newRevocableHashSignature = getRevocableTokenSignature(client, user); if (!revocableHashSignature.equals(newRevocableHashSignature)) { @@ -255,7 +258,8 @@ public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenReque additionalAuthorizationInfo, new HashSet<>(), revocableHashSignature, - false); //TODO populate response types + false, + null); //TODO populate response types return accessToken; } @@ -317,7 +321,8 @@ private OAuth2AccessToken createAccessToken(String userId, Map additionalAuthorizationAttributes, Set responseTypes, String revocableHashSignature, - boolean forceIdTokenCreation) throws AuthenticationException { + boolean forceIdTokenCreation, + Set externalGroupsForIdToken) throws AuthenticationException { String tokenId = UUID.randomUUID().toString(); OpenIdToken accessToken = new OpenIdToken(tokenId); if (validitySeconds > 0) { @@ -366,7 +371,7 @@ private OAuth2AccessToken createAccessToken(String userId, String token = JwtHelper.encode(content, signerProvider.getSigner()).getEncoded(); // This setter copies the value and returns. Don't change. accessToken.setValue(token); - populateIdToken(accessToken, jwtAccessToken, requestedScopes, responseTypes, clientId, forceIdTokenCreation); + populateIdToken(accessToken, jwtAccessToken, requestedScopes, responseTypes, clientId, forceIdTokenCreation, externalGroupsForIdToken); publish(new TokenIssuedEvent(accessToken, SecurityContextHolder.getContext().getAuthentication())); return accessToken; @@ -377,7 +382,8 @@ private void populateIdToken(OpenIdToken token, Set scopes, Set responseTypes, String aud, - boolean forceIdTokenCreation) { + boolean forceIdTokenCreation, + Set externalGroupsForIdToken) { if (forceIdTokenCreation || (scopes.contains("openid") && responseTypes.contains(OpenIdToken.ID_TOKEN))) { try { Map clone = new HashMap<>(accessTokenValues); @@ -390,6 +396,11 @@ private void populateIdToken(OpenIdToken token, } clone.put(SCOPE, idTokenScopes); clone.put(AUD, new HashSet(Arrays.asList(aud))); + + if (scopes.contains(ROLES) && !externalGroupsForIdToken.isEmpty()) { + clone.put(ROLES, externalGroupsForIdToken); + } + String content = JsonUtils.writeValueAsString(clone); String encoded = JwtHelper.encode(content, signerProvider.getSigner()).getEncoded(); token.setIdTokenValue(encoded); @@ -439,12 +450,12 @@ private void populateIdToken(OpenIdToken token, response.put(EMAIL, userEmail); } if (userAuthenticationTime!=null) { - response.put(Claims.AUTH_TIME, userAuthenticationTime.getTime() / 1000); + response.put(AUTH_TIME, userAuthenticationTime.getTime() / 1000); } } if (StringUtils.hasText(revocableHashSignature)) { - response.put(Claims.REVOCATION_SIGNATURE, revocableHashSignature); + response.put(REVOCATION_SIGNATURE, revocableHashSignature); } response.put(IAT, System.currentTimeMillis() / 1000); @@ -511,7 +522,12 @@ public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) modifiableUserScopes.addAll(OAuth2Utils.parseParameterList(externalScopes)); } - String nonce = authentication.getOAuth2Request().getRequestParameters().get(Claims.NONCE); + Set externalGroupsForIdToken = new HashSet<>(); + if (authentication.getUserAuthentication() instanceof UaaAuthentication) { + externalGroupsForIdToken = ((UaaAuthentication)authentication.getUserAuthentication()).getExternalGroups(); + } + + String nonce = authentication.getOAuth2Request().getRequestParameters().get(NONCE); Map additionalAuthorizationAttributes = getAdditionalAuthorizationAttributes( @@ -545,7 +561,8 @@ public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) additionalAuthorizationAttributes, responseTypes, revocableHashSignature, - wasIdTokenRequestedThroughAuthCodeScopeParameter); + wasIdTokenRequestedThroughAuthCodeScopeParameter, + externalGroupsForIdToken); return accessToken; } @@ -700,7 +717,7 @@ protected String getUserId(OAuth2Authentication authentication) { } if (StringUtils.hasText(revocableSignature)) { - response.put(Claims.REVOCATION_SIGNATURE, revocableSignature); + response.put(REVOCATION_SIGNATURE, revocableSignature); } response.put(AUD, resourceIds); @@ -922,10 +939,10 @@ private Map getClaimsForToken(String token) { throw new InvalidTokenException("Invalid issuer for token:"+claims.get(ISS)); } - String signature = (String)claims.get(Claims.REVOCATION_SIGNATURE); + String signature = (String)claims.get(REVOCATION_SIGNATURE); if (signature!=null) { //this ensures backwards compatibility during upgrade - String clientId = (String) claims.get(Claims.CID); - String userId = (String) claims.get(Claims.USER_ID); + String clientId = (String) claims.get(CID); + String userId = (String) claims.get(USER_ID); UaaUser user = null; ClientDetails client = clientDetailsService.loadClientByClientId(clientId); try { diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationSerializationTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationSerializationTests.java index e06f937a15c..1ba9568a3df 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationSerializationTests.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationSerializationTests.java @@ -16,10 +16,12 @@ package org.cloudfoundry.identity.uaa.authentication; import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.hamcrest.Matchers; import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; public class UaaAuthenticationSerializationTests { @@ -51,4 +53,13 @@ public void testDeserializationWithoutAuthenticatedTime() throws Exception { assertEquals(inTheFuture, authentication4.getExpiresAt()); assertTrue(authentication4.isAuthenticated()); } -} \ No newline at end of file + + @Test + public void deserialization_with_external_groups() throws Exception { + String dataWithExternalGroups ="{\"principal\":{\"id\":\"user-id\",\"name\":\"username\",\"email\":\"email\",\"origin\":\"uaa\",\"externalId\":null,\"zoneId\":\"uaa\"},\"credentials\":null,\"authorities\":[],\"externalGroups\":[\"something\",\"or\",\"other\",\"something\"],\"details\":null,\"authenticated\":true,\"authenticatedTime\":null,\"name\":\"username\"}"; + UaaAuthentication authentication = JsonUtils.readValue(dataWithExternalGroups, UaaAuthentication.class); + assertEquals(3, authentication.getExternalGroups().size()); + assertThat(authentication.getExternalGroups(), Matchers.containsInAnyOrder("something", "or", "other")); + assertTrue(authentication.isAuthenticated()); + } +} diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServicesTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServicesTests.java index e448637121d..2f91ea4a5e9 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServicesTests.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServicesTests.java @@ -13,12 +13,16 @@ package org.cloudfoundry.identity.uaa.oauth.token; import com.fasterxml.jackson.core.type.TypeReference; +import org.cloudfoundry.identity.uaa.UaaConfiguration; import org.cloudfoundry.identity.uaa.audit.AuditEvent; import org.cloudfoundry.identity.uaa.audit.AuditEventType; import org.cloudfoundry.identity.uaa.audit.event.TokenIssuedEvent; import org.cloudfoundry.identity.uaa.authentication.Origin; +import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; +import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.cloudfoundry.identity.uaa.client.ClientConstants; +import org.cloudfoundry.identity.uaa.login.saml.SamlIdentityProviderConfigurator; import org.cloudfoundry.identity.uaa.oauth.Claims; import org.cloudfoundry.identity.uaa.oauth.approval.Approval; import org.cloudfoundry.identity.uaa.oauth.approval.Approval.ApprovalStatus; @@ -47,6 +51,7 @@ import org.springframework.security.oauth2.common.exceptions.InvalidGrantException; import org.springframework.security.oauth2.common.exceptions.InvalidScopeException; import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; +import org.springframework.security.oauth2.common.util.OAuth2Utils; import org.springframework.security.oauth2.provider.AuthorizationRequest; import org.springframework.security.oauth2.provider.OAuth2RequestFactory; import org.springframework.security.oauth2.provider.client.BaseClientDetails; @@ -54,6 +59,8 @@ import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory; +import java.lang.reflect.Array; +import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; @@ -63,9 +70,11 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import static org.cloudfoundry.identity.uaa.user.UaaAuthority.USER_AUTHORITIES; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -95,7 +104,8 @@ public class UaaTokenServicesTests { public static final String ALL_GRANTS_CSV = "authorization_code,password,implicit,client_credentials"; public static final String CLIENTS = "clients"; public static final String SCIM = "scim"; - + public static final String OPENID = "openid"; + public static final String ROLES = "roles"; private TestApplicationEventPublisher publisher; private UaaTokenServices tokenServices = new UaaTokenServices(); @@ -657,6 +667,36 @@ public void testCreateAccessTokenImplicitGrant() { testCreateAccessTokenForAUser(authentication, true); } + @Test + public void create_id_token_with_roles_scope() { + Jwt idTokenJwt = getIdToken(Arrays.asList(OPENID, ROLES)); + assertTrue(idTokenJwt.getClaims().contains("\"roles\":[\"group2\",\"group1\"]")); + } + + @Test + public void create_id_token_without_roles_scope() { + Jwt idTokenJwt = getIdToken(Arrays.asList(OPENID)); + assertFalse(idTokenJwt.getClaims().contains("\"roles\"")); + } + + private Jwt getIdToken(List scopes) { + AuthorizationRequest authorizationRequest = new AuthorizationRequest(CLIENT_ID, scopes); + + authorizationRequest.setResponseTypes(new HashSet<>(Arrays.asList(OpenIdToken.ID_TOKEN))); + + UaaPrincipal uaaPrincipal = new UaaPrincipal(defaultUser.getId(), defaultUser.getUsername(), defaultUser.getEmail(), defaultUser.getOrigin(), defaultUser.getExternalId(), defaultUser.getZoneId()); + UaaAuthentication userAuthentication = new UaaAuthentication(uaaPrincipal, null, defaultUserAuthorities, new HashSet<>(Arrays.asList("group1", "group2")), null, true, System.currentTimeMillis(), System.currentTimeMillis() + 1000l * 60l); + + OAuth2Authentication authentication = new OAuth2Authentication(authorizationRequest.createOAuth2Request(), userAuthentication); + + OAuth2AccessToken accessToken = tokenServices.createAccessToken(authentication); + + Jwt tokenJwt = JwtHelper.decodeAndVerify(accessToken.getValue(), signerProvider.getVerifier()); + assertNotNull(tokenJwt); + + return JwtHelper.decodeAndVerify(((OpenIdToken) accessToken).getIdTokenValue(), signerProvider.getVerifier()); + } + @Test public void testCreateAccessWithNonExistingScopes() { List scopesThatDontExist = Arrays.asList("scope1","scope2"); diff --git a/docs/UAA-APIs.rst b/docs/UAA-APIs.rst index 4a2219a6c3f..156e62f153b 100644 --- a/docs/UAA-APIs.rst +++ b/docs/UAA-APIs.rst @@ -1087,7 +1087,7 @@ Fields *Available Fields* :: last_modified epoch timestamp Auto UAA sets the modification date UAA Provider Configuration (provided in JSON format as part of the ``config`` field on the Identity Provider - See class org.cloudfoundry.identity.uaa.zone.UaaIdentityProviderDefinition - ====================== =============== ======== ================================================================================================================================================================================================= + ============================= =============== ======== ================================================================================================================================================================================================= minLength int Required Minimum number of characters for a user provided password, 0+ maxLength int Required Maximum number of characters for a user provided password, 1+ requireUpperCaseCharacter int Required Minimum number of upper case characters for a user provided password, 0+ @@ -1115,7 +1115,7 @@ Fields *Available Fields* :: emailDomain List Optional List of email domains associated with the SAML provider for the purpose of associating users to the correct origin upon invitation. If null or empty list, no invitations are accepted. Wildcards supported. LDAP Provider Configuration (provided in JSON format as part of the ``config`` field on the Identity Provider - See class org.cloudfoundry.identity.uaa.ldap.LdapIdentityProviderDefinition - ====================== =============== ======== ================================================================================================================================================================================================= + ====================== =============== ======== ================================================================================================================================================================================================= ldapProfileFile String Required Value must be "ldap/ldap-search-and-bind.xml" (until other configuration options are supported) ldapGroupFile String Required Value must be "ldap/ldap-groups-map-to-scopes.xml" (until other configuration options are supported) baseUrl String Required URL to LDAP server, starts with ldap:// or ldaps:// diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java b/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java index 76b17f7b5d6..35f5d6574e7 100644 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java @@ -13,6 +13,7 @@ package org.cloudfoundry.identity.uaa.login.saml; +import org.apache.commons.collections.CollectionUtils; import org.cloudfoundry.identity.uaa.authentication.Origin; import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; @@ -53,8 +54,11 @@ import java.util.Collection; import java.util.Collections; import java.util.Date; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.GROUP_ATTRIBUTE_NAME; @@ -114,10 +118,12 @@ public Authentication authenticate(Authentication authentication) throws Authent ExpiringUsernameAuthenticationToken result = getExpiringUsernameAuthenticationToken(authentication); UaaPrincipal samlPrincipal = new UaaPrincipal(Origin.NotANumber, result.getName(), result.getName(), alias, result.getName(), zone.getId()); Collection samlAuthorities = retrieveSamlAuthorities(samlConfig, (SAMLCredential) result.getCredentials()); - Collection authorities = mapAuthorities(idp.getOriginKey(), samlConfig, samlAuthorities); + Collection authorities = mapAuthorities(idp.getOriginKey(), samlAuthorities); + + Set filteredExternalGroups = filterSamlAuthorities(samlConfig, samlAuthorities); UaaUser user = createIfMissing(samlPrincipal, addNew, authorities); UaaPrincipal principal = new UaaPrincipal(user); - return new LoginSamlAuthenticationToken(principal, result).getUaaAuthentication(user.getAuthorities()); + return new LoginSamlAuthenticationToken(principal, result).getUaaAuthentication(user.getAuthorities(), filteredExternalGroups); } protected ExpiringUsernameAuthenticationToken getExpiringUsernameAuthenticationToken(Authentication authentication) { @@ -130,20 +136,27 @@ protected void publish(ApplicationEvent event) { } } - protected Collection mapAuthorities(String origin, SamlIdentityProviderDefinition definition, Collection authorities) { - Collection result = Collections.EMPTY_LIST; + private Set filterSamlAuthorities(SamlIdentityProviderDefinition definition, Collection samlAuthorities) { + List whiteList = Collections.EMPTY_LIST; if (definition!=null && definition.getExternalGroupsWhitelist()!=null) { - List whiteList = definition.getExternalGroupsWhitelist(); - result = new LinkedList<>(); + whiteList = definition.getExternalGroupsWhitelist(); + } + Set authorities = samlAuthorities.stream().map(s -> s.getAuthority()).collect(Collectors.toSet()); + if (whiteList.isEmpty()) { + return authorities; + } + + return new HashSet<>(CollectionUtils.retainAll(authorities, whiteList)); + } + + protected Collection mapAuthorities(String origin, Collection authorities) { + Collection result = new LinkedList<>(); for (GrantedAuthority authority : authorities ) { String externalGroup = authority.getAuthority(); - if (whiteList.contains(externalGroup)) { for (ScimGroupExternalMember internalGroup : externalMembershipManager.getExternalGroupMapsByExternalGroup(externalGroup, origin)) { result.add(new SimpleGrantedAuthority(internalGroup.getDisplayName())); } - } } - } return result; } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java index d4aeaaa62a8..1096ddd0d3a 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java @@ -76,6 +76,7 @@ public class LoginSamlAuthenticationProviderTests extends JdbcTestBase { public static final String SAML_USER = "saml.user"; public static final String SAML_ADMIN = "saml.admin"; public static final String SAML_TEST = "saml.test"; + public static final String SAML_NOT_MAPPED = "saml.unmapped"; public static final String UAA_SAML_USER = "uaa.saml.user"; public static final String UAA_SAML_ADMIN = "uaa.saml.admin"; public static final String UAA_SAML_TEST = "uaa.saml.test"; @@ -136,7 +137,7 @@ public void configureProvider() throws Exception { when(usernameID.getValue()).thenReturn(username); consumer = mock(WebSSOProfileConsumer.class); Map> attributes = new HashMap<>(); - attributes.put("groups", Arrays.asList(SAML_USER,SAML_ADMIN)); + attributes.put("groups", Arrays.asList(SAML_USER,SAML_ADMIN,SAML_NOT_MAPPED)); attributes.put("2ndgroups", Arrays.asList(SAML_TEST)); credential = new SAMLCredential( usernameID, @@ -178,11 +179,8 @@ public void testAuthenticateSimple() { } @Test - public void test_multiple_white_listed_attributes() throws Exception { + public void test_multiple_group_attributes() throws Exception { providerDefinition.addAttributeMapping(ExternalIdentityProviderDefinition.GROUP_ATTRIBUTE_NAME, Arrays.asList("2ndgroups","groups")); - providerDefinition.addWhiteListedGroup(SAML_USER); - providerDefinition.addWhiteListedGroup(SAML_ADMIN); - providerDefinition.addWhiteListedGroup(SAML_TEST); provider.setConfig(JsonUtils.writeValueAsString(providerDefinition)); providerProvisioning.update(provider); UaaAuthentication authentication = getAuthentication(); @@ -198,10 +196,8 @@ public void test_multiple_white_listed_attributes() throws Exception { } @Test - public void test_white_listed_group() throws Exception { + public void test_group_mapping() throws Exception { providerDefinition.addAttributeMapping(ExternalIdentityProviderDefinition.GROUP_ATTRIBUTE_NAME, "groups"); - providerDefinition.addWhiteListedGroup(SAML_USER); - providerDefinition.addWhiteListedGroup(SAML_ADMIN); provider.setConfig(JsonUtils.writeValueAsString(providerDefinition)); providerProvisioning.update(provider); UaaAuthentication authentication = getAuthentication(); @@ -216,18 +212,18 @@ public void test_white_listed_group() throws Exception { } @Test - public void test_groups_not_white_listed() throws Exception { + public void externalGroup_NotMapped_ToScope() throws Exception { providerDefinition.addAttributeMapping(ExternalIdentityProviderDefinition.GROUP_ATTRIBUTE_NAME, "groups"); - providerDefinition.addWhiteListedGroup(SAML_ADMIN); provider.setConfig(JsonUtils.writeValueAsString(providerDefinition)); providerProvisioning.update(provider); UaaAuthentication authentication = getAuthentication(); - assertEquals("Two authorities should have been granted!", 2, authentication.getAuthorities().size()); + assertEquals("Three authorities should have been granted!", 3, authentication.getAuthorities().size()); assertThat(authentication.getAuthorities(), - Matchers.containsInAnyOrder( - new SimpleGrantedAuthority(UAA_SAML_ADMIN), - new SimpleGrantedAuthority(UaaAuthority.UAA_USER.getAuthority()) - ) + Matchers.containsInAnyOrder( + new SimpleGrantedAuthority(UAA_SAML_ADMIN), + new SimpleGrantedAuthority(UAA_SAML_USER), + new SimpleGrantedAuthority(UaaAuthority.UAA_USER.getAuthority()) + ) ); } @@ -238,6 +234,27 @@ public void test_group_attribute_not_set() throws Exception { assertEquals(UaaAuthority.UAA_USER.getAuthority(), uaaAuthentication.getAuthorities().iterator().next().getAuthority()); } + @Test + public void add_external_groups_to_authentication_without_whitelist() throws Exception { + providerDefinition.addAttributeMapping(ExternalIdentityProviderDefinition.GROUP_ATTRIBUTE_NAME, "groups"); + provider.setConfig(JsonUtils.writeValueAsString(providerDefinition)); + providerProvisioning.update(provider); + + UaaAuthentication authentication = getAuthentication(); + assertThat(authentication.getExternalGroups(), Matchers.containsInAnyOrder(SAML_ADMIN, SAML_USER, SAML_NOT_MAPPED)); + } + + @Test + public void add_external_groups_to_authentication_with_whitelist() throws Exception { + providerDefinition.addAttributeMapping(ExternalIdentityProviderDefinition.GROUP_ATTRIBUTE_NAME, "groups"); + providerDefinition.addWhiteListedGroup(SAML_ADMIN); + provider.setConfig(JsonUtils.writeValueAsString(providerDefinition)); + providerProvisioning.update(provider); + + UaaAuthentication authentication = getAuthentication(); + assertEquals(Collections.singleton(SAML_ADMIN), authentication.getExternalGroups()); + } + protected UaaAuthentication getAuthentication() { Authentication authentication = authprovider.authenticate(mockSamlAuthentication(Origin.SAML)); assertNotNull("Authentication should exist", authentication); From 7b8f17f59f0221478e08c28d13741a47e25a9f02 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Thu, 8 Oct 2015 14:15:30 -0700 Subject: [PATCH 048/103] Store the user attribute information received from the external SAML on the Shadow User Account [#104932356] https://www.pivotaltracker.com/story/show/104932356 Signed-off-by: Jonathan Lo --- .../ExternalIdentityProviderDefinition.java | 3 + .../saml/LoginSamlAuthenticationProvider.java | 34 +++++++--- .../LoginSamlAuthenticationProviderTests.java | 62 ++++++++++++++++--- 3 files changed, 85 insertions(+), 14 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/ExternalIdentityProviderDefinition.java b/common/src/main/java/org/cloudfoundry/identity/uaa/ExternalIdentityProviderDefinition.java index 93d250ebbbf..84cf8792780 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/ExternalIdentityProviderDefinition.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/ExternalIdentityProviderDefinition.java @@ -23,6 +23,9 @@ public class ExternalIdentityProviderDefinition extends AbstractIdentityProviderDefinition { public static final String GROUP_ATTRIBUTE_NAME = "external_groups"; //can be a string or a list of strings public static final String EMAIL_ATTRIBUTE_NAME = "email"; //can be a string + public static final String GIVEN_NAME_ATTRIBUTE_NAME = "given_name"; //can be a string + public static final String FAMILY_NAME_ATTRIBUTE_NAME = "family_name"; //can be a string + public static final String PHONE_NUMBER_ATTRIBUTE_NAME = "phone_number"; //can be a string public static final String EXTERNAL_GROUPS_WHITELIST = "externalGroupsWhitelist"; public static final String ATTRIBUTE_MAPPINGS = "attributeMappings"; diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java b/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java index 35f5d6574e7..a9f1b88a4e7 100644 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java @@ -54,12 +54,17 @@ import java.util.Collection; import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.EMAIL_ATTRIBUTE_NAME; +import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.FAMILY_NAME_ATTRIBUTE_NAME; +import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.GIVEN_NAME_ATTRIBUTE_NAME; import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.GROUP_ATTRIBUTE_NAME; public class LoginSamlAuthenticationProvider extends SAMLAuthenticationProvider implements ApplicationEventPublisherAware { @@ -121,7 +126,8 @@ public Authentication authenticate(Authentication authentication) throws Authent Collection authorities = mapAuthorities(idp.getOriginKey(), samlAuthorities); Set filteredExternalGroups = filterSamlAuthorities(samlConfig, samlAuthorities); - UaaUser user = createIfMissing(samlPrincipal, addNew, authorities); + Map userAttributes = retrieveUserAttributes(samlConfig, (SAMLCredential) result.getCredentials()); + UaaUser user = createIfMissing(samlPrincipal, addNew, authorities, userAttributes); UaaPrincipal principal = new UaaPrincipal(user); return new LoginSamlAuthenticationToken(principal, result).getUaaAuthentication(user.getAuthorities(), filteredExternalGroups); } @@ -182,7 +188,21 @@ public Collection retrieveSamlAuthorities(SamlIdenti return authorities == null ? Collections.EMPTY_LIST : authorities; } - protected UaaUser createIfMissing(UaaPrincipal samlPrincipal, boolean addNew, Collection authorities) { + public Map retrieveUserAttributes(SamlIdentityProviderDefinition definition, SAMLCredential credential) { + Map userAttributes = new HashMap<>(); + if (definition != null && definition.getAttributeMappings() != null) { + for (Map.Entry attributeMapping : definition.getAttributeMappings().entrySet()) { + if (attributeMapping.getValue() instanceof String) { + if (credential.getAttribute((String)attributeMapping.getValue()) != null) { + userAttributes.put(attributeMapping.getKey(), ((XSString) credential.getAttribute((String) attributeMapping.getValue()).getAttributeValues().get(0)).getValue()); + } + } + } + } + return userAttributes; + } + + protected UaaUser createIfMissing(UaaPrincipal samlPrincipal, boolean addNew, Collection authorities, Map userAttributes) { boolean userModified = false; UaaPrincipal uaaPrincipal = samlPrincipal; UaaUser user; @@ -194,7 +214,7 @@ protected UaaUser createIfMissing(UaaPrincipal samlPrincipal, boolean addNew, Co + "You can correct this by creating a shadow user for the SAML user.", e); } // Register new users automatically - publish(new NewUserAuthenticatedEvent(getUser(uaaPrincipal))); + publish(new NewUserAuthenticatedEvent(getUser(uaaPrincipal, userAttributes))); try { user = userDatabase.retrieveUserByName(uaaPrincipal.getName(), uaaPrincipal.getOrigin()); } catch (UsernameNotFoundException ex) { @@ -216,9 +236,11 @@ protected UaaUser createIfMissing(UaaPrincipal samlPrincipal, boolean addNew, Co return user; } - protected UaaUser getUser(UaaPrincipal principal) { + protected UaaUser getUser(UaaPrincipal principal, Map userAttributes) { String name = principal.getName(); - String email = null; + String email = userAttributes.get(EMAIL_ATTRIBUTE_NAME); + String givenName = userAttributes.get(GIVEN_NAME_ATTRIBUTE_NAME); + String familyName = userAttributes.get(FAMILY_NAME_ATTRIBUTE_NAME); String userId = Origin.NotANumber; String origin = principal.getOrigin()!=null?principal.getOrigin():Origin.LOGIN_SERVER; String zoneId = principal.getZoneId(); @@ -243,11 +265,9 @@ protected UaaUser getUser(UaaPrincipal principal) { email = name + "@unknown.org"; } } - String givenName = null; if (givenName == null) { givenName = email.split("@")[0]; } - String familyName = null; if (familyName == null) { familyName = email.split("@")[1]; } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java index 1096ddd0d3a..ac87ab69f0f 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java @@ -30,6 +30,7 @@ import org.cloudfoundry.identity.uaa.test.JdbcTestBase; import org.cloudfoundry.identity.uaa.user.JdbcUaaUserDatabase; import org.cloudfoundry.identity.uaa.user.UaaAuthority; +import org.cloudfoundry.identity.uaa.user.UaaUser; import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.cloudfoundry.identity.uaa.zone.IdentityProvider; import org.cloudfoundry.identity.uaa.zone.IdentityProviderProvisioning; @@ -62,6 +63,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -69,6 +71,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.anyObject; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class LoginSamlAuthenticationProviderTests extends JdbcTestBase { @@ -90,22 +93,28 @@ public class LoginSamlAuthenticationProviderTests extends JdbcTestBase { SamlIdentityProviderDefinition providerDefinition = new SamlIdentityProviderDefinition(); private IdentityProvider provider; - public List getAttributes(Map> values) { + public List getAttributes(Map values) { List result = new LinkedList<>(); - for (Map.Entry> entry : values.entrySet()) { + for (Map.Entry entry : values.entrySet()) { result.addAll(getAttributes(entry.getKey(), entry.getValue())); } return result; } - public List getAttributes(final String name, List values) { + public List getAttributes(final String name, Object value) { Attribute attribute = mock(Attribute.class); when(attribute.getName()).thenReturn(name); when(attribute.getFriendlyName()).thenReturn(name); List xmlObjects = new LinkedList<>(); - for (String s : values) { - AttributedStringImpl impl = new AttributedStringImpl("","",""); - impl.setValue(s); + if (value instanceof List) { + for (String s : (List)value) { + AttributedStringImpl impl = new AttributedStringImpl("", "", ""); + impl.setValue(s); + xmlObjects.add(impl); + } + } else { + AttributedStringImpl impl = new AttributedStringImpl("", "", ""); + impl.setValue((String)value); xmlObjects.add(impl); } when(attribute.getAttributeValues()).thenReturn(xmlObjects); @@ -136,7 +145,11 @@ public void configureProvider() throws Exception { NameID usernameID = mock(NameID.class); when(usernameID.getValue()).thenReturn(username); consumer = mock(WebSSOProfileConsumer.class); - Map> attributes = new HashMap<>(); + Map attributes = new HashMap<>(); + attributes.put("firstName", "Marissa"); + attributes.put("lastName", "Bloggs"); + attributes.put("emailAddress", "marissa.bloggs@test.com"); + attributes.put("phone", "1234567890"); attributes.put("groups", Arrays.asList(SAML_USER,SAML_ADMIN,SAML_NOT_MAPPED)); attributes.put("2ndgroups", Arrays.asList(SAML_TEST)); credential = new SAMLCredential( @@ -255,6 +268,41 @@ public void add_external_groups_to_authentication_with_whitelist() throws Except assertEquals(Collections.singleton(SAML_ADMIN), authentication.getExternalGroups()); } + @Test + public void shadowAccount_createdWith_MappedUserAttributes() throws Exception { + Map attributeMappings = new HashMap<>(); + attributeMappings.put("given_name", "firstName"); + attributeMappings.put("family_name", "lastName"); + attributeMappings.put("email", "emailAddress"); + attributeMappings.put("phone_number", "phone"); + providerDefinition.setAttributeMappings(attributeMappings); + provider.setConfig(JsonUtils.writeValueAsString(providerDefinition)); + providerProvisioning.update(provider); + + getAuthentication(); + UaaUser user = userDatabase.retrieveUserByName("marissa-saml", Origin.SAML); + assertEquals("Marissa", user.getGivenName()); + assertEquals("Bloggs", user.getFamilyName()); + assertEquals("marissa.bloggs@test.com", user.getEmail()); +// assertEquals("1234567890", user.get()); + } + + @Test + public void shadowUser_GetsCreatedWithDefaultValues_IfAttributeNotMapped() throws Exception { + Map attributeMappings = new HashMap<>(); + attributeMappings.put("surname", "lastName"); + attributeMappings.put("email", "emailAddress"); + providerDefinition.setAttributeMappings(attributeMappings); + provider.setConfig(JsonUtils.writeValueAsString(providerDefinition)); + providerProvisioning.update(provider); + + getAuthentication(); + UaaUser user = userDatabase.retrieveUserByName("marissa-saml", Origin.SAML); + assertEquals("marissa.bloggs", user.getGivenName()); + assertEquals("test.com", user.getFamilyName()); + assertEquals("marissa.bloggs@test.com", user.getEmail()); + } + protected UaaAuthentication getAuthentication() { Authentication authentication = authprovider.authenticate(mockSamlAuthentication(Origin.SAML)); assertNotNull("Authentication should exist", authentication); From bd2504e2e1cdd52a74ee76a440e93b27fa80a9ab Mon Sep 17 00:00:00 2001 From: Jonathan Lo Date: Wed, 7 Oct 2015 17:43:59 -0700 Subject: [PATCH 049/103] Test commit to diagnose travis build issues --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ea6ff70c3d1..26349154ac5 100644 --- a/README.md +++ b/README.md @@ -402,3 +402,4 @@ Here are some ways for you to get involved in the community: * Watch for upcoming articles on Cloud Foundry by [subscribing](http://blog.cloudfoundry.org) to the cloudfoundry.org blog + From cf94ed0411e51dfd8777c18e8d695bb329da933c Mon Sep 17 00:00:00 2001 From: Jonathan Lo Date: Wed, 7 Oct 2015 12:17:39 -0700 Subject: [PATCH 050/103] Update new ldap shadow users with extended ldap attributes [#103822132] https://www.pivotaltracker.com/story/show/103822132 --- .../ExternalLoginAuthenticationManager.java | 96 +++++---- .../LdapLoginAuthenticationManager.java | 36 ++-- .../uaa/ldap/ExtendedLdapUserDetails.java | 5 +- .../ldap/extension/ExtendedLdapUserImpl.java | 39 +++- .../identity/uaa/user/DialableByPhone.java | 17 ++ .../identity/uaa/user/Mailable.java | 17 ++ .../cloudfoundry/identity/uaa/user/Named.java | 19 ++ .../identity/uaa/user/UaaUser.java | 102 +++++---- .../identity/uaa/user/UaaUserPrototype.java | 197 ++++++++++++++++++ .../uaa/scim/bootstrap/ScimUserBootstrap.java | 1 + uaa/src/main/resources/ldap_init.ldif | 18 +- .../uaa/mock/ldap/LdapMockMvcTests.java | 33 ++- 12 files changed, 487 insertions(+), 93 deletions(-) create mode 100644 common/src/main/java/org/cloudfoundry/identity/uaa/user/DialableByPhone.java create mode 100644 common/src/main/java/org/cloudfoundry/identity/uaa/user/Mailable.java create mode 100644 common/src/main/java/org/cloudfoundry/identity/uaa/user/Named.java create mode 100644 common/src/main/java/org/cloudfoundry/identity/uaa/user/UaaUserPrototype.java diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java index 897dccfed51..98b4bfee0df 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java @@ -21,9 +21,13 @@ import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.cloudfoundry.identity.uaa.authentication.event.UserAuthenticationSuccessEvent; +import org.cloudfoundry.identity.uaa.user.DialableByPhone; +import org.cloudfoundry.identity.uaa.user.Mailable; +import org.cloudfoundry.identity.uaa.user.Named; import org.cloudfoundry.identity.uaa.user.UaaAuthority; import org.cloudfoundry.identity.uaa.user.UaaUser; import org.cloudfoundry.identity.uaa.user.UaaUserDatabase; +import org.cloudfoundry.identity.uaa.user.UaaUserPrototype; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.springframework.beans.factory.BeanNameAware; import org.springframework.context.ApplicationEvent; @@ -73,22 +77,25 @@ public void setApplicationEventPublisher(ApplicationEventPublisher eventPublishe public void setUserDatabase(UaaUserDatabase userDatabase) { this.userDatabase = userDatabase; } - public UaaUserDatabase getUserDatabase() { return this.userDatabase; } + + public UaaUserDatabase getUserDatabase() { + return this.userDatabase; + } @Override public Authentication authenticate(Authentication request) throws AuthenticationException { UserDetails req; if (request.getPrincipal() instanceof UserDetails) { - req = (UserDetails)request.getPrincipal(); + req = (UserDetails) request.getPrincipal(); } else if (request instanceof UsernamePasswordAuthenticationToken) { String username = request.getPrincipal().toString(); - String password = request.getCredentials()!=null ? request.getCredentials().toString() : ""; - req = new User( username, password, true, true, true, true, UaaAuthority.USER_AUTHORITIES); + String password = request.getCredentials() != null ? request.getCredentials().toString() : ""; + req = new User(username, password, true, true, true, true, UaaAuthority.USER_AUTHORITIES); } else if (request.getPrincipal() == null) { - logger.debug(this.getClass().getName() + "["+name+"] cannot process null principal"); + logger.debug(this.getClass().getName() + "[" + name + "] cannot process null principal"); return null; } else { - logger.debug(this.getClass().getName() + "["+name+"] cannot process request of type: " + request.getClass().getName()); + logger.debug(this.getClass().getName() + "[" + name + "] cannot process request of type: " + request.getClass().getName()); return null; } @@ -96,7 +103,7 @@ public Authentication authenticate(Authentication request) throws Authentication boolean addnew = false; try { UaaUser temp = userDatabase.retrieveUserByName(user.getUsername(), getOrigin()); - if (temp!=null) { + if (temp != null) { user = temp; } else { addnew = true; @@ -118,7 +125,7 @@ public Authentication authenticate(Authentication request) throws Authentication UaaAuthenticationDetails uaaAuthenticationDetails = null; if (request.getDetails() instanceof UaaAuthenticationDetails) { - uaaAuthenticationDetails = (UaaAuthenticationDetails)request.getDetails(); + uaaAuthenticationDetails = (UaaAuthenticationDetails) request.getDetails(); } else { uaaAuthenticationDetails = UaaAuthenticationDetails.UNKNOWN; } @@ -127,12 +134,12 @@ public Authentication authenticate(Authentication request) throws Authentication return success; } - protected Map getExtendedAuthorizationInfo(Authentication auth) { + protected Map getExtendedAuthorizationInfo(Authentication auth) { Object details = auth.getDetails(); - if (details!=null && details instanceof UaaAuthenticationDetails) { - UaaAuthenticationDetails uaaAuthenticationDetails = (UaaAuthenticationDetails)details; + if (details != null && details instanceof UaaAuthenticationDetails) { + UaaAuthenticationDetails uaaAuthenticationDetails = (UaaAuthenticationDetails) details; Map result = uaaAuthenticationDetails.getExtendedAuthorizationInfo(); - if (result!=null) { + if (result != null) { return result; } } @@ -152,6 +159,11 @@ protected UaaUser userAuthenticated(Authentication request, UaaUser user) { protected UaaUser getUser(UserDetails details, Map info) { String name = details.getUsername(); String email = info.get("email"); + + if (email == null && details instanceof Mailable) { + email = ((Mailable) details).getEmailAddress(); + } + if (name == null && email != null) { name = email; } @@ -163,36 +175,46 @@ protected UaaUser getUser(UserDetails details, Map info) { if (name.split("@").length == 2 && !name.startsWith("@") && !name.endsWith("@")) { email = name; } else { - email = name.replaceAll("@", "") + "@user.from."+getOrigin()+".cf"; + email = name.replaceAll("@", "") + "@user.from." + getOrigin() + ".cf"; } } else { - email = name + "@user.from."+getOrigin()+".cf"; + email = name + "@user.from." + getOrigin() + ".cf"; } } - String givenName = info.get("given_name"); - if (givenName == null) { - givenName = email.split("@")[0]; - } - String familyName = info.get("family_name"); - if (familyName == null) { - familyName = email.split("@")[1]; + String givenName; + String familyName; + if (details instanceof Named) { + Named names = (Named) details; + givenName = names.getGivenName(); + familyName = names.getFamilyName(); + } else { + givenName = info.get("given_name"); + if (givenName == null) { + givenName = email.split("@")[0]; + } + familyName = info.get("family_name"); + if (familyName == null) { + familyName = email.split("@")[1]; + } } - return new UaaUser( - "NaN", - name, - "" /*zero length password for login server */, - email, - UaaAuthority.USER_AUTHORITIES, - givenName, - familyName, - new Date(), - new Date(), - origin, - details.getUsername(), - false, - IdentityZoneHolder.get().getId(), - null, - null); + + String phoneNumber = (details instanceof DialableByPhone) ? ((DialableByPhone)details).getPhoneNumber() : null; + + UaaUserPrototype userPrototype = new UaaUserPrototype() + .withUsername(name) + .withPassword("") + .withEmail(email) + .withAuthorities(UaaAuthority.USER_AUTHORITIES) + .withGivenName(givenName) + .withFamilyName(familyName) + .withCreated(new Date()) + .withModified(new Date()) + .withOrigin(origin) + .withExternalId(details.getUsername()) + .withZoneId(IdentityZoneHolder.get().getId()) + .withPhoneNumber(phoneNumber); + + return new UaaUser(userPrototype); } @Override diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java index 6c0b03bc6a2..31a55c39615 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java @@ -17,6 +17,7 @@ import org.cloudfoundry.identity.uaa.ldap.ExtendedLdapUserDetails; import org.cloudfoundry.identity.uaa.user.UaaUser; +import org.cloudfoundry.identity.uaa.user.UaaUserPrototype; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; @@ -32,25 +33,26 @@ public class LdapLoginAuthenticationManager extends ExternalLoginAuthenticationM protected UaaUser getUser(UserDetails details, Map info) { UaaUser user = super.getUser(details, info); if (details instanceof LdapUserDetails) { - String mail = getEmail(user, (LdapUserDetails)details); - String origin = getOrigin(); String externalId = ((LdapUserDetails)details).getDn(); return new UaaUser( - user.getId(), - user.getUsername(), - user.getPassword(), - mail, - user.getAuthorities(), - user.getGivenName(), - user.getFamilyName(), - user.getCreated(), - user.getModified(), - origin, - externalId, - false, - IdentityZoneHolder.get().getId(), - null, - null); + new UaaUserPrototype() + .withId(user.getId()) + .withUsername(user.getUsername()) + .withPassword(user.getPassword()) + .withEmail(user.getEmail()) + .withAuthorities(user.getAuthorities()) + .withGivenName(user.getGivenName()) + .withFamilyName(user.getFamilyName()) + .withCreated(user.getCreated()) + .withModified(user.getModified()) + .withOrigin(user.getOrigin()) + .withSalt(user.getSalt()) + .withVerified(user.isVerified()) + .withPhoneNumber(user.getPhoneNumber()) + .withZoneId(user.getZoneId()) + .withPasswordLastModified(user.getPasswordLastModified()) + .withExternalId(externalId) + ); } else { logger.warn("Unable to get DN from user. Not an LDAP user:"+details+" of class:"+details.getClass()); return user.modifySource(getOrigin(), user.getExternalId()); diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserDetails.java b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserDetails.java index 1c5b85534b0..15a7013d124 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserDetails.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserDetails.java @@ -14,11 +14,14 @@ */ package org.cloudfoundry.identity.uaa.ldap; +import org.cloudfoundry.identity.uaa.user.DialableByPhone; +import org.cloudfoundry.identity.uaa.user.Mailable; +import org.cloudfoundry.identity.uaa.user.Named; import org.springframework.security.ldap.userdetails.LdapUserDetails; import java.util.Map; -public interface ExtendedLdapUserDetails extends LdapUserDetails { +public interface ExtendedLdapUserDetails extends LdapUserDetails, Mailable, Named, DialableByPhone { public String[] getMail(); diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/extension/ExtendedLdapUserImpl.java b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/extension/ExtendedLdapUserImpl.java index 04eb0a521b3..537aab3f280 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/extension/ExtendedLdapUserImpl.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/extension/ExtendedLdapUserImpl.java @@ -27,6 +27,10 @@ public class ExtendedLdapUserImpl implements ExtendedLdapUserDetails { private String mailAttributeName = "mail"; + private static final String givenNameAttributeName = "givenname"; + private static final String familyNameAttributeName = "sn"; + private static final String middleNameAttributeName = "initials"; + private static final String phoneNumberAttributeName = "telephonenumber"; private String dn; private String password; private String username; @@ -40,7 +44,6 @@ public class ExtendedLdapUserImpl implements ExtendedLdapUserDetails { private int graceLoginsRemaining = Integer.MAX_VALUE; private Map attributes = new HashMap<>(); - public ExtendedLdapUserImpl() {} public ExtendedLdapUserImpl(LdapUserDetails details) { setDn(details.getDn()); setUsername(details.getUsername()); @@ -157,4 +160,38 @@ public String getMailAttributeName() { public void setMailAttributeName(String mailAttributeName) { this.mailAttributeName = mailAttributeName; } + + @Override + public String getEmailAddress() { + String[] mailAddresses = getMail(); + return mailAddresses.length == 0 ? null : mailAddresses[0]; + } + + @Override + public String getGivenName() { + String[] attrValues = this.attributes.get(givenNameAttributeName); + if(attrValues == null) return null; + return attrValues[0]; + } + + @Override + public String getFamilyName() { + String[] attrValues = this.attributes.get(familyNameAttributeName); + if(attrValues == null) return null; + return attrValues[0]; + } + + @Override + public String getMiddleName() { + String[] attrValues = this.attributes.get(middleNameAttributeName); + if(attrValues == null) return null; + return attrValues[0]; + } + + @Override + public String getPhoneNumber() { + String[] attrValues = this.attributes.get(phoneNumberAttributeName); + if(attrValues == null) return null; + return attrValues[0]; + } } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/user/DialableByPhone.java b/common/src/main/java/org/cloudfoundry/identity/uaa/user/DialableByPhone.java new file mode 100644 index 00000000000..04893815413 --- /dev/null +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/user/DialableByPhone.java @@ -0,0 +1,17 @@ +package org.cloudfoundry.identity.uaa.user; + +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + *

+ * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + *

+ * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +public interface DialableByPhone { + public String getPhoneNumber(); +} diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/user/Mailable.java b/common/src/main/java/org/cloudfoundry/identity/uaa/user/Mailable.java new file mode 100644 index 00000000000..6104b44894e --- /dev/null +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/user/Mailable.java @@ -0,0 +1,17 @@ +package org.cloudfoundry.identity.uaa.user; + +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + *

+ * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + *

+ * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +public interface Mailable { + String getEmailAddress(); +} diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/user/Named.java b/common/src/main/java/org/cloudfoundry/identity/uaa/user/Named.java new file mode 100644 index 00000000000..87391f58f62 --- /dev/null +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/user/Named.java @@ -0,0 +1,19 @@ +package org.cloudfoundry.identity.uaa.user; + +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + *

+ * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + *

+ * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +public interface Named { + String getGivenName(); + String getFamilyName(); + String getMiddleName(); +} diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/user/UaaUser.java b/common/src/main/java/org/cloudfoundry/identity/uaa/user/UaaUser.java index 25e454d9fc3..997754a1930 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/user/UaaUser.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/user/UaaUser.java @@ -1,14 +1,14 @@ /******************************************************************************* - * Cloud Foundry - * Copyright (c) [2009-2014] Pivotal Software, Inc. All Rights Reserved. - * - * This product is licensed to you under the Apache License, Version 2.0 (the "License"). - * You may not use this product except in compliance with the License. - * - * This product includes a number of subcomponents with - * separate copyright notices and license terms. Your use of these - * subcomponents is subject to the terms and conditions of the - * subcomponent's license, as noted in the LICENSE file. + * Cloud Foundry + * Copyright (c) [2009-2014] Pivotal Software, Inc. All Rights Reserved. + *

+ * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + *

+ * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. *******************************************************************************/ package org.cloudfoundry.identity.uaa.user; @@ -53,6 +53,8 @@ public class UaaUser { private final Date passwordLastModified; + private final String phoneNumber; + public String getZoneId() { return zoneId; } @@ -65,12 +67,12 @@ public String getZoneId() { public UaaUser(String username, String password, String email, String givenName, String familyName) { this("NaN", username, password, email, UaaAuthority.USER_AUTHORITIES, givenName, familyName, new Date(), - new Date(), null, null, false,null,null, new Date()); + new Date(), null, null, false, null, null, new Date()); } public UaaUser(String username, String password, String email, String givenName, String familyName, String origin, String zoneId) { this("NaN", username, password, email, UaaAuthority.USER_AUTHORITIES, givenName, familyName, new Date(), - new Date(), origin, null, false, zoneId,null, new Date()); + new Date(), origin, null, false, zoneId, null, new Date()); } public UaaUser(String id, String username, String password, String email, @@ -78,25 +80,45 @@ public UaaUser(String id, String username, String password, String email, String givenName, String familyName, Date created, Date modified, String origin, String externalId, boolean verified, String zoneId, String salt, Date passwordLastModified) { - Assert.hasText(username, "Username cannot be empty"); - Assert.hasText(id, "Id cannot be null"); - Assert.hasText(email, "Email is required"); - this.id = id; - this.username = username; - this.password = password; - // TODO: Canonicalize email? - this.email = email; - this.familyName = familyName; - this.givenName = givenName; - this.created = created; - this.modified = modified; - this.authorities = authorities; - this.origin = origin; - this.externalId = externalId; - this.verified = verified; - this.zoneId = zoneId; - this.salt = salt; - this.passwordLastModified = passwordLastModified; + this(new UaaUserPrototype() + .withId(id) + .withUsername(username) + .withPassword(password) + .withEmail(email) + .withFamilyName(familyName) + .withGivenName(givenName) + .withCreated(created) + .withModified(modified) + .withAuthorities(authorities) + .withOrigin(origin) + .withExternalId(externalId) + .withVerified(verified) + .withZoneId(zoneId) + .withSalt(salt) + .withPasswordLastModified(passwordLastModified)); + } + + public UaaUser(UaaUserPrototype prototype) { + Assert.hasText(prototype.getId(), "Id cannot be null"); + Assert.hasText(prototype.getUsername(), "Username cannot be empty"); + Assert.hasText(prototype.getEmail(), "Email is required"); + + this.id = prototype.getId(); + this.username = prototype.getUsername(); + this.password = prototype.getPassword(); + this.email = prototype.getEmail(); + this.familyName = prototype.getFamilyName(); + this.givenName = prototype.getGivenName(); + this.created = prototype.getCreated(); + this.modified = prototype.getModified(); + this.authorities = prototype.getAuthorities(); + this.origin = prototype.getOrigin(); + this.externalId = prototype.getExternalId(); + this.verified = prototype.isVerified(); + this.zoneId = prototype.getZoneId(); + this.salt = prototype.getSalt(); + this.passwordLastModified = prototype.getPasswordLastModified(); + this.phoneNumber = prototype.getPhoneNumber(); } public String getId() { @@ -123,11 +145,17 @@ public String getFamilyName() { return familyName; } - public String getOrigin() { return origin; } + public String getOrigin() { + return origin; + } - public String getExternalId() { return externalId; } + public String getExternalId() { + return externalId; + } - public String getSalt() { return salt; } + public String getSalt() { + return salt; + } public List getAuthorities() { return authorities; @@ -156,7 +184,7 @@ public UaaUser authorities(Collection authorities) { @Override public String toString() { return "[UaaUser {id=" + id + ", username=" + username + ", email=" + email + ", givenName=" + givenName - + ", familyName=" + familyName + "}]"; + + ", familyName=" + familyName + "}]"; } public Date getModified() { @@ -194,4 +222,8 @@ public boolean isVerified() { public void setVerified(boolean verified) { this.verified = verified; } + + public String getPhoneNumber() { + return phoneNumber; + } } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/user/UaaUserPrototype.java b/common/src/main/java/org/cloudfoundry/identity/uaa/user/UaaUserPrototype.java new file mode 100644 index 00000000000..dba6078a149 --- /dev/null +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/user/UaaUserPrototype.java @@ -0,0 +1,197 @@ +package org.cloudfoundry.identity.uaa.user; + +import org.springframework.security.core.GrantedAuthority; + +import java.util.Date; +import java.util.List; + +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + *

+ * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + *

+ * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +public final class UaaUserPrototype { + + private String id = "NaN"; + + private String username; + + private String password; + + private String email; + + private String givenName; + + private String familyName; + + private String phoneNumber; + + private Date created; + + private Date modified; + + private String origin; + + private String externalId; + + private String salt; + + private Date passwordLastModified; + + private String zoneId; + + private List authorities; + + private boolean verified = false; + + public String getId() { + return id; + } + + public UaaUserPrototype withId(String id) { + this.id = id; + return this; + } + + public String getUsername() { + return username; + } + + public UaaUserPrototype withUsername(String username) { + this.username = username; + return this; + } + + public String getPassword() { + return password; + } + + public UaaUserPrototype withPassword(String password) { + this.password = password; + return this; + } + + public String getEmail() { + return email; + } + + public UaaUserPrototype withEmail(String email) { + this.email = email; + return this; + } + + public String getGivenName() { + return givenName; + } + + public UaaUserPrototype withGivenName(String givenName) { + this.givenName = givenName; + return this; + } + + public String getFamilyName() { + return familyName; + } + + public UaaUserPrototype withFamilyName(String familyName) { + this.familyName = familyName; + return this; + } + + public String getPhoneNumber() { + return phoneNumber; + } + + public UaaUserPrototype withPhoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + return this; + } + + public Date getCreated() { + return created; + } + + public UaaUserPrototype withCreated(Date created) { + this.created = created; + return this; + } + + public Date getModified() { + return modified; + } + + public UaaUserPrototype withModified(Date modified) { + this.modified = modified; + return this; + } + + public String getOrigin() { + return origin; + } + + public UaaUserPrototype withOrigin(String origin) { + this.origin = origin; + return this; + } + + public String getExternalId() { + return externalId; + } + + public UaaUserPrototype withExternalId(String externalId) { + this.externalId = externalId; + return this; + } + + public String getSalt() { + return salt; + } + + public UaaUserPrototype withSalt(String salt) { + this.salt = salt; + return this; + } + + public Date getPasswordLastModified() { + return passwordLastModified; + } + + public UaaUserPrototype withPasswordLastModified(Date passwordLastModified) { + this.passwordLastModified = passwordLastModified; + return this; + } + + public String getZoneId() { + return zoneId; + } + + public UaaUserPrototype withZoneId(String zoneId) { + this.zoneId = zoneId; + return this; + } + + public List getAuthorities() { + return authorities; + } + + public UaaUserPrototype withAuthorities(List authorities) { + this.authorities = authorities; + return this; + } + + public boolean isVerified() { + return verified; + } + + public UaaUserPrototype withVerified(boolean verified) { + this.verified = verified; + return this; + } +} diff --git a/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrap.java b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrap.java index 77abdceb176..bc58d1d46dc 100644 --- a/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrap.java +++ b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrap.java @@ -247,6 +247,7 @@ private void removeFromGroup(String scimUserId, String gName) { */ private ScimUser convertToScimUser(UaaUser user) { ScimUser scim = new ScimUser(user.getId(), user.getUsername(), user.getGivenName(), user.getFamilyName()); + scim.addPhoneNumber(user.getPhoneNumber()); scim.addEmail(user.getEmail()); scim.setOrigin(user.getOrigin()); scim.setExternalId(user.getExternalId()); diff --git a/uaa/src/main/resources/ldap_init.ldif b/uaa/src/main/resources/ldap_init.ldif index 756bf0a6e56..185fa57329c 100644 --- a/uaa/src/main/resources/ldap_init.ldif +++ b/uaa/src/main/resources/ldap_init.ldif @@ -19,6 +19,14 @@ sn: Administrator userPassword: adminsecret uid: 3378a03c-fc8c-46b6-8a76-f67f9d7a7b4a mail: admin@test.com +givenName: Bob +initials: X +telephoneNumber: 8885550987 +streetAddress: 1111 Admin St +l: Adminton +st: California +postalCode: 94119 +co: United States dn: cn=marissa,ou=Users,dc=test,dc=com changetype: add @@ -51,7 +59,15 @@ cn: marissa3 userPassword: ldap3 uid: 20f459e0-e30b-4d1f-998c-3ded7f769db3 mail: marissa3@test.com -sn: Marissa3 +sn: Lastnamerton +givenName: Marissa +initials: M +telephoneNumber: 8885550986 +streetAddress: 1111 Marissa St +l: Marissaville +st: Florida +postalCode: 32561 +co: United States dn: cn=marissa4,ou=Users,dc=test,dc=com changetype: add diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java index 5a660382a07..369024ee0d6 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java @@ -45,6 +45,7 @@ import org.junit.Assume; import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -571,6 +572,8 @@ public void runLdapTestblock() throws Exception { deleteLdapUsers(); testAuthenticate(); deleteLdapUsers(); + testExtendedAttributes(); + deleteLdapUsers(); testAuthenticateInactiveIdp(); deleteLdapUsers(); testAuthenticateFailure(); @@ -637,6 +640,22 @@ public void testAuthenticate() throws Exception { assertThat(result.getResponse().getContentAsString(), containsString("\"email\":\"marissa3@test.com\"")); } + public void testExtendedAttributes() throws Exception { + String username = "marissa3"; + String password = "ldap3"; + MvcResult result = performAuthentication(username, password); + assertThat(result.getResponse().getContentAsString(), containsString("\"username\":\"" + username + "\"")); + assertThat(result.getResponse().getContentAsString(), containsString("\"email\":\"marissa3@test.com\"")); + assertEquals("Marissa", getGivenName(username)); + assertEquals("Lastnamerton", getFamilyName(username)); + assertEquals("8885550986", getPhoneNumber(username)); +// assertThat(result.getResponse().getContentAsString(), containsString("\"givenname\":\"Marissa\"")); +// assertThat(result.getResponse().getContentAsString(), containsString("\"familyname\":\"Marissa3\"")); + //assertThat(result.getResponse().getContentAsString(), containsString("\"phonenumber\":\"8885550986\"")); + + + } + public void testAuthenticateInactiveIdp() throws Exception { IdentityProviderProvisioning provisioning = webApplicationContext.getBean(IdentityProviderProvisioning.class); IdentityProvider ldapProvider = provisioning.retrieveByOrigin(Origin.LDAP, IdentityZone.getUaa().getId()); @@ -658,7 +677,7 @@ public void testAuthenticateFailure() throws Exception { MockHttpServletRequestBuilder post = post("/authenticate") .accept(MediaType.APPLICATION_JSON) - .param("username",username) + .param("username", username) .param("password", password); mockMvc.perform(post) .andExpect(status().isUnauthorized()); @@ -768,6 +787,18 @@ private String getEmail(String username) { return jdbcTemplate.queryForObject("select email from users where username='" + username + "' and origin='" + Origin.LDAP + "'", String.class); } + private String getGivenName(String username) { + return jdbcTemplate.queryForObject("select givenname from users where username='" + username + "' and origin='" + Origin.LDAP + "'", String.class); + } + + private String getFamilyName(String username) { + return jdbcTemplate.queryForObject("select familyname from users where username='" + username + "' and origin='" + Origin.LDAP + "'", String.class); + } + + private String getPhoneNumber(String username) { + return jdbcTemplate.queryForObject("select phonenumber from users where username='" + username + "' and origin='" + Origin.LDAP + "'", String.class); + } + private MvcResult performAuthentication(String username, String password) throws Exception { return performAuthentication(username, password, HttpStatus.OK); } From f5bc59c5c8da21528fb04c7dc4ed40c97082fa69 Mon Sep 17 00:00:00 2001 From: Jonathan Lo Date: Wed, 7 Oct 2015 16:42:08 -0700 Subject: [PATCH 051/103] Add abilty to map extended attribute names via config [#104932164] https://www.pivotaltracker.com/story/show/104932164 --- .../ExternalLoginAuthenticationManager.java | 6 +- .../LdapLoginAuthenticationManager.java | 49 ------------ .../uaa/ldap/ExtendedLdapUserDetails.java | 3 +- .../uaa/ldap/ExtendedLdapUserMapper.java | 76 +++++++++++++------ .../ldap/extension/ExtendedLdapUserImpl.java | 43 ++++++++--- .../uaa/user/ExternallyIdentifiable.java | 17 +++++ .../cloudfoundry/identity/uaa/user/Named.java | 1 - .../LdapLoginAuthenticationManagerTests.java | 4 +- .../uaa/ldap/ExtendedLdapUserMapperTest.java | 72 ++++++++++++++++++ uaa/src/main/resources/ldap-integration.xml | 3 + 10 files changed, 186 insertions(+), 88 deletions(-) create mode 100644 common/src/main/java/org/cloudfoundry/identity/uaa/user/ExternallyIdentifiable.java diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java index 98b4bfee0df..2d8f842e248 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java @@ -22,6 +22,7 @@ import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.cloudfoundry.identity.uaa.authentication.event.UserAuthenticationSuccessEvent; import org.cloudfoundry.identity.uaa.user.DialableByPhone; +import org.cloudfoundry.identity.uaa.user.ExternallyIdentifiable; import org.cloudfoundry.identity.uaa.user.Mailable; import org.cloudfoundry.identity.uaa.user.Named; import org.cloudfoundry.identity.uaa.user.UaaAuthority; @@ -160,7 +161,7 @@ protected UaaUser getUser(UserDetails details, Map info) { String name = details.getUsername(); String email = info.get("email"); - if (email == null && details instanceof Mailable) { + if (details instanceof Mailable) { email = ((Mailable) details).getEmailAddress(); } @@ -199,6 +200,7 @@ protected UaaUser getUser(UserDetails details, Map info) { } String phoneNumber = (details instanceof DialableByPhone) ? ((DialableByPhone)details).getPhoneNumber() : null; + String externalId = (details instanceof ExternallyIdentifiable) ? ((ExternallyIdentifiable)details).getExternalId() : details.getUsername(); UaaUserPrototype userPrototype = new UaaUserPrototype() .withUsername(name) @@ -210,7 +212,7 @@ protected UaaUser getUser(UserDetails details, Map info) { .withCreated(new Date()) .withModified(new Date()) .withOrigin(origin) - .withExternalId(details.getUsername()) + .withExternalId(externalId) .withZoneId(IdentityZoneHolder.get().getId()) .withPhoneNumber(phoneNumber); diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java index 31a55c39615..9d16c2aaf56 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java @@ -17,59 +17,12 @@ import org.cloudfoundry.identity.uaa.ldap.ExtendedLdapUserDetails; import org.cloudfoundry.identity.uaa.user.UaaUser; -import org.cloudfoundry.identity.uaa.user.UaaUserPrototype; -import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.springframework.security.core.Authentication; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.ldap.userdetails.LdapUserDetails; - -import java.util.Map; public class LdapLoginAuthenticationManager extends ExternalLoginAuthenticationManager { private boolean autoAddAuthorities = false; - @Override - protected UaaUser getUser(UserDetails details, Map info) { - UaaUser user = super.getUser(details, info); - if (details instanceof LdapUserDetails) { - String externalId = ((LdapUserDetails)details).getDn(); - return new UaaUser( - new UaaUserPrototype() - .withId(user.getId()) - .withUsername(user.getUsername()) - .withPassword(user.getPassword()) - .withEmail(user.getEmail()) - .withAuthorities(user.getAuthorities()) - .withGivenName(user.getGivenName()) - .withFamilyName(user.getFamilyName()) - .withCreated(user.getCreated()) - .withModified(user.getModified()) - .withOrigin(user.getOrigin()) - .withSalt(user.getSalt()) - .withVerified(user.isVerified()) - .withPhoneNumber(user.getPhoneNumber()) - .withZoneId(user.getZoneId()) - .withPasswordLastModified(user.getPasswordLastModified()) - .withExternalId(externalId) - ); - } else { - logger.warn("Unable to get DN from user. Not an LDAP user:"+details+" of class:"+details.getClass()); - return user.modifySource(getOrigin(), user.getExternalId()); - } - } - - protected String getEmail(UaaUser user, LdapUserDetails details) { - String mail = user.getEmail(); - if (details instanceof ExtendedLdapUserDetails) { - String[] emails = ((ExtendedLdapUserDetails)details).getMail(); - if (emails!=null && emails.length>0) { - mail = emails[0]; - } - } - return mail; - } - @Override protected UaaUser userAuthenticated(Authentication request, UaaUser user) { boolean userModified = false; @@ -94,6 +47,4 @@ public boolean isAutoAddAuthorities() { public void setAutoAddAuthorities(boolean autoAddAuthorities) { this.autoAddAuthorities = autoAddAuthorities; } - - } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserDetails.java b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserDetails.java index 15a7013d124..fbbc4264bd6 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserDetails.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserDetails.java @@ -15,13 +15,14 @@ package org.cloudfoundry.identity.uaa.ldap; import org.cloudfoundry.identity.uaa.user.DialableByPhone; +import org.cloudfoundry.identity.uaa.user.ExternallyIdentifiable; import org.cloudfoundry.identity.uaa.user.Mailable; import org.cloudfoundry.identity.uaa.user.Named; import org.springframework.security.ldap.userdetails.LdapUserDetails; import java.util.Map; -public interface ExtendedLdapUserDetails extends LdapUserDetails, Mailable, Named, DialableByPhone { +public interface ExtendedLdapUserDetails extends LdapUserDetails, Mailable, Named, DialableByPhone, ExternallyIdentifiable { public String[] getMail(); diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserMapper.java b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserMapper.java index aefd8c0a959..43bfccc4d66 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserMapper.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserMapper.java @@ -1,14 +1,14 @@ /******************************************************************************* - * Cloud Foundry - * Copyright (c) [2009-2014] Pivotal Software, Inc. All Rights Reserved. - * - * This product is licensed to you under the Apache License, Version 2.0 (the "License"). - * You may not use this product except in compliance with the License. - * - * This product includes a number of subcomponents with - * separate copyright notices and license terms. Your use of these - * subcomponents is subject to the terms and conditions of the - * subcomponent's license, as noted in the LICENSE file. + * Cloud Foundry + * Copyright (c) [2009-2014] Pivotal Software, Inc. All Rights Reserved. + *

+ * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + *

+ * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. *******************************************************************************/ package org.cloudfoundry.identity.uaa.ldap; @@ -40,12 +40,15 @@ public class ExtendedLdapUserMapper extends LdapUserDetailsMapper { private static final Log logger = LogFactory.getLog(ExtendedLdapUserMapper.class); public static final String SUBSTITUTE_MAIL_ATTR_NAME = "substitute-mail-attribute"; private String mailAttributeName = "mail"; + private String givenNameAttributeName = "given_name"; + private String familyNameAttributeName = "family_name"; + private String phoneNumberAttributeName = "phone"; private String mailSubstitute = null; private boolean mailSubstituteOverrides = false; @Override public UserDetails mapUserFromContext(DirContextOperations ctx, String username, Collection authorities) { - LdapUserDetails ldapUserDetails = (LdapUserDetails)super.mapUserFromContext(ctx, username, authorities); + LdapUserDetails ldapUserDetails = (LdapUserDetails) super.mapUserFromContext(ctx, username, authorities); DirContextAdapter adapter = (DirContextAdapter) ctx; Map record = new HashMap(); @@ -53,13 +56,13 @@ public UserDetails mapUserFromContext(DirContextOperations ctx, String username, for (String attributeName : attributeNames) { try { Object[] objValues = adapter.getObjectAttributes(attributeName); - String[] values = new String[objValues!=null ? objValues.length : 0]; - for (int i=0; i record) { //default behavior String result = getMailAttributeName(); - if (getMailSubstitute()!=null) { + if (getMailSubstitute() != null) { String subemail = substituteMail(username); - record.put(SUBSTITUTE_MAIL_ATTR_NAME, new String[] {subemail}); + record.put(SUBSTITUTE_MAIL_ATTR_NAME, new String[]{subemail}); if (isMailSubstituteOverridesLdap() || - record.get(getMailAttributeName())==null || - record.get(getMailAttributeName()).length==0) { + record.get(getMailAttributeName()) == null || + record.get(getMailAttributeName()).length == 0) { result = SUBSTITUTE_MAIL_ATTR_NAME; } } @@ -97,7 +103,7 @@ protected String configureMailAttribute(String username, Map r } protected String substituteMail(String username) { - if (getMailSubstitute()==null) { + if (getMailSubstitute() == null) { return null; } else { return getMailSubstitute().replace("{0}", username); @@ -112,6 +118,30 @@ public void setMailAttributeName(String mailAttributeName) { this.mailAttributeName = mailAttributeName; } + public String getPhoneNumberAttributeName() { + return phoneNumberAttributeName; + } + + public void setPhoneNumberAttributeName(String phoneNumberAttributeName) { + this.phoneNumberAttributeName = phoneNumberAttributeName.toLowerCase(); + } + + public String getGivenNameAttributeName() { + return givenNameAttributeName; + } + + public void setGivenNameAttributeName(String givenNameAttributeName) { + this.givenNameAttributeName = givenNameAttributeName.toLowerCase(); + } + + public String getFamilyNameAttributeName() { + return familyNameAttributeName; + } + + public void setFamilyNameAttributeName(String familyNameAttributeName) { + this.familyNameAttributeName = familyNameAttributeName.toLowerCase(); + } + public String getMailSubstitute() { return mailSubstitute; } @@ -120,7 +150,7 @@ public void setMailSubstitute(String mailSubstitute) { if ("null".equals(mailSubstitute) || "".equals(mailSubstitute)) { mailSubstitute = null; } - if (mailSubstitute!=null && !mailSubstitute.contains("{0}")) { + if (mailSubstitute != null && !mailSubstitute.contains("{0}")) { throw new IllegalArgumentException("Invalid mail substitute pattern, {0} is missing."); } this.mailSubstitute = mailSubstitute; diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/extension/ExtendedLdapUserImpl.java b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/extension/ExtendedLdapUserImpl.java index 537aab3f280..944663e64ee 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/extension/ExtendedLdapUserImpl.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/extension/ExtendedLdapUserImpl.java @@ -27,10 +27,9 @@ public class ExtendedLdapUserImpl implements ExtendedLdapUserDetails { private String mailAttributeName = "mail"; - private static final String givenNameAttributeName = "givenname"; - private static final String familyNameAttributeName = "sn"; - private static final String middleNameAttributeName = "initials"; - private static final String phoneNumberAttributeName = "telephonenumber"; + private String givenNameAttributeName = "given_name"; + private String familyNameAttributeName = "family_name"; + private String phoneNumberAttributeName = "phone"; private String dn; private String password; private String username; @@ -158,7 +157,31 @@ public String getMailAttributeName() { } public void setMailAttributeName(String mailAttributeName) { - this.mailAttributeName = mailAttributeName; + this.mailAttributeName = mailAttributeName.toLowerCase(); + } + + public String getPhoneNumberAttributeName() { + return phoneNumberAttributeName; + } + + public void setPhoneNumberAttributeName(String phoneNumberAttributeName) { + this.phoneNumberAttributeName = phoneNumberAttributeName; + } + + public String getGivenNameAttributeName() { + return givenNameAttributeName; + } + + public void setGivenNameAttributeName(String givenNameAttributeName) { + this.givenNameAttributeName = givenNameAttributeName; + } + + public String getFamilyNameAttributeName() { + return familyNameAttributeName; + } + + public void setFamilyNameAttributeName(String familyNameAttributeName) { + this.familyNameAttributeName = familyNameAttributeName; } @Override @@ -182,16 +205,14 @@ public String getFamilyName() { } @Override - public String getMiddleName() { - String[] attrValues = this.attributes.get(middleNameAttributeName); + public String getPhoneNumber() { + String[] attrValues = this.attributes.get(phoneNumberAttributeName); if(attrValues == null) return null; return attrValues[0]; } @Override - public String getPhoneNumber() { - String[] attrValues = this.attributes.get(phoneNumberAttributeName); - if(attrValues == null) return null; - return attrValues[0]; + public String getExternalId() { + return getDn(); } } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/user/ExternallyIdentifiable.java b/common/src/main/java/org/cloudfoundry/identity/uaa/user/ExternallyIdentifiable.java new file mode 100644 index 00000000000..77b7f02a1e1 --- /dev/null +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/user/ExternallyIdentifiable.java @@ -0,0 +1,17 @@ +package org.cloudfoundry.identity.uaa.user; + +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + *

+ * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + *

+ * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +public interface ExternallyIdentifiable { + String getExternalId(); +} diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/user/Named.java b/common/src/main/java/org/cloudfoundry/identity/uaa/user/Named.java index 87391f58f62..3cf931b68b6 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/user/Named.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/user/Named.java @@ -15,5 +15,4 @@ public interface Named { String getGivenName(); String getFamilyName(); - String getMiddleName(); } diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java index 94ed8e01fad..0e1df76ac32 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java @@ -84,7 +84,9 @@ public void testGetUserWithExtendedLdapInfo() throws Exception { @Test public void testGetUserWithLdapInfo() throws Exception { UaaUser user = am.getUser(getLdapUserDetails(), info); - assertEquals(DN, user.getExternalId()); + // The LdapLoginAuthenticationManager is no longer responsible for overriding the external ID. + // Instead, see ExternalLoginAuthenticationManager and the ExternallyIdentifiable interface + // assertEquals(DN, user.getExternalId()); assertEquals(TEST_EMAIL, user.getEmail()); assertEquals(origin, user.getOrigin()); } diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserMapperTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserMapperTest.java index 2ba938ed123..a04a0540724 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserMapperTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserMapperTest.java @@ -1,15 +1,51 @@ package org.cloudfoundry.identity.uaa.ldap; +import org.cloudfoundry.identity.uaa.ldap.extension.ExtendedLdapUserImpl; +import org.junit.Assert; +import org.junit.Before; import org.junit.Test; +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.NameAwareAttributes; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.ldap.ppolicy.PasswordPolicyControl; +import javax.naming.Name; +import javax.naming.NamingEnumeration; +import javax.naming.directory.Attributes; +import javax.naming.directory.BasicAttributes; +import javax.naming.directory.DirContext; +import javax.naming.ldap.LdapName; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import static org.cloudfoundry.identity.uaa.ldap.ExtendedLdapUserMapper.SUBSTITUTE_MAIL_ATTR_NAME; +import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class ExtendedLdapUserMapperTest { + private Attributes attrs; + private DirContextAdapter adapter; + private ExtendedLdapUserMapper mapper; + private Collection authorities; + + @Before + public void setUp() throws Exception { + attrs = new NameAwareAttributes(); + authorities = Collections.emptyList(); + mapper = new ExtendedLdapUserMapper(); + } + @Test public void testConfigureMailAttribute() throws Exception { ExtendedLdapUserMapper mapper = new ExtendedLdapUserMapper(); @@ -29,4 +65,40 @@ public void testConfigureMailAttribute() throws Exception { result = mapper.configureMailAttribute("marissa", records); assertEquals("mail", result); } + + @Test + public void testGivenNameAttributeNameMapping() throws Exception { + attrs.put("givenName", "Marissa"); + adapter = new DirContextAdapter(attrs, new LdapName("cn=marissa,ou=Users,dc=test,dc=com")); + mapper.setGivenNameAttributeName("givenName"); + + ExtendedLdapUserImpl ldapUserDetails = getExtendedLdapUser(); + Assert.assertThat(ldapUserDetails.getGivenName(), is("Marissa")); + } + + @Test + public void testFamilyNameAttributeNameMapping() throws Exception { + attrs.put("lastName", "Lastnamerton"); + adapter = new DirContextAdapter(attrs, new LdapName("cn=marissa,ou=Users,dc=test,dc=com")); + mapper.setFamilyNameAttributeName("lastName"); + + ExtendedLdapUserImpl ldapUserDetails = getExtendedLdapUser(); + Assert.assertThat(ldapUserDetails.getFamilyName(), is("Lastnamerton")); + } + + @Test + public void testPhoneNumberAttributeNameMapping() throws Exception { + attrs.put("phoneNumber", "8675309"); + adapter = new DirContextAdapter(attrs, new LdapName("cn=marissa,ou=Users,dc=test,dc=com")); + mapper.setPhoneNumberAttributeName("phoneNumber"); + + ExtendedLdapUserImpl ldapUserDetails = getExtendedLdapUser(); + Assert.assertThat(ldapUserDetails.getPhoneNumber(), is("8675309")); + } + + private ExtendedLdapUserImpl getExtendedLdapUser() { + UserDetails userDetails = mapper.mapUserFromContext(adapter, "marissa", authorities); + assertThat(userDetails instanceof ExtendedLdapUserImpl, is(true)); + return (ExtendedLdapUserImpl)userDetails; + } } \ No newline at end of file diff --git a/uaa/src/main/resources/ldap-integration.xml b/uaa/src/main/resources/ldap-integration.xml index aacf04e10f5..8998b910034 100644 --- a/uaa/src/main/resources/ldap-integration.xml +++ b/uaa/src/main/resources/ldap-integration.xml @@ -47,6 +47,9 @@ + + + From c6874837a1079f6da3edfc784d36b54274f47d0a Mon Sep 17 00:00:00 2001 From: Mike Roda Date: Fri, 9 Oct 2015 11:13:04 -0400 Subject: [PATCH 052/103] Try to obtain the user details origin from x-forwarded-for Use the x-forwarded-for header, if it's available, for the client details origin. So if UAA is behind a reverse proxy, the origin will reflect the actual user IP address and not the proxy server's. --- .../uaa/authentication/UaaAuthenticationDetails.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationDetails.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationDetails.java index 7b98ea8732a..f2c783e2bbd 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationDetails.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationDetails.java @@ -52,7 +52,14 @@ public UaaAuthenticationDetails(HttpServletRequest request) { } public UaaAuthenticationDetails(HttpServletRequest request, String clientId) { WebAuthenticationDetails webAuthenticationDetails = new WebAuthenticationDetails(request); - this.origin = webAuthenticationDetails.getRemoteAddress(); + + String xForwardedFor = request.getHeader("X-Forwarded-For"); + if (xForwardedFor != null) { + this.origin = xForwardedFor; + } + else { + this.origin = webAuthenticationDetails.getRemoteAddress(); + } this.sessionId = webAuthenticationDetails.getSessionId(); if (clientId == null) { From 65bd6a20cdc33578e7cfb9822da3bfbe72f89caa Mon Sep 17 00:00:00 2001 From: Jeremy Coffield Date: Fri, 9 Oct 2015 11:00:38 -0700 Subject: [PATCH 053/103] Add configurability of LDAP extended attribute names. [#104932164] https://www.pivotaltracker.com/story/show/104932164 Signed-off-by: Jonathan Lo --- .../UaaAuthenticationDetails.java | 15 +- .../ExternalLoginAuthenticationManager.java | 86 +++++------ .../LdapLoginAuthenticationManager.java | 3 +- .../manager/LoginAuthenticationManager.java | 7 +- .../uaa/ldap/ExtendedLdapUserMapper.java | 12 +- .../ldap/extension/ExtendedLdapUserImpl.java | 13 +- ...xternalLoginAuthenticationManagerTest.java | 47 +++--- .../LdapLoginAuthenticationManagerTests.java | 144 +++++++++--------- uaa/src/main/resources/ldap-integration.xml | 6 +- 9 files changed, 162 insertions(+), 171 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationDetails.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationDetails.java index 7b98ea8732a..6b76c3309a0 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationDetails.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationDetails.java @@ -33,14 +33,14 @@ public class UaaAuthenticationDetails implements Serializable { public static final UaaAuthenticationDetails UNKNOWN = new UaaAuthenticationDetails(); + private boolean addNew; + private final String origin; private String sessionId; private String clientId; - private Map extendedAuthorizationInfo = new HashMap(); - private UaaAuthenticationDetails() { this.origin = "unknown"; this.sessionId = "unknown"; @@ -60,8 +60,7 @@ public UaaAuthenticationDetails(HttpServletRequest request, String clientId) { } else { this.clientId = clientId; } - Boolean addNew = Boolean.parseBoolean(request.getParameter(ADD_NEW)); - extendedAuthorizationInfo.put(ADD_NEW, addNew.toString()); + this.addNew = Boolean.parseBoolean(request.getParameter(ADD_NEW)); } public String getOrigin() { @@ -137,7 +136,11 @@ else if (!sessionId.equals(other.sessionId)) return true; } - public Map getExtendedAuthorizationInfo() { - return extendedAuthorizationInfo; + public boolean isAddNew() { + return addNew; + } + + public void setAddNew(boolean addNew) { + this.addNew = addNew; } } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java index 2d8f842e248..6e492f00ccf 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java @@ -85,22 +85,11 @@ public UaaUserDatabase getUserDatabase() { @Override public Authentication authenticate(Authentication request) throws AuthenticationException { - UserDetails req; - if (request.getPrincipal() instanceof UserDetails) { - req = (UserDetails) request.getPrincipal(); - } else if (request instanceof UsernamePasswordAuthenticationToken) { - String username = request.getPrincipal().toString(); - String password = request.getCredentials() != null ? request.getCredentials().toString() : ""; - req = new User(username, password, true, true, true, true, UaaAuthority.USER_AUTHORITIES); - } else if (request.getPrincipal() == null) { - logger.debug(this.getClass().getName() + "[" + name + "] cannot process null principal"); - return null; - } else { - logger.debug(this.getClass().getName() + "[" + name + "] cannot process request of type: " + request.getClass().getName()); + UaaUser user = getUser(request); + if (user == null) { return null; } - UaaUser user = getUser(req, getExtendedAuthorizationInfo(request)); boolean addnew = false; try { UaaUser temp = userDatabase.retrieveUserByName(user.getUsername(), getOrigin()); @@ -135,18 +124,6 @@ public Authentication authenticate(Authentication request) throws Authentication return success; } - protected Map getExtendedAuthorizationInfo(Authentication auth) { - Object details = auth.getDetails(); - if (details != null && details instanceof UaaAuthenticationDetails) { - UaaAuthenticationDetails uaaAuthenticationDetails = (UaaAuthenticationDetails) details; - Map result = uaaAuthenticationDetails.getExtendedAuthorizationInfo(); - if (result != null) { - return result; - } - } - return Collections.emptyMap(); - } - protected void publish(ApplicationEvent event) { if (eventPublisher != null) { eventPublisher.publishEvent(event); @@ -157,21 +134,32 @@ protected UaaUser userAuthenticated(Authentication request, UaaUser user) { return user; } - protected UaaUser getUser(UserDetails details, Map info) { - String name = details.getUsername(); - String email = info.get("email"); - - if (details instanceof Mailable) { - email = ((Mailable) details).getEmailAddress(); + protected UaaUser getUser(Authentication request) { + UserDetails userDetails; + if (request.getPrincipal() instanceof UserDetails) { + userDetails = (UserDetails) request.getPrincipal(); + } else if (request instanceof UsernamePasswordAuthenticationToken) { + String username = request.getPrincipal().toString(); + String password = request.getCredentials() != null ? request.getCredentials().toString() : ""; + userDetails = new User(username, password, true, true, true, true, UaaAuthority.USER_AUTHORITIES); + } else if (request.getPrincipal() == null) { + logger.debug(this.getClass().getName() + "[" + name + "] cannot process null principal"); + return null; + } else { + logger.debug(this.getClass().getName() + "[" + name + "] cannot process request of type: " + request.getClass().getName()); + return null; } - if (name == null && email != null) { - name = email; - } - if (name == null) { - throw new BadCredentialsException("Cannot determine username from credentials supplied"); - } - if (email == null) { + String name = userDetails.getUsername(); + String email; + + if (userDetails instanceof Mailable) { + email = ((Mailable) userDetails).getEmailAddress(); + + if (name == null) { + name = email; + } + } else if (name != null) { if (name.contains("@")) { if (name.split("@").length == 2 && !name.startsWith("@") && !name.endsWith("@")) { email = name; @@ -181,26 +169,24 @@ protected UaaUser getUser(UserDetails details, Map info) { } else { email = name + "@user.from." + getOrigin() + ".cf"; } + } else { + throw new BadCredentialsException("Cannot determine username from credentials supplied"); } + String givenName; String familyName; - if (details instanceof Named) { - Named names = (Named) details; + if (userDetails instanceof Named) { + Named names = (Named) userDetails; givenName = names.getGivenName(); familyName = names.getFamilyName(); } else { - givenName = info.get("given_name"); - if (givenName == null) { - givenName = email.split("@")[0]; - } - familyName = info.get("family_name"); - if (familyName == null) { - familyName = email.split("@")[1]; - } + String[] splitEmail = email.split("@"); + givenName = splitEmail[0]; + familyName = splitEmail[1]; } - String phoneNumber = (details instanceof DialableByPhone) ? ((DialableByPhone)details).getPhoneNumber() : null; - String externalId = (details instanceof ExternallyIdentifiable) ? ((ExternallyIdentifiable)details).getExternalId() : details.getUsername(); + String phoneNumber = (userDetails instanceof DialableByPhone) ? ((DialableByPhone) userDetails).getPhoneNumber() : null; + String externalId = (userDetails instanceof ExternallyIdentifiable) ? ((ExternallyIdentifiable) userDetails).getExternalId() : name; UaaUserPrototype userPrototype = new UaaUserPrototype() .withUsername(name) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java index 9d16c2aaf56..cc137685d4b 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java @@ -28,8 +28,7 @@ protected UaaUser userAuthenticated(Authentication request, UaaUser user) { boolean userModified = false; //we must check and see if the email address has changed between authentications if (request.getPrincipal() !=null && request.getPrincipal() instanceof ExtendedLdapUserDetails) { - ExtendedLdapUserDetails details = (ExtendedLdapUserDetails)request.getPrincipal(); - UaaUser fromRequest = getUser(details, getExtendedAuthorizationInfo(request)); + UaaUser fromRequest = getUser(request); if (fromRequest.getEmail()!=null && !fromRequest.getEmail().equals(user.getEmail())) { user = user.modifyEmail(fromRequest.getEmail()); userModified = true; diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LoginAuthenticationManager.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LoginAuthenticationManager.java index 8ec58716c42..2e89fbc1ec9 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LoginAuthenticationManager.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LoginAuthenticationManager.java @@ -82,12 +82,7 @@ public Authentication authenticate(Authentication request) throws Authentication if (authentication.isClientOnly()) { UaaUser user = getUser(req, info); UaaAuthenticationDetails authdetails = (UaaAuthenticationDetails) req.getDetails(); - boolean addNewAccounts = authdetails != null - && - authdetails.getExtendedAuthorizationInfo() != null - && - Boolean.parseBoolean(authdetails.getExtendedAuthorizationInfo().get( - UaaAuthenticationDetails.ADD_NEW)); + boolean addNewAccounts = authdetails != null && authdetails.isAddNew(); try { if (NotANumber.equals(user.getId())) { user = userDatabase.retrieveUserByName(user.getUsername(), user.getOrigin()); diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserMapper.java b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserMapper.java index 43bfccc4d66..205d602fbf8 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserMapper.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserMapper.java @@ -40,9 +40,9 @@ public class ExtendedLdapUserMapper extends LdapUserDetailsMapper { private static final Log logger = LogFactory.getLog(ExtendedLdapUserMapper.class); public static final String SUBSTITUTE_MAIL_ATTR_NAME = "substitute-mail-attribute"; private String mailAttributeName = "mail"; - private String givenNameAttributeName = "given_name"; - private String familyNameAttributeName = "family_name"; - private String phoneNumberAttributeName = "phone"; + private String givenNameAttributeName; + private String familyNameAttributeName; + private String phoneNumberAttributeName; private String mailSubstitute = null; private boolean mailSubstituteOverrides = false; @@ -123,7 +123,7 @@ public String getPhoneNumberAttributeName() { } public void setPhoneNumberAttributeName(String phoneNumberAttributeName) { - this.phoneNumberAttributeName = phoneNumberAttributeName.toLowerCase(); + this.phoneNumberAttributeName = phoneNumberAttributeName; } public String getGivenNameAttributeName() { @@ -131,7 +131,7 @@ public String getGivenNameAttributeName() { } public void setGivenNameAttributeName(String givenNameAttributeName) { - this.givenNameAttributeName = givenNameAttributeName.toLowerCase(); + this.givenNameAttributeName = givenNameAttributeName; } public String getFamilyNameAttributeName() { @@ -139,7 +139,7 @@ public String getFamilyNameAttributeName() { } public void setFamilyNameAttributeName(String familyNameAttributeName) { - this.familyNameAttributeName = familyNameAttributeName.toLowerCase(); + this.familyNameAttributeName = familyNameAttributeName; } public String getMailSubstitute() { diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/extension/ExtendedLdapUserImpl.java b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/extension/ExtendedLdapUserImpl.java index 944663e64ee..4d70b09892d 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/extension/ExtendedLdapUserImpl.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/extension/ExtendedLdapUserImpl.java @@ -27,9 +27,9 @@ public class ExtendedLdapUserImpl implements ExtendedLdapUserDetails { private String mailAttributeName = "mail"; - private String givenNameAttributeName = "given_name"; - private String familyNameAttributeName = "family_name"; - private String phoneNumberAttributeName = "phone"; + private String givenNameAttributeName; + private String familyNameAttributeName; + private String phoneNumberAttributeName; private String dn; private String password; private String username; @@ -165,7 +165,7 @@ public String getPhoneNumberAttributeName() { } public void setPhoneNumberAttributeName(String phoneNumberAttributeName) { - this.phoneNumberAttributeName = phoneNumberAttributeName; + this.phoneNumberAttributeName = phoneNumberAttributeName == null ? null : phoneNumberAttributeName.toLowerCase(); } public String getGivenNameAttributeName() { @@ -173,7 +173,7 @@ public String getGivenNameAttributeName() { } public void setGivenNameAttributeName(String givenNameAttributeName) { - this.givenNameAttributeName = givenNameAttributeName; + this.givenNameAttributeName = givenNameAttributeName == null ? null : givenNameAttributeName.toLowerCase(); } public String getFamilyNameAttributeName() { @@ -181,7 +181,7 @@ public String getFamilyNameAttributeName() { } public void setFamilyNameAttributeName(String familyNameAttributeName) { - this.familyNameAttributeName = familyNameAttributeName; + this.familyNameAttributeName = familyNameAttributeName == null ? null : familyNameAttributeName.toLowerCase(); } @Override @@ -192,6 +192,7 @@ public String getEmailAddress() { @Override public String getGivenName() { + if(givenNameAttributeName == null) return null; String[] attrValues = this.attributes.get(givenNameAttributeName); if(attrValues == null) return null; return attrValues[0]; diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManagerTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManagerTest.java index 3e7e1147916..52c5cce4fc7 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManagerTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManagerTest.java @@ -3,6 +3,9 @@ import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; import org.cloudfoundry.identity.uaa.authentication.event.UserAuthenticationSuccessEvent; +import org.cloudfoundry.identity.uaa.ldap.ExtendedLdapUserDetails; +import org.cloudfoundry.identity.uaa.ldap.extension.ExtendedLdapUserImpl; +import org.cloudfoundry.identity.uaa.user.Mailable; import org.cloudfoundry.identity.uaa.user.UaaUser; import org.cloudfoundry.identity.uaa.user.UaaUserDatabase; import org.junit.Before; @@ -30,6 +33,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; public class ExternalLoginAuthenticationManagerTest { @@ -60,10 +64,12 @@ private void mockUserDetails(UserDetails userDetails) { public void setUp() throws Exception { userDetails = mock(UserDetails.class); mockUserDetails(userDetails); + mockUaaWithUser(); + } + private void mockUaaWithUser() { applicationEventPublisher = mock(ApplicationEventPublisher.class); - user = mock(UaaUser.class); when(user.getUsername()).thenReturn(userName); when(user.getId()).thenReturn(userId); @@ -128,14 +134,10 @@ public void testAuthenticateUserDetailsPrincipal() throws Exception { @Test public void testAuthenticateWithAuthDetails() throws Exception { - Map extendedInfo = new HashMap<>(); - extendedInfo.put(UaaAuthenticationDetails.ADD_NEW,"false"); - UaaAuthenticationDetails uaaAuthenticationDetails = mock(UaaAuthenticationDetails.class); when(uaaAuthenticationDetails.getOrigin()).thenReturn(origin); when(uaaAuthenticationDetails.getClientId()).thenReturn(null); when(uaaAuthenticationDetails.getSessionId()).thenReturn(new RandomValueStringGenerator().generate()); - when(uaaAuthenticationDetails.getExtendedAuthorizationInfo()).thenReturn(extendedInfo); when(inputAuth.getDetails()).thenReturn(uaaAuthenticationDetails); Authentication result = manager.authenticate(inputAuth); @@ -149,26 +151,28 @@ public void testAuthenticateWithAuthDetails() throws Exception { @Test public void testNoUsernameOnlyEmail() throws Exception { - Map extendedInfo = new HashMap<>(); - extendedInfo.put(UaaAuthenticationDetails.ADD_NEW,"false"); String email = "joe@test.org"; - extendedInfo.put("email", email); + + userDetails = mock(UserDetails.class, withSettings().extraInterfaces(Mailable.class)); + when(((Mailable)userDetails).getEmailAddress()).thenReturn(email); + mockUserDetails(userDetails); + mockUaaWithUser(); UaaAuthenticationDetails uaaAuthenticationDetails = mock(UaaAuthenticationDetails.class); when(uaaAuthenticationDetails.getOrigin()).thenReturn(origin); when(uaaAuthenticationDetails.getClientId()).thenReturn(null); when(uaaAuthenticationDetails.getSessionId()).thenReturn(new RandomValueStringGenerator().generate()); - when(uaaAuthenticationDetails.getExtendedAuthorizationInfo()).thenReturn(extendedInfo); when(inputAuth.getDetails()).thenReturn(uaaAuthenticationDetails); - when(uaaUserDatabase.retrieveUserByName(eq(email), eq(origin))) - .thenReturn(null) - .thenReturn(user); when(user.getUsername()).thenReturn(email); + when(uaaUserDatabase.retrieveUserByName(email, origin)) + .thenReturn(user); + when(userDetails.getUsername()).thenReturn(null); Authentication result = manager.authenticate(inputAuth); assertNotNull(result); assertEquals(UaaAuthentication.class, result.getClass()); UaaAuthentication uaaAuthentication = (UaaAuthentication)result; + assertEquals(email,uaaAuthentication.getPrincipal().getName()); assertEquals(origin, uaaAuthentication.getPrincipal().getOrigin()); assertEquals(userId, uaaAuthentication.getPrincipal().getId()); @@ -176,14 +180,10 @@ public void testNoUsernameOnlyEmail() throws Exception { @Test(expected = BadCredentialsException.class) public void testNoUsernameNoEmail() throws Exception { - Map extendedInfo = new HashMap<>(); - extendedInfo.put(UaaAuthenticationDetails.ADD_NEW,"false"); - UaaAuthenticationDetails uaaAuthenticationDetails = mock(UaaAuthenticationDetails.class); when(uaaAuthenticationDetails.getOrigin()).thenReturn(origin); when(uaaAuthenticationDetails.getClientId()).thenReturn(null); when(uaaAuthenticationDetails.getSessionId()).thenReturn(new RandomValueStringGenerator().generate()); - when(uaaAuthenticationDetails.getExtendedAuthorizationInfo()).thenReturn(extendedInfo); when(inputAuth.getDetails()).thenReturn(uaaAuthenticationDetails); when(uaaUserDatabase.retrieveUserByName(anyString(), eq(origin))).thenReturn(null); when(userDetails.getUsername()).thenReturn(null); @@ -268,14 +268,21 @@ public void testAuthenticateLdapUserDetailsPrincipal() throws Exception { public void testAuthenticateCreateUserWithLdapUserDetailsPrincipal() throws Exception { String dn = "cn="+userName+",ou=Users,dc=test,dc=com"; String origin = "ldap"; - LdapUserDetails ldapUserDetails = mock(LdapUserDetails.class); - mockUserDetails(ldapUserDetails); - when(ldapUserDetails.getDn()).thenReturn(dn); + String email = "joe@test.org"; + + LdapUserDetails baseLdapUserDetails = mock(LdapUserDetails.class); + mockUserDetails(baseLdapUserDetails); + when(baseLdapUserDetails.getDn()).thenReturn(dn); + HashMap ldapAttrs = new HashMap<>(); + String ldapMailAttrName = "email"; + ldapAttrs.put(ldapMailAttrName, new String[]{email}); + ExtendedLdapUserImpl ldapUserDetails = new ExtendedLdapUserImpl(baseLdapUserDetails, ldapAttrs); + ldapUserDetails.setMailAttributeName(ldapMailAttrName); manager = new LdapLoginAuthenticationManager(); setupManager(); manager.setOrigin(origin); - + when(user.getEmail()).thenReturn(email); when(user.getOrigin()).thenReturn(origin); when(uaaUserDatabase.retrieveUserByName(eq(userName),eq(origin))) .thenReturn(null) diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java index 0e1df76ac32..becd7c0b730 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java @@ -29,6 +29,7 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.ldap.userdetails.LdapUserDetails; import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl; @@ -44,19 +45,48 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -public class LdapLoginAuthenticationManagerTests { +public class LdapLoginAuthenticationManagerTests { public static final String DN = "cn=marissa,ou=Users,dc=test,dc=com"; public static final String LDAP_EMAIL = "test@ldap.org"; public static final String TEST_EMAIL = "email@email.org"; - public static final String EXTERNAL_ID = "externalId"; public static final String USERNAME = "username"; + private static final String EMAIL_ATTRIBUTE = "email"; + private final String GIVEN_NAME_ATTRIBUTE = "firstname"; + private final String FAMILY_NAME_ATTRIBUTE = "surname"; + private final String PHONE_NUMBER_ATTTRIBUTE = "digits"; + + private static LdapUserDetails mockLdapUserDetails() { + LdapUserDetails userDetails = mock(LdapUserDetails.class); + setupGeneralExpectations(userDetails); + when(userDetails.getDn()).thenReturn(DN); + return userDetails; + } + + private static UserDetails mockNonLdapUserDetails() { + UserDetails userDetails = mock(UserDetails.class); + setupGeneralExpectations(userDetails); + return userDetails; + } + + private static void setupGeneralExpectations(UserDetails userDetails) { + when(userDetails.getUsername()).thenReturn(USERNAME); + when(userDetails.getPassword()).thenReturn("koala"); + when(userDetails.getAuthorities()).thenReturn(null); + when(userDetails.isAccountNonExpired()).thenReturn(true); + when(userDetails.isAccountNonLocked()).thenReturn(true); + when(userDetails.isCredentialsNonExpired()).thenReturn(true); + when(userDetails.isEnabled()).thenReturn(true); + } + LdapLoginAuthenticationManager am; ApplicationEventPublisher publisher; String origin = "test"; - Map info; + Map info = new HashMap<>(); UaaUser dbUser = getUaaUser(); Authentication auth; + ExtendedLdapUserImpl authUserDetail; + @Before public void setUp() { @@ -65,36 +95,42 @@ public void setUp() { am.setApplicationEventPublisher(publisher); am.setOrigin(origin); info = new HashMap<>(); - info.put("email",TEST_EMAIL); + String[] emails = {LDAP_EMAIL}; + String[] given_names = {"Marissa"}; + String[] family_names = {"Bloggs"}; + String[] phone_numbers = {"8675309"}; + info.put(EMAIL_ATTRIBUTE, emails); + info.put(GIVEN_NAME_ATTRIBUTE, given_names); + info.put(FAMILY_NAME_ATTRIBUTE, family_names); + info.put(PHONE_NUMBER_ATTTRIBUTE, phone_numbers); UaaUserDatabase db = mock(UaaUserDatabase.class); when(db.retrieveUserById(anyString())).thenReturn(dbUser); am.setUserDatabase(db); auth = mock(Authentication.class); when(auth.getAuthorities()).thenReturn(null); + authUserDetail = new ExtendedLdapUserImpl(mockLdapUserDetails(), info); + authUserDetail.setMailAttributeName(EMAIL_ATTRIBUTE); + authUserDetail.setGivenNameAttributeName(GIVEN_NAME_ATTRIBUTE); + authUserDetail.setFamilyNameAttributeName(FAMILY_NAME_ATTRIBUTE); + authUserDetail.setPhoneNumberAttributeName(PHONE_NUMBER_ATTTRIBUTE); + when(auth.getPrincipal()).thenReturn(authUserDetail); } @Test public void testGetUserWithExtendedLdapInfo() throws Exception { - UaaUser user = am.getUser(getExtendedLdapUserDetails(), info); + UaaUser user = am.getUser(auth); assertEquals(DN, user.getExternalId()); assertEquals(LDAP_EMAIL, user.getEmail()); assertEquals(origin, user.getOrigin()); } - @Test - public void testGetUserWithLdapInfo() throws Exception { - UaaUser user = am.getUser(getLdapUserDetails(), info); - // The LdapLoginAuthenticationManager is no longer responsible for overriding the external ID. - // Instead, see ExternalLoginAuthenticationManager and the ExternallyIdentifiable interface - // assertEquals(DN, user.getExternalId()); - assertEquals(TEST_EMAIL, user.getEmail()); - assertEquals(origin, user.getOrigin()); - } - @Test public void testGetUserWithNonLdapInfo() throws Exception { - UaaUser user = am.getUser(getUserDetails(), info); - assertEquals(USERNAME, user.getExternalId()); + UserDetails mockNonLdapUserDetails = mockNonLdapUserDetails(); + when(mockNonLdapUserDetails.getUsername()).thenReturn(TEST_EMAIL); + when(auth.getPrincipal()).thenReturn(mockNonLdapUserDetails); + UaaUser user = am.getUser(auth); + assertEquals(TEST_EMAIL, user.getExternalId()); assertEquals(TEST_EMAIL, user.getEmail()); assertEquals(origin, user.getOrigin()); } @@ -104,67 +140,31 @@ public void testUserAuthenticated() throws Exception { UaaUser user = getUaaUser(); am.setAutoAddAuthorities(true); UaaUser result = am.userAuthenticated(auth, user); - assertSame(dbUser,result); - verify(publisher,times(1)).publishEvent(Matchers.anyObject()); + assertSame(dbUser, result); + verify(publisher, times(1)).publishEvent(Matchers.anyObject()); am.setAutoAddAuthorities(false); result = am.userAuthenticated(auth, user); - assertSame(dbUser,result); - verify(publisher,times(2)).publishEvent(Matchers.anyObject()); - } - - protected User getUserDetails() { - UaaUser uaaUser = getUaaUser(); - return new User( - uaaUser.getUsername(), - uaaUser.getPassword(), - true, - true, - true, - true, - uaaUser.getAuthorities() - ); - } - - protected LdapUserDetails getLdapUserDetails() { - UaaUser uaaUser = getUaaUser(); - LdapUserDetailsImpl.Essence essence = new LdapUserDetailsImpl.Essence(); - essence.setDn(DN); - essence.setUsername(uaaUser.getUsername()); - essence.setPassword(uaaUser.getPassword()); - essence.setEnabled(true); - essence.setAccountNonExpired(true); - essence.setCredentialsNonExpired(true); - essence.setAccountNonLocked(true); - essence.setAuthorities(uaaUser.getAuthorities()); - return essence.createUserDetails(); - } - - protected ExtendedLdapUserDetails getExtendedLdapUserDetails() { - Map attributes = new HashMap<>(); - LdapUserDetails details = getLdapUserDetails(); - attributes.put(SpringSecurityLdapTemplate.DN_KEY, new String[] {details.getDn()}); - attributes.put("mail", new String[] {LDAP_EMAIL}); - ExtendedLdapUserImpl result = new ExtendedLdapUserImpl(details,attributes); - return result; + assertSame(dbUser, result); + verify(publisher, times(2)).publishEvent(Matchers.anyObject()); } protected UaaUser getUaaUser() { return new UaaUser( - "id", - USERNAME, - "password", - TEST_EMAIL, - UaaAuthority.USER_AUTHORITIES, - "givenname", - "familyname", - new Date(), - new Date(), - Origin.ORIGIN, - EXTERNAL_ID, - false, - IdentityZoneHolder.get().getId(), - null, - null); + "id", + USERNAME, + "password", + TEST_EMAIL, + UaaAuthority.USER_AUTHORITIES, + "givenname", + "familyname", + new Date(), + new Date(), + Origin.ORIGIN, + DN, + false, + IdentityZoneHolder.get().getId(), + null, + null); } } \ No newline at end of file diff --git a/uaa/src/main/resources/ldap-integration.xml b/uaa/src/main/resources/ldap-integration.xml index 8998b910034..42bf7de195e 100644 --- a/uaa/src/main/resources/ldap-integration.xml +++ b/uaa/src/main/resources/ldap-integration.xml @@ -47,9 +47,9 @@ - - - + + + From 4e0289469414f7d0642d8abe6180ed0982c540f6 Mon Sep 17 00:00:00 2001 From: Jonathan Lo Date: Fri, 9 Oct 2015 12:20:48 -0700 Subject: [PATCH 054/103] Remove unsupported attributes from LDIF. [#104932164] http://www.pivotaltracker.com/story/show/104932164 Signed-off-by: Jeremy Coffield --- uaa/src/main/resources/ldap-integration.xml | 6 +++--- uaa/src/main/resources/ldap_init.ldif | 2 -- uaa/src/main/resources/uaa.yml | 3 +++ 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/uaa/src/main/resources/ldap-integration.xml b/uaa/src/main/resources/ldap-integration.xml index 42bf7de195e..abcbbac6b49 100644 --- a/uaa/src/main/resources/ldap-integration.xml +++ b/uaa/src/main/resources/ldap-integration.xml @@ -47,9 +47,9 @@ - - - + + + diff --git a/uaa/src/main/resources/ldap_init.ldif b/uaa/src/main/resources/ldap_init.ldif index 185fa57329c..8ce206ebc95 100644 --- a/uaa/src/main/resources/ldap_init.ldif +++ b/uaa/src/main/resources/ldap_init.ldif @@ -26,7 +26,6 @@ streetAddress: 1111 Admin St l: Adminton st: California postalCode: 94119 -co: United States dn: cn=marissa,ou=Users,dc=test,dc=com changetype: add @@ -67,7 +66,6 @@ streetAddress: 1111 Marissa St l: Marissaville st: Florida postalCode: 32561 -co: United States dn: cn=marissa4,ou=Users,dc=test,dc=com changetype: add diff --git a/uaa/src/main/resources/uaa.yml b/uaa/src/main/resources/uaa.yml index 293b8a53c26..fc0732318bb 100755 --- a/uaa/src/main/resources/uaa.yml +++ b/uaa/src/main/resources/uaa.yml @@ -91,6 +91,9 @@ # sslCertificateAlias: ldaps # emailDomain: # - example.com +# attributeMappings: +# given_name: firstName +# family_name: surname #ldap: # profile: From de8b14e85301d68dc02a89d6e63855090f959863 Mon Sep 17 00:00:00 2001 From: Jeremy Coffield Date: Fri, 9 Oct 2015 13:49:05 -0700 Subject: [PATCH 055/103] Add necessary (empty) default value for parser. [#104932164] https://www.pivotaltracker.com/story/show/104932164 Signed-off-by: Jonathan Lo --- uaa/src/main/resources/ldap-integration.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uaa/src/main/resources/ldap-integration.xml b/uaa/src/main/resources/ldap-integration.xml index abcbbac6b49..42bf7de195e 100644 --- a/uaa/src/main/resources/ldap-integration.xml +++ b/uaa/src/main/resources/ldap-integration.xml @@ -47,9 +47,9 @@ - - - + + + From 7adc0b61dce6c284c46cc1453fbc46e9dd61a46b Mon Sep 17 00:00:00 2001 From: Jonathan Lo Date: Fri, 9 Oct 2015 17:29:07 -0700 Subject: [PATCH 056/103] fixing 500 error due to user details not being mocked out properly [#104932164] https://www.pivotaltracker.com/story/show/104932164 Signed-off-by: Jeremy Coffield --- .../ExternalLoginAuthenticationManager.java | 38 +++++++++++-------- ...xternalLoginAuthenticationManagerTest.java | 2 - .../LdapLoginAuthenticationManagerTests.java | 4 -- uaa/src/main/resources/ldap-integration.xml | 6 +-- uaa/src/main/resources/ldap_init.ldif | 8 ++-- .../uaa/mock/ldap/LdapMockMvcTests.java | 5 +-- 6 files changed, 31 insertions(+), 32 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java index 6e492f00ccf..9396b32593d 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java @@ -151,7 +151,7 @@ protected UaaUser getUser(Authentication request) { } String name = userDetails.getUsername(); - String email; + String email = null; if (userDetails instanceof Mailable) { email = ((Mailable) userDetails).getEmailAddress(); @@ -159,30 +159,38 @@ protected UaaUser getUser(Authentication request) { if (name == null) { name = email; } - } else if (name != null) { - if (name.contains("@")) { - if (name.split("@").length == 2 && !name.startsWith("@") && !name.endsWith("@")) { - email = name; + } + + if (email == null) { + if (name != null) { + if (name.contains("@")) { + if (name.split("@").length == 2 && !name.startsWith("@") && !name.endsWith("@")) { + email = name; + } else { + email = name.replaceAll("@", "") + "@user.from." + getOrigin() + ".cf"; + } } else { - email = name.replaceAll("@", "") + "@user.from." + getOrigin() + ".cf"; + email = name + "@user.from." + getOrigin() + ".cf"; } } else { - email = name + "@user.from." + getOrigin() + ".cf"; + throw new BadCredentialsException("Cannot determine username from credentials supplied"); } - } else { - throw new BadCredentialsException("Cannot determine username from credentials supplied"); } - String givenName; - String familyName; + String givenName = null; + String familyName = null; if (userDetails instanceof Named) { Named names = (Named) userDetails; givenName = names.getGivenName(); familyName = names.getFamilyName(); - } else { - String[] splitEmail = email.split("@"); - givenName = splitEmail[0]; - familyName = splitEmail[1]; + } + + if(givenName == null) { + givenName = email.split("@")[0]; + } + + if(familyName == null) { + familyName = email.split("@")[1]; } String phoneNumber = (userDetails instanceof DialableByPhone) ? ((DialableByPhone) userDetails).getPhoneNumber() : null; diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManagerTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManagerTest.java index 52c5cce4fc7..da1a19e2edc 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManagerTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManagerTest.java @@ -3,7 +3,6 @@ import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; import org.cloudfoundry.identity.uaa.authentication.event.UserAuthenticationSuccessEvent; -import org.cloudfoundry.identity.uaa.ldap.ExtendedLdapUserDetails; import org.cloudfoundry.identity.uaa.ldap.extension.ExtendedLdapUserImpl; import org.cloudfoundry.identity.uaa.user.Mailable; import org.cloudfoundry.identity.uaa.user.UaaUser; @@ -22,7 +21,6 @@ import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; import java.util.HashMap; -import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java index becd7c0b730..86324e0d4c0 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java @@ -15,9 +15,7 @@ package org.cloudfoundry.identity.uaa.authentication.manager; import org.cloudfoundry.identity.uaa.authentication.Origin; -import org.cloudfoundry.identity.uaa.ldap.ExtendedLdapUserDetails; import org.cloudfoundry.identity.uaa.ldap.extension.ExtendedLdapUserImpl; -import org.cloudfoundry.identity.uaa.ldap.extension.SpringSecurityLdapTemplate; import org.cloudfoundry.identity.uaa.user.UaaAuthority; import org.cloudfoundry.identity.uaa.user.UaaUser; import org.cloudfoundry.identity.uaa.user.UaaUserDatabase; @@ -28,10 +26,8 @@ import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.core.Authentication; -import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.ldap.userdetails.LdapUserDetails; -import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl; import java.util.Date; import java.util.HashMap; diff --git a/uaa/src/main/resources/ldap-integration.xml b/uaa/src/main/resources/ldap-integration.xml index 42bf7de195e..da5263a73a1 100644 --- a/uaa/src/main/resources/ldap-integration.xml +++ b/uaa/src/main/resources/ldap-integration.xml @@ -47,9 +47,9 @@ - - - + + + diff --git a/uaa/src/main/resources/ldap_init.ldif b/uaa/src/main/resources/ldap_init.ldif index 8ce206ebc95..76f9c8fbdec 100644 --- a/uaa/src/main/resources/ldap_init.ldif +++ b/uaa/src/main/resources/ldap_init.ldif @@ -19,9 +19,9 @@ sn: Administrator userPassword: adminsecret uid: 3378a03c-fc8c-46b6-8a76-f67f9d7a7b4a mail: admin@test.com -givenName: Bob +givenname: Bob initials: X -telephoneNumber: 8885550987 +telephonenumber: 8885550987 streetAddress: 1111 Admin St l: Adminton st: California @@ -59,9 +59,9 @@ userPassword: ldap3 uid: 20f459e0-e30b-4d1f-998c-3ded7f769db3 mail: marissa3@test.com sn: Lastnamerton -givenName: Marissa +givenname: Marissa initials: M -telephoneNumber: 8885550986 +telephonenumber: 8885550986 streetAddress: 1111 Marissa St l: Marissaville st: Florida diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java index 369024ee0d6..4ad2fe7b95e 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java @@ -45,7 +45,6 @@ import org.junit.Assume; import org.junit.Before; import org.junit.BeforeClass; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -59,7 +58,6 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.ldap.server.ApacheDSContainer; import org.springframework.security.ldap.server.ApacheDsSSLContainer; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; import org.springframework.security.web.FilterChainProxy; @@ -89,7 +87,6 @@ import static org.springframework.http.MediaType.TEXT_HTML_VALUE; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -709,7 +706,7 @@ public void validateEmailMissingForLdapUser() throws Exception { assertThat(result.getResponse().getContentAsString(), containsString("\"username\":\"" + username + "\"")); assertThat(result.getResponse().getContentAsString(), containsString("\"email\":\"marissa7@user.from.ldap.cf\"")); assertEquals("ldap", getOrigin(username)); - assertEquals("marissa7@user.from.ldap.cf",getEmail(username)); + assertEquals("marissa7@user.from.ldap.cf", getEmail(username)); } @Test From 90171d1858f7a40b5461f1a5d9fa2a6244bf10a9 Mon Sep 17 00:00:00 2001 From: Jonathan Lo Date: Mon, 12 Oct 2015 10:24:59 -0700 Subject: [PATCH 057/103] Save phone number on saml shadow user [#104932356] https://www.pivotaltracker.com/story/show/104932356 Signed-off-by: Madhura Bhave --- .../uaa/user/JdbcUaaUserDatabase.java | 28 +++++++++++---- .../saml/LoginSamlAuthenticationProvider.java | 34 +++++++++++-------- .../LoginSamlAuthenticationProviderTests.java | 2 +- 3 files changed, 41 insertions(+), 23 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabase.java b/common/src/main/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabase.java index 77f3ef617ca..2601ff44d8f 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabase.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabase.java @@ -14,6 +14,7 @@ import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -38,7 +39,7 @@ */ public class JdbcUaaUserDatabase implements UaaUserDatabase { - public static final String USER_FIELDS = "id,username,password,email,givenName,familyName,created,lastModified,authorities,origin,external_id,verified,identity_zone_id,salt,passwd_lastmodified "; + public static final String USER_FIELDS = "id,username,password,email,givenName,familyName,created,lastModified,authorities,origin,external_id,verified,identity_zone_id,salt,passwd_lastmodified,phoneNumber "; public static final String DEFAULT_USER_BY_USERNAME_QUERY = "select " + USER_FIELDS + "from users " + "where lower(username) = ? and active=? and origin=? and identity_zone_id=?"; @@ -95,16 +96,29 @@ private final class UaaUserRowMapper implements RowMapper { @Override public UaaUser mapRow(ResultSet rs, int rowNum) throws SQLException { String id = rs.getString(1); + UaaUserPrototype prototype = new UaaUserPrototype().withId(id) + .withUsername(rs.getString(2)) + .withPassword(rs.getString(3)) + .withEmail(rs.getString(4)) + .withAuthorities(getDefaultAuthorities(rs.getString(9))) + .withGivenName(rs.getString(5)) + .withFamilyName(rs.getString(6)) + .withPhoneNumber(rs.getString(16)) + .withCreated(rs.getTimestamp(7)) + .withModified(rs.getTimestamp(8)) + .withOrigin(rs.getString(10)) + .withExternalId(rs.getString(11)) + .withVerified(rs.getBoolean(12)) + .withZoneId(rs.getString(13)) + .withSalt(rs.getString(14)) + .withPasswordLastModified(rs.getTimestamp(15)); + if (userAuthoritiesQuery == null) { - return new UaaUser(id, rs.getString(2), rs.getString(3), rs.getString(4), - getDefaultAuthorities(rs.getString(9)), rs.getString(5), rs.getString(6), - rs.getTimestamp(7), rs.getTimestamp(8), rs.getString(10), rs.getString(11), rs.getBoolean(12), rs.getString(13), rs.getString(14),rs.getTimestamp(15)); + return new UaaUser(prototype); } else { List authorities = AuthorityUtils .commaSeparatedStringToAuthorityList(getAuthorities(id)); - return new UaaUser(id, rs.getString(2), rs.getString(3), rs.getString(4), - authorities, rs.getString(5), rs.getString(6), - rs.getTimestamp(7), rs.getTimestamp(8), rs.getString(10), rs.getString(11), rs.getBoolean(12), rs.getString(13), rs.getString(14),rs.getTimestamp(15)); + return new UaaUser(prototype.withAuthorities(authorities)); } } diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java b/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java index a9f1b88a4e7..c2169d52ba2 100644 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java @@ -25,6 +25,7 @@ import org.cloudfoundry.identity.uaa.scim.ScimGroupExternalMembershipManager; import org.cloudfoundry.identity.uaa.user.UaaUser; import org.cloudfoundry.identity.uaa.user.UaaUserDatabase; +import org.cloudfoundry.identity.uaa.user.UaaUserPrototype; import org.cloudfoundry.identity.uaa.zone.IdentityProvider; import org.cloudfoundry.identity.uaa.zone.IdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.zone.IdentityZone; @@ -66,6 +67,7 @@ import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.FAMILY_NAME_ATTRIBUTE_NAME; import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.GIVEN_NAME_ATTRIBUTE_NAME; import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.GROUP_ATTRIBUTE_NAME; +import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.PHONE_NUMBER_ATTRIBUTE_NAME; public class LoginSamlAuthenticationProvider extends SAMLAuthenticationProvider implements ApplicationEventPublisherAware { @@ -241,6 +243,7 @@ protected UaaUser getUser(UaaPrincipal principal, Map userAttribu String email = userAttributes.get(EMAIL_ATTRIBUTE_NAME); String givenName = userAttributes.get(GIVEN_NAME_ATTRIBUTE_NAME); String familyName = userAttributes.get(FAMILY_NAME_ATTRIBUTE_NAME); + String phoneNumber = userAttributes.get(PHONE_NUMBER_ATTRIBUTE_NAME); String userId = Origin.NotANumber; String origin = principal.getOrigin()!=null?principal.getOrigin():Origin.LOGIN_SERVER; String zoneId = principal.getZoneId(); @@ -272,21 +275,22 @@ protected UaaUser getUser(UaaPrincipal principal, Map userAttribu familyName = email.split("@")[1]; } return new UaaUser( - userId, - name, - "" /*zero length password for login server */, - email, - Collections.EMPTY_LIST, - givenName, - familyName, - new Date(), - new Date(), - origin, - name, - false, - zoneId, - null, - null); + new UaaUserPrototype().withId(userId) + .withUsername(name) + .withPassword("") /*zero length password for login server */ + .withEmail(email) + .withAuthorities(Collections.EMPTY_LIST) + .withGivenName(givenName) + .withFamilyName(familyName) + .withPhoneNumber(phoneNumber) + .withCreated(new Date()) + .withModified(new Date()) + .withOrigin(origin) + .withExternalId(name) + .withVerified(false) + .withZoneId(zoneId) + .withSalt(null) + .withPasswordLastModified(null)); } } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java index ac87ab69f0f..e32a39e6399 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java @@ -284,7 +284,7 @@ public void shadowAccount_createdWith_MappedUserAttributes() throws Exception { assertEquals("Marissa", user.getGivenName()); assertEquals("Bloggs", user.getFamilyName()); assertEquals("marissa.bloggs@test.com", user.getEmail()); -// assertEquals("1234567890", user.get()); + assertEquals("1234567890", user.getPhoneNumber()); } @Test From a4d70f8edd06a0941dbfd96138e106a71224e84f Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Mon, 12 Oct 2015 10:53:02 -0700 Subject: [PATCH 058/103] Remove unused new_invite_links section from the invitations response [finishes #105360776] https://www.pivotaltracker.com/story/show/105360776 Signed-off-by: Jonathan Lo --- .../identity/uaa/invitations/InvitationsResponse.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsResponse.java b/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsResponse.java index b3fed8740b7..1463025dc40 100644 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsResponse.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsResponse.java @@ -13,8 +13,6 @@ public class InvitationsResponse { @JsonProperty(value="new_invites") private List newInvites = new ArrayList<>(); - @JsonProperty(value="new_invite_links") - private List newInviteLinks = new ArrayList<>(); @JsonProperty(value="failed_invites") private List failedInvites = new ArrayList<>(); From 2d44d2c690f06e826775003c7bf748f174041d63 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Thu, 8 Oct 2015 17:54:50 -0700 Subject: [PATCH 059/103] Refactor URL construction to be based on request context (not config) [#104590682] https://www.pivotaltracker.com/story/show/104590682 Signed-off-by: Paul Warren --- .../identity/uaa/util/UaaUrlUtils.java | 17 +-- .../identity/uaa/util/UaaUrlUtilsTest.java | 133 ++++++++++++++++-- .../uaa/invitations/InvitationsEndpoint.java | 3 +- login/src/main/resources/login-ui.xml | 1 - .../EmailAccountCreationServiceTests.java | 21 ++- .../login/EmailChangeEmailServiceTest.java | 15 +- .../login/ResetPasswordControllerTest.java | 109 ++++++++------ .../login/AccountsControllerMockMvcTests.java | 58 +++++--- 8 files changed, 261 insertions(+), 96 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtils.java b/common/src/main/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtils.java index 2362532db82..9d38ead42b3 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtils.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtils.java @@ -16,6 +16,7 @@ import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.springframework.util.StringUtils; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder; import javax.servlet.http.HttpServletRequest; @@ -27,17 +28,13 @@ public class UaaUrlUtils { - private final String uaaBaseUrl; - - public UaaUrlUtils(String uaaBaseUrl) { - this.uaaBaseUrl = uaaBaseUrl; - } + public UaaUrlUtils() {} public String getUaaUrl() { return getUaaUrl(""); } - public String getUaaUrl(String path) { + public static String getUaaUrl(String path) { return getURIBuilder(path).build().toUriString(); } @@ -45,12 +42,8 @@ public String getUaaHost() { return getURIBuilder("").build().getHost(); } - private UriComponentsBuilder getURIBuilder(String path) { - UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(uaaBaseUrl).path(path); - String subdomain = IdentityZoneHolder.get().getSubdomain(); - if (!StringUtils.isEmpty(subdomain)) { - builder.host(subdomain + "." + builder.build().getHost()); - } + private static UriComponentsBuilder getURIBuilder(String path) { + UriComponentsBuilder builder = ServletUriComponentsBuilder.fromCurrentContextPath().path(path); return builder; } diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtilsTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtilsTest.java index 450af0f4e75..f1c42a547de 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtilsTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtilsTest.java @@ -18,12 +18,18 @@ import org.apache.commons.httpclient.util.URIUtil; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; +import org.cloudfoundry.identity.uaa.zone.MultitenancyFixture; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.util.UriUtils; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.core.StringStartsWith.startsWith; import static org.junit.Assert.*; public class UaaUrlUtilsTest { @@ -32,7 +38,11 @@ public class UaaUrlUtilsTest { @Before public void setUp() throws Exception { - uaaURLUtils = new UaaUrlUtils("http://uaa.example.com"); + uaaURLUtils = new UaaUrlUtils(); + + MockHttpServletRequest request = new MockHttpServletRequest(); + ServletRequestAttributes attrs = new ServletRequestAttributes(request); + RequestContextHolder.setRequestAttributes(attrs); } @After @@ -42,49 +52,142 @@ public void tearDown() throws Exception { @Test public void testGetUaaUrl() throws Exception { - assertEquals("http://uaa.example.com", uaaURLUtils.getUaaUrl()); + assertEquals("http://localhost", uaaURLUtils.getUaaUrl()); } @Test public void testGetUaaUrlWithPath() throws Exception { - assertEquals("http://uaa.example.com/login", uaaURLUtils.getUaaUrl("/login")); - assertEquals("http://uaa.example.com/login", uaaURLUtils.getUaaUrl("login")); + assertEquals("http://localhost/login", uaaURLUtils.getUaaUrl("/login")); } @Test public void testGetUaaUrlWithZone() throws Exception { setIdentityZone("zone1"); - assertEquals("http://zone1.uaa.example.com", uaaURLUtils.getUaaUrl()); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setScheme("http"); + request.setServerName("zone1.localhost"); + + ServletRequestAttributes attrs = new ServletRequestAttributes(request); + + RequestContextHolder.setRequestAttributes(attrs); + + assertEquals("http://zone1.localhost", uaaURLUtils.getUaaUrl()); } @Test public void testGetUaaUrlWithZoneAndPath() throws Exception { setIdentityZone("zone1"); - assertEquals("http://zone1.uaa.example.com/login", uaaURLUtils.getUaaUrl("/login")); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setScheme("http"); + request.setServerName("zone1.localhost"); + + ServletRequestAttributes attrs = new ServletRequestAttributes(request); + + RequestContextHolder.setRequestAttributes(attrs); + + assertEquals("http://zone1.localhost/login", uaaURLUtils.getUaaUrl("/login")); } @Test public void testGetHost() throws Exception { - assertEquals("uaa.example.com", uaaURLUtils.getUaaHost()); + assertEquals("localhost", uaaURLUtils.getUaaHost()); } @Test public void testGetHostWithZone() throws Exception { setIdentityZone("zone1"); - assertEquals("zone1.uaa.example.com", uaaURLUtils.getUaaHost()); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setScheme("http"); + request.setServerName("zone1.localhost"); + + ServletRequestAttributes attrs = new ServletRequestAttributes(request); + RequestContextHolder.setRequestAttributes(attrs); + + assertEquals("zone1.localhost", uaaURLUtils.getUaaHost()); + } + + @Test + public void testLocalhostPortAndContextPathUrl() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setScheme("http"); + request.setServerName("localhost"); + request.setServerPort(8080); + request.setContextPath("/uaa"); + + ServletRequestAttributes attrs = new ServletRequestAttributes(request); + + RequestContextHolder.setRequestAttributes(attrs); + + UaaUrlUtils urlUtils = new UaaUrlUtils(); + String url = urlUtils.getUaaUrl("/something"); + assertThat(url, is("http://localhost:8080/uaa/something")); + } + + @Test + public void testSecurityProtocol() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setScheme("https"); + request.setServerPort(8443); + request.setServerName("localhost"); + + ServletRequestAttributes attrs = new ServletRequestAttributes(request); + + RequestContextHolder.setRequestAttributes(attrs); + + UaaUrlUtils urlUtils = new UaaUrlUtils(); + String url = urlUtils.getUaaUrl("/something"); + assertThat(url, is("https://localhost:8443/something")); + } + + @Test + public void testMultiDomainUrls() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setScheme("http"); + request.setServerName("login.localhost"); + + ServletRequestAttributes attrs = new ServletRequestAttributes(request); + + RequestContextHolder.setRequestAttributes(attrs); + + UaaUrlUtils urlUtils = new UaaUrlUtils(); + String url = urlUtils.getUaaUrl("/something"); + assertThat(url, is("http://login.localhost/something")); } @Test - public void testDecodeScopes() throws Exception { - String xWWWFormEncodedscopes = "scim.userids+password.write+openid+cloud_controller.write+cloud_controller.read"; - System.out.println(URLDecoder.decode(xWWWFormEncodedscopes)); - System.out.println(URIUtil.decode(xWWWFormEncodedscopes,"UTF-8")); - System.out.println(UriUtils.decode(xWWWFormEncodedscopes,"UTF-8")); + public void testZonedAndMultiDomainUrls() { + IdentityZoneHolder.set(MultitenancyFixture.identityZone("testzone1-id", "testzone1")); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setScheme("http"); + request.setServerName("testzone1.login.localhost"); + + ServletRequestAttributes attrs = new ServletRequestAttributes(request); + + RequestContextHolder.setRequestAttributes(attrs); + + UaaUrlUtils urlUtils = new UaaUrlUtils(); + String url = urlUtils.getUaaUrl("/something"); + assertThat(url, is("http://testzone1.login.localhost/something")); + } + + @Test + public void testXForwardedPrefixUrls() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setScheme("http"); + request.setServerName("login.localhost"); + request.addHeader("X-Forwarded-Prefix", "/prefix"); + + ServletRequestAttributes attrs = new ServletRequestAttributes(request); + + RequestContextHolder.setRequestAttributes(attrs); - //Assert.assertEquals(URLDecoder.decode(xWWWFormEncodedscopes), UriUtils.decode(xWWWFormEncodedscopes, "UTF-8")); + UaaUrlUtils urlUtils = new UaaUrlUtils(); + String url = urlUtils.getUaaUrl("/something"); + assertThat(url, is("http://login.localhost/prefix/something")); } private void setIdentityZone(String subdomain) { @@ -92,4 +195,4 @@ private void setIdentityZone(String subdomain) { zone.setSubdomain(subdomain); IdentityZoneHolder.set(zone); } -} \ No newline at end of file +} diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsEndpoint.java b/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsEndpoint.java index 506cee02b53..8f2a8792252 100644 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsEndpoint.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsEndpoint.java @@ -9,6 +9,7 @@ import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceConflictException; import org.cloudfoundry.identity.uaa.util.DomainFilter; import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.cloudfoundry.identity.uaa.util.UaaUrlUtils; import org.cloudfoundry.identity.uaa.zone.IdentityProvider; import org.cloudfoundry.identity.uaa.zone.IdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; @@ -84,7 +85,7 @@ public ResponseEntity inviteUsers(@RequestBody InvitationsR if (providers.size() == 1) { ScimUser user = findOrCreateUser(email, providers.get(0).getOriginKey()); - String accountsUrl = ServletUriComponentsBuilder.fromCurrentContextPath().path("/invitations/accept").build().toUriString(); + String accountsUrl = UaaUrlUtils.getUaaUrl("/invitations/accept"); Map data = new HashMap<>(); data.put(InvitationConstants.USER_ID, user.getId()); diff --git a/login/src/main/resources/login-ui.xml b/login/src/main/resources/login-ui.xml index 7594257e363..76de31278d5 100644 --- a/login/src/main/resources/login-ui.xml +++ b/login/src/main/resources/login-ui.xml @@ -555,7 +555,6 @@ - diff --git a/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailAccountCreationServiceTests.java b/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailAccountCreationServiceTests.java index 9e357d9db1a..7db358a0b56 100644 --- a/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailAccountCreationServiceTests.java +++ b/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailAccountCreationServiceTests.java @@ -35,7 +35,6 @@ import org.springframework.web.context.request.ServletRequestAttributes; import org.thymeleaf.spring4.SpringTemplateEngine; -import java.sql.Time; import java.sql.Timestamp; import java.util.Arrays; import java.util.Collections; @@ -86,11 +85,17 @@ public void setUp() throws Exception { details = mock(ClientDetails.class); passwordValidator = mock(PasswordValidator.class); emailAccountCreationService = initEmailAccountCreationService("pivotal"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setScheme("http"); + request.setServerName("uaa.example.com"); + ServletRequestAttributes attrs = new ServletRequestAttributes(request); + RequestContextHolder.setRequestAttributes(attrs); } private EmailAccountCreationService initEmailAccountCreationService(String brand) { return new EmailAccountCreationService(templateEngine, messageService, codeStore, - scimUserProvisioning, clientDetailsService, passwordValidator, new UaaUrlUtils("http://uaa.example.com"), + scimUserProvisioning, clientDetailsService, passwordValidator, new UaaUrlUtils(), brand); } @@ -123,6 +128,12 @@ public void testBeginActivationInOtherZone() throws Exception { IdentityZone zone = MultitenancyFixture.identityZone("test-zone-id", "test"); IdentityZoneHolder.set(zone); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setScheme("http"); + request.setServerName("test.uaa.example.com"); + ServletRequestAttributes attrs = new ServletRequestAttributes(request); + RequestContextHolder.setRequestAttributes(attrs); + when(scimUserProvisioning.createUser(any(ScimUser.class), anyString())).thenReturn(user); when(codeStore.generateCode(eq(data), any(Timestamp.class))).thenReturn(code); emailAccountCreationService.beginActivation("user@example.com", "password", "login", redirectUri); @@ -295,6 +306,12 @@ public void testResendVerificationCodeWithinZone() throws Exception { IdentityZone zone = MultitenancyFixture.identityZone("test-zone-id", "test"); IdentityZoneHolder.set(zone); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setScheme("http"); + request.setServerName("test.uaa.example.com"); + ServletRequestAttributes attrs = new ServletRequestAttributes(request); + RequestContextHolder.setRequestAttributes(attrs); + setUpResendCodeExpectations(setUpForSuccess("http://example.com/redirect")); emailAccountCreationService.resendVerificationCode(user.getPrimaryEmail(), details.getClientId()); ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); diff --git a/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailChangeEmailServiceTest.java b/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailChangeEmailServiceTest.java index 814851cee95..1eedda57d35 100644 --- a/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailChangeEmailServiceTest.java +++ b/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailChangeEmailServiceTest.java @@ -93,7 +93,7 @@ public void setUp() throws Exception { codeStore = mock(ExpiringCodeStore.class); clientDetailsService = mock(ClientDetailsService.class); messageService = mock(EmailService.class); - uaaUrlUtils = new UaaUrlUtils("http://uaa.example.com/uaa"); + uaaUrlUtils = new UaaUrlUtils(); emailChangeEmailService = new EmailChangeEmailService(templateEngine, messageService, scimUserProvisioning, uaaUrlUtils, "pivotal", codeStore, clientDetailsService); request = new MockHttpServletRequest(); @@ -110,7 +110,7 @@ public void beginEmailChange() throws Exception { eq("new@example.com"), eq(MessageType.CHANGE_EMAIL), eq("Pivotal Email change verification"), - contains("Verify your email") + contains("Verify your email") ); } @@ -140,7 +140,7 @@ public void testBeginEmailChangeWithOssBrand() throws Exception { String emailBody = emailBodyArgument.getValue(); - assertThat(emailBody, containsString("Verify your email")); + assertThat(emailBody, containsString("Verify your email")); assertThat(emailBody, containsString("an account")); assertThat(emailBody, containsString("Cloud Foundry")); assertThat(emailBody, not(containsString("a Pivotal ID"))); @@ -150,6 +150,13 @@ public void testBeginEmailChangeWithOssBrand() throws Exception { public void testBeginEmailChangeInOtherZone() throws Exception { IdentityZoneHolder.set(MultitenancyFixture.identityZone("test-zone-id", "test")); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setScheme("http"); + request.setServerName("test.localhost"); + request.setContextPath("/login"); + ServletRequestAttributes attrs = new ServletRequestAttributes(request); + RequestContextHolder.setRequestAttributes(attrs); + setUpForBeginEmailChange(); ArgumentCaptor emailBodyArgument = ArgumentCaptor.forClass(String.class); @@ -163,7 +170,7 @@ public void testBeginEmailChangeInOtherZone() throws Exception { String emailBody = emailBodyArgument.getValue(); assertThat(emailBody, containsString(String.format("A request has been made to change the email for %s from %s to %s", "The Twiglet Zone", "user@example.com", "new@example.com"))); - assertThat(emailBody, containsString("Verify your email")); + assertThat(emailBody, containsString("Verify your email")); assertThat(emailBody, containsString("Thank you,
\n The Twiglet Zone")); } diff --git a/login/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerTest.java b/login/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerTest.java index f5d13ed66a0..5aff9f56f68 100644 --- a/login/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerTest.java +++ b/login/src/test/java/org/cloudfoundry/identity/uaa/login/ResetPasswordControllerTest.java @@ -34,11 +34,13 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.servlet.view.InternalResourceViewResolver; import org.thymeleaf.spring4.SpringTemplateEngine; @@ -84,15 +86,15 @@ public void setUp() throws Exception { resetPasswordService = mock(ResetPasswordService.class); messageService = mock(MessageService.class); codeStore = mock(ExpiringCodeStore.class); - ResetPasswordController controller = new ResetPasswordController(resetPasswordService, messageService, templateEngine, new UaaUrlUtils("http://foo/uaa"), "pivotal", codeStore); + ResetPasswordController controller = new ResetPasswordController(resetPasswordService, messageService, templateEngine, new UaaUrlUtils(), "pivotal", codeStore); InternalResourceViewResolver viewResolver = new InternalResourceViewResolver(); viewResolver.setPrefix("/WEB-INF/jsp"); viewResolver.setSuffix(".jsp"); mockMvc = MockMvcBuilders - .standaloneSetup(controller) - .setViewResolvers(viewResolver) - .build(); + .standaloneSetup(controller) + .setViewResolvers(viewResolver) + .build(); } @After @@ -125,11 +127,20 @@ public void forgotPassword_ConflictInOtherZone_SendsEmailWithUnavailableEmailHtm } private void forgotPasswordWithConflict(String zoneDomain, String brand) throws Exception { - String subdomain = zoneDomain == null ? "" : zoneDomain + "."; + String domain = zoneDomain == null ? "localhost" : zoneDomain + ".localhost"; when(resetPasswordService.forgotPassword("user@example.com", "", "")).thenThrow(new ConflictException("abcd")); MockHttpServletRequestBuilder post = post("/forgot_password.do") .contentType(APPLICATION_FORM_URLENCODED) .param("email", "user@example.com"); + + post.with(new RequestPostProcessor() { + @Override + public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { + request.setServerName(domain); + return request; + } + }); + mockMvc.perform(post) .andExpect(status().isFound()) .andExpect(redirectedUrl("email_sent?code=reset_password")); @@ -144,7 +155,7 @@ private void forgotPasswordWithConflict(String zoneDomain, String brand) throws String emailContent = captor.getValue(); assertThat(emailContent, containsString(String.format("A request has been made to reset your %s account password for %s", brand, "user@example.com"))); - assertThat(emailContent, containsString("Your account credentials for " + subdomain + "foo are managed by an external service. Please contact your administrator for password recovery requests.")); + assertThat(emailContent, containsString("Your account credentials for " + domain + " are managed by an external service. Please contact your administrator for password recovery requests.")); assertThat(emailContent, containsString("Thank you,
\n " + brand)); } @@ -163,22 +174,34 @@ public void forgotPassword_DoesNotSendEmail_UserNotFound() throws Exception { @Test public void forgotPassword_Successful() throws Exception { - forgotPasswordSuccessful("http://foo/uaa/reset_password?code=code1&email=user%40example.com", "Pivotal"); + forgotPasswordSuccessful("http://localhost/reset_password?code=code1&email=user%40example.com", "Pivotal", null); } @Test public void forgotPassword_SuccessfulInOtherZone() throws Exception { - IdentityZoneHolder.set(MultitenancyFixture.identityZone("test-zone-id", "testsubdomain")); - forgotPasswordSuccessful("http://testsubdomain.foo/uaa/reset_password?code=code1&email=user%40example.com", "The Twiglet Zone"); + IdentityZone zone = MultitenancyFixture.identityZone("test-zone-id", "testsubdomain"); + IdentityZoneHolder.set(zone); + forgotPasswordSuccessful("http://testsubdomain.localhost/reset_password?code=code1&email=user%40example.com", "The Twiglet Zone", zone); } - private void forgotPasswordSuccessful(String url, String brand) throws Exception { + private void forgotPasswordSuccessful(String url, String brand, IdentityZone zone) throws Exception { when(resetPasswordService.forgotPassword("user@example.com", "example", "redirect.example.com")).thenReturn(new ForgotPasswordInfo("123", new ExpiringCode("code1", new Timestamp(System.currentTimeMillis()), "someData"))); MockHttpServletRequestBuilder post = post("/forgot_password.do") .contentType(APPLICATION_FORM_URLENCODED) .param("email", "user@example.com") .param("client_id", "example") .param("redirect_uri", "redirect.example.com"); + + if (zone != null) { + post.with(new RequestPostProcessor() { + @Override + public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { + request.setServerName(zone.getSubdomain() + ".localhost"); + return request; + } + }); + } + mockMvc.perform(post) .andExpect(status().isFound()) .andExpect(redirectedUrl("email_sent?code=reset_password")); @@ -206,8 +229,8 @@ public void testForgotPasswordFormValidationFailure() throws Exception { @Test public void testInstructions() throws Exception { mockMvc.perform(get("/email_sent").param("code", "reset_password")) - .andExpect(status().isOk()) - .andExpect(model().attribute("code", "reset_password")); + .andExpect(status().isOk()) + .andExpect(model().attribute("code", "reset_password")); } @Test @@ -216,46 +239,46 @@ public void testResetPasswordPage() throws Exception { when(codeStore.generateCode(anyString(), any(Timestamp.class))).thenReturn(code); when(codeStore.retrieveCode(anyString())).thenReturn(code); mockMvc.perform(get("/reset_password").param("email", "user@example.com").param("code", "secret_code")) - .andExpect(status().isOk()) - .andExpect(view().name("reset_password")); + .andExpect(status().isOk()) + .andExpect(view().name("reset_password")); } @Test public void testResetPasswordSuccess() throws Exception { - ScimUser user = new ScimUser("user-id","foo@example.com","firstName","lastName"); + ScimUser user = new ScimUser("user-id", "foo@example.com", "firstName", "lastName"); user.setMeta(new ScimMeta(new Date(System.currentTimeMillis() - (1000 * 60 * 60 * 24)), new Date(System.currentTimeMillis() - (1000 * 60 * 60 * 24)), 0)); user.setPrimaryEmail("foo@example.com"); when(resetPasswordService.resetPassword("secret_code", "password")).thenReturn(new ResetPasswordResponse(user, "redirect.example.com")); MockHttpServletRequestBuilder post = post("/reset_password.do") - .contentType(APPLICATION_FORM_URLENCODED) - .param("code", "secret_code") - .param("email", "foo@example.com") - .param("password", "password") - .param("password_confirmation", "password"); + .contentType(APPLICATION_FORM_URLENCODED) + .param("code", "secret_code") + .param("email", "foo@example.com") + .param("password", "password") + .param("password_confirmation", "password"); mockMvc.perform(post) - .andExpect(status().isFound()) - .andExpect(redirectedUrl("redirect.example.com")) - .andExpect(model().attributeDoesNotExist("code")) - .andExpect(model().attributeDoesNotExist("password")) - .andExpect(model().attributeDoesNotExist("password_confirmation")); + .andExpect(status().isFound()) + .andExpect(redirectedUrl("redirect.example.com")) + .andExpect(model().attributeDoesNotExist("code")) + .andExpect(model().attributeDoesNotExist("password")) + .andExpect(model().attributeDoesNotExist("password_confirmation")); } @Test public void testResetPasswordFormValidationFailure() throws Exception { MockHttpServletRequestBuilder post = post("/reset_password.do") - .contentType(APPLICATION_FORM_URLENCODED) - .param("code", "123456") - .param("email", "foo@example.com") - .param("password", "pass") - .param("password_confirmation", "word"); + .contentType(APPLICATION_FORM_URLENCODED) + .param("code", "123456") + .param("email", "foo@example.com") + .param("password", "pass") + .param("password_confirmation", "word"); mockMvc.perform(post) - .andExpect(status().isUnprocessableEntity()) - .andExpect(view().name("reset_password")) - .andExpect(model().attribute("message_code", "form_error")) - .andExpect(model().attribute("email", "foo@example.com")) - .andExpect(model().attribute("code", "123456")); + .andExpect(status().isUnprocessableEntity()) + .andExpect(view().name("reset_password")) + .andExpect(model().attribute("message_code", "form_error")) + .andExpect(model().attribute("email", "foo@example.com")) + .andExpect(model().attribute("code", "123456")); verifyZeroInteractions(resetPasswordService); } @@ -265,16 +288,16 @@ public void testResetPasswordFormWithInvalidCode() throws Exception { when(resetPasswordService.resetPassword("bad_code", "password")).thenThrow(new UaaException("Bad code!")); MockHttpServletRequestBuilder post = post("/reset_password.do") - .contentType(APPLICATION_FORM_URLENCODED) - .param("code", "bad_code") - .param("email", "foo@example.com") - .param("password", "password") - .param("password_confirmation", "password"); + .contentType(APPLICATION_FORM_URLENCODED) + .param("code", "bad_code") + .param("email", "foo@example.com") + .param("password", "password") + .param("password_confirmation", "password"); mockMvc.perform(post) - .andExpect(status().isUnprocessableEntity()) - .andExpect(view().name("forgot_password")) - .andExpect(model().attribute("message_code", "bad_code")); + .andExpect(status().isUnprocessableEntity()) + .andExpect(view().name("forgot_password")) + .andExpect(model().attribute("message_code", "bad_code")); verify(resetPasswordService).resetPassword("bad_code", "password"); } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/AccountsControllerMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/AccountsControllerMockMvcTests.java index 698af59f1f5..829e9c64a1c 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/AccountsControllerMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/AccountsControllerMockMvcTests.java @@ -39,7 +39,9 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.not; +import static org.hamcrest.core.StringStartsWith.startsWith; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -242,10 +244,10 @@ public void testCreatingAnAccount() throws Exception { SecurityContext securityContext = (SecurityContext) mvcResult.getRequest().getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); Authentication authentication = securityContext.getAuthentication(); - Assert.assertThat(authentication.getPrincipal(), instanceOf(UaaPrincipal.class)); + assertThat(authentication.getPrincipal(), instanceOf(UaaPrincipal.class)); UaaPrincipal principal = (UaaPrincipal) authentication.getPrincipal(); - Assert.assertThat(principal.getEmail(), equalTo(userEmail)); - Assert.assertThat(principal.getOrigin(), equalTo(Origin.UAA)); + assertThat(principal.getEmail(), equalTo(userEmail)); + assertThat(principal.getOrigin(), equalTo(Origin.UAA)); } @Test @@ -270,10 +272,10 @@ public void testCreatingAnAccountWithAnEmptyClientId() throws Exception { SecurityContext securityContext = (SecurityContext) mvcResult.getRequest().getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); Authentication authentication = securityContext.getAuthentication(); - Assert.assertThat(authentication.getPrincipal(), instanceOf(UaaPrincipal.class)); + assertThat(authentication.getPrincipal(), instanceOf(UaaPrincipal.class)); UaaPrincipal principal = (UaaPrincipal) authentication.getPrincipal(); - Assert.assertThat(principal.getEmail(), equalTo(userEmail)); - Assert.assertThat(principal.getOrigin(), equalTo(Origin.UAA)); + assertThat(principal.getEmail(), equalTo(userEmail)); + assertThat(principal.getOrigin(), equalTo(Origin.UAA)); } @Test @@ -312,10 +314,30 @@ public void testCreatingAnAccountWithNoClientRedirect() throws Exception { SecurityContext securityContext = (SecurityContext) mvcResult.getRequest().getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); Authentication authentication = securityContext.getAuthentication(); - Assert.assertThat(authentication.getPrincipal(), instanceOf(UaaPrincipal.class)); + assertThat(authentication.getPrincipal(), instanceOf(UaaPrincipal.class)); UaaPrincipal principal = (UaaPrincipal) authentication.getPrincipal(); - Assert.assertThat(principal.getEmail(), equalTo(userEmail)); - Assert.assertThat(principal.getOrigin(), equalTo(Origin.UAA)); + assertThat(principal.getEmail(), equalTo(userEmail)); + assertThat(principal.getOrigin(), equalTo(Origin.UAA)); + } + + @Test + public void testCreatingAnAccountWithCustomUrl() throws Exception { + PredictableGenerator generator = new PredictableGenerator(); + JdbcExpiringCodeStore store = getWebApplicationContext().getBean(JdbcExpiringCodeStore.class); + store.setGenerator(generator); + + getMockMvc().perform(post("/create_account.do") + .with(new SetServerNameRequestPostProcessor("login.localhost")) + .param("email", userEmail) + .param("password", "secr3T") + .param("password_confirmation", "secr3T")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("accounts/email_sent")); + + Iterator receivedEmail = mailServer.getReceivedEmail(); + SmtpMessage message = (SmtpMessage) receivedEmail.next(); + String link = mockMvcTestClient.extractLink(message.getBody()); + assertThat(link, startsWith("http://login.localhost")); } @Test @@ -364,10 +386,10 @@ public void testCreatingAnAccountInAnotherZoneWithNoClientRedirect() throws Exce SecurityContext securityContext = (SecurityContext) mvcResult.getRequest().getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); Authentication authentication = securityContext.getAuthentication(); - Assert.assertThat(authentication.getPrincipal(), instanceOf(UaaPrincipal.class)); + assertThat(authentication.getPrincipal(), instanceOf(UaaPrincipal.class)); UaaPrincipal principal = (UaaPrincipal) authentication.getPrincipal(); - Assert.assertThat(principal.getEmail(), equalTo(userEmail)); - Assert.assertThat(principal.getOrigin(), equalTo(Origin.UAA)); + assertThat(principal.getEmail(), equalTo(userEmail)); + assertThat(principal.getOrigin(), equalTo(Origin.UAA)); } @Test @@ -417,10 +439,10 @@ public void testCreatingAnAccountInAnotherZoneWithClientRedirect() throws Except SecurityContext securityContext = (SecurityContext) mvcResult.getRequest().getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); Authentication authentication = securityContext.getAuthentication(); - Assert.assertThat(authentication.getPrincipal(), instanceOf(UaaPrincipal.class)); + assertThat(authentication.getPrincipal(), instanceOf(UaaPrincipal.class)); UaaPrincipal principal = (UaaPrincipal) authentication.getPrincipal(); - Assert.assertThat(principal.getEmail(), equalTo(userEmail)); - Assert.assertThat(principal.getOrigin(), equalTo(Origin.UAA)); + assertThat(principal.getEmail(), equalTo(userEmail)); + assertThat(principal.getOrigin(), equalTo(Origin.UAA)); } public static class PredictableGenerator extends RandomValueStringGenerator { @@ -475,9 +497,9 @@ private void createAccount(String expectedRedirectUri, String redirectUri) throw SecurityContext securityContext = (SecurityContext) mvcResult.getRequest().getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); Authentication authentication = securityContext.getAuthentication(); - Assert.assertThat(authentication.getPrincipal(), instanceOf(UaaPrincipal.class)); + assertThat(authentication.getPrincipal(), instanceOf(UaaPrincipal.class)); UaaPrincipal principal = (UaaPrincipal) authentication.getPrincipal(); - Assert.assertThat(principal.getEmail(), equalTo(userEmail)); - Assert.assertThat(principal.getOrigin(), equalTo(Origin.UAA)); + assertThat(principal.getEmail(), equalTo(userEmail)); + assertThat(principal.getOrigin(), equalTo(Origin.UAA)); } } From 3153e0632e8e4cf365f082492e3ecd0ec2fd1ba2 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Mon, 12 Oct 2015 17:48:45 -0700 Subject: [PATCH 060/103] Remove degenerate test Signed-off-by: Jeremy Coffield --- .../login/AccountsControllerMockMvcTests.java | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/AccountsControllerMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/AccountsControllerMockMvcTests.java index 829e9c64a1c..9f3e5db7255 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/AccountsControllerMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/AccountsControllerMockMvcTests.java @@ -320,26 +320,6 @@ public void testCreatingAnAccountWithNoClientRedirect() throws Exception { assertThat(principal.getOrigin(), equalTo(Origin.UAA)); } - @Test - public void testCreatingAnAccountWithCustomUrl() throws Exception { - PredictableGenerator generator = new PredictableGenerator(); - JdbcExpiringCodeStore store = getWebApplicationContext().getBean(JdbcExpiringCodeStore.class); - store.setGenerator(generator); - - getMockMvc().perform(post("/create_account.do") - .with(new SetServerNameRequestPostProcessor("login.localhost")) - .param("email", userEmail) - .param("password", "secr3T") - .param("password_confirmation", "secr3T")) - .andExpect(status().isFound()) - .andExpect(redirectedUrl("accounts/email_sent")); - - Iterator receivedEmail = mailServer.getReceivedEmail(); - SmtpMessage message = (SmtpMessage) receivedEmail.next(); - String link = mockMvcTestClient.extractLink(message.getBody()); - assertThat(link, startsWith("http://login.localhost")); - } - @Test public void testCreatingAnAccountInAnotherZoneWithNoClientRedirect() throws Exception { String subdomain = "mysubdomain2"; From a236e92e53c894e7822f9b32a04a77b32eca4b6e Mon Sep 17 00:00:00 2001 From: Jeremy Coffield Date: Tue, 13 Oct 2015 11:12:32 -0700 Subject: [PATCH 061/103] Display copyright organization depending on env brand. [#105267720] --- .../resources/templates/web/layouts/main.html | 12 +++++++----- .../identity/uaa/login/LoginMockMvcTests.java | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/login/src/main/resources/templates/web/layouts/main.html b/login/src/main/resources/templates/web/layouts/main.html index 481e09222d5..14be36df1a3 100644 --- a/login/src/main/resources/templates/web/layouts/main.html +++ b/login/src/main/resources/templates/web/layouts/main.html @@ -3,13 +3,15 @@ th:with="assetBaseUrl=${@environment.getProperty('assetBaseUrl','/resources/oss')}, pivotal=${@environment.getProperty('login.brand') == 'pivotal'}, isUaa=${T(org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder).isUaa()}, - zoneName=${T(org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder).get().getName()}"> + zoneName=${T(org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder).get().getName()}, + companyName=(${pivotal} ? 'Pivotal Software' : 'CloudFoundry.org Foundation'), + copyright=('Copyright © ' + ${companyName} + ', Inc. ' + ${#dates.year(#dates.createNow())}) + '. All Rights Reserved.'"> [[${pivotal and isUaa ? 'Pivotal' : (isUaa ? 'Cloud Foundry' : zoneName)}]] - + @@ -25,9 +27,9 @@

diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java index ad57e2c05d2..594f71b61d7 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java @@ -150,6 +150,22 @@ public void testLogin() throws Exception { .andExpect(model().attributeExists("prompts")); } + @Test + public void testCopyrightPivotal() throws Exception { + mockEnvironment.setProperty("login.brand", "pivotal"); + + getMockMvc().perform(get("/login")) + .andExpect(content().string(containsString("Copyright © Pivotal Software, Inc."))); + } + + @Test + public void testCopyrightCloudFoundry() throws Exception { + mockEnvironment.setProperty("login.brand", "cloudfoundry"); + + getMockMvc().perform(get("/login")) + .andExpect(content().string(containsString("Copyright © CloudFoundry.org Foundation, Inc."))); + } + @Test public void testForgotPasswordPageDoesNotHaveCsrf() throws Exception { getMockMvc().perform(get("/forgot_password")) From a501a40b40aed43cc3f4c29a6965e8cb526e68b5 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Tue, 13 Oct 2015 11:12:59 -0700 Subject: [PATCH 062/103] Populate id token with user information if scope 'profile' is requested [#99446736] https://www.pivotaltracker.com/story/show/99446736 Signed-off-by: Madhura Bhave --- .../identity/uaa/oauth/Claims.java | 2 + .../uaa/oauth/token/UaaTokenServices.java | 67 ++++++++++--------- .../oauth/token/UaaTokenServicesTests.java | 53 ++++++++++----- uaa/src/main/resources/uaa.yml | 2 + .../ScimGroupEndpointsIntegrationTests.java | 10 +-- .../ScimUserEndpointsIntegrationTests.java | 2 +- .../uaa/mock/ldap/LdapMockMvcTests.java | 2 + .../CheckDefaultAuthoritiesMvcMockTests.java | 6 +- .../uaa/mock/token/TokenMvcMockTests.java | 4 +- 9 files changed, 90 insertions(+), 58 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/Claims.java b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/Claims.java index c97bad8ee11..f499f7bd9a2 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/Claims.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/Claims.java @@ -28,6 +28,7 @@ public class Claims { public static final String NAME = "name"; public static final String GIVEN_NAME = "given_name"; public static final String FAMILY_NAME = "family_name"; + public static final String PHONE_NUMBER = "phone_number"; public static final String EMAIL = "email"; public static final String CLIENT_ID = "client_id"; public static final String EXP = "exp"; @@ -48,4 +49,5 @@ public class Claims { public static final String NONCE = "nonce"; public static final String ORIGIN = "origin"; public static final String ROLES = "roles"; + public static final String PROFILE = "profile"; } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServices.java b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServices.java index bb0129cf3a0..21f0e3bdde6 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServices.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServices.java @@ -92,12 +92,15 @@ import static org.cloudfoundry.identity.uaa.oauth.Claims.CLIENT_ID; import static org.cloudfoundry.identity.uaa.oauth.Claims.EMAIL; import static org.cloudfoundry.identity.uaa.oauth.Claims.EXP; +import static org.cloudfoundry.identity.uaa.oauth.Claims.FAMILY_NAME; +import static org.cloudfoundry.identity.uaa.oauth.Claims.GIVEN_NAME; import static org.cloudfoundry.identity.uaa.oauth.Claims.GRANT_TYPE; import static org.cloudfoundry.identity.uaa.oauth.Claims.IAT; import static org.cloudfoundry.identity.uaa.oauth.Claims.ISS; import static org.cloudfoundry.identity.uaa.oauth.Claims.JTI; import static org.cloudfoundry.identity.uaa.oauth.Claims.NONCE; import static org.cloudfoundry.identity.uaa.oauth.Claims.ORIGIN; +import static org.cloudfoundry.identity.uaa.oauth.Claims.PHONE_NUMBER; import static org.cloudfoundry.identity.uaa.oauth.Claims.REVOCATION_SIGNATURE; import static org.cloudfoundry.identity.uaa.oauth.Claims.ROLES; import static org.cloudfoundry.identity.uaa.oauth.Claims.SCOPE; @@ -105,6 +108,7 @@ import static org.cloudfoundry.identity.uaa.oauth.Claims.USER_ID; import static org.cloudfoundry.identity.uaa.oauth.Claims.USER_NAME; import static org.cloudfoundry.identity.uaa.oauth.Claims.ZONE_ID; +import static org.cloudfoundry.identity.uaa.oauth.Claims.PROFILE; /** @@ -243,9 +247,7 @@ public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenReque OAuth2AccessToken accessToken = createAccessToken( user.getId(), - user.getOrigin(), - user.getUsername(), - user.getEmail(), + user, claims.get(AUTH_TIME) != null ? new Date(((Long)claims.get(AUTH_TIME)) * 1000l) : null, validity != null ? validity.intValue() : accessTokenValiditySeconds, null, @@ -306,9 +308,7 @@ private void checkForApproval(String userid, private OAuth2AccessToken createAccessToken(String userId, - String origin, - String username, - String userEmail, + UaaUser user, Date userAuthenticationTime, int validitySeconds, Collection clientScopes, @@ -351,9 +351,7 @@ private OAuth2AccessToken createAccessToken(String userId, Map jwtAccessToken = createJWTAccessToken( accessToken, userId, - origin, - username, - userEmail, + user, userAuthenticationTime, clientScopes, requestedScopes, @@ -371,7 +369,7 @@ private OAuth2AccessToken createAccessToken(String userId, String token = JwtHelper.encode(content, signerProvider.getSigner()).getEncoded(); // This setter copies the value and returns. Don't change. accessToken.setValue(token); - populateIdToken(accessToken, jwtAccessToken, requestedScopes, responseTypes, clientId, forceIdTokenCreation, externalGroupsForIdToken); + populateIdToken(accessToken, jwtAccessToken, requestedScopes, responseTypes, clientId, forceIdTokenCreation, externalGroupsForIdToken, user); publish(new TokenIssuedEvent(accessToken, SecurityContextHolder.getContext().getAuthentication())); return accessToken; @@ -383,7 +381,8 @@ private void populateIdToken(OpenIdToken token, Set responseTypes, String aud, boolean forceIdTokenCreation, - Set externalGroupsForIdToken) { + Set externalGroupsForIdToken, + UaaUser user) { if (forceIdTokenCreation || (scopes.contains("openid") && responseTypes.contains(OpenIdToken.ID_TOKEN))) { try { Map clone = new HashMap<>(accessTokenValues); @@ -397,10 +396,21 @@ private void populateIdToken(OpenIdToken token, clone.put(SCOPE, idTokenScopes); clone.put(AUD, new HashSet(Arrays.asList(aud))); - if (scopes.contains(ROLES) && !externalGroupsForIdToken.isEmpty()) { + if (scopes.contains(ROLES) && (externalGroupsForIdToken != null && !externalGroupsForIdToken.isEmpty())) { clone.put(ROLES, externalGroupsForIdToken); } + if(scopes.contains(PROFILE) && user != null) { + String givenName = user.getGivenName(); + if(givenName != null) clone.put(GIVEN_NAME, givenName); + + String familyName = user.getFamilyName(); + if(familyName != null) clone.put(FAMILY_NAME, familyName); + + String phoneNumber = user.getPhoneNumber(); + if(phoneNumber != null) clone.put(PHONE_NUMBER, phoneNumber); + } + String content = JsonUtils.writeValueAsString(clone); String encoded = JwtHelper.encode(content, signerProvider.getSigner()).getEncoded(); token.setIdTokenValue(encoded); @@ -412,9 +422,7 @@ private void populateIdToken(OpenIdToken token, private Map createJWTAccessToken(OAuth2AccessToken token, String userId, - String origin, - String username, - String userEmail, + UaaUser user, Date userAuthenticationTime, Collection clientScopes, Set requestedScopes, @@ -444,10 +452,17 @@ private void populateIdToken(OpenIdToken token, } if (!"client_credentials".equals(grantType)) { response.put(USER_ID, userId); - response.put(ORIGIN, origin); - response.put(USER_NAME, username == null ? userId : username); - if (null != userEmail) { - response.put(EMAIL, userEmail); + if (user != null) { + String origin = user.getOrigin(); + if (StringUtils.hasLength(origin)) { + response.put(ORIGIN, origin); + } + String username = user.getUsername(); + response.put(USER_NAME, username == null ? userId : username); + String userEmail = user.getEmail(); + if (userEmail != null) { + response.put(EMAIL, userEmail); + } } if (userAuthenticationTime!=null) { response.put(AUTH_TIME, userAuthenticationTime.getTime() / 1000); @@ -478,11 +493,7 @@ private void populateIdToken(OpenIdToken token, @Override public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException { - - String origin = null; - String userId = null; - String username = null; - String userEmail = null; + String userId; Date userAuthenticationTime = null; UaaUser user = null; boolean wasIdTokenRequestedThroughAuthCodeScopeParameter = false; @@ -495,15 +506,11 @@ public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) } else { userId = getUserId(authentication); user = userDatabase.retrieveUserById(userId); - origin = user.getOrigin(); - username = user.getUsername(); - userEmail = user.getEmail(); if (authentication.getUserAuthentication() instanceof UaaAuthentication) { userAuthenticationTime = new Date(((UaaAuthentication)authentication.getUserAuthentication()).getAuthenticatedTime()); } } - ClientDetails client = clientDetailsService.loadClientByClientId(authentication.getOAuth2Request().getClientId()); String revocableHashSignature = getRevocableTokenSignature(client, user); @@ -546,9 +553,7 @@ public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) OAuth2AccessToken accessToken = createAccessToken( userId, - origin, - username, - userEmail, + user, userAuthenticationTime, validity != null ? validity.intValue() : accessTokenValiditySeconds, clientScopes, diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServicesTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServicesTests.java index 2f91ea4a5e9..3195a2d2002 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServicesTests.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServicesTests.java @@ -33,6 +33,7 @@ import org.cloudfoundry.identity.uaa.user.InMemoryUaaUserDatabase; import org.cloudfoundry.identity.uaa.user.UaaAuthority; import org.cloudfoundry.identity.uaa.user.UaaUser; +import org.cloudfoundry.identity.uaa.user.UaaUserPrototype; import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; @@ -73,10 +74,13 @@ import java.util.Set; import static org.cloudfoundry.identity.uaa.user.UaaAuthority.USER_AUTHORITIES; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; @@ -106,6 +110,7 @@ public class UaaTokenServicesTests { public static final String SCIM = "scim"; public static final String OPENID = "openid"; public static final String ROLES = "roles"; + public static final String PROFILE = "profile"; private TestApplicationEventPublisher publisher; private UaaTokenServices tokenServices = new UaaTokenServices(); @@ -123,21 +128,23 @@ public class UaaTokenServicesTests { private final String externalId = "externalId"; private UaaUser defaultUser = new UaaUser( - userId, - username, - PASSWORD, - email, - defaultUserAuthorities , - null, - null, - new Date(System.currentTimeMillis() - 15000), - new Date(System.currentTimeMillis() - 15000), - Origin.UAA, - externalId, - false, - IdentityZoneHolder.get().getId(), - userId, - new Date(System.currentTimeMillis() - 15000)); + new UaaUserPrototype() + .withId(userId) + .withUsername(username) + .withPassword(PASSWORD) + .withEmail(email) + .withAuthorities(defaultUserAuthorities) + .withGivenName("Marissa") + .withFamilyName("Bloggs") + .withPhoneNumber("1234567890") + .withCreated(new Date(System.currentTimeMillis() - 15000)) + .withModified(new Date(System.currentTimeMillis() - 15000)) + .withOrigin(Origin.UAA) + .withExternalId(externalId) + .withVerified(false) + .withZoneId(IdentityZoneHolder.get().getId()) + .withSalt(userId) + .withPasswordLastModified(new Date(System.currentTimeMillis() - 15000))); // Need to create a user with a modified time slightly in the past because // the token IAT is in seconds and the token @@ -679,6 +686,22 @@ public void create_id_token_without_roles_scope() { assertFalse(idTokenJwt.getClaims().contains("\"roles\"")); } + @Test + public void create_id_token_with_profile_scope() throws Exception { + Jwt idTokenJwt = getIdToken(Arrays.asList(OPENID, PROFILE)); + assertTrue(idTokenJwt.getClaims().contains("\"given_name\":\"" + defaultUser.getGivenName() + "\"")); + assertTrue(idTokenJwt.getClaims().contains("\"family_name\":\"" + defaultUser.getFamilyName() + "\"")); + assertTrue(idTokenJwt.getClaims().contains("\"phone_number\":\"" + defaultUser.getPhoneNumber() + "\"")); + } + + @Test + public void create_id_token_without_profile_scope() throws Exception { + Jwt idTokenJwt = getIdToken(Arrays.asList(OPENID)); + assertFalse(idTokenJwt.getClaims().contains("\"given_name\":")); + assertFalse(idTokenJwt.getClaims().contains("\"family_name\":")); + assertFalse(idTokenJwt.getClaims().contains("\"phone_number\":")); + } + private Jwt getIdToken(List scopes) { AuthorizationRequest authorizationRequest = new AuthorizationRequest(CLIENT_ID, scopes); diff --git a/uaa/src/main/resources/uaa.yml b/uaa/src/main/resources/uaa.yml index fc0732318bb..c23b4e9c2df 100755 --- a/uaa/src/main/resources/uaa.yml +++ b/uaa/src/main/resources/uaa.yml @@ -141,6 +141,8 @@ oauth: - uaa.user - approvals.me - oauth.approvals + - profile + - roles # Allow unverified users to log in. Defaults to true #allowUnverifiedUsers: false diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/ScimGroupEndpointsIntegrationTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/ScimGroupEndpointsIntegrationTests.java index 87601bf762f..46e5c113f79 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/ScimGroupEndpointsIntegrationTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/ScimGroupEndpointsIntegrationTests.java @@ -84,7 +84,7 @@ public class ScimGroupEndpointsIntegrationTests { private static final List defaultGroups = Arrays.asList("openid", "scim.me", "cloud_controller.read", "cloud_controller.write", "password.write", "scim.userids", "uaa.user", "approvals.me", - "oauth.approvals", "cloud_controller_service_permissions.read"); + "oauth.approvals", "cloud_controller_service_permissions.read", "profile", "roles"); @Rule @@ -180,13 +180,7 @@ private ScimGroup updateGroup(String id, String name, ScimGroupMember... members private void validateUserGroups(String id, String... groups) { List groupNames = groups != null ? Arrays.asList(groups) : Collections. emptyList(); - assertEquals(groupNames.size() + defaultGroups.size(), getUser(id).getGroups().size()); // there - // are - // 8 - // default - // user - // groups - // configured + assertEquals(groupNames.size() + defaultGroups.size(), getUser(id).getGroups().size()); for (ScimUser.Group g : getUser(id).getGroups()) { assertTrue(defaultGroups.contains(g.getDisplay()) || groupNames.contains(g.getDisplay())); } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/ScimUserEndpointsIntegrationTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/ScimUserEndpointsIntegrationTests.java index 66a9fd80d70..6422494e84a 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/ScimUserEndpointsIntegrationTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/ScimUserEndpointsIntegrationTests.java @@ -63,7 +63,7 @@ public class ScimUserEndpointsIntegrationTests { private final String usersEndpoint = "/Users"; - private static final int NUM_DEFAULT_GROUPS_ON_STARTUP = 10; + private static final int NUM_DEFAULT_GROUPS_ON_STARTUP = 12; @Rule public ServerRunning serverRunning = ServerRunning.isRunning(); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java index 4ad2fe7b95e..6f832914e88 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java @@ -845,6 +845,8 @@ public void testLdapScopesFromChainedAuth() throws Exception { "scim.me", "cloud_controller_service_permissions.read", "openid", + "profile", + "roles", "oauth.approvals", "uaa.user", "cloud_controller.read" diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/CheckDefaultAuthoritiesMvcMockTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/CheckDefaultAuthoritiesMvcMockTests.java index dc7e345f9b1..815feccc8ff 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/CheckDefaultAuthoritiesMvcMockTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/CheckDefaultAuthoritiesMvcMockTests.java @@ -34,7 +34,7 @@ public void setUp() throws Exception { @Test public void testDefaultAuthorities() throws Exception { - Assert.assertEquals(10, defaultAuthorities.size()); + Assert.assertEquals(12, defaultAuthorities.size()); String[] expected = new String[] { "openid", "scim.me", @@ -45,7 +45,9 @@ public void testDefaultAuthorities() throws Exception { "scim.userids", "uaa.user", "approvals.me", - "oauth.approvals" + "oauth.approvals", + "profile", + "roles" }; for (String s : expected) { Assert.assertTrue("Expecting authority to be present:"+s,defaultAuthorities.contains(s)); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java index 32d683ff93f..c36da445d07 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java @@ -1251,7 +1251,9 @@ public void testWildcardPasswordGrant() throws Exception { "scope.two", "scope.three")); - set1.remove("openid");//not matched here + set1.remove("openid"); + set1.remove("profile"); + set1.remove("roles"); validatePasswordGrantToken( clientId, userId, From d2ef86449f26f1d173e63c82698f5ec7c0a32236 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Thu, 15 Oct 2015 09:44:11 -0700 Subject: [PATCH 063/103] Add verification link API [#105489788] https://www.pivotaltracker.com/story/show/105489788 Signed-off-by: Jonathan Lo --- docs/UAA-APIs.rst | 40 ++- .../login/EmailAccountCreationService.java | 18 +- .../EmailAccountCreationServiceTests.java | 6 +- .../identity/uaa/scim/ScimUser.java | 3 + .../uaa/scim/endpoints/ScimUserEndpoints.java | 44 ++- .../scim/endpoints/VerificationResponse.java | 29 ++ .../UserAlreadyVerifiedException.java | 24 ++ .../identity/uaa/scim/util/ScimUtils.java | 104 +++++++ .../webapp/WEB-INF/spring/scim-endpoints.xml | 2 + .../ScimUserEndpointsMockMvcTests.java | 256 +++++++++++++++++- 10 files changed, 499 insertions(+), 27 deletions(-) create mode 100644 scim/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/VerificationResponse.java create mode 100644 scim/src/main/java/org/cloudfoundry/identity/uaa/scim/exception/UserAlreadyVerifiedException.java create mode 100644 scim/src/main/java/org/cloudfoundry/identity/uaa/scim/util/ScimUtils.java diff --git a/docs/UAA-APIs.rst b/docs/UAA-APIs.rst index 156e62f153b..146a9d4540f 100644 --- a/docs/UAA-APIs.rst +++ b/docs/UAA-APIs.rst @@ -37,7 +37,7 @@ Here is a summary of the different scopes that are known to the UAA. * **clients.secret** - ``/oauth/clients/*/secret`` endpoint. Scope required to change the password of a client. Considered an admin scope. * **scim.write** - Admin write access to all SCIM endpoints, ``/Users``, ``/Groups/``. * **scim.read** - Admin read access to all SCIM endpoints, ``/Users``, ``/Groups/``. -* **scim.create** - Reduced scope to be able to create a user using ``POST /Users`` (and verify their account using ``GET /Users/{id}/verify``) but not be able to modify, read or delete users. +* **scim.create** - Reduced scope to be able to create a user using ``POST /Users`` (get verification links ``GET /Users/{id}/verify-link`` or verify their account using ``GET /Users/{id}/verify``) but not be able to modify, read or delete users. * **scim.userids** - ``/ids/Users`` - Required to convert a username+origin to a user ID and vice versa. * **scim.zones** - Limited scope that only allows adding/removing a user to/from `zone management groups`_ under the path /Groups/zones * **scim.invite** - Scope required by a client in order to participate in invitations using the ``/invite_users`` endpoint. @@ -1542,6 +1542,42 @@ See `SCIM - Changing Password codeData = new HashMap<>(); - codeData.put("user_id", userId); - codeData.put("client_id", clientId); - codeData.put("redirect_uri", redirectUri); - String codeDataString = JsonUtils.writeValueAsString(codeData); - return new ExpiringCode(null, expiresAt, codeDataString); - } - @Override public AccountCreationResponse completeActivation(String code) throws IOException { @@ -192,7 +180,7 @@ private String getSubjectText() { } private String getEmailHtml(String code, String email) { - String accountsUrl = uaaUrlUtils.getUaaUrl("/verify_user"); + String accountsUrl = ScimUtils.getVerificationURL(null, null).toString(); final Context ctx = new Context(); if (IdentityZoneHolder.isUaa()) { diff --git a/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailAccountCreationServiceTests.java b/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailAccountCreationServiceTests.java index 7db358a0b56..430f25b2bf2 100644 --- a/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailAccountCreationServiceTests.java +++ b/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailAccountCreationServiceTests.java @@ -111,6 +111,7 @@ public void testBeginActivation() throws Exception { String data = setUpForSuccess(redirectUri); when(scimUserProvisioning.createUser(any(ScimUser.class), anyString())).thenReturn(user); when(codeStore.generateCode(eq(data), any(Timestamp.class))).thenReturn(code); + emailAccountCreationService.beginActivation("user@example.com", "password", "login", redirectUri); String emailBody = captorEmailBody("Activate your Pivotal ID"); @@ -355,8 +356,11 @@ private String setUpForSuccess(String userId, String redirectUri) throws Excepti Timestamp ts = new Timestamp(System.currentTimeMillis() + (60 * 60 * 1000)); // 1 hour Map data = new HashMap<>(); data.put("user_id", userId); + data.put("email", "user@example.com"); data.put("client_id", "login"); - data.put("redirect_uri", redirectUri); + if (redirectUri != null) { + data.put("redirect_uri", redirectUri); + } code = new ExpiringCode("the_secret_code", ts, JsonUtils.writeValueAsString(data)); diff --git a/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUser.java b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUser.java index 5b66caacdd4..23537b2e13f 100644 --- a/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUser.java +++ b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUser.java @@ -219,6 +219,7 @@ public String getValue() { } public void setValue(String value) { + Assert.notNull(value); this.value = value; } @@ -556,6 +557,8 @@ public String getPrimaryEmail() { } public void setPrimaryEmail(String value) { + Assert.notNull(value); + Email newPrimaryEmail = new Email(); newPrimaryEmail.setPrimary(true); newPrimaryEmail.setValue(value); diff --git a/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpoints.java b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpoints.java index e7a7708753b..9f3836cf5aa 100644 --- a/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpoints.java +++ b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpoints.java @@ -14,6 +14,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.cloudfoundry.identity.uaa.codestore.ExpiringCode; +import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; import org.cloudfoundry.identity.uaa.error.ConvertingExceptionView; import org.cloudfoundry.identity.uaa.error.ExceptionReport; import org.cloudfoundry.identity.uaa.oauth.approval.Approval; @@ -30,10 +32,11 @@ import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; import org.cloudfoundry.identity.uaa.scim.exception.ScimException; import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceConflictException; +import org.cloudfoundry.identity.uaa.scim.exception.UserAlreadyVerifiedException; +import org.cloudfoundry.identity.uaa.scim.util.ScimUtils; import org.cloudfoundry.identity.uaa.scim.validate.PasswordValidator; import org.cloudfoundry.identity.uaa.util.UaaPagingUtils; import org.cloudfoundry.identity.uaa.util.UaaStringUtils; -import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.springframework.beans.factory.InitializingBean; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.expression.spel.SpelEvaluationException; @@ -44,7 +47,10 @@ import org.springframework.jmx.export.annotation.ManagedMetric; import org.springframework.jmx.export.annotation.ManagedResource; import org.springframework.jmx.support.MetricType; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.codec.Hex; +import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.stereotype.Controller; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -118,6 +124,8 @@ public class ScimUserEndpoints implements InitializingBean { private PasswordValidator passwordValidator; + private ExpiringCodeStore codeStore; + /** * Set the message body converters to use. *

@@ -227,6 +235,36 @@ public ScimUser deleteUser(@PathVariable String userId, return user; } + @RequestMapping(value = "/Users/{userId}/verify-link", method = RequestMethod.GET) + @ResponseBody + public ResponseEntity getUserVerificationLink(@PathVariable String userId, + @RequestParam(value="client_id", required = false) String clientId, + @RequestParam(value="redirect_uri") String redirectUri) { + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication instanceof OAuth2Authentication) { + OAuth2Authentication oAuth2Authentication = (OAuth2Authentication)authentication; + + if (clientId==null) { + clientId = oAuth2Authentication.getOAuth2Request().getClientId(); + } + } + + VerificationResponse responseBody = new VerificationResponse(); + + ScimUser user = dao.retrieve(userId); + if (user.isVerified()) { + throw new UserAlreadyVerifiedException(); + } + + codeStore.retrieveLatest(user.getPrimaryEmail(), clientId); + + ExpiringCode expiringCode = ScimUtils.getExpiringCode(codeStore, userId, user.getPrimaryEmail(), clientId, redirectUri); + responseBody.setVerifyLink(ScimUtils.getVerificationURL(expiringCode, user.getPrimaryEmail())); + + return new ResponseEntity<>(responseBody, HttpStatus.OK); + } + @RequestMapping(value = "/Users/{userId}/verify", method = RequestMethod.GET) @ResponseBody public ScimUser verifyUser(@PathVariable String userId, @@ -411,4 +449,8 @@ public void setScimUserResourceMonitor(ResourceMonitor scimUserResourc public void setPasswordValidator(PasswordValidator passwordValidator) { this.passwordValidator = passwordValidator; } + + public void setCodeStore(ExpiringCodeStore codeStore) { + this.codeStore = codeStore; + } } diff --git a/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/VerificationResponse.java b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/VerificationResponse.java new file mode 100644 index 00000000000..9bcd658c9cb --- /dev/null +++ b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/VerificationResponse.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + *

+ * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + *

+ * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +package org.cloudfoundry.identity.uaa.scim.endpoints; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.net.URL; +public class VerificationResponse { + @JsonProperty(value="verify_link") + private URL verifyLink; + + public URL getVerifyLink() { + return verifyLink; + } + + public void setVerifyLink(URL verifyLink) { + this.verifyLink = verifyLink; + } +} diff --git a/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/exception/UserAlreadyVerifiedException.java b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/exception/UserAlreadyVerifiedException.java new file mode 100644 index 00000000000..5aa1d1410f4 --- /dev/null +++ b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/exception/UserAlreadyVerifiedException.java @@ -0,0 +1,24 @@ +package org.cloudfoundry.identity.uaa.scim.exception; + +import org.springframework.http.HttpStatus; + +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + *

+ * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + *

+ * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +public class UserAlreadyVerifiedException extends ScimException { + + public static final String DESC = "This user has already been verified."; + + public UserAlreadyVerifiedException() { + super(DESC, HttpStatus.METHOD_NOT_ALLOWED); + } +} diff --git a/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/util/ScimUtils.java b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/util/ScimUtils.java new file mode 100644 index 00000000000..6a4e25638f1 --- /dev/null +++ b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/util/ScimUtils.java @@ -0,0 +1,104 @@ +package org.cloudfoundry.identity.uaa.scim.util; + +import org.cloudfoundry.identity.uaa.codestore.ExpiringCode; +import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; +import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.cloudfoundry.identity.uaa.util.UaaUrlUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.Assert; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.sql.Timestamp; +import java.util.HashMap; +import java.util.IllegalFormatCodePointException; +import java.util.Map; + +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + *

+ * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + *

+ * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +public final class ScimUtils { + + private static final Logger logger = LoggerFactory.getLogger(ScimUtils.class); + + private ScimUtils() {} + + /** + * Generates a 1 hour expiring code. This code is revokable using {@link ExpiringCodeStore#retrieveLatest(String, String)}. + * + * @param codeStore + * the code store to use, must not be null + * @param userId + * the user id that will be included in the code's data, must not be null + * @param email + * the email that will be included in the code's data, must not be null + * @param clientId + * client id that will be included in the code's data, must not be null + * @param redirectUri + * the redirect uri that will be included in the code's data, may be null + * @return + * the expiring code + */ + public static ExpiringCode getExpiringCode(ExpiringCodeStore codeStore, String userId, String email, String clientId, String redirectUri) { + Assert.notNull(codeStore); + Assert.notNull(userId); + Assert.notNull(email); + + Map codeData = new HashMap<>(); + codeData.put("user_id", userId); + codeData.put("email", email); + codeData.put("client_id", clientId); + if (redirectUri != null) { + codeData.put("redirect_uri", redirectUri); + } + String codeDataString = JsonUtils.writeValueAsString(codeData); + + Timestamp expiresAt = new Timestamp(System.currentTimeMillis() + (60 * 60 * 1000)); // 1 hour + return codeStore.generateCode(codeDataString, expiresAt); + } + + /** + * Returns a verification URL that may be sent to a user. + * + * @param expiringCode + * the expiring code to include on the URL, may be null + * @param email + * the email to include on the URL, may be null + * @return + * the verification URL + */ + public static URL getVerificationURL(ExpiringCode expiringCode, String email) { + String url = ""; + try { + url = UaaUrlUtils.getUaaUrl("/verify_user"); + + if (expiringCode != null) { + url += "?code=" + expiringCode.getCode(); + } + + if (email != null) { + if (expiringCode != null) { + url += "&email=" + email; + } else { + url += "?email=" + email; + } + } + + return new URL(url); + } catch (MalformedURLException mfue) { + logger.error(String.format("Unexpected error creating user verification URL from %s", url), mfue); + } + throw new IllegalStateException(); + } +} diff --git a/uaa/src/main/webapp/WEB-INF/spring/scim-endpoints.xml b/uaa/src/main/webapp/WEB-INF/spring/scim-endpoints.xml index e27f6cc419d..ad67a824842 100644 --- a/uaa/src/main/webapp/WEB-INF/spring/scim-endpoints.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/scim-endpoints.xml @@ -117,6 +117,7 @@ + @@ -200,6 +201,7 @@ + diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsMockMvcTests.java index f382830f1b5..b851c021183 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsMockMvcTests.java @@ -12,30 +12,58 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.scim.endpoints; +import com.fasterxml.jackson.core.type.TypeReference; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; +import org.cloudfoundry.identity.uaa.codestore.ExpiringCode; +import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; +import org.cloudfoundry.identity.uaa.invitations.InvitationConstants; import org.cloudfoundry.identity.uaa.mock.InjectedMockContextTest; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; +import org.cloudfoundry.identity.uaa.scim.exception.UserAlreadyVerifiedException; +import org.cloudfoundry.identity.uaa.scim.test.JsonObjectMatcherUtils; import org.cloudfoundry.identity.uaa.test.TestClient; import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.cloudfoundry.identity.uaa.util.SetServerNameRequestPostProcessor; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneSwitchingFilter; +import org.json.JSONObject; import org.junit.Before; import org.junit.Test; import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.request.RequestPostProcessor; +import java.nio.charset.Charset; +import java.util.Arrays; import java.util.Collections; +import java.util.List; +import java.util.Map; import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.utils; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNot.not; +import static org.junit.Assert.assertThat; import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.security.oauth2.common.util.OAuth2Utils.CLIENT_ID; +import static org.springframework.security.oauth2.common.util.OAuth2Utils.REDIRECT_URI; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -43,11 +71,15 @@ public class ScimUserEndpointsMockMvcTests extends InjectedMockContextTest { + public static final String HTTP_REDIRECT_EXAMPLE_COM = "http://redirect.example.com"; private String scimReadWriteToken; private String scimCreateToken; private RandomValueStringGenerator generator = new RandomValueStringGenerator(); private TestClient testClient; private MockMvcUtils mockMvcUtils = utils(); + private ClientDetails clientDetails; + private ScimUserProvisioning usersRepository; + private ExpiringCodeStore codeStore; @Before public void setUp() throws Exception { @@ -57,9 +89,11 @@ public void setUp() throws Exception { String clientId = generator.generate().toLowerCase(); String clientSecret = generator.generate().toLowerCase(); String authorities = "scim.read,scim.write,password.write,oauth.approvals,scim.create"; - utils().createClient(this.getMockMvc(), adminToken, clientId, clientSecret, "oauth", "foo,bar", Collections.singletonList(MockMvcUtils.GrantType.client_credentials), authorities); + clientDetails = utils().createClient(this.getMockMvc(), adminToken, clientId, clientSecret, "oauth", "foo,bar", Collections.singletonList(MockMvcUtils.GrantType.client_credentials), authorities); scimReadWriteToken = testClient.getClientCredentialsOAuthAccessToken(clientId, clientSecret,"scim.read scim.write password.write"); scimCreateToken = testClient.getClientCredentialsOAuthAccessToken(clientId, clientSecret,"scim.create"); + usersRepository = getWebApplicationContext().getBean(ScimUserProvisioning.class); + codeStore = getWebApplicationContext().getBean(ExpiringCodeStore.class); } private ScimUser createUser(String token) throws Exception { @@ -140,6 +174,194 @@ public void testCreateUserWithScimCreateToken() throws Exception { createUser(scimCreateToken); } + @Test + public void verification_link() throws Exception { + ScimUser joel = setUpScimUser(); + + MockHttpServletRequestBuilder get = setUpVerificationLinkRequest(joel, scimCreateToken); + + MvcResult result = getMockMvc().perform(get) + .andExpect(status().isOk()) + .andReturn(); + + VerificationResponse verificationResponse = JsonUtils.readValue(result.getResponse().getContentAsString(), VerificationResponse.class); + assertThat(verificationResponse.getVerifyLink().toString(), startsWith("http://localhost/verify_user")); + + String query = verificationResponse.getVerifyLink().getQuery(); + + String code = getQueryStringParam(query, "code"); + assertThat(code, is(notNullValue())); + + ExpiringCode expiringCode = codeStore.retrieveCode(code); + assertThat(expiringCode.getExpiresAt().getTime(), is(greaterThan(System.currentTimeMillis()))); + Map data = JsonUtils.readValue(expiringCode.getData(), new TypeReference>() {}); + assertThat(data.get(InvitationConstants.USER_ID), is(notNullValue())); + assertThat(data.get(CLIENT_ID), is(clientDetails.getClientId())); + assertThat(data.get(REDIRECT_URI), is(HTTP_REDIRECT_EXAMPLE_COM)); + } + + @Test + public void verification_link_in_non_default_zone() throws Exception { + String subdomain = generator.generate().toLowerCase(); + MockMvcUtils.IdentityZoneCreationResult zoneResult = utils().createOtherIdentityZoneAndReturnResult(subdomain, getMockMvc(), getWebApplicationContext(), null); + String zonedClientId = "zonedClientId"; + String zonedClientSecret = "zonedClientSecret"; + BaseClientDetails zonedClientDetails = (BaseClientDetails)utils().createClient(this.getMockMvc(), zoneResult.getZoneAdminToken(), zonedClientId, zonedClientSecret, "oauth", null, Arrays.asList(new MockMvcUtils.GrantType[]{MockMvcUtils.GrantType.client_credentials}), "scim.create", null, zoneResult.getIdentityZone()); + zonedClientDetails.setClientSecret(zonedClientSecret); + String zonedScimCreateToken = utils().getClientCredentialsOAuthAccessToken(getMockMvc(), zonedClientDetails.getClientId(), zonedClientDetails.getClientSecret(), "scim.create", subdomain); + + ScimUser joel = setUpScimUser(); + + MockHttpServletRequestBuilder get = MockMvcRequestBuilders.get("/Users/" + joel.getId() + "/verify-link") + .with(new RequestPostProcessor() { + + @Override + public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { + request.setServerName(subdomain + ".localhost"); + return request; + } + }) + .header("Authorization", "Bearer " + zonedScimCreateToken) + .param("redirect_uri", HTTP_REDIRECT_EXAMPLE_COM) + .accept(APPLICATION_JSON); + + MvcResult result = getMockMvc().perform(get) + .andExpect(status().isOk()) + .andReturn(); + VerificationResponse verificationResponse = JsonUtils.readValue(result.getResponse().getContentAsString(), VerificationResponse.class); + assertThat(verificationResponse.getVerifyLink().toString(), startsWith("http://" + subdomain + ".localhost/verify_user")); + + String query = verificationResponse.getVerifyLink().getQuery(); + + String code = getQueryStringParam(query, "code"); + assertThat(code, is(notNullValue())); + + ExpiringCode expiringCode = codeStore.retrieveCode(code); + assertThat(expiringCode.getExpiresAt().getTime(), is(greaterThan(System.currentTimeMillis()))); + Map data = JsonUtils.readValue(expiringCode.getData(), new TypeReference>() {}); + assertThat(data.get(InvitationConstants.USER_ID), is(notNullValue())); + assertThat(data.get(CLIENT_ID), is(zonedClientDetails.getClientId())); + assertThat(data.get(REDIRECT_URI), is(HTTP_REDIRECT_EXAMPLE_COM)); + } + + @Test + public void create_user_without_email() throws Exception { + ScimUser joel = new ScimUser(null, "a_user", "Joel", "D'sa"); + + getMockMvc().perform(post("/Users") + .header("Authorization", "Bearer " + scimReadWriteToken) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(joel))) + .andExpect(status().isBadRequest()) + .andExpect(content() + .string(JsonObjectMatcherUtils.matchesJsonObject( + new JSONObject() + .put("error_description", "An email must be provided.") + .put("message", "An email must be provided.") + .put("error", "invalid_scim_resource")))); + } + + @Test + public void create_user_then_update_without_email() throws Exception { + ScimUser user = setUpScimUser(); + user.setEmails(null); + + getMockMvc().perform(put("/Users/" + user.getId()) + .header("Authorization", "Bearer " + scimReadWriteToken) + .header("If-Match", "\"" + user.getVersion() + "\"") + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(user))) + .andExpect(status().isBadRequest()) + .andExpect(content() + .string(JsonObjectMatcherUtils.matchesJsonObject( + new JSONObject() + .put("error_description", "An email must be provided.") + .put("message", "An email must be provided.") + .put("error", "invalid_scim_resource")))); + } + + @Test + public void verification_link_unverified_error() throws Exception { + ScimUser user = setUpScimUser(); + user.setVerified(true); + usersRepository.update(user.getId(), user); + + MockHttpServletRequestBuilder get = setUpVerificationLinkRequest(user, scimCreateToken); + + getMockMvc().perform(get) + .andExpect(status().isMethodNotAllowed()) + .andExpect(content() + .string(JsonObjectMatcherUtils.matchesJsonObject( + new JSONObject() + .put("error_description", UserAlreadyVerifiedException.DESC) + .put("message", UserAlreadyVerifiedException.DESC) + .put("error", "user_already_verified")))); + } + + @Test + public void verification_link_is_authorized_endpoint() throws Exception { + ScimUser joel = setUpScimUser(); + + MockHttpServletRequestBuilder get = MockMvcRequestBuilders.get("/Users/" + joel.getId() + "/verify-link") + .param("redirect_uri", HTTP_REDIRECT_EXAMPLE_COM) + .accept(APPLICATION_JSON); + + getMockMvc().perform(get) + .andExpect(status().isUnauthorized()); + } + + @Test + public void verification_link_secured_with_scimcreate() throws Exception { + ScimUser joel = setUpScimUser(); + + MockHttpServletRequestBuilder get = setUpVerificationLinkRequest(joel, scimReadWriteToken); + + getMockMvc().perform(get) + .andExpect(status().isForbidden()); + } + + @Test + public void verification_link_user_not_found() throws Exception{ + MockHttpServletRequestBuilder get = MockMvcRequestBuilders.get("/Users/12345/verify-link") + .header("Authorization", "Bearer " + scimCreateToken) + .param("redirect_uri", HTTP_REDIRECT_EXAMPLE_COM) + .accept(APPLICATION_JSON); + + getMockMvc().perform(get) + .andExpect(status().isNotFound()) + .andExpect(content() + .string(JsonObjectMatcherUtils.matchesJsonObject( + new JSONObject() + .put("error_description", "User 12345 does not exist") + .put("message", "User 12345 does not exist") + .put("error", "scim_resource_not_found")))); + } + + @Test + public void reverify_generate_new_code_and_invalidate_old_code() throws Exception{ + ScimUser joel = setUpScimUser(); + + MockHttpServletRequestBuilder get = setUpVerificationLinkRequest(joel, scimCreateToken); + + MvcResult firstResult = getMockMvc().perform(get) + .andExpect(status().isOk()) + .andReturn(); + VerificationResponse firstVerificationResponse = JsonUtils.readValue(firstResult.getResponse().getContentAsString(), VerificationResponse.class); + + String firstCode = getQueryStringParam(firstVerificationResponse.getVerifyLink().getQuery(), "code"); + + MvcResult secondResult = getMockMvc().perform(get) + .andExpect(status().isOk()) + .andReturn(); + VerificationResponse secondVerificationResponse = JsonUtils.readValue(secondResult.getResponse().getContentAsString(), VerificationResponse.class); + + String secondCode = getQueryStringParam(secondVerificationResponse.getVerifyLink().getQuery(), "code"); + + assertThat(firstCode.equals(secondCode), is(not(true))); + + assertThat(codeStore.retrieveCode(firstCode), is(nullValue())); + } + @Test public void testVerifyUser() throws Exception { verifyUser(scimReadWriteToken); @@ -227,11 +449,7 @@ private void verifyUser(String token) throws Exception { } private void getUser(String token, int status) throws Exception { - ScimUserProvisioning usersRepository = getWebApplicationContext().getBean(ScimUserProvisioning.class); - String email = "joe@"+generator.generate().toLowerCase()+".com"; - ScimUser joel = new ScimUser(null, email, "Joel", "D'sa"); - joel.addEmail(email); - joel = usersRepository.createUser(joel, "pas5Word"); + ScimUser joel = setUpScimUser(); getAndReturnUser(status, joel, token); } @@ -283,7 +501,7 @@ protected ScimUser updateUser(String token, int status) throws Exception { } protected ScimUser updateUser(String token, int status, ScimUser user) throws Exception { - MockHttpServletRequestBuilder put = MockMvcRequestBuilders.put("/Users/" + user.getId()) + MockHttpServletRequestBuilder put = put("/Users/" + user.getId()) .header("Authorization", "Bearer " + token) .header("If-Match", "\"" + user.getVersion() + "\"") .accept(APPLICATION_JSON) @@ -333,4 +551,28 @@ public void cannotCreateUserWithInvalidPasswordInDefaultZone() throws Exception .andExpect(jsonPath("$.message").value("Password must be no more than 255 characters in length.")); } + private MockHttpServletRequestBuilder setUpVerificationLinkRequest(ScimUser user, String token) { + return MockMvcRequestBuilders.get("/Users/" + user.getId() + "/verify-link") + .header("Authorization", "Bearer " + token) + .param("redirect_uri", HTTP_REDIRECT_EXAMPLE_COM) + .accept(APPLICATION_JSON); + } + + private ScimUser setUpScimUser() { + String email = "joe@"+generator.generate().toLowerCase()+".com"; + ScimUser joel = new ScimUser(null, email, "Joel", "D'sa"); + joel.addEmail(email); + joel = usersRepository.createUser(joel, "pas5Word"); + return joel; + } + + private String getQueryStringParam(String query, String key) { + List params = URLEncodedUtils.parse(query, Charset.defaultCharset()); + for (NameValuePair pair : params) { + if (key.equals(pair.getName())) { + return pair.getValue(); + } + } + return null; + } } From 29ff390babe82c7489296eb404b7f555a7856d78 Mon Sep 17 00:00:00 2001 From: Will Tran Date: Thu, 15 Oct 2015 16:52:19 -0400 Subject: [PATCH 064/103] Delete UAA-Multitenancy.md This is covered in https://github.com/cloudfoundry/uaa/blob/master/docs/UAA-APIs.rst#identity-zone-management-apis --- docs/UAA-Multitenancy.md | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 docs/UAA-Multitenancy.md diff --git a/docs/UAA-Multitenancy.md b/docs/UAA-Multitenancy.md deleted file mode 100644 index f919408c3e2..00000000000 --- a/docs/UAA-Multitenancy.md +++ /dev/null @@ -1,19 +0,0 @@ -# UAA Multitenancy - -## Identity Zones - -An Identity Zone represents the boundary behind which OAuth Clients, Users, and Identity Providers can interact. - -### Identiy Zone API - -#### PUT /identity-zones/{id} -* Requires scope `zones.write` -* Creates / updates an identity zone with the given {id} -* Sample request - -``` -curl -v -X PUT -H 'Authorization: Bearer [ACCESS_TOKEN]' \ --H 'Content-Type: application/json' \ --d'{"subdomain":"zone1","name":"Zone 1","description":"test zone"}' -``` -* Returns 201 for a newly created Identity Zone, 200 for an updated Identity Zone, or 409 if the subdomain already exists. \ No newline at end of file From 9cb9970d1420ab38e475e9d3fcab8c3f9099679b Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Fri, 16 Oct 2015 10:04:44 -0700 Subject: [PATCH 065/103] Add Tests Signed-off-by: Jonathan Lo --- .../login/LoginInfoEndpointTest.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/login/LoginInfoEndpointTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/login/LoginInfoEndpointTest.java index 32f16344e11..31872e8b2e4 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/login/LoginInfoEndpointTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/login/LoginInfoEndpointTest.java @@ -111,6 +111,44 @@ public void customSelfserviceLinks_OnlyApplyToDefaultZone() throws Exception { assertEquals("/forgot_password", model.asMap().get("forgotPasswordLink")); } + @Test + public void no_self_service_links_if_self_service_disabled() throws Exception { + LoginInfoEndpoint endpoint = getEndpoint(); + Map linksSet = new HashMap<>(); + linksSet.put("register", "/create_account"); + linksSet.put("passwd", "/forgot_password"); + endpoint.setLinks(linksSet); + + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("login.selfServiceLinksEnabled", "false"); + endpoint.setEnvironment(environment); + Model model = new ExtendedModelMap(); + endpoint.infoForJson(model, null); + Map links = (Map) model.asMap().get("links"); + assertNotNull(links); + assertNull(links.get("register")); + assertNull(links.get("passwd")); + } + + @Test + public void no_self_service_links_if_internal_user_management_disabled() throws Exception { + LoginInfoEndpoint endpoint = getEndpoint(); + Map linksSet = new HashMap<>(); + linksSet.put("register", "/create_account"); + linksSet.put("passwd", "/forgot_password"); + endpoint.setLinks(linksSet); + + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("disableInternalUserManagement", "true"); + endpoint.setEnvironment(environment); + Model model = new ExtendedModelMap(); + endpoint.infoForJson(model, null); + Map links = (Map) model.asMap().get("links"); + assertNotNull(links); + assertNull(links.get("register")); + assertNull(links.get("passwd")); + } + @Test public void testGeneratePasscodeForKnownUaaPrincipal() throws Exception { Map model = new HashMap<>(); From b8a07a3c6a11e78aa3e28f79d2067a4cb3e883cb Mon Sep 17 00:00:00 2001 From: Jonathan Lo Date: Fri, 16 Oct 2015 10:39:31 -0700 Subject: [PATCH 066/103] Disabling self service links if selfServiceLinksEnabled or not disableUserMangement for json requests. [finishes #105798774] https://www.pivotaltracker.com/story/show/105798774 Signed-off-by: Jeremy Coffield --- .../authentication/login/LoginInfoEndpoint.java | 16 ++++++++++++++-- .../login/LoginInfoEndpointTest.java | 8 ++------ uaa/src/main/webapp/WEB-INF/spring-servlet.xml | 2 ++ 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/login/LoginInfoEndpoint.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/login/LoginInfoEndpoint.java index 8548bf5fe58..80a76908c1e 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/login/LoginInfoEndpoint.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/login/LoginInfoEndpoint.java @@ -106,6 +106,17 @@ public class LoginInfoEndpoint { private ExpiringCodeStore expiringCodeStore; private ClientDetailsService clientDetailsService; + private boolean selfServiceLinksEnabled = true; + private boolean disableInternalUserManagement; + + public void setSelfServiceLinksEnabled(boolean selfServiceLinksEnabled) { + this.selfServiceLinksEnabled = selfServiceLinksEnabled; + } + + public void setDisableInternalUserManagement(boolean disableInternalUserManagement) { + this.disableInternalUserManagement = disableInternalUserManagement; + } + public void setExpiringCodeStore(ExpiringCodeStore expiringCodeStore) { this.expiringCodeStore = expiringCodeStore; } @@ -248,7 +259,6 @@ private String login(Model model, Principal principal, List excludedProm populatePrompts(model, excludedPrompts, nonHtml); if (principal == null) { - boolean selfServiceLinksEnabled = !"false".equalsIgnoreCase(environment.getProperty("login.selfServiceLinksEnabled")); if (selfServiceLinksEnabled && (!nonHtml)) { if(!IdentityZoneHolder.isUaa()) { model.addAttribute("createAccountLink", "/create_account"); @@ -433,7 +443,9 @@ protected ExpiringCode doGenerateCode(Object o) throws IOException { Map model = new HashMap<>(); model.put(Origin.UAA, getUaaBaseUrl()); model.put("login", getUaaBaseUrl().replaceAll(Origin.UAA, "login")); - model.putAll(getLinks()); + if (selfServiceLinksEnabled && !disableInternalUserManagement) { + model.putAll(getLinks()); + } return model; } diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/login/LoginInfoEndpointTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/login/LoginInfoEndpointTest.java index 31872e8b2e4..1c050b8a1ab 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/login/LoginInfoEndpointTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/login/LoginInfoEndpointTest.java @@ -119,10 +119,8 @@ public void no_self_service_links_if_self_service_disabled() throws Exception { linksSet.put("passwd", "/forgot_password"); endpoint.setLinks(linksSet); - MockEnvironment environment = new MockEnvironment(); - environment.setProperty("login.selfServiceLinksEnabled", "false"); - endpoint.setEnvironment(environment); Model model = new ExtendedModelMap(); + endpoint.setSelfServiceLinksEnabled(false); endpoint.infoForJson(model, null); Map links = (Map) model.asMap().get("links"); assertNotNull(links); @@ -138,10 +136,8 @@ public void no_self_service_links_if_internal_user_management_disabled() throws linksSet.put("passwd", "/forgot_password"); endpoint.setLinks(linksSet); - MockEnvironment environment = new MockEnvironment(); - environment.setProperty("disableInternalUserManagement", "true"); - endpoint.setEnvironment(environment); Model model = new ExtendedModelMap(); + endpoint.setDisableInternalUserManagement(true); endpoint.infoForJson(model, null); Map links = (Map) model.asMap().get("links"); assertNotNull(links); diff --git a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml index 0352bbb5861..dd168d0a6e2 100755 --- a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml +++ b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml @@ -277,6 +277,8 @@ + + From 3cb5666ebf3a8ceaeefb643eba16f5dddea976dc Mon Sep 17 00:00:00 2001 From: Jeremy Coffield Date: Fri, 16 Oct 2015 14:38:01 -0700 Subject: [PATCH 067/103] removed environment dependency from LoginInfoEndpoint - made more properties configurable as bean [#105798774] https://www.pivotaltracker.com/story/show/105798774 Signed-off-by: Jonathan Lo --- .../login/LoginInfoEndpoint.java | 20 +++++----- .../login/LoginInfoEndpointTest.java | 10 ++--- .../main/webapp/WEB-INF/spring-servlet.xml | 3 +- .../identity/uaa/login/LoginMockMvcTests.java | 40 ++++++++++--------- 4 files changed, 37 insertions(+), 36 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/login/LoginInfoEndpoint.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/login/LoginInfoEndpoint.java index 80a76908c1e..80938e58cf8 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/login/LoginInfoEndpoint.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/login/LoginInfoEndpoint.java @@ -31,7 +31,6 @@ import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.cloudfoundry.identity.uaa.util.UaaStringUtils; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; -import org.springframework.core.env.Environment; import org.springframework.core.io.support.PropertiesLoaderUtils; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.AuthenticationManager; @@ -95,8 +94,6 @@ public class LoginInfoEndpoint { private String uaaHost; - protected Environment environment; - private SamlIdentityProviderConfigurator idpDefinitions; private long codeExpirationMillis = 5 * 60 * 1000; @@ -108,6 +105,8 @@ public class LoginInfoEndpoint { private boolean selfServiceLinksEnabled = true; private boolean disableInternalUserManagement; + private String customSignupLink; + private String customPasswordLink; public void setSelfServiceLinksEnabled(boolean selfServiceLinksEnabled) { this.selfServiceLinksEnabled = selfServiceLinksEnabled; @@ -117,6 +116,14 @@ public void setDisableInternalUserManagement(boolean disableInternalUserManageme this.disableInternalUserManagement = disableInternalUserManagement; } + public void setCustomSignupLink(String customSignupLink) { + this.customSignupLink = customSignupLink; + } + + public void setCustomPasswordLink(String customPasswordLink) { + this.customPasswordLink = customPasswordLink; + } + public void setExpiringCodeStore(ExpiringCodeStore expiringCodeStore) { this.expiringCodeStore = expiringCodeStore; } @@ -141,11 +148,6 @@ public void setAuthenticationManager(AuthenticationManager authenticationManager this.authenticationManager = authenticationManager; } - public void setEnvironment(Environment environment) { - this.environment = environment; - } - - private String entityID = ""; public void setEntityID(String entityID) { @@ -264,8 +266,6 @@ private String login(Model model, Principal principal, List excludedProm model.addAttribute("createAccountLink", "/create_account"); model.addAttribute("forgotPasswordLink", "/forgot_password"); } else { - String customSignupLink = environment.getProperty("links.signup"); - String customPasswordLink = environment.getProperty("links.passwd"); if (StringUtils.hasText(customSignupLink)) { model.addAttribute("createAccountLink", customSignupLink); } else { diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/login/LoginInfoEndpointTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/login/LoginInfoEndpointTest.java index 1c050b8a1ab..7ff16863d54 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/login/LoginInfoEndpointTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/login/LoginInfoEndpointTest.java @@ -7,16 +7,15 @@ import org.cloudfoundry.identity.uaa.client.ClientConstants; import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; import org.cloudfoundry.identity.uaa.codestore.InMemoryExpiringCodeStore; +import org.cloudfoundry.identity.uaa.login.saml.LoginSamlAuthenticationToken; import org.cloudfoundry.identity.uaa.login.saml.SamlIdentityProviderConfigurator; import org.cloudfoundry.identity.uaa.login.saml.SamlIdentityProviderDefinition; -import org.cloudfoundry.identity.uaa.login.saml.LoginSamlAuthenticationToken; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.cloudfoundry.identity.uaa.zone.MultitenancyFixture; import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.springframework.mock.env.MockEnvironment; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpSession; import org.springframework.security.core.GrantedAuthority; @@ -92,10 +91,8 @@ public void testLoginReturnsOtherZone() throws Exception { @Test public void customSelfserviceLinks_OnlyApplyToDefaultZone() throws Exception { LoginInfoEndpoint endpoint = getEndpoint(); - MockEnvironment environment = new MockEnvironment(); - environment.setProperty("links.signup", "http://custom_signup_link"); - environment.setProperty("links.passwd", "http://custom_passwd_link"); - endpoint.setEnvironment(environment); + endpoint.setCustomSignupLink("http://custom_signup_link"); + endpoint.setCustomPasswordLink("http://custom_passwd_link"); Model model = new ExtendedModelMap(); endpoint.loginForHtml(model, null, new MockHttpServletRequest()); assertEquals("http://custom_signup_link", model.asMap().get("createAccountLink")); @@ -393,7 +390,6 @@ private LoginInfoEndpoint getEndpoint() { endpoint.setBaseUrl("http://someurl"); SamlIdentityProviderConfigurator emptyConfigurator = new SamlIdentityProviderConfigurator(); endpoint.setIdpDefinitions(emptyConfigurator); - endpoint.setEnvironment(new MockEnvironment()); endpoint.setPrompts(prompts); return endpoint; } diff --git a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml index dd168d0a6e2..c564aea8bd2 100755 --- a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml +++ b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml @@ -275,10 +275,11 @@ - + + diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java index 594f71b61d7..e6e83535dc4 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java @@ -40,7 +40,6 @@ import org.junit.Before; import org.junit.Test; import org.springframework.http.HttpHeaders; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.mock.env.MockEnvironment; import org.springframework.mock.env.MockPropertySource; import org.springframework.mock.web.MockHttpServletRequest; @@ -63,6 +62,7 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import org.springframework.util.ReflectionUtils; +import org.springframework.web.context.support.XmlWebApplicationContext; import javax.servlet.http.Cookie; import java.lang.reflect.Field; @@ -86,7 +86,6 @@ import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.http.MediaType.TEXT_HTML; import static org.springframework.security.oauth2.common.util.OAuth2Utils.CLIENT_ID; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.securityContext; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -116,11 +115,13 @@ public class LoginMockMvcTests extends InjectedMockContextTest { private RandomValueStringGenerator generator = new RandomValueStringGenerator(); private String adminToken; + private XmlWebApplicationContext webApplicationContext; @Before public void setUpContext() throws Exception { SecurityContextHolder.clearContext(); - mockEnvironment = (MockEnvironment) getWebApplicationContext().getEnvironment(); + webApplicationContext = getWebApplicationContext(); + mockEnvironment = (MockEnvironment) webApplicationContext.getEnvironment(); f.setAccessible(true); propertySource = (MockPropertySource)ReflectionUtils.getField(f, mockEnvironment); for (String s : propertySource.getPropertyNames()) { @@ -372,7 +373,6 @@ public void testLogOutWithClientRedirect() throws Exception { } } - @Test public void testLoginWithAnalytics() throws Exception { mockEnvironment.setProperty("analytics.code", "secret_code"); @@ -424,7 +424,7 @@ public void testAccessConfirmationPage() throws Exception { @Test public void testSignupsAndResetPasswordEnabled() throws Exception { - mockEnvironment.setProperty("login.selfServiceLinksEnabled", "true"); + webApplicationContext.getBean(LoginInfoEndpoint.class).setSelfServiceLinksEnabled(true); getMockMvc().perform(MockMvcRequestBuilders.get("/login")) .andExpect(xpath("//a[text()='Create account']").exists()) @@ -433,7 +433,7 @@ public void testSignupsAndResetPasswordEnabled() throws Exception { @Test public void testSignupsAndResetPasswordDisabledWithNoLinksConfigured() throws Exception { - mockEnvironment.setProperty("login.selfServiceLinksEnabled", "false"); + webApplicationContext.getBean(LoginInfoEndpoint.class).setSelfServiceLinksEnabled(false); getMockMvc().perform(MockMvcRequestBuilders.get("/login")) .andExpect(xpath("//a[text()='Create account']").doesNotExist()) @@ -442,9 +442,10 @@ public void testSignupsAndResetPasswordDisabledWithNoLinksConfigured() throws Ex @Test public void testSignupsAndResetPasswordDisabledWithSomeLinksConfigured() throws Exception { - mockEnvironment.setProperty("login.selfServiceLinksEnabled", "false"); - mockEnvironment.setProperty("links.signup", "http://example.com/signup"); - mockEnvironment.setProperty("links.passwd", "http://example.com/reset_passwd"); + LoginInfoEndpoint endpoint = webApplicationContext.getBean(LoginInfoEndpoint.class); + endpoint.setCustomSignupLink("http://example.com/signup"); + endpoint.setCustomPasswordLink("http://example.com/reset_passwd"); + endpoint.setSelfServiceLinksEnabled(false); getMockMvc().perform(MockMvcRequestBuilders.get("/login")) .andExpect(xpath("//a[text()='Create account']").doesNotExist()) @@ -453,9 +454,10 @@ public void testSignupsAndResetPasswordDisabledWithSomeLinksConfigured() throws @Test public void testSignupsAndResetPasswordEnabledWithCustomLinks() throws Exception { - mockEnvironment.setProperty("login.selfServiceLinksEnabled", "true"); - mockEnvironment.setProperty("links.signup", "http://example.com/signup"); - mockEnvironment.setProperty("links.passwd", "http://example.com/reset_passwd"); + LoginInfoEndpoint endpoint = webApplicationContext.getBean(LoginInfoEndpoint.class); + endpoint.setCustomSignupLink("http://example.com/signup"); + endpoint.setCustomPasswordLink("http://example.com/reset_passwd"); + endpoint.setSelfServiceLinksEnabled(true); getMockMvc().perform(MockMvcRequestBuilders.get("/login")) .andExpect(xpath("//a[text()='Create account']/@href").string("http://example.com/signup")) @@ -464,7 +466,7 @@ public void testSignupsAndResetPasswordEnabledWithCustomLinks() throws Exception @Test public void testLoginWithExplicitPrompts() throws Exception { - LoginInfoEndpoint controller = getWebApplicationContext().getBean(LoginInfoEndpoint.class); + LoginInfoEndpoint controller = webApplicationContext.getBean(LoginInfoEndpoint.class); List original = controller.getPrompts(); try { Prompt first = new Prompt("how", "text", "How did I get here?"); @@ -484,7 +486,7 @@ public void testLoginWithExplicitPrompts() throws Exception { @Test public void testLoginWithExplicitJsonPrompts() throws Exception { - LoginInfoEndpoint controller = getWebApplicationContext().getBean(LoginInfoEndpoint.class); + LoginInfoEndpoint controller = webApplicationContext.getBean(LoginInfoEndpoint.class); List original = controller.getPrompts(); try { Prompt first = new Prompt("how", "text", "How did I get here?"); @@ -551,7 +553,8 @@ public void testDefaultAndCustomSignupLink() throws Exception { getMockMvc().perform(get("/login").accept(TEXT_HTML)) .andExpect(status().isOk()) .andExpect(model().attribute("createAccountLink", "/create_account")); - mockEnvironment.setProperty("links.signup", "http://www.example.com/signup"); + LoginInfoEndpoint endpoint = webApplicationContext.getBean(LoginInfoEndpoint.class); + endpoint.setCustomSignupLink("http://www.example.com/signup"); getMockMvc().perform(get("/login").accept(TEXT_HTML)) .andExpect(status().isOk()) .andExpect(model().attribute("createAccountLink", "http://www.example.com/signup")); @@ -559,7 +562,7 @@ public void testDefaultAndCustomSignupLink() throws Exception { @Test public void testLocalSignupDisabled() throws Exception { - mockEnvironment.setProperty("login.selfServiceLinksEnabled", "false"); + webApplicationContext.getBean(LoginInfoEndpoint.class).setSelfServiceLinksEnabled(false); getMockMvc().perform(get("/login").accept(TEXT_HTML)) .andExpect(status().isOk()) .andExpect(model().attribute("createAccountLink", nullValue())); @@ -567,8 +570,9 @@ public void testLocalSignupDisabled() throws Exception { @Test public void testCustomSignupLinkWithLocalSignupDisabled() throws Exception { - mockEnvironment.setProperty("login.selfServiceLinksEnabled", "false"); - mockEnvironment.setProperty("links.signup", "http://www.example.com/signup"); + webApplicationContext.getBean(LoginInfoEndpoint.class).setSelfServiceLinksEnabled(false); + LoginInfoEndpoint endpoint = webApplicationContext.getBean(LoginInfoEndpoint.class); + endpoint.setCustomSignupLink("http://example.com/signup"); getMockMvc().perform(get("/login").accept(TEXT_HTML)) .andExpect(status().isOk()) .andExpect(model().attribute("createAccountLink", nullValue())); From 747d822e5206cc5df9ec43b59c2cc323db0dfcb5 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Mon, 19 Oct 2015 10:45:57 -0600 Subject: [PATCH 068/103] Log the error when failing to decode a token --- .../identity/uaa/oauth/token/UaaTokenServices.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServices.java b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServices.java index 21f0e3bdde6..58fd96f0d49 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServices.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServices.java @@ -20,7 +20,6 @@ import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.cloudfoundry.identity.uaa.client.ClientConstants; -import org.cloudfoundry.identity.uaa.oauth.Claims; import org.cloudfoundry.identity.uaa.oauth.approval.Approval; import org.cloudfoundry.identity.uaa.oauth.approval.Approval.ApprovalStatus; import org.cloudfoundry.identity.uaa.oauth.approval.ApprovalStore; @@ -101,6 +100,7 @@ import static org.cloudfoundry.identity.uaa.oauth.Claims.NONCE; import static org.cloudfoundry.identity.uaa.oauth.Claims.ORIGIN; import static org.cloudfoundry.identity.uaa.oauth.Claims.PHONE_NUMBER; +import static org.cloudfoundry.identity.uaa.oauth.Claims.PROFILE; import static org.cloudfoundry.identity.uaa.oauth.Claims.REVOCATION_SIGNATURE; import static org.cloudfoundry.identity.uaa.oauth.Claims.ROLES; import static org.cloudfoundry.identity.uaa.oauth.Claims.SCOPE; @@ -108,7 +108,6 @@ import static org.cloudfoundry.identity.uaa.oauth.Claims.USER_ID; import static org.cloudfoundry.identity.uaa.oauth.Claims.USER_NAME; import static org.cloudfoundry.identity.uaa.oauth.Claims.ZONE_ID; -import static org.cloudfoundry.identity.uaa.oauth.Claims.PROFILE; /** @@ -928,7 +927,7 @@ private Map getClaimsForToken(String token) { try { tokenJwt = JwtHelper.decodeAndVerify(token, signerProvider.getVerifier()); } catch (Throwable t) { - logger.debug("Invalid token (could not decode)"); + logger.debug("Invalid token (could not decode)", t); throw new InvalidTokenException("Invalid token (could not decode): " + token); } From 68565cbc45b10bed01560debb55e88368370e5dd Mon Sep 17 00:00:00 2001 From: Jonathan Lo Date: Mon, 19 Oct 2015 10:43:14 -0700 Subject: [PATCH 069/103] Remove email from /verify_users link [#105489788] https://www.pivotaltracker.com/story/show/105489788 Signed-off-by: Paul Warren --- .../uaa/login/EmailAccountCreationService.java | 4 +--- .../src/main/resources/templates/mail/activate.html | 4 ++-- .../uaa/login/EmailAccountCreationServiceTests.java | 12 ++++++------ .../uaa/scim/endpoints/ScimUserEndpoints.java | 2 +- .../identity/uaa/scim/util/ScimUtils.java | 12 +----------- 5 files changed, 11 insertions(+), 23 deletions(-) diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/login/EmailAccountCreationService.java b/login/src/main/java/org/cloudfoundry/identity/uaa/login/EmailAccountCreationService.java index 1c7de794814..f07fd0e9855 100644 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/login/EmailAccountCreationService.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/login/EmailAccountCreationService.java @@ -45,7 +45,6 @@ public class EmailAccountCreationService implements AccountCreationService { private final ClientDetailsService clientDetailsService; private final PasswordValidator passwordValidator; private final String brand; - private final UaaUrlUtils uaaUrlUtils; public EmailAccountCreationService( SpringTemplateEngine templateEngine, @@ -63,7 +62,6 @@ public EmailAccountCreationService( this.scimUserProvisioning = scimUserProvisioning; this.clientDetailsService = clientDetailsService; this.passwordValidator = passwordValidator; - this.uaaUrlUtils = uaaUrlUtils; this.brand = brand; } @@ -180,7 +178,7 @@ private String getSubjectText() { } private String getEmailHtml(String code, String email) { - String accountsUrl = ScimUtils.getVerificationURL(null, null).toString(); + String accountsUrl = ScimUtils.getVerificationURL(null).toString(); final Context ctx = new Context(); if (IdentityZoneHolder.isUaa()) { diff --git a/login/src/main/resources/templates/mail/activate.html b/login/src/main/resources/templates/mail/activate.html index 9654a233218..3c797d68073 100644 --- a/login/src/main/resources/templates/mail/activate.html +++ b/login/src/main/resources/templates/mail/activate.html @@ -8,8 +8,8 @@ A request has been made to activate an ExampleService for: user@example.com

- Activate your account + Activate your account to continue the registration process.

diff --git a/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailAccountCreationServiceTests.java b/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailAccountCreationServiceTests.java index 430f25b2bf2..5d4450aced3 100644 --- a/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailAccountCreationServiceTests.java +++ b/login/src/test/java/org/cloudfoundry/identity/uaa/login/EmailAccountCreationServiceTests.java @@ -117,7 +117,7 @@ public void testBeginActivation() throws Exception { String emailBody = captorEmailBody("Activate your Pivotal ID"); assertThat(emailBody, containsString("a Pivotal ID")); - assertThat(emailBody, containsString("Activate your account")); + assertThat(emailBody, containsString("Activate your account")); assertThat(emailBody, not(containsString("Cloud Foundry"))); } @@ -141,7 +141,7 @@ public void testBeginActivationInOtherZone() throws Exception { String emailBody = captorEmailBody("Activate your account"); assertThat(emailBody, containsString("A request has been made to activate an account for:")); - assertThat(emailBody, containsString("Activate your account")); + assertThat(emailBody, containsString("Activate your account")); assertThat(emailBody, containsString("Thank you,
\n " + zone.getName())); assertThat(emailBody, not(containsString("Cloud Foundry"))); assertThat(emailBody, not(containsString("Pivotal ID"))); @@ -159,7 +159,7 @@ public void testBeginActivationWithOssBrand() throws Exception { String emailBody = captorEmailBody("Activate your account"); assertThat(emailBody, containsString("an account")); - assertThat(emailBody, containsString("Activate your account")); + assertThat(emailBody, containsString("Activate your account")); assertThat(emailBody, not(containsString("Pivotal"))); } @@ -282,7 +282,7 @@ public void testResendVerificationCodeWithPivotalBrand() throws Exception { String emailBody = captorEmailBody("Activate your Pivotal ID"); assertThat(emailBody, containsString("a Pivotal ID")); - assertThat(emailBody, containsString("Activate your account")); + assertThat(emailBody, containsString("Activate your account")); assertThat(emailBody, containsString("Thank you,
\n Pivotal")); assertThat(emailBody, not(containsString("Cloud Foundry"))); } @@ -297,7 +297,7 @@ public void testResendVerificationCodeWithOssBrand() throws Exception { String emailBody = captorEmailBody("Activate your account"); assertThat(emailBody, containsString("an account")); - assertThat(emailBody, containsString("Activate your account")); + assertThat(emailBody, containsString("Activate your account")); assertThat(emailBody, containsString("Thank you,
\n Cloud Foundry")); assertThat(emailBody, not(containsString("Pivotal"))); } @@ -324,7 +324,7 @@ public void testResendVerificationCodeWithinZone() throws Exception { String emailBody = captorEmailBody("Activate your account"); assertThat(emailBody, containsString("an account")); - assertThat(emailBody, containsString("Activate your account")); + assertThat(emailBody, containsString("Activate your account")); assertThat(emailBody, not(containsString("Pivotal"))); assertThat(emailBody, containsString("Thank you,
\n " + zone.getName())); assertThat(emailBody, not(containsString("Cloud Foundry"))); diff --git a/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpoints.java b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpoints.java index 9f3836cf5aa..4bb72627c3e 100644 --- a/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpoints.java +++ b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpoints.java @@ -260,7 +260,7 @@ public ResponseEntity getUserVerificationLink(@PathVariabl codeStore.retrieveLatest(user.getPrimaryEmail(), clientId); ExpiringCode expiringCode = ScimUtils.getExpiringCode(codeStore, userId, user.getPrimaryEmail(), clientId, redirectUri); - responseBody.setVerifyLink(ScimUtils.getVerificationURL(expiringCode, user.getPrimaryEmail())); + responseBody.setVerifyLink(ScimUtils.getVerificationURL(expiringCode)); return new ResponseEntity<>(responseBody, HttpStatus.OK); } diff --git a/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/util/ScimUtils.java b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/util/ScimUtils.java index 6a4e25638f1..2e220826fbc 100644 --- a/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/util/ScimUtils.java +++ b/scim/src/main/java/org/cloudfoundry/identity/uaa/scim/util/ScimUtils.java @@ -73,12 +73,10 @@ public static ExpiringCode getExpiringCode(ExpiringCodeStore codeStore, String u * * @param expiringCode * the expiring code to include on the URL, may be null - * @param email - * the email to include on the URL, may be null * @return * the verification URL */ - public static URL getVerificationURL(ExpiringCode expiringCode, String email) { + public static URL getVerificationURL(ExpiringCode expiringCode) { String url = ""; try { url = UaaUrlUtils.getUaaUrl("/verify_user"); @@ -87,14 +85,6 @@ public static URL getVerificationURL(ExpiringCode expiringCode, String email) { url += "?code=" + expiringCode.getCode(); } - if (email != null) { - if (expiringCode != null) { - url += "&email=" + email; - } else { - url += "?email=" + email; - } - } - return new URL(url); } catch (MalformedURLException mfue) { logger.error(String.format("Unexpected error creating user verification URL from %s", url), mfue); From 74e7a3ebf278208c54f717675efcffd4f94c21c6 Mon Sep 17 00:00:00 2001 From: Jonathan Lo Date: Tue, 20 Oct 2015 11:21:04 -0700 Subject: [PATCH 070/103] Don't mutate shared objects on IDP tests. Signed-off-by: Jeremy Coffield --- .../IdentityProviderConfiguratorTests.java | 121 +++++++++--------- 1 file changed, 60 insertions(+), 61 deletions(-) diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/login/saml/IdentityProviderConfiguratorTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/login/saml/IdentityProviderConfiguratorTests.java index 29724ef412d..0ebddfb7e28 100755 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/login/saml/IdentityProviderConfiguratorTests.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/login/saml/IdentityProviderConfiguratorTests.java @@ -58,28 +58,26 @@ public static void initializeOpenSAML() throws Exception { if (!org.apache.xml.security.Init.isInitialized()) { DefaultBootstrap.bootstrap(); } - parseYaml(sampleYaml); } public static final String xmlWithoutID = "MIICmTCCAgKgAwIBAgIGAUPATqmEMA0GCSqGSIb3DQEBBQUAMIGPMQswCQYDVQQGEwJVUzETMBEG\n" + - "A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU\n" + - "MBIGA1UECwwLU1NPUHJvdmlkZXIxEDAOBgNVBAMMB1Bpdm90YWwxHDAaBgkqhkiG9w0BCQEWDWlu\n" + - "Zm9Ab2t0YS5jb20wHhcNMTQwMTIzMTgxMjM3WhcNNDQwMTIzMTgxMzM3WjCBjzELMAkGA1UEBhMC\n" + - "VVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNVBAoM\n" + - "BE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRAwDgYDVQQDDAdQaXZvdGFsMRwwGgYJKoZIhvcN\n" + - "AQkBFg1pbmZvQG9rdGEuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCeil67/TLOiTZU\n" + - "WWgW2XEGgFZ94bVO90v5J1XmcHMwL8v5Z/8qjdZLpGdwI7Ph0CyXMMNklpaR/Ljb8fsls3amdT5O\n" + - "Bw92Zo8ulcpjw2wuezTwL0eC0wY/GQDAZiXL59npE6U+fH1lbJIq92hx0HJSru/0O1q3+A/+jjZL\n" + - "3tL/SwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAI5BoWZoH6Mz9vhypZPOJCEKa/K+biZQsA4Zqsuk\n" + - "vvphhSERhqk/Nv76Vkl8uvJwwHbQrR9KJx4L3PRkGCG24rix71jEuXVGZUsDNM3CUKnARx4MEab6\n" + - "GFHNkZ6DmoT/PFagngecHu+EwmuDtaG0rEkFrARwe+d8Ru0BN558abFburn:oasis:names:tc:SAML:1.1:nameid-format:emailAddressurn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n"; + "A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU\n" + + "MBIGA1UECwwLU1NPUHJvdmlkZXIxEDAOBgNVBAMMB1Bpdm90YWwxHDAaBgkqhkiG9w0BCQEWDWlu\n" + + "Zm9Ab2t0YS5jb20wHhcNMTQwMTIzMTgxMjM3WhcNNDQwMTIzMTgxMzM3WjCBjzELMAkGA1UEBhMC\n" + + "VVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNVBAoM\n" + + "BE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRAwDgYDVQQDDAdQaXZvdGFsMRwwGgYJKoZIhvcN\n" + + "AQkBFg1pbmZvQG9rdGEuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCeil67/TLOiTZU\n" + + "WWgW2XEGgFZ94bVO90v5J1XmcHMwL8v5Z/8qjdZLpGdwI7Ph0CyXMMNklpaR/Ljb8fsls3amdT5O\n" + + "Bw92Zo8ulcpjw2wuezTwL0eC0wY/GQDAZiXL59npE6U+fH1lbJIq92hx0HJSru/0O1q3+A/+jjZL\n" + + "3tL/SwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAI5BoWZoH6Mz9vhypZPOJCEKa/K+biZQsA4Zqsuk\n" + + "vvphhSERhqk/Nv76Vkl8uvJwwHbQrR9KJx4L3PRkGCG24rix71jEuXVGZUsDNM3CUKnARx4MEab6\n" + + "GFHNkZ6DmoT/PFagngecHu+EwmuDtaG0rEkFrARwe+d8Ru0BN558abFburn:oasis:names:tc:SAML:1.1:nameid-format:emailAddressurn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n"; public static final String xml = String.format(xmlWithoutID, "http://www.okta.com/k2lw4l5bPODCMIIDBRYZ"); public static final String xmlWithoutHeader = xmlWithoutID.replace("", ""); SamlIdentityProviderConfigurator conf = null; - private static Map> data = null; SamlIdentityProviderDefinition singleAdd = null; SamlIdentityProviderDefinition singleAddWithoutHeader = null; private static final String singleAddAlias = "sample-alias"; @@ -168,7 +166,7 @@ public void setUp() throws Exception { ); } - private static void parseYaml(String sampleYaml) { + private static Map> parseYaml(String sampleYaml) { YamlMapFactoryBean factory = new YamlMapFactoryBean(); factory.setResolutionMethod(YamlProcessor.ResolutionMethod.OVERRIDE_AND_IGNORE); List resources = new ArrayList<>(); @@ -176,13 +174,15 @@ private static void parseYaml(String sampleYaml) { resources.add(resource); factory.setResources(resources.toArray(new Resource[resources.size()])); Map tmpdata = factory.getObject(); - data = new HashMap<>(); + Map> dataMap = new HashMap<>(); for (Map.Entry entry : ((Map)tmpdata.get("providers")).entrySet()) { - data.put(entry.getKey(), (Map)entry.getValue()); + dataMap.put(entry.getKey(), (Map)entry.getValue()); } - data = Collections.unmodifiableMap(data); + return Collections.unmodifiableMap(dataMap); } + private Map> sampleData = parseYaml(sampleYaml); + @Test public void testCloneIdentityProviderDefinition() throws Exception { SamlIdentityProviderDefinition clone = singleAdd.clone(); @@ -192,7 +192,7 @@ public void testCloneIdentityProviderDefinition() throws Exception { @Test public void testSingleAddProviderDefinition() throws Exception { - conf.setIdentityProviders(data); + conf.setIdentityProviders(sampleData); conf.afterPropertiesSet(); conf.addSamlIdentityProviderDefinition(singleAdd); testGetIdentityProviderDefinitions(4, false); @@ -200,7 +200,7 @@ public void testSingleAddProviderDefinition() throws Exception { @Test public void testSingleAddProviderWithoutXMLHeader() throws Exception { - conf.setIdentityProviders(data); + conf.setIdentityProviders(sampleData); conf.afterPropertiesSet(); conf.addSamlIdentityProviderDefinition(singleAddWithoutHeader); testGetIdentityProviderDefinitions(4, false); @@ -220,7 +220,7 @@ public void testAddNullProviderAlias() throws Exception { @Test public void testGetEntityID() throws Exception { Timer t = new Timer(); - conf.setIdentityProviders(data); + conf.setIdentityProviders(sampleData); conf.afterPropertiesSet(); for (SamlIdentityProviderDefinition def : conf.getIdentityProviderDefinitions()) { switch (def.getIdpEntityAlias()) { @@ -280,7 +280,7 @@ public void testGetIdentityProviderDefinititonsForAllowedProviders() throws Exce List clientIdpAliases = asList("simplesamlphp-url", "okta-local-2"); clientDetails.addAdditionalInformation(ClientConstants.ALLOWED_PROVIDERS, clientIdpAliases); - conf.setIdentityProviders(data); + conf.setIdentityProviders(sampleData); conf.afterPropertiesSet(); List clientIdps = conf.getIdentityProviderDefinitions(clientIdpAliases, IdentityZoneHolder.get()); assertEquals(2, clientIdps.size()); @@ -290,7 +290,7 @@ public void testGetIdentityProviderDefinititonsForAllowedProviders() throws Exce @Test public void testReturnAllIdpsInZoneForClientWithNoAllowedProviders() throws Exception { - conf.setIdentityProviders(data); + conf.setIdentityProviders(sampleData); conf.afterPropertiesSet(); SamlIdentityProviderDefinition samlIdentityProviderDefinitionInOtherZone = new SamlIdentityProviderDefinition(xml, "zoneIdpAlias","sample-nameID",1,true,true,"sample-link-test","sample-icon-url", "other-zone-id"); try { @@ -304,7 +304,7 @@ public void testReturnAllIdpsInZoneForClientWithNoAllowedProviders() throws Exce @Test public void testReturnNoIdpsInZoneForClientWithNoAllowedProviders() throws Exception { - conf.setIdentityProviders(data); + conf.setIdentityProviders(sampleData); conf.afterPropertiesSet(); String xmlMetadata = String.format(xmlWithoutID, new RandomValueStringGenerator().generate()); SamlIdentityProviderDefinition samlIdentityProviderDefinitionInOtherZone = new SamlIdentityProviderDefinition(xmlMetadata, "zoneIdpAlias","sample-nameID",1,true,true,"sample-link-test","sample-icon-url", "other-zone-id"); @@ -324,7 +324,7 @@ protected void testGetIdentityProviderDefinitions(int count) throws Exception { } protected void testGetIdentityProviderDefinitions(int count, boolean addData) throws Exception { if (addData) { - conf.setIdentityProviders(data); + conf.setIdentityProviders(sampleData); conf.afterPropertiesSet(); } List idps = conf.getIdentityProviderDefinitions(); @@ -412,14 +412,14 @@ public void testGetIdentityProviders() throws Exception { public void testDuplicateAlias_In_LegacyConfig() throws Exception { conf.setLegacyIdpMetaData("https://simplesamlphp.identity.cf-app.com/saml2/idp/metadata.php"); conf.setLegacyIdpIdentityAlias("simplesamlphp-url"); - conf.setIdentityProviders(data); + conf.setIdentityProviders(sampleData); conf.afterPropertiesSet(); } @Test public void testDuplicate_EntityID_IsRejected() throws Exception { - conf.setIdentityProviders(data); + conf.setIdentityProviders(sampleData); conf.afterPropertiesSet(); testGetIdentityProviderDefinitions(3, false); @@ -457,41 +457,40 @@ public void testDuplicate_EntityID_IsRejected() throws Exception { @Test public void testSetAddShadowUserOnLoginFromYaml() throws Exception { String yaml = " providers:\n" + - " provider-without-shadow-user-definition:\n" + - " idpMetadata: |\n" + - " " + - " " + - " " + - " urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + - " " + - " " + - " \n" + - " nameID: urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\n" + - " provider-with-shadow-users-enabled:\n" + - " idpMetadata: |\n" + - " " + - " " + - " " + - " urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + - " " + - " " + - " \n" + - " nameID: urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\n" + - " addShadowUserOnLogin: true\n" + - " provider-with-shadow-user-disabled:\n" + - " idpMetadata: |\n" + - " " + - " " + - " " + - " urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + - " " + - " " + - " \n" + - " nameID: urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\n" + - " addShadowUserOnLogin: false\n"; - - parseYaml(yaml); - conf.setIdentityProviders(data); + " provider-without-shadow-user-definition:\n" + + " idpMetadata: |\n" + + " " + + " " + + " " + + " urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + + " " + + " " + + " \n" + + " nameID: urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\n" + + " provider-with-shadow-users-enabled:\n" + + " idpMetadata: |\n" + + " " + + " " + + " " + + " urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + + " " + + " " + + " \n" + + " nameID: urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\n" + + " addShadowUserOnLogin: true\n" + + " provider-with-shadow-user-disabled:\n" + + " idpMetadata: |\n" + + " " + + " " + + " " + + " urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + + " " + + " " + + " \n" + + " nameID: urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\n" + + " addShadowUserOnLogin: false\n"; + + conf.setIdentityProviders(parseYaml(yaml)); conf.afterPropertiesSet(); for (SamlIdentityProviderDefinition def : conf.getIdentityProviderDefinitions()) { From 5264783b31de3754a1ca037ddcdcb046715cac52 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Tue, 20 Oct 2015 15:11:39 -0600 Subject: [PATCH 071/103] Add in configuration option to follow referral Set follow referral to true (previous was ignore) Introduce configuration option to ignore partial result error on group search Set ignorePartialResultException to true (default was false) for group search https://www.pivotaltracker.com/story/show/93717600 [#93717600] --- uaa/src/main/resources/ldap-integration.xml | 1 + .../resources/ldap/ldap-groups-populator.xml | 2 +- uaa/src/main/resources/ldap_init.ldif | 30 +++++++++++ uaa/src/main/resources/uaa.yml | 11 +++- .../uaa/mock/ldap/LdapMockMvcTests.java | 54 +++++++++++++++++++ .../ldap/server/ApacheDsSSLContainer.java | 31 ++++++----- 6 files changed, 114 insertions(+), 15 deletions(-) diff --git a/uaa/src/main/resources/ldap-integration.xml b/uaa/src/main/resources/ldap-integration.xml index da5263a73a1..dc22821b2ea 100644 --- a/uaa/src/main/resources/ldap-integration.xml +++ b/uaa/src/main/resources/ldap-integration.xml @@ -29,6 +29,7 @@ + diff --git a/uaa/src/main/resources/ldap/ldap-groups-populator.xml b/uaa/src/main/resources/ldap/ldap-groups-populator.xml index 1014ac6d221..2aeacf10dee 100644 --- a/uaa/src/main/resources/ldap/ldap-groups-populator.xml +++ b/uaa/src/main/resources/ldap/ldap-groups-populator.xml @@ -37,7 +37,7 @@ cn - + \ No newline at end of file diff --git a/uaa/src/main/resources/ldap_init.ldif b/uaa/src/main/resources/ldap_init.ldif index 76f9c8fbdec..cba440ff3e3 100644 --- a/uaa/src/main/resources/ldap_init.ldif +++ b/uaa/src/main/resources/ldap_init.ldif @@ -104,10 +104,23 @@ dn: cn=marissa7,ou=Users,dc=test,dc=com changetype: add objectClass: person objectClass: organizationalPerson +objectClass: inetOrgPerson cn: marissa7 userPassword: ldap7 +uid: 20f459e0-e30b-4d1f-998c-3ded7f769db7 sn: Marissa7 +dn: cn=marissa8,ou=Users,dc=test,dc=com +changetype: add +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: marissa8 +userPassword: ldap8 +uid: 20f459e0-e30b-4d1f-998c-3ded7f769db8 +sn: Marissa8 + + ############################################################################### # BEGIN GROUP TO SCOPE MAPPING ############################################################################### @@ -163,6 +176,23 @@ cn: superusers description: test.everything member: cn=marissa4,ou=Users,dc=test,dc=com +dn: cn=otherusers,ou=scopes,dc=test,dc=com +changetype: add +objectClass: groupOfNames +objectClass: top +cn: otherusers +description: test.everything +member: cn=marissa8,ou=Users,dc=test,dc=com + +# Invalid referral cause PartialResultsException + +dn: cn=Referral1,ou=scopes,dc=test,dc=com +changetype: add +objectclass: referral +objectclass: extensibleObject +member: cn=marissa8,ou=Users,dc=test,dc=com +ref: ldap://localhost:43389/cn=otherusers1,ou=scopes,dc=test,dc=com + ############################################################################### # END GROUP TO SCOPE MAPPING ############################################################################### diff --git a/uaa/src/main/resources/uaa.yml b/uaa/src/main/resources/uaa.yml index c23b4e9c2df..2552fde9fcb 100755 --- a/uaa/src/main/resources/uaa.yml +++ b/uaa/src/main/resources/uaa.yml @@ -102,8 +102,17 @@ # url: 'ldap://localhost:10389/' # userDn: 'cn=admin,dc=test,dc=com' # password: 'password' -# searchBase: '' +# searchBase: 'dc=test,dc=com' # searchFilter: 'cn={0}' +# referral: ignore +# groups: +# file: 'ldap/ldap-groups-map-to-scopes.xml' +# searchBase: 'dc=test,dc=com' +# groupSearchFilter: 'member={0}' +# searchSubtree: true +# maxSearchDepth: 10 +# autoAdd: true +# ignorePartialResultException: true #ldap: # profile: diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java index 6f832914e88..f3713e91c4c 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java @@ -560,6 +560,60 @@ public void testLoginInNonDefaultZone() throws Exception { assertEquals("marissa2@ldaptest.com", user.getEmail()); } + @Test + public void testLogin_partial_result_exception_on_group_search() throws Exception { + Assume.assumeThat("ldap-search-and-bind.xml", StringContains.containsString(ldapProfile)); + Assume.assumeThat("ldap-groups-map-to-scopes.xml", StringContains.containsString(ldapGroup)); + + setUp(); + String identityAccessToken = MockMvcUtils.utils().getClientOAuthAccessToken(mockMvc, "identity", "identitysecret", ""); + String adminAccessToken = MockMvcUtils.utils().getClientOAuthAccessToken(mockMvc, "admin", "adminsecret", ""); + IdentityZone zone = MockMvcUtils.utils().createZoneUsingWebRequest(mockMvc, identityAccessToken); + String zoneAdminToken = MockMvcUtils.utils().getZoneAdminToken(mockMvc, adminAccessToken, zone.getId()); + + LdapIdentityProviderDefinition definition = LdapIdentityProviderDefinition.searchAndBindMapGroupToScopes( + "ldap://localhost:33389", + "cn=admin,ou=Users,dc=test,dc=com", + "adminsecret", + "dc=test,dc=com", + "cn={0}", + "dc=test,dc=com", + "member={0}", + "mail", + null, + false, + true, + true, + 10, + true + ); + + IdentityProvider provider = new IdentityProvider(); + provider.setOriginKey(Origin.LDAP); + provider.setName("Test ldap provider"); + provider.setType(Origin.LDAP); + provider.setConfig(JsonUtils.writeValueAsString(definition)); + provider.setActive(true); + provider.setIdentityZoneId(zone.getId()); + provider = MockMvcUtils.utils().createIdpUsingWebRequest(mockMvc, zone.getId(), zoneAdminToken, provider, status().isCreated()); + + mockMvc.perform(post("/login.do").accept(TEXT_HTML_VALUE) + .with(cookieCsrf()) + .with(new SetServerNameRequestPostProcessor(zone.getSubdomain()+".localhost")) + .param("username", "marissa8") + .param("password", "ldap8")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("/")); + + IdentityZoneHolder.set(zone); + UaaUser user = userDatabase.retrieveUserByName("marissa8",Origin.LDAP); + IdentityZoneHolder.clear(); + assertNotNull(user); + assertEquals(Origin.LDAP, user.getOrigin()); + assertEquals(zone.getId(), user.getZoneId()); + + } + @Test public void runLdapTestblock() throws Exception { diff --git a/uaa/src/test/java/org/springframework/security/ldap/server/ApacheDsSSLContainer.java b/uaa/src/test/java/org/springframework/security/ldap/server/ApacheDsSSLContainer.java index e06d90bc9dd..251d435250c 100644 --- a/uaa/src/test/java/org/springframework/security/ldap/server/ApacheDsSSLContainer.java +++ b/uaa/src/test/java/org/springframework/security/ldap/server/ApacheDsSSLContainer.java @@ -4,14 +4,27 @@ import org.apache.directory.server.ldap.LdapServer; import org.apache.directory.server.ldap.handlers.extended.StartTlsHandler; import org.apache.directory.server.protocol.shared.transport.TcpTransport; -import org.springframework.util.ClassUtils; -import sun.security.x509.*; +import sun.security.x509.AlgorithmId; +import sun.security.x509.CertificateAlgorithmId; +import sun.security.x509.CertificateSerialNumber; +import sun.security.x509.CertificateValidity; +import sun.security.x509.CertificateVersion; +import sun.security.x509.CertificateX509Key; +import sun.security.x509.X500Name; +import sun.security.x509.X509CertImpl; +import sun.security.x509.X509CertInfo; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; -import java.security.*; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SignatureException; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; @@ -132,13 +145,8 @@ private static X509Certificate getSelfCertificate(X500Name x500Name, Date issueD certInfo.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber((new Random()).nextInt() & Integer.MAX_VALUE)); certInfo.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(AlgorithmId.get(signatureAlgorithm))); - if(isJava8()) { - certInfo.set(X509CertInfo.SUBJECT, x500Name); - certInfo.set(X509CertInfo.ISSUER, x500Name); - } else { - certInfo.set(X509CertInfo.SUBJECT, new CertificateSubjectName(x500Name)); - certInfo.set(X509CertInfo.ISSUER, new CertificateIssuerName(x500Name)); - } + certInfo.set(X509CertInfo.SUBJECT, x500Name); + certInfo.set(X509CertInfo.ISSUER, x500Name); certInfo.set(X509CertInfo.KEY, new CertificateX509Key(keyPair.getPublic())); certInfo.set(X509CertInfo.VALIDITY, new CertificateValidity(issueDate, expirationDate)); @@ -151,8 +159,5 @@ private static X509Certificate getSelfCertificate(X500Name x500Name, Date issueD } } - private static boolean isJava8() { - return ClassUtils.isPresent("java.util.Optional", ApacheDsSSLContainer.class.getClassLoader()); - } } From c52dda0ebf181d952afa67ba93b622a20d7df7e1 Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Thu, 27 Aug 2015 23:20:50 +0200 Subject: [PATCH 072/103] Fix obvious bugs found during automatic code inspection. Fixed usage of approval parameters in JdbcApprovalStore. Previously the approval parameter wasn't used at all when revokeApproval was called. Corrected validation check in ClientDetailsModification. Previously the check returned always true and didn't consider the "none" action. Corrected incorrect equals check in ExtendedUaaAuthority. I just ran "analyze code" with the latest intellij idea version and it brought up several warnings that indicate potential problems. I strongly suggest to to run a code analysis on the code yourself. --- .../identity/uaa/oauth/approval/JdbcApprovalStore.java | 2 +- .../identity/uaa/oauth/client/ClientDetailsModification.java | 3 ++- .../cloudfoundry/identity/uaa/user/ExtendedUaaAuthority.java | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/approval/JdbcApprovalStore.java b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/approval/JdbcApprovalStore.java index f8e1a8ee027..516704affa9 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/approval/JdbcApprovalStore.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/approval/JdbcApprovalStore.java @@ -134,7 +134,7 @@ public void setValues(PreparedStatement ps) throws SQLException { @Override public boolean revokeApproval(Approval approval) { - return revokeApprovals(String.format("user_id eq \"%s\" and client_id eq \"%s\" and scope eq \"%s\"")); + return revokeApprovals(String.format("user_id eq \"%s\" and client_id eq \"%s\" and scope eq \"%s\"", approval.getUserId(), approval.getClientId(), approval.getScope())); } @Override diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/client/ClientDetailsModification.java b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/client/ClientDetailsModification.java index 39296bb1bcb..5966fd68f98 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/client/ClientDetailsModification.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/client/ClientDetailsModification.java @@ -81,6 +81,7 @@ private boolean valid(String action) { || UPDATE.equals(action) || DELETE.equals(action) || UPDATE_SECRET.equals(action) - || SECRET.equals(SECRET)); + || SECRET.equals(action) + || NONE.equals(action)); } } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/user/ExtendedUaaAuthority.java b/common/src/main/java/org/cloudfoundry/identity/uaa/user/ExtendedUaaAuthority.java index c1216e56f2c..12cdb1f1928 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/user/ExtendedUaaAuthority.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/user/ExtendedUaaAuthority.java @@ -13,6 +13,7 @@ package org.cloudfoundry.identity.uaa.user; import java.util.Map; +import java.util.Objects; import org.springframework.security.core.GrantedAuthority; @@ -52,7 +53,7 @@ public boolean equals(Object obj) { return false; ExtendedUaaAuthority e = (ExtendedUaaAuthority) obj; - if (e.equals(authority) && e.additionalInfo.equals(additionalInfo)) { + if (Objects.equals(e.getAuthority(), authority) && e.additionalInfo.equals(additionalInfo)) { return true; } else { From 2b00a478f872abd9d68dc1fd146cb42e22e11c02 Mon Sep 17 00:00:00 2001 From: Jeremy Coffield Date: Tue, 20 Oct 2015 14:09:42 -0700 Subject: [PATCH 073/103] Add test for JdbcApprovalStore.revokeApproval. NONE is an invalid client modification action. Signed-off-by: Jonathan Lo --- .../oauth/client/ClientDetailsModification.java | 3 +-- .../uaa/oauth/approval/JdbcApprovalStoreTests.java | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/client/ClientDetailsModification.java b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/client/ClientDetailsModification.java index 5966fd68f98..0df6ab05855 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/client/ClientDetailsModification.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/client/ClientDetailsModification.java @@ -81,7 +81,6 @@ private boolean valid(String action) { || UPDATE.equals(action) || DELETE.equals(action) || UPDATE_SECRET.equals(action) - || SECRET.equals(action) - || NONE.equals(action)); + || SECRET.equals(action)); } } diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/approval/JdbcApprovalStoreTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/approval/JdbcApprovalStoreTests.java index 66f73bd6f84..f95125bb4a7 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/approval/JdbcApprovalStoreTests.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/approval/JdbcApprovalStoreTests.java @@ -19,6 +19,7 @@ import static org.cloudfoundry.identity.uaa.oauth.approval.Approval.ApprovalStatus.APPROVED; import static org.cloudfoundry.identity.uaa.oauth.approval.Approval.ApprovalStatus.DENIED; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import org.cloudfoundry.identity.uaa.audit.event.ApprovalModifiedEvent; import org.cloudfoundry.identity.uaa.oauth.approval.Approval.ApprovalStatus; @@ -118,6 +119,19 @@ public void canRevokeApprovals() { assertEquals(0, dao.getApprovals("user_id eq \"u1\"").size()); } + @Test + public void canRevokeSingleApproval() { + List approvals = dao.getApprovals("user_id eq \"u1\""); + assertEquals(2, approvals.size()); + + Approval toRevoke = approvals.get(0); + assertTrue(dao.revokeApproval(toRevoke)); + List approvalsAfterRevoke = dao.getApprovals("user_id eq \"u1\""); + + assertEquals(1, approvalsAfterRevoke.size()); + assertFalse(approvalsAfterRevoke.contains(toRevoke)); + } + @Test public void addSameApprovalRepeatedlyUpdatesExpiry() { assertTrue(dao.addApproval(new Approval("u2", "c2", "dash.user", 6000, APPROVED))); From e16fcd01fae4c4b1f4dcfc08acc97994638f4c96 Mon Sep 17 00:00:00 2001 From: Jeremy Coffield Date: Tue, 20 Oct 2015 15:22:42 -0700 Subject: [PATCH 074/103] Fix constructor behavior for ClientDetailsModification from prototype Signed-off-by: Jonathan Lo --- .../identity/uaa/oauth/client/ClientDetailsModification.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/client/ClientDetailsModification.java b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/client/ClientDetailsModification.java index 0df6ab05855..460be9a88bf 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/client/ClientDetailsModification.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/client/ClientDetailsModification.java @@ -43,7 +43,7 @@ public ClientDetailsModification(ClientDetails prototype) { } } if (prototype instanceof ClientDetailsModification) { - this.setAction(((ClientDetailsModification) prototype).getAction()); + this.action = ((ClientDetailsModification) prototype).getAction(); this.setApprovalsDeleted(((ClientDetailsModification) prototype).isApprovalsDeleted()); } } From 250db6c1d6748a98d1dae1eeb986d8f499de2d86 Mon Sep 17 00:00:00 2001 From: Madhura Date: Tue, 20 Oct 2015 16:28:28 -0700 Subject: [PATCH 075/103] Update UAA-Tokens.md --- docs/UAA-Tokens.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/UAA-Tokens.md b/docs/UAA-Tokens.md index 48029c07cdc..ee4bbc77c90 100644 --- a/docs/UAA-Tokens.md +++ b/docs/UAA-Tokens.md @@ -126,7 +126,7 @@ Scopes, are arbitrary strings, defined by the client itself. The UAA does use th as anything before the last dot. ### Client authorities, UAA groups and scopes -In the UAA each client has a list of ```client authorities```. This is ```List;``` of scopes +In the UAA each client has a list of ```client authorities```. This is ```List``` of scopes that represents the permissions the client has by itself. The second field the client has is the ```scopes``` field. The ```client scopes``` represents the permissions that the client uses when acting on behalf of a user. From 6a9ac825aabe5ff20b9286403b3cbeaf5e03b624 Mon Sep 17 00:00:00 2001 From: Madhura Date: Tue, 20 Oct 2015 16:52:11 -0700 Subject: [PATCH 076/103] Revert "Try to obtain the user details origin from x-forwarded-for header" --- .../uaa/authentication/UaaAuthenticationDetails.java | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationDetails.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationDetails.java index 460918dcadc..6b76c3309a0 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationDetails.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationDetails.java @@ -52,14 +52,7 @@ public UaaAuthenticationDetails(HttpServletRequest request) { } public UaaAuthenticationDetails(HttpServletRequest request, String clientId) { WebAuthenticationDetails webAuthenticationDetails = new WebAuthenticationDetails(request); - - String xForwardedFor = request.getHeader("X-Forwarded-For"); - if (xForwardedFor != null) { - this.origin = xForwardedFor; - } - else { - this.origin = webAuthenticationDetails.getRemoteAddress(); - } + this.origin = webAuthenticationDetails.getRemoteAddress(); this.sessionId = webAuthenticationDetails.getSessionId(); if (clientId == null) { From b78a02283f3aeceebfc03b75ff7025e97da75f65 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Wed, 21 Oct 2015 14:46:21 -0700 Subject: [PATCH 077/103] Handle updating attributes from SAML and LDAP for existing users [#105144676] https://www.pivotaltracker.com/story/show/105144676 Signed-off-by: Jonathan Lo --- .../LdapLoginAuthenticationManager.java | 17 ++- .../identity/uaa/user/UaaUser.java | 20 ++++ ...xternalLoginAuthenticationManagerTest.java | 2 + .../LdapLoginAuthenticationManagerTests.java | 105 ++++++++++++------ .../saml/LoginSamlAuthenticationProvider.java | 30 +++-- .../LoginSamlAuthenticationProviderTests.java | 69 +++++++++--- 6 files changed, 182 insertions(+), 61 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java index cc137685d4b..5354e75a197 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java @@ -15,10 +15,15 @@ package org.cloudfoundry.identity.uaa.authentication.manager; +import org.apache.commons.lang.StringUtils; import org.cloudfoundry.identity.uaa.ldap.ExtendedLdapUserDetails; import org.cloudfoundry.identity.uaa.user.UaaUser; +import org.cloudfoundry.identity.uaa.user.UaaUserPrototype; import org.springframework.security.core.Authentication; +import java.util.Collections; +import java.util.Date; + public class LdapLoginAuthenticationManager extends ExternalLoginAuthenticationManager { private boolean autoAddAuthorities = false; @@ -29,8 +34,8 @@ protected UaaUser userAuthenticated(Authentication request, UaaUser user) { //we must check and see if the email address has changed between authentications if (request.getPrincipal() !=null && request.getPrincipal() instanceof ExtendedLdapUserDetails) { UaaUser fromRequest = getUser(request); - if (fromRequest.getEmail()!=null && !fromRequest.getEmail().equals(user.getEmail())) { - user = user.modifyEmail(fromRequest.getEmail()); + if (haveUserAttributesChanged(user, fromRequest)) { + user = user.modifyAttributes(fromRequest.getEmail(), fromRequest.getGivenName(), fromRequest.getFamilyName(), fromRequest.getPhoneNumber()); userModified = true; } } @@ -46,4 +51,12 @@ public boolean isAutoAddAuthorities() { public void setAutoAddAuthorities(boolean autoAddAuthorities) { this.autoAddAuthorities = autoAddAuthorities; } + + private boolean haveUserAttributesChanged(UaaUser existingUser, UaaUser user) { + if (!StringUtils.equals(existingUser.getGivenName(), user.getGivenName()) || !StringUtils.equals(existingUser.getFamilyName(), user.getFamilyName()) || + !StringUtils.equals(existingUser.getPhoneNumber(), user.getPhoneNumber()) || !StringUtils.equals(existingUser.getEmail(), user.getEmail())) { + return true; + } + return false; + } } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/user/UaaUser.java b/common/src/main/java/org/cloudfoundry/identity/uaa/user/UaaUser.java index 997754a1930..d884cdf6f7d 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/user/UaaUser.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/user/UaaUser.java @@ -215,6 +215,26 @@ public UaaUser modifyId(String id) { return new UaaUser(id, username, password, email, authorities, givenName, familyName, created, modified, origin, externalId, verified, zoneId, salt, passwordLastModified); } + public UaaUser modifyAttributes(String email, String givenName, String familyName, String phoneNumber) { + return new UaaUser(new UaaUserPrototype() + .withEmail(email) + .withGivenName(givenName) + .withFamilyName(familyName) + .withPhoneNumber(phoneNumber) + .withModified(modified) + .withId(id) + .withUsername(username) + .withPassword(password) + .withAuthorities(authorities) + .withCreated(created) + .withOrigin(origin) + .withExternalId(externalId) + .withVerified(verified) + .withZoneId(zoneId) + .withSalt(salt) + .withPasswordLastModified(passwordLastModified)); + } + public boolean isVerified() { return verified; } diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManagerTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManagerTest.java index da1a19e2edc..56117b9a0d1 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManagerTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManagerTest.java @@ -282,6 +282,8 @@ public void testAuthenticateCreateUserWithLdapUserDetailsPrincipal() throws Exce manager.setOrigin(origin); when(user.getEmail()).thenReturn(email); when(user.getOrigin()).thenReturn(origin); + when(user.getGivenName()).thenReturn("joe"); + when(user.getFamilyName()).thenReturn("test.org"); when(uaaUserDatabase.retrieveUserByName(eq(userName),eq(origin))) .thenReturn(null) .thenReturn(user); diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java index 86324e0d4c0..4eec6427f30 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java @@ -19,9 +19,11 @@ import org.cloudfoundry.identity.uaa.user.UaaAuthority; import org.cloudfoundry.identity.uaa.user.UaaUser; import org.cloudfoundry.identity.uaa.user.UaaUserDatabase; +import org.cloudfoundry.identity.uaa.user.UaaUserPrototype; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Matchers; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; @@ -35,6 +37,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyObject; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -51,9 +55,10 @@ public class LdapLoginAuthenticationManagerTests { private final String GIVEN_NAME_ATTRIBUTE = "firstname"; private final String FAMILY_NAME_ATTRIBUTE = "surname"; private final String PHONE_NUMBER_ATTTRIBUTE = "digits"; + private static LdapUserDetails userDetails; private static LdapUserDetails mockLdapUserDetails() { - LdapUserDetails userDetails = mock(LdapUserDetails.class); + userDetails = mock(LdapUserDetails.class); setupGeneralExpectations(userDetails); when(userDetails.getDn()).thenReturn(DN); return userDetails; @@ -90,26 +95,14 @@ public void setUp() { publisher = mock(ApplicationEventPublisher.class); am.setApplicationEventPublisher(publisher); am.setOrigin(origin); - info = new HashMap<>(); - String[] emails = {LDAP_EMAIL}; - String[] given_names = {"Marissa"}; - String[] family_names = {"Bloggs"}; - String[] phone_numbers = {"8675309"}; - info.put(EMAIL_ATTRIBUTE, emails); - info.put(GIVEN_NAME_ATTRIBUTE, given_names); - info.put(FAMILY_NAME_ATTRIBUTE, family_names); - info.put(PHONE_NUMBER_ATTTRIBUTE, phone_numbers); + authUserDetail = getAuthDetails(LDAP_EMAIL, "Marissa", "Bloggs", "8675309"); + auth = mock(Authentication.class); + when(auth.getPrincipal()).thenReturn(authUserDetail); + UaaUserDatabase db = mock(UaaUserDatabase.class); when(db.retrieveUserById(anyString())).thenReturn(dbUser); am.setUserDatabase(db); - auth = mock(Authentication.class); when(auth.getAuthorities()).thenReturn(null); - authUserDetail = new ExtendedLdapUserImpl(mockLdapUserDetails(), info); - authUserDetail.setMailAttributeName(EMAIL_ATTRIBUTE); - authUserDetail.setGivenNameAttributeName(GIVEN_NAME_ATTRIBUTE); - authUserDetail.setFamilyNameAttributeName(FAMILY_NAME_ATTRIBUTE); - authUserDetail.setPhoneNumberAttributeName(PHONE_NUMBER_ATTTRIBUTE); - when(auth.getPrincipal()).thenReturn(authUserDetail); } @Test @@ -145,22 +138,68 @@ public void testUserAuthenticated() throws Exception { verify(publisher, times(2)).publishEvent(Matchers.anyObject()); } + @Test + public void update_existingUser_if_attributes_different() throws Exception { + ExtendedLdapUserImpl authDetails = getAuthDetails(LDAP_EMAIL, "MarissaChanged", "BloggsChanged", "8675309"); + when(auth.getPrincipal()).thenReturn(authDetails); + + UaaUser user = getUaaUser(); + am.userAuthenticated(auth, user); + ArgumentCaptor captor = ArgumentCaptor.forClass(ExternalGroupAuthorizationEvent.class); + verify(publisher, times(1)).publishEvent(captor.capture()); + + assertEquals(LDAP_EMAIL, captor.getValue().getUser().getEmail()); + assertEquals("MarissaChanged", captor.getValue().getUser().getGivenName()); + assertEquals("BloggsChanged", captor.getValue().getUser().getFamilyName()); + } + + @Test + public void dontUpdate_existingUser_if_attributes_same() throws Exception { + UaaUser user = getUaaUser(); + ExtendedLdapUserImpl authDetails = getAuthDetails(user.getEmail(), user.getGivenName(), user.getFamilyName(), user.getPhoneNumber()); + when(auth.getPrincipal()).thenReturn(authDetails); + + am.userAuthenticated(auth, user); + ArgumentCaptor captor = ArgumentCaptor.forClass(ExternalGroupAuthorizationEvent.class); + verify(publisher, times(1)).publishEvent(captor.capture()); + + assertEquals(user.getModified(), captor.getValue().getUser().getModified()); + } + + private ExtendedLdapUserImpl getAuthDetails(String email, String givenName, String familyName, String phoneNumber) { + String[] emails = {email}; + String[] given_names = {givenName}; + String[] family_names = {familyName}; + String[] phone_numbers = {phoneNumber}; + info.put(EMAIL_ATTRIBUTE, emails); + info.put(GIVEN_NAME_ATTRIBUTE, given_names); + info.put(FAMILY_NAME_ATTRIBUTE, family_names); + info.put(PHONE_NUMBER_ATTTRIBUTE, phone_numbers); + authUserDetail = new ExtendedLdapUserImpl(mockLdapUserDetails(), info); + authUserDetail.setMailAttributeName(EMAIL_ATTRIBUTE); + authUserDetail.setGivenNameAttributeName(GIVEN_NAME_ATTRIBUTE); + authUserDetail.setFamilyNameAttributeName(FAMILY_NAME_ATTRIBUTE); + authUserDetail.setPhoneNumberAttributeName(PHONE_NUMBER_ATTTRIBUTE); + return authUserDetail; + } + protected UaaUser getUaaUser() { - return new UaaUser( - "id", - USERNAME, - "password", - TEST_EMAIL, - UaaAuthority.USER_AUTHORITIES, - "givenname", - "familyname", - new Date(), - new Date(), - Origin.ORIGIN, - DN, - false, - IdentityZoneHolder.get().getId(), - null, - null); + return new UaaUser(new UaaUserPrototype() + .withId("id") + .withUsername(USERNAME) + .withPassword("password") + .withEmail(TEST_EMAIL) + .withAuthorities(UaaAuthority.USER_AUTHORITIES) + .withGivenName("givenname") + .withFamilyName("familyname") + .withPhoneNumber("8675309") + .withCreated(new Date()) + .withModified(new Date()) + .withOrigin(Origin.ORIGIN) + .withExternalId(DN) + .withVerified(false) + .withZoneId(IdentityZoneHolder.get().getId()) + .withSalt(null) + .withPasswordLastModified(null)); } } \ No newline at end of file diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java b/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java index c2169d52ba2..dc4df0d821c 100644 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java @@ -14,6 +14,7 @@ import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang.StringUtils; import org.cloudfoundry.identity.uaa.authentication.Origin; import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; @@ -208,6 +209,7 @@ protected UaaUser createIfMissing(UaaPrincipal samlPrincipal, boolean addNew, Co boolean userModified = false; UaaPrincipal uaaPrincipal = samlPrincipal; UaaUser user; + UaaUser userWithSamlAttributes = getUser(uaaPrincipal, userAttributes); try { user = userDatabase.retrieveUserByName(uaaPrincipal.getName(), uaaPrincipal.getOrigin()); } catch (UsernameNotFoundException e) { @@ -216,17 +218,21 @@ protected UaaUser createIfMissing(UaaPrincipal samlPrincipal, boolean addNew, Co + "You can correct this by creating a shadow user for the SAML user.", e); } // Register new users automatically - publish(new NewUserAuthenticatedEvent(getUser(uaaPrincipal, userAttributes))); + publish(new NewUserAuthenticatedEvent(userWithSamlAttributes)); try { user = userDatabase.retrieveUserByName(uaaPrincipal.getName(), uaaPrincipal.getOrigin()); } catch (UsernameNotFoundException ex) { throw new BadCredentialsException("Unable to establish shadow user for SAML user:"+ uaaPrincipal.getName()); } } + if (haveUserAttributesChanged(user, userWithSamlAttributes)) { + userModified = true; + user = user.modifyAttributes(userWithSamlAttributes.getEmail(), userWithSamlAttributes.getGivenName(), userWithSamlAttributes.getFamilyName(), userWithSamlAttributes.getPhoneNumber()); + } publish( new ExternalGroupAuthorizationEvent( user, - true, + userModified, authorities, true ) @@ -238,7 +244,7 @@ protected UaaUser createIfMissing(UaaPrincipal samlPrincipal, boolean addNew, Co return user; } - protected UaaUser getUser(UaaPrincipal principal, Map userAttributes) { + private UaaUser getUser(UaaPrincipal principal, Map userAttributes) { String name = principal.getName(); String email = userAttributes.get(EMAIL_ATTRIBUTE_NAME); String givenName = userAttributes.get(GIVEN_NAME_ATTRIBUTE_NAME); @@ -275,22 +281,30 @@ protected UaaUser getUser(UaaPrincipal principal, Map userAttribu familyName = email.split("@")[1]; } return new UaaUser( - new UaaUserPrototype().withId(userId) - .withUsername(name) - .withPassword("") /*zero length password for login server */ + new UaaUserPrototype() .withEmail(email) - .withAuthorities(Collections.EMPTY_LIST) .withGivenName(givenName) .withFamilyName(familyName) .withPhoneNumber(phoneNumber) - .withCreated(new Date()) .withModified(new Date()) + .withId(userId) + .withUsername(name) + .withPassword("") + .withAuthorities(Collections.EMPTY_LIST) + .withCreated(new Date()) .withOrigin(origin) .withExternalId(name) .withVerified(false) .withZoneId(zoneId) .withSalt(null) .withPasswordLastModified(null)); + } + private boolean haveUserAttributesChanged(UaaUser existingUser, UaaUser user) { + if (!StringUtils.equals(existingUser.getGivenName(), user.getGivenName()) || !StringUtils.equals(existingUser.getFamilyName(), user.getFamilyName()) || + !StringUtils.equals(existingUser.getPhoneNumber(), user.getPhoneNumber()) || !StringUtils.equals(existingUser.getEmail(), user.getEmail())) { + return true; + } + return false; } } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java index e32a39e6399..51ba8ddb3c2 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java @@ -63,7 +63,6 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Objects; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -71,7 +70,6 @@ import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.anyObject; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class LoginSamlAuthenticationProviderTests extends JdbcTestBase { @@ -141,23 +139,8 @@ public void configureProvider() throws Exception { externalManager.mapExternalGroup(uaaSamlAdmin.getId(), SAML_ADMIN, Origin.SAML); externalManager.mapExternalGroup(uaaSamlTest.getId(), SAML_TEST, Origin.SAML); - String username = "marissa-saml"; - NameID usernameID = mock(NameID.class); - when(usernameID.getValue()).thenReturn(username); consumer = mock(WebSSOProfileConsumer.class); - Map attributes = new HashMap<>(); - attributes.put("firstName", "Marissa"); - attributes.put("lastName", "Bloggs"); - attributes.put("emailAddress", "marissa.bloggs@test.com"); - attributes.put("phone", "1234567890"); - attributes.put("groups", Arrays.asList(SAML_USER,SAML_ADMIN,SAML_NOT_MAPPED)); - attributes.put("2ndgroups", Arrays.asList(SAML_TEST)); - credential = new SAMLCredential( - usernameID, - mock(Assertion.class), - "remoteEntityID", - getAttributes(attributes), - "localEntityID"); + credential = getUserCredential("marissa-saml", "Marissa", "Bloggs", "marissa.bloggs@test.com", "1234567890"); when(consumer.processAuthenticationResponse(anyObject())).thenReturn(credential); userDatabase = new JdbcUaaUserDatabase(jdbcTemplate); @@ -186,6 +169,25 @@ public void configureProvider() throws Exception { provider = providerProvisioning.create(provider); } + private SAMLCredential getUserCredential(String username, String firstName, String lastName, String emailAddress, String phoneNumber) { + NameID usernameID = mock(NameID.class); + when(usernameID.getValue()).thenReturn(username); + + Map attributes = new HashMap<>(); + attributes.put("firstName", firstName); + attributes.put("lastName", lastName); + attributes.put("emailAddress", emailAddress); + attributes.put("phone", phoneNumber); + attributes.put("groups", Arrays.asList(SAML_USER, SAML_ADMIN, SAML_NOT_MAPPED)); + attributes.put("2ndgroups", Arrays.asList(SAML_TEST)); + return new SAMLCredential( + usernameID, + mock(Assertion.class), + "remoteEntityID", + getAttributes(attributes), + "localEntityID"); + } + @Test public void testAuthenticateSimple() { authprovider.authenticate(mockSamlAuthentication(Origin.SAML)); @@ -268,6 +270,37 @@ public void add_external_groups_to_authentication_with_whitelist() throws Except assertEquals(Collections.singleton(SAML_ADMIN), authentication.getExternalGroups()); } + @Test + public void update_existingUser_if_attributes_different() throws Exception { + getAuthentication(); + + Map attributeMappings = new HashMap<>(); + attributeMappings.put("given_name", "firstName"); + attributeMappings.put("email", "emailAddress"); + providerDefinition.setAttributeMappings(attributeMappings); + provider.setConfig(JsonUtils.writeValueAsString(providerDefinition)); + providerProvisioning.update(provider); + + SAMLCredential credential = getUserCredential("marissa-saml", "Marissa-changed", null, "marissa.bloggs@change.org", null); + when(consumer.processAuthenticationResponse(anyObject())).thenReturn(credential); + getAuthentication(); + + UaaUser user = userDatabase.retrieveUserByName("marissa-saml", Origin.SAML); + assertEquals("Marissa-changed", user.getGivenName()); + assertEquals("marissa.bloggs@change.org", user.getEmail()); + } + + @Test + public void dont_update_existingUser_if_attributes_areTheSame() throws Exception { + getAuthentication(); + UaaUser user = userDatabase.retrieveUserByName("marissa-saml", Origin.SAML); + + getAuthentication(); + UaaUser existingUser = userDatabase.retrieveUserByName("marissa-saml", Origin.SAML); + + assertEquals(existingUser.getModified(), user.getModified()); + } + @Test public void shadowAccount_createdWith_MappedUserAttributes() throws Exception { Map attributeMappings = new HashMap<>(); From 1590941b05b252ef9e17e0aa3099573da66d3186 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Wed, 21 Oct 2015 10:18:39 -0600 Subject: [PATCH 078/103] There should be no session present when /oauth/token is invoked with a passcode. --- .../identity/uaa/login/PasscodeMockMvcTests.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/PasscodeMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/PasscodeMockMvcTests.java index 37683d79dd9..ad5f87dc497 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/PasscodeMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/PasscodeMockMvcTests.java @@ -127,8 +127,7 @@ public void testLoginUsingPasscodeWithSamlToken() throws Exception { .header("Authorization", basicDigestHeaderValue) .param("grant_type", "password") .param("passcode", passcode) - .param("response_type", "token") - .session(session); + .param("response_type", "token"); Map accessToken = @@ -192,8 +191,7 @@ public void testLoginUsingPasscodeWithUaaToken() throws Exception { .header("Authorization", basicDigestHeaderValue) .param("grant_type", "password") .param("passcode", passcode) - .param("response_type", "token") - .session(session); + .param("response_type", "token"); Map accessToken = From 8933f8e405ec293eb5841a25880bf9b33f4be06c Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Wed, 21 Oct 2015 21:15:12 -0600 Subject: [PATCH 079/103] Implement ability to map custom SAML user attributes to id token attributes https://www.pivotaltracker.com/story/show/106030472 [#106030472] Proper Serialization/Deserialization of UaaAuthentication object https://www.pivotaltracker.com/story/show/105964572 [#105964572] --- .../ExternalIdentityProviderDefinition.java | 1 + .../uaa/authentication/UaaAuthentication.java | 49 ++++-- .../UaaAuthenticationDeserializer.java | 82 +++++++++ .../UaaAuthenticationDetails.java | 37 ++-- .../UaaAuthenticationJsonBase.java | 42 +++++ .../UaaAuthenticationSerializer.java | 38 ++++ .../saml/LoginSamlAuthenticationToken.java | 19 +- .../identity/uaa/oauth/Claims.java | 1 + .../uaa/oauth/token/UaaTokenServices.java | 26 ++- .../uaa/oauth/token/UaaTokenStore.java | 14 +- .../identity/uaa/util/UaaStringUtils.java | 20 ++- .../UaaAuthenticationSerializationTests.java | 118 +++++++++++++ .../oauth/token/UaaTokenServicesTests.java | 14 +- .../uaa/oauth/token/UaaTokenStoreTests.java | 43 +++++ .../saml/LoginSamlAuthenticationProvider.java | 43 +++-- uaa/src/main/resources/uaa.yml | 1 + ...IdentityZoneEndpointsIntegrationTests.java | 41 ++--- .../ScimGroupEndpointsIntegrationTests.java | 3 +- .../ScimUserEndpointsIntegrationTests.java | 4 +- .../feature/OpenIdTokenGrantsIT.java | 2 +- .../uaa/integration/feature/SamlLoginIT.java | 164 ++++++++++++++++-- .../util/IntegrationTestUtils.java | 155 ++++++++++++----- .../LoginSamlAuthenticationProviderTests.java | 63 ++++++- .../uaa/mock/ldap/LdapMockMvcTests.java | 3 +- .../CheckDefaultAuthoritiesMvcMockTests.java | 5 +- .../uaa/mock/token/TokenMvcMockTests.java | 5 +- 26 files changed, 811 insertions(+), 182 deletions(-) create mode 100644 common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationDeserializer.java create mode 100644 common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationJsonBase.java create mode 100644 common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationSerializer.java diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/ExternalIdentityProviderDefinition.java b/common/src/main/java/org/cloudfoundry/identity/uaa/ExternalIdentityProviderDefinition.java index 84cf8792780..2a9bd72a5ed 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/ExternalIdentityProviderDefinition.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/ExternalIdentityProviderDefinition.java @@ -26,6 +26,7 @@ public class ExternalIdentityProviderDefinition extends AbstractIdentityProvider public static final String GIVEN_NAME_ATTRIBUTE_NAME = "given_name"; //can be a string public static final String FAMILY_NAME_ATTRIBUTE_NAME = "family_name"; //can be a string public static final String PHONE_NUMBER_ATTRIBUTE_NAME = "phone_number"; //can be a string + public static final String USER_ATTRIBUTE_PREFIX = "user.attribute."; public static final String EXTERNAL_GROUPS_WHITELIST = "externalGroupsWhitelist"; public static final String ATTRIBUTE_MAPPINGS = "attributeMappings"; diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java index 71ea7a6eeae..e638f2fa9f1 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java @@ -12,21 +12,29 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.authentication; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import java.io.Serializable; import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; +import static java.util.Collections.EMPTY_MAP; + /** * Authentication token which represents a user. */ +@JsonSerialize(using = UaaAuthenticationSerializer.class) +@JsonDeserialize(using = UaaAuthenticationDeserializer.class) public class UaaAuthentication implements Authentication, Serializable { + private List authorities; private Object credentials; private UaaPrincipal principal; @@ -35,6 +43,7 @@ public class UaaAuthentication implements Authentication, Serializable { private long authenticatedTime = -1l; private long expiresAt = -1l; private Set externalGroups; + private Map> userAttributes; /** * Creates a token with the supplied array of authorities. @@ -57,14 +66,13 @@ public UaaAuthentication(UaaPrincipal principal, this(principal, credentials, authorities, details, authenticated, authenticatedTime, -1); } - @JsonCreator - public UaaAuthentication(@JsonProperty("principal") UaaPrincipal principal, - @JsonProperty("credentials") Object credentials, - @JsonProperty("authorities") List authorities, - @JsonProperty("details") UaaAuthenticationDetails details, - @JsonProperty("authenticated") boolean authenticated, - @JsonProperty(value = "authenticatedTime", defaultValue = "-1") long authenticatedTime, - @JsonProperty(value = "expiresAt", defaultValue = "-1") long expiresAt) { + public UaaAuthentication(UaaPrincipal principal, + Object credentials, + List authorities, + UaaAuthenticationDetails details, + boolean authenticated, + long authenticatedTime, + long expiresAt) { if (principal == null || authorities == null) { throw new IllegalArgumentException("principal and authorities must not be null"); } @@ -81,12 +89,14 @@ public UaaAuthentication(UaaPrincipal uaaPrincipal, Object credentials, List uaaAuthorityList, Set externalGroups, + Map> userAttributes, UaaAuthenticationDetails details, boolean authenticated, long authenticatedTime, long expiresAt) { this(uaaPrincipal, credentials, uaaAuthorityList, details, authenticated, authenticatedTime, expiresAt); this.externalGroups = externalGroups; + this.userAttributes = new HashMap<>(userAttributes); } public long getAuthenticatedTime() { @@ -94,7 +104,6 @@ public long getAuthenticatedTime() { } @Override - @JsonIgnore public String getName() { // Should we return the ID for the principal name? (No, because the // UaaUserDatabase retrieves users by name.) @@ -170,4 +179,20 @@ public Set getExternalGroups() { public void setExternalGroups(Set externalGroups) { this.externalGroups = externalGroups; } + + public MultiValueMap getUserAttributes() { + return new LinkedMultiValueMap<>(userAttributes!=null?userAttributes: EMPTY_MAP); + } + + public Map> getUserAttributesAsMap() { + return userAttributes!=null ? new HashMap<>(userAttributes) : EMPTY_MAP; + } + + public void setUserAttributes(MultiValueMap userAttributes) { + this.userAttributes = new HashMap<>(); + for (Map.Entry> entry : userAttributes.entrySet()) { + this.userAttributes.put(entry.getKey(), entry.getValue()); + } + } + } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationDeserializer.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationDeserializer.java new file mode 100644 index 00000000000..d5fc9442b1d --- /dev/null +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationDeserializer.java @@ -0,0 +1,82 @@ +/* + * ***************************************************************************** + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + * ***************************************************************************** + */ +package org.cloudfoundry.identity.uaa.authentication; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import org.springframework.security.core.GrantedAuthority; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static java.util.Collections.EMPTY_LIST; +import static java.util.Collections.EMPTY_MAP; +import static java.util.Collections.EMPTY_SET; + +public class UaaAuthenticationDeserializer extends JsonDeserializer implements UaaAuthenticationJsonBase { + @Override + public UaaAuthentication deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + UaaAuthenticationDetails details = null; + UaaPrincipal princpal = null; + List authorities = EMPTY_LIST; + Set externalGroups = EMPTY_SET; + long expiresAt = -1; + long authenticatedTime = -1; + boolean authenticated = false; + Map> userAttributes = EMPTY_MAP; + while (jp.nextToken() != JsonToken.END_OBJECT) { + if (jp.getCurrentToken() == JsonToken.FIELD_NAME) { + String fieldName = jp.getCurrentName(); + jp.nextToken(); + if (NULL_STRING.equals(jp.getText())) { + //do nothing + } else if (DETAILS.equals(fieldName)) { + details = jp.readValueAs(UaaAuthenticationDetails.class); + } else if (PRINCIPAL.equals(fieldName)) { + princpal = jp.readValueAs(UaaPrincipal.class); + } else if (AUTHORITIES.equals(fieldName)) { + authorities = deserializeAuthorites(jp.readValueAs(new TypeReference>(){})); + } else if (EXTERNAL_GROUPS.equals(fieldName)) { + externalGroups = jp.readValueAs(new TypeReference>(){}); + } else if (EXPIRES_AT.equals(fieldName)) { + expiresAt = jp.getLongValue(); + } else if (AUTH_TIME.equals(fieldName)) { + authenticatedTime = jp.getLongValue(); + } else if (AUTHENTICATED.equals(fieldName)) { + authenticated = jp.getBooleanValue(); + } else if (USER_ATTRIBUTES.equals(fieldName)) { + userAttributes = jp.readValueAs(new TypeReference>>() {}); + } + } + } + if (princpal==null) { + throw new JsonMappingException("Missing "+UaaPrincipal.class.getName()); + } + return new UaaAuthentication(princpal, + null, + authorities, + externalGroups, + userAttributes, + details, + authenticated, + authenticatedTime, + expiresAt); + } +} diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationDetails.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationDetails.java index 6b76c3309a0..eb56d043269 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationDetails.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationDetails.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Cloud Foundry + * Cloud Foundry * Copyright (c) [2009-2014] Pivotal Software, Inc. All Rights Reserved. * * This product is licensed to you under the Apache License, Version 2.0 (the "License"). @@ -12,18 +12,16 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.authentication; -import java.io.Serializable; -import java.util.HashMap; -import java.util.Map; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.security.web.authentication.WebAuthenticationDetails; import javax.servlet.http.HttpServletRequest; - -import org.springframework.security.web.authentication.WebAuthenticationDetails; +import java.io.Serializable; /** * Contains additional information about the authentication request which may be * of use in auditing etc. - * + * * @author Luke Taylor * @author Dave Syer */ @@ -63,6 +61,16 @@ public UaaAuthenticationDetails(HttpServletRequest request, String clientId) { this.addNew = Boolean.parseBoolean(request.getParameter(ADD_NEW)); } + public UaaAuthenticationDetails(@JsonProperty("addNew") boolean addNew, + @JsonProperty("clientId") String clientId, + @JsonProperty("origin") String origin, + @JsonProperty("sessionId") String sessionId) { + this.addNew = addNew; + this.clientId = clientId; + this.origin = origin; + this.sessionId = sessionId; + } + public String getOrigin() { return origin; } @@ -75,6 +83,14 @@ public String getClientId() { return clientId; } + public boolean isAddNew() { + return addNew; + } + + public void setAddNew(boolean addNew) { + this.addNew = addNew; + } + @Override public String toString() { StringBuilder sb = new StringBuilder(); @@ -136,11 +152,4 @@ else if (!sessionId.equals(other.sessionId)) return true; } - public boolean isAddNew() { - return addNew; - } - - public void setAddNew(boolean addNew) { - this.addNew = addNew; - } } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationJsonBase.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationJsonBase.java new file mode 100644 index 00000000000..4d4dafefde2 --- /dev/null +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationJsonBase.java @@ -0,0 +1,42 @@ +/* + * ***************************************************************************** + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + * ***************************************************************************** + */ +package org.cloudfoundry.identity.uaa.authentication; + +import org.cloudfoundry.identity.uaa.util.UaaStringUtils; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +public interface UaaAuthenticationJsonBase { + String DETAILS = "details"; + String PRINCIPAL = "principal"; + String AUTHORITIES = "authorities"; + String EXTERNAL_GROUPS = "externalGroups"; + String EXPIRES_AT = "expiresAt"; + String AUTH_TIME = "authenticatedTime"; + String AUTHENTICATED = "authenticated"; + String USER_ATTRIBUTES = "userAttributes"; + String NULL_STRING = "null"; + + default Set serializeAuthorites(Collection authorities) { + return UaaStringUtils.getStringsFromAuthorities(authorities); + } + + default List deserializeAuthorites(Collection authorities) { + return UaaStringUtils.getAuthoritiesFromStrings(authorities); + } + +} diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationSerializer.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationSerializer.java new file mode 100644 index 00000000000..3036e244fb2 --- /dev/null +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationSerializer.java @@ -0,0 +1,38 @@ +/* + * ***************************************************************************** + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + * ***************************************************************************** + */ +package org.cloudfoundry.identity.uaa.authentication; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; + +public class UaaAuthenticationSerializer extends JsonSerializer implements UaaAuthenticationJsonBase { + @Override + public void serialize(UaaAuthentication value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + if (value.getDetails() instanceof UaaAuthenticationDetails) { + gen.writeObjectField(DETAILS, value.getDetails()); + } + gen.writeObjectField(PRINCIPAL, value.getPrincipal()); + gen.writeObjectField(AUTHORITIES, serializeAuthorites(value.getAuthorities())); + gen.writeObjectField(EXTERNAL_GROUPS, value.getExternalGroups()); + gen.writeNumberField(EXPIRES_AT, value.getExpiresAt()); + gen.writeNumberField(AUTH_TIME, value.getAuthenticatedTime()); + gen.writeBooleanField(AUTHENTICATED, value.isAuthenticated()); + gen.writeObjectField(USER_ATTRIBUTES, value.getUserAttributesAsMap()); + gen.writeEndObject(); + } +} diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationToken.java b/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationToken.java index 3f053e07b6a..781afd920e8 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationToken.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationToken.java @@ -16,11 +16,15 @@ import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.providers.ExpiringUsernameAuthenticationToken; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; -import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Set; +import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.USER_ATTRIBUTE_PREFIX; + public class LoginSamlAuthenticationToken extends ExpiringUsernameAuthenticationToken { @@ -36,7 +40,16 @@ public UaaPrincipal getUaaPrincipal() { return uaaPrincipal; } - public UaaAuthentication getUaaAuthentication(List uaaAuthorityList, Set externalGroups) { - return new UaaAuthentication(getUaaPrincipal(), getCredentials(), uaaAuthorityList, externalGroups, null, isAuthenticated(), System.currentTimeMillis(), getTokenExpiration()==null ? -1l : getTokenExpiration().getTime()); + public UaaAuthentication getUaaAuthentication(List uaaAuthorityList, + Set externalGroups, + MultiValueMap userAttributes) { + LinkedMultiValueMap customAttributes = new LinkedMultiValueMap<>(); + for (Map.Entry> entry : userAttributes.entrySet()) { + if (entry.getKey().startsWith(USER_ATTRIBUTE_PREFIX)) { + customAttributes.put(entry.getKey().substring(USER_ATTRIBUTE_PREFIX.length()), entry.getValue()); + } + } + UaaAuthentication authentication = new UaaAuthentication(getUaaPrincipal(), getCredentials(), uaaAuthorityList, externalGroups, customAttributes, null, isAuthenticated(), System.currentTimeMillis(), getTokenExpiration()==null ? -1l : getTokenExpiration().getTime()); + return authentication; } } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/Claims.java b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/Claims.java index f499f7bd9a2..2946d291422 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/Claims.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/Claims.java @@ -50,4 +50,5 @@ public class Claims { public static final String ORIGIN = "origin"; public static final String ROLES = "roles"; public static final String PROFILE = "profile"; + public static final String USER_ATTRIBUTES = "user_attributes"; } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServices.java b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServices.java index 58fd96f0d49..5210b0f44b6 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServices.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServices.java @@ -105,6 +105,7 @@ import static org.cloudfoundry.identity.uaa.oauth.Claims.ROLES; import static org.cloudfoundry.identity.uaa.oauth.Claims.SCOPE; import static org.cloudfoundry.identity.uaa.oauth.Claims.SUB; +import static org.cloudfoundry.identity.uaa.oauth.Claims.USER_ATTRIBUTES; import static org.cloudfoundry.identity.uaa.oauth.Claims.USER_ID; import static org.cloudfoundry.identity.uaa.oauth.Claims.USER_NAME; import static org.cloudfoundry.identity.uaa.oauth.Claims.ZONE_ID; @@ -260,7 +261,8 @@ public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenReque new HashSet<>(), revocableHashSignature, false, - null); //TODO populate response types + null, //TODO populate response types + null); return accessToken; } @@ -321,7 +323,8 @@ private OAuth2AccessToken createAccessToken(String userId, Set responseTypes, String revocableHashSignature, boolean forceIdTokenCreation, - Set externalGroupsForIdToken) throws AuthenticationException { + Set externalGroupsForIdToken, + Map> userAttributesForIdToken) throws AuthenticationException { String tokenId = UUID.randomUUID().toString(); OpenIdToken accessToken = new OpenIdToken(tokenId); if (validitySeconds > 0) { @@ -368,7 +371,7 @@ private OAuth2AccessToken createAccessToken(String userId, String token = JwtHelper.encode(content, signerProvider.getSigner()).getEncoded(); // This setter copies the value and returns. Don't change. accessToken.setValue(token); - populateIdToken(accessToken, jwtAccessToken, requestedScopes, responseTypes, clientId, forceIdTokenCreation, externalGroupsForIdToken, user); + populateIdToken(accessToken, jwtAccessToken, requestedScopes, responseTypes, clientId, forceIdTokenCreation, externalGroupsForIdToken, user, userAttributesForIdToken); publish(new TokenIssuedEvent(accessToken, SecurityContextHolder.getContext().getAuthentication())); return accessToken; @@ -381,7 +384,8 @@ private void populateIdToken(OpenIdToken token, String aud, boolean forceIdTokenCreation, Set externalGroupsForIdToken, - UaaUser user) { + UaaUser user, + Map> userAttributesForIdToken) { if (forceIdTokenCreation || (scopes.contains("openid") && responseTypes.contains(OpenIdToken.ID_TOKEN))) { try { Map clone = new HashMap<>(accessTokenValues); @@ -399,6 +403,10 @@ private void populateIdToken(OpenIdToken token, clone.put(ROLES, externalGroupsForIdToken); } + if (scopes.contains(USER_ATTRIBUTES) && userAttributesForIdToken!=null ) { + clone.put(USER_ATTRIBUTES, userAttributesForIdToken); + } + if(scopes.contains(PROFILE) && user != null) { String givenName = user.getGivenName(); if(givenName != null) clone.put(GIVEN_NAME, givenName); @@ -528,9 +536,11 @@ public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) modifiableUserScopes.addAll(OAuth2Utils.parseParameterList(externalScopes)); } - Set externalGroupsForIdToken = new HashSet<>(); + Set externalGroupsForIdToken = Collections.EMPTY_SET; + Map> userAttributesForIdToken = Collections.EMPTY_MAP; if (authentication.getUserAuthentication() instanceof UaaAuthentication) { externalGroupsForIdToken = ((UaaAuthentication)authentication.getUserAuthentication()).getExternalGroups(); + userAttributesForIdToken = ((UaaAuthentication)authentication.getUserAuthentication()).getUserAttributes(); } String nonce = authentication.getOAuth2Request().getRequestParameters().get(NONCE); @@ -566,7 +576,8 @@ public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) responseTypes, revocableHashSignature, wasIdTokenRequestedThroughAuthCodeScopeParameter, - externalGroupsForIdToken); + externalGroupsForIdToken, + userAttributesForIdToken); return accessToken; } @@ -933,8 +944,7 @@ private Map getClaimsForToken(String token) { Map claims = null; try { - claims = JsonUtils.readValue(tokenJwt.getClaims(), new TypeReference>() { - }); + claims = JsonUtils.readValue(tokenJwt.getClaims(), new TypeReference>() {}); } catch (JsonUtils.JsonUtilException e) { throw new IllegalStateException("Cannot read token claims", e); } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenStore.java b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenStore.java index 95e8d8019e2..f12ab4c8004 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenStore.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenStore.java @@ -53,6 +53,7 @@ public class UaaTokenStore implements AuthorizationCodeServices { public static final long EXPIRATION_TIME = 5*60*1000; public static final long LEGACY_CODE_EXPIRATION_TIME = 3*24*60*60*1000; + public static final String USER_AUTHENTICATION_UAA_AUTHENTICATION = "userAuthentication.uaaAuthentication"; public static final String USER_AUTHENTICATION_UAA_PRINCIPAL = "userAuthentication.uaaPrincipal"; public static final String USER_AUTHENTICATION_AUTHORITIES = "userAuthentication.authorities"; public static final String OAUTH2_REQUEST_PARAMETERS = "oauth2Request.requestParameters"; @@ -148,8 +149,12 @@ protected byte[] serializeOauth2Authentication(OAuth2Authentication auth2Authent Authentication userAuthentication = auth2Authentication.getUserAuthentication(); HashMap data = new HashMap<>(); if (userAuthentication!=null) { - data.put(USER_AUTHENTICATION_UAA_PRINCIPAL,JsonUtils.writeValueAsString(userAuthentication.getPrincipal())); - data.put(USER_AUTHENTICATION_AUTHORITIES,UaaStringUtils.getStringsFromAuthorities(userAuthentication.getAuthorities())); + if (userAuthentication instanceof UaaAuthentication) { + data.put(USER_AUTHENTICATION_UAA_AUTHENTICATION, JsonUtils.writeValueAsString(userAuthentication)); + } else { + data.put(USER_AUTHENTICATION_UAA_PRINCIPAL, JsonUtils.writeValueAsString(userAuthentication.getPrincipal())); + data.put(USER_AUTHENTICATION_AUTHORITIES, UaaStringUtils.getStringsFromAuthorities(userAuthentication.getAuthorities())); + } } data.put(OAUTH2_REQUEST_PARAMETERS, auth2Authentication.getOAuth2Request().getRequestParameters()); data.put(OAUTH2_REQUEST_CLIENT_ID, auth2Authentication.getOAuth2Request().getClientId()); @@ -171,7 +176,10 @@ protected byte[] serializeOauth2Authentication(OAuth2Authentication auth2Authent protected OAuth2Authentication deserializeOauth2Authentication(byte[] data) { Map map = JsonUtils.readValue(data, new TypeReference>() {}); Authentication userAuthentication = null; - if (map.get(USER_AUTHENTICATION_UAA_PRINCIPAL)!=null) { + if (map.get(USER_AUTHENTICATION_UAA_AUTHENTICATION) != null) { + userAuthentication = JsonUtils.readValue((String)map.get(USER_AUTHENTICATION_UAA_AUTHENTICATION), UaaAuthentication.class); + } + else if (map.get(USER_AUTHENTICATION_UAA_PRINCIPAL)!=null) { UaaPrincipal principal = JsonUtils.readValue((String)map.get(USER_AUTHENTICATION_UAA_PRINCIPAL), UaaPrincipal.class); Collection authorities = UaaStringUtils.getAuthoritiesFromStrings((Collection) map.get(USER_AUTHENTICATION_AUTHORITIES)); userAuthentication = new UaaAuthentication(principal, (List) authorities, UaaAuthenticationDetails.UNKNOWN); diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/util/UaaStringUtils.java b/common/src/main/java/org/cloudfoundry/identity/uaa/util/UaaStringUtils.java index 9e190b62c79..af184ade335 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/util/UaaStringUtils.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/util/UaaStringUtils.java @@ -20,6 +20,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -199,21 +200,24 @@ private static boolean isPassword(String key) { } public static Set getStringsFromAuthorities(Collection authorities) { + if (authorities==null) { + return Collections.EMPTY_SET; + } Set result = new HashSet<>(); - if (authorities!=null) { - for (GrantedAuthority authority : authorities) { - result.add(authority.getAuthority()); - } + for (GrantedAuthority authority : authorities) { + result.add(authority.getAuthority()); } return result; } public static List getAuthoritiesFromStrings(Collection authorities) { + if (authorities==null) { + return Collections.EMPTY_LIST; + } + List result = new LinkedList<>(); - if (authorities!=null) { - for (String s : authorities) { - result.add(new SimpleGrantedAuthority(s)); - } + for (String s : authorities) { + result.add(new SimpleGrantedAuthority(s)); } return result; } diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationSerializationTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationSerializationTests.java index 1ba9568a3df..cc9aa9485d0 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationSerializationTests.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationSerializationTests.java @@ -18,14 +18,104 @@ import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.hamcrest.Matchers; import org.junit.Test; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.everyItem; +import static org.hamcrest.Matchers.isIn; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class UaaAuthenticationSerializationTests { + public static final String COST_CENTER = "costCenter"; + public static final String DENVER_CO = "Denver,CO"; + public static final String MANAGER = "manager"; + public static final String JOHN_THE_SLOTH = "John the Sloth"; + public static final String KARI_THE_ANT_EATER = "Kari the Ant Eater"; + + @Test + public void test_serialization() throws Exception { + UaaPrincipal principal = new UaaPrincipal("id","username","email","origin","externalId","zoneId"); + HttpSession session = mock(HttpSession.class); + when(session.getId()).thenReturn("id"); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getRemoteAddr()).thenReturn("remoteAddr"); + when(request.getSession(false)).thenReturn(session); + UaaAuthenticationDetails details = new UaaAuthenticationDetails(request, "clientId"); + details.setAddNew(true); + + List authorities = Arrays.asList(new SimpleGrantedAuthority("role1"), new SimpleGrantedAuthority("role2")); + String credentials = "credentials"; + Map> userAttributes = new HashMap<>(); + userAttributes.put("atest", Arrays.asList("test1","test2","test3")); + userAttributes.put("btest", Arrays.asList("test1", "test2", "test3")); + Set externalGroups = new HashSet<>(Arrays.asList("group1","group2","group3")); + + boolean authenticated = true; + long authenticatedTime = System.currentTimeMillis(); + long expiresAt = Long.MAX_VALUE; + + UaaAuthentication expected = new UaaAuthentication(principal,credentials, authorities, externalGroups,userAttributes, details, authenticated, authenticatedTime, expiresAt); + String authenticationAsJson = JsonUtils.writeValueAsString(expected); + UaaAuthentication actual = JsonUtils.readValue(authenticationAsJson, UaaAuthentication.class); + + //validate authentication details + UaaAuthenticationDetails actualDetails = (UaaAuthenticationDetails)actual.getDetails(); + assertNotNull(actualDetails); + assertEquals("remoteAddr", actualDetails.getOrigin()); + assertEquals("id", actualDetails.getSessionId()); + assertEquals("clientId", actualDetails.getClientId()); + assertEquals(true, actualDetails.isAddNew()); + + //validate principal + UaaPrincipal actualPrincipal = actual.getPrincipal(); + assertEquals("id",actualPrincipal.getId()); + assertEquals("username",actualPrincipal.getName()); + assertEquals("email",actualPrincipal.getEmail()); + assertEquals("origin",actualPrincipal.getOrigin()); + assertEquals("externalId",actualPrincipal.getExternalId()); + assertEquals("zoneId", actualPrincipal.getZoneId()); + + //validate authorities + assertThat(actual.getAuthorities(), containsInAnyOrder(new SimpleGrantedAuthority("role1"), new SimpleGrantedAuthority("role2"))); + + //validate external groups + assertThat(actual.getExternalGroups(), containsInAnyOrder("group1","group2","group3")); + + //validate user attributes + assertEquals(2, actual.getUserAttributes().size()); + assertThat(actual.getUserAttributes().get("atest"),containsInAnyOrder("test1","test2","test3")); + assertThat(actual.getUserAttributes().get("btest"),containsInAnyOrder("test1","test2","test3")); + + //validate authenticated + assertEquals(authenticated, actual.isAuthenticated()); + + //validate authenticated time + assertEquals(authenticatedTime, actual.getAuthenticatedTime()); + + //validate expires at time + assertEquals(expiresAt, actual.getExpiresAt()); + + } + @Test public void testDeserializationWithoutAuthenticatedTime() throws Exception { String data ="{\"principal\":{\"id\":\"user-id\",\"name\":\"username\",\"email\":\"email\",\"origin\":\"uaa\",\"externalId\":null,\"zoneId\":\"uaa\"},\"credentials\":null,\"authorities\":[],\"details\":null,\"authenticated\":true,\"authenticatedTime\":1438649464353,\"name\":\"username\"}"; @@ -62,4 +152,32 @@ public void deserialization_with_external_groups() throws Exception { assertThat(authentication.getExternalGroups(), Matchers.containsInAnyOrder("something", "or", "other")); assertTrue(authentication.isAuthenticated()); } + + @Test + public void deserialization_with_user_attributes() throws Exception { + String dataWithoutUserAttributes ="{\"principal\":{\"id\":\"user-id\",\"name\":\"username\",\"email\":\"email\",\"origin\":\"uaa\",\"externalId\":null,\"zoneId\":\"uaa\"},\"credentials\":null,\"authorities\":[],\"externalGroups\":[\"something\",\"or\",\"other\",\"something\"],\"details\":null,\"authenticated\":true,\"authenticatedTime\":null,\"name\":\"username\"}"; + UaaAuthentication authentication = JsonUtils.readValue(dataWithoutUserAttributes, UaaAuthentication.class); + assertEquals(3, authentication.getExternalGroups().size()); + assertThat(authentication.getExternalGroups(), Matchers.containsInAnyOrder("something", "or", "other")); + assertTrue(authentication.isAuthenticated()); + + MultiValueMap userAttributes = new LinkedMultiValueMap<>(); + userAttributes.add(COST_CENTER, DENVER_CO); + userAttributes.add(MANAGER, JOHN_THE_SLOTH); + userAttributes.add(MANAGER, KARI_THE_ANT_EATER); + authentication.setUserAttributes(userAttributes); + + String dataWithUserAttributes = JsonUtils.writeValueAsString(authentication); + assertTrue("userAttributes should be part of the JSON", dataWithUserAttributes.contains("userAttributes")); + + UaaAuthentication authWithUserData = JsonUtils.readValue(dataWithUserAttributes, UaaAuthentication.class); + assertNotNull(authWithUserData.getUserAttributes()); + assertThat(authWithUserData.getUserAttributes().entrySet(), everyItem(isIn(userAttributes.entrySet()))); + assertThat(userAttributes.entrySet(), everyItem(isIn(authWithUserData.getUserAttributes().entrySet()))); + + assertEquals(3, authentication.getExternalGroups().size()); + assertThat(authentication.getExternalGroups(), Matchers.containsInAnyOrder("something", "or", "other")); + assertTrue(authentication.isAuthenticated()); + } + } diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServicesTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServicesTests.java index 3195a2d2002..9372cec355d 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServicesTests.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenServicesTests.java @@ -13,16 +13,13 @@ package org.cloudfoundry.identity.uaa.oauth.token; import com.fasterxml.jackson.core.type.TypeReference; -import org.cloudfoundry.identity.uaa.UaaConfiguration; import org.cloudfoundry.identity.uaa.audit.AuditEvent; import org.cloudfoundry.identity.uaa.audit.AuditEventType; import org.cloudfoundry.identity.uaa.audit.event.TokenIssuedEvent; import org.cloudfoundry.identity.uaa.authentication.Origin; import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; -import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.cloudfoundry.identity.uaa.client.ClientConstants; -import org.cloudfoundry.identity.uaa.login.saml.SamlIdentityProviderConfigurator; import org.cloudfoundry.identity.uaa.oauth.Claims; import org.cloudfoundry.identity.uaa.oauth.approval.Approval; import org.cloudfoundry.identity.uaa.oauth.approval.Approval.ApprovalStatus; @@ -52,16 +49,13 @@ import org.springframework.security.oauth2.common.exceptions.InvalidGrantException; import org.springframework.security.oauth2.common.exceptions.InvalidScopeException; import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; -import org.springframework.security.oauth2.common.util.OAuth2Utils; import org.springframework.security.oauth2.provider.AuthorizationRequest; +import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.OAuth2RequestFactory; import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.security.oauth2.provider.client.InMemoryClientDetailsService; -import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory; -import java.lang.reflect.Array; -import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; @@ -71,16 +65,12 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Set; import static org.cloudfoundry.identity.uaa.user.UaaAuthority.USER_AUTHORITIES; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; @@ -708,7 +698,7 @@ private Jwt getIdToken(List scopes) { authorizationRequest.setResponseTypes(new HashSet<>(Arrays.asList(OpenIdToken.ID_TOKEN))); UaaPrincipal uaaPrincipal = new UaaPrincipal(defaultUser.getId(), defaultUser.getUsername(), defaultUser.getEmail(), defaultUser.getOrigin(), defaultUser.getExternalId(), defaultUser.getZoneId()); - UaaAuthentication userAuthentication = new UaaAuthentication(uaaPrincipal, null, defaultUserAuthorities, new HashSet<>(Arrays.asList("group1", "group2")), null, true, System.currentTimeMillis(), System.currentTimeMillis() + 1000l * 60l); + UaaAuthentication userAuthentication = new UaaAuthentication(uaaPrincipal, null, defaultUserAuthorities, new HashSet<>(Arrays.asList("group1", "group2")),Collections.EMPTY_MAP, null, true, System.currentTimeMillis(), System.currentTimeMillis() + 1000l * 60l); OAuth2Authentication authentication = new OAuth2Authentication(authorizationRequest.createOAuth2Request(), userAuthentication); diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenStoreTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenStoreTests.java index 7547b0f1686..abcc3a4f983 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenStoreTests.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenStoreTests.java @@ -34,6 +34,8 @@ import org.springframework.security.oauth2.provider.TokenRequest; import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import javax.sql.DataSource; import java.io.PrintWriter; @@ -46,14 +48,18 @@ import java.sql.Timestamp; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.logging.Logger; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -98,6 +104,34 @@ public void createTokenStore() throws Exception { } + @Test + public void test_deserialization_of_uaa_authentication() throws Exception { + UaaAuthentication modifiedAuthentication = (UaaAuthentication) uaaAuthentication.getUserAuthentication(); + MultiValueMap userAttributes = new LinkedMultiValueMap<>(); + userAttributes.put("atest", Arrays.asList("test1","test2","test3")); + userAttributes.put("btest", Arrays.asList("test1","test2","test3")); + modifiedAuthentication.setUserAttributes(userAttributes); + + Set externalGroups = new HashSet<>(Arrays.asList("group1","group2","group3")); + modifiedAuthentication.setExternalGroups(externalGroups); + + String code = store.createAuthorizationCode(uaaAuthentication); + assertEquals(1, jdbcTemplate.queryForInt("SELECT count(*) FROM oauth_code WHERE code = ?", code)); + OAuth2Authentication authentication = store.consumeAuthorizationCode(code); + assertEquals(0, jdbcTemplate.queryForInt("SELECT count(*) FROM oauth_code WHERE code = ?", code)); + assertNotNull(authentication); + + UaaAuthentication userAuthentication = (UaaAuthentication) authentication.getUserAuthentication(); + assertNotNull(userAuthentication.getUserAttributes()); + assertEquals(2, userAuthentication.getUserAttributes().size()); + assertThat(userAuthentication.getUserAttributes().get("atest"), containsInAnyOrder("test1", "test2", "test3")); + assertThat(userAuthentication.getUserAttributes().get("btest"), containsInAnyOrder("test1", "test2", "test3")); + + assertNotNull(userAuthentication.getExternalGroups()); + assertEquals(3, userAuthentication.getExternalGroups().size()); + assertThat(userAuthentication.getExternalGroups(), containsInAnyOrder("group1","group2","group3")); + } + @Test public void test_ConsumeClientCredentials_From_OldStore() throws Exception { String code = legacyCodeServices.createAuthorizationCode(clientAuthentication); @@ -129,6 +163,13 @@ public void testStoreToken_PasswordGrant_UaaAuthentication() throws Exception { assertNotNull(code); } + @Test + public void deserialize_from_old_format() throws Exception { + OAuth2Authentication authentication = store.deserializeOauth2Authentication(UAA_AUTHENTICATION_DATA_OLD_STYLE); + assertNotNull(authentication); + assertEquals(principal, authentication.getUserAuthentication().getPrincipal()); + } + @Test public void testRetrieveToken() throws Exception { String code = store.createAuthorizationCode(clientAuthentication); @@ -340,4 +381,6 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl } } } + + private static final byte[] UAA_AUTHENTICATION_DATA_OLD_STYLE = new byte[] {123, 34, 111, 97, 117, 116, 104, 50, 82, 101, 113, 117, 101, 115, 116, 46, 114, 101, 115, 112, 111, 110, 115, 101, 84, 121, 112, 101, 115, 34, 58, 91, 93, 44, 34, 111, 97, 117, 116, 104, 50, 82, 101, 113, 117, 101, 115, 116, 46, 114, 101, 115, 111, 117, 114, 99, 101, 73, 100, 115, 34, 58, 91, 93, 44, 34, 117, 115, 101, 114, 65, 117, 116, 104, 101, 110, 116, 105, 99, 97, 116, 105, 111, 110, 46, 117, 97, 97, 80, 114, 105, 110, 99, 105, 112, 97, 108, 34, 58, 34, 123, 92, 34, 105, 100, 92, 34, 58, 92, 34, 117, 115, 101, 114, 105, 100, 92, 34, 44, 92, 34, 110, 97, 109, 101, 92, 34, 58, 92, 34, 117, 115, 101, 114, 110, 97, 109, 101, 92, 34, 44, 92, 34, 101, 109, 97, 105, 108, 92, 34, 58, 92, 34, 117, 115, 101, 114, 110, 97, 109, 101, 64, 116, 101, 115, 116, 46, 111, 114, 103, 92, 34, 44, 92, 34, 111, 114, 105, 103, 105, 110, 92, 34, 58, 92, 34, 117, 97, 97, 92, 34, 44, 92, 34, 101, 120, 116, 101, 114, 110, 97, 108, 73, 100, 92, 34, 58, 110, 117, 108, 108, 44, 92, 34, 122, 111, 110, 101, 73, 100, 92, 34, 58, 92, 34, 117, 97, 97, 92, 34, 125, 34, 44, 34, 111, 97, 117, 116, 104, 50, 82, 101, 113, 117, 101, 115, 116, 46, 114, 101, 113, 117, 101, 115, 116, 80, 97, 114, 97, 109, 101, 116, 101, 114, 115, 34, 58, 123, 34, 103, 114, 97, 110, 116, 95, 116, 121, 112, 101, 34, 58, 34, 112, 97, 115, 115, 119, 111, 114, 100, 34, 44, 34, 99, 108, 105, 101, 110, 116, 95, 105, 100, 34, 58, 34, 99, 108, 105, 101, 110, 116, 105, 100, 34, 44, 34, 115, 99, 111, 112, 101, 34, 58, 34, 111, 112, 101, 110, 105, 100, 34, 125, 44, 34, 111, 97, 117, 116, 104, 50, 82, 101, 113, 117, 101, 115, 116, 46, 114, 101, 100, 105, 114, 101, 99, 116, 85, 114, 105, 34, 58, 110, 117, 108, 108, 44, 34, 117, 115, 101, 114, 65, 117, 116, 104, 101, 110, 116, 105, 99, 97, 116, 105, 111, 110, 46, 97, 117, 116, 104, 111, 114, 105, 116, 105, 101, 115, 34, 58, 91, 34, 111, 112, 101, 110, 105, 100, 34, 93, 44, 34, 111, 97, 117, 116, 104, 50, 82, 101, 113, 117, 101, 115, 116, 46, 97, 117, 116, 104, 111, 114, 105, 116, 105, 101, 115, 34, 58, 91, 34, 111, 97, 117, 116, 104, 46, 108, 111, 103, 105, 110, 34, 93, 44, 34, 111, 97, 117, 116, 104, 50, 82, 101, 113, 117, 101, 115, 116, 46, 99, 108, 105, 101, 110, 116, 73, 100, 34, 58, 34, 99, 108, 105, 101, 110, 116, 105, 100, 34, 44, 34, 111, 97, 117, 116, 104, 50, 82, 101, 113, 117, 101, 115, 116, 46, 97, 112, 112, 114, 111, 118, 101, 100, 34, 58, 116, 114, 117, 101, 44, 34, 111, 97, 117, 116, 104, 50, 82, 101, 113, 117, 101, 115, 116, 46, 115, 99, 111, 112, 101, 34, 58, 91, 34, 111, 112, 101, 110, 105, 100, 34, 93, 125}; } diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java b/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java index dc4df0d821c..040655a48df 100644 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java @@ -15,6 +15,8 @@ import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.cloudfoundry.identity.uaa.authentication.Origin; import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; @@ -51,16 +53,17 @@ import org.springframework.security.saml.SAMLCredential; import org.springframework.security.saml.context.SAMLMessageContext; import org.springframework.security.saml.userdetails.SAMLUserDetailsService; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; -import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; -import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import java.util.stream.Collectors; @@ -71,7 +74,7 @@ import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.PHONE_NUMBER_ATTRIBUTE_NAME; public class LoginSamlAuthenticationProvider extends SAMLAuthenticationProvider implements ApplicationEventPublisherAware { - + private final static Log logger = LogFactory.getLog(LoginSamlAuthenticationProvider.class); private UaaUserDatabase userDatabase; private ApplicationEventPublisher eventPublisher; private IdentityProviderProvisioning identityProviderProvisioning; @@ -129,10 +132,10 @@ public Authentication authenticate(Authentication authentication) throws Authent Collection authorities = mapAuthorities(idp.getOriginKey(), samlAuthorities); Set filteredExternalGroups = filterSamlAuthorities(samlConfig, samlAuthorities); - Map userAttributes = retrieveUserAttributes(samlConfig, (SAMLCredential) result.getCredentials()); + MultiValueMap userAttributes = retrieveUserAttributes(samlConfig, (SAMLCredential) result.getCredentials()); UaaUser user = createIfMissing(samlPrincipal, addNew, authorities, userAttributes); UaaPrincipal principal = new UaaPrincipal(user); - return new LoginSamlAuthenticationToken(principal, result).getUaaAuthentication(user.getAuthorities(), filteredExternalGroups); + return new LoginSamlAuthenticationToken(principal, result).getUaaAuthentication(user.getAuthorities(), filteredExternalGroups, userAttributes); } protected ExpiringUsernameAuthenticationToken getExpiringUsernameAuthenticationToken(Authentication authentication) { @@ -191,13 +194,23 @@ public Collection retrieveSamlAuthorities(SamlIdenti return authorities == null ? Collections.EMPTY_LIST : authorities; } - public Map retrieveUserAttributes(SamlIdentityProviderDefinition definition, SAMLCredential credential) { - Map userAttributes = new HashMap<>(); + public MultiValueMap retrieveUserAttributes(SamlIdentityProviderDefinition definition, SAMLCredential credential) { + MultiValueMap userAttributes = new LinkedMultiValueMap<>(); if (definition != null && definition.getAttributeMappings() != null) { - for (Map.Entry attributeMapping : definition.getAttributeMappings().entrySet()) { + for (Entry attributeMapping : definition.getAttributeMappings().entrySet()) { if (attributeMapping.getValue() instanceof String) { if (credential.getAttribute((String)attributeMapping.getValue()) != null) { - userAttributes.put(attributeMapping.getKey(), ((XSString) credential.getAttribute((String) attributeMapping.getValue()).getAttributeValues().get(0)).getValue()); + String key = attributeMapping.getKey(); + int count = 0; + for (XMLObject xmlObject : credential.getAttribute((String) attributeMapping.getValue()).getAttributeValues()) { + if (xmlObject instanceof XSString) { + String value = ((XSString) xmlObject).getValue(); + userAttributes.add(key, value); + } else { + logger.debug(String.format("SAML user attribute %s at index %s is not of type XSString [zone:%s, origin:%s]", key, count, definition.getZoneId(), definition.getIdpEntityAlias())); + } + count++; + } } } } @@ -205,7 +218,7 @@ public Map retrieveUserAttributes(SamlIdentityProviderDefinition return userAttributes; } - protected UaaUser createIfMissing(UaaPrincipal samlPrincipal, boolean addNew, Collection authorities, Map userAttributes) { + protected UaaUser createIfMissing(UaaPrincipal samlPrincipal, boolean addNew, Collection authorities, MultiValueMap userAttributes) { boolean userModified = false; UaaPrincipal uaaPrincipal = samlPrincipal; UaaUser user; @@ -244,12 +257,12 @@ protected UaaUser createIfMissing(UaaPrincipal samlPrincipal, boolean addNew, Co return user; } - private UaaUser getUser(UaaPrincipal principal, Map userAttributes) { + protected UaaUser getUser(UaaPrincipal principal, MultiValueMap userAttributes) { String name = principal.getName(); - String email = userAttributes.get(EMAIL_ATTRIBUTE_NAME); - String givenName = userAttributes.get(GIVEN_NAME_ATTRIBUTE_NAME); - String familyName = userAttributes.get(FAMILY_NAME_ATTRIBUTE_NAME); - String phoneNumber = userAttributes.get(PHONE_NUMBER_ATTRIBUTE_NAME); + String email = userAttributes.getFirst(EMAIL_ATTRIBUTE_NAME); + String givenName = userAttributes.getFirst(GIVEN_NAME_ATTRIBUTE_NAME); + String familyName = userAttributes.getFirst(FAMILY_NAME_ATTRIBUTE_NAME); + String phoneNumber = userAttributes.getFirst(PHONE_NUMBER_ATTRIBUTE_NAME); String userId = Origin.NotANumber; String origin = principal.getOrigin()!=null?principal.getOrigin():Origin.LOGIN_SERVER; String zoneId = principal.getZoneId(); diff --git a/uaa/src/main/resources/uaa.yml b/uaa/src/main/resources/uaa.yml index 2552fde9fcb..64e07c45177 100755 --- a/uaa/src/main/resources/uaa.yml +++ b/uaa/src/main/resources/uaa.yml @@ -152,6 +152,7 @@ oauth: - oauth.approvals - profile - roles + - user_attributes # Allow unverified users to log in. Defaults to true #allowUnverifiedUsers: false diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/IdentityZoneEndpointsIntegrationTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/IdentityZoneEndpointsIntegrationTests.java index 9df4ca9a3fa..1f8819fa51d 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/IdentityZoneEndpointsIntegrationTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/IdentityZoneEndpointsIntegrationTests.java @@ -13,18 +13,14 @@ import java.util.Map; import java.util.UUID; -import com.fasterxml.jackson.core.type.TypeReference; import org.cloudfoundry.identity.uaa.ServerRunning; import org.cloudfoundry.identity.uaa.authentication.Origin; import org.cloudfoundry.identity.uaa.client.ClientConstants; -import org.cloudfoundry.identity.uaa.config.PasswordPolicy; import org.cloudfoundry.identity.uaa.integration.util.IntegrationTestUtils; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.test.TestAccountSetup; import org.cloudfoundry.identity.uaa.test.UaaTestAccounts; -import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.cloudfoundry.identity.uaa.zone.IdentityProvider; -import org.cloudfoundry.identity.uaa.zone.IdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneSwitchingFilter; import org.cloudfoundry.identity.uaa.zone.UaaIdentityProviderDefinition; @@ -32,7 +28,6 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.*; import org.springframework.http.client.ClientHttpResponse; @@ -51,7 +46,7 @@ public class IdentityZoneEndpointsIntegrationTests { public ServerRunning serverRunning = ServerRunning.isRunning(); private UaaTestAccounts testAccounts = UaaTestAccounts.standard(serverRunning); - + @Rule public OAuth2ContextSetup context = OAuth2ContextSetup.standard(serverRunning); @@ -93,8 +88,8 @@ public void testCreateZone() throws Exception { assertEquals(HttpStatus.CREATED, response.getStatusCode()); - RestTemplate adminClient = IntegrationTestUtils.getClientCredentialsTempate( - IntegrationTestUtils.getClientCredentialsResource(serverRunning.getBaseUrl(), new String[0], "admin", "adminsecret") + RestTemplate adminClient = IntegrationTestUtils.getClientCredentialsTemplate( + IntegrationTestUtils.getClientCredentialsResource(serverRunning.getBaseUrl(), new String[0], "admin", "adminsecret") ); String email = new RandomValueStringGenerator().generate() +"@samltesting.org"; ScimUser user = IntegrationTestUtils.createUser(adminClient, serverRunning.getBaseUrl(), email, "firstname", "lastname", email, true); @@ -124,7 +119,7 @@ public void testCreateZone() throws Exception { assertNotNull(identityProvider.getConfigValue(UaaIdentityProviderDefinition.class)); assertNull(identityProvider.getConfigValue(UaaIdentityProviderDefinition.class).getPasswordPolicy()); } - + @Test public void testCreateZoneWithClient() throws IOException { IdentityZone idZone = new IdentityZone(); @@ -136,33 +131,33 @@ public void testCreateZoneWithClient() throws IOException { serverRunning.getUrl("/identity-zones"), HttpMethod.POST, new HttpEntity<>(idZone), - new ParameterizedTypeReference() {}, + new ParameterizedTypeReference() {}, id); assertEquals(HttpStatus.CREATED, response.getStatusCode()); - + BaseClientDetails clientDetails = new BaseClientDetails("test123", null,"openid", "authorization_code", "uaa.resource"); clientDetails.setClientSecret("testSecret"); clientDetails.addAdditionalInformation(ClientConstants.ALLOWED_PROVIDERS, Collections.singleton(Origin.UAA)); - + ResponseEntity clientCreateResponse = client.exchange( serverRunning.getUrl("/identity-zones/"+id+"/clients"), HttpMethod.POST, new HttpEntity<>(clientDetails), - new ParameterizedTypeReference() {}, + new ParameterizedTypeReference() {}, id); - + assertEquals(HttpStatus.CREATED, clientCreateResponse.getStatusCode()); - + ResponseEntity clientDeleteResponse = client.exchange( serverRunning.getUrl("/identity-zones/"+id+"/clients/"+clientDetails.getClientId()), HttpMethod.DELETE, null, - new ParameterizedTypeReference() {}, + new ParameterizedTypeReference() {}, id); - + assertEquals(HttpStatus.OK, clientDeleteResponse.getStatusCode()); } - + @Test public void testCreateZoneWithNonUniqueSubdomain() { @@ -175,10 +170,10 @@ public void testCreateZoneWithNonUniqueSubdomain() { serverRunning.getUrl("/identity-zones"), HttpMethod.POST, new HttpEntity<>(idZone1), - new ParameterizedTypeReference() {}, + new ParameterizedTypeReference() {}, id1); assertEquals(HttpStatus.CREATED, response1.getStatusCode()); - + IdentityZone idZone2 = new IdentityZone(); String id2 = UUID.randomUUID().toString(); idZone2.setId(id2); @@ -188,12 +183,12 @@ public void testCreateZoneWithNonUniqueSubdomain() { serverRunning.getUrl("/identity-zones"), HttpMethod.POST, new HttpEntity<>(idZone2), - new ParameterizedTypeReference>() {}, + new ParameterizedTypeReference>() {}, id2); assertEquals(HttpStatus.CONFLICT, response2.getStatusCode()); Assert.assertTrue(response2.getBody().get("error_description").toLowerCase().contains("subdomain")); } - + static class IdentityClient extends ClientCredentialsResourceDetails { public IdentityClient(Object target) { IdentityZoneEndpointsIntegrationTests test = (IdentityZoneEndpointsIntegrationTests) target; @@ -205,5 +200,5 @@ public IdentityClient(Object target) { setAccessTokenUri(test.serverRunning.getAccessTokenUri()); } } - + } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/ScimGroupEndpointsIntegrationTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/ScimGroupEndpointsIntegrationTests.java index 46e5c113f79..aa430f8acfb 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/ScimGroupEndpointsIntegrationTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/ScimGroupEndpointsIntegrationTests.java @@ -22,7 +22,6 @@ import org.cloudfoundry.identity.uaa.test.TestAccountSetup; import org.cloudfoundry.identity.uaa.test.UaaTestAccounts; import org.cloudfoundry.identity.uaa.web.CookieBasedCsrfTokenRepository; -import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.junit.After; import org.junit.Before; @@ -84,7 +83,7 @@ public class ScimGroupEndpointsIntegrationTests { private static final List defaultGroups = Arrays.asList("openid", "scim.me", "cloud_controller.read", "cloud_controller.write", "password.write", "scim.userids", "uaa.user", "approvals.me", - "oauth.approvals", "cloud_controller_service_permissions.read", "profile", "roles"); + "oauth.approvals", "cloud_controller_service_permissions.read", "profile", "roles", "user_attributes"); @Rule diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/ScimUserEndpointsIntegrationTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/ScimUserEndpointsIntegrationTests.java index 6422494e84a..11437e49d55 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/ScimUserEndpointsIntegrationTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/ScimUserEndpointsIntegrationTests.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Cloud Foundry + * Cloud Foundry * Copyright (c) [2009-2014] Pivotal Software, Inc. All Rights Reserved. * * This product is licensed to you under the Apache License, Version 2.0 (the "License"). @@ -63,7 +63,7 @@ public class ScimUserEndpointsIntegrationTests { private final String usersEndpoint = "/Users"; - private static final int NUM_DEFAULT_GROUPS_ON_STARTUP = 12; + private static final int NUM_DEFAULT_GROUPS_ON_STARTUP = 13; @Rule public ServerRunning serverRunning = ServerRunning.isRunning(); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/OpenIdTokenGrantsIT.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/OpenIdTokenGrantsIT.java index a9318e7e890..a73ba5d5a5f 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/OpenIdTokenGrantsIT.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/OpenIdTokenGrantsIT.java @@ -109,7 +109,7 @@ public void setUp() throws Exception { ((RestTemplate)restOperations).setRequestFactory(new IntegrationTestUtils.StatelessRequestFactory()); ClientCredentialsResourceDetails clientCredentials = getClientCredentialsResource(new String[] {"scim.write"}, testAccounts.getAdminClientId(), testAccounts.getAdminClientSecret()); - client = IntegrationTestUtils.getClientCredentialsTempate(clientCredentials); + client = IntegrationTestUtils.getClientCredentialsTemplate(clientCredentials); user = createUser(new RandomValueStringGenerator().generate(), "openiduser", "openidlast", "test@openid,com",true); } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java index 09b8958e261..d3e664b4f00 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SamlLoginIT.java @@ -13,6 +13,7 @@ package org.cloudfoundry.identity.uaa.integration.feature; import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition; @@ -25,6 +26,7 @@ import org.cloudfoundry.identity.uaa.login.saml.SamlIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.login.test.LoginServerClassRunner; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; +import org.cloudfoundry.identity.uaa.oauth.Claims; import org.cloudfoundry.identity.uaa.scim.ScimGroup; import org.cloudfoundry.identity.uaa.scim.ScimGroupExternalMember; import org.cloudfoundry.identity.uaa.scim.ScimGroupMember; @@ -42,6 +44,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.openqa.selenium.By; +import org.openqa.selenium.Cookie; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; @@ -52,6 +55,8 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; +import org.springframework.security.jwt.Jwt; +import org.springframework.security.jwt.JwtHelper; import org.springframework.security.oauth2.client.test.TestAccounts; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; import org.springframework.security.oauth2.provider.client.BaseClientDetails; @@ -69,6 +74,7 @@ import java.util.Map; import java.util.UUID; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -248,10 +254,10 @@ protected IdentityProvider createIdentityProvider(String originKey) throws Excep * @throws Exception on error */ protected IdentityProvider createIdentityProvider(String originKey, boolean addShadowUserOnLogin) throws Exception { - RestTemplate identityClient = IntegrationTestUtils.getClientCredentialsTempate( + RestTemplate identityClient = IntegrationTestUtils.getClientCredentialsTemplate( IntegrationTestUtils.getClientCredentialsResource(baseUrl, new String[0], "identity", "identitysecret") ); - RestTemplate adminClient = IntegrationTestUtils.getClientCredentialsTempate( + RestTemplate adminClient = IntegrationTestUtils.getClientCredentialsTemplate( IntegrationTestUtils.getClientCredentialsResource(baseUrl, new String[0], "admin", "adminsecret") ); String email = new RandomValueStringGenerator().generate() +"@samltesting.org"; @@ -284,10 +290,10 @@ protected BaseClientDetails createClientAndSpecifyProvider(String clientId, Iden String redirectUri) throws Exception { - RestTemplate identityClient = IntegrationTestUtils.getClientCredentialsTempate( + RestTemplate identityClient = IntegrationTestUtils.getClientCredentialsTemplate( IntegrationTestUtils.getClientCredentialsResource(baseUrl, new String[0], "identity", "identitysecret") ); - RestTemplate adminClient = IntegrationTestUtils.getClientCredentialsTempate( + RestTemplate adminClient = IntegrationTestUtils.getClientCredentialsTemplate( IntegrationTestUtils.getClientCredentialsResource(baseUrl, new String[0], "admin", "adminsecret") ); String email = new RandomValueStringGenerator().generate() +"@samltesting.org"; @@ -345,11 +351,11 @@ public void perform_SamlInvitation_Automatic_Redirect_In_Zone2(String username, String zoneUrl = baseUrl.replace("localhost",zoneId+".localhost"); //identity client token - RestTemplate identityClient = IntegrationTestUtils.getClientCredentialsTempate( - IntegrationTestUtils.getClientCredentialsResource(baseUrl,new String[] {"zones.write", "zones.read", "scim.zones"}, "identity", "identitysecret") + RestTemplate identityClient = IntegrationTestUtils.getClientCredentialsTemplate( + IntegrationTestUtils.getClientCredentialsResource(baseUrl, new String[]{"zones.write", "zones.read", "scim.zones"}, "identity", "identitysecret") ); //admin client token - to create users - RestTemplate adminClient = IntegrationTestUtils.getClientCredentialsTempate( + RestTemplate adminClient = IntegrationTestUtils.getClientCredentialsTemplate( IntegrationTestUtils.getClientCredentialsResource(baseUrl, new String[0], "admin", "adminsecret") ); //create the zone @@ -436,12 +442,12 @@ public void testSamlLoginClientIDPAuthorizationAutomaticRedirectInZone1() throws String zoneId = "testzone1"; //identity client token - RestTemplate identityClient = IntegrationTestUtils.getClientCredentialsTempate( - IntegrationTestUtils.getClientCredentialsResource(baseUrl,new String[] {"zones.write", "zones.read", "scim.zones"}, "identity", "identitysecret") + RestTemplate identityClient = IntegrationTestUtils.getClientCredentialsTemplate( + IntegrationTestUtils.getClientCredentialsResource(baseUrl, new String[]{"zones.write", "zones.read", "scim.zones"}, "identity", "identitysecret") ); //admin client token - to create users - RestTemplate adminClient = IntegrationTestUtils.getClientCredentialsTempate( - IntegrationTestUtils.getClientCredentialsResource(baseUrl,new String[0] , "admin", "adminsecret") + RestTemplate adminClient = IntegrationTestUtils.getClientCredentialsTemplate( + IntegrationTestUtils.getClientCredentialsResource(baseUrl, new String[0], "admin", "adminsecret") ); //create the zone IntegrationTestUtils.createZoneOrUpdateSubdomain(identityClient, baseUrl, zoneId, zoneId); @@ -507,12 +513,12 @@ public void testSamlLogin_Map_Groups_In_Zone1() throws Exception { String zoneUrl = baseUrl.replace("localhost", "testzone1.localhost"); //identity client token - RestTemplate identityClient = IntegrationTestUtils.getClientCredentialsTempate( - IntegrationTestUtils.getClientCredentialsResource(baseUrl,new String[] {"zones.write", "zones.read", "scim.zones"}, "identity", "identitysecret") + RestTemplate identityClient = IntegrationTestUtils.getClientCredentialsTemplate( + IntegrationTestUtils.getClientCredentialsResource(baseUrl, new String[]{"zones.write", "zones.read", "scim.zones"}, "identity", "identitysecret") ); //admin client token - to create users - RestTemplate adminClient = IntegrationTestUtils.getClientCredentialsTempate( - IntegrationTestUtils.getClientCredentialsResource(baseUrl,new String[0] , "admin", "adminsecret") + RestTemplate adminClient = IntegrationTestUtils.getClientCredentialsTemplate( + IntegrationTestUtils.getClientCredentialsResource(baseUrl, new String[0], "admin", "adminsecret") ); //create the zone IntegrationTestUtils.createZoneOrUpdateSubdomain(identityClient, baseUrl, zoneId, zoneId); @@ -596,17 +602,137 @@ public void testSamlLogin_Map_Groups_In_Zone1() throws Exception { } + @Test + public void testSamlLogin_Custom_User_Attributes_In_ID_Token() throws Exception { + + final String COST_CENTER = "costCenter"; + final String COST_CENTERS = "costCenters"; + final String DENVER_CO = "Denver,CO"; + final String MANAGER = "manager"; + final String MANAGERS = "managers"; + final String JOHN_THE_SLOTH = "John the Sloth"; + final String KARI_THE_ANT_EATER = "Kari the Ant Eater"; + + + //ensure we are able to resolve DNS for hostname testzone1.localhost + assumeTrue("Expected testzone1/2.localhost to resolve to 127.0.0.1", doesSupportZoneDNS()); + String zoneId = "testzone1"; + String zoneUrl = baseUrl.replace("localhost", "testzone1.localhost"); + + //identity client token + RestTemplate identityClient = IntegrationTestUtils.getClientCredentialsTemplate( + IntegrationTestUtils.getClientCredentialsResource(baseUrl, new String[]{"zones.write", "zones.read", "scim.zones"}, "identity", "identitysecret") + ); + //admin client token - to create users + RestTemplate adminClient = IntegrationTestUtils.getClientCredentialsTemplate( + IntegrationTestUtils.getClientCredentialsResource(baseUrl, new String[0], "admin", "adminsecret") + ); + //create the zone + IntegrationTestUtils.createZoneOrUpdateSubdomain(identityClient, baseUrl, zoneId, zoneId); + + //create a zone admin user + String email = new RandomValueStringGenerator().generate() +"@samltesting.org"; + ScimUser user = IntegrationTestUtils.createUser(adminClient, baseUrl,email ,"firstname", "lastname", email, true); + IntegrationTestUtils.makeZoneAdmin(identityClient, baseUrl, user.getId(), zoneId); + + //get the zone admin token + String zoneAdminToken = + IntegrationTestUtils.getAuthorizationCodeToken(serverRunning, + UaaTestAccounts.standard(serverRunning), + "identity", + "identitysecret", + email, + "secr3T"); + + SamlIdentityProviderDefinition samlIdentityProviderDefinition = createTestZone1IDP("simplesamlphp"); + samlIdentityProviderDefinition.addAttributeMapping(ExternalIdentityProviderDefinition.USER_ATTRIBUTE_PREFIX+COST_CENTERS, COST_CENTER); + samlIdentityProviderDefinition.addAttributeMapping(ExternalIdentityProviderDefinition.USER_ATTRIBUTE_PREFIX+MANAGERS, MANAGER); + + IdentityProvider provider = new IdentityProvider(); + provider.setIdentityZoneId(zoneId); + provider.setType(Origin.SAML); + provider.setActive(true); + provider.setConfig(JsonUtils.writeValueAsString(samlIdentityProviderDefinition)); + provider.setOriginKey(samlIdentityProviderDefinition.getIdpEntityAlias()); + provider.setName("simplesamlphp for testzone1"); + + provider = IntegrationTestUtils.createOrUpdateProvider(zoneAdminToken,baseUrl,provider); + assertEquals(provider.getOriginKey(), provider.getConfigValue(SamlIdentityProviderDefinition.class).getIdpEntityAlias()); + + List idps = Arrays.asList(provider.getOriginKey()); + + String adminClientInZone = new RandomValueStringGenerator().generate(); + BaseClientDetails clientDetails = new BaseClientDetails(adminClientInZone, null, "openid,user_attributes", "authorization_code,client_credentials", "uaa.admin,scim.read,scim.write,uaa.resource", zoneUrl); + clientDetails.setClientSecret("secret"); + clientDetails.addAdditionalInformation(ClientConstants.AUTO_APPROVE, true); + clientDetails.addAdditionalInformation(ClientConstants.ALLOWED_PROVIDERS, idps); + + clientDetails = IntegrationTestUtils.createClientAsZoneAdmin(zoneAdminToken, baseUrl, zoneId, clientDetails); + clientDetails.setClientSecret("secret"); + + String adminTokenInZone = IntegrationTestUtils.getClientCredentialsToken(zoneUrl,clientDetails.getClientId(), "secret"); + + webDriver.get(zoneUrl + "/logout.do"); + + String authUrl = zoneUrl + "/oauth/authorize?client_id=" + clientDetails.getClientId() + "&redirect_uri=" + URLEncoder.encode(zoneUrl) + "&response_type=code&state=8tp0tR"; + webDriver.get(authUrl); + //we should now be in the Simple SAML PHP site + webDriver.findElement(By.xpath("//h2[contains(text(), 'Enter your username and password')]")); + webDriver.findElement(By.name("username")).clear(); + webDriver.findElement(By.name("username")).sendKeys("marissa5"); + webDriver.findElement(By.name("password")).sendKeys("saml5"); + webDriver.findElement(By.xpath("//input[@value='Login']")).click(); + assertThat(webDriver.findElement(By.cssSelector("h1")).getText(), Matchers.containsString("Where to?")); + + Cookie cookie= webDriver.manage().getCookieNamed("JSESSIONID"); + + //do an auth code grant + //pass up the jsessionid + System.out.println("cookie = " + String.format("%s=%s",cookie.getName(), cookie.getValue())); + + serverRunning.setHostName("testzone1.localhost"); + Map authCodeTokenResponse = IntegrationTestUtils.getAuthorizationCodeTokenMap(serverRunning, + UaaTestAccounts.standard(serverRunning), + clientDetails.getClientId(), + clientDetails.getClientSecret(), + null, + null, + "token id_token", + cookie.getValue(), + zoneUrl, + false); + + webDriver.get(baseUrl + "/logout.do"); + webDriver.get(zoneUrl + "/logout.do"); + + //validate that we have an ID token, and that it contains costCenter and manager values + + String idToken = authCodeTokenResponse.get("id_token"); + assertNotNull(idToken); + + Jwt idTokenClaims = JwtHelper.decode(idToken); + Map claims = JsonUtils.readValue(idTokenClaims.getClaims(), new TypeReference>() {}); + + assertNotNull(claims.get(Claims.USER_ATTRIBUTES)); + Map> userAttributes = (Map>) claims.get(Claims.USER_ATTRIBUTES); + assertThat(userAttributes.get(COST_CENTERS), containsInAnyOrder(DENVER_CO)); + assertThat(userAttributes.get(MANAGERS), containsInAnyOrder(JOHN_THE_SLOTH, KARI_THE_ANT_EATER)); + + } + + + @Test public void testSimpleSamlPhpLoginInTestZone1Works() throws Exception { assumeTrue("Expected testzone1/2.localhost to resolve to 127.0.0.1", doesSupportZoneDNS()); String zoneId = "testzone1"; - RestTemplate identityClient = IntegrationTestUtils.getClientCredentialsTempate( - IntegrationTestUtils.getClientCredentialsResource(baseUrl,new String[] {"zones.write", "zones.read", "scim.zones"}, "identity", "identitysecret") + RestTemplate identityClient = IntegrationTestUtils.getClientCredentialsTemplate( + IntegrationTestUtils.getClientCredentialsResource(baseUrl, new String[]{"zones.write", "zones.read", "scim.zones"}, "identity", "identitysecret") ); - RestTemplate adminClient = IntegrationTestUtils.getClientCredentialsTempate( - IntegrationTestUtils.getClientCredentialsResource(baseUrl,new String[0] , "admin", "adminsecret") + RestTemplate adminClient = IntegrationTestUtils.getClientCredentialsTemplate( + IntegrationTestUtils.getClientCredentialsResource(baseUrl, new String[0], "admin", "adminsecret") ); IdentityZone zone = IntegrationTestUtils.createZoneOrUpdateSubdomain(identityClient, baseUrl, zoneId, zoneId); String email = new RandomValueStringGenerator().generate() +"@samltesting.org"; diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/util/IntegrationTestUtils.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/util/IntegrationTestUtils.java index db55a567eee..064382bab9b 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/util/IntegrationTestUtils.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/util/IntegrationTestUtils.java @@ -30,7 +30,6 @@ import org.cloudfoundry.identity.uaa.zone.IdentityProvider; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneSwitchingFilter; -import org.flywaydb.core.internal.util.StringUtils; import org.junit.Assert; import org.openqa.selenium.OutputType; import org.openqa.selenium.TakesScreenshot; @@ -56,6 +55,7 @@ import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; import org.springframework.web.client.DefaultResponseErrorHandler; import org.springframework.web.client.RestTemplate; @@ -100,7 +100,7 @@ public static ClientCredentialsResourceDetails getClientCredentialsResource(Stri return resource; } - public static RestTemplate getClientCredentialsTempate(ClientCredentialsResourceDetails details) { + public static RestTemplate getClientCredentialsTemplate(ClientCredentialsResourceDetails details) { RestTemplate client = new OAuth2RestTemplate(details); client.setRequestFactory(new StatelessRequestFactory()); client.setErrorHandler(new OAuth2ErrorHandler(details) { @@ -639,21 +639,58 @@ public static Map getAuthorizationCodeTokenMap(ServerRunning serv String clientSecret, String username, String password) throws Exception { + AuthorizationCodeResourceDetails resource = testAccounts.getDefaultAuthorizationCodeResource(); + resource.setClientId(clientId); + resource.setClientSecret(clientSecret); + + return getAuthorizationCodeTokenMap(serverRunning, + testAccounts, + clientId, + clientSecret, + username, + password, + null, + null, + resource.getPreEstablishedRedirectUri(), + true); + } + + public static Map getAuthorizationCodeTokenMap(ServerRunning serverRunning, + UaaTestAccounts testAccounts, + String clientId, + String clientSecret, + String username, + String password, + String tokenResponseType, + String jSessionId, + String redirectUri, + boolean callCheckToken) throws Exception { // TODO Fix to use json API rather than HTML HttpHeaders headers = new HttpHeaders(); + if (StringUtils.hasText(jSessionId)) { + headers.add("Cookie", "JSESSIONID="+jSessionId); + } // TODO: should be able to handle just TEXT_HTML headers.setAccept(Arrays.asList(MediaType.TEXT_HTML, MediaType.ALL)); - AuthorizationCodeResourceDetails resource = testAccounts.getDefaultAuthorizationCodeResource(); - resource.setClientId(clientId); - resource.setClientSecret(clientSecret); + String mystateid = "mystateid"; + ServerRunning.UriBuilder builder = serverRunning.buildUri("/oauth/authorize") + .queryParam("response_type", "code") + .queryParam("state", mystateid) + .queryParam("client_id", clientId); + if( StringUtils.hasText(redirectUri)) { + builder = builder.queryParam("redirect_uri", redirectUri); + } + URI uri = builder.build(); + + ResponseEntity result = + serverRunning.createRestTemplate().exchange( + uri.toString(), + HttpMethod.GET, + new HttpEntity<>(null,headers), + Void.class + ); - URI uri = serverRunning.buildUri("/oauth/authorize").queryParam("response_type", "code") - .queryParam("state", "mystateid").queryParam("client_id", resource.getClientId()) - .queryParam("redirect_uri", resource.getPreEstablishedRedirectUri()).build(); - ResponseEntity result = serverRunning.createRestTemplate().exchange( - uri.toString(),HttpMethod.GET, new HttpEntity<>(null,headers), - Void.class); assertEquals(HttpStatus.FOUND, result.getStatusCode()); String location = result.getHeaders().getLocation().toString(); @@ -671,25 +708,28 @@ public static Map getAuthorizationCodeTokenMap(ServerRunning serv headers.add("Cookie", cookie); } } - // should be directed to the login screen... - assertTrue(response.getBody().contains("/login.do")); - assertTrue(response.getBody().contains("username")); - assertTrue(response.getBody().contains("password")); - String csrf = IntegrationTestUtils.extractCookieCsrf(response.getBody()); MultiValueMap formData = new LinkedMultiValueMap<>(); - formData.add("username", username); - formData.add("password", password); - formData.add(CookieBasedCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME, csrf); - - // Should be redirected to the original URL, but now authenticated - result = serverRunning.postForResponse("/login.do", headers, formData); - assertEquals(HttpStatus.FOUND, result.getStatusCode()); + if (!StringUtils.hasText(jSessionId)) { + // should be directed to the login screen... + assertTrue(response.getBody().contains("/login.do")); + assertTrue(response.getBody().contains("username")); + assertTrue(response.getBody().contains("password")); + String csrf = IntegrationTestUtils.extractCookieCsrf(response.getBody()); + + formData.add("username", username); + formData.add("password", password); + formData.add(CookieBasedCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME, csrf); + + // Should be redirected to the original URL, but now authenticated + result = serverRunning.postForResponse("/login.do", headers, formData); + assertEquals(HttpStatus.FOUND, result.getStatusCode()); - headers.remove("Cookie"); - if (result.getHeaders().containsKey("Set-Cookie")) { - for (String cookie : result.getHeaders().get("Set-Cookie")) { - headers.add("Cookie", cookie); + headers.remove("Cookie"); + if (result.getHeaders().containsKey("Set-Cookie")) { + for (String cookie : result.getHeaders().get("Set-Cookie")) { + headers.add("Cookie", cookie); + } } } @@ -713,17 +753,22 @@ public static Map getAuthorizationCodeTokenMap(ServerRunning serv assertEquals(HttpStatus.FOUND, response.getStatusCode()); location = response.getHeaders().getLocation().toString(); } - assertTrue("Wrong location: " + location, - location.matches(resource.getPreEstablishedRedirectUri() + ".*code=.+")); + if (StringUtils.hasText(redirectUri)) { + assertTrue("Wrong location: " + location, location.matches(redirectUri + ".*code=.+")); + } formData.clear(); - formData.add("client_id", resource.getClientId()); - formData.add("redirect_uri", resource.getPreEstablishedRedirectUri()); + formData.add("client_id", clientId); formData.add("grant_type", "authorization_code"); + if (StringUtils.hasText(redirectUri)) { + formData.add("redirect_uri", redirectUri); + } + if (StringUtils.hasText(tokenResponseType)) { + formData.add("response_type", tokenResponseType); + } formData.add("code", location.split("code=")[1].split("&")[0]); HttpHeaders tokenHeaders = new HttpHeaders(); - tokenHeaders.set("Authorization", - testAccounts.getAuthorizationHeader(resource.getClientId(), resource.getClientSecret())); + tokenHeaders.set("Authorization",testAccounts.getAuthorizationHeader(clientId, clientSecret)); @SuppressWarnings("rawtypes") ResponseEntity tokenResponse = serverRunning.postForMap("/oauth/token", formData, tokenHeaders); assertEquals(HttpStatus.OK, tokenResponse.getStatusCode()); @@ -733,14 +778,15 @@ public static Map getAuthorizationCodeTokenMap(ServerRunning serv Map body = tokenResponse.getBody(); formData = new LinkedMultiValueMap<>(); - headers.set("Authorization", - testAccounts.getAuthorizationHeader(resource.getClientId(), resource.getClientSecret())); + headers.set("Authorization", testAccounts.getAuthorizationHeader(clientId, clientSecret)); formData.add("token", accessToken.getValue()); - tokenResponse = serverRunning.postForMap("/check_token", formData, headers); - assertEquals(HttpStatus.OK, tokenResponse.getStatusCode()); - //System.err.println(tokenResponse.getBody()); - assertNotNull(tokenResponse.getBody().get("iss")); + if (callCheckToken) { + tokenResponse = serverRunning.postForMap("/check_token", formData, headers); + assertEquals(HttpStatus.OK, tokenResponse.getStatusCode()); + //System.err.println(tokenResponse.getBody()); + assertNotNull(tokenResponse.getBody().get("iss")); + } return body; } @@ -790,14 +836,33 @@ public static void clearAllButJsessionID(HttpHeaders headers) { } } - public static class StatelessRequestFactory extends HttpComponentsClientHttpRequestFactory { + public static class HttpRequestFactory extends HttpComponentsClientHttpRequestFactory { + private final boolean disableRedirect; + private final boolean disableCookieHandling; + + public HttpRequestFactory(boolean disableCookieHandling, boolean disableRedirect) { + this.disableCookieHandling = disableCookieHandling; + this.disableRedirect = disableRedirect; + } + @Override public HttpClient getHttpClient() { - return HttpClientBuilder.create() - .useSystemProperties() - .disableRedirectHandling() - .disableCookieManagement() - .build(); + HttpClientBuilder builder = HttpClientBuilder.create() + .useSystemProperties(); + if (disableRedirect) { + builder = builder.disableRedirectHandling(); + } + if (disableCookieHandling) { + builder = builder.disableCookieManagement(); + } + return builder.build(); + } + } + + + public static class StatelessRequestFactory extends HttpRequestFactory { + public StatelessRequestFactory() { + super(true, true); } } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java index 51ba8ddb3c2..662974533d5 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java @@ -36,7 +36,6 @@ import org.cloudfoundry.identity.uaa.zone.IdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.JdbcIdentityProviderProvisioning; -import org.hamcrest.Matchers; import org.junit.Before; import org.junit.Test; import org.opensaml.saml2.core.Assertion; @@ -64,6 +63,8 @@ import java.util.List; import java.util.Map; +import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.USER_ATTRIBUTE_PREFIX; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; @@ -81,6 +82,14 @@ public class LoginSamlAuthenticationProviderTests extends JdbcTestBase { public static final String UAA_SAML_USER = "uaa.saml.user"; public static final String UAA_SAML_ADMIN = "uaa.saml.admin"; public static final String UAA_SAML_TEST = "uaa.saml.test"; + + public static final String COST_CENTER = "costCenter"; + public static final String DENVER_CO = "Denver,CO"; + public static final String MANAGER = "manager"; + public static final String JOHN_THE_SLOTH = "John the Sloth"; + public static final String KARI_THE_ANT_EATER = "Kari the Ant Eater"; + + IdentityProviderProvisioning providerProvisioning; ApplicationEventPublisher publisher; JdbcUaaUserDatabase userDatabase; @@ -141,6 +150,14 @@ public void configureProvider() throws Exception { consumer = mock(WebSSOProfileConsumer.class); credential = getUserCredential("marissa-saml", "Marissa", "Bloggs", "marissa.bloggs@test.com", "1234567890"); + Map attributes = new HashMap<>(); + attributes.put("firstName", "Marissa"); + attributes.put("lastName", "Bloggs"); + attributes.put("emailAddress", "marissa.bloggs@test.com"); + attributes.put("phone", "1234567890"); + attributes.put("groups", Arrays.asList(SAML_USER,SAML_ADMIN,SAML_NOT_MAPPED)); + attributes.put("2ndgroups", Arrays.asList(SAML_TEST)); + when(consumer.processAuthenticationResponse(anyObject())).thenReturn(credential); userDatabase = new JdbcUaaUserDatabase(jdbcTemplate); @@ -180,6 +197,8 @@ private SAMLCredential getUserCredential(String username, String firstName, Stri attributes.put("phone", phoneNumber); attributes.put("groups", Arrays.asList(SAML_USER, SAML_ADMIN, SAML_NOT_MAPPED)); attributes.put("2ndgroups", Arrays.asList(SAML_TEST)); + attributes.put(COST_CENTER, Arrays.asList(DENVER_CO)); + attributes.put(MANAGER, Arrays.asList(JOHN_THE_SLOTH, KARI_THE_ANT_EATER)); return new SAMLCredential( usernameID, mock(Assertion.class), @@ -201,7 +220,7 @@ public void test_multiple_group_attributes() throws Exception { UaaAuthentication authentication = getAuthentication(); assertEquals("Four authorities should have been granted!", 4, authentication.getAuthorities().size()); assertThat(authentication.getAuthorities(), - Matchers.containsInAnyOrder( + containsInAnyOrder( new SimpleGrantedAuthority(UAA_SAML_ADMIN), new SimpleGrantedAuthority(UAA_SAML_USER), new SimpleGrantedAuthority(UAA_SAML_TEST), @@ -218,7 +237,7 @@ public void test_group_mapping() throws Exception { UaaAuthentication authentication = getAuthentication(); assertEquals("Three authorities should have been granted!", 3, authentication.getAuthorities().size()); assertThat(authentication.getAuthorities(), - Matchers.containsInAnyOrder( + containsInAnyOrder( new SimpleGrantedAuthority(UAA_SAML_ADMIN), new SimpleGrantedAuthority(UAA_SAML_USER), new SimpleGrantedAuthority(UaaAuthority.UAA_USER.getAuthority()) @@ -234,10 +253,10 @@ public void externalGroup_NotMapped_ToScope() throws Exception { UaaAuthentication authentication = getAuthentication(); assertEquals("Three authorities should have been granted!", 3, authentication.getAuthorities().size()); assertThat(authentication.getAuthorities(), - Matchers.containsInAnyOrder( - new SimpleGrantedAuthority(UAA_SAML_ADMIN), - new SimpleGrantedAuthority(UAA_SAML_USER), - new SimpleGrantedAuthority(UaaAuthority.UAA_USER.getAuthority()) + containsInAnyOrder( + new SimpleGrantedAuthority(UAA_SAML_ADMIN), + new SimpleGrantedAuthority(UAA_SAML_USER), + new SimpleGrantedAuthority(UaaAuthority.UAA_USER.getAuthority()) ) ); } @@ -256,7 +275,7 @@ public void add_external_groups_to_authentication_without_whitelist() throws Exc providerProvisioning.update(provider); UaaAuthentication authentication = getAuthentication(); - assertThat(authentication.getExternalGroups(), Matchers.containsInAnyOrder(SAML_ADMIN, SAML_USER, SAML_NOT_MAPPED)); + assertThat(authentication.getExternalGroups(), containsInAnyOrder(SAML_ADMIN, SAML_USER, SAML_NOT_MAPPED)); } @Test @@ -329,11 +348,37 @@ public void shadowUser_GetsCreatedWithDefaultValues_IfAttributeNotMapped() throw provider.setConfig(JsonUtils.writeValueAsString(providerDefinition)); providerProvisioning.update(provider); - getAuthentication(); + UaaAuthentication authentication = getAuthentication(); UaaUser user = userDatabase.retrieveUserByName("marissa-saml", Origin.SAML); assertEquals("marissa.bloggs", user.getGivenName()); assertEquals("test.com", user.getFamilyName()); assertEquals("marissa.bloggs@test.com", user.getEmail()); + assertEquals("No custom attributes have been mapped", 0, authentication.getUserAttributes().size()); + } + + @Test + public void user_authentication_contains_custom_attributes() throws Exception { + String COST_CENTERS = COST_CENTER+"s"; + String MANAGERS = MANAGER+"s"; + + Map attributeMappings = new HashMap<>(); + + attributeMappings.put(USER_ATTRIBUTE_PREFIX+COST_CENTERS, COST_CENTER); + attributeMappings.put(USER_ATTRIBUTE_PREFIX+MANAGERS, MANAGER); + + providerDefinition.setAttributeMappings(attributeMappings); + provider.setConfig(JsonUtils.writeValueAsString(providerDefinition)); + providerProvisioning.update(provider); + + UaaAuthentication authentication = getAuthentication(); + + assertEquals("Expected two user attributes", 2, authentication.getUserAttributes().size()); + assertNotNull("Expected cost center attribute", authentication.getUserAttributes().get(COST_CENTERS)); + assertEquals(DENVER_CO, authentication.getUserAttributes().getFirst(COST_CENTERS)); + + assertNotNull("Expected manager attribute", authentication.getUserAttributes().get(MANAGERS)); + assertEquals("Expected 2 manager attribute values", 2, authentication.getUserAttributes().get(MANAGERS).size()); + assertThat(authentication.getUserAttributes().get(MANAGERS), containsInAnyOrder(JOHN_THE_SLOTH, KARI_THE_ANT_EATER)); } protected UaaAuthentication getAuthentication() { diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java index f3713e91c4c..aef470a8cea 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java @@ -903,7 +903,8 @@ public void testLdapScopesFromChainedAuth() throws Exception { "roles", "oauth.approvals", "uaa.user", - "cloud_controller.read" + "cloud_controller.read", + "user_attributes" }; assertThat(list, arrayContainingInAnyOrder(getAuthorities(auth.getAuthorities()))); } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/CheckDefaultAuthoritiesMvcMockTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/CheckDefaultAuthoritiesMvcMockTests.java index 815feccc8ff..d3350bdc035 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/CheckDefaultAuthoritiesMvcMockTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/CheckDefaultAuthoritiesMvcMockTests.java @@ -34,7 +34,7 @@ public void setUp() throws Exception { @Test public void testDefaultAuthorities() throws Exception { - Assert.assertEquals(12, defaultAuthorities.size()); + Assert.assertEquals(13, defaultAuthorities.size()); String[] expected = new String[] { "openid", "scim.me", @@ -47,7 +47,8 @@ public void testDefaultAuthorities() throws Exception { "approvals.me", "oauth.approvals", "profile", - "roles" + "roles", + "user_attributes" }; for (String s : expected) { Assert.assertTrue("Expecting authority to be present:"+s,defaultAuthorities.contains(s)); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java index c36da445d07..059b6fb8af5 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java @@ -50,7 +50,6 @@ import org.junit.Test; import org.springframework.http.MediaType; import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.mock.env.MockEnvironment; import org.springframework.mock.web.MockHttpSession; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -102,7 +101,6 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; @@ -841,7 +839,7 @@ public void testOpenIdToken() throws Exception { //authorization_code grant - requesting id_token UaaPrincipal p = new UaaPrincipal(developer.getId(),developer.getUserName(),developer.getPrimaryEmail(), Origin.UAA,"", IdentityZoneHolder.get().getId()); - UaaAuthentication auth = new UaaAuthentication(p, UaaAuthority.USER_AUTHORITIES, mock(UaaAuthenticationDetails.class)); + UaaAuthentication auth = new UaaAuthentication(p, UaaAuthority.USER_AUTHORITIES, new UaaAuthenticationDetails(false, "clientId",Origin.ORIGIN,"sessionId")); Assert.assertTrue(auth.isAuthenticated()); SecurityContextHolder.getContext().setAuthentication(auth); @@ -1254,6 +1252,7 @@ public void testWildcardPasswordGrant() throws Exception { set1.remove("openid"); set1.remove("profile"); set1.remove("roles"); + set1.remove(Claims.USER_ATTRIBUTES); validatePasswordGrantToken( clientId, userId, From 6d7622be372b5f5006eb35c7f4711133b6b9d17e Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Thu, 22 Oct 2015 11:21:08 -0700 Subject: [PATCH 080/103] Don't populate id token with external groups if whitelist is not specified or is empty [finishes #106326860] https://www.pivotaltracker.com/story/show/106326860 --- .../uaa/login/saml/LoginSamlAuthenticationProvider.java | 3 --- .../uaa/login/saml/LoginSamlAuthenticationProviderTests.java | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java b/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java index 040655a48df..e5b548a7c79 100644 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java @@ -154,9 +154,6 @@ private Set filterSamlAuthorities(SamlIdentityProviderDefinition definit whiteList = definition.getExternalGroupsWhitelist(); } Set authorities = samlAuthorities.stream().map(s -> s.getAuthority()).collect(Collectors.toSet()); - if (whiteList.isEmpty()) { - return authorities; - } return new HashSet<>(CollectionUtils.retainAll(authorities, whiteList)); } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java index 662974533d5..f062ef7aace 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProviderTests.java @@ -269,13 +269,13 @@ public void test_group_attribute_not_set() throws Exception { } @Test - public void add_external_groups_to_authentication_without_whitelist() throws Exception { + public void dontAdd_external_groups_to_authentication_without_whitelist() throws Exception { providerDefinition.addAttributeMapping(ExternalIdentityProviderDefinition.GROUP_ATTRIBUTE_NAME, "groups"); provider.setConfig(JsonUtils.writeValueAsString(providerDefinition)); providerProvisioning.update(provider); UaaAuthentication authentication = getAuthentication(); - assertThat(authentication.getExternalGroups(), containsInAnyOrder(SAML_ADMIN, SAML_USER, SAML_NOT_MAPPED)); + assertEquals(Collections.EMPTY_SET, authentication.getExternalGroups()); } @Test From ff4485af63dc19c16452f47e1526323810949d41 Mon Sep 17 00:00:00 2001 From: Jeremy Coffield Date: Thu, 22 Oct 2015 09:41:22 -0700 Subject: [PATCH 081/103] Add external groups from LDAP to OpenID token. Signed-off-by: Paul Warren --- .../uaa/authentication/UaaAuthentication.java | 72 +++++++--- .../ExternalLoginAuthenticationManager.java | 43 +++--- .../LdapLoginAuthenticationManager.java | 69 ++++++++++ .../manager/UaaAuthenticationPrototype.java | 126 ++++++++++++++++++ ...xternalLoginAuthenticationManagerTest.java | 8 +- .../LdapLoginAuthenticationManagerTests.java | 6 +- .../saml/LoginSamlAuthenticationProvider.java | 4 +- .../uaa/zone/IdentityProviderEndpoints.java | 4 + uaa/src/main/resources/ldap_init.ldif | 8 ++ .../main/webapp/WEB-INF/spring-servlet.xml | 5 + .../uaa/mock/ldap/LdapMockMvcTests.java | 40 ++++++ 11 files changed, 340 insertions(+), 45 deletions(-) create mode 100644 common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/UaaAuthenticationPrototype.java diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java index e638f2fa9f1..c8a95649bc6 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java @@ -14,6 +14,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.cloudfoundry.identity.uaa.authentication.manager.UaaAuthenticationPrototype; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.util.LinkedMultiValueMap; @@ -49,12 +50,16 @@ public class UaaAuthentication implements Authentication, Serializable { * Creates a token with the supplied array of authorities. * * @param authorities the collection of GrantedAuthoritys for the - * principal represented by this authentication object. + * principal represented by this authentication object. */ public UaaAuthentication(UaaPrincipal principal, List authorities, UaaAuthenticationDetails details) { - this(principal, null, authorities, details, true, System.currentTimeMillis()); + this(UaaAuthenticationPrototype.alreadyAuthenticated() + .withPrincipal(principal) + .withDetails(details) + .withAuthorities(authorities) + ); } public UaaAuthentication(UaaPrincipal principal, @@ -63,9 +68,16 @@ public UaaAuthentication(UaaPrincipal principal, UaaAuthenticationDetails details, boolean authenticated, long authenticatedTime) { - this(principal, credentials, authorities, details, authenticated, authenticatedTime, -1); - } - + this(UaaAuthenticationPrototype.notYetAuthenticated() + .withPrincipal(principal) + .withAuthenticated(authenticated) + .withAuthenticatedTime(authenticatedTime) + .withDetails(details) + .withAuthorities(authorities) + .withCredentials(credentials) + ); + } + public UaaAuthentication(UaaPrincipal principal, Object credentials, List authorities, @@ -73,16 +85,15 @@ public UaaAuthentication(UaaPrincipal principal, boolean authenticated, long authenticatedTime, long expiresAt) { - if (principal == null || authorities == null) { - throw new IllegalArgumentException("principal and authorities must not be null"); - } - this.principal = principal; - this.authorities = authorities; - this.details = details; - this.credentials = credentials; - this.authenticated = authenticated; - this.authenticatedTime = authenticatedTime <= 0 ? -1 : authenticatedTime; - this.expiresAt = expiresAt <= 0 ? -1 : expiresAt; + this(UaaAuthenticationPrototype.notYetAuthenticated() + .withPrincipal(principal) + .withAuthenticated(authenticated) + .withAuthenticatedTime(authenticatedTime) + .withDetails(details) + .withExpiresAt(expiresAt) + .withAuthorities(authorities) + .withCredentials(credentials) + ); } public UaaAuthentication(UaaPrincipal uaaPrincipal, @@ -94,9 +105,34 @@ public UaaAuthentication(UaaPrincipal uaaPrincipal, boolean authenticated, long authenticatedTime, long expiresAt) { - this(uaaPrincipal, credentials, uaaAuthorityList, details, authenticated, authenticatedTime, expiresAt); - this.externalGroups = externalGroups; - this.userAttributes = new HashMap<>(userAttributes); + this(UaaAuthenticationPrototype.notYetAuthenticated() + .withPrincipal(uaaPrincipal) + .withAuthenticated(authenticated) + .withAuthenticatedTime(authenticatedTime) + .withExternalGroups(externalGroups) + .withDetails(details) + .withExpiresAt(expiresAt) + .withAuthorities(uaaAuthorityList) + .withCredentials(credentials) + .withAttributes(userAttributes) + ); + } + + public UaaAuthentication(UaaAuthenticationPrototype prototype) { + if (prototype.getPrincipal() == null || prototype.getAuthorities() == null) { + throw new IllegalArgumentException("principal and authorities must not be null"); + } + this.principal = prototype.getPrincipal(); + this.authorities = prototype.getAuthorities(); + this.details = prototype.getDetails(); + this.credentials = prototype.getCredentials(); + this.authenticated = prototype.isAuthenticated(); + this.authenticatedTime = prototype.getAuthenticatedTime(); + if (this.authenticatedTime <= 0) this.authenticatedTime = -1; + this.expiresAt = prototype.getExpiresAt(); + if (this.expiresAt <= 0) this.expiresAt = -1; + this.externalGroups = prototype.getExternalGroups(); + this.userAttributes = prototype.getAttributes(); } public long getAuthenticatedTime() { diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java index 9396b32593d..0b4c4435948 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java @@ -45,7 +45,7 @@ import java.util.Collections; import java.util.Date; -import java.util.Map; +import java.util.Set; public class ExternalLoginAuthenticationManager implements AuthenticationManager, ApplicationEventPublisherAware, BeanNameAware { @@ -83,6 +83,10 @@ public UaaUserDatabase getUserDatabase() { return this.userDatabase; } + protected Set getExternalGroups(Object principal) { + return Collections.EMPTY_SET; + } + @Override public Authentication authenticate(Authentication request) throws AuthenticationException { UaaUser user = getUser(request); @@ -113,13 +117,18 @@ public Authentication authenticate(Authentication request) throws Authentication //user is authenticated and exists in UAA user = userAuthenticated(request, user); - UaaAuthenticationDetails uaaAuthenticationDetails = null; + UaaAuthenticationDetails uaaAuthenticationDetails; if (request.getDetails() instanceof UaaAuthenticationDetails) { uaaAuthenticationDetails = (UaaAuthenticationDetails) request.getDetails(); } else { uaaAuthenticationDetails = UaaAuthenticationDetails.UNKNOWN; } - Authentication success = new UaaAuthentication(new UaaPrincipal(user), user.getAuthorities(), uaaAuthenticationDetails); + Authentication success = new UaaAuthentication(UaaAuthenticationPrototype.alreadyAuthenticated() + .withPrincipal(new UaaPrincipal(user)) + .withAuthorities(user.getAuthorities()) + .withDetails(uaaAuthenticationDetails) + .withExternalGroups(getExternalGroups(request.getPrincipal())) + ); publish(new UserAuthenticationSuccessEvent(user, success)); return success; } @@ -185,11 +194,11 @@ protected UaaUser getUser(Authentication request) { familyName = names.getFamilyName(); } - if(givenName == null) { + if (givenName == null) { givenName = email.split("@")[0]; } - if(familyName == null) { + if (familyName == null) { familyName = email.split("@")[1]; } @@ -197,18 +206,18 @@ protected UaaUser getUser(Authentication request) { String externalId = (userDetails instanceof ExternallyIdentifiable) ? ((ExternallyIdentifiable) userDetails).getExternalId() : name; UaaUserPrototype userPrototype = new UaaUserPrototype() - .withUsername(name) - .withPassword("") - .withEmail(email) - .withAuthorities(UaaAuthority.USER_AUTHORITIES) - .withGivenName(givenName) - .withFamilyName(familyName) - .withCreated(new Date()) - .withModified(new Date()) - .withOrigin(origin) - .withExternalId(externalId) - .withZoneId(IdentityZoneHolder.get().getId()) - .withPhoneNumber(phoneNumber); + .withUsername(name) + .withPassword("") + .withEmail(email) + .withAuthorities(UaaAuthority.USER_AUTHORITIES) + .withGivenName(givenName) + .withFamilyName(familyName) + .withCreated(new Date()) + .withModified(new Date()) + .withOrigin(origin) + .withExternalId(externalId) + .withZoneId(IdentityZoneHolder.get().getId()) + .withPhoneNumber(phoneNumber); return new UaaUser(userPrototype); } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java index 5354e75a197..114ee770c3b 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java @@ -16,18 +16,58 @@ package org.cloudfoundry.identity.uaa.authentication.manager; import org.apache.commons.lang.StringUtils; +import org.cloudfoundry.identity.uaa.authentication.Origin; import org.cloudfoundry.identity.uaa.ldap.ExtendedLdapUserDetails; +import org.cloudfoundry.identity.uaa.ldap.LdapIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.ldap.extension.SpringSecurityLdapTemplate; import org.cloudfoundry.identity.uaa.user.UaaUser; import org.cloudfoundry.identity.uaa.user.UaaUserPrototype; +import org.cloudfoundry.identity.uaa.zone.IdentityProvider; +import org.cloudfoundry.identity.uaa.zone.IdentityProviderProvisioning; +import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; +import org.springframework.ldap.core.ContextSource; import org.springframework.security.core.Authentication; import java.util.Collections; import java.util.Date; +import javax.naming.directory.SearchControls; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + public class LdapLoginAuthenticationManager extends ExternalLoginAuthenticationManager { + private final IdentityProviderProvisioning idpProvisioning; + + private final SpringSecurityLdapTemplate ldapTemplate; + private final String groupSearchBase; + private final String groupSearchFilter; + private boolean autoAddAuthorities = false; + public LdapLoginAuthenticationManager(IdentityProviderProvisioning idpProvisioning, ContextSource contextSource, String groupSearchBase, String groupSearchFilter, boolean searchSubtree) { + this.idpProvisioning = idpProvisioning; + this.groupSearchBase = groupSearchBase; + this.groupSearchFilter = groupSearchFilter; + + if (contextSource != null) { + ldapTemplate = new SpringSecurityLdapTemplate(contextSource); + + SearchControls searchControls = new SearchControls(); + int searchScope = searchSubtree ? SearchControls.SUBTREE_SCOPE : SearchControls.ONELEVEL_SCOPE; + searchControls.setSearchScope(searchScope); + ldapTemplate.setSearchControls(searchControls); + + ldapTemplate.setIgnorePartialResultException(true); + } else { + ldapTemplate = null; + } + } + @Override protected UaaUser userAuthenticated(Authentication request, UaaUser user) { boolean userModified = false; @@ -52,6 +92,35 @@ public void setAutoAddAuthorities(boolean autoAddAuthorities) { this.autoAddAuthorities = autoAddAuthorities; } + @Override + protected Set getExternalGroups(Object principal) { + if(idpProvisioning == null || ldapTemplate == null) return Collections.EMPTY_SET; + + IdentityProvider idp = idpProvisioning.retrieveByOrigin(Origin.LDAP, IdentityZoneHolder.get().getId()); + LdapIdentityProviderDefinition def = idp.getConfigValue(LdapIdentityProviderDefinition.class); + List whitelist = def.getExternalGroupsWhitelist(); + + Set groups = new HashSet<>(); + + if (principal instanceof ExtendedLdapUserDetails) { + Set> userRoles = ldapTemplate.searchForMultipleAttributeValues( + groupSearchBase, + groupSearchFilter, + new String[]{((ExtendedLdapUserDetails)principal).getDn()}, + new String[] {"cn"}); + + for (Map row : userRoles) { + for(String groupName : row.get("cn")) { + if (whitelist.contains(groupName)) { + groups.add(groupName); + } + } + } + } + + return groups; + } + private boolean haveUserAttributesChanged(UaaUser existingUser, UaaUser user) { if (!StringUtils.equals(existingUser.getGivenName(), user.getGivenName()) || !StringUtils.equals(existingUser.getFamilyName(), user.getFamilyName()) || !StringUtils.equals(existingUser.getPhoneNumber(), user.getPhoneNumber()) || !StringUtils.equals(existingUser.getEmail(), user.getEmail())) { diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/UaaAuthenticationPrototype.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/UaaAuthenticationPrototype.java new file mode 100644 index 00000000000..80d0380e19a --- /dev/null +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/UaaAuthenticationPrototype.java @@ -0,0 +1,126 @@ +/* + * ****************************************************************************** + * Cloud Foundry Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + * + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + * ****************************************************************************** + */ + +package org.cloudfoundry.identity.uaa.authentication.manager; + +import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; +import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; +import org.springframework.security.core.GrantedAuthority; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class UaaAuthenticationPrototype { + private List authorities; + private Object credentials = null; + private UaaPrincipal principal; + private UaaAuthenticationDetails details; + private boolean authenticated; + private long authenticatedTime = System.currentTimeMillis(); + private long expiresAt = -1l; + private Set externalGroups; + private Map> attributes; + + private UaaAuthenticationPrototype() { + } + + public static UaaAuthenticationPrototype alreadyAuthenticated() { + return new UaaAuthenticationPrototype().withAuthenticated(true).withAuthenticatedTime(System.currentTimeMillis()); + } + + public static UaaAuthenticationPrototype notYetAuthenticated() { + return new UaaAuthenticationPrototype(); + } + + public UaaAuthenticationPrototype withAuthorities(List authorities) { + this.authorities = authorities; + return this; + } + + public UaaAuthenticationPrototype withCredentials(Object credentials) { + this.credentials = credentials; + return this; + } + + public UaaAuthenticationPrototype withPrincipal(UaaPrincipal principal){ + this.principal = principal; + return this; + } + + public UaaAuthenticationPrototype withDetails(UaaAuthenticationDetails details) { + this.details = details; + return this; + } + + public UaaAuthenticationPrototype withAuthenticated(boolean authenticated) { + this.authenticated = authenticated; + return this; + } + + public UaaAuthenticationPrototype withAuthenticatedTime(long authenticatedTime) { + this.authenticatedTime = authenticatedTime; + return this; + } + + public UaaAuthenticationPrototype withExpiresAt(long expiresAt) { + this.expiresAt = expiresAt; + return this; + } + + public UaaAuthenticationPrototype withExternalGroups(Set externalGroups) { + this.externalGroups = externalGroups; + return this; + } + + public UaaAuthenticationPrototype withAttributes(Map> attributes) { + this.attributes = attributes; + return this; + } + + + public List getAuthorities() { + return authorities; + } + + public Object getCredentials() { + return credentials; + } + + public UaaPrincipal getPrincipal() { + return principal; + } + + public UaaAuthenticationDetails getDetails() { + return details; + } + + public boolean isAuthenticated() { + return authenticated; + } + + public long getAuthenticatedTime() { + return authenticatedTime; + } + + public long getExpiresAt() { + return expiresAt; + } + + public Set getExternalGroups() { + return externalGroups; + } + + public Map> getAttributes() { return attributes; } +} diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManagerTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManagerTest.java index 56117b9a0d1..8541ec1374d 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManagerTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManagerTest.java @@ -246,7 +246,7 @@ public void testAuthenticateLdapUserDetailsPrincipal() throws Exception { LdapUserDetails ldapUserDetails = mock(LdapUserDetails.class); mockUserDetails(ldapUserDetails); when(ldapUserDetails.getDn()).thenReturn(dn); - manager = new LdapLoginAuthenticationManager(); + manager = new LdapLoginAuthenticationManager(null, null, null, null, true); setupManager(); manager.setOrigin(origin); when(user.getOrigin()).thenReturn(origin); @@ -277,7 +277,7 @@ public void testAuthenticateCreateUserWithLdapUserDetailsPrincipal() throws Exce ExtendedLdapUserImpl ldapUserDetails = new ExtendedLdapUserImpl(baseLdapUserDetails, ldapAttrs); ldapUserDetails.setMailAttributeName(ldapMailAttrName); - manager = new LdapLoginAuthenticationManager(); + manager = new LdapLoginAuthenticationManager(null, null, null, null, true); setupManager(); manager.setOrigin(origin); when(user.getEmail()).thenReturn(email); @@ -309,7 +309,7 @@ public void testAuthenticateCreateUserWithLdapUserDetailsPrincipal() throws Exce public void testAuthenticateCreateUserWithUserDetailsPrincipal() throws Exception { String origin = "ldap"; - manager = new LdapLoginAuthenticationManager(); + manager = new LdapLoginAuthenticationManager(null, null, null, null, true); setupManager(); manager.setOrigin(origin); @@ -372,4 +372,4 @@ public void testAuthenticateUserDoesNotExists() throws Exception { -} \ No newline at end of file +} diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java index 4eec6427f30..30bcccf98fe 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java @@ -37,8 +37,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyObject; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -91,7 +89,7 @@ private static void setupGeneralExpectations(UserDetails userDetails) { @Before public void setUp() { - am = new LdapLoginAuthenticationManager(); + am = new LdapLoginAuthenticationManager(null, null, null, null, true); publisher = mock(ApplicationEventPublisher.class); am.setApplicationEventPublisher(publisher); am.setOrigin(origin); @@ -202,4 +200,4 @@ protected UaaUser getUaaUser() { .withSalt(null) .withPasswordLastModified(null)); } -} \ No newline at end of file +} diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java b/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java index e5b548a7c79..010644a1d1e 100644 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java @@ -113,19 +113,19 @@ public Authentication authenticate(Authentication authentication) throws Authent SAMLAuthenticationToken token = (SAMLAuthenticationToken) authentication; SAMLMessageContext context = token.getCredentials(); String alias = context.getPeerExtendedMetadata().getAlias(); - boolean addNew = true; IdentityProvider idp; SamlIdentityProviderDefinition samlConfig; try { idp = identityProviderProvisioning.retrieveByOrigin(alias, IdentityZoneHolder.get().getId()); samlConfig = idp.getConfigValue(SamlIdentityProviderDefinition.class); - addNew = samlConfig.isAddShadowUserOnLogin(); + if (!idp.isActive()) { throw new ProviderNotFoundException("Identity Provider has been disabled by administrator."); } } catch (EmptyResultDataAccessException x) { throw new ProviderNotFoundException("Not identity provider found in zone."); } + boolean addNew = samlConfig.isAddShadowUserOnLogin(); ExpiringUsernameAuthenticationToken result = getExpiringUsernameAuthenticationToken(authentication); UaaPrincipal samlPrincipal = new UaaPrincipal(Origin.NotANumber, result.getName(), result.getName(), alias, result.getName(), zone.getId()); Collection samlAuthorities = retrieveSamlAuthorities(samlConfig, (SAMLCredential) result.getCredentials()); diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProviderEndpoints.java b/login/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProviderEndpoints.java index f1950a4432b..9b653987daf 100644 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProviderEndpoints.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProviderEndpoints.java @@ -187,6 +187,10 @@ protected String getExceptionString(Exception x) { } protected static class NoOpLdapLoginAuthenticationManager extends LdapLoginAuthenticationManager { + protected NoOpLdapLoginAuthenticationManager() { + super(null, null, null, null, true); + } + @Override public Authentication authenticate(Authentication request) throws AuthenticationException { return request; diff --git a/uaa/src/main/resources/ldap_init.ldif b/uaa/src/main/resources/ldap_init.ldif index cba440ff3e3..5b0f3c2a381 100644 --- a/uaa/src/main/resources/ldap_init.ldif +++ b/uaa/src/main/resources/ldap_init.ldif @@ -150,6 +150,14 @@ cn: uaa.admin member: cn=admin,ou=Users,dc=test,dc=com member: cn=marissa3,ou=Users,dc=test,dc=com +#This groups contains scopes as comma separated list in the description attribute +dn: cn=thirdmarissa,ou=scopes,dc=test,dc=com +changetype: add +objectClass: groupOfNames +objectClass: top +cn: thirdmarissa +member: cn=marissa3,ou=Users,dc=test,dc=com + dn: cn=developers,ou=scopes,dc=test,dc=com changetype: add objectClass: groupOfNames diff --git a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml index c564aea8bd2..94bc4071e99 100755 --- a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml +++ b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml @@ -303,6 +303,11 @@ + + + + + diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java index aef470a8cea..117180be3fd 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java @@ -14,6 +14,7 @@ import org.cloudfoundry.identity.uaa.TestClassNullifier; import org.cloudfoundry.identity.uaa.authentication.Origin; +import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; import org.cloudfoundry.identity.uaa.authentication.manager.AuthzAuthenticationManager; import org.cloudfoundry.identity.uaa.authentication.manager.ChainedAuthenticationManager; import org.cloudfoundry.identity.uaa.ldap.ExtendedLdapUserMapper; @@ -76,11 +77,13 @@ import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.CookieCsrfPostProcessor.cookieCsrf; import static org.hamcrest.Matchers.arrayContainingInAnyOrder; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; @@ -614,6 +617,43 @@ public void testLogin_partial_result_exception_on_group_search() throws Exceptio } + @Test + public void external_groups () throws Exception { + setUp(); + IdentityProviderProvisioning idpProvisioning = webApplicationContext.getBean(IdentityProviderProvisioning.class); + IdentityProvider idp = idpProvisioning.retrieveByOrigin(Origin.LDAP, IdentityZone.getUaa().getId()); + LdapIdentityProviderDefinition def = idp.getConfigValue(LdapIdentityProviderDefinition.class); + def.addWhiteListedGroup("admins"); + def.addWhiteListedGroup("thirdmarissa"); + idp.setConfig(JsonUtils.writeValueAsString(def)); + idpProvisioning.update(idp); + + AuthenticationManager manager = webApplicationContext.getBean(ChainedAuthenticationManager.class); + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("marissa3","ldap3"); + Authentication auth = manager.authenticate(token); + assertNotNull(auth); + assertTrue(auth instanceof UaaAuthentication); + UaaAuthentication uaaAuth = (UaaAuthentication)auth; + Set externalGroups = uaaAuth.getExternalGroups(); + assertNotNull(externalGroups); + assertEquals(2, externalGroups.size()); + assertThat(externalGroups, containsInAnyOrder("admins", "thirdmarissa")); + } + + @Test + public void external_groups_with_default_whitelist () throws Exception { + setUp(); + + AuthenticationManager manager = webApplicationContext.getBean(ChainedAuthenticationManager.class); + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("marissa3","ldap3"); + Authentication auth = manager.authenticate(token); + assertNotNull(auth); + assertTrue(auth instanceof UaaAuthentication); + UaaAuthentication uaaAuth = (UaaAuthentication)auth; + Set externalGroups = uaaAuth.getExternalGroups(); + assertNotNull(externalGroups); + assertEquals(0, externalGroups.size()); + } @Test public void runLdapTestblock() throws Exception { From e147db27a8dbf80b949367d3d83305919790259b Mon Sep 17 00:00:00 2001 From: Jonathan Lo Date: Thu, 22 Oct 2015 12:17:55 -0700 Subject: [PATCH 082/103] Change resource import scope for LDAP integration. [#105497272] https://www.pivotaltracker.com/story/show/105497272 Signed-off-by: Jeremy Coffield --- uaa/src/main/webapp/WEB-INF/spring-servlet.xml | 5 ++--- .../org/cloudfoundry/identity/uaa/login/BootstrapTests.java | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml index 94bc4071e99..e580d4d2b60 100755 --- a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml +++ b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml @@ -302,6 +302,8 @@ + + @@ -313,10 +315,7 @@ - - - diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java index c0c66362481..27a3a252723 100755 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java @@ -18,7 +18,6 @@ import org.cloudfoundry.identity.uaa.authentication.Origin; import org.cloudfoundry.identity.uaa.authentication.login.Prompt; import org.cloudfoundry.identity.uaa.authentication.manager.PeriodLockoutPolicy; -import org.cloudfoundry.identity.uaa.config.IdentityProviderBootstrap; import org.cloudfoundry.identity.uaa.config.LockoutPolicy; import org.cloudfoundry.identity.uaa.config.PasswordPolicy; import org.cloudfoundry.identity.uaa.config.YamlServletProfileInitializer; @@ -644,4 +643,4 @@ public RequestDispatcher getNamedDispatcher(String path) { return context; } -} \ No newline at end of file +} From 44f4ab314848c444bf1c9ce75f669dcda6dc47f4 Mon Sep 17 00:00:00 2001 From: Jeremy Coffield Date: Thu, 22 Oct 2015 13:55:04 -0700 Subject: [PATCH 083/103] Move UaaAuthenticationPrototype to correct namespace. Signed-off-by: Jonathan Lo --- .../identity/uaa/authentication/UaaAuthentication.java | 1 - .../{manager => }/UaaAuthenticationPrototype.java | 4 +--- .../manager/ExternalLoginAuthenticationManager.java | 1 + 3 files changed, 2 insertions(+), 4 deletions(-) rename common/src/main/java/org/cloudfoundry/identity/uaa/authentication/{manager => }/UaaAuthenticationPrototype.java (94%) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java index c8a95649bc6..d699626ede5 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java @@ -14,7 +14,6 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.cloudfoundry.identity.uaa.authentication.manager.UaaAuthenticationPrototype; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.util.LinkedMultiValueMap; diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/UaaAuthenticationPrototype.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationPrototype.java similarity index 94% rename from common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/UaaAuthenticationPrototype.java rename to common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationPrototype.java index 80d0380e19a..188e7b6ecdf 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/UaaAuthenticationPrototype.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationPrototype.java @@ -12,10 +12,8 @@ * ****************************************************************************** */ -package org.cloudfoundry.identity.uaa.authentication.manager; +package org.cloudfoundry.identity.uaa.authentication; -import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; -import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.springframework.security.core.GrantedAuthority; import java.util.List; diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java index 0b4c4435948..45a0fb042d3 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java @@ -19,6 +19,7 @@ import org.apache.commons.logging.LogFactory; import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; +import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationPrototype; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.cloudfoundry.identity.uaa.authentication.event.UserAuthenticationSuccessEvent; import org.cloudfoundry.identity.uaa.user.DialableByPhone; From 10a561873f1c042c726e5d0a70052c996b1dcf01 Mon Sep 17 00:00:00 2001 From: Jonathan Lo Date: Thu, 22 Oct 2015 14:05:59 -0700 Subject: [PATCH 084/103] {Git swallowed our merge commit and retroactively fast-forwarded...} [finishes #105497272] https://www.pivotaltracker.com/story/show/105497272 From e799dd264b9bc98fb3f0f38115d8195540cb22d5 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Thu, 22 Oct 2015 16:48:11 -0700 Subject: [PATCH 085/103] Update docs with attributeMapping and externalGroupWhitelist configuration on IDPs [finishes #106251512] https://www.pivotaltracker.com/story/show/106251512 --- docs/UAA-APIs.rst | 60 +++++++++++++++++--------------- uaa/src/main/resources/login.yml | 8 +++++ uaa/src/main/resources/uaa.yml | 3 ++ 3 files changed, 43 insertions(+), 28 deletions(-) diff --git a/docs/UAA-APIs.rst b/docs/UAA-APIs.rst index 9d6325aad8d..4e45af6d09d 100644 --- a/docs/UAA-APIs.rst +++ b/docs/UAA-APIs.rst @@ -1103,36 +1103,40 @@ Fields *Available Fields* :: SAML Provider Configuration (provided in JSON format as part of the ``config`` field on the Identity Provider - See class org.cloudfoundry.identity.uaa.login.saml.SamlIdentityProviderDefinition ====================== =============== ======== ================================================================================================================================================================================================= - idpEntityAlias String Required Must match ``originKey`` in the provider definition - zoneId String Required Must match ``identityZoneId`` in the provider definition - metaDataLocation String Required SAML Metadata - either an XML string or a URL that will deliver XML content - nameID String Optional The name ID to use for the username, default is "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified". Currently the UAA expects the username to be a valid email address. - assertionConsumerIndex int Optional SAML assertion consumer index, default is 0 - metadataTrustCheck boolean Optional Should metadata be validated, defaults to false - showSamlLink boolean Optional Should the SAML login link be displayed on the login page, defaults to false - linkText String Optional Required if the ``showSamlLink`` is set to true. - iconUrl String Optional Reserved for future use - emailDomain List Optional List of email domains associated with the SAML provider for the purpose of associating users to the correct origin upon invitation. If null or empty list, no invitations are accepted. Wildcards supported. + idpEntityAlias String Required Must match ``originKey`` in the provider definition + zoneId String Required Must match ``identityZoneId`` in the provider definition + metaDataLocation String Required SAML Metadata - either an XML string or a URL that will deliver XML content + nameID String Optional The name ID to use for the username, default is "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified". Currently the UAA expects the username to be a valid email address. + assertionConsumerIndex int Optional SAML assertion consumer index, default is 0 + metadataTrustCheck boolean Optional Should metadata be validated, defaults to false + showSamlLink boolean Optional Should the SAML login link be displayed on the login page, defaults to false + linkText String Optional Required if the ``showSamlLink`` is set to true. + iconUrl String Optional Reserved for future use + emailDomain List Optional List of email domains associated with the SAML provider for the purpose of associating users to the correct origin upon invitation. If null or empty list, no invitations are accepted. Wildcards supported. + attributeMappings Map Optional List of UAA attributes mapped to attributes in the SAML assertion. Currently we support mapping given_name, family_name, email, phone_number and external_groups. + externalGroupsWhitelist List Optional List of external groups that will be included in the ID Token if the ROLES scope is requested. LDAP Provider Configuration (provided in JSON format as part of the ``config`` field on the Identity Provider - See class org.cloudfoundry.identity.uaa.ldap.LdapIdentityProviderDefinition ====================== =============== ======== ================================================================================================================================================================================================= - ldapProfileFile String Required Value must be "ldap/ldap-search-and-bind.xml" (until other configuration options are supported) - ldapGroupFile String Required Value must be "ldap/ldap-groups-map-to-scopes.xml" (until other configuration options are supported) - baseUrl String Required URL to LDAP server, starts with ldap:// or ldaps:// - bindUserDn String Required Valid user DN for an LDAP record that has permission to search the LDAP tree - bindPassword String Required Password for user the above ``bindUserDn`` - userSearchBase String Required search base - defines where in the LDAP tree the UAA will search for a user - userSearchFilter String Required user search filter used when searching for a user. {0} denotes the username in the search query. - groupSearchBase String Required search base - defines where in the LDAP tree the UAA will search for user groups - groupSearchFilter String Required Typically "memberOf={0}" group search filter used when searching for a group. {0} denotes the user DN in the search query, or the group DN in case of a nested group search. - mailAttributeName String Required the name of the attribute that contains the user's email address. In most cases this is "mail" - mailSubstitute String Optional If the user records do not contain an email address, the UAA can create one. It could be "{0}@unknown.org" where - mailSubstituteOverridesLdap boolean Optional Set to true only if you always wish to override the LDAP supplied user email address - autoAddGroups boolean Required Currently not used - groupSearchSubTree boolean Required Should the sub tree be searched for user groups - groupMaxSearchDepth int Required When searching for nested groups (groups within groups) - skipSSLVerification boolean Optional Set to true if you wish to skip SSL certificate verification - emailDomain List Optional List of email domains associated with the LDAP provider for the purpose of associating users to the correct origin upon invitation. If null or empty list, no invitations are accepted. Wildcards supported. + ldapProfileFile String Required Value must be "ldap/ldap-search-and-bind.xml" (until other configuration options are supported) + ldapGroupFile String Required Value must be "ldap/ldap-groups-map-to-scopes.xml" (until other configuration options are supported) + baseUrl String Required URL to LDAP server, starts with ldap:// or ldaps:// + bindUserDn String Required Valid user DN for an LDAP record that has permission to search the LDAP tree + bindPassword String Required Password for user the above ``bindUserDn`` + userSearchBase String Required search base - defines where in the LDAP tree the UAA will search for a user + userSearchFilter String Required user search filter used when searching for a user. {0} denotes the username in the search query. + groupSearchBase String Required search base - defines where in the LDAP tree the UAA will search for user groups + groupSearchFilter String Required Typically "memberOf={0}" group search filter used when searching for a group. {0} denotes the user DN in the search query, or the group DN in case of a nested group search. + mailAttributeName String Required the name of the attribute that contains the user's email address. In most cases this is "mail" + mailSubstitute String Optional If the user records do not contain an email address, the UAA can create one. It could be "{0}@unknown.org" where + mailSubstituteOverridesLdap boolean Optional Set to true only if you always wish to override the LDAP supplied user email address + autoAddGroups boolean Required Currently not used + groupSearchSubTree boolean Required Should the sub tree be searched for user groups + groupMaxSearchDepth int Required When searching for nested groups (groups within groups) + skipSSLVerification boolean Optional Set to true if you wish to skip SSL certificate verification + emailDomain List Optional List of email domains associated with the LDAP provider for the purpose of associating users to the correct origin upon invitation. If null or empty list, no invitations are accepted. Wildcards supported. + attributeMappings Map Optional List of UAA attributes mapped to attributes from LDAP. Currently we support mapping given_name, family_name, email, phone_number and external_groups. + externalGroupsWhitelist List Optional List of external groups that will be included in the ID Token if the ROLES scope is requested. Curl Example POST (Creating a SAML provider):: @@ -1150,7 +1154,7 @@ Curl Example POST (Creating an LDAP provider):: -XPOST -H"Accept:application/json" \ -H"Content-Type:application/json" \ -H"X-Identity-Zone-Id:testzone1" \ - -d '{"originKey":"ldap","name":"myldap for testzone1","type":"ldap","config":"{\"baseUrl\":\"ldaps://localhost:33636\",\"skipSSLVerification\":true,\"bindUserDn\":\"cn=admin,ou=Users,dc=test,dc=com\",\"bindPassword\":\"adminsecret\",\"userSearchBase\":\"dc=test,dc=com\",\"userSearchFilter\":\"cn={0}\",\"groupSearchBase\":\"ou=scopes,dc=test,dc=com\",\"groupSearchFilter\":\"member={0}\",\"mailAttributeName\":\"mail\",\"mailSubstitute\":null,\"ldapProfileFile\":\"ldap/ldap-search-and-bind.xml\",\"ldapGroupFile\":\"ldap/ldap-groups-map-to-scopes.xml\",\"mailSubstituteOverridesLdap\":false,\"autoAddGroups\":true,\"groupSearchSubTree\":true,\"maxGroupSearchDepth\":10,\"emailDomain\":[\"example.com\",\"another.example.com\"]}","active":true,"identityZoneId":"testzone1"}' \ + -d '{"originKey":"ldap","name":"myldap for testzone1","type":"ldap","config":"{\"baseUrl\":\"ldaps://localhost:33636\",\"skipSSLVerification\":true,\"bindUserDn\":\"cn=admin,ou=Users,dc=test,dc=com\",\"bindPassword\":\"adminsecret\",\"userSearchBase\":\"dc=test,dc=com\",\"userSearchFilter\":\"cn={0}\",\"groupSearchBase\":\"ou=scopes,dc=test,dc=com\",\"groupSearchFilter\":\"member={0}\",\"mailAttributeName\":\"mail\",\"mailSubstitute\":null,\"ldapProfileFile\":\"ldap/ldap-search-and-bind.xml\",\"ldapGroupFile\":\"ldap/ldap-groups-map-to-scopes.xml\",\"mailSubstituteOverridesLdap\":false,\"autoAddGroups\":true,\"groupSearchSubTree\":true,\"maxGroupSearchDepth\":10,\"emailDomain\":[\"example.com\",\"another.example.com\"]}",\"attributeMappings\":{"phone_number":"phone","given_name":"firstName","external_groups":"roles","family_name":"lastName","email":"email"},"externalGroupsWhitelist":["admin","user"],"active":true,"identityZoneId":"testzone1"}' \ http://localhost:8080/uaa/identity-providers Curl Example PUT (Updating a UAA provider):: diff --git a/uaa/src/main/resources/login.yml b/uaa/src/main/resources/login.yml index 7f28ed9b5bf..be6e13e40e9 100644 --- a/uaa/src/main/resources/login.yml +++ b/uaa/src/main/resources/login.yml @@ -132,6 +132,14 @@ login: # linkText: 'Okta Preview 1' # iconUrl: 'http://link.to/icon.jpg' # addShadowUserOnLogin: true +# externalGroupsWhitelist: +# - admin +# - user +# emailDomain: +# - example.com +# attributeMappings: +# given_name: firstName +# family_name: surname # okta-local-2: # idpMetadata: | # MIICmTCCAgKgAwIBAgIGAUPATqmEMA0GCSqGSIb3DQEBBQUAMIGPMQswCQYDVQQGEwJVUzETMBEG diff --git a/uaa/src/main/resources/uaa.yml b/uaa/src/main/resources/uaa.yml index 64e07c45177..6410920b132 100755 --- a/uaa/src/main/resources/uaa.yml +++ b/uaa/src/main/resources/uaa.yml @@ -89,6 +89,9 @@ # lQ23NhTaljASF0g8AZ7SZEItU8JFYqf/KnNJ7FPwo4LbMbr7Zg6BRKBvnQ== # -----END CERTIFICATE-----' # sslCertificateAlias: ldaps +# externalGroupsWhitelist: +# - admin +# - user # emailDomain: # - example.com # attributeMappings: From e06dd84d68bca9f05c1f1556c802c5fdcdc59fd2 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Fri, 23 Oct 2015 00:12:57 -0600 Subject: [PATCH 086/103] Revert "Move UaaAuthenticationPrototype to correct namespace." This reverts commit 44f4ab314848c444bf1c9ce75f669dcda6dc47f4. --- .../identity/uaa/authentication/UaaAuthentication.java | 1 + .../manager/ExternalLoginAuthenticationManager.java | 1 - .../{ => manager}/UaaAuthenticationPrototype.java | 4 +++- 3 files changed, 4 insertions(+), 2 deletions(-) rename common/src/main/java/org/cloudfoundry/identity/uaa/authentication/{ => manager}/UaaAuthenticationPrototype.java (94%) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java index d699626ede5..c8a95649bc6 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java @@ -14,6 +14,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.cloudfoundry.identity.uaa.authentication.manager.UaaAuthenticationPrototype; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.util.LinkedMultiValueMap; diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java index 45a0fb042d3..0b4c4435948 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java @@ -19,7 +19,6 @@ import org.apache.commons.logging.LogFactory; import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; -import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationPrototype; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.cloudfoundry.identity.uaa.authentication.event.UserAuthenticationSuccessEvent; import org.cloudfoundry.identity.uaa.user.DialableByPhone; diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationPrototype.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/UaaAuthenticationPrototype.java similarity index 94% rename from common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationPrototype.java rename to common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/UaaAuthenticationPrototype.java index 188e7b6ecdf..80d0380e19a 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthenticationPrototype.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/UaaAuthenticationPrototype.java @@ -12,8 +12,10 @@ * ****************************************************************************** */ -package org.cloudfoundry.identity.uaa.authentication; +package org.cloudfoundry.identity.uaa.authentication.manager; +import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; +import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.springframework.security.core.GrantedAuthority; import java.util.List; From 9b4497512cd016ca6f9dd5a57ca7cd739a868703 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Fri, 23 Oct 2015 00:13:09 -0600 Subject: [PATCH 087/103] Revert "Change resource import scope for LDAP integration." This reverts commit e147db27a8dbf80b949367d3d83305919790259b. --- uaa/src/main/webapp/WEB-INF/spring-servlet.xml | 5 +++-- .../org/cloudfoundry/identity/uaa/login/BootstrapTests.java | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml index e580d4d2b60..94bc4071e99 100755 --- a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml +++ b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml @@ -302,8 +302,6 @@ - - @@ -315,7 +313,10 @@ + + + diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java index 27a3a252723..c0c66362481 100755 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java @@ -18,6 +18,7 @@ import org.cloudfoundry.identity.uaa.authentication.Origin; import org.cloudfoundry.identity.uaa.authentication.login.Prompt; import org.cloudfoundry.identity.uaa.authentication.manager.PeriodLockoutPolicy; +import org.cloudfoundry.identity.uaa.config.IdentityProviderBootstrap; import org.cloudfoundry.identity.uaa.config.LockoutPolicy; import org.cloudfoundry.identity.uaa.config.PasswordPolicy; import org.cloudfoundry.identity.uaa.config.YamlServletProfileInitializer; @@ -643,4 +644,4 @@ public RequestDispatcher getNamedDispatcher(String path) { return context; } -} +} \ No newline at end of file From dff2246be99648f48e0f90946527711e43f47fa3 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Fri, 23 Oct 2015 00:13:25 -0600 Subject: [PATCH 088/103] Revert "Add external groups from LDAP to OpenID token." This reverts commit ff4485af63dc19c16452f47e1526323810949d41. --- .../uaa/authentication/UaaAuthentication.java | 72 +++------- .../ExternalLoginAuthenticationManager.java | 43 +++--- .../LdapLoginAuthenticationManager.java | 69 ---------- .../manager/UaaAuthenticationPrototype.java | 126 ------------------ ...xternalLoginAuthenticationManagerTest.java | 8 +- .../LdapLoginAuthenticationManagerTests.java | 6 +- .../saml/LoginSamlAuthenticationProvider.java | 4 +- .../uaa/zone/IdentityProviderEndpoints.java | 4 - uaa/src/main/resources/ldap_init.ldif | 8 -- .../main/webapp/WEB-INF/spring-servlet.xml | 5 - .../uaa/mock/ldap/LdapMockMvcTests.java | 40 ------ 11 files changed, 45 insertions(+), 340 deletions(-) delete mode 100644 common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/UaaAuthenticationPrototype.java diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java index c8a95649bc6..e638f2fa9f1 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaAuthentication.java @@ -14,7 +14,6 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.cloudfoundry.identity.uaa.authentication.manager.UaaAuthenticationPrototype; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.util.LinkedMultiValueMap; @@ -50,16 +49,12 @@ public class UaaAuthentication implements Authentication, Serializable { * Creates a token with the supplied array of authorities. * * @param authorities the collection of GrantedAuthoritys for the - * principal represented by this authentication object. + * principal represented by this authentication object. */ public UaaAuthentication(UaaPrincipal principal, List authorities, UaaAuthenticationDetails details) { - this(UaaAuthenticationPrototype.alreadyAuthenticated() - .withPrincipal(principal) - .withDetails(details) - .withAuthorities(authorities) - ); + this(principal, null, authorities, details, true, System.currentTimeMillis()); } public UaaAuthentication(UaaPrincipal principal, @@ -68,16 +63,9 @@ public UaaAuthentication(UaaPrincipal principal, UaaAuthenticationDetails details, boolean authenticated, long authenticatedTime) { - this(UaaAuthenticationPrototype.notYetAuthenticated() - .withPrincipal(principal) - .withAuthenticated(authenticated) - .withAuthenticatedTime(authenticatedTime) - .withDetails(details) - .withAuthorities(authorities) - .withCredentials(credentials) - ); - } - + this(principal, credentials, authorities, details, authenticated, authenticatedTime, -1); + } + public UaaAuthentication(UaaPrincipal principal, Object credentials, List authorities, @@ -85,15 +73,16 @@ public UaaAuthentication(UaaPrincipal principal, boolean authenticated, long authenticatedTime, long expiresAt) { - this(UaaAuthenticationPrototype.notYetAuthenticated() - .withPrincipal(principal) - .withAuthenticated(authenticated) - .withAuthenticatedTime(authenticatedTime) - .withDetails(details) - .withExpiresAt(expiresAt) - .withAuthorities(authorities) - .withCredentials(credentials) - ); + if (principal == null || authorities == null) { + throw new IllegalArgumentException("principal and authorities must not be null"); + } + this.principal = principal; + this.authorities = authorities; + this.details = details; + this.credentials = credentials; + this.authenticated = authenticated; + this.authenticatedTime = authenticatedTime <= 0 ? -1 : authenticatedTime; + this.expiresAt = expiresAt <= 0 ? -1 : expiresAt; } public UaaAuthentication(UaaPrincipal uaaPrincipal, @@ -105,34 +94,9 @@ public UaaAuthentication(UaaPrincipal uaaPrincipal, boolean authenticated, long authenticatedTime, long expiresAt) { - this(UaaAuthenticationPrototype.notYetAuthenticated() - .withPrincipal(uaaPrincipal) - .withAuthenticated(authenticated) - .withAuthenticatedTime(authenticatedTime) - .withExternalGroups(externalGroups) - .withDetails(details) - .withExpiresAt(expiresAt) - .withAuthorities(uaaAuthorityList) - .withCredentials(credentials) - .withAttributes(userAttributes) - ); - } - - public UaaAuthentication(UaaAuthenticationPrototype prototype) { - if (prototype.getPrincipal() == null || prototype.getAuthorities() == null) { - throw new IllegalArgumentException("principal and authorities must not be null"); - } - this.principal = prototype.getPrincipal(); - this.authorities = prototype.getAuthorities(); - this.details = prototype.getDetails(); - this.credentials = prototype.getCredentials(); - this.authenticated = prototype.isAuthenticated(); - this.authenticatedTime = prototype.getAuthenticatedTime(); - if (this.authenticatedTime <= 0) this.authenticatedTime = -1; - this.expiresAt = prototype.getExpiresAt(); - if (this.expiresAt <= 0) this.expiresAt = -1; - this.externalGroups = prototype.getExternalGroups(); - this.userAttributes = prototype.getAttributes(); + this(uaaPrincipal, credentials, uaaAuthorityList, details, authenticated, authenticatedTime, expiresAt); + this.externalGroups = externalGroups; + this.userAttributes = new HashMap<>(userAttributes); } public long getAuthenticatedTime() { diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java index 0b4c4435948..9396b32593d 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java @@ -45,7 +45,7 @@ import java.util.Collections; import java.util.Date; -import java.util.Set; +import java.util.Map; public class ExternalLoginAuthenticationManager implements AuthenticationManager, ApplicationEventPublisherAware, BeanNameAware { @@ -83,10 +83,6 @@ public UaaUserDatabase getUserDatabase() { return this.userDatabase; } - protected Set getExternalGroups(Object principal) { - return Collections.EMPTY_SET; - } - @Override public Authentication authenticate(Authentication request) throws AuthenticationException { UaaUser user = getUser(request); @@ -117,18 +113,13 @@ public Authentication authenticate(Authentication request) throws Authentication //user is authenticated and exists in UAA user = userAuthenticated(request, user); - UaaAuthenticationDetails uaaAuthenticationDetails; + UaaAuthenticationDetails uaaAuthenticationDetails = null; if (request.getDetails() instanceof UaaAuthenticationDetails) { uaaAuthenticationDetails = (UaaAuthenticationDetails) request.getDetails(); } else { uaaAuthenticationDetails = UaaAuthenticationDetails.UNKNOWN; } - Authentication success = new UaaAuthentication(UaaAuthenticationPrototype.alreadyAuthenticated() - .withPrincipal(new UaaPrincipal(user)) - .withAuthorities(user.getAuthorities()) - .withDetails(uaaAuthenticationDetails) - .withExternalGroups(getExternalGroups(request.getPrincipal())) - ); + Authentication success = new UaaAuthentication(new UaaPrincipal(user), user.getAuthorities(), uaaAuthenticationDetails); publish(new UserAuthenticationSuccessEvent(user, success)); return success; } @@ -194,11 +185,11 @@ protected UaaUser getUser(Authentication request) { familyName = names.getFamilyName(); } - if (givenName == null) { + if(givenName == null) { givenName = email.split("@")[0]; } - if (familyName == null) { + if(familyName == null) { familyName = email.split("@")[1]; } @@ -206,18 +197,18 @@ protected UaaUser getUser(Authentication request) { String externalId = (userDetails instanceof ExternallyIdentifiable) ? ((ExternallyIdentifiable) userDetails).getExternalId() : name; UaaUserPrototype userPrototype = new UaaUserPrototype() - .withUsername(name) - .withPassword("") - .withEmail(email) - .withAuthorities(UaaAuthority.USER_AUTHORITIES) - .withGivenName(givenName) - .withFamilyName(familyName) - .withCreated(new Date()) - .withModified(new Date()) - .withOrigin(origin) - .withExternalId(externalId) - .withZoneId(IdentityZoneHolder.get().getId()) - .withPhoneNumber(phoneNumber); + .withUsername(name) + .withPassword("") + .withEmail(email) + .withAuthorities(UaaAuthority.USER_AUTHORITIES) + .withGivenName(givenName) + .withFamilyName(familyName) + .withCreated(new Date()) + .withModified(new Date()) + .withOrigin(origin) + .withExternalId(externalId) + .withZoneId(IdentityZoneHolder.get().getId()) + .withPhoneNumber(phoneNumber); return new UaaUser(userPrototype); } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java index 114ee770c3b..5354e75a197 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java @@ -16,58 +16,18 @@ package org.cloudfoundry.identity.uaa.authentication.manager; import org.apache.commons.lang.StringUtils; -import org.cloudfoundry.identity.uaa.authentication.Origin; import org.cloudfoundry.identity.uaa.ldap.ExtendedLdapUserDetails; -import org.cloudfoundry.identity.uaa.ldap.LdapIdentityProviderDefinition; -import org.cloudfoundry.identity.uaa.ldap.extension.SpringSecurityLdapTemplate; import org.cloudfoundry.identity.uaa.user.UaaUser; import org.cloudfoundry.identity.uaa.user.UaaUserPrototype; -import org.cloudfoundry.identity.uaa.zone.IdentityProvider; -import org.cloudfoundry.identity.uaa.zone.IdentityProviderProvisioning; -import org.cloudfoundry.identity.uaa.zone.IdentityZone; -import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; -import org.springframework.ldap.core.ContextSource; import org.springframework.security.core.Authentication; import java.util.Collections; import java.util.Date; -import javax.naming.directory.SearchControls; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - public class LdapLoginAuthenticationManager extends ExternalLoginAuthenticationManager { - private final IdentityProviderProvisioning idpProvisioning; - - private final SpringSecurityLdapTemplate ldapTemplate; - private final String groupSearchBase; - private final String groupSearchFilter; - private boolean autoAddAuthorities = false; - public LdapLoginAuthenticationManager(IdentityProviderProvisioning idpProvisioning, ContextSource contextSource, String groupSearchBase, String groupSearchFilter, boolean searchSubtree) { - this.idpProvisioning = idpProvisioning; - this.groupSearchBase = groupSearchBase; - this.groupSearchFilter = groupSearchFilter; - - if (contextSource != null) { - ldapTemplate = new SpringSecurityLdapTemplate(contextSource); - - SearchControls searchControls = new SearchControls(); - int searchScope = searchSubtree ? SearchControls.SUBTREE_SCOPE : SearchControls.ONELEVEL_SCOPE; - searchControls.setSearchScope(searchScope); - ldapTemplate.setSearchControls(searchControls); - - ldapTemplate.setIgnorePartialResultException(true); - } else { - ldapTemplate = null; - } - } - @Override protected UaaUser userAuthenticated(Authentication request, UaaUser user) { boolean userModified = false; @@ -92,35 +52,6 @@ public void setAutoAddAuthorities(boolean autoAddAuthorities) { this.autoAddAuthorities = autoAddAuthorities; } - @Override - protected Set getExternalGroups(Object principal) { - if(idpProvisioning == null || ldapTemplate == null) return Collections.EMPTY_SET; - - IdentityProvider idp = idpProvisioning.retrieveByOrigin(Origin.LDAP, IdentityZoneHolder.get().getId()); - LdapIdentityProviderDefinition def = idp.getConfigValue(LdapIdentityProviderDefinition.class); - List whitelist = def.getExternalGroupsWhitelist(); - - Set groups = new HashSet<>(); - - if (principal instanceof ExtendedLdapUserDetails) { - Set> userRoles = ldapTemplate.searchForMultipleAttributeValues( - groupSearchBase, - groupSearchFilter, - new String[]{((ExtendedLdapUserDetails)principal).getDn()}, - new String[] {"cn"}); - - for (Map row : userRoles) { - for(String groupName : row.get("cn")) { - if (whitelist.contains(groupName)) { - groups.add(groupName); - } - } - } - } - - return groups; - } - private boolean haveUserAttributesChanged(UaaUser existingUser, UaaUser user) { if (!StringUtils.equals(existingUser.getGivenName(), user.getGivenName()) || !StringUtils.equals(existingUser.getFamilyName(), user.getFamilyName()) || !StringUtils.equals(existingUser.getPhoneNumber(), user.getPhoneNumber()) || !StringUtils.equals(existingUser.getEmail(), user.getEmail())) { diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/UaaAuthenticationPrototype.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/UaaAuthenticationPrototype.java deleted file mode 100644 index 80d0380e19a..00000000000 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/UaaAuthenticationPrototype.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * ****************************************************************************** - * Cloud Foundry Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. - * - * This product is licensed to you under the Apache License, Version 2.0 (the "License"). - * You may not use this product except in compliance with the License. - * - * This product includes a number of subcomponents with - * separate copyright notices and license terms. Your use of these - * subcomponents is subject to the terms and conditions of the - * subcomponent's license, as noted in the LICENSE file. - * ****************************************************************************** - */ - -package org.cloudfoundry.identity.uaa.authentication.manager; - -import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; -import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; -import org.springframework.security.core.GrantedAuthority; - -import java.util.List; -import java.util.Map; -import java.util.Set; - -public class UaaAuthenticationPrototype { - private List authorities; - private Object credentials = null; - private UaaPrincipal principal; - private UaaAuthenticationDetails details; - private boolean authenticated; - private long authenticatedTime = System.currentTimeMillis(); - private long expiresAt = -1l; - private Set externalGroups; - private Map> attributes; - - private UaaAuthenticationPrototype() { - } - - public static UaaAuthenticationPrototype alreadyAuthenticated() { - return new UaaAuthenticationPrototype().withAuthenticated(true).withAuthenticatedTime(System.currentTimeMillis()); - } - - public static UaaAuthenticationPrototype notYetAuthenticated() { - return new UaaAuthenticationPrototype(); - } - - public UaaAuthenticationPrototype withAuthorities(List authorities) { - this.authorities = authorities; - return this; - } - - public UaaAuthenticationPrototype withCredentials(Object credentials) { - this.credentials = credentials; - return this; - } - - public UaaAuthenticationPrototype withPrincipal(UaaPrincipal principal){ - this.principal = principal; - return this; - } - - public UaaAuthenticationPrototype withDetails(UaaAuthenticationDetails details) { - this.details = details; - return this; - } - - public UaaAuthenticationPrototype withAuthenticated(boolean authenticated) { - this.authenticated = authenticated; - return this; - } - - public UaaAuthenticationPrototype withAuthenticatedTime(long authenticatedTime) { - this.authenticatedTime = authenticatedTime; - return this; - } - - public UaaAuthenticationPrototype withExpiresAt(long expiresAt) { - this.expiresAt = expiresAt; - return this; - } - - public UaaAuthenticationPrototype withExternalGroups(Set externalGroups) { - this.externalGroups = externalGroups; - return this; - } - - public UaaAuthenticationPrototype withAttributes(Map> attributes) { - this.attributes = attributes; - return this; - } - - - public List getAuthorities() { - return authorities; - } - - public Object getCredentials() { - return credentials; - } - - public UaaPrincipal getPrincipal() { - return principal; - } - - public UaaAuthenticationDetails getDetails() { - return details; - } - - public boolean isAuthenticated() { - return authenticated; - } - - public long getAuthenticatedTime() { - return authenticatedTime; - } - - public long getExpiresAt() { - return expiresAt; - } - - public Set getExternalGroups() { - return externalGroups; - } - - public Map> getAttributes() { return attributes; } -} diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManagerTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManagerTest.java index 8541ec1374d..56117b9a0d1 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManagerTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManagerTest.java @@ -246,7 +246,7 @@ public void testAuthenticateLdapUserDetailsPrincipal() throws Exception { LdapUserDetails ldapUserDetails = mock(LdapUserDetails.class); mockUserDetails(ldapUserDetails); when(ldapUserDetails.getDn()).thenReturn(dn); - manager = new LdapLoginAuthenticationManager(null, null, null, null, true); + manager = new LdapLoginAuthenticationManager(); setupManager(); manager.setOrigin(origin); when(user.getOrigin()).thenReturn(origin); @@ -277,7 +277,7 @@ public void testAuthenticateCreateUserWithLdapUserDetailsPrincipal() throws Exce ExtendedLdapUserImpl ldapUserDetails = new ExtendedLdapUserImpl(baseLdapUserDetails, ldapAttrs); ldapUserDetails.setMailAttributeName(ldapMailAttrName); - manager = new LdapLoginAuthenticationManager(null, null, null, null, true); + manager = new LdapLoginAuthenticationManager(); setupManager(); manager.setOrigin(origin); when(user.getEmail()).thenReturn(email); @@ -309,7 +309,7 @@ public void testAuthenticateCreateUserWithLdapUserDetailsPrincipal() throws Exce public void testAuthenticateCreateUserWithUserDetailsPrincipal() throws Exception { String origin = "ldap"; - manager = new LdapLoginAuthenticationManager(null, null, null, null, true); + manager = new LdapLoginAuthenticationManager(); setupManager(); manager.setOrigin(origin); @@ -372,4 +372,4 @@ public void testAuthenticateUserDoesNotExists() throws Exception { -} +} \ No newline at end of file diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java index 30bcccf98fe..4eec6427f30 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java @@ -37,6 +37,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyObject; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -89,7 +91,7 @@ private static void setupGeneralExpectations(UserDetails userDetails) { @Before public void setUp() { - am = new LdapLoginAuthenticationManager(null, null, null, null, true); + am = new LdapLoginAuthenticationManager(); publisher = mock(ApplicationEventPublisher.class); am.setApplicationEventPublisher(publisher); am.setOrigin(origin); @@ -200,4 +202,4 @@ protected UaaUser getUaaUser() { .withSalt(null) .withPasswordLastModified(null)); } -} +} \ No newline at end of file diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java b/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java index 010644a1d1e..e5b548a7c79 100644 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/login/saml/LoginSamlAuthenticationProvider.java @@ -113,19 +113,19 @@ public Authentication authenticate(Authentication authentication) throws Authent SAMLAuthenticationToken token = (SAMLAuthenticationToken) authentication; SAMLMessageContext context = token.getCredentials(); String alias = context.getPeerExtendedMetadata().getAlias(); + boolean addNew = true; IdentityProvider idp; SamlIdentityProviderDefinition samlConfig; try { idp = identityProviderProvisioning.retrieveByOrigin(alias, IdentityZoneHolder.get().getId()); samlConfig = idp.getConfigValue(SamlIdentityProviderDefinition.class); - + addNew = samlConfig.isAddShadowUserOnLogin(); if (!idp.isActive()) { throw new ProviderNotFoundException("Identity Provider has been disabled by administrator."); } } catch (EmptyResultDataAccessException x) { throw new ProviderNotFoundException("Not identity provider found in zone."); } - boolean addNew = samlConfig.isAddShadowUserOnLogin(); ExpiringUsernameAuthenticationToken result = getExpiringUsernameAuthenticationToken(authentication); UaaPrincipal samlPrincipal = new UaaPrincipal(Origin.NotANumber, result.getName(), result.getName(), alias, result.getName(), zone.getId()); Collection samlAuthorities = retrieveSamlAuthorities(samlConfig, (SAMLCredential) result.getCredentials()); diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProviderEndpoints.java b/login/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProviderEndpoints.java index 9b653987daf..f1950a4432b 100644 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProviderEndpoints.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityProviderEndpoints.java @@ -187,10 +187,6 @@ protected String getExceptionString(Exception x) { } protected static class NoOpLdapLoginAuthenticationManager extends LdapLoginAuthenticationManager { - protected NoOpLdapLoginAuthenticationManager() { - super(null, null, null, null, true); - } - @Override public Authentication authenticate(Authentication request) throws AuthenticationException { return request; diff --git a/uaa/src/main/resources/ldap_init.ldif b/uaa/src/main/resources/ldap_init.ldif index 5b0f3c2a381..cba440ff3e3 100644 --- a/uaa/src/main/resources/ldap_init.ldif +++ b/uaa/src/main/resources/ldap_init.ldif @@ -150,14 +150,6 @@ cn: uaa.admin member: cn=admin,ou=Users,dc=test,dc=com member: cn=marissa3,ou=Users,dc=test,dc=com -#This groups contains scopes as comma separated list in the description attribute -dn: cn=thirdmarissa,ou=scopes,dc=test,dc=com -changetype: add -objectClass: groupOfNames -objectClass: top -cn: thirdmarissa -member: cn=marissa3,ou=Users,dc=test,dc=com - dn: cn=developers,ou=scopes,dc=test,dc=com changetype: add objectClass: groupOfNames diff --git a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml index 94bc4071e99..c564aea8bd2 100755 --- a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml +++ b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml @@ -303,11 +303,6 @@ - - - - - diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java index 117180be3fd..aef470a8cea 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java @@ -14,7 +14,6 @@ import org.cloudfoundry.identity.uaa.TestClassNullifier; import org.cloudfoundry.identity.uaa.authentication.Origin; -import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; import org.cloudfoundry.identity.uaa.authentication.manager.AuthzAuthenticationManager; import org.cloudfoundry.identity.uaa.authentication.manager.ChainedAuthenticationManager; import org.cloudfoundry.identity.uaa.ldap.ExtendedLdapUserMapper; @@ -77,13 +76,11 @@ import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.CookieCsrfPostProcessor.cookieCsrf; import static org.hamcrest.Matchers.arrayContainingInAnyOrder; -import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; @@ -617,43 +614,6 @@ public void testLogin_partial_result_exception_on_group_search() throws Exceptio } - @Test - public void external_groups () throws Exception { - setUp(); - IdentityProviderProvisioning idpProvisioning = webApplicationContext.getBean(IdentityProviderProvisioning.class); - IdentityProvider idp = idpProvisioning.retrieveByOrigin(Origin.LDAP, IdentityZone.getUaa().getId()); - LdapIdentityProviderDefinition def = idp.getConfigValue(LdapIdentityProviderDefinition.class); - def.addWhiteListedGroup("admins"); - def.addWhiteListedGroup("thirdmarissa"); - idp.setConfig(JsonUtils.writeValueAsString(def)); - idpProvisioning.update(idp); - - AuthenticationManager manager = webApplicationContext.getBean(ChainedAuthenticationManager.class); - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("marissa3","ldap3"); - Authentication auth = manager.authenticate(token); - assertNotNull(auth); - assertTrue(auth instanceof UaaAuthentication); - UaaAuthentication uaaAuth = (UaaAuthentication)auth; - Set externalGroups = uaaAuth.getExternalGroups(); - assertNotNull(externalGroups); - assertEquals(2, externalGroups.size()); - assertThat(externalGroups, containsInAnyOrder("admins", "thirdmarissa")); - } - - @Test - public void external_groups_with_default_whitelist () throws Exception { - setUp(); - - AuthenticationManager manager = webApplicationContext.getBean(ChainedAuthenticationManager.class); - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("marissa3","ldap3"); - Authentication auth = manager.authenticate(token); - assertNotNull(auth); - assertTrue(auth instanceof UaaAuthentication); - UaaAuthentication uaaAuth = (UaaAuthentication)auth; - Set externalGroups = uaaAuth.getExternalGroups(); - assertNotNull(externalGroups); - assertEquals(0, externalGroups.size()); - } @Test public void runLdapTestblock() throws Exception { From 80cbef1cd358abf33a859756391bc2413b8e0c69 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Thu, 22 Oct 2015 13:57:17 -0600 Subject: [PATCH 089/103] Clean up a bit. All perform the same logic. --- .../ldap/extension/ExtendedLdapUserImpl.java | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/extension/ExtendedLdapUserImpl.java b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/extension/ExtendedLdapUserImpl.java index 4d70b09892d..fb39e73634d 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/extension/ExtendedLdapUserImpl.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/extension/ExtendedLdapUserImpl.java @@ -18,6 +18,7 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.ldap.userdetails.LdapUserDetails; +import org.springframework.util.StringUtils; import java.util.Collection; import java.util.Collections; @@ -192,28 +193,32 @@ public String getEmailAddress() { @Override public String getGivenName() { - if(givenNameAttributeName == null) return null; - String[] attrValues = this.attributes.get(givenNameAttributeName); - if(attrValues == null) return null; - return attrValues[0]; + return getFirst(givenNameAttributeName); } @Override public String getFamilyName() { - String[] attrValues = this.attributes.get(familyNameAttributeName); - if(attrValues == null) return null; - return attrValues[0]; + return getFirst(familyNameAttributeName); } @Override public String getPhoneNumber() { - String[] attrValues = this.attributes.get(phoneNumberAttributeName); - if(attrValues == null) return null; - return attrValues[0]; + return getFirst(phoneNumberAttributeName); } @Override public String getExternalId() { return getDn(); } + + protected String getFirst(String attributeName) { + if (!StringUtils.hasText(attributeName)) { + return null; + } + String[] attrValues = this.attributes.get(attributeName); + if(attrValues == null || attrValues.length==0) { + return null; + } + return attrValues[0]; + } } From 7e4d9800354be8a08dba65b3f377deb6dde0ae05 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Thu, 22 Oct 2015 23:58:11 -0600 Subject: [PATCH 090/103] Refactor LDAP authentication across zone Goal: LDAP authentication should work the same way in the UAA zone as in a different zone Purpose: Noticed that developers are not aware the difference in implementation for how LDAP is loaded in the default zone (through the main context) and in non default zones (through dynamic context loading). This leads to diminished test coverage and bugs. How: We will load the default UAA zone LDAP system exactly the same way we load any other zone. From the DB using a dynamic context. --- .../uaa/config/IdentityProviderBootstrap.java | 38 +- .../ldap/LdapIdentityProviderDefinition.java | 385 ++++++++++++------ .../identity/uaa/util/UaaMapUtils.java | 38 ++ .../config/IdentityProviderBootstrapTest.java | 11 +- .../LdapIdentityProviderDefinitionTest.java | 7 +- .../identity/uaa/util/UaaMapUtilsTest.java | 59 +++ .../DynamicLdapAuthenticationManager.java | 5 +- ...DynamicZoneAwareAuthenticationManager.java | 14 +- .../main/webapp/WEB-INF/spring-servlet.xml | 77 ---- .../webapp/WEB-INF/spring/oauth-endpoints.xml | 7 - .../identity/uaa/BootstrapTests.java | 10 +- ...micZoneAwareAuthenticationManagerTest.java | 15 +- .../identity/uaa/login/BootstrapTests.java | 3 - ...thenticationManagerConfigurationTests.java | 23 -- .../uaa/mock/ldap/LdapMockMvcTests.java | 113 ++--- 15 files changed, 479 insertions(+), 326 deletions(-) create mode 100644 common/src/main/java/org/cloudfoundry/identity/uaa/util/UaaMapUtils.java create mode 100644 common/src/test/java/org/cloudfoundry/identity/uaa/util/UaaMapUtilsTest.java diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrap.java b/common/src/main/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrap.java index 14c91ed2089..9ae62369611 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrap.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrap.java @@ -18,12 +18,14 @@ import org.cloudfoundry.identity.uaa.login.saml.SamlIdentityProviderConfigurator; import org.cloudfoundry.identity.uaa.login.saml.SamlIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.cloudfoundry.identity.uaa.util.UaaMapUtils; import org.cloudfoundry.identity.uaa.zone.IdentityProvider; import org.cloudfoundry.identity.uaa.zone.IdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.UaaIdentityProviderDefinition; import org.json.JSONException; import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.env.AbstractEnvironment; import org.springframework.core.env.Environment; import org.springframework.dao.EmptyResultDataAccessException; @@ -32,6 +34,11 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; + +import static org.cloudfoundry.identity.uaa.ldap.LdapIdentityProviderDefinition.LDAP; +import static org.cloudfoundry.identity.uaa.ldap.LdapIdentityProviderDefinition.LDAP_PROPERTY_NAMES; +import static org.cloudfoundry.identity.uaa.ldap.LdapIdentityProviderDefinition.LDAP_PROPERTY_TYPES; public class IdentityProviderBootstrap implements InitializingBean { public static final String DEFAULT_MAP = "{\"default\":\"default\"}"; @@ -66,6 +73,7 @@ protected void addSamlProviders() { provider.setType(Origin.SAML); provider.setOriginKey(def.getIdpEntityAlias()); provider.setName("UAA SAML Identity Provider["+provider.getOriginKey()+"]"); + provider.setActive(true); try { provider.setConfig(JsonUtils.writeValueAsString(def)); } catch (JsonUtils.JsonUtilException x) { @@ -82,23 +90,42 @@ public void setLdapConfig(HashMap ldapConfig) { protected void addLdapProvider() { boolean ldapProfile = Arrays.asList(environment.getActiveProfiles()).contains(Origin.LDAP); if (ldapConfig != null || ldapProfile) { + boolean active = ldapProfile && ldapConfig!=null; IdentityProvider provider = new IdentityProvider(); + provider.setActive(ldapProfile); provider.setOriginKey(Origin.LDAP); provider.setType(Origin.LDAP); provider.setName("UAA LDAP Provider"); - String json = getLdapConfigAsDefinition(ldapConfig); + provider.setActive(active); + Map ldap = new HashMap<>(); + ldap.put(LDAP, ldapConfig); + String json = getLdapConfigAsDefinition(ldap); provider.setConfig(json); providers.add(provider); } } - private String getLdapConfigAsDefinition(HashMap ldapConfig) { - if (ldapConfig==null || ldapConfig.isEmpty()) { - JsonUtils.writeValueAsString(new LdapIdentityProviderDefinition()); + + + protected String getLdapConfigAsDefinition(Map ldapConfig) { + ldapConfig = UaaMapUtils.flatten(ldapConfig); + populateLdapEnvironment(ldapConfig); + if (ldapConfig.isEmpty()) { + return JsonUtils.writeValueAsString(new LdapIdentityProviderDefinition()); } return JsonUtils.writeValueAsString(LdapIdentityProviderDefinition.fromConfig(ldapConfig)); } + protected void populateLdapEnvironment(Map ldapConfig) { + //this method reads the environment and overwrites values (needed by LdapMockMvcTests that overrides properties through env) + AbstractEnvironment env = (AbstractEnvironment)environment; + for (String property : LDAP_PROPERTY_NAMES) { + if (env.containsProperty(property) && LDAP_PROPERTY_TYPES.get(property)!=null) { + ldapConfig.put(property, env.getProperty(property, LDAP_PROPERTY_TYPES.get(property))); + } + } + } + public void setKeystoneConfig(HashMap keystoneConfig) { this.keystoneConfig = keystoneConfig; } @@ -106,10 +133,12 @@ public void setKeystoneConfig(HashMap keystoneConfig) { protected void addKeystoneProvider() { boolean keystoneProfile = Arrays.asList(environment.getActiveProfiles()).contains(Origin.KEYSTONE); if (keystoneConfig != null || keystoneProfile) { + boolean active = keystoneProfile && keystoneConfig!=null; IdentityProvider provider = new IdentityProvider(); provider.setOriginKey(Origin.KEYSTONE); provider.setType(Origin.KEYSTONE); provider.setName("UAA LDAP Provider"); + provider.setActive(active); String json = keystoneConfig != null ? JsonUtils.writeValueAsString(keystoneConfig) : DEFAULT_MAP; provider.setConfig(json); providers.add(provider); @@ -136,7 +165,6 @@ public void afterPropertiesSet() throws Exception { }catch (EmptyResultDataAccessException x){ } provider.setIdentityZoneId(zoneId); - provider.setActive(true); if (existing==null) { provisioning.create(provider); } else { diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinition.java b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinition.java index e96f38bdade..db10bad7e6b 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinition.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinition.java @@ -14,22 +14,124 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition; -import org.cloudfoundry.identity.uaa.config.NestedMapPropertySource; import org.springframework.core.env.AbstractEnvironment; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; import org.springframework.util.StringUtils; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; public class LdapIdentityProviderDefinition extends ExternalIdentityProviderDefinition { + public static final String LDAP = "ldap"; + public static final String LDAP_PREFIX = LDAP + "."; + public static final String LDAP_ATTRIBUTE_MAPPINGS = LDAP_PREFIX + ATTRIBUTE_MAPPINGS; + public static final String LDAP_BASE_LOCAL_PASSWORD_COMPARE = LDAP_PREFIX + "base.localPasswordCompare"; + public static final String LDAP_BASE_MAIL_ATTRIBUTE_NAME = LDAP_PREFIX + "base.mailAttributeName"; + public static final String LDAP_BASE_MAIL_SUBSTITUTE = LDAP_PREFIX + "base.mailSubstitute"; + public static final String LDAP_BASE_MAIL_SUBSTITUTE_OVERRIDES_LDAP = LDAP_PREFIX + "base.mailSubstituteOverridesLdap"; + public static final String LDAP_BASE_PASSWORD = LDAP_PREFIX + "base.password"; + public static final String LDAP_BASE_PASSWORD_ATTRIBUTE_NAME = LDAP_PREFIX + "base.passwordAttributeName"; + public static final String LDAP_BASE_PASSWORD_ENCODER = LDAP_PREFIX + "base.passwordEncoder"; + public static final String LDAP_BASE_REFERRAL = LDAP_PREFIX + "base.referral"; + public static final String LDAP_BASE_SEARCH_BASE = LDAP_PREFIX + "base.searchBase"; + public static final String LDAP_BASE_SEARCH_FILTER = LDAP_PREFIX + "base.searchFilter"; + public static final String LDAP_BASE_URL = LDAP_PREFIX + "base.url"; + public static final String LDAP_BASE_USER_DN = LDAP_PREFIX + "base.userDn"; + public static final String LDAP_BASE_USER_DN_PATTERN = LDAP_PREFIX + "base.userDnPattern"; + public static final String LDAP_BASE_USER_DN_PATTERN_DELIMITER = LDAP_PREFIX + "base.userDnPatternDelimiter"; + public static final String LDAP_EMAIL_DOMAIN = LDAP_PREFIX + EMAIL_DOMAIN_ATTR; + public static final String LDAP_EXTERNAL_GROUPS_WHITELIST = LDAP_PREFIX + "externalGroupsWhitelist"; + public static final String LDAP_GROUP_FILE_GROUPS_AS_SCOPES = "ldap/ldap-groups-as-scopes.xml"; + public static final String LDAP_GROUP_FILE_GROUPS_MAP_TO_SCOPES = "ldap/ldap-groups-map-to-scopes.xml"; + public static final String LDAP_GROUP_FILE_GROUPS_NULL_XML = "ldap/ldap-groups-null.xml"; + public static final String LDAP_GROUPS_AUTO_ADD = LDAP_PREFIX + "groups.autoAdd"; + public static final String LDAP_GROUPS_FILE = LDAP_PREFIX + "groups.file"; + public static final String LDAP_GROUPS_GROUP_ROLE_ATTRIBUTE = LDAP_PREFIX + "groups.groupRoleAttribute"; + public static final String LDAP_GROUPS_GROUP_SEARCH_FILTER = LDAP_PREFIX + "groups.groupSearchFilter"; + public static final String LDAP_GROUPS_IGNORE_PARTIAL_RESULT_EXCEPTION = LDAP_PREFIX + "groups.ignorePartialResultException"; + public static final String LDAP_GROUPS_MAX_SEARCH_DEPTH = LDAP_PREFIX + "groups.maxSearchDepth"; + public static final String LDAP_GROUPS_SEARCH_BASE = LDAP_PREFIX + "groups.searchBase"; + public static final String LDAP_GROUPS_SEARCH_SUBTREE = LDAP_PREFIX + "groups.searchSubtree"; + public static final String LDAP_PROFILE_FILE = LDAP_PREFIX + "profile.file"; + public static final String LDAP_PROFILE_FILE_SEARCH_AND_BIND = "ldap/ldap-search-and-bind.xml"; + public static final String LDAP_PROFILE_FILE_SEARCH_AND_COMPARE = "ldap/ldap-search-and-compare.xml"; + public static final String LDAP_PROFILE_FILE_SIMPLE_BIND = "ldap/ldap-simple-bind.xml"; + public static final String LDAP_SSL_SKIPVERIFICATION = LDAP_PREFIX + "ssl.skipverification"; + public static final String MAIL = "mail"; + + public static final List LDAP_PROPERTY_NAMES = Collections.unmodifiableList( + Arrays.asList( + LDAP_ATTRIBUTE_MAPPINGS, + LDAP_BASE_LOCAL_PASSWORD_COMPARE, + LDAP_BASE_MAIL_ATTRIBUTE_NAME, + LDAP_BASE_MAIL_SUBSTITUTE, + LDAP_BASE_MAIL_SUBSTITUTE_OVERRIDES_LDAP, + LDAP_BASE_PASSWORD, + LDAP_BASE_PASSWORD_ATTRIBUTE_NAME, + LDAP_BASE_PASSWORD_ENCODER, + LDAP_BASE_REFERRAL, + LDAP_BASE_SEARCH_BASE, + LDAP_BASE_SEARCH_FILTER, + LDAP_BASE_URL, + LDAP_BASE_USER_DN, + LDAP_BASE_USER_DN_PATTERN, + LDAP_BASE_USER_DN_PATTERN_DELIMITER, + LDAP_EMAIL_DOMAIN, + LDAP_EXTERNAL_GROUPS_WHITELIST, + LDAP_GROUPS_AUTO_ADD, + LDAP_GROUPS_FILE, + LDAP_GROUPS_GROUP_ROLE_ATTRIBUTE, + LDAP_GROUPS_GROUP_SEARCH_FILTER, + LDAP_GROUPS_IGNORE_PARTIAL_RESULT_EXCEPTION, + LDAP_GROUPS_MAX_SEARCH_DEPTH, + LDAP_GROUPS_SEARCH_BASE, + LDAP_GROUPS_SEARCH_SUBTREE, + LDAP_PROFILE_FILE, + LDAP_SSL_SKIPVERIFICATION + ) + ); + + public static final Map> LDAP_PROPERTY_TYPES = new HashMap<>(); + static { + LDAP_PROPERTY_TYPES.put(LDAP_ATTRIBUTE_MAPPINGS, Map.class); + LDAP_PROPERTY_TYPES.put(LDAP_BASE_LOCAL_PASSWORD_COMPARE, Boolean.class); + LDAP_PROPERTY_TYPES.put(LDAP_BASE_MAIL_ATTRIBUTE_NAME, String.class); + LDAP_PROPERTY_TYPES.put(LDAP_BASE_MAIL_SUBSTITUTE, String.class); + LDAP_PROPERTY_TYPES.put(LDAP_BASE_MAIL_SUBSTITUTE_OVERRIDES_LDAP, Boolean.class); + LDAP_PROPERTY_TYPES.put(LDAP_BASE_PASSWORD, String.class); + LDAP_PROPERTY_TYPES.put(LDAP_BASE_PASSWORD_ATTRIBUTE_NAME, String.class); + LDAP_PROPERTY_TYPES.put(LDAP_BASE_PASSWORD_ENCODER, String.class); + LDAP_PROPERTY_TYPES.put(LDAP_BASE_REFERRAL, String.class); + LDAP_PROPERTY_TYPES.put(LDAP_BASE_SEARCH_BASE, String.class); + LDAP_PROPERTY_TYPES.put(LDAP_BASE_SEARCH_FILTER, String.class); + LDAP_PROPERTY_TYPES.put(LDAP_BASE_URL, String.class); + LDAP_PROPERTY_TYPES.put(LDAP_BASE_USER_DN, String.class); + LDAP_PROPERTY_TYPES.put(LDAP_BASE_USER_DN_PATTERN, String.class); + LDAP_PROPERTY_TYPES.put(LDAP_BASE_USER_DN_PATTERN_DELIMITER, String.class); + LDAP_PROPERTY_TYPES.put(LDAP_EMAIL_DOMAIN, List.class); + LDAP_PROPERTY_TYPES.put(LDAP_EXTERNAL_GROUPS_WHITELIST, List.class); + LDAP_PROPERTY_TYPES.put(LDAP_GROUPS_AUTO_ADD, Boolean.class); + LDAP_PROPERTY_TYPES.put(LDAP_GROUPS_FILE, String.class); + LDAP_PROPERTY_TYPES.put(LDAP_GROUPS_GROUP_ROLE_ATTRIBUTE, String.class); + LDAP_PROPERTY_TYPES.put(LDAP_GROUPS_GROUP_SEARCH_FILTER, String.class); + LDAP_PROPERTY_TYPES.put(LDAP_GROUPS_IGNORE_PARTIAL_RESULT_EXCEPTION, Boolean.class); + LDAP_PROPERTY_TYPES.put(LDAP_GROUPS_MAX_SEARCH_DEPTH, Integer.class); + LDAP_PROPERTY_TYPES.put(LDAP_GROUPS_SEARCH_BASE, String.class); + LDAP_PROPERTY_TYPES.put(LDAP_GROUPS_SEARCH_SUBTREE, Boolean.class); + LDAP_PROPERTY_TYPES.put(LDAP_PROFILE_FILE, String.class); + LDAP_PROPERTY_TYPES.put(LDAP_SSL_SKIPVERIFICATION, Boolean.class); + } private String ldapProfileFile; private String baseUrl; - private boolean skipSSLVerification; + private String referral; + private Boolean skipSSLVerification; private String userDNPattern; + private String userDNPatternDelimiter; private String bindUserDn; private String bindPassword; @@ -38,16 +140,18 @@ public class LdapIdentityProviderDefinition extends ExternalIdentityProviderDefi private String passwordAttributeName; private String passwordEncoder; - private String mailAttributeName; + private Boolean localPasswordCompare; + private String mailAttributeName = MAIL; private String mailSubstitute; - private boolean mailSubstituteOverridesLdap = false; - private String ldapGroupFile; + private Boolean mailSubstituteOverridesLdap = false; + private String ldapGroupFile = null; private String groupSearchBase; private String groupSearchFilter; + private Boolean groupsIgnorePartialResults; - private boolean autoAddGroups = true; - private boolean groupSearchSubTree = true; + private Boolean autoAddGroups = true; + private Boolean groupSearchSubTree = true; private int maxGroupSearchDepth = 10; private String groupRoleAttribute; @@ -61,11 +165,11 @@ public static LdapIdentityProviderDefinition searchAndBindMapGroupToScopes( String groupSearchFilter, String mailAttributeName, String mailSubstitute, - boolean mailSubstituteOverridesLdap, - boolean autoAddGroups, - boolean groupSearchSubTree, + Boolean mailSubstituteOverridesLdap, + Boolean autoAddGroups, + Boolean groupSearchSubTree, int groupMaxSearchDepth, - boolean skipSSLVerification) { + Boolean skipSSLVerification) { LdapIdentityProviderDefinition definition = new LdapIdentityProviderDefinition(); definition.baseUrl = baseUrl; @@ -77,8 +181,8 @@ public static LdapIdentityProviderDefinition searchAndBindMapGroupToScopes( definition.groupSearchFilter = groupSearchFilter; definition.mailAttributeName = mailAttributeName; definition.mailSubstitute = mailSubstitute; - definition.ldapProfileFile="ldap/ldap-search-and-bind.xml"; - definition.ldapGroupFile="ldap/ldap-groups-map-to-scopes.xml"; + definition.ldapProfileFile=LDAP_PROFILE_FILE_SEARCH_AND_BIND; + definition.ldapGroupFile= LDAP_GROUP_FILE_GROUPS_MAP_TO_SCOPES; definition.mailSubstituteOverridesLdap = mailSubstituteOverridesLdap; definition.autoAddGroups = autoAddGroups; definition.groupSearchSubTree = groupSearchSubTree; @@ -87,73 +191,73 @@ public static LdapIdentityProviderDefinition searchAndBindMapGroupToScopes( return definition; } - public static LdapIdentityProviderDefinition fromConfig(Map ldapConfig) { + public static LdapIdentityProviderDefinition fromConfig(Map ldapConfig) { LdapIdentityProviderDefinition definition = new LdapIdentityProviderDefinition(); if (ldapConfig==null || ldapConfig.isEmpty()) { return definition; } - NestedMapPropertySource source = new NestedMapPropertySource("ldap", ldapConfig); - if (source.getProperty("emailDomain")!=null) { - definition.setEmailDomain((List) source.getProperty("emailDomain")); + + if (ldapConfig.get(LDAP_EMAIL_DOMAIN)!=null) { + definition.setEmailDomain((List) ldapConfig.get(LDAP_EMAIL_DOMAIN)); } - if (source.getProperty("externalGroupsWhitelist")!=null) { - definition.setExternalGroupsWhitelist((List) source.getProperty("externalGroupsWhitelist")); + + if (ldapConfig.get(LDAP_EXTERNAL_GROUPS_WHITELIST)!=null) { + definition.setExternalGroupsWhitelist((List) ldapConfig.get(LDAP_EXTERNAL_GROUPS_WHITELIST)); } - if (source.getProperty(ATTRIBUTE_MAPPINGS)!=null) { - definition.setAttributeMappings((Map) source.getProperty(ATTRIBUTE_MAPPINGS)); + + if (ldapConfig.get(LDAP_ATTRIBUTE_MAPPINGS)!=null) { + definition.setAttributeMappings((Map) ldapConfig.get(LDAP_ATTRIBUTE_MAPPINGS)); } - definition.setLdapProfileFile((String) source.getProperty("profile.file")); + definition.setLdapProfileFile((String) ldapConfig.get(LDAP_PROFILE_FILE)); if (definition.getLdapProfileFile()==null) { return definition; } + switch (definition.getLdapProfileFile()) { - case "ldap/ldap-simple-bind.xml" : { - definition.setUserDNPattern((String) source.getProperty("base.userDnPattern")); + case LDAP_PROFILE_FILE_SIMPLE_BIND: { + definition.setUserDNPattern((String) ldapConfig.get(LDAP_BASE_USER_DN_PATTERN)); + if (ldapConfig.get(LDAP_BASE_USER_DN_PATTERN_DELIMITER)!=null) { + definition.setUserDNPatternDelimiter((String)ldapConfig.get(LDAP_BASE_USER_DN_PATTERN_DELIMITER)); + } break; } - case "ldap/ldap-search-and-bind.xml": - case "ldap/ldap-search-and-compare.xml" : { - definition.setBindUserDn((String) source.getProperty("base.userDn")); - definition.setBindPassword((String) source.getProperty("base.password")); - definition.setUserSearchBase((String) source.getProperty("base.searchBase")); - definition.setUserSearchFilter((String) source.getProperty("base.searchFilter")); + case LDAP_PROFILE_FILE_SEARCH_AND_COMPARE: + case LDAP_PROFILE_FILE_SEARCH_AND_BIND: { + definition.setBindUserDn((String) ldapConfig.get(LDAP_BASE_USER_DN)); + definition.setBindPassword((String) ldapConfig.get(LDAP_BASE_PASSWORD)); + definition.setUserSearchBase((String) ldapConfig.get(LDAP_BASE_SEARCH_BASE)); + definition.setUserSearchFilter((String) ldapConfig.get(LDAP_BASE_SEARCH_FILTER)); break; } default: return definition; } - definition.setBaseUrl((String) source.getProperty("base.url")); - Boolean skipSslVerification = (Boolean) source.getProperty("ssl.skipverification"); - if (skipSslVerification!=null) { - definition.setSkipSSLVerification(skipSslVerification); + definition.setBaseUrl((String) ldapConfig.get(LDAP_BASE_URL)); + definition.setSkipSSLVerification((Boolean) ldapConfig.get(LDAP_SSL_SKIPVERIFICATION)); + definition.setReferral((String) ldapConfig.get(LDAP_BASE_REFERRAL)); + definition.setMailSubstituteOverridesLdap((Boolean)ldapConfig.get(LDAP_BASE_MAIL_SUBSTITUTE_OVERRIDES_LDAP)); + if (StringUtils.hasText((String) ldapConfig.get(LDAP_BASE_MAIL_ATTRIBUTE_NAME))) { + definition.setMailAttributeName((String) ldapConfig.get(LDAP_BASE_MAIL_ATTRIBUTE_NAME)); } - Boolean mailSubstituteOverridesLdap = (Boolean)source.getProperty("base.mailSubstituteOverridesLdap"); - if (mailSubstituteOverridesLdap!=null) { - definition.setMailSubstituteOverridesLdap(mailSubstituteOverridesLdap); + definition.setMailSubstitute((String) ldapConfig.get(LDAP_BASE_MAIL_SUBSTITUTE)); + definition.setPasswordAttributeName((String) ldapConfig.get(LDAP_BASE_PASSWORD_ATTRIBUTE_NAME)); + definition.setPasswordEncoder((String) ldapConfig.get(LDAP_BASE_PASSWORD_ENCODER)); + definition.setLocalPasswordCompare((Boolean)ldapConfig.get(LDAP_BASE_LOCAL_PASSWORD_COMPARE)); + if (StringUtils.hasText((String) ldapConfig.get(LDAP_GROUPS_FILE))) { + definition.setLdapGroupFile((String) ldapConfig.get(LDAP_GROUPS_FILE)); } - definition.setMailAttributeName((String) source.getProperty("base.mailAttributeName")); - definition.setMailSubstitute((String) source.getProperty("base.mailSubstitute")); - definition.setPasswordAttributeName((String) source.getProperty("base.passwordAttributeName")); - definition.setPasswordEncoder((String) source.getProperty("base.passwordEncoder")); - - definition.setLdapGroupFile((String) source.getProperty("groups.file")); - if (StringUtils.hasText(definition.getLdapGroupFile())) { - definition.setGroupSearchBase((String) source.getProperty("groups.searchBase")); - definition.setGroupSearchFilter((String) source.getProperty("groups.groupSearchFilter")); - if (source.getProperty("groups.maxSearchDepth") != null) { - definition.setMaxGroupSearchDepth((Integer) source.getProperty("groups.maxSearchDepth")); - } - Boolean searchSubTree = (Boolean) source.getProperty("groups.searchSubtree"); - if (searchSubTree != null) { - definition.setGroupSearchSubTree(searchSubTree); + if (StringUtils.hasText(definition.getLdapGroupFile()) && !LDAP_GROUP_FILE_GROUPS_NULL_XML.equals(definition.getLdapGroupFile())) { + definition.setGroupSearchBase((String) ldapConfig.get(LDAP_GROUPS_SEARCH_BASE)); + definition.setGroupSearchFilter((String) ldapConfig.get(LDAP_GROUPS_GROUP_SEARCH_FILTER)); + definition.setGroupsIgnorePartialResults((Boolean)ldapConfig.get(LDAP_GROUPS_IGNORE_PARTIAL_RESULT_EXCEPTION)); + if (ldapConfig.get(LDAP_GROUPS_MAX_SEARCH_DEPTH) != null) { + definition.setMaxGroupSearchDepth((Integer) ldapConfig.get(LDAP_GROUPS_MAX_SEARCH_DEPTH)); } - Boolean autoAdd = (Boolean) source.getProperty("groups.autoAdd"); - if (autoAdd!=null) { - definition.setAutoAddGroups(autoAdd); - } - definition.setGroupRoleAttribute((String) source.getProperty("groups.groupRoleAttribute")); + definition.setGroupSearchSubTree((Boolean) ldapConfig.get(LDAP_GROUPS_SEARCH_SUBTREE)); + definition.setAutoAddGroups((Boolean) ldapConfig.get(LDAP_GROUPS_AUTO_ADD)); + definition.setGroupRoleAttribute((String) ldapConfig.get(LDAP_GROUPS_GROUP_ROLE_ATTRIBUTE)); } return definition; } @@ -162,33 +266,53 @@ public static LdapIdentityProviderDefinition fromConfig(Map ldapC public ConfigurableEnvironment getLdapConfigurationEnvironment() { Map properties = new HashMap<>(); - properties.put("ldap.ssl.skipverification", isSkipSSLVerification()); - - if ("ldap/ldap-search-and-bind.xml".equals(ldapProfileFile)) { - properties.put("ldap.profile.file", getLdapProfileFile()); - properties.put("ldap.base.url", getBaseUrl()); - properties.put("ldap.base.userDn", getBindUserDn()); - properties.put("ldap.base.password", getBindPassword()); - properties.put("ldap.base.searchBase", getUserSearchBase()); - properties.put("ldap.base.searchFilter", getUserSearchFilter()); - properties.put("ldap.base.mailAttributeName", getMailAttributeName()); - properties.put("ldap.base.mailSubstitute", getMailSubstitute()); - properties.put("ldap.base.mailSubstituteOverridesLdap", isMailSubstituteOverridesLdap()); - } - if ("ldap/ldap-groups-map-to-scopes.xml".equals(ldapGroupFile)) { - properties.put("ldap.groups.file", getLdapGroupFile()); - properties.put("ldap.groups.autoAdd", isAutoAddGroups()); - properties.put("ldap.groups.searchBase", getGroupSearchBase()); - properties.put("ldap.groups.searchFilter", getGroupSearchFilter()); - properties.put("ldap.groups.searchSubtree", isGroupSearchSubTree()); - properties.put("ldap.groups.maxSearchDepth", getMaxGroupSearchDepth()); - } + setIfNotNull(LDAP_ATTRIBUTE_MAPPINGS, getAttributeMappings(), properties); + setIfNotNull(LDAP_BASE_LOCAL_PASSWORD_COMPARE, isLocalPasswordCompare(), properties); + setIfNotNull(LDAP_BASE_MAIL_ATTRIBUTE_NAME, getMailAttributeName(), properties); + setIfNotNull(LDAP_BASE_MAIL_SUBSTITUTE, getMailSubstitute(), properties); + setIfNotNull(LDAP_BASE_MAIL_SUBSTITUTE_OVERRIDES_LDAP, isMailSubstituteOverridesLdap(), properties); + setIfNotNull(LDAP_BASE_PASSWORD, getBindPassword(), properties); + setIfNotNull(LDAP_BASE_PASSWORD_ATTRIBUTE_NAME, getPasswordAttributeName(), properties); + setIfNotNull(LDAP_BASE_PASSWORD_ENCODER, getPasswordEncoder(), properties); + setIfNotNull(LDAP_BASE_REFERRAL, getReferral(), properties); + setIfNotNull(LDAP_BASE_SEARCH_BASE, getUserSearchBase(), properties); + setIfNotNull(LDAP_BASE_SEARCH_FILTER, getUserSearchFilter(), properties); + setIfNotNull(LDAP_BASE_URL, getBaseUrl(), properties); + setIfNotNull(LDAP_BASE_USER_DN, getBindUserDn(), properties); + setIfNotNull(LDAP_BASE_USER_DN_PATTERN, getUserDNPattern(), properties); + setIfNotNull(LDAP_BASE_USER_DN_PATTERN_DELIMITER, getUserDNPatternDelimiter(), properties); + setIfNotNull(LDAP_EMAIL_DOMAIN, getEmailDomain(), properties); + setIfNotNull(LDAP_EXTERNAL_GROUPS_WHITELIST, getExternalGroupsWhitelist(), properties); + setIfNotNull(LDAP_GROUPS_AUTO_ADD, isAutoAddGroups(), properties); + setIfNotNull(LDAP_GROUPS_FILE, getLdapGroupFile(), properties); + setIfNotNull(LDAP_GROUPS_GROUP_ROLE_ATTRIBUTE, getGroupRoleAttribute(), properties); + setIfNotNull(LDAP_GROUPS_GROUP_SEARCH_FILTER, getGroupSearchFilter(), properties); + setIfNotNull(LDAP_GROUPS_IGNORE_PARTIAL_RESULT_EXCEPTION, isGroupsIgnorePartialResults(), properties); + setIfNotNull(LDAP_GROUPS_MAX_SEARCH_DEPTH, getMaxGroupSearchDepth(), properties); + setIfNotNull(LDAP_GROUPS_SEARCH_BASE, getGroupSearchBase(), properties); + setIfNotNull(LDAP_GROUPS_SEARCH_SUBTREE, isGroupSearchSubTree(), properties); + setIfNotNull(LDAP_PROFILE_FILE, getLdapProfileFile(), properties); + setIfNotNull(LDAP_SSL_SKIPVERIFICATION, isSkipSSLVerification(), properties); MapPropertySource source = new MapPropertySource("ldap", properties); return new LdapConfigEnvironment(source); } - public boolean isAutoAddGroups() { + protected void setIfNotNull(String property, Object value, Map map) { + if (value!=null) { + map.put(property, value); + } + } + + public String getReferral() { + return referral; + } + + public void setReferral(String referral) { + this.referral = referral; + } + + public Boolean isAutoAddGroups() { return autoAddGroups; } @@ -228,8 +352,8 @@ public String getMailSubstitute() { return mailSubstitute; } - public boolean isMailSubstituteOverridesLdap() { - return mailSubstituteOverridesLdap; + public Boolean isMailSubstituteOverridesLdap() { + return mailSubstituteOverridesLdap==null ? false : mailSubstituteOverridesLdap; } public String getUserSearchBase() { @@ -240,7 +364,7 @@ public String getUserSearchFilter() { return userSearchFilter; } - public boolean isGroupSearchSubTree() { + public Boolean isGroupSearchSubTree() { return groupSearchSubTree; } @@ -248,11 +372,11 @@ public int getMaxGroupSearchDepth() { return maxGroupSearchDepth; } - public boolean isSkipSSLVerification() { - return skipSSLVerification; + public Boolean isSkipSSLVerification() { + return skipSSLVerification==null?false:skipSSLVerification; } - public void setAutoAddGroups(boolean autoAddGroups) { + public void setAutoAddGroups(Boolean autoAddGroups) { this.autoAddGroups = autoAddGroups; } @@ -276,7 +400,7 @@ public void setGroupSearchFilter(String groupSearchFilter) { this.groupSearchFilter = groupSearchFilter; } - public void setGroupSearchSubTree(boolean groupSearchSubTree) { + public void setGroupSearchSubTree(Boolean groupSearchSubTree) { this.groupSearchSubTree = groupSearchSubTree; } @@ -296,7 +420,7 @@ public void setMailSubstitute(String mailSubstitute) { this.mailSubstitute = mailSubstitute; } - public void setMailSubstituteOverridesLdap(boolean mailSubstituteOverridesLdap) { + public void setMailSubstituteOverridesLdap(Boolean mailSubstituteOverridesLdap) { this.mailSubstituteOverridesLdap = mailSubstituteOverridesLdap; } @@ -304,7 +428,7 @@ public void setMaxGroupSearchDepth(int maxGroupSearchDepth) { this.maxGroupSearchDepth = maxGroupSearchDepth; } - public void setSkipSSLVerification(boolean skipSSLVerification) { + public void setSkipSSLVerification(Boolean skipSSLVerification) { this.skipSSLVerification = skipSSLVerification; } @@ -349,64 +473,83 @@ public void setGroupRoleAttribute(String groupRoleAttribute) { } @JsonIgnore - public boolean isConfigured() { + public Boolean isConfigured() { return StringUtils.hasText(getBaseUrl()); } + public Boolean isLocalPasswordCompare() { + return localPasswordCompare; + } + + public void setLocalPasswordCompare(Boolean localPasswordCompare) { + this.localPasswordCompare = localPasswordCompare; + } + + public String getUserDNPatternDelimiter() { + return userDNPatternDelimiter; + } + + public void setUserDNPatternDelimiter(String userDNPatternDelimiter) { + this.userDNPatternDelimiter = userDNPatternDelimiter; + } + + public Boolean isGroupsIgnorePartialResults() { + return groupsIgnorePartialResults; + } + + public void setGroupsIgnorePartialResults(Boolean groupsIgnorePartialResults) { + this.groupsIgnorePartialResults = groupsIgnorePartialResults; + } + @Override public boolean equals(Object o) { if (this == o) return true; - if (!(o instanceof LdapIdentityProviderDefinition)) return false; + if (o == null || getClass() != o.getClass()) return false; LdapIdentityProviderDefinition that = (LdapIdentityProviderDefinition) o; - if (autoAddGroups != that.autoAddGroups) return false; + if (skipSSLVerification != that.skipSSLVerification) return false; + if (localPasswordCompare != that.localPasswordCompare) return false; if (mailSubstituteOverridesLdap != that.mailSubstituteOverridesLdap) return false; - if (!baseUrl.equals(that.baseUrl)) return false; - if (bindPassword != null ? !bindPassword.equals(that.bindPassword) : that.bindPassword != null) return false; - if (bindUserDn != null ? !bindUserDn.equals(that.bindUserDn) : that.bindUserDn != null) return false; - if (groupSearchBase != null ? !groupSearchBase.equals(that.groupSearchBase) : that.groupSearchBase != null) + if (groupsIgnorePartialResults != that.groupsIgnorePartialResults) return false; + if (autoAddGroups != that.autoAddGroups) return false; + if (groupSearchSubTree != that.groupSearchSubTree) return false; + if (maxGroupSearchDepth != that.maxGroupSearchDepth) return false; + if (ldapProfileFile != null ? !ldapProfileFile.equals(that.ldapProfileFile) : that.ldapProfileFile != null) return false; - if (groupSearchFilter != null ? !groupSearchFilter.equals(that.groupSearchFilter) : that.groupSearchFilter != null) + if (baseUrl != null ? !baseUrl.equals(that.baseUrl) : that.baseUrl != null) return false; + if (referral != null ? !referral.equals(that.referral) : that.referral != null) return false; + if (userDNPattern != null ? !userDNPattern.equals(that.userDNPattern) : that.userDNPattern != null) return false; - if (!ldapGroupFile.equals(that.ldapGroupFile)) return false; - if (!ldapProfileFile.equals(that.ldapProfileFile)) return false; - if (!mailAttributeName.equals(that.mailAttributeName)) return false; - if (mailSubstitute != null ? !mailSubstitute.equals(that.mailSubstitute) : that.mailSubstitute != null) + if (userDNPatternDelimiter != null ? !userDNPatternDelimiter.equals(that.userDNPatternDelimiter) : that.userDNPatternDelimiter != null) return false; + if (bindUserDn != null ? !bindUserDn.equals(that.bindUserDn) : that.bindUserDn != null) return false; + if (bindPassword != null ? !bindPassword.equals(that.bindPassword) : that.bindPassword != null) return false; if (userSearchBase != null ? !userSearchBase.equals(that.userSearchBase) : that.userSearchBase != null) return false; if (userSearchFilter != null ? !userSearchFilter.equals(that.userSearchFilter) : that.userSearchFilter != null) return false; - if (groupSearchSubTree!=that.groupSearchSubTree) + if (passwordAttributeName != null ? !passwordAttributeName.equals(that.passwordAttributeName) : that.passwordAttributeName != null) + return false; + if (passwordEncoder != null ? !passwordEncoder.equals(that.passwordEncoder) : that.passwordEncoder != null) return false; - if (maxGroupSearchDepth!=that.maxGroupSearchDepth) + if (mailAttributeName != null ? !mailAttributeName.equals(that.mailAttributeName) : that.mailAttributeName != null) return false; - if (skipSSLVerification!=that.skipSSLVerification) + if (mailSubstitute != null ? !mailSubstitute.equals(that.mailSubstitute) : that.mailSubstitute != null) + return false; + if (ldapGroupFile != null ? !ldapGroupFile.equals(that.ldapGroupFile) : that.ldapGroupFile != null) + return false; + if (groupSearchBase != null ? !groupSearchBase.equals(that.groupSearchBase) : that.groupSearchBase != null) + return false; + if (groupSearchFilter != null ? !groupSearchFilter.equals(that.groupSearchFilter) : that.groupSearchFilter != null) return false; + return !(groupRoleAttribute != null ? !groupRoleAttribute.equals(that.groupRoleAttribute) : that.groupRoleAttribute != null); - return true; } @Override public int hashCode() { - int result = baseUrl.hashCode(); - result = 31 * result + (bindUserDn != null ? bindUserDn.hashCode() : 0); - result = 31 * result + (bindPassword != null ? bindPassword.hashCode() : 0); - result = 31 * result + (userSearchBase != null ? userSearchBase.hashCode() : 0); - result = 31 * result + (userSearchFilter != null ? userSearchFilter.hashCode() : 0); - result = 31 * result + (groupSearchBase != null ? groupSearchBase.hashCode() : 0); - result = 31 * result + (groupSearchFilter != null ? groupSearchFilter.hashCode() : 0); - result = 31 * result + mailAttributeName.hashCode(); - result = 31 * result + (mailSubstitute != null ? mailSubstitute.hashCode() : 0); - result = 31 * result + ldapProfileFile.hashCode(); - result = 31 * result + ldapGroupFile.hashCode(); - result = 31 * result + (mailSubstituteOverridesLdap ? 1 : 0); - result = 31 * result + (autoAddGroups ? 1 : 0); - result = 31 * result + (groupSearchSubTree ? 1 : 0); - result = 31 * result + (skipSSLVerification ? 1 : 0); - result = 31 * result + maxGroupSearchDepth; - return result; + return baseUrl != null ? baseUrl.hashCode() : 0; } public static class LdapConfigEnvironment extends AbstractEnvironment { diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/util/UaaMapUtils.java b/common/src/main/java/org/cloudfoundry/identity/uaa/util/UaaMapUtils.java new file mode 100644 index 00000000000..3b7b64079a3 --- /dev/null +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/util/UaaMapUtils.java @@ -0,0 +1,38 @@ +/* + * ***************************************************************************** + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + * ***************************************************************************** + */ + +package org.cloudfoundry.identity.uaa.util; + + +import org.cloudfoundry.identity.uaa.config.NestedMapPropertySource; + +import java.util.HashMap; +import java.util.Map; + +public class UaaMapUtils { + + public static Map flatten(Map map) { + HashMap result = new HashMap<>(); + if (map==null || map.isEmpty()) { + return result; + } + NestedMapPropertySource properties = new NestedMapPropertySource("map",map); + for (String property : properties.getPropertyNames()) { + if (properties.getProperty(property)!=null) { + result.put(property, properties.getProperty(property)); + } + } + return result; + } +} diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrapTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrapTest.java index ae01fe13285..c2d196131bf 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrapTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrapTest.java @@ -35,14 +35,13 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import static org.cloudfoundry.identity.uaa.AbstractIdentityProviderDefinition.EMAIL_DOMAIN_ATTR; -import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.EXTERNAL_GROUPS_WHITELIST; import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.ATTRIBUTE_MAPPINGS; +import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.EXTERNAL_GROUPS_WHITELIST; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -106,7 +105,9 @@ public void testLdapBootstrap() throws Exception { @Test public void testRemovedLdapBootstrapIsInactive() throws Exception { IdentityProviderProvisioning provisioning = new JdbcIdentityProviderProvisioning(jdbcTemplate); - IdentityProviderBootstrap bootstrap = new IdentityProviderBootstrap(provisioning, new MockEnvironment()); + MockEnvironment env = new MockEnvironment(); + env.setActiveProfiles(Origin.LDAP); + IdentityProviderBootstrap bootstrap = new IdentityProviderBootstrap(provisioning, env); HashMap ldapConfig = new HashMap<>(); ldapConfig.put("testkey","testvalue"); bootstrap.setLdapConfig(ldapConfig); @@ -179,8 +180,10 @@ public void testKeystoneBootstrap() throws Exception { @Test public void testRemovedKeystoneBootstrapIsInactive() throws Exception { + MockEnvironment env = new MockEnvironment(); + env.setActiveProfiles(Origin.KEYSTONE); IdentityProviderProvisioning provisioning = new JdbcIdentityProviderProvisioning(jdbcTemplate); - IdentityProviderBootstrap bootstrap = new IdentityProviderBootstrap(provisioning, new MockEnvironment()); + IdentityProviderBootstrap bootstrap = new IdentityProviderBootstrap(provisioning, env); HashMap keystoneConfig = new HashMap<>(); keystoneConfig.put("testkey", "testvalue"); bootstrap.setKeystoneConfig(keystoneConfig); diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinitionTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinitionTest.java index 5b7bbafe313..7cd3807da5e 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinitionTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinitionTest.java @@ -15,6 +15,7 @@ import org.cloudfoundry.identity.uaa.config.YamlMapFactoryBean; import org.cloudfoundry.identity.uaa.config.YamlProcessor; import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.cloudfoundry.identity.uaa.util.UaaMapUtils; import org.junit.Before; import org.junit.Test; import org.springframework.core.env.ConfigurableEnvironment; @@ -25,7 +26,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -125,7 +125,10 @@ public Map getLdapConfig(String config) throws UnsupportedEncodin YamlMapFactoryBean factory = new YamlMapFactoryBean(); factory.setResolutionMethod(YamlProcessor.ResolutionMethod.OVERRIDE_AND_IGNORE); factory.setResources(new Resource[]{new ByteArrayResource(config.getBytes("UTF-8"))}); - return (Map) factory.getObject().get("ldap"); + Map map = (Map) factory.getObject().get("ldap"); + Map result = new HashMap<>(); + result.put("ldap", map); + return UaaMapUtils.flatten(result); } @Test diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/util/UaaMapUtilsTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/util/UaaMapUtilsTest.java new file mode 100644 index 00000000000..543796077e0 --- /dev/null +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/util/UaaMapUtilsTest.java @@ -0,0 +1,59 @@ +/* + * ***************************************************************************** + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + * ***************************************************************************** + */ + +package org.cloudfoundry.identity.uaa.util; + +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; + + +public class UaaMapUtilsTest { + + @Test + public void testFlatten() { + Map top = new HashMap<>(); + Map secondA = new HashMap<>(); + Map secondB = new HashMap<>(); + Map thirdA = new HashMap<>(); + Map thirdB = new HashMap<>(); + Map thirdC = new HashMap<>(); + Map value = new HashMap<>(); + + top.put("secondA", secondA); + top.put("secondB", secondB); + + secondA.put("thirdA", thirdA); + secondA.put("thirdB", thirdB); + + secondB.put("thirdC", thirdC); + secondB.put("thirdB", thirdB); + + thirdA.put("keyA", "valueA"); + thirdB.put("keyB", "valueB"); + thirdC.put("keyC", "valueC"); + thirdC.put("value", value); + + Map flat = UaaMapUtils.flatten(top); + assertSame(value, flat.get("secondB.thirdC.value")); + assertSame(secondA, flat.get("secondA")); + assertEquals("valueC", flat.get("secondB.thirdC.keyC")); + + + } +} \ No newline at end of file diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicLdapAuthenticationManager.java b/login/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicLdapAuthenticationManager.java index 90aebf676fb..31121b666bb 100644 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicLdapAuthenticationManager.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicLdapAuthenticationManager.java @@ -5,7 +5,6 @@ import org.cloudfoundry.identity.uaa.scim.ScimGroupExternalMembershipManager; import org.cloudfoundry.identity.uaa.scim.ScimGroupProvisioning; import org.springframework.beans.BeansException; -import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.context.support.GenericApplicationContext; @@ -35,6 +34,10 @@ public DynamicLdapAuthenticationManager(LdapIdentityProviderDefinition definitio this.ldapLoginAuthenticationManager = ldapLoginAuthenticationManager; } + public ClassPathXmlApplicationContext getContext() { + return context; + } + public synchronized AuthenticationManager getLdapAuthenticationManager() throws BeansException { if (definition==null) { return null; diff --git a/login/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicZoneAwareAuthenticationManager.java b/login/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicZoneAwareAuthenticationManager.java index 378f7dab64d..9650cd55f11 100644 --- a/login/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicZoneAwareAuthenticationManager.java +++ b/login/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicZoneAwareAuthenticationManager.java @@ -38,19 +38,16 @@ public class DynamicZoneAwareAuthenticationManager implements AuthenticationMana private final IdentityProviderProvisioning provisioning; private final AuthenticationManager internalUaaAuthenticationManager; - private final AuthenticationManager authzAuthenticationMgr; private final ConcurrentMap ldapAuthManagers = new ConcurrentHashMap<>(); private final ScimGroupExternalMembershipManager scimGroupExternalMembershipManager; private final ScimGroupProvisioning scimGroupProvisioning; private final LdapLoginAuthenticationManager ldapLoginAuthenticationManager; - public DynamicZoneAwareAuthenticationManager(AuthenticationManager authzAuthenticationMgr, - IdentityProviderProvisioning provisioning, + public DynamicZoneAwareAuthenticationManager(IdentityProviderProvisioning provisioning, AuthenticationManager internalUaaAuthenticationManager, ScimGroupExternalMembershipManager scimGroupExternalMembershipManager, ScimGroupProvisioning scimGroupProvisioning, LdapLoginAuthenticationManager ldapLoginAuthenticationManager) { - this.authzAuthenticationMgr = authzAuthenticationMgr; this.provisioning = provisioning; this.internalUaaAuthenticationManager = internalUaaAuthenticationManager; this.scimGroupExternalMembershipManager = scimGroupExternalMembershipManager; @@ -61,13 +58,8 @@ public DynamicZoneAwareAuthenticationManager(AuthenticationManager authzAuthenti @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { IdentityZone zone = IdentityZoneHolder.get(); - //if zone==uaa just use the authzAuthenticationMgr bean - if (zone.equals(IdentityZone.getUaa())) { - return authzAuthenticationMgr.authenticate(authentication); - } else { - //chain it exactly like the UAA - return getChainedAuthenticationManager(zone).authenticate(authentication); - } + //chain it exactly like the UAA + return getChainedAuthenticationManager(zone).authenticate(authentication); } protected ChainedAuthenticationManager getChainedAuthenticationManager(IdentityZone zone) { diff --git a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml index c564aea8bd2..20e097d1856 100755 --- a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml +++ b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml @@ -308,81 +308,4 @@ - - - - - - - - - - - - - - - - - - org.cloudfoundry.identity.uaa.authentication.AccountNotVerifiedException - org.cloudfoundry.identity.uaa.authentication.AuthenticationPolicyRejectionException - - - - - - - - - org.springframework.security.authentication.ProviderNotFoundException - - - - - - - - - - - - - - - - - - - - - - - - - - - - org.cloudfoundry.identity.uaa.authentication.AccountNotVerifiedException - org.cloudfoundry.identity.uaa.authentication.AuthenticationPolicyRejectionException - - - - - - - - - org.springframework.security.authentication.ProviderNotFoundException - - - - - - - - - - - diff --git a/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml b/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml index 675981527b7..9d518d8a23a 100755 --- a/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml @@ -394,15 +394,8 @@ - - - - - - - diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/BootstrapTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/BootstrapTests.java index a469aeda247..033552d4de8 100755 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/BootstrapTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/BootstrapTests.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Cloud Foundry + * Cloud Foundry * Copyright (c) [2009-2014] Pivotal Software, Inc. All Rights Reserved. * * This product is licensed to you under the Apache License, Version 2.0 (the "License"). @@ -12,7 +12,7 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa; -import org.cloudfoundry.identity.uaa.authentication.manager.ChainedAuthenticationManager; +import org.cloudfoundry.identity.uaa.authentication.manager.DynamicZoneAwareAuthenticationManager; import org.cloudfoundry.identity.uaa.config.YamlServletProfileInitializer; import org.cloudfoundry.identity.uaa.oauth.ClientAdminBootstrap; import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; @@ -51,7 +51,7 @@ /** * @author Dave Syer - * + * */ public class BootstrapTests { @@ -122,11 +122,11 @@ public void testLdapProfile() throws Exception { context = getServletContext("ldap,default", "file:./src/main/webapp/WEB-INF/spring-servlet.xml"); AuthenticationManager authenticationManager = null; try { - authenticationManager = context.getBean("authzAuthenticationMgr", AuthenticationManager.class); + authenticationManager = context.getBean("zoneAwareAuthzAuthenticationManager", AuthenticationManager.class); } catch (NoSuchBeanDefinitionException e) { } assertNotNull(authenticationManager); - assertEquals(ChainedAuthenticationManager.class, authenticationManager.getClass()); + assertEquals(DynamicZoneAwareAuthenticationManager.class, authenticationManager.getClass()); } private ConfigurableApplicationContext getServletContext(String... resources) { diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicZoneAwareAuthenticationManagerTest.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicZoneAwareAuthenticationManagerTest.java index 6e85ee2953f..f933608cd10 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicZoneAwareAuthenticationManagerTest.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicZoneAwareAuthenticationManagerTest.java @@ -19,12 +19,10 @@ import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.Authentication; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -53,7 +51,6 @@ public class DynamicZoneAwareAuthenticationManagerTest { true); - AuthenticationManager authzAuthenticationMgr = mock(AuthenticationManager.class); AuthenticationManager uaaAuthenticationMgr = mock(AuthenticationManager.class); ScimGroupExternalMembershipManager scimGroupExternalMembershipManager = mock(ScimGroupExternalMembershipManager.class); ScimGroupProvisioning scimGroupProvisioning = mock(ScimGroupProvisioning.class); @@ -88,10 +85,9 @@ public void beforeAndAfter() throws Exception { @Test public void testAuthenticateInUaaZone() throws Exception { - when(authzAuthenticationMgr.authenticate(any(Authentication.class))).thenReturn(success); DynamicZoneAwareAuthenticationManager manager = getDynamicZoneAwareAuthenticationManager(); Authentication result = manager.authenticate(null); - assertSame(success, result); + assertNull(result); verifyZeroInteractions(uaaAuthenticationMgr); } @@ -107,7 +103,6 @@ public void testNonUAAZoneUaaNotActive() throws Exception { Authentication result = manager.authenticate(success); assertSame(success, result); verifyZeroInteractions(uaaAuthenticationMgr); - verifyZeroInteractions(authzAuthenticationMgr); } @Test @@ -125,7 +120,6 @@ public void testNonUAAZoneUaaActiveAccountNotVerified() throws Exception { //expected } verify(mockManager, times(0)).authenticate(any(Authentication.class)); - verifyZeroInteractions(authzAuthenticationMgr); } @Test @@ -143,7 +137,6 @@ public void testNonUAAZoneUaaActiveAccountLocked() throws Exception { //expected } verify(mockManager, times(0)).authenticate(any(Authentication.class)); - verifyZeroInteractions(authzAuthenticationMgr); } @Test @@ -156,7 +149,6 @@ public void testNonUAAZoneUaaActiveUaaAuthenticationSucccess() throws Exception DynamicLdapAuthenticationManager mockManager = manager.getLdapAuthenticationManager(null, null); assertSame(success, manager.authenticate(success)); verify(mockManager, times(0)).authenticate(any(Authentication.class)); - verifyZeroInteractions(authzAuthenticationMgr); } @Test @@ -169,7 +161,6 @@ public void testNonUAAZoneUaaActiveUaaAuthenticationFailure() throws Exception { DynamicLdapAuthenticationManager mockManager = manager.getLdapAuthenticationManager(null, null); when(mockManager.authenticate(any(Authentication.class))).thenReturn(success); assertSame(success, manager.authenticate(success)); - verifyZeroInteractions(authzAuthenticationMgr); } @Test @@ -184,7 +175,6 @@ public void testAuthenticateInNoneUaaZoneWithLdapProvider() throws Exception { Authentication result = manager.authenticate(success); assertSame(success, result); verifyZeroInteractions(uaaAuthenticationMgr); - verifyZeroInteractions(authzAuthenticationMgr); } @Test @@ -199,7 +189,6 @@ public void testAuthenticateInNoneUaaZoneWithInactiveProviders() throws Exceptio assertNull(manager.authenticate(success)); verifyZeroInteractions(uaaAuthenticationMgr); verifyZeroInteractions(mockManager); - verifyZeroInteractions(authzAuthenticationMgr); } protected DynamicZoneAwareAuthenticationManager getDynamicZoneAwareAuthenticationManager() { @@ -209,7 +198,6 @@ protected DynamicZoneAwareAuthenticationManager getDynamicZoneAwareAuthenticatio if (mock) { final DynamicLdapAuthenticationManager mockLdapManager = mock(DynamicLdapAuthenticationManager.class); return new DynamicZoneAwareAuthenticationManager( - authzAuthenticationMgr, providerProvisioning, uaaAuthenticationMgr, scimGroupExternalMembershipManager, @@ -225,7 +213,6 @@ public DynamicLdapAuthenticationManager getLdapAuthenticationManager(IdentityZon } else { return new DynamicZoneAwareAuthenticationManager( - authzAuthenticationMgr, providerProvisioning, uaaAuthenticationMgr, scimGroupExternalMembershipManager, diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java index c0c66362481..74bf7575eff 100755 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/BootstrapTests.java @@ -18,7 +18,6 @@ import org.cloudfoundry.identity.uaa.authentication.Origin; import org.cloudfoundry.identity.uaa.authentication.login.Prompt; import org.cloudfoundry.identity.uaa.authentication.manager.PeriodLockoutPolicy; -import org.cloudfoundry.identity.uaa.config.IdentityProviderBootstrap; import org.cloudfoundry.identity.uaa.config.LockoutPolicy; import org.cloudfoundry.identity.uaa.config.PasswordPolicy; import org.cloudfoundry.identity.uaa.config.YamlServletProfileInitializer; @@ -373,8 +372,6 @@ public void testBootstrappedIdps() throws Exception { SamlIdentityProviderConfigurator samlProviders = context.getBean("metaDataProviders", SamlIdentityProviderConfigurator.class); IdentityProviderProvisioning providerProvisioning = context.getBean("identityProviderProvisioning", IdentityProviderProvisioning.class); //ensure that ldap has been loaded up - assertNotNull(context.getBean("ldapPooled")); - assertFalse(context.getBean("ldapPooled", Boolean.class).booleanValue()); assertFalse(context.getBean(SimpleSearchQueryConverter.class).isDbCaseInsensitive()); //ensure we have some saml providers in login.yml //we have provided 4 here, but the original login.yml may add, but not remove some diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/authentication/AuthzAuthenticationManagerConfigurationTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/authentication/AuthzAuthenticationManagerConfigurationTests.java index a0b160dd617..37838ec44a2 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/authentication/AuthzAuthenticationManagerConfigurationTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/authentication/AuthzAuthenticationManagerConfigurationTests.java @@ -16,8 +16,6 @@ package org.cloudfoundry.identity.uaa.mock.authentication; import org.cloudfoundry.identity.uaa.authentication.manager.AuthzAuthenticationManager; -import org.cloudfoundry.identity.uaa.authentication.manager.ChainedAuthenticationManager; -import org.cloudfoundry.identity.uaa.authentication.manager.CheckIdpEnabledAuthenticationManager; import org.cloudfoundry.identity.uaa.authentication.manager.PeriodLockoutPolicy; import org.cloudfoundry.identity.uaa.test.YamlServletProfileInitializerContextInitializer; import org.junit.After; @@ -26,9 +24,6 @@ import org.springframework.mock.env.MockEnvironment; import org.springframework.web.context.support.XmlWebApplicationContext; -import java.util.Arrays; -import java.util.List; - import static org.junit.Assert.assertEquals; public class AuthzAuthenticationManagerConfigurationTests { @@ -51,24 +46,6 @@ public void tearDown() throws Exception { webApplicationContext = null; environment = null; } - /** - * We have a condition in the AutzhAuthenticationManager that automatically - * fails a password validation for zero length password. - * This test prevents that the authzAuthenticationMgr gets swapped out without - * the developer being notified. - * @throws Exception - */ - @Test - public void verifyAuthzAuthenticationManagerClassInStandardProfile() throws Exception { - webApplicationContext.refresh(); - String[] profiles = webApplicationContext.getEnvironment().getActiveProfiles(); - List plist = Arrays.asList(profiles); - if (plist.contains("ldap") || plist.contains("keystone")) { - assertEquals(ChainedAuthenticationManager.class, webApplicationContext.getBean("authzAuthenticationMgr").getClass()); - } else { - assertEquals(CheckIdpEnabledAuthenticationManager.class, webApplicationContext.getBean("authzAuthenticationMgr").getClass()); - } - } @Test public void testAuthzAuthenticationManagerUsesGlobalLockoutPolicy() throws Exception { diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java index aef470a8cea..fb389bccf1a 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java @@ -15,7 +15,7 @@ import org.cloudfoundry.identity.uaa.TestClassNullifier; import org.cloudfoundry.identity.uaa.authentication.Origin; import org.cloudfoundry.identity.uaa.authentication.manager.AuthzAuthenticationManager; -import org.cloudfoundry.identity.uaa.authentication.manager.ChainedAuthenticationManager; +import org.cloudfoundry.identity.uaa.authentication.manager.DynamicZoneAwareAuthenticationManager; import org.cloudfoundry.identity.uaa.ldap.ExtendedLdapUserMapper; import org.cloudfoundry.identity.uaa.ldap.LdapIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.ldap.ProcessLdapProperties; @@ -31,6 +31,7 @@ import org.cloudfoundry.identity.uaa.user.UaaUserDatabase; import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.cloudfoundry.identity.uaa.util.SetServerNameRequestPostProcessor; +import org.cloudfoundry.identity.uaa.util.UaaStringUtils; import org.cloudfoundry.identity.uaa.zone.IdentityProvider; import org.cloudfoundry.identity.uaa.zone.IdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.zone.IdentityProviderValidationRequest; @@ -76,6 +77,7 @@ import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.CookieCsrfPostProcessor.cookieCsrf; import static org.hamcrest.Matchers.arrayContainingInAnyOrder; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -137,7 +139,7 @@ public static void startApacheDS() throws Exception { apacheDS.start(); } - XmlWebApplicationContext webApplicationContext; + XmlWebApplicationContext mainContext; MockMvc mockMvc; TestClient testClient; @@ -172,39 +174,39 @@ public void setUp() throws Exception { mockEnvironment.setProperty("ldap.base.password","adminsecret"); mockEnvironment.setProperty("ldap.ssl.skipverification","true"); - webApplicationContext = new XmlWebApplicationContext(); - webApplicationContext.setEnvironment(mockEnvironment); - webApplicationContext.setServletContext(new MockServletContext()); - new YamlServletProfileInitializerContextInitializer().initializeContext(webApplicationContext, "uaa.yml,login.yml"); - webApplicationContext.setConfigLocation("file:./src/main/webapp/WEB-INF/spring-servlet.xml"); - webApplicationContext.getEnvironment().addActiveProfile("default"); - webApplicationContext.getEnvironment().addActiveProfile("ldap"); - webApplicationContext.refresh(); + mainContext = new XmlWebApplicationContext(); + mainContext.setEnvironment(mockEnvironment); + mainContext.setServletContext(new MockServletContext()); + new YamlServletProfileInitializerContextInitializer().initializeContext(mainContext, "uaa.yml,login.yml"); + mainContext.setConfigLocation("file:./src/main/webapp/WEB-INF/spring-servlet.xml"); + mainContext.getEnvironment().addActiveProfile("default"); + mainContext.getEnvironment().addActiveProfile("ldap"); + mainContext.refresh(); - List profiles = Arrays.asList(webApplicationContext.getEnvironment().getActiveProfiles()); + List profiles = Arrays.asList(mainContext.getEnvironment().getActiveProfiles()); Assume.assumeTrue(profiles.contains("ldap")); //we need to reinitialize the context if we change the ldap.profile.file property - FilterChainProxy springSecurityFilterChain = webApplicationContext.getBean("springSecurityFilterChain", FilterChainProxy.class); - mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).addFilter(springSecurityFilterChain) + FilterChainProxy springSecurityFilterChain = mainContext.getBean("springSecurityFilterChain", FilterChainProxy.class); + mockMvc = MockMvcBuilders.webAppContextSetup(mainContext).addFilter(springSecurityFilterChain) .build(); testClient = new TestClient(mockMvc); - jdbcTemplate = webApplicationContext.getBean(JdbcTemplate.class); - LimitSqlAdapter limitSqlAdapter = webApplicationContext.getBean(LimitSqlAdapter.class); + jdbcTemplate = mainContext.getBean(JdbcTemplate.class); + LimitSqlAdapter limitSqlAdapter = mainContext.getBean(LimitSqlAdapter.class); JdbcPagingListFactory pagingListFactory = new JdbcPagingListFactory(jdbcTemplate, limitSqlAdapter); gDB = new JdbcScimGroupProvisioning(jdbcTemplate, pagingListFactory); uDB = new JdbcScimUserProvisioning(jdbcTemplate, pagingListFactory); - userDatabase = webApplicationContext.getBean(UaaUserDatabase.class); + userDatabase = mainContext.getBean(UaaUserDatabase.class); } @After public void tearDown() throws Exception { System.clearProperty("ldap.profile.file"); System.clearProperty("ldap.base.mailSubstitute"); - if (webApplicationContext!=null) { - Flyway flyway = webApplicationContext.getBean(Flyway.class); + if (mainContext!=null) { + Flyway flyway = mainContext.getBean(Flyway.class); flyway.clean(); - webApplicationContext.destroy(); + mainContext.destroy(); } } @@ -393,7 +395,7 @@ public void testLdapConfigurationBeforeSave() throws Exception { .andReturn(); assertThat(result.getResponse().getContentAsString(), containsString("Caused by:")); - ProcessLdapProperties processLdapProperties = webApplicationContext.getBean(ProcessLdapProperties.class); + ProcessLdapProperties processLdapProperties = getBean(ProcessLdapProperties.class); if (processLdapProperties.isLdapsUrl()) { token = new UsernamePasswordAuthentication("marissa2", "ldap"); @@ -657,9 +659,25 @@ public void runLdapTestblock() throws Exception { deleteLdapUsers(); } + public Object getBean(String name) { + IdentityProviderProvisioning provisioning = mainContext.getBean(IdentityProviderProvisioning.class); + IdentityProvider ldapProvider = provisioning.retrieveByOrigin(Origin.LDAP, IdentityZoneHolder.get().getId()); + DynamicZoneAwareAuthenticationManager zm = mainContext.getBean(DynamicZoneAwareAuthenticationManager.class); + zm.getLdapAuthenticationManager(IdentityZone.getUaa(), ldapProvider).getLdapAuthenticationManager(); + return zm.getLdapAuthenticationManager(IdentityZone.getUaa(), ldapProvider).getContext().getBean(name); + } + + public T getBean(Class clazz) { + IdentityProviderProvisioning provisioning = mainContext.getBean(IdentityProviderProvisioning.class); + IdentityProvider ldapProvider = provisioning.retrieveByOrigin(Origin.LDAP, IdentityZoneHolder.get().getId()); + DynamicZoneAwareAuthenticationManager zm = mainContext.getBean(DynamicZoneAwareAuthenticationManager.class); + zm.getLdapAuthenticationManager(IdentityZone.getUaa(), ldapProvider).getLdapAuthenticationManager(); + return zm.getLdapAuthenticationManager(IdentityZone.getUaa(), ldapProvider).getContext().getBean(clazz); + } + public void printProfileType() throws Exception { - assertEquals(ldapProfile, webApplicationContext.getBean("testLdapProfile")); - assertEquals(ldapGroup, webApplicationContext.getBean("testLdapGroup")); + assertEquals(ldapProfile, getBean("testLdapProfile")); + assertEquals(ldapGroup, getBean("testLdapGroup")); } public void testLogin() throws Exception { @@ -708,7 +726,7 @@ public void testExtendedAttributes() throws Exception { } public void testAuthenticateInactiveIdp() throws Exception { - IdentityProviderProvisioning provisioning = webApplicationContext.getBean(IdentityProviderProvisioning.class); + IdentityProviderProvisioning provisioning = mainContext.getBean(IdentityProviderProvisioning.class); IdentityProvider ldapProvider = provisioning.retrieveByOrigin(Origin.LDAP, IdentityZone.getUaa().getId()); try { ldapProvider.setActive(false); @@ -776,7 +794,7 @@ public void validateCustomEmailForLdapUser() throws Exception { assertEquals("ldap", getOrigin(username)); assertEquals("marissa7@ldaptest.org",getEmail(username)); - ExtendedLdapUserMapper mapper = webApplicationContext.getBean(ExtendedLdapUserMapper.class); + ExtendedLdapUserMapper mapper = getBean(ExtendedLdapUserMapper.class); try { mapper.setMailSubstitute(null); assertNull(mapper.getMailSubstitute()); @@ -871,7 +889,7 @@ public void testLdapScopes() throws Exception { if (!ldapGroup.equals("ldap-groups-as-scopes.xml")) { return; } - AuthenticationManager manager = (AuthenticationManager)webApplicationContext.getBean("ldapAuthenticationManager"); + AuthenticationManager manager = (AuthenticationManager)getBean("ldapAuthenticationManager"); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("marissa3","ldap3"); Authentication auth = manager.authenticate(token); assertNotNull(auth); @@ -886,7 +904,7 @@ public void testLdapScopesFromChainedAuth() throws Exception { if (!ldapGroup.equals("ldap-groups-as-scopes.xml")) { return; } - AuthenticationManager manager = (AuthenticationManager)webApplicationContext.getBean("authzAuthenticationMgr"); + AuthenticationManager manager = (AuthenticationManager)mainContext.getBean("zoneAwareAuthzAuthenticationManager"); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("marissa3","ldap3"); Authentication auth = manager.authenticate(token); assertNotNull(auth); @@ -914,34 +932,29 @@ public void testNestedLdapScopes() throws Exception { if (!ldapGroup.equals("ldap-groups-as-scopes.xml")) { return; } - AuthenticationManager manager = (AuthenticationManager)webApplicationContext.getBean("ldapAuthenticationManager"); + Set defaultAuthorities = new HashSet((Set)mainContext.getBean("defaultUserAuthorities")); + AuthenticationManager manager = mainContext.getBean(DynamicZoneAwareAuthenticationManager.class); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("marissa4","ldap4"); Authentication auth = manager.authenticate(token); assertNotNull(auth); - String[] list = new String[] { - "test.read", - "test.write", - "test.everything", - }; - assertThat(list, arrayContainingInAnyOrder(getAuthorities(auth.getAuthorities()))); + defaultAuthorities.addAll(Arrays.asList("test.read","test.write","test.everything" )); + assertThat(UaaStringUtils.getStringsFromAuthorities(auth.getAuthorities()), containsInAnyOrder(defaultAuthorities.toArray())); } public void doTestNestedLdapGroupsMappedToScopes(String username, String password, String[] expected) throws Exception { if (!ldapGroup.equals("ldap-groups-map-to-scopes.xml")) { return; } - Set externalGroupSet = new HashSet(); - externalGroupSet.add("internal.superuser|cn=superusers,ou=scopes,dc=test,dc=com"); - externalGroupSet.add("internal.everything|cn=superusers,ou=scopes,dc=test,dc=com"); - externalGroupSet.add("internal.write|cn=operators,ou=scopes,dc=test,dc=com"); - externalGroupSet.add("internal.read|cn=developers,ou=scopes,dc=test,dc=com"); - AuthenticationManager manager = (AuthenticationManager)webApplicationContext.getBean("ldapAuthenticationManager"); + AuthenticationManager manager = mainContext.getBean(DynamicZoneAwareAuthenticationManager.class); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username,password); Authentication auth = manager.authenticate(token); assertNotNull(auth); - String[] list = expected; - assertThat(list, arrayContainingInAnyOrder(getAuthorities(auth.getAuthorities()))); + Set defaultAuthorities = new HashSet((Set)mainContext.getBean("defaultUserAuthorities")); + for (String s : expected) { + defaultAuthorities.add(s); + } + assertThat(UaaStringUtils.getStringsFromAuthorities(auth.getAuthorities()), containsInAnyOrder(defaultAuthorities.toArray())); } public void testNestedLdapGroupsMappedToScopes() throws Exception { @@ -1011,7 +1024,7 @@ public void testStopIfException() throws Exception { assertNotNull(user.getId()); performAuthentication("user@example.com", "n1cel0ngp455w0rd", HttpStatus.OK); - AuthzAuthenticationManager authzAuthenticationManager = webApplicationContext.getBean(AuthzAuthenticationManager.class); + AuthzAuthenticationManager authzAuthenticationManager = mainContext.getBean(AuthzAuthenticationManager.class); authzAuthenticationManager.setAllowUnverifiedUsers(false); performAuthentication("user@example.com", "n1cel0ngp455w0rd", HttpStatus.FORBIDDEN); } @@ -1021,20 +1034,14 @@ public void doTestNestedLdapGroupsMappedToScopesWithDefaultScopes(String usernam if (!ldapGroup.equals("ldap-groups-map-to-scopes.xml")) { return; } - Set externalGroupSet = new HashSet<>(); - externalGroupSet.add("internal.superuser|cn=superusers,ou=scopes,dc=test,dc=com"); - externalGroupSet.add("internal.everything|cn=superusers,ou=scopes,dc=test,dc=com"); - externalGroupSet.add("internal.write|cn=operators,ou=scopes,dc=test,dc=com"); - externalGroupSet.add("internal.read|cn=developers,ou=scopes,dc=test,dc=com"); - AuthenticationManager manager = webApplicationContext.getBean(ChainedAuthenticationManager.class); + AuthenticationManager manager = mainContext.getBean(DynamicZoneAwareAuthenticationManager.class); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username,password); Authentication auth = manager.authenticate(token); assertNotNull(auth); - Set defaultAuthorities = (Set)webApplicationContext.getBean("defaultUserAuthorities"); - String[] list = expected; - defaultAuthorities.addAll(Arrays.asList(list)); - list = defaultAuthorities.toArray(new String[0]); - assertThat(list, arrayContainingInAnyOrder(getAuthorities(auth.getAuthorities()))); + Set defaultAuthorities = new HashSet((Set)mainContext.getBean("defaultUserAuthorities")); + defaultAuthorities.addAll(Arrays.asList(expected)); + + assertThat(UaaStringUtils.getStringsFromAuthorities(auth.getAuthorities()), containsInAnyOrder(defaultAuthorities.toArray())); } From f862f2f1704020d9a8311fff93a18d78b102e939 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Fri, 23 Oct 2015 00:11:42 -0600 Subject: [PATCH 091/103] Add a test case to read custom LDAP user attributes --- .../LdapIdentityProviderDefinitionTest.java | 11 ++++- uaa/src/main/resources/ldap_db_init.ldif | 20 +++++++- uaa/src/main/resources/ldap_init.ldif | 15 ++++++ uaa/src/main/resources/uaa.yml | 25 +++++----- .../uaa/mock/ldap/LdapMockMvcTests.java | 48 +++++++++++++++++++ 5 files changed, 105 insertions(+), 14 deletions(-) diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinitionTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinitionTest.java index 7cd3807da5e..1ab9cc09f62 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinitionTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinitionTest.java @@ -290,7 +290,7 @@ public void test_Search_and_Compare_Config() throws Exception { } @Test - public void test_Search_and_Compare_With_Groups_1_Config() throws Exception { + public void test_Search_and_Compare_With_Groups_1_Config_And_Custom_Attributes() throws Exception { String config = "ldap:\n" + " profile:\n" + " file: ldap/ldap-search-and-compare.xml\n" + @@ -315,7 +315,10 @@ public void test_Search_and_Compare_With_Groups_1_Config() throws Exception { " searchSubtree: false\n" + " groupSearchFilter: member={0}\n" + " maxSearchDepth: 20\n" + - " autoAdd: false"; + " autoAdd: false\n"+ + " attributeMappings:\n" + + " user.attribute.employeeCostCenter: costCenter\n" + + " user.attribute.terribleBosses: manager\n"; LdapIdentityProviderDefinition def = LdapIdentityProviderDefinition.fromConfig(getLdapConfig(config)); @@ -339,6 +342,10 @@ public void test_Search_and_Compare_With_Groups_1_Config() throws Exception { assertEquals(20, def.getMaxGroupSearchDepth()); assertFalse(def.isAutoAddGroups()); assertEquals("scopenames", def.getGroupRoleAttribute()); + + assertEquals(2, def.getAttributeMappings().size()); + assertEquals("costCenter", def.getAttributeMappings().get("user.attribute.employeeCostCenter")); + assertEquals("manager", def.getAttributeMappings().get("user.attribute.terribleBosses")); } @Test diff --git a/uaa/src/main/resources/ldap_db_init.ldif b/uaa/src/main/resources/ldap_db_init.ldif index 26d2051b622..3ed255567e2 100644 --- a/uaa/src/main/resources/ldap_db_init.ldif +++ b/uaa/src/main/resources/ldap_db_init.ldif @@ -34,4 +34,22 @@ olcAccess: to attrs=userPassword olcAccess: to * by self write by dn.base="cn=admin,dc=test,dc=com" write - by * read \ No newline at end of file + by * read + +dn: cn=schema,cn=config +changetype: modify +add: olcAttributeTypes +olcAttributeTypes: ( + 1.3.6.1.4.1.35015.1.2.4 + NAME 'costCenter' + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + ) + +dn: cn=schema,cn=config +changetype: modify +add: olcAttributeTypes +olcAttributeTypes: ( + 1.3.6.1.4.1.35015.1.2.5 + NAME 'uaaManager' + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + ) \ No newline at end of file diff --git a/uaa/src/main/resources/ldap_init.ldif b/uaa/src/main/resources/ldap_init.ldif index cba440ff3e3..1ebd40cb037 100644 --- a/uaa/src/main/resources/ldap_init.ldif +++ b/uaa/src/main/resources/ldap_init.ldif @@ -120,6 +120,21 @@ userPassword: ldap8 uid: 20f459e0-e30b-4d1f-998c-3ded7f769db8 sn: Marissa8 +dn: cn=marissa9,ou=Users,dc=test,dc=com +changetype: add +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +objectClass: extensibleObject +cn: marissa9 +mail: marissa9@test.com +userPassword: ldap9 +uid: 20f459e0-e30b-4d1f-998c-3ded7f769db9 +sn: Marissa9 +costCenter: Denver,CO +uaaManager: John the Sloth +uaaManager: Kari the Ant Eater + ############################################################################### # BEGIN GROUP TO SCOPE MAPPING diff --git a/uaa/src/main/resources/uaa.yml b/uaa/src/main/resources/uaa.yml index 6410920b132..f102b96a54d 100755 --- a/uaa/src/main/resources/uaa.yml +++ b/uaa/src/main/resources/uaa.yml @@ -97,17 +97,20 @@ # attributeMappings: # given_name: firstName # family_name: surname - -#ldap: -# profile: -# file: ldap/ldap-search-and-bind.xml -# base: -# url: 'ldap://localhost:10389/' -# userDn: 'cn=admin,dc=test,dc=com' -# password: 'password' -# searchBase: 'dc=test,dc=com' -# searchFilter: 'cn={0}' -# referral: ignore +# user.attribute.employeeCostCenter: costCenter +# user.attribute.terribleBosses: uaaManager + + +ldap: + profile: + file: ldap/ldap-search-and-bind.xml + base: + url: 'ldap://localhost:389/' + userDn: 'cn=admin,dc=test,dc=com' + password: 'password' + searchBase: 'dc=test,dc=com' + searchFilter: 'cn={0}' + referral: follow # groups: # file: 'ldap/ldap-groups-map-to-scopes.xml' # searchBase: 'dc=test,dc=com' diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java index fb389bccf1a..c4373fb3592 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java @@ -14,6 +14,7 @@ import org.cloudfoundry.identity.uaa.TestClassNullifier; import org.cloudfoundry.identity.uaa.authentication.Origin; +import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; import org.cloudfoundry.identity.uaa.authentication.manager.AuthzAuthenticationManager; import org.cloudfoundry.identity.uaa.authentication.manager.DynamicZoneAwareAuthenticationManager; import org.cloudfoundry.identity.uaa.ldap.ExtendedLdapUserMapper; @@ -46,6 +47,7 @@ import org.junit.Assume; import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -59,6 +61,7 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContext; import org.springframework.security.ldap.server.ApacheDsSSLContainer; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; import org.springframework.security.web.FilterChainProxy; @@ -87,6 +90,7 @@ import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.http.MediaType.TEXT_HTML_VALUE; +import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; @@ -214,6 +218,36 @@ private void deleteLdapUsers() { jdbcTemplate.update("delete from users where origin='" + Origin.LDAP + "'"); } + @Test + @Ignore + public void testCustomUserAttributes() throws Exception { + Assume.assumeThat("ldap-groups-null.xml", StringContains.containsString(ldapGroup)); + + final String MANAGER = "manager"; + final String MANAGERS = MANAGER+"s"; + final String DENVER_CO = "Denver,CO"; + final String COST_CENTER = "costCenter"; + final String COST_CENTERS = COST_CENTER+"s"; + final String JOHN_THE_SLOTH = "John the Sloth"; + final String KARI_THE_ANT_EATER = "Kari the Ant Eater"; + + setUp(); + + String username = "marissa9"; + String password = "ldap9"; + MvcResult result = performUiAuthentication(username, password, HttpStatus.FOUND); + + UaaAuthentication authentication = (UaaAuthentication) ((SecurityContext) result.getRequest().getSession().getAttribute(SPRING_SECURITY_CONTEXT_KEY)).getAuthentication(); + + assertEquals("Expected two user attributes", 2, authentication.getUserAttributes().size()); + assertNotNull("Expected cost center attribute", authentication.getUserAttributes().get(COST_CENTERS)); + assertEquals(DENVER_CO, authentication.getUserAttributes().getFirst(COST_CENTERS)); + + assertNotNull("Expected manager attribute", authentication.getUserAttributes().get(MANAGERS)); + assertEquals("Expected 2 manager attribute values", 2, authentication.getUserAttributes().get(MANAGERS).size()); + assertThat(authentication.getUserAttributes().get(MANAGERS), containsInAnyOrder(JOHN_THE_SLOTH, KARI_THE_ANT_EATER)); + } + @Test public void testLdapConfigurationBeforeSave() throws Exception { Assume.assumeThat("ldap-search-and-bind.xml", StringContains.containsString(ldapProfile)); @@ -725,6 +759,7 @@ public void testExtendedAttributes() throws Exception { } + public void testAuthenticateInactiveIdp() throws Exception { IdentityProviderProvisioning provisioning = mainContext.getBean(IdentityProviderProvisioning.class); IdentityProvider ldapProvider = provisioning.retrieveByOrigin(Origin.LDAP, IdentityZone.getUaa().getId()); @@ -884,6 +919,19 @@ private MvcResult performAuthentication(String username, String password, HttpSt .andReturn(); } + private MvcResult performUiAuthentication(String username, String password, HttpStatus status) throws Exception { + MockHttpServletRequestBuilder post = + post("/login.do") + .with(cookieCsrf()) + .accept(MediaType.TEXT_HTML) + .param("username", username) + .param("password", password); + + return mockMvc.perform(post) + .andExpect(status().is(status.value())) + .andReturn(); + } + public void testLdapScopes() throws Exception { if (!ldapGroup.equals("ldap-groups-as-scopes.xml")) { From 629fc14e582394eeb5fae1f2965d019d1ed636fc Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Fri, 23 Oct 2015 09:12:14 -0600 Subject: [PATCH 092/103] LDAP and SAML documentation update --- docs/UAA-APIs.rst | 58 ++++++++++++----------------------------------- docs/UAA-LDAP.md | 46 +++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 43 deletions(-) diff --git a/docs/UAA-APIs.rst b/docs/UAA-APIs.rst index 4e45af6d09d..c8913dce4c2 100644 --- a/docs/UAA-APIs.rst +++ b/docs/UAA-APIs.rst @@ -1102,22 +1102,22 @@ Fields *Available Fields* :: emailDomain List Optional List of email domains associated with the UAA provider. If null and no domains are explicitly matched with any other providers, the UAA acts as a catch-all, wherein the email will be associated with the UAA provider. Wildcards supported. SAML Provider Configuration (provided in JSON format as part of the ``config`` field on the Identity Provider - See class org.cloudfoundry.identity.uaa.login.saml.SamlIdentityProviderDefinition - ====================== =============== ======== ================================================================================================================================================================================================= - idpEntityAlias String Required Must match ``originKey`` in the provider definition - zoneId String Required Must match ``identityZoneId`` in the provider definition - metaDataLocation String Required SAML Metadata - either an XML string or a URL that will deliver XML content - nameID String Optional The name ID to use for the username, default is "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified". Currently the UAA expects the username to be a valid email address. - assertionConsumerIndex int Optional SAML assertion consumer index, default is 0 - metadataTrustCheck boolean Optional Should metadata be validated, defaults to false - showSamlLink boolean Optional Should the SAML login link be displayed on the login page, defaults to false - linkText String Optional Required if the ``showSamlLink`` is set to true. - iconUrl String Optional Reserved for future use - emailDomain List Optional List of email domains associated with the SAML provider for the purpose of associating users to the correct origin upon invitation. If null or empty list, no invitations are accepted. Wildcards supported. - attributeMappings Map Optional List of UAA attributes mapped to attributes in the SAML assertion. Currently we support mapping given_name, family_name, email, phone_number and external_groups. - externalGroupsWhitelist List Optional List of external groups that will be included in the ID Token if the ROLES scope is requested. + ====================== ====================== ======== ================================================================================================================================================================================================================================================================================================================================================================================================================================================= + idpEntityAlias String Required Must match ``originKey`` in the provider definition + zoneId String Required Must match ``identityZoneId`` in the provider definition + metaDataLocation String Required SAML Metadata - either an XML string or a URL that will deliver XML content + nameID String Optional The name ID to use for the username, default is "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified". Currently the UAA expects the username to be a valid email address. + assertionConsumerIndex int Optional SAML assertion consumer index, default is 0 + metadataTrustCheck boolean Optional Should metadata be validated, defaults to false + showSamlLink boolean Optional Should the SAML login link be displayed on the login page, defaults to false + linkText String Optional Required if the ``showSamlLink`` is set to true. + iconUrl String Optional Reserved for future use + emailDomain List Optional List of email domains associated with the SAML provider for the purpose of associating users to the correct origin upon invitation. If null or empty list, no invitations are accepted. Wildcards supported. + attributeMappings Map Optional List of UAA attributes mapped to attributes in the SAML assertion. Currently we support mapping given_name, family_name, email, phone_number and external_groups. Also supports custom user attributes to be populated in the id_token when the `user_attributes` scope is requested. The attributes are pulled out of the user records and have the format `user.attribute.: ` + externalGroupsWhitelist List Optional List of external groups that will be included in the ID Token if the `roles` scope is requested. LDAP Provider Configuration (provided in JSON format as part of the ``config`` field on the Identity Provider - See class org.cloudfoundry.identity.uaa.ldap.LdapIdentityProviderDefinition - ====================== =============== ======== ================================================================================================================================================================================================= + ====================== ====================== ======== ================================================================================================================================================================================================= ldapProfileFile String Required Value must be "ldap/ldap-search-and-bind.xml" (until other configuration options are supported) ldapGroupFile String Required Value must be "ldap/ldap-groups-map-to-scopes.xml" (until other configuration options are supported) baseUrl String Required URL to LDAP server, starts with ldap:// or ldaps:// @@ -1136,7 +1136,7 @@ Fields *Available Fields* :: skipSSLVerification boolean Optional Set to true if you wish to skip SSL certificate verification emailDomain List Optional List of email domains associated with the LDAP provider for the purpose of associating users to the correct origin upon invitation. If null or empty list, no invitations are accepted. Wildcards supported. attributeMappings Map Optional List of UAA attributes mapped to attributes from LDAP. Currently we support mapping given_name, family_name, email, phone_number and external_groups. - externalGroupsWhitelist List Optional List of external groups that will be included in the ID Token if the ROLES scope is requested. + externalGroupsWhitelist List Optional List of external groups (`DN` distinguished names`) that can be included in the ID Token if the `roles` scope is requested. See `UAA-LDAP.md UAA-LDAP.md`_ for more information Curl Example POST (Creating a SAML provider):: @@ -2767,31 +2767,3 @@ for ease of use, and providing links to more detailed metrics. "spring.profiles.active": [] } -Detailed Metrics: ``GET /varz/{domain}`` ----------------------------------------- - -More detailed metrics can be obtained from the links in ``/varz``. All -except the ``env`` link (the OS env vars) are just the top-level domains -in the JMX ``MBeanServer``. In the case of ``Catalina`` there are some -known cycles in the object graph which we avoid by restricting the -result to the most interesting areas to do with request processing. - -* Request: ``GET /varz/{domain}`` -* Response Body (for domain=Catalina):: - - { - "global_request_processor": { - "http-8080": { - "processing_time": 0, - "max_time": 0, - "request_count": 0, - "bytes_sent": 0, - "bytes_received": 0, - "error_count": 0, - "modeler_type": "org.apache.coyote.RequestGroupInfo" - } - } - } - -Beans from the Spring application context are exposed at -``/varz/spring.application``. diff --git a/docs/UAA-LDAP.md b/docs/UAA-LDAP.md index e362b1bc9c9..3b89f57f217 100644 --- a/docs/UAA-LDAP.md +++ b/docs/UAA-LDAP.md @@ -513,6 +513,12 @@ In the above example, the user `marissa`'s UAA email always become `generated-m
This property is always used. +* `ldap.base.referral` + Should the LDAP client instruct the server to follow referrals. + Possible values are `ignore` and `follow`. The default is `follow` +
This property is always used. + + * `ldap.base.userDnPattern` one or more patterns used to construct DN. Contains one or more patterns used to construct a DN. @@ -619,6 +625,12 @@ In the above example, the user `marissa`'s UAA email always become `generated-m
This property is always used, but may be omitted when no group integration is desired. +* `ldap.groups.ignorePartialResultException` + How should the client react when it receives a `partial results` message back from the LDAP server. + If set to true, it is ignored. If set to false, authentication and group search will be marked as failed. + Default is `true`. User searches are always ignoring partial results, and always expect 1 result back from the query. + + * `ldap.group.searchBase` the search base for the group search. This references the [group-search-base](http://docs.spring.io/spring-security/site/docs/3.0.x/reference/ldap.html) @@ -661,3 +673,37 @@ In the above example, the user `marissa`'s UAA email always become `generated-m boolean value, true indicates that groups(scopes) will be added automatically if they don't exist
This property is used by the LDAP Groups as Scopes mapping + + +* `ldap.emailDomain` + List value, + Optional List of email domains associated with the UAA provider that selects an authentication source for an invited user. + If null and no domains are explicitly matched with any other providers, the UAA acts as a catch-all, + wherein the email will be associated with the UAA provider. Wildcards supported. + + +* `ldap.externalGroupsWhitelist` + List value, + Optional List of external groups that will be included in the ID Token if the `roles` scope is requested. + The list should contain `DN` values for the groups that are associated with the user. + The display name of the group in the ID token will be the taken from the `ldap.group.groupRoleAttribute` attribute + + +* `ldap.attributeMappings` + Map value where Object can be a String or a List, + Optional List of UAA attributes mapped to attributes from LDAP that are presented as part of the ID token + when the `profile` scope is requested. + Currently we support mapping for keys `given_name`(String), `family_name`(String), `phone_number`(String). + LDAP integration also supports custom user attributes to be populated in the id_token when the `user_attributes` scope + is requested. The attributes are pulled out of the user records and have the format + `user.attribute.: ` + +

+ldap:
+  attributeMappings:
+    first_name: givenname
+    given_name: sn
+    phone_number: telephonenumber
+    user.attribute.employeeCostCenter: costCenter
+    user.attribute.terribleBosses: manager
+
From 90d8c44bbe9b90f7dafc45387430bbd8d8d90068 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Fri, 23 Oct 2015 09:12:14 -0600 Subject: [PATCH 093/103] LDAP and SAML documentation update --- docs/UAA-APIs.rst | 58 ++++++++++++----------------------------------- docs/UAA-LDAP.md | 46 +++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 43 deletions(-) diff --git a/docs/UAA-APIs.rst b/docs/UAA-APIs.rst index 4e45af6d09d..c8913dce4c2 100644 --- a/docs/UAA-APIs.rst +++ b/docs/UAA-APIs.rst @@ -1102,22 +1102,22 @@ Fields *Available Fields* :: emailDomain List Optional List of email domains associated with the UAA provider. If null and no domains are explicitly matched with any other providers, the UAA acts as a catch-all, wherein the email will be associated with the UAA provider. Wildcards supported. SAML Provider Configuration (provided in JSON format as part of the ``config`` field on the Identity Provider - See class org.cloudfoundry.identity.uaa.login.saml.SamlIdentityProviderDefinition - ====================== =============== ======== ================================================================================================================================================================================================= - idpEntityAlias String Required Must match ``originKey`` in the provider definition - zoneId String Required Must match ``identityZoneId`` in the provider definition - metaDataLocation String Required SAML Metadata - either an XML string or a URL that will deliver XML content - nameID String Optional The name ID to use for the username, default is "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified". Currently the UAA expects the username to be a valid email address. - assertionConsumerIndex int Optional SAML assertion consumer index, default is 0 - metadataTrustCheck boolean Optional Should metadata be validated, defaults to false - showSamlLink boolean Optional Should the SAML login link be displayed on the login page, defaults to false - linkText String Optional Required if the ``showSamlLink`` is set to true. - iconUrl String Optional Reserved for future use - emailDomain List Optional List of email domains associated with the SAML provider for the purpose of associating users to the correct origin upon invitation. If null or empty list, no invitations are accepted. Wildcards supported. - attributeMappings Map Optional List of UAA attributes mapped to attributes in the SAML assertion. Currently we support mapping given_name, family_name, email, phone_number and external_groups. - externalGroupsWhitelist List Optional List of external groups that will be included in the ID Token if the ROLES scope is requested. + ====================== ====================== ======== ================================================================================================================================================================================================================================================================================================================================================================================================================================================= + idpEntityAlias String Required Must match ``originKey`` in the provider definition + zoneId String Required Must match ``identityZoneId`` in the provider definition + metaDataLocation String Required SAML Metadata - either an XML string or a URL that will deliver XML content + nameID String Optional The name ID to use for the username, default is "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified". Currently the UAA expects the username to be a valid email address. + assertionConsumerIndex int Optional SAML assertion consumer index, default is 0 + metadataTrustCheck boolean Optional Should metadata be validated, defaults to false + showSamlLink boolean Optional Should the SAML login link be displayed on the login page, defaults to false + linkText String Optional Required if the ``showSamlLink`` is set to true. + iconUrl String Optional Reserved for future use + emailDomain List Optional List of email domains associated with the SAML provider for the purpose of associating users to the correct origin upon invitation. If null or empty list, no invitations are accepted. Wildcards supported. + attributeMappings Map Optional List of UAA attributes mapped to attributes in the SAML assertion. Currently we support mapping given_name, family_name, email, phone_number and external_groups. Also supports custom user attributes to be populated in the id_token when the `user_attributes` scope is requested. The attributes are pulled out of the user records and have the format `user.attribute.: ` + externalGroupsWhitelist List Optional List of external groups that will be included in the ID Token if the `roles` scope is requested. LDAP Provider Configuration (provided in JSON format as part of the ``config`` field on the Identity Provider - See class org.cloudfoundry.identity.uaa.ldap.LdapIdentityProviderDefinition - ====================== =============== ======== ================================================================================================================================================================================================= + ====================== ====================== ======== ================================================================================================================================================================================================= ldapProfileFile String Required Value must be "ldap/ldap-search-and-bind.xml" (until other configuration options are supported) ldapGroupFile String Required Value must be "ldap/ldap-groups-map-to-scopes.xml" (until other configuration options are supported) baseUrl String Required URL to LDAP server, starts with ldap:// or ldaps:// @@ -1136,7 +1136,7 @@ Fields *Available Fields* :: skipSSLVerification boolean Optional Set to true if you wish to skip SSL certificate verification emailDomain List Optional List of email domains associated with the LDAP provider for the purpose of associating users to the correct origin upon invitation. If null or empty list, no invitations are accepted. Wildcards supported. attributeMappings Map Optional List of UAA attributes mapped to attributes from LDAP. Currently we support mapping given_name, family_name, email, phone_number and external_groups. - externalGroupsWhitelist List Optional List of external groups that will be included in the ID Token if the ROLES scope is requested. + externalGroupsWhitelist List Optional List of external groups (`DN` distinguished names`) that can be included in the ID Token if the `roles` scope is requested. See `UAA-LDAP.md UAA-LDAP.md`_ for more information Curl Example POST (Creating a SAML provider):: @@ -2767,31 +2767,3 @@ for ease of use, and providing links to more detailed metrics. "spring.profiles.active": [] } -Detailed Metrics: ``GET /varz/{domain}`` ----------------------------------------- - -More detailed metrics can be obtained from the links in ``/varz``. All -except the ``env`` link (the OS env vars) are just the top-level domains -in the JMX ``MBeanServer``. In the case of ``Catalina`` there are some -known cycles in the object graph which we avoid by restricting the -result to the most interesting areas to do with request processing. - -* Request: ``GET /varz/{domain}`` -* Response Body (for domain=Catalina):: - - { - "global_request_processor": { - "http-8080": { - "processing_time": 0, - "max_time": 0, - "request_count": 0, - "bytes_sent": 0, - "bytes_received": 0, - "error_count": 0, - "modeler_type": "org.apache.coyote.RequestGroupInfo" - } - } - } - -Beans from the Spring application context are exposed at -``/varz/spring.application``. diff --git a/docs/UAA-LDAP.md b/docs/UAA-LDAP.md index e362b1bc9c9..3b89f57f217 100644 --- a/docs/UAA-LDAP.md +++ b/docs/UAA-LDAP.md @@ -513,6 +513,12 @@ In the above example, the user `marissa`'s UAA email always become `generated-m
This property is always used. +* `ldap.base.referral` + Should the LDAP client instruct the server to follow referrals. + Possible values are `ignore` and `follow`. The default is `follow` +
This property is always used. + + * `ldap.base.userDnPattern` one or more patterns used to construct DN. Contains one or more patterns used to construct a DN. @@ -619,6 +625,12 @@ In the above example, the user `marissa`'s UAA email always become `generated-m
This property is always used, but may be omitted when no group integration is desired. +* `ldap.groups.ignorePartialResultException` + How should the client react when it receives a `partial results` message back from the LDAP server. + If set to true, it is ignored. If set to false, authentication and group search will be marked as failed. + Default is `true`. User searches are always ignoring partial results, and always expect 1 result back from the query. + + * `ldap.group.searchBase` the search base for the group search. This references the [group-search-base](http://docs.spring.io/spring-security/site/docs/3.0.x/reference/ldap.html) @@ -661,3 +673,37 @@ In the above example, the user `marissa`'s UAA email always become `generated-m boolean value, true indicates that groups(scopes) will be added automatically if they don't exist
This property is used by the LDAP Groups as Scopes mapping + + +* `ldap.emailDomain` + List value, + Optional List of email domains associated with the UAA provider that selects an authentication source for an invited user. + If null and no domains are explicitly matched with any other providers, the UAA acts as a catch-all, + wherein the email will be associated with the UAA provider. Wildcards supported. + + +* `ldap.externalGroupsWhitelist` + List value, + Optional List of external groups that will be included in the ID Token if the `roles` scope is requested. + The list should contain `DN` values for the groups that are associated with the user. + The display name of the group in the ID token will be the taken from the `ldap.group.groupRoleAttribute` attribute + + +* `ldap.attributeMappings` + Map value where Object can be a String or a List, + Optional List of UAA attributes mapped to attributes from LDAP that are presented as part of the ID token + when the `profile` scope is requested. + Currently we support mapping for keys `given_name`(String), `family_name`(String), `phone_number`(String). + LDAP integration also supports custom user attributes to be populated in the id_token when the `user_attributes` scope + is requested. The attributes are pulled out of the user records and have the format + `user.attribute.: ` + +
+ldap:
+  attributeMappings:
+    first_name: givenname
+    given_name: sn
+    phone_number: telephonenumber
+    user.attribute.employeeCostCenter: costCenter
+    user.attribute.terribleBosses: manager
+
From a252210f7d28cedfe13ed1db62e8a37f6916e651 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Fri, 23 Oct 2015 16:28:17 -0600 Subject: [PATCH 094/103] Configure both ApacheDS and OpenLDAP in such a way that we can have a shared LDIF data schema with custom attributes objectClass: customUaaUser costCenter: Denver,CO uaaManager: John the Sloth uaaManager: Kari the Ant Eater LDAP Search for OpenLDAP ldapsearch -b "dc=test,dc=com" -D 'cn=admin,dc=test,dc=com' -w password "cn=marissa9" LDAP Search for ApacheDS ldapsearch -b "dc=test,dc=com" -D 'uid=admin,ou=system' -w secret -p 33389 -h "localhost" "cn=marissa9" --- uaa/src/main/resources/ldap_db_init.ldif | 13 +- uaa/src/main/resources/ldap_init.ldif | 6 +- .../main/resources/ldap_init_apacheds.ldif | 38 +++ .../uaa/mock/ldap/LdapMockMvcTests.java | 8 +- .../ldap/server/ApacheDsSSLContainer.java | 230 +++++++++++++++++- 5 files changed, 279 insertions(+), 16 deletions(-) create mode 100644 uaa/src/main/resources/ldap_init_apacheds.ldif diff --git a/uaa/src/main/resources/ldap_db_init.ldif b/uaa/src/main/resources/ldap_db_init.ldif index 3ed255567e2..ea5d463a21f 100644 --- a/uaa/src/main/resources/ldap_db_init.ldif +++ b/uaa/src/main/resources/ldap_db_init.ldif @@ -52,4 +52,15 @@ olcAttributeTypes: ( 1.3.6.1.4.1.35015.1.2.5 NAME 'uaaManager' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 - ) \ No newline at end of file + ) + +dn: cn=schema,cn=config +changetype: modify +add: olcObjectClasses +olcObjectClasses: ( + 1.3.6.1.4.1.35015.1.2.6 + NAME 'customUaaUser' + SUP top + AUXILIARY + MUST (uaaManager $ costCenter) + ) diff --git a/uaa/src/main/resources/ldap_init.ldif b/uaa/src/main/resources/ldap_init.ldif index 1ebd40cb037..55bef42451a 100644 --- a/uaa/src/main/resources/ldap_init.ldif +++ b/uaa/src/main/resources/ldap_init.ldif @@ -54,6 +54,7 @@ changetype: add objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson +objectClass: customUaaUser cn: marissa3 userPassword: ldap3 uid: 20f459e0-e30b-4d1f-998c-3ded7f769db3 @@ -66,6 +67,9 @@ streetAddress: 1111 Marissa St l: Marissaville st: Florida postalCode: 32561 +costCenter: Denver,CO +uaaManager: John the Sloth +uaaManager: Kari the Ant Eater dn: cn=marissa4,ou=Users,dc=test,dc=com changetype: add @@ -125,7 +129,7 @@ changetype: add objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson -objectClass: extensibleObject +objectClass: customUaaUser cn: marissa9 mail: marissa9@test.com userPassword: ldap9 diff --git a/uaa/src/main/resources/ldap_init_apacheds.ldif b/uaa/src/main/resources/ldap_init_apacheds.ldif new file mode 100644 index 00000000000..ae809863473 --- /dev/null +++ b/uaa/src/main/resources/ldap_init_apacheds.ldif @@ -0,0 +1,38 @@ +dn: m-oid=1.3.6.1.4.1.15265.0.361,ou=attributeTypes,cn=other,ou=schema +changetype: add +objectClass: metaAttributeType +objectClass: metaTop +objectClass: top +objectClass: extensibleObject +m-name: costCenter +m-oid: 1.3.6.1.4.1.15265.0.361 +m-singlevalue: FALSE +m-syntax: 1.3.6.1.4.1.1466.115.121.1.44 +m-equality: caseIgnoreMatch +m-substr: caseIgnoreSubstringsMatch + +dn: m-oid=1.3.6.1.4.1.15265.0.362,ou=attributeTypes,cn=other,ou=schema +changetype: add +objectClass: metaAttributeType +objectClass: metaTop +objectClass: top +objectClass: extensibleObject +m-name: uaaManager +m-oid: 1.3.6.1.4.1.15265.0.362 +m-singlevalue: FALSE +m-syntax: 1.3.6.1.4.1.1466.115.121.1.44 +m-equality: caseIgnoreMatch +m-substr: caseIgnoreSubstringsMatch + + +dn: m-oid=1.3.6.1.4.1.15265.0.363, ou=objectClasses, cn=other, ou=schema +changetype: add +objectClass: metaObjectClass +objectClass: metaTop +objectClass: top +m-typeObjectClass: AUXILIARY +m-description: Holder of attributes since extensibleObject doesnt work +m-oid: 1.3.6.1.4.1.15265.0.363 +m-name: customUaaUser +m-must: costcenter +m-must: uaamanager \ No newline at end of file diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java index c4373fb3592..0ae2bbda629 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java @@ -52,6 +52,8 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.jdbc.core.JdbcTemplate; @@ -135,12 +137,11 @@ public static void startApacheDS() throws Exception { tmpDir.deleteOnExit(); System.out.println(tmpDir); //configure properties for running against ApacheDS - apacheDS = new ApacheDsSSLContainer("dc=test,dc=com","classpath:ldap_init.ldif"); + apacheDS = new ApacheDsSSLContainer("dc=test,dc=com",new Resource[] {new ClassPathResource("ldap_init_apacheds.ldif"), new ClassPathResource("ldap_init.ldif")}); apacheDS.setWorkingDirectory(tmpDir); apacheDS.setPort(33389); apacheDS.setSslPort(33636); apacheDS.afterPropertiesSet(); - apacheDS.start(); } XmlWebApplicationContext mainContext; @@ -221,6 +222,7 @@ private void deleteLdapUsers() { @Test @Ignore public void testCustomUserAttributes() throws Exception { + Thread.sleep(Long.MAX_VALUE); Assume.assumeThat("ldap-groups-null.xml", StringContains.containsString(ldapGroup)); final String MANAGER = "manager"; @@ -985,7 +987,7 @@ public void testNestedLdapScopes() throws Exception { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("marissa4","ldap4"); Authentication auth = manager.authenticate(token); assertNotNull(auth); - defaultAuthorities.addAll(Arrays.asList("test.read","test.write","test.everything" )); + defaultAuthorities.addAll(Arrays.asList("test.read", "test.write", "test.everything")); assertThat(UaaStringUtils.getStringsFromAuthorities(auth.getAuthorities()), containsInAnyOrder(defaultAuthorities.toArray())); } diff --git a/uaa/src/test/java/org/springframework/security/ldap/server/ApacheDsSSLContainer.java b/uaa/src/test/java/org/springframework/security/ldap/server/ApacheDsSSLContainer.java index 251d435250c..9386c0d2027 100644 --- a/uaa/src/test/java/org/springframework/security/ldap/server/ApacheDsSSLContainer.java +++ b/uaa/src/test/java/org/springframework/security/ldap/server/ApacheDsSSLContainer.java @@ -1,9 +1,26 @@ package org.springframework.security.ldap.server; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.directory.server.core.DefaultDirectoryService; +import org.apache.directory.server.core.authn.AuthenticationInterceptor; +import org.apache.directory.server.core.interceptor.Interceptor; +import org.apache.directory.server.core.partition.Partition; +import org.apache.directory.server.core.partition.impl.btree.jdbm.JdbmPartition; +import org.apache.directory.server.core.referral.ReferralInterceptor; import org.apache.directory.server.ldap.LdapServer; import org.apache.directory.server.ldap.handlers.extended.StartTlsHandler; +import org.apache.directory.server.protocol.shared.store.LdifFileLoader; import org.apache.directory.server.protocol.shared.transport.TcpTransport; +import org.apache.directory.shared.ldap.exception.LdapNameNotFoundException; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.Lifecycle; +import org.springframework.core.io.Resource; import sun.security.x509.AlgorithmId; import sun.security.x509.CertificateAlgorithmId; import sun.security.x509.CertificateSerialNumber; @@ -28,15 +45,30 @@ import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Date; +import java.util.List; import java.util.Random; -public class ApacheDsSSLContainer extends ApacheDSContainer { +public class ApacheDsSSLContainer implements InitializingBean, DisposableBean, Lifecycle, ApplicationContextAware { + private static final Log logger = LogFactory.getLog(ApacheDsSSLContainer.class); + + + final DefaultDirectoryService service; + LdapServer server; + + private ApplicationContext ctxt; + private File workingDir; + + private boolean running; + private final Resource[] ldifResources; + private final JdbmPartition partition; + private final String root; + private int port = 53389; private int sslPort = 53636; private String keystoreFile; - private File workingDir; private boolean useStartTLS = false; public boolean isUseStartTLS() { @@ -55,17 +87,44 @@ public void setKeystoreFile(String keystoreFile) { this.keystoreFile = keystoreFile; } - public ApacheDsSSLContainer(String root, String ldifs) throws Exception { - super(root, ldifs); + public ApacheDsSSLContainer(String root, Resource[] ldifs) throws Exception { + this.ldifResources = ldifs; + service = new DefaultDirectoryService(); + List list = new ArrayList(); + + //list.add(new NormalizationInterceptor()); + list.add(new AuthenticationInterceptor()); + list.add(new ReferralInterceptor()); + // list.add( new AciAuthorizationInterceptor() ); + // list.add( new DefaultAuthorizationInterceptor() ); + //list.add(new ExceptionInterceptor()); + // list.add( new ChangeLogInterceptor() ); + //list.add(new OperationalAttributeInterceptor()); + // list.add( new SchemaInterceptor() ); + //list.add(new SubentryInterceptor()); + // list.add( new CollectiveAttributeInterceptor() ); + // list.add( new EventInterceptor() ); + // list.add( new TriggerInterceptor() ); + // list.add( new JournalInterceptor() ); + + //service.setInterceptors(list); + partition = new JdbmPartition(); + partition.setId("rootPartition"); + partition.setSuffix(root); + this.root = root; + service.addPartition(partition); + service.setExitVmOnShutdown(false); + service.setShutdownHookEnabled(false); + service.getChangeLog().setEnabled(false); + service.setDenormalizeOpAttrsEnabled(true); } - @Override public void setWorkingDirectory(File workingDir) { - super.setWorkingDirectory(workingDir); this.workingDir = workingDir; if (!workingDir.mkdirs()) { - throw new RuntimeException("Unable to create directory:"+workingDir); + throw new RuntimeException("Unable to create directory:" + workingDir); } + service.setWorkingDirectory(workingDir); } public File getWorkingDirectory() { @@ -95,9 +154,7 @@ public void setSslPort(int sslPort) { this.sslPort = sslPort; } - @Override public void setPort(int port) { - super.setPort(port); this.port = port; } @@ -128,9 +185,9 @@ public File getKeystore(File directory) throws Exception { String keystoreName = "ldap.keystore"; File keystore = new File(directory, keystoreName); if (!keystore.createNewFile()) { - throw new FileNotFoundException("Unable to create file:"+keystore); + throw new FileNotFoundException("Unable to create file:" + keystore); } - keyStore.store(new FileOutputStream(keystore,false), keyPass); + keyStore.store(new FileOutputStream(keystore, false), keyPass); return keystore; } @@ -159,5 +216,156 @@ private static X509Certificate getSelfCertificate(X500Name x500Name, Date issueD } } + @Override + public void start() { + if (isRunning()) { + return; + } + + if (service.isStarted()) { + throw new IllegalStateException("DirectoryService is already running."); + } + + logger.info("Starting directory server..."); + try { + service.startup(); + server.start(); + } catch (Exception e) { + throw new RuntimeException("Server startup failed", e); + } + + try { + service.getAdminSession().lookup(partition.getSuffixDn()); + } catch (LdapNameNotFoundException e) { + try { +// LdapDN dn = new LdapDN(root); +// Assert.isTrue(root.startsWith("dc=")); +// String dc = root.substring(3, root.indexOf(',')); +// ServerEntry entry = service.newEntry(dn); +// entry.add("objectClass", "top", "domain", "extensibleObject"); +// entry.add("dc", dc); +// service.getAdminSession().add(entry); + addPartition("testPartition", root); + } catch (Exception e1) { + logger.error("Failed to create dc entry", e1); + } + } catch (Exception e) { + logger.error("Lookup failed", e); + } + + running = true; + + try { + importLdifs(); + } catch (Exception e) { + throw new RuntimeException("Failed to import LDIF file(s)", e); + } + } + + protected Partition addPartition(String partitionId, String partitionDn) + throws Exception { + Partition partition = new JdbmPartition(); + partition.setId(partitionId); + partition.setSuffix(partitionDn); + service.addPartition(partition); + return partition; + } + + public void stop() { + if (!isRunning()) { + return; + } + + logger.info("Shutting down directory server ..."); + try { + server.stop(); + service.shutdown(); + } catch (Exception e) { + logger.error("Shutdown failed", e); + return; + } + + running = false; + + if (workingDir.exists()) { + logger.info("Deleting working directory " + workingDir.getAbsolutePath()); + deleteDir(workingDir); + } + } + + protected void importLdifs() throws Exception { + // Import any ldif files + Resource[] ldifs = ldifResources; + + // Note that we can't just import using the ServerContext returned + // from starting Apache DS, apparently because of the long-running issue + // DIRSERVER-169. + // We need a standard context. + // DirContext dirContext = contextSource.getReadWriteContext(); + + if (ldifs == null || ldifs.length == 0) { + return; + } + for (Resource resource : ldifs) { + String ldifFile; + try { + ldifFile = resource.getFile().getAbsolutePath(); + } catch (IOException e) { + ldifFile = resource.getURI().toString(); + } + logger.info("Loading LDIF file: " + ldifFile); + new LdifFileLoader( + service.getAdminSession(), + new File(ldifFile), + null, + getClass().getClassLoader() + ).execute(); + } + } + + protected String createTempDirectory(String prefix) throws IOException { + String parentTempDir = System.getProperty("java.io.tmpdir"); + String fileNamePrefix = prefix + System.nanoTime(); + String fileName = fileNamePrefix; + + for (int i = 0; i < 1000; i++) { + File tempDir = new File(parentTempDir, fileName); + if (!tempDir.exists()) { + return tempDir.getAbsolutePath(); + } + fileName = fileNamePrefix + "~" + i; + } + + throw new IOException("Failed to create a temporary directory for file at " + + new File(parentTempDir, fileNamePrefix)); + } + + protected boolean deleteDir(File dir) { + if (dir.isDirectory()) { + String[] children = dir.list(); + for (String child : children) { + boolean success = deleteDir(new File(dir, child)); + if (!success) { + return false; + } + } + } + + return dir.delete(); + } + + public boolean isRunning() { + return running; + } + + public void destroy() throws Exception { + stop(); + } + + public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { + ctxt = applicationContext; + } + } From 6f50f708d6cff78f1bccc830dab680fe7ecbaa89 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Mon, 26 Oct 2015 09:53:40 -0700 Subject: [PATCH 095/103] Fix indentation Signed-off-by: Jeremy Coffield --- uaa/src/main/resources/login.yml | 10 +++++----- uaa/src/main/resources/uaa.yml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/uaa/src/main/resources/login.yml b/uaa/src/main/resources/login.yml index be6e13e40e9..f783d80f393 100644 --- a/uaa/src/main/resources/login.yml +++ b/uaa/src/main/resources/login.yml @@ -133,13 +133,13 @@ login: # iconUrl: 'http://link.to/icon.jpg' # addShadowUserOnLogin: true # externalGroupsWhitelist: -# - admin -# - user +# - admin +# - user # emailDomain: -# - example.com +# - example.com # attributeMappings: -# given_name: firstName -# family_name: surname +# given_name: firstName +# family_name: surname # okta-local-2: # idpMetadata: | # MIICmTCCAgKgAwIBAgIGAUPATqmEMA0GCSqGSIb3DQEBBQUAMIGPMQswCQYDVQQGEwJVUzETMBEG diff --git a/uaa/src/main/resources/uaa.yml b/uaa/src/main/resources/uaa.yml index f102b96a54d..de49bac7c00 100755 --- a/uaa/src/main/resources/uaa.yml +++ b/uaa/src/main/resources/uaa.yml @@ -90,8 +90,8 @@ # -----END CERTIFICATE-----' # sslCertificateAlias: ldaps # externalGroupsWhitelist: -# - admin -# - user +# - admin +# - user # emailDomain: # - example.com # attributeMappings: From 4be51ce5aa79152d1500709d17cf5c2bbd2f139d Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Mon, 26 Oct 2015 10:44:45 -0600 Subject: [PATCH 096/103] Map custom user attributes for LDAP users into the id_token https://www.pivotaltracker.com/story/show/102288632 [#102288632] --- .../ExternalLoginAuthenticationManager.java | 15 +- .../LdapLoginAuthenticationManager.java | 38 ++++- .../uaa/config/IdentityProviderBootstrap.java | 22 ++- .../uaa/ldap/ExtendedLdapUserDetails.java | 6 +- .../ldap/LdapIdentityProviderDefinition.java | 43 +++--- .../ldap/extension/ExtendedLdapUserImpl.java | 17 +++ .../identity/uaa/util/UaaMapUtils.java | 61 +++++++- .../LdapLoginAuthenticationManagerTests.java | 137 +++++++++++++++--- .../config/IdentityProviderBootstrapTest.java | 4 +- .../uaa/ldap/ExtendedLdapUserMapperTest.java | 12 +- uaa/src/main/resources/ldap_init.ldif | 18 +++ uaa/src/main/resources/uaa.yml | 20 +-- .../main/webapp/WEB-INF/spring-servlet.xml | 1 + .../uaa/mock/ldap/LdapMockMvcTests.java | 11 +- 14 files changed, 321 insertions(+), 84 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java index 9396b32593d..0dc83341a65 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java @@ -42,10 +42,10 @@ import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; -import java.util.Collections; import java.util.Date; -import java.util.Map; public class ExternalLoginAuthenticationManager implements AuthenticationManager, ApplicationEventPublisherAware, BeanNameAware { @@ -119,17 +119,26 @@ public Authentication authenticate(Authentication request) throws Authentication } else { uaaAuthenticationDetails = UaaAuthenticationDetails.UNKNOWN; } - Authentication success = new UaaAuthentication(new UaaPrincipal(user), user.getAuthorities(), uaaAuthenticationDetails); + UaaAuthentication success = new UaaAuthentication(new UaaPrincipal(user), user.getAuthorities(), uaaAuthenticationDetails); + if (request.getPrincipal() instanceof UserDetails) { + success.setUserAttributes(getUserAttributes((UserDetails) request.getPrincipal())); + } publish(new UserAuthenticationSuccessEvent(user, success)); return success; } + protected MultiValueMap getUserAttributes(UserDetails request) { + return new LinkedMultiValueMap<>(); + } + protected void publish(ApplicationEvent event) { if (eventPublisher != null) { eventPublisher.publishEvent(event); } } + + protected UaaUser userAuthenticated(Authentication request, UaaUser user) { return user; } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java index 5354e75a197..484619b4a1b 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java @@ -17,16 +17,48 @@ import org.apache.commons.lang.StringUtils; import org.cloudfoundry.identity.uaa.ldap.ExtendedLdapUserDetails; +import org.cloudfoundry.identity.uaa.ldap.LdapIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.user.UaaUser; -import org.cloudfoundry.identity.uaa.user.UaaUserPrototype; +import org.cloudfoundry.identity.uaa.zone.IdentityProvider; +import org.cloudfoundry.identity.uaa.zone.IdentityProviderProvisioning; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.util.MultiValueMap; -import java.util.Collections; -import java.util.Date; +import java.util.Arrays; +import java.util.Map; public class LdapLoginAuthenticationManager extends ExternalLoginAuthenticationManager { + public static final String USER_ATTRIBUTE_PREFIX = "user.attribute."; private boolean autoAddAuthorities = false; + private IdentityProviderProvisioning provisioning; + + public void setProvisioning(IdentityProviderProvisioning provisioning) { + this.provisioning = provisioning; + } + + @Override + protected MultiValueMap getUserAttributes(UserDetails request) { + MultiValueMap result = super.getUserAttributes(request); + if (provisioning!=null) { + IdentityProvider provider = provisioning.retrieveByOrigin(getOrigin(), IdentityZoneHolder.get().getId()); + if (request instanceof ExtendedLdapUserDetails) { + ExtendedLdapUserDetails ldapDetails = ((ExtendedLdapUserDetails) request); + for (Map.Entry entry : provider.getConfigValue(LdapIdentityProviderDefinition.class).getAttributeMappings().entrySet()) { + if (entry.getKey().startsWith(USER_ATTRIBUTE_PREFIX) && entry.getValue() != null) { + String key = entry.getKey().substring(USER_ATTRIBUTE_PREFIX.length()); + String[] values = ldapDetails.getAttribute((String) entry.getValue(), false); + if (values != null && values.length > 0) { + result.put(key, Arrays.asList(values)); + } + } + } + } + } + return result; + } @Override protected UaaUser userAuthenticated(Authentication request, UaaUser user) { diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrap.java b/common/src/main/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrap.java index 9ae62369611..667d9de20ef 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrap.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrap.java @@ -90,40 +90,48 @@ public void setLdapConfig(HashMap ldapConfig) { protected void addLdapProvider() { boolean ldapProfile = Arrays.asList(environment.getActiveProfiles()).contains(Origin.LDAP); if (ldapConfig != null || ldapProfile) { - boolean active = ldapProfile && ldapConfig!=null; IdentityProvider provider = new IdentityProvider(); provider.setActive(ldapProfile); provider.setOriginKey(Origin.LDAP); provider.setType(Origin.LDAP); provider.setName("UAA LDAP Provider"); - provider.setActive(active); Map ldap = new HashMap<>(); ldap.put(LDAP, ldapConfig); - String json = getLdapConfigAsDefinition(ldap); - provider.setConfig(json); + LdapIdentityProviderDefinition json = getLdapConfigAsDefinition(ldap); + provider.setConfig(JsonUtils.writeValueAsString(json)); + provider.setActive(ldapProfile && json.isConfigured()); providers.add(provider); } } - protected String getLdapConfigAsDefinition(Map ldapConfig) { + protected LdapIdentityProviderDefinition getLdapConfigAsDefinition(Map ldapConfig) { ldapConfig = UaaMapUtils.flatten(ldapConfig); populateLdapEnvironment(ldapConfig); if (ldapConfig.isEmpty()) { - return JsonUtils.writeValueAsString(new LdapIdentityProviderDefinition()); + return new LdapIdentityProviderDefinition(); } - return JsonUtils.writeValueAsString(LdapIdentityProviderDefinition.fromConfig(ldapConfig)); + return LdapIdentityProviderDefinition.fromConfig(ldapConfig); } protected void populateLdapEnvironment(Map ldapConfig) { //this method reads the environment and overwrites values (needed by LdapMockMvcTests that overrides properties through env) AbstractEnvironment env = (AbstractEnvironment)environment; + //these are our known complex data structures in the properties for (String property : LDAP_PROPERTY_NAMES) { if (env.containsProperty(property) && LDAP_PROPERTY_TYPES.get(property)!=null) { ldapConfig.put(property, env.getProperty(property, LDAP_PROPERTY_TYPES.get(property))); } } + + //but we can also have string properties like ldap.attributeMappings.user.attribute.mapToAttributeName=mapFromAttributeName + Map stringProperties = UaaMapUtils.getPropertiesStartingWith(env, "ldap."); + for (Map.Entry entry : stringProperties.entrySet()) { + if (!LDAP_PROPERTY_NAMES.contains(entry.getKey())) { + ldapConfig.put(entry.getKey(), entry.getValue()); + } + } } public void setKeystoneConfig(HashMap keystoneConfig) { diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserDetails.java b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserDetails.java index fbbc4264bd6..4ae79ce5ba5 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserDetails.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserDetails.java @@ -24,8 +24,10 @@ public interface ExtendedLdapUserDetails extends LdapUserDetails, Mailable, Named, DialableByPhone, ExternallyIdentifiable { - public String[] getMail(); + String[] getMail(); - public Map getAttributes(); + Map getAttributes(); + + String[] getAttribute(String name, boolean caseSensitive); } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinition.java b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinition.java index db10bad7e6b..1beaaa4819f 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinition.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinition.java @@ -211,27 +211,28 @@ public static LdapIdentityProviderDefinition fromConfig(Map ldap } definition.setLdapProfileFile((String) ldapConfig.get(LDAP_PROFILE_FILE)); - if (definition.getLdapProfileFile()==null) { - return definition; - } - switch (definition.getLdapProfileFile()) { - case LDAP_PROFILE_FILE_SIMPLE_BIND: { - definition.setUserDNPattern((String) ldapConfig.get(LDAP_BASE_USER_DN_PATTERN)); - if (ldapConfig.get(LDAP_BASE_USER_DN_PATTERN_DELIMITER)!=null) { - definition.setUserDNPatternDelimiter((String)ldapConfig.get(LDAP_BASE_USER_DN_PATTERN_DELIMITER)); + final String profileFile = definition.getLdapProfileFile(); + if (StringUtils.hasText(profileFile)) { + switch (profileFile) { + case LDAP_PROFILE_FILE_SIMPLE_BIND: { + definition.setUserDNPattern((String) ldapConfig.get(LDAP_BASE_USER_DN_PATTERN)); + if (ldapConfig.get(LDAP_BASE_USER_DN_PATTERN_DELIMITER) != null) { + definition.setUserDNPatternDelimiter((String) ldapConfig.get(LDAP_BASE_USER_DN_PATTERN_DELIMITER)); + } + break; } - break; - } - case LDAP_PROFILE_FILE_SEARCH_AND_COMPARE: - case LDAP_PROFILE_FILE_SEARCH_AND_BIND: { - definition.setBindUserDn((String) ldapConfig.get(LDAP_BASE_USER_DN)); - definition.setBindPassword((String) ldapConfig.get(LDAP_BASE_PASSWORD)); - definition.setUserSearchBase((String) ldapConfig.get(LDAP_BASE_SEARCH_BASE)); - definition.setUserSearchFilter((String) ldapConfig.get(LDAP_BASE_SEARCH_FILTER)); - break; + case LDAP_PROFILE_FILE_SEARCH_AND_COMPARE: + case LDAP_PROFILE_FILE_SEARCH_AND_BIND: { + definition.setBindUserDn((String) ldapConfig.get(LDAP_BASE_USER_DN)); + definition.setBindPassword((String) ldapConfig.get(LDAP_BASE_PASSWORD)); + definition.setUserSearchBase((String) ldapConfig.get(LDAP_BASE_SEARCH_BASE)); + definition.setUserSearchFilter((String) ldapConfig.get(LDAP_BASE_SEARCH_FILTER)); + break; + } + default: + break; } - default: return definition; } definition.setBaseUrl((String) ldapConfig.get(LDAP_BASE_URL)); @@ -259,6 +260,12 @@ public static LdapIdentityProviderDefinition fromConfig(Map ldap definition.setAutoAddGroups((Boolean) ldapConfig.get(LDAP_GROUPS_AUTO_ADD)); definition.setGroupRoleAttribute((String) ldapConfig.get(LDAP_GROUPS_GROUP_ROLE_ATTRIBUTE)); } + final String LDAP_ATTR_MAP_PREFIX = "ldap."+ATTRIBUTE_MAPPINGS+"."; + for (Map.Entry entry : ldapConfig.entrySet()) { + if (!LDAP_PROPERTY_NAMES.contains(entry.getKey()) && entry.getKey().startsWith(LDAP_ATTR_MAP_PREFIX+USER_ATTRIBUTE_PREFIX)) { + definition.addAttributeMapping(entry.getKey().substring(LDAP_ATTR_MAP_PREFIX.length()), entry.getValue()); + } + } return definition; } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/extension/ExtendedLdapUserImpl.java b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/extension/ExtendedLdapUserImpl.java index fb39e73634d..d7c9092c863 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/extension/ExtendedLdapUserImpl.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/extension/ExtendedLdapUserImpl.java @@ -73,6 +73,23 @@ public Map getAttributes() { return Collections.unmodifiableMap(attributes); } + @Override + public String[] getAttribute(String name, boolean caseSensitive) { + if (name==null) { + return null; + } + String[] value = getAttributes().get(name); + if (value != null || caseSensitive) { + return getAttributes().get(name); + } + for (Map.Entry a : getAttributes().entrySet()) { + if (a.getKey().equalsIgnoreCase(name)) { + return a.getValue(); + } + } + return null; + } + public String getDn() { return dn; } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/util/UaaMapUtils.java b/common/src/main/java/org/cloudfoundry/identity/uaa/util/UaaMapUtils.java index 3b7b64079a3..250d9681059 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/util/UaaMapUtils.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/util/UaaMapUtils.java @@ -16,23 +16,78 @@ import org.cloudfoundry.identity.uaa.config.NestedMapPropertySource; +import org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.PropertySource; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.Map.Entry; public class UaaMapUtils { public static Map flatten(Map map) { HashMap result = new HashMap<>(); - if (map==null || map.isEmpty()) { + if (map == null || map.isEmpty()) { return result; } - NestedMapPropertySource properties = new NestedMapPropertySource("map",map); + NestedMapPropertySource properties = new NestedMapPropertySource("map", map); for (String property : properties.getPropertyNames()) { - if (properties.getProperty(property)!=null) { + if (properties.getProperty(property) != null) { result.put(property, properties.getProperty(property)); } } return result; } + + + public static Map getPropertiesStartingWith(ConfigurableEnvironment aEnv, + String aKeyPrefix) { + Map result = new HashMap<>(); + Map map = getAllProperties(aEnv); + for (Entry entry : map.entrySet()) { + String key = entry.getKey(); + if (key.startsWith(aKeyPrefix)) { + result.put(key, entry.getValue()); + } + } + return result; + } + + public static Map getAllProperties(ConfigurableEnvironment aEnv) { + Map result = new HashMap<>(); + aEnv.getPropertySources().forEach(ps -> addAll(result, getAllProperties(ps))); + return result; + } + + public static Map getAllProperties(PropertySource aPropSource) { + Map result = new HashMap<>(); + + if (aPropSource instanceof CompositePropertySource) { + CompositePropertySource cps = (CompositePropertySource) aPropSource; + cps.getPropertySources().forEach(ps -> addAll(result, getAllProperties(ps))); + return result; + } + + if (aPropSource instanceof EnumerablePropertySource) { + EnumerablePropertySource ps = (EnumerablePropertySource) aPropSource; + Arrays.asList(ps.getPropertyNames()).forEach(key -> result.put(key, ps.getProperty(key))); + return result; + } + + //unable to iterate over it + + return result; + } + + private static void addAll(Map aBase, Map aToBeAdded) { + for (Entry entry : aToBeAdded.entrySet()) { + if (aBase.containsKey(entry.getKey())) { + continue; + } + aBase.put(entry.getKey(), entry.getValue()); + } + } } diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java index 4eec6427f30..ce1ddaa4ada 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java @@ -15,11 +15,15 @@ package org.cloudfoundry.identity.uaa.authentication.manager; import org.cloudfoundry.identity.uaa.authentication.Origin; +import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; +import org.cloudfoundry.identity.uaa.ldap.LdapIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.ldap.extension.ExtendedLdapUserImpl; import org.cloudfoundry.identity.uaa.user.UaaAuthority; import org.cloudfoundry.identity.uaa.user.UaaUser; import org.cloudfoundry.identity.uaa.user.UaaUserDatabase; import org.cloudfoundry.identity.uaa.user.UaaUserPrototype; +import org.cloudfoundry.identity.uaa.zone.IdentityProvider; +import org.cloudfoundry.identity.uaa.zone.IdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.junit.Before; import org.junit.Test; @@ -35,11 +39,14 @@ import java.util.HashMap; import java.util.Map; +import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.USER_ATTRIBUTE_PREFIX; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyObject; +import static org.junit.Assert.assertThat; import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -57,6 +64,14 @@ public class LdapLoginAuthenticationManagerTests { private final String PHONE_NUMBER_ATTTRIBUTE = "digits"; private static LdapUserDetails userDetails; + final String DENVER_CO = "Denver,CO"; + final String COST_CENTER = "costCenter"; + final String COST_CENTERS = "costCenters"; + final String JOHN_THE_SLOTH = "John the Sloth"; + final String KARI_THE_ANT_EATER = "Kari the Ant Eater"; + final String UAA_MANAGER = "uaaManager"; + final String MANAGERS = "managers"; + private static LdapUserDetails mockLdapUserDetails() { userDetails = mock(LdapUserDetails.class); setupGeneralExpectations(userDetails); @@ -87,6 +102,9 @@ private static void setupGeneralExpectations(UserDetails userDetails) { UaaUser dbUser = getUaaUser(); Authentication auth; ExtendedLdapUserImpl authUserDetail; + IdentityProviderProvisioning provisioning; + IdentityProvider provider; + LdapIdentityProviderDefinition definition; @Before @@ -103,6 +121,31 @@ public void setUp() { when(db.retrieveUserById(anyString())).thenReturn(dbUser); am.setUserDatabase(db); when(auth.getAuthorities()).thenReturn(null); + + provider = mock(IdentityProvider.class); + provisioning = mock(IdentityProviderProvisioning.class); + when(provisioning.retrieveByOrigin(anyString(),anyString())).thenReturn(provider); + Map attributeMappings = new HashMap<>(); + definition = LdapIdentityProviderDefinition.searchAndBindMapGroupToScopes( + "baseUrl", + "bindUserDn", + "bindPassword", + "userSearchBase", + "userSearchFilter", + "grouSearchBase", + "groupSearchFilter", + "mailAttributeName", + "mailSubstitute", + false, + false, + false, + 1, + false + ); + definition.addAttributeMapping(USER_ATTRIBUTE_PREFIX+MANAGERS, UAA_MANAGER); + definition.addAttributeMapping(USER_ATTRIBUTE_PREFIX+COST_CENTERS, COST_CENTER); + when(provider.getConfigValue(LdapIdentityProviderDefinition.class)).thenReturn(definition); + am.setProvisioning(provisioning); } @Test @@ -166,7 +209,40 @@ public void dontUpdate_existingUser_if_attributes_same() throws Exception { assertEquals(user.getModified(), captor.getValue().getUser().getModified()); } - private ExtendedLdapUserImpl getAuthDetails(String email, String givenName, String familyName, String phoneNumber) { + @Test + public void test_custom_user_attributes() throws Exception { + + UaaUser user = getUaaUser(); + ExtendedLdapUserImpl authDetails = + getAuthDetails( + user.getEmail(), + user.getGivenName(), + user.getFamilyName(), + user.getPhoneNumber(), + new AttributeInfo(UAA_MANAGER, new String[] {KARI_THE_ANT_EATER, JOHN_THE_SLOTH}), + new AttributeInfo(COST_CENTER, new String[] {DENVER_CO}) + ); + when(auth.getPrincipal()).thenReturn(authDetails); + + UaaUserDatabase db = mock(UaaUserDatabase.class); + when(db.retrieveUserByName(anyString(), eq(Origin.LDAP))).thenReturn(user); + when(db.retrieveUserById(anyString())).thenReturn(user); + am.setOrigin(Origin.LDAP); + am.setUserDatabase(db); + + UaaAuthentication authentication = (UaaAuthentication)am.authenticate(auth); + + assertEquals("Expected two user attributes", 2, authentication.getUserAttributes().size()); + assertNotNull("Expected cost center attribute", authentication.getUserAttributes().get(COST_CENTERS)); + assertEquals(DENVER_CO, authentication.getUserAttributes().getFirst(COST_CENTERS)); + + assertNotNull("Expected manager attribute", authentication.getUserAttributes().get(MANAGERS)); + assertEquals("Expected 2 manager attribute values", 2, authentication.getUserAttributes().get(MANAGERS).size()); + assertThat(authentication.getUserAttributes().get(MANAGERS), containsInAnyOrder(JOHN_THE_SLOTH, KARI_THE_ANT_EATER)); + + } + + private ExtendedLdapUserImpl getAuthDetails(String email, String givenName, String familyName, String phoneNumber, AttributeInfo... attributes) { String[] emails = {email}; String[] given_names = {givenName}; String[] family_names = {familyName}; @@ -175,6 +251,10 @@ private ExtendedLdapUserImpl getAuthDetails(String email, String givenName, Stri info.put(GIVEN_NAME_ATTRIBUTE, given_names); info.put(FAMILY_NAME_ATTRIBUTE, family_names); info.put(PHONE_NUMBER_ATTTRIBUTE, phone_numbers); + for (AttributeInfo i : attributes) { + info.put(i.getName(), i.getValues()); + } + authUserDetail = new ExtendedLdapUserImpl(mockLdapUserDetails(), info); authUserDetail.setMailAttributeName(EMAIL_ATTRIBUTE); authUserDetail.setGivenNameAttributeName(GIVEN_NAME_ATTRIBUTE); @@ -185,21 +265,40 @@ private ExtendedLdapUserImpl getAuthDetails(String email, String givenName, Stri protected UaaUser getUaaUser() { return new UaaUser(new UaaUserPrototype() - .withId("id") - .withUsername(USERNAME) - .withPassword("password") - .withEmail(TEST_EMAIL) - .withAuthorities(UaaAuthority.USER_AUTHORITIES) - .withGivenName("givenname") - .withFamilyName("familyname") - .withPhoneNumber("8675309") - .withCreated(new Date()) - .withModified(new Date()) - .withOrigin(Origin.ORIGIN) - .withExternalId(DN) - .withVerified(false) - .withZoneId(IdentityZoneHolder.get().getId()) - .withSalt(null) - .withPasswordLastModified(null)); + .withId("id") + .withUsername(USERNAME) + .withPassword("password") + .withEmail(TEST_EMAIL) + .withAuthorities(UaaAuthority.USER_AUTHORITIES) + .withGivenName("givenname") + .withFamilyName("familyname") + .withPhoneNumber("8675309") + .withCreated(new Date()) + .withModified(new Date()) + .withOrigin(Origin.ORIGIN) + .withExternalId(DN) + .withVerified(false) + .withZoneId(IdentityZoneHolder.get().getId()) + .withSalt(null) + .withPasswordLastModified(null)); + } + + + public static class AttributeInfo { + final String name; + final String[] values; + + public AttributeInfo(String name, String[] values) { + this.name = name; + this.values = values; + } + + public String getName() { + return name; + } + + public String[] getValues() { + return values; + } } } \ No newline at end of file diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrapTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrapTest.java index c2d196131bf..3c333487ffd 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrapTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityProviderBootstrapTest.java @@ -109,13 +109,12 @@ public void testRemovedLdapBootstrapIsInactive() throws Exception { env.setActiveProfiles(Origin.LDAP); IdentityProviderBootstrap bootstrap = new IdentityProviderBootstrap(provisioning, env); HashMap ldapConfig = new HashMap<>(); - ldapConfig.put("testkey","testvalue"); + ldapConfig.put("base.url","ldap://localhost:389/"); bootstrap.setLdapConfig(ldapConfig); bootstrap.afterPropertiesSet(); IdentityProvider ldapProvider = provisioning.retrieveByOrigin(Origin.LDAP, IdentityZoneHolder.get().getId()); assertNotNull(ldapProvider); - assertEquals(JsonUtils.writeValueAsString(LdapIdentityProviderDefinition.fromConfig(new HashMap<>())), ldapProvider.getConfig()); assertNotNull(ldapProvider.getCreated()); assertNotNull(ldapProvider.getLastModified()); assertEquals(Origin.LDAP, ldapProvider.getType()); @@ -134,7 +133,6 @@ public void testRemovedLdapBootstrapIsInactive() throws Exception { bootstrap.afterPropertiesSet(); ldapProvider = provisioning.retrieveByOrigin(Origin.LDAP, IdentityZoneHolder.get().getId()); assertNotNull(ldapProvider); - assertEquals(JsonUtils.writeValueAsString(new LdapIdentityProviderDefinition()), ldapProvider.getConfig()); assertNotNull(ldapProvider.getCreated()); assertNotNull(ldapProvider.getLastModified()); assertEquals(Origin.LDAP, ldapProvider.getType()); diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserMapperTest.java b/common/src/test/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserMapperTest.java index a04a0540724..64ae637e028 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserMapperTest.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserMapperTest.java @@ -8,16 +8,9 @@ import org.springframework.ldap.core.NameAwareAttributes; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.ldap.ppolicy.PasswordPolicyControl; -import javax.naming.Name; -import javax.naming.NamingEnumeration; import javax.naming.directory.Attributes; -import javax.naming.directory.BasicAttributes; -import javax.naming.directory.DirContext; import javax.naming.ldap.LdapName; -import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -27,10 +20,6 @@ import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class ExtendedLdapUserMapperTest { @@ -38,6 +27,7 @@ public class ExtendedLdapUserMapperTest { private DirContextAdapter adapter; private ExtendedLdapUserMapper mapper; private Collection authorities; + private String UAA_MANAGER; @Before public void setUp() throws Exception { diff --git a/uaa/src/main/resources/ldap_init.ldif b/uaa/src/main/resources/ldap_init.ldif index 55bef42451a..6cbedceeeb9 100644 --- a/uaa/src/main/resources/ldap_init.ldif +++ b/uaa/src/main/resources/ldap_init.ldif @@ -212,6 +212,24 @@ objectclass: extensibleObject member: cn=marissa8,ou=Users,dc=test,dc=com ref: ldap://localhost:43389/cn=otherusers1,ou=scopes,dc=test,dc=com +#This groups contains scopes as comma separated list in the description attribute +dn: cn=marissaniner,ou=scopes,dc=test,dc=com +changetype: add +objectClass: groupOfNames +objectClass: top +cn: marissaniner +description: marissaniner +member: cn=marissa9,ou=Users,dc=test,dc=com + +#This groups contains scopes as comma separated list in the description attribute +dn: cn=marissaniner2,ou=scopes,dc=test,dc=com +changetype: add +objectClass: groupOfNames +objectClass: top +cn: marissaniner2 +description: marissaniner2 +member: cn=marissa9,ou=Users,dc=test,dc=com + ############################################################################### # END GROUP TO SCOPE MAPPING ############################################################################### diff --git a/uaa/src/main/resources/uaa.yml b/uaa/src/main/resources/uaa.yml index de49bac7c00..f9af3cb5bd2 100755 --- a/uaa/src/main/resources/uaa.yml +++ b/uaa/src/main/resources/uaa.yml @@ -101,16 +101,16 @@ # user.attribute.terribleBosses: uaaManager -ldap: - profile: - file: ldap/ldap-search-and-bind.xml - base: - url: 'ldap://localhost:389/' - userDn: 'cn=admin,dc=test,dc=com' - password: 'password' - searchBase: 'dc=test,dc=com' - searchFilter: 'cn={0}' - referral: follow +#ldap: +# profile: +# file: ldap/ldap-search-and-bind.xml +# base: +# url: 'ldap://localhost:389/' +# userDn: 'cn=admin,dc=test,dc=com' +# password: 'password' +# searchBase: 'dc=test,dc=com' +# searchFilter: 'cn={0}' +# referral: follow # groups: # file: 'ldap/ldap-groups-map-to-scopes.xml' # searchBase: 'dc=test,dc=com' diff --git a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml index 20e097d1856..cf5637ce585 100755 --- a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml +++ b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml @@ -306,6 +306,7 @@ + diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java index 0ae2bbda629..3f106159d55 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java @@ -47,7 +47,6 @@ import org.junit.Assume; import org.junit.Before; import org.junit.BeforeClass; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -80,6 +79,7 @@ import java.util.List; import java.util.Set; +import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.ATTRIBUTE_MAPPINGS; import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.CookieCsrfPostProcessor.cookieCsrf; import static org.hamcrest.Matchers.arrayContainingInAnyOrder; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -220,19 +220,20 @@ private void deleteLdapUsers() { } @Test - @Ignore public void testCustomUserAttributes() throws Exception { - Thread.sleep(Long.MAX_VALUE); Assume.assumeThat("ldap-groups-null.xml", StringContains.containsString(ldapGroup)); - final String MANAGER = "manager"; - final String MANAGERS = MANAGER+"s"; + final String MANAGER = "uaaManager"; + final String MANAGERS = "managers"; final String DENVER_CO = "Denver,CO"; final String COST_CENTER = "costCenter"; final String COST_CENTERS = COST_CENTER+"s"; final String JOHN_THE_SLOTH = "John the Sloth"; final String KARI_THE_ANT_EATER = "Kari the Ant Eater"; + createMockEnvironment(); + mockEnvironment.setProperty("ldap."+ ATTRIBUTE_MAPPINGS+".user.attribute."+MANAGERS, MANAGER); + mockEnvironment.setProperty("ldap."+ATTRIBUTE_MAPPINGS+".user.attribute."+COST_CENTERS, COST_CENTER); setUp(); String username = "marissa9"; From 52ec3edb67139e9263b9af4f0e1b00a56ddf36f4 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Mon, 26 Oct 2015 16:27:05 -0600 Subject: [PATCH 097/103] Add in integration test for LDAP getting an id_token with custom attributes --- .../uaa/integration/LdapIntegationTests.java | 192 ++++++++++++++++++ .../util/IntegrationTestUtils.java | 33 +++ 2 files changed, 225 insertions(+) create mode 100644 uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/LdapIntegationTests.java diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/LdapIntegationTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/LdapIntegationTests.java new file mode 100644 index 00000000000..09f19d2c2f1 --- /dev/null +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/LdapIntegationTests.java @@ -0,0 +1,192 @@ +/* + * ***************************************************************************** + * Cloud Foundry + * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + * ***************************************************************************** + */ +package org.cloudfoundry.identity.uaa.integration; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.cloudfoundry.identity.uaa.ServerRunning; +import org.cloudfoundry.identity.uaa.authentication.Origin; +import org.cloudfoundry.identity.uaa.client.ClientConstants; +import org.cloudfoundry.identity.uaa.integration.util.IntegrationTestUtils; +import org.cloudfoundry.identity.uaa.ldap.LdapIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.oauth.Claims; +import org.cloudfoundry.identity.uaa.scim.ScimUser; +import org.cloudfoundry.identity.uaa.test.UaaTestAccounts; +import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.cloudfoundry.identity.uaa.zone.IdentityProvider; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.security.jwt.Jwt; +import org.springframework.security.jwt.JwtHelper; +import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; +import org.springframework.security.oauth2.provider.client.BaseClientDetails; +import org.springframework.web.client.RestTemplate; + +import java.net.Inet4Address; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.USER_ATTRIBUTE_PREFIX; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assume.assumeTrue; + +public class LdapIntegationTests { + + @Rule + public ServerRunning serverRunning = ServerRunning.isRunning(); + + + @Test + public void test_LDAP_Custom_User_Attributes_In_ID_Token() throws Exception { + + final String COST_CENTER = "costCenter"; + final String COST_CENTERS = "costCenters"; + final String DENVER_CO = "Denver,CO"; + final String MANAGER = "uaaManager"; + final String MANAGERS = "managers"; + final String JOHN_THE_SLOTH = "John the Sloth"; + final String KARI_THE_ANT_EATER = "Kari the Ant Eater"; + + + //ensure we are able to resolve DNS for hostname testzone1.localhost + assumeTrue("Expected LDAP profile to be enabled testzone1/2.localhost to resolve to 127.0.0.1", doesSupportZoneDNS_and_isLdapEnabled()); + + String baseUrl = serverRunning.getBaseUrl(); + + String zoneId = "testzone1"; + String zoneUrl = baseUrl.replace("localhost", "testzone1.localhost"); + + //identity client token + RestTemplate identityClient = IntegrationTestUtils.getClientCredentialsTemplate( + IntegrationTestUtils.getClientCredentialsResource(baseUrl, new String[]{"zones.write", "zones.read", "scim.zones"}, "identity", "identitysecret") + ); + //admin client token - to create users + RestTemplate adminClient = IntegrationTestUtils.getClientCredentialsTemplate( + IntegrationTestUtils.getClientCredentialsResource(baseUrl, new String[0], "admin", "adminsecret") + ); + //create the zone + IntegrationTestUtils.createZoneOrUpdateSubdomain(identityClient, baseUrl, zoneId, zoneId); + + //create a zone admin user + String email = new RandomValueStringGenerator().generate() +"@samltesting.org"; + ScimUser user = IntegrationTestUtils.createUser(adminClient, baseUrl,email ,"firstname", "lastname", email, true); + IntegrationTestUtils.makeZoneAdmin(identityClient, baseUrl, user.getId(), zoneId); + + //get the zone admin token + String zoneAdminToken = + IntegrationTestUtils.getAuthorizationCodeToken(serverRunning, + UaaTestAccounts.standard(serverRunning), + "identity", + "identitysecret", + email, + "secr3T"); + + LdapIdentityProviderDefinition ldapIdentityProviderDefinition = LdapIdentityProviderDefinition.searchAndBindMapGroupToScopes( + "ldap://localhost:389/", + "cn=admin,dc=test,dc=com", + "password", + "dc=test,dc=com", + "cn={0}", + "ou=scopes,dc=test,dc=com", + "member={0}", + "mail", + null, + false, + true, + true, + 100, + true); + ldapIdentityProviderDefinition.addAttributeMapping(USER_ATTRIBUTE_PREFIX+COST_CENTERS, COST_CENTER); + ldapIdentityProviderDefinition.addAttributeMapping(USER_ATTRIBUTE_PREFIX+MANAGERS, MANAGER); + + IdentityProvider provider = new IdentityProvider(); + provider.setIdentityZoneId(zoneId); + provider.setType(Origin.LDAP); + provider.setActive(true); + provider.setConfig(JsonUtils.writeValueAsString(ldapIdentityProviderDefinition)); + provider.setOriginKey(Origin.LDAP); + provider.setName("simplesamlphp for uaa"); + provider = IntegrationTestUtils.createOrUpdateProvider(zoneAdminToken,baseUrl,provider); + assertNotNull(provider.getId()); + + assertEquals(Origin.LDAP, provider.getOriginKey()); + + List idps = Arrays.asList(provider.getOriginKey()); + + String adminClientInZone = new RandomValueStringGenerator().generate(); + BaseClientDetails clientDetails = new BaseClientDetails(adminClientInZone, null, "openid,user_attributes", "password,authorization_code,client_credentials", "uaa.admin,scim.read,scim.write,uaa.resource", zoneUrl); + clientDetails.setClientSecret("secret"); + clientDetails.addAdditionalInformation(ClientConstants.AUTO_APPROVE, true); + clientDetails.addAdditionalInformation(ClientConstants.ALLOWED_PROVIDERS, idps); + + clientDetails = IntegrationTestUtils.createClientAsZoneAdmin(zoneAdminToken, baseUrl, zoneId, clientDetails); + clientDetails.setClientSecret("secret"); + + + String idToken = + (String) IntegrationTestUtils.getPasswordToken(zoneUrl, + clientDetails.getClientId(), + clientDetails.getClientSecret(), + "marissa9", + "ldap9", + "openid user_attributes") + .get("id_token"); + + assertNotNull(idToken); + + Jwt idTokenClaims = JwtHelper.decode(idToken); + Map claims = JsonUtils.readValue(idTokenClaims.getClaims(), new TypeReference>() {}); + + assertNotNull(claims.get(Claims.USER_ATTRIBUTES)); + Map> userAttributes = (Map>) claims.get(Claims.USER_ATTRIBUTES); + assertThat(userAttributes.get(COST_CENTERS), containsInAnyOrder(DENVER_CO)); + assertThat(userAttributes.get(MANAGERS), containsInAnyOrder(JOHN_THE_SLOTH, KARI_THE_ANT_EATER)); + + //no user_attribute scope provided + idToken = + (String) IntegrationTestUtils.getPasswordToken(zoneUrl, + clientDetails.getClientId(), + clientDetails.getClientSecret(), + "marissa9", + "ldap9", + "openid") + .get("id_token"); + + assertNotNull(idToken); + + idTokenClaims = JwtHelper.decode(idToken); + claims = JsonUtils.readValue(idTokenClaims.getClaims(), new TypeReference>() {}); + assertNull(claims.get(Claims.USER_ATTRIBUTES)); + } + + protected boolean doesSupportZoneDNS_and_isLdapEnabled() { + String profile = System.getProperty("spring.profiles.active",""); + if (!profile.contains(Origin.LDAP)) { + return false; + } + + try { + return Arrays.equals(Inet4Address.getByName("testzone1.localhost").getAddress(), new byte[] {127,0,0,1}) && + Arrays.equals(Inet4Address.getByName("testzone2.localhost").getAddress(), new byte[] {127,0,0,1}); + } catch (UnknownHostException e) { + return false; + } + } + +} diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/util/IntegrationTestUtils.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/util/IntegrationTestUtils.java index 064382bab9b..9967e5dd1ac 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/util/IntegrationTestUtils.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/util/IntegrationTestUtils.java @@ -602,6 +602,39 @@ public static String getClientCredentialsToken(String baseUrl, return accessToken.getValue(); } + public static Map getPasswordToken(String baseUrl, + String clientId, + String clientSecret, + String username, + String password, + String scopes) throws Exception { + RestTemplate template = new RestTemplate(); + template.setRequestFactory(new StatelessRequestFactory()); + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("grant_type", "password"); + formData.add("client_id", clientId); + formData.add("username", username); + formData.add("password", password); + formData.add("response_type", "token id_token"); + if (StringUtils.hasText(scopes)) { + formData.add("scope", scopes); + } + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("Authorization", "Basic " + new String(Base64.encode(String.format("%s:%s", clientId, clientSecret).getBytes()))); + + @SuppressWarnings("rawtypes") + ResponseEntity response = template.exchange( + baseUrl + "/oauth/token", + HttpMethod.POST, + new HttpEntity(formData, headers), + Map.class); + + Assert.assertEquals(HttpStatus.OK, response.getStatusCode()); + return response.getBody(); + } + public static String getClientCredentialsToken(ServerRunning serverRunning, String clientId, String clientSecret) throws Exception { From d22af90effe8a96e8281761cbe1a9c761719e3bf Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Mon, 26 Oct 2015 18:42:11 -0600 Subject: [PATCH 098/103] One must be able to customize the name of the person attributes for LDAP https://www.pivotaltracker.com/story/show/106326628 [#106326628] --- .../ExternalLoginAuthenticationManager.java | 2 - .../LdapLoginAuthenticationManager.java | 13 +++- .../ldap/LdapIdentityProviderDefinition.java | 3 +- .../ldap/extension/ExtendedLdapUserImpl.java | 59 ++++--------------- docs/UAA-LDAP.md | 3 +- uaa/src/main/resources/ldap-integration.xml | 6 +- uaa/src/main/resources/ldap_init.ldif | 4 ++ .../uaa/mock/ldap/LdapMockMvcTests.java | 24 ++++++-- 8 files changed, 51 insertions(+), 63 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java index 0dc83341a65..871fa284030 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java @@ -137,8 +137,6 @@ protected void publish(ApplicationEvent event) { } } - - protected UaaUser userAuthenticated(Authentication request, UaaUser user) { return user; } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java index 484619b4a1b..2398b4ca530 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java @@ -27,6 +27,8 @@ import org.springframework.util.MultiValueMap; import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Map; public class LdapLoginAuthenticationManager extends ExternalLoginAuthenticationManager { @@ -39,6 +41,9 @@ public void setProvisioning(IdentityProviderProvisioning provisioning) { this.provisioning = provisioning; } + public static final List ALREADY_MAPPED_ATTRS = + Collections.unmodifiableList(Arrays.asList("first_name", "family_name", "phone_number")); + @Override protected MultiValueMap getUserAttributes(UserDetails request) { MultiValueMap result = super.getUserAttributes(request); @@ -49,9 +54,11 @@ protected MultiValueMap getUserAttributes(UserDetails request) { for (Map.Entry entry : provider.getConfigValue(LdapIdentityProviderDefinition.class).getAttributeMappings().entrySet()) { if (entry.getKey().startsWith(USER_ATTRIBUTE_PREFIX) && entry.getValue() != null) { String key = entry.getKey().substring(USER_ATTRIBUTE_PREFIX.length()); - String[] values = ldapDetails.getAttribute((String) entry.getValue(), false); - if (values != null && values.length > 0) { - result.put(key, Arrays.asList(values)); + if (! ALREADY_MAPPED_ATTRS.contains(key)) { + String[] values = ldapDetails.getAttribute((String) entry.getValue(), false); + if (values != null && values.length > 0) { + result.put(key, Arrays.asList(values)); + } } } } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinition.java b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinition.java index 1beaaa4819f..2ac0e3f7852 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinition.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinition.java @@ -14,6 +14,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.config.NestedMapPropertySource; import org.springframework.core.env.AbstractEnvironment; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; @@ -301,7 +302,7 @@ public ConfigurableEnvironment getLdapConfigurationEnvironment() { setIfNotNull(LDAP_PROFILE_FILE, getLdapProfileFile(), properties); setIfNotNull(LDAP_SSL_SKIPVERIFICATION, isSkipSSLVerification(), properties); - MapPropertySource source = new MapPropertySource("ldap", properties); + MapPropertySource source = new NestedMapPropertySource("ldap", properties); return new LdapConfigEnvironment(source); } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/extension/ExtendedLdapUserImpl.java b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/extension/ExtendedLdapUserImpl.java index d7c9092c863..311ad84562f 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/extension/ExtendedLdapUserImpl.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/extension/ExtendedLdapUserImpl.java @@ -18,7 +18,6 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.ldap.userdetails.LdapUserDetails; -import org.springframework.util.StringUtils; import java.util.Collection; import java.util.Collections; @@ -39,9 +38,6 @@ public class ExtendedLdapUserImpl implements ExtendedLdapUserDetails { private boolean accountNonLocked = true; private boolean credentialsNonExpired = true; private boolean enabled = true; - // PPolicy data - private int timeBeforeExpiration = Integer.MAX_VALUE; - private int graceLoginsRemaining = Integer.MAX_VALUE; private Map attributes = new HashMap<>(); public ExtendedLdapUserImpl(LdapUserDetails details) { @@ -154,52 +150,24 @@ public void setEnabled(boolean enabled) { this.enabled = enabled; } - public int getTimeBeforeExpiration() { - return timeBeforeExpiration; - } - - public void setTimeBeforeExpiration(int timeBeforeExpiration) { - this.timeBeforeExpiration = timeBeforeExpiration; - } - - public int getGraceLoginsRemaining() { - return graceLoginsRemaining; - } - - public void setGraceLoginsRemaining(int graceLoginsRemaining) { - this.graceLoginsRemaining = graceLoginsRemaining; - } - public String getMailAttributeName() { return mailAttributeName; } public void setMailAttributeName(String mailAttributeName) { - this.mailAttributeName = mailAttributeName.toLowerCase(); - } - - public String getPhoneNumberAttributeName() { - return phoneNumberAttributeName; + this.mailAttributeName = mailAttributeName; } public void setPhoneNumberAttributeName(String phoneNumberAttributeName) { - this.phoneNumberAttributeName = phoneNumberAttributeName == null ? null : phoneNumberAttributeName.toLowerCase(); - } - - public String getGivenNameAttributeName() { - return givenNameAttributeName; + this.phoneNumberAttributeName = phoneNumberAttributeName; } public void setGivenNameAttributeName(String givenNameAttributeName) { - this.givenNameAttributeName = givenNameAttributeName == null ? null : givenNameAttributeName.toLowerCase(); - } - - public String getFamilyNameAttributeName() { - return familyNameAttributeName; + this.givenNameAttributeName = givenNameAttributeName; } public void setFamilyNameAttributeName(String familyNameAttributeName) { - this.familyNameAttributeName = familyNameAttributeName == null ? null : familyNameAttributeName.toLowerCase(); + this.familyNameAttributeName = familyNameAttributeName; } @Override @@ -210,17 +178,17 @@ public String getEmailAddress() { @Override public String getGivenName() { - return getFirst(givenNameAttributeName); + return getFirst(givenNameAttributeName,false); } @Override public String getFamilyName() { - return getFirst(familyNameAttributeName); + return getFirst(familyNameAttributeName,false); } @Override public String getPhoneNumber() { - return getFirst(phoneNumberAttributeName); + return getFirst(phoneNumberAttributeName,false); } @Override @@ -228,14 +196,11 @@ public String getExternalId() { return getDn(); } - protected String getFirst(String attributeName) { - if (!StringUtils.hasText(attributeName)) { - return null; - } - String[] attrValues = this.attributes.get(attributeName); - if(attrValues == null || attrValues.length==0) { - return null; + protected String getFirst(String attributeName, boolean caseSensitive) { + String[] result = getAttribute(attributeName, caseSensitive); + if (result!=null && result.length>0) { + return result[0]; } - return attrValues[0]; + return null; } } diff --git a/docs/UAA-LDAP.md b/docs/UAA-LDAP.md index 3b89f57f217..29cc3fd270c 100644 --- a/docs/UAA-LDAP.md +++ b/docs/UAA-LDAP.md @@ -702,8 +702,9 @@ In the above example, the user `marissa`'s UAA email always become `generated-m ldap: attributeMappings: first_name: givenname - given_name: sn + family_name: sn phone_number: telephonenumber + email: mail user.attribute.employeeCostCenter: costCenter user.attribute.terribleBosses: manager diff --git a/uaa/src/main/resources/ldap-integration.xml b/uaa/src/main/resources/ldap-integration.xml index dc22821b2ea..1875a8046ef 100644 --- a/uaa/src/main/resources/ldap-integration.xml +++ b/uaa/src/main/resources/ldap-integration.xml @@ -48,9 +48,9 @@ - - - + + + diff --git a/uaa/src/main/resources/ldap_init.ldif b/uaa/src/main/resources/ldap_init.ldif index 6cbedceeeb9..da90ec9ec15 100644 --- a/uaa/src/main/resources/ldap_init.ldif +++ b/uaa/src/main/resources/ldap_init.ldif @@ -138,6 +138,10 @@ sn: Marissa9 costCenter: Denver,CO uaaManager: John the Sloth uaaManager: Kari the Ant Eater +givenname: Marissa +initials: M +telephonenumber: 8885550986 +mail: marissa9-custom@test.com ############################################################################### diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java index 3f106159d55..0d355f7b56f 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java @@ -230,10 +230,22 @@ public void testCustomUserAttributes() throws Exception { final String COST_CENTERS = COST_CENTER+"s"; final String JOHN_THE_SLOTH = "John the Sloth"; final String KARI_THE_ANT_EATER = "Kari the Ant Eater"; + final String FIRST_NAME = "first_name"; + final String FAMILY_NAME = "family_name"; + final String PHONE_NUMBER = "phone_number"; + final String EMAIL = "email"; + createMockEnvironment(); mockEnvironment.setProperty("ldap."+ ATTRIBUTE_MAPPINGS+".user.attribute."+MANAGERS, MANAGER); mockEnvironment.setProperty("ldap."+ATTRIBUTE_MAPPINGS+".user.attribute."+COST_CENTERS, COST_CENTER); + + //test to remap the user/person properties + mockEnvironment.setProperty("ldap."+ATTRIBUTE_MAPPINGS+".user.attribute."+FIRST_NAME, "sn"); + mockEnvironment.setProperty("ldap."+ATTRIBUTE_MAPPINGS+".user.attribute."+PHONE_NUMBER, "givenname"); + mockEnvironment.setProperty("ldap."+ATTRIBUTE_MAPPINGS+".user.attribute."+FAMILY_NAME, "telephonenumber"); + mockEnvironment.setProperty("ldap."+ATTRIBUTE_MAPPINGS+".user.attribute."+EMAIL, "mail"); + setUp(); String username = "marissa9"; @@ -242,13 +254,18 @@ public void testCustomUserAttributes() throws Exception { UaaAuthentication authentication = (UaaAuthentication) ((SecurityContext) result.getRequest().getSession().getAttribute(SPRING_SECURITY_CONTEXT_KEY)).getAuthentication(); - assertEquals("Expected two user attributes", 2, authentication.getUserAttributes().size()); + assertEquals("Expected two user attributes", 3, authentication.getUserAttributes().size()); assertNotNull("Expected cost center attribute", authentication.getUserAttributes().get(COST_CENTERS)); assertEquals(DENVER_CO, authentication.getUserAttributes().getFirst(COST_CENTERS)); assertNotNull("Expected manager attribute", authentication.getUserAttributes().get(MANAGERS)); assertEquals("Expected 2 manager attribute values", 2, authentication.getUserAttributes().get(MANAGERS).size()); assertThat(authentication.getUserAttributes().get(MANAGERS), containsInAnyOrder(JOHN_THE_SLOTH, KARI_THE_ANT_EATER)); + + assertEquals("8885550986", getFamilyName(username)); + assertEquals("Marissa", getPhoneNumber(username)); + assertEquals("Marissa9", getGivenName(username)); + assertThat(authentication.getUserAttributes().get(EMAIL), containsInAnyOrder("marissa9@test.com", "marissa9-custom@test.com")); } @Test @@ -755,11 +772,6 @@ public void testExtendedAttributes() throws Exception { assertEquals("Marissa", getGivenName(username)); assertEquals("Lastnamerton", getFamilyName(username)); assertEquals("8885550986", getPhoneNumber(username)); -// assertThat(result.getResponse().getContentAsString(), containsString("\"givenname\":\"Marissa\"")); -// assertThat(result.getResponse().getContentAsString(), containsString("\"familyname\":\"Marissa3\"")); - //assertThat(result.getResponse().getContentAsString(), containsString("\"phonenumber\":\"8885550986\"")); - - } From bb0688c1af4d65ec92f83cb9fcebf03fd28e2cc7 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Tue, 27 Oct 2015 08:21:59 -0600 Subject: [PATCH 099/103] Align all properties to have the same format as SAML properties https://www.pivotaltracker.com/story/show/106326628 [#106326628] --- .../manager/LdapLoginAuthenticationManager.java | 17 ++++++----------- .../ldap/LdapIdentityProviderDefinition.java | 12 +++++++++--- docs/UAA-LDAP.md | 7 ++++--- uaa/src/main/resources/ldap-integration.xml | 6 +++--- uaa/src/main/resources/uaa.yml | 5 +++-- .../uaa/mock/ldap/LdapMockMvcTests.java | 11 +++++------ 6 files changed, 30 insertions(+), 28 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java index 2398b4ca530..f285e305dc7 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java @@ -27,8 +27,6 @@ import org.springframework.util.MultiValueMap; import java.util.Arrays; -import java.util.Collections; -import java.util.List; import java.util.Map; public class LdapLoginAuthenticationManager extends ExternalLoginAuthenticationManager { @@ -41,9 +39,6 @@ public void setProvisioning(IdentityProviderProvisioning provisioning) { this.provisioning = provisioning; } - public static final List ALREADY_MAPPED_ATTRS = - Collections.unmodifiableList(Arrays.asList("first_name", "family_name", "phone_number")); - @Override protected MultiValueMap getUserAttributes(UserDetails request) { MultiValueMap result = super.getUserAttributes(request); @@ -51,14 +46,14 @@ protected MultiValueMap getUserAttributes(UserDetails request) { IdentityProvider provider = provisioning.retrieveByOrigin(getOrigin(), IdentityZoneHolder.get().getId()); if (request instanceof ExtendedLdapUserDetails) { ExtendedLdapUserDetails ldapDetails = ((ExtendedLdapUserDetails) request); - for (Map.Entry entry : provider.getConfigValue(LdapIdentityProviderDefinition.class).getAttributeMappings().entrySet()) { + LdapIdentityProviderDefinition ldapIdentityProviderDefinition = provider.getConfigValue(LdapIdentityProviderDefinition.class); + Map providerMappings = ldapIdentityProviderDefinition.getAttributeMappings(); + for (Map.Entry entry : providerMappings.entrySet()) { if (entry.getKey().startsWith(USER_ATTRIBUTE_PREFIX) && entry.getValue() != null) { String key = entry.getKey().substring(USER_ATTRIBUTE_PREFIX.length()); - if (! ALREADY_MAPPED_ATTRS.contains(key)) { - String[] values = ldapDetails.getAttribute((String) entry.getValue(), false); - if (values != null && values.length > 0) { - result.put(key, Arrays.asList(values)); - } + String[] values = ldapDetails.getAttribute((String) entry.getValue(), false); + if (values != null && values.length > 0) { + result.put(key, Arrays.asList(values)); } } } diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinition.java b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinition.java index 2ac0e3f7852..6e368d4e071 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinition.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/LdapIdentityProviderDefinition.java @@ -192,8 +192,10 @@ public static LdapIdentityProviderDefinition searchAndBindMapGroupToScopes( return definition; } + /** + * Load a LDAP definition from the Yaml config (IdentityProviderBootstrap) + */ public static LdapIdentityProviderDefinition fromConfig(Map ldapConfig) { - LdapIdentityProviderDefinition definition = new LdapIdentityProviderDefinition(); if (ldapConfig==null || ldapConfig.isEmpty()) { return definition; @@ -261,9 +263,13 @@ public static LdapIdentityProviderDefinition fromConfig(Map ldap definition.setAutoAddGroups((Boolean) ldapConfig.get(LDAP_GROUPS_AUTO_ADD)); definition.setGroupRoleAttribute((String) ldapConfig.get(LDAP_GROUPS_GROUP_ROLE_ATTRIBUTE)); } - final String LDAP_ATTR_MAP_PREFIX = "ldap."+ATTRIBUTE_MAPPINGS+"."; + + //if flat attributes are set in the properties + final String LDAP_ATTR_MAP_PREFIX = LDAP_ATTRIBUTE_MAPPINGS+"."; for (Map.Entry entry : ldapConfig.entrySet()) { - if (!LDAP_PROPERTY_NAMES.contains(entry.getKey()) && entry.getKey().startsWith(LDAP_ATTR_MAP_PREFIX+USER_ATTRIBUTE_PREFIX)) { + if (!LDAP_PROPERTY_NAMES.contains(entry.getKey()) && + entry.getKey().startsWith(LDAP_ATTR_MAP_PREFIX) && + entry.getValue() instanceof String) { definition.addAttributeMapping(entry.getKey().substring(LDAP_ATTR_MAP_PREFIX.length()), entry.getValue()); } } diff --git a/docs/UAA-LDAP.md b/docs/UAA-LDAP.md index 29cc3fd270c..027728178e8 100644 --- a/docs/UAA-LDAP.md +++ b/docs/UAA-LDAP.md @@ -704,7 +704,8 @@ ldap: first_name: givenname family_name: sn phone_number: telephonenumber - email: mail - user.attribute.employeeCostCenter: costCenter - user.attribute.terribleBosses: manager + user: + attribute: + employeeCostCenter: costCenter + terribleBosses: manager diff --git a/uaa/src/main/resources/ldap-integration.xml b/uaa/src/main/resources/ldap-integration.xml index 1875a8046ef..587599f11d9 100644 --- a/uaa/src/main/resources/ldap-integration.xml +++ b/uaa/src/main/resources/ldap-integration.xml @@ -48,9 +48,9 @@ - - - + + + diff --git a/uaa/src/main/resources/uaa.yml b/uaa/src/main/resources/uaa.yml index f9af3cb5bd2..2721627829a 100755 --- a/uaa/src/main/resources/uaa.yml +++ b/uaa/src/main/resources/uaa.yml @@ -95,8 +95,9 @@ # emailDomain: # - example.com # attributeMappings: -# given_name: firstName -# family_name: surname +# given_name: givenname +# family_name: sn +# phone_number: telephonenumber # user.attribute.employeeCostCenter: costCenter # user.attribute.terribleBosses: uaaManager diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java index 0d355f7b56f..7b91e7a6b15 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java @@ -80,6 +80,7 @@ import java.util.Set; import static org.cloudfoundry.identity.uaa.ExternalIdentityProviderDefinition.ATTRIBUTE_MAPPINGS; +import static org.cloudfoundry.identity.uaa.ldap.LdapIdentityProviderDefinition.LDAP_ATTRIBUTE_MAPPINGS; import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.CookieCsrfPostProcessor.cookieCsrf; import static org.hamcrest.Matchers.arrayContainingInAnyOrder; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -241,10 +242,9 @@ public void testCustomUserAttributes() throws Exception { mockEnvironment.setProperty("ldap."+ATTRIBUTE_MAPPINGS+".user.attribute."+COST_CENTERS, COST_CENTER); //test to remap the user/person properties - mockEnvironment.setProperty("ldap."+ATTRIBUTE_MAPPINGS+".user.attribute."+FIRST_NAME, "sn"); - mockEnvironment.setProperty("ldap."+ATTRIBUTE_MAPPINGS+".user.attribute."+PHONE_NUMBER, "givenname"); - mockEnvironment.setProperty("ldap."+ATTRIBUTE_MAPPINGS+".user.attribute."+FAMILY_NAME, "telephonenumber"); - mockEnvironment.setProperty("ldap."+ATTRIBUTE_MAPPINGS+".user.attribute."+EMAIL, "mail"); + mockEnvironment.setProperty(LDAP_ATTRIBUTE_MAPPINGS+"."+FIRST_NAME, "sn"); + mockEnvironment.setProperty(LDAP_ATTRIBUTE_MAPPINGS+"."+PHONE_NUMBER, "givenname"); + mockEnvironment.setProperty(LDAP_ATTRIBUTE_MAPPINGS+"."+FAMILY_NAME, "telephonenumber"); setUp(); @@ -254,7 +254,7 @@ public void testCustomUserAttributes() throws Exception { UaaAuthentication authentication = (UaaAuthentication) ((SecurityContext) result.getRequest().getSession().getAttribute(SPRING_SECURITY_CONTEXT_KEY)).getAuthentication(); - assertEquals("Expected two user attributes", 3, authentication.getUserAttributes().size()); + assertEquals("Expected two user attributes", 2, authentication.getUserAttributes().size()); assertNotNull("Expected cost center attribute", authentication.getUserAttributes().get(COST_CENTERS)); assertEquals(DENVER_CO, authentication.getUserAttributes().getFirst(COST_CENTERS)); @@ -265,7 +265,6 @@ public void testCustomUserAttributes() throws Exception { assertEquals("8885550986", getFamilyName(username)); assertEquals("Marissa", getPhoneNumber(username)); assertEquals("Marissa9", getGivenName(username)); - assertThat(authentication.getUserAttributes().get(EMAIL), containsInAnyOrder("marissa9@test.com", "marissa9-custom@test.com")); } @Test From 44964d0f2b12ba0268e5c1ac1805421bbbfdc1a5 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Tue, 27 Oct 2015 12:33:36 -0600 Subject: [PATCH 100/103] Add in plumbing for LDAP groups in id_token https://www.pivotaltracker.com/story/show/105497272 [#105497272] Allow 'groups as scopes' to also retrive the thirdmarrisa group so we don't have exceptions in the test cases auto add groups should be a zone property --- .../LdapLoginAuthenticationManager.java | 17 ++++--- .../LdapLoginAuthenticationManagerTests.java | 6 ++- uaa/src/main/resources/ldap_init.ldif | 8 ++++ .../main/webapp/WEB-INF/spring-servlet.xml | 5 -- .../uaa/mock/ldap/LdapMockMvcTests.java | 48 +++++++++++++++++-- 5 files changed, 67 insertions(+), 17 deletions(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java index f285e305dc7..3879e9ae787 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java @@ -32,7 +32,6 @@ public class LdapLoginAuthenticationManager extends ExternalLoginAuthenticationManager { public static final String USER_ATTRIBUTE_PREFIX = "user.attribute."; - private boolean autoAddAuthorities = false; private IdentityProviderProvisioning provisioning; public void setProvisioning(IdentityProviderProvisioning provisioning) { @@ -78,12 +77,16 @@ protected UaaUser userAuthenticated(Authentication request, UaaUser user) { return getUserDatabase().retrieveUserById(user.getId()); } - public boolean isAutoAddAuthorities() { - return autoAddAuthorities; - } - - public void setAutoAddAuthorities(boolean autoAddAuthorities) { - this.autoAddAuthorities = autoAddAuthorities; + protected boolean isAutoAddAuthorities() { + Boolean result = true; + if (provisioning!=null) { + IdentityProvider provider = provisioning.retrieveByOrigin(getOrigin(), IdentityZoneHolder.get().getId()); + LdapIdentityProviderDefinition ldapIdentityProviderDefinition = provider.getConfigValue(LdapIdentityProviderDefinition.class); + if (ldapIdentityProviderDefinition!=null) { + result = ldapIdentityProviderDefinition.isAutoAddGroups(); + } + } + return result!=null ? result.booleanValue() : true; } private boolean haveUserAttributesChanged(UaaUser existingUser, UaaUser user) { diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java index ce1ddaa4ada..3672d78cbb3 100644 --- a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java @@ -169,13 +169,15 @@ public void testGetUserWithNonLdapInfo() throws Exception { @Test public void testUserAuthenticated() throws Exception { + + UaaUser user = getUaaUser(); - am.setAutoAddAuthorities(true); + definition.setAutoAddGroups(true); UaaUser result = am.userAuthenticated(auth, user); assertSame(dbUser, result); verify(publisher, times(1)).publishEvent(Matchers.anyObject()); - am.setAutoAddAuthorities(false); + definition.setAutoAddGroups(false); result = am.userAuthenticated(auth, user); assertSame(dbUser, result); verify(publisher, times(2)).publishEvent(Matchers.anyObject()); diff --git a/uaa/src/main/resources/ldap_init.ldif b/uaa/src/main/resources/ldap_init.ldif index da90ec9ec15..15c289c942c 100644 --- a/uaa/src/main/resources/ldap_init.ldif +++ b/uaa/src/main/resources/ldap_init.ldif @@ -173,6 +173,14 @@ cn: uaa.admin member: cn=admin,ou=Users,dc=test,dc=com member: cn=marissa3,ou=Users,dc=test,dc=com +dn: cn=thirdmarissa,ou=scopes,dc=test,dc=com +changetype: add +objectClass: groupOfNames +objectClass: top +cn: thirdmarissa +description: thirdmarissa +member: cn=marissa3,ou=Users,dc=test,dc=com + dn: cn=developers,ou=scopes,dc=test,dc=com changetype: add objectClass: groupOfNames diff --git a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml index cf5637ce585..c8cff6e777c 100755 --- a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml +++ b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml @@ -298,14 +298,9 @@ - - - - - diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java index 7b91e7a6b15..c7355ef4abd 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java @@ -89,6 +89,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; @@ -220,9 +221,48 @@ private void deleteLdapUsers() { jdbcTemplate.update("delete from users where origin='" + Origin.LDAP + "'"); } + @Test + public void test_whitelisted_external_groups() throws Exception { + Assume.assumeThat("ldap-groups-map-to-scopes.xml, ldap-groups-as-scopes.xml", StringContains.containsString(ldapGroup)); + setUp(); + IdentityProviderProvisioning idpProvisioning = mainContext.getBean(IdentityProviderProvisioning.class); + IdentityProvider idp = idpProvisioning.retrieveByOrigin(Origin.LDAP, IdentityZone.getUaa().getId()); + LdapIdentityProviderDefinition def = idp.getConfigValue(LdapIdentityProviderDefinition.class); + def.addWhiteListedGroup("admins"); + def.addWhiteListedGroup("thirdmarissa"); + idp.setConfig(JsonUtils.writeValueAsString(def)); + idpProvisioning.update(idp); + AuthenticationManager manager = mainContext.getBean(DynamicZoneAwareAuthenticationManager.class); + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("marissa3", "ldap3"); + Authentication auth = manager.authenticate(token); + assertNotNull(auth); + assertTrue(auth instanceof UaaAuthentication); + UaaAuthentication uaaAuth = (UaaAuthentication) auth; + Set externalGroups = uaaAuth.getExternalGroups(); + assertNotNull(externalGroups); + assertEquals(2, externalGroups.size()); + assertThat(externalGroups, containsInAnyOrder("admins", "thirdmarissa")); + } + + @Test + public void test_external_groups_with_default_whitelist() throws Exception { + Assume.assumeThat("ldap-groups-map-to-scopes.xml, ldap-groups-as-scopes.xml", StringContains.containsString(ldapGroup)); + setUp(); + AuthenticationManager manager = mainContext.getBean(DynamicZoneAwareAuthenticationManager.class); + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("marissa3", "ldap3"); + Authentication auth = manager.authenticate(token); + assertNotNull(auth); + assertTrue(auth instanceof UaaAuthentication); + UaaAuthentication uaaAuth = (UaaAuthentication) auth; + Set externalGroups = uaaAuth.getExternalGroups(); + assertNotNull(externalGroups); + assertEquals(0, externalGroups.size()); + } + + @Test public void testCustomUserAttributes() throws Exception { - Assume.assumeThat("ldap-groups-null.xml", StringContains.containsString(ldapGroup)); + Assume.assumeThat("ldap-groups-map-to-scopes.xml, ldap-groups-as-scopes.xml", StringContains.containsString(ldapGroup)); final String MANAGER = "uaaManager"; final String MANAGERS = "managers"; @@ -957,7 +997,8 @@ public void testLdapScopes() throws Exception { assertNotNull(auth); String[] list = new String[]{ "uaa.admin", - "cloud_controller.read" + "cloud_controller.read", + "thirdmarissa" }; assertThat(list, arrayContainingInAnyOrder(getAuthorities(auth.getAuthorities()))); } @@ -984,7 +1025,8 @@ public void testLdapScopesFromChainedAuth() throws Exception { "oauth.approvals", "uaa.user", "cloud_controller.read", - "user_attributes" + "user_attributes", + "thirdmarissa" }; assertThat(list, arrayContainingInAnyOrder(getAuthorities(auth.getAuthorities()))); } From 1d07df6da4307c95123a786108cb82188e7836b6 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Tue, 27 Oct 2015 13:19:08 -0600 Subject: [PATCH 101/103] Default extraction of LDAP groupnames for externalGroups into id_token https://www.pivotaltracker.com/story/show/105497272 [#105497272] --- .../ExternalLoginAuthenticationManager.java | 11 +++++- .../LdapLoginAuthenticationManager.java | 37 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java index 871fa284030..e5721ab18d2 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/ExternalLoginAuthenticationManager.java @@ -46,6 +46,9 @@ import org.springframework.util.MultiValueMap; import java.util.Date; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; public class ExternalLoginAuthenticationManager implements AuthenticationManager, ApplicationEventPublisherAware, BeanNameAware { @@ -121,7 +124,9 @@ public Authentication authenticate(Authentication request) throws Authentication } UaaAuthentication success = new UaaAuthentication(new UaaPrincipal(user), user.getAuthorities(), uaaAuthenticationDetails); if (request.getPrincipal() instanceof UserDetails) { - success.setUserAttributes(getUserAttributes((UserDetails) request.getPrincipal())); + UserDetails userDetails = (UserDetails) request.getPrincipal(); + success.setUserAttributes(getUserAttributes(userDetails)); + success.setExternalGroups(new HashSet<>(getExternalUserAuthorities(userDetails))); } publish(new UserAuthenticationSuccessEvent(user, success)); return success; @@ -131,6 +136,10 @@ protected MultiValueMap getUserAttributes(UserDetails request) { return new LinkedMultiValueMap<>(); } + protected List getExternalUserAuthorities(UserDetails request) { + return new LinkedList<>(); + } + protected void publish(ApplicationEvent event) { if (eventPublisher != null) { eventPublisher.publishEvent(event); diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java index 3879e9ae787..54490b21605 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java @@ -18,16 +18,25 @@ import org.apache.commons.lang.StringUtils; import org.cloudfoundry.identity.uaa.ldap.ExtendedLdapUserDetails; import org.cloudfoundry.identity.uaa.ldap.LdapIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.ldap.extension.LdapAuthority; import org.cloudfoundry.identity.uaa.user.UaaUser; import org.cloudfoundry.identity.uaa.zone.IdentityProvider; import org.cloudfoundry.identity.uaa.zone.IdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.util.MultiValueMap; import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; import java.util.Map; +import java.util.Set; + +import static java.util.Collections.EMPTY_LIST; public class LdapLoginAuthenticationManager extends ExternalLoginAuthenticationManager { @@ -61,6 +70,34 @@ protected MultiValueMap getUserAttributes(UserDetails request) { return result; } + @Override + protected List getExternalUserAuthorities(UserDetails request) { + List result = super.getExternalUserAuthorities(request); + if (provisioning!=null) { + IdentityProvider provider = provisioning.retrieveByOrigin(getOrigin(), IdentityZoneHolder.get().getId()); + LdapIdentityProviderDefinition ldapIdentityProviderDefinition = provider.getConfigValue(LdapIdentityProviderDefinition.class); + List externalWhiteList = ldapIdentityProviderDefinition.getExternalGroupsWhitelist(); + result = new LinkedList<>(getAuthoritesAsNames(request.getAuthorities())); + result.retainAll(externalWhiteList); + } + return result; + } + + protected Set getAuthoritesAsNames(Collection authorities) { + Set result = new HashSet<>(); + authorities = new LinkedList(authorities!=null?authorities: EMPTY_LIST); + for (GrantedAuthority a : authorities) { + if (a instanceof LdapAuthority) { + LdapAuthority la = (LdapAuthority)a; + String[] groupNames = la.getAttributeValues("cn"); + if (groupNames!=null) { + result.addAll(Arrays.asList(groupNames)); + } + } + } + return result; + } + @Override protected UaaUser userAuthenticated(Authentication request, UaaUser user) { boolean userModified = false; From 99007b46668f8e164cacaed460a8a7f0b8e81f9f Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Tue, 27 Oct 2015 16:14:34 -0600 Subject: [PATCH 102/103] Add check for "roles" in id_token in an integration test --- .../uaa/integration/LdapIntegationTests.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/LdapIntegationTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/LdapIntegationTests.java index 09f19d2c2f1..244ae3f7986 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/LdapIntegationTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/LdapIntegationTests.java @@ -114,6 +114,9 @@ public void test_LDAP_Custom_User_Attributes_In_ID_Token() throws Exception { true); ldapIdentityProviderDefinition.addAttributeMapping(USER_ATTRIBUTE_PREFIX+COST_CENTERS, COST_CENTER); ldapIdentityProviderDefinition.addAttributeMapping(USER_ATTRIBUTE_PREFIX+MANAGERS, MANAGER); + ldapIdentityProviderDefinition.addWhiteListedGroup("marissaniner"); + ldapIdentityProviderDefinition.addWhiteListedGroup("marissaniner2"); + IdentityProvider provider = new IdentityProvider(); provider.setIdentityZoneId(zoneId); @@ -130,7 +133,7 @@ public void test_LDAP_Custom_User_Attributes_In_ID_Token() throws Exception { List idps = Arrays.asList(provider.getOriginKey()); String adminClientInZone = new RandomValueStringGenerator().generate(); - BaseClientDetails clientDetails = new BaseClientDetails(adminClientInZone, null, "openid,user_attributes", "password,authorization_code,client_credentials", "uaa.admin,scim.read,scim.write,uaa.resource", zoneUrl); + BaseClientDetails clientDetails = new BaseClientDetails(adminClientInZone, null, "openid,user_attributes,roles", "password,authorization_code,client_credentials", "uaa.admin,scim.read,scim.write,uaa.resource", zoneUrl); clientDetails.setClientSecret("secret"); clientDetails.addAdditionalInformation(ClientConstants.AUTO_APPROVE, true); clientDetails.addAdditionalInformation(ClientConstants.ALLOWED_PROVIDERS, idps); @@ -145,7 +148,7 @@ public void test_LDAP_Custom_User_Attributes_In_ID_Token() throws Exception { clientDetails.getClientSecret(), "marissa9", "ldap9", - "openid user_attributes") + "openid user_attributes roles") .get("id_token"); assertNotNull(idToken); @@ -158,6 +161,11 @@ public void test_LDAP_Custom_User_Attributes_In_ID_Token() throws Exception { assertThat(userAttributes.get(COST_CENTERS), containsInAnyOrder(DENVER_CO)); assertThat(userAttributes.get(MANAGERS), containsInAnyOrder(JOHN_THE_SLOTH, KARI_THE_ANT_EATER)); + + assertNotNull(claims.get(Claims.ROLES)); + List roles = (List) claims.get(Claims.ROLES); + assertThat(roles, containsInAnyOrder("marissaniner", "marissaniner2")); + //no user_attribute scope provided idToken = (String) IntegrationTestUtils.getPasswordToken(zoneUrl, @@ -173,6 +181,7 @@ public void test_LDAP_Custom_User_Attributes_In_ID_Token() throws Exception { idTokenClaims = JwtHelper.decode(idToken); claims = JsonUtils.readValue(idTokenClaims.getClaims(), new TypeReference>() {}); assertNull(claims.get(Claims.USER_ATTRIBUTES)); + assertNull(claims.get(Claims.ROLES)); } protected boolean doesSupportZoneDNS_and_isLdapEnabled() { From be0e444e03961cbba05bf565c207d3e50cba3484 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Wed, 28 Oct 2015 14:12:48 -0600 Subject: [PATCH 103/103] Bump release version to 2.7.1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 88927de9d52..52b72022a51 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=2.7.1-SNAPSHOT +version=2.7.1