Merge pull request #4476 from stianst/ACCOUNT32

KEYCLOAK-1250 Profile and console loader for new account management c…
This commit is contained in:
Stan Silvert 2017-09-14 15:55:27 -04:00 committed by GitHub
commit c597123d1a
9 changed files with 276 additions and 14 deletions

View file

@ -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<Feature> disabled;

View file

@ -62,6 +62,7 @@
<module name="org.bouncycastle" />
<module name="javax.api"/>
<module name="javax.activation.api"/>
<module name="javax.json.api"/>
<module name="org.apache.httpcomponents"/>
<module name="org.twitter4j"/>
<module name="javax.transaction.api"/>

View file

@ -81,6 +81,7 @@
<elytron.undertow-server.version>1.0.0.Final</elytron.undertow-server.version>
<woodstox.version>5.0.3</woodstox.version>
<xmlsec.version>2.0.5</xmlsec.version>
<glassfish.json.version>1.0.4</glassfish.json.version>
<!-- Authorization Drools Policy Provider -->
<version.org.drools>6.4.0.Final</version.org.drools>
@ -414,6 +415,11 @@
<version>${wildfly.version}</version>
<type>zip</type>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.json</artifactId>
<version>${glassfish.json.version}</version>
</dependency>
<!-- Twitter -->
<dependency>

View file

@ -58,6 +58,10 @@
<groupId>javax.mail</groupId>
<artifactId>javax.mail-api</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.json</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi</artifactId>

View file

@ -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}")

View file

@ -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;
}
}

View file

@ -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<MediaType> 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 {
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);
}
}

View file

@ -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<String> themeNames = new LinkedList<>(themeProvider.nameSet(type));
Collections.sort(themeNames);
if (!Profile.isFeatureEnabled(Profile.Feature.ACCOUNT2)) {
themeNames.remove("keycloak-preview");
}
List<ThemeInfoRepresentation> themes = new LinkedList<>();
info.getThemes().put(type.toString().toLowerCase(), themes);

View file

@ -5,5 +5,8 @@
}, {
"name" : "keycloak",
"types": [ "admin", "account", "login", "common", "email", "welcome" ]
}, {
"name" : "keycloak-preview",
"types": [ "account" ]
}]
}