KEYCLOAK-12921 Fix NPE in client validation on startup
This commit is contained in:
parent
dda829710e
commit
0b8adc7874
4 changed files with 100 additions and 44 deletions
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"realm": "realm-validation",
|
||||||
|
"clients": [
|
||||||
|
{
|
||||||
|
"name": "my-client",
|
||||||
|
"baseUrl": "/product-portal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in a new issue