Handling events after transaction completion using a separate session

Closes #15656
This commit is contained in:
Pedro Igor 2023-02-08 08:42:34 -03:00 committed by Marek Posolda
parent d4604984d0
commit 9e46b9e43f
3 changed files with 127 additions and 15 deletions

View file

@ -17,7 +17,10 @@
package org.keycloak.events.email;
import static org.keycloak.models.utils.KeycloakModelUtils.runJobInTransaction;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Resteasy;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.events.Event;
@ -25,7 +28,12 @@ import org.keycloak.events.EventListenerProvider;
import org.keycloak.events.EventListenerTransaction;
import org.keycloak.events.EventType;
import org.keycloak.events.admin.AdminEvent;
import org.keycloak.http.HttpRequest;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RealmProvider;
import org.keycloak.models.UserModel;
@ -41,17 +49,16 @@ public class EmailEventListenerProvider implements EventListenerProvider {
private KeycloakSession session;
private RealmProvider model;
private EmailTemplateProvider emailTemplateProvider;
private Set<EventType> includedEvents;
private EventListenerTransaction tx = new EventListenerTransaction(null, this::sendEmail);
private final KeycloakSessionFactory sessionFactory;
public EmailEventListenerProvider(KeycloakSession session, EmailTemplateProvider emailTemplateProvider, Set<EventType> includedEvents) {
public EmailEventListenerProvider(KeycloakSession session, Set<EventType> includedEvents) {
this.session = session;
this.model = session.realms();
this.emailTemplateProvider = emailTemplateProvider;
this.includedEvents = includedEvents;
this.session.getTransactionManager().enlistAfterCompletion(tx);
this.sessionFactory = session.getKeycloakSessionFactory();
}
@Override
@ -64,15 +71,37 @@ public class EmailEventListenerProvider implements EventListenerProvider {
}
private void sendEmail(Event event) {
RealmModel realm = model.getRealm(event.getRealmId());
UserModel user = session.users().getUserById(realm, event.getUserId());
if (user != null && user.getEmail() != null && user.isEmailVerified()) {
try {
emailTemplateProvider.setRealm(realm).setUser(user).sendEvent(event);
} catch (EmailException e) {
log.error("Failed to send type mail", e);
HttpRequest request = session.getContext().getHttpRequest();
runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
KeycloakContext context = session.getContext();
RealmModel realm = session.realms().getRealm(event.getRealmId());
context.setRealm(realm);
String clientId = event.getClientId();
if (clientId != null) {
ClientModel client = realm.getClientByClientId(clientId);
context.setClient(client);
}
Resteasy.pushContext(HttpRequest.class, request);
UserModel user = session.users().getUserById(realm, event.getUserId());
if (user != null && user.getEmail() != null && user.isEmailVerified()) {
try {
EmailTemplateProvider emailTemplateProvider = session.getProvider(EmailTemplateProvider.class);
emailTemplateProvider.setRealm(realm).setUser(user).sendEvent(event);
} catch (EmailException e) {
log.error("Failed to send type mail", e);
}
}
}
}
});
}
@Override

View file

@ -40,6 +40,9 @@ import java.util.Set;
public class EmailEventListenerProviderFactory implements EventListenerProviderFactory {
private static final Set<EventType> SUPPORTED_EVENTS = new HashSet<>();
public static final String ID = "email";
static {
Collections.addAll(SUPPORTED_EVENTS, EventType.LOGIN_ERROR, EventType.UPDATE_PASSWORD, EventType.REMOVE_TOTP, EventType.UPDATE_TOTP);
}
@ -48,8 +51,7 @@ public class EmailEventListenerProviderFactory implements EventListenerProviderF
@Override
public EventListenerProvider create(KeycloakSession session) {
EmailTemplateProvider emailTemplateProvider = session.getProvider(EmailTemplateProvider.class);
return new EmailEventListenerProvider(session, emailTemplateProvider, includedEvents);
return new EmailEventListenerProvider(session, includedEvents);
}
@Override
@ -82,7 +84,7 @@ public class EmailEventListenerProviderFactory implements EventListenerProviderF
@Override
public String getId() {
return "email";
return ID;
}
@Override

View file

@ -0,0 +1,81 @@
/*
* Copyright 2023 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* 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.admin.event;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.common.Profile;
import org.keycloak.events.EventType;
import org.keycloak.events.email.EmailEventListenerProviderFactory;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
import org.keycloak.testsuite.console.page.events.LoginEvents;
import org.keycloak.testsuite.util.GreenMailRule;
import org.keycloak.testsuite.util.UserBuilder;
@DisableFeature(value = Profile.Feature.ACCOUNT2, skipRestart = true) // TODO remove this (KEYCLOAK-16228)
public class EmailEventListenerTest extends AbstractEventTest {
@Rule
public GreenMailRule greenMail = new GreenMailRule();
@Before
public void init() {
RealmRepresentation realm = testRealmResource().toRepresentation();
realm.setSmtpServer(suiteContext.getSmtpServer());
testRealmResource().update(realm);
configRep.setEventsEnabled(true);
configRep.setEventsListeners(List.of(EmailEventListenerProviderFactory.ID));
saveConfig();
RealmResource realmResource = testRealmResource();
realmResource.users().create(UserBuilder.create()
.username("alice")
.email("alice@keycloak.org")
.emailVerified(true)
.password("alice").build());
realmResource.clearEvents();
}
@Test
public void eventAttributesTest() {
accountPage.navigateTo();
loginPage.form().login("alice", "invalid");
loginPage.assertCurrent();
assertNotNull(greenMail.getLastReceivedMessage());
}
}