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 javax.ws.rs.core.UriBuilder;
|
||||
import java.net.URI;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class ResolveRelative {
|
||||
|
||||
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("/")) {
|
||||
return url;
|
||||
} else if (rootUrl != null) {
|
||||
return resolveRootUrl(session, rootUrl) + url;
|
||||
return resolveRootUrl(frontendUrl, adminUrl, rootUrl) + url;
|
||||
} 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(String frontendUrl, String adminUrl, String rootUrl) {
|
||||
if (rootUrl != null) {
|
||||
if (rootUrl.equals(Constants.AUTH_BASE_URL_PROP)) {
|
||||
rootUrl = session.getContext().getUri(UrlType.FRONTEND).getBaseUri().toString();
|
||||
rootUrl = frontendUrl;
|
||||
if (rootUrl.endsWith("/")) {
|
||||
rootUrl = rootUrl.substring(0, rootUrl.length() - 1);
|
||||
}
|
||||
} else if (rootUrl.equals(Constants.AUTH_ADMIN_URL_PROP)) {
|
||||
rootUrl = session.getContext().getUri(UrlType.ADMIN).getBaseUri().toString();
|
||||
rootUrl = adminUrl;
|
||||
if (rootUrl.endsWith("/")) {
|
||||
rootUrl = rootUrl.substring(0, rootUrl.length() - 1);
|
||||
}
|
||||
|
|
|
@ -41,8 +41,11 @@ public class DefaultClientValidationProvider implements ClientValidationProvider
|
|||
}
|
||||
|
||||
private void validate(ClientModel client) throws ValidationException {
|
||||
String resolvedRootUrl = ResolveRelative.resolveRootUrl(context.getSession(), client.getRootUrl());
|
||||
String resolvedBaseUrl = ResolveRelative.resolveRelativeUri(context.getSession(), resolvedRootUrl, client.getBaseUrl());
|
||||
// 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 authServerUrl = "https://localhost/auth";
|
||||
|
||||
String resolvedRootUrl = ResolveRelative.resolveRootUrl(authServerUrl, authServerUrl, client.getRootUrl());
|
||||
String resolvedBaseUrl = ResolveRelative.resolveRelativeUri(authServerUrl, authServerUrl, resolvedRootUrl, client.getBaseUrl());
|
||||
|
||||
validateRootUrl(resolvedRootUrl);
|
||||
validateBaseUrl(resolvedBaseUrl);
|
||||
|
|
|
@ -17,26 +17,30 @@
|
|||
|
||||
package org.keycloak.testsuite.model;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.junit.Assert;
|
||||
import org.junit.FixMethodOrder;
|
||||
import org.junit.Test;
|
||||
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.ProtocolMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
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.services.managers.RealmManager;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
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.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,40 +52,68 @@ import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.A
|
|||
public class ImportTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
@Test
|
||||
public void demoDelete() throws Exception {
|
||||
public void demoDelete() {
|
||||
// was having trouble deleting this realm from admin console
|
||||
removeRealm("demo-delete");
|
||||
removeRealm("demo-delete");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void install2() throws Exception {
|
||||
testingClient.server().run(session -> {
|
||||
RealmModel realm = session.realms().getRealmByName("demo");
|
||||
public void install2() {
|
||||
testingClient.server().run(session -> {
|
||||
RealmModel realm = session.realms().getRealmByName("demo");
|
||||
|
||||
Assert.assertEquals(600, realm.getAccessCodeLifespanUserAction());
|
||||
Assert.assertEquals(Constants.DEFAULT_ACCESS_TOKEN_LIFESPAN_FOR_IMPLICIT_FLOW_TIMEOUT, realm.getAccessTokenLifespanForImplicitFlow());
|
||||
Assert.assertEquals(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT, realm.getOfflineSessionIdleTimeout());
|
||||
Assert.assertEquals(1, realm.getRequiredCredentials().size());
|
||||
Assert.assertEquals("password", realm.getRequiredCredentials().get(0).getType());
|
||||
Assert.assertEquals(600, realm.getAccessCodeLifespanUserAction());
|
||||
Assert.assertEquals(Constants.DEFAULT_ACCESS_TOKEN_LIFESPAN_FOR_IMPLICIT_FLOW_TIMEOUT, realm.getAccessTokenLifespanForImplicitFlow());
|
||||
Assert.assertEquals(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT, realm.getOfflineSessionIdleTimeout());
|
||||
Assert.assertEquals(1, realm.getRequiredCredentials().size());
|
||||
Assert.assertEquals("password", realm.getRequiredCredentials().get(0).getType());
|
||||
});
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
testingClient.server().run(session -> {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void verifyRequiredCredentials(List<RequiredCredentialModel> requiredCreds, String expectedType) {
|
||||
|
||||
Assert.assertEquals(1, requiredCreds.size());
|
||||
Assert.assertEquals(expectedType, requiredCreds.get(0).getType());
|
||||
}
|
||||
|
||||
private static void assertGssProtocolMapper(ProtocolMapperModel gssCredentialMapper) {
|
||||
|
||||
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);
|
||||
synchronized (t) {
|
||||
t.start();
|
||||
try {
|
||||
t.wait(10000);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RunOnServerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (err.get() != null) {
|
||||
throw new RunOnServerException(err.get());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -97,4 +129,5 @@ public class ImportTest extends AbstractTestRealmKeycloakTest {
|
|||
testRealm.setId("demo");
|
||||
adminClient.realms().create(testRealm);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"realm": "realm-validation",
|
||||
"clients": [
|
||||
{
|
||||
"name": "my-client",
|
||||
"baseUrl": "/product-portal"
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Reference in a new issue