keycloak-uncached

Changes

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