diff --git a/server-spi/src/main/java/org/keycloak/models/OrganizationModel.java b/server-spi/src/main/java/org/keycloak/models/OrganizationModel.java index e9c1882cb3..88a689843a 100644 --- a/server-spi/src/main/java/org/keycloak/models/OrganizationModel.java +++ b/server-spi/src/main/java/org/keycloak/models/OrganizationModel.java @@ -25,6 +25,7 @@ import java.util.stream.Stream; public interface OrganizationModel { String ORGANIZATION_ATTRIBUTE = "kc.org"; + String ORGANIZATION_NAME_ATTRIBUTE = "kc.org.name"; String ORGANIZATION_DOMAIN_ATTRIBUTE = "kc.org.domain"; String BROKER_PUBLIC = "kc.org.broker.public"; diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/inviteorg/InviteOrgActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/inviteorg/InviteOrgActionTokenHandler.java index 4766d9b153..1d9607698a 100644 --- a/services/src/main/java/org/keycloak/authentication/actiontoken/inviteorg/InviteOrgActionTokenHandler.java +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/inviteorg/InviteOrgActionTokenHandler.java @@ -43,8 +43,8 @@ import org.keycloak.services.messages.Messages; import org.keycloak.sessions.AuthenticationSessionCompoundId; import org.keycloak.sessions.AuthenticationSessionModel; +import java.util.List; import java.util.Objects; -import java.util.stream.Stream; /** * Action token handler for handling invitation of an existing user to an organization. A new user is handled in registration {@link org.keycloak.services.resources.LoginActionsService}. @@ -114,6 +114,8 @@ public class InviteOrgActionTokenHandler extends AbstractActionTokenHandler msgParams = new HashMap<>(attributes); + msgParams.putAll(messagesBundle); + attributes.put("msg", new MessageFormatterMethod(locale, msgParams)); attributes.put("advancedMsg", new AdvancedMessageFormatterMethod(locale, messagesBundle)); } catch (IOException e) { logger.warn("Failed to load messages", e); @@ -441,7 +443,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { } attributes.put("message", wholeMessage); } else { - attributes.put("message", null); + attributes.remove("message"); } attributes.put("messagesPerField", messagesPerField); } @@ -486,7 +488,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri)); attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri)); attributes.put("auth", new AuthenticationContextBean(context, page)); - attributes.put(Constants.EXECUTION, execution); + setAttribute(Constants.EXECUTION, execution); if (realm.isInternationalizationEnabled()) { UriBuilder b; @@ -893,7 +895,11 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { @Override public LoginFormsProvider setAttribute(String name, Object value) { - this.attributes.put(name, value); + if (value == null) { + attributes.remove(name); + } else { + attributes.put(name, value); + } return this; } diff --git a/services/src/main/java/org/keycloak/services/messages/Messages.java b/services/src/main/java/org/keycloak/services/messages/Messages.java index 12d991bb2d..0d477cf557 100755 --- a/services/src/main/java/org/keycloak/services/messages/Messages.java +++ b/services/src/main/java/org/keycloak/services/messages/Messages.java @@ -320,4 +320,5 @@ public class Messages { public static final String OAUTH2_DEVICE_VERIFICATION_FAILED_HEADER = "oauth2DeviceVerificationFailedHeader"; public static final String OAUTH2_DEVICE_CONSENT_DENIED = "oauth2DeviceConsentDeniedMessage"; + public static final String CONFIRM_ORGANIZATION_MEMBERSHIP = "organization.confirm-membership"; } diff --git a/services/src/main/java/org/keycloak/theme/beans/MessageFormatterMethod.java b/services/src/main/java/org/keycloak/theme/beans/MessageFormatterMethod.java index dfb737d6bf..f2d508ade4 100755 --- a/services/src/main/java/org/keycloak/theme/beans/MessageFormatterMethod.java +++ b/services/src/main/java/org/keycloak/theme/beans/MessageFormatterMethod.java @@ -17,6 +17,8 @@ package org.keycloak.theme.beans; +import static java.util.Optional.ofNullable; + import freemarker.template.SimpleScalar; import freemarker.template.TemplateMethodModelEx; import freemarker.template.TemplateModelException; @@ -26,6 +28,8 @@ import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.Optional; import java.util.Properties; /** @@ -40,13 +44,22 @@ public class MessageFormatterMethod implements TemplateMethodModelEx { this.messages = messages; } + public MessageFormatterMethod(Locale locale, Map messages) { + this.locale = locale; + this.messages = new Properties(); + this.messages.putAll(ofNullable(messages).orElse(Map.of())); + } + @Override public Object exec(List list) throws TemplateModelException { if (list.size() >= 1) { // resolve any remaining ${} expressions List resolved = resolve(list.subList(1, list.size())); String key = list.get(0).toString(); - return new MessageFormat(messages.getProperty(key,key),locale).format(resolved.toArray()); + String value = messages.getOrDefault(key, key).toString(); + // try to also resolve placeholders if present in the message bundle + value = (String) resolve(List.of(value)).get(0); + return new MessageFormat(value, locale).format(resolved.toArray()); } else { return null; } 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 b30000e334..153c6a602f 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 @@ -223,6 +223,7 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest { // not yet a member Assert.assertFalse(organization.members().getAll().stream().anyMatch(actual -> user.getId().equals(actual.getId()))); // confirm the intent of membership + assertThat(infoPage.getInfo(), containsString("You are about to join organization " + organizationName)); infoPage.clickToContinue(); assertThat(infoPage.getInfo(), containsString("Your account has been updated.")); // now a member diff --git a/themes/src/main/resources/theme/base/login/messages/messages_en.properties b/themes/src/main/resources/theme/base/login/messages/messages_en.properties index d8172f1f31..fe49ca08e2 100755 --- a/themes/src/main/resources/theme/base/login/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/login/messages/messages_en.properties @@ -517,3 +517,5 @@ doLogout=Logout readOnlyUsernameMessage=You can''t update your username as it is read-only. error-invalid-multivalued-size=Attribute {0} must have at least {1} and at most {2} value(s). + +requiredAction.organization.confirm-membership=You are about to join organization ${kc.org.name} \ No newline at end of file