diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.1.0.Beta1.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.1.0.Beta1.xml index 98eecbd853..5b2c21dd28 100755 --- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.1.0.Beta1.xml +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.1.0.Beta1.xml @@ -23,6 +23,15 @@ + + + + + + + + + @@ -35,9 +44,14 @@ + + + + + \ No newline at end of file diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBObjectToMapMapper.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBObjectToMapMapper.java index cfcf9bb1c2..1de0867b4a 100644 --- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBObjectToMapMapper.java +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBObjectToMapMapper.java @@ -8,7 +8,7 @@ import java.util.HashMap; import java.util.Map; /** - * For now, there is support just for convert to Map + * For now, there is support just for convert to Map * * @author Marek Posolda */ @@ -18,10 +18,10 @@ public class BasicDBObjectToMapMapper implements Mapper { public Map convertObject(MapperContext context) { BasicDBObject dbObjectToConvert = context.getObjectToConvert(); - HashMap result = new HashMap(); + HashMap result = new HashMap(); for (Map.Entry entry : dbObjectToConvert.entrySet()) { String key = entry.getKey(); - String value = (String)entry.getValue(); + Object value = entry.getValue(); if (key.contains(MapMapper.DOT_PLACEHOLDER)) { key = key.replaceAll(MapMapper.DOT_PLACEHOLDER, "."); diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/MapMapper.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/MapMapper.java index 80cf9d7081..988ce34185 100644 --- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/MapMapper.java +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/MapMapper.java @@ -8,7 +8,7 @@ import java.util.Map; import java.util.Set; /** - * For now, we support just convert from Map + * For now, we support just convert from Map * * @author Marek Posolda */ @@ -31,7 +31,7 @@ public class MapMapper implements Mapper { Set entries = objectToConvert.entrySet(); for (Map.Entry entry : entries) { String key = (String)entry.getKey(); - String value = (String)entry.getValue(); + Object value = entry.getValue(); if (key.contains(".")) { key = key.replaceAll("\\.", DOT_PLACEHOLDER); diff --git a/core/src/main/java/org/keycloak/ServiceUrlConstants.java b/core/src/main/java/org/keycloak/ServiceUrlConstants.java index 7ff32542cf..90e71702cc 100755 --- a/core/src/main/java/org/keycloak/ServiceUrlConstants.java +++ b/core/src/main/java/org/keycloak/ServiceUrlConstants.java @@ -13,5 +13,7 @@ public interface ServiceUrlConstants { public static final String TOKEN_SERVICE_DIRECT_GRANT_PATH = "/realms/{realm-name}/protocol/openid-connect/grants/access"; public static final String ACCOUNT_SERVICE_PATH = "/realms/{realm-name}/account"; public static final String REALM_INFO_PATH = "/realms/{realm-name}"; + public static final String CLIENTS_MANAGEMENT_REGISTER_NODE_PATH = "/realms/{realm-name}/clients-managements/register-node"; + public static final String CLIENTS_MANAGEMENT_UNREGISTER_NODE_PATH = "/realms/{realm-name}/clients-managements/unregister-node"; } diff --git a/core/src/main/java/org/keycloak/adapters/AdapterConstants.java b/core/src/main/java/org/keycloak/adapters/AdapterConstants.java index a4f7f51bc4..ddc68aa653 100755 --- a/core/src/main/java/org/keycloak/adapters/AdapterConstants.java +++ b/core/src/main/java/org/keycloak/adapters/AdapterConstants.java @@ -20,8 +20,11 @@ public interface AdapterConstants { String AUTH_DATA_PARAM_NAME = "org.keycloak.json.adapterConfig"; // Attribute passed in codeToToken request from adapter to Keycloak and saved in ClientSession. Contains ID of HttpSession on adapter - public static final String HTTP_SESSION_ID = "http_session_id"; + public static final String APPLICATION_SESSION_STATE = "application_session_state"; // Attribute passed in codeToToken request from adapter to Keycloak and saved in ClientSession. Contains hostname of adapter where HttpSession is served - public static final String HTTP_SESSION_HOST = "http_session_host"; + public static final String APPLICATION_SESSION_HOST = "application_session_host"; + + // Attribute passed in registerNode request for register new application cluster node once he joined cluster + public static final String APPLICATION_CLUSTER_HOST = "application_cluster_host"; } diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java index 7f62fd18bb..778f9f5b34 100755 --- a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java +++ b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java @@ -17,7 +17,8 @@ import org.codehaus.jackson.annotate.JsonPropertyOrder; "connection-pool-size", "allow-any-hostname", "disable-trust-manager", "truststore", "truststore-password", "client-keystore", "client-keystore-password", "client-key-password", - "auth-server-url-for-backend-requests", "always-refresh-token" + "auth-server-url-for-backend-requests", "always-refresh-token", + "register-node-at-startup", "register-node-period" }) public class AdapterConfig extends BaseAdapterConfig { @@ -41,6 +42,10 @@ public class AdapterConfig extends BaseAdapterConfig { protected String authServerUrlForBackendRequests; @JsonProperty("always-refresh-token") protected boolean alwaysRefreshToken = false; + @JsonProperty("register-node-at-startup") + protected boolean registerNodeAtStartup = false; + @JsonProperty("register-node-period") + protected int registerNodePeriod = -1; public boolean isAllowAnyHostname() { return allowAnyHostname; @@ -121,4 +126,20 @@ public class AdapterConfig extends BaseAdapterConfig { public void setAlwaysRefreshToken(boolean alwaysRefreshToken) { this.alwaysRefreshToken = alwaysRefreshToken; } + + public boolean isRegisterNodeAtStartup() { + return registerNodeAtStartup; + } + + public void setRegisterNodeAtStartup(boolean registerNodeAtStartup) { + this.registerNodeAtStartup = registerNodeAtStartup; + } + + public int getRegisterNodePeriod() { + return registerNodePeriod; + } + + public void setRegisterNodePeriod(int registerNodePeriod) { + this.registerNodePeriod = registerNodePeriod; + } } diff --git a/core/src/main/java/org/keycloak/representations/idm/ApplicationRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ApplicationRepresentation.java index 5a3a8fd7ae..faee3c8f8d 100755 --- a/core/src/main/java/org/keycloak/representations/idm/ApplicationRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/ApplicationRepresentation.java @@ -25,7 +25,8 @@ public class ApplicationRepresentation { protected String protocol; protected Map attributes; protected Boolean fullScopeAllowed; - + protected Integer nodeReRegistrationTimeout; + protected Map registeredNodes; public String getId() { return id; @@ -162,4 +163,20 @@ public class ApplicationRepresentation { public void setAttributes(Map attributes) { this.attributes = attributes; } + + public Integer getNodeReRegistrationTimeout() { + return nodeReRegistrationTimeout; + } + + public void setNodeReRegistrationTimeout(Integer nodeReRegistrationTimeout) { + this.nodeReRegistrationTimeout = nodeReRegistrationTimeout; + } + + public Map getRegisteredNodes() { + return registeredNodes; + } + + public void setRegisteredNodes(Map registeredNodes) { + this.registeredNodes = registeredNodes; + } } diff --git a/events/api/src/main/java/org/keycloak/events/EventType.java b/events/api/src/main/java/org/keycloak/events/EventType.java index 917b772aba..859a6f7189 100755 --- a/events/api/src/main/java/org/keycloak/events/EventType.java +++ b/events/api/src/main/java/org/keycloak/events/EventType.java @@ -41,6 +41,8 @@ public enum EventType { SEND_RESET_PASSWORD, SEND_RESET_PASSWORD_ERROR, SOCIAL_LOGIN, - SOCIAL_LOGIN_ERROR + SOCIAL_LOGIN_ERROR, + REGISTER_NODE, + UNREGISTER_NODE } diff --git a/examples/demo-template/customer-app/src/main/java/org/keycloak/example/AdminClient.java b/examples/demo-template/customer-app/src/main/java/org/keycloak/example/AdminClient.java index 611292b131..c1ccc38b48 100755 --- a/examples/demo-template/customer-app/src/main/java/org/keycloak/example/AdminClient.java +++ b/examples/demo-template/customer-app/src/main/java/org/keycloak/example/AdminClient.java @@ -43,7 +43,7 @@ public class AdminClient { HttpClient client = new HttpClientBuilder() .disableTrustManager().build(); try { - HttpGet get = new HttpGet(AdapterUtils.getBaseUrl(req.getRequestURL().toString(), session) + "/auth/admin/realms/demo/roles"); + HttpGet get = new HttpGet(AdapterUtils.getOrigin(req.getRequestURL().toString(), session) + "/auth/admin/realms/demo/roles"); get.addHeader("Authorization", "Bearer " + session.getTokenString()); try { HttpResponse response = client.execute(get); diff --git a/examples/demo-template/customer-app/src/main/java/org/keycloak/example/CustomerDatabaseClient.java b/examples/demo-template/customer-app/src/main/java/org/keycloak/example/CustomerDatabaseClient.java index ec1ed04b46..0cb400f2a4 100755 --- a/examples/demo-template/customer-app/src/main/java/org/keycloak/example/CustomerDatabaseClient.java +++ b/examples/demo-template/customer-app/src/main/java/org/keycloak/example/CustomerDatabaseClient.java @@ -50,7 +50,7 @@ public class CustomerDatabaseClient { HttpClient client = new HttpClientBuilder() .disableTrustManager().build(); try { - HttpGet get = new HttpGet(AdapterUtils.getBaseUrl(req.getRequestURL().toString(), session) + "/database/customers"); + HttpGet get = new HttpGet(AdapterUtils.getOrigin(req.getRequestURL().toString(), session) + "/database/customers"); get.addHeader("Authorization", "Bearer " + session.getTokenString()); try { HttpResponse response = client.execute(get); diff --git a/examples/demo-template/product-app/src/main/java/org/keycloak/example/oauth/ProductDatabaseClient.java b/examples/demo-template/product-app/src/main/java/org/keycloak/example/oauth/ProductDatabaseClient.java index 9557f28e0a..c8e9cf000c 100755 --- a/examples/demo-template/product-app/src/main/java/org/keycloak/example/oauth/ProductDatabaseClient.java +++ b/examples/demo-template/product-app/src/main/java/org/keycloak/example/oauth/ProductDatabaseClient.java @@ -40,7 +40,7 @@ public class ProductDatabaseClient HttpClient client = new HttpClientBuilder() .disableTrustManager().build(); try { - HttpGet get = new HttpGet(AdapterUtils.getBaseUrl(req.getRequestURL().toString(), session) + "/database/products"); + HttpGet get = new HttpGet(AdapterUtils.getOrigin(req.getRequestURL().toString(), session) + "/database/products"); get.addHeader("Authorization", "Bearer " + session.getTokenString()); try { HttpResponse response = client.execute(get); diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java index 69e61aaf20..95e31d77ef 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java @@ -157,6 +157,16 @@ public class AdapterDeploymentContext { return (this.accountUrl != null) ? this.accountUrl : delegate.getAccountUrl(); } + @Override + public String getRegisterNodeUrl() { + return (this.registerNodeUrl != null) ? this.registerNodeUrl : delegate.getRegisterNodeUrl(); + } + + @Override + public String getUnregisterNodeUrl() { + return (this.unregisterNodeUrl != null) ? this.unregisterNodeUrl : delegate.getUnregisterNodeUrl(); + } + @Override public String getResourceName() { return delegate.getResourceName(); @@ -336,6 +346,26 @@ public class AdapterDeploymentContext { public void setAlwaysRefreshToken(boolean alwaysRefreshToken) { delegate.setAlwaysRefreshToken(alwaysRefreshToken); } + + @Override + public int getRegisterNodePeriod() { + return delegate.getRegisterNodePeriod(); + } + + @Override + public void setRegisterNodePeriod(int registerNodePeriod) { + delegate.setRegisterNodePeriod(registerNodePeriod); + } + + @Override + public void setRegisterNodeAtStartup(boolean registerNodeAtStartup) { + delegate.setRegisterNodeAtStartup(registerNodeAtStartup); + } + + @Override + public boolean isRegisterNodeAtStartup() { + return delegate.isRegisterNodeAtStartup(); + } } protected KeycloakUriBuilder getBaseBuilder(HttpFacade facade, String base) { diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterUtils.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterUtils.java index 0048d6aa50..e41a8b5fc0 100644 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterUtils.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterUtils.java @@ -8,7 +8,7 @@ import org.keycloak.util.UriUtils; */ public class AdapterUtils { - public static String getBaseUrl(String browserRequestURL, KeycloakSecurityContext session) { + public static String getOrigin(String browserRequestURL, KeycloakSecurityContext session) { if (session instanceof RefreshableKeycloakSecurityContext) { KeycloakDeployment deployment = ((RefreshableKeycloakSecurityContext)session).getDeployment(); switch (deployment.getRelativeUrls()) { @@ -16,10 +16,9 @@ public class AdapterUtils { // Resolve baseURI from the request return UriUtils.getOrigin(browserRequestURL); case BROWSER_ONLY: + case NEVER: // Resolve baseURI from the codeURL (This is already non-relative and based on our hostname) return UriUtils.getOrigin(deployment.getCodeUrl()); - case NEVER: - return ""; default: return ""; } diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java index c3761126f8..1095b47ce7 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java @@ -31,6 +31,8 @@ public class KeycloakDeployment { protected String refreshUrl; protected KeycloakUriBuilder logoutUrl; protected String accountUrl; + protected String registerNodeUrl; + protected String unregisterNodeUrl; protected String resourceName; protected boolean bearerOnly; @@ -48,6 +50,8 @@ public class KeycloakDeployment { protected String corsAllowedMethods; protected boolean exposeToken; protected boolean alwaysRefreshToken; + protected boolean registerNodeAtStartup; + protected int registerNodePeriod; protected volatile int notBefore; public KeycloakDeployment() { @@ -136,6 +140,8 @@ public class KeycloakDeployment { accountUrl = authUrlBuilder.clone().path(ServiceUrlConstants.ACCOUNT_SERVICE_PATH).build(getRealm()).toString(); realmInfoUrl = authUrlBuilder.clone().path(ServiceUrlConstants.REALM_INFO_PATH).build(getRealm()).toString(); codeUrl = authUrlBuilder.clone().path(ServiceUrlConstants.TOKEN_SERVICE_ACCESS_CODE_PATH).build(getRealm()).toString(); + registerNodeUrl = authUrlBuilder.clone().path(ServiceUrlConstants.CLIENTS_MANAGEMENT_REGISTER_NODE_PATH).build(getRealm()).toString(); + unregisterNodeUrl = authUrlBuilder.clone().path(ServiceUrlConstants.CLIENTS_MANAGEMENT_UNREGISTER_NODE_PATH).build(getRealm()).toString(); } public RelativeUrlsUsed getRelativeUrls() { @@ -166,6 +172,14 @@ public class KeycloakDeployment { return accountUrl; } + public String getRegisterNodeUrl() { + return registerNodeUrl; + } + + public String getUnregisterNodeUrl() { + return unregisterNodeUrl; + } + public void setResourceName(String resourceName) { this.resourceName = resourceName; } @@ -289,4 +303,20 @@ public class KeycloakDeployment { public void setAlwaysRefreshToken(boolean alwaysRefreshToken) { this.alwaysRefreshToken = alwaysRefreshToken; } + + public boolean isRegisterNodeAtStartup() { + return registerNodeAtStartup; + } + + public void setRegisterNodeAtStartup(boolean registerNodeAtStartup) { + this.registerNodeAtStartup = registerNodeAtStartup; + } + + public int getRegisterNodePeriod() { + return registerNodePeriod; + } + + public void setRegisterNodePeriod(int registerNodePeriod) { + this.registerNodePeriod = registerNodePeriod; + } } diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java index 6e5c29fd86..cbb5fef00e 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java @@ -61,11 +61,13 @@ public class KeycloakDeploymentBuilder { deployment.setBearerOnly(adapterConfig.isBearerOnly()); deployment.setAlwaysRefreshToken(adapterConfig.isAlwaysRefreshToken()); + deployment.setRegisterNodeAtStartup(adapterConfig.isRegisterNodeAtStartup()); + deployment.setRegisterNodePeriod(adapterConfig.getRegisterNodePeriod()); if (realmKeyPem == null && adapterConfig.isBearerOnly() && adapterConfig.getAuthServerUrl() == null) { throw new IllegalArgumentException("For bearer auth, you must set the realm-public-key or auth-server-url"); } - if (realmKeyPem == null || !deployment.isBearerOnly()) { + if (realmKeyPem == null || !deployment.isBearerOnly() || deployment.isRegisterNodeAtStartup() || deployment.getRegisterNodePeriod() != -1) { deployment.setClient(new HttpClientBuilder().build(adapterConfig)); } if (adapterConfig.getAuthServerUrl() == null && (!deployment.isBearerOnly() || realmKeyPem == null)) { diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/NodesRegistrationLifecycle.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/NodesRegistrationLifecycle.java new file mode 100644 index 0000000000..b344702812 --- /dev/null +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/NodesRegistrationLifecycle.java @@ -0,0 +1,120 @@ +package org.keycloak.adapters; + +import java.io.IOException; +import java.util.Timer; +import java.util.TimerTask; + +import org.jboss.logging.Logger; +import org.keycloak.enums.RelativeUrlsUsed; +import org.keycloak.util.HostUtils; + +/** + * @author Marek Posolda + */ +public class NodesRegistrationLifecycle { + + private static final Logger log = Logger.getLogger(NodesRegistrationLifecycle.class); + + private final KeycloakDeployment deployment; + private final Timer timer; + + // True if at least one event was successfully sent + private volatile boolean registered = false; + + public NodesRegistrationLifecycle(KeycloakDeployment deployment) { + this.deployment = deployment; + this.timer = new Timer(); + } + + public void start() { + if (!deployment.isRegisterNodeAtStartup() && deployment.getRegisterNodePeriod() <= 0) { + log.info("Skip registration of cluster nodes at startup"); + return; + } + + if (deployment.getRelativeUrls() == RelativeUrlsUsed.ALL_REQUESTS) { + log.warn("Skip registration of cluster nodes at startup as Keycloak node can't be contacted. Make sure to not use relative URI in adapters configuration!"); + return; + } + + if (deployment.isRegisterNodeAtStartup()) { + boolean success = sendRegistrationEvent(); + if (!success) { + throw new IllegalStateException("Failed to register node to keycloak at startup"); + } + } + + if (deployment.getRegisterNodePeriod() > 0) { + addPeriodicListener(); + } + } + + public void stop() { + removePeriodicListener(); + if (registered) { + sendUnregistrationEvent(); + } + } + + protected void addPeriodicListener() { + TimerTask task = new TimerTask() { + + @Override + public void run() { + sendRegistrationEvent(); + } + }; + + long interval = deployment.getRegisterNodePeriod() * 1000; + log.info("Setup of periodic re-registration event sending each " + interval + " ms"); + timer.schedule(task, interval, interval); + } + + protected void removePeriodicListener() { + timer.cancel(); + } + + protected boolean sendRegistrationEvent() { + log.info("Sending registration event right now"); + + String host = HostUtils.getIpAddress(); + try { + ServerRequest.invokeRegisterNode(deployment, host); + log.infof("Node '%s' successfully registered in Keycloak", host); + registered = true; + return true; + } catch (ServerRequest.HttpFailure failure) { + log.error("failed to register node to keycloak"); + log.error("status from server: " + failure.getStatus()); + if (failure.getError() != null) { + log.error(" " + failure.getError()); + } + return false; + } catch (IOException e) { + log.error("failed to register node to keycloak", e); + return false; + } + } + + protected boolean sendUnregistrationEvent() { + log.info("Sending UNregistration event right now"); + + String host = HostUtils.getIpAddress(); + try { + ServerRequest.invokeUnregisterNode(deployment, host); + log.infof("Node '%s' successfully unregistered from Keycloak", host); + return true; + } catch (ServerRequest.HttpFailure failure) { + log.error("failed to unregister node from keycloak"); + log.error("status from server: " + failure.getStatus()); + if (failure.getError() != null) { + log.error(" " + failure.getError()); + } + return false; + } catch (IOException e) { + log.error("failed to unregister node from keycloak", e); + return false; + } + } + +} diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java index b20697d354..438ad63cfe 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java @@ -101,8 +101,8 @@ public class ServerRequest { formparams.add(new BasicNameValuePair(OAuth2Constants.CODE, code)); formparams.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, redirectUri)); if (sessionId != null) { - formparams.add(new BasicNameValuePair(AdapterConstants.HTTP_SESSION_ID, sessionId)); - formparams.add(new BasicNameValuePair(AdapterConstants.HTTP_SESSION_HOST, HostUtils.getIpAddress())); + formparams.add(new BasicNameValuePair(AdapterConstants.APPLICATION_SESSION_STATE, sessionId)); + formparams.add(new BasicNameValuePair(AdapterConstants.APPLICATION_SESSION_HOST, HostUtils.getIpAddress())); } HttpResponse response = null; HttpPost post = new HttpPost(codeUrl); @@ -212,6 +212,46 @@ public class ServerRequest { } } + public static void invokeRegisterNode(KeycloakDeployment deployment, String host) throws HttpFailure, IOException { + String registerNodeUrl = deployment.getRegisterNodeUrl(); + String client_id = deployment.getResourceName(); + Map credentials = deployment.getResourceCredentials(); + HttpClient client = deployment.getClient(); + + invokeClientManagementRequest(client, host, registerNodeUrl, client_id, credentials); + } + + public static void invokeUnregisterNode(KeycloakDeployment deployment, String host) throws HttpFailure, IOException { + String unregisterNodeUrl = deployment.getUnregisterNodeUrl(); + String client_id = deployment.getResourceName(); + Map credentials = deployment.getResourceCredentials(); + HttpClient client = deployment.getClient(); + + invokeClientManagementRequest(client, host, unregisterNodeUrl, client_id, credentials); + } + + public static void invokeClientManagementRequest(HttpClient client, String host, String endpointUrl, String clientId, Map credentials) throws HttpFailure, IOException { + List formparams = new ArrayList(); + formparams.add(new BasicNameValuePair(AdapterConstants.APPLICATION_CLUSTER_HOST, host)); + + HttpPost post = new HttpPost(endpointUrl); + + String clientSecret = credentials.get(CredentialRepresentation.SECRET); + if (clientSecret != null) { + String authorization = BasicAuthHelper.createHeader(clientId, clientSecret); + post.setHeader("Authorization", authorization); + } + + UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); + post.setEntity(form); + HttpResponse response = client.execute(post); + int status = response.getStatusLine().getStatusCode(); + if (status != 204) { + HttpEntity entity = response.getEntity(); + error(status, entity); + } + } + public static void error(int status, HttpEntity entity) throws HttpFailure, IOException { String body = null; if (entity != null) { diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java index 9bf2c42636..ab4fb3f57a 100755 --- a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java +++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java @@ -21,6 +21,7 @@ import org.keycloak.adapters.AuthOutcome; import org.keycloak.adapters.HttpFacade; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeploymentBuilder; +import org.keycloak.adapters.NodesRegistrationLifecycle; import org.keycloak.adapters.PreAuthActionsHandler; import org.keycloak.adapters.RefreshableKeycloakSecurityContext; @@ -46,6 +47,7 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif private static final Logger log = Logger.getLogger(KeycloakAuthenticatorValve.class); protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement(); protected AdapterDeploymentContext deploymentContext; + protected NodesRegistrationLifecycle nodesRegistrationLifecycle; @Override @@ -74,7 +76,11 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif @Override public void lifecycleEvent(LifecycleEvent event) { - if (event.getType() == Lifecycle.AFTER_START_EVENT) init(); + if (event.getType() == Lifecycle.AFTER_START_EVENT) { + init(); + } else if (event.getType() == Lifecycle.BEFORE_STOP_EVENT) { + beforeStop(); + } } private static InputStream getJSONFromServletContext(ServletContext servletContext) { @@ -119,6 +125,13 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif context.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext); AuthenticatedActionsValve actions = new AuthenticatedActionsValve(deploymentContext, getNext(), getContainer(), getController()); setNext(actions); + + nodesRegistrationLifecycle = new NodesRegistrationLifecycle(kd); + nodesRegistrationLifecycle.start(); + } + + protected void beforeStop() { + nodesRegistrationLifecycle.stop(); } @Override diff --git a/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/KeycloakAuthenticatorValve.java b/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/KeycloakAuthenticatorValve.java index 208882c447..de123895f5 100755 --- a/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/KeycloakAuthenticatorValve.java +++ b/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/KeycloakAuthenticatorValve.java @@ -20,6 +20,7 @@ import org.keycloak.adapters.AuthOutcome; import org.keycloak.adapters.HttpFacade; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeploymentBuilder; +import org.keycloak.adapters.NodesRegistrationLifecycle; import org.keycloak.adapters.PreAuthActionsHandler; import org.keycloak.adapters.RefreshableKeycloakSecurityContext; import org.keycloak.adapters.ServerRequest; @@ -47,6 +48,7 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif private final static Logger log = Logger.getLogger(""+KeycloakAuthenticatorValve.class); protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement(); protected AdapterDeploymentContext deploymentContext; + protected NodesRegistrationLifecycle nodesRegistrationLifecycle; @Override public void lifecycleEvent(LifecycleEvent event) { @@ -58,6 +60,8 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif } } else if (Lifecycle.AFTER_START_EVENT.equals(event.getType())) { initInternal(); + } else if (event.getType() == Lifecycle.BEFORE_STOP_EVENT) { + beforeStop(); } } @@ -99,6 +103,13 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif context.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext); AuthenticatedActionsValve actions = new AuthenticatedActionsValve(deploymentContext, getNext(), getContainer(), getObjectName()); setNext(actions); + + nodesRegistrationLifecycle = new NodesRegistrationLifecycle(kd); + nodesRegistrationLifecycle.start(); + } + + protected void beforeStop() { + nodesRegistrationLifecycle.stop(); } private static InputStream getJSONFromServletContext(ServletContext servletContext) { diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java index 4c024562e9..9919dfa834 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java @@ -25,13 +25,18 @@ import io.undertow.server.handlers.form.FormParserFactory; import io.undertow.servlet.ServletExtension; import io.undertow.servlet.api.AuthMethodConfig; import io.undertow.servlet.api.DeploymentInfo; +import io.undertow.servlet.api.InstanceFactory; +import io.undertow.servlet.api.InstanceHandle; +import io.undertow.servlet.api.ListenerInfo; import io.undertow.servlet.api.LoginConfig; import io.undertow.servlet.api.ServletSessionConfig; +import io.undertow.servlet.util.ImmediateInstanceHandle; import org.jboss.logging.Logger; import org.keycloak.adapters.AdapterConstants; import org.keycloak.adapters.AdapterDeploymentContext; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeploymentBuilder; +import org.keycloak.adapters.NodesRegistrationLifecycle; import javax.servlet.ServletContext; import java.io.ByteArrayInputStream; @@ -96,7 +101,7 @@ public class KeycloakServletExtension implements ServletExtension { } log.debug("KeycloakServletException initialization"); InputStream is = getConfigInputStream(servletContext); - KeycloakDeployment deployment = null; + final KeycloakDeployment deployment; if (is == null) { log.warn("No adapter configuration. Keycloak is unconfigured and will deny all requests."); deployment = new KeycloakDeployment(); @@ -143,6 +148,17 @@ public class KeycloakServletExtension implements ServletExtension { ServletSessionConfig cookieConfig = new ServletSessionConfig(); cookieConfig.setPath(deploymentInfo.getContextPath()); deploymentInfo.setServletSessionConfig(cookieConfig); + + deploymentInfo.addListener(new ListenerInfo(UndertowNodesRegistrationLifecycleWrapper.class, new InstanceFactory() { + + @Override + public InstanceHandle createInstance() throws InstantiationException { + NodesRegistrationLifecycle nodesRegistration = new NodesRegistrationLifecycle(deployment); + UndertowNodesRegistrationLifecycleWrapper listener = new UndertowNodesRegistrationLifecycleWrapper(nodesRegistration); + return new ImmediateInstanceHandle(listener); + } + + })); } protected ServletKeycloakAuthMech createAuthenticationMechanism(DeploymentInfo deploymentInfo, AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement userSessionManagement) { diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowNodesRegistrationLifecycleWrapper.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowNodesRegistrationLifecycleWrapper.java new file mode 100644 index 0000000000..5c97f6b7ae --- /dev/null +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowNodesRegistrationLifecycleWrapper.java @@ -0,0 +1,28 @@ +package org.keycloak.adapters.undertow; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +import org.keycloak.adapters.NodesRegistrationLifecycle; + +/** + * @author Marek Posolda + */ +public class UndertowNodesRegistrationLifecycleWrapper implements ServletContextListener { + + private final NodesRegistrationLifecycle delegate; + + public UndertowNodesRegistrationLifecycleWrapper(NodesRegistrationLifecycle delegate) { + this.delegate = delegate; + } + + @Override + public void contextInitialized(ServletContextEvent sce) { + delegate.start(); + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + delegate.stop(); + } +} diff --git a/model/api/src/main/java/org/keycloak/models/ApplicationModel.java b/model/api/src/main/java/org/keycloak/models/ApplicationModel.java index 2433fadf99..14cd081dd2 100755 --- a/model/api/src/main/java/org/keycloak/models/ApplicationModel.java +++ b/model/api/src/main/java/org/keycloak/models/ApplicationModel.java @@ -1,6 +1,7 @@ package org.keycloak.models; import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -37,4 +38,20 @@ public interface ApplicationModel extends RoleContainerModel, ClientModel { boolean isBearerOnly(); void setBearerOnly(boolean only); + int getNodeReRegistrationTimeout(); + + void setNodeReRegistrationTimeout(int timeout); + + Map getRegisteredNodes(); + + /** + * Register node or just update the 'lastReRegistration' time if this node is already registered + * + * @param nodeHost + * @param registrationTime + */ + void registerNode(String nodeHost, int registrationTime); + + void unregisterNode(String nodeHost); + } diff --git a/model/api/src/main/java/org/keycloak/models/entities/ApplicationEntity.java b/model/api/src/main/java/org/keycloak/models/entities/ApplicationEntity.java index 95fefb9a68..c39ede4447 100644 --- a/model/api/src/main/java/org/keycloak/models/entities/ApplicationEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/ApplicationEntity.java @@ -2,6 +2,7 @@ package org.keycloak.models.entities; import java.util.ArrayList; import java.util.List; +import java.util.Map; /** * @author Marek Posolda @@ -12,10 +13,13 @@ public class ApplicationEntity extends ClientEntity { private String managementUrl; private String baseUrl; private boolean bearerOnly; + private int nodeReRegistrationTimeout; // We are using names of defaultRoles (not ids) private List defaultRoles = new ArrayList(); + private Map registeredNodes; + public boolean isSurrogateAuthRequired() { return surrogateAuthRequired; } @@ -55,5 +59,21 @@ public class ApplicationEntity extends ClientEntity { public void setDefaultRoles(List defaultRoles) { this.defaultRoles = defaultRoles; } + + public int getNodeReRegistrationTimeout() { + return nodeReRegistrationTimeout; + } + + public void setNodeReRegistrationTimeout(int nodeReRegistrationTimeout) { + this.nodeReRegistrationTimeout = nodeReRegistrationTimeout; + } + + public Map getRegisteredNodes() { + return registeredNodes; + } + + public void setRegisteredNodes(Map registeredNodes) { + this.registeredNodes = registeredNodes; + } } diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index 0baa697cc9..85331aca8e 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -222,6 +222,7 @@ public class ModelToRepresentation { rep.setSurrogateAuthRequired(applicationModel.isSurrogateAuthRequired()); rep.setBaseUrl(applicationModel.getBaseUrl()); rep.setNotBefore(applicationModel.getNotBefore()); + rep.setNodeReRegistrationTimeout(applicationModel.getNodeReRegistrationTimeout()); Set redirectUris = applicationModel.getRedirectUris(); if (redirectUris != null) { @@ -237,6 +238,10 @@ public class ModelToRepresentation { rep.setDefaultRoles(applicationModel.getDefaultRoles().toArray(new String[0])); } + if (!applicationModel.getRegisteredNodes().isEmpty()) { + rep.setRegisteredNodes(new HashMap(applicationModel.getRegisteredNodes())); + } + return rep; } diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index 2d45360331..31e26c3609 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -374,8 +374,16 @@ public class RepresentationToModel { if (resourceRep.isBearerOnly() != null) applicationModel.setBearerOnly(resourceRep.isBearerOnly()); if (resourceRep.isPublicClient() != null) applicationModel.setPublicClient(resourceRep.isPublicClient()); if (resourceRep.getProtocol() != null) applicationModel.setProtocol(resourceRep.getProtocol()); - if (resourceRep.isFullScopeAllowed() != null) applicationModel.setFullScopeAllowed(resourceRep.isFullScopeAllowed()); - else applicationModel.setFullScopeAllowed(true); + if (resourceRep.isFullScopeAllowed() != null) { + applicationModel.setFullScopeAllowed(resourceRep.isFullScopeAllowed()); + } else { + applicationModel.setFullScopeAllowed(true); + } + if (resourceRep.getNodeReRegistrationTimeout() != null) { + applicationModel.setNodeReRegistrationTimeout(resourceRep.getNodeReRegistrationTimeout()); + } else { + applicationModel.setNodeReRegistrationTimeout(-1); + } applicationModel.updateApplication(); if (resourceRep.getNotBefore() != null) { @@ -426,6 +434,12 @@ public class RepresentationToModel { } } + if (resourceRep.getRegisteredNodes() != null) { + for (Map.Entry entry : resourceRep.getRegisteredNodes().entrySet()) { + applicationModel.registerNode(entry.getKey(), entry.getValue()); + } + } + if (addDefaultRoles && resourceRep.getDefaultRoles() != null) { applicationModel.updateDefaultRoles(resourceRep.getDefaultRoles()); } @@ -448,6 +462,7 @@ public class RepresentationToModel { if (rep.getAdminUrl() != null) resource.setManagementUrl(rep.getAdminUrl()); if (rep.getBaseUrl() != null) resource.setBaseUrl(rep.getBaseUrl()); if (rep.isSurrogateAuthRequired() != null) resource.setSurrogateAuthRequired(rep.isSurrogateAuthRequired()); + if (rep.getNodeReRegistrationTimeout() != null) resource.setNodeReRegistrationTimeout(rep.getNodeReRegistrationTimeout()); resource.updateApplication(); if (rep.getProtocol() != null) resource.setProtocol(rep.getProtocol()); @@ -475,6 +490,12 @@ public class RepresentationToModel { resource.setWebOrigins(new HashSet(webOrigins)); } + if (rep.getRegisteredNodes() != null) { + for (Map.Entry entry : rep.getRegisteredNodes().entrySet()) { + resource.registerNode(entry.getKey(), entry.getValue()); + } + } + if (rep.getClaims() != null) { setClaims(resource, rep.getClaims()); } diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/ApplicationAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/ApplicationAdapter.java index 50e3ccc97e..fd238811de 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/ApplicationAdapter.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/ApplicationAdapter.java @@ -9,6 +9,7 @@ import org.keycloak.models.cache.entities.CachedApplication; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -185,6 +186,36 @@ public class ApplicationAdapter extends ClientAdapter implements ApplicationMode return roles; } + @Override + public int getNodeReRegistrationTimeout() { + if (updated != null) return updated.getNodeReRegistrationTimeout(); + return cached.getNodeReRegistrationTimeout(); + } + + @Override + public void setNodeReRegistrationTimeout(int timeout) { + getDelegateForUpdate(); + updated.setNodeReRegistrationTimeout(timeout); + } + + @Override + public Map getRegisteredNodes() { + if (updated != null) return updated.getRegisteredNodes(); + return cached.getRegisteredNodes(); + } + + @Override + public void registerNode(String nodeHost, int registrationTime) { + getDelegateForUpdate(); + updated.registerNode(nodeHost, registrationTime); + } + + @Override + public void unregisterNode(String nodeHost) { + getDelegateForUpdate(); + updated.unregisterNode(nodeHost); + } + @Override public boolean hasScope(RoleModel role) { if (super.hasScope(role)) { diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedApplication.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedApplication.java index 004c7ff100..e537ea7483 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedApplication.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedApplication.java @@ -10,6 +10,7 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.TreeMap; /** * @author Bill Burke @@ -22,6 +23,8 @@ public class CachedApplication extends CachedClient { private List defaultRoles = new LinkedList(); private boolean bearerOnly; private Map roles = new HashMap(); + private int nodeReRegistrationTimeout; + private Map registeredNodes; public CachedApplication(RealmCache cache, RealmProvider delegate, RealmModel realm, ApplicationModel model) { super(cache, delegate, realm, model); @@ -35,7 +38,8 @@ public class CachedApplication extends CachedClient { cache.addCachedRole(new CachedApplicationRole(id, role, realm)); } - + nodeReRegistrationTimeout = model.getNodeReRegistrationTimeout(); + registeredNodes = new TreeMap(model.getRegisteredNodes()); } public boolean isSurrogateAuthRequired() { @@ -62,4 +66,11 @@ public class CachedApplication extends CachedClient { return roles; } + public int getNodeReRegistrationTimeout() { + return nodeReRegistrationTimeout; + } + + public Map getRegisteredNodes() { + return registeredNodes; + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ApplicationAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ApplicationAdapter.java index 05c4d5c85a..9eab924577 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/ApplicationAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ApplicationAdapter.java @@ -9,6 +9,7 @@ import org.keycloak.models.RoleModel; import org.keycloak.models.jpa.entities.ApplicationEntity; import org.keycloak.models.jpa.entities.RoleEntity; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.util.Time; import javax.persistence.EntityManager; import javax.persistence.TypedQuery; @@ -16,6 +17,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -260,6 +262,35 @@ public class ApplicationAdapter extends ClientAdapter implements ApplicationMode em.flush(); } + @Override + public int getNodeReRegistrationTimeout() { + return applicationEntity.getNodeReRegistrationTimeout(); + } + + @Override + public void setNodeReRegistrationTimeout(int timeout) { + applicationEntity.setNodeReRegistrationTimeout(timeout); + } + + @Override + public Map getRegisteredNodes() { + return applicationEntity.getRegisteredNodes(); + } + + @Override + public void registerNode(String nodeHost, int registrationTime) { + Map currentNodes = getRegisteredNodes(); + currentNodes.put(nodeHost, registrationTime); + em.flush(); + } + + @Override + public void unregisterNode(String nodeHost) { + Map currentNodes = getRegisteredNodes(); + currentNodes.remove(nodeHost); + em.flush(); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ApplicationEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ApplicationEntity.java index a20ca3d3a5..48a9ff0a84 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ApplicationEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ApplicationEntity.java @@ -1,14 +1,19 @@ package org.keycloak.models.jpa.entities; import javax.persistence.CascadeType; +import javax.persistence.CollectionTable; import javax.persistence.Column; +import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; +import javax.persistence.MapKeyColumn; import javax.persistence.OneToMany; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; +import java.util.Map; /** * @author Bill Burke @@ -29,6 +34,9 @@ public class ApplicationEntity extends ClientEntity { @Column(name="BEARER_ONLY") private boolean bearerOnly; + @Column(name="NODE_REREG_TIMEOUT") + private int nodeReRegistrationTimeout; + @OneToMany(fetch = FetchType.EAGER, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "application") Collection roles = new ArrayList(); @@ -36,6 +44,12 @@ public class ApplicationEntity extends ClientEntity { @JoinTable(name="APPLICATION_DEFAULT_ROLES", joinColumns = { @JoinColumn(name="APPLICATION_ID")}, inverseJoinColumns = { @JoinColumn(name="ROLE_ID")}) Collection defaultRoles = new ArrayList(); + @ElementCollection + @MapKeyColumn(name="NAME") + @Column(name="VALUE") + @CollectionTable(name="APP_NODE_REGISTRATIONS", joinColumns={ @JoinColumn(name="APPLICATION_ID") }) + Map registeredNodes = new HashMap(); + public boolean isSurrogateAuthRequired() { return surrogateAuthRequired; } @@ -83,4 +97,20 @@ public class ApplicationEntity extends ClientEntity { public void setBearerOnly(boolean bearerOnly) { this.bearerOnly = bearerOnly; } + + public int getNodeReRegistrationTimeout() { + return nodeReRegistrationTimeout; + } + + public void setNodeReRegistrationTimeout(int nodeReRegistrationTimeout) { + this.nodeReRegistrationTimeout = nodeReRegistrationTimeout; + } + + public Map getRegisteredNodes() { + return registeredNodes; + } + + public void setRegisteredNodes(Map registeredNodes) { + this.registeredNodes = registeredNodes; + } } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ApplicationAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ApplicationAdapter.java index 2b6ef4cb0b..3f84c2b3d9 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ApplicationAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ApplicationAdapter.java @@ -11,10 +11,14 @@ import org.keycloak.models.RoleModel; import org.keycloak.models.mongo.keycloak.entities.MongoApplicationEntity; import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity; import org.keycloak.models.mongo.utils.MongoModelUtils; +import org.keycloak.util.Time; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -218,6 +222,41 @@ public class ApplicationAdapter extends ClientAdapter im updateMongoEntity(); } + @Override + public int getNodeReRegistrationTimeout() { + return getMongoEntity().getNodeReRegistrationTimeout(); + } + + @Override + public void setNodeReRegistrationTimeout(int timeout) { + getMongoEntity().setNodeReRegistrationTimeout(timeout); + updateMongoEntity(); + } + + @Override + public Map getRegisteredNodes() { + return getMongoEntity().getRegisteredNodes() == null ? Collections.emptyMap() : Collections.unmodifiableMap(getMongoEntity().getRegisteredNodes()); + } + + @Override + public void registerNode(String nodeHost, int registrationTime) { + MongoApplicationEntity entity = getMongoEntity(); + if (entity.getRegisteredNodes() == null) { + entity.setRegisteredNodes(new HashMap()); + } + + entity.getRegisteredNodes().put(nodeHost, registrationTime); + updateMongoEntity(); + } + + @Override + public void unregisterNode(String nodeHost) { + MongoApplicationEntity entity = getMongoEntity(); + if (entity.getRegisteredNodes() == null) return; + + entity.getRegisteredNodes().remove(nodeHost); + updateMongoEntity(); + } @Override public boolean equals(Object o) { diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java index 7c6ba7b3d8..da26379f14 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java @@ -137,7 +137,7 @@ public class UserAdapter extends AbstractMongoAdapter implement @Override public Map getAttributes() { - return user.getAttributes()==null ? Collections.EMPTY_MAP : Collections.unmodifiableMap(user.getAttributes()); + return user.getAttributes()==null ? Collections.emptyMap() : Collections.unmodifiableMap(user.getAttributes()); } public MongoUserEntity getUser() { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnect.java b/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnect.java index e520d8d004..73ac4b51b1 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnect.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnect.java @@ -23,8 +23,6 @@ package org.keycloak.protocol.oidc; import org.jboss.logging.Logger; import org.jboss.resteasy.client.core.executors.ApacheHttpClient4Executor; -import org.jboss.resteasy.spi.HttpRequest; -import org.keycloak.ClientConnection; import org.keycloak.OAuth2Constants; import org.keycloak.models.ApplicationModel; import org.keycloak.models.ClientSessionModel; @@ -142,7 +140,7 @@ public class OpenIDConnect implements LoginProtocol { ApacheHttpClient4Executor executor = ResourceAdminManager.createExecutor(); try { - new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, app, clientSession, executor, 0); + new ResourceAdminManager().logoutClientSession(uriInfo.getRequestUri(), realm, app, clientSession, executor); } finally { executor.getHttpClient().getConnectionManager().shutdown(); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnectService.java b/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnectService.java index 44b3934666..d713450346 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnectService.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnectService.java @@ -37,7 +37,6 @@ import org.keycloak.services.ForbiddenException; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus; import org.keycloak.services.managers.ClientSessionCode; -import org.keycloak.services.managers.RealmManager; import org.keycloak.services.resources.Cors; import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.resources.flows.Flows; @@ -52,12 +51,10 @@ import javax.ws.rs.HeaderParam; import javax.ws.rs.OPTIONS; import javax.ws.rs.POST; import javax.ws.rs.Path; -import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.CacheControl; import javax.ws.rs.core.Context; -import javax.ws.rs.core.Cookie; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; @@ -612,15 +609,15 @@ public class OpenIDConnectService { .build(); } - String httpSessionId = formData.getFirst(AdapterConstants.HTTP_SESSION_ID); - if (httpSessionId != null) { - String httpSessionHost = formData.getFirst(AdapterConstants.HTTP_SESSION_HOST); - logger.infof("Http Session '%s' saved in ClientSession for client '%s'. Host is '%s'", httpSessionId, client.getClientId(), httpSessionHost); + String adapterSessionId = formData.getFirst(AdapterConstants.APPLICATION_SESSION_STATE); + if (adapterSessionId != null) { + String adapterSessionHost = formData.getFirst(AdapterConstants.APPLICATION_SESSION_HOST); + logger.infof("Adapter Session '%s' saved in ClientSession for client '%s'. Host is '%s'", adapterSessionId, client.getClientId(), adapterSessionHost); - event.detail(AdapterConstants.HTTP_SESSION_ID, httpSessionId); - clientSession.setNote(AdapterConstants.HTTP_SESSION_ID, httpSessionId); - event.detail(AdapterConstants.HTTP_SESSION_HOST, httpSessionHost); - clientSession.setNote(AdapterConstants.HTTP_SESSION_HOST, httpSessionHost); + event.detail(AdapterConstants.APPLICATION_SESSION_STATE, adapterSessionId); + clientSession.setNote(AdapterConstants.APPLICATION_SESSION_STATE, adapterSessionId); + event.detail(AdapterConstants.APPLICATION_SESSION_HOST, adapterSessionHost); + clientSession.setNote(AdapterConstants.APPLICATION_SESSION_HOST, adapterSessionHost); } AccessToken token = tokenManager.createClientAccessToken(accessCode.getRequestedRoles(), realm, client, user, userSession); @@ -646,6 +643,21 @@ public class OpenIDConnectService { } protected ClientModel authorizeClient(String authorizationHeader, MultivaluedMap formData, EventBuilder event) { + ClientModel client = authorizeClientBase(authorizationHeader, formData, event, realm); + + if ( (client instanceof ApplicationModel) && ((ApplicationModel)client).isBearerOnly()) { + Map error = new HashMap(); + error.put(OAuth2Constants.ERROR, "invalid_client"); + error.put(OAuth2Constants.ERROR_DESCRIPTION, "Bearer-only not allowed"); + event.error(Errors.INVALID_CLIENT); + throw new BadRequestException("Bearer-only not allowed", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build()); + } + + return client; + } + + // Just authorize client without further checking about client type + public static ClientModel authorizeClientBase(String authorizationHeader, MultivaluedMap formData, EventBuilder event, RealmModel realm) { String client_id; String clientSecret; if (authorizationHeader != null) { @@ -686,14 +698,6 @@ public class OpenIDConnectService { throw new BadRequestException("Client is not enabled", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build()); } - if ( (client instanceof ApplicationModel) && ((ApplicationModel)client).isBearerOnly()) { - Map error = new HashMap(); - error.put(OAuth2Constants.ERROR, "invalid_client"); - error.put(OAuth2Constants.ERROR_DESCRIPTION, "Bearer-only not allowed"); - event.error(Errors.INVALID_CLIENT); - throw new BadRequestException("Bearer-only not allowed", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build()); - } - if (!client.isPublicClient()) { if (clientSecret == null || !client.validateSecret(clientSecret)) { Map error = new HashMap(); @@ -702,6 +706,7 @@ public class OpenIDConnectService { throw new BadRequestException("Unauthorized Client", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build()); } } + return client; } diff --git a/services/src/main/java/org/keycloak/services/managers/ApplicationManager.java b/services/src/main/java/org/keycloak/services/managers/ApplicationManager.java index 3f5de4d3e5..5463f39d16 100755 --- a/services/src/main/java/org/keycloak/services/managers/ApplicationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/ApplicationManager.java @@ -9,10 +9,16 @@ import org.keycloak.models.UserSessionProvider; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.adapters.config.BaseRealmConfig; import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.util.Time; import java.net.URI; +import java.util.Collections; import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.TreeSet; /** * @author Bill Burke @@ -46,6 +52,38 @@ public class ApplicationManager { } } + public Set validateRegisteredNodes(ApplicationModel application) { + Map registeredNodes = application.getRegisteredNodes(); + if (registeredNodes == null || registeredNodes.isEmpty()) { + return Collections.emptySet(); + } + + int currentTime = Time.currentTime(); + + Set validatedNodes = new TreeSet(); + if (application.getNodeReRegistrationTimeout() > 0) { + List toRemove = new LinkedList(); + for (Map.Entry entry : registeredNodes.entrySet()) { + Integer lastReRegistration = entry.getValue(); + if (lastReRegistration + application.getNodeReRegistrationTimeout() < currentTime) { + toRemove.add(entry.getKey()); + } else { + validatedNodes.add(entry.getKey()); + } + } + + // Remove time-outed nodes + for (String node : toRemove) { + application.unregisterNode(node); + } + } else { + // Periodic node reRegistration is disabled, so allow all nodes + validatedNodes.addAll(registeredNodes.keySet()); + } + + return validatedNodes; + } + @JsonPropertyOrder({"realm", "realm-public-key", "bearer-only", "auth-server-url", "ssl-required", "resource", "public-client", "credentials", "use-resource-role-mappings"}) diff --git a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java index 2a4576f99e..d959883bb5 100755 --- a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java +++ b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java @@ -20,6 +20,7 @@ import org.keycloak.representations.adapters.action.PushNotBeforeAction; import org.keycloak.representations.adapters.action.UserStats; import org.keycloak.services.util.HttpClientBuilder; import org.keycloak.services.util.ResolveRelative; +import org.keycloak.util.KeycloakUriBuilder; import org.keycloak.util.MultivaluedHashMap; import org.keycloak.util.StringPropertyReplacer; import org.keycloak.util.Time; @@ -29,11 +30,11 @@ import javax.ws.rs.core.UriBuilder; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; +import java.util.Collections; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.TreeMap; /** * @author Bill Burke @@ -41,7 +42,7 @@ import java.util.TreeMap; */ public class ResourceAdminManager { protected static Logger logger = Logger.getLogger(ResourceAdminManager.class); - private static final String KC_SESSION_HOST = "${kc_session_host}"; + private static final String APPLICATION_SESSION_HOST_PROPERTY = "${application.session.host}"; public static ApacheHttpClient4Executor createExecutor() { HttpClient client = new HttpClientBuilder() @@ -63,6 +64,29 @@ public class ResourceAdminManager { return StringPropertyReplacer.replaceProperties(absoluteURI); } + private List getAllManagementUrls(URI requestUri, ApplicationModel application) { + String baseMgmtUrl = getManagementUrl(requestUri, application); + if (baseMgmtUrl == null) { + return Collections.emptyList(); + } + + Set registeredNodesHosts = new ApplicationManager().validateRegisteredNodes(application); + + // No-cluster setup + if (registeredNodesHosts.isEmpty()) { + return Arrays.asList(baseMgmtUrl); + } + + List result = new LinkedList(); + KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(baseMgmtUrl); + for (String nodeHost : registeredNodesHosts) { + String currentNodeUri = uriBuilder.clone().host(nodeHost).build().toString(); + result.add(currentNodeUri); + } + + return result; + } + public void logoutUser(URI requestUri, RealmModel realm, UserModel user, KeycloakSession keycloakSession) { List userSessions = keycloakSession.sessions().getUserSessions(realm, user); logoutUserSessions(requestUri, realm, userSessions); @@ -82,7 +106,7 @@ public class ResourceAdminManager { logger.infov("logging out resources: " + clientSessions); for (Map.Entry> entry : clientSessions.entrySet()) { - logoutApplication(requestUri, realm, entry.getKey(), entry.getValue(), executor, 0); + logoutClientSessions(requestUri, realm, entry.getKey(), entry.getValue(), executor); } } finally { executor.getHttpClient().getConnectionManager().shutdown(); @@ -108,34 +132,18 @@ public class ResourceAdminManager { logger.debugv("logging out {0} resources ", clientSessions.size()); for (Map.Entry> entry : clientSessions.entrySet()) { - logoutApplication(requestUri, realm, entry.getKey(), entry.getValue(), executor, 0); + logoutClientSessions(requestUri, realm, entry.getKey(), entry.getValue(), executor); } } finally { executor.getHttpClient().getConnectionManager().shutdown(); } } - public void logoutAll(URI requestUri, RealmModel realm) { + public void logoutUserFromApplication(URI requestUri, RealmModel realm, ApplicationModel resource, UserModel user, KeycloakSession session) { ApacheHttpClient4Executor executor = createExecutor(); try { - realm.setNotBefore(Time.currentTime()); - List resources = realm.getApplications(); - logger.debugv("logging out {0} resources ", resources.size()); - for (ApplicationModel resource : resources) { - logoutApplication(requestUri, realm, resource, (List)null, executor, realm.getNotBefore()); - } - } finally { - executor.getHttpClient().getConnectionManager().shutdown(); - } - } - - public void logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, List userSessions) { - ApacheHttpClient4Executor executor = createExecutor(); - - try { - resource.setNotBefore(Time.currentTime()); - + List userSessions = session.sessions().getUserSessions(realm, user); List ourAppClientSessions = null; if (userSessions != null) { MultivaluedHashMap clientSessions = new MultivaluedHashMap(); @@ -145,18 +153,18 @@ public class ResourceAdminManager { ourAppClientSessions = clientSessions.get(resource); } - logoutApplication(requestUri, realm, resource, ourAppClientSessions, executor, resource.getNotBefore()); + logoutClientSessions(requestUri, realm, resource, ourAppClientSessions, executor); } finally { executor.getHttpClient().getConnectionManager().shutdown(); } } - public boolean logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, ClientSessionModel clientSession, ApacheHttpClient4Executor client, int notBefore) { - return logoutApplication(requestUri, realm, resource, Arrays.asList(clientSession), client, notBefore); + public boolean logoutClientSession(URI requestUri, RealmModel realm, ApplicationModel resource, ClientSessionModel clientSession, ApacheHttpClient4Executor client) { + return logoutClientSessions(requestUri, realm, resource, Arrays.asList(clientSession), client); } - protected boolean logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, List clientSessions, ApacheHttpClient4Executor client, int notBefore) { + protected boolean logoutClientSessions(URI requestUri, RealmModel realm, ApplicationModel resource, List clientSessions, ApacheHttpClient4Executor client) { String managementUrl = getManagementUrl(requestUri, resource); if (managementUrl != null) { @@ -165,22 +173,22 @@ public class ResourceAdminManager { if (clientSessions != null && clientSessions.size() > 0) { adapterSessionIds = new MultivaluedHashMap(); for (ClientSessionModel clientSession : clientSessions) { - String adapterSessionId = clientSession.getNote(AdapterConstants.HTTP_SESSION_ID); + String adapterSessionId = clientSession.getNote(AdapterConstants.APPLICATION_SESSION_STATE); if (adapterSessionId != null) { - String host = clientSession.getNote(AdapterConstants.HTTP_SESSION_HOST); + String host = clientSession.getNote(AdapterConstants.APPLICATION_SESSION_HOST); adapterSessionIds.add(host, adapterSessionId); } } } - if (managementUrl.contains(KC_SESSION_HOST) && adapterSessionIds != null) { + if (managementUrl.contains(APPLICATION_SESSION_HOST_PROPERTY) && adapterSessionIds != null) { boolean allPassed = true; // Send logout separately to each host (needed for single-sign-out in cluster for non-distributable apps - KEYCLOAK-748) for (Map.Entry> entry : adapterSessionIds.entrySet()) { String host = entry.getKey(); List sessionIds = entry.getValue(); - String currentHostMgmtUrl = managementUrl.replace(KC_SESSION_HOST, host); - allPassed = logoutApplicationOnHost(realm, resource, sessionIds, client, notBefore, currentHostMgmtUrl) && allPassed; + String currentHostMgmtUrl = managementUrl.replace(APPLICATION_SESSION_HOST_PROPERTY, host); + allPassed = sendLogoutRequest(realm, resource, sessionIds, client, 0, currentHostMgmtUrl) && allPassed; } return allPassed; @@ -193,7 +201,7 @@ public class ResourceAdminManager { allSessionIds.addAll(currentIds); } } - return logoutApplicationOnHost(realm, resource, allSessionIds, client, notBefore, managementUrl); + return sendLogoutRequest(realm, resource, allSessionIds, client, 0, managementUrl); } } else { logger.debugv("Can't logout {0}: no management url", resource.getName()); @@ -201,7 +209,54 @@ public class ResourceAdminManager { } } - protected boolean logoutApplicationOnHost(RealmModel realm, ApplicationModel resource, List adapterSessionIds, ApacheHttpClient4Executor client, int notBefore, String managementUrl) { + // Methods for logout all + + public void logoutAll(URI requestUri, RealmModel realm) { + ApacheHttpClient4Executor executor = createExecutor(); + + try { + realm.setNotBefore(Time.currentTime()); + List resources = realm.getApplications(); + logger.debugv("logging out {0} resources ", resources.size()); + for (ApplicationModel resource : resources) { + logoutApplication(requestUri, realm, resource, executor, realm.getNotBefore()); + } + } finally { + executor.getHttpClient().getConnectionManager().shutdown(); + } + } + + public void logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource) { + ApacheHttpClient4Executor executor = createExecutor(); + try { + resource.setNotBefore(Time.currentTime()); + logoutApplication(requestUri, realm, resource, executor, resource.getNotBefore()); + } finally { + executor.getHttpClient().getConnectionManager().shutdown(); + } + } + + + protected boolean logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, ApacheHttpClient4Executor executor, int notBefore) { + List mgmtUrls = getAllManagementUrls(requestUri, resource); + if (mgmtUrls.isEmpty()) { + logger.debug("No management URL or no registered cluster nodes for the application " + resource.getName()); + return false; + } + + logger.info("Send logoutApplication for URLs: " + mgmtUrls); + + // Propagate this to all hosts + boolean anyFailed = false; + for (String mgmtUrl : mgmtUrls) { + if (!sendLogoutRequest(realm, resource, null, executor, notBefore, mgmtUrl)) { + anyFailed = true; + } + } + return !anyFailed; + } + + protected boolean sendLogoutRequest(RealmModel realm, ApplicationModel resource, List adapterSessionIds, ApacheHttpClient4Executor client, int notBefore, String managementUrl) { LogoutAction adminAction = new LogoutAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getName(), adapterSessionIds, notBefore); String token = new TokenManager().encodeToken(realm, adminAction); logger.infov("logout resource {0} url: {1} sessionIds: " + adapterSessionIds, resource.getName(), managementUrl); @@ -245,33 +300,43 @@ public class ResourceAdminManager { } - protected boolean pushRevocationPolicy(URI requestUri, RealmModel realm, ApplicationModel resource, int notBefore, ApacheHttpClient4Executor client) { - if (notBefore <= 0) return false; - String managementUrl = getManagementUrl(requestUri, resource); - if (managementUrl != null) { - PushNotBeforeAction adminAction = new PushNotBeforeAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getName(), notBefore); - String token = new TokenManager().encodeToken(realm, adminAction); - logger.infov("pushRevocation resource: {0} url: {1}", resource.getName(), managementUrl); - ClientRequest request = client.createRequest(UriBuilder.fromUri(managementUrl).path(AdapterConstants.K_PUSH_NOT_BEFORE).build().toString()); - ClientResponse response; - try { - response = request.body(MediaType.TEXT_PLAIN_TYPE, token).post(); - } catch (Exception e) { - throw new RuntimeException(e); - } - - try { - boolean success = response.getStatus() == 204; - logger.debug("pushRevocation success."); - return success; - } finally { - response.releaseConnection(); - } - } else { - logger.debug("no management URL for application: " + resource.getName()); + protected boolean pushRevocationPolicy(URI requestUri, RealmModel realm, ApplicationModel resource, int notBefore, ApacheHttpClient4Executor executor) { + List mgmtUrls = getAllManagementUrls(requestUri, resource); + if (mgmtUrls.isEmpty()) { + logger.debug("No management URL or no registered cluster nodes for the application " + resource.getName()); return false; } + logger.info("Sending push revocation to URLS: " + mgmtUrls); + // Propagate this to all hosts + boolean anyFailed= false; + for (String mgmtUrl : mgmtUrls) { + if (!sendPushRevocationPolicyRequest(realm, resource, notBefore, executor, mgmtUrl)) { + anyFailed = true; + } + } + return !anyFailed; + } + + protected boolean sendPushRevocationPolicyRequest(RealmModel realm, ApplicationModel resource, int notBefore, ApacheHttpClient4Executor client, String managementUrl) { + PushNotBeforeAction adminAction = new PushNotBeforeAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getName(), notBefore); + String token = new TokenManager().encodeToken(realm, adminAction); + logger.infov("pushRevocation resource: {0} url: {1}", resource.getName(), managementUrl); + ClientRequest request = client.createRequest(UriBuilder.fromUri(managementUrl).path(AdapterConstants.K_PUSH_NOT_BEFORE).build().toString()); + ClientResponse response; + try { + response = request.body(MediaType.TEXT_PLAIN_TYPE, token).post(); + } catch (Exception e) { + logger.warn("Failed to send revocation request", e); + return false; + } + try { + boolean success = response.getStatus() == 204; + logger.debug("pushRevocation success."); + return success; + } finally { + response.releaseConnection(); + } } } diff --git a/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java b/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java new file mode 100644 index 0000000000..689d892d95 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java @@ -0,0 +1,193 @@ +package org.keycloak.services.resources; + +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; +import javax.ws.rs.ext.Providers; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.spi.BadRequestException; +import org.jboss.resteasy.spi.HttpRequest; +import org.jboss.resteasy.spi.UnauthorizedException; +import org.keycloak.ClientConnection; +import org.keycloak.OAuth2Constants; +import org.keycloak.adapters.AdapterConstants; +import org.keycloak.events.Errors; +import org.keycloak.events.EventBuilder; +import org.keycloak.events.EventType; +import org.keycloak.models.ApplicationModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.protocol.oidc.OpenIDConnectService; +import org.keycloak.services.ForbiddenException; +import org.keycloak.util.Time; + +/** + * @author Marek Posolda + */ +public class ClientsManagementService { + + protected static final Logger logger = Logger.getLogger(ClientsManagementService.class); + + private RealmModel realm; + + private EventBuilder event; + + @Context + private HttpRequest request; + + @Context + protected HttpHeaders headers; + + @Context + private UriInfo uriInfo; + + @Context + private ClientConnection clientConnection; + + @Context + protected Providers providers; + + @Context + protected KeycloakSession session; + + public ClientsManagementService(RealmModel realm, EventBuilder event) { + this.realm = realm; + this.event = event; + } + + public static UriBuilder clientsManagementBaseUrl(UriBuilder baseUriBuilder) { + return baseUriBuilder.path(RealmsResource.class).path(RealmsResource.class, "getClientsManagementService"); + } + + public static UriBuilder registerNodeUrl(UriBuilder baseUriBuilder) { + UriBuilder uriBuilder = clientsManagementBaseUrl(baseUriBuilder); + return uriBuilder.path(ClientsManagementService.class, "registerNode"); + } + + public static UriBuilder unregisterNodeUrl(UriBuilder baseUriBuilder) { + UriBuilder uriBuilder = clientsManagementBaseUrl(baseUriBuilder); + return uriBuilder.path(ClientsManagementService.class, "unregisterNode"); + } + + /** + * URL invoked by adapter to register new application cluster node. Each application cluster node will invoke this URL once it joins cluster + * + * @param authorizationHeader + * @param formData + * @return + */ + @Path("register-node") + @POST + @Produces("application/json") + public Response registerNode(@HeaderParam(HttpHeaders.AUTHORIZATION) String authorizationHeader, final MultivaluedMap formData) { + if (!checkSsl()) { + throw new ForbiddenException("HTTPS required"); + } + + event.event(EventType.REGISTER_NODE); + + if (!realm.isEnabled()) { + event.error(Errors.REALM_DISABLED); + throw new UnauthorizedException("Realm not enabled"); + } + + ApplicationModel application = authorizeApplication(authorizationHeader, formData); + String nodeHost = getApplicationClusterHost(formData); + + logger.infof("Registering cluster host '%s' for client '%s'", nodeHost, application.getName()); + + application.registerNode(nodeHost, Time.currentTime()); + + return Response.noContent().build(); + } + + + /** + * URL invoked by adapter to register new application cluster node. Each application cluster node will invoke this URL once it joins cluster + * + * @param authorizationHeader + * @param formData + * @return + */ + @Path("unregister-node") + @POST + @Produces("application/json") + public Response unregisterNode(@HeaderParam(HttpHeaders.AUTHORIZATION) String authorizationHeader, final MultivaluedMap formData) { + if (!checkSsl()) { + throw new ForbiddenException("HTTPS required"); + } + + event.event(EventType.UNREGISTER_NODE); + + if (!realm.isEnabled()) { + event.error(Errors.REALM_DISABLED); + throw new UnauthorizedException("Realm not enabled"); + } + + ApplicationModel application = authorizeApplication(authorizationHeader, formData); + String nodeHost = getApplicationClusterHost(formData); + + logger.infof("Unregistering cluster host '%s' for client '%s'", nodeHost, application.getName()); + + application.unregisterNode(nodeHost); + + return Response.noContent().build(); + } + + protected ApplicationModel authorizeApplication(String authorizationHeader, MultivaluedMap formData) { + ClientModel client = OpenIDConnectService.authorizeClientBase(authorizationHeader, formData, event, realm); + + if (client.isPublicClient()) { + Map error = new HashMap(); + error.put(OAuth2Constants.ERROR, "invalid_client"); + error.put(OAuth2Constants.ERROR_DESCRIPTION, "Public clients not allowed"); + event.error(Errors.INVALID_CLIENT); + throw new BadRequestException("Public clients not allowed", javax.ws.rs.core.Response.status(javax.ws.rs.core.Response.Status.BAD_REQUEST).entity(error).type("application/json").build()); + } + + if (!(client instanceof ApplicationModel)) { + Map error = new HashMap(); + error.put(OAuth2Constants.ERROR, "invalid_client"); + error.put(OAuth2Constants.ERROR_DESCRIPTION, "Just applications are allowed"); + event.error(Errors.INVALID_CLIENT); + throw new BadRequestException("ust applications are allowed", javax.ws.rs.core.Response.status(javax.ws.rs.core.Response.Status.BAD_REQUEST).entity(error).type("application/json").build()); + } + + return (ApplicationModel)client; + } + + protected String getApplicationClusterHost(MultivaluedMap formData) { + String applicationClusterHost = formData.getFirst(AdapterConstants.APPLICATION_CLUSTER_HOST); + if (applicationClusterHost == null || applicationClusterHost.length() == 0) { + Map error = new HashMap(); + error.put(OAuth2Constants.ERROR, "invalid_request"); + error.put(OAuth2Constants.ERROR_DESCRIPTION, "application cluster host not specified"); + event.error(Errors.INVALID_CODE); + throw new BadRequestException("Cluster host not specified", javax.ws.rs.core.Response.status(javax.ws.rs.core.Response.Status.BAD_REQUEST).entity(error).type("application/json").build()); + } + + return applicationClusterHost; + } + + + + private boolean checkSsl() { + if (uriInfo.getBaseUri().getScheme().equals("https")) { + return true; + } else { + return !realm.getSslRequired().isRequired(clientConnection); + } + } +} 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 785b1014ac..2feeaaaebf 100755 --- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java @@ -134,6 +134,16 @@ public class RealmsResource { return service; } + @Path("{realm}/clients-managements") + public ClientsManagementService getClientsManagementService(final @PathParam("realm") String name) { + RealmManager realmManager = new RealmManager(session); + RealmModel realm = locateRealm(name, realmManager); + EventBuilder event = new EventsManager(realm, session, clientConnection).createEventBuilder(); + ClientsManagementService service = new ClientsManagementService(realm, event); + ResteasyProviderFactory.getInstance().injectProperties(service); + return service; + } + protected RealmModel locateRealm(String name, RealmManager realmManager) { RealmModel realm = realmManager.getRealmByName(name); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java index 15a2773084..0c84d11a7f 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java @@ -335,7 +335,7 @@ public class ApplicationResource { @POST public void logoutAll() { auth.requireManage(); - new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, application, null); + new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, application); } /** @@ -351,8 +351,7 @@ public class ApplicationResource { throw new NotFoundException("User not found"); } - List userSessions = session.sessions().getUserSessions(realm, user); - new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, application, userSessions); + new ResourceAdminManager().logoutUserFromApplication(uriInfo.getRequestUri(), realm, application, user, session); } diff --git a/testsuite/docker-cluster/shared-files/deploy-examples.sh b/testsuite/docker-cluster/shared-files/deploy-examples.sh index f71f697a9a..d2e3287965 100644 --- a/testsuite/docker-cluster/shared-files/deploy-examples.sh +++ b/testsuite/docker-cluster/shared-files/deploy-examples.sh @@ -29,7 +29,8 @@ sed -i -e 's/false/true/' admin-access.war/WEB-INF/web.xml # Configure other examples for I in *.war/WEB-INF/keycloak.json; do - sed -i -e 's/\"auth-server-url\".*: \"\/auth\",/&\n \"auth-server-url-for-backend-requests\": \"http:\/\/\$\{jboss.host.name\}:8080\/auth\",/' $I; + sed -i -e 's/\"auth-server-url\".*: \"\/auth\",/&\n \"auth-server-url-for-backend-requests\": \"http:\/\/\$\{jboss.host.name\}:8080\/auth\",\ + \n \"register-node-at-startup\": false,\n \"register-node-period\": 30,/' $I; done; # Enable distributable for customer-portal @@ -37,6 +38,6 @@ sed -i -e 's/<\/module-name>/&\n /' customer-portal.war/WEB # Configure testrealm.json - Enable adminUrl to access adapters on local machine sed -i -e 's/\"adminUrl\": \"\/customer-portal/\"adminUrl\": \"http:\/\/\$\{jboss.host.name\}:8080\/customer-portal/' /keycloak-docker-cluster/examples/testrealm.json -sed -i -e 's/\"adminUrl\": \"\/product-portal/\"adminUrl\": \"http:\/\/\$\{kc_session_host\}:8080\/product-portal/' /keycloak-docker-cluster/examples/testrealm.json +sed -i -e 's/\"adminUrl\": \"\/product-portal/\"adminUrl\": \"http:\/\/\$\{application.session.host\}:8080\/product-portal/' /keycloak-docker-cluster/examples/testrealm.json diff --git a/testsuite/docker-cluster/shared-files/keycloak-run-node.sh b/testsuite/docker-cluster/shared-files/keycloak-run-node.sh index 7d350aa762..bc0d1e65ca 100644 --- a/testsuite/docker-cluster/shared-files/keycloak-run-node.sh +++ b/testsuite/docker-cluster/shared-files/keycloak-run-node.sh @@ -23,9 +23,8 @@ function prepareHost cp -r /keycloak-docker-cluster/deployments/* $JBOSS_HOME/standalone/deployments/ # Enable Infinispan provider - sed -i "s|keycloak.userSessions.provider:mem|keycloak.userSessions.provider:infinispan|" $JBOSS_HOME/standalone/deployments/auth-server.war/WEB-INF/classes/META-INF/keycloak-server.json - sed -i "s|keycloak.realm.cache.provider:mem|keycloak.realm.cache.provider:infinispan|" $JBOSS_HOME/standalone/deployments/auth-server.war/WEB-INF/classes/META-INF/keycloak-server.json - sed -i "s|keycloak.user.cache.provider:mem|keycloak.user.cache.provider:infinispan|" $JBOSS_HOME/standalone/deployments/auth-server.war/WEB-INF/classes/META-INF/keycloak-server.json + sed -i "s|\"provider\".*: \"mem\"|\"provider\": \"infinispan\"|" $JBOSS_HOME/standalone/deployments/auth-server.war/WEB-INF/classes/META-INF/keycloak-server.json + sed -i -e "s/\"connectionsJpa\"/\n \"connectionsInfinispan\": \{\n \"default\" : \{\n \"cacheContainer\" : \"java:jboss\/infinispan\/Keycloak\"\n \}\n \},\n &/" $JBOSS_HOME/standalone/deployments/auth-server.war/WEB-INF/classes/META-INF/keycloak-server.json # Deploy and configure examples /keycloak-docker-cluster/shared-files/deploy-examples.sh diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ApplicationModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ApplicationModelTest.java index 84bd57ccda..bb8c651043 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ApplicationModelTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ApplicationModelTest.java @@ -45,6 +45,9 @@ public class ApplicationModelTest extends AbstractModelTest { application.addWebOrigin("origin-1"); application.addWebOrigin("origin-2"); + application.registerNode("node1", 10); + application.registerNode("10.20.30.40", 50); + application.updateApplication(); } @@ -84,6 +87,7 @@ public class ApplicationModelTest extends AbstractModelTest { Assert.assertTrue(expected.getRedirectUris().containsAll(actual.getRedirectUris())); Assert.assertTrue(expected.getWebOrigins().containsAll(actual.getWebOrigins())); + Assert.assertTrue(expected.getRegisteredNodes().equals(actual.getRegisteredNodes())); } public static void assertEquals(List expected, List actual) { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java index ac0eb6a156..97c433e2a1 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java @@ -97,6 +97,12 @@ public class ImportTest extends AbstractModelTest { Assert.assertTrue(apps.values().contains(accountApp)); realm.getApplications().containsAll(apps.values()); + Assert.assertEquals(50, application.getNodeReRegistrationTimeout()); + Map appRegisteredNodes = application.getRegisteredNodes(); + Assert.assertEquals(2, appRegisteredNodes.size()); + Assert.assertTrue(10 == appRegisteredNodes.get("node1")); + Assert.assertTrue(20 == appRegisteredNodes.get("172.10.15.20")); + // Test finding applications by ID Assert.assertNull(realm.getApplicationById("982734")); Assert.assertEquals(application, realm.getApplicationById(application.getId())); diff --git a/testsuite/integration/src/test/resources/model/testrealm.json b/testsuite/integration/src/test/resources/model/testrealm.json index a5bf38012e..fdb8f82a2e 100755 --- a/testsuite/integration/src/test/resources/model/testrealm.json +++ b/testsuite/integration/src/test/resources/model/testrealm.json @@ -94,7 +94,12 @@ "applications": [ { "name": "Application", - "enabled": true + "enabled": true, + "nodeReRegistrationTimeout": 50, + "registeredNodes": { + "node1": 10, + "172.10.15.20": 20 + } }, { "name": "OtherApp",