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.
This commit is contained in:
parent
7f2f4aae67
commit
2dfbbff343
16 changed files with 248 additions and 7 deletions
|
@ -0,0 +1,15 @@
|
|||
package org.keycloak.services.resource;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.theme.Theme;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* <p>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();
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package org.keycloak.services.resource;
|
||||
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
/**
|
||||
* <p>A factory that creates {@link AccountResourceProvider} instances.
|
||||
*/
|
||||
public interface AccountResourceProviderFactory extends ProviderFactory<AccountResourceProvider> {
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package org.keycloak.services.resource;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.Spi;
|
||||
|
||||
/**
|
||||
* <p>A {@link Spi} to replace Account resources.
|
||||
*
|
||||
* <p>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<? extends Provider> getProviderClass() {
|
||||
return AccountResourceProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||
return AccountResourceProviderFactory.class;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
org.keycloak.services.resources.account.AccountConsoleFactory
|
|
@ -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("<html><head><title>Account</title></head><body><h1>Custom Account Console</h1></body></html>").build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Scope config) {}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {}
|
||||
|
||||
@Override
|
||||
public void close() {}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
org.keycloak.testsuite.theme.CustomAccountResourceProviderFactory
|
|
@ -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();
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -12,6 +12,12 @@
|
|||
"types": [
|
||||
"login"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "custom-account-provider",
|
||||
"types": [
|
||||
"account"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
accountResourceProvider=ext-custom-account-console
|
Loading…
Reference in a new issue