/******************************************************************************* * Cloud Foundry * Copyright (c) [2009-2016] 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.core.type.TypeReference; import org.apache.http.NameValuePair; import org.apache.http.client.utils.URLEncodedUtils; import org.cloudfoundry.identity.uaa.account.UserAccountStatus; import org.cloudfoundry.identity.uaa.approval.Approval; import org.cloudfoundry.identity.uaa.approval.ApprovalStore; import org.cloudfoundry.identity.uaa.codestore.ExpiringCode; import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; import org.cloudfoundry.identity.uaa.constants.OriginKeys; 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.provider.IdentityProvider; import org.cloudfoundry.identity.uaa.provider.JdbcIdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.provider.UaaIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.resources.SearchResults; 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.util.JsonUtils; import org.cloudfoundry.identity.uaa.util.SetServerNameRequestPostProcessor; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.cloudfoundry.identity.uaa.zone.IdentityZoneSwitchingFilter; import org.hamcrest.MatcherAssert; import org.json.JSONObject; import org.junit.Before; import org.junit.Test; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.jdbc.core.JdbcTemplate; 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 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.codestore.ExpiringCodeType.REGISTRATION; import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.CookieCsrfPostProcessor.cookieCsrf; import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.utils; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.startsWith; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.core.Is.is; 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.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.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated; 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.patch; 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.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.util.StringUtils.hasText; public class ScimUserEndpointsMockMvcTests extends InjectedMockContextTest { public static final String HTTP_REDIRECT_EXAMPLE_COM = "http://redirect.example.com"; public static final String USER_PASSWORD = "pas5Word"; private String scimReadWriteToken; private String scimCreateToken; private String uaaAdminToken; private String uaaAdminTokenInOtherZone; private RandomValueStringGenerator generator = new RandomValueStringGenerator(); private MockMvcUtils mockMvcUtils = utils(); private ClientDetails clientDetails; private ScimUserProvisioning usersRepository; private ExpiringCodeStore codeStore; @Before public void setUp() throws Exception { String adminToken = testClient.getClientCredentialsOAuthAccessToken("admin", "adminsecret", "clients.read clients.write clients.secret clients.admin uaa.admin"); String clientId = generator.generate().toLowerCase(); String clientSecret = generator.generate().toLowerCase(); String authorities = "scim.read,scim.write,password.write,oauth.approvals,scim.create,uaa.admin"; clientDetails = utils().createClient(this.getMockMvc(), adminToken, clientId, clientSecret, Collections.singleton("oauth"), Arrays.asList("foo","bar"), Collections.singletonList("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); uaaAdminToken = testClient.getClientCredentialsOAuthAccessToken(clientId, clientSecret, "uaa.admin"); } private ScimUser createUser(String token) throws Exception { return createUser(token, null); } private ScimUser createUser(String token, String subdomain) throws Exception { return createUser(getScimUser(), token, subdomain); } private ScimUser createUser(ScimUser user, String token, String subdomain) throws Exception { return createUser(user,token,subdomain, null); } private ScimUser createUser(ScimUser user, String token, String subdomain, String switchZone) throws Exception { String password = hasText(user.getPassword()) ? user.getPassword() : "pas5word"; user.setPassword(password); MvcResult result = createUserAndReturnResult(user, token, subdomain, switchZone) .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(); user = JsonUtils.readValue(result.getResponse().getContentAsString(), ScimUser.class); user.setPassword(password); return user; } 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"; ScimUser user = new ScimUser(); user.setUserName(email); user.setName(new ScimUser.Name("Joe", "User")); user.addEmail(email); return user; } @Test public void testCanCreateUserWithExclamationMark() throws Exception { String email = "joe!!@"+generator.generate().toLowerCase()+".com"; ScimUser user = getScimUser(); user.getEmails().clear(); user.setUserName(email); user.setPrimaryEmail(email); 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()) .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 test_Create_User_More_Than_One_Email() throws Exception { ScimUser scimUser = getScimUser(); String secondEmail = "joe@"+generator.generate().toLowerCase()+".com"; scimUser.addEmail(secondEmail); createUserAndReturnResult(scimUser, scimReadWriteToken, null, null) .andExpect(status().isBadRequest()); } @Test public void testCreateUser() throws Exception { createUser(scimReadWriteToken); } @Test public void testCreateUserWithScimCreateToken() throws Exception { createUser(scimCreateToken); } @Test public void createUserWithUaaAdminToken() throws Exception { createUser(uaaAdminToken); } @Test public void createUserInOtherZoneWithUaaAdminToken() throws Exception { IdentityZone otherIdentityZone = getIdentityZone(); createUser(getScimUser(), uaaAdminToken, IdentityZone.getUaa().getSubdomain(), otherIdentityZone.getId()); } @Test public void default_password_policy_does_not_allow_empty_passwords() throws Exception { IdentityZone otherIdentityZone = getIdentityZone(); ScimUser scimUser = getScimUser(); scimUser.setPassword(""); IdentityProvider<UaaIdentityProviderDefinition> uaa = getWebApplicationContext().getBean(JdbcIdentityProviderProvisioning.class).retrieveByOrigin( OriginKeys.UAA, otherIdentityZone.getId() ); ResultActions result = createUserAndReturnResult(scimUser, uaaAdminToken, IdentityZone.getUaa().getSubdomain(), otherIdentityZone.getId()); result.andExpect(status().isBadRequest()) .andExpect(jsonPath("$.message").value("Password must be at least 1 characters in length.")); } @Test public void createUserInOtherZoneWithUaaAdminTokenFromNonDefaultZone() throws Exception { IdentityZone identityZone = getIdentityZone(); String authorities = "uaa.admin"; clientDetails = utils().createClient(this.getMockMvc(), uaaAdminToken, "testClientId", "testClientSecret", null, null, Collections.singletonList("client_credentials"), authorities, null, identityZone); String uaaAdminTokenFromOtherZone = testClient.getClientCredentialsOAuthAccessToken("testClientId", "testClientSecret", "uaa.admin", identityZone.getSubdomain()); byte[] requestBody = JsonUtils.writeValueAsBytes(getScimUser()); MockHttpServletRequestBuilder post = post("/Users") .header("Authorization", "Bearer " + uaaAdminTokenFromOtherZone) .contentType(APPLICATION_JSON) .content(requestBody); post.with(new SetServerNameRequestPostProcessor(identityZone.getSubdomain() + ".localhost")); post.header(IdentityZoneSwitchingFilter.HEADER, IdentityZone.getUaa().getId()); getMockMvc().perform(post).andExpect(status().isForbidden()); } @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()))); assertThat(expiringCode.getIntent(), is(REGISTRATION.name())); Map<String, String> data = JsonUtils.readValue(expiringCode.getData(), new TypeReference<Map<String, String>>() {}); 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, Collections.singleton("oauth"), null, Arrays.asList(new String[]{"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(zoneResult.getIdentityZone()); MockHttpServletRequestBuilder get = MockMvcRequestBuilders.get("/Users/" + joel.getId() + "/verify-link") .header("Host", subdomain + ".localhost") .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())); IdentityZoneHolder.set(zoneResult.getIdentityZone()); ExpiringCode expiringCode = codeStore.retrieveCode(code); IdentityZoneHolder.clear(); assertThat(expiringCode.getExpiresAt().getTime(), is(greaterThan(System.currentTimeMillis()))); assertThat(expiringCode.getIntent(), is(REGISTRATION.name())); Map<String, String> data = JsonUtils.readValue(expiringCode.getData(), new TypeReference<Map<String, String>>() {}); 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 verification_link_in_non_default_zone_using_switch() throws Exception { String subdomain = generator.generate().toLowerCase(); MockMvcUtils.IdentityZoneCreationResult zoneResult = utils().createOtherIdentityZoneAndReturnResult(subdomain, getMockMvc(), getWebApplicationContext(), null); String zonedClientId = "admin"; String zonedClientSecret = "adminsecret"; String zonedScimCreateToken = utils().getClientCredentialsOAuthAccessToken(getMockMvc(), zonedClientId, zonedClientSecret, "uaa.admin", null); ScimUser joel = setUpScimUser(zoneResult.getIdentityZone()); MockHttpServletRequestBuilder get = MockMvcRequestBuilders.get("/Users/" + joel.getId() + "/verify-link") .header("Host", "localhost") .header("Authorization", "Bearer " + zonedScimCreateToken) .header(IdentityZoneSwitchingFilter.SUBDOMAIN_HEADER, subdomain) .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())); IdentityZoneHolder.set(zoneResult.getIdentityZone()); ExpiringCode expiringCode = codeStore.retrieveCode(code); IdentityZoneHolder.clear(); assertThat(expiringCode.getExpiresAt().getTime(), is(greaterThan(System.currentTimeMillis()))); assertThat(expiringCode.getIntent(), is(REGISTRATION.name())); Map<String, String> data = JsonUtils.readValue(expiringCode.getData(), new TypeReference<Map<String, String>>() {}); assertThat(data.get(InvitationConstants.USER_ID), is(notNullValue())); assertThat(data.get(CLIENT_ID), is("admin")); assertThat(data.get(REDIRECT_URI), is(HTTP_REDIRECT_EXAMPLE_COM)); } @Test public void create_user_without_username() throws Exception { ScimUser user = new ScimUser(null, null, "Joel", "D'sa"); user.setPassword("password"); user.setPrimaryEmail("test@test.org"); getMockMvc().perform(post("/Users") .header("Authorization", "Bearer " + scimReadWriteToken) .contentType(APPLICATION_JSON) .content(JsonUtils.writeValueAsString(user))) .andExpect(status().isBadRequest()) .andExpect(content() .string(JsonObjectMatcherUtils.matchesJsonObject( new JSONObject() .put("error_description", "A username must be provided.") .put("message", "A username must be provided.") .put("error", "invalid_scim_resource")))); } @Test public void create_user_without_email() throws Exception { ScimUser user = new ScimUser(null, "a_user", "Joel", "D'sa"); user.setPassword("password"); getMockMvc().perform(post("/Users") .header("Authorization", "Bearer " + scimReadWriteToken) .contentType(APPLICATION_JSON) .content(JsonUtils.writeValueAsString(user))) .andExpect(status().isBadRequest()) .andExpect(content() .string(JsonObjectMatcherUtils.matchesJsonObject( new JSONObject() .put("error_description", "Exactly one email must be provided.") .put("message", "Exactly one 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", "Exactly one email must be provided.") .put("message", "Exactly one email must be provided.") .put("error", "invalid_scim_resource")))); } @Test public void patch_user_to_inactive_then_login() throws Exception { ScimUser user = setUpScimUser(); user.setVerified(true); boolean active = true; user.setActive(active); getMockMvc().perform( patch("/Users/" + user.getId()) .header("Authorization", "Bearer " + scimReadWriteToken) .header("If-Match", "\"" + user.getVersion() + "\"") .contentType(APPLICATION_JSON) .content(JsonUtils.writeValueAsString(user))) .andExpect(status().isOk()) .andExpect(jsonPath("$.active", equalTo(active))); performAuthentication(user, true); active = false; user.setActive(active); getMockMvc().perform( patch("/Users/" + user.getId()) .header("Authorization", "Bearer " + scimReadWriteToken) .header("If-Match", "\"" + (user.getVersion()+1) + "\"") .contentType(APPLICATION_JSON) .content(JsonUtils.writeValueAsString(user))) .andExpect(status().isOk()) .andExpect(jsonPath("$.active", equalTo(active))); performAuthentication(user, false); } public void performAuthentication(ScimUser user, boolean success) throws Exception { getMockMvc().perform( post("/login.do") .accept("text/html") .with(cookieCsrf()) .param("username", user.getUserName()) .param("password", USER_PASSWORD)) .andDo(print()) .andExpect(success ? authenticated() : unauthenticated()); } @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 listUsers_in_anotherZone() throws Exception { String subdomain = generator.generate(); MockMvcUtils.IdentityZoneCreationResult result = utils().createOtherIdentityZoneAndReturnResult(subdomain, getMockMvc(), getWebApplicationContext(), null); String zoneAdminToken = result.getZoneAdminToken(); createUser(getScimUser(), zoneAdminToken, IdentityZone.getUaa().getSubdomain(), result.getIdentityZone().getId()); MockHttpServletRequestBuilder get = MockMvcRequestBuilders.get("/Users") .header("X-Identity-Zone-Subdomain", subdomain) .header("Authorization", "Bearer " + zoneAdminToken) .accept(APPLICATION_JSON); MvcResult mvcResult = getMockMvc().perform(get) .andExpect(status().isOk()) .andReturn(); SearchResults searchResults = JsonUtils.readValue(mvcResult.getResponse().getContentAsString(), SearchResults.class); MatcherAssert.assertThat(searchResults.getResources().size(), is(1)); } @Test public void testVerifyUser() throws Exception { verifyUser(scimReadWriteToken); } @Test public void testVerifyUserWithScimCreateToken() throws Exception { verifyUser(scimCreateToken); } @Test public void testCreateUserInZoneUsingAdminClient() throws Exception { String subdomain = generator.generate(); mockMvcUtils.createOtherIdentityZone(subdomain, getMockMvc(), getWebApplicationContext()); String zoneAdminToken = testClient.getClientCredentialsOAuthAccessToken("admin", "admin-secret", "scim.write", subdomain); createUser(zoneAdminToken, subdomain); } @Test public void testCreateUserInZoneUsingZoneAdminUser() throws Exception { String subdomain = generator.generate(); MockMvcUtils.IdentityZoneCreationResult result = utils().createOtherIdentityZoneAndReturnResult(subdomain, getMockMvc(), getWebApplicationContext(), null); String zoneAdminToken = result.getZoneAdminToken(); createUser(getScimUser(), zoneAdminToken, IdentityZone.getUaa().getSubdomain(), result.getIdentityZone().getId()); } @Test public void testUserSelfAccess_Get_and_Post() throws Exception { ScimUser user = getScimUser(); user.setPassword("secret"); user = createUser(user, scimReadWriteToken, IdentityZone.getUaa().getSubdomain()); String selfToken = testClient.getUserOAuthAccessToken("cf", "", user.getUserName(), "secret", ""); user.setName(new ScimUser.Name("Given1","Family1")); user = updateUser(selfToken, HttpStatus.OK.value(), user ); user = getAndReturnUser(HttpStatus.OK.value(), user, selfToken); } @Test public void testCreateUserInOtherZoneIsUnauthorized() throws Exception { String subdomain = generator.generate(); mockMvcUtils.createOtherIdentityZone(subdomain, getMockMvc(), getWebApplicationContext()); String otherSubdomain = generator.generate(); mockMvcUtils.createOtherIdentityZone(otherSubdomain, getMockMvc(), getWebApplicationContext()); String zoneAdminToken = testClient.getClientCredentialsOAuthAccessToken("admin", "admin-secret", "scim.write", subdomain); ScimUser user = getScimUser(); byte[] requestBody = JsonUtils.writeValueAsBytes(user); MockHttpServletRequestBuilder post = post("/Users") .with(new SetServerNameRequestPostProcessor(otherSubdomain + ".localhost")) .header("Authorization", "Bearer " + zoneAdminToken) .contentType(APPLICATION_JSON) .content(requestBody); getMockMvc().perform(post).andExpect(status().isUnauthorized()); } @Test public void testUnlockAccount() throws Exception { ScimUser userToLockout = createUser(uaaAdminToken); attemptFailedLogin(5, userToLockout.getUserName(), ""); UserAccountStatus alteredAccountStatus = new UserAccountStatus(); alteredAccountStatus.setLocked(false); updateAccountStatus(userToLockout, alteredAccountStatus) .andExpect(status().isOk()) .andExpect(content().json(JsonUtils.writeValueAsString(alteredAccountStatus))); attemptLogin(userToLockout) .andExpect(redirectedUrl("/")); } @Test public void testAccountStatusEmptyPatchDoesNotUnlock() throws Exception { ScimUser userToLockout = createUser(uaaAdminToken); attemptFailedLogin(5, userToLockout.getUserName(), ""); updateAccountStatus(userToLockout, new UserAccountStatus()) .andExpect(status().isOk()) .andExpect(content().json("{}")); attemptLogin(userToLockout) .andExpect(redirectedUrl("/login?error=account_locked")); } @Test public void testUpdateStatusCannotLock() throws Exception { ScimUser user = createUser(uaaAdminToken); UserAccountStatus alteredAccountStatus = new UserAccountStatus(); alteredAccountStatus.setLocked(true); updateAccountStatus(user, alteredAccountStatus) .andExpect(status().isBadRequest()); attemptLogin(user) .andExpect(redirectedUrl("/")); } @Test public void testUnlockAccountWhenNotLocked() throws Exception { ScimUser userToLockout = createUser(uaaAdminToken); UserAccountStatus alteredAccountStatus = new UserAccountStatus(); alteredAccountStatus.setLocked(false); updateAccountStatus(userToLockout, alteredAccountStatus) .andExpect(status().isOk()) .andExpect(content().json(JsonUtils.writeValueAsString(alteredAccountStatus))); attemptLogin(userToLockout) .andExpect(redirectedUrl("/")); } @Test public void testForcePasswordExpireAccountInvalid() throws Exception { ScimUser user = createUser(uaaAdminToken); UserAccountStatus alteredAccountStatus = new UserAccountStatus(); alteredAccountStatus.setPasswordChangeRequired(false); updateAccountStatus(user, alteredAccountStatus) .andExpect(status().isBadRequest()); assertFalse(usersRepository.checkPasswordChangeIndividuallyRequired(user.getId())); } @Test public void testForcePasswordExpireAccountExternalUser() throws Exception { ScimUser user = createUser(uaaAdminToken); user.setOrigin("NOT_UAA"); updateUser(uaaAdminToken, HttpStatus.OK.value(), user); UserAccountStatus alteredAccountStatus = new UserAccountStatus(); alteredAccountStatus.setPasswordChangeRequired(true); updateAccountStatus(user, alteredAccountStatus) .andExpect(status().isBadRequest()); assertFalse(usersRepository.checkPasswordChangeIndividuallyRequired(user.getId())); } @Test public void testForcePasswordChange() throws Exception { ScimUser user = createUser(uaaAdminToken); assertFalse(usersRepository.checkPasswordChangeIndividuallyRequired(user.getId())); UserAccountStatus alteredAccountStatus = new UserAccountStatus(); alteredAccountStatus.setPasswordChangeRequired(true); updateAccountStatus(user, alteredAccountStatus) .andExpect(status().isOk()) .andExpect(content().json(JsonUtils.writeValueAsString(alteredAccountStatus))); assertTrue(usersRepository.checkPasswordChangeIndividuallyRequired(user.getId())); } @Test public void testTryMultipleStatusUpdatesWithInvalidLock() throws Exception { ScimUser user = createUser(uaaAdminToken); UserAccountStatus alteredAccountStatus = new UserAccountStatus(); alteredAccountStatus.setPasswordChangeRequired(true); alteredAccountStatus.setLocked(true); updateAccountStatus(user, alteredAccountStatus) .andExpect(status().isBadRequest()); assertFalse(usersRepository.checkPasswordChangeIndividuallyRequired(user.getId())); attemptLogin(user) .andExpect(redirectedUrl("/")); } @Test public void testTryMultipleStatusUpdatesWithInvalidRemovalOfPasswordChange() throws Exception { ScimUser user = createUser(uaaAdminToken); attemptFailedLogin(5, user.getUserName(), ""); UserAccountStatus alteredAccountStatus = new UserAccountStatus(); alteredAccountStatus.setPasswordChangeRequired(false); alteredAccountStatus.setLocked(false); updateAccountStatus(user, alteredAccountStatus) .andExpect(status().isBadRequest()); assertFalse(usersRepository.checkPasswordChangeIndividuallyRequired(user.getId())); attemptLogin(user) .andExpect(redirectedUrl("/login?error=account_locked")); } private ResultActions updateAccountStatus(ScimUser user, UserAccountStatus alteredAccountStatus) throws Exception { String jsonStatus = JsonUtils.writeValueAsString(alteredAccountStatus); return getMockMvc() .perform( patch("/Users/" + user.getId() + "/status") .header("Authorization", "Bearer " + uaaAdminToken) .accept(APPLICATION_JSON) .contentType(APPLICATION_JSON) .content(jsonStatus) ); } private ResultActions attemptLogin(ScimUser user) throws Exception { return getMockMvc() .perform(post("/login.do") .with(cookieCsrf()) .param("username", user.getUserName()) .param("password", user.getPassword())); } private void attemptFailedLogin(int numberOfAttempts, String username, String subdomain) throws Exception { String requestDomain = subdomain.equals("") ? "localhost" : subdomain + ".localhost"; MockHttpServletRequestBuilder post = post("/login.do") .with(new SetServerNameRequestPostProcessor(requestDomain)) .with(cookieCsrf()) .param("username", username) .param("password", "wrong_password"); for (int i = 0; i < numberOfAttempts ; i++) { getMockMvc().perform(post) .andExpect(redirectedUrl("/login?error=login_failure")); } } private void verifyUser(String token) 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"); MockHttpServletRequestBuilder get = MockMvcRequestBuilders.get("/Users/" + joel.getId() + "/verify") .header("Authorization", "Bearer " + token) .accept(APPLICATION_JSON); getMockMvc().perform(get) .andExpect(status().isOk()) .andExpect(header().string("ETag", "\"0\"")) .andExpect(jsonPath("$.userName").value(email)) .andExpect(jsonPath("$.emails[0].value").value(email)) .andExpect(jsonPath("$.name.familyName").value("D'sa")) .andExpect(jsonPath("$.name.givenName").value("Joel")) .andExpect(jsonPath("$.verified").value(true)); } private void getUser(String token, int status) throws Exception { ScimUser joel = setUpScimUser(); getAndReturnUser(status, joel, token); } protected ScimUser getAndReturnUser(int status, ScimUser user, String token) throws Exception { MockHttpServletRequestBuilder get = MockMvcRequestBuilders.get("/Users/" + user.getId()) .header("Authorization", "Bearer " + token) .accept(APPLICATION_JSON); if (status== HttpStatus.OK.value()) { String json = getMockMvc().perform(get) .andExpect(status().is(status)) .andExpect(header().string("ETag", "\""+user.getVersion()+"\"")) .andExpect(jsonPath("$.userName").value(user.getPrimaryEmail())) .andExpect(jsonPath("$.emails[0].value").value(user.getPrimaryEmail())) .andExpect(jsonPath("$.name.familyName").value(user.getFamilyName())) .andExpect(jsonPath("$.name.givenName").value(user.getGivenName())) .andReturn().getResponse().getContentAsString(); return JsonUtils.readValue(json, ScimUser.class); } else { getMockMvc().perform(get) .andExpect(status().is(status)); return null; } } @Test public void testGetUser() throws Exception { getUser(scimReadWriteToken, HttpStatus.OK.value()); } @Test public void testGetUserWithInvalidAttributes() throws Exception { String nonexistentAttribute = "displayBlaBla"; MockHttpServletRequestBuilder get = get("/Users") .header("Authorization", "Bearer " + scimReadWriteToken) .contentType(MediaType.APPLICATION_JSON) .param("attributes", nonexistentAttribute) .accept(APPLICATION_JSON); MvcResult mvcResult = getMockMvc().perform(get) .andExpect(status().isOk()) .andReturn(); String body = mvcResult.getResponse().getContentAsString(); List<Map> attList = (List) JsonUtils.readValue(body, Map.class).get("resources"); for (Map<String, Object> attMap : attList) { assertNull(attMap.get(nonexistentAttribute)); } } @Test public void testGetUserWithScimCreateToken() throws Exception { getUser(scimCreateToken,HttpStatus.FORBIDDEN.value()); } @Test public void getUsersWithUaaAdminToken() throws Exception { setUpScimUser(); MockHttpServletRequestBuilder get = MockMvcRequestBuilders.get("/Users") .header("Authorization", "Bearer " + uaaAdminToken) .accept(APPLICATION_JSON); getMockMvc().perform(get) .andExpect(status().isOk()); } @Test public void getUserFromOtherZoneWithUaaAdminToken() throws Exception{ IdentityZone otherIdentityZone = getIdentityZone(); ScimUser user = setUpScimUser(otherIdentityZone); MockHttpServletRequestBuilder get = MockMvcRequestBuilders.get("/Users/", user.getId()) .header("Authorization", "Bearer " + uaaAdminToken) .accept(APPLICATION_JSON); getMockMvc().perform(get) .andExpect(status().isOk()); } protected ScimUser updateUser(String token, int status) throws Exception { ScimUserProvisioning usersRepository = getWebApplicationContext().getBean(ScimUserProvisioning.class); String email = "otheruser@"+generator.generate().toLowerCase()+".com"; ScimUser user = new ScimUser(null, email, "Other", "User"); user.addEmail(email); user = usersRepository.createUser(user, "pas5Word"); if (status==HttpStatus.BAD_REQUEST.value()) { user.setUserName(null); } else { String username2 = "ou"+generator.generate().toLowerCase(); user.setUserName(username2); } user.setName(new ScimUser.Name("Joe", "Smith")); return updateUser(token, status, user); } protected ScimUser updateUser(String token, int status, ScimUser user) throws Exception { MockHttpServletRequestBuilder put = put("/Users/" + user.getId()) .header("Authorization", "Bearer " + token) .header("If-Match", "\"" + user.getVersion() + "\"") .accept(APPLICATION_JSON) .contentType(APPLICATION_JSON) .content(JsonUtils.writeValueAsBytes(user)); if (status == HttpStatus.OK.value()) { String json = getMockMvc().perform(put) .andExpect(status().isOk()) .andExpect(header().string("ETag", "\"1\"")) .andExpect(jsonPath("$.userName").value(user.getUserName())) .andExpect(jsonPath("$.emails[0].value").value(user.getPrimaryEmail())) .andExpect(jsonPath("$.name.givenName").value(user.getGivenName())) .andExpect(jsonPath("$.name.familyName").value(user.getFamilyName())) .andReturn().getResponse().getContentAsString(); return JsonUtils.readValue(json, ScimUser.class); } else { getMockMvc().perform(put) .andExpect(status().is(status)); return null; } } @Test public void testUpdateUser() throws Exception { updateUser(scimReadWriteToken, HttpStatus.OK.value()); } @Test public void testUpdateUser_No_Username_Returns_400() throws Exception { updateUser(scimReadWriteToken, HttpStatus.BAD_REQUEST.value()); } @Test public void testUpdateUserWithScimCreateToken() throws Exception { updateUser(scimCreateToken, HttpStatus.FORBIDDEN.value()); } @Test public void testUpdateUserWithUaaAdminToken() throws Exception { updateUser(uaaAdminToken, HttpStatus.OK.value()); } @Test public void testUpdateUserInOtherZoneWithUaaAdminToken() throws Exception { IdentityZone identityZone = getIdentityZone(); ScimUser user = setUpScimUser(identityZone); user.setName(new ScimUser.Name("changed", "name")); getMockMvc().perform(put("/Users/" + user.getId()) .header("Authorization", "Bearer " + uaaAdminToken) .header(IdentityZoneSwitchingFilter.HEADER, identityZone.getId()) .header("If-Match", "\"" + user.getVersion() + "\"") .contentType(APPLICATION_JSON) .content(JsonUtils.writeValueAsBytes(user))) .andExpect(status().isOk()) .andExpect(header().string("ETag", "\"1\"")) .andExpect(jsonPath("$.userName").value(user.getUserName())) .andExpect(jsonPath("$.emails[0].value").value(user.getPrimaryEmail())) .andExpect(jsonPath("$.name.givenName").value(user.getGivenName())) .andExpect(jsonPath("$.name.familyName").value(user.getFamilyName())); } @Test public void delete_user_clears_approvals() throws Exception { ApprovalStore store = getWebApplicationContext().getBean(ApprovalStore.class); JdbcTemplate template = getWebApplicationContext().getBean(JdbcTemplate.class); ScimUser user = setUpScimUser(); Approval approval = new Approval(); approval.setClientId("cf"); approval.setUserId(user.getId()); approval.setScope("openid"); approval.setStatus(Approval.ApprovalStatus.APPROVED); store.addApproval(approval); assertEquals(1, (long)template.queryForObject("select count(*) from authz_approvals where user_id=?", Integer.class, user.getId())); testDeleteUserWithUaaAdminToken(user); assertEquals(0, (long)template.queryForObject("select count(*) from authz_approvals where user_id=?", Integer.class, user.getId())); } @Test public void testDeleteUserWithUaaAdminToken() throws Exception { ScimUser user = setUpScimUser(); testDeleteUserWithUaaAdminToken(user); } public void testDeleteUserWithUaaAdminToken(ScimUser user) throws Exception { getMockMvc().perform((delete("/Users/" + user.getId())) .header("Authorization", "Bearer " + uaaAdminToken) .contentType(APPLICATION_JSON) .content(JsonUtils.writeValueAsBytes(user))) .andExpect(status().isOk()) .andExpect(jsonPath("$.userName").value(user.getUserName())) .andExpect(jsonPath("$.emails[0].value").value(user.getPrimaryEmail())) .andExpect(jsonPath("$.name.givenName").value(user.getGivenName())) .andExpect(jsonPath("$.name.familyName").value(user.getFamilyName())); } @Test public void testDeleteUserInOtherZoneWithUaaAdminToken() throws Exception { IdentityZone identityZone = getIdentityZone(); ScimUser user = setUpScimUser(identityZone); getMockMvc().perform((delete("/Users/" + user.getId())) .header("Authorization", "Bearer " + uaaAdminToken) .header(IdentityZoneSwitchingFilter.HEADER, identityZone.getId()) .contentType(APPLICATION_JSON) .content(JsonUtils.writeValueAsBytes(user))) .andExpect(status().isOk()) .andExpect(jsonPath("$.userName").value(user.getUserName())) .andExpect(jsonPath("$.emails[0].value").value(user.getPrimaryEmail())) .andExpect(jsonPath("$.name.givenName").value(user.getGivenName())) .andExpect(jsonPath("$.name.familyName").value(user.getFamilyName())); } @Test public void cannotCreateUserWithInvalidPasswordInDefaultZone() throws Exception { ScimUser user = getScimUser(); user.setPassword(new RandomValueStringGenerator(260).generate()); byte[] requestBody = JsonUtils.writeValueAsBytes(user); MockHttpServletRequestBuilder post = post("/Users") .header("Authorization", "Bearer " + scimCreateToken) .contentType(APPLICATION_JSON) .content(requestBody); getMockMvc().perform(post) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.error").value("invalid_password")) .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() { return setUpScimUser(IdentityZoneHolder.get()); } private ScimUser setUpScimUser(IdentityZone zone) { IdentityZone original = IdentityZoneHolder.get(); try { IdentityZoneHolder.set(zone); String email = "joe@" + generator.generate().toLowerCase() + ".com"; ScimUser joel = new ScimUser(null, email, "Joel", "D'sa"); joel.setVerified(false); joel.addEmail(email); joel = usersRepository.createUser(joel, USER_PASSWORD); return joel; } finally { IdentityZoneHolder.set(original); } } private String getQueryStringParam(String query, String key) { List<NameValuePair> params = URLEncodedUtils.parse(query, Charset.defaultCharset()); for (NameValuePair pair : params) { if (key.equals(pair.getName())) { return pair.getValue(); } } return null; } private IdentityZone getIdentityZone() throws Exception { String subdomain = generator.generate(); return mockMvcUtils.createOtherIdentityZone(subdomain, getMockMvc(), getWebApplicationContext()); } }