Disabled organization should not execute invitations

Closes #45760

Signed-off-by: vramik <vramik@redhat.com>
This commit is contained in:
vramik
2026-03-24 21:28:31 +01:00
committed by Pedro Igor
parent df92e7aac8
commit 43864c1375
9 changed files with 135 additions and 0 deletions
@@ -49,6 +49,7 @@ public interface Errors {
String EMAIL_IN_USE = "email_in_use";
String EMAIL_ALREADY_VERIFIED = "email_already_verified";
String ORG_NOT_FOUND = "org_not_found";
String ORG_DISABLED = "org_disabled";
String USER_ORG_MEMBER_ALREADY = "user_org_member_already";
String INVALID_REDIRECT_URI = "invalid_redirect_uri";
@@ -84,6 +84,10 @@ public class InviteOrgActionTokenHandler extends AbstractActionTokenHandler<Invi
return invalidOrganizationResponse(tokenContext, token);
}
if (!organization.isEnabled()) {
return disabledOrganizationResponse(tokenContext, token);
}
InvitationManager invitationManager = orgProvider.getInvitationManager();
OrganizationInvitationModel invitation = invitationManager.getById(token.getId());
AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
@@ -121,6 +125,10 @@ public class InviteOrgActionTokenHandler extends AbstractActionTokenHandler<Invi
return invalidOrganizationResponse(tokenContext, token);
}
if (!organization.isEnabled()) {
return disabledOrganizationResponse(tokenContext, token);
}
if (organization.isMember(user)) {
return alreadyMemberResponse(organization, user, tokenContext, token);
}
@@ -206,6 +214,23 @@ public class InviteOrgActionTokenHandler extends AbstractActionTokenHandler<Invi
.createInfoPage();
}
private Response disabledOrganizationResponse(ActionTokenContext<InviteOrgActionToken> tokenContext, InviteOrgActionToken token) {
EventBuilder event = tokenContext.getEvent();
KeycloakSession session = tokenContext.getSession();
AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
event.detail(Details.TOKEN_ID, token.getId())
.detail(Details.EMAIL, token.getEmail())
.detail(Details.ORG_ID, token.getOrgId())
.error(Errors.ORG_DISABLED);
return session.getProvider(LoginFormsProvider.class)
.setStatus(Status.BAD_REQUEST)
.setAuthenticationSession(authSession)
.setAttribute("messageHeader", Messages.STALE_INVITE_ORG_LINK)
.setInfo(Messages.ORG_DISABLED)
.createInfoPage();
}
private Response alreadyMemberResponse(OrganizationModel organization, UserModel user, ActionTokenContext<InviteOrgActionToken> tokenContext, InviteOrgActionToken token) {
EventBuilder event = tokenContext.getEvent();
KeycloakSession session = tokenContext.getSession();
@@ -319,6 +319,11 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
return false;
}
if (!organization.isEnabled()) {
error.accept(List.of(new FormMessage("The organization is not available at this time.")));
return false;
}
// make sure the organization is set to the session so that UP org-related validators can run
session.getContext().setOrganization(organization);
session.setAttribute(InviteOrgActionToken.class.getName(), token);
@@ -94,6 +94,10 @@ public class OrganizationInvitationResource {
}
public Response inviteUser(String email, String firstName, String lastName) {
if (!organization.isEnabled()) {
throw ErrorResponse.error("Organization is disabled", Status.BAD_REQUEST);
}
if (StringUtil.isBlank(email)) {
throw ErrorResponse.error("Email is required to invite a member", Status.BAD_REQUEST);
}
@@ -138,6 +142,10 @@ public class OrganizationInvitationResource {
}
public Response inviteExistingUser(String id) {
if (!organization.isEnabled()) {
throw ErrorResponse.error("Organization is disabled", Status.BAD_REQUEST);
}
if (StringUtil.isBlank(id)) {
throw new BadRequestException("To invite a member you need to provide the user id");
}
@@ -317,6 +325,10 @@ public class OrganizationInvitationResource {
@APIResponse(responseCode = "404", description = "Not Found")
})
public Response resendInvitation(@PathParam("id") String id) {
if (!organization.isEnabled()) {
throw ErrorResponse.error("Organization is disabled", Status.BAD_REQUEST);
}
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
InvitationManager invitationManager = provider.getInvitationManager();
@@ -39,6 +39,8 @@ public class Messages {
public static final String ORG_NOT_FOUND = "orgNotFoundMessage";
public static final String ORG_DISABLED = "orgDisabledMessage";
public static final String ORG_MEMBER_ALREADY = "orgMemberAlready";
public static final String INVALID_ORG_INVITE = "invalidOrgInviteMessage";
@@ -29,4 +29,9 @@ public class OrganizationAttributeUpdater extends ServerResourceUpdater<Organiza
this.rep.setRedirectUrl(redirectUrl);
return this;
}
public OrganizationAttributeUpdater setEnabled(boolean enabled) {
this.rep.setEnabled(enabled);
return this;
}
}
@@ -545,6 +545,48 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
Assert.assertNotNull(organization.members().member(user.getId()).toRepresentation());
}
@Test
public void testAcceptInvitationLinkAfterOrgDisabled() throws IOException, MessagingException {
UserRepresentation user = createUser("invited", "invited@myemail.com");
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
organization.members().inviteExistingUser(user.getId()).close();
String link = getInvitationLinkFromEmail(user.getFirstName(), user.getLastName());
// Disable the organization after the invitation was sent
try (OrganizationAttributeUpdater oau = new OrganizationAttributeUpdater(organization).setEnabled(false).update()) {
driver.navigate().to(link);
assertThat(infoPage.isCurrent(), is(true));
assertThat(infoPage.getInfo(), containsString("The organization is not available at this time and cannot accept new members."));
// User should not be added to organization
List<MemberRepresentation> members = organization.members().search(user.getEmail(), Boolean.TRUE, null, null);
assertThat(members, empty());
}
// After re-enabling, the link should work again
acceptInvitation(organization, user);
}
@Test
public void testNewUserRegistrationAfterOrgDisabled() throws IOException, MessagingException {
String email = "inviteduser@email";
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
organization.members().inviteUser(email, "Homer", "Simpson").close();
String link = getInvitationLinkFromEmail();
// Disable the organization after the invitation was sent
try (OrganizationAttributeUpdater oau = new OrganizationAttributeUpdater(organization).setEnabled(false).update()) {
driver.navigate().to(link);
// Registration page should show error about disabled org
assertThat(driver.getPageSource(), containsString("The organization is not available at this time and cannot accept new members."));
}
}
@Test
public void testInvitationLinkAfterInvitationDeleted() throws IOException, MessagingException {
String email = "inviteduser@email";
@@ -28,6 +28,7 @@ import org.keycloak.admin.client.resource.OrganizationResource;
import org.keycloak.representations.idm.OrganizationInvitationRepresentation;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.updaters.OrganizationAttributeUpdater;
import org.keycloak.testsuite.util.MailServer;
import org.junit.After;
@@ -382,6 +383,47 @@ public class OrganizationInvitationManagementTest extends AbstractOrganizationTe
assertThat(org2Invitations.get(0).getOrganizationId(), equalTo(org2Rep.getId()));
}
@Test
public void testSendInvitationToDisabledOrganization() throws Exception {
try (OrganizationAttributeUpdater oau = new OrganizationAttributeUpdater(organization).setEnabled(false).update()) {
try (Response response = organization.members().inviteUser("user@test-org.com", "John", "Doe")) {
assertThat(response.getStatus(), equalTo(400));
assertThat(response.readEntity(String.class), containsString("Organization is disabled"));
}
}
}
@Test
public void testResendInvitationToDisabledOrganization() throws Exception {
sendInvitation("user@test-org.com", "John", "Doe");
List<OrganizationInvitationRepresentation> invitations = organization.invitations().list();
assertThat(invitations, hasSize(1));
String invitationId = invitations.get(0).getId();
try (OrganizationAttributeUpdater oau = new OrganizationAttributeUpdater(organization).setEnabled(false).update()) {
try (Response response = organization.invitations().resend(invitationId)) {
assertThat(response.getStatus(), equalTo(400));
assertThat(response.readEntity(String.class), containsString("Organization is disabled"));
}
}
}
@Test
public void testInvitationWorksAfterReEnablingOrganization() throws Exception {
try (OrganizationAttributeUpdater oau = new OrganizationAttributeUpdater(organization).setEnabled(false).update()) {
try (Response response = organization.members().inviteUser("user@test-org.com", "John", "Doe")) {
assertThat(response.getStatus(), equalTo(400));
}
}
// After re-enabling (OrganizationAttributeUpdater restores original state), invitation should work
sendInvitation("user@test-org.com", "John", "Doe");
List<OrganizationInvitationRepresentation> invitations = organization.invitations().list();
assertThat(invitations, hasSize(1));
assertThat(invitations.get(0).getEmail(), equalTo("user@test-org.com"));
}
private void sendInvitation(String email, String firstName, String lastName) {
sendInvitationToOrganization(organization, email, firstName, lastName);
}
@@ -566,4 +566,5 @@ notMemberOfOrganization=User is not a member of the organization {0}
notMemberOfAnyOrganization=User is not a member of any organization
emailVerificationPending=A verification email was sent to {0}. You can submit without changes to resend the verification email, or enter a different email address.
orgMemberAlready=You are already a member of the {1} organization.
orgDisabledMessage=The organization is not available at this time and cannot accept new members.
staleInviteOrgLink=The link you clicked is no longer valid. It may have expired or already been used.