KEYCLOAK-12921 Fix NPE in client validation on startup

This commit is contained in:
stianst 2020-02-07 09:34:36 +01:00 committed by Stian Thorgersen
parent dda829710e
commit 0b8adc7874
4 changed files with 100 additions and 44 deletions

View file

@ -22,32 +22,43 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.urls.UrlType; import org.keycloak.urls.UrlType;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
import java.net.URI;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class ResolveRelative { public class ResolveRelative {
public static String resolveRelativeUri(KeycloakSession session, String rootUrl, String url) { public static String resolveRelativeUri(KeycloakSession session, String rootUrl, String url) {
String frontendUrl = session.getContext().getUri(UrlType.FRONTEND).getBaseUri().toString();
String adminUrl = session.getContext().getUri(UrlType.ADMIN).getBaseUri().toString();
return resolveRelativeUri(frontendUrl, adminUrl, rootUrl, url);
}
public static String resolveRelativeUri(String frontendUrl, String adminUrl, String rootUrl, String url) {
if (url == null || !url.startsWith("/")) { if (url == null || !url.startsWith("/")) {
return url; return url;
} else if (rootUrl != null) { } else if (rootUrl != null) {
return resolveRootUrl(session, rootUrl) + url; return resolveRootUrl(frontendUrl, adminUrl, rootUrl) + url;
} else { } else {
return session.getContext().getUri().getBaseUriBuilder().replacePath(url).build().toString(); return UriBuilder.fromUri(frontendUrl).replacePath(url).build().toString();
} }
} }
public static String resolveRootUrl(KeycloakSession session, String rootUrl) {
String frontendUrl = session.getContext().getUri(UrlType.FRONTEND).getBaseUri().toString();
String adminUrl = session.getContext().getUri(UrlType.ADMIN).getBaseUri().toString();
return resolveRootUrl(frontendUrl, adminUrl, rootUrl);
}
public static String resolveRootUrl(KeycloakSession session, String rootUrl) { public static String resolveRootUrl(String frontendUrl, String adminUrl, String rootUrl) {
if (rootUrl != null) { if (rootUrl != null) {
if (rootUrl.equals(Constants.AUTH_BASE_URL_PROP)) { if (rootUrl.equals(Constants.AUTH_BASE_URL_PROP)) {
rootUrl = session.getContext().getUri(UrlType.FRONTEND).getBaseUri().toString(); rootUrl = frontendUrl;
if (rootUrl.endsWith("/")) { if (rootUrl.endsWith("/")) {
rootUrl = rootUrl.substring(0, rootUrl.length() - 1); rootUrl = rootUrl.substring(0, rootUrl.length() - 1);
} }
} else if (rootUrl.equals(Constants.AUTH_ADMIN_URL_PROP)) { } else if (rootUrl.equals(Constants.AUTH_ADMIN_URL_PROP)) {
rootUrl = session.getContext().getUri(UrlType.ADMIN).getBaseUri().toString(); rootUrl = adminUrl;
if (rootUrl.endsWith("/")) { if (rootUrl.endsWith("/")) {
rootUrl = rootUrl.substring(0, rootUrl.length() - 1); rootUrl = rootUrl.substring(0, rootUrl.length() - 1);
} }

View file

@ -41,8 +41,11 @@ public class DefaultClientValidationProvider implements ClientValidationProvider
} }
private void validate(ClientModel client) throws ValidationException { private void validate(ClientModel client) throws ValidationException {
String resolvedRootUrl = ResolveRelative.resolveRootUrl(context.getSession(), client.getRootUrl()); // Use a fake URL for validating relative URLs as we may not be validating clients in the context of a request (import at startup)
String resolvedBaseUrl = ResolveRelative.resolveRelativeUri(context.getSession(), resolvedRootUrl, client.getBaseUrl()); String authServerUrl = "https://localhost/auth";
String resolvedRootUrl = ResolveRelative.resolveRootUrl(authServerUrl, authServerUrl, client.getRootUrl());
String resolvedBaseUrl = ResolveRelative.resolveRelativeUri(authServerUrl, authServerUrl, resolvedRootUrl, client.getBaseUrl());
validateRootUrl(resolvedRootUrl); validateRootUrl(resolvedRootUrl);
validateBaseUrl(resolvedBaseUrl); validateBaseUrl(resolvedBaseUrl);

View file

@ -17,26 +17,30 @@
package org.keycloak.testsuite.model; package org.keycloak.testsuite.model;
import org.apache.commons.io.IOUtils;
import org.junit.Assert; import org.junit.Assert;
import org.junit.FixMethodOrder; import org.junit.FixMethodOrder;
import org.junit.Test; import org.junit.Test;
import org.junit.runners.MethodSorters; import org.junit.runners.MethodSorters;
import org.keycloak.common.constants.KerberosConstants; import org.keycloak.authorization.policy.evaluation.Realm;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredCredentialModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import java.util.List;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
import org.keycloak.testsuite.runonserver.RunOnServerException;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertTrue;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
/** /**
@ -48,13 +52,13 @@ import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.A
public class ImportTest extends AbstractTestRealmKeycloakTest { public class ImportTest extends AbstractTestRealmKeycloakTest {
@Test @Test
public void demoDelete() throws Exception { public void demoDelete() {
// was having trouble deleting this realm from admin console // was having trouble deleting this realm from admin console
removeRealm("demo-delete"); removeRealm("demo-delete");
} }
@Test @Test
public void install2() throws Exception { public void install2() {
testingClient.server().run(session -> { testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("demo"); RealmModel realm = session.realms().getRealmByName("demo");
@ -66,22 +70,50 @@ public class ImportTest extends AbstractTestRealmKeycloakTest {
}); });
} }
private static void verifyRequiredCredentials(List<RequiredCredentialModel> requiredCreds, String expectedType) { // KEYCLOAK-12921 NPE importing realm with no request context
@Test
public void importWithoutRequestContext() throws IOException {
final String realmString = IOUtils.toString(getClass().getResourceAsStream("/model/realm-validation.json"), StandardCharsets.UTF_8);
Assert.assertEquals(1, requiredCreds.size()); testingClient.server().run(session -> {
Assert.assertEquals(expectedType, requiredCreds.get(0).getType()); RealmRepresentation testRealm = JsonSerialization.readValue(realmString, RealmRepresentation.class);
AtomicReference<Throwable> err = new AtomicReference<>();
// Need a new thread to not get context from thread processing request to run-on-server endpoint
Thread t = new Thread(() -> {
try {
KeycloakSession session2 = session.getKeycloakSessionFactory().create();
session2.getContext().setRealm(session.getContext().getRealm());
session2.getTransactionManager().begin();
RealmModel realmModel = new RealmManager(session2).importRealm(testRealm);
session2.getTransactionManager().commit();
session2.getTransactionManager().begin();
session.realms().removeRealm(realmModel.getId());
session2.getTransactionManager().commit();
session2.close();
} catch (Throwable th) {
err.set(th);
}
});
synchronized (t) {
t.start();
try {
t.wait(10000);
} catch (InterruptedException e) {
throw new RunOnServerException(e);
}
} }
private static void assertGssProtocolMapper(ProtocolMapperModel gssCredentialMapper) { if (err.get() != null) {
throw new RunOnServerException(err.get());
Assert.assertEquals(KerberosConstants.GSS_DELEGATION_CREDENTIAL_DISPLAY_NAME, gssCredentialMapper.getName()); }
Assert.assertEquals( OIDCLoginProtocol.LOGIN_PROTOCOL, gssCredentialMapper.getProtocol()); });
Assert.assertEquals(UserSessionNoteMapper.PROVIDER_ID, gssCredentialMapper.getProtocolMapper());
String includeInAccessToken = gssCredentialMapper.getConfig().get(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN);
String includeInIdToken = gssCredentialMapper.getConfig().get(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN);
Assert.assertTrue(includeInAccessToken.equalsIgnoreCase("true"));
Assert.assertTrue(includeInIdToken == null || Boolean.parseBoolean(includeInIdToken) == false);
} }
@Override @Override
@ -97,4 +129,5 @@ public class ImportTest extends AbstractTestRealmKeycloakTest {
testRealm.setId("demo"); testRealm.setId("demo");
adminClient.realms().create(testRealm); adminClient.realms().create(testRealm);
} }
} }

View file

@ -0,0 +1,9 @@
{
"realm": "realm-validation",
"clients": [
{
"name": "my-client",
"baseUrl": "/product-portal"
}
]
}