Show a message when confirming an invitation link

Closes #29794

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor 2024-05-22 18:33:11 -03:00 committed by Alexander Schwartz
parent 4c8abfb61f
commit 2d4d32764c
7 changed files with 32 additions and 6 deletions

View file

@ -25,6 +25,7 @@ import java.util.stream.Stream;
public interface OrganizationModel { public interface OrganizationModel {
String ORGANIZATION_ATTRIBUTE = "kc.org"; String ORGANIZATION_ATTRIBUTE = "kc.org";
String ORGANIZATION_NAME_ATTRIBUTE = "kc.org.name";
String ORGANIZATION_DOMAIN_ATTRIBUTE = "kc.org.domain"; String ORGANIZATION_DOMAIN_ATTRIBUTE = "kc.org.domain";
String BROKER_PUBLIC = "kc.org.broker.public"; String BROKER_PUBLIC = "kc.org.broker.public";

View file

@ -43,8 +43,8 @@ import org.keycloak.services.messages.Messages;
import org.keycloak.sessions.AuthenticationSessionCompoundId; import org.keycloak.sessions.AuthenticationSessionCompoundId;
import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionModel;
import java.util.List;
import java.util.Objects; 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}. * 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<Invi
.setAuthenticationSession(authSession) .setAuthenticationSession(authSession)
.setSuccess(Messages.CONFIRM_EXECUTION_OF_ACTIONS) .setSuccess(Messages.CONFIRM_EXECUTION_OF_ACTIONS)
.setAttribute(Constants.TEMPLATE_ATTR_ACTION_URI, confirmUri) .setAttribute(Constants.TEMPLATE_ATTR_ACTION_URI, confirmUri)
.setAttribute(Constants.TEMPLATE_ATTR_REQUIRED_ACTIONS, List.of(Messages.CONFIRM_ORGANIZATION_MEMBERSHIP))
.setAttribute(OrganizationModel.ORGANIZATION_NAME_ATTRIBUTE, organization.getName())
.createInfoPage(); .createInfoPage();
} }

View file

@ -404,7 +404,9 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
Properties messagesBundle; Properties messagesBundle;
try { try {
messagesBundle = theme.getEnhancedMessages(realm, locale); messagesBundle = theme.getEnhancedMessages(realm, locale);
attributes.put("msg", new MessageFormatterMethod(locale, messagesBundle)); Map<Object, Object> msgParams = new HashMap<>(attributes);
msgParams.putAll(messagesBundle);
attributes.put("msg", new MessageFormatterMethod(locale, msgParams));
attributes.put("advancedMsg", new AdvancedMessageFormatterMethod(locale, messagesBundle)); attributes.put("advancedMsg", new AdvancedMessageFormatterMethod(locale, messagesBundle));
} catch (IOException e) { } catch (IOException e) {
logger.warn("Failed to load messages", e); logger.warn("Failed to load messages", e);
@ -441,7 +443,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
} }
attributes.put("message", wholeMessage); attributes.put("message", wholeMessage);
} else { } else {
attributes.put("message", null); attributes.remove("message");
} }
attributes.put("messagesPerField", messagesPerField); 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("url", new UrlBean(realm, theme, baseUri, this.actionUri));
attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri)); attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri));
attributes.put("auth", new AuthenticationContextBean(context, page)); attributes.put("auth", new AuthenticationContextBean(context, page));
attributes.put(Constants.EXECUTION, execution); setAttribute(Constants.EXECUTION, execution);
if (realm.isInternationalizationEnabled()) { if (realm.isInternationalizationEnabled()) {
UriBuilder b; UriBuilder b;
@ -893,7 +895,11 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
@Override @Override
public LoginFormsProvider setAttribute(String name, Object value) { 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; return this;
} }

View file

@ -320,4 +320,5 @@ public class Messages {
public static final String OAUTH2_DEVICE_VERIFICATION_FAILED_HEADER = "oauth2DeviceVerificationFailedHeader"; public static final String OAUTH2_DEVICE_VERIFICATION_FAILED_HEADER = "oauth2DeviceVerificationFailedHeader";
public static final String OAUTH2_DEVICE_CONSENT_DENIED = "oauth2DeviceConsentDeniedMessage"; public static final String OAUTH2_DEVICE_CONSENT_DENIED = "oauth2DeviceConsentDeniedMessage";
public static final String CONFIRM_ORGANIZATION_MEMBERSHIP = "organization.confirm-membership";
} }

View file

@ -17,6 +17,8 @@
package org.keycloak.theme.beans; package org.keycloak.theme.beans;
import static java.util.Optional.ofNullable;
import freemarker.template.SimpleScalar; import freemarker.template.SimpleScalar;
import freemarker.template.TemplateMethodModelEx; import freemarker.template.TemplateMethodModelEx;
import freemarker.template.TemplateModelException; import freemarker.template.TemplateModelException;
@ -26,6 +28,8 @@ import java.text.MessageFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Properties; import java.util.Properties;
/** /**
@ -40,13 +44,22 @@ public class MessageFormatterMethod implements TemplateMethodModelEx {
this.messages = messages; this.messages = messages;
} }
public MessageFormatterMethod(Locale locale, Map<Object, Object> messages) {
this.locale = locale;
this.messages = new Properties();
this.messages.putAll(ofNullable(messages).orElse(Map.of()));
}
@Override @Override
public Object exec(List list) throws TemplateModelException { public Object exec(List list) throws TemplateModelException {
if (list.size() >= 1) { if (list.size() >= 1) {
// resolve any remaining ${} expressions // resolve any remaining ${} expressions
List<Object> resolved = resolve(list.subList(1, list.size())); List<Object> resolved = resolve(list.subList(1, list.size()));
String key = list.get(0).toString(); 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 { } else {
return null; return null;
} }

View file

@ -223,6 +223,7 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
// not yet a member // not yet a member
Assert.assertFalse(organization.members().getAll().stream().anyMatch(actual -> user.getId().equals(actual.getId()))); Assert.assertFalse(organization.members().getAll().stream().anyMatch(actual -> user.getId().equals(actual.getId())));
// confirm the intent of membership // confirm the intent of membership
assertThat(infoPage.getInfo(), containsString("You are about to join organization " + organizationName));
infoPage.clickToContinue(); infoPage.clickToContinue();
assertThat(infoPage.getInfo(), containsString("Your account has been updated.")); assertThat(infoPage.getInfo(), containsString("Your account has been updated."));
// now a member // now a member

View file

@ -517,3 +517,5 @@ doLogout=Logout
readOnlyUsernameMessage=You can''t update your username as it is read-only. 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). 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}