diff --git a/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java b/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java index 57ed7a9e22..f883d9ec10 100755 --- a/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java +++ b/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java @@ -60,6 +60,7 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider { protected FreeMarkerProvider freeMarker; protected RealmModel realm; protected UserModel user; + protected String userEmail; protected final Map attributes = new HashMap<>(); public FreeMarkerEmailTemplateProvider(KeycloakSession session) { diff --git a/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationMemberResource.java b/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationMemberResource.java index e7aac33399..a84c6b45eb 100644 --- a/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationMemberResource.java +++ b/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationMemberResource.java @@ -63,6 +63,7 @@ import org.keycloak.services.resources.admin.AdminEventBuilder; import org.keycloak.services.resources.admin.UserResource; import org.keycloak.services.resources.admin.UsersResource; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; +import org.keycloak.storage.adapter.InMemoryUserAdapter; import org.keycloak.utils.StringUtil; @Provider @@ -125,9 +126,6 @@ public class OrganizationMemberResource { UserModel user = session.users().getUserByEmail(realm, rep.getEmail()); InviteOrgActionToken token = null; - // TODO not sure if this client id is right or if we should get one from the user somehow... - // TODO not really sure if the token is getting signed so we need to figure out where that's happening... maybe in the serialize method? - // TODO the expiration is set to a day in seconds but we should probably get this from configuration instead String link = null; int tokenExpiration = Time.currentTime() + realm.getActionTokenGeneratedByAdminLifespan(); if (user != null) { @@ -138,13 +136,18 @@ public class OrganizationMemberResource { .build(realm.getName()).toString(); } else { // this path lets us invite a user that doesn't exist yet, letting them register into the organization - token = new InviteOrgActionToken(null, tokenExpiration, rep.getEmail(), session.getContext().getClient().getClientId()); + token = new InviteOrgActionToken(null, tokenExpiration, rep.getEmail(), Constants.ACCOUNT_MANAGEMENT_CLIENT_ID); token.setOrgId(organization.getId()); link = LoginActionsService.registrationFormProcessor(session.getContext().getUri()) .queryParam(Constants.ORG_TOKEN, token.serialize(session, realm, session.getContext().getUri())) .build(realm.getName()).toString(); } + if (user == null ) { + user = new InMemoryUserAdapter(session, realm, null); + user.setEmail(rep.getEmail()); + } + try { session .getProvider(EmailTemplateProvider.class) diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationInvitationLinkTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationInvitationLinkTest.java index 46b1ea918f..f7a1e99168 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationInvitationLinkTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationInvitationLinkTest.java @@ -19,9 +19,12 @@ package org.keycloak.testsuite.organization.admin; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import java.io.IOException; import java.util.Map; +import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; @@ -31,13 +34,23 @@ import org.jboss.arquillian.graphene.page.Page; import org.junit.Rule; import org.junit.Test; import org.keycloak.admin.client.resource.OrganizationResource; +import org.keycloak.authentication.requiredactions.TermsAndConditions; import org.keycloak.common.Profile.Feature; +import org.keycloak.common.util.Time; +import org.keycloak.common.util.UriUtils; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.Constants; +import org.keycloak.models.UserModel; import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.RequiredActionProviderRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.Assert; +import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.arquillian.annotation.EnableFeature; +import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.InfoPage; +import org.keycloak.testsuite.pages.RegisterPage; import org.keycloak.testsuite.util.GreenMailRule; import org.keycloak.testsuite.util.MailUtils; import org.keycloak.testsuite.util.UserBuilder; @@ -45,12 +58,18 @@ import org.keycloak.testsuite.util.UserBuilder; @EnableFeature(Feature.ORGANIZATION) public class OrganizationInvitationLinkTest extends AbstractOrganizationTest { + @Rule + public AssertEvents events = new AssertEvents(this); + @Rule public GreenMailRule greenMail = new GreenMailRule(); @Page protected InfoPage infoPage; + @Page + protected RegisterPage registerPage; + @Override public void configureTestRealm(RealmRepresentation testRealm) { Map smtpConfig = testRealm.getSmtpServer(); @@ -84,4 +103,48 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest { assertThat(infoPage.getInfo(), containsString("Your account has been updated.")); Assert.assertTrue(organization.members().getAll().stream().anyMatch(actual -> user.getId().equals(actual.getId()))); } + + + @Test + public void testInviteNewUserRegistration() throws IOException { + + UserRepresentation user = UserBuilder.create() + .username("invitedUser") + .email("inviteduser@email") + .enabled(true) + .build(); + // User isn't created when we send the invite + OrganizationResource organization = testRealm().organizations().get(createOrganization().getId()); + organization.members().inviteMember(user).close(); + + MimeMessage message = greenMail.getLastReceivedMessage(); + Assert.assertNotNull(message); + String link = MailUtils.getPasswordResetEmailLink(message); + String orgToken = UriUtils.parseQueryParameters(link, false).values().stream().map(strings -> strings.get(0)).findFirst().orElse(null); + Assert.assertNotNull(orgToken); + + driver.manage().timeouts().pageLoadTimeout(1, TimeUnit.DAYS); + driver.navigate().to(link.trim()); + Assert.assertFalse(organization.members().getAll().stream().anyMatch(actual -> user.getId().equals(actual.getId()))); + + registerPage.assertCurrent(); + registerPage.register("firstName", "lastName", "inviteduser@myemail", + "invitedUser", "password", "password", null, true, null); + + assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + String userId = events.expectRegister("invitedUser", "inviteduser@email") + .assertEvent().getUserId(); + UserRepresentation registeredUser = assertUserRegistered(userId, "invitedUser"); + Assert.assertTrue(organization.members().getAll().stream().anyMatch(actual -> registeredUser.getId().equals(actual.getId()))); + } + + private UserRepresentation assertUserRegistered(String userId, String username) { + events.expectLogin().detail("username", username.toLowerCase()).user(userId).assertEvent(); + + UserRepresentation user = testRealm().users().get(userId).toRepresentation(); + org.junit.Assert.assertNotNull(user); + org.junit.Assert.assertNotNull(user.getCreatedTimestamp()); + return user; + } } \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationMemberTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationMemberTest.java index c18dfe1258..e43c981af0 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationMemberTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationMemberTest.java @@ -33,7 +33,6 @@ import static org.keycloak.models.OrganizationModel.ORGANIZATION_ATTRIBUTE; import java.util.ArrayList; import java.util.List; - import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.core.Response; @@ -296,86 +295,4 @@ public class OrganizationMemberTest extends AbstractOrganizationTest { assertFalse(testRealm().groups().groups().stream().anyMatch(group -> group.getName().startsWith("kc.org."))); } - @Test - public void testSearchMembers() { - - // create test users, ordered by username (e-mail). - OrganizationResource organization = testRealm().organizations().get(createOrganization().getId()); - List expected = new ArrayList<>(); - expected.add(addMember(organization, "batwoman@neworg.org", "Katherine", "Kane")); - expected.add(addMember(organization, "brucewayne@neworg.org", "Bruce", "Wayne")); - expected.add(addMember(organization, "harveydent@neworg.org", "Harvey", "Dent")); - expected.add(addMember(organization, "marthaw@neworg.org", "Martha", "Wayne")); - expected.add(addMember(organization, "thejoker@neworg.org", "Jack", "White")); - - // exact search - username/e-mail, first name, last name. - List existing = organization.members().search("brucewayne@neworg.org", true, null, null); - assertThat(existing, hasSize(1)); - assertThat(existing.get(0).getUsername(), is(equalTo("brucewayne@neworg.org"))); - assertThat(existing.get(0).getEmail(), is(equalTo("brucewayne@neworg.org"))); - assertThat(existing.get(0).getFirstName(), is(equalTo("Bruce"))); - assertThat(existing.get(0).getLastName(), is(equalTo("Wayne"))); - - existing = organization.members().search("Harvey", true, null, null); - assertThat(existing, hasSize(1)); - assertThat(existing.get(0).getUsername(), is(equalTo("harveydent@neworg.org"))); - assertThat(existing.get(0).getEmail(), is(equalTo("harveydent@neworg.org"))); - assertThat(existing.get(0).getFirstName(), is(equalTo("Harvey"))); - assertThat(existing.get(0).getLastName(), is(equalTo("Dent"))); - - existing = organization.members().search("Wayne", true, null, null); - assertThat(existing, hasSize(2)); - assertThat(existing.get(0).getUsername(), is(equalTo("brucewayne@neworg.org"))); - assertThat(existing.get(1).getUsername(), is(equalTo("marthaw@neworg.org"))); - - existing = organization.members().search("Gordon", true, null, null); - assertThat(existing, is(empty())); - - // partial search - partial e-mail should match all users. - existing = organization.members().search("neworg", false, null, null); - assertThat(existing, hasSize(5)); - for (int i = 0; i < 5; i++) { // returned entries should also be ordered. - assertThat(expected.get(i).getId(), is(equalTo(expected.get(i).getId()))); - assertThat(expected.get(i).getUsername(), is(equalTo(expected.get(i).getUsername()))); - assertThat(expected.get(i).getEmail(), is(equalTo(expected.get(i).getEmail()))); - assertThat(expected.get(i).getFirstName(), is(equalTo(expected.get(i).getFirstName()))); - assertThat(expected.get(i).getLastName(), is(equalTo(expected.get(i).getLastName()))); - } - - // partial search using 'th' search string - should match 'Katherine' by name, 'Jack' by username/e-mail - // and 'Martha' either by username or first name. - existing = organization.members().search("th", false, null, null); - assertThat(existing, hasSize(3)); - assertThat(existing.get(0).getUsername(), is(equalTo("batwoman@neworg.org"))); - assertThat(existing.get(0).getFirstName(), is(equalTo("Katherine"))); - assertThat(existing.get(1).getUsername(), is(equalTo("marthaw@neworg.org"))); - assertThat(existing.get(1).getFirstName(), is(equalTo("Martha"))); - assertThat(existing.get(2).getUsername(), is(equalTo("thejoker@neworg.org"))); - assertThat(existing.get(2).getFirstName(), is(equalTo("Jack"))); - - // partial search using 'way' - should match both 'Bruce' (either by username or last name) and 'Martha' by last name. - existing = organization.members().search("way", false, null, null); - assertThat(existing, hasSize(2)); - assertThat(existing.get(0).getUsername(), is(equalTo("brucewayne@neworg.org"))); - assertThat(existing.get(0).getFirstName(), is(equalTo("Bruce"))); - assertThat(existing.get(1).getUsername(), is(equalTo("marthaw@neworg.org"))); - assertThat(existing.get(1).getFirstName(), is(equalTo("Martha"))); - - // partial search using with no match - e.g. 'nonexistent'. - existing = organization.members().search("nonexistent", false, null, null); - assertThat(existing, is(empty())); - - // paginated search - try to fetch 3 users per page. - existing = organization.members().search("", false, 0, 3); - assertThat(existing, hasSize(3)); - assertThat(existing.get(0).getUsername(), is(equalTo("batwoman@neworg.org"))); - assertThat(existing.get(1).getUsername(), is(equalTo("brucewayne@neworg.org"))); - assertThat(existing.get(2).getUsername(), is(equalTo("harveydent@neworg.org"))); - - existing = organization.members().search("", false, 3, 3); - assertThat(existing, hasSize(2)); - assertThat(existing.get(0).getUsername(), is(equalTo("marthaw@neworg.org"))); - assertThat(existing.get(1).getUsername(), is(equalTo("thejoker@neworg.org"))); - } - } \ No newline at end of file