KEYCLOAK-1250 Profile and console loader for new account management console
This commit is contained in:
parent
685acb786a
commit
ee35673615
9 changed files with 276 additions and 14 deletions
|
@ -35,13 +35,13 @@ import java.util.Set;
|
||||||
public class Profile {
|
public class Profile {
|
||||||
|
|
||||||
public enum Feature {
|
public enum Feature {
|
||||||
AUTHORIZATION, IMPERSONATION, SCRIPTS, DOCKER
|
AUTHORIZATION, IMPERSONATION, SCRIPTS, DOCKER, ACCOUNT2
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum ProfileValue {
|
private enum ProfileValue {
|
||||||
PRODUCT(Feature.AUTHORIZATION, Feature.SCRIPTS, Feature.DOCKER),
|
PRODUCT(Feature.AUTHORIZATION, Feature.SCRIPTS, Feature.DOCKER, Feature.ACCOUNT2),
|
||||||
PREVIEW,
|
PREVIEW(Feature.ACCOUNT2),
|
||||||
COMMUNITY(Feature.DOCKER);
|
COMMUNITY(Feature.DOCKER, Feature.ACCOUNT2);
|
||||||
|
|
||||||
private List<Feature> disabled;
|
private List<Feature> disabled;
|
||||||
|
|
||||||
|
|
|
@ -62,6 +62,7 @@
|
||||||
<module name="org.bouncycastle" />
|
<module name="org.bouncycastle" />
|
||||||
<module name="javax.api"/>
|
<module name="javax.api"/>
|
||||||
<module name="javax.activation.api"/>
|
<module name="javax.activation.api"/>
|
||||||
|
<module name="javax.json.api"/>
|
||||||
<module name="org.apache.httpcomponents"/>
|
<module name="org.apache.httpcomponents"/>
|
||||||
<module name="org.twitter4j"/>
|
<module name="org.twitter4j"/>
|
||||||
<module name="javax.transaction.api"/>
|
<module name="javax.transaction.api"/>
|
||||||
|
|
6
pom.xml
6
pom.xml
|
@ -81,6 +81,7 @@
|
||||||
<elytron.undertow-server.version>1.0.0.Final</elytron.undertow-server.version>
|
<elytron.undertow-server.version>1.0.0.Final</elytron.undertow-server.version>
|
||||||
<woodstox.version>5.0.3</woodstox.version>
|
<woodstox.version>5.0.3</woodstox.version>
|
||||||
<xmlsec.version>2.0.5</xmlsec.version>
|
<xmlsec.version>2.0.5</xmlsec.version>
|
||||||
|
<glassfish.json.version>1.0.4</glassfish.json.version>
|
||||||
|
|
||||||
<!-- Authorization Drools Policy Provider -->
|
<!-- Authorization Drools Policy Provider -->
|
||||||
<version.org.drools>6.4.0.Final</version.org.drools>
|
<version.org.drools>6.4.0.Final</version.org.drools>
|
||||||
|
@ -414,6 +415,11 @@
|
||||||
<version>${wildfly.version}</version>
|
<version>${wildfly.version}</version>
|
||||||
<type>zip</type>
|
<type>zip</type>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.glassfish</groupId>
|
||||||
|
<artifactId>javax.json</artifactId>
|
||||||
|
<version>${glassfish.json.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- Twitter -->
|
<!-- Twitter -->
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|
|
@ -58,6 +58,10 @@
|
||||||
<groupId>javax.mail</groupId>
|
<groupId>javax.mail</groupId>
|
||||||
<artifactId>javax.mail-api</artifactId>
|
<artifactId>javax.mail-api</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.glassfish</groupId>
|
||||||
|
<artifactId>javax.json</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<artifactId>keycloak-server-spi</artifactId>
|
<artifactId>keycloak-server-spi</artifactId>
|
||||||
|
|
|
@ -209,7 +209,7 @@ public class RealmsResource {
|
||||||
public Object getAccountService(final @PathParam("realm") String name) {
|
public Object getAccountService(final @PathParam("realm") String name) {
|
||||||
RealmModel realm = init(name);
|
RealmModel realm = init(name);
|
||||||
EventBuilder event = new EventBuilder(realm, session, clientConnection);
|
EventBuilder event = new EventBuilder(realm, session, clientConnection);
|
||||||
return AccountLoader.getAccountService(session, event);
|
return new AccountLoader().getAccountService(session, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Path("{realm}")
|
@Path("{realm}")
|
||||||
|
|
|
@ -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<String, Object> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -27,12 +27,16 @@ import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.services.managers.AppAuthManager;
|
import org.keycloak.services.managers.AppAuthManager;
|
||||||
import org.keycloak.services.managers.Auth;
|
import org.keycloak.services.managers.Auth;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
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.HttpMethod;
|
||||||
|
import javax.ws.rs.InternalServerErrorException;
|
||||||
import javax.ws.rs.NotAuthorizedException;
|
import javax.ws.rs.NotAuthorizedException;
|
||||||
import javax.ws.rs.NotFoundException;
|
import javax.ws.rs.NotFoundException;
|
||||||
import javax.ws.rs.core.HttpHeaders;
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -42,10 +46,7 @@ public class AccountLoader {
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(AccountLoader.class);
|
private static final Logger logger = Logger.getLogger(AccountLoader.class);
|
||||||
|
|
||||||
private AccountLoader() {
|
public Object getAccountService(KeycloakSession session, EventBuilder event) {
|
||||||
}
|
|
||||||
|
|
||||||
public static Object getAccountService(KeycloakSession session, EventBuilder event) {
|
|
||||||
RealmModel realm = session.getContext().getRealm();
|
RealmModel realm = session.getContext().getRealm();
|
||||||
|
|
||||||
ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
|
ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
|
||||||
|
@ -59,6 +60,9 @@ public class AccountLoader {
|
||||||
MediaType content = headers.getMediaType();
|
MediaType content = headers.getMediaType();
|
||||||
List<MediaType> accepts = headers.getAcceptableMediaTypes();
|
List<MediaType> accepts = headers.getAcceptableMediaTypes();
|
||||||
|
|
||||||
|
Theme theme = getTheme(session);
|
||||||
|
boolean deprecatedAccount = isDeprecatedFormsAccountConsole(theme);
|
||||||
|
|
||||||
if (request.getHttpMethod().equals(HttpMethod.OPTIONS)) {
|
if (request.getHttpMethod().equals(HttpMethod.OPTIONS)) {
|
||||||
return new CorsPreflightService(request);
|
return new CorsPreflightService(request);
|
||||||
} else if ((accepts.contains(MediaType.APPLICATION_JSON_TYPE) || MediaType.APPLICATION_JSON_TYPE.equals(content)) && !request.getUri().getPath().endsWith("keycloak.json")) {
|
} 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();
|
accountRestService.init();
|
||||||
return accountRestService;
|
return accountRestService;
|
||||||
} else {
|
} else {
|
||||||
AccountFormService accountFormService = new AccountFormService(realm, client, event);
|
if (deprecatedAccount) {
|
||||||
ResteasyProviderFactory.getInstance().injectProperties(accountFormService);
|
AccountFormService accountFormService = new AccountFormService(realm, client, event);
|
||||||
accountFormService.init();
|
ResteasyProviderFactory.getInstance().injectProperties(accountFormService);
|
||||||
return 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.services.resources.admin.info;
|
||||||
import org.keycloak.broker.provider.IdentityProvider;
|
import org.keycloak.broker.provider.IdentityProvider;
|
||||||
import org.keycloak.broker.provider.IdentityProviderFactory;
|
import org.keycloak.broker.provider.IdentityProviderFactory;
|
||||||
import org.keycloak.broker.social.SocialIdentityProvider;
|
import org.keycloak.broker.social.SocialIdentityProvider;
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
import org.keycloak.component.ComponentFactory;
|
import org.keycloak.component.ComponentFactory;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
import org.keycloak.events.admin.OperationType;
|
import org.keycloak.events.admin.OperationType;
|
||||||
|
@ -169,6 +170,10 @@ public class ServerInfoAdminResource {
|
||||||
List<String> themeNames = new LinkedList<>(themeProvider.nameSet(type));
|
List<String> themeNames = new LinkedList<>(themeProvider.nameSet(type));
|
||||||
Collections.sort(themeNames);
|
Collections.sort(themeNames);
|
||||||
|
|
||||||
|
if (!Profile.isFeatureEnabled(Profile.Feature.ACCOUNT2)) {
|
||||||
|
themeNames.remove("keycloak-preview");
|
||||||
|
}
|
||||||
|
|
||||||
List<ThemeInfoRepresentation> themes = new LinkedList<>();
|
List<ThemeInfoRepresentation> themes = new LinkedList<>();
|
||||||
info.getThemes().put(type.toString().toLowerCase(), themes);
|
info.getThemes().put(type.toString().toLowerCase(), themes);
|
||||||
|
|
||||||
|
|
|
@ -5,5 +5,8 @@
|
||||||
}, {
|
}, {
|
||||||
"name" : "keycloak",
|
"name" : "keycloak",
|
||||||
"types": [ "admin", "account", "login", "common", "email", "welcome" ]
|
"types": [ "admin", "account", "login", "common", "email", "welcome" ]
|
||||||
|
}, {
|
||||||
|
"name" : "keycloak-preview",
|
||||||
|
"types": [ "account" ]
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue