getComponentConfigProperties(KeycloakSession session, ComponentRepresentation component) {
return getComponentConfigProperties(session, component.getProviderType(), component.getProviderId());
}
@@ -102,5 +105,14 @@ public class ComponentUtil {
((OnUpdateComponent)session.userStorageManager()).onUpdate(session, realm, oldModel, newModel);
}
}
+ public static void notifyPreRemove(KeycloakSession session, RealmModel realm, ComponentModel model) {
+ try {
+ ComponentFactory factory = getComponentFactory(session, model);
+ factory.preRemove(session, realm, model);
+ } catch (IllegalArgumentException iae) {
+ // We allow to remove broken providers without throwing an exception
+ logger.warn(iae.getMessage());
+ }
+ }
}
diff --git a/server-spi-private/src/main/java/org/keycloak/scripting/EvaluatableScriptAdapter.java b/server-spi-private/src/main/java/org/keycloak/scripting/EvaluatableScriptAdapter.java
new file mode 100644
index 00000000000..2a76add9261
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/scripting/EvaluatableScriptAdapter.java
@@ -0,0 +1,14 @@
+package org.keycloak.scripting;
+
+import org.keycloak.models.ScriptModel;
+
+/**
+ * Wraps a {@link ScriptModel} so it can be evaluated with custom bindings.
+ *
+ * @author Jay Anslow
+ */
+public interface EvaluatableScriptAdapter {
+ ScriptModel getScriptModel();
+
+ Object eval(ScriptBindingsConfigurer bindingsConfigurer) throws ScriptExecutionException;
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/scripting/InvocableScriptAdapter.java b/server-spi-private/src/main/java/org/keycloak/scripting/InvocableScriptAdapter.java
index c3859aba357..17bb4a1822a 100644
--- a/server-spi-private/src/main/java/org/keycloak/scripting/InvocableScriptAdapter.java
+++ b/server-spi-private/src/main/java/org/keycloak/scripting/InvocableScriptAdapter.java
@@ -56,7 +56,7 @@ public class InvocableScriptAdapter implements Invocable {
}
this.scriptModel = scriptModel;
- this.scriptEngine = loadScriptIntoEngine(scriptModel, scriptEngine);
+ this.scriptEngine = scriptEngine;
}
@Override
@@ -101,17 +101,6 @@ public class InvocableScriptAdapter implements Invocable {
return candidate != null;
}
- private ScriptEngine loadScriptIntoEngine(ScriptModel script, ScriptEngine engine) {
-
- try {
- engine.eval(script.getCode());
- } catch (ScriptException se) {
- throw new ScriptExecutionException(script, se);
- }
-
- return engine;
- }
-
private Invocable getInvocableEngine() {
return (Invocable) scriptEngine;
}
diff --git a/server-spi-private/src/main/java/org/keycloak/scripting/ScriptingProvider.java b/server-spi-private/src/main/java/org/keycloak/scripting/ScriptingProvider.java
index 67bad5a9c92..ef2990f6ca7 100644
--- a/server-spi-private/src/main/java/org/keycloak/scripting/ScriptingProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/scripting/ScriptingProvider.java
@@ -38,6 +38,14 @@ public interface ScriptingProvider extends Provider {
*/
InvocableScriptAdapter prepareInvocableScript(ScriptModel scriptModel, ScriptBindingsConfigurer bindingsConfigurer);
+ /**
+ * Returns an {@link EvaluatableScriptAdapter} based on the given {@link ScriptModel}.
+ * The {@code EvaluatableScriptAdapter} wraps a dedicated {@link ScriptEngine} that was populated with empty bindings.
+ *
+ * @param scriptModel the scriptModel to wrap
+ */
+ EvaluatableScriptAdapter prepareEvaluatableScript(ScriptModel scriptModel);
+
/**
* Creates a new {@link ScriptModel} instance.
*
diff --git a/server-spi/src/main/java/org/keycloak/component/ComponentFactory.java b/server-spi/src/main/java/org/keycloak/component/ComponentFactory.java
index 695637fc0a9..b4c6f446109 100644
--- a/server-spi/src/main/java/org/keycloak/component/ComponentFactory.java
+++ b/server-spi/src/main/java/org/keycloak/component/ComponentFactory.java
@@ -79,6 +79,18 @@ public interface ComponentFactory ex
}
+ /**
+ * Called before the component is removed.
+ *
+ * @param session
+ * @param realm
+ * @param model model of the component, which is going to be removed
+ */
+ default
+ void preRemove(KeycloakSession session, RealmModel realm, ComponentModel model) {
+
+ }
+
/**
* These are config properties that are common across all implementation of this component type
*
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
index 23d06e3d0f8..af7d2f7a3c5 100755
--- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
@@ -471,6 +471,7 @@ public class AuthenticationProcessor {
LoginFormsProvider provider = getSession().getProvider(LoginFormsProvider.class)
.setUser(getUser())
.setActionUri(action)
+ .setExecution(getExecution().getId())
.setFormData(request.getDecodedFormParameters())
.setClientSessionCode(accessCode);
if (getForwardedErrorMessage() != null) {
diff --git a/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java
index 82c12ec5009..575677d0b36 100755
--- a/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java
+++ b/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java
@@ -270,6 +270,7 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
URI actionUrl = getActionUrl(executionId, code);
LoginFormsProvider form = processor.getSession().getProvider(LoginFormsProvider.class)
.setActionUri(actionUrl)
+ .setExecution(executionId)
.setClientSessionCode(code)
.setFormData(formData)
.setErrors(errors);
diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
index 1d9475a80d4..3afb34ce8e3 100755
--- a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
+++ b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
@@ -137,11 +137,15 @@ public class RequiredActionContextResult implements RequiredActionContext {
ClientModel client = authenticationSession.getClient();
return LoginActionsService.requiredActionProcessor(getUriInfo())
.queryParam(OAuth2Constants.CODE, code)
- .queryParam(Constants.EXECUTION, factory.getId())
+ .queryParam(Constants.EXECUTION, getExecution())
.queryParam(Constants.CLIENT_ID, client.getClientId())
.build(getRealm().getName());
}
+ private String getExecution() {
+ return factory.getId();
+ }
+
@Override
public String generateCode() {
ClientSessionCode accessCode = new ClientSessionCode<>(session, getRealm(), getAuthenticationSession());
@@ -164,6 +168,7 @@ public class RequiredActionContextResult implements RequiredActionContext {
LoginFormsProvider provider = getSession().getProvider(LoginFormsProvider.class)
.setUser(getUser())
.setActionUri(action)
+ .setExecution(getExecution())
.setClientSessionCode(accessCode);
return provider;
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
index 7189b955f6c..ca841d02f0b 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
@@ -169,6 +169,7 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator
.setStatus(Response.Status.OK)
.setAttribute(LoginFormsProvider.IDENTITY_PROVIDER_BROKER_CONTEXT, brokerContext)
.setActionUri(action)
+ .setExecution(context.getExecution().getId())
.createIdpLinkEmailPage();
context.forceChallenge(challenge);
}
diff --git a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
index 73afdf0ff59..7c95281f143 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
@@ -86,6 +86,7 @@ public class ResourceSetService {
}
@POST
+ @NoCache
@Consumes("application/json")
@Produces("application/json")
public Response create(@Context UriInfo uriInfo, ResourceRepresentation resource) {
@@ -288,8 +289,8 @@ public class ResourceSetService {
@Path("/search")
@GET
- @Produces("application/json")
@NoCache
+ @Produces("application/json")
public Response find(@QueryParam("name") String name) {
this.auth.realm().requireViewAuthorization();
StoreFactory storeFactory = authorization.getStoreFactory();
diff --git a/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java b/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java
index ed328f54506..1ab354663be 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java
@@ -77,6 +77,7 @@ public class ScopeService {
}
@POST
+ @NoCache
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response create(@Context UriInfo uriInfo, ScopeRepresentation scope) {
@@ -150,6 +151,7 @@ public class ScopeService {
@Path("{id}")
@GET
+ @NoCache
@Produces(MediaType.APPLICATION_JSON)
public Response findById(@PathParam("id") String id) {
this.auth.realm().requireViewAuthorization();
@@ -164,6 +166,7 @@ public class ScopeService {
@Path("{id}/resources")
@GET
+ @NoCache
@Produces(MediaType.APPLICATION_JSON)
public Response getResources(@PathParam("id") String id) {
this.auth.realm().requireViewAuthorization();
@@ -186,6 +189,7 @@ public class ScopeService {
@Path("{id}/permissions")
@GET
+ @NoCache
@Produces(MediaType.APPLICATION_JSON)
public Response getPermissions(@PathParam("id") String id) {
this.auth.realm().requireViewAuthorization();
@@ -231,6 +235,7 @@ public class ScopeService {
}
@GET
+ @NoCache
@Produces("application/json")
public Response findAll(@QueryParam("scopeId") String id,
@QueryParam("name") String name,
diff --git a/services/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeStatementMapper.java b/services/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeStatementMapper.java
new file mode 100644
index 00000000000..4856fb64730
--- /dev/null
+++ b/services/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeStatementMapper.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (c) eHealth
+ */
+package org.keycloak.broker.saml.mappers;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
+import org.keycloak.broker.provider.BrokeredIdentityContext;
+import org.keycloak.broker.saml.SAMLEndpoint;
+import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
+import org.keycloak.common.util.CollectionUtil;
+import org.keycloak.dom.saml.v2.assertion.AssertionType;
+import org.keycloak.dom.saml.v2.assertion.AttributeStatementType.ASTChoiceType;
+import org.keycloak.dom.saml.v2.assertion.AttributeType;
+import org.keycloak.models.IdentityProviderMapperModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.provider.ProviderConfigProperty;
+
+/**
+ * @author Frederik Libert
+ *
+ */
+public class UserAttributeStatementMapper extends AbstractIdentityProviderMapper {
+
+ private static final String USER_ATTR_LOCALE = "locale";
+
+ private static final String[] COMPATIBLE_PROVIDERS = {SAMLIdentityProviderFactory.PROVIDER_ID};
+
+ private static final List CONFIG_PROPERTIES = new ArrayList<>();
+
+ public static final String ATTRIBUTE_NAME_PATTERN = "attribute.name.pattern";
+
+ public static final String USER_ATTRIBUTE_FIRST_NAME = "user.attribute.firstName";
+
+ public static final String USER_ATTRIBUTE_LAST_NAME = "user.attribute.lastName";
+
+ public static final String USER_ATTRIBUTE_EMAIL = "user.attribute.email";
+
+ public static final String USER_ATTRIBUTE_LANGUAGE = "user.attribute.language";
+
+ private static final String USE_FRIENDLY_NAMES = "use.friendly.names";
+
+ static {
+ ProviderConfigProperty property;
+ property = new ProviderConfigProperty();
+ property.setName(ATTRIBUTE_NAME_PATTERN);
+ property.setLabel("Attribute Name Pattern");
+ property.setHelpText("Pattern of attribute names in assertion that must be mapped. Leave blank to map all attributes.");
+ property.setType(ProviderConfigProperty.STRING_TYPE);
+ CONFIG_PROPERTIES.add(property);
+ property = new ProviderConfigProperty();
+ property.setName(USER_ATTRIBUTE_FIRST_NAME);
+ property.setLabel("User Attribute FirstName");
+ property.setHelpText("Define which saml Attribute must be mapped to the User property firstName.");
+ property.setType(ProviderConfigProperty.STRING_TYPE);
+ CONFIG_PROPERTIES.add(property);
+ property = new ProviderConfigProperty();
+ property.setName(USER_ATTRIBUTE_LAST_NAME);
+ property.setLabel("User Attribute LastName");
+ property.setHelpText("Define which saml Attribute must be mapped to the User property lastName.");
+ property.setType(ProviderConfigProperty.STRING_TYPE);
+ CONFIG_PROPERTIES.add(property);
+ property = new ProviderConfigProperty();
+ property.setName(USER_ATTRIBUTE_EMAIL);
+ property.setLabel("User Attribute Email");
+ property.setHelpText("Define which saml Attribute must be mapped to the User property email.");
+ property.setType(ProviderConfigProperty.STRING_TYPE);
+ CONFIG_PROPERTIES.add(property);
+ property = new ProviderConfigProperty();
+ property.setName(USER_ATTRIBUTE_LANGUAGE);
+ property.setLabel("User Attribute Language");
+ property.setHelpText("Define which saml Attribute must be mapped to the User attribute locale.");
+ property.setType(ProviderConfigProperty.STRING_TYPE);
+ CONFIG_PROPERTIES.add(property);
+ property = new ProviderConfigProperty();
+ property.setName(USE_FRIENDLY_NAMES);
+ property.setLabel("Use Attribute Friendly Name");
+ property.setHelpText("Define which name to give to each mapped user attribute: name or friendlyName.");
+ property.setType(ProviderConfigProperty.BOOLEAN_TYPE);
+ CONFIG_PROPERTIES.add(property);
+ }
+
+ public static final String PROVIDER_ID = "saml-user-attributestatement-idp-mapper";
+
+ @Override
+ public List getConfigProperties() {
+ return CONFIG_PROPERTIES;
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public String[] getCompatibleProviders() {
+ return COMPATIBLE_PROVIDERS.clone();
+ }
+
+ @Override
+ public String getDisplayCategory() {
+ return "AttributeStatement Importer";
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "AttributeStatement Importer";
+ }
+
+ @Override
+ public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
+ String firstNameAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_FIRST_NAME);
+ String lastNameAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_LAST_NAME);
+ String emailAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_EMAIL);
+ String langAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_LANGUAGE);
+ Boolean useFriendlyNames = Boolean.valueOf(mapperModel.getConfig().get(USE_FRIENDLY_NAMES));
+ List attributesInContext = findAttributesInContext(context, getAttributePattern(mapperModel));
+ for (AttributeType a : attributesInContext) {
+ String attribute = useFriendlyNames ? a.getFriendlyName() : a.getName();
+ List attributeValuesInContext = a.getAttributeValue().stream().filter(Objects::nonNull).map(Object::toString).collect(Collectors.toList());
+ if (!attributeValuesInContext.isEmpty()) {
+ // set as attribute anyway
+ context.setUserAttribute(attribute, attributeValuesInContext);
+ // set as special field ?
+ if (Objects.equals(attribute, emailAttribute)) {
+ setIfNotEmpty(context::setEmail, attributeValuesInContext);
+ } else if (Objects.equals(attribute, firstNameAttribute)) {
+ setIfNotEmpty(context::setFirstName, attributeValuesInContext);
+ } else if (Objects.equals(attribute, lastNameAttribute)) {
+ setIfNotEmpty(context::setLastName, attributeValuesInContext);
+ } else if (Objects.equals(attribute, langAttribute)) {
+ context.setUserAttribute(USER_ATTR_LOCALE, attributeValuesInContext);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
+ String firstNameAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_FIRST_NAME);
+ String lastNameAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_LAST_NAME);
+ String emailAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_EMAIL);
+ String langAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_LANGUAGE);
+ Boolean useFriendlyNames = Boolean.valueOf(mapperModel.getConfig().get(USE_FRIENDLY_NAMES));
+ List attributesInContext = findAttributesInContext(context, getAttributePattern(mapperModel));
+
+ Set assertedUserAttributes = new HashSet();
+ for (AttributeType a : attributesInContext) {
+ String attribute = useFriendlyNames ? a.getFriendlyName() : a.getName();
+ List attributeValuesInContext = a.getAttributeValue().stream().filter(Objects::nonNull).map(Object::toString).collect(Collectors.toList());
+ List currentAttributeValues = user.getAttributes().get(attribute);
+ if (attributeValuesInContext == null) {
+ // attribute no longer sent by brokered idp, remove it
+ user.removeAttribute(attribute);
+ } else if (currentAttributeValues == null) {
+ // new attribute sent by brokered idp, add it
+ user.setAttribute(attribute, attributeValuesInContext);
+ } else if (!CollectionUtil.collectionEquals(attributeValuesInContext, currentAttributeValues)) {
+ // attribute sent by brokered idp has different values as before, update it
+ user.setAttribute(attribute, attributeValuesInContext);
+ }
+ if (Objects.equals(attribute, emailAttribute)) {
+ setIfNotEmpty(context::setEmail, attributeValuesInContext);
+ } else if (Objects.equals(attribute, firstNameAttribute)) {
+ setIfNotEmpty(context::setFirstName, attributeValuesInContext);
+ } else if (Objects.equals(attribute, lastNameAttribute)) {
+ setIfNotEmpty(context::setLastName, attributeValuesInContext);
+ } else if (Objects.equals(attribute, langAttribute)) {
+ if(attributeValuesInContext == null) {
+ user.removeAttribute(USER_ATTR_LOCALE);
+ } else {
+ user.setAttribute(USER_ATTR_LOCALE, attributeValuesInContext);
+ }
+ assertedUserAttributes.add(USER_ATTR_LOCALE);
+ }
+ // Mark attribute as handled
+ assertedUserAttributes.add(attribute);
+ }
+ // Remove user attributes that were not referenced in assertion.
+ user.getAttributes().keySet().stream().filter(a -> !assertedUserAttributes.contains(a)).forEach(a -> user.removeAttribute(a));
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Import all saml attributes found in attributestatements in assertion into user properties or attributes.";
+ }
+
+ private Optional getAttributePattern(IdentityProviderMapperModel mapperModel) {
+ String attributePatternConfig = mapperModel.getConfig().get(ATTRIBUTE_NAME_PATTERN);
+ return Optional.ofNullable(attributePatternConfig != null ? Pattern.compile(attributePatternConfig) : null);
+ }
+
+ private List findAttributesInContext(BrokeredIdentityContext context, Optional attributePattern) {
+ AssertionType assertion = (AssertionType) context.getContextData().get(SAMLEndpoint.SAML_ASSERTION);
+
+ return assertion.getAttributeStatements().stream()//
+ .flatMap(statement -> statement.getAttributes().stream())//
+ .filter(item -> !attributePattern.isPresent() || attributePattern.get().matcher(item.getAttribute().getName()).matches())//
+ .map(ASTChoiceType::getAttribute)//
+ .collect(Collectors.toList());
+ }
+
+ private void setIfNotEmpty(Consumer consumer, List values) {
+ if (values != null && !values.isEmpty()) {
+ consumer.accept(values.get(0));
+ }
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
index affaf204b0b..d7eb01cf1c2 100755
--- a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
+++ b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
@@ -76,6 +76,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
private Map httpResponseHeaders = new HashMap();
private String accessRequestMessage;
private URI actionUri;
+ private String execution;
private List messages = null;
private MessageType messageType = MessageType.ERROR;
@@ -230,6 +231,11 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
b = UriBuilder.fromUri(baseUri).path(uriInfo.getPath());
break;
}
+
+ if (execution != null) {
+ b.queryParam(Constants.EXECUTION, execution);
+ }
+
attributes.put("locale", new LocaleBean(realm, locale, b, messagesBundle));
}
}
@@ -366,7 +372,13 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri));
if (realm.isInternationalizationEnabled()) {
- UriBuilder b = UriBuilder.fromUri(baseUri).path(uriInfo.getPath());
+ UriBuilder b = UriBuilder.fromUri(baseUri)
+ .path(uriInfo.getPath());
+
+ if (execution != null) {
+ b.queryParam(Constants.EXECUTION, execution);
+ }
+
attributes.put("locale", new LocaleBean(realm, locale, b, messagesBundle));
}
}
@@ -590,6 +602,12 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
return this;
}
+ @Override
+ public LoginFormsProvider setExecution(String execution) {
+ this.execution = execution;
+ return this;
+ }
+
@Override
public LoginFormsProvider setResponseHeader(String headerName, String headerValue) {
this.httpResponseHeaders.put(headerName, headerValue);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
index 26d012b0e11..3a7e4c0e369 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
@@ -269,6 +269,12 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
}
private Response checkOIDCParams() {
+ // If request is not OIDC request, but pure OAuth2 request and response_type is just 'token', then 'nonce' is not mandatory
+ boolean isOIDCRequest = TokenUtil.isOIDCRequest(request.getScope());
+ if (!isOIDCRequest && parsedResponseType.toString().equals(OIDCResponseType.TOKEN)) {
+ return null;
+ }
+
if (parsedResponseType.isImplicitOrHybridFlow() && request.getNonce() == null) {
ServicesLogger.LOGGER.missingParameter(OIDCLoginProtocol.NONCE_PARAM);
event.error(Errors.INVALID_REQUEST);
@@ -354,10 +360,12 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
private void checkRedirectUri() {
String redirectUriParam = request.getRedirectUriParam();
+ boolean isOIDCRequest = TokenUtil.isOIDCRequest(request.getScope());
event.detail(Details.REDIRECT_URI, redirectUriParam);
- redirectUri = RedirectUtils.verifyRedirectUri(uriInfo, redirectUriParam, realm, client);
+ // redirect_uri parameter is required per OpenID Connect, but optional per OAuth2
+ redirectUri = RedirectUtils.verifyRedirectUri(uriInfo, redirectUriParam, realm, client, isOIDCRequest);
if (redirectUri == null) {
event.error(Errors.INVALID_REDIRECT_URI);
throw new ErrorPageException(session, Messages.INVALID_PARAMETER, OIDCLoginProtocol.REDIRECT_URI_PARAM);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
index 6aa13e23a85..4870415f237 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -560,13 +560,9 @@ public class TokenEndpoint {
// https://tools.ietf.org/html/rfc7636#section-4.6
private String generateS256CodeChallenge(String codeVerifier) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-256");
- md.update(codeVerifier.getBytes());
- StringBuilder sb = new StringBuilder();
- for (byte b : md.digest()) {
- String hex = String.format("%02x", b);
- sb.append(hex);
- }
- String codeVerifierEncoded = Base64Url.encode(sb.toString().getBytes());
+ md.update(codeVerifier.getBytes("ISO_8859_1"));
+ byte[] digestBytes = md.digest();
+ String codeVerifierEncoded = Base64Url.encode(digestBytes);
return codeVerifierEncoded;
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java
index 60f5493c22d..c61bdd072cc 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java
@@ -26,6 +26,7 @@ import org.keycloak.services.Urls;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
+import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
@@ -38,12 +39,16 @@ public class RedirectUtils {
public static String verifyRealmRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm) {
Set validRedirects = getValidateRedirectUris(uriInfo, realm);
- return verifyRedirectUri(uriInfo, null, redirectUri, realm, validRedirects);
+ return verifyRedirectUri(uriInfo, null, redirectUri, realm, validRedirects, true);
}
public static String verifyRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm, ClientModel client) {
+ return verifyRedirectUri(uriInfo, redirectUri, realm, client, true);
+ }
+
+ public static String verifyRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm, ClientModel client, boolean requireRedirectUri) {
if (client != null)
- return verifyRedirectUri(uriInfo, client.getRootUrl(), redirectUri, realm, client.getRedirectUris());
+ return verifyRedirectUri(uriInfo, client.getRootUrl(), redirectUri, realm, client.getRedirectUris(), requireRedirectUri);
return null;
}
@@ -69,10 +74,16 @@ public class RedirectUtils {
return redirects;
}
- private static String verifyRedirectUri(UriInfo uriInfo, String rootUrl, String redirectUri, RealmModel realm, Set validRedirects) {
+ private static String verifyRedirectUri(UriInfo uriInfo, String rootUrl, String redirectUri, RealmModel realm, Set validRedirects, boolean requireRedirectUri) {
if (redirectUri == null) {
- logger.debug("No Redirect URI parameter specified");
- return null;
+ if (!requireRedirectUri) {
+ redirectUri = getSingleValidRedirectUri(validRedirects);
+ }
+
+ if (redirectUri == null) {
+ logger.debug("No Redirect URI parameter specified");
+ return null;
+ }
} else if (validRedirects.isEmpty()) {
logger.debug("No Redirect URIs supplied");
redirectUri = null;
@@ -149,4 +160,14 @@ public class RedirectUtils {
return false;
}
+ private static String getSingleValidRedirectUri(Collection validRedirects) {
+ if (validRedirects.size() != 1) return null;
+ String validRedirect = validRedirects.iterator().next();
+ int idx = validRedirect.indexOf("/*");
+ if (idx > -1) {
+ validRedirect = validRedirect.substring(0, idx);
+ }
+ return validRedirect;
+ }
+
}
diff --git a/services/src/main/java/org/keycloak/scripting/AbstractEvaluatableScriptAdapter.java b/services/src/main/java/org/keycloak/scripting/AbstractEvaluatableScriptAdapter.java
new file mode 100644
index 00000000000..534883a879f
--- /dev/null
+++ b/services/src/main/java/org/keycloak/scripting/AbstractEvaluatableScriptAdapter.java
@@ -0,0 +1,76 @@
+package org.keycloak.scripting;
+
+import javax.script.Bindings;
+import javax.script.ScriptContext;
+import javax.script.ScriptEngine;
+import javax.script.ScriptException;
+
+import org.keycloak.models.ScriptModel;
+
+/**
+ * Abstract class for wrapping a {@link ScriptModel} to make it evaluatable.
+ *
+ * @author Jay Anslow
+ */
+abstract class AbstractEvaluatableScriptAdapter implements EvaluatableScriptAdapter {
+ /**
+ * Holds the {@link ScriptModel}.
+ */
+ private final ScriptModel scriptModel;
+
+ AbstractEvaluatableScriptAdapter(final ScriptModel scriptModel) {
+ if (scriptModel == null) {
+ throw new IllegalArgumentException("scriptModel must not be null");
+ }
+ this.scriptModel = scriptModel;
+ }
+
+ @Override
+ public Object eval(final ScriptBindingsConfigurer bindingsConfigurer) throws ScriptExecutionException {
+ return evalUnchecked(createBindings(bindingsConfigurer));
+ }
+
+ @Override
+ public ScriptModel getScriptModel() {
+ return scriptModel;
+ }
+
+ /**
+ * Note, calling this method modifies the underlying {@link ScriptEngine},
+ * preventing concurrent use of the ScriptEngine (Nashorn's {@link ScriptEngine} and
+ * {@link javax.script.CompiledScript} is thread-safe, but {@link Bindings} isn't).
+ */
+ InvocableScriptAdapter prepareInvokableScript(final ScriptBindingsConfigurer bindingsConfigurer) {
+ final Bindings bindings = createBindings(bindingsConfigurer);
+ evalUnchecked(bindings);
+ final ScriptEngine engine = getEngine();
+ engine.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
+ return new InvocableScriptAdapter(scriptModel, engine);
+ }
+
+ protected String getCode() {
+ return scriptModel.getCode();
+ }
+
+ protected abstract ScriptEngine getEngine();
+
+ protected abstract Object eval(Bindings bindings) throws ScriptException;
+
+ private Object evalUnchecked(final Bindings bindings) {
+ try {
+ return eval(bindings);
+ }
+ catch (ScriptException e) {
+ throw new ScriptExecutionException(scriptModel, e);
+ }
+ }
+
+ private Bindings createBindings(final ScriptBindingsConfigurer bindingsConfigurer) {
+ if (bindingsConfigurer == null) {
+ throw new IllegalArgumentException("bindingsConfigurer must not be null");
+ }
+ final Bindings bindings = getEngine().createBindings();
+ bindingsConfigurer.configureBindings(bindings);
+ return bindings;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/scripting/CompiledEvaluatableScriptAdapter.java b/services/src/main/java/org/keycloak/scripting/CompiledEvaluatableScriptAdapter.java
new file mode 100644
index 00000000000..7359dc9233e
--- /dev/null
+++ b/services/src/main/java/org/keycloak/scripting/CompiledEvaluatableScriptAdapter.java
@@ -0,0 +1,40 @@
+package org.keycloak.scripting;
+
+import javax.script.Bindings;
+import javax.script.CompiledScript;
+import javax.script.ScriptEngine;
+import javax.script.ScriptException;
+
+import org.keycloak.models.ScriptModel;
+
+/**
+ * Wraps a compiled {@link ScriptModel} so it can be evaluated.
+ *
+ * @author Jay Anslow
+ */
+class CompiledEvaluatableScriptAdapter extends AbstractEvaluatableScriptAdapter {
+ /**
+ * Holds the {@link CompiledScript} for the {@link ScriptModel}.
+ */
+ private final CompiledScript compiledScript;
+
+ CompiledEvaluatableScriptAdapter(final ScriptModel scriptModel, final CompiledScript compiledScript) {
+ super(scriptModel);
+
+ if (compiledScript == null) {
+ throw new IllegalArgumentException("compiledScript must not be null");
+ }
+
+ this.compiledScript = compiledScript;
+ }
+
+ @Override
+ protected ScriptEngine getEngine() {
+ return compiledScript.getEngine();
+ }
+
+ @Override
+ protected Object eval(final Bindings bindings) throws ScriptException {
+ return compiledScript.eval(bindings);
+ }
+}
diff --git a/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.java b/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.java
index 601da8e5d5d..d781460183a 100644
--- a/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.java
+++ b/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.java
@@ -16,12 +16,14 @@
*/
package org.keycloak.scripting;
-import org.keycloak.models.ScriptModel;
-
import javax.script.Bindings;
-import javax.script.ScriptContext;
+import javax.script.Compilable;
+import javax.script.CompiledScript;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
+import javax.script.ScriptException;
+
+import org.keycloak.models.ScriptModel;
/**
* A {@link ScriptingProvider} that uses a {@link ScriptEngineManager} to evaluate scripts with a {@link ScriptEngine}.
@@ -32,8 +34,7 @@ public class DefaultScriptingProvider implements ScriptingProvider {
private final ScriptEngineManager scriptEngineManager;
- public DefaultScriptingProvider(ScriptEngineManager scriptEngineManager) {
-
+ DefaultScriptingProvider(ScriptEngineManager scriptEngineManager) {
if (scriptEngineManager == null) {
throw new IllegalStateException("scriptEngineManager must not be null!");
}
@@ -44,13 +45,22 @@ public class DefaultScriptingProvider implements ScriptingProvider {
/**
* Wraps the provided {@link ScriptModel} in a {@link javax.script.Invocable} instance with bindings configured through the {@link ScriptBindingsConfigurer}.
*
- * @param scriptModel must not be {@literal null}
+ * @param scriptModel must not be {@literal null}
* @param bindingsConfigurer must not be {@literal null}
- * @return
*/
@Override
public InvocableScriptAdapter prepareInvocableScript(ScriptModel scriptModel, ScriptBindingsConfigurer bindingsConfigurer) {
+ final AbstractEvaluatableScriptAdapter evaluatable = prepareEvaluatableScript(scriptModel);
+ return evaluatable.prepareInvokableScript(bindingsConfigurer);
+ }
+ /**
+ * Wraps the provided {@link ScriptModel} in a {@link javax.script.Invocable} instance with bindings configured through the {@link ScriptBindingsConfigurer}.
+ *
+ * @param scriptModel must not be {@literal null}
+ */
+ @Override
+ public AbstractEvaluatableScriptAdapter prepareEvaluatableScript(ScriptModel scriptModel) {
if (scriptModel == null) {
throw new IllegalArgumentException("script must not be null");
}
@@ -59,13 +69,18 @@ public class DefaultScriptingProvider implements ScriptingProvider {
throw new IllegalArgumentException("script must not be null or empty");
}
- if (bindingsConfigurer == null) {
- throw new IllegalArgumentException("bindingsConfigurer must not be null");
+ ScriptEngine engine = createPreparedScriptEngine(scriptModel);
+
+ if (engine instanceof Compilable) {
+ try {
+ final CompiledScript compiledScript = ((Compilable) engine).compile(scriptModel.getCode());
+ return new CompiledEvaluatableScriptAdapter(scriptModel, compiledScript);
+ }
+ catch (ScriptException e) {
+ throw new ScriptExecutionException(scriptModel, e);
+ }
}
-
- ScriptEngine engine = createPreparedScriptEngine(scriptModel, bindingsConfigurer);
-
- return new InvocableScriptAdapter(scriptModel, engine);
+ return new UncompiledEvaluatableScriptAdapter(scriptModel, engine);
}
//TODO allow scripts to be maintained independently of other components, e.g. with dedicated persistence
@@ -74,38 +89,27 @@ public class DefaultScriptingProvider implements ScriptingProvider {
@Override
public ScriptModel createScript(String realmId, String mimeType, String scriptName, String scriptCode, String scriptDescription) {
+ return new Script(null /* scriptId */, realmId, scriptName, mimeType, scriptCode, scriptDescription);
+ }
- ScriptModel script = new Script(null /* scriptId */, realmId, scriptName, mimeType, scriptCode, scriptDescription);
- return script;
+ @Override
+ public void close() {
+ //NOOP
}
/**
* Looks-up a {@link ScriptEngine} with prepared {@link Bindings} for the given {@link ScriptModel Script}.
- *
- * @param script
- * @param bindingsConfigurer
- * @return
*/
- private ScriptEngine createPreparedScriptEngine(ScriptModel script, ScriptBindingsConfigurer bindingsConfigurer) {
-
+ private ScriptEngine createPreparedScriptEngine(ScriptModel script) {
ScriptEngine scriptEngine = lookupScriptEngineFor(script);
if (scriptEngine == null) {
throw new IllegalStateException("Could not find ScriptEngine for script: " + script);
}
- configureBindings(bindingsConfigurer, scriptEngine);
-
return scriptEngine;
}
- private void configureBindings(ScriptBindingsConfigurer bindingsConfigurer, ScriptEngine engine) {
-
- Bindings bindings = engine.createBindings();
- bindingsConfigurer.configureBindings(bindings);
- engine.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
- }
-
/**
* Looks-up a {@link ScriptEngine} based on the MIME-type provided by the given {@link Script}.
*/
@@ -114,13 +118,9 @@ public class DefaultScriptingProvider implements ScriptingProvider {
try {
Thread.currentThread().setContextClassLoader(DefaultScriptingProvider.class.getClassLoader());
return scriptEngineManager.getEngineByMimeType(script.getMimeType());
- } finally {
+ }
+ finally {
Thread.currentThread().setContextClassLoader(cl);
}
}
-
- @Override
- public void close() {
- //NOOP
- }
}
diff --git a/services/src/main/java/org/keycloak/scripting/UncompiledEvaluatableScriptAdapter.java b/services/src/main/java/org/keycloak/scripting/UncompiledEvaluatableScriptAdapter.java
new file mode 100644
index 00000000000..8464fdf9799
--- /dev/null
+++ b/services/src/main/java/org/keycloak/scripting/UncompiledEvaluatableScriptAdapter.java
@@ -0,0 +1,39 @@
+package org.keycloak.scripting;
+
+import javax.script.Bindings;
+import javax.script.ScriptEngine;
+import javax.script.ScriptException;
+
+import org.keycloak.models.ScriptModel;
+
+/**
+ * Wraps an uncompiled {@link ScriptModel} so it can be evaluated.
+ *
+ * @author Jay Anslow
+ */
+class UncompiledEvaluatableScriptAdapter extends AbstractEvaluatableScriptAdapter {
+ /**
+ * Holds the {@link ScriptEngine} instance.
+ */
+ private final ScriptEngine scriptEngine;
+
+ UncompiledEvaluatableScriptAdapter(final ScriptModel scriptModel, final ScriptEngine scriptEngine) {
+ super(scriptModel);
+ if (scriptEngine == null) {
+ throw new IllegalArgumentException("scriptEngine must not be null");
+ }
+
+ this.scriptEngine = scriptEngine;
+ }
+
+ @Override
+ protected ScriptEngine getEngine() {
+ return scriptEngine;
+ }
+
+ @Override
+ protected Object eval(final Bindings bindings) throws ScriptException {
+ return getEngine().eval(getCode(), bindings);
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/ServicesLogger.java b/services/src/main/java/org/keycloak/services/ServicesLogger.java
index bd239a6e519..2d6c4738429 100644
--- a/services/src/main/java/org/keycloak/services/ServicesLogger.java
+++ b/services/src/main/java/org/keycloak/services/ServicesLogger.java
@@ -406,7 +406,7 @@ public interface ServicesLogger extends BasicLogger {
void failedToCloseProviderSession(@Cause Throwable t);
@LogMessage(level = WARN)
- @Message(id=91, value="Request is missing scope 'openid' so it's not treated as OIDC, but just pure OAuth2 request. This can have impact in future versions (eg. removed IDToken from the Token Response)")
+ @Message(id=91, value="Request is missing scope 'openid' so it's not treated as OIDC, but just pure OAuth2 request.")
@Once
void oidcScopeMissing();
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index 07bd1f60bcc..6c917591c3f 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -644,12 +644,15 @@ public class AuthenticationManager {
// Skip grant screen if everything was already approved by this user
if (realmRoles.size() > 0 || resourceRoles.size() > 0 || protocolMappers.size() > 0) {
+ String execution = AuthenticatedClientSessionModel.Action.OAUTH_GRANT.name();
+
accessCode.
setAction(AuthenticatedClientSessionModel.Action.REQUIRED_ACTIONS.name());
- authSession.setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, AuthenticatedClientSessionModel.Action.OAUTH_GRANT.name());
+ authSession.setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, execution);
return session.getProvider(LoginFormsProvider.class)
+ .setExecution(execution)
.setClientSessionCode(accessCode.getCode())
.setAccessRequest(realmRoles, resourceRoles, protocolMappers)
.createOAuthGrant();
diff --git a/services/src/main/java/org/keycloak/services/util/AuthenticationFlowURLHelper.java b/services/src/main/java/org/keycloak/services/util/AuthenticationFlowURLHelper.java
index 3726b99f29b..489f73f5845 100644
--- a/services/src/main/java/org/keycloak/services/util/AuthenticationFlowURLHelper.java
+++ b/services/src/main/java/org/keycloak/services/util/AuthenticationFlowURLHelper.java
@@ -58,6 +58,7 @@ public class AuthenticationFlowURLHelper {
return session.getProvider(LoginFormsProvider.class)
.setActionUri(lastStepUrl)
+ .setExecution(getExecutionId(authSession))
.createLoginExpiredPage();
}
@@ -76,7 +77,7 @@ public class AuthenticationFlowURLHelper {
public URI getLastExecutionUrl(AuthenticationSessionModel authSession) {
- String executionId = authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
+ String executionId = getExecutionId(authSession);
String latestFlowPath = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH);
if (latestFlowPath == null) {
@@ -90,4 +91,8 @@ public class AuthenticationFlowURLHelper {
return getLastExecutionUrl(latestFlowPath, executionId, authSession.getClient().getClientId());
}
+ private String getExecutionId(AuthenticationSessionModel authSession) {
+ return authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
+ }
+
}
diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-remote-store.xsl b/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl
similarity index 80%
rename from testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-remote-store.xsl
rename to testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl
index 0d6f8332be8..ee9c29d419b 100644
--- a/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-remote-store.xsl
+++ b/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl
@@ -33,6 +33,14 @@
+
+
+
+
+
+
+
diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/pom.xml b/testsuite/integration-arquillian/servers/cache-server/jboss/pom.xml
index 8c7f8303553..9c2d1f9909c 100644
--- a/testsuite/integration-arquillian/servers/cache-server/jboss/pom.xml
+++ b/testsuite/integration-arquillian/servers/cache-server/jboss/pom.xml
@@ -100,7 +100,7 @@
xml-maven-plugin
- configure-adapter-debug-log
+ configure-keycloak-caches
process-test-resources
transform
@@ -111,8 +111,9 @@
${cache.server.jboss.home}/standalone/configuration
standalone.xml
+ clustered.xml
- ${common.resources}/add-keycloak-remote-store.xsl
+ ${common.resources}/add-keycloak-caches.xsl
${cache.server.jboss.home}/standalone/configuration
@@ -173,6 +174,23 @@
true
+
+ copy-cache-server-configuration-for-dc-2
+ process-resources
+
+ copy-resources
+
+
+ ${cache.server.jboss.home}/standalone-dc-2/deployments
+ true
+
+
+ ${cache.server.jboss.home}/standalone/deployments
+
+
+ true
+
+
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/Retry.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/Retry.java
similarity index 100%
rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/Retry.java
rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/Retry.java
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java
index 94293ddc5d4..97347d9081a 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java
@@ -142,6 +142,7 @@ public class AuthServerTestEnricher {
containers.stream()
.filter(c -> c.getQualifier().startsWith(AUTH_SERVER_CONTAINER + "-cross-dc-"))
+ .sorted((a, b) -> a.getQualifier().compareTo(b.getQualifier()))
.forEach(c -> {
String portOffsetString = c.getArquillianContainer().getContainerConfiguration().getContainerProperties().getOrDefault("bindHttpPortOffset", "0");
String dcString = c.getArquillianContainer().getContainerConfiguration().getContainerProperties().getOrDefault("dataCenter", "0");
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/CacheStatisticsControllerEnricher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/CacheStatisticsControllerEnricher.java
new file mode 100644
index 00000000000..4091ca4db41
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/CacheStatisticsControllerEnricher.java
@@ -0,0 +1,356 @@
+package org.keycloak.testsuite.arquillian;
+
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.testsuite.Retry;
+import java.util.Map;
+import org.jboss.arquillian.core.api.Instance;
+import org.jboss.arquillian.core.api.annotation.Inject;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import javax.management.MBeanServerConnection;
+import javax.management.ObjectName;
+import javax.management.remote.JMXConnector;
+import javax.management.remote.JMXServiceURL;
+import org.jboss.arquillian.container.spi.Container;
+import org.jboss.arquillian.container.spi.ContainerRegistry;
+import org.jboss.arquillian.test.spi.TestEnricher;
+import java.io.IOException;
+import java.lang.reflect.Parameter;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+import javax.management.Attribute;
+import javax.management.AttributeNotFoundException;
+import javax.management.InstanceNotFoundException;
+import javax.management.IntrospectionException;
+import javax.management.MBeanAttributeInfo;
+import javax.management.MBeanException;
+import javax.management.MBeanInfo;
+import javax.management.MalformedObjectNameException;
+import javax.management.ReflectionException;
+import org.keycloak.testsuite.arquillian.annotation.JmxInfinispanCacheStatistics;
+import java.util.Set;
+import org.keycloak.testsuite.arquillian.annotation.JmxInfinispanChannelStatistics;
+import org.keycloak.testsuite.arquillian.jmx.JmxConnectorRegistry;
+import org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow;
+import java.io.NotSerializableException;
+import java.lang.management.ManagementFactory;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.jboss.arquillian.core.spi.Validate;
+import org.jboss.logging.Logger;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class CacheStatisticsControllerEnricher implements TestEnricher {
+
+ private static final Logger LOG = Logger.getLogger(CacheStatisticsControllerEnricher.class);
+
+ @Inject
+ private Instance registry;
+
+ @Inject
+ private Instance jmxConnectorRegistry;
+
+ @Inject
+ private Instance suiteContext;
+
+ @Override
+ public void enrich(Object testCase) {
+ Validate.notNull(registry.get(), "registry should not be null");
+ Validate.notNull(jmxConnectorRegistry.get(), "jmxConnectorRegistry should not be null");
+ Validate.notNull(suiteContext.get(), "suiteContext should not be null");
+
+ for (Field field : FieldUtils.getAllFields(testCase.getClass())) {
+ JmxInfinispanCacheStatistics annotation = field.getAnnotation(JmxInfinispanCacheStatistics.class);
+
+ if (annotation == null) {
+ continue;
+ }
+
+ try {
+ FieldUtils.writeField(field, testCase, getInfinispanCacheStatistics(annotation), true);
+ } catch (IOException | IllegalAccessException | MalformedObjectNameException e) {
+ throw new RuntimeException("Could not set value on field " + field);
+ }
+ }
+ }
+
+ private InfinispanStatistics getInfinispanCacheStatistics(JmxInfinispanCacheStatistics annotation) throws MalformedObjectNameException, IOException, MalformedURLException {
+ MBeanServerConnection mbsc = getJmxServerConnection(annotation);
+
+ ObjectName mbeanName = new ObjectName(String.format(
+ "%s:type=%s,name=\"%s(%s)\",manager=\"%s\",component=%s",
+ annotation.domain().isEmpty() ? getDefaultDomain(annotation.dcIndex(), annotation.dcNodeIndex()) : InfinispanConnectionProvider.JMX_DOMAIN,
+ annotation.type(),
+ annotation.cacheName(),
+ annotation.cacheMode(),
+ annotation.cacheManagerName(),
+ annotation.component()
+ ));
+
+ InfinispanStatistics value = new InfinispanCacheStatisticsImpl(mbsc, mbeanName);
+
+ if (annotation.domain().isEmpty()) {
+ try {
+ Retry.execute(() -> value.reset(), 2, 150);
+ } catch (RuntimeException ex) {
+ if (annotation.dcIndex() != -1 && annotation.dcNodeIndex() != -1
+ && suiteContext.get().getAuthServerBackendsInfo(annotation.dcIndex()).get(annotation.dcNodeIndex()).isStarted()) {
+ LOG.warn("Could not reset statistics for " + mbeanName);
+ }
+ }
+ }
+
+ return value;
+ }
+
+ private InfinispanStatistics getJGroupsChannelStatistics(JmxInfinispanChannelStatistics annotation) throws MalformedObjectNameException, IOException, MalformedURLException {
+ MBeanServerConnection mbsc = getJmxServerConnection(annotation);
+
+ ObjectName mbeanName = new ObjectName(String.format(
+ "%s:type=%s,cluster=\"%s\"",
+ annotation.domain().isEmpty() ? getDefaultDomain(annotation.dcIndex(), annotation.dcNodeIndex()) : InfinispanConnectionProvider.JMX_DOMAIN,
+ annotation.type(),
+ annotation.cluster()
+ ));
+
+ InfinispanStatistics value = new InfinispanChannelStatisticsImpl(mbsc, mbeanName);
+
+ if (annotation.domain().isEmpty()) {
+ try {
+ Retry.execute(() -> value.reset(), 2, 150);
+ } catch (RuntimeException ex) {
+ if (annotation.dcIndex() != -1 && annotation.dcNodeIndex() != -1
+ && suiteContext.get().getAuthServerBackendsInfo(annotation.dcIndex()).get(annotation.dcNodeIndex()).isStarted()) {
+ LOG.warn("Could not reset statistics for " + mbeanName);
+ }
+ }
+ }
+
+ return value;
+ }
+
+ @Override
+ public Object[] resolve(Method method) {
+ Object[] values = new Object[method.getParameterCount()];
+
+ for (int i = 0; i < method.getParameterCount(); i ++) {
+ Parameter param = method.getParameters()[i];
+
+ JmxInfinispanCacheStatistics annotation = param.getAnnotation(JmxInfinispanCacheStatistics.class);
+ if (annotation != null) try {
+ values[i] = getInfinispanCacheStatistics(annotation);
+ } catch (IOException | MalformedObjectNameException e) {
+ throw new RuntimeException("Could not set value on field " + param);
+ }
+
+ JmxInfinispanChannelStatistics channelAnnotation = param.getAnnotation(JmxInfinispanChannelStatistics.class);
+ if (channelAnnotation != null) try {
+ values[i] = getJGroupsChannelStatistics(channelAnnotation);
+ } catch (IOException | MalformedObjectNameException e) {
+ throw new RuntimeException("Could not set value on field " + param);
+ }
+ }
+
+ return values;
+ }
+
+ private String getDefaultDomain(int dcIndex, int dcNodeIndex) {
+ if (dcIndex != -1 && dcNodeIndex != -1) {
+ return InfinispanConnectionProvider.JMX_DOMAIN + "-" + suiteContext.get().getAuthServerBackendsInfo(dcIndex).get(dcNodeIndex).getQualifier();
+ }
+ return InfinispanConnectionProvider.JMX_DOMAIN;
+ }
+
+ private MBeanServerConnection getJmxServerConnection(JmxInfinispanCacheStatistics annotation) throws MalformedURLException, IOException {
+ final String host;
+ final int port;
+
+ if (annotation.dcIndex() != -1 && annotation.dcNodeIndex() != -1) {
+ ContainerInfo node = suiteContext.get().getAuthServerBackendsInfo(annotation.dcIndex()).get(annotation.dcNodeIndex());
+ Container container = node.getArquillianContainer();
+ if (container.getDeployableContainer() instanceof KeycloakOnUndertow) {
+ return ManagementFactory.getPlatformMBeanServer();
+ }
+ host = "localhost";
+ port = container.getContainerConfiguration().getContainerProperties().containsKey("managementPort")
+ ? Integer.valueOf(container.getContainerConfiguration().getContainerProperties().get("managementPort"))
+ : 9990;
+ } else {
+ host = annotation.host().isEmpty()
+ ? System.getProperty((annotation.hostProperty().isEmpty()
+ ? "keycloak.connectionsInfinispan.remoteStoreServer"
+ : annotation.hostProperty()))
+ : annotation.host();
+
+ port = annotation.managementPort() == -1
+ ? Integer.valueOf(System.getProperty((annotation.managementPortProperty().isEmpty()
+ ? "cache.server.management.port"
+ : annotation.managementPortProperty())))
+ : annotation.managementPort();
+ }
+
+ JMXServiceURL url = new JMXServiceURL("service:jmx:remote+http://" + host + ":" + port);
+ JMXConnector jmxc = jmxConnectorRegistry.get().getConnection(url);
+
+ return jmxc.getMBeanServerConnection();
+ }
+
+ private MBeanServerConnection getJmxServerConnection(JmxInfinispanChannelStatistics annotation) throws MalformedURLException, IOException {
+ final String host;
+ final int port;
+
+ if (annotation.dcIndex() != -1 && annotation.dcNodeIndex() != -1) {
+ ContainerInfo node = suiteContext.get().getAuthServerBackendsInfo(annotation.dcIndex()).get(annotation.dcNodeIndex());
+ Container container = node.getArquillianContainer();
+ if (container.getDeployableContainer() instanceof KeycloakOnUndertow) {
+ return ManagementFactory.getPlatformMBeanServer();
+ }
+ host = "localhost";
+ port = container.getContainerConfiguration().getContainerProperties().containsKey("managementPort")
+ ? Integer.valueOf(container.getContainerConfiguration().getContainerProperties().get("managementPort"))
+ : 9990;
+ } else {
+ host = annotation.host().isEmpty()
+ ? System.getProperty((annotation.hostProperty().isEmpty()
+ ? "keycloak.connectionsInfinispan.remoteStoreServer"
+ : annotation.hostProperty()))
+ : annotation.host();
+
+ port = annotation.managementPort() == -1
+ ? Integer.valueOf(System.getProperty((annotation.managementPortProperty().isEmpty()
+ ? "cache.server.management.port"
+ : annotation.managementPortProperty())))
+ : annotation.managementPort();
+ }
+
+ JMXServiceURL url = new JMXServiceURL("service:jmx:remote+http://" + host + ":" + port);
+ JMXConnector jmxc = jmxConnectorRegistry.get().getConnection(url);
+
+ return jmxc.getMBeanServerConnection();
+ }
+
+ private static abstract class CacheStatisticsImpl implements InfinispanStatistics {
+
+ protected final MBeanServerConnection mbsc;
+ private final ObjectName mbeanNameTemplate;
+ private ObjectName mbeanName;
+
+ public CacheStatisticsImpl(MBeanServerConnection mbsc, ObjectName mbeanNameTemplate) {
+ this.mbsc = mbsc;
+ this.mbeanNameTemplate = mbeanNameTemplate;
+ }
+
+ @Override
+ public boolean exists() {
+ try {
+ getMbeanName();
+ return true;
+ } catch (Exception ex) {
+ return false;
+ }
+ }
+
+ @Override
+ public Map getStatistics() {
+ try {
+ MBeanInfo mBeanInfo = mbsc.getMBeanInfo(getMbeanName());
+ String[] statAttrs = Arrays.asList(mBeanInfo.getAttributes()).stream()
+ .filter(MBeanAttributeInfo::isReadable)
+ .map(MBeanAttributeInfo::getName)
+ .collect(Collectors.toList())
+ .toArray(new String[] {});
+ return mbsc.getAttributes(getMbeanName(), statAttrs)
+ .asList()
+ .stream()
+ .collect(Collectors.toMap(Attribute::getName, Attribute::getValue));
+ } catch (IOException | InstanceNotFoundException | ReflectionException | IntrospectionException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ protected ObjectName getMbeanName() throws IOException, RuntimeException {
+ if (this.mbeanName == null) {
+ Set queryNames = mbsc.queryNames(mbeanNameTemplate, null);
+ if (queryNames.isEmpty()) {
+ throw new RuntimeException("No MBean of template " + mbeanNameTemplate + " found at JMX server");
+ }
+ this.mbeanName = queryNames.iterator().next();
+ }
+
+ return this.mbeanName;
+ }
+
+ @Override
+ public Comparable getSingleStatistics(String statisticsName) {
+ try {
+ return (Comparable) mbsc.getAttribute(getMbeanName(), statisticsName);
+ } catch (IOException | InstanceNotFoundException | MBeanException | ReflectionException | AttributeNotFoundException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ @Override
+ public void waitToBecomeAvailable(int time, TimeUnit unit) {
+ long timeInMillis = TimeUnit.MILLISECONDS.convert(time, unit);
+ Retry.execute(() -> {
+ try {
+ getMbeanName();
+ if (! isAvailable()) throw new RuntimeException("Not available");
+ } catch (Exception ex) {
+ throw new RuntimeException("Timed out while waiting for " + mbeanNameTemplate + " to become available", ex);
+ }
+ }, 1 + (int) timeInMillis / 100, 100);
+ }
+
+ protected abstract boolean isAvailable();
+ }
+
+ private static class InfinispanCacheStatisticsImpl extends CacheStatisticsImpl {
+
+ public InfinispanCacheStatisticsImpl(MBeanServerConnection mbsc, ObjectName mbeanName) {
+ super(mbsc, mbeanName);
+ }
+
+ @Override
+ public void reset() {
+ try {
+ mbsc.invoke(getMbeanName(), "resetStatistics", new Object[] {}, new String[] {});
+ } catch (IOException | InstanceNotFoundException | MBeanException | ReflectionException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ @Override
+ protected boolean isAvailable() {
+ return getSingleStatistics(Constants.STAT_CACHE_ELAPSED_TIME) != null;
+ }
+ }
+
+ private static class InfinispanChannelStatisticsImpl extends CacheStatisticsImpl {
+
+ public InfinispanChannelStatisticsImpl(MBeanServerConnection mbsc, ObjectName mbeanName) {
+ super(mbsc, mbeanName);
+ }
+
+ @Override
+ public void reset() {
+ try {
+ mbsc.invoke(getMbeanName(), "resetStats", new Object[] {}, new String[] {});
+ } catch (NotSerializableException ex) {
+ // Ignore return value not serializable, the invocation has already done its job
+ } catch (IOException | InstanceNotFoundException | MBeanException | ReflectionException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ @Override
+ protected boolean isAvailable() {
+ return Objects.equals(getSingleStatistics(Constants.STAT_CHANNEL_CONNECTED), Boolean.TRUE);
+ }
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/InfinispanStatistics.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/InfinispanStatistics.java
new file mode 100644
index 00000000000..b315937d360
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/InfinispanStatistics.java
@@ -0,0 +1,88 @@
+/*
+ * 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.testsuite.arquillian;
+
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public interface InfinispanStatistics {
+
+ public static class Constants {
+ public static final String DOMAIN_INFINISPAN_DATAGRID = InfinispanConnectionProvider.JMX_DOMAIN;
+
+ public static final String TYPE_CHANNEL = "channel";
+ public static final String TYPE_CACHE = "Cache";
+ public static final String TYPE_CACHE_MANAGER = "CacheManager";
+
+ public static final String COMPONENT_STATISTICS = "Statistics";
+
+ /** Cache statistics */
+ public static final String STAT_CACHE_AVERAGE_READ_TIME = "averageReadTime";
+ public static final String STAT_CACHE_AVERAGE_WRITE_TIME = "averageWriteTime";
+ public static final String STAT_CACHE_ELAPSED_TIME = "elapsedTime";
+ public static final String STAT_CACHE_EVICTIONS = "evictions";
+ public static final String STAT_CACHE_HITS = "hits";
+ public static final String STAT_CACHE_HIT_RATIO = "hitRatio";
+ public static final String STAT_CACHE_MISSES = "misses";
+ public static final String STAT_CACHE_NUMBER_OF_ENTRIES = "numberOfEntries";
+ public static final String STAT_CACHE_NUMBER_OF_ENTRIES_IN_MEMORY = "numberOfEntriesInMemory";
+ public static final String STAT_CACHE_READ_WRITE_RATIO = "readWriteRatio";
+ public static final String STAT_CACHE_REMOVE_HITS = "removeHits";
+ public static final String STAT_CACHE_REMOVE_MISSES = "removeMisses";
+ public static final String STAT_CACHE_STORES = "stores";
+ public static final String STAT_CACHE_TIME_SINCE_RESET = "timeSinceReset";
+
+ /** JGroups channel statistics */
+ public static final String STAT_CHANNEL_ADDRESS = "address";
+ public static final String STAT_CHANNEL_ADDRESS_UUID = "address_uuid";
+ public static final String STAT_CHANNEL_CLOSED = "closed";
+ public static final String STAT_CHANNEL_CLUSTER_NAME = "cluster_name";
+ public static final String STAT_CHANNEL_CONNECTED = "connected";
+ public static final String STAT_CHANNEL_CONNECTING = "connecting";
+ public static final String STAT_CHANNEL_DISCARD_OWN_MESSAGES = "discard_own_messages";
+ public static final String STAT_CHANNEL_OPEN = "open";
+ public static final String STAT_CHANNEL_RECEIVED_BYTES = "received_bytes";
+ public static final String STAT_CHANNEL_RECEIVED_MESSAGES = "received_messages";
+ public static final String STAT_CHANNEL_SENT_BYTES = "sent_bytes";
+ public static final String STAT_CHANNEL_SENT_MESSAGES = "sent_messages";
+ public static final String STAT_CHANNEL_STATE = "state";
+ public static final String STAT_CHANNEL_STATS = "stats";
+ public static final String STAT_CHANNEL_VIEW = "view";
+
+ }
+
+ Map getStatistics();
+
+ Comparable getSingleStatistics(String statisticsName);
+
+ void waitToBecomeAvailable(int time, TimeUnit unit);
+
+ /**
+ * Resets the statistics counters.
+ */
+ void reset();
+
+ /**
+ * Returns {@code true} iff the statistics represented by this object can be retrieved from the server.
+ */
+ boolean exists();
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java
index 7757b076d11..33dc8c21401 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java
@@ -32,6 +32,7 @@ import org.jboss.arquillian.graphene.location.CustomizableURLResourceProvider;
import org.jboss.arquillian.test.spi.enricher.resource.ResourceProvider;
import org.jboss.arquillian.test.spi.execution.TestExecutionDecider;
import org.keycloak.testsuite.arquillian.h2.H2TestEnricher;
+import org.keycloak.testsuite.arquillian.jmx.JmxConnectorRegistryCreator;
import org.keycloak.testsuite.arquillian.karaf.CustomKarafContainer;
import org.keycloak.testsuite.arquillian.migration.MigrationTestExecutionDecider;
import org.keycloak.testsuite.arquillian.provider.AdminClientProvider;
@@ -44,6 +45,7 @@ import org.keycloak.testsuite.drone.HtmlUnitScreenshots;
import org.keycloak.testsuite.drone.KeycloakDronePostSetup;
import org.keycloak.testsuite.drone.KeycloakHtmlUnitInstantiator;
import org.keycloak.testsuite.drone.KeycloakWebDriverConfigurator;
+import org.jboss.arquillian.test.spi.TestEnricher;
/**
*
@@ -65,6 +67,8 @@ public class KeycloakArquillianExtension implements LoadableExtension {
.service(DeploymentScenarioGenerator.class, DeploymentTargetModifier.class)
.service(ApplicationArchiveProcessor.class, DeploymentArchiveProcessor.class)
.service(DeployableContainer.class, CustomKarafContainer.class)
+ .service(TestEnricher.class, CacheStatisticsControllerEnricher.class)
+ .observer(JmxConnectorRegistryCreator.class)
.observer(AuthServerTestEnricher.class)
.observer(AppServerTestEnricher.class)
.observer(H2TestEnricher.class);
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/TestContext.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/TestContext.java
index 30b0405c51c..cfe6d84565c 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/TestContext.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/TestContext.java
@@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
import javax.ws.rs.NotFoundException;
@@ -54,7 +55,7 @@ public final class TestContext {
private boolean initialized;
// Key is realmName, value are objects to clean after the test method
- private Map cleanups = new HashMap<>();
+ private Map cleanups = new ConcurrentHashMap<>();
public TestContext(SuiteContext suiteContext, Class testClass) {
this.suiteContext = suiteContext;
@@ -146,7 +147,11 @@ public final class TestContext {
TestCleanup cleanup = cleanups.get(realmName);
if (cleanup == null) {
cleanup = new TestCleanup(adminClient, realmName);
- cleanups.put(realmName, cleanup);
+ TestCleanup existing = cleanups.putIfAbsent(realmName, cleanup);
+
+ if (existing != null) {
+ cleanup = existing;
+ }
}
return cleanup;
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanCacheStatistics.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanCacheStatistics.java
new file mode 100644
index 00000000000..2dd7bbc2305
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanCacheStatistics.java
@@ -0,0 +1,66 @@
+/*
+ * 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.testsuite.arquillian.annotation;
+
+import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
+import org.keycloak.testsuite.arquillian.InfinispanStatistics;
+import org.keycloak.testsuite.arquillian.InfinispanStatistics.Constants;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation for a field / method parameter annotating {@link InfinispanStatistics} object that would be used
+ * to access statistics via JMX. By default, the access to "work" cache at remote infinispan / JDG server is requested
+ * yet the same annotation is used for other caches as well.
+ *
+ * @author hmlnarik
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD, ElementType.PARAMETER})
+public @interface JmxInfinispanCacheStatistics {
+
+ /** JMX domain. Should be set to default (@{code ""}) if the node to get the statistics from should be obtained from {@link #dcIndex()} and {@link #dcNodeIndex()}. */
+ String domain() default "";
+
+ // JMX address properties
+ String type() default Constants.TYPE_CACHE;
+ String cacheName() default "work";
+ String cacheMode() default "*";
+ String cacheManagerName() default "*";
+ String component() default Constants.COMPONENT_STATISTICS;
+
+ // Host address - either given by arrangement of DC ...
+
+ /** Index of the data center, starting from 0 */
+ int dcIndex() default -1;
+ /** Index of the node within data center, starting from 0. Nodes are ordered by arquillian qualifier as per {@link AuthServerTestEnricher} */
+ int dcNodeIndex() default -1;
+
+ // ... or by specific host/port
+
+ /** Port for management */
+ int managementPort() default -1;
+ /** Name of system property to obtain management port from */
+ String managementPortProperty() default "";
+ /** Host name to connect to */
+ String host() default "";
+ /** Name of system property to obtain host name from */
+ String hostProperty() default "";
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanChannelStatistics.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanChannelStatistics.java
new file mode 100644
index 00000000000..41e9f20f512
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanChannelStatistics.java
@@ -0,0 +1,55 @@
+/*
+ * 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.testsuite.arquillian.annotation;
+
+import org.keycloak.testsuite.arquillian.InfinispanStatistics.Constants;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ *
+ * @author hmlnarik
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD, ElementType.PARAMETER})
+public @interface JmxInfinispanChannelStatistics {
+
+ /** JMX domain. Should be set to default (@{code ""}) if the node to get the statistics from should be obtained from {@link #dcIndex()} and {@link #dcNodeIndex()}. */
+ String domain() default "";
+
+ // JMX address properties
+ String type() default Constants.TYPE_CHANNEL;
+ String cluster() default "*";
+
+ // Host address - either given by arrangement of DC ...
+
+ /** Index of the data center, starting from 0 */
+ int dcIndex() default -1;
+ /** Index of the node within data center, starting from 0. Nodes are ordered by arquillian qualifier as per {@link AuthServerTestEnricher} */
+ int dcNodeIndex() default -1;
+
+ /** Port for management */
+ int managementPort() default -1;
+ /** Name of system property to obtain management port from */
+ String managementPortProperty() default "";
+ /** Host name to connect to */
+ String host() default "";
+ /** Name of system property to obtain host name from */
+ String hostProperty() default "";
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/RegistryCreator.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/RegistryCreator.java
index a2b6ea735d1..41278fc8784 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/RegistryCreator.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/RegistryCreator.java
@@ -35,6 +35,7 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
+import org.mvel2.MVEL;
import static org.keycloak.testsuite.arquillian.containers.SecurityActions.isClassPresent;
import static org.keycloak.testsuite.arquillian.containers.SecurityActions.loadClass;
@@ -97,10 +98,14 @@ public class RegistryCreator {
private static final String ENABLED = "enabled";
- private boolean isEnabled(ContainerDef containerDef) {
+ private static boolean isEnabled(ContainerDef containerDef) {
Map props = containerDef.getContainerProperties();
- return !props.containsKey(ENABLED)
- || (props.containsKey(ENABLED) && props.get(ENABLED).equals("true"));
+ try {
+ return !props.containsKey(ENABLED)
+ || (props.containsKey(ENABLED) && ! props.get(ENABLED).isEmpty() && MVEL.evalToBoolean(props.get(ENABLED), (Object) null));
+ } catch (Exception ex) {
+ return false;
+ }
}
@SuppressWarnings("rawtypes")
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/jmx/JmxConnectorRegistry.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/jmx/JmxConnectorRegistry.java
new file mode 100644
index 00000000000..3a87c5bd93b
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/jmx/JmxConnectorRegistry.java
@@ -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.testsuite.arquillian.jmx;
+
+import javax.management.remote.JMXConnector;
+import javax.management.remote.JMXServiceURL;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public interface JmxConnectorRegistry {
+ JMXConnector getConnection(JMXServiceURL url);
+
+ void closeAll();
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/jmx/JmxConnectorRegistryCreator.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/jmx/JmxConnectorRegistryCreator.java
new file mode 100644
index 00000000000..50c9b965c65
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/jmx/JmxConnectorRegistryCreator.java
@@ -0,0 +1,73 @@
+/*
+ * 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.testsuite.arquillian.jmx;
+
+import java.io.IOException;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import javax.management.remote.JMXConnector;
+import javax.management.remote.JMXConnectorFactory;
+import javax.management.remote.JMXServiceURL;
+import org.jboss.arquillian.core.api.InstanceProducer;
+import org.jboss.arquillian.core.api.annotation.ApplicationScoped;
+import org.jboss.arquillian.core.api.annotation.Inject;
+import org.jboss.arquillian.core.api.annotation.Observes;
+import org.jboss.arquillian.test.spi.event.suite.BeforeSuite;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class JmxConnectorRegistryCreator {
+
+ @Inject
+ @ApplicationScoped
+ private InstanceProducer connectorRegistry;
+
+ public void configureJmxConnectorRegistry(@Observes BeforeSuite event) {
+ if (connectorRegistry.get() == null) {
+ connectorRegistry.set(new JmxConnectorRegistry() {
+
+ private volatile ConcurrentMap connectors = new ConcurrentHashMap<>();
+
+ @Override
+ public JMXConnector getConnection(JMXServiceURL url) {
+ JMXConnector res = connectors.get(url);
+ if (res == null) {
+ try {
+ final JMXConnector conn = JMXConnectorFactory.newJMXConnector(url, null);
+ res = connectors.putIfAbsent(url, conn);
+ if (res == null) {
+ res = conn;
+ }
+ res.connect();
+ } catch (IOException ex) {
+ throw new RuntimeException("Could not instantiate JMX connector for " + url, ex);
+ }
+ }
+ return res;
+ }
+
+ @Override
+ public void closeAll() {
+ connectors.values().forEach(c -> { try { c.close(); } catch (IOException e) {} });
+ connectors.clear();
+ }
+ });
+ }
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/LoadBalancerControllerProvider.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/LoadBalancerControllerProvider.java
index 4f99feb06fd..af1703d6cbf 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/LoadBalancerControllerProvider.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/LoadBalancerControllerProvider.java
@@ -2,14 +2,8 @@ package org.keycloak.testsuite.arquillian.provider;
import org.keycloak.testsuite.arquillian.annotation.LoadBalancer;
import java.lang.annotation.Annotation;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import org.jboss.arquillian.container.spi.event.KillContainer;
-import org.jboss.arquillian.container.spi.event.StartContainer;
-import org.jboss.arquillian.container.spi.event.StopContainer;
import org.jboss.arquillian.core.api.Instance;
import org.jboss.arquillian.core.api.annotation.Inject;
-import org.jboss.arquillian.core.api.annotation.Observes;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.arquillian.test.spi.enricher.resource.ResourceProvider;
import org.keycloak.testsuite.arquillian.LoadBalancerController;
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LanguageComboboxAwarePage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LanguageComboboxAwarePage.java
new file mode 100644
index 00000000000..29d512e79d2
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LanguageComboboxAwarePage.java
@@ -0,0 +1,44 @@
+/*
+ * 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.testsuite.pages;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+
+/**
+ * @author Marek Posolda
+ */
+public abstract class LanguageComboboxAwarePage extends AbstractPage {
+
+ @FindBy(id = "kc-current-locale-link")
+ private WebElement languageText;
+
+ @FindBy(id = "kc-locale-dropdown")
+ private WebElement localeDropdown;
+
+ public String getLanguageDropdownText() {
+ return languageText.getText();
+ }
+
+ public void openLanguage(String language){
+ WebElement langLink = localeDropdown.findElement(By.xpath("//a[text()='" + language + "']"));
+ String url = langLink.getAttribute("href");
+ driver.navigate().to(url);
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java
index 11d8fb2f7bf..b025ec74d5f 100755
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java
@@ -26,7 +26,7 @@ import org.openqa.selenium.support.FindBy;
/**
* @author Stian Thorgersen
*/
-public class LoginPage extends AbstractPage {
+public class LoginPage extends LanguageComboboxAwarePage {
@ArquillianResource
protected OAuthClient oauth;
@@ -75,12 +75,6 @@ public class LoginPage extends AbstractPage {
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);
@@ -191,14 +185,4 @@ public class LoginPage extends AbstractPage {
assertCurrent();
}
- public String getLanguageDropdownText() {
- return languageText.getText();
- }
-
- public void openLanguage(String language){
- WebElement langLink = localeDropdown.findElement(By.xpath("//a[text()='" +language +"']"));
- String url = langLink.getAttribute("href");
- driver.navigate().to(url);
- }
-
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPasswordUpdatePage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPasswordUpdatePage.java
index 93d203d97d0..7a963e13362 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPasswordUpdatePage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPasswordUpdatePage.java
@@ -22,7 +22,7 @@ import org.openqa.selenium.support.FindBy;
/**
* @author Stian Thorgersen
*/
-public class LoginPasswordUpdatePage extends AbstractPage {
+public class LoginPasswordUpdatePage extends LanguageComboboxAwarePage {
@FindBy(id = "password-new")
private WebElement newPasswordInput;
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/OAuthGrantPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/OAuthGrantPage.java
index 1a550eca9d1..cfb1f065eac 100755
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/OAuthGrantPage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/OAuthGrantPage.java
@@ -22,7 +22,7 @@ import org.openqa.selenium.support.FindBy;
/**
* @author Stian Thorgersen
*/
-public class OAuthGrantPage extends AbstractPage {
+public class OAuthGrantPage extends LanguageComboboxAwarePage {
@FindBy(css = "input[name=\"accept\"]")
private WebElement acceptButton;
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
index 4c89eaa6bbb..207a317e963 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
@@ -102,7 +102,7 @@ public class OAuthClient {
private String maxAge;
- private String responseType = OAuth2Constants.CODE;
+ private String responseType;
private String responseMode;
@@ -171,6 +171,8 @@ public class OAuthClient {
clientSessionState = null;
clientSessionHost = null;
maxAge = null;
+ responseType = OAuth2Constants.CODE;
+ responseMode = null;
nonce = null;
request = null;
requestUri = null;
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/TestCleanup.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/TestCleanup.java
index 2101c0b0468..192712e1801 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/TestCleanup.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/TestCleanup.java
@@ -17,13 +17,13 @@
package org.keycloak.testsuite.util;
-import java.util.LinkedList;
import java.util.List;
import javax.ws.rs.NotFoundException;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.common.util.ConcurrentMultivaluedHashMap;
/**
* Enlist resources to be cleaned after test method
@@ -32,18 +32,21 @@ import org.keycloak.admin.client.resource.RealmResource;
*/
public class TestCleanup {
+ private static final String IDENTITY_PROVIDER_ALIASES = "IDENTITY_PROVIDER_ALIASES";
+ private static final String USER_IDS = "USER_IDS";
+ private static final String COMPONENT_IDS = "COMPONENT_IDS";
+ private static final String CLIENT_UUIDS = "CLIENT_UUIDS";
+ private static final String ROLE_IDS = "ROLE_IDS";
+ private static final String GROUP_IDS = "GROUP_IDS";
+ private static final String AUTH_FLOW_IDS = "AUTH_FLOW_IDS";
+ private static final String AUTH_CONFIG_IDS = "AUTH_CONFIG_IDS";
+
private final Keycloak adminClient;
private final String realmName;
+ // Key is kind of entity (eg. "client", "role", "user" etc), Values are all kind of entities of given type to cleanup
+ private ConcurrentMultivaluedHashMap entities = new ConcurrentMultivaluedHashMap<>();
- private List identityProviderAliases;
- private List userIds;
- private List componentIds;
- private List clientUuids;
- private List roleIds;
- private List groupIds;
- private List authFlowIds;
- private List authConfigIds;
public TestCleanup(Keycloak adminClient, String realmName) {
this.adminClient = adminClient;
@@ -52,74 +55,49 @@ public class TestCleanup {
public void addUserId(String userId) {
- if (userIds == null) {
- userIds = new LinkedList<>();
- }
- userIds.add(userId);
+ entities.add(USER_IDS, userId);
}
public void addIdentityProviderAlias(String identityProviderAlias) {
- if (identityProviderAliases == null) {
- identityProviderAliases = new LinkedList<>();
- }
- identityProviderAliases.add(identityProviderAlias);
+ entities.add(IDENTITY_PROVIDER_ALIASES, identityProviderAlias);
}
public void addComponentId(String componentId) {
- if (componentIds == null) {
- componentIds = new LinkedList<>();
- }
- if (componentId == null) return;
- componentIds.add(componentId);
+ entities.add(COMPONENT_IDS, componentId);
}
public void addClientUuid(String clientUuid) {
- if (clientUuids == null) {
- clientUuids = new LinkedList<>();
- }
- clientUuids.add(clientUuid);
+ entities.add(CLIENT_UUIDS, clientUuid);
}
public void addRoleId(String roleId) {
- if (roleIds == null) {
- roleIds = new LinkedList<>();
- }
- roleIds.add(roleId);
+ entities.add(ROLE_IDS, roleId);
}
public void addGroupId(String groupId) {
- if (groupIds == null) {
- groupIds = new LinkedList<>();
- }
- groupIds.add(groupId);
+ entities.add(GROUP_IDS, groupId);
}
public void addAuthenticationFlowId(String flowId) {
- if (authFlowIds == null) {
- authFlowIds = new LinkedList<>();
- }
- authFlowIds.add(flowId);
+ entities.add(AUTH_FLOW_IDS, flowId);
}
public void addAuthenticationConfigId(String executionConfigId) {
- if (authConfigIds == null) {
- authConfigIds = new LinkedList<>();
- }
- authConfigIds.add(executionConfigId);
+ entities.add(AUTH_CONFIG_IDS, executionConfigId);
}
public void executeCleanup() {
- if (adminClient == null) throw new RuntimeException("ADMIN CLIENT NULL");
RealmResource realm = adminClient.realm(realmName);
+ List userIds = entities.get(USER_IDS);
if (userIds != null) {
for (String userId : userIds) {
try {
@@ -130,6 +108,7 @@ public class TestCleanup {
}
}
+ List identityProviderAliases = entities.get(IDENTITY_PROVIDER_ALIASES);
if (identityProviderAliases != null) {
for (String idpAlias : identityProviderAliases) {
try {
@@ -140,6 +119,7 @@ public class TestCleanup {
}
}
+ List componentIds = entities.get(COMPONENT_IDS);
if (componentIds != null) {
for (String componentId : componentIds) {
try {
@@ -150,6 +130,7 @@ public class TestCleanup {
}
}
+ List clientUuids = entities.get(CLIENT_UUIDS);
if (clientUuids != null) {
for (String clientUuId : clientUuids) {
try {
@@ -160,6 +141,7 @@ public class TestCleanup {
}
}
+ List roleIds = entities.get(ROLE_IDS);
if (roleIds != null) {
for (String roleId : roleIds) {
try {
@@ -170,6 +152,7 @@ public class TestCleanup {
}
}
+ List groupIds = entities.get(GROUP_IDS);
if (groupIds != null) {
for (String groupId : groupIds) {
try {
@@ -180,6 +163,7 @@ public class TestCleanup {
}
}
+ List authFlowIds = entities.get(AUTH_FLOW_IDS);
if (authFlowIds != null) {
for (String flowId : authFlowIds) {
try {
@@ -190,6 +174,7 @@ public class TestCleanup {
}
}
+ List authConfigIds = entities.get(AUTH_CONFIG_IDS);
if (authConfigIds != null) {
for (String configId : authConfigIds) {
try {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserStorageRestTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserStorageRestTest.java
index 15f0564c449..d7f494f6c15 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserStorageRestTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserStorageRestTest.java
@@ -156,6 +156,65 @@ public class UserStorageRestTest extends AbstractAdminTest {
}
+
+ // KEYCLOAK-4438
+ @Test
+ public void testKerberosAuthenticatorDisabledWhenProviderRemoved() {
+ // Assert kerberos authenticator DISABLED
+ AuthenticationExecutionInfoRepresentation kerberosExecution = findKerberosExecution();
+ Assert.assertEquals(kerberosExecution.getRequirement(), AuthenticationExecutionModel.Requirement.DISABLED.toString());
+
+ // create LDAP provider with kerberos
+ ComponentRepresentation ldapRep = new ComponentRepresentation();
+ ldapRep.setName("ldap2");
+ ldapRep.setProviderId("ldap");
+ ldapRep.setProviderType(UserStorageProvider.class.getName());
+ ldapRep.setConfig(new MultivaluedHashMap<>());
+ ldapRep.getConfig().putSingle("priority", Integer.toString(2));
+ ldapRep.getConfig().putSingle(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION, "true");
+
+
+ String id = createComponent(ldapRep);
+
+ // Assert kerberos authenticator ALTERNATIVE
+ kerberosExecution = findKerberosExecution();
+ Assert.assertEquals(kerberosExecution.getRequirement(), AuthenticationExecutionModel.Requirement.ALTERNATIVE.toString());
+
+ // Remove LDAP provider
+ realm.components().component(id).remove();
+
+ // Assert kerberos authenticator DISABLED
+ kerberosExecution = findKerberosExecution();
+ Assert.assertEquals(kerberosExecution.getRequirement(), AuthenticationExecutionModel.Requirement.DISABLED.toString());
+
+ // Add kerberos provider
+ ComponentRepresentation kerberosRep = new ComponentRepresentation();
+ kerberosRep.setName("kerberos");
+ kerberosRep.setProviderId("kerberos");
+ kerberosRep.setProviderType(UserStorageProvider.class.getName());
+ kerberosRep.setConfig(new MultivaluedHashMap<>());
+ kerberosRep.getConfig().putSingle("priority", Integer.toString(2));
+
+ id = createComponent(kerberosRep);
+
+
+ // Assert kerberos authenticator ALTERNATIVE
+ kerberosExecution = findKerberosExecution();
+ Assert.assertEquals(kerberosExecution.getRequirement(), AuthenticationExecutionModel.Requirement.ALTERNATIVE.toString());
+
+ // Switch kerberos authenticator to REQUIRED
+ kerberosExecution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED.toString());
+ realm.flows().updateExecutions("browser", kerberosExecution);
+
+ // Remove Kerberos provider
+ realm.components().component(id).remove();
+
+ // Assert kerberos authenticator DISABLED
+ kerberosExecution = findKerberosExecution();
+ Assert.assertEquals(kerberosExecution.getRequirement(), AuthenticationExecutionModel.Requirement.DISABLED.toString());
+ }
+
+
@Test
public void testValidateAndCreateLdapProvider() {
// Invalid filter
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GroupPolicyManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GroupPolicyManagementTest.java
new file mode 100644
index 00000000000..57d86a79ee0
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GroupPolicyManagementTest.java
@@ -0,0 +1,210 @@
+/*
+ * 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.testsuite.admin.client.authorization;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.core.Response;
+
+import org.junit.Test;
+import org.keycloak.admin.client.resource.AuthorizationResource;
+import org.keycloak.admin.client.resource.GroupPoliciesResource;
+import org.keycloak.admin.client.resource.GroupPolicyResource;
+import org.keycloak.admin.client.resource.PolicyResource;
+import org.keycloak.admin.client.resource.RolePoliciesResource;
+import org.keycloak.admin.client.resource.RolePolicyResource;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.GroupRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.authorization.DecisionStrategy;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.Logic;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
+import org.keycloak.testsuite.util.GroupBuilder;
+import org.keycloak.testsuite.util.RealmBuilder;
+
+/**
+ * @author Pedro Igor
+ */
+public class GroupPolicyManagementTest extends AbstractPolicyManagementTest {
+
+ @Override
+ protected RealmBuilder createTestRealm() {
+ return super.createTestRealm().group(GroupBuilder.create().name("Group A")
+ .subGroups(Arrays.asList("Group B", "Group D").stream().map(name -> {
+ if ("Group B".equals(name)) {
+ return GroupBuilder.create().name(name).subGroups(Arrays.asList("Group C", "Group E").stream().map(new Function() {
+ @Override
+ public GroupRepresentation apply(String name) {
+ return GroupBuilder.create().name(name).build();
+ }
+ }).collect(Collectors.toList())).build();
+ }
+ return GroupBuilder.create().name(name).build();
+ }).collect(Collectors.toList()))
+ .build()).group(GroupBuilder.create().name("Group E").build());
+ }
+
+ @Test
+ public void testCreate() {
+ AuthorizationResource authorization = getClient().authorization();
+ GroupPolicyRepresentation representation = new GroupPolicyRepresentation();
+
+ representation.setName("Group Policy");
+ representation.setDescription("description");
+ representation.setDecisionStrategy(DecisionStrategy.CONSENSUS);
+ representation.setLogic(Logic.NEGATIVE);
+ representation.setGroupsClaim("groups");
+ representation.addGroupPath("/Group A/Group B/Group C", true);
+ representation.addGroupPath("Group E");
+
+ assertCreated(authorization, representation);
+ }
+
+ @Test
+ public void testUpdate() {
+ AuthorizationResource authorization = getClient().authorization();
+ GroupPolicyRepresentation representation = new GroupPolicyRepresentation();
+
+ representation.setName("Update Group Policy");
+ representation.setDescription("description");
+ representation.setDecisionStrategy(DecisionStrategy.CONSENSUS);
+ representation.setLogic(Logic.NEGATIVE);
+ representation.setGroupsClaim("groups");
+ representation.addGroupPath("/Group A/Group B/Group C", true);
+ representation.addGroupPath("Group E");
+
+ assertCreated(authorization, representation);
+
+ representation.setName("changed");
+ representation.setDescription("changed");
+ representation.setDecisionStrategy(DecisionStrategy.AFFIRMATIVE);
+ representation.setLogic(Logic.POSITIVE);
+ representation.removeGroup("/Group A/Group B");
+
+ GroupPoliciesResource policies = authorization.policies().group();
+ GroupPolicyResource permission = policies.findById(representation.getId());
+
+ permission.update(representation);
+ assertRepresentation(representation, permission);
+
+ for (GroupPolicyRepresentation.GroupDefinition roleDefinition : representation.getGroups()) {
+ if (roleDefinition.getPath().equals("Group E")) {
+ roleDefinition.setExtendChildren(true);
+ }
+ }
+
+ permission.update(representation);
+ assertRepresentation(representation, permission);
+
+ representation.getGroups().clear();
+ representation.addGroupPath("/Group A/Group B");
+
+ permission.update(representation);
+ assertRepresentation(representation, permission);
+ }
+
+ @Test
+ public void testDelete() {
+ AuthorizationResource authorization = getClient().authorization();
+ GroupPolicyRepresentation representation = new GroupPolicyRepresentation();
+
+ representation.setName("Delete Group Policy");
+ representation.setGroupsClaim("groups");
+ representation.addGroupPath("/Group A/Group B/Group C", true);
+ representation.addGroupPath("Group E");
+
+ GroupPoliciesResource policies = authorization.policies().group();
+ Response response = policies.create(representation);
+ GroupPolicyRepresentation created = response.readEntity(GroupPolicyRepresentation.class);
+
+ policies.findById(created.getId()).remove();
+
+ GroupPolicyResource removed = policies.findById(created.getId());
+
+ try {
+ removed.toRepresentation();
+ fail("Permission not removed");
+ } catch (NotFoundException ignore) {
+
+ }
+ }
+
+ @Test
+ public void testGenericConfig() {
+ AuthorizationResource authorization = getClient().authorization();
+ GroupPolicyRepresentation representation = new GroupPolicyRepresentation();
+
+ representation.setName("Test Generic Config Permission");
+ representation.setGroupsClaim("groups");
+ representation.addGroupPath("/Group A");
+
+ GroupPoliciesResource policies = authorization.policies().group();
+ Response response = policies.create(representation);
+ GroupPolicyRepresentation created = response.readEntity(GroupPolicyRepresentation.class);
+
+ PolicyResource policy = authorization.policies().policy(created.getId());
+ PolicyRepresentation genericConfig = policy.toRepresentation();
+
+ assertNotNull(genericConfig.getConfig());
+ assertNotNull(genericConfig.getConfig().get("groups"));
+
+ GroupRepresentation group = getRealm().groups().groups().stream().filter(groupRepresentation -> groupRepresentation.getName().equals("Group A")).findFirst().get();
+
+ assertTrue(genericConfig.getConfig().get("groups").contains(group.getId()));
+ }
+
+ private void assertCreated(AuthorizationResource authorization, GroupPolicyRepresentation representation) {
+ GroupPoliciesResource policies = authorization.policies().group();
+ Response response = policies.create(representation);
+ GroupPolicyRepresentation created = response.readEntity(GroupPolicyRepresentation.class);
+ GroupPolicyResource policy = policies.findById(created.getId());
+ assertRepresentation(representation, policy);
+ }
+
+ private void assertRepresentation(GroupPolicyRepresentation representation, GroupPolicyResource permission) {
+ GroupPolicyRepresentation actual = permission.toRepresentation();
+ assertRepresentation(representation, actual, () -> permission.resources(), () -> Collections.emptyList(), () -> permission.associatedPolicies());
+ assertEquals(representation.getGroups().size(), actual.getGroups().size());
+ assertEquals(0, actual.getGroups().stream().filter(actualDefinition -> !representation.getGroups().stream()
+ .filter(groupDefinition -> getGroupPath(actualDefinition.getId()).equals(getCanonicalGroupPath(groupDefinition.getPath())) && actualDefinition.isExtendChildren() == groupDefinition.isExtendChildren())
+ .findFirst().isPresent())
+ .count());
+ }
+
+ private String getGroupPath(String id) {
+ return getRealm().groups().group(id).toRepresentation().getPath();
+ }
+
+ private String getCanonicalGroupPath(String path) {
+ if (path.charAt(0) == '/') {
+ return path;
+ }
+ return "/" + path;
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupNamePolicyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupNamePolicyTest.java
new file mode 100644
index 00000000000..cc4b9118f96
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupNamePolicyTest.java
@@ -0,0 +1,302 @@
+/*
+ * 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.testsuite.authz;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.AuthorizationResource;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.ClientsResource;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.authorization.client.AuthorizationDeniedException;
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.authorization.client.Configuration;
+import org.keycloak.authorization.client.representation.AuthorizationRequest;
+import org.keycloak.authorization.client.representation.AuthorizationResponse;
+import org.keycloak.authorization.client.representation.PermissionRequest;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.mappers.GroupMembershipMapper;
+import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
+import org.keycloak.representations.idm.GroupRepresentation;
+import org.keycloak.representations.idm.ProtocolMapperRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.util.AdminClientUtil;
+import org.keycloak.testsuite.util.ClientBuilder;
+import org.keycloak.testsuite.util.GroupBuilder;
+import org.keycloak.testsuite.util.RealmBuilder;
+import org.keycloak.testsuite.util.RoleBuilder;
+import org.keycloak.testsuite.util.RolesBuilder;
+import org.keycloak.testsuite.util.UserBuilder;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ * @author Pedro Igor
+ */
+public class GroupNamePolicyTest extends AbstractKeycloakTest {
+
+ @Override
+ public void addTestRealms(List testRealms) {
+ ProtocolMapperRepresentation groupProtocolMapper = new ProtocolMapperRepresentation();
+
+ groupProtocolMapper.setName("groups");
+ groupProtocolMapper.setProtocolMapper(GroupMembershipMapper.PROVIDER_ID);
+ groupProtocolMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+ groupProtocolMapper.setConsentRequired(false);
+ Map config = new HashMap<>();
+ config.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, "groups");
+ config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
+ config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
+ groupProtocolMapper.setConfig(config);
+
+ testRealms.add(RealmBuilder.create().name("authz-test")
+ .roles(RolesBuilder.create()
+ .realmRole(RoleBuilder.create().name("uma_authorization").build())
+ )
+ .group(GroupBuilder.create().name("Group A")
+ .subGroups(Arrays.asList("Group B", "Group D").stream().map(name -> {
+ if ("Group B".equals(name)) {
+ return GroupBuilder.create().name(name).subGroups(Arrays.asList("Group C", "Group E").stream().map(new Function() {
+ @Override
+ public GroupRepresentation apply(String name) {
+ return GroupBuilder.create().name(name).build();
+ }
+ }).collect(Collectors.toList())).build();
+ }
+ return GroupBuilder.create().name(name).build();
+ }).collect(Collectors.toList())).build())
+ .group(GroupBuilder.create().name("Group E").build())
+ .user(UserBuilder.create().username("marta").password("password").addRoles("uma_authorization").addGroups("Group A"))
+ .user(UserBuilder.create().username("alice").password("password").addRoles("uma_authorization"))
+ .user(UserBuilder.create().username("kolo").password("password").addRoles("uma_authorization"))
+ .client(ClientBuilder.create().clientId("resource-server-test")
+ .secret("secret")
+ .authorizationServicesEnabled(true)
+ .redirectUris("http://localhost/resource-server-test")
+ .defaultRoles("uma_protection")
+ .directAccessGrants()
+ .protocolMapper(groupProtocolMapper))
+ .build());
+ }
+
+ @Before
+ public void configureAuthorization() throws Exception {
+ createResource("Resource A");
+ createResource("Resource B");
+ createResource("Resource C");
+
+ createGroupPolicy("Only Group A Policy", "/Group A", true);
+ createGroupPolicy("Only Group B Policy", "/Group A/Group B", false);
+ createGroupPolicy("Only Group C Policy", "/Group A/Group B/Group C", false);
+
+ createResourcePermission("Resource A Permission", "Resource A", "Only Group A Policy");
+ createResourcePermission("Resource B Permission", "Resource B", "Only Group B Policy");
+ createResourcePermission("Resource C Permission", "Resource C", "Only Group C Policy");
+
+ RealmResource realm = getRealm();
+ GroupRepresentation group = getGroup("/Group A/Group B/Group C");
+ UserRepresentation user = realm.users().search("kolo").get(0);
+
+ realm.users().get(user.getId()).joinGroup(group.getId());
+
+ group = getGroup("/Group A/Group B");
+ user = realm.users().search("alice").get(0);
+
+ realm.users().get(user.getId()).joinGroup(group.getId());
+ }
+
+ @Test
+ public void testExactNameMatch() {
+ AuthzClient authzClient = getAuthzClient();
+ PermissionRequest request = new PermissionRequest();
+
+ request.setResourceSetName("Resource A");
+
+ String ticket = authzClient.protection().permission().forResource(request).getTicket();
+ AuthorizationResponse response = authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
+
+ assertNotNull(response.getRpt());
+
+ try {
+ authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
+ fail("Should fail because user is not granted with expected group");
+ } catch (AuthorizationDeniedException ignore) {
+
+ }
+
+ try {
+ authzClient.authorization("alice", "password").authorize(new AuthorizationRequest(ticket));
+ fail("Should fail because user is not granted with expected group");
+ } catch (AuthorizationDeniedException ignore) {
+
+ }
+ }
+
+ @Test
+ public void testOnlyChildrenPolicy() throws Exception {
+ RealmResource realm = getRealm();
+ AuthzClient authzClient = getAuthzClient();
+ PermissionRequest request = new PermissionRequest();
+
+ request.setResourceSetName("Resource B");
+
+ String ticket = authzClient.protection().permission().forResource(request).getTicket();
+
+ try {
+ authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
+ fail("Should fail because user is not granted with expected group");
+ } catch (AuthorizationDeniedException ignore) {
+
+ }
+
+ AuthorizationResponse response = authzClient.authorization("alice", "password").authorize(new AuthorizationRequest(ticket));
+
+ assertNotNull(response.getRpt());
+
+ try {
+ authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
+ fail("Should fail because user is not granted with expected role");
+ } catch (AuthorizationDeniedException ignore) {
+
+ }
+
+ request = new PermissionRequest();
+
+ request.setResourceSetName("Resource C");
+
+ ticket = authzClient.protection().permission().forResource(request).getTicket();
+
+ response = authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
+
+ assertNotNull(response.getRpt());
+ }
+
+ private void createGroupPolicy(String name, String groupPath, boolean extendChildren) {
+ GroupPolicyRepresentation policy = new GroupPolicyRepresentation();
+
+ policy.setName(name);
+ policy.setGroupsClaim("groups");
+ policy.addGroupPath(groupPath, extendChildren);
+
+ getClient().authorization().policies().group().create(policy);
+ }
+
+ private void createResourcePermission(String name, String resource, String... policies) {
+ ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
+
+ permission.setName(name);
+ permission.addResource(resource);
+ permission.addPolicy(policies);
+
+ getClient().authorization().permissions().resource().create(permission);
+ }
+
+ private void createResource(String name) {
+ AuthorizationResource authorization = getClient().authorization();
+ ResourceRepresentation resource = new ResourceRepresentation(name);
+
+ authorization.resources().create(resource);
+ }
+
+ private RealmResource getRealm() {
+ try {
+ return AdminClientUtil.createAdminClient().realm("authz-test");
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to create admin client");
+ }
+ }
+
+ private ClientResource getClient(RealmResource realm) {
+ ClientsResource clients = realm.clients();
+ return clients.findByClientId("resource-server-test").stream().map(representation -> clients.get(representation.getId())).findFirst().orElseThrow(() -> new RuntimeException("Expected client [resource-server-test]"));
+ }
+
+ private AuthzClient getAuthzClient() {
+ try {
+ return AuthzClient.create(JsonSerialization.readValue(getClass().getResourceAsStream("/authorization-test/default-keycloak.json"), Configuration.class));
+ } catch (IOException cause) {
+ throw new RuntimeException("Failed to create authz client", cause);
+ }
+ }
+
+ private ClientResource getClient() {
+ return getClient(getRealm());
+ }
+
+ private GroupRepresentation getGroup(String path) {
+ String[] parts = path.split("/");
+ RealmResource realm = getRealm();
+ GroupRepresentation parent = null;
+
+ for (String part : parts) {
+ if ("".equals(part)) {
+ continue;
+ }
+ if (parent == null) {
+ parent = realm.groups().groups().stream().filter(new Predicate() {
+ @Override
+ public boolean test(GroupRepresentation groupRepresentation) {
+ return part.equals(groupRepresentation.getName());
+ }
+ }).findFirst().get();
+ continue;
+ }
+
+ GroupRepresentation group = getGroup(part, parent.getSubGroups());
+
+ if (path.endsWith(group.getName())) {
+ return group;
+ }
+
+ parent = group;
+ }
+
+ return null;
+ }
+
+ private GroupRepresentation getGroup(String name, List groups) {
+ for (GroupRepresentation group : groups) {
+ if (name.equals(group.getName())) {
+ return group;
+ }
+
+ GroupRepresentation child = getGroup(name, group.getSubGroups());
+
+ if (child != null && name.equals(child.getName())) {
+ return child;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupPathPolicyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupPathPolicyTest.java
new file mode 100644
index 00000000000..19f74b42fc7
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupPathPolicyTest.java
@@ -0,0 +1,284 @@
+/*
+ * 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.testsuite.authz;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.AuthorizationResource;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.ClientsResource;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.authorization.client.AuthorizationDeniedException;
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.authorization.client.Configuration;
+import org.keycloak.authorization.client.representation.AuthorizationRequest;
+import org.keycloak.authorization.client.representation.AuthorizationResponse;
+import org.keycloak.authorization.client.representation.PermissionRequest;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.mappers.GroupMembershipMapper;
+import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
+import org.keycloak.representations.idm.GroupRepresentation;
+import org.keycloak.representations.idm.ProtocolMapperRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.util.AdminClientUtil;
+import org.keycloak.testsuite.util.ClientBuilder;
+import org.keycloak.testsuite.util.GroupBuilder;
+import org.keycloak.testsuite.util.RealmBuilder;
+import org.keycloak.testsuite.util.RoleBuilder;
+import org.keycloak.testsuite.util.RolesBuilder;
+import org.keycloak.testsuite.util.UserBuilder;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ * @author Pedro Igor
+ */
+public class GroupPathPolicyTest extends AbstractKeycloakTest {
+
+ @Override
+ public void addTestRealms(List testRealms) {
+ ProtocolMapperRepresentation groupProtocolMapper = new ProtocolMapperRepresentation();
+
+ groupProtocolMapper.setName("groups");
+ groupProtocolMapper.setProtocolMapper(GroupMembershipMapper.PROVIDER_ID);
+ groupProtocolMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+ groupProtocolMapper.setConsentRequired(false);
+ Map config = new HashMap<>();
+ config.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, "groups");
+ config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
+ config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
+ config.put("full.path", "true");
+ groupProtocolMapper.setConfig(config);
+
+ testRealms.add(RealmBuilder.create().name("authz-test")
+ .roles(RolesBuilder.create()
+ .realmRole(RoleBuilder.create().name("uma_authorization").build())
+ )
+ .group(GroupBuilder.create().name("Group A")
+ .subGroups(Arrays.asList("Group B", "Group D").stream().map(name -> {
+ if ("Group B".equals(name)) {
+ return GroupBuilder.create().name(name).subGroups(Arrays.asList("Group C", "Group E").stream().map(new Function() {
+ @Override
+ public GroupRepresentation apply(String name) {
+ return GroupBuilder.create().name(name).build();
+ }
+ }).collect(Collectors.toList())).build();
+ }
+ return GroupBuilder.create().name(name).build();
+ }).collect(Collectors.toList())).build())
+ .group(GroupBuilder.create().name("Group E").build())
+ .user(UserBuilder.create().username("marta").password("password").addRoles("uma_authorization").addGroups("Group A"))
+ .user(UserBuilder.create().username("alice").password("password").addRoles("uma_authorization"))
+ .user(UserBuilder.create().username("kolo").password("password").addRoles("uma_authorization"))
+ .client(ClientBuilder.create().clientId("resource-server-test")
+ .secret("secret")
+ .authorizationServicesEnabled(true)
+ .redirectUris("http://localhost/resource-server-test")
+ .defaultRoles("uma_protection")
+ .directAccessGrants()
+ .protocolMapper(groupProtocolMapper))
+ .build());
+ }
+
+ @Before
+ public void configureAuthorization() throws Exception {
+ createResource("Resource A");
+ createResource("Resource B");
+
+ createGroupPolicy("Parent And Children Policy", "/Group A", true);
+ createGroupPolicy("Only Children Policy", "/Group A/Group B/Group C", false);
+
+ createResourcePermission("Resource A Permission", "Resource A", "Parent And Children Policy");
+ createResourcePermission("Resource B Permission", "Resource B", "Only Children Policy");
+ }
+
+ @Test
+ public void testAllowParentAndChildren() {
+ AuthzClient authzClient = getAuthzClient();
+ PermissionRequest request = new PermissionRequest();
+
+ request.setResourceSetName("Resource A");
+
+ String ticket = authzClient.protection().permission().forResource(request).getTicket();
+ AuthorizationResponse response = authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
+
+ assertNotNull(response.getRpt());
+
+ RealmResource realm = getRealm();
+ GroupRepresentation group = getGroup("/Group A/Group B/Group C");
+ UserRepresentation user = realm.users().search("kolo").get(0);
+
+ realm.users().get(user.getId()).joinGroup(group.getId());
+
+ ticket = authzClient.protection().permission().forResource(request).getTicket();
+ response = authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
+
+ assertNotNull(response.getRpt());
+ }
+
+ @Test
+ public void testOnlyChildrenPolicy() throws Exception {
+ RealmResource realm = getRealm();
+ AuthzClient authzClient = getAuthzClient();
+ PermissionRequest request = new PermissionRequest();
+
+ request.setResourceSetName("Resource B");
+
+ String ticket = authzClient.protection().permission().forResource(request).getTicket();
+
+ try {
+ authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
+ fail("Should fail because user is not granted with expected role");
+ } catch (AuthorizationDeniedException ignore) {
+
+ }
+
+ GroupRepresentation group = getGroup("/Group A/Group B/Group C");
+ UserRepresentation user = realm.users().search("kolo").get(0);
+
+ realm.users().get(user.getId()).joinGroup(group.getId());
+
+ AuthorizationResponse response = authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
+
+ assertNotNull(response.getRpt());
+
+ try {
+ authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
+ fail("Should fail because user is not granted with expected role");
+ } catch (AuthorizationDeniedException ignore) {
+
+ }
+ }
+
+ private void createGroupPolicy(String name, String groupPath, boolean extendChildren) {
+ GroupPolicyRepresentation policy = new GroupPolicyRepresentation();
+
+ policy.setName(name);
+ policy.setGroupsClaim("groups");
+ policy.addGroupPath(groupPath, extendChildren);
+
+ getClient().authorization().policies().group().create(policy);
+ }
+
+ private void createResourcePermission(String name, String resource, String... policies) {
+ ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
+
+ permission.setName(name);
+ permission.addResource(resource);
+ permission.addPolicy(policies);
+
+ getClient().authorization().permissions().resource().create(permission);
+ }
+
+ private void createResource(String name) {
+ AuthorizationResource authorization = getClient().authorization();
+ ResourceRepresentation resource = new ResourceRepresentation(name);
+
+ authorization.resources().create(resource);
+ }
+
+ private RealmResource getRealm() {
+ try {
+ return AdminClientUtil.createAdminClient().realm("authz-test");
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to create admin client");
+ }
+ }
+
+ private ClientResource getClient(RealmResource realm) {
+ ClientsResource clients = realm.clients();
+ return clients.findByClientId("resource-server-test").stream().map(representation -> clients.get(representation.getId())).findFirst().orElseThrow(() -> new RuntimeException("Expected client [resource-server-test]"));
+ }
+
+ private AuthzClient getAuthzClient() {
+ try {
+ return AuthzClient.create(JsonSerialization.readValue(getClass().getResourceAsStream("/authorization-test/default-keycloak.json"), Configuration.class));
+ } catch (IOException cause) {
+ throw new RuntimeException("Failed to create authz client", cause);
+ }
+ }
+
+ private ClientResource getClient() {
+ return getClient(getRealm());
+ }
+
+ private GroupRepresentation getGroup(String path) {
+ String[] parts = path.split("/");
+ RealmResource realm = getRealm();
+ GroupRepresentation parent = null;
+
+ for (String part : parts) {
+ if ("".equals(part)) {
+ continue;
+ }
+ if (parent == null) {
+ parent = realm.groups().groups().stream().filter(new Predicate() {
+ @Override
+ public boolean test(GroupRepresentation groupRepresentation) {
+ return part.equals(groupRepresentation.getName());
+ }
+ }).findFirst().get();
+ continue;
+ }
+
+ GroupRepresentation group = getGroup(part, parent.getSubGroups());
+
+ if (path.endsWith(group.getName())) {
+ return group;
+ }
+
+ parent = group;
+ }
+
+ return null;
+ }
+
+ private GroupRepresentation getGroup(String name, List groups) {
+ for (GroupRepresentation group : groups) {
+ if (name.equals(group.getName())) {
+ return group;
+ }
+
+ GroupRepresentation child = getGroup(name, group.getSubGroups());
+
+ if (child != null && name.equals(child.getName())) {
+ return child;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java
index 84527f74b13..2baa336cf96 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java
@@ -19,13 +19,20 @@ package org.keycloak.testsuite.crossdc;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.events.log.JBossLoggingEventListenerProviderFactory;
import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.Retry;
+import org.keycloak.testsuite.arquillian.InfinispanStatistics;
import org.keycloak.testsuite.events.EventsListenerProviderFactory;
import org.keycloak.testsuite.util.TestCleanup;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import org.hamcrest.Matcher;
import org.junit.Before;
+import static org.junit.Assert.assertThat;
/**
*
@@ -78,4 +85,31 @@ public abstract class AbstractAdminCrossDCTest extends AbstractCrossDCTest {
protected TestCleanup getCleanup() {
return getCleanup(REALM_NAME);
}
+
+ protected void assertSingleStatistics(InfinispanStatistics stats, String key, Runnable testedCode, Function> matcherOnOldStat) {
+ stats.reset();
+
+ T oldStat = (T) stats.getSingleStatistics(key);
+ testedCode.run();
+
+ Retry.execute(() -> {
+ T newStat = (T) stats.getSingleStatistics(key);
+
+ Matcher super T> matcherInstance = matcherOnOldStat.apply(oldStat);
+ assertThat(newStat, matcherInstance);
+ }, 5, 200);
+ }
+
+ protected void assertStatistics(InfinispanStatistics stats, Runnable testedCode, BiConsumer