From ee35673615d5508bb3e492223e2077bb890ddc57 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Thu, 14 Sep 2017 19:53:02 +0200 Subject: [PATCH] KEYCLOAK-1250 Profile and console loader for new account management console --- .../java/org/keycloak/common/Profile.java | 8 +- .../keycloak-services/main/module.xml | 1 + pom.xml | 6 + services/pom.xml | 4 + .../services/resources/RealmsResource.java | 2 +- .../resources/account/AccountConsole.java | 215 ++++++++++++++++++ .../resources/account/AccountLoader.java | 44 +++- .../admin/info/ServerInfoAdminResource.java | 5 + .../META-INF/keycloak-themes.json | 5 +- 9 files changed, 276 insertions(+), 14 deletions(-) create mode 100644 services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java diff --git a/common/src/main/java/org/keycloak/common/Profile.java b/common/src/main/java/org/keycloak/common/Profile.java index 7f97e55b8a..5a64c88890 100755 --- a/common/src/main/java/org/keycloak/common/Profile.java +++ b/common/src/main/java/org/keycloak/common/Profile.java @@ -35,13 +35,13 @@ import java.util.Set; public class Profile { public enum Feature { - AUTHORIZATION, IMPERSONATION, SCRIPTS, DOCKER + AUTHORIZATION, IMPERSONATION, SCRIPTS, DOCKER, ACCOUNT2 } private enum ProfileValue { - PRODUCT(Feature.AUTHORIZATION, Feature.SCRIPTS, Feature.DOCKER), - PREVIEW, - COMMUNITY(Feature.DOCKER); + PRODUCT(Feature.AUTHORIZATION, Feature.SCRIPTS, Feature.DOCKER, Feature.ACCOUNT2), + PREVIEW(Feature.ACCOUNT2), + COMMUNITY(Feature.DOCKER, Feature.ACCOUNT2); private List disabled; diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml index 18e162abd3..e85db0e5b5 100755 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml @@ -62,6 +62,7 @@ + diff --git a/pom.xml b/pom.xml index 375d767146..337c568076 100755 --- a/pom.xml +++ b/pom.xml @@ -81,6 +81,7 @@ 1.0.0.Final 5.0.3 2.0.5 + 1.0.4 6.4.0.Final @@ -414,6 +415,11 @@ ${wildfly.version} zip + + org.glassfish + javax.json + ${glassfish.json.version} + diff --git a/services/pom.xml b/services/pom.xml index 703d48594b..9cd490e922 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -58,6 +58,10 @@ javax.mail javax.mail-api + + org.glassfish + javax.json + org.keycloak keycloak-server-spi diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java index 18a6fd9930..d7f4967e2a 100755 --- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java @@ -209,7 +209,7 @@ public class RealmsResource { public Object getAccountService(final @PathParam("realm") String name) { RealmModel realm = init(name); EventBuilder event = new EventBuilder(realm, session, clientConnection); - return AccountLoader.getAccountService(session, event); + return new AccountLoader().getAccountService(session, event); } @Path("{realm}") 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 new file mode 100644 index 0000000000..20f3e79271 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java @@ -0,0 +1,215 @@ +package org.keycloak.services.resources.account; + +import org.jboss.resteasy.annotations.cache.NoCache; +import org.keycloak.common.Version; +import org.keycloak.models.*; +import org.keycloak.models.Constants; +import org.keycloak.services.Urls; +import org.keycloak.services.managers.ClientManager; +import org.keycloak.theme.BrowserSecurityHeaderSetup; +import org.keycloak.theme.FreeMarkerException; +import org.keycloak.theme.FreeMarkerUtil; +import org.keycloak.theme.Theme; +import org.keycloak.utils.MediaType; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import java.io.IOException; +import java.io.StringWriter; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.json.Json; +import javax.json.JsonObjectBuilder; +import javax.json.JsonWriter; +import org.jboss.logging.Logger; +import org.keycloak.models.ClientModel; +import org.keycloak.protocol.oidc.utils.RedirectUtils; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.services.managers.AppAuthManager; +import org.keycloak.services.managers.Auth; +import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.util.ResolveRelative; +import org.keycloak.services.validation.Validation; + +/** + * Created by st on 29/03/17. + */ +public class AccountConsole { + private static final Logger logger = Logger.getLogger(AccountConsole.class); + + private final Pattern bundleParamPattern = Pattern.compile("(\\{\\s*(\\d+)\\s*\\})"); + + @Context + protected KeycloakSession session; + @Context + protected UriInfo uriInfo; + + private final AppAuthManager authManager; + private final RealmModel realm; + private final ClientModel client; + private final Theme theme; + + private Auth auth; + + public AccountConsole(RealmModel realm, ClientModel client, Theme theme) { + this.realm = realm; + this.client = client; + this.theme = theme; + this.authManager = new AppAuthManager(); + } + + public void init() { + AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm); + if (authResult != null) { + auth = new Auth(realm, authResult.getToken(), authResult.getUser(), client, authResult.getSession(), true); + } + } + + @GET + @NoCache + public Response getMainPage() throws URISyntaxException, IOException, FreeMarkerException { + if (!uriInfo.getRequestUri().getPath().endsWith("/")) { + return Response.status(302).location(uriInfo.getRequestUriBuilder().path("/").build()).build(); + } else { + Map map = new HashMap<>(); + + URI baseUri = uriInfo.getBaseUri(); + + String authUrl = baseUri.toString(); + authUrl = authUrl.substring(0, authUrl.length() - 1); + + map.put("authUrl", authUrl); + map.put("baseUrl", authUrl + "/realms/" + realm.getName() + "/account"); + map.put("realm", realm.getName()); + map.put("resourceUrl", Urls.themeRoot(baseUri) + "/account/" + theme.getName()); + map.put("resourceVersion", Version.RESOURCES_VERSION); + + String[] referrer = getReferrer(); + if (referrer != null) { + map.put("referrer", referrer[0]); + map.put("referrer_uri", referrer[1]); + } + + try { + if (auth != null) { + Locale locale = session.getContext().resolveLocale(auth.getUser()); + map.put("locale", locale.toLanguageTag()); + map.put("msg", messagesToJsonString(theme.getMessages(locale))); + } + } catch (Exception e) { + logger.warn("Failed to load messages", e); + } + + map.put("properties", theme.getProperties()); + + FreeMarkerUtil freeMarkerUtil = new FreeMarkerUtil(); + String result = freeMarkerUtil.processTemplate(map, "index.ftl", theme); + Response.ResponseBuilder builder = Response.status(Response.Status.OK).type(MediaType.TEXT_HTML_UTF_8).language(Locale.ENGLISH).entity(result); + BrowserSecurityHeaderSetup.headers(builder, realm); + return builder.build(); + } + } + + private String messagesToJsonString(Properties props) { + if (props == null) return ""; + + JsonObjectBuilder json = Json.createObjectBuilder(); + for (String prop : props.stringPropertyNames()) { + json.add(prop, convertPropValue(props.getProperty(prop))); + } + + return json.build().toString(); + } + + private String convertPropValue(String propertyValue) { + propertyValue = propertyValue.replace("''", "%27"); + propertyValue = propertyValue.replace("'", "%27"); + propertyValue = propertyValue.replace("\"", "%22"); + + propertyValue = putJavaParamsInNgTranslateFormat(propertyValue); + + return propertyValue; + } + + // Put java resource bundle params in ngx-translate format + // Do you like {0} and {1} ? + // becomes + // Do you like {{param_0}} and {{param_1}} ? + private String putJavaParamsInNgTranslateFormat(String propertyValue) { + Matcher matcher = bundleParamPattern.matcher(propertyValue); + while (matcher.find()) { + propertyValue = propertyValue.replace(matcher.group(1), "{{param_" + matcher.group(2) + "}}"); + } + + return propertyValue; + } + + @GET + @Path("index.html") + public Response getIndexHtmlRedirect() { + return Response.status(302).location(session.getContext().getUri().getRequestUriBuilder().path("../").build()).build(); + } + + @GET + @Path("keycloak.json") + @Produces(MediaType.APPLICATION_JSON) + @NoCache + public ClientManager.InstallationAdapterConfig getConfig() { + ClientModel accountClient = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID); + if (accountClient == null) { + throw new javax.ws.rs.NotFoundException("Account console client not found"); + } + RealmManager realmMgr = new RealmManager(session); + URI baseUri = session.getContext().getUri().getBaseUri(); + return new ClientManager(realmMgr).toInstallationRepresentation(realm, accountClient, baseUri); + } + + // TODO: took this code from elsewhere - refactor + private String[] getReferrer() { + String referrer = uriInfo.getQueryParameters().getFirst("referrer"); + if (referrer == null) { + return null; + } + + String referrerUri = uriInfo.getQueryParameters().getFirst("referrer_uri"); + + ClientModel referrerClient = realm.getClientByClientId(referrer); + if (referrerClient != null) { + if (referrerUri != null) { + referrerUri = RedirectUtils.verifyRedirectUri(uriInfo, referrerUri, realm, referrerClient); + } else { + referrerUri = ResolveRelative.resolveRelativeUri(uriInfo.getRequestUri(), client.getRootUrl(), referrerClient.getBaseUrl()); + } + + if (referrerUri != null) { + String referrerName = referrerClient.getName(); + if (Validation.isBlank(referrerName)) { + referrerName = referrer; + } + return new String[]{referrerName, referrerUri}; + } + } else if (referrerUri != null) { + referrerClient = realm.getClientByClientId(referrer); + if (client != null) { + referrerUri = RedirectUtils.verifyRedirectUri(uriInfo, referrerUri, realm, referrerClient); + + if (referrerUri != null) { + return new String[]{referrer, referrerUri}; + } + } + } + + return null; + } + +} 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 11c3161ef0..9aef0d0a0f 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 @@ -27,12 +27,16 @@ 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.theme.Theme; +import org.keycloak.theme.ThemeProvider; import javax.ws.rs.HttpMethod; +import javax.ws.rs.InternalServerErrorException; import javax.ws.rs.NotAuthorizedException; import javax.ws.rs.NotFoundException; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; +import java.io.IOException; import java.util.List; /** @@ -42,10 +46,7 @@ public class AccountLoader { private static final Logger logger = Logger.getLogger(AccountLoader.class); - private AccountLoader() { - } - - public static Object getAccountService(KeycloakSession session, EventBuilder event) { + public Object getAccountService(KeycloakSession session, EventBuilder event) { RealmModel realm = session.getContext().getRealm(); ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID); @@ -59,6 +60,9 @@ public class AccountLoader { MediaType content = headers.getMediaType(); List accepts = headers.getAcceptableMediaTypes(); + Theme theme = getTheme(session); + boolean deprecatedAccount = isDeprecatedFormsAccountConsole(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)) && !request.getUri().getPath().endsWith("keycloak.json")) { @@ -73,10 +77,34 @@ public class AccountLoader { accountRestService.init(); return accountRestService; } else { - AccountFormService accountFormService = new AccountFormService(realm, client, event); - ResteasyProviderFactory.getInstance().injectProperties(accountFormService); - accountFormService.init(); - return accountFormService; + if (deprecatedAccount) { + AccountFormService accountFormService = new AccountFormService(realm, client, event); + ResteasyProviderFactory.getInstance().injectProperties(accountFormService); + accountFormService.init(); + return accountFormService; + } else { + AccountConsole console = new AccountConsole(realm, client, theme); + ResteasyProviderFactory.getInstance().injectProperties(console); + console.init(); + return console; + } + } + } + + private Theme getTheme(KeycloakSession session) { + try { + ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending"); + return themeProvider.getTheme(session.getContext().getRealm().getAccountTheme(), Theme.Type.ACCOUNT); + } catch (IOException e) { + throw new InternalServerErrorException(e); + } + } + + private boolean isDeprecatedFormsAccountConsole(Theme theme) { + try { + return Boolean.parseBoolean(theme.getProperties().getProperty("deprecatedMode", "true")); + } catch (IOException e) { + throw new InternalServerErrorException(e); } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java index a391c1d445..377697a1d3 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java @@ -20,6 +20,7 @@ package org.keycloak.services.resources.admin.info; import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.broker.provider.IdentityProviderFactory; import org.keycloak.broker.social.SocialIdentityProvider; +import org.keycloak.common.Profile; import org.keycloak.component.ComponentFactory; import org.keycloak.events.EventType; import org.keycloak.events.admin.OperationType; @@ -169,6 +170,10 @@ public class ServerInfoAdminResource { List themeNames = new LinkedList<>(themeProvider.nameSet(type)); Collections.sort(themeNames); + if (!Profile.isFeatureEnabled(Profile.Feature.ACCOUNT2)) { + themeNames.remove("keycloak-preview"); + } + List themes = new LinkedList<>(); info.getThemes().put(type.toString().toLowerCase(), themes); diff --git a/themes/src/main/resources-community/META-INF/keycloak-themes.json b/themes/src/main/resources-community/META-INF/keycloak-themes.json index 12711eae54..71270a3aa0 100755 --- a/themes/src/main/resources-community/META-INF/keycloak-themes.json +++ b/themes/src/main/resources-community/META-INF/keycloak-themes.json @@ -5,5 +5,8 @@ }, { "name" : "keycloak", "types": [ "admin", "account", "login", "common", "email", "welcome" ] + }, { + "name" : "keycloak-preview", + "types": [ "account" ] }] -} \ No newline at end of file +}