KEYCLOAK-759 dynamic registration of managementUrls in cluster

This commit is contained in:
mposolda 2014-10-15 19:57:34 +02:00
parent 8545457f5a
commit 7d8f265789
43 changed files with 990 additions and 114 deletions

View file

@ -23,6 +23,15 @@
<constraints nullable="false"/> <constraints nullable="false"/>
</column> </column>
</createTable> </createTable>
<createTable tableName="APP_NODE_REGISTRATIONS">
<column name="APPLICATION_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="VALUE" type="INT"/>
<column name="NAME" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
</createTable>
<addColumn tableName="CLIENT_SESSION"> <addColumn tableName="CLIENT_SESSION">
<column name="AUTH_METHOD" type="VARCHAR(255)"/> <column name="AUTH_METHOD" type="VARCHAR(255)"/>
</addColumn> </addColumn>
@ -35,9 +44,14 @@
<addColumn tableName="REALM"> <addColumn tableName="REALM">
<column name="CERTIFICATE" type="VARCHAR(2048)"/> <column name="CERTIFICATE" type="VARCHAR(2048)"/>
</addColumn> </addColumn>
<addColumn tableName="CLIENT">
<column name="NODE_REREG_TIMEOUT" type="INTEGER"/>
</addColumn>
<addPrimaryKey columnNames="CLIENT_ID, NAME" constraintName="CONSTRAINT_3C" tableName="CLIENT_ATTRIBUTES"/> <addPrimaryKey columnNames="CLIENT_ID, NAME" constraintName="CONSTRAINT_3C" tableName="CLIENT_ATTRIBUTES"/>
<addPrimaryKey columnNames="CLIENT_SESSION, NAME" constraintName="CONSTRAINT_5E" tableName="CLIENT_SESSION_NOTE"/> <addPrimaryKey columnNames="CLIENT_SESSION, NAME" constraintName="CONSTRAINT_5E" tableName="CLIENT_SESSION_NOTE"/>
<addPrimaryKey columnNames="APPLICATION_ID, NAME" constraintName="CONSTRAINT_84" tableName="APP_NODE_REGISTRATIONS"/>
<addForeignKeyConstraint baseColumnNames="CLIENT_ID" baseTableName="CLIENT_ATTRIBUTES" constraintName="FK3C47C64BEACCA966" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="CLIENT"/> <addForeignKeyConstraint baseColumnNames="CLIENT_ID" baseTableName="CLIENT_ATTRIBUTES" constraintName="FK3C47C64BEACCA966" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="CLIENT"/>
<addForeignKeyConstraint baseColumnNames="CLIENT_SESSION" baseTableName="CLIENT_SESSION_NOTE" constraintName="FK5EDFB00FF51C2736" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="CLIENT_SESSION"/> <addForeignKeyConstraint baseColumnNames="CLIENT_SESSION" baseTableName="CLIENT_SESSION_NOTE" constraintName="FK5EDFB00FF51C2736" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="CLIENT_SESSION"/>
<addForeignKeyConstraint baseColumnNames="APPLICATION_ID" baseTableName="APP_NODE_REGISTRATIONS" constraintName="FK8454723BA992F594" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="CLIENT"/>
</changeSet> </changeSet>
</databaseChangeLog> </databaseChangeLog>

View file

@ -8,7 +8,7 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
/** /**
* For now, there is support just for convert to Map<String, String> * For now, there is support just for convert to Map<String, simpleType>
* *
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
@ -18,10 +18,10 @@ public class BasicDBObjectToMapMapper implements Mapper<BasicDBObject, Map> {
public Map convertObject(MapperContext<BasicDBObject, Map> context) { public Map convertObject(MapperContext<BasicDBObject, Map> context) {
BasicDBObject dbObjectToConvert = context.getObjectToConvert(); BasicDBObject dbObjectToConvert = context.getObjectToConvert();
HashMap<String, String> result = new HashMap<String, String>(); HashMap<String, Object> result = new HashMap<String, Object>();
for (Map.Entry<String, Object> entry : dbObjectToConvert.entrySet()) { for (Map.Entry<String, Object> entry : dbObjectToConvert.entrySet()) {
String key = entry.getKey(); String key = entry.getKey();
String value = (String)entry.getValue(); Object value = entry.getValue();
if (key.contains(MapMapper.DOT_PLACEHOLDER)) { if (key.contains(MapMapper.DOT_PLACEHOLDER)) {
key = key.replaceAll(MapMapper.DOT_PLACEHOLDER, "."); key = key.replaceAll(MapMapper.DOT_PLACEHOLDER, ".");

View file

@ -8,7 +8,7 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
/** /**
* For now, we support just convert from Map<String, String> * For now, we support just convert from Map<String, simpleType>
* *
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
@ -31,7 +31,7 @@ public class MapMapper<T extends Map> implements Mapper<T, BasicDBObject> {
Set<Map.Entry> entries = objectToConvert.entrySet(); Set<Map.Entry> entries = objectToConvert.entrySet();
for (Map.Entry entry : entries) { for (Map.Entry entry : entries) {
String key = (String)entry.getKey(); String key = (String)entry.getKey();
String value = (String)entry.getValue(); Object value = entry.getValue();
if (key.contains(".")) { if (key.contains(".")) {
key = key.replaceAll("\\.", DOT_PLACEHOLDER); key = key.replaceAll("\\.", DOT_PLACEHOLDER);

View file

@ -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 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 ACCOUNT_SERVICE_PATH = "/realms/{realm-name}/account";
public static final String REALM_INFO_PATH = "/realms/{realm-name}"; 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";
} }

View file

@ -20,8 +20,11 @@ public interface AdapterConstants {
String AUTH_DATA_PARAM_NAME = "org.keycloak.json.adapterConfig"; 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 // 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 // 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";
} }

View file

@ -17,7 +17,8 @@ import org.codehaus.jackson.annotate.JsonPropertyOrder;
"connection-pool-size", "connection-pool-size",
"allow-any-hostname", "disable-trust-manager", "truststore", "truststore-password", "allow-any-hostname", "disable-trust-manager", "truststore", "truststore-password",
"client-keystore", "client-keystore-password", "client-key-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 { public class AdapterConfig extends BaseAdapterConfig {
@ -41,6 +42,10 @@ public class AdapterConfig extends BaseAdapterConfig {
protected String authServerUrlForBackendRequests; protected String authServerUrlForBackendRequests;
@JsonProperty("always-refresh-token") @JsonProperty("always-refresh-token")
protected boolean alwaysRefreshToken = false; protected boolean alwaysRefreshToken = false;
@JsonProperty("register-node-at-startup")
protected boolean registerNodeAtStartup = false;
@JsonProperty("register-node-period")
protected int registerNodePeriod = -1;
public boolean isAllowAnyHostname() { public boolean isAllowAnyHostname() {
return allowAnyHostname; return allowAnyHostname;
@ -121,4 +126,20 @@ public class AdapterConfig extends BaseAdapterConfig {
public void setAlwaysRefreshToken(boolean alwaysRefreshToken) { public void setAlwaysRefreshToken(boolean alwaysRefreshToken) {
this.alwaysRefreshToken = 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;
}
} }

View file

@ -25,7 +25,8 @@ public class ApplicationRepresentation {
protected String protocol; protected String protocol;
protected Map<String, String> attributes; protected Map<String, String> attributes;
protected Boolean fullScopeAllowed; protected Boolean fullScopeAllowed;
protected Integer nodeReRegistrationTimeout;
protected Map<String, Integer> registeredNodes;
public String getId() { public String getId() {
return id; return id;
@ -162,4 +163,20 @@ public class ApplicationRepresentation {
public void setAttributes(Map<String, String> attributes) { public void setAttributes(Map<String, String> attributes) {
this.attributes = attributes; this.attributes = attributes;
} }
public Integer getNodeReRegistrationTimeout() {
return nodeReRegistrationTimeout;
}
public void setNodeReRegistrationTimeout(Integer nodeReRegistrationTimeout) {
this.nodeReRegistrationTimeout = nodeReRegistrationTimeout;
}
public Map<String, Integer> getRegisteredNodes() {
return registeredNodes;
}
public void setRegisteredNodes(Map<String, Integer> registeredNodes) {
this.registeredNodes = registeredNodes;
}
} }

View file

@ -41,6 +41,8 @@ public enum EventType {
SEND_RESET_PASSWORD, SEND_RESET_PASSWORD,
SEND_RESET_PASSWORD_ERROR, SEND_RESET_PASSWORD_ERROR,
SOCIAL_LOGIN, SOCIAL_LOGIN,
SOCIAL_LOGIN_ERROR SOCIAL_LOGIN_ERROR,
REGISTER_NODE,
UNREGISTER_NODE
} }

View file

@ -43,7 +43,7 @@ public class AdminClient {
HttpClient client = new HttpClientBuilder() HttpClient client = new HttpClientBuilder()
.disableTrustManager().build(); .disableTrustManager().build();
try { 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()); get.addHeader("Authorization", "Bearer " + session.getTokenString());
try { try {
HttpResponse response = client.execute(get); HttpResponse response = client.execute(get);

View file

@ -50,7 +50,7 @@ public class CustomerDatabaseClient {
HttpClient client = new HttpClientBuilder() HttpClient client = new HttpClientBuilder()
.disableTrustManager().build(); .disableTrustManager().build();
try { 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()); get.addHeader("Authorization", "Bearer " + session.getTokenString());
try { try {
HttpResponse response = client.execute(get); HttpResponse response = client.execute(get);

View file

@ -40,7 +40,7 @@ public class ProductDatabaseClient
HttpClient client = new HttpClientBuilder() HttpClient client = new HttpClientBuilder()
.disableTrustManager().build(); .disableTrustManager().build();
try { 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()); get.addHeader("Authorization", "Bearer " + session.getTokenString());
try { try {
HttpResponse response = client.execute(get); HttpResponse response = client.execute(get);

View file

@ -157,6 +157,16 @@ public class AdapterDeploymentContext {
return (this.accountUrl != null) ? this.accountUrl : delegate.getAccountUrl(); 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 @Override
public String getResourceName() { public String getResourceName() {
return delegate.getResourceName(); return delegate.getResourceName();
@ -336,6 +346,26 @@ public class AdapterDeploymentContext {
public void setAlwaysRefreshToken(boolean alwaysRefreshToken) { public void setAlwaysRefreshToken(boolean alwaysRefreshToken) {
delegate.setAlwaysRefreshToken(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) { protected KeycloakUriBuilder getBaseBuilder(HttpFacade facade, String base) {

View file

@ -8,7 +8,7 @@ import org.keycloak.util.UriUtils;
*/ */
public class AdapterUtils { public class AdapterUtils {
public static String getBaseUrl(String browserRequestURL, KeycloakSecurityContext session) { public static String getOrigin(String browserRequestURL, KeycloakSecurityContext session) {
if (session instanceof RefreshableKeycloakSecurityContext) { if (session instanceof RefreshableKeycloakSecurityContext) {
KeycloakDeployment deployment = ((RefreshableKeycloakSecurityContext)session).getDeployment(); KeycloakDeployment deployment = ((RefreshableKeycloakSecurityContext)session).getDeployment();
switch (deployment.getRelativeUrls()) { switch (deployment.getRelativeUrls()) {
@ -16,10 +16,9 @@ public class AdapterUtils {
// Resolve baseURI from the request // Resolve baseURI from the request
return UriUtils.getOrigin(browserRequestURL); return UriUtils.getOrigin(browserRequestURL);
case BROWSER_ONLY: case BROWSER_ONLY:
case NEVER:
// Resolve baseURI from the codeURL (This is already non-relative and based on our hostname) // Resolve baseURI from the codeURL (This is already non-relative and based on our hostname)
return UriUtils.getOrigin(deployment.getCodeUrl()); return UriUtils.getOrigin(deployment.getCodeUrl());
case NEVER:
return "";
default: default:
return ""; return "";
} }

View file

@ -31,6 +31,8 @@ public class KeycloakDeployment {
protected String refreshUrl; protected String refreshUrl;
protected KeycloakUriBuilder logoutUrl; protected KeycloakUriBuilder logoutUrl;
protected String accountUrl; protected String accountUrl;
protected String registerNodeUrl;
protected String unregisterNodeUrl;
protected String resourceName; protected String resourceName;
protected boolean bearerOnly; protected boolean bearerOnly;
@ -48,6 +50,8 @@ public class KeycloakDeployment {
protected String corsAllowedMethods; protected String corsAllowedMethods;
protected boolean exposeToken; protected boolean exposeToken;
protected boolean alwaysRefreshToken; protected boolean alwaysRefreshToken;
protected boolean registerNodeAtStartup;
protected int registerNodePeriod;
protected volatile int notBefore; protected volatile int notBefore;
public KeycloakDeployment() { public KeycloakDeployment() {
@ -136,6 +140,8 @@ public class KeycloakDeployment {
accountUrl = authUrlBuilder.clone().path(ServiceUrlConstants.ACCOUNT_SERVICE_PATH).build(getRealm()).toString(); accountUrl = authUrlBuilder.clone().path(ServiceUrlConstants.ACCOUNT_SERVICE_PATH).build(getRealm()).toString();
realmInfoUrl = authUrlBuilder.clone().path(ServiceUrlConstants.REALM_INFO_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(); 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() { public RelativeUrlsUsed getRelativeUrls() {
@ -166,6 +172,14 @@ public class KeycloakDeployment {
return accountUrl; return accountUrl;
} }
public String getRegisterNodeUrl() {
return registerNodeUrl;
}
public String getUnregisterNodeUrl() {
return unregisterNodeUrl;
}
public void setResourceName(String resourceName) { public void setResourceName(String resourceName) {
this.resourceName = resourceName; this.resourceName = resourceName;
} }
@ -289,4 +303,20 @@ public class KeycloakDeployment {
public void setAlwaysRefreshToken(boolean alwaysRefreshToken) { public void setAlwaysRefreshToken(boolean alwaysRefreshToken) {
this.alwaysRefreshToken = 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;
}
} }

View file

@ -61,11 +61,13 @@ public class KeycloakDeploymentBuilder {
deployment.setBearerOnly(adapterConfig.isBearerOnly()); deployment.setBearerOnly(adapterConfig.isBearerOnly());
deployment.setAlwaysRefreshToken(adapterConfig.isAlwaysRefreshToken()); deployment.setAlwaysRefreshToken(adapterConfig.isAlwaysRefreshToken());
deployment.setRegisterNodeAtStartup(adapterConfig.isRegisterNodeAtStartup());
deployment.setRegisterNodePeriod(adapterConfig.getRegisterNodePeriod());
if (realmKeyPem == null && adapterConfig.isBearerOnly() && adapterConfig.getAuthServerUrl() == null) { 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"); 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)); deployment.setClient(new HttpClientBuilder().build(adapterConfig));
} }
if (adapterConfig.getAuthServerUrl() == null && (!deployment.isBearerOnly() || realmKeyPem == null)) { if (adapterConfig.getAuthServerUrl() == null && (!deployment.isBearerOnly() || realmKeyPem == null)) {

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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;
}
}
}

View file

@ -101,8 +101,8 @@ public class ServerRequest {
formparams.add(new BasicNameValuePair(OAuth2Constants.CODE, code)); formparams.add(new BasicNameValuePair(OAuth2Constants.CODE, code));
formparams.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, redirectUri)); formparams.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, redirectUri));
if (sessionId != null) { if (sessionId != null) {
formparams.add(new BasicNameValuePair(AdapterConstants.HTTP_SESSION_ID, sessionId)); formparams.add(new BasicNameValuePair(AdapterConstants.APPLICATION_SESSION_STATE, sessionId));
formparams.add(new BasicNameValuePair(AdapterConstants.HTTP_SESSION_HOST, HostUtils.getIpAddress())); formparams.add(new BasicNameValuePair(AdapterConstants.APPLICATION_SESSION_HOST, HostUtils.getIpAddress()));
} }
HttpResponse response = null; HttpResponse response = null;
HttpPost post = new HttpPost(codeUrl); 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<String, String> 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<String, String> 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<String, String> credentials) throws HttpFailure, IOException {
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
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 { public static void error(int status, HttpEntity entity) throws HttpFailure, IOException {
String body = null; String body = null;
if (entity != null) { if (entity != null) {

View file

@ -21,6 +21,7 @@ import org.keycloak.adapters.AuthOutcome;
import org.keycloak.adapters.HttpFacade; import org.keycloak.adapters.HttpFacade;
import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder; import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.NodesRegistrationLifecycle;
import org.keycloak.adapters.PreAuthActionsHandler; import org.keycloak.adapters.PreAuthActionsHandler;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext; 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); private static final Logger log = Logger.getLogger(KeycloakAuthenticatorValve.class);
protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement(); protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement();
protected AdapterDeploymentContext deploymentContext; protected AdapterDeploymentContext deploymentContext;
protected NodesRegistrationLifecycle nodesRegistrationLifecycle;
@Override @Override
@ -74,7 +76,11 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
@Override @Override
public void lifecycleEvent(LifecycleEvent event) { 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) { private static InputStream getJSONFromServletContext(ServletContext servletContext) {
@ -119,6 +125,13 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
context.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext); context.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext);
AuthenticatedActionsValve actions = new AuthenticatedActionsValve(deploymentContext, getNext(), getContainer(), getController()); AuthenticatedActionsValve actions = new AuthenticatedActionsValve(deploymentContext, getNext(), getContainer(), getController());
setNext(actions); setNext(actions);
nodesRegistrationLifecycle = new NodesRegistrationLifecycle(kd);
nodesRegistrationLifecycle.start();
}
protected void beforeStop() {
nodesRegistrationLifecycle.stop();
} }
@Override @Override

View file

@ -20,6 +20,7 @@ import org.keycloak.adapters.AuthOutcome;
import org.keycloak.adapters.HttpFacade; import org.keycloak.adapters.HttpFacade;
import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder; import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.NodesRegistrationLifecycle;
import org.keycloak.adapters.PreAuthActionsHandler; import org.keycloak.adapters.PreAuthActionsHandler;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext; import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.adapters.ServerRequest; 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); private final static Logger log = Logger.getLogger(""+KeycloakAuthenticatorValve.class);
protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement(); protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement();
protected AdapterDeploymentContext deploymentContext; protected AdapterDeploymentContext deploymentContext;
protected NodesRegistrationLifecycle nodesRegistrationLifecycle;
@Override @Override
public void lifecycleEvent(LifecycleEvent event) { 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())) { } else if (Lifecycle.AFTER_START_EVENT.equals(event.getType())) {
initInternal(); 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); context.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext);
AuthenticatedActionsValve actions = new AuthenticatedActionsValve(deploymentContext, getNext(), getContainer(), getObjectName()); AuthenticatedActionsValve actions = new AuthenticatedActionsValve(deploymentContext, getNext(), getContainer(), getObjectName());
setNext(actions); setNext(actions);
nodesRegistrationLifecycle = new NodesRegistrationLifecycle(kd);
nodesRegistrationLifecycle.start();
}
protected void beforeStop() {
nodesRegistrationLifecycle.stop();
} }
private static InputStream getJSONFromServletContext(ServletContext servletContext) { private static InputStream getJSONFromServletContext(ServletContext servletContext) {

View file

@ -25,13 +25,18 @@ import io.undertow.server.handlers.form.FormParserFactory;
import io.undertow.servlet.ServletExtension; import io.undertow.servlet.ServletExtension;
import io.undertow.servlet.api.AuthMethodConfig; import io.undertow.servlet.api.AuthMethodConfig;
import io.undertow.servlet.api.DeploymentInfo; 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.LoginConfig;
import io.undertow.servlet.api.ServletSessionConfig; import io.undertow.servlet.api.ServletSessionConfig;
import io.undertow.servlet.util.ImmediateInstanceHandle;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.adapters.AdapterConstants; import org.keycloak.adapters.AdapterConstants;
import org.keycloak.adapters.AdapterDeploymentContext; import org.keycloak.adapters.AdapterDeploymentContext;
import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder; import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.NodesRegistrationLifecycle;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
@ -96,7 +101,7 @@ public class KeycloakServletExtension implements ServletExtension {
} }
log.debug("KeycloakServletException initialization"); log.debug("KeycloakServletException initialization");
InputStream is = getConfigInputStream(servletContext); InputStream is = getConfigInputStream(servletContext);
KeycloakDeployment deployment = null; final KeycloakDeployment deployment;
if (is == null) { if (is == null) {
log.warn("No adapter configuration. Keycloak is unconfigured and will deny all requests."); log.warn("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
deployment = new KeycloakDeployment(); deployment = new KeycloakDeployment();
@ -143,6 +148,17 @@ public class KeycloakServletExtension implements ServletExtension {
ServletSessionConfig cookieConfig = new ServletSessionConfig(); ServletSessionConfig cookieConfig = new ServletSessionConfig();
cookieConfig.setPath(deploymentInfo.getContextPath()); cookieConfig.setPath(deploymentInfo.getContextPath());
deploymentInfo.setServletSessionConfig(cookieConfig); deploymentInfo.setServletSessionConfig(cookieConfig);
deploymentInfo.addListener(new ListenerInfo(UndertowNodesRegistrationLifecycleWrapper.class, new InstanceFactory<UndertowNodesRegistrationLifecycleWrapper>() {
@Override
public InstanceHandle<UndertowNodesRegistrationLifecycleWrapper> createInstance() throws InstantiationException {
NodesRegistrationLifecycle nodesRegistration = new NodesRegistrationLifecycle(deployment);
UndertowNodesRegistrationLifecycleWrapper listener = new UndertowNodesRegistrationLifecycleWrapper(nodesRegistration);
return new ImmediateInstanceHandle<UndertowNodesRegistrationLifecycleWrapper>(listener);
}
}));
} }
protected ServletKeycloakAuthMech createAuthenticationMechanism(DeploymentInfo deploymentInfo, AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement userSessionManagement) { protected ServletKeycloakAuthMech createAuthenticationMechanism(DeploymentInfo deploymentInfo, AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement userSessionManagement) {

View file

@ -0,0 +1,28 @@
package org.keycloak.adapters.undertow;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import org.keycloak.adapters.NodesRegistrationLifecycle;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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();
}
}

View file

@ -1,6 +1,7 @@
package org.keycloak.models; package org.keycloak.models;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
/** /**
@ -37,4 +38,20 @@ public interface ApplicationModel extends RoleContainerModel, ClientModel {
boolean isBearerOnly(); boolean isBearerOnly();
void setBearerOnly(boolean only); void setBearerOnly(boolean only);
int getNodeReRegistrationTimeout();
void setNodeReRegistrationTimeout(int timeout);
Map<String, Integer> 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);
} }

View file

@ -2,6 +2,7 @@ package org.keycloak.models.entities;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -12,10 +13,13 @@ public class ApplicationEntity extends ClientEntity {
private String managementUrl; private String managementUrl;
private String baseUrl; private String baseUrl;
private boolean bearerOnly; private boolean bearerOnly;
private int nodeReRegistrationTimeout;
// We are using names of defaultRoles (not ids) // We are using names of defaultRoles (not ids)
private List<String> defaultRoles = new ArrayList<String>(); private List<String> defaultRoles = new ArrayList<String>();
private Map<String, Integer> registeredNodes;
public boolean isSurrogateAuthRequired() { public boolean isSurrogateAuthRequired() {
return surrogateAuthRequired; return surrogateAuthRequired;
} }
@ -55,5 +59,21 @@ public class ApplicationEntity extends ClientEntity {
public void setDefaultRoles(List<String> defaultRoles) { public void setDefaultRoles(List<String> defaultRoles) {
this.defaultRoles = defaultRoles; this.defaultRoles = defaultRoles;
} }
public int getNodeReRegistrationTimeout() {
return nodeReRegistrationTimeout;
}
public void setNodeReRegistrationTimeout(int nodeReRegistrationTimeout) {
this.nodeReRegistrationTimeout = nodeReRegistrationTimeout;
}
public Map<String, Integer> getRegisteredNodes() {
return registeredNodes;
}
public void setRegisteredNodes(Map<String, Integer> registeredNodes) {
this.registeredNodes = registeredNodes;
}
} }

View file

@ -222,6 +222,7 @@ public class ModelToRepresentation {
rep.setSurrogateAuthRequired(applicationModel.isSurrogateAuthRequired()); rep.setSurrogateAuthRequired(applicationModel.isSurrogateAuthRequired());
rep.setBaseUrl(applicationModel.getBaseUrl()); rep.setBaseUrl(applicationModel.getBaseUrl());
rep.setNotBefore(applicationModel.getNotBefore()); rep.setNotBefore(applicationModel.getNotBefore());
rep.setNodeReRegistrationTimeout(applicationModel.getNodeReRegistrationTimeout());
Set<String> redirectUris = applicationModel.getRedirectUris(); Set<String> redirectUris = applicationModel.getRedirectUris();
if (redirectUris != null) { if (redirectUris != null) {
@ -237,6 +238,10 @@ public class ModelToRepresentation {
rep.setDefaultRoles(applicationModel.getDefaultRoles().toArray(new String[0])); rep.setDefaultRoles(applicationModel.getDefaultRoles().toArray(new String[0]));
} }
if (!applicationModel.getRegisteredNodes().isEmpty()) {
rep.setRegisteredNodes(new HashMap<String, Integer>(applicationModel.getRegisteredNodes()));
}
return rep; return rep;
} }

View file

@ -374,8 +374,16 @@ public class RepresentationToModel {
if (resourceRep.isBearerOnly() != null) applicationModel.setBearerOnly(resourceRep.isBearerOnly()); if (resourceRep.isBearerOnly() != null) applicationModel.setBearerOnly(resourceRep.isBearerOnly());
if (resourceRep.isPublicClient() != null) applicationModel.setPublicClient(resourceRep.isPublicClient()); if (resourceRep.isPublicClient() != null) applicationModel.setPublicClient(resourceRep.isPublicClient());
if (resourceRep.getProtocol() != null) applicationModel.setProtocol(resourceRep.getProtocol()); if (resourceRep.getProtocol() != null) applicationModel.setProtocol(resourceRep.getProtocol());
if (resourceRep.isFullScopeAllowed() != null) applicationModel.setFullScopeAllowed(resourceRep.isFullScopeAllowed()); if (resourceRep.isFullScopeAllowed() != null) {
else applicationModel.setFullScopeAllowed(true); applicationModel.setFullScopeAllowed(resourceRep.isFullScopeAllowed());
} else {
applicationModel.setFullScopeAllowed(true);
}
if (resourceRep.getNodeReRegistrationTimeout() != null) {
applicationModel.setNodeReRegistrationTimeout(resourceRep.getNodeReRegistrationTimeout());
} else {
applicationModel.setNodeReRegistrationTimeout(-1);
}
applicationModel.updateApplication(); applicationModel.updateApplication();
if (resourceRep.getNotBefore() != null) { if (resourceRep.getNotBefore() != null) {
@ -426,6 +434,12 @@ public class RepresentationToModel {
} }
} }
if (resourceRep.getRegisteredNodes() != null) {
for (Map.Entry<String, Integer> entry : resourceRep.getRegisteredNodes().entrySet()) {
applicationModel.registerNode(entry.getKey(), entry.getValue());
}
}
if (addDefaultRoles && resourceRep.getDefaultRoles() != null) { if (addDefaultRoles && resourceRep.getDefaultRoles() != null) {
applicationModel.updateDefaultRoles(resourceRep.getDefaultRoles()); applicationModel.updateDefaultRoles(resourceRep.getDefaultRoles());
} }
@ -448,6 +462,7 @@ public class RepresentationToModel {
if (rep.getAdminUrl() != null) resource.setManagementUrl(rep.getAdminUrl()); if (rep.getAdminUrl() != null) resource.setManagementUrl(rep.getAdminUrl());
if (rep.getBaseUrl() != null) resource.setBaseUrl(rep.getBaseUrl()); if (rep.getBaseUrl() != null) resource.setBaseUrl(rep.getBaseUrl());
if (rep.isSurrogateAuthRequired() != null) resource.setSurrogateAuthRequired(rep.isSurrogateAuthRequired()); if (rep.isSurrogateAuthRequired() != null) resource.setSurrogateAuthRequired(rep.isSurrogateAuthRequired());
if (rep.getNodeReRegistrationTimeout() != null) resource.setNodeReRegistrationTimeout(rep.getNodeReRegistrationTimeout());
resource.updateApplication(); resource.updateApplication();
if (rep.getProtocol() != null) resource.setProtocol(rep.getProtocol()); if (rep.getProtocol() != null) resource.setProtocol(rep.getProtocol());
@ -475,6 +490,12 @@ public class RepresentationToModel {
resource.setWebOrigins(new HashSet<String>(webOrigins)); resource.setWebOrigins(new HashSet<String>(webOrigins));
} }
if (rep.getRegisteredNodes() != null) {
for (Map.Entry<String, Integer> entry : rep.getRegisteredNodes().entrySet()) {
resource.registerNode(entry.getKey(), entry.getValue());
}
}
if (rep.getClaims() != null) { if (rep.getClaims() != null) {
setClaims(resource, rep.getClaims()); setClaims(resource, rep.getClaims());
} }

View file

@ -9,6 +9,7 @@ import org.keycloak.models.cache.entities.CachedApplication;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
/** /**
@ -185,6 +186,36 @@ public class ApplicationAdapter extends ClientAdapter implements ApplicationMode
return roles; 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<String, Integer> 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 @Override
public boolean hasScope(RoleModel role) { public boolean hasScope(RoleModel role) {
if (super.hasScope(role)) { if (super.hasScope(role)) {

View file

@ -10,6 +10,7 @@ import java.util.HashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.TreeMap;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -22,6 +23,8 @@ public class CachedApplication extends CachedClient {
private List<String> defaultRoles = new LinkedList<String>(); private List<String> defaultRoles = new LinkedList<String>();
private boolean bearerOnly; private boolean bearerOnly;
private Map<String, String> roles = new HashMap<String, String>(); private Map<String, String> roles = new HashMap<String, String>();
private int nodeReRegistrationTimeout;
private Map<String, Integer> registeredNodes;
public CachedApplication(RealmCache cache, RealmProvider delegate, RealmModel realm, ApplicationModel model) { public CachedApplication(RealmCache cache, RealmProvider delegate, RealmModel realm, ApplicationModel model) {
super(cache, delegate, realm, model); super(cache, delegate, realm, model);
@ -35,7 +38,8 @@ public class CachedApplication extends CachedClient {
cache.addCachedRole(new CachedApplicationRole(id, role, realm)); cache.addCachedRole(new CachedApplicationRole(id, role, realm));
} }
nodeReRegistrationTimeout = model.getNodeReRegistrationTimeout();
registeredNodes = new TreeMap<String, Integer>(model.getRegisteredNodes());
} }
public boolean isSurrogateAuthRequired() { public boolean isSurrogateAuthRequired() {
@ -62,4 +66,11 @@ public class CachedApplication extends CachedClient {
return roles; return roles;
} }
public int getNodeReRegistrationTimeout() {
return nodeReRegistrationTimeout;
}
public Map<String, Integer> getRegisteredNodes() {
return registeredNodes;
}
} }

View file

@ -9,6 +9,7 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.jpa.entities.ApplicationEntity; import org.keycloak.models.jpa.entities.ApplicationEntity;
import org.keycloak.models.jpa.entities.RoleEntity; import org.keycloak.models.jpa.entities.RoleEntity;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.util.Time;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import javax.persistence.TypedQuery; import javax.persistence.TypedQuery;
@ -16,6 +17,7 @@ import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
/** /**
@ -260,6 +262,35 @@ public class ApplicationAdapter extends ClientAdapter implements ApplicationMode
em.flush(); em.flush();
} }
@Override
public int getNodeReRegistrationTimeout() {
return applicationEntity.getNodeReRegistrationTimeout();
}
@Override
public void setNodeReRegistrationTimeout(int timeout) {
applicationEntity.setNodeReRegistrationTimeout(timeout);
}
@Override
public Map<String, Integer> getRegisteredNodes() {
return applicationEntity.getRegisteredNodes();
}
@Override
public void registerNode(String nodeHost, int registrationTime) {
Map<String, Integer> currentNodes = getRegisteredNodes();
currentNodes.put(nodeHost, registrationTime);
em.flush();
}
@Override
public void unregisterNode(String nodeHost) {
Map<String, Integer> currentNodes = getRegisteredNodes();
currentNodes.remove(nodeHost);
em.flush();
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View file

@ -1,14 +1,19 @@
package org.keycloak.models.jpa.entities; package org.keycloak.models.jpa.entities;
import javax.persistence.CascadeType; import javax.persistence.CascadeType;
import javax.persistence.CollectionTable;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.FetchType; import javax.persistence.FetchType;
import javax.persistence.JoinColumn; import javax.persistence.JoinColumn;
import javax.persistence.JoinTable; import javax.persistence.JoinTable;
import javax.persistence.MapKeyColumn;
import javax.persistence.OneToMany; import javax.persistence.OneToMany;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -29,6 +34,9 @@ public class ApplicationEntity extends ClientEntity {
@Column(name="BEARER_ONLY") @Column(name="BEARER_ONLY")
private boolean bearerOnly; private boolean bearerOnly;
@Column(name="NODE_REREG_TIMEOUT")
private int nodeReRegistrationTimeout;
@OneToMany(fetch = FetchType.EAGER, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "application") @OneToMany(fetch = FetchType.EAGER, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "application")
Collection<RoleEntity> roles = new ArrayList<RoleEntity>(); Collection<RoleEntity> roles = new ArrayList<RoleEntity>();
@ -36,6 +44,12 @@ public class ApplicationEntity extends ClientEntity {
@JoinTable(name="APPLICATION_DEFAULT_ROLES", joinColumns = { @JoinColumn(name="APPLICATION_ID")}, inverseJoinColumns = { @JoinColumn(name="ROLE_ID")}) @JoinTable(name="APPLICATION_DEFAULT_ROLES", joinColumns = { @JoinColumn(name="APPLICATION_ID")}, inverseJoinColumns = { @JoinColumn(name="ROLE_ID")})
Collection<RoleEntity> defaultRoles = new ArrayList<RoleEntity>(); Collection<RoleEntity> defaultRoles = new ArrayList<RoleEntity>();
@ElementCollection
@MapKeyColumn(name="NAME")
@Column(name="VALUE")
@CollectionTable(name="APP_NODE_REGISTRATIONS", joinColumns={ @JoinColumn(name="APPLICATION_ID") })
Map<String, Integer> registeredNodes = new HashMap<String, Integer>();
public boolean isSurrogateAuthRequired() { public boolean isSurrogateAuthRequired() {
return surrogateAuthRequired; return surrogateAuthRequired;
} }
@ -83,4 +97,20 @@ public class ApplicationEntity extends ClientEntity {
public void setBearerOnly(boolean bearerOnly) { public void setBearerOnly(boolean bearerOnly) {
this.bearerOnly = bearerOnly; this.bearerOnly = bearerOnly;
} }
public int getNodeReRegistrationTimeout() {
return nodeReRegistrationTimeout;
}
public void setNodeReRegistrationTimeout(int nodeReRegistrationTimeout) {
this.nodeReRegistrationTimeout = nodeReRegistrationTimeout;
}
public Map<String, Integer> getRegisteredNodes() {
return registeredNodes;
}
public void setRegisteredNodes(Map<String, Integer> registeredNodes) {
this.registeredNodes = registeredNodes;
}
} }

View file

@ -11,10 +11,14 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.mongo.keycloak.entities.MongoApplicationEntity; import org.keycloak.models.mongo.keycloak.entities.MongoApplicationEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity; import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity;
import org.keycloak.models.mongo.utils.MongoModelUtils; import org.keycloak.models.mongo.utils.MongoModelUtils;
import org.keycloak.util.Time;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
/** /**
@ -218,6 +222,41 @@ public class ApplicationAdapter extends ClientAdapter<MongoApplicationEntity> im
updateMongoEntity(); updateMongoEntity();
} }
@Override
public int getNodeReRegistrationTimeout() {
return getMongoEntity().getNodeReRegistrationTimeout();
}
@Override
public void setNodeReRegistrationTimeout(int timeout) {
getMongoEntity().setNodeReRegistrationTimeout(timeout);
updateMongoEntity();
}
@Override
public Map<String, Integer> getRegisteredNodes() {
return getMongoEntity().getRegisteredNodes() == null ? Collections.<String, Integer>emptyMap() : Collections.unmodifiableMap(getMongoEntity().getRegisteredNodes());
}
@Override
public void registerNode(String nodeHost, int registrationTime) {
MongoApplicationEntity entity = getMongoEntity();
if (entity.getRegisteredNodes() == null) {
entity.setRegisteredNodes(new HashMap<String, Integer>());
}
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 @Override
public boolean equals(Object o) { public boolean equals(Object o) {

View file

@ -137,7 +137,7 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
@Override @Override
public Map<String, String> getAttributes() { public Map<String, String> getAttributes() {
return user.getAttributes()==null ? Collections.EMPTY_MAP : Collections.unmodifiableMap(user.getAttributes()); return user.getAttributes()==null ? Collections.<String, String>emptyMap() : Collections.unmodifiableMap(user.getAttributes());
} }
public MongoUserEntity getUser() { public MongoUserEntity getUser() {

View file

@ -23,8 +23,6 @@ package org.keycloak.protocol.oidc;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.resteasy.client.core.executors.ApacheHttpClient4Executor; 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.OAuth2Constants;
import org.keycloak.models.ApplicationModel; import org.keycloak.models.ApplicationModel;
import org.keycloak.models.ClientSessionModel; import org.keycloak.models.ClientSessionModel;
@ -142,7 +140,7 @@ public class OpenIDConnect implements LoginProtocol {
ApacheHttpClient4Executor executor = ResourceAdminManager.createExecutor(); ApacheHttpClient4Executor executor = ResourceAdminManager.createExecutor();
try { try {
new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, app, clientSession, executor, 0); new ResourceAdminManager().logoutClientSession(uriInfo.getRequestUri(), realm, app, clientSession, executor);
} finally { } finally {
executor.getHttpClient().getConnectionManager().shutdown(); executor.getHttpClient().getConnectionManager().shutdown();
} }

View file

@ -37,7 +37,6 @@ import org.keycloak.services.ForbiddenException;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus; import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus;
import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.Cors; import org.keycloak.services.resources.Cors;
import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.resources.flows.Flows; 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.OPTIONS;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam; import javax.ws.rs.QueryParam;
import javax.ws.rs.core.CacheControl; import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
@ -612,15 +609,15 @@ public class OpenIDConnectService {
.build(); .build();
} }
String httpSessionId = formData.getFirst(AdapterConstants.HTTP_SESSION_ID); String adapterSessionId = formData.getFirst(AdapterConstants.APPLICATION_SESSION_STATE);
if (httpSessionId != null) { if (adapterSessionId != null) {
String httpSessionHost = formData.getFirst(AdapterConstants.HTTP_SESSION_HOST); String adapterSessionHost = formData.getFirst(AdapterConstants.APPLICATION_SESSION_HOST);
logger.infof("Http Session '%s' saved in ClientSession for client '%s'. Host is '%s'", httpSessionId, client.getClientId(), httpSessionHost); 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); event.detail(AdapterConstants.APPLICATION_SESSION_STATE, adapterSessionId);
clientSession.setNote(AdapterConstants.HTTP_SESSION_ID, httpSessionId); clientSession.setNote(AdapterConstants.APPLICATION_SESSION_STATE, adapterSessionId);
event.detail(AdapterConstants.HTTP_SESSION_HOST, httpSessionHost); event.detail(AdapterConstants.APPLICATION_SESSION_HOST, adapterSessionHost);
clientSession.setNote(AdapterConstants.HTTP_SESSION_HOST, httpSessionHost); clientSession.setNote(AdapterConstants.APPLICATION_SESSION_HOST, adapterSessionHost);
} }
AccessToken token = tokenManager.createClientAccessToken(accessCode.getRequestedRoles(), realm, client, user, userSession); AccessToken token = tokenManager.createClientAccessToken(accessCode.getRequestedRoles(), realm, client, user, userSession);
@ -646,6 +643,21 @@ public class OpenIDConnectService {
} }
protected ClientModel authorizeClient(String authorizationHeader, MultivaluedMap<String, String> formData, EventBuilder event) { protected ClientModel authorizeClient(String authorizationHeader, MultivaluedMap<String, String> formData, EventBuilder event) {
ClientModel client = authorizeClientBase(authorizationHeader, formData, event, realm);
if ( (client instanceof ApplicationModel) && ((ApplicationModel)client).isBearerOnly()) {
Map<String, String> error = new HashMap<String, String>();
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<String, String> formData, EventBuilder event, RealmModel realm) {
String client_id; String client_id;
String clientSecret; String clientSecret;
if (authorizationHeader != null) { 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()); 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<String, String> error = new HashMap<String, String>();
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 (!client.isPublicClient()) {
if (clientSecret == null || !client.validateSecret(clientSecret)) { if (clientSecret == null || !client.validateSecret(clientSecret)) {
Map<String, String> error = new HashMap<String, String>(); Map<String, String> error = new HashMap<String, String>();
@ -702,6 +706,7 @@ public class OpenIDConnectService {
throw new BadRequestException("Unauthorized Client", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build()); throw new BadRequestException("Unauthorized Client", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build());
} }
} }
return client; return client;
} }

View file

@ -9,10 +9,16 @@ import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.adapters.config.BaseRealmConfig; import org.keycloak.representations.adapters.config.BaseRealmConfig;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.util.Time;
import java.net.URI; import java.net.URI;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -46,6 +52,38 @@ public class ApplicationManager {
} }
} }
public Set<String> validateRegisteredNodes(ApplicationModel application) {
Map<String, Integer> registeredNodes = application.getRegisteredNodes();
if (registeredNodes == null || registeredNodes.isEmpty()) {
return Collections.emptySet();
}
int currentTime = Time.currentTime();
Set<String> validatedNodes = new TreeSet<String>();
if (application.getNodeReRegistrationTimeout() > 0) {
List<String> toRemove = new LinkedList<String>();
for (Map.Entry<String, Integer> 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", @JsonPropertyOrder({"realm", "realm-public-key", "bearer-only", "auth-server-url", "ssl-required",
"resource", "public-client", "credentials", "resource", "public-client", "credentials",
"use-resource-role-mappings"}) "use-resource-role-mappings"})

View file

@ -20,6 +20,7 @@ import org.keycloak.representations.adapters.action.PushNotBeforeAction;
import org.keycloak.representations.adapters.action.UserStats; import org.keycloak.representations.adapters.action.UserStats;
import org.keycloak.services.util.HttpClientBuilder; import org.keycloak.services.util.HttpClientBuilder;
import org.keycloak.services.util.ResolveRelative; import org.keycloak.services.util.ResolveRelative;
import org.keycloak.util.KeycloakUriBuilder;
import org.keycloak.util.MultivaluedHashMap; import org.keycloak.util.MultivaluedHashMap;
import org.keycloak.util.StringPropertyReplacer; import org.keycloak.util.StringPropertyReplacer;
import org.keycloak.util.Time; import org.keycloak.util.Time;
@ -29,11 +30,11 @@ import javax.ws.rs.core.UriBuilder;
import java.net.URI; import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.Collections;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.TreeMap;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -41,7 +42,7 @@ import java.util.TreeMap;
*/ */
public class ResourceAdminManager { public class ResourceAdminManager {
protected static Logger logger = Logger.getLogger(ResourceAdminManager.class); 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() { public static ApacheHttpClient4Executor createExecutor() {
HttpClient client = new HttpClientBuilder() HttpClient client = new HttpClientBuilder()
@ -63,6 +64,29 @@ public class ResourceAdminManager {
return StringPropertyReplacer.replaceProperties(absoluteURI); return StringPropertyReplacer.replaceProperties(absoluteURI);
} }
private List<String> getAllManagementUrls(URI requestUri, ApplicationModel application) {
String baseMgmtUrl = getManagementUrl(requestUri, application);
if (baseMgmtUrl == null) {
return Collections.emptyList();
}
Set<String> registeredNodesHosts = new ApplicationManager().validateRegisteredNodes(application);
// No-cluster setup
if (registeredNodesHosts.isEmpty()) {
return Arrays.asList(baseMgmtUrl);
}
List<String> result = new LinkedList<String>();
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) { public void logoutUser(URI requestUri, RealmModel realm, UserModel user, KeycloakSession keycloakSession) {
List<UserSessionModel> userSessions = keycloakSession.sessions().getUserSessions(realm, user); List<UserSessionModel> userSessions = keycloakSession.sessions().getUserSessions(realm, user);
logoutUserSessions(requestUri, realm, userSessions); logoutUserSessions(requestUri, realm, userSessions);
@ -82,7 +106,7 @@ public class ResourceAdminManager {
logger.infov("logging out resources: " + clientSessions); logger.infov("logging out resources: " + clientSessions);
for (Map.Entry<ApplicationModel, List<ClientSessionModel>> entry : clientSessions.entrySet()) { for (Map.Entry<ApplicationModel, List<ClientSessionModel>> entry : clientSessions.entrySet()) {
logoutApplication(requestUri, realm, entry.getKey(), entry.getValue(), executor, 0); logoutClientSessions(requestUri, realm, entry.getKey(), entry.getValue(), executor);
} }
} finally { } finally {
executor.getHttpClient().getConnectionManager().shutdown(); executor.getHttpClient().getConnectionManager().shutdown();
@ -108,34 +132,18 @@ public class ResourceAdminManager {
logger.debugv("logging out {0} resources ", clientSessions.size()); logger.debugv("logging out {0} resources ", clientSessions.size());
for (Map.Entry<ApplicationModel, List<ClientSessionModel>> entry : clientSessions.entrySet()) { for (Map.Entry<ApplicationModel, List<ClientSessionModel>> entry : clientSessions.entrySet()) {
logoutApplication(requestUri, realm, entry.getKey(), entry.getValue(), executor, 0); logoutClientSessions(requestUri, realm, entry.getKey(), entry.getValue(), executor);
} }
} finally { } finally {
executor.getHttpClient().getConnectionManager().shutdown(); 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(); ApacheHttpClient4Executor executor = createExecutor();
try { try {
realm.setNotBefore(Time.currentTime()); List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, user);
List<ApplicationModel> resources = realm.getApplications();
logger.debugv("logging out {0} resources ", resources.size());
for (ApplicationModel resource : resources) {
logoutApplication(requestUri, realm, resource, (List<ClientSessionModel>)null, executor, realm.getNotBefore());
}
} finally {
executor.getHttpClient().getConnectionManager().shutdown();
}
}
public void logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, List<UserSessionModel> userSessions) {
ApacheHttpClient4Executor executor = createExecutor();
try {
resource.setNotBefore(Time.currentTime());
List<ClientSessionModel> ourAppClientSessions = null; List<ClientSessionModel> ourAppClientSessions = null;
if (userSessions != null) { if (userSessions != null) {
MultivaluedHashMap<ApplicationModel, ClientSessionModel> clientSessions = new MultivaluedHashMap<ApplicationModel, ClientSessionModel>(); MultivaluedHashMap<ApplicationModel, ClientSessionModel> clientSessions = new MultivaluedHashMap<ApplicationModel, ClientSessionModel>();
@ -145,18 +153,18 @@ public class ResourceAdminManager {
ourAppClientSessions = clientSessions.get(resource); ourAppClientSessions = clientSessions.get(resource);
} }
logoutApplication(requestUri, realm, resource, ourAppClientSessions, executor, resource.getNotBefore()); logoutClientSessions(requestUri, realm, resource, ourAppClientSessions, executor);
} finally { } finally {
executor.getHttpClient().getConnectionManager().shutdown(); executor.getHttpClient().getConnectionManager().shutdown();
} }
} }
public boolean logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, ClientSessionModel clientSession, ApacheHttpClient4Executor client, int notBefore) { public boolean logoutClientSession(URI requestUri, RealmModel realm, ApplicationModel resource, ClientSessionModel clientSession, ApacheHttpClient4Executor client) {
return logoutApplication(requestUri, realm, resource, Arrays.asList(clientSession), client, notBefore); return logoutClientSessions(requestUri, realm, resource, Arrays.asList(clientSession), client);
} }
protected boolean logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, List<ClientSessionModel> clientSessions, ApacheHttpClient4Executor client, int notBefore) { protected boolean logoutClientSessions(URI requestUri, RealmModel realm, ApplicationModel resource, List<ClientSessionModel> clientSessions, ApacheHttpClient4Executor client) {
String managementUrl = getManagementUrl(requestUri, resource); String managementUrl = getManagementUrl(requestUri, resource);
if (managementUrl != null) { if (managementUrl != null) {
@ -165,22 +173,22 @@ public class ResourceAdminManager {
if (clientSessions != null && clientSessions.size() > 0) { if (clientSessions != null && clientSessions.size() > 0) {
adapterSessionIds = new MultivaluedHashMap<String, String>(); adapterSessionIds = new MultivaluedHashMap<String, String>();
for (ClientSessionModel clientSession : clientSessions) { for (ClientSessionModel clientSession : clientSessions) {
String adapterSessionId = clientSession.getNote(AdapterConstants.HTTP_SESSION_ID); String adapterSessionId = clientSession.getNote(AdapterConstants.APPLICATION_SESSION_STATE);
if (adapterSessionId != null) { if (adapterSessionId != null) {
String host = clientSession.getNote(AdapterConstants.HTTP_SESSION_HOST); String host = clientSession.getNote(AdapterConstants.APPLICATION_SESSION_HOST);
adapterSessionIds.add(host, adapterSessionId); adapterSessionIds.add(host, adapterSessionId);
} }
} }
} }
if (managementUrl.contains(KC_SESSION_HOST) && adapterSessionIds != null) { if (managementUrl.contains(APPLICATION_SESSION_HOST_PROPERTY) && adapterSessionIds != null) {
boolean allPassed = true; boolean allPassed = true;
// Send logout separately to each host (needed for single-sign-out in cluster for non-distributable apps - KEYCLOAK-748) // Send logout separately to each host (needed for single-sign-out in cluster for non-distributable apps - KEYCLOAK-748)
for (Map.Entry<String, List<String>> entry : adapterSessionIds.entrySet()) { for (Map.Entry<String, List<String>> entry : adapterSessionIds.entrySet()) {
String host = entry.getKey(); String host = entry.getKey();
List<String> sessionIds = entry.getValue(); List<String> sessionIds = entry.getValue();
String currentHostMgmtUrl = managementUrl.replace(KC_SESSION_HOST, host); String currentHostMgmtUrl = managementUrl.replace(APPLICATION_SESSION_HOST_PROPERTY, host);
allPassed = logoutApplicationOnHost(realm, resource, sessionIds, client, notBefore, currentHostMgmtUrl) && allPassed; allPassed = sendLogoutRequest(realm, resource, sessionIds, client, 0, currentHostMgmtUrl) && allPassed;
} }
return allPassed; return allPassed;
@ -193,7 +201,7 @@ public class ResourceAdminManager {
allSessionIds.addAll(currentIds); allSessionIds.addAll(currentIds);
} }
} }
return logoutApplicationOnHost(realm, resource, allSessionIds, client, notBefore, managementUrl); return sendLogoutRequest(realm, resource, allSessionIds, client, 0, managementUrl);
} }
} else { } else {
logger.debugv("Can't logout {0}: no management url", resource.getName()); 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<String> 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<ApplicationModel> 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<String> 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<String> adapterSessionIds, ApacheHttpClient4Executor client, int notBefore, String managementUrl) {
LogoutAction adminAction = new LogoutAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getName(), adapterSessionIds, notBefore); LogoutAction adminAction = new LogoutAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getName(), adapterSessionIds, notBefore);
String token = new TokenManager().encodeToken(realm, adminAction); String token = new TokenManager().encodeToken(realm, adminAction);
logger.infov("logout resource {0} url: {1} sessionIds: " + adapterSessionIds, resource.getName(), managementUrl); 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) { protected boolean pushRevocationPolicy(URI requestUri, RealmModel realm, ApplicationModel resource, int notBefore, ApacheHttpClient4Executor executor) {
if (notBefore <= 0) return false; List<String> mgmtUrls = getAllManagementUrls(requestUri, resource);
String managementUrl = getManagementUrl(requestUri, resource); if (mgmtUrls.isEmpty()) {
if (managementUrl != null) { logger.debug("No management URL or no registered cluster nodes for the application " + resource.getName());
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());
return false; 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();
}
} }
} }

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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<String, String> 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<String, String> 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<String, String> formData) {
ClientModel client = OpenIDConnectService.authorizeClientBase(authorizationHeader, formData, event, realm);
if (client.isPublicClient()) {
Map<String, String> error = new HashMap<String, String>();
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<String, String> error = new HashMap<String, String>();
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<String, String> formData) {
String applicationClusterHost = formData.getFirst(AdapterConstants.APPLICATION_CLUSTER_HOST);
if (applicationClusterHost == null || applicationClusterHost.length() == 0) {
Map<String, String> error = new HashMap<String, String>();
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);
}
}
}

View file

@ -134,6 +134,16 @@ public class RealmsResource {
return service; 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) { protected RealmModel locateRealm(String name, RealmManager realmManager) {
RealmModel realm = realmManager.getRealmByName(name); RealmModel realm = realmManager.getRealmByName(name);

View file

@ -335,7 +335,7 @@ public class ApplicationResource {
@POST @POST
public void logoutAll() { public void logoutAll() {
auth.requireManage(); 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"); throw new NotFoundException("User not found");
} }
List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, user); new ResourceAdminManager().logoutUserFromApplication(uriInfo.getRequestUri(), realm, application, user, session);
new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, application, userSessions);
} }

View file

@ -29,7 +29,8 @@ sed -i -e 's/false/true/' admin-access.war/WEB-INF/web.xml
# Configure other examples # Configure other examples
for I in *.war/WEB-INF/keycloak.json; do 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; done;
# Enable distributable for customer-portal # Enable distributable for customer-portal
@ -37,6 +38,6 @@ sed -i -e 's/<\/module-name>/&\n <distributable \/>/' customer-portal.war/WEB
# Configure testrealm.json - Enable adminUrl to access adapters on local machine # 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\": \"\/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

View file

@ -23,9 +23,8 @@ function prepareHost
cp -r /keycloak-docker-cluster/deployments/* $JBOSS_HOME/standalone/deployments/ cp -r /keycloak-docker-cluster/deployments/* $JBOSS_HOME/standalone/deployments/
# Enable Infinispan provider # 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|\"provider\".*: \"mem\"|\"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 -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
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
# Deploy and configure examples # Deploy and configure examples
/keycloak-docker-cluster/shared-files/deploy-examples.sh /keycloak-docker-cluster/shared-files/deploy-examples.sh

View file

@ -45,6 +45,9 @@ public class ApplicationModelTest extends AbstractModelTest {
application.addWebOrigin("origin-1"); application.addWebOrigin("origin-1");
application.addWebOrigin("origin-2"); application.addWebOrigin("origin-2");
application.registerNode("node1", 10);
application.registerNode("10.20.30.40", 50);
application.updateApplication(); application.updateApplication();
} }
@ -84,6 +87,7 @@ public class ApplicationModelTest extends AbstractModelTest {
Assert.assertTrue(expected.getRedirectUris().containsAll(actual.getRedirectUris())); Assert.assertTrue(expected.getRedirectUris().containsAll(actual.getRedirectUris()));
Assert.assertTrue(expected.getWebOrigins().containsAll(actual.getWebOrigins())); Assert.assertTrue(expected.getWebOrigins().containsAll(actual.getWebOrigins()));
Assert.assertTrue(expected.getRegisteredNodes().equals(actual.getRegisteredNodes()));
} }
public static void assertEquals(List<RoleModel> expected, List<RoleModel> actual) { public static void assertEquals(List<RoleModel> expected, List<RoleModel> actual) {

View file

@ -97,6 +97,12 @@ public class ImportTest extends AbstractModelTest {
Assert.assertTrue(apps.values().contains(accountApp)); Assert.assertTrue(apps.values().contains(accountApp));
realm.getApplications().containsAll(apps.values()); realm.getApplications().containsAll(apps.values());
Assert.assertEquals(50, application.getNodeReRegistrationTimeout());
Map<String, Integer> 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 // Test finding applications by ID
Assert.assertNull(realm.getApplicationById("982734")); Assert.assertNull(realm.getApplicationById("982734"));
Assert.assertEquals(application, realm.getApplicationById(application.getId())); Assert.assertEquals(application, realm.getApplicationById(application.getId()));

View file

@ -94,7 +94,12 @@
"applications": [ "applications": [
{ {
"name": "Application", "name": "Application",
"enabled": true "enabled": true,
"nodeReRegistrationTimeout": 50,
"registeredNodes": {
"node1": 10,
"172.10.15.20": 20
}
}, },
{ {
"name": "OtherApp", "name": "OtherApp",