Merge remote-tracking branch 'upstream/master'

This commit is contained in:
mhajas 2016-02-12 16:10:03 +01:00
commit 474043b688
171 changed files with 6294 additions and 1096 deletions

View file

@ -41,7 +41,7 @@
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -73,7 +73,7 @@
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -49,6 +49,9 @@ public abstract class KeycloakDependencyProcessor implements DeploymentUnitProce
String deploymentName = deploymentUnit.getName();
if (!KeycloakAdapterConfigService.getInstance().isSecureDeployment(deploymentName)) {
WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY);
if (warMetaData == null) {
return;
}
JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData();
if (webMetaData == null) {
return;

View file

@ -72,7 +72,7 @@
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -236,7 +236,7 @@
kc.createLogoutUrl = function(options) {
var url = getRealmUrl()
+ '/protocol/openid-connect/logout'
+ '?redirect_uri=' + encodeURIComponent(adapter.redirectUri(options));
+ '?redirect_uri=' + encodeURIComponent(adapter.redirectUri(options, false));
return url;
}
@ -842,14 +842,18 @@
return createPromise().promise;
},
redirectUri: function(options) {
redirectUri: function(options, encodeHash) {
if (arguments.length == 1) {
encodeHash = true;
}
if (options && options.redirectUri) {
return options.redirectUri;
} else if (kc.redirectUri) {
return kc.redirectUri;
} else {
var redirectUri = location.href;
if (location.hash) {
if (location.hash && encodeHash) {
redirectUri = redirectUri.substring(0, location.href.indexOf('#'));
redirectUri += (redirectUri.indexOf('?') == -1 ? '?' : '&') + 'redirect_fragment=' + encodeURIComponent(location.hash.substring(1));
}

View file

@ -75,7 +75,7 @@
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -53,7 +53,7 @@
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -52,7 +52,7 @@
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -75,7 +75,7 @@
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>

View file

@ -49,6 +49,9 @@ public abstract class KeycloakDependencyProcessor implements DeploymentUnitProce
String deploymentName = deploymentUnit.getName();
if (!KeycloakAdapterConfigService.getInstance().isSecureDeployment(deploymentName)) {
WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY);
if (warMetaData == null) {
return;
}
JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData();
if (webMetaData == null) {
return;

View file

@ -89,7 +89,7 @@
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>

View file

@ -49,6 +49,9 @@ public abstract class KeycloakDependencyProcessor implements DeploymentUnitProce
String deploymentName = deploymentUnit.getName();
if (!KeycloakAdapterConfigService.getInstance().isSecureDeployment(deploymentName)) {
WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY);
if (warMetaData == null) {
return;
}
JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData();
if (webMetaData == null) {
return;

View file

@ -53,7 +53,7 @@
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -48,6 +48,9 @@ public abstract class KeycloakDependencyProcessor implements DeploymentUnitProce
String deploymentName = deploymentUnit.getName();
if (Configuration.INSTANCE.getSecureDeployment(deploymentName) == null) {
WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY);
if (warMetaData == null) {
return;
}
JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData();
if (webMetaData == null) {
return;

View file

@ -61,7 +61,7 @@
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -69,7 +69,7 @@
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -69,7 +69,7 @@
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>

View file

@ -47,6 +47,9 @@ public abstract class KeycloakDependencyProcessor implements DeploymentUnitProce
String deploymentName = deploymentUnit.getName();
if (Configuration.INSTANCE.getSecureDeployment(deploymentName) == null) {
WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY);
if (warMetaData == null) {
return;
}
JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData();
if (webMetaData == null) {
return;

View file

@ -67,7 +67,7 @@
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -45,7 +45,7 @@
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -23,7 +23,6 @@ import org.keycloak.adapters.spi.KeycloakAccount;
import org.keycloak.common.util.Encode;
import org.keycloak.common.util.MultivaluedHashMap;
import javax.servlet.ReadListener;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
@ -174,54 +173,11 @@ public class FilterSessionStore implements AdapterSessionStore {
public ServletInputStream getInputStream() throws IOException {
if (needRequestRestore && body != null) {
final ByteArrayInputStream is = new ByteArrayInputStream(body);
return new ServletInputStream() {
private int lastIndex = 0;
private ReadListener readListener = null;
@Override
public boolean isFinished() {
return lastIndex == body.length;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
this.readListener = readListener;
if (!isFinished()) {
try {
readListener.onDataAvailable();
} catch (IOException e) {
readListener.onError(e);
}
} else {
try {
readListener.onAllDataRead();
} catch (IOException e) {
readListener.onError(e);
}
}
}
@Override
public int read() throws IOException {
int i = -1;
if (!isFinished()) {
i = body[lastIndex];
lastIndex++;
if (isFinished() && readListener != null) {
try {
readListener.onAllDataRead();
} catch (IOException e) {
readListener.onError(e);
throw e;
}
}
}
return i;
return is.read();
}
};
}

View file

@ -47,7 +47,7 @@
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -0,0 +1,77 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.representations.idm;
import java.io.Serializable;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class AbstractAuthenticationExecutionRepresentation implements Serializable {
private String authenticatorConfig;
private String authenticator;
private boolean authenticatorFlow;
private String requirement;
private int priority;
public String getAuthenticatorConfig() {
return authenticatorConfig;
}
public void setAuthenticatorConfig(String authenticatorConfig) {
this.authenticatorConfig = authenticatorConfig;
}
public String getAuthenticator() {
return authenticator;
}
public void setAuthenticator(String authenticator) {
this.authenticator = authenticator;
}
public String getRequirement() {
return requirement;
}
public void setRequirement(String requirement) {
this.requirement = requirement;
}
public int getPriority() {
return priority;
}
public void setPriority(int priority) {
this.priority = priority;
}
/**
* Is the referenced authenticator a flow?
*
* @return
*/
public boolean isAutheticatorFlow() {
return authenticatorFlow;
}
public void setAutheticatorFlow(boolean autheticatorFlow) {
this.authenticatorFlow = autheticatorFlow;
}
}

View file

@ -0,0 +1,50 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.representations.idm;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class AuthenticationExecutionExportRepresentation extends AbstractAuthenticationExecutionRepresentation {
private String flowAlias;
private boolean userSetupAllowed;
public boolean isUserSetupAllowed() {
return userSetupAllowed;
}
public void setUserSetupAllowed(boolean userSetupAllowed) {
this.userSetupAllowed = userSetupAllowed;
}
/**
* If this execution is a flow, this is the flowId pointing to an AuthenticationFlowModel
*
* @return
*/
public String getFlowAlias() {
return flowAlias;
}
public void setFlowAlias(String flowId) {
this.flowAlias = flowId;
}
}

View file

@ -0,0 +1,128 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.representations.idm;
import java.io.Serializable;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class AuthenticationExecutionInfoRepresentation implements Serializable {
protected String id;
protected String requirement;
protected String displayName;
protected List<String> requirementChoices;
protected Boolean configurable;
protected Boolean authenticationFlow;
protected String providerId;
protected String authenticationConfig;
protected String flowId;
protected int level;
protected int index;
public String getId() {
return id;
}
public void setId(String execution) {
this.id = execution;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public String getRequirement() {
return requirement;
}
public void setRequirement(String requirement) {
this.requirement = requirement;
}
public List<String> getRequirementChoices() {
return requirementChoices;
}
public void setRequirementChoices(List<String> requirementChoices) {
this.requirementChoices = requirementChoices;
}
public Boolean getConfigurable() {
return configurable;
}
public void setConfigurable(Boolean configurable) {
this.configurable = configurable;
}
public String getProviderId() {
return providerId;
}
public void setProviderId(String providerId) {
this.providerId = providerId;
}
public String getAuthenticationConfig() {
return authenticationConfig;
}
public void setAuthenticationConfig(String authenticationConfig) {
this.authenticationConfig = authenticationConfig;
}
public Boolean getAuthenticationFlow() {
return authenticationFlow;
}
public void setAuthenticationFlow(Boolean authenticationFlow) {
this.authenticationFlow = authenticationFlow;
}
public int getLevel() {
return level;
}
public void setLevel(int level) {
this.level = level;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
public String getFlowId() {
return flowId;
}
public void setFlowId(String flowId) {
this.flowId = flowId;
}
}

View file

@ -17,87 +17,36 @@
package org.keycloak.representations.idm;
import java.io.Serializable;
import java.util.Comparator;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class AuthenticationExecutionRepresentation implements Serializable {
public class AuthenticationExecutionRepresentation extends AbstractAuthenticationExecutionRepresentation {
private String authenticatorConfig;
private String authenticator;
private String flowAlias;
private boolean autheticatorFlow;
private String requirement;
private boolean userSetupAllowed;
private int priority;
private String id;
private String flowId;
private String parentFlow;
public String getAuthenticatorConfig() {
return authenticatorConfig;
public String getId() {
return id;
}
public void setAuthenticatorConfig(String authenticatorConfig) {
this.authenticatorConfig = authenticatorConfig;
public void setId(String id) {
this.id = id;
}
public String getAuthenticator() {
return authenticator;
public String getFlowId() {
return flowId;
}
public void setAuthenticator(String authenticator) {
this.authenticator = authenticator;
public void setFlowId(String flowId) {
this.flowId = flowId;
}
public String getRequirement() {
return requirement;
public String getParentFlow() {
return parentFlow;
}
public void setRequirement(String requirement) {
this.requirement = requirement;
public void setParentFlow(String parentFlow) {
this.parentFlow = parentFlow;
}
public int getPriority() {
return priority;
}
public void setPriority(int priority) {
this.priority = priority;
}
public boolean isUserSetupAllowed() {
return userSetupAllowed;
}
public void setUserSetupAllowed(boolean userSetupAllowed) {
this.userSetupAllowed = userSetupAllowed;
}
/**
* If this execution is a flow, this is the flowId pointing to an AuthenticationFlowModel
*
* @return
*/
public String getFlowAlias() {
return flowAlias;
}
public void setFlowAlias(String flowId) {
this.flowAlias = flowId;
}
/**
* Is the referenced authenticator a flow?
*
* @return
*/
public boolean isAutheticatorFlow() {
return autheticatorFlow;
}
public void setAutheticatorFlow(boolean autheticatorFlow) {
this.autheticatorFlow = autheticatorFlow;
}
}

View file

@ -26,12 +26,21 @@ import java.util.List;
*/
public class AuthenticationFlowRepresentation implements Serializable {
private String id;
private String alias;
private String description;
private String providerId;
private boolean topLevel;
private boolean builtIn;
protected List<AuthenticationExecutionRepresentation> authenticationExecutions;
protected List<AuthenticationExecutionExportRepresentation> authenticationExecutions;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getAlias() {
return alias;
@ -73,11 +82,11 @@ public class AuthenticationFlowRepresentation implements Serializable {
this.builtIn = builtIn;
}
public List<AuthenticationExecutionRepresentation> getAuthenticationExecutions() {
public List<AuthenticationExecutionExportRepresentation> getAuthenticationExecutions() {
return authenticationExecutions;
}
public void setAuthenticationExecutions(List<AuthenticationExecutionRepresentation> authenticationExecutions) {
public void setAuthenticationExecutions(List<AuthenticationExecutionExportRepresentation> authenticationExecutions) {
this.authenticationExecutions = authenticationExecutions;
}
}

View file

@ -0,0 +1,65 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.representations.idm;
import java.util.List;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class AuthenticatorConfigInfoRepresentation {
protected String name;
protected String providerId;
protected String helpText;
protected List<ConfigPropertyRepresentation> properties;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getHelpText() {
return helpText;
}
public String getProviderId() {
return providerId;
}
public void setProviderId(String providerId) {
this.providerId = providerId;
}
public void setHelpText(String helpText) {
this.helpText = helpText;
}
public List<ConfigPropertyRepresentation> getProperties() {
return properties;
}
public void setProperties(List<ConfigPropertyRepresentation> properties) {
this.properties = properties;
}
}

View file

@ -89,6 +89,9 @@
<local-cache name="sessions"/>
<local-cache name="offlineSessions"/>
<local-cache name="loginFailures"/>
<local-cache name="realmVersions">
<transaction mode="BATCH" locking="PESSIMISTIC"/>
</local-cache>
</cache-container>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>

View file

@ -28,12 +28,6 @@
}
},
"realmCache": {
"infinispan" : {
"enabled": true
}
},
"userSessionPersister": {
"provider": "jpa"
},
@ -67,8 +61,16 @@
}
},
"realmCache": {
"provider": "infinispan-locking",
"infinispan-locking" : {
"enabled": true
}
},
"connectionsInfinispan": {
"default" : {
"provider": "locking",
"locking": {
"cacheContainer" : "java:comp/env/infinispan/Keycloak"
}
}

View file

@ -107,6 +107,12 @@
(via the subsystem, or auth-method in web.xml
</para>
</simplesect>
<simplesect>
<title>Client Registration service endpoints moved</title>
<para>
The Client Registration service endpoints have been moved from <literal>{realm}/clients</literal> to <literal>{realm}/clients-registrations</literal>.
</para>
</simplesect>
<simplesect>
<title>Deprecated OpenID Connect endpoints</title>
<para>

View file

@ -27,7 +27,7 @@
<para>
The Client Registration Service provides built-in support for Keycloak Client Representations, OpenID Connect
Client Meta Data and SAML Entity Descriptors. It's also possible to plugin custom client registration providers
if required. The Client Registration Service endpoint is <literal>&lt;KEYCLOAK URL&gt;/realms/&lt;realm&gt;/clients/&lt;provider&gt;</literal>.
if required. The Client Registration Service endpoint is <literal>&lt;KEYCLOAK URL&gt;/realms/&lt;realm&gt;/clients-registrations/&lt;provider&gt;</literal>.
</para>
<para>
The built-in supported <literal>providers</literal> are:
@ -123,23 +123,23 @@ Authorization: bearer eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJmMjJmNzQyYy04ZjNlLTQ2M....
</para>
<para>
To create a client create a Client Representation (JSON) then do a HTTP POST to:
<literal>&lt;KEYCLOAK URL&gt;/realms/&lt;realm&gt;/clients/default</literal>. It will return a Client Representation
<literal>&lt;KEYCLOAK URL&gt;/realms/&lt;realm&gt;/clients-registrations/default</literal>. It will return a Client Representation
that also includes the registration access token. You should save the registration access token somewhere
if you want to retrieve the config, update or delete the client later.
</para>
<para>
To retrieve the Client Representation then do a HTTP GET to:
<literal>&lt;KEYCLOAK URL&gt;/realms/&lt;realm&gt;/clients/default/&lt;client id&gt;</literal>. It will also
<literal>&lt;KEYCLOAK URL&gt;/realms/&lt;realm&gt;/clients-registrations/default/&lt;client id&gt;</literal>. It will also
return a new registration access token.
</para>
<para>
To update the Client Representation then do a HTTP PUT to with the updated Client Representation to:
<literal>&lt;KEYCLOAK URL&gt;/realms/&lt;realm&gt;/clients/default/&lt;client id&gt;</literal>. It will also
<literal>&lt;KEYCLOAK URL&gt;/realms/&lt;realm&gt;/clients-registrations/default/&lt;client id&gt;</literal>. It will also
return a new registration access token.
</para>
<para>
To delete the Client Representation then do a HTTP DELETE to:
<literal>&lt;KEYCLOAK URL&gt;/realms/&lt;realm&gt;/clients/default/&lt;client id&gt;</literal>
<literal>&lt;KEYCLOAK URL&gt;/realms/&lt;realm&gt;/clients-registrations/default/&lt;client id&gt;</literal>
</para>
</section>
@ -155,7 +155,7 @@ Authorization: basic BASE64(client-id + ':' + client-secret)
</para>
<para>
To retrieve the Adapter Configuration then do a HTTP GET to:
<literal>&lt;KEYCLOAK URL&gt;//realms/&lt;realm&gt;/clients/installation/&lt;client id&gt;</literal>
<literal>&lt;KEYCLOAK URL&gt;//realms/&lt;realm&gt;/clients-registrations/installation/&lt;client id&gt;</literal>
</para>
<para>
No authentication is required for public clients. This means that for the JavaScript adapter you can
@ -172,7 +172,7 @@ Authorization: basic BASE64(client-id + ':' + client-secret)
</para>
<para>
The endpoint to use these specifications to register clients in Keycloak is:
<literal>&lt;KEYCLOAK URL&gt;/realms/&lt;realm&gt;/clients/oidc[/&lt;client id&gt;]</literal>.
<literal>&lt;KEYCLOAK URL&gt;/realms/&lt;realm&gt;/clients-registrations/oidc[/&lt;client id&gt;]</literal>.
</para>
<para>
This endpoints can also be found in the OpenID Connect Discovery endpoint for the realm:
@ -190,7 +190,7 @@ Authorization: basic BASE64(client-id + ':' + client-secret)
</para>
<para>
To create a client do a HTTP POST with the SAML Entity Descriptor to:
<literal>&lt;KEYCLOAK URL&gt;/realms/&lt;realm&gt;/clients/saml2-entity-descriptor</literal>.
<literal>&lt;KEYCLOAK URL&gt;/realms/&lt;realm&gt;/clients-registrations/saml2-entity-descriptor</literal>.
</para>
</section>

View file

@ -396,7 +396,7 @@ bin/add-user.[sh|bat] -r master -u <username> -p <password>
<term>connection-pool-size</term>
<listitem>
<para>
How many connections can be in the pool.
How many connections can be in the pool (128 by default).
</para>
</listitem>
</varlistentry>
@ -404,7 +404,24 @@ bin/add-user.[sh|bat] -r master -u <username> -p <password>
<term>max-pooled-per-route</term>
<listitem>
<para>
How many connections can be pooled per host.
How many connections can be pooled per host (64 by default).
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>connection-ttl-millis</term>
<listitem>
<para>
Maximum connection time to live in milliseconds. Not set by default.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>max-connection-idle-time-millis</term>
<listitem>
<para>
Maximum time the connection might stay idle in the connection pool (900 seconds by default). Will start background cleaner thread of Apache HTTP client.
Set to -1 to disable this checking and the background thread.
</para>
</listitem>
</varlistentry>

View file

@ -50,7 +50,7 @@
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -58,7 +58,7 @@
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -48,7 +48,7 @@
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -202,7 +202,28 @@ An Angular JS example using Keycloak to secure it.
If you are already logged in, you will not be asked for a username and password, but you will be redirected to
an oauth grant page. This page asks you if you want to grant certain permissions to the third-part app.
Step 9: Pure HTML5/Javascript Example
Step 10: Angular2 JS Example
----------------------------------
An Angular2 JS example using Keycloak to secure it. Angular2 is in beta version yet.
To install angular2
```
$ cd keycloak/examples/demo-template/angular2-product-app/src/main/webapp/
$ npm install
```
Transpile TypeScript to JavaScript before running the application.
```
$ npm run tsc
```
[http://localhost:8080/angular2-product](http://localhost:8080/angular2-product)
If you are already logged in, you will not be asked for a username and password, but you will be redirected to
an oauth grant page. This page asks you if you want to grant certain permissions to the third-part app.
Step 11: Pure HTML5/Javascript Example
----------------------------------
An pure HTML5/Javascript example using Keycloak to secure it.
@ -211,7 +232,7 @@ An pure HTML5/Javascript example using Keycloak to secure it.
If you are already logged in, you will not be asked for a username and password, but you will be redirected to
an oauth grant page. This page asks you if you want to grant certain permissions to the third-part app.
Step 10: Service Account Example
Step 12: Service Account Example
================================
An example for retrieve service account dedicated to the Client Application itself (not to any user).
@ -222,7 +243,7 @@ Client authentication is done with OAuth2 Client Credentials Grant in out-of-bou
The example also shows different methods of client authentication. There is ProductSAClientSecretServlet using traditional authentication with clientId and client_secret,
but there is also ProductSAClientSignedJWTServlet using client authentication with JWT signed by client private key.
Step 11: Offline Access Example
Step 13: Offline Access Example
===============================
An example for retrieve offline token, which is then saved to the database and can be used by application anytime later. Offline token
is valid even if user is already logged out from SSO. Server restart also won't invalidate offline token. Offline token can be revoked by the user in

View file

@ -42,7 +42,7 @@
<dependencies>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-examples-demo-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.9.0.Final-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>org.keycloak.example.demo</groupId>
<artifactId>angular2-product-example</artifactId>
<packaging>war</packaging>
<name>Angular2 Product Portal JS</name>
<description/>
<build>
<finalName>angular2-product</finalName>
<plugins>
<plugin>
<groupId>org.jboss.as.plugins</groupId>
<artifactId>jboss-as-maven-plugin</artifactId>
<configuration>
<skip>false</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.wildfly.plugins</groupId>
<artifactId>wildfly-maven-plugin</artifactId>
<configuration>
<skip>false</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,78 @@
import {Http, Headers,
RequestOptions, Response} from 'angular2/http';
import {Component} from 'angular2/core';
import {Observable} from 'rxjs/Observable';
import {KeycloakService} from './keycloak';
@Component({
selector: 'my-app',
template:
`
<div id="content-area" class="col-md-9" role="main">
<div id="content">
<h1>Angular2 Product (Beta)</h1>
<h2><span>Products</span></h2>
<button type="button" (click)="logout()">Sign Out</button>
<button type="button" (click)="reloadData()">Reload</button>
<table class="table" [hidden]="!products.length">
<thead>
<tr>
<th>Product Listing</th>
</tr>
</thead>
<tbody>
<tr *ngFor="#p of products">
<td>{{p}}</td>
</tr>
</tbody>
</table>
</div>
</div>
`
})
export class AppComponent {
constructor(private _kc:KeycloakService, private http:Http){ }
products : string[] = [];
logout(){
this._kc.logout();
}
reloadData() {
//angular dont have http interceptor yet
this._kc.getToken().then(
token=>{
let headers = new Headers({
'Accept': 'application/json',
'Authorization': 'Bearer ' + token
});
let options = new RequestOptions({ headers: headers });
this.http.get('/database/products', options)
.map(res => <string[]> res.json())
.subscribe(
prods => this.products = prods,
error => console.log(error));
},
error=>{
console.log(error);
}
);
}
private handleError (error: Response) {
console.error(error);
return Observable.throw(error.json().error || 'Server error');
}
}

View file

@ -0,0 +1,49 @@
import {Injectable} from 'angular2/core';
declare var Keycloak: any;
@Injectable()
export class KeycloakService {
static auth : any = {};
static init() : Promise<any>{
let keycloakAuth : any = new Keycloak('keycloak.json');
KeycloakService.auth.loggedIn = false;
return new Promise((resolve,reject)=>{
keycloakAuth.init({ onLoad: 'login-required' })
.success( () => {
KeycloakService.auth.loggedIn = true;
KeycloakService.auth.authz = keycloakAuth;
KeycloakService.auth.logoutUrl = keycloakAuth.authServerUrl + "/realms/demo/tokens/logout?redirect_uri=/angular2-product/index.html";
resolve(null);
})
.error(()=> {
reject(null);
});
});
}
logout(){
console.log('*** LOGOUT');
KeycloakService.auth.loggedIn = false;
KeycloakService.auth.authz = null;
window.location.href = KeycloakService.auth.logoutUrl;
}
getToken(): Promise<string>{
return new Promise<string>((resolve,reject)=>{
if (KeycloakService.auth.authz.token) {
KeycloakService.auth.authz.updateToken(5).success(function() {
resolve(<string>KeycloakService.auth.authz.token);
})
.error(function() {
reject('Failed to refresh token');
});
}
});
}
}

View file

@ -0,0 +1,14 @@
import 'rxjs/Rx';
import {bootstrap} from 'angular2/platform/browser';
import {HTTP_BINDINGS} from 'angular2/http';
import {KeycloakService} from './keycloak';
import {AppComponent} from './app';
KeycloakService.init().then(
o=>{
bootstrap(AppComponent,[HTTP_BINDINGS, KeycloakService]);
},
x=>{
window.location.reload();
}
);

View file

@ -0,0 +1,49 @@
<!doctype html>
<html>
<head>
<title>Angular 2 QuickStart</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<!-- 3. Display the application -->
<body>
<my-app>Loading...</my-app>
<!-- 1. Load libraries -->
<!-- IE required polyfills, in this exact order -->
<script src="node_modules/es6-shim/es6-shim.min.js"></script>
<script src="node_modules/systemjs/dist/system-polyfills.js"></script>
<script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="node_modules/rxjs/bundles/Rx.js"></script>
<script src="node_modules/angular2/bundles/angular2.dev.js"></script>
<script src="node_modules/angular2/bundles/http.js"></script>
<script src="/auth/js/keycloak.js"></script>
<!-- 2. Configure SystemJS -->
<script>
System.config({
packages: {
app: {
format: 'register',
defaultExtension: 'js'
}
}
});
System.import('app/main')
.then(null, console.error.bind(console));
</script>
</body>
</html>

View file

@ -0,0 +1,8 @@
{
"realm": "demo",
"realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"auth-server-url": "/auth",
"ssl-required": "external",
"resource": "angular2-product",
"public-client": true
}

View file

@ -0,0 +1,25 @@
{
"name": "angular2-product-app",
"version": "1.0.0",
"scripts": {
"tsc": "tsc",
"tsc:w": "tsc -w",
"lite": "lite-server",
"start": "concurrent \"npm run tsc:w\" \"npm run lite\" "
},
"license": "ISC",
"dependencies": {
"angular2": "2.0.0-beta.3",
"systemjs": "0.19.6",
"es6-promise": "^3.0.2",
"es6-shim": "^0.33.3",
"reflect-metadata": "0.1.2",
"rxjs": "5.0.0-beta.0",
"zone.js": "0.5.11"
},
"devDependencies": {
"concurrently": "^1.0.0",
"lite-server": "^2.0.1",
"typescript": "^1.7.5"
}
}

View file

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "es5",
"module": "system",
"moduleResolution": "node",
"sourceMap": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"removeComments": false,
"noImplicitAny": false
},
"exclude": [
"node_modules"
]
}

View file

@ -42,7 +42,7 @@
<dependencies>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -42,7 +42,7 @@
<dependencies>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -48,7 +48,7 @@
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -42,7 +42,7 @@
<dependencies>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -42,7 +42,7 @@
<dependencies>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -34,7 +34,7 @@
<dependencies>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -147,6 +147,15 @@
"redirectUris": [
"/angular-product/*"
]
},
{
"clientId": "angular2-product",
"enabled": true,
"publicClient": true,
"baseUrl": "/angular2-product/index.html",
"redirectUris": [
"/angular2-product/*"
]
},
{
"clientId": "customer-portal-cli",

View file

@ -34,7 +34,7 @@
<dependencies>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -34,7 +34,7 @@
<dependencies>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -52,7 +52,7 @@
<dependencies>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -53,7 +53,7 @@
<dependencies>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>

View file

@ -44,7 +44,7 @@
<dependencies>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -42,7 +42,7 @@
<dependencies>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View file

@ -47,7 +47,7 @@
<dependencies>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>

View file

@ -0,0 +1,194 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.admin.client.resource;
import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
import org.keycloak.representations.idm.AuthenticationExecutionRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.representations.idm.AuthenticatorConfigInfoRepresentation;
import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
import org.keycloak.representations.idm.ConfigPropertyRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public interface AuthenticationManagementResource {
@GET
@Path("/form-providers")
@Produces(MediaType.APPLICATION_JSON)
List<Map<String, Object>> getFormProviders();
@Path("/authenticator-providers")
@GET
@Produces(MediaType.APPLICATION_JSON)
List<Map<String, Object>> getAuthenticatorProviders();
@Path("/client-authenticator-providers")
@GET
@Produces(MediaType.APPLICATION_JSON)
List<Map<String, Object>> getClientAuthenticatorProviders();
@Path("/form-action-providers")
@GET
@Produces(MediaType.APPLICATION_JSON)
List<Map<String, Object>> getFormActionProviders();
@Path("/flows")
@GET
@Produces(MediaType.APPLICATION_JSON)
List<AuthenticationFlowRepresentation> getFlows();
@Path("/flows")
@POST
@Consumes(MediaType.APPLICATION_JSON)
Response createFlow(AuthenticationFlowRepresentation model);
@Path("/flows/{id}")
@GET
@Produces(MediaType.APPLICATION_JSON)
AuthenticationFlowRepresentation getFlow(@PathParam("id") String id);
@Path("/flows/{id}")
@DELETE
void deleteFlow(@PathParam("id") String id);
@Path("/flows/{flowAlias}/copy")
@POST
@Consumes(MediaType.APPLICATION_JSON)
Response copy(@PathParam("flowAlias") String flowAlias, Map<String, String> data);
@Path("/flows/{flowAlias}/executions/flow")
@POST
@Consumes(MediaType.APPLICATION_JSON)
void addExecutionFlow(@PathParam("flowAlias") String flowAlias, Map<String, String> data);
@Path("/flows/{flowAlias}/executions/execution")
@POST
@Consumes(MediaType.APPLICATION_JSON)
void addExecution(@PathParam("flowAlias") String flowAlias, Map<String, String> data);
@Path("/flows/{flowAlias}/executions")
@GET
@Produces(MediaType.APPLICATION_JSON)
Response getExecutions(@PathParam("flowAlias") String flowAlias);
@Path("/flows/{flowAlias}/executions")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
void updateExecutions(@PathParam("flowAlias") String flowAlias, AuthenticationExecutionInfoRepresentation rep);
@Path("/executions")
@POST
@Consumes(MediaType.APPLICATION_JSON)
Response addExecution(AuthenticationExecutionRepresentation model);
@Path("/executions/{executionId}/raise-priority")
@POST
void raisePriority(@PathParam("executionId") String execution);
@Path("/executions/{executionId}/lower-priority")
@POST
void lowerPriority(@PathParam("executionId") String execution);
@Path("/executions/{executionId}")
@DELETE
void removeExecution(@PathParam("executionId") String execution);
@Path("/executions/{executionId}/config")
@POST
@Consumes(MediaType.APPLICATION_JSON)
Response newExecutionConfig(@PathParam("executionId") String execution, AuthenticatorConfigRepresentation config);
@Path("/executions/{executionId}/config/{id}")
@GET
@Produces(MediaType.APPLICATION_JSON)
AuthenticatorConfigRepresentation getAuthenticatorConfig(@PathParam("executionId") String execution,@PathParam("id") String id);
@Path("unregistered-required-actions")
@GET
@Produces(MediaType.APPLICATION_JSON)
List<Map<String, String>> getUnregisteredRequiredActions();
@Path("register-required-action")
@POST
@Consumes(MediaType.APPLICATION_JSON)
void registereRequiredAction(Map<String, String> data);
@Path("required-actions")
@GET
@Produces(MediaType.APPLICATION_JSON)
List<RequiredActionProviderRepresentation> getRequiredActions();
@Path("required-actions/{alias}")
@GET
@Produces(MediaType.APPLICATION_JSON)
RequiredActionProviderRepresentation getRequiredAction(@PathParam("alias") String alias);
@Path("required-actions/{alias}")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
void updateRequiredAction(@PathParam("alias") String alias, RequiredActionProviderRepresentation rep);
@Path("required-actions/{alias}")
@DELETE
void updateRequiredAction(@PathParam("alias") String alias);
@Path("config-description/{providerId}")
@GET
@Produces(MediaType.APPLICATION_JSON)
AuthenticatorConfigInfoRepresentation getAuthenticatorConfigDescription(@PathParam("providerId") String providerId);
@Path("per-client-config-description")
@GET
@Produces(MediaType.APPLICATION_JSON)
Map<String, List<ConfigPropertyRepresentation>> getPerClientConfigDescription();
@Path("config")
@POST
@Consumes(MediaType.APPLICATION_JSON)
Response createAuthenticatorConfig(AuthenticatorConfigRepresentation config);
@Path("config/{id}")
@GET
@Produces(MediaType.APPLICATION_JSON)
AuthenticatorConfigRepresentation getAuthenticatorConfig(@PathParam("id") String id);
@Path("config/{id}")
@DELETE
void removeAuthenticatorConfig(@PathParam("id") String id);
@Path("config/{id}")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
void updateAuthenticatorConfig(@PathParam("id") String id, AuthenticatorConfigRepresentation config);
}

View file

@ -142,4 +142,8 @@ public interface RealmResource {
@Path("clients-initial-access")
ClientInitialAccessResource clientInitialAccess();
@Path("authentication")
@Consumes(MediaType.APPLICATION_JSON)
AuthenticationManagementResource flows();
}

View file

@ -177,7 +177,7 @@ public class ClientRegistration {
}
public ClientRegistrationBuilder url(String authUrl, String realm) {
url = HttpUtil.getUrl(authUrl, "realms", realm, "clients");
url = HttpUtil.getUrl(authUrl, "realms", realm, "clients-registrations");
return this;
}

View file

@ -37,11 +37,11 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
protected static final Logger logger = Logger.getLogger(DefaultInfinispanConnectionProviderFactory.class);
private Config.Scope config;
protected Config.Scope config;
private EmbeddedCacheManager cacheManager;
protected EmbeddedCacheManager cacheManager;
private boolean containerManaged;
protected boolean containerManaged;
@Override
public InfinispanConnectionProvider create(KeycloakSession session) {
@ -73,7 +73,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
}
private void lazyInit() {
protected void lazyInit() {
if (cacheManager == null) {
synchronized (this) {
if (cacheManager == null) {
@ -88,7 +88,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
}
}
private void initContainerManaged(String cacheContainerLookup) {
protected void initContainerManaged(String cacheContainerLookup) {
try {
cacheManager = (EmbeddedCacheManager) new InitialContext().lookup(cacheContainerLookup);
containerManaged = true;
@ -99,7 +99,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
}
}
private void initEmbedded() {
protected void initEmbedded() {
GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
boolean clustered = config.getBoolean("clustered", false);

View file

@ -546,7 +546,9 @@ public class ClientAdapter implements ClientModel {
public RoleModel getRole(String name) {
if (updated != null) return updated.getRole(name);
String id = cached.getRoles().get(name);
if (id == null) return null;
if (id == null) {
return null;
}
return cacheSession.getRoleById(id, cachedRealm);
}

View file

@ -0,0 +1,10 @@
package org.keycloak.models.cache.infinispan.counter;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface Revisioned {
Long getRevision();
void setRevision(Long revision);
}

View file

@ -0,0 +1,401 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.cache.infinispan.counter;
import org.jboss.logging.Logger;
import org.keycloak.migration.MigrationModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientTemplateModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakTransaction;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RealmProvider;
import org.keycloak.models.RoleModel;
import org.keycloak.models.cache.CacheRealmProvider;
import org.keycloak.models.cache.RealmCache;
import org.keycloak.models.cache.entities.CachedClient;
import org.keycloak.models.cache.entities.CachedClientRole;
import org.keycloak.models.cache.entities.CachedClientTemplate;
import org.keycloak.models.cache.entities.CachedGroup;
import org.keycloak.models.cache.entities.CachedRealm;
import org.keycloak.models.cache.entities.CachedRealmRole;
import org.keycloak.models.cache.entities.CachedRole;
import org.keycloak.models.cache.infinispan.ClientAdapter;
import org.keycloak.models.cache.infinispan.ClientTemplateAdapter;
import org.keycloak.models.cache.infinispan.GroupAdapter;
import org.keycloak.models.cache.infinispan.RealmAdapter;
import org.keycloak.models.cache.infinispan.RoleAdapter;
import org.keycloak.models.cache.infinispan.counter.entities.RevisionedCachedClient;
import org.keycloak.models.cache.infinispan.counter.entities.RevisionedCachedClientRole;
import org.keycloak.models.cache.infinispan.counter.entities.RevisionedCachedClientTemplate;
import org.keycloak.models.cache.infinispan.counter.entities.RevisionedCachedGroup;
import org.keycloak.models.cache.infinispan.counter.entities.RevisionedCachedRealm;
import org.keycloak.models.cache.infinispan.counter.entities.RevisionedCachedRealmRole;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class RevisionedCacheRealmProvider implements CacheRealmProvider {
protected static final Logger logger = Logger.getLogger(RevisionedCacheRealmProvider.class);
protected RevisionedRealmCache cache;
protected KeycloakSession session;
protected RealmProvider delegate;
protected boolean transactionActive;
protected boolean setRollbackOnly;
protected Set<String> realmInvalidations = new HashSet<>();
protected Set<String> appInvalidations = new HashSet<>();
protected Set<String> clientTemplateInvalidations = new HashSet<>();
protected Set<String> roleInvalidations = new HashSet<>();
protected Set<String> groupInvalidations = new HashSet<>();
protected Map<String, RealmModel> managedRealms = new HashMap<>();
protected Map<String, ClientModel> managedApplications = new HashMap<>();
protected Map<String, ClientTemplateModel> managedClientTemplates = new HashMap<>();
protected Map<String, RoleModel> managedRoles = new HashMap<>();
protected Map<String, GroupModel> managedGroups = new HashMap<>();
protected boolean clearAll;
public RevisionedCacheRealmProvider(RevisionedRealmCache cache, KeycloakSession session) {
this.cache = cache;
this.session = session;
session.getTransaction().enlistAfterCompletion(getTransaction());
}
@Override
public void clear() {
cache.clear();
}
@Override
public MigrationModel getMigrationModel() {
return getDelegate().getMigrationModel();
}
@Override
public RealmProvider getDelegate() {
if (!transactionActive) throw new IllegalStateException("Cannot access delegate without a transaction");
if (delegate != null) return delegate;
delegate = session.getProvider(RealmProvider.class);
return delegate;
}
@Override
public void registerRealmInvalidation(String id) {
realmInvalidations.add(id);
}
@Override
public void registerApplicationInvalidation(String id) {
appInvalidations.add(id);
}
@Override
public void registerClientTemplateInvalidation(String id) {
clientTemplateInvalidations.add(id);
}
@Override
public void registerRoleInvalidation(String id) {
roleInvalidations.add(id);
}
@Override
public void registerGroupInvalidation(String id) {
groupInvalidations.add(id);
}
protected void runInvalidations() {
for (String id : realmInvalidations) {
cache.invalidateCachedRealmById(id);
}
for (String id : roleInvalidations) {
cache.invalidateRoleById(id);
}
for (String id : groupInvalidations) {
cache.invalidateGroupById(id);
}
for (String id : appInvalidations) {
cache.invalidateCachedApplicationById(id);
}
for (String id : clientTemplateInvalidations) {
cache.invalidateCachedClientTemplateById(id);
}
}
private KeycloakTransaction getTransaction() {
return new KeycloakTransaction() {
@Override
public void begin() {
transactionActive = true;
}
@Override
public void commit() {
if (delegate == null) return;
if (clearAll) {
cache.clear();
}
runInvalidations();
transactionActive = false;
}
@Override
public void rollback() {
setRollbackOnly = true;
runInvalidations();
transactionActive = false;
}
@Override
public void setRollbackOnly() {
setRollbackOnly = true;
}
@Override
public boolean getRollbackOnly() {
return setRollbackOnly;
}
@Override
public boolean isActive() {
return transactionActive;
}
};
}
@Override
public RealmModel createRealm(String name) {
RealmModel realm = getDelegate().createRealm(name);
registerRealmInvalidation(realm.getId());
return realm;
}
@Override
public RealmModel createRealm(String id, String name) {
RealmModel realm = getDelegate().createRealm(id, name);
registerRealmInvalidation(realm.getId());
return realm;
}
@Override
public RealmModel getRealm(String id) {
CachedRealm cached = cache.getCachedRealm(id);
if (cached != null) {
logger.tracev("by id cache hit: {0}", cached.getName());
}
if (cached == null) {
Long loaded = UpdateCounter.current();
RealmModel model = getDelegate().getRealm(id);
if (model == null) return null;
if (realmInvalidations.contains(id)) return model;
cached = new RevisionedCachedRealm(loaded, cache, this, model);
logger.tracev("try caching realm: {0} {1}", cached.getName(), loaded);
cache.addCachedRealm(cached);
} else if (realmInvalidations.contains(id)) {
return getDelegate().getRealm(id);
} else if (managedRealms.containsKey(id)) {
return managedRealms.get(id);
}
RealmAdapter adapter = new RealmAdapter(cached, this);
managedRealms.put(id, adapter);
return adapter;
}
@Override
public RealmModel getRealmByName(String name) {
CachedRealm cached = cache.getCachedRealmByName(name);
if (cached != null) {
logger.tracev("by name cache hit: {0}", cached.getName());
}
if (cached == null) {
Long loaded = UpdateCounter.current();
RealmModel model = getDelegate().getRealmByName(name);
if (model == null) return null;
if (realmInvalidations.contains(model.getId())) return model;
cached = new RevisionedCachedRealm(loaded, cache, this, model);
logger.tracev("try caching realm: {0}", cached.getName());
cache.addCachedRealm(cached);
} else if (realmInvalidations.contains(cached.getId())) {
return getDelegate().getRealmByName(name);
} else if (managedRealms.containsKey(cached.getId())) {
return managedRealms.get(cached.getId());
}
RealmAdapter adapter = new RealmAdapter(cached, this);
managedRealms.put(cached.getId(), adapter);
return adapter;
}
@Override
public List<RealmModel> getRealms() {
// Retrieve realms from backend
List<RealmModel> backendRealms = getDelegate().getRealms();
// Return cache delegates to ensure cache invalidated during write operations
List<RealmModel> cachedRealms = new LinkedList<RealmModel>();
for (RealmModel realm : backendRealms) {
RealmModel cached = getRealm(realm.getId());
cachedRealms.add(cached);
}
return cachedRealms;
}
@Override
public boolean removeRealm(String id) {
cache.invalidateCachedRealmById(id);
RealmModel realm = getDelegate().getRealm(id);
Set<RoleModel> realmRoles = null;
if (realm != null) {
realmRoles = realm.getRoles();
}
boolean didIt = getDelegate().removeRealm(id);
realmInvalidations.add(id);
// TODO: Temporary workaround to invalidate cached realm roles
if (didIt && realmRoles != null) {
for (RoleModel role : realmRoles) {
roleInvalidations.add(role.getId());
}
}
return didIt;
}
@Override
public void close() {
if (delegate != null) delegate.close();
}
@Override
public RoleModel getRoleById(String id, RealmModel realm) {
CachedRole cached = cache.getRole(id);
if (cached != null && !cached.getRealm().equals(realm.getId())) {
cached = null;
}
if (cached == null) {
Long loaded = UpdateCounter.current();
RoleModel model = getDelegate().getRoleById(id, realm);
if (model == null) return null;
if (roleInvalidations.contains(id)) return model;
if (model.getContainer() instanceof ClientModel) {
cached = new RevisionedCachedClientRole(loaded, ((ClientModel) model.getContainer()).getId(), model, realm);
} else {
cached = new RevisionedCachedRealmRole(loaded, model, realm);
}
cache.addCachedRole(cached);
} else if (roleInvalidations.contains(id)) {
return getDelegate().getRoleById(id, realm);
} else if (managedRoles.containsKey(id)) {
return managedRoles.get(id);
}
RoleAdapter adapter = new RoleAdapter(cached, cache, this, realm);
managedRoles.put(id, adapter);
return adapter;
}
@Override
public GroupModel getGroupById(String id, RealmModel realm) {
CachedGroup cached = cache.getGroup(id);
if (cached != null && !cached.getRealm().equals(realm.getId())) {
cached = null;
}
if (cached == null) {
Long loaded = UpdateCounter.current();
GroupModel model = getDelegate().getGroupById(id, realm);
if (model == null) return null;
if (groupInvalidations.contains(id)) return model;
cached = new RevisionedCachedGroup(loaded, realm, model);
cache.addCachedGroup(cached);
} else if (groupInvalidations.contains(id)) {
return getDelegate().getGroupById(id, realm);
} else if (managedGroups.containsKey(id)) {
return managedGroups.get(id);
}
GroupAdapter adapter = new GroupAdapter(cached, this, session, realm);
managedGroups.put(id, adapter);
return adapter;
}
@Override
public ClientModel getClientById(String id, RealmModel realm) {
CachedClient cached = cache.getApplication(id);
if (cached != null && !cached.getRealm().equals(realm.getId())) {
cached = null;
}
if (cached != null) {
logger.tracev("client by id cache hit: {0}", cached.getClientId());
}
if (cached == null) {
Long loaded = UpdateCounter.current();
ClientModel model = getDelegate().getClientById(id, realm);
if (model == null) return null;
if (appInvalidations.contains(id)) return model;
cached = new RevisionedCachedClient(loaded, cache, getDelegate(), realm, model);
cache.addCachedClient(cached);
} else if (appInvalidations.contains(id)) {
return getDelegate().getClientById(id, realm);
} else if (managedApplications.containsKey(id)) {
return managedApplications.get(id);
}
ClientAdapter adapter = new ClientAdapter(realm, cached, this, cache);
managedApplications.put(id, adapter);
return adapter;
}
@Override
public ClientTemplateModel getClientTemplateById(String id, RealmModel realm) {
CachedClientTemplate cached = cache.getClientTemplate(id);
if (cached != null && !cached.getRealm().equals(realm.getId())) {
cached = null;
}
if (cached == null) {
Long loaded = UpdateCounter.current();
ClientTemplateModel model = getDelegate().getClientTemplateById(id, realm);
if (model == null) return null;
if (clientTemplateInvalidations.contains(id)) return model;
cached = new RevisionedCachedClientTemplate(loaded, cache, getDelegate(), realm, model);
cache.addCachedClientTemplate(cached);
} else if (clientTemplateInvalidations.contains(id)) {
return getDelegate().getClientTemplateById(id, realm);
} else if (managedClientTemplates.containsKey(id)) {
return managedClientTemplates.get(id);
}
ClientTemplateModel adapter = new ClientTemplateAdapter(realm, cached, this, cache);
managedClientTemplates.put(id, adapter);
return adapter;
}
}

View file

@ -0,0 +1,161 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.cache.infinispan.counter;
import org.infinispan.Cache;
import org.infinispan.notifications.Listener;
import org.infinispan.notifications.cachelistener.annotation.CacheEntriesEvicted;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryInvalidated;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryRemoved;
import org.infinispan.notifications.cachelistener.event.CacheEntriesEvictedEvent;
import org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent;
import org.infinispan.notifications.cachelistener.event.CacheEntryInvalidatedEvent;
import org.infinispan.notifications.cachelistener.event.CacheEntryRemovedEvent;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.cache.CacheRealmProvider;
import org.keycloak.models.cache.CacheRealmProviderFactory;
import org.keycloak.models.cache.entities.CachedClient;
import org.keycloak.models.cache.entities.CachedRealm;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class RevisionedCacheRealmProviderFactory implements CacheRealmProviderFactory {
private static final Logger log = Logger.getLogger(RevisionedCacheRealmProviderFactory.class);
protected volatile RevisionedRealmCache realmCache;
protected final ConcurrentHashMap<String, String> realmLookup = new ConcurrentHashMap<>();
@Override
public CacheRealmProvider create(KeycloakSession session) {
lazyInit(session);
return new RevisionedCacheRealmProvider(realmCache, session);
}
private void lazyInit(KeycloakSession session) {
if (realmCache == null) {
synchronized (this) {
if (realmCache == null) {
Cache<String, Object> cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
Cache<String, Long> counterCache = session.getProvider(InfinispanConnectionProvider.class).getCache(RevisionedConnectionProviderFactory.VERSION_CACHE_NAME);
cache.addListener(new CacheListener());
realmCache = new RevisionedRealmCache(cache, counterCache, realmLookup);
}
}
}
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return "infinispan-revisioned";
}
@Listener
public class CacheListener {
@CacheEntryCreated
public void created(CacheEntryCreatedEvent<String, Object> event) {
if (!event.isPre()) {
Object object = event.getValue();
if (object != null) {
if (object instanceof CachedRealm) {
CachedRealm realm = (CachedRealm) object;
realmLookup.put(realm.getName(), realm.getId());
log.tracev("Realm added realm={0}", realm.getName());
}
}
}
}
@CacheEntryRemoved
public void removed(CacheEntryRemovedEvent<String, Object> event) {
if (event.isPre()) {
Object object = event.getValue();
if (object != null) {
remove(object);
}
}
}
@CacheEntryInvalidated
public void removed(CacheEntryInvalidatedEvent<String, Object> event) {
if (event.isPre()) {
Object object = event.getValue();
if (object != null) {
remove(object);
}
}
}
@CacheEntriesEvicted
public void userEvicted(CacheEntriesEvictedEvent<String, Object> event) {
for (Object object : event.getEntries().values()) {
remove(object);
}
}
private void remove(Object object) {
if (object instanceof CachedRealm) {
CachedRealm realm = (CachedRealm) object;
realmLookup.remove(realm.getName());
for (String r : realm.getRealmRoles().values()) {
realmCache.evictCachedRoleById(r);
}
for (String c : realm.getClients().values()) {
realmCache.evictCachedApplicationById(c);
}
log.tracev("Realm removed realm={0}", realm.getName());
} else if (object instanceof CachedClient) {
CachedClient client = (CachedClient) object;
for (String r : client.getRoles().values()) {
realmCache.evictCachedRoleById(r);
}
log.tracev("Client removed client={0}", client.getId());
}
}
}
}

View file

@ -0,0 +1,47 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.cache.infinispan.counter;
import org.infinispan.configuration.cache.Configuration;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.jboss.logging.Logger;
import org.keycloak.connections.infinispan.DefaultInfinispanConnectionProviderFactory;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class RevisionedConnectionProviderFactory extends DefaultInfinispanConnectionProviderFactory {
public static final String VERSION_CACHE_NAME = "realmVersions";
protected static final Logger logger = Logger.getLogger(RevisionedConnectionProviderFactory.class);
@Override
public String getId() {
return "revisioned";
}
protected void initEmbedded() {
super.initEmbedded();
ConfigurationBuilder counterConfigBuilder = new ConfigurationBuilder();
Configuration counterCacheConfiguration = counterConfigBuilder.build();
cacheManager.defineConfiguration(VERSION_CACHE_NAME, counterCacheConfiguration);
}
}

View file

@ -0,0 +1,255 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.cache.infinispan.counter;
import org.infinispan.Cache;
import org.jboss.logging.Logger;
import org.keycloak.models.cache.RealmCache;
import org.keycloak.models.cache.entities.CachedClient;
import org.keycloak.models.cache.entities.CachedClientTemplate;
import org.keycloak.models.cache.entities.CachedGroup;
import org.keycloak.models.cache.entities.CachedRealm;
import org.keycloak.models.cache.entities.CachedRole;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class RevisionedRealmCache implements RealmCache {
protected static final Logger logger = Logger.getLogger(RevisionedRealmCache.class);
protected final Cache<String, Long> revisions;
protected final Cache<String, Object> cache;
protected final ConcurrentHashMap<String, String> realmLookup;
public RevisionedRealmCache(Cache<String, Object> cache, Cache<String, Long> revisions, ConcurrentHashMap<String, String> realmLookup) {
this.cache = cache;
this.realmLookup = realmLookup;
this.revisions = revisions;
}
public Cache<String, Object> getCache() {
return cache;
}
private <T> T get(String id, Class<T> type) {
Revisioned o = (Revisioned)cache.get(id);
if (o == null) {
return null;
}
Long rev = revisions.get(id);
if (rev == null) {
logger.tracev("get() missing rev");
return null;
}
long oRev = o.getRevision() == null ? -1L : o.getRevision().longValue();
if (rev > oRev) {
logger.tracev("stale rev: {0} o.rev: {1}", rev.longValue(), oRev);
return null;
}
return o != null && type.isInstance(o) ? type.cast(o) : null;
}
protected Object invalidateObject(String id) {
Object removed = cache.remove(id);
revisions.put(id, UpdateCounter.next());
return removed;
}
protected void addRevisioned(String id, Revisioned object) {
Long rev = revisions.get(id);
if (rev == null) {
logger.tracev("rev was null in addRevisioned, adding one");
rev = UpdateCounter.next();
revisions.put(id, rev);
return;
}
cache.putForExternalRead(id, object);
}
@Override
public void clear() {
cache.clear();
}
@Override
public CachedRealm getCachedRealm(String id) {
return get(id, CachedRealm.class);
}
@Override
public void invalidateCachedRealm(CachedRealm realm) {
logger.tracev("Invalidating realm {0}", realm.getId());
invalidateObject(realm.getId());
realmLookup.remove(realm.getName());
}
@Override
public void invalidateCachedRealmById(String id) {
CachedRealm cached = (CachedRealm) invalidateObject(id);
if (cached != null) realmLookup.remove(cached.getName());
}
@Override
public void addCachedRealm(CachedRealm realm) {
logger.tracev("Adding realm {0}", realm.getId());
addRevisioned(realm.getId(), (Revisioned) realm);
realmLookup.put(realm.getName(), realm.getId());
}
@Override
public CachedRealm getCachedRealmByName(String name) {
String id = realmLookup.get(name);
return id != null ? getCachedRealm(id) : null;
}
@Override
public CachedClient getApplication(String id) {
return get(id, CachedClient.class);
}
@Override
public void invalidateApplication(CachedClient app) {
logger.tracev("Removing application {0}", app.getId());
invalidateObject(app.getId());
}
@Override
public void addCachedClient(CachedClient app) {
logger.tracev("Adding application {0}", app.getId());
addRevisioned(app.getId(), (Revisioned) app);
}
@Override
public void invalidateCachedApplicationById(String id) {
CachedClient client = (CachedClient)invalidateObject(id);
if (client != null) logger.tracev("Removing application {0}", client.getClientId());
}
@Override
public void evictCachedApplicationById(String id) {
logger.tracev("Evicting application {0}", id);
cache.evict(id);
}
@Override
public CachedGroup getGroup(String id) {
return get(id, CachedGroup.class);
}
@Override
public void invalidateGroup(CachedGroup role) {
logger.tracev("Removing group {0}", role.getId());
invalidateObject(role.getId());
}
@Override
public void addCachedGroup(CachedGroup role) {
logger.tracev("Adding group {0}", role.getId());
addRevisioned(role.getId(), (Revisioned) role);
}
@Override
public void invalidateCachedGroupById(String id) {
logger.tracev("Removing group {0}", id);
invalidateObject(id);
}
@Override
public void invalidateGroupById(String id) {
logger.tracev("Removing group {0}", id);
invalidateObject(id);
}
@Override
public CachedRole getRole(String id) {
return get(id, CachedRole.class);
}
@Override
public void invalidateRole(CachedRole role) {
logger.tracev("Removing role {0}", role.getId());
invalidateObject(role.getId());
}
@Override
public void invalidateRoleById(String id) {
logger.tracev("Removing role {0}", id);
invalidateObject(id);
}
@Override
public void evictCachedRoleById(String id) {
logger.tracev("Evicting role {0}", id);
cache.evict(id);
}
@Override
public void addCachedRole(CachedRole role) {
logger.tracev("Adding role {0}", role.getId());
addRevisioned(role.getId(), (Revisioned) role);
}
@Override
public void invalidateCachedRoleById(String id) {
logger.tracev("Removing role {0}", id);
invalidateObject(id);
}
@Override
public CachedClientTemplate getClientTemplate(String id) {
return get(id, CachedClientTemplate.class);
}
@Override
public void invalidateClientTemplate(CachedClientTemplate app) {
logger.tracev("Removing client template {0}", app.getId());
invalidateObject(app.getId());
}
@Override
public void addCachedClientTemplate(CachedClientTemplate app) {
logger.tracev("Adding client template {0}", app.getId());
addRevisioned(app.getId(), (Revisioned) app);
}
@Override
public void invalidateCachedClientTemplateById(String id) {
logger.tracev("Removing client template {0}", id);
invalidateObject(id);
}
@Override
public void evictCachedClientTemplateById(String id) {
logger.tracev("Evicting client template {0}", id);
cache.evict(id);
}
}

View file

@ -0,0 +1,20 @@
package org.keycloak.models.cache.infinispan.counter;
import java.util.concurrent.atomic.AtomicLong;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class UpdateCounter {
private static final AtomicLong counter = new AtomicLong();
public static long current() {
return counter.get();
}
public static long next() {
return counter.incrementAndGet();
}
}

View file

@ -0,0 +1,41 @@
package org.keycloak.models.cache.infinispan.counter.entities;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RealmProvider;
import org.keycloak.models.RoleModel;
import org.keycloak.models.cache.RealmCache;
import org.keycloak.models.cache.entities.CachedClient;
import org.keycloak.models.cache.entities.CachedClientRole;
import org.keycloak.models.cache.infinispan.counter.Revisioned;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class RevisionedCachedClient extends CachedClient implements Revisioned {
public RevisionedCachedClient(Long revision, RealmCache cache, RealmProvider delegate, RealmModel realm, ClientModel model) {
super(cache, delegate, realm, model);
this.revision = revision;
}
private Long revision;
@Override
public Long getRevision() {
return revision;
}
@Override
public void setRevision(Long revision) {
this.revision = revision;
}
@Override
protected void cacheRoles(RealmCache cache, RealmModel realm, ClientModel model) {
for (RoleModel role : model.getRoles()) {
roles.put(role.getName(), role.getId());
}
}
}

View file

@ -0,0 +1,32 @@
package org.keycloak.models.cache.infinispan.counter.entities;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.cache.entities.CachedClientRole;
import org.keycloak.models.cache.infinispan.counter.Revisioned;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class RevisionedCachedClientRole extends CachedClientRole implements Revisioned {
public RevisionedCachedClientRole(Long revision, String idClient, RoleModel model, RealmModel realm) {
super(idClient, model, realm);
this.revision = revision;
}
private Long revision;
@Override
public Long getRevision() {
return revision;
}
@Override
public void setRevision(Long revision) {
this.revision = revision;
}
}

View file

@ -0,0 +1,34 @@
package org.keycloak.models.cache.infinispan.counter.entities;
import org.keycloak.models.ClientTemplateModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RealmProvider;
import org.keycloak.models.cache.RealmCache;
import org.keycloak.models.cache.entities.CachedClientTemplate;
import org.keycloak.models.cache.infinispan.counter.Revisioned;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class RevisionedCachedClientTemplate extends CachedClientTemplate implements Revisioned {
public RevisionedCachedClientTemplate(Long revision, RealmCache cache, RealmProvider delegate, RealmModel realm, ClientTemplateModel model) {
super(cache, delegate, realm, model);
this.revision = revision;
}
private Long revision;
@Override
public Long getRevision() {
return revision;
}
@Override
public void setRevision(Long revision) {
this.revision = revision;
}
}

View file

@ -0,0 +1,31 @@
package org.keycloak.models.cache.infinispan.counter.entities;
import org.keycloak.models.GroupModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.cache.entities.CachedGroup;
import org.keycloak.models.cache.infinispan.counter.Revisioned;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class RevisionedCachedGroup extends CachedGroup implements Revisioned {
public RevisionedCachedGroup(Long revision, RealmModel realm, GroupModel group) {
super(realm, group);
this.revision = revision;
}
private Long revision;
@Override
public Long getRevision() {
return revision;
}
@Override
public void setRevision(Long revision) {
this.revision = revision;
}
}

View file

@ -0,0 +1,59 @@
package org.keycloak.models.cache.infinispan.counter.entities;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientTemplateModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RealmProvider;
import org.keycloak.models.RoleModel;
import org.keycloak.models.cache.RealmCache;
import org.keycloak.models.cache.entities.CachedClient;
import org.keycloak.models.cache.entities.CachedClientTemplate;
import org.keycloak.models.cache.entities.CachedRealm;
import org.keycloak.models.cache.entities.CachedRealmRole;
import org.keycloak.models.cache.entities.CachedRole;
import org.keycloak.models.cache.infinispan.counter.Revisioned;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class RevisionedCachedRealm extends CachedRealm implements Revisioned {
public RevisionedCachedRealm(Long revision, RealmCache cache, RealmProvider delegate, RealmModel model) {
super(cache, delegate, model);
this.revision = revision;
}
private Long revision;
@Override
public Long getRevision() {
return revision;
}
@Override
public void setRevision(Long revision) {
this.revision = revision;
}
@Override
protected void cacheClientTemplates(RealmCache cache, RealmProvider delegate, RealmModel model) {
for (ClientTemplateModel template : model.getClientTemplates()) {
clientTemplates.add(template.getId());
}
}
@Override
protected void cacheClients(RealmCache cache, RealmProvider delegate, RealmModel model) {
for (ClientModel client : model.getClients()) {
clients.put(client.getClientId(), client.getId());
}
}
@Override
protected void cacheRealmRoles(RealmCache cache, RealmModel model) {
for (RoleModel role : model.getRoles()) {
realmRoles.put(role.getName(), role.getId());
}
}
}

View file

@ -0,0 +1,31 @@
package org.keycloak.models.cache.infinispan.counter.entities;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.cache.entities.CachedRealmRole;
import org.keycloak.models.cache.infinispan.counter.Revisioned;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class RevisionedCachedRealmRole extends CachedRealmRole implements Revisioned {
public RevisionedCachedRealmRole(Long revision, RoleModel model, RealmModel realm) {
super(model, realm);
this.revision = revision;
}
private Long revision;
@Override
public Long getRevision() {
return revision;
}
@Override
public void setRevision(Long revision) {
this.revision = revision;
}
}

View file

@ -0,0 +1,30 @@
package org.keycloak.models.cache.infinispan.counter.entities;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.cache.entities.CachedUser;
import org.keycloak.models.cache.infinispan.counter.Revisioned;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class RevisionedCachedUser extends CachedUser implements Revisioned {
public RevisionedCachedUser(Long revision, RealmModel realm, UserModel user) {
super(realm, user);
this.revision = revision;
}
private Long revision;
@Override
public Long getRevision() {
return revision;
}
@Override
public void setRevision(Long revision) {
this.revision = revision;
}
}

View file

@ -0,0 +1,463 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.cache.infinispan.locking;
import org.jboss.logging.Logger;
import org.keycloak.migration.MigrationModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientTemplateModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakTransaction;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RealmProvider;
import org.keycloak.models.RoleModel;
import org.keycloak.models.cache.CacheRealmProvider;
import org.keycloak.models.cache.entities.CachedClient;
import org.keycloak.models.cache.entities.CachedClientTemplate;
import org.keycloak.models.cache.entities.CachedGroup;
import org.keycloak.models.cache.entities.CachedRealm;
import org.keycloak.models.cache.entities.CachedRole;
import org.keycloak.models.cache.infinispan.ClientAdapter;
import org.keycloak.models.cache.infinispan.ClientTemplateAdapter;
import org.keycloak.models.cache.infinispan.GroupAdapter;
import org.keycloak.models.cache.infinispan.RealmAdapter;
import org.keycloak.models.cache.infinispan.RoleAdapter;
import org.keycloak.models.cache.infinispan.counter.entities.RevisionedCachedClient;
import org.keycloak.models.cache.infinispan.counter.entities.RevisionedCachedClientRole;
import org.keycloak.models.cache.infinispan.counter.entities.RevisionedCachedClientTemplate;
import org.keycloak.models.cache.infinispan.counter.entities.RevisionedCachedGroup;
import org.keycloak.models.cache.infinispan.counter.entities.RevisionedCachedRealm;
import org.keycloak.models.cache.infinispan.counter.entities.RevisionedCachedRealmRole;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class LockingCacheRealmProvider implements CacheRealmProvider {
protected static final Logger logger = Logger.getLogger(LockingCacheRealmProvider.class);
protected LockingRealmCache cache;
protected KeycloakSession session;
protected RealmProvider delegate;
protected boolean transactionActive;
protected boolean setRollbackOnly;
protected Set<String> realmInvalidations = new HashSet<>();
protected Set<String> appInvalidations = new HashSet<>();
protected Set<String> clientTemplateInvalidations = new HashSet<>();
protected Set<String> roleInvalidations = new HashSet<>();
protected Set<String> groupInvalidations = new HashSet<>();
protected Map<String, RealmModel> managedRealms = new HashMap<>();
protected Map<String, ClientModel> managedApplications = new HashMap<>();
protected Map<String, ClientTemplateModel> managedClientTemplates = new HashMap<>();
protected Map<String, RoleModel> managedRoles = new HashMap<>();
protected Map<String, GroupModel> managedGroups = new HashMap<>();
protected boolean clearAll;
public LockingCacheRealmProvider(LockingRealmCache cache, KeycloakSession session) {
this.cache = cache;
this.session = session;
session.getTransaction().enlistPrepare(getPrepareTransaction());
session.getTransaction().enlistAfterCompletion(getAfterTransaction());
}
@Override
public void clear() {
cache.clear();
}
@Override
public MigrationModel getMigrationModel() {
return getDelegate().getMigrationModel();
}
@Override
public RealmProvider getDelegate() {
if (!transactionActive) throw new IllegalStateException("Cannot access delegate without a transaction");
if (delegate != null) return delegate;
delegate = session.getProvider(RealmProvider.class);
return delegate;
}
@Override
public void registerRealmInvalidation(String id) {
realmInvalidations.add(id);
}
@Override
public void registerApplicationInvalidation(String id) {
appInvalidations.add(id);
}
@Override
public void registerClientTemplateInvalidation(String id) {
clientTemplateInvalidations.add(id);
}
@Override
public void registerRoleInvalidation(String id) {
roleInvalidations.add(id);
}
@Override
public void registerGroupInvalidation(String id) {
groupInvalidations.add(id);
}
protected void runInvalidations() {
for (String id : realmInvalidations) {
cache.invalidateCachedRealmById(id);
}
for (String id : roleInvalidations) {
cache.invalidateRoleById(id);
}
for (String id : groupInvalidations) {
cache.invalidateGroupById(id);
}
for (String id : appInvalidations) {
cache.invalidateCachedApplicationById(id);
}
for (String id : clientTemplateInvalidations) {
cache.invalidateCachedClientTemplateById(id);
}
}
private KeycloakTransaction getPrepareTransaction() {
return new KeycloakTransaction() {
@Override
public void begin() {
transactionActive = true;
}
@Override
public void commit() {
if (delegate == null) return;
List<String> invalidates = new LinkedList<>();
for (String id : realmInvalidations) {
invalidates.add(id);
}
for (String id : roleInvalidations) {
invalidates.add(id);
}
for (String id : groupInvalidations) {
invalidates.add(id);
}
for (String id : appInvalidations) {
invalidates.add(id);
}
for (String id : clientTemplateInvalidations) {
invalidates.add(id);
}
Collections.sort(invalidates); // lock ordering
cache.getRevisions().startBatch();
for (String id : invalidates) {
cache.getRevisions().getAdvancedCache().lock(id);
}
}
@Override
public void rollback() {
setRollbackOnly = true;
transactionActive = false;
}
@Override
public void setRollbackOnly() {
setRollbackOnly = true;
}
@Override
public boolean getRollbackOnly() {
return setRollbackOnly;
}
@Override
public boolean isActive() {
return transactionActive;
}
};
}
private KeycloakTransaction getAfterTransaction() {
return new KeycloakTransaction() {
@Override
public void begin() {
transactionActive = true;
}
@Override
public void commit() {
try {
if (delegate == null) return;
if (clearAll) {
cache.clear();
}
runInvalidations();
transactionActive = false;
} finally {
cache.endRevisionBatch();
}
}
@Override
public void rollback() {
try {
setRollbackOnly = true;
runInvalidations();
transactionActive = false;
} finally {
cache.endRevisionBatch();
}
}
@Override
public void setRollbackOnly() {
setRollbackOnly = true;
}
@Override
public boolean getRollbackOnly() {
return setRollbackOnly;
}
@Override
public boolean isActive() {
return transactionActive;
}
};
}
@Override
public RealmModel createRealm(String name) {
RealmModel realm = getDelegate().createRealm(name);
registerRealmInvalidation(realm.getId());
return realm;
}
@Override
public RealmModel createRealm(String id, String name) {
RealmModel realm = getDelegate().createRealm(id, name);
registerRealmInvalidation(realm.getId());
return realm;
}
@Override
public RealmModel getRealm(String id) {
CachedRealm cached = cache.getCachedRealm(id);
if (cached != null) {
logger.tracev("by id cache hit: {0}", cached.getName());
}
if (cached == null) {
Long loaded = cache.getCurrentRevision(id);
RealmModel model = getDelegate().getRealm(id);
if (model == null) return null;
if (realmInvalidations.contains(id)) return model;
cached = new RevisionedCachedRealm(loaded, cache, this, model);
cache.addCachedRealm(cached);
} else if (realmInvalidations.contains(id)) {
return getDelegate().getRealm(id);
} else if (managedRealms.containsKey(id)) {
return managedRealms.get(id);
}
RealmAdapter adapter = new RealmAdapter(cached, this);
managedRealms.put(id, adapter);
return adapter;
}
@Override
public RealmModel getRealmByName(String name) {
CachedRealm cached = cache.getCachedRealmByName(name);
if (cached != null) {
logger.tracev("by name cache hit: {0}", cached.getName());
}
if (cached == null) {
RealmModel model = getDelegate().getRealmByName(name);
if (model == null) return null;
if (realmInvalidations.contains(model.getId())) return model;
cached = new RevisionedCachedRealm(null, cache, this, model);
cache.addCachedRealm(cached);
} else if (realmInvalidations.contains(cached.getId())) {
return getDelegate().getRealmByName(name);
} else if (managedRealms.containsKey(cached.getId())) {
return managedRealms.get(cached.getId());
}
RealmAdapter adapter = new RealmAdapter(cached, this);
managedRealms.put(cached.getId(), adapter);
return adapter;
}
@Override
public List<RealmModel> getRealms() {
// Retrieve realms from backend
List<RealmModel> backendRealms = getDelegate().getRealms();
// Return cache delegates to ensure cache invalidated during write operations
List<RealmModel> cachedRealms = new LinkedList<RealmModel>();
for (RealmModel realm : backendRealms) {
RealmModel cached = getRealm(realm.getId());
cachedRealms.add(cached);
}
return cachedRealms;
}
@Override
public boolean removeRealm(String id) {
cache.invalidateCachedRealmById(id);
RealmModel realm = getDelegate().getRealm(id);
Set<RoleModel> realmRoles = null;
if (realm != null) {
realmRoles = realm.getRoles();
}
boolean didIt = getDelegate().removeRealm(id);
realmInvalidations.add(id);
// TODO: Temporary workaround to invalidate cached realm roles
if (didIt && realmRoles != null) {
for (RoleModel role : realmRoles) {
roleInvalidations.add(role.getId());
}
}
return didIt;
}
@Override
public void close() {
if (delegate != null) delegate.close();
}
@Override
public RoleModel getRoleById(String id, RealmModel realm) {
CachedRole cached = cache.getRole(id);
if (cached != null && !cached.getRealm().equals(realm.getId())) {
cached = null;
}
if (cached == null) {
Long loaded = cache.getCurrentRevision(id);
RoleModel model = getDelegate().getRoleById(id, realm);
if (model == null) return null;
if (roleInvalidations.contains(id)) return model;
if (model.getContainer() instanceof ClientModel) {
cached = new RevisionedCachedClientRole(loaded, ((ClientModel) model.getContainer()).getId(), model, realm);
} else {
cached = new RevisionedCachedRealmRole(loaded, model, realm);
}
cache.addCachedRole(cached);
} else if (roleInvalidations.contains(id)) {
return getDelegate().getRoleById(id, realm);
} else if (managedRoles.containsKey(id)) {
return managedRoles.get(id);
}
RoleAdapter adapter = new RoleAdapter(cached, cache, this, realm);
managedRoles.put(id, adapter);
return adapter;
}
@Override
public GroupModel getGroupById(String id, RealmModel realm) {
CachedGroup cached = cache.getGroup(id);
if (cached != null && !cached.getRealm().equals(realm.getId())) {
cached = null;
}
if (cached == null) {
Long loaded = cache.getCurrentRevision(id);
GroupModel model = getDelegate().getGroupById(id, realm);
if (model == null) return null;
if (groupInvalidations.contains(id)) return model;
cached = new RevisionedCachedGroup(loaded, realm, model);
cache.addCachedGroup(cached);
} else if (groupInvalidations.contains(id)) {
return getDelegate().getGroupById(id, realm);
} else if (managedGroups.containsKey(id)) {
return managedGroups.get(id);
}
GroupAdapter adapter = new GroupAdapter(cached, this, session, realm);
managedGroups.put(id, adapter);
return adapter;
}
@Override
public ClientModel getClientById(String id, RealmModel realm) {
CachedClient cached = cache.getApplication(id);
if (cached != null && !cached.getRealm().equals(realm.getId())) {
cached = null;
}
if (cached != null && cached.getClientId().equals("client")) {
logger.tracev("client by id cache hit: {0}", cached.getClientId());
}
if (cached == null) {
Long loaded = cache.getCurrentRevision(id);
ClientModel model = getDelegate().getClientById(id, realm);
if (model == null) return null;
if (appInvalidations.contains(id)) return model;
cached = new RevisionedCachedClient(loaded, cache, getDelegate(), realm, model);
cache.addCachedClient(cached);
} else if (appInvalidations.contains(id)) {
return getDelegate().getClientById(id, realm);
} else if (managedApplications.containsKey(id)) {
return managedApplications.get(id);
}
ClientAdapter adapter = new ClientAdapter(realm, cached, this, cache);
managedApplications.put(id, adapter);
return adapter;
}
@Override
public ClientTemplateModel getClientTemplateById(String id, RealmModel realm) {
CachedClientTemplate cached = cache.getClientTemplate(id);
if (cached != null && !cached.getRealm().equals(realm.getId())) {
cached = null;
}
if (cached == null) {
Long loaded = cache.getCurrentRevision(id);
ClientTemplateModel model = getDelegate().getClientTemplateById(id, realm);
if (model == null) return null;
if (clientTemplateInvalidations.contains(id)) return model;
cached = new RevisionedCachedClientTemplate(loaded, cache, getDelegate(), realm, model);
cache.addCachedClientTemplate(cached);
} else if (clientTemplateInvalidations.contains(id)) {
return getDelegate().getClientTemplateById(id, realm);
} else if (managedClientTemplates.containsKey(id)) {
return managedClientTemplates.get(id);
}
ClientTemplateModel adapter = new ClientTemplateAdapter(realm, cached, this, cache);
managedClientTemplates.put(id, adapter);
return adapter;
}
}

View file

@ -0,0 +1,161 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.cache.infinispan.locking;
import org.infinispan.Cache;
import org.infinispan.notifications.Listener;
import org.infinispan.notifications.cachelistener.annotation.CacheEntriesEvicted;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryInvalidated;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryRemoved;
import org.infinispan.notifications.cachelistener.event.CacheEntriesEvictedEvent;
import org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent;
import org.infinispan.notifications.cachelistener.event.CacheEntryInvalidatedEvent;
import org.infinispan.notifications.cachelistener.event.CacheEntryRemovedEvent;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.cache.CacheRealmProvider;
import org.keycloak.models.cache.CacheRealmProviderFactory;
import org.keycloak.models.cache.entities.CachedClient;
import org.keycloak.models.cache.entities.CachedRealm;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class LockingCacheRealmProviderFactory implements CacheRealmProviderFactory {
private static final Logger log = Logger.getLogger(LockingCacheRealmProviderFactory.class);
protected volatile LockingRealmCache realmCache;
protected final ConcurrentHashMap<String, String> realmLookup = new ConcurrentHashMap<>();
@Override
public CacheRealmProvider create(KeycloakSession session) {
lazyInit(session);
return new LockingCacheRealmProvider(realmCache, session);
}
private void lazyInit(KeycloakSession session) {
if (realmCache == null) {
synchronized (this) {
if (realmCache == null) {
Cache<String, Object> cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
Cache<String, Long> counterCache = session.getProvider(InfinispanConnectionProvider.class).getCache(LockingConnectionProviderFactory.VERSION_CACHE_NAME);
cache.addListener(new CacheListener());
realmCache = new LockingRealmCache(cache, counterCache, realmLookup);
}
}
}
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return "infinispan-locking";
}
@Listener
public class CacheListener {
@CacheEntryCreated
public void created(CacheEntryCreatedEvent<String, Object> event) {
if (!event.isPre()) {
Object object = event.getValue();
if (object != null) {
if (object instanceof CachedRealm) {
CachedRealm realm = (CachedRealm) object;
realmLookup.put(realm.getName(), realm.getId());
log.tracev("Realm added realm={0}", realm.getName());
}
}
}
}
@CacheEntryRemoved
public void removed(CacheEntryRemovedEvent<String, Object> event) {
if (event.isPre()) {
Object object = event.getValue();
if (object != null) {
remove(object);
}
}
}
@CacheEntryInvalidated
public void removed(CacheEntryInvalidatedEvent<String, Object> event) {
if (event.isPre()) {
Object object = event.getValue();
if (object != null) {
remove(object);
}
}
}
@CacheEntriesEvicted
public void userEvicted(CacheEntriesEvictedEvent<String, Object> event) {
for (Object object : event.getEntries().values()) {
remove(object);
}
}
private void remove(Object object) {
if (object instanceof CachedRealm) {
CachedRealm realm = (CachedRealm) object;
realmLookup.remove(realm.getName());
for (String r : realm.getRealmRoles().values()) {
realmCache.evictCachedRoleById(r);
}
for (String c : realm.getClients().values()) {
realmCache.evictCachedApplicationById(c);
}
log.tracev("Realm removed realm={0}", realm.getName());
} else if (object instanceof CachedClient) {
CachedClient client = (CachedClient) object;
for (String r : client.getRoles().values()) {
realmCache.evictCachedRoleById(r);
}
log.tracev("Client removed client={0}", client.getId());
}
}
}
}

View file

@ -0,0 +1,54 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.cache.infinispan.locking;
import org.infinispan.configuration.cache.Configuration;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.transaction.LockingMode;
import org.infinispan.transaction.TransactionMode;
import org.infinispan.transaction.lookup.DummyTransactionManagerLookup;
import org.jboss.logging.Logger;
import org.keycloak.connections.infinispan.DefaultInfinispanConnectionProviderFactory;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class LockingConnectionProviderFactory extends DefaultInfinispanConnectionProviderFactory {
public static final String VERSION_CACHE_NAME = "realmVersions";
protected static final Logger logger = Logger.getLogger(LockingConnectionProviderFactory.class);
@Override
public String getId() {
return "locking";
}
protected void initEmbedded() {
super.initEmbedded();
ConfigurationBuilder counterConfigBuilder = new ConfigurationBuilder();
counterConfigBuilder.invocationBatching().enable()
.transaction().transactionMode(TransactionMode.TRANSACTIONAL);
counterConfigBuilder.transaction().transactionManagerLookup(new DummyTransactionManagerLookup());
counterConfigBuilder.transaction().lockingMode(LockingMode.PESSIMISTIC);
Configuration counterCacheConfiguration = counterConfigBuilder.build();
cacheManager.defineConfiguration(VERSION_CACHE_NAME, counterCacheConfiguration);
}
}

View file

@ -0,0 +1,294 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.cache.infinispan.locking;
import org.infinispan.Cache;
import org.jboss.logging.Logger;
import org.keycloak.models.cache.RealmCache;
import org.keycloak.models.cache.entities.CachedClient;
import org.keycloak.models.cache.entities.CachedClientTemplate;
import org.keycloak.models.cache.entities.CachedGroup;
import org.keycloak.models.cache.entities.CachedRealm;
import org.keycloak.models.cache.entities.CachedRole;
import org.keycloak.models.cache.infinispan.counter.Revisioned;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class LockingRealmCache implements RealmCache {
protected static final Logger logger = Logger.getLogger(LockingRealmCache.class);
protected final Cache<String, Long> revisions;
protected final Cache<String, Object> cache;
final AtomicLong realmCounter = new AtomicLong();
final AtomicLong clientCounter = new AtomicLong();
final AtomicLong clientTemplateCounter = new AtomicLong();
final AtomicLong roleCounter = new AtomicLong();
final AtomicLong groupCounter = new AtomicLong();
protected final ConcurrentHashMap<String, String> realmLookup;
public LockingRealmCache(Cache<String, Object> cache, Cache<String, Long> revisions, ConcurrentHashMap<String, String> realmLookup) {
this.cache = cache;
this.realmLookup = realmLookup;
this.revisions = revisions;
}
public Cache<String, Object> getCache() {
return cache;
}
public Cache<String, Long> getRevisions() {
return revisions;
}
public void startRevisionBatch() {
revisions.startBatch();
}
public void endRevisionBatch() {
try {
revisions.endBatch(true);
} catch (Exception e) {
}
}
private <T> T get(String id, Class<T> type) {
Revisioned o = (Revisioned)cache.get(id);
if (o == null) {
return null;
}
Long rev = revisions.get(id);
if (rev == null) {
logger.tracev("get() missing rev");
return null;
}
long oRev = o.getRevision() == null ? -1L : o.getRevision().longValue();
if (rev > oRev) {
logger.tracev("get() rev: {0} o.rev: {1}", rev.longValue(), oRev);
return null;
}
return o != null && type.isInstance(o) ? type.cast(o) : null;
}
protected Object invalidateObject(String id, AtomicLong counter) {
Object removed = cache.remove(id);
revisions.put(id, counter.incrementAndGet());
return removed;
}
protected void addRevisioned(String id, Revisioned object, AtomicLong counter) {
//startRevisionBatch();
try {
//revisions.getAdvancedCache().lock(id);
Long rev = revisions.get(id);
if (rev == null) {
rev = counter.incrementAndGet();
revisions.put(id, rev);
return;
}
revisions.startBatch();
revisions.getAdvancedCache().lock(id);
rev = revisions.get(id);
if (rev == null) {
rev = counter.incrementAndGet();
revisions.put(id, rev);
return;
}
if (rev.equals(object.getRevision())) {
cache.putForExternalRead(id, object);
}
} finally {
endRevisionBatch();
}
}
public Long getCurrentRevision(String id) {
return revisions.get(id);
}
@Override
public void clear() {
cache.clear();
}
@Override
public CachedRealm getCachedRealm(String id) {
return get(id, CachedRealm.class);
}
@Override
public void invalidateCachedRealm(CachedRealm realm) {
logger.tracev("Invalidating realm {0}", realm.getId());
invalidateObject(realm.getId(), realmCounter);
realmLookup.remove(realm.getName());
}
@Override
public void invalidateCachedRealmById(String id) {
CachedRealm cached = (CachedRealm) invalidateObject(id, realmCounter);
if (cached != null) realmLookup.remove(cached.getName());
}
@Override
public void addCachedRealm(CachedRealm realm) {
logger.tracev("Adding realm {0}", realm.getId());
addRevisioned(realm.getId(), (Revisioned) realm, realmCounter);
realmLookup.put(realm.getName(), realm.getId());
}
@Override
public CachedRealm getCachedRealmByName(String name) {
String id = realmLookup.get(name);
return id != null ? getCachedRealm(id) : null;
}
@Override
public CachedClient getApplication(String id) {
return get(id, CachedClient.class);
}
@Override
public void invalidateApplication(CachedClient app) {
logger.tracev("Removing application {0}", app.getId());
invalidateObject(app.getId(), clientCounter);
}
@Override
public void addCachedClient(CachedClient app) {
logger.tracev("Adding application {0}", app.getId());
addRevisioned(app.getId(), (Revisioned) app, clientCounter);
}
@Override
public void invalidateCachedApplicationById(String id) {
CachedClient client = (CachedClient)invalidateObject(id, clientCounter);
if (client != null) logger.tracev("Removing application {0}", client.getClientId());
}
@Override
public void evictCachedApplicationById(String id) {
logger.tracev("Evicting application {0}", id);
cache.evict(id);
}
@Override
public CachedGroup getGroup(String id) {
return get(id, CachedGroup.class);
}
@Override
public void invalidateGroup(CachedGroup role) {
logger.tracev("Removing group {0}", role.getId());
invalidateObject(role.getId(), groupCounter);
}
@Override
public void addCachedGroup(CachedGroup role) {
logger.tracev("Adding group {0}", role.getId());
addRevisioned(role.getId(), (Revisioned) role, groupCounter);
}
@Override
public void invalidateCachedGroupById(String id) {
logger.tracev("Removing group {0}", id);
invalidateObject(id, groupCounter);
}
@Override
public void invalidateGroupById(String id) {
logger.tracev("Removing group {0}", id);
invalidateObject(id, groupCounter);
}
@Override
public CachedRole getRole(String id) {
return get(id, CachedRole.class);
}
@Override
public void invalidateRole(CachedRole role) {
logger.tracev("Removing role {0}", role.getId());
invalidateObject(role.getId(), roleCounter);
}
@Override
public void invalidateRoleById(String id) {
logger.tracev("Removing role {0}", id);
invalidateObject(id, roleCounter);
}
@Override
public void evictCachedRoleById(String id) {
logger.tracev("Evicting role {0}", id);
cache.evict(id);
}
@Override
public void addCachedRole(CachedRole role) {
logger.tracev("Adding role {0}", role.getId());
addRevisioned(role.getId(), (Revisioned) role, roleCounter);
}
@Override
public void invalidateCachedRoleById(String id) {
logger.tracev("Removing role {0}", id);
invalidateObject(id, roleCounter);
}
@Override
public CachedClientTemplate getClientTemplate(String id) {
return get(id, CachedClientTemplate.class);
}
@Override
public void invalidateClientTemplate(CachedClientTemplate app) {
logger.tracev("Removing client template {0}", app.getId());
invalidateObject(app.getId(), clientTemplateCounter);
}
@Override
public void addCachedClientTemplate(CachedClientTemplate app) {
logger.tracev("Adding client template {0}", app.getId());
addRevisioned(app.getId(), (Revisioned) app, clientTemplateCounter);
}
@Override
public void invalidateCachedClientTemplateById(String id) {
logger.tracev("Removing client template {0}", id);
invalidateObject(id, clientTemplateCounter);
}
@Override
public void evictCachedClientTemplateById(String id) {
logger.tracev("Evicting client template {0}", id);
cache.evict(id);
}
}

View file

@ -0,0 +1,160 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.cache.infinispan.skewed;
import org.infinispan.Cache;
import org.infinispan.notifications.Listener;
import org.infinispan.notifications.cachelistener.annotation.CacheEntriesEvicted;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryInvalidated;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryRemoved;
import org.infinispan.notifications.cachelistener.event.CacheEntriesEvictedEvent;
import org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent;
import org.infinispan.notifications.cachelistener.event.CacheEntryInvalidatedEvent;
import org.infinispan.notifications.cachelistener.event.CacheEntryRemovedEvent;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.cache.CacheRealmProvider;
import org.keycloak.models.cache.CacheRealmProviderFactory;
import org.keycloak.models.cache.entities.CachedClient;
import org.keycloak.models.cache.entities.CachedRealm;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class RepeatableReadWriteSkewCacheRealmProviderFactory implements CacheRealmProviderFactory {
private static final Logger log = Logger.getLogger(RepeatableReadWriteSkewCacheRealmProviderFactory.class);
protected volatile RepeatableReadWriteSkewRealmCache realmCache;
protected final ConcurrentHashMap<String, String> realmLookup = new ConcurrentHashMap<>();
@Override
public CacheRealmProvider create(KeycloakSession session) {
lazyInit(session);
return new RepeatableReadWriteSkewRealmCacheProvider(realmCache, session);
}
private void lazyInit(KeycloakSession session) {
if (realmCache == null) {
synchronized (this) {
if (realmCache == null) {
Cache<String, Object> cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
cache.addListener(new CacheListener());
realmCache = new RepeatableReadWriteSkewRealmCache(cache, realmLookup);
}
}
}
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return "infinispan-versioned";
}
@Listener
public class CacheListener {
@CacheEntryCreated
public void created(CacheEntryCreatedEvent<String, Object> event) {
if (!event.isPre()) {
Object object = event.getValue();
if (object != null) {
if (object instanceof CachedRealm) {
CachedRealm realm = (CachedRealm) object;
realmLookup.put(realm.getName(), realm.getId());
log.tracev("Realm added realm={0}", realm.getName());
}
}
}
}
@CacheEntryRemoved
public void removed(CacheEntryRemovedEvent<String, Object> event) {
if (event.isPre()) {
Object object = event.getValue();
if (object != null) {
remove(object);
}
}
}
@CacheEntryInvalidated
public void removed(CacheEntryInvalidatedEvent<String, Object> event) {
if (event.isPre()) {
Object object = event.getValue();
if (object != null) {
remove(object);
}
}
}
@CacheEntriesEvicted
public void userEvicted(CacheEntriesEvictedEvent<String, Object> event) {
for (Object object : event.getEntries().values()) {
remove(object);
}
}
private void remove(Object object) {
if (object instanceof CachedRealm) {
CachedRealm realm = (CachedRealm) object;
realmLookup.remove(realm.getName());
for (String r : realm.getRealmRoles().values()) {
realmCache.evictCachedRoleById(r);
}
for (String c : realm.getClients().values()) {
realmCache.evictCachedApplicationById(c);
}
log.tracev("Realm removed realm={0}", realm.getName());
} else if (object instanceof CachedClient) {
CachedClient client = (CachedClient) object;
for (String r : client.getRoles().values()) {
realmCache.evictCachedRoleById(r);
}
log.tracev("Client removed client={0}", client.getId());
}
}
}
}

View file

@ -0,0 +1,164 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.cache.infinispan.skewed;
import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.Configuration;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.cache.VersioningScheme;
import org.infinispan.configuration.global.GlobalConfigurationBuilder;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.transaction.lookup.DummyTransactionManagerLookup;
import org.infinispan.util.concurrent.IsolationLevel;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.connections.infinispan.DefaultInfinispanConnectionProvider;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import javax.naming.InitialContext;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class RepeatableReadWriteSkewConnectionProviderFactory implements InfinispanConnectionProviderFactory {
protected static final Logger logger = Logger.getLogger(RepeatableReadWriteSkewConnectionProviderFactory.class);
private Config.Scope config;
private EmbeddedCacheManager cacheManager;
private boolean containerManaged;
@Override
public InfinispanConnectionProvider create(KeycloakSession session) {
lazyInit();
return new DefaultInfinispanConnectionProvider(cacheManager);
}
@Override
public void close() {
if (cacheManager != null && !containerManaged) {
cacheManager.stop();
}
cacheManager = null;
}
@Override
public String getId() {
return "versioned";
}
@Override
public void init(Config.Scope config) {
this.config = config;
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
private void lazyInit() {
if (cacheManager == null) {
synchronized (this) {
if (cacheManager == null) {
String cacheContainer = config.get("cacheContainer");
if (cacheContainer != null) {
initContainerManaged(cacheContainer);
} else {
initEmbedded();
}
}
}
}
}
private void initContainerManaged(String cacheContainerLookup) {
try {
cacheManager = (EmbeddedCacheManager) new InitialContext().lookup(cacheContainerLookup);
containerManaged = true;
logger.debugv("Using container managed Infinispan cache container, lookup={1}", cacheContainerLookup);
} catch (Exception e) {
throw new RuntimeException("Failed to retrieve cache container", e);
}
}
private void initEmbedded() {
GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
boolean clustered = config.getBoolean("clustered", false);
boolean async = config.getBoolean("async", true);
boolean allowDuplicateJMXDomains = config.getBoolean("allowDuplicateJMXDomains", true);
if (clustered) {
gcb.transport().defaultTransport();
}
gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains);
cacheManager = new DefaultCacheManager(gcb.build());
containerManaged = false;
logger.debug("Started embedded Infinispan cache container");
ConfigurationBuilder invalidationConfigBuilder = new ConfigurationBuilder();
if (clustered) {
invalidationConfigBuilder.clustering().cacheMode(async ? CacheMode.INVALIDATION_ASYNC : CacheMode.INVALIDATION_SYNC);
}
invalidationConfigBuilder.transaction().transactionManagerLookup(new DummyTransactionManagerLookup());
invalidationConfigBuilder.locking().isolationLevel(IsolationLevel.REPEATABLE_READ).writeSkewCheck(true).versioning().enable().scheme(VersioningScheme.SIMPLE);
cacheManager.defineConfiguration(InfinispanConnectionProvider.REALM_CACHE_NAME, invalidationConfigBuilder.build());
ConfigurationBuilder userConfigBuilder = new ConfigurationBuilder();
if (clustered) {
userConfigBuilder.clustering().cacheMode(async ? CacheMode.INVALIDATION_ASYNC : CacheMode.INVALIDATION_SYNC);
}
Configuration userCacheConfiguration = userConfigBuilder.build();
cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_CACHE_NAME, userCacheConfiguration);
ConfigurationBuilder sessionConfigBuilder = new ConfigurationBuilder();
if (clustered) {
String sessionsMode = config.get("sessionsMode", "distributed");
if (sessionsMode.equalsIgnoreCase("replicated")) {
sessionConfigBuilder.clustering().cacheMode(async ? CacheMode.REPL_ASYNC : CacheMode.REPL_SYNC);
} else if (sessionsMode.equalsIgnoreCase("distributed")) {
sessionConfigBuilder.clustering().cacheMode(async ? CacheMode.DIST_ASYNC : CacheMode.DIST_SYNC);
} else {
throw new RuntimeException("Invalid value for sessionsMode");
}
sessionConfigBuilder.clustering().hash()
.numOwners(config.getInt("sessionsOwners", 2))
.numSegments(config.getInt("sessionsSegments", 60)).build();
}
Configuration sessionCacheConfiguration = sessionConfigBuilder.build();
cacheManager.defineConfiguration(InfinispanConnectionProvider.SESSION_CACHE_NAME, sessionCacheConfiguration);
cacheManager.defineConfiguration(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, sessionCacheConfiguration);
cacheManager.defineConfiguration(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, sessionCacheConfiguration);
}
}

View file

@ -0,0 +1,269 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.cache.infinispan.skewed;
import org.infinispan.Cache;
import org.jboss.logging.Logger;
import org.keycloak.models.cache.RealmCache;
import org.keycloak.models.cache.entities.CachedClient;
import org.keycloak.models.cache.entities.CachedClientTemplate;
import org.keycloak.models.cache.entities.CachedGroup;
import org.keycloak.models.cache.entities.CachedRealm;
import org.keycloak.models.cache.entities.CachedRole;
import javax.transaction.NotSupportedException;
import javax.transaction.Status;
import javax.transaction.SystemException;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class RepeatableReadWriteSkewRealmCache implements RealmCache {
protected static final Logger logger = Logger.getLogger(RepeatableReadWriteSkewRealmCache.class);
protected final Cache<String, Object> cache;
protected final ConcurrentHashMap<String, String> realmLookup;
public RepeatableReadWriteSkewRealmCache(Cache<String, Object> cache, ConcurrentHashMap<String, String> realmLookup) {
this.cache = cache;
this.realmLookup = realmLookup;
}
public Cache<String, Object> getCache() {
return cache;
}
public void startBatch() {
logger.trace("*** START BATCH ***");
try {
if (cache.getAdvancedCache().getTransactionManager().getStatus() == Status.STATUS_NO_TRANSACTION) {
cache.getAdvancedCache().getTransactionManager().begin();
}
} catch (NotSupportedException e) {
throw new RuntimeException(e);
} catch (SystemException e) {
throw new RuntimeException(e);
}
}
public void endBatch(boolean commit) {
logger.trace("*** END BATCH ***");
try {
if (cache.getAdvancedCache().getTransactionManager().getStatus() == Status.STATUS_ACTIVE) {
if (commit) {
cache.getAdvancedCache().getTransactionManager().commit();
} else {
cache.getAdvancedCache().getTransactionManager().rollback();
}
}
} catch (Exception e) {
//throw new RuntimeException(e);
}
}
@Override
public void clear() {
cache.clear();
}
@Override
public CachedRealm getCachedRealm(String id) {
return get(id, CachedRealm.class);
}
@Override
public void invalidateCachedRealm(CachedRealm realm) {
logger.tracev("Invalidating realm {0}", realm.getId());
invalidate(realm.getId());
realmLookup.remove(realm.getName());
}
protected Object invalidate(String id) {
startBatch();
Object rtn = cache.remove(id);
logger.trace("*** END BATCH ***");
try {
if (cache.getAdvancedCache().getTransactionManager().getStatus() == Status.STATUS_ACTIVE) {
if (true) {
cache.getAdvancedCache().getTransactionManager().commit();
} else {
cache.getAdvancedCache().getTransactionManager().rollback();
}
}
} catch (Exception e) {
logger.trace("Failed to commit invalidate");
}
return rtn;
}
@Override
public void invalidateCachedRealmById(String id) {
CachedRealm cached = (CachedRealm) invalidate(id);
if (cached != null) realmLookup.remove(cached.getName());
}
@Override
public void addCachedRealm(CachedRealm realm) {
logger.tracev("Adding realm {0}", realm.getId());
cache.putForExternalRead(realm.getId(), realm);
realmLookup.put(realm.getName(), realm.getId());
}
@Override
public CachedRealm getCachedRealmByName(String name) {
String id = realmLookup.get(name);
return id != null ? getCachedRealm(id) : null;
}
@Override
public CachedClient getApplication(String id) {
return get(id, CachedClient.class);
}
@Override
public void invalidateApplication(CachedClient app) {
logger.tracev("Removing application {0}", app.getId());
invalidate(app.getId());
}
@Override
public void addCachedClient(CachedClient app) {
logger.tracev("Adding application {0}", app.getId());
cache.putForExternalRead(app.getId(), app);
}
@Override
public void invalidateCachedApplicationById(String id) {
logger.tracev("Removing application {0}", id);
invalidate(id);
}
@Override
public void evictCachedApplicationById(String id) {
logger.tracev("Evicting application {0}", id);
cache.evict(id);
}
@Override
public CachedGroup getGroup(String id) {
return get(id, CachedGroup.class);
}
@Override
public void invalidateGroup(CachedGroup role) {
logger.tracev("Removing group {0}", role.getId());
invalidate(role.getId());
}
@Override
public void addCachedGroup(CachedGroup role) {
logger.tracev("Adding group {0}", role.getId());
cache.putForExternalRead(role.getId(), role);
}
@Override
public void invalidateCachedGroupById(String id) {
logger.tracev("Removing group {0}", id);
invalidate(id);
}
@Override
public void invalidateGroupById(String id) {
logger.tracev("Removing group {0}", id);
invalidate(id);
}
@Override
public CachedRole getRole(String id) {
return get(id, CachedRole.class);
}
@Override
public void invalidateRole(CachedRole role) {
logger.tracev("Removing role {0}", role.getId());
invalidate(role.getId());
}
@Override
public void invalidateRoleById(String id) {
logger.tracev("Removing role {0}", id);
invalidate(id);
}
@Override
public void evictCachedRoleById(String id) {
logger.tracev("Evicting role {0}", id);
cache.evict(id);
}
@Override
public void addCachedRole(CachedRole role) {
logger.tracev("Adding role {0}", role.getId());
cache.putForExternalRead(role.getId(), role);
}
@Override
public void invalidateCachedRoleById(String id) {
logger.tracev("Removing role {0}", id);
invalidate(id);
}
private <T> T get(String id, Class<T> type) {
Object o = cache.get(id);
return o != null && type.isInstance(o) ? type.cast(o) : null;
}
@Override
public CachedClientTemplate getClientTemplate(String id) {
return get(id, CachedClientTemplate.class);
}
@Override
public void invalidateClientTemplate(CachedClientTemplate app) {
logger.tracev("Removing client template {0}", app.getId());
invalidate(app.getId());
}
@Override
public void addCachedClientTemplate(CachedClientTemplate app) {
logger.tracev("Adding client template {0}", app.getId());
cache.putForExternalRead(app.getId(), app);
}
@Override
public void invalidateCachedClientTemplateById(String id) {
logger.tracev("Removing client template {0}", id);
invalidate(id);
}
@Override
public void evictCachedClientTemplateById(String id) {
logger.tracev("Evicting client template {0}", id);
invalidate(id);
}
}

View file

@ -0,0 +1,472 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.cache.infinispan.skewed;
import org.jboss.logging.Logger;
import org.keycloak.migration.MigrationModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientTemplateModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakTransaction;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RealmProvider;
import org.keycloak.models.RoleModel;
import org.keycloak.models.cache.CacheRealmProvider;
import org.keycloak.models.cache.entities.CachedClient;
import org.keycloak.models.cache.entities.CachedClientRole;
import org.keycloak.models.cache.entities.CachedClientTemplate;
import org.keycloak.models.cache.entities.CachedGroup;
import org.keycloak.models.cache.entities.CachedRealm;
import org.keycloak.models.cache.entities.CachedRealmRole;
import org.keycloak.models.cache.entities.CachedRole;
import org.keycloak.models.cache.infinispan.ClientAdapter;
import org.keycloak.models.cache.infinispan.ClientTemplateAdapter;
import org.keycloak.models.cache.infinispan.GroupAdapter;
import org.keycloak.models.cache.infinispan.RealmAdapter;
import org.keycloak.models.cache.infinispan.RoleAdapter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* DO NOT USE THIS!!
*
* Tries unsuccessfully to use Infinispan with REPEATABLE_READ, write-skew-checking
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class RepeatableReadWriteSkewRealmCacheProvider implements CacheRealmProvider {
protected static final Logger logger = Logger.getLogger(RepeatableReadWriteSkewRealmCacheProvider.class);
protected RepeatableReadWriteSkewRealmCache cache;
protected KeycloakSession session;
protected RealmProvider delegate;
protected boolean transactionActive;
protected boolean setRollbackOnly;
protected Set<String> realmInvalidations = new HashSet<>();
protected Set<String> appInvalidations = new HashSet<>();
protected Set<String> clientTemplateInvalidations = new HashSet<>();
protected Set<String> roleInvalidations = new HashSet<>();
protected Set<String> groupInvalidations = new HashSet<>();
protected Map<String, RealmModel> managedRealms = new HashMap<>();
protected Map<String, ClientModel> managedApplications = new HashMap<>();
protected Map<String, ClientTemplateModel> managedClientTemplates = new HashMap<>();
protected Map<String, RoleModel> managedRoles = new HashMap<>();
protected Map<String, GroupModel> managedGroups = new HashMap<>();
protected boolean clearAll;
public RepeatableReadWriteSkewRealmCacheProvider(RepeatableReadWriteSkewRealmCache cache, KeycloakSession session) {
this.cache = cache;
this.session = session;
session.getTransaction().enlistAfterCompletion(getTransaction());
}
@Override
public void clear() {
cache.clear();
}
@Override
public MigrationModel getMigrationModel() {
return getDelegate().getMigrationModel();
}
@Override
public RealmProvider getDelegate() {
if (!transactionActive) throw new IllegalStateException("Cannot access delegate without a transaction");
if (delegate != null) return delegate;
delegate = session.getProvider(RealmProvider.class);
return delegate;
}
@Override
public void registerRealmInvalidation(String id) {
realmInvalidations.add(id);
}
@Override
public void registerApplicationInvalidation(String id) {
appInvalidations.add(id);
}
@Override
public void registerClientTemplateInvalidation(String id) {
clientTemplateInvalidations.add(id);
}
@Override
public void registerRoleInvalidation(String id) {
roleInvalidations.add(id);
}
@Override
public void registerGroupInvalidation(String id) {
groupInvalidations.add(id);
}
protected void runInvalidations() {
for (String id : realmInvalidations) {
cache.invalidateCachedRealmById(id);
}
for (String id : roleInvalidations) {
cache.invalidateRoleById(id);
}
for (String id : groupInvalidations) {
cache.invalidateGroupById(id);
}
for (String id : appInvalidations) {
cache.invalidateCachedApplicationById(id);
}
for (String id : clientTemplateInvalidations) {
cache.invalidateCachedClientTemplateById(id);
}
}
private KeycloakTransaction getTransaction() {
return new KeycloakTransaction() {
@Override
public void begin() {
transactionActive = true;
}
@Override
public void commit() {
if (delegate == null) return;
if (clearAll) {
cache.clear();
}
runInvalidations();
transactionActive = false;
}
@Override
public void rollback() {
setRollbackOnly = true;
runInvalidations();
transactionActive = false;
}
@Override
public void setRollbackOnly() {
setRollbackOnly = true;
}
@Override
public boolean getRollbackOnly() {
return setRollbackOnly;
}
@Override
public boolean isActive() {
return transactionActive;
}
};
}
@Override
public RealmModel createRealm(String name) {
RealmModel realm = getDelegate().createRealm(name);
registerRealmInvalidation(realm.getId());
return realm;
}
@Override
public RealmModel createRealm(String id, String name) {
RealmModel realm = getDelegate().createRealm(id, name);
registerRealmInvalidation(realm.getId());
return realm;
}
@Override
public RealmModel getRealm(String id) {
//cache.startBatch();
cache.startBatch();
boolean batchEnded = false;
try {
CachedRealm cached = cache.getCachedRealm(id);
boolean wasNull = cached == null;
if (cached == null) {
RealmModel model = getDelegate().getRealm(id);
if (model == null) return null;
if (realmInvalidations.contains(id)) return model;
cached = new CachedRealm(cache, this, model);
cache.addCachedRealm(cached);
try {
batchEnded = true;
cache.endBatch(true);
logger.trace("returning new cached realm");
} catch (Exception exception) {
logger.trace("failed to add to cache", exception);
return model;
}
} else if (realmInvalidations.contains(id)) {
return getDelegate().getRealm(id);
} else if (managedRealms.containsKey(id)) {
return managedRealms.get(id);
}
if (!wasNull) logger.trace("returning cached realm: " + cached.getName());
RealmAdapter adapter = new RealmAdapter(cached, this);
managedRealms.put(id, adapter);
return adapter;
} finally {
if (!batchEnded) cache.endBatch(true);
}
}
@Override
public RealmModel getRealmByName(String name) {
cache.startBatch();
boolean batchEnded = false;
try {
CachedRealm cached = cache.getCachedRealmByName(name);
boolean wasNull = cached == null;
if (cached == null) {
RealmModel model = getDelegate().getRealmByName(name);
if (model == null) return null;
if (realmInvalidations.contains(model.getId())) return model;
cached = new CachedRealm(cache, this, model);
cache.addCachedRealm(cached);
try {
batchEnded = true;
cache.endBatch(true);
logger.trace("returning new cached realm: " + cached.getName());
} catch (Exception exception) {
logger.trace("failed to add to cache", exception);
return model;
}
} else if (realmInvalidations.contains(cached.getId())) {
return getDelegate().getRealmByName(name);
} else if (managedRealms.containsKey(cached.getId())) {
return managedRealms.get(cached.getId());
}
if (!wasNull) logger.trace("returning cached realm: " + cached.getName());
RealmAdapter adapter = new RealmAdapter(cached, this);
managedRealms.put(cached.getId(), adapter);
return adapter;
} finally {
if (!batchEnded) cache.endBatch(true);
}
}
@Override
public List<RealmModel> getRealms() {
// Retrieve realms from backend
List<RealmModel> backendRealms = getDelegate().getRealms();
// Return cache delegates to ensure cache invalidated during write operations
List<RealmModel> cachedRealms = new LinkedList<RealmModel>();
for (RealmModel realm : backendRealms) {
RealmModel cached = getRealm(realm.getId());
cachedRealms.add(cached);
}
return cachedRealms;
}
@Override
public boolean removeRealm(String id) {
cache.invalidateCachedRealmById(id);
RealmModel realm = getDelegate().getRealm(id);
Set<RoleModel> realmRoles = null;
if (realm != null) {
realmRoles = realm.getRoles();
}
boolean didIt = getDelegate().removeRealm(id);
realmInvalidations.add(id);
// TODO: Temporary workaround to invalidate cached realm roles
if (didIt && realmRoles != null) {
for (RoleModel role : realmRoles) {
roleInvalidations.add(role.getId());
}
}
return didIt;
}
@Override
public void close() {
if (delegate != null) delegate.close();
}
@Override
public RoleModel getRoleById(String id, RealmModel realm) {
cache.startBatch();
boolean batchEnded = false;
try {
CachedRole cached = cache.getRole(id);
if (cached != null && !cached.getRealm().equals(realm.getId())) {
cached = null;
}
if (cached == null) {
RoleModel model = getDelegate().getRoleById(id, realm);
if (model == null) return null;
if (roleInvalidations.contains(id)) return model;
if (model.getContainer() instanceof ClientModel) {
cached = new CachedClientRole(((ClientModel) model.getContainer()).getId(), model, realm);
} else {
cached = new CachedRealmRole(model, realm);
}
cache.addCachedRole(cached);
try {
batchEnded = true;
cache.endBatch(true);
} catch (Exception exception) {
logger.trace("failed to add to cache", exception);
return model;
}
} else if (roleInvalidations.contains(id)) {
return getDelegate().getRoleById(id, realm);
} else if (managedRoles.containsKey(id)) {
return managedRoles.get(id);
}
RoleAdapter adapter = new RoleAdapter(cached, cache, this, realm);
managedRoles.put(id, adapter);
return adapter;
} finally {
if (!batchEnded) cache.endBatch(true);
}
}
@Override
public GroupModel getGroupById(String id, RealmModel realm) {
cache.startBatch();
boolean batchEnded = false;
try {
CachedGroup cached = cache.getGroup(id);
if (cached != null && !cached.getRealm().equals(realm.getId())) {
cached = null;
}
if (cached == null) {
GroupModel model = getDelegate().getGroupById(id, realm);
if (model == null) return null;
if (groupInvalidations.contains(id)) return model;
cached = new CachedGroup(realm, model);
cache.addCachedGroup(cached);
try {
batchEnded = true;
cache.endBatch(true);
} catch (Exception exception) {
logger.trace("failed to add to cache", exception);
return model;
}
} else if (groupInvalidations.contains(id)) {
return getDelegate().getGroupById(id, realm);
} else if (managedGroups.containsKey(id)) {
return managedGroups.get(id);
}
GroupAdapter adapter = new GroupAdapter(cached, this, session, realm);
managedGroups.put(id, adapter);
return adapter;
} finally {
if (!batchEnded) cache.endBatch(true);
}
}
@Override
public ClientModel getClientById(String id, RealmModel realm) {
cache.startBatch();
boolean batchEnded = false;
CachedClient cached = cache.getApplication(id);
try {
if (cached != null && !cached.getRealm().equals(realm.getId())) {
cached = null;
}
if (cached == null) {
ClientModel model = getDelegate().getClientById(id, realm);
if (model == null) return null;
if (appInvalidations.contains(id)) return model;
cached = new CachedClient(cache, getDelegate(), realm, model);
cache.addCachedClient(cached);
try {
batchEnded = true;
cache.endBatch(true);
} catch (Exception exception) {
logger.trace("failed to add to cache", exception);
return model;
}
} else if (appInvalidations.contains(id)) {
return getDelegate().getClientById(id, realm);
} else if (managedApplications.containsKey(id)) {
return managedApplications.get(id);
}
ClientAdapter adapter = new ClientAdapter(realm, cached, this, cache);
managedApplications.put(id, adapter);
return adapter;
} finally {
if (!batchEnded) cache.endBatch(true);
}
}
@Override
public ClientTemplateModel getClientTemplateById(String id, RealmModel realm) {
cache.startBatch();
boolean batchEnded = false;
try {
CachedClientTemplate cached = cache.getClientTemplate(id);
if (cached != null && !cached.getRealm().equals(realm.getId())) {
cached = null;
}
if (cached == null) {
ClientTemplateModel model = getDelegate().getClientTemplateById(id, realm);
if (model == null) return null;
if (clientTemplateInvalidations.contains(id)) return model;
cached = new CachedClientTemplate(cache, getDelegate(), realm, model);
cache.addCachedClientTemplate(cached);
try {
batchEnded = true;
cache.endBatch(true);
} catch (Exception exception) {
logger.trace("failed to add to cache", exception);
return model;
}
} else if (clientTemplateInvalidations.contains(id)) {
return getDelegate().getClientTemplateById(id, realm);
} else if (managedClientTemplates.containsKey(id)) {
return managedClientTemplates.get(id);
}
ClientTemplateModel adapter = new ClientTemplateAdapter(realm, cached, this, cache);
managedClientTemplates.put(id, adapter);
return adapter;
} finally {
if (!batchEnded) cache.endBatch(true);
}
}
}

View file

@ -24,6 +24,7 @@ import org.keycloak.common.util.Time;
import org.keycloak.models.*;
import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.sessions.infinispan.entities.*;
import org.keycloak.models.sessions.infinispan.initializer.TimeAwareInitializerState;
import org.keycloak.models.sessions.infinispan.stream.*;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RealmInfoUtil;
@ -410,6 +411,19 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
loginFailureCache.remove(new LoginFailureKey(realm.getId(), user.getEmail()));
}
@Override
public int getClusterStartupTime() {
TimeAwareInitializerState state = (TimeAwareInitializerState) offlineSessionCache.get(InfinispanUserSessionProviderFactory.SESSION_INITIALIZER_STATE_KEY);
int startTime;
if (state == null) {
log.warn("Cluster startup time not yet available. Fallback to local startup time");
startTime = (int)(session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000);
} else {
startTime = state.getClusterStartupTime();
}
return startTime;
}
@Override
public void close() {
}

View file

@ -34,6 +34,9 @@ import org.keycloak.provider.ProviderEventListener;
public class InfinispanUserSessionProviderFactory implements UserSessionProviderFactory {
private static final String STATE_KEY_PREFIX = "initializerState";
public static final String SESSION_INITIALIZER_STATE_KEY = STATE_KEY_PREFIX + "::offlineUserSessions";
private static final Logger log = Logger.getLogger(InfinispanUserSessionProviderFactory.class);
private Config.Scope config;
@ -84,7 +87,7 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
Cache<String, SessionEntity> cache = connections.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME);
InfinispanUserSessionInitializer initializer = new InfinispanUserSessionInitializer(sessionFactory, cache, new OfflineUserSessionLoader(), maxErrors, sessionsPerSegment, "offlineUserSessions");
InfinispanUserSessionInitializer initializer = new InfinispanUserSessionInitializer(sessionFactory, cache, new OfflineUserSessionLoader(), maxErrors, sessionsPerSegment, SESSION_INITIALIZER_STATE_KEY);
initializer.initCache();
initializer.loadPersistentSessions();
}

View file

@ -35,6 +35,7 @@ import org.infinispan.notifications.cachemanagerlistener.annotation.ViewChanged;
import org.infinispan.notifications.cachemanagerlistener.event.ViewChangedEvent;
import org.infinispan.remoting.transport.Transport;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask;
@ -51,8 +52,6 @@ public class InfinispanUserSessionInitializer {
private static final Logger log = Logger.getLogger(InfinispanUserSessionInitializer.class);
private static final String STATE_KEY_PREFIX = "initializerState";
private final KeycloakSessionFactory sessionFactory;
private final Cache<String, SessionEntity> cache;
private final SessionLoader sessionLoader;
@ -60,21 +59,18 @@ public class InfinispanUserSessionInitializer {
private final int sessionsPerSegment;
private final String stateKey;
private volatile CountDownLatch latch = new CountDownLatch(1);
public InfinispanUserSessionInitializer(KeycloakSessionFactory sessionFactory, Cache<String, SessionEntity> cache, SessionLoader sessionLoader, int maxErrors, int sessionsPerSegment, String stateKeySuffix) {
public InfinispanUserSessionInitializer(KeycloakSessionFactory sessionFactory, Cache<String, SessionEntity> cache, SessionLoader sessionLoader, int maxErrors, int sessionsPerSegment, String stateKey) {
this.sessionFactory = sessionFactory;
this.cache = cache;
this.sessionLoader = sessionLoader;
this.maxErrors = maxErrors;
this.sessionsPerSegment = sessionsPerSegment;
this.stateKey = STATE_KEY_PREFIX + "::" + stateKeySuffix;
this.stateKey = stateKey;
}
public void initCache() {
this.cache.getAdvancedCache().getComponentRegistry().registerComponent(sessionFactory, KeycloakSessionFactory.class);
cache.getCacheManager().addListener(new ViewChangeListener());
}
@ -86,7 +82,7 @@ public class InfinispanUserSessionInitializer {
while (!isFinished()) {
if (!isCoordinator()) {
try {
latch.await(500, TimeUnit.MILLISECONDS);
Thread.sleep(1000);
} catch (InterruptedException ie) {
log.error("Interrupted", ie);
}
@ -104,8 +100,10 @@ public class InfinispanUserSessionInitializer {
private InitializerState getOrCreateInitializerState() {
InitializerState state = (InitializerState) cache.get(stateKey);
TimeAwareInitializerState state = (TimeAwareInitializerState) cache.get(stateKey);
if (state == null) {
int startTime = (int)(sessionFactory.getServerStartupTimestamp() / 1000);
final int[] count = new int[1];
// Rather use separate transactions for update and counting
@ -113,7 +111,7 @@ public class InfinispanUserSessionInitializer {
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
sessionLoader.init(session);
sessionLoader.init(session, startTime);
}
});
@ -126,8 +124,9 @@ public class InfinispanUserSessionInitializer {
});
state = new InitializerState();
state = new TimeAwareInitializerState();
state.init(count[0], sessionsPerSegment);
state.setClusterStartupTime(startTime);
saveStateToCache(state);
}
return state;
@ -251,24 +250,6 @@ public class InfinispanUserSessionInitializer {
}
}
@Listener
public class ViewChangeListener {
@ViewChanged
public void viewChanged(ViewChangedEvent event) {
boolean isCoordinator = isCoordinator();
log.debug("View Changed: is coordinator: " + isCoordinator);
if (isCoordinator) {
latch.countDown();
latch = new CountDownLatch(1);
}
}
}
public static class WorkerResult implements Serializable {
private Integer segment;

View file

@ -33,14 +33,13 @@ public class OfflineUserSessionLoader implements SessionLoader {
private static final Logger log = Logger.getLogger(OfflineUserSessionLoader.class);
@Override
public void init(KeycloakSession session) {
public void init(KeycloakSession session, int clusterStartupTime) {
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
int startTime = (int)(session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000);
log.debugf("Clearing detached sessions from persistent storage and updating timestamps to %d", startTime);
log.debugf("Clearing detached sessions from persistent storage and updating timestamps to %d", clusterStartupTime);
persister.clearDetachedUserSessions();
persister.updateAllTimestamps(startTime);
persister.updateAllTimestamps(clusterStartupTime);
}
@Override

View file

@ -26,7 +26,7 @@ import org.keycloak.models.KeycloakSession;
*/
public interface SessionLoader extends Serializable {
void init(KeycloakSession session);
void init(KeycloakSession session, int clusterStartupTime);
int getSessionsCount(KeycloakSession session);

View file

@ -0,0 +1,34 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.sessions.infinispan.initializer;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class TimeAwareInitializerState extends InitializerState {
private int clusterStartupTime;
public int getClusterStartupTime() {
return clusterStartupTime;
}
public void setClusterStartupTime(int clusterStartupTime) {
this.clusterStartupTime = clusterStartupTime;
}
}

View file

@ -16,3 +16,5 @@
#
org.keycloak.connections.infinispan.DefaultInfinispanConnectionProviderFactory
org.keycloak.models.cache.infinispan.counter.RevisionedConnectionProviderFactory
org.keycloak.models.cache.infinispan.locking.LockingConnectionProviderFactory

View file

@ -16,3 +16,5 @@
#
org.keycloak.models.cache.infinispan.InfinispanCacheRealmProviderFactory
org.keycloak.models.cache.infinispan.counter.RevisionedCacheRealmProviderFactory
org.keycloak.models.cache.infinispan.locking.LockingCacheRealmProviderFactory

View file

@ -0,0 +1,88 @@
package org.keycloak.models.sessions.infinispan.initializer;
import org.infinispan.Cache;
import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.Configuration;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.cache.VersioningScheme;
import org.infinispan.configuration.global.GlobalConfigurationBuilder;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.transaction.LockingMode;
import org.infinispan.transaction.TransactionMode;
import org.infinispan.transaction.lookup.DummyTransactionManagerLookup;
import org.infinispan.util.concurrent.IsolationLevel;
import org.junit.Ignore;
import org.junit.Test;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@Ignore
public class ConcurrencyLockingTest {
@Test
public void testLocking() throws Exception {
final DefaultCacheManager cacheManager = getVersionedCacheManager();
Cache<String, String> cache = cacheManager.getCache("COUNTER_CACHE");
cache.put("key", "init");
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(new Runnable() {
@Override
public void run() {
Cache<String, String> cache = cacheManager.getCache("COUNTER_CACHE");
cache.startBatch();
System.out.println("thread lock");
cache.getAdvancedCache().lock("key");
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
cache.endBatch(true);
}
});
Thread.sleep(10);
cache.startBatch();
cache.getAdvancedCache().lock("key");
cache.put("key", "1234");
System.out.println("after put");
cache.endBatch(true);
Thread.sleep(1000000);
}
protected DefaultCacheManager getVersionedCacheManager() {
GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
boolean allowDuplicateJMXDomains = true;
gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains);
final DefaultCacheManager cacheManager = new DefaultCacheManager(gcb.build());
ConfigurationBuilder invalidationConfigBuilder = new ConfigurationBuilder();
Configuration invalidationCacheConfiguration = invalidationConfigBuilder.build();
cacheManager.defineConfiguration(InfinispanConnectionProvider.REALM_CACHE_NAME, invalidationCacheConfiguration);
ConfigurationBuilder counterConfigBuilder = new ConfigurationBuilder();
counterConfigBuilder.invocationBatching().enable();
counterConfigBuilder.transaction().transactionManagerLookup(new DummyTransactionManagerLookup());
counterConfigBuilder.transaction().lockingMode(LockingMode.PESSIMISTIC);
Configuration counterCacheConfiguration = counterConfigBuilder.build();
cacheManager.defineConfiguration("COUNTER_CACHE", counterCacheConfiguration);
return cacheManager;
}
}

View file

@ -0,0 +1,269 @@
package org.keycloak.models.sessions.infinispan.initializer;
import org.infinispan.Cache;
import org.infinispan.commons.CacheException;
import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.Configuration;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.cache.VersioningScheme;
import org.infinispan.configuration.global.GlobalConfigurationBuilder;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.transaction.TransactionMode;
import org.infinispan.transaction.TransactionProtocol;
import org.infinispan.transaction.lookup.DummyTransactionManagerLookup;
import org.infinispan.util.concurrent.IsolationLevel;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import javax.transaction.NotSupportedException;
import javax.transaction.Status;
import javax.transaction.SystemException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Unit tests to make sure our model caching concurrency model will work.
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@Ignore
public class ConcurrencyVersioningTest {
public static abstract class AbstractThread implements Runnable {
EmbeddedCacheManager cacheManager;
boolean success;
CountDownLatch latch = new CountDownLatch(1);
public AbstractThread(EmbeddedCacheManager cacheManager) {
this.cacheManager = cacheManager;
}
public boolean isSuccess() {
return success;
}
public CountDownLatch getLatch() {
return latch;
}
}
public static class RemoveThread extends AbstractThread {
public RemoveThread(EmbeddedCacheManager cacheManager) {
super(cacheManager);
}
public void run() {
Cache<String, String> cache = cacheManager.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
try {
startBatch(cache);
cache.remove("key");
//cache.getAdvancedCache().getTransactionManager().commit();
endBatch(cache);
success = true;
} catch (Exception e) {
success = false;
}
latch.countDown();
}
}
public static class UpdateThread extends AbstractThread {
public UpdateThread(EmbeddedCacheManager cacheManager) {
super(cacheManager);
}
public void run() {
Cache<String, String> cache = cacheManager.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
try {
startBatch(cache);
cache.putForExternalRead("key", "value2");
//cache.getAdvancedCache().getTransactionManager().commit();
endBatch(cache);
success = true;
} catch (Exception e) {
success = false;
}
latch.countDown();
}
}
/**
* Tests that if remove executes before put, then put still succeeds.
*
* @throws Exception
*/
@Test
public void testGetRemovePutOnNonExisting() throws Exception {
final DefaultCacheManager cacheManager = getVersionedCacheManager();
ExecutorService executor = Executors.newSingleThreadExecutor();
RemoveThread removeThread = new RemoveThread(cacheManager);
Cache<String, String> cache = cacheManager.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
cache.remove("key");
startBatch(cache);
cache.get("key");
executor.execute(removeThread);
removeThread.getLatch().await();
cache.putForExternalRead("key", "value1");
endBatch(cache);
Assert.assertEquals(cache.get("key"), "value1");
Assert.assertTrue(removeThread.isSuccess());
}
/**
* Test that if a put of an existing key is removed after the put and before tx commit, it is evicted
*
* @throws Exception
*/
@Test
public void testGetRemovePutOnExisting() throws Exception {
final DefaultCacheManager cacheManager = getVersionedCacheManager();
ExecutorService executor = Executors.newSingleThreadExecutor();
RemoveThread removeThread = new RemoveThread(cacheManager);
Cache<String, String> cache = cacheManager.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
cache.put("key", "value0");
startBatch(cache);
cache.get("key");
executor.execute(removeThread);
removeThread.getLatch().await();
cache.put("key", "value1");
try {
endBatch(cache);
Assert.fail("Write skew should be detected");
} catch (Exception e) {
}
Assert.assertNull(cache.get("key"));
Assert.assertTrue(removeThread.isSuccess());
}
/**
* Test that if a put of an existing key is removed after the put and before tx commit, it is evicted
*
* @throws Exception
*/
@Test
public void testGetRemovePutEternalOnExisting() throws Exception {
final DefaultCacheManager cacheManager = getVersionedCacheManager();
ExecutorService executor = Executors.newSingleThreadExecutor();
RemoveThread removeThread = new RemoveThread(cacheManager);
Cache<String, String> cache = cacheManager.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
cache.put("key", "value0");
startBatch(cache);
cache.get("key");
executor.execute(removeThread);
cache.putForExternalRead("key", "value1");
removeThread.getLatch().await();
try {
endBatch(cache);
// Assert.fail("Write skew should be detected");
} catch (Exception e) {
}
Assert.assertNull(cache.get("key"));
Assert.assertTrue(removeThread.isSuccess());
}
@Test
public void testPutExternalRemoveOnExisting() throws Exception {
final DefaultCacheManager cacheManager = getVersionedCacheManager();
ExecutorService executor = Executors.newSingleThreadExecutor();
RemoveThread removeThread = new RemoveThread(cacheManager);
Cache<String, String> cache = cacheManager.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
cache.put("key", "value0");
startBatch(cache);
cache.putForExternalRead("key", "value1");
executor.execute(removeThread);
removeThread.getLatch().await();
try {
endBatch(cache);
// Assert.fail("Write skew should be detected");
} catch (Exception e) {
}
Assert.assertNull(cache.get("key"));
Assert.assertTrue(removeThread.isSuccess());
}
public static void startBatch(Cache<String, String> cache) {
try {
if (cache.getAdvancedCache().getTransactionManager().getStatus() == Status.STATUS_NO_TRANSACTION) {
System.out.println("begin");
cache.getAdvancedCache().getTransactionManager().begin();
}
} catch (NotSupportedException e) {
throw new RuntimeException(e);
} catch (SystemException e) {
throw new RuntimeException(e);
}
}
public static void endBatch(Cache<String, String> cache) {
boolean commit = true;
try {
if (cache.getAdvancedCache().getTransactionManager().getStatus() == Status.STATUS_ACTIVE) {
if (commit) {
cache.getAdvancedCache().getTransactionManager().commit();
} else {
cache.getAdvancedCache().getTransactionManager().rollback();
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
protected DefaultCacheManager getVersionedCacheManager() {
GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
boolean clustered = false;
boolean async = false;
boolean allowDuplicateJMXDomains = true;
if (clustered) {
gcb.transport().defaultTransport();
}
gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains);
final DefaultCacheManager cacheManager = new DefaultCacheManager(gcb.build());
ConfigurationBuilder invalidationConfigBuilder = new ConfigurationBuilder();
invalidationConfigBuilder
//.invocationBatching().enable()
.transaction().transactionMode(TransactionMode.TRANSACTIONAL)
.transaction().transactionManagerLookup(new DummyTransactionManagerLookup())
.locking().isolationLevel(IsolationLevel.REPEATABLE_READ).writeSkewCheck(true).versioning().enable().scheme(VersioningScheme.SIMPLE);
//invalidationConfigBuilder.locking().isolationLevel(IsolationLevel.REPEATABLE_READ).writeSkewCheck(true).versioning().enable().scheme(VersioningScheme.SIMPLE);
if (clustered) {
invalidationConfigBuilder.clustering().cacheMode(async ? CacheMode.INVALIDATION_ASYNC : CacheMode.INVALIDATION_SYNC);
}
Configuration invalidationCacheConfiguration = invalidationConfigBuilder.build();
cacheManager.defineConfiguration(InfinispanConnectionProvider.REALM_CACHE_NAME, invalidationCacheConfiguration);
return cacheManager;
}
}

View file

@ -633,8 +633,8 @@ public class ClientAdapter implements ClientModel {
roleEntity.setClient(entity);
roleEntity.setClientRole(true);
roleEntity.setRealmId(realm.getId());
em.persist(roleEntity);
entity.getRoles().add(roleEntity);
em.persist(roleEntity);
em.flush();
return new RoleAdapter(realm, em, roleEntity);
}
@ -667,12 +667,21 @@ public class ClientAdapter implements ClientModel {
@Override
public Set<RoleModel> getRoles() {
Set<RoleModel> list = new HashSet<RoleModel>();
/*
Collection<RoleEntity> roles = entity.getRoles();
if (roles == null) return list;
for (RoleEntity entity : roles) {
list.add(new RoleAdapter(realm, em, entity));
}
*/
TypedQuery<RoleEntity> query = em.createNamedQuery("getClientRoles", RoleEntity.class);
query.setParameter("client", entity);
List<RoleEntity> roles = query.getResultList();
for (RoleEntity roleEntity : roles) {
list.add(new RoleAdapter(realm, em, roleEntity));
}
return list;
}
@Override

View file

@ -124,9 +124,18 @@ public class JpaRealmProvider implements RealmProvider {
.setParameter("realm", realm).executeUpdate();
num = em.createNamedQuery("deleteGroupsByRealm")
.setParameter("realm", realm).executeUpdate();
TypedQuery<ClientEntity> query = em.createNamedQuery("getClientsByRealm", ClientEntity.class);
query.setParameter("realm", realm);
List<ClientEntity> clients = query.getResultList();
for (ClientEntity a : clients) {
adapter.removeClient(a.getId());
}
/*
for (ClientEntity a : new LinkedList<>(realm.getClients())) {
adapter.removeClient(a.getId());
}
*/
for (ClientTemplateEntity a : new LinkedList<>(realm.getClientTemplates())) {
adapter.removeClientTemplate(a.getId());
}

View file

@ -729,9 +729,11 @@ public class RealmAdapter implements RealmModel {
@Override
public List<ClientModel> getClients() {
List<ClientModel> list = new ArrayList<ClientModel>();
if (realm.getClients() == null) return list;
for (ClientEntity entity : realm.getClients()) {
List<ClientModel> list = new LinkedList<>();
TypedQuery<ClientEntity> query = em.createNamedQuery("getClientsByRealm", ClientEntity.class);
query.setParameter("realm", realm);
List<ClientEntity> clients = query.getResultList();
for (ClientEntity entity : clients) {
list.add(new ClientAdapter(this, em, session, entity));
}
return list;
@ -794,11 +796,9 @@ public class RealmAdapter implements RealmModel {
clientEntity = a;
}
}
if (client == null) {
return false;
}
em.remove(clientEntity);
if (clientEntity == null) return false;
em.createNamedQuery("deleteScopeMappingByClient").setParameter("client", clientEntity).executeUpdate();
em.remove(clientEntity);
em.flush();
return true;
@ -1017,6 +1017,7 @@ public class RealmAdapter implements RealmModel {
entity.setFullSyncPeriod(model.getFullSyncPeriod());
entity.setChangedSyncPeriod(model.getChangedSyncPeriod());
entity.setLastSync(model.getLastSync());
entity.setRealm(realm);
em.persist(entity);
realm.getUserFederationProviders().add(entity);

View file

@ -19,6 +19,8 @@ package org.keycloak.models.jpa.entities;
import org.keycloak.models.AuthenticationExecutionModel;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
@ -43,6 +45,7 @@ import javax.persistence.Table;
public class AuthenticationExecutionEntity {
@Id
@Column(name="ID", length = 36)
@Access(AccessType.PROPERTY) // we do this because relationships often fetch id, but not entity. This avoids an extra SQL
protected String id;
@ManyToOne(fetch = FetchType.LAZY)

Some files were not shown because too many files have changed in this diff Show more