package org.cloudfoundry.identity.uaa.invitations; 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.codestore.ExpiringCodeType; import org.cloudfoundry.identity.uaa.constants.OriginKeys; 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.scim.ScimUser; import org.cloudfoundry.identity.uaa.zone.BrandingInformation; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneConfiguration; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.cloudfoundry.identity.uaa.zone.IdentityZoneProvisioning; import org.cloudfoundry.identity.uaa.zone.MultitenantJdbcClientDetailsService; 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.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 java.util.Arrays; import java.util.Collections; import java.util.Map; import static org.apache.commons.lang3.StringUtils.contains; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.ORIGIN; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA; import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.utils; import static org.cloudfoundry.identity.uaa.util.JsonUtils.readValue; import static org.cloudfoundry.identity.uaa.util.JsonUtils.writeValueAsString; import static org.cloudfoundry.identity.uaa.zone.IdentityZoneSwitchingFilter.HEADER; import static org.cloudfoundry.identity.uaa.zone.IdentityZoneSwitchingFilter.SUBDOMAIN_HEADER; 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.assertFalse; 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; 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 ClientDetails clientDetails; private String adminToken; private String authorities; private String domain; private ExpiringCodeStore codeStore; @Before public void setUp() throws Exception { adminToken = utils().getClientCredentialsOAuthAccessToken(getMockMvc(), "admin", "adminsecret", "clients.read clients.write clients.secret scim.read scim.write clients.admin uaa.admin", null); clientId = generator.generate().toLowerCase(); clientSecret = generator.generate().toLowerCase(); authorities = "scim.read,scim.invite"; clientDetails = utils().createClient(this.getMockMvc(), adminToken, clientId, clientSecret, Collections.singleton("oauth"), Arrays.asList("scim.read","scim.invite"), Arrays.asList(new String[]{"client_credentials", "password"}), authorities); scimInviteToken = utils().getClientCredentialsOAuthAccessToken(getMockMvc(), clientId, clientSecret, "scim.read scim.invite", null); domain = generator.generate().toLowerCase()+".com"; IdentityProvider<UaaIdentityProviderDefinition> uaaProvider = getWebApplicationContext().getBean(JdbcIdentityProviderProvisioning.class).retrieveByOrigin(UAA, IdentityZone.getUaa().getId()); if (uaaProvider.getConfig()==null) { uaaProvider.setConfig(new UaaIdentityProviderDefinition(null,null)); } uaaProvider.getConfig().setEmailDomain(Arrays.asList(domain, "example.com")); getWebApplicationContext().getBean(JdbcIdentityProviderProvisioning.class).update(uaaProvider); codeStore = getWebApplicationContext().getBean(ExpiringCodeStore.class); } @After public void cleanUpDomainList() throws Exception { IdentityProvider<UaaIdentityProviderDefinition> uaaProvider = getWebApplicationContext().getBean(JdbcIdentityProviderProvisioning.class).retrieveByOrigin(UAA, IdentityZone.getUaa().getId()); uaaProvider.getConfig().setEmailDomain(null); getWebApplicationContext().getBean(JdbcIdentityProviderProvisioning.class).update(uaaProvider); } @Test 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); assertResponseAndCodeCorrect(new String[] {email}, redirectUrl, null, response, clientDetails); } @Test 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); assertResponseAndCodeCorrect(emails, redirectUri, null, response, clientDetails); } @Test 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, null); InvitationsResponse response = sendRequestWithTokenAndReturnResponse(userToken, null, clientId, redirectUri, email); assertResponseAndCodeCorrect(new String[] {email}, redirectUri, null, response, clientDetails); } @Test public void invite_User_In_Zone_With_DefaultZone_UaaAdmin() throws Exception { String subdomain = generator.generate(); MockMvcUtils.IdentityZoneCreationResult result = utils().createOtherIdentityZoneAndReturnResult(subdomain, getMockMvc(), getWebApplicationContext(), null); String email = "user1@example.com"; String redirectUrl = "example.com"; InvitationsRequest invitations = new InvitationsRequest(new String[] {email}); String requestBody = writeValueAsString(invitations); MockHttpServletRequestBuilder post = post("/invite_users") .param(OAuth2Utils.REDIRECT_URI, redirectUrl) .header("Authorization", "Bearer " + adminToken) .header(SUBDOMAIN_HEADER, result.getIdentityZone().getSubdomain()) .contentType(APPLICATION_JSON) .content(requestBody); MvcResult mvcResult = getMockMvc().perform(post) .andExpect(status().isOk()) .andReturn(); InvitationsResponse invitationsResponse = readValue(mvcResult.getResponse().getContentAsString(), InvitationsResponse.class); BaseClientDetails defaultClientDetails = new BaseClientDetails(); defaultClientDetails.setClientId("admin"); assertResponseAndCodeCorrect(new String[] {email}, redirectUrl, result.getIdentityZone(), invitationsResponse, defaultClientDetails); } @Test public void invite_User_In_Zone_With_DefaultZone_ZoneAdmin() throws Exception { String subdomain = generator.generate(); MockMvcUtils.IdentityZoneCreationResult result = utils().createOtherIdentityZoneAndReturnResult(subdomain, getMockMvc(), getWebApplicationContext(), null); String zonifiedAdminClientId = generator.generate().toLowerCase(); String zonifiedAdminClientSecret = generator.generate().toLowerCase(); ClientDetails zonifiedScimInviteClientDetails = utils().createClient(this.getMockMvc(), adminToken, zonifiedAdminClientId , zonifiedAdminClientSecret, Collections.singleton("oauth"), null, Arrays.asList(new String[]{"client_credentials", "password"}), "zones."+result.getIdentityZone().getId()+".admin"); String zonifiedScimInviteToken = utils().getClientCredentialsOAuthAccessToken(getMockMvc(), zonifiedAdminClientId , zonifiedAdminClientSecret, "zones."+result.getIdentityZone().getId()+".admin", null); String email = "user1@example.com"; String redirectUrl = "example.com"; InvitationsRequest invitations = new InvitationsRequest(new String[] {email}); String requestBody = writeValueAsString(invitations); MockHttpServletRequestBuilder post = post("/invite_users") .param(OAuth2Utils.REDIRECT_URI, redirectUrl) .header("Authorization", "Bearer " + zonifiedScimInviteToken) .header(SUBDOMAIN_HEADER, result.getIdentityZone().getSubdomain()) .contentType(APPLICATION_JSON) .content(requestBody); MvcResult mvcResult = getMockMvc().perform(post) .andExpect(status().isOk()) .andReturn(); InvitationsResponse invitationsResponse = readValue(mvcResult.getResponse().getContentAsString(), InvitationsResponse.class); assertResponseAndCodeCorrect(new String[] {email}, redirectUrl, result.getIdentityZone(), invitationsResponse, zonifiedScimInviteClientDetails); } @Test public void invite_User_In_Zone_With_DefaultZone_ScimInvite() throws Exception { String subdomain = generator.generate(); MockMvcUtils.IdentityZoneCreationResult result = utils().createOtherIdentityZoneAndReturnResult(subdomain, getMockMvc(), getWebApplicationContext(), null); String zonifiedScimInviteClientId = generator.generate().toLowerCase(); String zonifiedScimInviteClientSecret = generator.generate().toLowerCase(); ClientDetails zonifiedScimInviteClientDetails = utils().createClient(this.getMockMvc(), adminToken, zonifiedScimInviteClientId, zonifiedScimInviteClientSecret, Collections.singleton("oauth"), null, Arrays.asList(new String[]{"client_credentials", "password"}), "zones."+result.getIdentityZone().getId()+".scim.invite"); String zonifiedScimInviteToken = utils().getClientCredentialsOAuthAccessToken(getMockMvc(), zonifiedScimInviteClientId, zonifiedScimInviteClientSecret, "zones."+result.getIdentityZone().getId()+".scim.invite", null); String email = "user1@example.com"; String redirectUrl = "example.com"; InvitationsRequest invitations = new InvitationsRequest(new String[] {email}); String requestBody = writeValueAsString(invitations); MockHttpServletRequestBuilder post = post("/invite_users") .param(OAuth2Utils.REDIRECT_URI, redirectUrl) .header("Authorization", "Bearer " + zonifiedScimInviteToken) .header(HEADER, result.getIdentityZone().getId()) .contentType(APPLICATION_JSON) .content(requestBody); MvcResult mvcResult = getMockMvc().perform(post) .andExpect(status().isOk()) .andReturn(); InvitationsResponse invitationsResponse = readValue(mvcResult.getResponse().getContentAsString(), InvitationsResponse.class); assertResponseAndCodeCorrect(new String[] {email}, redirectUrl, result.getIdentityZone(), invitationsResponse, zonifiedScimInviteClientDetails); } @Test public void invite_User_Within_Zone() throws Exception { String subdomain = generator.generate().toLowerCase(); 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, Collections.singleton("oauth"), Arrays.asList("scim.read","scim.invite"), Arrays.asList("client_credentials", "password"), authorities, Collections.singleton("http://redirect.uri"), result.getIdentityZone()); zonedClientDetails.setClientSecret(zonedClientSecret); String zonedScimInviteToken = utils().getClientCredentialsOAuthAccessToken(getMockMvc(), zonedClientDetails.getClientId(), zonedClientDetails.getClientSecret(), "scim.read scim.invite", subdomain); String email = "user1@example.com"; String redirectUrl = "example.com"; InvitationsResponse response = sendRequestWithTokenAndReturnResponse(zonedScimInviteToken, result.getIdentityZone().getSubdomain(), zonedClientDetails.getClientId(), redirectUrl, email); assertResponseAndCodeCorrect(new String[] {email}, redirectUrl, result.getIdentityZone(), response, zonedClientDetails); } @Test 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(); String email = generator.generate().toLowerCase()+"@"+domain; ScimUser user1 = new ScimUser(null, username1, "givenName", "familyName"); user1.setPrimaryEmail(email); user1.setOrigin(UAA); user1.setPassword("password"); utils().createUser(getMockMvc(), clientAdminToken, user1); ScimUser user2 = new ScimUser(null, username2, "givenName", "familyName"); user2.setPrimaryEmail(email); user2.setOrigin(UAA); user2.setPassword("password"); utils().createUser(getMockMvc(), clientAdminToken, user2); String userToken = utils().getScimInviteUserToken(getMockMvc(), clientId, clientSecret, null); 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 invite_User_With_Invalid_Emails() throws Exception { String invalidEmail1 = "user1example."; String invalidEmail2 = "user1example@"; String invalidEmail3 = "user1example@invalid"; String redirectUrl = "test.com"; InvitationsResponse response = sendRequestWithTokenAndReturnResponse(scimInviteToken, null, clientId, redirectUrl, invalidEmail1, invalidEmail2, invalidEmail3); assertEquals(0, response.getNewInvites().size()); assertEquals(3, response.getFailedInvites().size()); assertEquals("email.invalid", response.getFailedInvites().get(0).getErrorCode()); assertEquals("email.invalid", response.getFailedInvites().get(1).getErrorCode()); assertEquals("email.invalid", response.getFailedInvites().get(2).getErrorCode()); assertEquals(invalidEmail1 + " is invalid email.", response.getFailedInvites().get(0).getErrorMessage()); assertEquals(invalidEmail2 + " is invalid email.", response.getFailedInvites().get(1).getErrorMessage()); assertEquals(invalidEmail3 + " is invalid email.", response.getFailedInvites().get(2).getErrorMessage()); } @Test public void accept_Invitation_Email_With_Default_CompanyName() throws Exception { getMockMvc().perform(get(getAcceptInvitationLink(null))) .andExpect(content().string(containsString("Create your account"))) .andExpect(content().string(containsString("Create account"))); } @Test public void accept_Invitation_Email_With_CompanyName() throws Exception { IdentityZoneConfiguration defaultConfig = IdentityZoneHolder.get().getConfig(); BrandingInformation branding = new BrandingInformation(); branding.setCompanyName("Best Company"); IdentityZoneConfiguration config = new IdentityZoneConfiguration(); config.setBranding(branding); IdentityZone defaultZone = IdentityZoneHolder.getUaaZone(); defaultZone.setConfig(config); getWebApplicationContext().getBean(IdentityZoneProvisioning.class).update(defaultZone); try { getMockMvc().perform(get(getAcceptInvitationLink(null))) .andExpect(content().string(containsString("Create your Best Company account"))) .andExpect(content().string(containsString("Create Best Company account"))) .andExpect(content().string(not(containsString("Create account")))); } finally { defaultZone.setConfig(defaultConfig); getWebApplicationContext().getBean(IdentityZoneProvisioning.class).update(defaultZone); } } @Test public void accept_Invitation_Email_Within_Zone() throws Exception { String subdomain = generator.generate(); IdentityZone zone = MockMvcUtils.createOtherIdentityZone(subdomain, getMockMvc(), getWebApplicationContext()); BrandingInformation branding = new BrandingInformation(); branding.setCompanyName("Best Company"); IdentityZoneConfiguration config = new IdentityZoneConfiguration(); config.setBranding(branding); zone.setConfig(config); getWebApplicationContext().getBean(IdentityZoneProvisioning.class).update(zone); BaseClientDetails client = MockMvcUtils.getClientDetailsModification(clientId, clientSecret, Collections.singleton("oauth"), Arrays.asList("scim.read","scim.invite"), Arrays.asList(new String[]{"client_credentials", "password"}), authorities, Collections.singleton("http://redirect.uri")); IdentityZone original = IdentityZoneHolder.get(); try { IdentityZoneHolder.set(zone); getWebApplicationContext().getBean(MultitenantJdbcClientDetailsService.class).addClientDetails(client); } finally { IdentityZoneHolder.set(original); } String acceptInvitationLink = getAcceptInvitationLink(zone); getMockMvc().perform(get(acceptInvitationLink) .header("Host",(subdomain + ".localhost"))) .andExpect(content().string(containsString("Create your account"))) .andExpect(content().string(containsString("Best Company"))) .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, null); 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("<form action=\"/invitations/accept.do\" method=\"post\" novalidate=\"novalidate\">"))); } public InvitationsResponse sendRequestWithTokenAndReturnResponse(String token, String subdomain, String clientId, String redirectUri, String...emails) throws Exception { return MockMvcUtils.utils().sendRequestWithTokenAndReturnResponse(getWebApplicationContext(), getMockMvc(), token, subdomain, clientId, redirectUri, emails); } public 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)); } private void assertResponseAndCodeCorrect(String[] emails, String redirectUrl, IdentityZone zone, 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(OriginKeys.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())); String link = response.getNewInvites().get(i).getInviteLink().toString(); assertFalse(contains(link, "@")); assertFalse(contains(link, "%40")); if (zone!=null && StringUtils.hasText(zone.getSubdomain())) { assertThat(link, startsWith("http://" + zone.getSubdomain() + ".localhost/invitations/accept")); IdentityZoneHolder.set(zone); } else { assertThat(link, 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); IdentityZoneHolder.clear(); assertThat(expiringCode.getExpiresAt().getTime(), is(greaterThan(System.currentTimeMillis()))); assertThat(expiringCode.getIntent(), is(ExpiringCodeType.INVITATION.name())); Map<String, String> data = readValue(expiringCode.getData(), new TypeReference<Map<String, String>>() {}); assertThat(data.get(InvitationConstants.USER_ID), is(notNullValue())); assertThat(data.get(InvitationConstants.EMAIL), is(emails[i])); assertThat(data.get(ORIGIN), is(OriginKeys.UAA)); assertThat(data.get(CLIENT_ID), is(clientDetails.getClientId())); assertThat(data.get(REDIRECT_URI), is(redirectUrl)); } } private String getAcceptInvitationLink(IdentityZone zone) throws Exception { String userToken = utils().getScimInviteUserToken(getMockMvc(), clientId, clientSecret, zone); String email = generator.generate().toLowerCase() + "@"+domain; InvitationsResponse response = sendRequestWithTokenAndReturnResponse(userToken, zone==null?null:zone.getSubdomain(), clientId, "example.com", email); return response.getNewInvites().get(0).getInviteLink().toString(); } }