Merge pull request #773 from mposolda/dynamic-adapter-registration
KEYCLOAK-759 dynamic registration of managementUrls in cluster
This commit is contained in:
commit
0dae2240e5
43 changed files with 990 additions and 114 deletions
|
@ -23,6 +23,15 @@
|
|||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</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">
|
||||
<column name="AUTH_METHOD" type="VARCHAR(255)"/>
|
||||
</addColumn>
|
||||
|
@ -35,9 +44,14 @@
|
|||
<addColumn tableName="REALM">
|
||||
<column name="CERTIFICATE" type="VARCHAR(2048)"/>
|
||||
</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_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_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>
|
||||
</databaseChangeLog>
|
|
@ -8,7 +8,7 @@ import java.util.HashMap;
|
|||
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>
|
||||
*/
|
||||
|
@ -18,10 +18,10 @@ public class BasicDBObjectToMapMapper implements Mapper<BasicDBObject, Map> {
|
|||
public Map convertObject(MapperContext<BasicDBObject, Map> context) {
|
||||
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()) {
|
||||
String key = entry.getKey();
|
||||
String value = (String)entry.getValue();
|
||||
Object value = entry.getValue();
|
||||
|
||||
if (key.contains(MapMapper.DOT_PLACEHOLDER)) {
|
||||
key = key.replaceAll(MapMapper.DOT_PLACEHOLDER, ".");
|
||||
|
|
|
@ -8,7 +8,7 @@ import java.util.Map;
|
|||
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>
|
||||
*/
|
||||
|
@ -31,7 +31,7 @@ public class MapMapper<T extends Map> implements Mapper<T, BasicDBObject> {
|
|||
Set<Map.Entry> entries = objectToConvert.entrySet();
|
||||
for (Map.Entry entry : entries) {
|
||||
String key = (String)entry.getKey();
|
||||
String value = (String)entry.getValue();
|
||||
Object value = entry.getValue();
|
||||
|
||||
if (key.contains(".")) {
|
||||
key = key.replaceAll("\\.", DOT_PLACEHOLDER);
|
||||
|
|
|
@ -13,5 +13,7 @@ public interface ServiceUrlConstants {
|
|||
public static final String TOKEN_SERVICE_DIRECT_GRANT_PATH = "/realms/{realm-name}/protocol/openid-connect/grants/access";
|
||||
public static final String ACCOUNT_SERVICE_PATH = "/realms/{realm-name}/account";
|
||||
public static final String REALM_INFO_PATH = "/realms/{realm-name}";
|
||||
public static final String CLIENTS_MANAGEMENT_REGISTER_NODE_PATH = "/realms/{realm-name}/clients-managements/register-node";
|
||||
public static final String CLIENTS_MANAGEMENT_UNREGISTER_NODE_PATH = "/realms/{realm-name}/clients-managements/unregister-node";
|
||||
|
||||
}
|
||||
|
|
|
@ -20,8 +20,11 @@ public interface AdapterConstants {
|
|||
String AUTH_DATA_PARAM_NAME = "org.keycloak.json.adapterConfig";
|
||||
|
||||
// Attribute passed in codeToToken request from adapter to Keycloak and saved in ClientSession. Contains ID of HttpSession on adapter
|
||||
public static final String HTTP_SESSION_ID = "http_session_id";
|
||||
public static final String APPLICATION_SESSION_STATE = "application_session_state";
|
||||
|
||||
// Attribute passed in codeToToken request from adapter to Keycloak and saved in ClientSession. Contains hostname of adapter where HttpSession is served
|
||||
public static final String HTTP_SESSION_HOST = "http_session_host";
|
||||
public static final String APPLICATION_SESSION_HOST = "application_session_host";
|
||||
|
||||
// Attribute passed in registerNode request for register new application cluster node once he joined cluster
|
||||
public static final String APPLICATION_CLUSTER_HOST = "application_cluster_host";
|
||||
}
|
||||
|
|
|
@ -17,7 +17,8 @@ import org.codehaus.jackson.annotate.JsonPropertyOrder;
|
|||
"connection-pool-size",
|
||||
"allow-any-hostname", "disable-trust-manager", "truststore", "truststore-password",
|
||||
"client-keystore", "client-keystore-password", "client-key-password",
|
||||
"auth-server-url-for-backend-requests", "always-refresh-token"
|
||||
"auth-server-url-for-backend-requests", "always-refresh-token",
|
||||
"register-node-at-startup", "register-node-period"
|
||||
})
|
||||
public class AdapterConfig extends BaseAdapterConfig {
|
||||
|
||||
|
@ -41,6 +42,10 @@ public class AdapterConfig extends BaseAdapterConfig {
|
|||
protected String authServerUrlForBackendRequests;
|
||||
@JsonProperty("always-refresh-token")
|
||||
protected boolean alwaysRefreshToken = false;
|
||||
@JsonProperty("register-node-at-startup")
|
||||
protected boolean registerNodeAtStartup = false;
|
||||
@JsonProperty("register-node-period")
|
||||
protected int registerNodePeriod = -1;
|
||||
|
||||
public boolean isAllowAnyHostname() {
|
||||
return allowAnyHostname;
|
||||
|
@ -121,4 +126,20 @@ public class AdapterConfig extends BaseAdapterConfig {
|
|||
public void setAlwaysRefreshToken(boolean alwaysRefreshToken) {
|
||||
this.alwaysRefreshToken = alwaysRefreshToken;
|
||||
}
|
||||
|
||||
public boolean isRegisterNodeAtStartup() {
|
||||
return registerNodeAtStartup;
|
||||
}
|
||||
|
||||
public void setRegisterNodeAtStartup(boolean registerNodeAtStartup) {
|
||||
this.registerNodeAtStartup = registerNodeAtStartup;
|
||||
}
|
||||
|
||||
public int getRegisterNodePeriod() {
|
||||
return registerNodePeriod;
|
||||
}
|
||||
|
||||
public void setRegisterNodePeriod(int registerNodePeriod) {
|
||||
this.registerNodePeriod = registerNodePeriod;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,8 @@ public class ApplicationRepresentation {
|
|||
protected String protocol;
|
||||
protected Map<String, String> attributes;
|
||||
protected Boolean fullScopeAllowed;
|
||||
|
||||
protected Integer nodeReRegistrationTimeout;
|
||||
protected Map<String, Integer> registeredNodes;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
|
@ -162,4 +163,20 @@ public class ApplicationRepresentation {
|
|||
public void setAttributes(Map<String, String> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,8 @@ public enum EventType {
|
|||
SEND_RESET_PASSWORD,
|
||||
SEND_RESET_PASSWORD_ERROR,
|
||||
SOCIAL_LOGIN,
|
||||
SOCIAL_LOGIN_ERROR
|
||||
SOCIAL_LOGIN_ERROR,
|
||||
|
||||
REGISTER_NODE,
|
||||
UNREGISTER_NODE
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ public class AdminClient {
|
|||
HttpClient client = new HttpClientBuilder()
|
||||
.disableTrustManager().build();
|
||||
try {
|
||||
HttpGet get = new HttpGet(AdapterUtils.getBaseUrl(req.getRequestURL().toString(), session) + "/auth/admin/realms/demo/roles");
|
||||
HttpGet get = new HttpGet(AdapterUtils.getOrigin(req.getRequestURL().toString(), session) + "/auth/admin/realms/demo/roles");
|
||||
get.addHeader("Authorization", "Bearer " + session.getTokenString());
|
||||
try {
|
||||
HttpResponse response = client.execute(get);
|
||||
|
|
|
@ -50,7 +50,7 @@ public class CustomerDatabaseClient {
|
|||
HttpClient client = new HttpClientBuilder()
|
||||
.disableTrustManager().build();
|
||||
try {
|
||||
HttpGet get = new HttpGet(AdapterUtils.getBaseUrl(req.getRequestURL().toString(), session) + "/database/customers");
|
||||
HttpGet get = new HttpGet(AdapterUtils.getOrigin(req.getRequestURL().toString(), session) + "/database/customers");
|
||||
get.addHeader("Authorization", "Bearer " + session.getTokenString());
|
||||
try {
|
||||
HttpResponse response = client.execute(get);
|
||||
|
|
|
@ -40,7 +40,7 @@ public class ProductDatabaseClient
|
|||
HttpClient client = new HttpClientBuilder()
|
||||
.disableTrustManager().build();
|
||||
try {
|
||||
HttpGet get = new HttpGet(AdapterUtils.getBaseUrl(req.getRequestURL().toString(), session) + "/database/products");
|
||||
HttpGet get = new HttpGet(AdapterUtils.getOrigin(req.getRequestURL().toString(), session) + "/database/products");
|
||||
get.addHeader("Authorization", "Bearer " + session.getTokenString());
|
||||
try {
|
||||
HttpResponse response = client.execute(get);
|
||||
|
|
|
@ -157,6 +157,16 @@ public class AdapterDeploymentContext {
|
|||
return (this.accountUrl != null) ? this.accountUrl : delegate.getAccountUrl();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRegisterNodeUrl() {
|
||||
return (this.registerNodeUrl != null) ? this.registerNodeUrl : delegate.getRegisterNodeUrl();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUnregisterNodeUrl() {
|
||||
return (this.unregisterNodeUrl != null) ? this.unregisterNodeUrl : delegate.getUnregisterNodeUrl();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getResourceName() {
|
||||
return delegate.getResourceName();
|
||||
|
@ -336,6 +346,26 @@ public class AdapterDeploymentContext {
|
|||
public void setAlwaysRefreshToken(boolean alwaysRefreshToken) {
|
||||
delegate.setAlwaysRefreshToken(alwaysRefreshToken);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRegisterNodePeriod() {
|
||||
return delegate.getRegisterNodePeriod();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRegisterNodePeriod(int registerNodePeriod) {
|
||||
delegate.setRegisterNodePeriod(registerNodePeriod);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRegisterNodeAtStartup(boolean registerNodeAtStartup) {
|
||||
delegate.setRegisterNodeAtStartup(registerNodeAtStartup);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRegisterNodeAtStartup() {
|
||||
return delegate.isRegisterNodeAtStartup();
|
||||
}
|
||||
}
|
||||
|
||||
protected KeycloakUriBuilder getBaseBuilder(HttpFacade facade, String base) {
|
||||
|
|
|
@ -8,7 +8,7 @@ import org.keycloak.util.UriUtils;
|
|||
*/
|
||||
public class AdapterUtils {
|
||||
|
||||
public static String getBaseUrl(String browserRequestURL, KeycloakSecurityContext session) {
|
||||
public static String getOrigin(String browserRequestURL, KeycloakSecurityContext session) {
|
||||
if (session instanceof RefreshableKeycloakSecurityContext) {
|
||||
KeycloakDeployment deployment = ((RefreshableKeycloakSecurityContext)session).getDeployment();
|
||||
switch (deployment.getRelativeUrls()) {
|
||||
|
@ -16,10 +16,9 @@ public class AdapterUtils {
|
|||
// Resolve baseURI from the request
|
||||
return UriUtils.getOrigin(browserRequestURL);
|
||||
case BROWSER_ONLY:
|
||||
case NEVER:
|
||||
// Resolve baseURI from the codeURL (This is already non-relative and based on our hostname)
|
||||
return UriUtils.getOrigin(deployment.getCodeUrl());
|
||||
case NEVER:
|
||||
return "";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
|
|
@ -31,6 +31,8 @@ public class KeycloakDeployment {
|
|||
protected String refreshUrl;
|
||||
protected KeycloakUriBuilder logoutUrl;
|
||||
protected String accountUrl;
|
||||
protected String registerNodeUrl;
|
||||
protected String unregisterNodeUrl;
|
||||
|
||||
protected String resourceName;
|
||||
protected boolean bearerOnly;
|
||||
|
@ -48,6 +50,8 @@ public class KeycloakDeployment {
|
|||
protected String corsAllowedMethods;
|
||||
protected boolean exposeToken;
|
||||
protected boolean alwaysRefreshToken;
|
||||
protected boolean registerNodeAtStartup;
|
||||
protected int registerNodePeriod;
|
||||
protected volatile int notBefore;
|
||||
|
||||
public KeycloakDeployment() {
|
||||
|
@ -136,6 +140,8 @@ public class KeycloakDeployment {
|
|||
accountUrl = authUrlBuilder.clone().path(ServiceUrlConstants.ACCOUNT_SERVICE_PATH).build(getRealm()).toString();
|
||||
realmInfoUrl = authUrlBuilder.clone().path(ServiceUrlConstants.REALM_INFO_PATH).build(getRealm()).toString();
|
||||
codeUrl = authUrlBuilder.clone().path(ServiceUrlConstants.TOKEN_SERVICE_ACCESS_CODE_PATH).build(getRealm()).toString();
|
||||
registerNodeUrl = authUrlBuilder.clone().path(ServiceUrlConstants.CLIENTS_MANAGEMENT_REGISTER_NODE_PATH).build(getRealm()).toString();
|
||||
unregisterNodeUrl = authUrlBuilder.clone().path(ServiceUrlConstants.CLIENTS_MANAGEMENT_UNREGISTER_NODE_PATH).build(getRealm()).toString();
|
||||
}
|
||||
|
||||
public RelativeUrlsUsed getRelativeUrls() {
|
||||
|
@ -166,6 +172,14 @@ public class KeycloakDeployment {
|
|||
return accountUrl;
|
||||
}
|
||||
|
||||
public String getRegisterNodeUrl() {
|
||||
return registerNodeUrl;
|
||||
}
|
||||
|
||||
public String getUnregisterNodeUrl() {
|
||||
return unregisterNodeUrl;
|
||||
}
|
||||
|
||||
public void setResourceName(String resourceName) {
|
||||
this.resourceName = resourceName;
|
||||
}
|
||||
|
@ -289,4 +303,20 @@ public class KeycloakDeployment {
|
|||
public void setAlwaysRefreshToken(boolean alwaysRefreshToken) {
|
||||
this.alwaysRefreshToken = alwaysRefreshToken;
|
||||
}
|
||||
|
||||
public boolean isRegisterNodeAtStartup() {
|
||||
return registerNodeAtStartup;
|
||||
}
|
||||
|
||||
public void setRegisterNodeAtStartup(boolean registerNodeAtStartup) {
|
||||
this.registerNodeAtStartup = registerNodeAtStartup;
|
||||
}
|
||||
|
||||
public int getRegisterNodePeriod() {
|
||||
return registerNodePeriod;
|
||||
}
|
||||
|
||||
public void setRegisterNodePeriod(int registerNodePeriod) {
|
||||
this.registerNodePeriod = registerNodePeriod;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,11 +61,13 @@ public class KeycloakDeploymentBuilder {
|
|||
|
||||
deployment.setBearerOnly(adapterConfig.isBearerOnly());
|
||||
deployment.setAlwaysRefreshToken(adapterConfig.isAlwaysRefreshToken());
|
||||
deployment.setRegisterNodeAtStartup(adapterConfig.isRegisterNodeAtStartup());
|
||||
deployment.setRegisterNodePeriod(adapterConfig.getRegisterNodePeriod());
|
||||
|
||||
if (realmKeyPem == null && adapterConfig.isBearerOnly() && adapterConfig.getAuthServerUrl() == null) {
|
||||
throw new IllegalArgumentException("For bearer auth, you must set the realm-public-key or auth-server-url");
|
||||
}
|
||||
if (realmKeyPem == null || !deployment.isBearerOnly()) {
|
||||
if (realmKeyPem == null || !deployment.isBearerOnly() || deployment.isRegisterNodeAtStartup() || deployment.getRegisterNodePeriod() != -1) {
|
||||
deployment.setClient(new HttpClientBuilder().build(adapterConfig));
|
||||
}
|
||||
if (adapterConfig.getAuthServerUrl() == null && (!deployment.isBearerOnly() || realmKeyPem == null)) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -101,8 +101,8 @@ public class ServerRequest {
|
|||
formparams.add(new BasicNameValuePair(OAuth2Constants.CODE, code));
|
||||
formparams.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, redirectUri));
|
||||
if (sessionId != null) {
|
||||
formparams.add(new BasicNameValuePair(AdapterConstants.HTTP_SESSION_ID, sessionId));
|
||||
formparams.add(new BasicNameValuePair(AdapterConstants.HTTP_SESSION_HOST, HostUtils.getIpAddress()));
|
||||
formparams.add(new BasicNameValuePair(AdapterConstants.APPLICATION_SESSION_STATE, sessionId));
|
||||
formparams.add(new BasicNameValuePair(AdapterConstants.APPLICATION_SESSION_HOST, HostUtils.getIpAddress()));
|
||||
}
|
||||
HttpResponse response = null;
|
||||
HttpPost post = new HttpPost(codeUrl);
|
||||
|
@ -212,6 +212,46 @@ public class ServerRequest {
|
|||
}
|
||||
}
|
||||
|
||||
public static void invokeRegisterNode(KeycloakDeployment deployment, String host) throws HttpFailure, IOException {
|
||||
String registerNodeUrl = deployment.getRegisterNodeUrl();
|
||||
String client_id = deployment.getResourceName();
|
||||
Map<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 {
|
||||
String body = null;
|
||||
if (entity != null) {
|
||||
|
|
|
@ -21,6 +21,7 @@ import org.keycloak.adapters.AuthOutcome;
|
|||
import org.keycloak.adapters.HttpFacade;
|
||||
import org.keycloak.adapters.KeycloakDeployment;
|
||||
import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
||||
import org.keycloak.adapters.NodesRegistrationLifecycle;
|
||||
import org.keycloak.adapters.PreAuthActionsHandler;
|
||||
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
||||
|
||||
|
@ -46,6 +47,7 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
|
|||
private static final Logger log = Logger.getLogger(KeycloakAuthenticatorValve.class);
|
||||
protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement();
|
||||
protected AdapterDeploymentContext deploymentContext;
|
||||
protected NodesRegistrationLifecycle nodesRegistrationLifecycle;
|
||||
|
||||
|
||||
@Override
|
||||
|
@ -74,7 +76,11 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
|
|||
|
||||
@Override
|
||||
public void lifecycleEvent(LifecycleEvent event) {
|
||||
if (event.getType() == Lifecycle.AFTER_START_EVENT) init();
|
||||
if (event.getType() == Lifecycle.AFTER_START_EVENT) {
|
||||
init();
|
||||
} else if (event.getType() == Lifecycle.BEFORE_STOP_EVENT) {
|
||||
beforeStop();
|
||||
}
|
||||
}
|
||||
|
||||
private static InputStream getJSONFromServletContext(ServletContext servletContext) {
|
||||
|
@ -119,6 +125,13 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
|
|||
context.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext);
|
||||
AuthenticatedActionsValve actions = new AuthenticatedActionsValve(deploymentContext, getNext(), getContainer(), getController());
|
||||
setNext(actions);
|
||||
|
||||
nodesRegistrationLifecycle = new NodesRegistrationLifecycle(kd);
|
||||
nodesRegistrationLifecycle.start();
|
||||
}
|
||||
|
||||
protected void beforeStop() {
|
||||
nodesRegistrationLifecycle.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -20,6 +20,7 @@ import org.keycloak.adapters.AuthOutcome;
|
|||
import org.keycloak.adapters.HttpFacade;
|
||||
import org.keycloak.adapters.KeycloakDeployment;
|
||||
import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
||||
import org.keycloak.adapters.NodesRegistrationLifecycle;
|
||||
import org.keycloak.adapters.PreAuthActionsHandler;
|
||||
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
||||
import org.keycloak.adapters.ServerRequest;
|
||||
|
@ -47,6 +48,7 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
|
|||
private final static Logger log = Logger.getLogger(""+KeycloakAuthenticatorValve.class);
|
||||
protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement();
|
||||
protected AdapterDeploymentContext deploymentContext;
|
||||
protected NodesRegistrationLifecycle nodesRegistrationLifecycle;
|
||||
|
||||
@Override
|
||||
public void lifecycleEvent(LifecycleEvent event) {
|
||||
|
@ -58,6 +60,8 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
|
|||
}
|
||||
} else if (Lifecycle.AFTER_START_EVENT.equals(event.getType())) {
|
||||
initInternal();
|
||||
} else if (event.getType() == Lifecycle.BEFORE_STOP_EVENT) {
|
||||
beforeStop();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,6 +103,13 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
|
|||
context.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext);
|
||||
AuthenticatedActionsValve actions = new AuthenticatedActionsValve(deploymentContext, getNext(), getContainer(), getObjectName());
|
||||
setNext(actions);
|
||||
|
||||
nodesRegistrationLifecycle = new NodesRegistrationLifecycle(kd);
|
||||
nodesRegistrationLifecycle.start();
|
||||
}
|
||||
|
||||
protected void beforeStop() {
|
||||
nodesRegistrationLifecycle.stop();
|
||||
}
|
||||
|
||||
private static InputStream getJSONFromServletContext(ServletContext servletContext) {
|
||||
|
|
|
@ -25,13 +25,18 @@ import io.undertow.server.handlers.form.FormParserFactory;
|
|||
import io.undertow.servlet.ServletExtension;
|
||||
import io.undertow.servlet.api.AuthMethodConfig;
|
||||
import io.undertow.servlet.api.DeploymentInfo;
|
||||
import io.undertow.servlet.api.InstanceFactory;
|
||||
import io.undertow.servlet.api.InstanceHandle;
|
||||
import io.undertow.servlet.api.ListenerInfo;
|
||||
import io.undertow.servlet.api.LoginConfig;
|
||||
import io.undertow.servlet.api.ServletSessionConfig;
|
||||
import io.undertow.servlet.util.ImmediateInstanceHandle;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.adapters.AdapterConstants;
|
||||
import org.keycloak.adapters.AdapterDeploymentContext;
|
||||
import org.keycloak.adapters.KeycloakDeployment;
|
||||
import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
||||
import org.keycloak.adapters.NodesRegistrationLifecycle;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import java.io.ByteArrayInputStream;
|
||||
|
@ -96,7 +101,7 @@ public class KeycloakServletExtension implements ServletExtension {
|
|||
}
|
||||
log.debug("KeycloakServletException initialization");
|
||||
InputStream is = getConfigInputStream(servletContext);
|
||||
KeycloakDeployment deployment = null;
|
||||
final KeycloakDeployment deployment;
|
||||
if (is == null) {
|
||||
log.warn("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
|
||||
deployment = new KeycloakDeployment();
|
||||
|
@ -143,6 +148,17 @@ public class KeycloakServletExtension implements ServletExtension {
|
|||
ServletSessionConfig cookieConfig = new ServletSessionConfig();
|
||||
cookieConfig.setPath(deploymentInfo.getContextPath());
|
||||
deploymentInfo.setServletSessionConfig(cookieConfig);
|
||||
|
||||
deploymentInfo.addListener(new ListenerInfo(UndertowNodesRegistrationLifecycleWrapper.class, new InstanceFactory<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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package org.keycloak.models;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
|
@ -37,4 +38,20 @@ public interface ApplicationModel extends RoleContainerModel, ClientModel {
|
|||
boolean isBearerOnly();
|
||||
void setBearerOnly(boolean only);
|
||||
|
||||
int getNodeReRegistrationTimeout();
|
||||
|
||||
void setNodeReRegistrationTimeout(int timeout);
|
||||
|
||||
Map<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);
|
||||
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package org.keycloak.models.entities;
|
|||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -12,10 +13,13 @@ public class ApplicationEntity extends ClientEntity {
|
|||
private String managementUrl;
|
||||
private String baseUrl;
|
||||
private boolean bearerOnly;
|
||||
private int nodeReRegistrationTimeout;
|
||||
|
||||
// We are using names of defaultRoles (not ids)
|
||||
private List<String> defaultRoles = new ArrayList<String>();
|
||||
|
||||
private Map<String, Integer> registeredNodes;
|
||||
|
||||
public boolean isSurrogateAuthRequired() {
|
||||
return surrogateAuthRequired;
|
||||
}
|
||||
|
@ -55,5 +59,21 @@ public class ApplicationEntity extends ClientEntity {
|
|||
public void setDefaultRoles(List<String> 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -222,6 +222,7 @@ public class ModelToRepresentation {
|
|||
rep.setSurrogateAuthRequired(applicationModel.isSurrogateAuthRequired());
|
||||
rep.setBaseUrl(applicationModel.getBaseUrl());
|
||||
rep.setNotBefore(applicationModel.getNotBefore());
|
||||
rep.setNodeReRegistrationTimeout(applicationModel.getNodeReRegistrationTimeout());
|
||||
|
||||
Set<String> redirectUris = applicationModel.getRedirectUris();
|
||||
if (redirectUris != null) {
|
||||
|
@ -237,6 +238,10 @@ public class ModelToRepresentation {
|
|||
rep.setDefaultRoles(applicationModel.getDefaultRoles().toArray(new String[0]));
|
||||
}
|
||||
|
||||
if (!applicationModel.getRegisteredNodes().isEmpty()) {
|
||||
rep.setRegisteredNodes(new HashMap<String, Integer>(applicationModel.getRegisteredNodes()));
|
||||
}
|
||||
|
||||
return rep;
|
||||
}
|
||||
|
||||
|
|
|
@ -374,8 +374,16 @@ public class RepresentationToModel {
|
|||
if (resourceRep.isBearerOnly() != null) applicationModel.setBearerOnly(resourceRep.isBearerOnly());
|
||||
if (resourceRep.isPublicClient() != null) applicationModel.setPublicClient(resourceRep.isPublicClient());
|
||||
if (resourceRep.getProtocol() != null) applicationModel.setProtocol(resourceRep.getProtocol());
|
||||
if (resourceRep.isFullScopeAllowed() != null) applicationModel.setFullScopeAllowed(resourceRep.isFullScopeAllowed());
|
||||
else applicationModel.setFullScopeAllowed(true);
|
||||
if (resourceRep.isFullScopeAllowed() != null) {
|
||||
applicationModel.setFullScopeAllowed(resourceRep.isFullScopeAllowed());
|
||||
} else {
|
||||
applicationModel.setFullScopeAllowed(true);
|
||||
}
|
||||
if (resourceRep.getNodeReRegistrationTimeout() != null) {
|
||||
applicationModel.setNodeReRegistrationTimeout(resourceRep.getNodeReRegistrationTimeout());
|
||||
} else {
|
||||
applicationModel.setNodeReRegistrationTimeout(-1);
|
||||
}
|
||||
applicationModel.updateApplication();
|
||||
|
||||
if (resourceRep.getNotBefore() != null) {
|
||||
|
@ -426,6 +434,12 @@ public class RepresentationToModel {
|
|||
}
|
||||
}
|
||||
|
||||
if (resourceRep.getRegisteredNodes() != null) {
|
||||
for (Map.Entry<String, Integer> entry : resourceRep.getRegisteredNodes().entrySet()) {
|
||||
applicationModel.registerNode(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
if (addDefaultRoles && resourceRep.getDefaultRoles() != null) {
|
||||
applicationModel.updateDefaultRoles(resourceRep.getDefaultRoles());
|
||||
}
|
||||
|
@ -448,6 +462,7 @@ public class RepresentationToModel {
|
|||
if (rep.getAdminUrl() != null) resource.setManagementUrl(rep.getAdminUrl());
|
||||
if (rep.getBaseUrl() != null) resource.setBaseUrl(rep.getBaseUrl());
|
||||
if (rep.isSurrogateAuthRequired() != null) resource.setSurrogateAuthRequired(rep.isSurrogateAuthRequired());
|
||||
if (rep.getNodeReRegistrationTimeout() != null) resource.setNodeReRegistrationTimeout(rep.getNodeReRegistrationTimeout());
|
||||
resource.updateApplication();
|
||||
|
||||
if (rep.getProtocol() != null) resource.setProtocol(rep.getProtocol());
|
||||
|
@ -475,6 +490,12 @@ public class RepresentationToModel {
|
|||
resource.setWebOrigins(new HashSet<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) {
|
||||
setClaims(resource, rep.getClaims());
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import org.keycloak.models.cache.entities.CachedApplication;
|
|||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
|
@ -185,6 +186,36 @@ public class ApplicationAdapter extends ClientAdapter implements ApplicationMode
|
|||
return roles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNodeReRegistrationTimeout() {
|
||||
if (updated != null) return updated.getNodeReRegistrationTimeout();
|
||||
return cached.getNodeReRegistrationTimeout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNodeReRegistrationTimeout(int timeout) {
|
||||
getDelegateForUpdate();
|
||||
updated.setNodeReRegistrationTimeout(timeout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<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
|
||||
public boolean hasScope(RoleModel role) {
|
||||
if (super.hasScope(role)) {
|
||||
|
|
|
@ -10,6 +10,7 @@ import java.util.HashMap;
|
|||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* @author <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 boolean bearerOnly;
|
||||
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) {
|
||||
super(cache, delegate, realm, model);
|
||||
|
@ -35,7 +38,8 @@ public class CachedApplication extends CachedClient {
|
|||
cache.addCachedRole(new CachedApplicationRole(id, role, realm));
|
||||
}
|
||||
|
||||
|
||||
nodeReRegistrationTimeout = model.getNodeReRegistrationTimeout();
|
||||
registeredNodes = new TreeMap<String, Integer>(model.getRegisteredNodes());
|
||||
}
|
||||
|
||||
public boolean isSurrogateAuthRequired() {
|
||||
|
@ -62,4 +66,11 @@ public class CachedApplication extends CachedClient {
|
|||
return roles;
|
||||
}
|
||||
|
||||
public int getNodeReRegistrationTimeout() {
|
||||
return nodeReRegistrationTimeout;
|
||||
}
|
||||
|
||||
public Map<String, Integer> getRegisteredNodes() {
|
||||
return registeredNodes;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import org.keycloak.models.RoleModel;
|
|||
import org.keycloak.models.jpa.entities.ApplicationEntity;
|
||||
import org.keycloak.models.jpa.entities.RoleEntity;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.util.Time;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.TypedQuery;
|
||||
|
@ -16,6 +17,7 @@ import java.util.ArrayList;
|
|||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
|
@ -260,6 +262,35 @@ public class ApplicationAdapter extends ClientAdapter implements ApplicationMode
|
|||
em.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNodeReRegistrationTimeout() {
|
||||
return applicationEntity.getNodeReRegistrationTimeout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNodeReRegistrationTimeout(int timeout) {
|
||||
applicationEntity.setNodeReRegistrationTimeout(timeout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<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
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
package org.keycloak.models.jpa.entities;
|
||||
|
||||
import javax.persistence.CascadeType;
|
||||
import javax.persistence.CollectionTable;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.ElementCollection;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.FetchType;
|
||||
import javax.persistence.JoinColumn;
|
||||
import javax.persistence.JoinTable;
|
||||
import javax.persistence.MapKeyColumn;
|
||||
import javax.persistence.OneToMany;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -29,6 +34,9 @@ public class ApplicationEntity extends ClientEntity {
|
|||
@Column(name="BEARER_ONLY")
|
||||
private boolean bearerOnly;
|
||||
|
||||
@Column(name="NODE_REREG_TIMEOUT")
|
||||
private int nodeReRegistrationTimeout;
|
||||
|
||||
@OneToMany(fetch = FetchType.EAGER, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "application")
|
||||
Collection<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")})
|
||||
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() {
|
||||
return surrogateAuthRequired;
|
||||
}
|
||||
|
@ -83,4 +97,20 @@ public class ApplicationEntity extends ClientEntity {
|
|||
public void setBearerOnly(boolean bearerOnly) {
|
||||
this.bearerOnly = bearerOnly;
|
||||
}
|
||||
|
||||
public int getNodeReRegistrationTimeout() {
|
||||
return nodeReRegistrationTimeout;
|
||||
}
|
||||
|
||||
public void setNodeReRegistrationTimeout(int nodeReRegistrationTimeout) {
|
||||
this.nodeReRegistrationTimeout = nodeReRegistrationTimeout;
|
||||
}
|
||||
|
||||
public Map<String, Integer> getRegisteredNodes() {
|
||||
return registeredNodes;
|
||||
}
|
||||
|
||||
public void setRegisteredNodes(Map<String, Integer> registeredNodes) {
|
||||
this.registeredNodes = registeredNodes;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,10 +11,14 @@ import org.keycloak.models.RoleModel;
|
|||
import org.keycloak.models.mongo.keycloak.entities.MongoApplicationEntity;
|
||||
import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity;
|
||||
import org.keycloak.models.mongo.utils.MongoModelUtils;
|
||||
import org.keycloak.util.Time;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
|
@ -218,6 +222,41 @@ public class ApplicationAdapter extends ClientAdapter<MongoApplicationEntity> im
|
|||
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
|
||||
public boolean equals(Object o) {
|
||||
|
|
|
@ -137,7 +137,7 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
|
|||
|
||||
@Override
|
||||
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() {
|
||||
|
|
|
@ -23,8 +23,6 @@ package org.keycloak.protocol.oidc;
|
|||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.client.core.executors.ApacheHttpClient4Executor;
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.ClientConnection;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.models.ApplicationModel;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
|
@ -142,7 +140,7 @@ public class OpenIDConnect implements LoginProtocol {
|
|||
ApacheHttpClient4Executor executor = ResourceAdminManager.createExecutor();
|
||||
|
||||
try {
|
||||
new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, app, clientSession, executor, 0);
|
||||
new ResourceAdminManager().logoutClientSession(uriInfo.getRequestUri(), realm, app, clientSession, executor);
|
||||
} finally {
|
||||
executor.getHttpClient().getConnectionManager().shutdown();
|
||||
}
|
||||
|
|
|
@ -37,7 +37,6 @@ import org.keycloak.services.ForbiddenException;
|
|||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus;
|
||||
import org.keycloak.services.managers.ClientSessionCode;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
import org.keycloak.services.resources.Cors;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
import org.keycloak.services.resources.flows.Flows;
|
||||
|
@ -52,12 +51,10 @@ import javax.ws.rs.HeaderParam;
|
|||
import javax.ws.rs.OPTIONS;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.CacheControl;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.Cookie;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
|
@ -612,15 +609,15 @@ public class OpenIDConnectService {
|
|||
.build();
|
||||
}
|
||||
|
||||
String httpSessionId = formData.getFirst(AdapterConstants.HTTP_SESSION_ID);
|
||||
if (httpSessionId != null) {
|
||||
String httpSessionHost = formData.getFirst(AdapterConstants.HTTP_SESSION_HOST);
|
||||
logger.infof("Http Session '%s' saved in ClientSession for client '%s'. Host is '%s'", httpSessionId, client.getClientId(), httpSessionHost);
|
||||
String adapterSessionId = formData.getFirst(AdapterConstants.APPLICATION_SESSION_STATE);
|
||||
if (adapterSessionId != null) {
|
||||
String adapterSessionHost = formData.getFirst(AdapterConstants.APPLICATION_SESSION_HOST);
|
||||
logger.infof("Adapter Session '%s' saved in ClientSession for client '%s'. Host is '%s'", adapterSessionId, client.getClientId(), adapterSessionHost);
|
||||
|
||||
event.detail(AdapterConstants.HTTP_SESSION_ID, httpSessionId);
|
||||
clientSession.setNote(AdapterConstants.HTTP_SESSION_ID, httpSessionId);
|
||||
event.detail(AdapterConstants.HTTP_SESSION_HOST, httpSessionHost);
|
||||
clientSession.setNote(AdapterConstants.HTTP_SESSION_HOST, httpSessionHost);
|
||||
event.detail(AdapterConstants.APPLICATION_SESSION_STATE, adapterSessionId);
|
||||
clientSession.setNote(AdapterConstants.APPLICATION_SESSION_STATE, adapterSessionId);
|
||||
event.detail(AdapterConstants.APPLICATION_SESSION_HOST, adapterSessionHost);
|
||||
clientSession.setNote(AdapterConstants.APPLICATION_SESSION_HOST, adapterSessionHost);
|
||||
}
|
||||
|
||||
AccessToken token = tokenManager.createClientAccessToken(accessCode.getRequestedRoles(), realm, client, user, userSession);
|
||||
|
@ -646,6 +643,21 @@ public class OpenIDConnectService {
|
|||
}
|
||||
|
||||
protected ClientModel authorizeClient(String authorizationHeader, MultivaluedMap<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 clientSecret;
|
||||
if (authorizationHeader != null) {
|
||||
|
@ -686,14 +698,6 @@ public class OpenIDConnectService {
|
|||
throw new BadRequestException("Client is not enabled", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build());
|
||||
}
|
||||
|
||||
if ( (client instanceof ApplicationModel) && ((ApplicationModel)client).isBearerOnly()) {
|
||||
Map<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 (clientSecret == null || !client.validateSecret(clientSecret)) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,10 +9,16 @@ import org.keycloak.models.UserSessionProvider;
|
|||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.representations.adapters.config.BaseRealmConfig;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.util.Time;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* @author <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",
|
||||
"resource", "public-client", "credentials",
|
||||
"use-resource-role-mappings"})
|
||||
|
|
|
@ -20,6 +20,7 @@ import org.keycloak.representations.adapters.action.PushNotBeforeAction;
|
|||
import org.keycloak.representations.adapters.action.UserStats;
|
||||
import org.keycloak.services.util.HttpClientBuilder;
|
||||
import org.keycloak.services.util.ResolveRelative;
|
||||
import org.keycloak.util.KeycloakUriBuilder;
|
||||
import org.keycloak.util.MultivaluedHashMap;
|
||||
import org.keycloak.util.StringPropertyReplacer;
|
||||
import org.keycloak.util.Time;
|
||||
|
@ -29,11 +30,11 @@ import javax.ws.rs.core.UriBuilder;
|
|||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -41,7 +42,7 @@ import java.util.TreeMap;
|
|||
*/
|
||||
public class ResourceAdminManager {
|
||||
protected static Logger logger = Logger.getLogger(ResourceAdminManager.class);
|
||||
private static final String KC_SESSION_HOST = "${kc_session_host}";
|
||||
private static final String APPLICATION_SESSION_HOST_PROPERTY = "${application.session.host}";
|
||||
|
||||
public static ApacheHttpClient4Executor createExecutor() {
|
||||
HttpClient client = new HttpClientBuilder()
|
||||
|
@ -63,6 +64,29 @@ public class ResourceAdminManager {
|
|||
return StringPropertyReplacer.replaceProperties(absoluteURI);
|
||||
}
|
||||
|
||||
private List<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) {
|
||||
List<UserSessionModel> userSessions = keycloakSession.sessions().getUserSessions(realm, user);
|
||||
logoutUserSessions(requestUri, realm, userSessions);
|
||||
|
@ -82,7 +106,7 @@ public class ResourceAdminManager {
|
|||
logger.infov("logging out resources: " + clientSessions);
|
||||
|
||||
for (Map.Entry<ApplicationModel, List<ClientSessionModel>> entry : clientSessions.entrySet()) {
|
||||
logoutApplication(requestUri, realm, entry.getKey(), entry.getValue(), executor, 0);
|
||||
logoutClientSessions(requestUri, realm, entry.getKey(), entry.getValue(), executor);
|
||||
}
|
||||
} finally {
|
||||
executor.getHttpClient().getConnectionManager().shutdown();
|
||||
|
@ -108,34 +132,18 @@ public class ResourceAdminManager {
|
|||
|
||||
logger.debugv("logging out {0} resources ", clientSessions.size());
|
||||
for (Map.Entry<ApplicationModel, List<ClientSessionModel>> entry : clientSessions.entrySet()) {
|
||||
logoutApplication(requestUri, realm, entry.getKey(), entry.getValue(), executor, 0);
|
||||
logoutClientSessions(requestUri, realm, entry.getKey(), entry.getValue(), executor);
|
||||
}
|
||||
} finally {
|
||||
executor.getHttpClient().getConnectionManager().shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
public void logoutAll(URI requestUri, RealmModel realm) {
|
||||
public void logoutUserFromApplication(URI requestUri, RealmModel realm, ApplicationModel resource, UserModel user, KeycloakSession session) {
|
||||
ApacheHttpClient4Executor executor = createExecutor();
|
||||
|
||||
try {
|
||||
realm.setNotBefore(Time.currentTime());
|
||||
List<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<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, user);
|
||||
List<ClientSessionModel> ourAppClientSessions = null;
|
||||
if (userSessions != null) {
|
||||
MultivaluedHashMap<ApplicationModel, ClientSessionModel> clientSessions = new MultivaluedHashMap<ApplicationModel, ClientSessionModel>();
|
||||
|
@ -145,18 +153,18 @@ public class ResourceAdminManager {
|
|||
ourAppClientSessions = clientSessions.get(resource);
|
||||
}
|
||||
|
||||
logoutApplication(requestUri, realm, resource, ourAppClientSessions, executor, resource.getNotBefore());
|
||||
logoutClientSessions(requestUri, realm, resource, ourAppClientSessions, executor);
|
||||
} finally {
|
||||
executor.getHttpClient().getConnectionManager().shutdown();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public boolean logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, ClientSessionModel clientSession, ApacheHttpClient4Executor client, int notBefore) {
|
||||
return logoutApplication(requestUri, realm, resource, Arrays.asList(clientSession), client, notBefore);
|
||||
public boolean logoutClientSession(URI requestUri, RealmModel realm, ApplicationModel resource, ClientSessionModel clientSession, ApacheHttpClient4Executor client) {
|
||||
return logoutClientSessions(requestUri, realm, resource, Arrays.asList(clientSession), client);
|
||||
}
|
||||
|
||||
protected boolean logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, List<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);
|
||||
if (managementUrl != null) {
|
||||
|
||||
|
@ -165,22 +173,22 @@ public class ResourceAdminManager {
|
|||
if (clientSessions != null && clientSessions.size() > 0) {
|
||||
adapterSessionIds = new MultivaluedHashMap<String, String>();
|
||||
for (ClientSessionModel clientSession : clientSessions) {
|
||||
String adapterSessionId = clientSession.getNote(AdapterConstants.HTTP_SESSION_ID);
|
||||
String adapterSessionId = clientSession.getNote(AdapterConstants.APPLICATION_SESSION_STATE);
|
||||
if (adapterSessionId != null) {
|
||||
String host = clientSession.getNote(AdapterConstants.HTTP_SESSION_HOST);
|
||||
String host = clientSession.getNote(AdapterConstants.APPLICATION_SESSION_HOST);
|
||||
adapterSessionIds.add(host, adapterSessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (managementUrl.contains(KC_SESSION_HOST) && adapterSessionIds != null) {
|
||||
if (managementUrl.contains(APPLICATION_SESSION_HOST_PROPERTY) && adapterSessionIds != null) {
|
||||
boolean allPassed = true;
|
||||
// Send logout separately to each host (needed for single-sign-out in cluster for non-distributable apps - KEYCLOAK-748)
|
||||
for (Map.Entry<String, List<String>> entry : adapterSessionIds.entrySet()) {
|
||||
String host = entry.getKey();
|
||||
List<String> sessionIds = entry.getValue();
|
||||
String currentHostMgmtUrl = managementUrl.replace(KC_SESSION_HOST, host);
|
||||
allPassed = logoutApplicationOnHost(realm, resource, sessionIds, client, notBefore, currentHostMgmtUrl) && allPassed;
|
||||
String currentHostMgmtUrl = managementUrl.replace(APPLICATION_SESSION_HOST_PROPERTY, host);
|
||||
allPassed = sendLogoutRequest(realm, resource, sessionIds, client, 0, currentHostMgmtUrl) && allPassed;
|
||||
}
|
||||
|
||||
return allPassed;
|
||||
|
@ -193,7 +201,7 @@ public class ResourceAdminManager {
|
|||
allSessionIds.addAll(currentIds);
|
||||
}
|
||||
}
|
||||
return logoutApplicationOnHost(realm, resource, allSessionIds, client, notBefore, managementUrl);
|
||||
return sendLogoutRequest(realm, resource, allSessionIds, client, 0, managementUrl);
|
||||
}
|
||||
} else {
|
||||
logger.debugv("Can't logout {0}: no management url", resource.getName());
|
||||
|
@ -201,7 +209,54 @@ public class ResourceAdminManager {
|
|||
}
|
||||
}
|
||||
|
||||
protected boolean logoutApplicationOnHost(RealmModel realm, ApplicationModel resource, List<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);
|
||||
String token = new TokenManager().encodeToken(realm, adminAction);
|
||||
logger.infov("logout resource {0} url: {1} sessionIds: " + adapterSessionIds, resource.getName(), managementUrl);
|
||||
|
@ -245,10 +300,26 @@ public class ResourceAdminManager {
|
|||
}
|
||||
|
||||
|
||||
protected boolean pushRevocationPolicy(URI requestUri, RealmModel realm, ApplicationModel resource, int notBefore, ApacheHttpClient4Executor client) {
|
||||
if (notBefore <= 0) return false;
|
||||
String managementUrl = getManagementUrl(requestUri, resource);
|
||||
if (managementUrl != null) {
|
||||
protected boolean pushRevocationPolicy(URI requestUri, RealmModel realm, ApplicationModel resource, int notBefore, ApacheHttpClient4Executor executor) {
|
||||
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("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);
|
||||
|
@ -257,9 +328,9 @@ public class ResourceAdminManager {
|
|||
try {
|
||||
response = request.body(MediaType.TEXT_PLAIN_TYPE, token).post();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
logger.warn("Failed to send revocation request", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
boolean success = response.getStatus() == 204;
|
||||
logger.debug("pushRevocation success.");
|
||||
|
@ -267,11 +338,5 @@ public class ResourceAdminManager {
|
|||
} finally {
|
||||
response.releaseConnection();
|
||||
}
|
||||
} else {
|
||||
logger.debug("no management URL for application: " + resource.getName());
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -134,6 +134,16 @@ public class RealmsResource {
|
|||
return service;
|
||||
}
|
||||
|
||||
@Path("{realm}/clients-managements")
|
||||
public ClientsManagementService getClientsManagementService(final @PathParam("realm") String name) {
|
||||
RealmManager realmManager = new RealmManager(session);
|
||||
RealmModel realm = locateRealm(name, realmManager);
|
||||
EventBuilder event = new EventsManager(realm, session, clientConnection).createEventBuilder();
|
||||
ClientsManagementService service = new ClientsManagementService(realm, event);
|
||||
ResteasyProviderFactory.getInstance().injectProperties(service);
|
||||
return service;
|
||||
}
|
||||
|
||||
|
||||
protected RealmModel locateRealm(String name, RealmManager realmManager) {
|
||||
RealmModel realm = realmManager.getRealmByName(name);
|
||||
|
|
|
@ -335,7 +335,7 @@ public class ApplicationResource {
|
|||
@POST
|
||||
public void logoutAll() {
|
||||
auth.requireManage();
|
||||
new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, application, null);
|
||||
new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, application);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -351,8 +351,7 @@ public class ApplicationResource {
|
|||
throw new NotFoundException("User not found");
|
||||
}
|
||||
|
||||
List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, user);
|
||||
new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, application, userSessions);
|
||||
new ResourceAdminManager().logoutUserFromApplication(uriInfo.getRequestUri(), realm, application, user, session);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -29,7 +29,8 @@ sed -i -e 's/false/true/' admin-access.war/WEB-INF/web.xml
|
|||
|
||||
# Configure other examples
|
||||
for I in *.war/WEB-INF/keycloak.json; do
|
||||
sed -i -e 's/\"auth-server-url\".*: \"\/auth\",/&\n \"auth-server-url-for-backend-requests\": \"http:\/\/\$\{jboss.host.name\}:8080\/auth\",/' $I;
|
||||
sed -i -e 's/\"auth-server-url\".*: \"\/auth\",/&\n \"auth-server-url-for-backend-requests\": \"http:\/\/\$\{jboss.host.name\}:8080\/auth\",\
|
||||
\n \"register-node-at-startup\": false,\n \"register-node-period\": 30,/' $I;
|
||||
done;
|
||||
|
||||
# Enable distributable for customer-portal
|
||||
|
@ -37,6 +38,6 @@ sed -i -e 's/<\/module-name>/&\n <distributable \/>/' customer-portal.war/WEB
|
|||
|
||||
# Configure testrealm.json - Enable adminUrl to access adapters on local machine
|
||||
sed -i -e 's/\"adminUrl\": \"\/customer-portal/\"adminUrl\": \"http:\/\/\$\{jboss.host.name\}:8080\/customer-portal/' /keycloak-docker-cluster/examples/testrealm.json
|
||||
sed -i -e 's/\"adminUrl\": \"\/product-portal/\"adminUrl\": \"http:\/\/\$\{kc_session_host\}:8080\/product-portal/' /keycloak-docker-cluster/examples/testrealm.json
|
||||
sed -i -e 's/\"adminUrl\": \"\/product-portal/\"adminUrl\": \"http:\/\/\$\{application.session.host\}:8080\/product-portal/' /keycloak-docker-cluster/examples/testrealm.json
|
||||
|
||||
|
||||
|
|
|
@ -23,9 +23,8 @@ function prepareHost
|
|||
cp -r /keycloak-docker-cluster/deployments/* $JBOSS_HOME/standalone/deployments/
|
||||
|
||||
# Enable Infinispan provider
|
||||
sed -i "s|keycloak.userSessions.provider:mem|keycloak.userSessions.provider:infinispan|" $JBOSS_HOME/standalone/deployments/auth-server.war/WEB-INF/classes/META-INF/keycloak-server.json
|
||||
sed -i "s|keycloak.realm.cache.provider:mem|keycloak.realm.cache.provider:infinispan|" $JBOSS_HOME/standalone/deployments/auth-server.war/WEB-INF/classes/META-INF/keycloak-server.json
|
||||
sed -i "s|keycloak.user.cache.provider:mem|keycloak.user.cache.provider:infinispan|" $JBOSS_HOME/standalone/deployments/auth-server.war/WEB-INF/classes/META-INF/keycloak-server.json
|
||||
sed -i "s|\"provider\".*: \"mem\"|\"provider\": \"infinispan\"|" $JBOSS_HOME/standalone/deployments/auth-server.war/WEB-INF/classes/META-INF/keycloak-server.json
|
||||
sed -i -e "s/\"connectionsJpa\"/\n \"connectionsInfinispan\": \{\n \"default\" : \{\n \"cacheContainer\" : \"java:jboss\/infinispan\/Keycloak\"\n \}\n \},\n &/" $JBOSS_HOME/standalone/deployments/auth-server.war/WEB-INF/classes/META-INF/keycloak-server.json
|
||||
|
||||
# Deploy and configure examples
|
||||
/keycloak-docker-cluster/shared-files/deploy-examples.sh
|
||||
|
|
|
@ -45,6 +45,9 @@ public class ApplicationModelTest extends AbstractModelTest {
|
|||
application.addWebOrigin("origin-1");
|
||||
application.addWebOrigin("origin-2");
|
||||
|
||||
application.registerNode("node1", 10);
|
||||
application.registerNode("10.20.30.40", 50);
|
||||
|
||||
application.updateApplication();
|
||||
}
|
||||
|
||||
|
@ -84,6 +87,7 @@ public class ApplicationModelTest extends AbstractModelTest {
|
|||
|
||||
Assert.assertTrue(expected.getRedirectUris().containsAll(actual.getRedirectUris()));
|
||||
Assert.assertTrue(expected.getWebOrigins().containsAll(actual.getWebOrigins()));
|
||||
Assert.assertTrue(expected.getRegisteredNodes().equals(actual.getRegisteredNodes()));
|
||||
}
|
||||
|
||||
public static void assertEquals(List<RoleModel> expected, List<RoleModel> actual) {
|
||||
|
|
|
@ -97,6 +97,12 @@ public class ImportTest extends AbstractModelTest {
|
|||
Assert.assertTrue(apps.values().contains(accountApp));
|
||||
realm.getApplications().containsAll(apps.values());
|
||||
|
||||
Assert.assertEquals(50, application.getNodeReRegistrationTimeout());
|
||||
Map<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
|
||||
Assert.assertNull(realm.getApplicationById("982734"));
|
||||
Assert.assertEquals(application, realm.getApplicationById(application.getId()));
|
||||
|
|
|
@ -94,7 +94,12 @@
|
|||
"applications": [
|
||||
{
|
||||
"name": "Application",
|
||||
"enabled": true
|
||||
"enabled": true,
|
||||
"nodeReRegistrationTimeout": 50,
|
||||
"registeredNodes": {
|
||||
"node1": 10,
|
||||
"172.10.15.20": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "OtherApp",
|
||||
|
|
Loading…
Reference in a new issue