group permissions

This commit is contained in:
Bill Burke 2017-06-01 20:16:35 -04:00
commit b9f7a43a72
227 changed files with 6087 additions and 2073 deletions

View file

@ -35,6 +35,8 @@ import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @author <a href="mailto:brad.culley@spartasystems.com">Brad Culley</a>
* @author <a href="mailto:john.ament@spartasystems.com">John D. Ament</a>
* @version $Revision: 1 $
*/
public class KeycloakDeployment {
@ -88,6 +90,7 @@ public class KeycloakDeployment {
// https://tools.ietf.org/html/rfc7636
protected boolean pkce = false;
protected boolean ignoreOAuthQueryParameter;
public KeycloakDeployment() {
}
@ -436,4 +439,11 @@ public class KeycloakDeployment {
this.pkce = pkce;
}
public void setIgnoreOAuthQueryParameter(boolean ignoreOAuthQueryParameter) {
this.ignoreOAuthQueryParameter = ignoreOAuthQueryParameter;
}
public boolean isOAuthQueryParameterEnabled() {
return !this.ignoreOAuthQueryParameter;
}
}

View file

@ -37,6 +37,8 @@ import java.security.PublicKey;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @author <a href="mailto:brad.culley@spartasystems.com">Brad Culley</a>
* @author <a href="mailto:john.ament@spartasystems.com">John D. Ament</a>
* @version $Revision: 1 $
*/
public class KeycloakDeploymentBuilder {
@ -113,6 +115,7 @@ public class KeycloakDeploymentBuilder {
deployment.setTokenMinimumTimeToLive(adapterConfig.getTokenMinimumTimeToLive());
deployment.setMinTimeBetweenJwksRequests(adapterConfig.getMinTimeBetweenJwksRequests());
deployment.setPublicKeyCacheTtl(adapterConfig.getPublicKeyCacheTtl());
deployment.setIgnoreOAuthQueryParameter(adapterConfig.isIgnoreOAuthQueryParameter());
if (realmKeyPem == null && adapterConfig.isBearerOnly() && adapterConfig.getAuthServerUrl() == null) {
throw new IllegalArgumentException("For bearer auth, you must set the realm-public-key or auth-server-url");

View file

@ -22,6 +22,8 @@ import org.keycloak.adapters.spi.HttpFacade;
/**
* @author <a href="mailto:froehlich.ch@gmail.com">Christian Froehlich</a>
* @author <a href="mailto:brad.culley@spartasystems.com">Brad Culley</a>
* @author <a href="mailto:john.ament@spartasystems.com">John D. Ament</a>
* @version $Revision: 1 $
*/
public class QueryParamterTokenRequestAuthenticator extends BearerTokenRequestAuthenticator {
@ -33,6 +35,9 @@ public class QueryParamterTokenRequestAuthenticator extends BearerTokenRequestAu
}
public AuthOutcome authenticate(HttpFacade exchange) {
if(!deployment.isOAuthQueryParameterEnabled()) {
return AuthOutcome.NOT_ATTEMPTED;
}
tokenString = null;
tokenString = getAccessTokenFromQueryParamter(exchange);
if (tokenString == null || tokenString.trim().isEmpty()) {

View file

@ -29,10 +29,13 @@ import org.keycloak.common.util.PemUtils;
import org.keycloak.enums.TokenStore;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
* @author <a href="mailto:brad.culley@spartasystems.com">Brad Culley</a>
* @author <a href="mailto:john.ament@spartasystems.com">John D. Ament</a>
*/
public class KeycloakDeploymentBuilderTest {
@ -58,6 +61,7 @@ public class KeycloakDeploymentBuilderTest {
assertTrue(deployment.isPublicClient());
assertTrue(deployment.isEnableBasicAuth());
assertTrue(deployment.isExposeToken());
assertFalse(deployment.isOAuthQueryParameterEnabled());
assertEquals("234234-234234-234234", deployment.getResourceCredentials().get("secret"));
assertEquals(ClientIdAndSecretCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId());
assertEquals(20, ((ThreadSafeClientConnManager) deployment.getClient().getConnectionManager()).getMaxTotal());

View file

@ -0,0 +1,49 @@
/*
* 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.adapters;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* @author <a href="mailto:brad.culley@spartasystems.com">Brad Culley</a>
* @author <a href="mailto:john.ament@spartasystems.com">John D. Ament</a>
*/
public class KeycloakDeploymentTest {
@Test
public void shouldNotEnableOAuthQueryParamWhenIgnoreIsTrue() {
KeycloakDeployment keycloakDeployment = new KeycloakDeployment();
keycloakDeployment.setIgnoreOAuthQueryParameter(true);
assertFalse(keycloakDeployment.isOAuthQueryParameterEnabled());
}
@Test
public void shouldEnableOAuthQueryParamWhenIgnoreIsFalse() {
KeycloakDeployment keycloakDeployment = new KeycloakDeployment();
keycloakDeployment.setIgnoreOAuthQueryParameter(false);
assertTrue(keycloakDeployment.isOAuthQueryParameterEnabled());
}
@Test
public void shouldEnableOAuthQueryParamWhenIgnoreNotSet() {
KeycloakDeployment keycloakDeployment = new KeycloakDeployment();
assertTrue(keycloakDeployment.isOAuthQueryParameterEnabled());
}
}

View file

@ -32,5 +32,6 @@
"principal-attribute": "email",
"token-minimum-time-to-live": 10,
"min-time-between-jwks-requests": 20,
"public-key-cache-ttl": 120
"public-key-cache-ttl": 120,
"ignore-oauth-query-parameter": true
}

View file

@ -33,6 +33,7 @@ import org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve;
import org.keycloak.adapters.undertow.KeycloakServletExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
@ -60,6 +61,7 @@ import java.util.Set;
@Configuration
@ConditionalOnWebApplication
@EnableConfigurationProperties(KeycloakSpringBootProperties.class)
@ConditionalOnProperty(value = "keycloak.enabled", matchIfMissing = true)
public class KeycloakAutoConfiguration {
private KeycloakSpringBootProperties keycloakProperties;

View file

@ -34,6 +34,11 @@ public class KeycloakSpringBootProperties extends AdapterConfig {
@JsonIgnore
private Map config = new HashMap();
/**
* Allow enabling of Keycloak Spring Boot adapter by configuration.
*/
private boolean enabled = true;
public Map getConfig() {
return config;
}
@ -43,6 +48,14 @@ public class KeycloakSpringBootProperties extends AdapterConfig {
*/
private List<SecurityConstraint> securityConstraints = new ArrayList<SecurityConstraint>();
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
/**
* This matches security-constraint of the servlet spec
*/

View file

@ -0,0 +1,41 @@
/*
* 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.adapters.springsecurity.authentication;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
/**
* To return the forbidden code with the corresponding message.
*
* @author emilienbondu
*
*/
public class KeycloakAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unable to authenticate using the Authorization header");
}
}

View file

@ -17,6 +17,13 @@
package org.keycloak.adapters.springsecurity.filter;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.keycloak.adapters.AdapterDeploymentContext;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.KeycloakDeployment;
@ -25,7 +32,7 @@ import org.keycloak.adapters.spi.AuthChallenge;
import org.keycloak.adapters.spi.AuthOutcome;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.adapters.springsecurity.KeycloakAuthenticationException;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationEntryPoint;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationFailureHandler;
import org.keycloak.adapters.springsecurity.authentication.SpringSecurityRequestAuthenticator;
import org.keycloak.adapters.springsecurity.facade.SimpleHttpFacade;
import org.keycloak.adapters.springsecurity.token.AdapterTokenStoreFactory;
@ -41,19 +48,12 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Provides a Keycloak authentication processing filter.
*
@ -61,17 +61,15 @@ import java.io.IOException;
* @version $Revision: 1 $
*/
public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter implements ApplicationContextAware {
public static final String DEFAULT_LOGIN_URL = "/sso/login";
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String SCHEME_BEARER = "bearer ";
public static final String SCHEME_BASIC = "basic ";
/**
* Request matcher that matches requests to the {@link KeycloakAuthenticationEntryPoint#DEFAULT_LOGIN_URI default login URI}
* and any request with a <code>Authorization</code> header.
* Request matcher that matches all requests.
*/
public static final RequestMatcher DEFAULT_REQUEST_MATCHER =
new OrRequestMatcher(new AntPathRequestMatcher(DEFAULT_LOGIN_URL), new RequestHeaderRequestMatcher(AUTHORIZATION_HEADER));
private static RequestMatcher DEFAULT_REQUEST_MATCHER = new AntPathRequestMatcher("/**");
private static final Logger log = LoggerFactory.getLogger(KeycloakAuthenticationProcessingFilter.class);
@ -89,7 +87,7 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati
*/
public KeycloakAuthenticationProcessingFilter(AuthenticationManager authenticationManager) {
this(authenticationManager, DEFAULT_REQUEST_MATCHER);
setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler(DEFAULT_LOGIN_URL));
setAuthenticationFailureHandler(new KeycloakAuthenticationFailureHandler());
}
/**
@ -140,7 +138,18 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati
log.debug("Auth outcome: {}", result);
if (AuthOutcome.FAILED.equals(result)) {
throw new KeycloakAuthenticationException("Auth outcome: " + result);
AuthChallenge challenge = authenticator.getChallenge();
if (challenge != null) {
challenge.challenge(facade);
}
throw new KeycloakAuthenticationException("Invalid authorization header, see WWW-Authenticate header for details");
}
if (AuthOutcome.NOT_ATTEMPTED.equals(result)) {
AuthChallenge challenge = authenticator.getChallenge();
if (challenge != null) {
challenge.challenge(facade);
}
throw new KeycloakAuthenticationException("Authorization header not found, see WWW-Authenticate header");
}
else if (AuthOutcome.AUTHENTICATED.equals(result)) {
@ -211,22 +220,10 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
if (this.isBearerTokenRequest(request)) {
SecurityContextHolder.clearContext();
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unable to authenticate bearer token");
return;
}
else if (this.isBasicAuthRequest(request)) {
SecurityContextHolder.clearContext();
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unable to authenticate with basic authentication");
return;
}
super.unsuccessfulAuthentication(request, response, failed);
}
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
super.unsuccessfulAuthentication(request, response, failed);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {

View file

@ -27,6 +27,7 @@ import org.keycloak.adapters.OidcKeycloakAccount;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.adapters.springsecurity.KeycloakAuthenticationException;
import org.keycloak.adapters.springsecurity.account.KeycloakRole;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationFailureHandler;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.keycloak.common.enums.SslRequired;
import org.keycloak.common.util.KeycloakUriBuilder;
@ -89,6 +90,8 @@ public class KeycloakAuthenticationProcessingFilterTest {
@Mock
private AuthenticationFailureHandler failureHandler;
private KeycloakAuthenticationFailureHandler keycloakFailureHandler;
@Mock
private OidcKeycloakAccount keycloakAccount;
@ -106,6 +109,7 @@ public class KeycloakAuthenticationProcessingFilterTest {
MockitoAnnotations.initMocks(this);
request = spy(new MockHttpServletRequest());
filter = new KeycloakAuthenticationProcessingFilter(authenticationManager);
keycloakFailureHandler = new KeycloakAuthenticationFailureHandler();
filter.setApplicationContext(applicationContext);
filter.setAuthenticationSuccessHandler(successHandler);
@ -155,10 +159,12 @@ public class KeycloakAuthenticationProcessingFilterTest {
when(keycloakDeployment.getStateCookieName()).thenReturn("kc-cookie");
when(keycloakDeployment.getSslRequired()).thenReturn(SslRequired.NONE);
when(keycloakDeployment.isBearerOnly()).thenReturn(Boolean.FALSE);
filter.attemptAuthentication(request, response);
verify(response).setStatus(302);
verify(response).setHeader(eq("Location"), startsWith("http://localhost:8080/auth"));
try {
filter.attemptAuthentication(request, response);
} catch (KeycloakAuthenticationException e) {
verify(response).setStatus(302);
verify(response).setHeader(eq("Location"), startsWith("http://localhost:8080/auth"));
}
}
@Test(expected = KeycloakAuthenticationException.class)
@ -210,8 +216,7 @@ public class KeycloakAuthenticationProcessingFilterTest {
AuthenticationException exception = new BadCredentialsException("OOPS");
this.setBearerAuthHeader(request);
filter.unsuccessfulAuthentication(request, response, exception);
verify(response).sendError(eq(HttpServletResponse.SC_UNAUTHORIZED), anyString());
verify(failureHandler, never()).onAuthenticationFailure(any(HttpServletRequest.class), any(HttpServletResponse.class),
verify(failureHandler).onAuthenticationFailure(any(HttpServletRequest.class), any(HttpServletResponse.class),
any(AuthenticationException.class));
}
@ -220,10 +225,18 @@ public class KeycloakAuthenticationProcessingFilterTest {
AuthenticationException exception = new BadCredentialsException("OOPS");
this.setBasicAuthHeader(request);
filter.unsuccessfulAuthentication(request, response, exception);
verify(response).sendError(eq(HttpServletResponse.SC_UNAUTHORIZED), anyString());
verify(failureHandler, never()).onAuthenticationFailure(any(HttpServletRequest.class), any(HttpServletResponse.class),
verify(failureHandler).onAuthenticationFailure(any(HttpServletRequest.class), any(HttpServletResponse.class),
any(AuthenticationException.class));
}
@Test
public void testDefaultFailureHanlder() throws Exception {
AuthenticationException exception = new BadCredentialsException("OOPS");
filter.setAuthenticationFailureHandler(keycloakFailureHandler);
filter.unsuccessfulAuthentication(request, response, exception);
verify(response).sendError(eq(HttpServletResponse.SC_UNAUTHORIZED), any(String.class));
}
@Test(expected = UnsupportedOperationException.class)
public void testSetAllowSessionCreation() throws Exception {

View file

@ -178,7 +178,14 @@ public class SharedAttributeDefinitons {
.setXmlName("autodetect-bearer-only")
.setAllowExpression(true)
.setDefaultValue(new ModelNode(false))
.build();
.build();
protected static final SimpleAttributeDefinition IGNORE_OAUTH_QUERY_PARAMETER =
new SimpleAttributeDefinitionBuilder("ignore-oauth-query-parameter", ModelType.BOOLEAN, true)
.setXmlName("ignore-oauth-query-parameter")
.setAllowExpression(true)
.setDefaultValue(new ModelNode(false))
.build();
protected static final List<SimpleAttributeDefinition> ATTRIBUTES = new ArrayList<SimpleAttributeDefinition>();
static {
@ -206,6 +213,7 @@ public class SharedAttributeDefinitons {
ATTRIBUTES.add(TOKEN_STORE);
ATTRIBUTES.add(PRINCIPAL_ATTRIBUTE);
ATTRIBUTES.add(AUTODETECT_BEARER_ONLY);
ATTRIBUTES.add(IGNORE_OAUTH_QUERY_PARAMETER);
}
/**

View file

@ -48,6 +48,7 @@ keycloak.realm.register-node-period=how often to re-register node
keycloak.realm.token-store=cookie or session storage for auth session data
keycloak.realm.principal-attribute=token attribute to use to set Principal name
keycloak.realm.autodetect-bearer-only=autodetect bearer-only requests
keycloak.realm.ignore-oauth-query-parameter=disable query parameter parsing for access_token
keycloak.secure-deployment=A deployment secured by Keycloak
keycloak.secure-deployment.add=Add a deployment to be secured by Keycloak
@ -86,6 +87,7 @@ keycloak.secure-deployment.turn-off-change-session-id-on-login=The session id is
keycloak.secure-deployment.token-minimum-time-to-live=The adapter will refresh the token if the current token is expired OR will expire in 'token-minimum-time-to-live' seconds or less
keycloak.secure-deployment.min-time-between-jwks-requests=If adapter recognize token signed by unknown public key, it will try to download new public key from keycloak server. However it won't try to download if already tried it in less than 'min-time-between-jwks-requests' seconds
keycloak.secure-deployment.autodetect-bearer-only=autodetect bearer-only requests
keycloak.secure-deployment.ignore-oauth-query-parameter=disable query parameter parsing for access_token
keycloak.secure-deployment.credential=Credential value

View file

@ -67,6 +67,7 @@
<xs:element name="token-store" type="xs:string" minOccurs="0" maxOccurs="1"/>
<xs:element name="principal-attribute" type="xs:string" minOccurs="0" maxOccurs="1"/>
<xs:element name="autodetect-bearer-only" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
<xs:element name="ignore-oauth-query-parameter" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
</xs:all>
<xs:attribute name="name" type="xs:string" use="required">
<xs:annotation>
@ -111,6 +112,7 @@
<xs:element name="token-minimum-time-to-live" type="xs:integer" minOccurs="0" maxOccurs="1"/>
<xs:element name="min-time-between-jwks-requests" type="xs:integer" minOccurs="0" maxOccurs="1"/>
<xs:element name="autodetect-bearer-only" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
<xs:element name="ignore-oauth-query-parameter" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
</xs:all>
<xs:attribute name="name" type="xs:string" use="required">
<xs:annotation>

View file

@ -179,6 +179,12 @@ public class SharedAttributeDefinitons {
.setDefaultValue(new ModelNode(false))
.build();
protected static final SimpleAttributeDefinition IGNORE_OAUTH_QUERY_PARAMETER =
new SimpleAttributeDefinitionBuilder("ignore-oauth-query-parameter", ModelType.BOOLEAN, true)
.setXmlName("ignore-oauth-query-parameter")
.setAllowExpression(true)
.setDefaultValue(new ModelNode(false))
.build();
@ -209,6 +215,7 @@ public class SharedAttributeDefinitons {
ATTRIBUTES.add(TOKEN_STORE);
ATTRIBUTES.add(PRINCIPAL_ATTRIBUTE);
ATTRIBUTES.add(AUTODETECT_BEARER_ONLY);
ATTRIBUTES.add(IGNORE_OAUTH_QUERY_PARAMETER);
}
private static boolean isSet(ModelNode attributes, SimpleAttributeDefinition def) {

View file

@ -48,6 +48,7 @@ keycloak.realm.register-node-period=how often to re-register node
keycloak.realm.token-store=cookie or session storage for auth session data
keycloak.realm.principal-attribute=token attribute to use to set Principal name
keycloak.realm.autodetect-bearer-only=autodetect bearer-only requests
keycloak.realm.ignore-oauth-query-parameter=disable query parameter parsing for access_token
keycloak.secure-deployment=A deployment secured by Keycloak
keycloak.secure-deployment.add=Add a deployment to be secured by Keycloak
@ -86,6 +87,7 @@ keycloak.secure-deployment.principal-attribute=token attribute to use to set Pri
keycloak.secure-deployment.turn-off-change-session-id-on-login=The session id is changed by default on a successful login. Change this to true if you want to turn this off
keycloak.secure-deployment.token-minimum-time-to-live=The adapter will refresh the token if the current token is expired OR will expire in 'token-minimum-time-to-live' seconds or less
keycloak.secure-deployment.min-time-between-jwks-requests=If adapter recognize token signed by unknown public key, it will try to download new public key from keycloak server. However it won't try to download if already tried it in less than 'min-time-between-jwks-requests' seconds
keycloak.secure-deployment.ignore-oauth-query-parameter=disable query parameter parsing for access_token
keycloak.secure-deployment.credential=Credential value

View file

@ -67,6 +67,7 @@
<xs:element name="token-store" type="xs:string" minOccurs="0" maxOccurs="1"/>
<xs:element name="principal-attribute" type="xs:string" minOccurs="0" maxOccurs="1"/>
<xs:element name="autodetect-bearer-only" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
<xs:element name="ignore-oauth-query-parameter" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
</xs:all>
<xs:attribute name="name" type="xs:string" use="required">
<xs:annotation>
@ -111,6 +112,7 @@
<xs:element name="token-minimum-time-to-live" type="xs:integer" minOccurs="0" maxOccurs="1"/>
<xs:element name="min-time-between-jwks-requests" type="xs:integer" minOccurs="0" maxOccurs="1"/>
<xs:element name="autodetect-bearer-only" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
<xs:element name="ignore-oauth-query-parameter" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
</xs:all>
<xs:attribute name="name" type="xs:string" use="required">
<xs:annotation>

View file

@ -53,10 +53,12 @@ import org.keycloak.saml.SAML2AuthnRequestBuilder;
import org.keycloak.saml.SAMLRequestParser;
import org.keycloak.saml.SignatureAlgorithm;
import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.constants.JBossSAMLConstants;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.util.Base64;
import org.keycloak.saml.common.util.DocumentUtil;
import org.keycloak.saml.processing.api.saml.v2.sig.SAML2Signature;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil;
@ -74,10 +76,14 @@ import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.util.*;
import javax.xml.namespace.QName;
import org.keycloak.dom.saml.v2.SAML2Object;
import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
import org.keycloak.rotation.KeyLocator;
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
import org.keycloak.saml.processing.core.util.XMLEncryptionUtil;
/**
*
@ -210,7 +216,7 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
return AuthOutcome.FAILED;
}
}
return handleLoginResponse((ResponseType) statusResponse, postBinding, onCreateSession);
return handleLoginResponse(holder, postBinding, onCreateSession);
} finally {
sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.NONE);
}
@ -312,7 +318,8 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
return false;
}
protected AuthOutcome handleLoginResponse(final ResponseType responseType, boolean postBinding, OnSessionCreated onCreateSession) {
protected AuthOutcome handleLoginResponse(SAMLDocumentHolder responseHolder, boolean postBinding, OnSessionCreated onCreateSession) {
final ResponseType responseType = (ResponseType) responseHolder.getSamlObject();
AssertionType assertion = null;
if (! isSuccessfulSamlResponse(responseType) || responseType.getAssertions() == null || responseType.getAssertions().isEmpty()) {
challenge = new AuthChallenge() {
@ -357,11 +364,12 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
if (deployment.getIDP().getSingleSignOnService().validateAssertionSignature()) {
try {
validateSamlSignature(new SAMLDocumentHolder(AssertionUtil.asDocument(assertion)), postBinding, GeneralConstants.SAML_RESPONSE_KEY);
validateSamlSignature(new SAMLDocumentHolder(buildAssertionDocument(responseHolder, assertion)), postBinding, GeneralConstants.SAML_RESPONSE_KEY);
} catch (VerificationException e) {
log.error("Failed to verify saml assertion signature", e);
challenge = new AuthChallenge() {
@Override
public boolean challenge(HttpFacade exchange) {
SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.INVALID_SIGNATURE, responseType);
@ -376,8 +384,24 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
}
};
return AuthOutcome.FAILED;
} catch (ProcessingException e) {
e.printStackTrace();
} catch (Exception e) {
log.error("Error processing validation of SAML assertion: " + e.getMessage());
challenge = new AuthChallenge() {
@Override
public boolean challenge(HttpFacade exchange) {
SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.EXTRACTION_FAILURE);
exchange.getRequest().setError(error);
exchange.getResponse().sendError(403);
return true;
}
@Override
public int getResponseCode() {
return 403;
}
};
return AuthOutcome.FAILED;
}
}
@ -480,6 +504,21 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
&& Objects.equals(responseType.getStatus().getStatusCode().getValue().toString(), JBossSAMLURIConstants.STATUS_SUCCESS.get());
}
private Document buildAssertionDocument(final SAMLDocumentHolder responseHolder, AssertionType assertion) throws ConfigurationException, ProcessingException {
Element encryptedAssertion = org.keycloak.saml.common.util.DocumentUtil.getElement(responseHolder.getSamlDocument(), new QName(JBossSAMLConstants.ENCRYPTED_ASSERTION.get()));
if (encryptedAssertion != null) {
// encrypted assertion.
// We'll need to decrypt it first.
Document encryptedAssertionDocument = DocumentUtil.createDocument();
encryptedAssertionDocument.appendChild(encryptedAssertionDocument.importNode(encryptedAssertion, true));
Element assertionElement = XMLEncryptionUtil.decryptElementInDocument(encryptedAssertionDocument, deployment.getDecryptionKey());
Document assertionDocument = DocumentUtil.createDocument();
assertionDocument.appendChild(assertionDocument.importNode(assertionElement, true));
return assertionDocument;
}
return AssertionUtil.asDocument(assertion);
}
private String getAttributeValue(Object attrValue) {
String value = null;
if (attrValue instanceof String) {

View file

@ -124,6 +124,11 @@
<artifactId>keycloak-spring-boot-starter</artifactId>
<version>3.2.0.CR1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-client</artifactId>
<version>3.2.0.CR1-SNAPSHOT</version>
</dependency>
</dependencies>
</dependencyManagement>

View file

@ -24,6 +24,8 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
* Configuration for Java based adapters
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @author <a href="mailto:brad.culley@spartasystems.com">Brad Culley</a>
* @author <a href="mailto:john.ament@spartasystems.com">John D. Ament</a>
* @version $Revision: 1 $
*/
@JsonPropertyOrder({"realm", "realm-public-key", "auth-server-url", "ssl-required",
@ -38,7 +40,7 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
"register-node-at-startup", "register-node-period", "token-store", "principal-attribute",
"proxy-url", "turn-off-change-session-id-on-login", "token-minimum-time-to-live",
"min-time-between-jwks-requests", "public-key-cache-ttl",
"policy-enforcer"
"policy-enforcer", "ignore-oauth-query-parameter"
})
public class AdapterConfig extends BaseAdapterConfig implements AdapterHttpClientConfig {
@ -81,6 +83,8 @@ public class AdapterConfig extends BaseAdapterConfig implements AdapterHttpClien
// https://tools.ietf.org/html/rfc7636
@JsonProperty("enable-pkce")
protected boolean pkce = false;
@JsonProperty("ignore-oauth-query-parameter")
protected boolean ignoreOAuthQueryParameter = false;
/**
* The Proxy url to use for requests to the auth-server, configurable via the adapter config property {@code proxy-url}.
@ -257,4 +261,11 @@ public class AdapterConfig extends BaseAdapterConfig implements AdapterHttpClien
this.pkce = pkce;
}
public boolean isIgnoreOAuthQueryParameter() {
return ignoreOAuthQueryParameter;
}
public void setIgnoreOAuthQueryParameter(boolean ignoreOAuthQueryParameter) {
this.ignoreOAuthQueryParameter = ignoreOAuthQueryParameter;
}
}

View file

@ -19,13 +19,11 @@
package org.keycloak.representations.idm.authorization;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.authorization.DecisionEffect;
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -123,7 +121,7 @@ public class PolicyEvaluationResponse {
private PolicyRepresentation policy;
private DecisionEffect status;
private List<PolicyResultRepresentation> associatedPolicies;
private List<ScopeRepresentation> scopes = new ArrayList<>();
private Set<String> scopes = new HashSet<>();
public PolicyRepresentation getPolicy() {
return policy;
@ -162,11 +160,11 @@ public class PolicyEvaluationResponse {
return this.policy.equals(policy.getPolicy());
}
public void setScopes(List<ScopeRepresentation> scopes) {
public void setScopes(Set<String> scopes) {
this.scopes = scopes;
}
public List<ScopeRepresentation> getScopes() {
public Set<String> getScopes() {
return scopes;
}
}

View file

@ -92,7 +92,9 @@
<local-cache name="authenticationSessions"/>
<local-cache name="offlineSessions"/>
<local-cache name="loginFailures"/>
<local-cache name="authorization"/>
<local-cache name="authorization">
<eviction max-entries="10000" strategy="LRU"/>
</local-cache>
<local-cache name="actionTokens"/>
<local-cache name="work"/>
<local-cache name="keys">

View file

@ -326,6 +326,10 @@ public class LDAPOperationManager {
filter = "(&(objectClass=*)(" + getUuidAttributeName() + LDAPConstants.EQUAL + id + "))";
}
if (logger.isTraceEnabled()) {
logger.tracef("Using filter for lookup user by LDAP ID: %s", filter);
}
return filter;
}

View file

@ -163,8 +163,13 @@ public interface RealmResource {
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response partialImport(PartialImportRepresentation rep);
Response partialImport(PartialImportRepresentation rep);
@Path("partial-export")
@POST
@Produces(MediaType.APPLICATION_JSON)
RealmRepresentation partialExport(@QueryParam("exportGroupsAndRoles") Boolean exportGroupsAndRoles,
@QueryParam("exportClients") Boolean exportClients);
@Path("authentication")
@Consumes(MediaType.APPLICATION_JSON)
AuthenticationManagementResource flows();

View file

@ -17,8 +17,11 @@
package org.keycloak.client.registration;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.IOException;
import java.io.InputStream;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClients;
import org.keycloak.representations.adapters.config.AdapterConfig;
@ -26,8 +29,8 @@ import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
import java.io.InputStream;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -73,7 +76,7 @@ public class ClientRegistration {
public ClientRepresentation create(ClientRepresentation client) throws ClientRegistrationException {
String content = serialize(client);
InputStream resultStream = httpUtil.doPost(content, JSON, JSON, DEFAULT);
InputStream resultStream = httpUtil.doPost(content, JSON, UTF_8, JSON, DEFAULT);
return deserialize(resultStream, ClientRepresentation.class);
}
@ -89,7 +92,7 @@ public class ClientRegistration {
public ClientRepresentation update(ClientRepresentation client) throws ClientRegistrationException {
String content = serialize(client);
InputStream resultStream = httpUtil.doPut(content, JSON, JSON, DEFAULT, client.getClientId());
InputStream resultStream = httpUtil.doPut(content, JSON, UTF_8, JSON, DEFAULT, client.getClientId());
return resultStream != null ? deserialize(resultStream, ClientRepresentation.class) : null;
}
@ -129,7 +132,7 @@ public class ClientRegistration {
public OIDCClientRepresentation create(OIDCClientRepresentation client) throws ClientRegistrationException {
String content = serialize(client);
InputStream resultStream = httpUtil.doPost(content, JSON, JSON, OIDC);
InputStream resultStream = httpUtil.doPost(content, JSON, UTF_8, JSON, OIDC);
return deserialize(resultStream, OIDCClientRepresentation.class);
}
@ -140,7 +143,7 @@ public class ClientRegistration {
public OIDCClientRepresentation update(OIDCClientRepresentation client) throws ClientRegistrationException {
String content = serialize(client);
InputStream resultStream = httpUtil.doPut(content, JSON, JSON, OIDC, client.getClientId());
InputStream resultStream = httpUtil.doPut(content, JSON, UTF_8, JSON, OIDC, client.getClientId());
return resultStream != null ? deserialize(resultStream, OIDCClientRepresentation.class) : null;
}
@ -157,7 +160,7 @@ public class ClientRegistration {
public class SAMLClientRegistration {
public ClientRepresentation create(String entityDescriptor) throws ClientRegistrationException {
InputStream resultStream = httpUtil.doPost(entityDescriptor, XML, JSON, SAML);
InputStream resultStream = httpUtil.doPost(entityDescriptor, XML, UTF_8, JSON, SAML);
return deserialize(resultStream, ClientRepresentation.class);
}

View file

@ -31,6 +31,7 @@ import org.keycloak.common.util.StreamUtil;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -52,13 +53,13 @@ class HttpUtil {
this.auth = auth;
}
InputStream doPost(String content, String contentType, String acceptType, String... path) throws ClientRegistrationException {
InputStream doPost(String content, String contentType, Charset charset, String acceptType, String... path) throws ClientRegistrationException {
try {
HttpPost request = new HttpPost(getUrl(baseUri, path));
request.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
request.setHeader(HttpHeaders.CONTENT_TYPE, contentType(contentType, charset));
request.setHeader(HttpHeaders.ACCEPT, acceptType);
request.setEntity(new StringEntity(content));
request.setEntity(new StringEntity(content, charset));
addAuth(request);
@ -77,6 +78,10 @@ class HttpUtil {
throw new ClientRegistrationException("Failed to send request", e);
}
}
private String contentType(String contentType, Charset charset) {
return contentType + ";charset=" + charset.name();
}
InputStream doGet(String acceptType, String... path) throws ClientRegistrationException {
try {
@ -105,13 +110,13 @@ class HttpUtil {
}
}
InputStream doPut(String content, String contentType, String acceptType, String... path) throws ClientRegistrationException {
InputStream doPut(String content, String contentType, Charset charset, String acceptType, String... path) throws ClientRegistrationException {
try {
HttpPut request = new HttpPut(getUrl(baseUri, path));
request.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
request.setHeader(HttpHeaders.CONTENT_TYPE, contentType(contentType, charset));
request.setHeader(HttpHeaders.ACCEPT, acceptType);
request.setEntity(new StringEntity(content));
request.setEntity(new StringEntity(content, charset));
addAuth(request);

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<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">
<modelVersion>4.0.0</modelVersion>
<parent>
@ -12,26 +13,32 @@
<name>keycloak-test-helper</name>
<description>Helper library to test application using Keycloak.</description>
<packaging>jar</packaging>
<properties>
<resteasy.client.version>3.0.7.Final</resteasy.client.version>
</properties>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-client-registration-api</artifactId>
<version>3.2.0.CR1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-client</artifactId>
<version>3.2.0.CR1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-client</artifactId>
<version>3.0.7.Final</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jackson2-provider</artifactId>
<version>3.0.7.Final</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-client-registration-api</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-client</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-client</artifactId>
<version>3.0.7.Final</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jackson2-provider</artifactId>
<version>3.0.7.Final</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,64 @@
/*
* 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.test.page;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
/**
* @author <a href="mailto:bruno@abstractj.org">Bruno Oliveira</a>
*/
public class IndexPage {
public static final String UNAUTHORIZED = "401 Unauthorized";
@FindBy(name = "loginBtn")
private WebElement loginButton;
@FindBy(name = "logoutBtn")
private WebElement logoutButton;
@FindBy(name = "adminBtn")
private WebElement adminButton;
@FindBy(name = "publicBtn")
private WebElement publicButton;
@FindBy(name = "securedBtn")
private WebElement securedBtn;
public void clickLogin() {
loginButton.click();
}
public void clickLogout() {
logoutButton.click();
}
public void clickAdmin() {
adminButton.click();
}
public void clickPublic() {
publicButton.click();
}
public void clickSecured() {
securedBtn.click();
}
}

View file

@ -0,0 +1,87 @@
/*
* 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.test.page;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class LoginPage {
@FindBy(id = "username")
private WebElement usernameInput;
@FindBy(id = "password")
private WebElement passwordInput;
@FindBy(id = "totp")
private WebElement totp;
@FindBy(id = "rememberMe")
private WebElement rememberMe;
@FindBy(name = "login")
private WebElement submitButton;
@FindBy(name = "cancel")
private WebElement cancelButton;
@FindBy(linkText = "Register")
private WebElement registerLink;
@FindBy(linkText = "Forgot Password?")
private WebElement resetPasswordLink;
@FindBy(linkText = "Username")
private WebElement recoverUsernameLink;
@FindBy(className = "alert-error")
private WebElement loginErrorMessage;
@FindBy(className = "alert-warning")
private WebElement loginWarningMessage;
@FindBy(className = "alert-success")
private WebElement loginSuccessMessage;
@FindBy(className = "alert-info")
private WebElement loginInfoMessage;
@FindBy(className = "instruction")
private WebElement instruction;
@FindBy(id = "kc-current-locale-link")
private WebElement languageText;
@FindBy(id = "kc-locale-dropdown")
private WebElement localeDropdown;
public void login(String username, String password) {
usernameInput.clear();
usernameInput.sendKeys(username);
passwordInput.clear();
passwordInput.sendKeys(password);
submitButton.click();
}
}

View file

@ -0,0 +1,71 @@
/*
* 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.test.page;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
/**
* @author <a href="mailto:bruno@abstractj.org">Bruno Oliveira</a>
*/
public class ProfilePage {
@FindBy(name = "profileBtn")
private WebElement profileButton;
@FindBy(name = "tokenBtn")
private WebElement tokenButton;
@FindBy(name = "logoutBtn")
private WebElement logoutButton;
@FindBy(name = "accountBtn")
private WebElement accountButton;
@FindBy(id = "token-content")
private WebElement tokenContent;
@FindBy(id = "username")
private WebElement username;
public String getUsername() {
return username.getText();
}
public void clickProfile() {
profileButton.click();
}
public void clickToken() {
tokenButton.click();
}
public void clickLogout() {
logoutButton.click();
}
public void clickAccount() {
accountButton.click();
}
public String getTokenContent() throws Exception {
return tokenContent.getText();
}
}

View file

@ -15,6 +15,10 @@
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-adapter</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>

View file

@ -27,8 +27,6 @@ import org.keycloak.representations.idm.authorization.Logic;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -49,7 +47,7 @@ public class PolicyAdapter implements Policy, CachedModel<Policy> {
@Override
public Policy getDelegateForUpdate() {
if (updated == null) {
cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourceServerId());
cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(), cached.getResourceServerId());
updated = cacheSession.getPolicyStoreDelegate().findById(cached.getId(), cached.getResourceServerId());
if (updated == null) throw new IllegalStateException("Not found in database");
}
@ -98,6 +96,7 @@ public class PolicyAdapter implements Policy, CachedModel<Policy> {
@Override
public void setName(String name) {
getDelegateForUpdate();
cacheSession.registerPolicyInvalidation(cached.getId(), name, cached.getResourcesIds(), cached.getResourceServerId());
updated.setName(name);
}
@ -208,7 +207,6 @@ public class PolicyAdapter implements Policy, CachedModel<Policy> {
public void addScope(Scope scope) {
getDelegateForUpdate();
updated.addScope(scope);
}
@Override
@ -235,6 +233,9 @@ public class PolicyAdapter implements Policy, CachedModel<Policy> {
@Override
public void addResource(Resource resource) {
getDelegateForUpdate();
HashSet<String> resources = new HashSet<>();
resources.add(resource.getId());
cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), resources, cached.getResourceServerId());
updated.addResource(resource);
}
@ -242,6 +243,9 @@ public class PolicyAdapter implements Policy, CachedModel<Policy> {
@Override
public void removeResource(Resource resource) {
getDelegateForUpdate();
HashSet<String> resources = new HashSet<>();
resources.add(resource.getId());
cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), resources, cached.getResourceServerId());
updated.removeResource(resource);
}

View file

@ -26,6 +26,8 @@ import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -44,7 +46,7 @@ public class ResourceAdapter implements Resource, CachedModel<Resource> {
@Override
public Resource getDelegateForUpdate() {
if (updated == null) {
cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), cached.getResourceServerId());
cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), cached.getType(), cached.getUri(), cached.getScopesIds(), cached.getResourceServerId());
updated = cacheSession.getResourceStoreDelegate().findById(cached.getId(), cached.getResourceServerId());
if (updated == null) throw new IllegalStateException("Not found in database");
}
@ -93,6 +95,7 @@ public class ResourceAdapter implements Resource, CachedModel<Resource> {
@Override
public void setName(String name) {
getDelegateForUpdate();
cacheSession.registerResourceInvalidation(cached.getId(), name, cached.getType(), cached.getUri(), cached.getScopesIds(), cached.getResourceServerId());
updated.setName(name);
}
@ -124,8 +127,8 @@ public class ResourceAdapter implements Resource, CachedModel<Resource> {
@Override
public void setUri(String uri) {
getDelegateForUpdate();
cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), cached.getType(), uri, cached.getScopesIds(), cached.getResourceServerId());
updated.setUri(uri);
}
@Override
@ -137,6 +140,7 @@ public class ResourceAdapter implements Resource, CachedModel<Resource> {
@Override
public void setType(String type) {
getDelegateForUpdate();
cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), type, cached.getUri(), cached.getScopesIds(), cached.getResourceServerId());
updated.setType(type);
}
@ -164,6 +168,7 @@ public class ResourceAdapter implements Resource, CachedModel<Resource> {
@Override
public void updateScopes(Set<Scope> scopes) {
getDelegateForUpdate();
cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), cached.getType(), cached.getUri(), scopes.stream().map(scope1 -> scope1.getId()).collect(Collectors.toSet()), cached.getResourceServerId());
updated.updateScopes(scopes);
}

View file

@ -21,7 +21,9 @@ import org.jboss.logging.Logger;
import org.keycloak.models.cache.infinispan.CacheManager;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
import org.keycloak.models.cache.infinispan.authorization.events.AuthorizationCacheInvalidationEvent;
import org.keycloak.models.cache.infinispan.authorization.stream.InResourcePredicate;
import org.keycloak.models.cache.infinispan.authorization.stream.InResourceServerPredicate;
import org.keycloak.models.cache.infinispan.authorization.stream.InScopePredicate;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
@ -65,28 +67,54 @@ public class StoreFactoryCacheManager extends CacheManager {
public void scopeUpdated(String id, String name, String serverId, Set<String> invalidations) {
invalidations.add(id);
invalidations.add(StoreFactoryCacheSession.getScopeByNameCacheKey(name, serverId));
invalidations.add(StoreFactoryCacheSession.getResourceByScopeCacheKey(id, serverId));
}
public void scopeRemoval(String id, String name, String serverId, Set<String> invalidations) {
scopeUpdated(id, name, serverId, invalidations);
addInvalidations(InScopePredicate.create().scope(id), invalidations);
}
public void resourceUpdated(String id, String name, String serverId, Set<String> invalidations) {
public void resourceUpdated(String id, String name, String type, String uri, Set<String> scopes, String serverId, Set<String> invalidations) {
invalidations.add(id);
invalidations.add(StoreFactoryCacheSession.getResourceByNameCacheKey(name, serverId));
if (type != null) {
invalidations.add(StoreFactoryCacheSession.getResourceByTypeCacheKey(type, serverId));
addInvalidations(InResourcePredicate.create().resource(type), invalidations);
}
if (uri != null) {
invalidations.add(StoreFactoryCacheSession.getResourceByUriCacheKey(uri, serverId));
}
if (scopes != null) {
for (String scope : scopes) {
invalidations.add(StoreFactoryCacheSession.getResourceByScopeCacheKey(scope, serverId));
addInvalidations(InScopePredicate.create().scope(scope), invalidations);
}
}
}
public void resourceRemoval(String id, String name, String serverId, Set<String> invalidations) {
resourceUpdated(id, name, serverId, invalidations);
public void resourceRemoval(String id, String name, String type, String uri, String owner, Set<String> scopes, String serverId, Set<String> invalidations) {
resourceUpdated(id, name, type, uri, scopes, serverId, invalidations);
invalidations.add(StoreFactoryCacheSession.getResourceByOwnerCacheKey(owner, serverId));
addInvalidations(InResourcePredicate.create().resource(id), invalidations);
}
public void policyUpdated(String id, String name, String serverId, Set<String> invalidations) {
public void policyUpdated(String id, String name, Set<String> resources, String serverId, Set<String> invalidations) {
invalidations.add(id);
invalidations.add(StoreFactoryCacheSession.getPolicyByNameCacheKey(name, serverId));
if (resources != null) {
for (String resource : resources) {
invalidations.add(StoreFactoryCacheSession.getPolicyByResource(resource, serverId));
}
}
}
public void policyRemoval(String id, String name, String serverId, Set<String> invalidations) {
policyUpdated(id, name, serverId, invalidations);
public void policyRemoval(String id, String name, Set<String> resources, String serverId, Set<String> invalidations) {
policyUpdated(id, name, resources, serverId, invalidations);
}

View file

@ -16,6 +16,18 @@
*/
package org.keycloak.models.cache.infinispan.authorization;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.jboss.logging.Logger;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Resource;
@ -34,7 +46,12 @@ import org.keycloak.models.cache.infinispan.authorization.entities.CachedResourc
import org.keycloak.models.cache.infinispan.authorization.entities.CachedResourceServer;
import org.keycloak.models.cache.infinispan.authorization.entities.CachedScope;
import org.keycloak.models.cache.infinispan.authorization.entities.PolicyListQuery;
import org.keycloak.models.cache.infinispan.authorization.entities.PolicyQuery;
import org.keycloak.models.cache.infinispan.authorization.entities.PolicyResourceListQuery;
import org.keycloak.models.cache.infinispan.authorization.entities.PolicyScopeListQuery;
import org.keycloak.models.cache.infinispan.authorization.entities.ResourceListQuery;
import org.keycloak.models.cache.infinispan.authorization.entities.ResourceQuery;
import org.keycloak.models.cache.infinispan.authorization.entities.ResourceScopeListQuery;
import org.keycloak.models.cache.infinispan.authorization.entities.ResourceServerListQuery;
import org.keycloak.models.cache.infinispan.authorization.entities.ScopeListQuery;
import org.keycloak.models.cache.infinispan.authorization.events.PolicyRemovedEvent;
@ -48,12 +65,6 @@ import org.keycloak.models.cache.infinispan.authorization.events.ScopeUpdatedEve
import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
import java.util.HashMap;
import java.util.HashSet;
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 $
@ -233,20 +244,20 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
invalidationEvents.add(ScopeUpdatedEvent.create(id, name, serverId));
}
public void registerResourceInvalidation(String id, String name, String serverId) {
cache.resourceUpdated(id, name, serverId, invalidations);
public void registerResourceInvalidation(String id, String name, String type, String uri, Set<String> scopes, String serverId) {
cache.resourceUpdated(id, name, type, uri, scopes, serverId, invalidations);
ResourceAdapter adapter = managedResources.get(id);
if (adapter != null) adapter.invalidateFlag();
invalidationEvents.add(ResourceUpdatedEvent.create(id, name, serverId));
invalidationEvents.add(ResourceUpdatedEvent.create(id, name, type, uri, scopes, serverId));
}
public void registerPolicyInvalidation(String id, String name, String serverId) {
cache.policyUpdated(id, name, serverId, invalidations);
public void registerPolicyInvalidation(String id, String name, Set<String> resources, String serverId) {
cache.policyUpdated(id, name, resources, serverId, invalidations);
PolicyAdapter adapter = managedPolicies.get(id);
if (adapter != null) adapter.invalidateFlag();
invalidationEvents.add(PolicyUpdatedEvent.create(id, name, serverId));
invalidationEvents.add(PolicyUpdatedEvent.create(id, name, resources, serverId));
}
public ResourceServerStore getResourceServerStoreDelegate() {
@ -277,10 +288,38 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
return "resource.name." + name + "." + serverId;
}
public static String getResourceByOwnerCacheKey(String owner, String serverId) {
return "resource.owner." + owner + "." + serverId;
}
public static String getResourceByTypeCacheKey(String type, String serverId) {
return "resource.type." + type + "." + serverId;
}
public static String getResourceByUriCacheKey(String uri, String serverId) {
return "resource.uri." + uri + "." + serverId;
}
public static String getResourceByScopeCacheKey(String scopeId, String serverId) {
return "resource.scope." + scopeId + "." + serverId;
}
public static String getPolicyByNameCacheKey(String name, String serverId) {
return "policy.name." + name + "." + serverId;
}
public static String getPolicyByResource(String resourceId, String serverId) {
return "policy.resource." + resourceId + "." + serverId;
}
public static String getPolicyByResourceType(String type, String serverId) {
return "policy.resource.type." + type + "." + serverId;
}
public static String getPolicyByScope(String scope, String serverId) {
return "policy.scope." + scope + "." + serverId;
}
public StoreFactory getDelegate() {
if (delegate != null) return delegate;
delegate = session.getProvider(StoreFactory.class);
@ -451,7 +490,7 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
@Override
public Resource create(String name, ResourceServer resourceServer, String owner) {
Resource resource = getResourceStoreDelegate().create(name, resourceServer, owner);
registerResourceInvalidation(resource.getId(), resource.getName(), resourceServer.getId());
registerResourceInvalidation(resource.getId(), resource.getName(), resource.getType(), resource.getUri(), resource.getScopes().stream().map(scope -> scope.getId()).collect(Collectors.toSet()), resourceServer.getId());
return resource;
}
@ -462,8 +501,8 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
if (resource == null) return;
cache.invalidateObject(id);
invalidationEvents.add(ResourceRemovedEvent.create(id, resource.getName(), resource.getResourceServer().getId()));
cache.resourceRemoval(id, resource.getName(), resource.getResourceServer().getId(), invalidations);
invalidationEvents.add(ResourceRemovedEvent.create(id, resource.getName(), resource.getType(), resource.getUri(), resource.getOwner(), resource.getScopes().stream().map(scope -> scope.getId()).collect(Collectors.toSet()), resource.getResourceServer().getId()));
cache.resourceRemoval(id, resource.getName(), resource.getType(), resource.getUri(), resource.getOwner(), resource.getScopes().stream().map(scope -> scope.getId()).collect(Collectors.toSet()), resource.getResourceServer().getId(), invalidations);
getResourceStoreDelegate().delete(id);
}
@ -498,37 +537,37 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
public Resource findByName(String name, String resourceServerId) {
if (name == null) return null;
String cacheKey = getResourceByNameCacheKey(name, resourceServerId);
ResourceListQuery query = cache.get(cacheKey, ResourceListQuery.class);
if (query != null) {
logger.tracev("resource by name cache hit: {0}", name);
}
if (query == null) {
Long loaded = cache.getCurrentRevision(cacheKey);
Resource model = getResourceStoreDelegate().findByName(name, resourceServerId);
if (model == null) return null;
if (invalidations.contains(model.getId())) return model;
query = new ResourceListQuery(loaded, cacheKey, model.getId(), resourceServerId);
cache.addRevisioned(query, startupRevision);
return model;
} else if (invalidations.contains(cacheKey)) {
return getResourceStoreDelegate().findByName(name, resourceServerId);
} else {
String id = query.getResources().iterator().next();
if (invalidations.contains(id)) {
return getResourceStoreDelegate().findByName(name, resourceServerId);
List<Resource> result = cacheQuery(cacheKey, ResourceListQuery.class, () -> {
Resource resource = getResourceStoreDelegate().findByName(name, resourceServerId);
if (resource == null) {
return Collections.emptyList();
}
return findById(id, query.getResourceServerId());
return Arrays.asList(resource);
},
(revision, resources) -> new ResourceListQuery(revision, cacheKey, resources.stream().map(resource -> resource.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId);
if (result.isEmpty()) {
return null;
}
return result.get(0);
}
@Override
public List<Resource> findByOwner(String ownerId, String resourceServerId) {
return getResourceStoreDelegate().findByOwner(ownerId, resourceServerId);
String cacheKey = getResourceByOwnerCacheKey(ownerId, resourceServerId);
return cacheQuery(cacheKey, ResourceListQuery.class, () -> getResourceStoreDelegate().findByOwner(ownerId, resourceServerId),
(revision, resources) -> new ResourceListQuery(revision, cacheKey, resources.stream().map(resource -> resource.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId);
}
@Override
public List<Resource> findByUri(String uri, String resourceServerId) {
return getResourceStoreDelegate().findByUri(uri, resourceServerId);
if (uri == null) return null;
String cacheKey = getResourceByUriCacheKey(uri, resourceServerId);
return cacheQuery(cacheKey, ResourceListQuery.class, () -> getResourceStoreDelegate().findByUri(uri, resourceServerId),
(revision, resources) -> new ResourceListQuery(revision, cacheKey, resources.stream().map(resource -> resource.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId);
}
@Override
@ -543,21 +582,52 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
@Override
public List<Resource> findByScope(List<String> ids, String resourceServerId) {
return getResourceStoreDelegate().findByScope(ids, resourceServerId);
if (ids == null) return null;
List<Resource> result = new ArrayList<>();
for (String id : ids) {
String cacheKey = getResourceByScopeCacheKey(id, resourceServerId);
result.addAll(cacheQuery(cacheKey, ResourceScopeListQuery.class, () -> getResourceStoreDelegate().findByScope(Arrays.asList(id), resourceServerId), (revision, resources) -> new ResourceScopeListQuery(revision, cacheKey, id, resources.stream().map(resource -> resource.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId));
}
return result;
}
@Override
public List<Resource> findByType(String type, String resourceServerId) {
return getResourceStoreDelegate().findByType(type, resourceServerId);
if (type == null) return null;
String cacheKey = getResourceByTypeCacheKey(type, resourceServerId);
return cacheQuery(cacheKey, ResourceListQuery.class, () -> getResourceStoreDelegate().findByType(type, resourceServerId),
(revision, resources) -> new ResourceListQuery(revision, cacheKey, resources.stream().map(resource -> resource.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId);
}
private <R, Q extends ResourceQuery> List<R> cacheQuery(String cacheKey, Class<Q> queryType, Supplier<List<R>> resultSupplier, BiFunction<Long, List<R>, Q> querySupplier, String resourceServerId) {
Q query = cache.get(cacheKey, queryType);
if (query != null) {
logger.tracev("cache hit for key: {0}", cacheKey);
}
if (query == null) {
Long loaded = cache.getCurrentRevision(cacheKey);
List<R> model = resultSupplier.get();
if (model == null) return null;
if (invalidations.contains(cacheKey)) return model;
query = querySupplier.apply(loaded, model);
cache.addRevisioned(query, startupRevision);
return model;
} else if (query.isInvalid(invalidations)) {
return resultSupplier.get();
} else {
return query.getResources().stream().map(resourceId -> (R) findById(resourceId, resourceServerId)).collect(Collectors.toList());
}
}
}
protected class PolicyCache implements PolicyStore {
@Override
public Policy create(AbstractPolicyRepresentation representation, ResourceServer resourceServer) {
Policy resource = getPolicyStoreDelegate().create(representation, resourceServer);
registerPolicyInvalidation(resource.getId(), resource.getName(), resourceServer.getId());
return resource;
Policy policy = getPolicyStoreDelegate().create(representation, resourceServer);
registerPolicyInvalidation(policy.getId(), policy.getName(), policy.getResources().stream().map(resource1 -> resource1.getId()).collect(Collectors.toSet()), resourceServer.getId());
return policy;
}
@Override
@ -567,8 +637,8 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
if (policy == null) return;
cache.invalidateObject(id);
invalidationEvents.add(PolicyRemovedEvent.create(id, policy.getName(), policy.getResourceServer().getId()));
cache.policyRemoval(id, policy.getName(), policy.getResourceServer().getId(), invalidations);
invalidationEvents.add(PolicyRemovedEvent.create(id, policy.getName(), policy.getResources().stream().map(resource1 -> resource1.getId()).collect(Collectors.toSet()), policy.getResourceServer().getId()));
cache.policyRemoval(id, policy.getName(), policy.getResources().stream().map(resource1 -> resource1.getId()).collect(Collectors.toSet()), policy.getResourceServer().getId(), invalidations);
getPolicyStoreDelegate().delete(id);
}
@ -604,27 +674,22 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
public Policy findByName(String name, String resourceServerId) {
if (name == null) return null;
String cacheKey = getPolicyByNameCacheKey(name, resourceServerId);
PolicyListQuery query = cache.get(cacheKey, PolicyListQuery.class);
if (query != null) {
logger.tracev("policy by name cache hit: {0}", name);
}
if (query == null) {
Long loaded = cache.getCurrentRevision(cacheKey);
Policy model = getPolicyStoreDelegate().findByName(name, resourceServerId);
if (model == null) return null;
if (invalidations.contains(model.getId())) return model;
query = new PolicyListQuery(loaded, cacheKey, model.getId(), resourceServerId);
cache.addRevisioned(query, startupRevision);
return model;
} else if (invalidations.contains(cacheKey)) {
return getPolicyStoreDelegate().findByName(name, resourceServerId);
} else {
String id = query.getPolicies().iterator().next();
if (invalidations.contains(id)) {
return getPolicyStoreDelegate().findByName(name, resourceServerId);
List<Policy> result = cacheQuery(cacheKey, PolicyListQuery.class, () -> {
Policy policy = getPolicyStoreDelegate().findByName(name, resourceServerId);
if (policy == null) {
return Collections.emptyList();
}
return findById(id, query.getResourceServerId());
return Arrays.asList(policy);
},
(revision, policies) -> new PolicyListQuery(revision, cacheKey, policies.stream().map(policy -> policy.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId);
if (result.isEmpty()) {
return null;
}
return result.get(0);
}
@Override
@ -639,17 +704,29 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
@Override
public List<Policy> findByResource(String resourceId, String resourceServerId) {
return getPolicyStoreDelegate().findByResource(resourceId, resourceServerId);
String cacheKey = getPolicyByResource(resourceId, resourceServerId);
return cacheQuery(cacheKey, PolicyResourceListQuery.class, () -> getPolicyStoreDelegate().findByResource(resourceId, resourceServerId),
(revision, policies) -> new PolicyResourceListQuery(revision, cacheKey, resourceId, policies.stream().map(policy -> policy.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId);
}
@Override
public List<Policy> findByResourceType(String resourceType, String resourceServerId) {
return getPolicyStoreDelegate().findByResourceType(resourceType, resourceServerId);
String cacheKey = getPolicyByResourceType(resourceType, resourceServerId);
return cacheQuery(cacheKey, PolicyResourceListQuery.class, () -> getPolicyStoreDelegate().findByResourceType(resourceType, resourceServerId),
(revision, policies) -> new PolicyResourceListQuery(revision, cacheKey, resourceType, policies.stream().map(policy -> policy.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId);
}
@Override
public List<Policy> findByScopeIds(List<String> scopeIds, String resourceServerId) {
return getPolicyStoreDelegate().findByScopeIds(scopeIds, resourceServerId);
if (scopeIds == null) return null;
List<Policy> result = new ArrayList<>();
for (String id : scopeIds) {
String cacheKey = getPolicyByScope(id, resourceServerId);
result.addAll(cacheQuery(cacheKey, PolicyScopeListQuery.class, () -> getPolicyStoreDelegate().findByScopeIds(Arrays.asList(id), resourceServerId), (revision, resources) -> new PolicyScopeListQuery(revision, cacheKey, id, resources.stream().map(resource -> resource.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId));
}
return result;
}
@Override
@ -661,6 +738,26 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
public List<Policy> findDependentPolicies(String id, String resourceServerId) {
return getPolicyStoreDelegate().findDependentPolicies(id, resourceServerId);
}
private <R, Q extends PolicyQuery> List<R> cacheQuery(String cacheKey, Class<Q> queryType, Supplier<List<R>> resultSupplier, BiFunction<Long, List<R>, Q> querySupplier, String resourceServerId) {
Q query = cache.get(cacheKey, queryType);
if (query != null) {
logger.tracev("cache hit for key: {0}", cacheKey);
}
if (query == null) {
Long loaded = cache.getCurrentRevision(cacheKey);
List<R> model = resultSupplier.get();
if (model == null) return null;
if (invalidations.contains(cacheKey)) return model;
query = querySupplier.apply(loaded, model);
cache.addRevisioned(query, startupRevision);
return model;
} else if (query.isInvalid(invalidations)) {
return resultSupplier.get();
} else {
return query.getPolicies().stream().map(resourceId -> (R) findById(resourceId, resourceServerId)).collect(Collectors.toList());
}
}
}

View file

@ -0,0 +1,24 @@
/*
* Copyright 2017 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.authorization.entities;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public interface InResource {
String getResourceId();
}

View file

@ -0,0 +1,24 @@
/*
* 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.authorization.entities;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public interface InScope {
String getScopeId();
}

View file

@ -9,7 +9,7 @@ import java.util.Set;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class PolicyListQuery extends AbstractRevisioned implements InResourceServer {
public class PolicyListQuery extends AbstractRevisioned implements PolicyQuery {
private final Set<String> policies;
private final String serverId;
@ -33,4 +33,9 @@ public class PolicyListQuery extends AbstractRevisioned implements InResourceSer
public Set<String> getPolicies() {
return policies;
}
@Override
public boolean isInvalid(Set<String> invalidations) {
return invalidations.contains(getId()) || invalidations.contains(getResourceServerId());
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright 2017 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.authorization.entities;
import java.util.Set;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public interface PolicyQuery extends InResourceServer, Revisioned {
Set<String> getPolicies();
boolean isInvalid(Set<String> invalidations);
}

View file

@ -0,0 +1,42 @@
/*
* Copyright 2017 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.authorization.entities;
import java.util.Set;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class PolicyResourceListQuery extends PolicyListQuery implements InResource {
private final String resourceId;
public PolicyResourceListQuery(Long revision, String id, String resourceId, Set<String> policies, String serverId) {
super(revision, id, policies, serverId);
this.resourceId = resourceId;
}
@Override
public boolean isInvalid(Set<String> invalidations) {
return super.isInvalid(invalidations) || invalidations.contains(getResourceId());
}
@Override
public String getResourceId() {
return resourceId;
}
}

View file

@ -0,0 +1,42 @@
/*
* Copyright 2017 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.authorization.entities;
import java.util.Set;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class PolicyScopeListQuery extends PolicyListQuery implements InScope {
private final String scopeId;
public PolicyScopeListQuery(Long revision, String id, String scopeId, Set<String> resources, String serverId) {
super(revision, id, resources, serverId);
this.scopeId = scopeId;
}
@Override
public String getScopeId() {
return scopeId;
}
@Override
public boolean isInvalid(Set<String> invalidations) {
return super.isInvalid(invalidations) || invalidations.contains(getScopeId());
}
}

View file

@ -9,7 +9,7 @@ import java.util.Set;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ResourceListQuery extends AbstractRevisioned implements InResourceServer {
public class ResourceListQuery extends AbstractRevisioned implements ResourceQuery, InResourceServer {
private final Set<String> resources;
private final String serverId;
@ -33,4 +33,9 @@ public class ResourceListQuery extends AbstractRevisioned implements InResourceS
public Set<String> getResources() {
return resources;
}
@Override
public boolean isInvalid(Set<String> invalidations) {
return invalidations.contains(getId()) || invalidations.contains(getResourceServerId());
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright 2017 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.authorization.entities;
import java.util.Set;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public interface ResourceQuery extends Revisioned {
Set<String> getResources();
boolean isInvalid(Set<String> invalidations);
}

View file

@ -0,0 +1,26 @@
package org.keycloak.models.cache.infinispan.authorization.entities;
import java.util.Set;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class ResourceScopeListQuery extends ResourceListQuery implements InScope {
private final String scopeId;
public ResourceScopeListQuery(Long revision, String id, String scopeId, Set<String> resources, String serverId) {
super(revision, id, resources, serverId);
this.scopeId = scopeId;
}
@Override
public String getScopeId() {
return scopeId;
}
@Override
public boolean isInvalid(Set<String> invalidations) {
return super.isInvalid(invalidations) || invalidations.contains(getScopeId());
}
}

View file

@ -17,11 +17,11 @@
package org.keycloak.models.cache.infinispan.authorization.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.authorization.StoreFactoryCacheManager;
import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
import java.util.Set;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@ -29,12 +29,14 @@ public class PolicyRemovedEvent extends InvalidationEvent implements Authorizati
private String id;
private String name;
private Set<String> resources;
private String serverId;
public static PolicyRemovedEvent create(String id, String name, String serverId) {
public static PolicyRemovedEvent create(String id, String name, Set<String> resources, String serverId) {
PolicyRemovedEvent event = new PolicyRemovedEvent();
event.id = id;
event.name = name;
event.resources = resources;
event.serverId = serverId;
return event;
}
@ -51,6 +53,6 @@ public class PolicyRemovedEvent extends InvalidationEvent implements Authorizati
@Override
public void addInvalidations(StoreFactoryCacheManager cache, Set<String> invalidations) {
cache.policyRemoval(id, name, serverId, invalidations);
cache.policyRemoval(id, name, resources, serverId, invalidations);
}
}

View file

@ -29,12 +29,14 @@ public class PolicyUpdatedEvent extends InvalidationEvent implements Authorizati
private String id;
private String name;
private static Set<String> resources;
private String serverId;
public static PolicyUpdatedEvent create(String id, String name, String serverId) {
public static PolicyUpdatedEvent create(String id, String name, Set<String> resources, String serverId) {
PolicyUpdatedEvent event = new PolicyUpdatedEvent();
event.id = id;
event.name = name;
event.resources = resources;
event.serverId = serverId;
return event;
}
@ -51,6 +53,6 @@ public class PolicyUpdatedEvent extends InvalidationEvent implements Authorizati
@Override
public void addInvalidations(StoreFactoryCacheManager cache, Set<String> invalidations) {
cache.policyUpdated(id, name, serverId, invalidations);
cache.policyUpdated(id, name, resources, serverId, invalidations);
}
}

View file

@ -29,12 +29,20 @@ public class ResourceRemovedEvent extends InvalidationEvent implements Authoriza
private String id;
private String name;
private String owner;
private String serverId;
private String type;
private String uri;
private Set<String> scopes;
public static ResourceRemovedEvent create(String id, String name, String serverId) {
public static ResourceRemovedEvent create(String id, String name, String type, String uri, String owner, Set<String> scopes, String serverId) {
ResourceRemovedEvent event = new ResourceRemovedEvent();
event.id = id;
event.name = name;
event.type = type;
event.uri = uri;
event.owner = owner;
event.scopes = scopes;
event.serverId = serverId;
return event;
}
@ -51,6 +59,6 @@ public class ResourceRemovedEvent extends InvalidationEvent implements Authoriza
@Override
public void addInvalidations(StoreFactoryCacheManager cache, Set<String> invalidations) {
cache.resourceRemoval(id, name, serverId, invalidations);
cache.resourceRemoval(id, name, type, uri, owner, scopes, serverId, invalidations);
}
}

View file

@ -30,11 +30,17 @@ public class ResourceUpdatedEvent extends InvalidationEvent implements Authoriza
private String id;
private String name;
private String serverId;
private String type;
private String uri;
private Set<String> scopes;
public static ResourceUpdatedEvent create(String id, String name, String serverId) {
public static ResourceUpdatedEvent create(String id, String name, String type, String uri, Set<String> scopes, String serverId) {
ResourceUpdatedEvent event = new ResourceUpdatedEvent();
event.id = id;
event.name = name;
event.type = type;
event.uri = uri;
event.scopes = scopes;
event.serverId = serverId;
return event;
}
@ -51,6 +57,6 @@ public class ResourceUpdatedEvent extends InvalidationEvent implements Authoriza
@Override
public void addInvalidations(StoreFactoryCacheManager cache, Set<String> invalidations) {
cache.resourceUpdated(id, name, serverId, invalidations);
cache.resourceUpdated(id, name, type, uri, scopes, serverId, invalidations);
}
}

View file

@ -0,0 +1,50 @@
/*
* Copyright 2017 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.authorization.stream;
import java.io.Serializable;
import java.util.Map;
import java.util.function.Predicate;
import org.keycloak.models.cache.infinispan.authorization.entities.InResource;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class InResourcePredicate implements Predicate<Map.Entry<String, Revisioned>>, Serializable {
private String resourceId;
public static InResourcePredicate create() {
return new InResourcePredicate();
}
public InResourcePredicate resource(String id) {
resourceId = id;
return this;
}
@Override
public boolean test(Map.Entry<String, Revisioned> entry) {
Object value = entry.getValue();
if (value == null) return false;
if (!(value instanceof InResource)) return false;
return resourceId.equals(((InResource)value).getResourceId());
}
}

View file

@ -0,0 +1,35 @@
package org.keycloak.models.cache.infinispan.authorization.stream;
import java.io.Serializable;
import java.util.Map;
import java.util.function.Predicate;
import org.keycloak.models.cache.infinispan.authorization.entities.InResourceServer;
import org.keycloak.models.cache.infinispan.authorization.entities.InScope;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class InScopePredicate implements Predicate<Map.Entry<String, Revisioned>>, Serializable {
private String scopeId;
public static InScopePredicate create() {
return new InScopePredicate();
}
public InScopePredicate scope(String id) {
scopeId = id;
return this;
}
@Override
public boolean test(Map.Entry<String, Revisioned> entry) {
Object value = entry.getValue();
if (value == null) return false;
if (!(value instanceof InScope)) return false;
return scopeId.equals(((InScope)value).getScopeId());
}
}

View file

@ -29,18 +29,20 @@ import org.keycloak.models.jpa.entities.ClientEntity;
import org.keycloak.models.jpa.entities.ClientTemplateEntity;
import org.keycloak.models.jpa.entities.ProtocolMapperEntity;
import org.keycloak.models.jpa.entities.RoleEntity;
import org.keycloak.models.jpa.entities.ScopeMappingEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -222,7 +224,7 @@ public class ClientAdapter implements ClientModel, JpaModel<ClientEntity> {
}
@Override
public Set<RoleModel> getRealmScopeMappings() {
public Set<RoleModel> getRealmScopeMappings() {
Set<RoleModel> roleMappings = getScopeMappings();
Set<RoleModel> appRoles = new HashSet<>();
@ -240,47 +242,22 @@ public class ClientAdapter implements ClientModel, JpaModel<ClientEntity> {
@Override
public Set<RoleModel> getScopeMappings() {
TypedQuery<String> query = em.createNamedQuery("clientScopeMappingIds", String.class);
query.setParameter("client", getEntity());
List<String> ids = query.getResultList();
Set<RoleModel> roles = new HashSet<RoleModel>();
for (String roleId : ids) {
RoleModel role = realm.getRoleById(roleId);
if (role == null) continue;
roles.add(role);
}
return roles;
return getEntity().getScopeMapping().stream()
.map(RoleEntity::getId)
.map(realm::getRoleById)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
}
@Override
public void addScopeMapping(RoleModel role) {
Set<RoleModel> roles = getScopeMappings();
if (roles.contains(role)) return;
ScopeMappingEntity entity = new ScopeMappingEntity();
entity.setClient(getEntity());
RoleEntity roleEntity = RoleAdapter.toRoleEntity(role, em);
entity.setRole(roleEntity);
em.persist(entity);
em.flush();
em.detach(entity);
getEntity().getScopeMapping().add(roleEntity);
}
@Override
public void deleteScopeMapping(RoleModel role) {
TypedQuery<ScopeMappingEntity> query = getRealmScopeMappingQuery(role);
List<ScopeMappingEntity> results = query.getResultList();
if (results.size() == 0) return;
for (ScopeMappingEntity entity : results) {
em.remove(entity);
}
}
protected TypedQuery<ScopeMappingEntity> getRealmScopeMappingQuery(RoleModel role) {
TypedQuery<ScopeMappingEntity> query = em.createNamedQuery("hasScope", ScopeMappingEntity.class);
query.setParameter("client", getEntity());
RoleEntity roleEntity = RoleAdapter.toRoleEntity(role, em);
query.setParameter("role", roleEntity);
return query;
getEntity().getScopeMapping().remove(RoleAdapter.toRoleEntity(role, em));
}
@Override
@ -689,7 +666,6 @@ public class ClientAdapter implements ClientModel, JpaModel<ClientEntity> {
}
RoleEntity roleEntity = RoleAdapter.toRoleEntity(role, em);
entities.add(roleEntity);
em.flush();
}
@Override

View file

@ -43,6 +43,7 @@ import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -133,14 +134,6 @@ public class JpaRealmProvider implements RealmProvider {
int num = em.createNamedQuery("deleteGroupRoleMappingsByRealm")
.setParameter("realm", realm).executeUpdate();
num = em.createNamedQuery("deleteGroupAttributesByRealm")
.setParameter("realm", realm).executeUpdate();
num = em.createNamedQuery("deleteGroupsByRealm")
.setParameter("realm", realm).executeUpdate();
num = em.createNamedQuery("deleteComponentConfigByRealm")
.setParameter("realm", realm).executeUpdate();
num = em.createNamedQuery("deleteComponentByRealm")
.setParameter("realm", realm).executeUpdate();
TypedQuery<String> query = em.createNamedQuery("getClientIdsByRealm", String.class);
query.setParameter("realm", realm.getId());
@ -228,7 +221,6 @@ public class JpaRealmProvider implements RealmProvider {
roleEntity.setClientRole(true);
roleEntity.setRealmId(realm.getId());
em.persist(roleEntity);
em.flush();
RoleAdapter adapter = new RoleAdapter(session, realm, em, roleEntity);
return adapter;
}
@ -281,10 +273,11 @@ public class JpaRealmProvider implements RealmProvider {
RoleEntity roleEntity = em.getReference(RoleEntity.class, role.getId());
String compositeRoleTable = JpaUtils.getTableNameForNativeQuery("COMPOSITE_ROLE", em);
em.createNativeQuery("delete from " + compositeRoleTable + " where CHILD_ROLE = :role").setParameter("role", roleEntity).executeUpdate();
em.createNamedQuery("deleteScopeMappingByRole").setParameter("role", roleEntity).executeUpdate();
realm.getClients().forEach(c -> c.deleteScopeMapping(role));
em.createNamedQuery("deleteTemplateScopeMappingByRole").setParameter("role", roleEntity).executeUpdate();
int val = em.createNamedQuery("deleteGroupRoleMappingsByRole").setParameter("roleId", roleEntity.getId()).executeUpdate();
em.flush();
em.remove(roleEntity);
session.getKeycloakSessionFactory().publish(new RoleContainerModel.RoleRemovedEvent() {
@ -337,27 +330,23 @@ public class JpaRealmProvider implements RealmProvider {
@Override
public List<GroupModel> getGroups(RealmModel realm) {
List<String> groups = em.createNamedQuery("getAllGroupIdsByRealm", String.class)
.setParameter("realm", realm.getId()).getResultList();
if (groups == null) return Collections.EMPTY_LIST;
List<GroupModel> list = new LinkedList<>();
for (String id : groups) {
list.add(session.realms().getGroupById(id, realm));
}
return Collections.unmodifiableList(list);
RealmEntity ref = em.getReference(RealmEntity.class, realm.getId());
return ref.getGroups().stream()
.map(g -> session.realms().getGroupById(g.getId(), realm))
.collect(Collectors.collectingAndThen(
Collectors.toList(), Collections::unmodifiableList));
}
@Override
public List<GroupModel> getTopLevelGroups(RealmModel realm) {
List<String> groups = em.createNamedQuery("getTopLevelGroupIds", String.class)
.setParameter("realm", realm.getId())
.getResultList();
if (groups == null) return Collections.EMPTY_LIST;
List<GroupModel> list = new LinkedList<>();
for (String id : groups) {
list.add(session.realms().getGroupById(id, realm));
}
return Collections.unmodifiableList(list);
RealmEntity ref = em.getReference(RealmEntity.class, realm.getId());
return ref.getGroups().stream()
.filter(g -> g.getParent() == null)
.map(g -> session.realms().getGroupById(g.getId(), realm))
.collect(Collectors.collectingAndThen(
Collectors.toList(), Collections::unmodifiableList));
}
@Override
@ -377,9 +366,11 @@ public class JpaRealmProvider implements RealmProvider {
if (!groupEntity.getRealm().getId().equals(realm.getId())) {
return false;
}
// I don't think we need this as GroupEntity has cascade removal. It causes batch errors if you turn this on.
// em.createNamedQuery("deleteGroupAttributesByGroup").setParameter("group", groupEntity).executeUpdate();
em.createNamedQuery("deleteGroupRoleMappingsByGroup").setParameter("group", groupEntity).executeUpdate();
RealmEntity realmEntity = em.getReference(RealmEntity.class, realm.getId());
realmEntity.getGroups().remove(groupEntity);
em.remove(groupEntity);
return true;
@ -401,6 +392,7 @@ public class JpaRealmProvider implements RealmProvider {
RealmEntity realmEntity = em.getReference(RealmEntity.class, realm.getId());
groupEntity.setRealm(realmEntity);
em.persist(groupEntity);
realmEntity.getGroups().add(groupEntity);
GroupAdapter adapter = new GroupAdapter(realm, em, groupEntity);
return adapter;
@ -494,9 +486,6 @@ public class JpaRealmProvider implements RealmProvider {
ClientEntity clientEntity = ((ClientAdapter)client).getEntity();
em.createNamedQuery("deleteScopeMappingByClient").setParameter("client", clientEntity).executeUpdate();
em.flush();
session.getKeycloakSessionFactory().publish(new RealmModel.ClientRemovedEvent() {
@Override
public ClientModel getClient() {

View file

@ -58,7 +58,6 @@ import org.keycloak.models.utils.ComponentUtil;
import org.keycloak.models.utils.KeycloakModelUtils;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@ -68,7 +67,10 @@ import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -1375,16 +1377,10 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
@Override
public List<AuthenticationFlowModel> getAuthenticationFlows() {
TypedQuery<AuthenticationFlowEntity> query = em.createNamedQuery("getAuthenticationFlowsByRealm", AuthenticationFlowEntity.class);
query.setParameter("realm", realm);
List<AuthenticationFlowEntity> flows = query.getResultList();
if (flows.isEmpty()) return Collections.EMPTY_LIST;
List<AuthenticationFlowModel> models = new LinkedList<>();
for (AuthenticationFlowEntity entity : flows) {
AuthenticationFlowModel model = entityToModel(entity);
models.add(model);
}
return Collections.unmodifiableList(models);
return realm.getAuthenticationFlows().stream()
.map(this::entityToModel)
.collect(Collectors.collectingAndThen(
Collectors.toList(), Collections::unmodifiableList));
}
@Override
@ -1461,26 +1457,20 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
entity.setRealm(realm);
realm.getAuthenticationFlows().add(entity);
em.persist(entity);
em.flush();
model.setId(entity.getId());
return model;
}
@Override
public List<AuthenticationExecutionModel> getAuthenticationExecutions(String flowId) {
TypedQuery<AuthenticationExecutionEntity> query = em.createNamedQuery("getAuthenticationExecutionsByFlow", AuthenticationExecutionEntity.class);
AuthenticationFlowEntity flow = em.getReference(AuthenticationFlowEntity.class, flowId);
query.setParameter("realm", realm);
query.setParameter("parentFlow", flow);
List<AuthenticationExecutionEntity> queryResult = query.getResultList();
if (queryResult.isEmpty()) return Collections.EMPTY_LIST;
List<AuthenticationExecutionModel> executions = new LinkedList<>();
for (AuthenticationExecutionEntity entity : queryResult) {
AuthenticationExecutionModel model = entityToModel(entity);
executions.add(model);
}
Collections.sort(executions, AuthenticationExecutionModel.ExecutionComparator.SINGLETON);
return Collections.unmodifiableList(executions);
return flow.getExecutions().stream()
.filter(e -> getId().equals(e.getRealm().getId()))
.map(this::entityToModel)
.sorted(AuthenticationExecutionModel.ExecutionComparator.SINGLETON)
.collect(Collectors.collectingAndThen(
Collectors.toList(), Collections::unmodifiableList));
}
public AuthenticationExecutionModel entityToModel(AuthenticationExecutionEntity entity) {
@ -1519,7 +1509,6 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
entity.setRealm(realm);
entity.setAutheticatorFlow(model.isAuthenticatorFlow());
em.persist(entity);
em.flush();
model.setId(entity.getId());
return model;
@ -1557,7 +1546,6 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
auth.setConfig(model.getConfig());
realm.getAuthenticatorConfigs().add(auth);
em.persist(auth);
em.flush();
model.setId(auth.getId());
return model;
}
@ -1850,12 +1838,14 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
c.setSubType(model.getSubType());
c.setRealm(realm);
em.persist(c);
realm.getComponents().add(c);
setConfig(model, c);
model.setId(c.getId());
return model;
}
protected void setConfig(ComponentModel model, ComponentEntity c) {
c.getComponentConfigs().clear();
for (String key : model.getConfig().keySet()) {
List<String> vals = model.getConfig().get(key);
if (vals == null) {
@ -1867,7 +1857,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
config.setName(key);
config.setValue(val);
config.setComponent(c);
em.persist(config);
c.getComponentConfigs().add(config);
}
}
}
@ -1884,8 +1874,6 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
c.setProviderType(component.getProviderType());
c.setParentId(component.getParentId());
c.setSubType(component.getSubType());
em.createNamedQuery("deleteComponentConfigByComponent").setParameter("component", c).executeUpdate();
em.flush();
setConfig(component, c);
ComponentUtil.notifyUpdated(session, this, old, component);
@ -1898,55 +1886,39 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
if (c == null) return;
session.users().preRemove(this, component);
removeComponents(component.getId());
em.createNamedQuery("deleteComponentConfigByComponent").setParameter("component", c).executeUpdate();
em.remove(c);
getEntity().getComponents().remove(c);
}
@Override
public void removeComponents(String parentId) {
TypedQuery<String> query = em.createNamedQuery("getComponentIdsByParent", String.class)
.setParameter("realm", realm)
.setParameter("parentId", parentId);
List<String> results = query.getResultList();
if (results.isEmpty()) return;
for (String id : results) {
session.users().preRemove(this, getComponent(id));
}
em.createNamedQuery("deleteComponentConfigByParent").setParameter("parentId", parentId).executeUpdate();
em.createNamedQuery("deleteComponentByParent").setParameter("parentId", parentId).executeUpdate();
Predicate<ComponentEntity> sameParent = c -> Objects.equals(parentId, c.getParentId());
getEntity().getComponents().stream()
.filter(sameParent)
.map(this::entityToModel)
.forEach(c -> session.users().preRemove(this, c));
getEntity().getComponents().removeIf(sameParent);
}
@Override
public List<ComponentModel> getComponents(String parentId, String providerType) {
public List<ComponentModel> getComponents(String parentId, final String providerType) {
if (parentId == null) parentId = getId();
TypedQuery<ComponentEntity> query = em.createNamedQuery("getComponentsByParentAndType", ComponentEntity.class)
.setParameter("realm", realm)
.setParameter("parentId", parentId)
.setParameter("providerType", providerType);
List<ComponentEntity> results = query.getResultList();
List<ComponentModel> rtn = new LinkedList<>();
for (ComponentEntity c : results) {
ComponentModel model = entityToModel(c);
rtn.add(model);
final String parent = parentId;
}
return rtn;
return realm.getComponents().stream()
.filter(c -> parent.equals(c.getParentId())
&& providerType.equals(c.getProviderType()))
.map(this::entityToModel)
.collect(Collectors.toList());
}
@Override
public List<ComponentModel> getComponents(String parentId) {
TypedQuery<ComponentEntity> query = em.createNamedQuery("getComponentsByParent", ComponentEntity.class)
.setParameter("realm", realm)
.setParameter("parentId", parentId);
List<ComponentEntity> results = query.getResultList();
List<ComponentModel> rtn = new LinkedList<>();
for (ComponentEntity c : results) {
ComponentModel model = entityToModel(c);
rtn.add(model);
}
return rtn;
public List<ComponentModel> getComponents(final String parentId) {
return realm.getComponents().stream()
.filter(c -> parentId.equals(c.getParentId()))
.map(this::entityToModel)
.collect(Collectors.toList());
}
protected ComponentModel entityToModel(ComponentEntity c) {
@ -1958,10 +1930,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
model.setSubType(c.getSubType());
model.setParentId(c.getParentId());
MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
TypedQuery<ComponentConfigEntity> configQuery = em.createNamedQuery("getComponentConfig", ComponentConfigEntity.class)
.setParameter("component", c);
List<ComponentConfigEntity> configResults = configQuery.getResultList();
for (ComponentConfigEntity configEntity : configResults) {
for (ComponentConfigEntity configEntity : c.getComponentConfigs()) {
config.add(configEntity.getName(), configEntity.getValue());
}
model.setConfig(config);
@ -1970,16 +1939,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
@Override
public List<ComponentModel> getComponents() {
TypedQuery<ComponentEntity> query = em.createNamedQuery("getComponents", ComponentEntity.class)
.setParameter("realm", realm);
List<ComponentEntity> results = query.getResultList();
List<ComponentModel> rtn = new LinkedList<>();
for (ComponentEntity c : results) {
ComponentModel model = entityToModel(c);
rtn.add(model);
}
return rtn;
return realm.getComponents().stream().map(this::entityToModel).collect(Collectors.toList());
}
@Override

View file

@ -101,7 +101,6 @@ public class RoleAdapter implements RoleModel, JpaModel<RoleEntity> {
if (composite.equals(entity)) return;
}
getEntity().getCompositeRoles().add(entity);
em.flush();
}
@Override

View file

@ -37,11 +37,6 @@ import javax.persistence.Table;
*/
@Table(name="AUTHENTICATION_EXECUTION")
@Entity
@NamedQueries({
@NamedQuery(name="getAuthenticationExecutionsByFlow", query="select authenticator from AuthenticationExecutionEntity authenticator where authenticator.realm = :realm and authenticator.parentFlow = :parentFlow"),
@NamedQuery(name="deleteAuthenticationExecutionsByRealm", query="delete from AuthenticationExecutionEntity authenticator where authenticator.realm = :realm"),
@NamedQuery(name="deleteAuthenticationExecutionsByRealmAndFlow", query="delete from AuthenticationExecutionEntity authenticator where authenticator.realm = :realm and authenticator.parentFlow = :parentFlow"),
})
public class AuthenticationExecutionEntity {
@Id
@Column(name="ID", length = 36)

View file

@ -39,10 +39,6 @@ import java.util.Collection;
*/
@Table(name="AUTHENTICATION_FLOW")
@Entity
@NamedQueries({
@NamedQuery(name="getAuthenticationFlowsByRealm", query="select flow from AuthenticationFlowEntity flow where flow.realm = :realm"),
@NamedQuery(name="deleteAuthenticationFlowByRealm", query="delete from AuthenticationFlowEntity flow where flow.realm = :realm")
})
public class AuthenticationFlowEntity {
@Id
@Column(name="ID", length = 36)

View file

@ -162,6 +162,10 @@ public class ClientEntity {
@JoinTable(name="CLIENT_DEFAULT_ROLES", joinColumns = { @JoinColumn(name="CLIENT_ID")}, inverseJoinColumns = { @JoinColumn(name="ROLE_ID")})
Collection<RoleEntity> defaultRoles = new ArrayList<RoleEntity>();
@OneToMany(fetch = FetchType.LAZY)
@JoinTable(name="SCOPE_MAPPING", joinColumns = { @JoinColumn(name="CLIENT_ID")}, inverseJoinColumns = { @JoinColumn(name="ROLE_ID")})
protected Set<RoleEntity> scopeMapping = new HashSet<>();
@ElementCollection
@MapKeyColumn(name="NAME")
@Column(name="VALUE")
@ -456,6 +460,14 @@ public class ClientEntity {
this.useTemplateMappers = useTemplateMappers;
}
public Set<RoleEntity> getScopeMapping() {
return scopeMapping;
}
public void setScopeMapping(Set<RoleEntity> scopeMapping) {
this.scopeMapping = scopeMapping;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View file

@ -33,12 +33,6 @@ import javax.persistence.Table;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@NamedQueries({
@NamedQuery(name="getComponentConfig", query="select attr from ComponentConfigEntity attr where attr.component = :component"),
@NamedQuery(name="deleteComponentConfigByComponent", query="delete from ComponentConfigEntity attr where attr.component = :component"),
@NamedQuery(name="deleteComponentConfigByRealm", query="delete from ComponentConfigEntity attr where attr.component IN (select u from ComponentEntity u where u.realm=:realm)"),
@NamedQuery(name="deleteComponentConfigByParent", query="delete from ComponentConfigEntity attr where attr.component IN (select u from ComponentEntity u where u.parentId=:parentId)"),
})
@Table(name="COMPONENT_CONFIG")
@Entity
public class ComponentConfigEntity {

View file

@ -17,8 +17,12 @@
package org.keycloak.models.jpa.entities;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
@ -27,19 +31,12 @@ import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import javax.persistence.Table;
/**
* @author <a href="mailto:bburke@redhat.com">Bill Burke</a>
*/
@NamedQueries({
@NamedQuery(name="getComponents", query="select attr from ComponentEntity attr where attr.realm = :realm"),
@NamedQuery(name="getComponentsByParentAndType", query="select attr from ComponentEntity attr where attr.realm = :realm and attr.providerType = :providerType and attr.parentId = :parentId"),
@NamedQuery(name="getComponentByParent", query="select attr from ComponentEntity attr where attr.realm = :realm and attr.parentId = :parentId"),
@NamedQuery(name="getComponentIdsByParent", query="select attr.id from ComponentEntity attr where attr.realm = :realm and attr.parentId = :parentId"),
@NamedQuery(name="deleteComponentByRealm", query="delete from ComponentEntity c where c.realm = :realm"),
@NamedQuery(name="deleteComponentByParent", query="delete from ComponentEntity c where c.parentId = :parentId")
})
@Entity
@Table(name="COMPONENT")
public class ComponentEntity {
@ -68,6 +65,9 @@ public class ComponentEntity {
@Column(name="SUB_TYPE")
protected String subType;
@OneToMany(fetch = FetchType.LAZY, cascade ={ CascadeType.ALL}, orphanRemoval = true, mappedBy = "component")
Set<ComponentConfigEntity> componentConfigs = new HashSet<>();
public String getId() {
return id;
}
@ -124,6 +124,14 @@ public class ComponentEntity {
this.realm = realm;
}
public Set<ComponentConfigEntity> getComponentConfigs() {
return componentConfigs;
}
public void setComponentConfigs(Set<ComponentConfigEntity> componentConfigs) {
this.componentConfigs = componentConfigs;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View file

@ -35,8 +35,6 @@ import javax.persistence.Table;
*/
@NamedQueries({
@NamedQuery(name="getGroupAttributesByNameAndValue", query="select attr from GroupAttributeEntity attr where attr.name = :name and attr.value = :value"),
@NamedQuery(name="deleteGroupAttributesByGroup", query="delete from GroupAttributeEntity attr where attr.group = :group"),
@NamedQuery(name="deleteGroupAttributesByRealm", query="delete from GroupAttributeEntity attr where attr.group IN (select u from GroupEntity u where u.realm=:realm)")
})
@Table(name="GROUP_ATTRIBUTE")
@Entity

View file

@ -38,13 +38,7 @@ import java.util.Collection;
* @version $Revision: 1 $
*/
@NamedQueries({
@NamedQuery(name="getAllGroupsByRealm", query="select u from GroupEntity u where u.realm = :realm order by u.name"),
@NamedQuery(name="getAllGroupIdsByRealm", query="select u.id from GroupEntity u where u.realm.id = :realm order by u.name"),
@NamedQuery(name="getGroupById", query="select u from GroupEntity u where u.id = :id and u.realm = :realm"),
@NamedQuery(name="getGroupIdsByParent", query="select u.id from GroupEntity u where u.parent = :parent"),
@NamedQuery(name="getTopLevelGroupIds", query="select u.id from GroupEntity u where u.parent is null and u.realm.id = :realm"),
@NamedQuery(name="getGroupCount", query="select count(u) from GroupEntity u where u.realm = :realm"),
@NamedQuery(name="deleteGroupsByRealm", query="delete from GroupEntity u where u.realm = :realm")
})
@Entity
@Table(name="KEYCLOAK_GROUP")

View file

@ -159,6 +159,9 @@ public class RealmEntity {
@JoinTable(name="REALM_DEFAULT_GROUPS", joinColumns = { @JoinColumn(name="REALM_ID")}, inverseJoinColumns = { @JoinColumn(name="GROUP_ID")})
protected Collection<GroupEntity> defaultGroups = new ArrayList<>();
@OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm")
protected Collection<GroupEntity> groups = new ArrayList<>();
@Column(name="EVENTS_ENABLED")
protected boolean eventsEnabled;
@Column(name="EVENTS_EXPIRATION")
@ -199,6 +202,9 @@ public class RealmEntity {
@OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm")
Collection<AuthenticationFlowEntity> authenticationFlows = new ArrayList<>();
@OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.ALL}, orphanRemoval = true, mappedBy = "realm")
Set<ComponentEntity> components = new HashSet<>();
@Column(name="BROWSER_FLOW")
protected String browserFlow;
@ -426,6 +432,14 @@ public class RealmEntity {
this.defaultGroups = defaultGroups;
}
public Collection<GroupEntity> getGroups() {
return groups;
}
public void setGroups(Collection<GroupEntity> groups) {
this.groups = groups;
}
public String getPasswordPolicy() {
return passwordPolicy;
}
@ -623,6 +637,14 @@ public class RealmEntity {
this.authenticationFlows = authenticationFlows;
}
public Set<ComponentEntity> getComponents() {
return components;
}
public void setComponents(Set<ComponentEntity> components) {
this.components = components;
}
public String getOtpPolicyType() {
return otpPolicyType;
}

View file

@ -1,138 +0,0 @@
/*
* 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.jpa.entities;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
import java.io.Serializable;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@NamedQueries({
@NamedQuery(name="hasScope", query="select m from ScopeMappingEntity m where m.client = :client and m.role = :role"),
@NamedQuery(name="clientScopeMappings", query="select m from ScopeMappingEntity m where m.client = :client"),
@NamedQuery(name="clientScopeMappingIds", query="select m.role.id from ScopeMappingEntity m where m.client = :client"),
@NamedQuery(name="deleteScopeMappingByRole", query="delete from ScopeMappingEntity where role = :role"),
@NamedQuery(name="deleteScopeMappingByClient", query="delete from ScopeMappingEntity where client = :client")
})
@Table(name="SCOPE_MAPPING")
@Entity
@IdClass(ScopeMappingEntity.Key.class)
public class ScopeMappingEntity {
@Id
@ManyToOne(fetch= FetchType.LAZY)
@JoinColumn(name = "CLIENT_ID")
protected ClientEntity client;
@Id
@ManyToOne(fetch= FetchType.LAZY)
@JoinColumn(name="ROLE_ID")
protected RoleEntity role;
public ClientEntity getClient() {
return client;
}
public void setClient(ClientEntity client) {
this.client = client;
}
public RoleEntity getRole() {
return role;
}
public void setRole(RoleEntity role) {
this.role = role;
}
public static class Key implements Serializable {
protected ClientEntity client;
protected RoleEntity role;
public Key() {
}
public Key(ClientEntity client, RoleEntity role) {
this.client = client;
this.role = role;
}
public ClientEntity getClient() {
return client;
}
public RoleEntity getRole() {
return role;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Key key = (Key) o;
if (client != null ? !client.getId().equals(key.client != null ? key.client.getId() : null) : key.client != null) return false;
if (role != null ? !role.getId().equals(key.role != null ? key.role.getId() : null) : key.role != null) return false;
return true;
}
@Override
public int hashCode() {
int result = client != null ? client.getId().hashCode() : 0;
result = 31 * result + (role != null ? role.getId().hashCode() : 0);
return result;
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
if (!(o instanceof ScopeMappingEntity)) return false;
ScopeMappingEntity key = (ScopeMappingEntity) o;
if (client != null ? !client.getId().equals(key.client != null ? key.client.getId() : null) : key.client != null) return false;
if (role != null ? !role.getId().equals(key.role != null ? key.role.getId() : null) : key.role != null) return false;
return true;
}
@Override
public int hashCode() {
int result = client != null ? client.getId().hashCode() : 0;
result = 31 * result + (role != null ? role.getId().hashCode() : 0);
return result;
}
}

View file

@ -24,4 +24,139 @@
<addPrimaryKey columnNames="USER_SESSION_ID,CLIENT_ID, OFFLINE_FLAG" constraintName="CONSTRAINT_OFFL_CL_SES_PK3" tableName="OFFLINE_CLIENT_SESSION"/>
</changeSet>
</databaseChangeLog>
<changeSet author="glavoie@gmail.com" id="3.2.0.idx">
<createIndex indexName="IDX_ASSOC_POL_ASSOC_POL_ID" tableName="ASSOCIATED_POLICY">
<column name="ASSOCIATED_POLICY_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_AUTH_EXEC_REALM_FLOW" tableName="AUTHENTICATION_EXECUTION">
<column name="REALM_ID" type="VARCHAR(36)"/>
<column name="FLOW_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_AUTH_EXEC_FLOW" tableName="AUTHENTICATION_EXECUTION">
<column name="FLOW_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_AUTH_FLOW_REALM" tableName="AUTHENTICATION_FLOW">
<column name="REALM_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_AUTH_CONFIG_REALM" tableName="AUTHENTICATOR_CONFIG">
<column name="REALM_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_CLIENT_CLIENT_TEMPL_ID" tableName="CLIENT">
<column name="CLIENT_TEMPLATE_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_CLIENT_DEF_ROLES_CLIENT" tableName="CLIENT_DEFAULT_ROLES">
<column name="CLIENT_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_CLIENT_ID_PROV_MAP_CLIENT" tableName="CLIENT_IDENTITY_PROV_MAPPING">
<column name="CLIENT_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_CLIENT_SESSION_SESSION" tableName="CLIENT_SESSION">
<column name="SESSION_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_COMPONENT_REALM" tableName="COMPONENT">
<column name="REALM_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_COMPO_CONFIG_COMPO" tableName="COMPONENT_CONFIG">
<column name="COMPONENT_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_COMPOSITE" tableName="COMPOSITE_ROLE">
<column name="COMPOSITE" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_COMPOSITE_CHILD" tableName="COMPOSITE_ROLE">
<column name="CHILD_ROLE" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_CREDENTIAL_ATTR_CRED" tableName="CREDENTIAL_ATTRIBUTE">
<column name="CREDENTIAL_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_FED_CRED_ATTR_CRED" tableName="FED_CREDENTIAL_ATTRIBUTE">
<column name="CREDENTIAL_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_GROUP_ATTR_GROUP" tableName="GROUP_ATTRIBUTE">
<column name="GROUP_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_GROUP_ROLE_MAPP_GROUP" tableName="GROUP_ROLE_MAPPING">
<column name="GROUP_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_IDENT_PROV_REALM" tableName="IDENTITY_PROVIDER">
<column name="REALM_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_ID_PROV_MAPP_REALM" tableName="IDENTITY_PROVIDER_MAPPER">
<column name="REALM_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_KEYCLOAK_ROLE_CLIENT" tableName="KEYCLOAK_ROLE">
<column name="CLIENT" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_KEYCLOAK_ROLE_REALM" tableName="KEYCLOAK_ROLE">
<column name="REALM" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_PROTOCOL_MAPPER_CLIENT" tableName="PROTOCOL_MAPPER">
<column name="CLIENT_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_PROTO_MAPP_CLIENT_TEMPL" tableName="PROTOCOL_MAPPER">
<column name="CLIENT_TEMPLATE_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_REALM_MASTER_ADM_CLI" tableName="REALM">
<column name="MASTER_ADMIN_CLIENT" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_REALM_ATTR_REALM" tableName="REALM_ATTRIBUTE">
<column name="REALM_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_REALM_DEF_GRP_REALM" tableName="REALM_DEFAULT_GROUPS">
<column name="REALM_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_REALM_DEF_ROLES_REALM" tableName="REALM_DEFAULT_ROLES">
<column name="REALM_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_REALM_EVT_TYPES_REALM" tableName="REALM_ENABLED_EVENT_TYPES">
<column name="REALM_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_REALM_EVT_LIST_REALM" tableName="REALM_EVENTS_LISTENERS">
<column name="REALM_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_REALM_SUPP_LOCAL_REALM" tableName="REALM_SUPPORTED_LOCALES">
<column name="REALM_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_REDIR_URI_CLIENT" tableName="REDIRECT_URIS">
<column name="CLIENT_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_REQ_ACT_PROV_REALM" tableName="REQUIRED_ACTION_PROVIDER">
<column name="REALM_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_RES_POLICY_POLICY" tableName="RESOURCE_POLICY">
<column name="POLICY_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_RES_SCOPE_SCOPE" tableName="RESOURCE_SCOPE">
<column name="SCOPE_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_RES_SERV_POL_RES_SERV" tableName="RESOURCE_SERVER_POLICY">
<column name="RESOURCE_SERVER_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_RES_SRV_RES_RES_SRV" tableName="RESOURCE_SERVER_RESOURCE">
<column name="RESOURCE_SERVER_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_RES_SRV_SCOPE_RES_SRV" tableName="RESOURCE_SERVER_SCOPE">
<column name="RESOURCE_SERVER_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_SCOPE_MAPPING_ROLE" tableName="SCOPE_MAPPING">
<column name="ROLE_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_SCOPE_POLICY_POLICY" tableName="SCOPE_POLICY">
<column name="POLICY_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_TEMPL_SCOPE_MAPP_ROLE" tableName="TEMPLATE_SCOPE_MAPPING">
<column name="ROLE_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_USR_FED_MAP_FED_PRV" tableName="USER_FEDERATION_MAPPER">
<column name="FEDERATION_PROVIDER_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_USR_FED_MAP_REALM" tableName="USER_FEDERATION_MAPPER">
<column name="REALM_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_USR_FED_PRV_REALM" tableName="USER_FEDERATION_PROVIDER">
<column name="REALM_ID" type="VARCHAR(36)"/>
</createIndex>
<createIndex indexName="IDX_WEB_ORIG_CLIENT" tableName="WEB_ORIGINS">
<column name="CLIENT_ID" type="VARCHAR(36)"/>
</createIndex>
</changeSet>
</databaseChangeLog>

View file

@ -37,7 +37,6 @@
<class>org.keycloak.models.jpa.entities.UserRequiredActionEntity</class>
<class>org.keycloak.models.jpa.entities.UserAttributeEntity</class>
<class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
<class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>
<class>org.keycloak.models.jpa.entities.IdentityProviderEntity</class>
<class>org.keycloak.models.jpa.entities.IdentityProviderMapperEntity</class>
<class>org.keycloak.models.jpa.entities.ClientIdentityProviderMappingEntity</class>

View file

@ -64,7 +64,7 @@
<h2.version>1.3.173</h2.version>
<hibernate.entitymanager.version>5.0.7.Final</hibernate.entitymanager.version>
<hibernate.javax.persistence.version>1.0.0.Final</hibernate.javax.persistence.version>
<infinispan.version>8.1.0.Final</infinispan.version>
<infinispan.version>8.2.6.Final</infinispan.version>
<jackson.version>2.5.4</jackson.version>
<javax.mail.version>1.5.5</javax.mail.version>
<jboss.logging.version>3.3.0.Final</jboss.logging.version>
@ -79,7 +79,7 @@
<sun.istack.version>2.21</sun.istack.version>
<sun.jaxb.version>2.2.11</sun.jaxb.version>
<sun.xsom.version>20140925</sun.xsom.version>
<undertow.version>1.3.15.Final</undertow.version>
<undertow.version>1.4.11.Final</undertow.version>
<xmlsec.version>2.0.5</xmlsec.version>
<!-- Authorization Drools Policy Provider -->

View file

@ -146,7 +146,7 @@ public abstract class AbstractParser implements ParserNamespaceSupport {
}
private boolean valid(String str) {
return str != null && str.length() > 0;
return str != null && ! str.isEmpty();
}
});

View file

@ -308,6 +308,11 @@ public class SAMLParserUtil {
return parseNameIDType(xmlEventReader);
}
} else if (xmlEvent instanceof EndElement) {
// consume the end element tag
EndElement end = StaxParserUtil.getNextEndElement(xmlEventReader);
String endElementTag = StaxParserUtil.getEndElementName(end);
if (! StaxParserUtil.matches(end, JBossSAMLConstants.ATTRIBUTE_VALUE.get()))
throw logger.parserUnknownEndElement(endElementTag);
return "";
}

View file

@ -200,4 +200,20 @@ public class SAMLParserTest {
assertThat(parsedObject, instanceOf(EntityDescriptorType.class));
}
}
@Test
public void testEmptyAttributeValue() throws Exception {
try (InputStream st = SAMLParserTest.class.getResourceAsStream("KEYCLOAK-4790-Empty-attribute-value.xml")) {
Object parsedObject = parser.parse(st);
assertThat(parsedObject, instanceOf(ResponseType.class));
}
}
@Test
public void testEmptyAttributeValueLast() throws Exception {
try (InputStream st = SAMLParserTest.class.getResourceAsStream("KEYCLOAK-4790-Empty-attribute-value-last.xml")) {
Object parsedObject = parser.parse(st);
assertThat(parsedObject, instanceOf(ResponseType.class));
}
}
}

View file

@ -0,0 +1,35 @@
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_d9e9e102-f048-48fb-a1a3-b5a82d9cd9c3" Version="2.0"
IssueInstant="2017-04-24T12:50:14.645Z" Destination="https://y/auth/realms/administration/broker/saml/endpoint"
Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="ID_638a829f-7ad2-408e-b3e5-5f2240010ds7f">
<saml:Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">https://x/</saml:Issuer>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
<Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" ID="_0cceed2a-e409-4faa-a411-c647be748f2b" IssueInstant="2017-04-24T12:50:14.645Z" Version="2.0">
<Issuer>https://x/</Issuer>
<Subject>
<NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName">C=c,OU=ou</NameID>
<SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<SubjectConfirmationData InResponseTo="ID_638a829f-7ad2-408e-b3e5-5f224001057f" NotOnOrAfter="2017-04-24T12:55:14.645Z" Recipient="https://y/auth/realms/administration/broker/saml/endpoint"/>
</SubjectConfirmation>
</Subject>
<Conditions NotBefore="2017-04-24T12:45:14.380Z" NotOnOrAfter="2017-04-24T13:45:14.380Z">
<AudienceRestriction>
<Audience>https://x/auth/realms/administration</Audience>
</AudienceRestriction>
</Conditions>
<AttributeStatement>
<Attribute Name="urn:oid:0.9.2342.19200300.100.1.3" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<AttributeValue>aa</AttributeValue>
</Attribute>
<Attribute Name="urn:oid:0.9.2342.19200300.100.1.2" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<AttributeValue/>
</Attribute>
</AttributeStatement>
<AuthnStatement AuthnInstant="2017-04-24T12:50:14.037Z" SessionIndex="_0cceed2a-e409-4faa-a411-c647be748f2b">
<AuthnContext>
<AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:X509</AuthnContextClassRef>
</AuthnContext>
</AuthnStatement>
</Assertion>
</samlp:Response>

View file

@ -0,0 +1,35 @@
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_d9e9e102-f048-48fb-a1a3-b5a82d9cd9c3" Version="2.0"
IssueInstant="2017-04-24T12:50:14.645Z" Destination="https://y/auth/realms/administration/broker/saml/endpoint"
Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="ID_638a829f-7ad2-408e-b3e5-5f2240010ds7f">
<saml:Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">https://x/</saml:Issuer>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
<Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" ID="_0cceed2a-e409-4faa-a411-c647be748f2b" IssueInstant="2017-04-24T12:50:14.645Z" Version="2.0">
<Issuer>https://x/</Issuer>
<Subject>
<NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName">C=c,OU=ou</NameID>
<SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<SubjectConfirmationData InResponseTo="ID_638a829f-7ad2-408e-b3e5-5f224001057f" NotOnOrAfter="2017-04-24T12:55:14.645Z" Recipient="https://y/auth/realms/administration/broker/saml/endpoint"/>
</SubjectConfirmation>
</Subject>
<Conditions NotBefore="2017-04-24T12:45:14.380Z" NotOnOrAfter="2017-04-24T13:45:14.380Z">
<AudienceRestriction>
<Audience>https://x/auth/realms/administration</Audience>
</AudienceRestriction>
</Conditions>
<AttributeStatement>
<Attribute Name="urn:oid:0.9.2342.19200300.100.1.2" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<AttributeValue/>
</Attribute>
<Attribute Name="urn:oid:0.9.2342.19200300.100.1.3" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<AttributeValue>aa</AttributeValue>
</Attribute>
</AttributeStatement>
<AuthnStatement AuthnInstant="2017-04-24T12:50:14.037Z" SessionIndex="_0cceed2a-e409-4faa-a411-c647be748f2b">
<AuthnContext>
<AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:X509</AuthnContextClassRef>
</AuthnContext>
</AuthnStatement>
</Assertion>
</samlp:Response>

View file

@ -58,7 +58,7 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon
/**
* ClientSessionModel attached to this flow
* AuthenticationSessionModel attached to this flow
*
* @return
*/
@ -74,7 +74,7 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon
/**
* Get the action URL for the required action.
*
* @param code client session access code
* @param code authentication session access code
* @return
*/
URI getActionUrl(String code);
@ -114,7 +114,7 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon
void resetFlow(Runnable afterResetListener);
/**
* Fork the current flow. The client session will be cloned and set to point at the realm's browser login flow. The Response will be the result
* Fork the current flow. The authentication session will be cloned and set to point at the realm's browser login flow. The Response will be the result
* of this fork. The previous flow will still be set at the current execution. This is used by reset password when it sends an email.
* It sends an email linking to the current flow and redirects the browser to a new browser login flow.
*
@ -125,7 +125,7 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon
void fork();
/**
* Fork the current flow. The client session will be cloned and set to point at the realm's browser login flow. The Response will be the result
* Fork the current flow. The authentication session will be cloned and set to point at the realm's browser login flow. The Response will be the result
* of this fork. The previous flow will still be set at the current execution. This is used by reset password when it sends an email.
* It sends an email linking to the current flow and redirects the browser to a new browser login flow.
*
@ -135,7 +135,7 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon
*/
void forkWithSuccessMessage(FormMessage message);
/**
* Fork the current flow. The client session will be cloned and set to point at the realm's browser login flow. The Response will be the result
* Fork the current flow. The authentication session will be cloned and set to point at the realm's browser login flow. The Response will be the result
* of this fork. The previous flow will still be set at the current execution. This is used by reset password when it sends an email.
* It sends an email linking to the current flow and redirects the browser to a new browser login flow.
*

View file

@ -62,7 +62,7 @@ public enum FlowStatus {
ATTEMPTED,
/**
* This flow is being forked. The current client session is being cloned, reset, and redirected to browser login.
* This flow is being forked. The current authentication session is being cloned, reset, and redirected to browser login.
*
*/
FORK,

View file

@ -17,6 +17,7 @@
package org.keycloak.broker.provider;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.broker.provider.util.IdentityBrokerState;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.sessions.AuthenticationSessionModel;
@ -30,13 +31,13 @@ public class AuthenticationRequest {
private final KeycloakSession session;
private final UriInfo uriInfo;
private final String state;
private final IdentityBrokerState state;
private final HttpRequest httpRequest;
private final RealmModel realm;
private final String redirectUri;
private final AuthenticationSessionModel authSession;
public AuthenticationRequest(KeycloakSession session, RealmModel realm, AuthenticationSessionModel authSession, HttpRequest httpRequest, UriInfo uriInfo, String state, String redirectUri) {
public AuthenticationRequest(KeycloakSession session, RealmModel realm, AuthenticationSessionModel authSession, HttpRequest httpRequest, UriInfo uriInfo, IdentityBrokerState state, String redirectUri) {
this.session = session;
this.realm = realm;
this.httpRequest = httpRequest;
@ -54,7 +55,7 @@ public class AuthenticationRequest {
return this.uriInfo;
}
public String getState() {
public IdentityBrokerState getState() {
return this.state;
}

View file

@ -0,0 +1,90 @@
/*
* 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.broker.provider.util;
import java.util.regex.Pattern;
/**
* Encapsulates parsing logic related to state passed to identity provider in "state" (or RelayState) parameter
*
* Not Thread-safe
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class IdentityBrokerState {
private String decodedState;
private String clientId;
private String encodedState;
private IdentityBrokerState() {
}
public static IdentityBrokerState decoded(String decodedState, String clientId) {
IdentityBrokerState state = new IdentityBrokerState();
state.decodedState = decodedState;
state.clientId = clientId;
return state;
}
public static IdentityBrokerState encoded(String encodedState) {
IdentityBrokerState state = new IdentityBrokerState();
state.encodedState = encodedState;
return state;
}
public String getDecodedState() {
if (decodedState == null) {
decode();
}
return decodedState;
}
public String getClientId() {
if (decodedState == null) {
decode();
}
return clientId;
}
public String getEncodedState() {
if (encodedState == null) {
encode();
}
return encodedState;
}
private void decode() {
String[] decoded = DOT.split(encodedState, 0);
decodedState = decoded[0];
if (decoded.length > 0) {
clientId = decoded[1];
}
}
private void encode() {
encodedState = decodedState + "." + clientId;
}
private static final Pattern DOT = Pattern.compile("\\.");
}

View file

@ -22,7 +22,6 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.provider.Provider;
import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;

View file

@ -52,8 +52,12 @@ public interface Constants {
int DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT = 2592000;
String VERIFY_EMAIL_KEY = "VERIFY_EMAIL_KEY";
String EXECUTION = "execution";
String CLIENT_ID = "client_id";
String KEY = "key";
String SKIP_LINK = "skipLink";
// Prefix for user attributes used in various "context"data maps
String USER_ATTRIBUTES_PREFIX = "user.attributes.";

View file

@ -54,7 +54,7 @@ public class ComponentUtil {
return getComponentFactory(session, component.getProviderType(), component.getProviderId());
}
private static Map<String, ProviderConfigProperty> getComponentConfigProperties(KeycloakSession session, String providerType, String providerId) {
public static Map<String, ProviderConfigProperty> getComponentConfigProperties(KeycloakSession session, String providerType, String providerId) {
try {
ComponentFactory componentFactory = getComponentFactory(session, providerType, providerId);
List<ProviderConfigProperty> l = componentFactory.getConfigProperties();

View file

@ -872,8 +872,6 @@ public class ModelToRepresentation {
return scope;
}).collect(Collectors.toSet()));
resource.setTypedScopes(new ArrayList<>());
if (resource.getType() != null) {
ResourceStore resourceStore = authorization.getStoreFactory().getResourceStore();
for (Resource typed : resourceStore.findByType(resource.getType(), resourceServer.getId())) {

View file

@ -151,6 +151,8 @@ public class RepresentationToModel {
if (rep.getMaxDeltaTimeSeconds() != null) newRealm.setMaxDeltaTimeSeconds(rep.getMaxDeltaTimeSeconds());
if (rep.getFailureFactor() != null) newRealm.setFailureFactor(rep.getFailureFactor());
if (rep.isEventsEnabled() != null) newRealm.setEventsEnabled(rep.isEventsEnabled());
if (rep.getEnabledEventTypes() != null)
newRealm.setEnabledEventTypes(new HashSet<>(rep.getEnabledEventTypes()));
if (rep.getEventsExpiration() != null) newRealm.setEventsExpiration(rep.getEventsExpiration());
if (rep.getEventsListeners() != null) newRealm.setEventsListeners(new HashSet<>(rep.getEventsListeners()));
if (rep.isAdminEventsEnabled() != null) newRealm.setAdminEventsEnabled(rep.isAdminEventsEnabled());

View file

@ -17,15 +17,18 @@
package org.keycloak.models.utils;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ComponentExportRepresentation;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@ -70,4 +73,86 @@ public class StripSecretsUtils {
return rep;
}
public static RealmRepresentation stripForExport(KeycloakSession session, RealmRepresentation rep) {
strip(rep);
List<ClientRepresentation> clients = rep.getClients();
if (clients != null) {
for (ClientRepresentation c : clients) {
strip(c);
}
}
List<IdentityProviderRepresentation> providers = rep.getIdentityProviders();
if (providers != null) {
for (IdentityProviderRepresentation r : providers) {
strip(r);
}
}
MultivaluedHashMap<String, ComponentExportRepresentation> components = rep.getComponents();
if (components != null) {
for (Map.Entry<String, List<ComponentExportRepresentation>> ent : components.entrySet()) {
for (ComponentExportRepresentation c : ent.getValue()) {
strip(session, ent.getKey(), c);
}
}
}
List<UserRepresentation> users = rep.getUsers();
if (users != null) {
for (UserRepresentation u: users) {
strip(u);
}
}
users = rep.getFederatedUsers();
if (users != null) {
for (UserRepresentation u: users) {
strip(u);
}
}
return rep;
}
public static UserRepresentation strip(UserRepresentation user) {
user.setCredentials(null);
return user;
}
public static ClientRepresentation strip(ClientRepresentation rep) {
if (rep.getSecret() != null) {
rep.setSecret(ComponentRepresentation.SECRET_VALUE);
}
return rep;
}
public static ComponentExportRepresentation strip(KeycloakSession session, String providerType, ComponentExportRepresentation rep) {
Map<String, ProviderConfigProperty> configProperties = ComponentUtil.getComponentConfigProperties(session, providerType, rep.getProviderId());
if (rep.getConfig() == null) {
return rep;
}
Iterator<Map.Entry<String, List<String>>> itr = rep.getConfig().entrySet().iterator();
while (itr.hasNext()) {
Map.Entry<String, List<String>> next = itr.next();
ProviderConfigProperty configProperty = configProperties.get(next.getKey());
if (configProperty != null) {
if (configProperty.isSecret()) {
next.setValue(Collections.singletonList(ComponentRepresentation.SECRET_VALUE));
}
} else {
itr.remove();
}
}
MultivaluedHashMap<String, ComponentExportRepresentation> sub = rep.getSubComponents();
for (Map.Entry<String, List<ComponentExportRepresentation>> ent: sub.entrySet()) {
for (ComponentExportRepresentation c: ent.getValue()) {
strip(session, ent.getKey(), c);
}
}
return rep;
}
}

View file

@ -91,7 +91,7 @@ public class HashAlgorithmPasswordPolicyProviderFactory implements PasswordPolic
String providerId = value != null && value.length() > 0 ? value : PasswordPolicy.HASH_ALGORITHM_DEFAULT;
PasswordHashProvider provider = session.getProvider(PasswordHashProvider.class, providerId);
if (provider == null) {
throw new ModelException("Password hashing provider not found");
throw new PasswordPolicyConfigException("Password hashing provider not found");
}
return providerId;
}

View file

@ -73,7 +73,7 @@ public class HistoryPasswordPolicyProvider implements PasswordPolicyProvider {
@Override
public Object parseConfig(String value) {
return value != null ? Integer.parseInt(value) : HistoryPasswordPolicyProviderFactory.DEFAULT_VALUE;
return parseInteger(value, HistoryPasswordPolicyProviderFactory.DEFAULT_VALUE);
}
@Override

View file

@ -47,7 +47,7 @@ public class LengthPasswordPolicyProvider implements PasswordPolicyProvider {
@Override
public Object parseConfig(String value) {
return value != null ? Integer.parseInt(value) : 8;
return parseInteger(value, 8);
}
@Override

View file

@ -53,7 +53,7 @@ public class LowerCasePasswordPolicyProvider implements PasswordPolicyProvider {
@Override
public Object parseConfig(String value) {
return value != null ? Integer.parseInt(value) : 1;
return parseInteger(value, 1);
}
@Override

View file

@ -23,6 +23,7 @@ import org.keycloak.models.UserModel;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -55,9 +56,13 @@ public class RegexPatternsPasswordPolicyProvider implements PasswordPolicyProvid
@Override
public Object parseConfig(String value) {
if (value == null) {
throw new IllegalArgumentException("Config required");
throw new PasswordPolicyConfigException("Config required");
}
try {
return Pattern.compile(value);
} catch (PatternSyntaxException e) {
throw new PasswordPolicyConfigException("Not a valid regular expression");
}
return Pattern.compile(value);
}
@Override

View file

@ -53,7 +53,7 @@ public class SpecialCharsPasswordPolicyProvider implements PasswordPolicyProvide
@Override
public Object parseConfig(String value) {
return value != null ? Integer.parseInt(value) : 1;
return parseInteger(value, 1);
}
@Override

View file

@ -53,7 +53,7 @@ public class UpperCasePasswordPolicyProvider implements PasswordPolicyProvider {
@Override
public Object parseConfig(String value) {
return value != null ? Integer.parseInt(value) : 1;
return parseInteger(value, 1);
}
@Override

View file

@ -1,81 +0,0 @@
/*
* 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;
import java.util.Map;
import java.util.Set;
import org.keycloak.sessions.CommonClientSessionModel;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface ClientSessionModel extends CommonClientSessionModel {
public UserSessionModel getUserSession();
public void setUserSession(UserSessionModel userSession);
public String getRedirectUri();
public void setRedirectUri(String uri);
public Map<String, ExecutionStatus> getExecutionStatus();
public void setExecutionStatus(String authenticator, ExecutionStatus status);
public void clearExecutionStatus();
public UserModel getAuthenticatedUser();
public void setAuthenticatedUser(UserModel user);
/**
* Required actions that are attached to this client session.
*
* @return
*/
Set<String> getRequiredActions();
void addRequiredAction(String action);
void removeRequiredAction(String action);
void addRequiredAction(UserModel.RequiredAction action);
void removeRequiredAction(UserModel.RequiredAction action);
/**
* These are notes you want applied to the UserSessionModel when the client session is attached to it.
*
* @param name
* @param value
*/
public void setUserSessionNote(String name, String value);
/**
* These are notes you want applied to the UserSessionModel when the client session is attached to it.
*
* @return
*/
public Map<String, String> getUserSessionNotes();
public void clearUserSessionNotes();
public String getNote(String name);
public void setNote(String name, String value);
public void removeNote(String name);
public Map<String, String> getNotes();
}

View file

@ -17,6 +17,7 @@
package org.keycloak.models;
import org.keycloak.policy.PasswordPolicyConfigException;
import org.keycloak.policy.PasswordPolicyProvider;
import java.io.Serializable;
@ -68,10 +69,17 @@ public class PasswordPolicy implements Serializable {
PasswordPolicyProvider provider = session.getProvider(PasswordPolicyProvider.class, key);
if (provider == null) {
throw new IllegalArgumentException("Unsupported policy");
throw new PasswordPolicyConfigException("Password policy not found");
}
policyConfig.put(key, provider.parseConfig(config));
Object o;
try {
o = provider.parseConfig(config);
} catch (PasswordPolicyConfigException e) {
throw new ModelException("Invalid config for " + key + ": " + e.getMessage());
}
policyConfig.put(key, o);
}
}

View file

@ -0,0 +1,14 @@
package org.keycloak.policy;
import org.keycloak.models.ModelException;
/**
* Created by st on 23/05/17.
*/
public class PasswordPolicyConfigException extends ModelException {
public PasswordPolicyConfigException(String message) {
super(message);
}
}

View file

@ -33,4 +33,12 @@ public interface PasswordPolicyProvider extends Provider {
PolicyError validate(String user, String password);
Object parseConfig(String value);
default Integer parseInteger(String value, Integer defaultValue) {
try {
return value != null ? Integer.parseInt(value) : defaultValue;
} catch (NumberFormatException e) {
throw new PasswordPolicyConfigException("Not a valid number");
}
}
}

View file

@ -33,6 +33,7 @@ import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
@ -485,15 +486,17 @@ public class AuthenticationProcessor {
return LoginActionsService.loginActionsBaseUrl(getUriInfo())
.path(AuthenticationProcessor.this.flowPath)
.queryParam(OAuth2Constants.CODE, code)
.queryParam("execution", getExecution().getId())
.queryParam(Constants.EXECUTION, getExecution().getId())
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
.build(getRealm().getName());
}
@Override
public URI getActionTokenUrl(String tokenString) {
return LoginActionsService.actionTokenProcessor(getUriInfo())
.queryParam("key", tokenString)
.queryParam("execution", getExecution().getId())
.queryParam(Constants.KEY, tokenString)
.queryParam(Constants.EXECUTION, getExecution().getId())
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
.build(getRealm().getName());
}
@ -501,7 +504,8 @@ public class AuthenticationProcessor {
public URI getRefreshExecutionUrl() {
return LoginActionsService.loginActionsBaseUrl(getUriInfo())
.path(AuthenticationProcessor.this.flowPath)
.queryParam("execution", getExecution().getId())
.queryParam(Constants.EXECUTION, getExecution().getId())
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
.build(getRealm().getName());
}

View file

@ -24,6 +24,8 @@ import org.keycloak.events.EventBuilder;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
@ -247,9 +249,11 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
}
public URI getActionUrl(String executionId, String code) {
ClientModel client = processor.getAuthenticationSession().getClient();
return LoginActionsService.registrationFormProcessor(processor.getUriInfo())
.queryParam(OAuth2Constants.CODE, code)
.queryParam("execution", executionId)
.queryParam(Constants.EXECUTION, executionId)
.queryParam(Constants.CLIENT_ID, client.getClientId())
.build(processor.getRealm().getName());
}

View file

@ -23,6 +23,8 @@ import org.keycloak.common.ClientConnection;
import org.keycloak.common.util.Time;
import org.keycloak.events.EventBuilder;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
@ -132,9 +134,11 @@ public class RequiredActionContextResult implements RequiredActionContext {
@Override
public URI getActionUrl(String code) {
ClientModel client = authenticationSession.getClient();
return LoginActionsService.requiredActionProcessor(getUriInfo())
.queryParam(OAuth2Constants.CODE, code)
.queryParam("execution", factory.getId())
.queryParam(Constants.EXECUTION, factory.getId())
.queryParam(Constants.CLIENT_ID, client.getClientId())
.build(getRealm().getName());
}

View file

@ -26,7 +26,6 @@ import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.sessions.AuthenticationSessionModel;
import java.util.function.Function;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilderException;
import javax.ws.rs.core.UriInfo;
@ -45,7 +44,7 @@ public class ActionTokenContext<T extends JsonWebToken> {
@FunctionalInterface
public interface ProcessBrokerFlow {
Response brokerLoginFlow(String code, String execution, String flowPath);
Response brokerLoginFlow(String code, String execution, String clientId, String flowPath);
};
private final KeycloakSession session;
@ -113,7 +112,7 @@ public class ActionTokenContext<T extends JsonWebToken> {
ClientModel client = realm.getClientByClientId(clientId == null ? Constants.ACCOUNT_MANAGEMENT_CLIENT_ID : clientId);
authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, true);
authSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
authSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
String redirectUri = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString();
authSession.setRedirectUri(redirectUri);
@ -158,6 +157,7 @@ public class ActionTokenContext<T extends JsonWebToken> {
}
public Response brokerFlow(String code, String flowPath) {
return processBrokerFlow.brokerLoginFlow(code, getExecutionId(), flowPath);
ClientModel client = authenticationSession.getClient();
return processBrokerFlow.brokerLoginFlow(code, getExecutionId(), client.getClientId(), flowPath);
}
}

View file

@ -23,6 +23,7 @@ import org.keycloak.authentication.actiontoken.*;
import org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticator;
import org.keycloak.events.*;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.Constants;
import org.keycloak.models.UserModel;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.messages.Messages;
@ -86,7 +87,7 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH
return tokenContext.getSession().getProvider(LoginFormsProvider.class)
.setSuccess(Messages.IDENTITY_PROVIDER_LINK_SUCCESS, token.getIdentityProviderAlias(), token.getIdentityProviderUsername())
.setAttribute("skipLink", true)
.setAttribute(Constants.SKIP_LINK, true)
.createInfoPage();
}

View file

@ -132,7 +132,10 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator
brokerContext.getUsername(), brokerContext.getIdpConfig().getAlias()
);
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo));
String link = builder.queryParam("execution", context.getExecution().getId()).build(realm.getName()).toString();
String link = builder
.queryParam(Constants.EXECUTION, context.getExecution().getId())
.queryParam(Constants.CLIENT_ID, context.getExecution().getId())
.build(realm.getName()).toString();
long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);
try {

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