adding test and minor updates to cover inviting existing users
Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
584e92aaba
commit
e0bdb42d41
7 changed files with 110 additions and 11 deletions
|
@ -73,4 +73,9 @@ public interface OrganizationMembersResource {
|
||||||
|
|
||||||
@Path("{id}")
|
@Path("{id}")
|
||||||
OrganizationMemberResource member(@PathParam("id") String id);
|
OrganizationMemberResource member(@PathParam("id") String id);
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("invite")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
Response inviteMember(UserRepresentation rep);
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ import org.keycloak.models.Constants;
|
||||||
*/
|
*/
|
||||||
public class InviteOrgActionToken extends DefaultActionToken {
|
public class InviteOrgActionToken extends DefaultActionToken {
|
||||||
|
|
||||||
public static final String TOKEN_TYPE = "invite-org";
|
public static final String TOKEN_TYPE = "ORGIVT";
|
||||||
|
|
||||||
private static final String JSON_FIELD_REDIRECT_URI = "reduri";
|
private static final String JSON_FIELD_REDIRECT_URI = "reduri";
|
||||||
private static final String JSON_ORG_ID = "org_id";
|
private static final String JSON_ORG_ID = "org_id";
|
||||||
|
|
|
@ -133,9 +133,4 @@ public class InviteOrgActionTokenHandler extends AbstractActionTokenHandler<Invi
|
||||||
String nextAction = AuthenticationManager.nextRequiredAction(session, authSession, tokenContext.getRequest(), event);
|
String nextAction = AuthenticationManager.nextRequiredAction(session, authSession, tokenContext.getRequest(), event);
|
||||||
return AuthenticationManager.redirectToRequiredActions(session, realm, authSession, uriInfo, nextAction);
|
return AuthenticationManager.redirectToRequiredActions(session, realm, authSession, uriInfo, nextAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isVerifyEmailActionSet(UserModel user, AuthenticationSessionModel authSession) {
|
|
||||||
return Stream.concat(user.getRequiredActionsStream(), authSession.getRequiredActions().stream())
|
|
||||||
.anyMatch(RequiredAction.VERIFY_EMAIL.name()::equals);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,7 @@ import java.util.Objects;
|
||||||
|
|
||||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||||
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
|
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.ModelException;
|
import org.keycloak.models.ModelException;
|
||||||
import org.keycloak.models.OrganizationModel;
|
import org.keycloak.models.OrganizationModel;
|
||||||
|
@ -117,30 +118,32 @@ public class OrganizationMemberResource {
|
||||||
@Path("invite")
|
@Path("invite")
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
public Response inviteMember(UserRepresentation rep) {
|
public Response inviteMember(UserRepresentation rep) {
|
||||||
if (rep == null || StringUtil.isBlank(rep.getEmail()) || StringUtil.isBlank(rep.getUsername())) {
|
if (rep == null || StringUtil.isBlank(rep.getEmail())) {
|
||||||
throw new BadRequestException("To invite a member you need to provide an email and/or username");
|
throw new BadRequestException("To invite a member you need to provide an email and/or username");
|
||||||
}
|
}
|
||||||
|
|
||||||
UserModel user = session.users().getUserByUsername(realm, rep.getEmail());
|
UserModel user = session.users().getUserByEmail(realm, rep.getEmail());
|
||||||
|
|
||||||
InviteOrgActionToken token = null;
|
InviteOrgActionToken token = null;
|
||||||
// TODO not sure if this client id is right or if we should get one from the user somehow...
|
// 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 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
|
// TODO the expiration is set to a day in seconds but we should probably get this from configuration instead
|
||||||
String link = null;
|
String link = null;
|
||||||
|
int tokenExpiration = Time.currentTime() + realm.getActionTokenGeneratedByAdminLifespan();
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
token = new InviteOrgActionToken(user.getId(), 86400, user.getEmail(), session.getContext().getClient().getClientId());
|
token = new InviteOrgActionToken(user.getId(), tokenExpiration, user.getEmail(), Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
|
||||||
|
token.setOrgId(organization.getId());
|
||||||
link = LoginActionsService.actionTokenProcessor(session.getContext().getUri())
|
link = LoginActionsService.actionTokenProcessor(session.getContext().getUri())
|
||||||
.queryParam("key", token.serialize(session, realm, session.getContext().getUri()))
|
.queryParam("key", token.serialize(session, realm, session.getContext().getUri()))
|
||||||
.build(realm.getName()).toString();
|
.build(realm.getName()).toString();
|
||||||
} else {
|
} else {
|
||||||
// this path lets us invite a user that doesn't exist yet, letting them register into the organization
|
// this path lets us invite a user that doesn't exist yet, letting them register into the organization
|
||||||
token = new InviteOrgActionToken(null, 86400, rep.getEmail(), session.getContext().getClient().getClientId());
|
token = new InviteOrgActionToken(null, tokenExpiration, rep.getEmail(), session.getContext().getClient().getClientId());
|
||||||
|
token.setOrgId(organization.getId());
|
||||||
link = LoginActionsService.registrationFormProcessor(session.getContext().getUri())
|
link = LoginActionsService.registrationFormProcessor(session.getContext().getUri())
|
||||||
.queryParam(Constants.ORG_TOKEN, token.serialize(session, realm, session.getContext().getUri()))
|
.queryParam(Constants.ORG_TOKEN, token.serialize(session, realm, session.getContext().getUri()))
|
||||||
.build(realm.getName()).toString();
|
.build(realm.getName()).toString();
|
||||||
}
|
}
|
||||||
token.setOrgId(organization.getId());
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
session
|
session
|
||||||
|
|
|
@ -40,6 +40,9 @@ public class InfoPage extends LanguageComboboxAwarePage {
|
||||||
@FindBy(linkText = "» Klicken Sie hier um fortzufahren")
|
@FindBy(linkText = "» Klicken Sie hier um fortzufahren")
|
||||||
private WebElement clickToContinueDe;
|
private WebElement clickToContinueDe;
|
||||||
|
|
||||||
|
@FindBy(linkText = "» Click here to proceed")
|
||||||
|
private WebElement clickToContinue;
|
||||||
|
|
||||||
@FindBy(linkText = "« Zpět na aplikaci")
|
@FindBy(linkText = "« Zpět na aplikaci")
|
||||||
private WebElement backToApplicationCs;
|
private WebElement backToApplicationCs;
|
||||||
|
|
||||||
|
@ -65,6 +68,10 @@ public class InfoPage extends LanguageComboboxAwarePage {
|
||||||
clickToContinueDe.click();
|
clickToContinueDe.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void clickToContinue() {
|
||||||
|
clickToContinue.click();
|
||||||
|
}
|
||||||
|
|
||||||
public void clickBackToApplicationLinkCs() {
|
public void clickBackToApplicationLinkCs() {
|
||||||
backToApplicationCs.click();
|
backToApplicationCs.click();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.testsuite.organization.admin;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import jakarta.mail.internet.MimeMessage;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
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.common.Profile.Feature;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
|
import org.keycloak.testsuite.Assert;
|
||||||
|
import org.keycloak.testsuite.admin.ApiUtil;
|
||||||
|
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||||
|
import org.keycloak.testsuite.pages.InfoPage;
|
||||||
|
import org.keycloak.testsuite.util.GreenMailRule;
|
||||||
|
import org.keycloak.testsuite.util.MailUtils;
|
||||||
|
import org.keycloak.testsuite.util.UserBuilder;
|
||||||
|
|
||||||
|
@EnableFeature(Feature.ORGANIZATION)
|
||||||
|
public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public GreenMailRule greenMail = new GreenMailRule();
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected InfoPage infoPage;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
|
Map<String, String> smtpConfig = testRealm.getSmtpServer();
|
||||||
|
super.configureTestRealm(testRealm);
|
||||||
|
testRealm.setSmtpServer(smtpConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInviteExistingUser() throws IOException {
|
||||||
|
UserRepresentation user = UserBuilder.create()
|
||||||
|
.username("invited")
|
||||||
|
.email("invited@myemail.com")
|
||||||
|
.password("password")
|
||||||
|
.enabled(true)
|
||||||
|
.build();
|
||||||
|
try (Response response = testRealm().users().create(user)) {
|
||||||
|
user.setId(ApiUtil.getCreatedId(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||||
|
|
||||||
|
organization.members().inviteMember(user).close();
|
||||||
|
|
||||||
|
MimeMessage message = greenMail.getLastReceivedMessage();
|
||||||
|
Assert.assertNotNull(message);
|
||||||
|
String link = MailUtils.getPasswordResetEmailLink(message);
|
||||||
|
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())));
|
||||||
|
infoPage.clickToContinue();
|
||||||
|
assertThat(infoPage.getInfo(), containsString("Your account has been updated."));
|
||||||
|
Assert.assertTrue(organization.members().getAll().stream().anyMatch(actual -> user.getId().equals(actual.getId())));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
<#ftl output_format="plainText">
|
||||||
|
${kcSanitize(msg("orgInviteBodyHtml", link, linkExpiration, realmName, linkExpirationFormatter(linkExpiration)))}
|
Loading…
Reference in a new issue