From 2dfbbff3431ae273c522d26536a8c85a29c9f5bc Mon Sep 17 00:00:00 2001 From: Garth <244253+xgp@users.noreply.github.com> Date: Thu, 5 Oct 2023 15:08:01 +0200 Subject: [PATCH] added AccountResource SPI, Provider and ProviderFactory. (#22317) Added AccountResource SPI, Provider and ProviderFactory. updated AccountLoader to load provider(s) and check if it is compatible with the chosen theme. --- .../resource/AccountResourceProvider.java | 15 +++++ .../AccountResourceProviderFactory.java | 9 +++ .../services/resource/AccountResourceSpi.java | 34 ++++++++++++ .../services/org.keycloak.provider.Spi | 1 + .../main/java/org/keycloak/theme/Theme.java | 2 + .../resources/account/AccountConsole.java | 12 +++- .../account/AccountConsoleFactory.java | 55 +++++++++++++++++++ .../resources/account/AccountLoader.java | 18 ++++-- ...es.resource.AccountResourceProviderFactory | 1 + .../CustomAccountResourceProviderFactory.java | 47 ++++++++++++++++ ...es.resource.AccountResourceProviderFactory | 1 + .../account/AccountRestServiceTest.java | 22 ++++++++ .../testsuite/admin/ServerInfoTest.java | 2 +- .../CustomAccountResourceProviderTest.java | 27 +++++++++ .../resources/META-INF/keycloak-themes.json | 8 ++- .../account/theme.properties | 1 + 16 files changed, 248 insertions(+), 7 deletions(-) create mode 100644 server-spi-private/src/main/java/org/keycloak/services/resource/AccountResourceProvider.java create mode 100644 server-spi-private/src/main/java/org/keycloak/services/resource/AccountResourceProviderFactory.java create mode 100644 server-spi-private/src/main/java/org/keycloak/services/resource/AccountResourceSpi.java create mode 100644 services/src/main/java/org/keycloak/services/resources/account/AccountConsoleFactory.java create mode 100644 services/src/main/resources/META-INF/services/org.keycloak.services.resource.AccountResourceProviderFactory create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/theme/CustomAccountResourceProviderFactory.java create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.services.resource.AccountResourceProviderFactory create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/theme/CustomAccountResourceProviderTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/theme/custom-account-provider/account/theme.properties diff --git a/server-spi-private/src/main/java/org/keycloak/services/resource/AccountResourceProvider.java b/server-spi-private/src/main/java/org/keycloak/services/resource/AccountResourceProvider.java new file mode 100644 index 00000000000..8f64d40db85 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/services/resource/AccountResourceProvider.java @@ -0,0 +1,15 @@ +package org.keycloak.services.resource; + +import org.keycloak.provider.Provider; +import org.keycloak.theme.Theme; + +import java.io.IOException; + +/** + *

A {@link AccountResourceProvider} creates JAX-RS resource instances for the Account endpoints, allowing + * an implementor to override the behavior of the entire Account console. + */ +public interface AccountResourceProvider extends Provider { + /** Returns a JAX-RS resource instance. */ + Object getResource(); +} diff --git a/server-spi-private/src/main/java/org/keycloak/services/resource/AccountResourceProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/services/resource/AccountResourceProviderFactory.java new file mode 100644 index 00000000000..8a9c0ce2fde --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/services/resource/AccountResourceProviderFactory.java @@ -0,0 +1,9 @@ +package org.keycloak.services.resource; + +import org.keycloak.provider.ProviderFactory; + +/** + *

A factory that creates {@link AccountResourceProvider} instances. + */ +public interface AccountResourceProviderFactory extends ProviderFactory { +} diff --git a/server-spi-private/src/main/java/org/keycloak/services/resource/AccountResourceSpi.java b/server-spi-private/src/main/java/org/keycloak/services/resource/AccountResourceSpi.java new file mode 100644 index 00000000000..43889cb7a01 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/services/resource/AccountResourceSpi.java @@ -0,0 +1,34 @@ +package org.keycloak.services.resource; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + *

A {@link Spi} to replace Account resources. + * + *

Implementors can use this {@link Spi} to override the behavior of the Account endpoints and resources by + * creating JAX-RS resources that override those served at /account by default. + */ +public class AccountResourceSpi implements Spi { + + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return "account-resource"; + } + + @Override + public Class getProviderClass() { + return AccountResourceProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return AccountResourceProviderFactory.class; + } +} diff --git a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi index 8e9c90c5bea..6c8d5d862e5 100755 --- a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -37,6 +37,7 @@ org.keycloak.exportimport.ImportSpi org.keycloak.timer.TimerSpi org.keycloak.scripting.ScriptingSpi org.keycloak.services.managers.BruteForceProtectorSpi +org.keycloak.services.resource.AccountResourceSpi org.keycloak.services.resource.RealmResourceSPI org.keycloak.sessions.AuthenticationSessionSpi org.keycloak.sessions.StickySessionEncoderSpi diff --git a/server-spi/src/main/java/org/keycloak/theme/Theme.java b/server-spi/src/main/java/org/keycloak/theme/Theme.java index cf2fe18dbf0..e01bdf3be31 100755 --- a/server-spi/src/main/java/org/keycloak/theme/Theme.java +++ b/server-spi/src/main/java/org/keycloak/theme/Theme.java @@ -30,6 +30,8 @@ import java.util.Properties; */ public interface Theme { + public static final String ACCOUNT_RESOURCE_PROVIDER_KEY = "accountResourceProvider"; + enum Type { LOGIN, ACCOUNT, ADMIN, EMAIL, WELCOME, COMMON }; String getName(); diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java b/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java index 0973df958cc..7a18e790f3a 100644 --- a/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java @@ -35,6 +35,7 @@ import org.keycloak.services.Urls; import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.resource.AccountResourceProvider; import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.util.ResolveRelative; import org.keycloak.services.validation.Validation; @@ -49,7 +50,7 @@ import org.keycloak.utils.MediaType; /** * Created by st on 29/03/17. */ -public class AccountConsole { +public class AccountConsole implements AccountResourceProvider { // Used when some other context (ie. IdentityBrokerService) wants to forward error to account management and display it here public static final String ACCOUNT_MGMT_FORWARDED_ERROR_NOTE = "ACCOUNT_MGMT_FORWARDED_ERROR"; @@ -71,6 +72,7 @@ public class AccountConsole { this.client = client; this.theme = theme; this.authManager = new AppAuthManager(); + init(); } public void init() { @@ -80,6 +82,14 @@ public class AccountConsole { } } + @Override + public Object getResource() { + return this; + } + + @Override + public void close() {} + @GET @NoCache public Response getMainPage() throws IOException, FreeMarkerException { diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountConsoleFactory.java b/services/src/main/java/org/keycloak/services/resources/account/AccountConsoleFactory.java new file mode 100644 index 00000000000..2b994cf00fb --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountConsoleFactory.java @@ -0,0 +1,55 @@ +package org.keycloak.services.resources.account; + +import java.io.IOException; +import org.keycloak.Config.Scope; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.services.resource.AccountResourceProvider; +import org.keycloak.services.resource.AccountResourceProviderFactory; +import org.keycloak.theme.Theme; +import jakarta.ws.rs.InternalServerErrorException; +import jakarta.ws.rs.NotFoundException; +import org.keycloak.models.Constants; + +public class AccountConsoleFactory implements AccountResourceProviderFactory { + + @Override + public String getId() { + return "default"; + } + + @Override + public AccountResourceProvider create(KeycloakSession session) { + RealmModel realm = session.getContext().getRealm(); + ClientModel client = getAccountManagementClient(realm); + Theme theme = getTheme(session); + return new AccountConsole(session, client, theme); + } + + @Override + public void init(Scope config) {} + + @Override + public void postInit(KeycloakSessionFactory factory) {} + + @Override + public void close() {} + + static Theme getTheme(KeycloakSession session) { + try { + return session.theme().getTheme(Theme.Type.ACCOUNT); + } catch (IOException e) { + throw new InternalServerErrorException(e); + } + } + + static ClientModel getAccountManagementClient(RealmModel realm) { + ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID); + if (client == null || !client.isEnabled()) { + throw new NotFoundException("account management not enabled"); + } + return client; + } +} diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountLoader.java b/services/src/main/java/org/keycloak/services/resources/account/AccountLoader.java index 9b2557f8474..d247721abea 100644 --- a/services/src/main/java/org/keycloak/services/resources/account/AccountLoader.java +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountLoader.java @@ -29,6 +29,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.resource.AccountResourceProvider; import org.keycloak.services.resources.Cors; import org.keycloak.theme.Theme; @@ -78,14 +79,14 @@ public class AccountLoader { Theme theme = getTheme(session); UriInfo uriInfo = session.getContext().getUri(); + AccountResourceProvider accountResourceProvider = getAccountResourceProvider(theme); + if (request.getHttpMethod().equals(HttpMethod.OPTIONS)) { return new CorsPreflightService(request); } else if ((accepts.contains(MediaType.APPLICATION_JSON_TYPE) || MediaType.APPLICATION_JSON_TYPE.equals(content)) && !uriInfo.getPath().endsWith("keycloak.json")) { return getAccountRestService(client, null); - } else if (Profile.isFeatureEnabled(Profile.Feature.ACCOUNT2) || Profile.isFeatureEnabled(Profile.Feature.ACCOUNT3)) { - AccountConsole console = new AccountConsole(session, client, theme); - console.init(); - return console; + } else if (accountResourceProvider != null) { + return accountResourceProvider.getResource(); } else { throw new NotFoundException(); } @@ -108,6 +109,7 @@ public class AccountLoader { } } + private AccountRestService getAccountRestService(ClientModel client, String versionStr) { AuthenticationManager.AuthResult authResult = new AppAuthManager.BearerTokenAuthenticator(session) .setAudience(client.getClientId()) @@ -147,4 +149,12 @@ public class AccountLoader { return client; } + private AccountResourceProvider getAccountResourceProvider(Theme theme) { + try { + if (theme.getProperties().containsKey(Theme.ACCOUNT_RESOURCE_PROVIDER_KEY)) { + return session.getProvider(AccountResourceProvider.class, theme.getProperties().getProperty(Theme.ACCOUNT_RESOURCE_PROVIDER_KEY)); + } + } catch (IOException ignore) {} + return session.getProvider(AccountResourceProvider.class); + } } diff --git a/services/src/main/resources/META-INF/services/org.keycloak.services.resource.AccountResourceProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.services.resource.AccountResourceProviderFactory new file mode 100644 index 00000000000..bd642ab38f7 --- /dev/null +++ b/services/src/main/resources/META-INF/services/org.keycloak.services.resource.AccountResourceProviderFactory @@ -0,0 +1 @@ +org.keycloak.services.resources.account.AccountConsoleFactory diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/theme/CustomAccountResourceProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/theme/CustomAccountResourceProviderFactory.java new file mode 100644 index 00000000000..003a2123c15 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/theme/CustomAccountResourceProviderFactory.java @@ -0,0 +1,47 @@ +package org.keycloak.testsuite.theme; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.jboss.resteasy.annotations.cache.NoCache; +import org.keycloak.Config.Scope; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.services.resource.AccountResourceProvider; +import org.keycloak.services.resource.AccountResourceProviderFactory; + +public class CustomAccountResourceProviderFactory implements AccountResourceProviderFactory, AccountResourceProvider { + public static final String ID = "ext-custom-account-console"; + + @Override + public String getId() { + return ID; + } + + @Override + public AccountResourceProvider create(KeycloakSession session) { + return this; + } + + @Override + public Object getResource() { + return this; + } + + @GET + @NoCache + @Produces(MediaType.TEXT_HTML) + public Response getMainPage() { + return Response.ok().entity("Account

Custom Account Console

").build(); + } + + @Override + public void init(Scope config) {} + + @Override + public void postInit(KeycloakSessionFactory factory) {} + + @Override + public void close() {} +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.services.resource.AccountResourceProviderFactory b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.services.resource.AccountResourceProviderFactory new file mode 100644 index 00000000000..c2e7da80440 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.services.resource.AccountResourceProviderFactory @@ -0,0 +1 @@ +org.keycloak.testsuite.theme.CustomAccountResourceProviderFactory \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java index c04f7c3fc11..a33c047257c 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java @@ -1614,6 +1614,28 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { } @Test + public void testCustomAccountResourceTheme() throws Exception { + String accountTheme = ""; + try { + RealmRepresentation realmRep = adminClient.realm("test").toRepresentation(); + accountTheme = realmRep.getAccountTheme(); + realmRep.setAccountTheme("custom-account-provider"); + adminClient.realm("test").update(realmRep); + + SimpleHttp.Response response = SimpleHttp.doGet(getAccountUrl(null), httpClient) + .header("Accept", "text/html") + .asResponse(); + assertEquals(200, response.getStatus()); + + String html = response.asString(); + assertTrue(html.contains("Custom Account Console")); + } finally { + RealmRepresentation realmRep = testRealm().toRepresentation(); + realmRep.setAccountTheme(accountTheme); + testRealm().update(realmRep); + } + } + @EnableFeature(Profile.Feature.UPDATE_EMAIL) public void testEmailWhenUpdateEmailEnabled() throws Exception { reconnectAdminClient(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ServerInfoTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ServerInfoTest.java index 1b70feb34a2..2224125effc 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ServerInfoTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ServerInfoTest.java @@ -56,8 +56,8 @@ public class ServerInfoTest extends AbstractKeycloakTest { assertNotNull(info.getProviders().get("authenticator")); assertNotNull(info.getThemes()); - // Not checking account themes for now as old account console is going to be removed soon, which would remove "keycloak" theme. So that is just to avoid another "test to update" when it is removed :) assertNotNull(info.getThemes().get("account")); + Assert.assertNames(info.getThemes().get("account"), "base", "keycloak.v2", "custom-account-provider"); Assert.assertNames(info.getThemes().get("admin"), "base", "keycloak.v2"); Assert.assertNames(info.getThemes().get("email"), "base", "keycloak"); Assert.assertNames(info.getThemes().get("login"), "address", "base", "environment-agnostic", "keycloak"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/theme/CustomAccountResourceProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/theme/CustomAccountResourceProviderTest.java new file mode 100644 index 00000000000..8b2b7d0db2b --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/theme/CustomAccountResourceProviderTest.java @@ -0,0 +1,27 @@ +package org.keycloak.testsuite.theme; + +import java.io.IOException; +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.services.resource.AccountResourceProvider; +import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; +import org.keycloak.testsuite.theme.CustomAccountResourceProviderFactory; +import org.keycloak.theme.Theme; + +public class CustomAccountResourceProviderTest extends AbstractTestRealmKeycloakTest { + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + + } + + @Test + public void testProviderOverride() { + testingClient.server().run(session -> { + AccountResourceProvider arp = session.getProvider(AccountResourceProvider.class, "ext-custom-account-console"); + Assert.assertTrue(arp instanceof CustomAccountResourceProviderFactory); + }); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-themes.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-themes.json index e5d4937bd74..1961f3c3773 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-themes.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-themes.json @@ -12,6 +12,12 @@ "types": [ "login" ] + }, + { + "name": "custom-account-provider", + "types": [ + "account" + ] } ] -} \ No newline at end of file +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/theme/custom-account-provider/account/theme.properties b/testsuite/integration-arquillian/tests/base/src/test/resources/theme/custom-account-provider/account/theme.properties new file mode 100644 index 00000000000..4da7224f785 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/theme/custom-account-provider/account/theme.properties @@ -0,0 +1 @@ +accountResourceProvider=ext-custom-account-console