KEYCLOAK-759 dynamic registration of managementUrls in cluster
This commit is contained in:
parent
8545457f5a
commit
7d8f265789
43 changed files with 990 additions and 114 deletions
|
@ -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>
|
|
@ -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, ".");
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 "";
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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.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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"})
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
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);
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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()));
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in a new issue