mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-17 20:44:50 -06:00
[RLM] Provide a action to notify users by email based on a configurable time
Closes #41788 Signed-off-by: Martin Kanis <mkanis@redhat.com>
This commit is contained in:
@@ -77,6 +77,15 @@ public class ResourcePolicyActionRepresentation {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder before(ResourcePolicyActionRepresentation targetAction, Duration timeBeforeTarget) {
|
||||
// Calculate absolute time: targetAction.after - timeBeforeTarget
|
||||
String targetAfter = targetAction.getConfig().get(AFTER_KEY).get(0);
|
||||
long targetTime = Long.parseLong(targetAfter);
|
||||
long thisTime = targetTime - timeBeforeTarget.toMillis();
|
||||
action.setAfter(thisTime);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withConfig(String key, String value) {
|
||||
action.setConfig(key, value);
|
||||
return this;
|
||||
|
||||
@@ -17,16 +17,27 @@
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.email.EmailException;
|
||||
import org.keycloak.email.EmailTemplateProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
public class NotifyUserActionProvider implements ResourceActionProvider {
|
||||
|
||||
private static final String ACCOUNT_DISABLE_NOTIFICATION_SUBJECT = "accountDisableNotificationSubject";
|
||||
private static final String ACCOUNT_DELETE_NOTIFICATION_SUBJECT = "accountDeleteNotificationSubject";
|
||||
private static final String ACCOUNT_DISABLE_NOTIFICATION_BODY = "accountDisableNotificationBody";
|
||||
private static final String ACCOUNT_DELETE_NOTIFICATION_BODY = "accountDeleteNotificationBody";
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final ComponentModel actionModel;
|
||||
private final Logger log = Logger.getLogger(NotifyUserActionProvider.class);
|
||||
@@ -43,23 +54,142 @@ public class NotifyUserActionProvider implements ResourceActionProvider {
|
||||
@Override
|
||||
public void run(List<String> userIds) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
EmailTemplateProvider emailProvider = session.getProvider(EmailTemplateProvider.class).setRealm(realm);
|
||||
|
||||
String subjectKey = getSubjectKey();
|
||||
String bodyTemplate = getBodyTemplate();
|
||||
Map<String, Object> bodyAttributes = getBodyAttributes();
|
||||
|
||||
for (String id : userIds) {
|
||||
UserModel user = session.users().getUserById(realm, id);
|
||||
|
||||
if (user != null) {
|
||||
log.debugv("Disabling user {0} ({1})", user.getUsername(), user.getId());
|
||||
user.setSingleAttribute(getMessageKey(), getMessage());
|
||||
if (user != null && user.getEmail() != null) {
|
||||
try {
|
||||
emailProvider.setUser(user).send(subjectKey, bodyTemplate, bodyAttributes);
|
||||
log.debugv("Notification email sent to user {0} ({1})", user.getUsername(), user.getEmail());
|
||||
} catch (EmailException e) {
|
||||
log.errorv(e, "Failed to send notification email to user {0} ({1})", user.getUsername(), user.getEmail());
|
||||
}
|
||||
} else if (user != null && user.getEmail() == null) {
|
||||
log.warnv("User {0} has no email address, skipping notification", user.getUsername());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getMessageKey() {
|
||||
return actionModel.getConfig().getFirstOrDefault("message_key", "message");
|
||||
private String getSubjectKey() {
|
||||
String nextActionType = getNextActionType();
|
||||
String customSubjectKey = actionModel.getConfig().getFirst("custom_subject_key");
|
||||
|
||||
if (customSubjectKey != null && !customSubjectKey.trim().isEmpty()) {
|
||||
return customSubjectKey;
|
||||
}
|
||||
|
||||
// Return default subject key based on next action type
|
||||
String defaultSubjectKey = getDefaultSubjectKey(nextActionType);
|
||||
return defaultSubjectKey;
|
||||
}
|
||||
|
||||
private String getMessage() {
|
||||
return actionModel.getConfig().getFirstOrDefault(getMessageKey(), "sent");
|
||||
private String getBodyTemplate() {
|
||||
return "resource-policy-notification.ftl";
|
||||
}
|
||||
|
||||
private Map<String, Object> getBodyAttributes() {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
|
||||
String nextActionType = getNextActionType();
|
||||
|
||||
// Custom message override or default based on action type
|
||||
String customMessage = actionModel.getConfig().getFirst("custom_message");
|
||||
if (customMessage != null && !customMessage.trim().isEmpty()) {
|
||||
attributes.put("messageKey", "customMessage");
|
||||
attributes.put("customMessage", customMessage);
|
||||
} else {
|
||||
attributes.put("messageKey", getDefaultMessageKey(nextActionType));
|
||||
}
|
||||
|
||||
// Calculate days remaining until next action
|
||||
int daysRemaining = calculateDaysUntilNextAction();
|
||||
|
||||
// Message parameters for internationalization
|
||||
attributes.put("daysRemaining", daysRemaining);
|
||||
attributes.put("reason", actionModel.getConfig().getFirstOrDefault("reason", "inactivity"));
|
||||
attributes.put("realmName", realm.getDisplayName() != null ? realm.getDisplayName() : realm.getName());
|
||||
attributes.put("nextActionType", nextActionType);
|
||||
attributes.put("subjectKey", getSubjectKey());
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private String getNextActionType() {
|
||||
ComponentModel nextAction = getNextNonNotificationAction();
|
||||
return nextAction != null ? nextAction.getProviderId() : "unknown-action";
|
||||
}
|
||||
|
||||
private int calculateDaysUntilNextAction() {
|
||||
ComponentModel nextAction = getNextNonNotificationAction();
|
||||
if (nextAction == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
String currentAfter = actionModel.get("after");
|
||||
String nextAfter = nextAction.get("after");
|
||||
|
||||
if (currentAfter == null || nextAfter == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
long currentMillis = Long.parseLong(currentAfter);
|
||||
long nextMillis = Long.parseLong(nextAfter);
|
||||
Duration difference = Duration.ofMillis(nextMillis - currentMillis);
|
||||
return Math.toIntExact(difference.toDays());
|
||||
} catch (NumberFormatException e) {
|
||||
log.warnv("Invalid days format: current={0}, next={1}", currentAfter, nextAfter);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private ComponentModel getNextNonNotificationAction() {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
ComponentModel policyModel = realm.getComponent(actionModel.getParentId());
|
||||
|
||||
List<ComponentModel> actions = realm.getComponentsStream(policyModel.getId(), ResourceActionProvider.class.getName())
|
||||
.sorted((a, b) -> {
|
||||
int priorityA = Integer.parseInt(a.get("priority", "0"));
|
||||
int priorityB = Integer.parseInt(b.get("priority", "0"));
|
||||
return Integer.compare(priorityA, priorityB);
|
||||
})
|
||||
.toList();
|
||||
|
||||
// Find current action and return next non-notification action
|
||||
boolean foundCurrent = false;
|
||||
for (ComponentModel action : actions) {
|
||||
if (foundCurrent && !action.getProviderId().equals("notify-user-action-provider")) {
|
||||
return action;
|
||||
}
|
||||
if (action.getId().equals(actionModel.getId())) {
|
||||
foundCurrent = true;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getDefaultSubjectKey(String actionType) {
|
||||
return switch (actionType) {
|
||||
case "disable-user-action-provider" -> ACCOUNT_DISABLE_NOTIFICATION_SUBJECT;
|
||||
case "delete-user-action-provider" -> ACCOUNT_DELETE_NOTIFICATION_SUBJECT;
|
||||
default -> "accountNotificationSubject";
|
||||
};
|
||||
}
|
||||
|
||||
private String getDefaultMessageKey(String actionType) {
|
||||
return switch (actionType) {
|
||||
case "disable-user-action-provider" -> ACCOUNT_DISABLE_NOTIFICATION_BODY;
|
||||
case "delete-user-action-provider" -> ACCOUNT_DELETE_NOTIFICATION_BODY;
|
||||
default -> "accountNotificationBody";
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.Config;
|
||||
@@ -61,11 +62,23 @@ public class NotifyUserActionProviderFactory implements ResourceActionProviderFa
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "";
|
||||
return "Sends email notifications to users based on configurable templates";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return List.of();
|
||||
return Arrays.asList(
|
||||
new ProviderConfigProperty("reason", "Reason",
|
||||
"Reason for the action (inactivity, policy violation, compliance requirement)",
|
||||
ProviderConfigProperty.STRING_TYPE, ""),
|
||||
|
||||
new ProviderConfigProperty("custom_subject_key", "Custom Subject Message Key",
|
||||
"Override default subject with custom message property key (optional)",
|
||||
ProviderConfigProperty.STRING_TYPE, ""),
|
||||
|
||||
new ProviderConfigProperty("custom_message", "Custom Message",
|
||||
"Override default message with custom text (optional)",
|
||||
ProviderConfigProperty.TEXT_TYPE, "")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,6 @@ package org.keycloak.models.policy;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||
|
||||
public class UserActionBuilder {
|
||||
|
||||
private final ResourceAction action;
|
||||
|
||||
@@ -3,11 +3,12 @@ package org.keycloak.tests.admin.model.policy;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.keycloak.tests.admin.model.policy.ResourcePolicyManagementTest.findEmailByRecipient;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -27,6 +28,8 @@ import org.keycloak.representations.resources.policies.ResourcePolicyRepresentat
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.injection.LifeCycle;
|
||||
import org.keycloak.testframework.mail.MailServer;
|
||||
import org.keycloak.testframework.mail.annotations.InjectMailServer;
|
||||
import org.keycloak.testframework.realm.GroupConfigBuilder;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.realm.UserConfigBuilder;
|
||||
@@ -45,6 +48,9 @@ public class GroupMembershipJoinPolicyTest {
|
||||
@InjectRealm(lifecycle = LifeCycle.METHOD)
|
||||
ManagedRealm managedRealm;
|
||||
|
||||
@InjectMailServer
|
||||
private MailServer mailServer;
|
||||
|
||||
@Test
|
||||
public void testEventsOnGroupMembershipJoin() {
|
||||
String groupId;
|
||||
@@ -77,7 +83,7 @@ public class GroupMembershipJoinPolicyTest {
|
||||
String userId;
|
||||
|
||||
try (Response response = managedRealm.admin().users().create(UserConfigBuilder.create()
|
||||
.username("generic-user").build())) {
|
||||
.username("generic-user").email("generic-user@example.com").build())) {
|
||||
userId = ApiUtil.getCreatedId(response);
|
||||
}
|
||||
|
||||
@@ -88,18 +94,21 @@ public class GroupMembershipJoinPolicyTest {
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
|
||||
UserModel user = session.users().getUserById(realm, userId);
|
||||
assertNull(user.getAttributes().get("message"));
|
||||
|
||||
try {
|
||||
// set offset to 7 days - notify action should run now
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
user = session.users().getUserById(realm, userId);
|
||||
assertNotNull(user.getAttributes().get("message"));
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
}));
|
||||
|
||||
// Verify that the notify action was executed by checking email was sent
|
||||
MimeMessage testUserMessage = findEmailByRecipient(mailServer, "generic-user@example.com");
|
||||
assertNotNull(testUserMessage, "The first action (notify) should have sent an email.");
|
||||
|
||||
mailServer.runCleanup();
|
||||
}
|
||||
|
||||
private static RealmModel configureSessionContext(KeycloakSession session) {
|
||||
|
||||
@@ -19,24 +19,32 @@ package org.keycloak.tests.admin.model.policy;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import org.hamcrest.Matchers;
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.admin.client.resource.RealmResourcePolicies;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.policy.DeleteUserActionProviderFactory;
|
||||
import org.keycloak.models.policy.DisableUserActionProviderFactory;
|
||||
import org.keycloak.models.policy.NotifyUserActionProviderFactory;
|
||||
import org.keycloak.models.policy.ResourceAction;
|
||||
@@ -52,6 +60,8 @@ import org.keycloak.representations.resources.policies.ResourcePolicyConditionRe
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.InjectUser;
|
||||
import org.keycloak.testframework.mail.MailServer;
|
||||
import org.keycloak.testframework.mail.annotations.InjectMailServer;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.injection.LifeCycle;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
@@ -61,6 +71,7 @@ import org.keycloak.testframework.realm.UserConfigBuilder;
|
||||
import org.keycloak.testframework.remote.providers.runonserver.RunOnServer;
|
||||
import org.keycloak.testframework.remote.runonserver.InjectRunOnServer;
|
||||
import org.keycloak.testframework.remote.runonserver.RunOnServerClient;
|
||||
import org.keycloak.tests.utils.MailUtils;
|
||||
|
||||
@KeycloakIntegrationTest(config = RLMServerConfig.class)
|
||||
public class ResourcePolicyManagementTest {
|
||||
@@ -76,6 +87,9 @@ public class ResourcePolicyManagementTest {
|
||||
@InjectUser(ref = "alice", config = DefaultUserConfig.class, lifecycle = LifeCycle.METHOD)
|
||||
private ManagedUser userAlice;
|
||||
|
||||
@InjectMailServer
|
||||
private MailServer mailServer;
|
||||
|
||||
@Test
|
||||
public void testCreate() {
|
||||
List<ResourcePolicyRepresentation> expectedPolicies = ResourcePolicyRepresentation.create()
|
||||
@@ -207,7 +221,7 @@ public class ResourcePolicyManagementTest {
|
||||
).build()).close();
|
||||
|
||||
// create a new user - should bind the user to the policy and setup the first action
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").build());
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").email("testuser@example.com").build());
|
||||
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
@@ -232,9 +246,6 @@ public class ResourcePolicyManagementTest {
|
||||
manager.runScheduledActions();
|
||||
|
||||
user = session.users().getUserById(realm, user.getId());
|
||||
// Verify that ONLY the first action (notify) was executed.
|
||||
assertNotNull(user.getAttributes().get("message"), "The first action (notify) should have run.");
|
||||
assertTrue(user.isEnabled(), "The second action (disable) should NOT have run.");
|
||||
|
||||
// Verify that the next action was scheduled for the user
|
||||
ResourceAction disableAction = manager.getActions(policy).get(1);
|
||||
@@ -245,6 +256,12 @@ public class ResourcePolicyManagementTest {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify that the first action (notify) was executed by checking email was sent
|
||||
MimeMessage testUserMessage = findEmailByRecipient(mailServer, "testuser@example.com");
|
||||
assertNotNull(testUserMessage, "The first action (notify) should have sent an email.");
|
||||
|
||||
mailServer.runCleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -380,7 +397,7 @@ public class ResourcePolicyManagementTest {
|
||||
assertThat(policy.getName(), is("test-policy"));
|
||||
|
||||
// create a new user - should bind the user to the policy and setup the first action
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").build());
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").email("testuser@example.com").build());
|
||||
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
@@ -392,14 +409,18 @@ public class ResourcePolicyManagementTest {
|
||||
manager.runScheduledActions();
|
||||
|
||||
UserModel user = session.users().getUserByUsername(realm, "testuser");
|
||||
// Verify that ONLY the first action (notify) was executed.
|
||||
assertNotNull(user.getAttributes().get("message"), "The first action (notify) should have run.");
|
||||
assertTrue(user.isEnabled(), "The second action (disable) should NOT have run.");
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify that the first action (notify) was executed by checking email was sent
|
||||
MimeMessage testUserMessage = findEmailByRecipient(mailServer, "testuser@example.com");
|
||||
assertNotNull(testUserMessage, "The first action (notify) should have sent an email.");
|
||||
|
||||
mailServer.runCleanup();
|
||||
|
||||
// disable the policy - scheduled actions should be paused and policy should not activate for new users
|
||||
policy.getConfig().putSingle("enabled", "false");
|
||||
managedRealm.admin().resources().policies().policy(policy.getId()).update(policy).close();
|
||||
@@ -440,7 +461,7 @@ public class ResourcePolicyManagementTest {
|
||||
managedRealm.admin().resources().policies().policy(policy.getId()).update(policy).close();
|
||||
|
||||
// create a third user - should bind the user to the policy as it is enabled again
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("thirduser").build());
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("thirduser").email("thirduser@example.com").build());
|
||||
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
@@ -455,14 +476,19 @@ public class ResourcePolicyManagementTest {
|
||||
// Verify that the action was executed as the policy was re-enabled.
|
||||
assertFalse(user.isEnabled(), "The second action (disable) should have run as the policy was re-enabled.");
|
||||
|
||||
// Verify that the third user was bound to the policy and had the first action executed.
|
||||
// Verify that the third user was bound to the policy
|
||||
user = session.users().getUserByUsername(realm, "thirduser");
|
||||
assertNotNull(user.getAttributes().get("message"), "The first action (notify) should have run.");
|
||||
assertTrue(user.isEnabled(), "The second action (disable) should NOT have run");
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify that the first action (notify) was executed by checking email was sent
|
||||
testUserMessage = findEmailByRecipient(mailServer, "thirduser@example.com");
|
||||
assertNotNull(testUserMessage, "The first action (notify) should have sent an email.");
|
||||
|
||||
mailServer.runCleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -478,7 +504,7 @@ public class ResourcePolicyManagementTest {
|
||||
).build()).close();
|
||||
|
||||
// create a new user - should bind the user to the policy and setup the only action in the policy
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").build());
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").email("testuser@example.com").build());
|
||||
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
@@ -489,9 +515,6 @@ public class ResourcePolicyManagementTest {
|
||||
manager.runScheduledActions();
|
||||
|
||||
UserModel user = session.users().getUserByUsername(realm, "testuser");
|
||||
// Verify that the action (notify) was executed.
|
||||
assertNotNull(user.getAttributes().get("message"), "The action (notify) should have run.");
|
||||
user.removeAttribute("message");
|
||||
ResourcePolicy policy = manager.getPolicies().get(0);
|
||||
ResourceAction action = manager.getActions(policy).get(0);
|
||||
|
||||
@@ -503,12 +526,258 @@ public class ResourcePolicyManagementTest {
|
||||
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(12).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
user = session.users().getUserByUsername(realm, "testuser");
|
||||
assertNotNull(user.getAttributes().get("message"), "The action (notify) should have run again.");
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify that there should be two emails sent
|
||||
assertEquals(2, findEmailsByRecipient(mailServer, "testuser@example.com").size());
|
||||
mailServer.runCleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotifyUserActionSendsEmailWithDefaultDisableMessage() {
|
||||
// Create policy: disable at 10 days, notify 3 days before (at day 7)
|
||||
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(7))
|
||||
.withConfig("reason", "inactivity")
|
||||
.build(),
|
||||
ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(10))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").email("test@example.com").name("John", "").build());
|
||||
|
||||
runOnServer.run(session -> {
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
|
||||
try {
|
||||
// Simulate user being 7 days old (eligible for notify action)
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(7).toSeconds()));
|
||||
|
||||
manager.runScheduledActions();
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify email was sent to our test user
|
||||
MimeMessage testUserMessage = findEmailByRecipient(mailServer, "test@example.com");
|
||||
assertNotNull(testUserMessage, "No email found for test@example.com");
|
||||
verifyEmailContent(testUserMessage, "test@example.com", "Disable", "John", "3", "inactivity");
|
||||
|
||||
mailServer.runCleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotifyUserActionSendsEmailWithDefaultDeleteMessage() {
|
||||
// Create policy: delete at 30 days, notify 15 days before (at day 15)
|
||||
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(15))
|
||||
.withConfig("reason", "inactivity")
|
||||
.build(),
|
||||
ResourcePolicyActionRepresentation.create().of(DeleteUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(30))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser2").email("test2@example.com").name("Jane", "").build());
|
||||
|
||||
runOnServer.run(session -> {
|
||||
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
|
||||
try {
|
||||
// Simulate user being 15 days old
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(15).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify email was sent to our test user
|
||||
MimeMessage testUserMessage = findEmailByRecipient(mailServer, "test2@example.com");
|
||||
assertNotNull(testUserMessage, "No email found for test2@example.com");
|
||||
verifyEmailContent(testUserMessage, "test2@example.com", "Deletion", "Jane", "15", "inactivity", "permanently deleted");
|
||||
|
||||
mailServer.runCleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotifyUserActionWithCustomMessageOverride() {
|
||||
// Create policy: disable at 7 days, notify 2 days before (at day 5) with custom message
|
||||
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.withConfig("reason", "compliance requirement")
|
||||
.withConfig("custom_message", "Your account requires immediate attention due to new compliance policies.")
|
||||
.withConfig("custom_subject_key", "customComplianceSubject")
|
||||
.build(),
|
||||
ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(7))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser3").email("test3@example.com").name("Bob", "").build());
|
||||
|
||||
runOnServer.run(session -> {
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
|
||||
try {
|
||||
// Simulate user being 5 days old
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(5).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify email was sent to our test user
|
||||
MimeMessage testUserMessage = findEmailByRecipient(mailServer, "test3@example.com");
|
||||
assertNotNull(testUserMessage, "No email found for test3@example.com");
|
||||
verifyEmailContent(testUserMessage, "test3@example.com", "", "Bob", "2", "immediate attention due to new compliance policies");
|
||||
|
||||
mailServer.runCleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotifyUserActionSkipsUsersWithoutEmailButLogsWarning() {
|
||||
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build(),
|
||||
ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(10))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser4").name("NoEmail", "").build());
|
||||
|
||||
runOnServer.run(session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
|
||||
try {
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(5).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
|
||||
// But should still create state record for the policy flow
|
||||
UserModel user = session.users().getUserByUsername(realm, "testuser4");
|
||||
ResourcePolicyStateProvider stateProvider = session.getProvider(ResourcePolicyStateProvider.class);
|
||||
var scheduledActions = stateProvider.getScheduledActionsByResource(user.getId());
|
||||
assertEquals(1, scheduledActions.size());
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Should NOT send email to user without email address
|
||||
MimeMessage testUserMessage = findEmailByRecipientContaining("testuser4");
|
||||
assertNull(testUserMessage, "No email should be sent to user without email address");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompleteUserLifecycleWithMultipleNotifications() {
|
||||
// Create policy: just disable at 30 days with one notification before
|
||||
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(15))
|
||||
.withConfig("reason", "inactivity")
|
||||
.build(),
|
||||
ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(30))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser5").email("testuser5@example.com").name("TestUser5", "").build());
|
||||
|
||||
runOnServer.run(session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
UserModel user = session.users().getUserByUsername(realm, "testuser5");
|
||||
|
||||
try {
|
||||
// Day 15: First notification - this should run the notify action and schedule the disable action
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(15).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
|
||||
// Check that user is still enabled after notification
|
||||
user = session.users().getUserById(realm, user.getId());
|
||||
assertTrue(user.isEnabled(), "User should still be enabled after notification");
|
||||
|
||||
// Day 30 + 15 minutes: Disable user - run 15 minutes after the scheduled time to ensure it's due
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(30).toSeconds()) + Math.toIntExact(Duration.ofMinutes(15).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
|
||||
// Verify user is disabled
|
||||
user = session.users().getUserById(realm, user.getId());
|
||||
assertNotNull(user, "User should still exist after disable");
|
||||
assertFalse(user.isEnabled(), "User should be disabled");
|
||||
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify notification was sent
|
||||
MimeMessage testUserMessage = findEmailByRecipient(mailServer, "testuser5@example.com");
|
||||
assertNotNull(testUserMessage, "No email found for testuser5@example.com");
|
||||
verifyEmailContent(testUserMessage, "testuser5@example.com", "Disable", "TestUser5", "15", "inactivity");
|
||||
|
||||
mailServer.runCleanup();
|
||||
}
|
||||
|
||||
public static List<MimeMessage> findEmailsByRecipient(MailServer mailServer, String expectedRecipient) {
|
||||
return Arrays.stream(mailServer.getReceivedMessages())
|
||||
.filter(msg -> {
|
||||
try {
|
||||
return MailUtils.getRecipient(msg).equals(expectedRecipient);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
public static MimeMessage findEmailByRecipient(MailServer mailServer, String expectedRecipient) {
|
||||
return Arrays.stream(mailServer.getReceivedMessages())
|
||||
.filter(msg -> {
|
||||
try {
|
||||
return MailUtils.getRecipient(msg).equals(expectedRecipient);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private MimeMessage findEmailByRecipientContaining(String recipientPart) {
|
||||
return Arrays.stream(mailServer.getReceivedMessages())
|
||||
.filter(msg -> {
|
||||
try {
|
||||
return MailUtils.getRecipient(msg).contains(recipientPart);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private static RealmModel configureSessionContext(KeycloakSession session) {
|
||||
@@ -517,6 +786,30 @@ public class ResourcePolicyManagementTest {
|
||||
return realm;
|
||||
}
|
||||
|
||||
public static void verifyEmailContent(MimeMessage message, String expectedRecipient, String subjectContains,
|
||||
String... contentContains) {
|
||||
try {
|
||||
assertEquals(expectedRecipient, MailUtils.getRecipient(message));
|
||||
assertTrue(message.getSubject().contains(subjectContains),
|
||||
"Subject should contain '" + subjectContains + "'");
|
||||
|
||||
MailUtils.EmailBody body = MailUtils.getBody(message);
|
||||
String textContent = body.getText();
|
||||
String htmlContent = body.getHtml();
|
||||
|
||||
for (String expectedContent : contentContains) {
|
||||
boolean foundInText = textContent.contains(expectedContent);
|
||||
boolean foundInHtml = htmlContent.contains(expectedContent);
|
||||
assertTrue(foundInText || foundInHtml,
|
||||
"Email content should contain: " + expectedContent +
|
||||
"\nText: " + textContent +
|
||||
"\nHTML: " + htmlContent);
|
||||
}
|
||||
} catch (MessagingException | IOException e) {
|
||||
fail("Failed to read email message: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultUserConfig implements UserConfig {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.keycloak.tests.admin.model.policy;
|
||||
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
@@ -15,6 +16,8 @@ import org.keycloak.representations.resources.policies.ResourcePolicyActionRepre
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.mail.MailServer;
|
||||
import org.keycloak.testframework.mail.annotations.InjectMailServer;
|
||||
import org.keycloak.testframework.oauth.OAuthClient;
|
||||
import org.keycloak.testframework.oauth.annotations.InjectOAuthClient;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
@@ -32,6 +35,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.keycloak.tests.admin.model.policy.ResourcePolicyManagementTest.findEmailByRecipient;
|
||||
|
||||
@KeycloakIntegrationTest(config = RLMServerConfig.class)
|
||||
public class UserCreationTimePolicyTest {
|
||||
@@ -53,6 +57,9 @@ public class UserCreationTimePolicyTest {
|
||||
@InjectOAuthClient
|
||||
OAuthClient oauth;
|
||||
|
||||
@InjectMailServer
|
||||
private MailServer mailServer;
|
||||
|
||||
@Test
|
||||
public void testDisableUserBasedOnCreationDate() {
|
||||
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
@@ -91,12 +98,17 @@ public class UserCreationTimePolicyTest {
|
||||
manager.runScheduledActions();
|
||||
user = session.users().getUserByUsername(realm, "alice");
|
||||
assertTrue(user.isEnabled());
|
||||
assertNotNull(user.getAttributes().get("message"));
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
}));
|
||||
|
||||
// Verify that the notify action was executed by checking email was sent
|
||||
MimeMessage testUserMessage = findEmailByRecipient(mailServer, "alice@wornderland.org");
|
||||
assertNotNull(testUserMessage, "The first action (notify) should have sent an email.");
|
||||
|
||||
mailServer.runCleanup();
|
||||
|
||||
// logging-in with alice should not reset the policy - we should still run the disable action next
|
||||
oauth.openLoginForm();
|
||||
loginPage.fillLogin("alice", "alice");
|
||||
@@ -114,7 +126,6 @@ public class UserCreationTimePolicyTest {
|
||||
manager.runScheduledActions();
|
||||
UserModel user = session.users().getUserByUsername(realm, "alice");
|
||||
assertFalse(user.isEnabled());
|
||||
assertNotNull(user.getAttributes().get("message"));
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
|
||||
@@ -17,13 +17,18 @@
|
||||
|
||||
package org.keycloak.tests.admin.model.policy;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.keycloak.tests.admin.model.policy.ResourcePolicyManagementTest.findEmailByRecipient;
|
||||
import static org.keycloak.tests.admin.model.policy.ResourcePolicyManagementTest.findEmailsByRecipient;
|
||||
import static org.keycloak.tests.admin.model.policy.ResourcePolicyManagementTest.verifyEmailContent;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.common.util.Time;
|
||||
@@ -42,6 +47,8 @@ import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.InjectUser;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.injection.LifeCycle;
|
||||
import org.keycloak.testframework.mail.MailServer;
|
||||
import org.keycloak.testframework.mail.annotations.InjectMailServer;
|
||||
import org.keycloak.testframework.oauth.OAuthClient;
|
||||
import org.keycloak.testframework.oauth.annotations.InjectOAuthClient;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
@@ -78,9 +85,17 @@ public class UserSessionRefreshTimePolicyTest {
|
||||
@InjectOAuthClient
|
||||
OAuthClient oauth;
|
||||
|
||||
@InjectMailServer
|
||||
private MailServer mailServer;
|
||||
|
||||
@BeforeEach
|
||||
public void onBefore() {
|
||||
oauth.realm("default");
|
||||
|
||||
runOnServer.run(session -> {
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
manager.removePolicies();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -111,13 +126,11 @@ public class UserSessionRefreshTimePolicyTest {
|
||||
|
||||
UserModel user = session.users().getUserByUsername(realm, username);
|
||||
assertTrue(user.isEnabled());
|
||||
assertNull(user.getAttributes().get("message"));
|
||||
|
||||
// running the scheduled tasks now shouldn't pick up any action as none are due to run yet
|
||||
manager.runScheduledActions();
|
||||
user = session.users().getUserByUsername(realm, username);
|
||||
assertTrue(user.isEnabled());
|
||||
assertNull(user.getAttributes().get("message"));
|
||||
|
||||
try {
|
||||
// set offset to 6 days - notify action should run now
|
||||
@@ -125,12 +138,17 @@ public class UserSessionRefreshTimePolicyTest {
|
||||
manager.runScheduledActions();
|
||||
user = session.users().getUserByUsername(realm, username);
|
||||
assertTrue(user.isEnabled());
|
||||
assertNotNull(user.getAttributes().get("message"));
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
}));
|
||||
|
||||
// Verify that the notify action was executed by checking email was sent
|
||||
MimeMessage testUserMessage = findEmailByRecipient(mailServer, "master-admin@email.org");
|
||||
assertNotNull(testUserMessage, "The first action (notify) should have sent an email.");
|
||||
|
||||
mailServer.runCleanup();
|
||||
|
||||
// trigger a login event that should reset the flow of the policy
|
||||
oauth.openLoginForm();
|
||||
|
||||
@@ -170,14 +188,16 @@ public class UserSessionRefreshTimePolicyTest {
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.withConfig("message_key", "notifier1")
|
||||
.withConfig("custom_subject_key", "notifier1_subject")
|
||||
.withConfig("custom_message", "notifier1_message")
|
||||
.build()
|
||||
).of(UserSessionRefreshTimeResourcePolicyProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.LOGIN.toString())
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(10))
|
||||
.withConfig("message_key", "notifier2")
|
||||
.withConfig("custom_subject_key", "notifier2_subject")
|
||||
.withConfig("custom_message", "notifier2_message")
|
||||
.build())
|
||||
.build()).close();
|
||||
|
||||
@@ -195,42 +215,47 @@ public class UserSessionRefreshTimePolicyTest {
|
||||
UserProvider users = session.users();
|
||||
UserModel user = users.getUserByUsername(realm, username);
|
||||
assertTrue(user.isEnabled());
|
||||
assertNull(user.getFirstAttribute("notifier1"));
|
||||
assertNull(user.getFirstAttribute("notifier2"));
|
||||
|
||||
try {
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(7).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
user = users.getUserByUsername(realm, username);
|
||||
assertTrue(user.isEnabled());
|
||||
assertNotNull(user.getFirstAttribute("notifier1"));
|
||||
assertNull(user.getFirstAttribute("notifier2"));
|
||||
user.removeAttribute("notifier1");
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify that the first notify action was executed by checking email was sent
|
||||
List<MimeMessage> testUserMessages = findEmailsByRecipient(mailServer, "master-admin@email.org");
|
||||
// Only one notify message should be sent
|
||||
assertEquals(1, testUserMessages.size());
|
||||
assertNotNull(testUserMessages.get(0), "The first action (notify) should have sent an email.");
|
||||
verifyEmailContent(testUserMessages.get(0), "master-admin@email.org", "notifier1_subject", "notifier1_message");
|
||||
|
||||
mailServer.runCleanup();
|
||||
|
||||
runOnServer.run(session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
|
||||
UserModel user = session.users().getUserByUsername(realm, username);
|
||||
try {
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(11).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
user = users.getUserByUsername(realm, username);
|
||||
user = session.users().getUserByUsername(realm, username);
|
||||
assertTrue(user.isEnabled());
|
||||
assertNotNull(user.getFirstAttribute("notifier2"));
|
||||
assertNull(user.getFirstAttribute("notifier1"));
|
||||
user.removeAttribute("notifier2");
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
|
||||
try {
|
||||
manager.runScheduledActions();
|
||||
assertNull(user.getFirstAttribute("notifier1"));
|
||||
assertNull(user.getFirstAttribute("notifier2"));
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Verify that the second notify action was executed by checking email was sent
|
||||
testUserMessages = findEmailsByRecipient(mailServer, "master-admin@email.org");
|
||||
// Only one notify message should be sent
|
||||
assertEquals(1, testUserMessages.size());
|
||||
assertNotNull(testUserMessages.get(0), "The second action (notify) should have sent an email.");
|
||||
verifyEmailContent(testUserMessages.get(0), "master-admin@email.org", "notifier2_subject", "notifier2_message");
|
||||
}
|
||||
|
||||
private static RealmModel configureSessionContext(KeycloakSession session) {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<#import "template.ftl" as layout>
|
||||
<@layout.emailLayout>
|
||||
<h2>${kcSanitize(msg(subjectKey, daysRemaining, reason))?no_esc}</h2>
|
||||
|
||||
<p>Dear ${user.firstName!user.username},</p>
|
||||
|
||||
<#if messageKey == "customMessage">
|
||||
<p>${kcSanitize(customMessage)?no_esc}</p>
|
||||
<#else>
|
||||
<p>${kcSanitize(msg(messageKey, daysRemaining, reason))?no_esc}</p>
|
||||
</#if>
|
||||
|
||||
<#if daysRemaining gt 0>
|
||||
<p><strong>Time remaining: ${daysRemaining} day<#if daysRemaining != 1>s</#if></strong></p>
|
||||
</#if>
|
||||
|
||||
<p>If you have questions, please contact your ${realmName} administrator.</p>
|
||||
|
||||
<p>
|
||||
Best regards,<br>
|
||||
${realmName} Administration
|
||||
</p>
|
||||
</@layout.emailLayout>
|
||||
@@ -28,6 +28,16 @@ eventRemoveTotpSubject=Remove OTP
|
||||
eventRemoveTotpBody=OTP was removed from your account on {0} from {1}. If this was not you, please contact an administrator.
|
||||
eventRemoveTotpBodyHtml=<p>OTP was removed from your account on {0} from {1}. If this was not you, please contact an administrator.</p>
|
||||
eventUpdatePasswordSubject=Update password
|
||||
|
||||
# Resource Policy Notifications
|
||||
accountNotificationSubject=Account Notification
|
||||
accountNotificationBody=Your account requires attention. Please contact your administrator if you have questions.
|
||||
|
||||
accountDisableNotificationSubject=Account Disable Warning
|
||||
accountDisableNotificationBody=Your account will be disabled in {0} days due to {1}. Please log in to prevent this action.
|
||||
|
||||
accountDeleteNotificationSubject=Account Deletion Notice
|
||||
accountDeleteNotificationBody=Your account will be permanently deleted in {0} days due to {1}. All data will be lost. Please contact your administrator immediately if this is unexpected.
|
||||
eventUpdatePasswordBody=Your password was changed on {0} from {1}. If this was not you, please contact an administrator.
|
||||
eventUpdatePasswordBodyHtml=<p>Your password was changed on {0} from {1}. If this was not you, please contact an administrator.</p>
|
||||
eventUpdateTotpSubject=Update OTP
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
${kcSanitize(msg(subjectKey, daysRemaining, reason))?no_esc}
|
||||
|
||||
Dear ${user.firstName!user.username},
|
||||
|
||||
<#if messageKey == "customMessage">
|
||||
${kcSanitize(customMessage)?no_esc}
|
||||
<#else>
|
||||
${kcSanitize(msg(messageKey, daysRemaining, reason))?no_esc}
|
||||
</#if>
|
||||
|
||||
<#if daysRemaining gt 0>
|
||||
Time remaining: ${daysRemaining} day<#if daysRemaining != 1>s</#if>
|
||||
</#if>
|
||||
|
||||
If you have questions, please contact your ${realmName} administrator.
|
||||
|
||||
Best regards,
|
||||
${realmName} Administration
|
||||
Reference in New Issue
Block a user