keycloak-uncached
Changes
services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java 4(+4 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java 6(+6 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ContainerInfo.java 13(+11 -2)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java 10(+9 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/RegistrationFlowTest.java 2(+0 -2)
Details
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/JpaKeycloakTransaction.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/JpaKeycloakTransaction.java
index 6ed0052..c8212f4 100755
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/JpaKeycloakTransaction.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/JpaKeycloakTransaction.java
@@ -17,6 +17,7 @@
package org.keycloak.connections.jpa;
+import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakTransaction;
import javax.persistence.EntityManager;
@@ -28,6 +29,8 @@ import javax.persistence.PersistenceException;
*/
public class JpaKeycloakTransaction implements KeycloakTransaction {
+ private static final Logger logger = Logger.getLogger(JpaKeycloakTransaction.class);
+
protected EntityManager em;
public JpaKeycloakTransaction(EntityManager em) {
@@ -42,6 +45,7 @@ public class JpaKeycloakTransaction implements KeycloakTransaction {
@Override
public void commit() {
try {
+ logger.trace("Committing transaction");
em.getTransaction().commit();
} catch (PersistenceException e) {
throw PersistenceExceptionConverter.convert(e.getCause() != null ? e.getCause() : e);
@@ -50,6 +54,7 @@ public class JpaKeycloakTransaction implements KeycloakTransaction {
@Override
public void rollback() {
+ logger.trace("Rollback transaction");
em.getTransaction().rollback();
}
diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
index a51a5a0..d59c7f2 100755
--- a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
+++ b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
@@ -171,6 +171,10 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
attributes.put("login", new LoginBean(formData));
+ if (status != null) {
+ attributes.put("statusCode", status.getStatusCode());
+ }
+
switch (page) {
case LOGIN_CONFIG_TOTP:
attributes.put("totp", new TotpBean(session, realm, user));
diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/model/UrlBean.java b/services/src/main/java/org/keycloak/forms/login/freemarker/model/UrlBean.java
index 0c574c1..4b5052a 100755
--- a/services/src/main/java/org/keycloak/forms/login/freemarker/model/UrlBean.java
+++ b/services/src/main/java/org/keycloak/forms/login/freemarker/model/UrlBean.java
@@ -33,7 +33,7 @@ public class UrlBean {
private String realm;
public UrlBean(RealmModel realm, Theme theme, URI baseURI, URI actionUri) {
- this.realm = realm.getName();
+ this.realm = realm != null ? realm.getName() : null;
this.theme = theme;
this.baseURI = baseURI;
this.actionuri = actionUri;
diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java
index 85ad054..8b081ea 100755
--- a/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java
+++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java
@@ -41,6 +41,8 @@ public class DefaultKeycloakTransactionManager implements KeycloakTransactionMan
private boolean rollback;
private KeycloakSession session;
private JTAPolicy jtaPolicy = JTAPolicy.REQUIRES_NEW;
+ // Used to prevent double committing/rollback if there is an uncaught exception
+ protected boolean completed;
public DefaultKeycloakTransactionManager(KeycloakSession session) {
this.session = session;
@@ -90,6 +92,8 @@ public class DefaultKeycloakTransactionManager implements KeycloakTransactionMan
throw new IllegalStateException("Transaction already active");
}
+ completed = false;
+
if (jtaPolicy == JTAPolicy.REQUIRES_NEW) {
JtaTransactionManagerLookup jtaLookup = session.getProvider(JtaTransactionManagerLookup.class);
if (jtaLookup != null) {
@@ -109,6 +113,12 @@ public class DefaultKeycloakTransactionManager implements KeycloakTransactionMan
@Override
public void commit() {
+ if (completed) {
+ return;
+ } else {
+ completed = true;
+ }
+
RuntimeException exception = null;
for (KeycloakTransaction tx : prepare) {
try {
@@ -156,6 +166,12 @@ public class DefaultKeycloakTransactionManager implements KeycloakTransactionMan
@Override
public void rollback() {
+ if (completed) {
+ return;
+ } else {
+ completed = true;
+ }
+
RuntimeException exception = null;
rollback(exception);
}
diff --git a/services/src/main/java/org/keycloak/services/error/KeycloakErrorHandler.java b/services/src/main/java/org/keycloak/services/error/KeycloakErrorHandler.java
new file mode 100644
index 0000000..fb09223
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/error/KeycloakErrorHandler.java
@@ -0,0 +1,154 @@
+package org.keycloak.services.error;
+
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.spi.Failure;
+import org.jboss.resteasy.spi.HttpResponse;
+import org.jboss.resteasy.spi.ResteasyProviderFactory;
+import org.keycloak.Config;
+import org.keycloak.forms.login.freemarker.model.UrlBean;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakTransaction;
+import org.keycloak.models.KeycloakTransactionManager;
+import org.keycloak.models.RealmModel;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.services.messages.Messages;
+import org.keycloak.services.util.LocaleHelper;
+import org.keycloak.theme.FreeMarkerUtil;
+import org.keycloak.theme.Theme;
+import org.keycloak.theme.ThemeProvider;
+import org.keycloak.theme.beans.MessageBean;
+import org.keycloak.theme.beans.MessageFormatterMethod;
+import org.keycloak.theme.beans.MessageType;
+import org.keycloak.utils.MediaType;
+import org.keycloak.utils.MediaTypeMatcher;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+import java.io.IOException;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@Provider
+public class KeycloakErrorHandler implements ExceptionMapper<Throwable> {
+
+ private static final Logger logger = Logger.getLogger(KeycloakErrorHandler.class);
+
+ private static final Pattern realmNamePattern = Pattern.compile(".*/realms/([^/]+).*");
+
+ @Context
+ private UriInfo uriInfo;
+
+ @Context
+ private KeycloakSession session;
+
+ @Context
+ private HttpHeaders headers;
+
+ @Context
+ private HttpResponse response;
+
+ @Override
+ public Response toResponse(Throwable throwable) {
+ KeycloakTransaction tx = ResteasyProviderFactory.getContextData(KeycloakTransaction.class);
+ tx.setRollbackOnly();
+
+ int statusCode = getStatusCode(throwable);
+
+ if (statusCode >= 500 && statusCode <= 599) {
+ logger.error("Uncaught server error", throwable);
+ }
+
+ if (!MediaTypeMatcher.isHtmlRequest(headers)) {
+ return Response.status(statusCode).build();
+ }
+
+ try {
+ RealmModel realm = resolveRealm();
+
+ ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending");
+ Theme theme = themeProvider.getTheme(realm.getLoginTheme(), Theme.Type.LOGIN);
+
+ Locale locale = LocaleHelper.getLocale(session, realm, null);
+
+ FreeMarkerUtil freeMarker = new FreeMarkerUtil();
+ Map<String, Object> attributes = initAttributes(realm, theme, locale, statusCode);
+
+ String templateName = "error.ftl";
+
+ String content = freeMarker.processTemplate(attributes, templateName, theme);
+ return Response.status(statusCode).type(MediaType.TEXT_HTML_UTF_8_TYPE).entity(content).build();
+ } catch (Throwable t) {
+ logger.error("Failed to create error page", t);
+ return Response.serverError().build();
+ }
+ }
+
+ private int getStatusCode(Throwable throwable) {
+ int status = Response.Status.INTERNAL_SERVER_ERROR.getStatusCode();
+ if (throwable instanceof WebApplicationException) {
+ WebApplicationException ex = (WebApplicationException) throwable;
+ status = ex.getResponse().getStatus();
+ }
+ if (throwable instanceof Failure) {
+ Failure f = (Failure) throwable;
+ status = f.getErrorCode();
+ }
+ return status;
+ }
+
+ private RealmModel resolveRealm() {
+ String path = uriInfo.getPath();
+ Matcher m = realmNamePattern.matcher(path);
+ String realmName;
+ if(m.matches()) {
+ realmName = m.group(1);
+ } else {
+ realmName = Config.getAdminRealm();
+ }
+
+ RealmManager realmManager = new RealmManager(session);
+ RealmModel realm = realmManager.getRealmByName(realmName);
+ if (realm == null) {
+ realm = realmManager.getRealmByName(Config.getAdminRealm());
+ }
+
+ return realm;
+ }
+
+ private Map<String, Object> initAttributes(RealmModel realm, Theme theme, Locale locale, int statusCode) throws IOException {
+ Map<String, Object> attributes = new HashMap<>();
+
+ attributes.put("statusCode", statusCode);
+
+ attributes.put("realm", realm);
+ attributes.put("url", new UrlBean(realm, theme, uriInfo.getBaseUri(), null));
+
+ Properties messagesBundle = theme.getMessages(locale);
+
+ String errorKey = statusCode == 404 ? Messages.PAGE_NOT_FOUND : Messages.INTERNAL_SERVER_ERROR;
+ String errorMessage = messagesBundle.getProperty(errorKey);
+
+ attributes.put("message", new MessageBean(errorMessage, MessageType.ERROR));
+
+ try {
+ attributes.put("msg", new MessageFormatterMethod(locale, theme.getMessages(locale)));
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ try {
+ attributes.put("properties", theme.getProperties());
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ return attributes;
+ }
+
+}
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 35359f4..d99f83c 100755
--- a/services/src/main/java/org/keycloak/services/messages/Messages.java
+++ b/services/src/main/java/org/keycloak/services/messages/Messages.java
@@ -217,4 +217,9 @@ public class Messages {
public static final String DIFFERENT_USER_AUTHENTICATED = "differentUserAuthenticated";
public static final String BROKER_LINKING_SESSION_EXPIRED = "brokerLinkingSessionExpired";
+
+ public static final String PAGE_NOT_FOUND = "pageNotFound";
+
+ public static final String INTERNAL_SERVER_ERROR = "internalServerError";
+
}
diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
index d4e2905..20dcf46 100644
--- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
+++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
@@ -42,6 +42,7 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.DefaultKeycloakSessionFactory;
import org.keycloak.services.ServicesLogger;
+import org.keycloak.services.error.KeycloakErrorHandler;
import org.keycloak.services.filters.KeycloakTransactionCommitter;
import org.keycloak.services.managers.ApplianceBootstrap;
import org.keycloak.services.managers.RealmManager;
@@ -127,6 +128,7 @@ public class KeycloakApplication extends Application {
classes.add(JsResource.class);
classes.add(KeycloakTransactionCommitter.class);
+ classes.add(KeycloakErrorHandler.class);
singletons.add(new ObjectMapperResolver(Boolean.parseBoolean(System.getProperty("keycloak.jsonPrettyPrint", "false"))));
diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
index d7f4967..84ea21f 100755
--- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
@@ -36,6 +36,7 @@ import org.keycloak.services.resource.RealmResourceProvider;
import org.keycloak.services.resources.account.AccountLoader;
import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.services.util.ResolveRelative;
+import org.keycloak.utils.MediaTypeMatcher;
import org.keycloak.utils.ProfileHelper;
import org.keycloak.wellknown.WellKnownProvider;
diff --git a/services/src/main/java/org/keycloak/utils/MediaTypeMatcher.java b/services/src/main/java/org/keycloak/utils/MediaTypeMatcher.java
new file mode 100644
index 0000000..8fde467
--- /dev/null
+++ b/services/src/main/java/org/keycloak/utils/MediaTypeMatcher.java
@@ -0,0 +1,16 @@
+package org.keycloak.utils;
+
+import javax.ws.rs.core.HttpHeaders;
+
+public class MediaTypeMatcher {
+
+ public static boolean isHtmlRequest(HttpHeaders headers) {
+ for (javax.ws.rs.core.MediaType m : headers.getAcceptableMediaTypes()) {
+ if (!m.isWildcardType() && m.isCompatible(javax.ws.rs.core.MediaType.TEXT_HTML_TYPE)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
index 953cb95..d20d09f 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
@@ -690,6 +690,12 @@ public class TestingResourceProvider implements RealmResourceProvider {
return Response.noContent().build();
}
+ @GET
+ @Path("/uncaught-error")
+ public Response uncaughtError() {
+ throw new RuntimeException("Uncaught error");
+ }
+
private void suspendTask(String taskName) {
TimerProvider.TimerTaskContext taskContext = session.getProvider(TimerProvider.class).cancelTask(taskName);
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ContainerInfo.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ContainerInfo.java
index 37abe7b..505c104 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ContainerInfo.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ContainerInfo.java
@@ -1,12 +1,13 @@
package org.keycloak.testsuite.arquillian;
import org.jboss.arquillian.container.spi.Container;
+import org.jboss.arquillian.container.spi.Container.State;
+import org.keycloak.common.util.KeycloakUriBuilder;
+import java.net.URISyntaxException;
import java.net.URL;
import java.util.Map;
import java.util.Objects;
-import java.util.stream.Stream;
-import org.jboss.arquillian.container.spi.Container.State;
/**
*
@@ -41,6 +42,14 @@ public class ContainerInfo {
return contextRoot;
}
+ public KeycloakUriBuilder getUriBuilder() {
+ try {
+ return KeycloakUriBuilder.fromUri(getContextRoot().toURI());
+ } catch (URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
public void setContextRoot(URL contextRoot) {
this.contextRoot = contextRoot;
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java
index ea6fe95..8b551f0 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java
@@ -261,12 +261,20 @@ public interface TestingResource {
@Produces(MediaType.APPLICATION_JSON)
Response suspendPeriodicTasks();
-
@POST
@Path("/restore-periodic-tasks")
@Produces(MediaType.APPLICATION_JSON)
Response restorePeriodicTasks();
+ @GET
+ @Path("/uncaught-error")
+ @Produces(MediaType.TEXT_HTML_UTF_8)
+ Response uncaughtError();
+
+ @GET
+ @Path("/uncaught-error")
+ Response uncaughtErrorJson();
+
@POST
@Path("/run-on-server")
@Consumes(MediaType.TEXT_PLAIN_UTF_8)
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/RegistrationFlowTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/RegistrationFlowTest.java
index ce5f344..3c2889d 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/RegistrationFlowTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/RegistrationFlowTest.java
@@ -55,8 +55,6 @@ public class RegistrationFlowTest extends AbstractAuthenticationTest {
authMgmtResource.addExecution("registration2", data2);
Assert.fail("Not expected to add execution of type 'registration-profile-action' under top flow");
} catch (BadRequestException bre) {
- String errorMessage = bre.getResponse().readEntity(String.class);
- Assert.assertEquals("No authentication provider found for id: registration-profile-action", errorMessage);
}
// Should success to add execution under form flow
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/error/UncaughtErrorPageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/error/UncaughtErrorPageTest.java
new file mode 100644
index 0000000..94e3831
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/error/UncaughtErrorPageTest.java
@@ -0,0 +1,66 @@
+package org.keycloak.testsuite.error;
+
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Test;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.pages.ErrorPage;
+
+import javax.ws.rs.core.Response;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public class UncaughtErrorPageTest extends AbstractKeycloakTest {
+
+ @Page
+ private ErrorPage errorPage;
+
+ @Override
+ public void addTestRealms(List<RealmRepresentation> testRealms) {
+ }
+
+ @Test
+ public void invalidResource() throws MalformedURLException {
+ checkPageNotFound("/auth/nosuch");
+ }
+
+ @Test
+ public void invalidRealm() throws MalformedURLException {
+ checkPageNotFound("/auth/realms/nosuch");
+ }
+
+ @Test
+ public void invalidRealmResource() throws MalformedURLException {
+ checkPageNotFound("/auth/realms/master/nosuch");
+ }
+
+ @Test
+ public void uncaughtErrorJson() {
+ Response response = testingClient.testing().uncaughtError();
+ assertNull(response.getEntity());
+ assertEquals(500, response.getStatus());
+ }
+
+ @Test
+ public void uncaughtError() throws MalformedURLException {
+ URI uri = suiteContext.getAuthServerInfo().getUriBuilder().path("/auth/realms/master/testing/uncaught-error").build();
+ driver.navigate().to(uri.toURL());
+
+ assertTrue(errorPage.isCurrent());
+ assertEquals("An internal server error has occurred", errorPage.getError());
+ }
+
+ private void checkPageNotFound(String path) throws MalformedURLException {
+ URI uri = suiteContext.getAuthServerInfo().getUriBuilder().path(path).build();
+ driver.navigate().to(uri.toURL());
+
+ assertTrue(errorPage.isCurrent());
+ assertEquals("Page not found", errorPage.getError());
+ }
+
+}
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 3cd3b12..56d7a53 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
@@ -260,4 +260,7 @@ p3pPolicy=CP="This is not a P3P policy!"
doX509Login=You will be logged in as\:
clientCertificate=X509 client certificate\:
-noCertificate=[No Certificate]
\ No newline at end of file
+noCertificate=[No Certificate]
+
+pageNotFound=Page not found
+internalServerError=An internal server error has occurred