Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

KEYCLOAK-5924 Add error handler for uncaught errors #4769

Merged
merged 1 commit into from Nov 30, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -17,6 +17,7 @@

package org.keycloak.connections.jpa;

import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakTransaction;

import javax.persistence.EntityManager;
Expand All @@ -28,6 +29,8 @@
*/
public class JpaKeycloakTransaction implements KeycloakTransaction {

private static final Logger logger = Logger.getLogger(JpaKeycloakTransaction.class);

protected EntityManager em;

public JpaKeycloakTransaction(EntityManager em) {
Expand All @@ -42,6 +45,7 @@ public void begin() {
@Override
public void commit() {
try {
logger.trace("Committing transaction");
em.getTransaction().commit();
} catch (PersistenceException e) {
throw PersistenceExceptionConverter.convert(e.getCause() != null ? e.getCause() : e);
Expand All @@ -50,6 +54,7 @@ public void commit() {

@Override
public void rollback() {
logger.trace("Rollback transaction");
em.getTransaction().rollback();
}

Expand Down
Expand Up @@ -171,6 +171,10 @@ protected Response createResponse(LoginFormsPages page) {

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));
Expand Down
Expand Up @@ -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;
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -90,6 +92,8 @@ public void begin() {
throw new IllegalStateException("Transaction already active");
}

completed = false;

if (jtaPolicy == JTAPolicy.REQUIRES_NEW) {
JtaTransactionManagerLookup jtaLookup = session.getProvider(JtaTransactionManagerLookup.class);
if (jtaLookup != null) {
Expand All @@ -109,6 +113,12 @@ public void begin() {

@Override
public void commit() {
if (completed) {
return;
} else {
completed = true;
}

RuntimeException exception = null;
for (KeycloakTransaction tx : prepare) {
try {
Expand Down Expand Up @@ -156,6 +166,12 @@ public void commit() {

@Override
public void rollback() {
if (completed) {
return;
} else {
completed = true;
}

RuntimeException exception = null;
rollback(exception);
}
Expand Down
@@ -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;
}

}
Expand Up @@ -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";

}
Expand Up @@ -42,6 +42,7 @@
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;
Expand Down Expand Up @@ -127,6 +128,7 @@ public KeycloakApplication(@Context ServletContext context, @Context Dispatcher
classes.add(JsResource.class);

classes.add(KeycloakTransactionCommitter.class);
classes.add(KeycloakErrorHandler.class);

singletons.add(new ObjectMapperResolver(Boolean.parseBoolean(System.getProperty("keycloak.jsonPrettyPrint", "false"))));

Expand Down
Expand Up @@ -36,6 +36,7 @@
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;

Expand Down
16 changes: 16 additions & 0 deletions 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;
}

}
Expand Up @@ -690,6 +690,12 @@ public Response suspendPeriodicTasks() {
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);

Expand Down
@@ -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;

/**
*
Expand Down Expand Up @@ -41,6 +42,14 @@ public URL getContextRoot() {
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;
}
Expand Down
Expand Up @@ -261,12 +261,20 @@ public UserRepresentation getUserByUsernameFromFedProviderFactory(@QueryParam("r
@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)
Expand Down
Expand Up @@ -55,8 +55,6 @@ public void testAddExecution() {
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
Expand Down