mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-05 22:39:52 -06:00
merge
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
package org.keycloak.adapters.springsecurity;
|
||||
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.TYPE;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
/**
|
||||
* Add this annotation to a class that extends {@code KeycloakWebSecurityConfigurerAdapter} to provide
|
||||
* a keycloak based Spring security configuration.
|
||||
*
|
||||
* @author Hendrik Ebbers
|
||||
*/
|
||||
@Retention(value = RUNTIME)
|
||||
@Target(value = { TYPE })
|
||||
@Configuration
|
||||
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
|
||||
@EnableWebSecurity
|
||||
public @interface KeycloakConfiguration {
|
||||
}
|
||||
@@ -61,15 +61,19 @@ import org.springframework.util.Assert;
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter implements ApplicationContextAware {
|
||||
|
||||
|
||||
public static final String DEFAULT_LOGIN_URL = "/sso/login";
|
||||
public static final String AUTHORIZATION_HEADER = "Authorization";
|
||||
public static final String SCHEME_BEARER = "bearer ";
|
||||
public static final String SCHEME_BASIC = "basic ";
|
||||
|
||||
|
||||
/**
|
||||
* Request matcher that matches all requests.
|
||||
* Request matcher that matches requests to the {@link KeycloakAuthenticationEntryPoint#DEFAULT_LOGIN_URI default login URI}
|
||||
* and any request with a <code>Authorization</code> header.
|
||||
*/
|
||||
private static RequestMatcher DEFAULT_REQUEST_MATCHER = new AntPathRequestMatcher("/**");
|
||||
public static final RequestMatcher DEFAULT_REQUEST_MATCHER =
|
||||
new OrRequestMatcher(new AntPathRequestMatcher(DEFAULT_LOGIN_URL), new RequestHeaderRequestMatcher(AUTHORIZATION_HEADER));
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(KeycloakAuthenticationProcessingFilter.class);
|
||||
|
||||
@@ -107,7 +111,7 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati
|
||||
*
|
||||
*/
|
||||
public KeycloakAuthenticationProcessingFilter(AuthenticationManager authenticationManager, RequestMatcher
|
||||
requiresAuthenticationRequestMatcher) {
|
||||
requiresAuthenticationRequestMatcher) {
|
||||
super(requiresAuthenticationRequestMatcher);
|
||||
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
|
||||
this.authenticationManager = authenticationManager;
|
||||
@@ -138,20 +142,27 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati
|
||||
log.debug("Auth outcome: {}", result);
|
||||
|
||||
if (AuthOutcome.FAILED.equals(result)) {
|
||||
AuthChallenge challenge = authenticator.getChallenge();
|
||||
AuthChallenge challenge = authenticator.getChallenge();
|
||||
if (challenge != null) {
|
||||
challenge.challenge(facade);
|
||||
}
|
||||
throw new KeycloakAuthenticationException("Invalid authorization header, see WWW-Authenticate header for details");
|
||||
}
|
||||
|
||||
if (AuthOutcome.NOT_ATTEMPTED.equals(result)) {
|
||||
AuthChallenge challenge = authenticator.getChallenge();
|
||||
AuthChallenge challenge = authenticator.getChallenge();
|
||||
if (challenge != null) {
|
||||
challenge.challenge(facade);
|
||||
}
|
||||
throw new KeycloakAuthenticationException("Authorization header not found, see WWW-Authenticate header");
|
||||
if (deployment.isBearerOnly()) {
|
||||
// no redirection in this mode, throwing exception for the spring handler
|
||||
throw new KeycloakAuthenticationException("Authorization header not found, see WWW-Authenticate header");
|
||||
} else {
|
||||
// let continue if challenged, it may redirect
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
else if (AuthOutcome.AUTHENTICATED.equals(result)) {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
Assert.notNull(authentication, "Authentication SecurityContextHolder was null");
|
||||
@@ -193,7 +204,7 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati
|
||||
|
||||
@Override
|
||||
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
|
||||
Authentication authResult) throws IOException, ServletException {
|
||||
Authentication authResult) throws IOException, ServletException {
|
||||
|
||||
if (!(this.isBearerTokenRequest(request) || this.isBasicAuthRequest(request))) {
|
||||
super.successfulAuthentication(request, response, chain, authResult);
|
||||
@@ -220,10 +231,10 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException failed) throws IOException, ServletException {
|
||||
super.unsuccessfulAuthentication(request, response, failed);
|
||||
}
|
||||
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException failed) throws IOException, ServletException {
|
||||
super.unsuccessfulAuthentication(request, response, failed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
@@ -259,4 +270,4 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati
|
||||
public final void setContinueChainBeforeSuccessfulAuthentication(boolean continueChainBeforeSuccessfulAuthentication) {
|
||||
throw new UnsupportedOperationException("This filter does not support explicitly setting a continue chain before success policy");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,12 +159,10 @@ public class KeycloakAuthenticationProcessingFilterTest {
|
||||
when(keycloakDeployment.getStateCookieName()).thenReturn("kc-cookie");
|
||||
when(keycloakDeployment.getSslRequired()).thenReturn(SslRequired.NONE);
|
||||
when(keycloakDeployment.isBearerOnly()).thenReturn(Boolean.FALSE);
|
||||
try {
|
||||
filter.attemptAuthentication(request, response);
|
||||
} catch (KeycloakAuthenticationException e) {
|
||||
verify(response).setStatus(302);
|
||||
verify(response).setHeader(eq("Location"), startsWith("http://localhost:8080/auth"));
|
||||
}
|
||||
|
||||
filter.attemptAuthentication(request, response);
|
||||
verify(response).setStatus(302);
|
||||
verify(response).setHeader(eq("Location"), startsWith("http://localhost:8080/auth"));
|
||||
}
|
||||
|
||||
@Test(expected = KeycloakAuthenticationException.class)
|
||||
@@ -173,6 +171,13 @@ public class KeycloakAuthenticationProcessingFilterTest {
|
||||
filter.attemptAuthentication(request, response);
|
||||
}
|
||||
|
||||
@Test(expected = KeycloakAuthenticationException.class)
|
||||
public void testAttemptAuthenticationWithInvalidTokenBearerOnly() throws Exception {
|
||||
when(keycloakDeployment.isBearerOnly()).thenReturn(Boolean.TRUE);
|
||||
request.addHeader("Authorization", "Bearer xxx");
|
||||
filter.attemptAuthentication(request, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccessfulAuthenticationInteractive() throws Exception {
|
||||
Authentication authentication = new KeycloakAuthenticationToken(keycloakAccount, authorities);
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.authorization.policy.provider.group;
|
||||
|
||||
import static org.keycloak.models.utils.ModelToRepresentation.buildGroupPath;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.keycloak.authorization.attribute.Attributes;
|
||||
import org.keycloak.authorization.model.Policy;
|
||||
import org.keycloak.authorization.policy.evaluation.Evaluation;
|
||||
import org.keycloak.authorization.policy.provider.PolicyProvider;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.utils.ModelToRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public class GroupPolicyProvider implements PolicyProvider {
|
||||
|
||||
private final Function<Policy, GroupPolicyRepresentation> representationFunction;
|
||||
|
||||
public GroupPolicyProvider(Function<Policy, GroupPolicyRepresentation> representationFunction) {
|
||||
this.representationFunction = representationFunction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void evaluate(Evaluation evaluation) {
|
||||
GroupPolicyRepresentation policy = representationFunction.apply(evaluation.getPolicy());
|
||||
RealmModel realm = evaluation.getAuthorizationProvider().getRealm();
|
||||
Attributes.Entry groupsClaim = evaluation.getContext().getIdentity().getAttributes().getValue(policy.getGroupsClaim());
|
||||
|
||||
if (groupsClaim == null || groupsClaim.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (GroupPolicyRepresentation.GroupDefinition definition : policy.getGroups()) {
|
||||
GroupModel allowedGroup = realm.getGroupById(definition.getId());
|
||||
|
||||
for (int i = 0; i < groupsClaim.size(); i++) {
|
||||
String group = groupsClaim.asString(i);
|
||||
|
||||
if (group.indexOf('/') != -1) {
|
||||
String allowedGroupPath = buildGroupPath(allowedGroup);
|
||||
if (group.equals(allowedGroupPath) || (definition.isExtendChildren() && group.startsWith(allowedGroupPath))) {
|
||||
evaluation.grant();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// in case the group from the claim does not represent a path, we just check an exact name match
|
||||
if (group.equals(allowedGroup.getName())) {
|
||||
evaluation.grant();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* 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.authorization.policy.provider.group;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.authorization.AuthorizationProvider;
|
||||
import org.keycloak.authorization.model.Policy;
|
||||
import org.keycloak.authorization.policy.provider.PolicyProvider;
|
||||
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.utils.ModelToRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public class GroupPolicyProviderFactory implements PolicyProviderFactory<GroupPolicyRepresentation> {
|
||||
|
||||
private GroupPolicyProvider provider = new GroupPolicyProvider(policy -> toRepresentation(policy, new GroupPolicyRepresentation()));
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "group";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Group";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroup() {
|
||||
return "Identity Based";
|
||||
}
|
||||
|
||||
@Override
|
||||
public PolicyProvider create(AuthorizationProvider authorization) {
|
||||
return provider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PolicyProvider create(KeycloakSession session) {
|
||||
return provider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupPolicyRepresentation toRepresentation(Policy policy, GroupPolicyRepresentation representation) {
|
||||
representation.setGroupsClaim(policy.getConfig().get("groupsClaim"));
|
||||
try {
|
||||
representation.setGroups(getGroupsDefinition(policy.getConfig()));
|
||||
} catch (IOException cause) {
|
||||
throw new RuntimeException("Failed to deserialize groups", cause);
|
||||
}
|
||||
return representation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<GroupPolicyRepresentation> getRepresentationType() {
|
||||
return GroupPolicyRepresentation.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Policy policy, GroupPolicyRepresentation representation, AuthorizationProvider authorization) {
|
||||
updatePolicy(policy, representation.getGroupsClaim(), representation.getGroups(), authorization);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdate(Policy policy, GroupPolicyRepresentation representation, AuthorizationProvider authorization) {
|
||||
updatePolicy(policy, representation.getGroupsClaim(), representation.getGroups(), authorization);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onImport(Policy policy, PolicyRepresentation representation, AuthorizationProvider authorization) {
|
||||
try {
|
||||
updatePolicy(policy, representation.getConfig().get("groupsClaim"), getGroupsDefinition(representation.getConfig()), authorization);
|
||||
} catch (IOException cause) {
|
||||
throw new RuntimeException("Failed to deserialize groups", cause);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExport(Policy policy, PolicyRepresentation representation, AuthorizationProvider authorizationProvider) {
|
||||
Map<String, String> config = new HashMap<>();
|
||||
GroupPolicyRepresentation groupPolicy = toRepresentation(policy, new GroupPolicyRepresentation());
|
||||
Set<GroupPolicyRepresentation.GroupDefinition> groups = groupPolicy.getGroups();
|
||||
|
||||
for (GroupPolicyRepresentation.GroupDefinition definition: groups) {
|
||||
GroupModel group = authorizationProvider.getRealm().getGroupById(definition.getId());
|
||||
definition.setId(null);
|
||||
definition.setPath(ModelToRepresentation.buildGroupPath(group));
|
||||
}
|
||||
|
||||
try {
|
||||
config.put("groupsClaim", groupPolicy.getGroupsClaim());
|
||||
config.put("groups", JsonSerialization.writeValueAsString(groups));
|
||||
} catch (IOException cause) {
|
||||
throw new RuntimeException("Failed to export group policy [" + policy.getName() + "]", cause);
|
||||
}
|
||||
|
||||
representation.setConfig(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
factory.register(event -> {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
private void updatePolicy(Policy policy, String groupsClaim, Set<GroupPolicyRepresentation.GroupDefinition> groups, AuthorizationProvider authorization) {
|
||||
if (groupsClaim == null) {
|
||||
throw new RuntimeException("Group claims property not provided");
|
||||
}
|
||||
|
||||
if (groups == null || groups.isEmpty()) {
|
||||
throw new RuntimeException("You must provide at least one group");
|
||||
}
|
||||
|
||||
Map<String, String> config = new HashMap<>(policy.getConfig());
|
||||
|
||||
config.put("groupsClaim", groupsClaim);
|
||||
|
||||
List<GroupModel> topLevelGroups = authorization.getRealm().getTopLevelGroups();
|
||||
|
||||
for (GroupPolicyRepresentation.GroupDefinition definition : groups) {
|
||||
GroupModel group = null;
|
||||
|
||||
if (definition.getId() != null) {
|
||||
group = authorization.getRealm().getGroupById(definition.getId());
|
||||
}
|
||||
|
||||
if (group == null) {
|
||||
String path = definition.getPath();
|
||||
String canonicalPath = path.startsWith("/") ? path.substring(1, path.length()) : path;
|
||||
|
||||
if (canonicalPath != null) {
|
||||
String[] parts = canonicalPath.split("/");
|
||||
GroupModel parent = null;
|
||||
|
||||
for (String part : parts) {
|
||||
if (parent == null) {
|
||||
parent = topLevelGroups.stream().filter(groupModel -> groupModel.getName().equals(part)).findFirst().orElseThrow(() -> new RuntimeException("Top level group with name [" + part + "] not found"));
|
||||
} else {
|
||||
group = parent.getSubGroups().stream().filter(groupModel -> groupModel.getName().equals(part)).findFirst().orElseThrow(() -> new RuntimeException("Group with name [" + part + "] not found"));
|
||||
parent = group;
|
||||
}
|
||||
}
|
||||
|
||||
if (parts.length == 1) {
|
||||
group = parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (group == null) {
|
||||
throw new RuntimeException("Group with id [" + definition.getId() + "] not found");
|
||||
}
|
||||
|
||||
definition.setId(group.getId());
|
||||
definition.setPath(null);
|
||||
}
|
||||
|
||||
try {
|
||||
config.put("groups", JsonSerialization.writeValueAsString(groups));
|
||||
} catch (IOException cause) {
|
||||
throw new RuntimeException("Failed to serialize groups", cause);
|
||||
}
|
||||
|
||||
policy.setConfig(config);
|
||||
}
|
||||
|
||||
private HashSet<GroupPolicyRepresentation.GroupDefinition> getGroupsDefinition(Map<String, String> config) throws IOException {
|
||||
return new HashSet<>(Arrays.asList(JsonSerialization.readValue(config.get("groups"), GroupPolicyRepresentation.GroupDefinition[].class)));
|
||||
}
|
||||
}
|
||||
@@ -17,43 +17,44 @@
|
||||
*/
|
||||
package org.keycloak.authorization.policy.provider.js;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.script.ScriptEngine;
|
||||
import javax.script.ScriptException;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import org.keycloak.authorization.AuthorizationProvider;
|
||||
import org.keycloak.authorization.model.Policy;
|
||||
import org.keycloak.authorization.policy.evaluation.Evaluation;
|
||||
import org.keycloak.authorization.policy.provider.PolicyProvider;
|
||||
import org.keycloak.scripting.EvaluatableScriptAdapter;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public class JSPolicyProvider implements PolicyProvider {
|
||||
class JSPolicyProvider implements PolicyProvider {
|
||||
|
||||
private Supplier<ScriptEngine> engineProvider;
|
||||
private final BiFunction<AuthorizationProvider, Policy, EvaluatableScriptAdapter> evaluatableScript;
|
||||
|
||||
public JSPolicyProvider(Supplier<ScriptEngine> engineProvider) {
|
||||
this.engineProvider = engineProvider;
|
||||
JSPolicyProvider(final BiFunction<AuthorizationProvider, Policy, EvaluatableScriptAdapter> evaluatableScript) {
|
||||
this.evaluatableScript = evaluatableScript;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void evaluate(Evaluation evaluation) {
|
||||
ScriptEngine engine = engineProvider.get();
|
||||
|
||||
engine.put("$evaluation", evaluation);
|
||||
|
||||
Policy policy = evaluation.getPolicy();
|
||||
AuthorizationProvider authorization = evaluation.getAuthorizationProvider();
|
||||
final EvaluatableScriptAdapter adapter = evaluatableScript.apply(authorization, policy);
|
||||
|
||||
try {
|
||||
engine.eval(policy.getConfig().get("code"));
|
||||
} catch (ScriptException e) {
|
||||
//how to deal with long running scripts -> timeout?
|
||||
adapter.eval(bindings -> {
|
||||
bindings.put("script", adapter.getScriptModel());
|
||||
bindings.put("$evaluation", evaluation);
|
||||
});
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new RuntimeException("Error evaluating JS Policy [" + policy.getName() + "].", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package org.keycloak.authorization.policy.provider.js;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import javax.script.ScriptEngineManager;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.authorization.AuthorizationProvider;
|
||||
import org.keycloak.authorization.model.Policy;
|
||||
@@ -11,17 +7,20 @@ import org.keycloak.authorization.policy.provider.PolicyProvider;
|
||||
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.ScriptModel;
|
||||
import org.keycloak.representations.idm.authorization.JSPolicyRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
|
||||
import org.keycloak.scripting.EvaluatableScriptAdapter;
|
||||
import org.keycloak.scripting.ScriptingProvider;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public class JSPolicyProviderFactory implements PolicyProviderFactory<JSPolicyRepresentation> {
|
||||
|
||||
private static final String ENGINE = "nashorn";
|
||||
|
||||
private JSPolicyProvider provider = new JSPolicyProvider(() -> new ScriptEngineManager().getEngineByName(ENGINE));
|
||||
private final JSPolicyProvider provider = new JSPolicyProvider(this::getEvaluatableScript);
|
||||
private ScriptCache scriptCache;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
@@ -69,13 +68,16 @@ public class JSPolicyProviderFactory implements PolicyProviderFactory<JSPolicyRe
|
||||
updatePolicy(policy, representation.getConfig().get("code"));
|
||||
}
|
||||
|
||||
private void updatePolicy(Policy policy, String code) {
|
||||
policy.putConfig("code", code);
|
||||
@Override
|
||||
public void onRemove(final Policy policy, final AuthorizationProvider authorization) {
|
||||
scriptCache.remove(policy.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
|
||||
int maxEntries = Integer.parseInt(config.get("cache-max-entries", "100"));
|
||||
int maxAge = Integer.parseInt(config.get("cache-entry-max-age", "-1"));
|
||||
scriptCache = new ScriptCache(maxEntries, maxAge);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -92,4 +94,26 @@ public class JSPolicyProviderFactory implements PolicyProviderFactory<JSPolicyRe
|
||||
public String getId() {
|
||||
return "js";
|
||||
}
|
||||
|
||||
private EvaluatableScriptAdapter getEvaluatableScript(final AuthorizationProvider authz, final Policy policy) {
|
||||
return scriptCache.computeIfAbsent(policy.getId(), id -> {
|
||||
final ScriptingProvider scripting = authz.getKeycloakSession().getProvider(ScriptingProvider.class);
|
||||
ScriptModel script = getScriptModel(policy, authz.getRealm(), scripting);
|
||||
return scripting.prepareEvaluatableScript(script);
|
||||
});
|
||||
}
|
||||
|
||||
private ScriptModel getScriptModel(final Policy policy, final RealmModel realm, final ScriptingProvider scripting) {
|
||||
String scriptName = policy.getName();
|
||||
String scriptCode = policy.getConfig().get("code");
|
||||
String scriptDescription = policy.getDescription();
|
||||
|
||||
//TODO lookup script by scriptId instead of creating it every time
|
||||
return scripting.createScript(realm.getId(), ScriptModel.TEXT_JAVASCRIPT, scriptName, scriptCode, scriptDescription);
|
||||
}
|
||||
|
||||
private void updatePolicy(Policy policy, String code) {
|
||||
scriptCache.remove(policy.getId());
|
||||
policy.putConfig("code", code);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* 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.authorization.policy.provider.js;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.keycloak.scripting.EvaluatableScriptAdapter;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public class ScriptCache {
|
||||
|
||||
/**
|
||||
* The load factor.
|
||||
*/
|
||||
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
|
||||
|
||||
private final Map<String, CacheEntry> cache;
|
||||
|
||||
private final AtomicBoolean writing = new AtomicBoolean(false);
|
||||
|
||||
private final long maxAge;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @param maxEntries the maximum number of entries to keep in the cache
|
||||
*/
|
||||
public ScriptCache(int maxEntries) {
|
||||
this(maxEntries, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @param maxEntries the maximum number of entries to keep in the cache
|
||||
* @param maxAge the time in milliseconds that an entry can stay in the cache. If {@code -1}, entries never expire
|
||||
*/
|
||||
public ScriptCache(final int maxEntries, long maxAge) {
|
||||
cache = new LinkedHashMap<String, CacheEntry>(16, DEFAULT_LOAD_FACTOR, true) {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry eldest) {
|
||||
return cache.size() > maxEntries;
|
||||
}
|
||||
};
|
||||
this.maxAge = maxAge;
|
||||
}
|
||||
|
||||
public EvaluatableScriptAdapter computeIfAbsent(String id, Function<String, EvaluatableScriptAdapter> function) {
|
||||
try {
|
||||
if (parkForWriteAndCheckInterrupt()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
CacheEntry entry = cache.computeIfAbsent(id, key -> new CacheEntry(key, function.apply(id), maxAge));
|
||||
|
||||
if (entry != null) {
|
||||
return entry.value();
|
||||
}
|
||||
|
||||
return null;
|
||||
} finally {
|
||||
writing.lazySet(false);
|
||||
}
|
||||
}
|
||||
|
||||
public EvaluatableScriptAdapter get(String uri) {
|
||||
if (parkForReadAndCheckInterrupt()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
CacheEntry cached = cache.get(uri);
|
||||
|
||||
if (cached != null) {
|
||||
return removeIfExpired(cached);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void remove(String key) {
|
||||
try {
|
||||
if (parkForWriteAndCheckInterrupt()) {
|
||||
return;
|
||||
}
|
||||
|
||||
cache.remove(key);
|
||||
} finally {
|
||||
writing.lazySet(false);
|
||||
}
|
||||
}
|
||||
|
||||
private EvaluatableScriptAdapter removeIfExpired(CacheEntry cached) {
|
||||
if (cached == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (cached.isExpired()) {
|
||||
remove(cached.key());
|
||||
return null;
|
||||
}
|
||||
|
||||
return cached.value();
|
||||
}
|
||||
|
||||
private boolean parkForWriteAndCheckInterrupt() {
|
||||
while (!writing.compareAndSet(false, true)) {
|
||||
LockSupport.parkNanos(1L);
|
||||
if (Thread.interrupted()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean parkForReadAndCheckInterrupt() {
|
||||
while (writing.get()) {
|
||||
LockSupport.parkNanos(1L);
|
||||
if (Thread.interrupted()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static final class CacheEntry {
|
||||
|
||||
final String key;
|
||||
final EvaluatableScriptAdapter value;
|
||||
final long expiration;
|
||||
|
||||
CacheEntry(String key, EvaluatableScriptAdapter value, long maxAge) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
if(maxAge == -1) {
|
||||
expiration = -1;
|
||||
} else {
|
||||
expiration = System.currentTimeMillis() + maxAge;
|
||||
}
|
||||
}
|
||||
|
||||
String key() {
|
||||
return key;
|
||||
}
|
||||
|
||||
EvaluatableScriptAdapter value() {
|
||||
return value;
|
||||
}
|
||||
|
||||
boolean isExpired() {
|
||||
return expiration != -1 ? System.currentTimeMillis() > expiration : false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,4 +41,5 @@ org.keycloak.authorization.policy.provider.role.RolePolicyProviderFactory
|
||||
org.keycloak.authorization.policy.provider.scope.ScopePolicyProviderFactory
|
||||
org.keycloak.authorization.policy.provider.time.TimePolicyProviderFactory
|
||||
org.keycloak.authorization.policy.provider.user.UserPolicyProviderFactory
|
||||
org.keycloak.authorization.policy.provider.client.ClientPolicyProviderFactory
|
||||
org.keycloak.authorization.policy.provider.client.ClientPolicyProviderFactory
|
||||
org.keycloak.authorization.policy.provider.group.GroupPolicyProviderFactory
|
||||
@@ -31,9 +31,9 @@ public class ConcurrentMultivaluedHashMap<K, V> extends ConcurrentHashMap<K, Lis
|
||||
{
|
||||
public void putSingle(K key, V value)
|
||||
{
|
||||
List<V> list = new CopyOnWriteArrayList<>();
|
||||
List<V> list = createListInstance();
|
||||
list.add(value);
|
||||
put(key, list);
|
||||
put(key, list); // Just override with new List instance
|
||||
}
|
||||
|
||||
public void addAll(K key, V... newValues)
|
||||
@@ -84,8 +84,15 @@ public class ConcurrentMultivaluedHashMap<K, V> extends ConcurrentHashMap<K, Lis
|
||||
public final List<V> getList(K key)
|
||||
{
|
||||
List<V> list = get(key);
|
||||
if (list == null)
|
||||
put(key, list = new CopyOnWriteArrayList<V>());
|
||||
|
||||
if (list == null) {
|
||||
list = createListInstance();
|
||||
List<V> existing = putIfAbsent(key, list);
|
||||
if (existing != null) {
|
||||
list = existing;
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -97,4 +104,8 @@ public class ConcurrentMultivaluedHashMap<K, V> extends ConcurrentHashMap<K, Lis
|
||||
}
|
||||
}
|
||||
|
||||
protected List<V> createListInstance() {
|
||||
return new CopyOnWriteArrayList<>();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* 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.representations.idm.authorization;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public class GroupPolicyRepresentation extends AbstractPolicyRepresentation {
|
||||
|
||||
private String groupsClaim;
|
||||
private Set<GroupDefinition> groups;
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return "group";
|
||||
}
|
||||
|
||||
public String getGroupsClaim() {
|
||||
return groupsClaim;
|
||||
}
|
||||
|
||||
public void setGroupsClaim(String groupsClaim) {
|
||||
this.groupsClaim = groupsClaim;
|
||||
}
|
||||
|
||||
public Set<GroupDefinition> getGroups() {
|
||||
return groups;
|
||||
}
|
||||
|
||||
public void setGroups(Set<GroupDefinition> groups) {
|
||||
this.groups = groups;
|
||||
}
|
||||
|
||||
public void addGroup(String... ids) {
|
||||
for (String id : ids) {
|
||||
addGroup(id, false);
|
||||
}
|
||||
}
|
||||
|
||||
public void addGroup(String id, boolean extendChildren) {
|
||||
if (groups == null) {
|
||||
groups = new HashSet<>();
|
||||
}
|
||||
groups.add(new GroupDefinition(id, extendChildren));
|
||||
}
|
||||
|
||||
public void addGroupPath(String... paths) {
|
||||
for (String path : paths) {
|
||||
addGroupPath(path, false);
|
||||
}
|
||||
}
|
||||
|
||||
public void addGroupPath(String path, boolean extendChildren) {
|
||||
if (groups == null) {
|
||||
groups = new HashSet<>();
|
||||
}
|
||||
groups.add(new GroupDefinition(null, path, extendChildren));
|
||||
}
|
||||
|
||||
public void removeGroup(String... ids) {
|
||||
if (groups != null) {
|
||||
for (final String id : ids) {
|
||||
if (!groups.remove(id)) {
|
||||
for (GroupDefinition group : new HashSet<>(groups)) {
|
||||
if (group.getPath().startsWith(id)) {
|
||||
groups.remove(group);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class GroupDefinition {
|
||||
|
||||
private String id;
|
||||
private String path;
|
||||
private boolean extendChildren;
|
||||
|
||||
public GroupDefinition() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public GroupDefinition(String id) {
|
||||
this(id, false);
|
||||
}
|
||||
|
||||
public GroupDefinition(String id, boolean extendChildren) {
|
||||
this(id, null, extendChildren);
|
||||
}
|
||||
|
||||
public GroupDefinition(String id, String path, boolean extendChildren) {
|
||||
this.id = id;
|
||||
this.path = path;
|
||||
this.extendChildren = extendChildren;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public void setPath(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public boolean isExtendChildren() {
|
||||
return extendChildren;
|
||||
}
|
||||
|
||||
public void setExtendChildren(boolean extendChildren) {
|
||||
this.extendChildren = extendChildren;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,4 +156,9 @@ public class KerberosFederationProviderFactory implements UserStorageProviderFac
|
||||
AuthenticationExecutionModel.Requirement.ALTERNATIVE, AuthenticationExecutionModel.Requirement.DISABLED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preRemove(KeycloakSession session, RealmModel realm, ComponentModel model) {
|
||||
CredentialHelper.setOrReplaceAuthenticationRequirement(session, realm, CredentialRepresentation.KERBEROS,
|
||||
AuthenticationExecutionModel.Requirement.DISABLED, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,8 +384,14 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LD
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void preRemove(KeycloakSession session, RealmModel realm, ComponentModel model) {
|
||||
String allowKerberosCfg = model.getConfig().getFirst(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION);
|
||||
if (Boolean.valueOf(allowKerberosCfg)) {
|
||||
CredentialHelper.setOrReplaceAuthenticationRequirement(session, realm, CredentialRepresentation.KERBEROS,
|
||||
AuthenticationExecutionModel.Requirement.DISABLED, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SynchronizationResult sync(KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model) {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.admin.client.resource;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public interface GroupPoliciesResource {
|
||||
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
Response create(GroupPolicyRepresentation representation);
|
||||
|
||||
@Path("{id}")
|
||||
GroupPolicyResource findById(@PathParam("id") String id);
|
||||
|
||||
@Path("/search")
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@NoCache
|
||||
GroupPolicyRepresentation findByName(@QueryParam("name") String name);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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.admin.client.resource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public interface GroupPolicyResource {
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@NoCache
|
||||
GroupPolicyRepresentation toRepresentation();
|
||||
|
||||
@PUT
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
void update(GroupPolicyRepresentation representation);
|
||||
|
||||
@DELETE
|
||||
void remove();
|
||||
|
||||
@Path("/associatedPolicies")
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@NoCache
|
||||
List<PolicyRepresentation> associatedPolicies();
|
||||
|
||||
@Path("/dependentPolicies")
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@NoCache
|
||||
List<PolicyRepresentation> dependentPolicies();
|
||||
|
||||
@Path("/resources")
|
||||
@GET
|
||||
@Produces("application/json")
|
||||
@NoCache
|
||||
List<ResourceRepresentation> resources();
|
||||
|
||||
}
|
||||
@@ -89,4 +89,7 @@ public interface PoliciesResource {
|
||||
|
||||
@Path("client")
|
||||
ClientPoliciesResource client();
|
||||
|
||||
@Path("group")
|
||||
GroupPoliciesResource group();
|
||||
}
|
||||
|
||||
@@ -156,8 +156,12 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
|
||||
String nodeName = config.get("nodeName", System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME));
|
||||
String jgroupsUdpMcastAddr = config.get("jgroupsUdpMcastAddr", System.getProperty(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR));
|
||||
configureTransport(gcb, nodeName, jgroupsUdpMcastAddr);
|
||||
gcb.globalJmxStatistics()
|
||||
.jmxDomain(InfinispanConnectionProvider.JMX_DOMAIN + "-" + nodeName);
|
||||
}
|
||||
gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains);
|
||||
gcb.globalJmxStatistics()
|
||||
.allowDuplicateDomains(allowDuplicateJMXDomains)
|
||||
.enable();
|
||||
|
||||
cacheManager = new DefaultCacheManager(gcb.build());
|
||||
containerManaged = false;
|
||||
@@ -339,8 +343,13 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
|
||||
channel.setName(nodeName);
|
||||
JGroupsTransport transport = new JGroupsTransport(channel);
|
||||
|
||||
gcb.transport().nodeName(nodeName);
|
||||
gcb.transport().transport(transport);
|
||||
gcb.transport()
|
||||
.nodeName(nodeName)
|
||||
.transport(transport)
|
||||
.globalJmxStatistics()
|
||||
.jmxDomain(InfinispanConnectionProvider.JMX_DOMAIN + "-" + nodeName)
|
||||
.enable()
|
||||
;
|
||||
|
||||
logger.infof("Configured jgroups transport with the channel name: %s", nodeName);
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -55,6 +55,7 @@ public interface InfinispanConnectionProvider extends Provider {
|
||||
String JBOSS_NODE_NAME = "jboss.node.name";
|
||||
String JGROUPS_UDP_MCAST_ADDR = "jgroups.udp.mcast_addr";
|
||||
|
||||
String JMX_DOMAIN = "jboss.datagrid-infinispan";
|
||||
|
||||
<K, V> Cache<K, V> getCache(String name);
|
||||
|
||||
|
||||
@@ -1885,6 +1885,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
||||
ComponentEntity c = em.find(ComponentEntity.class, component.getId());
|
||||
if (c == null) return;
|
||||
session.users().preRemove(this, component);
|
||||
ComponentUtil.notifyPreRemove(session, this, component);
|
||||
removeComponents(component.getId());
|
||||
getEntity().getComponents().remove(c);
|
||||
}
|
||||
@@ -1896,7 +1897,10 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
||||
getEntity().getComponents().stream()
|
||||
.filter(sameParent)
|
||||
.map(this::entityToModel)
|
||||
.forEach(c -> session.users().preRemove(this, c));
|
||||
.forEach((ComponentModel c) -> {
|
||||
session.users().preRemove(this, c);
|
||||
ComponentUtil.notifyPreRemove(session, this, c);
|
||||
});
|
||||
|
||||
getEntity().getComponents().removeIf(sameParent);
|
||||
}
|
||||
|
||||
@@ -107,6 +107,10 @@ public interface Attributes {
|
||||
return values.length;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return values.length == 0;
|
||||
}
|
||||
|
||||
public String asString(int idx) {
|
||||
if (idx >= values.length) {
|
||||
throw new IllegalArgumentException("Invalid index [" + idx + "]. Values are [" + values + "].");
|
||||
|
||||
@@ -120,4 +120,6 @@ public interface LoginFormsProvider extends Provider {
|
||||
public LoginFormsProvider setStatus(Response.Status status);
|
||||
|
||||
LoginFormsProvider setActionUri(URI requestUri);
|
||||
|
||||
LoginFormsProvider setExecution(String execution);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
package org.keycloak.models.utils;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.component.ComponentFactory;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
@@ -38,6 +39,8 @@ import java.util.Map;
|
||||
*/
|
||||
public class ComponentUtil {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(ComponentUtil.class);
|
||||
|
||||
public static Map<String, ProviderConfigProperty> 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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 <a href="mailto:jay@anslow.me.uk">Jay Anslow</a>
|
||||
*/
|
||||
public interface EvaluatableScriptAdapter {
|
||||
ScriptModel getScriptModel();
|
||||
|
||||
Object eval(ScriptBindingsConfigurer bindingsConfigurer) throws ScriptExecutionException;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}.
|
||||
* <p>The {@code EvaluatableScriptAdapter} wraps a dedicated {@link ScriptEngine} that was populated with empty bindings.</p>
|
||||
*
|
||||
* @param scriptModel the scriptModel to wrap
|
||||
*/
|
||||
EvaluatableScriptAdapter prepareEvaluatableScript(ScriptModel scriptModel);
|
||||
|
||||
/**
|
||||
* Creates a new {@link ScriptModel} instance.
|
||||
*
|
||||
|
||||
@@ -79,6 +79,18 @@ public interface ComponentFactory<CreatedType, ProviderType extends Provider> 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
|
||||
*
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<AuthenticationSessionModel> 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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 <a href="mailto:frelibert@yahoo.com">Frederik Libert</a>
|
||||
*
|
||||
*/
|
||||
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<ProviderConfigProperty> 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<ProviderConfigProperty> 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<AttributeType> attributesInContext = findAttributesInContext(context, getAttributePattern(mapperModel));
|
||||
for (AttributeType a : attributesInContext) {
|
||||
String attribute = useFriendlyNames ? a.getFriendlyName() : a.getName();
|
||||
List<String> 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<AttributeType> attributesInContext = findAttributesInContext(context, getAttributePattern(mapperModel));
|
||||
|
||||
Set<String> assertedUserAttributes = new HashSet<String>();
|
||||
for (AttributeType a : attributesInContext) {
|
||||
String attribute = useFriendlyNames ? a.getFriendlyName() : a.getName();
|
||||
List<String> attributeValuesInContext = a.getAttributeValue().stream().filter(Objects::nonNull).map(Object::toString).collect(Collectors.toList());
|
||||
List<String> 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<Pattern> getAttributePattern(IdentityProviderMapperModel mapperModel) {
|
||||
String attributePatternConfig = mapperModel.getConfig().get(ATTRIBUTE_NAME_PATTERN);
|
||||
return Optional.ofNullable(attributePatternConfig != null ? Pattern.compile(attributePatternConfig) : null);
|
||||
}
|
||||
|
||||
private List<AttributeType> findAttributesInContext(BrokeredIdentityContext context, Optional<Pattern> 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<String> consumer, List<String> values) {
|
||||
if (values != null && !values.isEmpty()) {
|
||||
consumer.accept(values.get(0));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -76,6 +76,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
||||
private Map<String, String> httpResponseHeaders = new HashMap<String, String>();
|
||||
private String accessRequestMessage;
|
||||
private URI actionUri;
|
||||
private String execution;
|
||||
|
||||
private List<FormMessage> 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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String> 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<String> validRedirects) {
|
||||
private static String verifyRedirectUri(UriInfo uriInfo, String rootUrl, String redirectUri, RealmModel realm, Set<String> 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<String> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 <a href="mailto:jay@anslow.me.uk">Jay Anslow</a>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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 <a href="mailto:jay@anslow.me.uk">Jay Anslow</a>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <a href="mailto:jay@anslow.me.uk">Jay Anslow</a>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -33,6 +33,14 @@
|
||||
</xsl:copy>
|
||||
</xsl:template>
|
||||
|
||||
<xsl:template match="//*[local-name()='subsystem' and starts-with(namespace-uri(), $nsCacheServer)]
|
||||
/*[local-name()='cache-container' and starts-with(namespace-uri(), $nsCacheServer) and @name='clustered']">
|
||||
<xsl:copy>
|
||||
<xsl:apply-templates select="@* | node()" />
|
||||
<replicated-cache name="work" start="EAGER" batching="false" />
|
||||
</xsl:copy>
|
||||
</xsl:template>
|
||||
|
||||
<xsl:template match="@*|node()">
|
||||
<xsl:copy>
|
||||
<xsl:apply-templates select="@*|node()" />
|
||||
@@ -100,7 +100,7 @@
|
||||
<artifactId>xml-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>configure-adapter-debug-log</id>
|
||||
<id>configure-keycloak-caches</id>
|
||||
<phase>process-test-resources</phase>
|
||||
<goals>
|
||||
<goal>transform</goal>
|
||||
@@ -111,8 +111,9 @@
|
||||
<dir>${cache.server.jboss.home}/standalone/configuration</dir>
|
||||
<includes>
|
||||
<include>standalone.xml</include>
|
||||
<include>clustered.xml</include>
|
||||
</includes>
|
||||
<stylesheet>${common.resources}/add-keycloak-remote-store.xsl</stylesheet>
|
||||
<stylesheet>${common.resources}/add-keycloak-caches.xsl</stylesheet>
|
||||
<outputDir>${cache.server.jboss.home}/standalone/configuration</outputDir>
|
||||
</transformationSet>
|
||||
</transformationSets>
|
||||
@@ -173,6 +174,23 @@
|
||||
<overwrite>true</overwrite>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>copy-cache-server-configuration-for-dc-2</id>
|
||||
<phase>process-resources</phase>
|
||||
<goals>
|
||||
<goal>copy-resources</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${cache.server.jboss.home}/standalone-dc-2/deployments</outputDirectory>
|
||||
<includeEmptyDirs>true</includeEmptyDirs>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>${cache.server.jboss.home}/standalone/deployments</directory>
|
||||
</resource>
|
||||
</resources>
|
||||
<overwrite>true</overwrite>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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<ContainerRegistry> registry;
|
||||
|
||||
@Inject
|
||||
private Instance<JmxConnectorRegistry> jmxConnectorRegistry;
|
||||
|
||||
@Inject
|
||||
private Instance<SuiteContext> 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<String, Object> 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<ObjectName> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> 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();
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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<String, TestCleanup> cleanups = new HashMap<>();
|
||||
private Map<String, TestCleanup> 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;
|
||||
}
|
||||
|
||||
@@ -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 "";
|
||||
|
||||
}
|
||||
@@ -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 "";
|
||||
}
|
||||
@@ -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<String, String> 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")
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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<JmxConnectorRegistry> connectorRegistry;
|
||||
|
||||
public void configureJmxConnectorRegistry(@Observes BeforeSuite event) {
|
||||
if (connectorRegistry.get() == null) {
|
||||
connectorRegistry.set(new JmxConnectorRegistry() {
|
||||
|
||||
private volatile ConcurrentMap<JMXServiceURL, JMXConnector> 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ import org.openqa.selenium.support.FindBy;
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import org.openqa.selenium.support.FindBy;
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class LoginPasswordUpdatePage extends AbstractPage {
|
||||
public class LoginPasswordUpdatePage extends LanguageComboboxAwarePage {
|
||||
|
||||
@FindBy(id = "password-new")
|
||||
private WebElement newPasswordInput;
|
||||
|
||||
@@ -22,7 +22,7 @@ import org.openqa.selenium.support.FindBy;
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class OAuthGrantPage extends AbstractPage {
|
||||
public class OAuthGrantPage extends LanguageComboboxAwarePage {
|
||||
|
||||
@FindBy(css = "input[name=\"accept\"]")
|
||||
private WebElement acceptButton;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<String, String> entities = new ConcurrentMultivaluedHashMap<>();
|
||||
|
||||
private List<String> identityProviderAliases;
|
||||
private List<String> userIds;
|
||||
private List<String> componentIds;
|
||||
private List<String> clientUuids;
|
||||
private List<String> roleIds;
|
||||
private List<String> groupIds;
|
||||
private List<String> authFlowIds;
|
||||
private List<String> 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<String> userIds = entities.get(USER_IDS);
|
||||
if (userIds != null) {
|
||||
for (String userId : userIds) {
|
||||
try {
|
||||
@@ -130,6 +108,7 @@ public class TestCleanup {
|
||||
}
|
||||
}
|
||||
|
||||
List<String> identityProviderAliases = entities.get(IDENTITY_PROVIDER_ALIASES);
|
||||
if (identityProviderAliases != null) {
|
||||
for (String idpAlias : identityProviderAliases) {
|
||||
try {
|
||||
@@ -140,6 +119,7 @@ public class TestCleanup {
|
||||
}
|
||||
}
|
||||
|
||||
List<String> componentIds = entities.get(COMPONENT_IDS);
|
||||
if (componentIds != null) {
|
||||
for (String componentId : componentIds) {
|
||||
try {
|
||||
@@ -150,6 +130,7 @@ public class TestCleanup {
|
||||
}
|
||||
}
|
||||
|
||||
List<String> clientUuids = entities.get(CLIENT_UUIDS);
|
||||
if (clientUuids != null) {
|
||||
for (String clientUuId : clientUuids) {
|
||||
try {
|
||||
@@ -160,6 +141,7 @@ public class TestCleanup {
|
||||
}
|
||||
}
|
||||
|
||||
List<String> roleIds = entities.get(ROLE_IDS);
|
||||
if (roleIds != null) {
|
||||
for (String roleId : roleIds) {
|
||||
try {
|
||||
@@ -170,6 +152,7 @@ public class TestCleanup {
|
||||
}
|
||||
}
|
||||
|
||||
List<String> groupIds = entities.get(GROUP_IDS);
|
||||
if (groupIds != null) {
|
||||
for (String groupId : groupIds) {
|
||||
try {
|
||||
@@ -180,6 +163,7 @@ public class TestCleanup {
|
||||
}
|
||||
}
|
||||
|
||||
List<String> authFlowIds = entities.get(AUTH_FLOW_IDS);
|
||||
if (authFlowIds != null) {
|
||||
for (String flowId : authFlowIds) {
|
||||
try {
|
||||
@@ -190,6 +174,7 @@ public class TestCleanup {
|
||||
}
|
||||
}
|
||||
|
||||
List<String> authConfigIds = entities.get(AUTH_CONFIG_IDS);
|
||||
if (authConfigIds != null) {
|
||||
for (String configId : authConfigIds) {
|
||||
try {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
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<String, GroupRepresentation>() {
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public class GroupNamePolicyTest extends AbstractKeycloakTest {
|
||||
|
||||
@Override
|
||||
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||
ProtocolMapperRepresentation groupProtocolMapper = new ProtocolMapperRepresentation();
|
||||
|
||||
groupProtocolMapper.setName("groups");
|
||||
groupProtocolMapper.setProtocolMapper(GroupMembershipMapper.PROVIDER_ID);
|
||||
groupProtocolMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
groupProtocolMapper.setConsentRequired(false);
|
||||
Map<String, String> 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<String, GroupRepresentation>() {
|
||||
@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<GroupRepresentation>() {
|
||||
@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<GroupRepresentation> 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;
|
||||
}
|
||||
}
|
||||
@@ -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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public class GroupPathPolicyTest extends AbstractKeycloakTest {
|
||||
|
||||
@Override
|
||||
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||
ProtocolMapperRepresentation groupProtocolMapper = new ProtocolMapperRepresentation();
|
||||
|
||||
groupProtocolMapper.setName("groups");
|
||||
groupProtocolMapper.setProtocolMapper(GroupMembershipMapper.PROVIDER_ID);
|
||||
groupProtocolMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
groupProtocolMapper.setConsentRequired(false);
|
||||
Map<String, String> 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<String, GroupRepresentation>() {
|
||||
@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<GroupRepresentation>() {
|
||||
@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<GroupRepresentation> 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;
|
||||
}
|
||||
}
|
||||
@@ -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 <T extends Comparable> void assertSingleStatistics(InfinispanStatistics stats, String key, Runnable testedCode, Function<T, Matcher<? super T>> 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<Map<String, Object>, Map<String, Object>> assertionOnStats) {
|
||||
stats.reset();
|
||||
|
||||
Map<String, Object> oldStat = stats.getStatistics();
|
||||
testedCode.run();
|
||||
|
||||
Retry.execute(() -> {
|
||||
Map<String, Object> newStat = stats.getStatistics();
|
||||
assertionOnStats.accept(oldStat, newStat);
|
||||
}, 5, 200);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -32,14 +32,21 @@ import org.jboss.arquillian.test.api.ArquillianResource;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
|
||||
import static org.hamcrest.Matchers.lessThan;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author hmlnarik
|
||||
*/
|
||||
public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
// Keep the following constants in sync with arquillian
|
||||
public static final String QUALIFIER_NODE_BALANCER = "auth-server-balancer-cross-dc";
|
||||
|
||||
@ArquillianResource
|
||||
@LoadBalancer(value = "auth-server-balancer-cross-dc")
|
||||
@LoadBalancer(value = QUALIFIER_NODE_BALANCER)
|
||||
protected LoadBalancerController loadBalancerCtrl;
|
||||
|
||||
@ArquillianResource
|
||||
@@ -103,6 +110,11 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest
|
||||
return Keycloak.getInstance(node.getContextRoot() + "/auth", AuthRealm.MASTER, AuthRealm.ADMIN, AuthRealm.ADMIN, Constants.ADMIN_CLI_CLIENT_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates admin client directed to the given node.
|
||||
* @param node
|
||||
* @return
|
||||
*/
|
||||
protected Keycloak getAdminClientFor(ContainerInfo node) {
|
||||
Keycloak adminClient = backendAdminClients.get(node);
|
||||
if (adminClient == null && node.equals(suiteContext.getAuthServerInfo())) {
|
||||
@@ -111,13 +123,17 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest
|
||||
return adminClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables routing requests to the given data center in the load balancer.
|
||||
* @param dcIndex
|
||||
*/
|
||||
public void disableDcOnLoadBalancer(int dcIndex) {
|
||||
log.infof("Disabling load balancer for dc=%d", dcIndex);
|
||||
this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex).forEach(c -> loadBalancerCtrl.disableBackendNodeByName(c.getQualifier()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables all started nodes in the given data center
|
||||
* Enables routing requests to all started nodes to the given data center in the load balancer.
|
||||
* @param dcIndex
|
||||
*/
|
||||
public void enableDcOnLoadBalancer(int dcIndex) {
|
||||
@@ -132,11 +148,21 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables routing requests to the given node within the given data center in the load balancer.
|
||||
* @param dcIndex
|
||||
* @param nodeIndex
|
||||
*/
|
||||
public void disableLoadBalancerNode(int dcIndex, int nodeIndex) {
|
||||
log.infof("Disabling load balancer for dc=%d, node=%d", dcIndex, nodeIndex);
|
||||
loadBalancerCtrl.disableBackendNodeByName(this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex).get(nodeIndex).getQualifier());
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables routing requests to the given node within the given data center in the load balancer.
|
||||
* @param dcIndex
|
||||
* @param nodeIndex
|
||||
*/
|
||||
public void enableLoadBalancerNode(int dcIndex, int nodeIndex) {
|
||||
log.infof("Enabling load balancer for dc=%d, node=%d", dcIndex, nodeIndex);
|
||||
final ContainerInfo backendNode = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex).get(nodeIndex);
|
||||
@@ -149,11 +175,53 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest
|
||||
loadBalancerCtrl.enableBackendNodeByName(backendNode.getQualifier());
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a manually-controlled backend auth-server node in cross-DC scenario.
|
||||
* @param dcIndex
|
||||
* @param nodeIndex
|
||||
* @return Started instance descriptor.
|
||||
*/
|
||||
public ContainerInfo startBackendNode(int dcIndex, int nodeIndex) {
|
||||
assertThat((Integer) dcIndex, lessThan(this.suiteContext.getDcAuthServerBackendsInfo().size()));
|
||||
final List<ContainerInfo> dcNodes = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex);
|
||||
assertThat((Integer) nodeIndex, lessThan(dcNodes.size()));
|
||||
ContainerInfo dcNode = dcNodes.get(nodeIndex);
|
||||
assertTrue("Node " + dcNode.getQualifier() + " has to be controlled manually", dcNode.isManual());
|
||||
containerController.start(dcNode.getQualifier());
|
||||
return dcNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops a manually-controlled backend auth-server node in cross-DC scenario.
|
||||
* @param dcIndex
|
||||
* @param nodeIndex
|
||||
* @return Stopped instance descriptor.
|
||||
*/
|
||||
public ContainerInfo stopBackendNode(int dcIndex, int nodeIndex) {
|
||||
assertThat((Integer) dcIndex, lessThan(this.suiteContext.getDcAuthServerBackendsInfo().size()));
|
||||
final List<ContainerInfo> dcNodes = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex);
|
||||
assertThat((Integer) nodeIndex, lessThan(dcNodes.size()));
|
||||
ContainerInfo dcNode = dcNodes.get(nodeIndex);
|
||||
assertTrue("Node " + dcNode.getQualifier() + " has to be controlled manually", dcNode.isManual());
|
||||
containerController.stop(dcNode.getQualifier());
|
||||
return dcNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns stream of all nodes in the given dc that are started manually.
|
||||
* @param dcIndex
|
||||
* @return
|
||||
*/
|
||||
public Stream<ContainerInfo> getManuallyStartedBackendNodes(int dcIndex) {
|
||||
final List<ContainerInfo> dcNodes = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex);
|
||||
return dcNodes.stream().filter(ContainerInfo::isManual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns stream of all nodes in the given dc that are started automatically.
|
||||
* @param dcIndex
|
||||
* @return
|
||||
*/
|
||||
public Stream<ContainerInfo> getAutomaticallyStartedBackendNodes(int dcIndex) {
|
||||
final List<ContainerInfo> dcNodes = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex);
|
||||
return dcNodes.stream().filter(c -> ! c.isManual());
|
||||
|
||||
@@ -17,16 +17,13 @@
|
||||
package org.keycloak.testsuite.crossdc;
|
||||
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.events.admin.OperationType;
|
||||
import org.keycloak.events.admin.ResourceType;
|
||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.Retry;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.arquillian.ContainerInfo;
|
||||
import org.keycloak.testsuite.page.LoginPasswordUpdatePage;
|
||||
import org.keycloak.testsuite.pages.ErrorPage;
|
||||
import org.keycloak.testsuite.util.AdminEventPaths;
|
||||
import org.keycloak.testsuite.util.GreenMailRule;
|
||||
import org.keycloak.testsuite.util.MailUtils;
|
||||
import java.io.IOException;
|
||||
@@ -36,12 +33,20 @@ import javax.mail.MessagingException;
|
||||
import javax.mail.internet.MimeMessage;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.jboss.arquillian.test.api.ArquillianResource;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import org.keycloak.testsuite.arquillian.annotation.JmxInfinispanCacheStatistics;
|
||||
import org.keycloak.testsuite.arquillian.annotation.JmxInfinispanChannelStatistics;
|
||||
import org.keycloak.testsuite.arquillian.InfinispanStatistics;
|
||||
import org.keycloak.testsuite.arquillian.InfinispanStatistics.Constants;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.hamcrest.Matchers;
|
||||
import static org.hamcrest.Matchers.greaterThan;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -69,7 +74,16 @@ public class ActionTokenCrossDCTest extends AbstractAdminCrossDCTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendResetPasswordEmailSuccessWorksInCrossDc() throws IOException, MessagingException {
|
||||
public void sendResetPasswordEmailSuccessWorksInCrossDc(
|
||||
@JmxInfinispanCacheStatistics(dcIndex=0, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.ACTION_TOKEN_CACHE) InfinispanStatistics cacheDc0Node0Statistics,
|
||||
@JmxInfinispanCacheStatistics(dcIndex=0, dcNodeIndex=1, cacheName=InfinispanConnectionProvider.ACTION_TOKEN_CACHE) InfinispanStatistics cacheDc0Node1Statistics,
|
||||
@JmxInfinispanCacheStatistics(dcIndex=1, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.ACTION_TOKEN_CACHE) InfinispanStatistics cacheDc1Node0Statistics,
|
||||
@JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
|
||||
startBackendNode(0, 1);
|
||||
cacheDc0Node1Statistics.waitToBecomeAvailable(10, TimeUnit.SECONDS);
|
||||
|
||||
Comparable originalNumberOfEntries = cacheDc0Node0Statistics.getSingleStatistics(Constants.STAT_CACHE_NUMBER_OF_ENTRIES);
|
||||
|
||||
UserRepresentation userRep = new UserRepresentation();
|
||||
userRep.setEnabled(true);
|
||||
userRep.setUsername("user1");
|
||||
@@ -88,21 +102,33 @@ public class ActionTokenCrossDCTest extends AbstractAdminCrossDCTest {
|
||||
|
||||
String link = MailUtils.getPasswordResetEmailLink(message);
|
||||
|
||||
driver.navigate().to(link);
|
||||
Retry.execute(() -> channelStatisticsCrossDc.reset(), 3, 100);
|
||||
|
||||
assertSingleStatistics(cacheDc0Node0Statistics, Constants.STAT_CACHE_NUMBER_OF_ENTRIES,
|
||||
() -> driver.navigate().to(link),
|
||||
Matchers::is
|
||||
);
|
||||
|
||||
passwordUpdatePage.assertCurrent();
|
||||
|
||||
passwordUpdatePage.changePassword("new-pass", "new-pass");
|
||||
// Verify that there was at least one message sent via the channel
|
||||
assertSingleStatistics(channelStatisticsCrossDc, Constants.STAT_CHANNEL_SENT_MESSAGES,
|
||||
() -> passwordUpdatePage.changePassword("new-pass", "new-pass"),
|
||||
old -> greaterThan((Comparable) 0l)
|
||||
);
|
||||
|
||||
// Verify that the caches are synchronized
|
||||
assertThat(cacheDc0Node0Statistics.getSingleStatistics(Constants.STAT_CACHE_NUMBER_OF_ENTRIES), greaterThan(originalNumberOfEntries));
|
||||
assertThat(cacheDc0Node0Statistics.getSingleStatistics(Constants.STAT_CACHE_NUMBER_OF_ENTRIES),
|
||||
is(cacheDc1Node0Statistics.getSingleStatistics(Constants.STAT_CACHE_NUMBER_OF_ENTRIES)));
|
||||
|
||||
assertEquals("Your account has been updated.", driver.getTitle());
|
||||
|
||||
disableDcOnLoadBalancer(0);
|
||||
enableDcOnLoadBalancer(1);
|
||||
|
||||
Retry.execute(() -> {
|
||||
driver.navigate().to(link);
|
||||
errorPage.assertCurrent();
|
||||
}, 3, 400);
|
||||
driver.navigate().to(link);
|
||||
errorPage.assertCurrent();
|
||||
}
|
||||
|
||||
@Ignore("KEYCLOAK-5030")
|
||||
@@ -144,9 +170,10 @@ public class ActionTokenCrossDCTest extends AbstractAdminCrossDCTest {
|
||||
loadBalancerCtrl.enableBackendNodeByName(c.getQualifier());
|
||||
});
|
||||
|
||||
driver.navigate().to(link);
|
||||
|
||||
errorPage.assertCurrent();
|
||||
Retry.execute(() -> {
|
||||
driver.navigate().to(link);
|
||||
errorPage.assertCurrent();
|
||||
}, 3, 400);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -627,12 +627,13 @@ public class ExportImportUtil {
|
||||
assertPredicate(scopes, scopePredicates);
|
||||
|
||||
List<PolicyRepresentation> policies = authzResource.policies().policies();
|
||||
Assert.assertEquals(13, policies.size());
|
||||
Assert.assertEquals(14, policies.size());
|
||||
List<Predicate<PolicyRepresentation>> policyPredicates = new ArrayList<>();
|
||||
policyPredicates.add(policyRepresentation -> "Any Admin Policy".equals(policyRepresentation.getName()));
|
||||
policyPredicates.add(policyRepresentation -> "Any User Policy".equals(policyRepresentation.getName()));
|
||||
policyPredicates.add(representation -> "Client and Realm Role Policy".equals(representation.getName()));
|
||||
policyPredicates.add(representation -> "Client Test Policy".equals(representation.getName()));
|
||||
policyPredicates.add(representation -> "Group Policy Test".equals(representation.getName()));
|
||||
policyPredicates.add(policyRepresentation -> "Only Premium User Policy".equals(policyRepresentation.getName()));
|
||||
policyPredicates.add(policyRepresentation -> "wburke policy".equals(policyRepresentation.getName()));
|
||||
policyPredicates.add(policyRepresentation -> "All Users Policy".equals(policyRepresentation.getName()));
|
||||
|
||||
@@ -16,19 +16,30 @@
|
||||
*/
|
||||
package org.keycloak.testsuite.i18n;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.apache.http.impl.client.DefaultHttpClient;
|
||||
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
|
||||
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
|
||||
import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient4Engine;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.adapters.HttpClientBuilder;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.pages.AppPage;
|
||||
import org.keycloak.testsuite.pages.LanguageComboboxAwarePage;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.keycloak.testsuite.ProfileAssume;
|
||||
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
|
||||
import org.keycloak.testsuite.pages.OAuthGrantPage;
|
||||
import org.keycloak.testsuite.util.IdentityProviderBuilder;
|
||||
|
||||
/**
|
||||
@@ -37,9 +48,19 @@ import org.keycloak.testsuite.util.IdentityProviderBuilder;
|
||||
*/
|
||||
public class LoginPageTest extends AbstractI18NTest {
|
||||
|
||||
@Page
|
||||
protected AppPage appPage;
|
||||
|
||||
@Page
|
||||
protected LoginPage loginPage;
|
||||
|
||||
@Page
|
||||
protected LoginPasswordUpdatePage changePasswordPage;
|
||||
|
||||
@Page
|
||||
protected OAuthGrantPage grantPage;
|
||||
|
||||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
testRealm.addIdentityProvider(IdentityProviderBuilder.create()
|
||||
@@ -63,11 +84,7 @@ public class LoginPageTest extends AbstractI18NTest {
|
||||
loginPage.open();
|
||||
Assert.assertEquals("English", loginPage.getLanguageDropdownText());
|
||||
|
||||
loginPage.openLanguage("Deutsch");
|
||||
Assert.assertEquals("Deutsch", loginPage.getLanguageDropdownText());
|
||||
|
||||
loginPage.openLanguage("English");
|
||||
Assert.assertEquals("English", loginPage.getLanguageDropdownText());
|
||||
switchLanguageToGermanAndBack("Username or email", "Benutzername oder E-Mail", loginPage);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -109,6 +126,8 @@ public class LoginPageTest extends AbstractI18NTest {
|
||||
|
||||
response = client.target(driver.getCurrentUrl()).request().acceptLanguage("en").get();
|
||||
Assert.assertTrue(response.readEntity(String.class).contains("Log in to test"));
|
||||
|
||||
client.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -119,4 +138,73 @@ public class LoginPageTest extends AbstractI18NTest {
|
||||
Assert.assertEquals("MyOIDC", loginPage.findSocialButton("myoidc").getText());
|
||||
|
||||
}
|
||||
|
||||
|
||||
// KEYCLOAK-3887
|
||||
@Test
|
||||
public void languageChangeRequiredActions() {
|
||||
UserResource user = ApiUtil.findUserByUsernameId(testRealm(), "test-user@localhost");
|
||||
UserRepresentation userRep = user.toRepresentation();
|
||||
userRep.setRequiredActions(Arrays.asList(UserModel.RequiredAction.UPDATE_PASSWORD.toString()));
|
||||
user.update(userRep);
|
||||
|
||||
loginPage.open();
|
||||
|
||||
loginPage.login("test-user@localhost", "password");
|
||||
changePasswordPage.assertCurrent();
|
||||
Assert.assertEquals("English", changePasswordPage.getLanguageDropdownText());
|
||||
|
||||
// Switch language
|
||||
switchLanguageToGermanAndBack("Update password", "Passwort aktualisieren", changePasswordPage);
|
||||
|
||||
// Update password
|
||||
changePasswordPage.changePassword("password", "password");
|
||||
|
||||
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||
}
|
||||
|
||||
|
||||
// KEYCLOAK-3887
|
||||
@Test
|
||||
public void languageChangeConsentScreen() {
|
||||
// Set client, which requires consent
|
||||
oauth.clientId("third-party");
|
||||
|
||||
loginPage.open();
|
||||
|
||||
loginPage.login("test-user@localhost", "password");
|
||||
|
||||
grantPage.assertCurrent();
|
||||
Assert.assertEquals("English", grantPage.getLanguageDropdownText());
|
||||
|
||||
// Switch language
|
||||
switchLanguageToGermanAndBack("Do you grant these access privileges?", "Wollen Sie diese Zugriffsrechte", changePasswordPage);
|
||||
|
||||
// Confirm grant
|
||||
grantPage.accept();
|
||||
|
||||
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||
|
||||
// Revert client
|
||||
oauth.clientId("test-app");
|
||||
}
|
||||
|
||||
|
||||
private void switchLanguageToGermanAndBack(String expectedEnglishMessage, String expectedGermanMessage, LanguageComboboxAwarePage page) {
|
||||
// Switch language to Deutsch
|
||||
page.openLanguage("Deutsch");
|
||||
Assert.assertEquals("Deutsch", page.getLanguageDropdownText());
|
||||
String pageSource = driver.getPageSource();
|
||||
Assert.assertFalse(pageSource.contains(expectedEnglishMessage));
|
||||
Assert.assertTrue(pageSource.contains(expectedGermanMessage));
|
||||
|
||||
// Revert language
|
||||
page.openLanguage("English");
|
||||
Assert.assertEquals("English", page.getLanguageDropdownText());
|
||||
pageSource = driver.getPageSource();
|
||||
Assert.assertTrue(pageSource.contains(expectedEnglishMessage));
|
||||
Assert.assertFalse(pageSource.contains(expectedGermanMessage));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,19 +15,25 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.testsuite.oidc;
|
||||
package org.keycloak.testsuite.oauth;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
@@ -35,6 +41,7 @@ import org.keycloak.testsuite.ActionURIUtils;
|
||||
import org.keycloak.testsuite.Assert;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.admin.AbstractAdminTest;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
|
||||
import org.keycloak.testsuite.pages.AppPage;
|
||||
import org.keycloak.testsuite.pages.ErrorPage;
|
||||
@@ -46,9 +53,11 @@ import org.keycloak.testsuite.util.OAuthClient;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* Test for scenarios when 'scope=openid' is missing. Which means we have pure OAuth2 request (not OpenID Connect)
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class ScopeParameterTest extends AbstractTestRealmKeycloakTest {
|
||||
public class OAuth2OnlyTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
@Rule
|
||||
public AssertEvents events = new AssertEvents(this);
|
||||
@@ -71,6 +80,18 @@ public class ScopeParameterTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
ClientRepresentation client = new ClientRepresentation();
|
||||
client.setClientId("more-uris-client");
|
||||
client.setEnabled(true);
|
||||
client.setRedirectUris(Arrays.asList("http://localhost:8180/auth/realms/master/app/auth", "http://localhost:8180/foo"));
|
||||
client.setBaseUrl("http://localhost:8180/auth/realms/master/app/auth");
|
||||
|
||||
testRealm.getClients().add(client);
|
||||
|
||||
ClientRepresentation testApp = testRealm.getClients().stream()
|
||||
.filter(cl -> cl.getClientId().equals("test-app"))
|
||||
.findFirst().get();
|
||||
testApp.setImplicitFlowEnabled(true);
|
||||
}
|
||||
|
||||
@Before
|
||||
@@ -82,20 +103,13 @@ public class ScopeParameterTest extends AbstractTestRealmKeycloakTest {
|
||||
* will faile and the clientID will always be "sample-public-client
|
||||
* @see AccessTokenTest#testAuthorizationNegotiateHeaderIgnored()
|
||||
*/
|
||||
oauth.clientId("test-app");
|
||||
oauth.maxAge(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||
RealmRepresentation realm = AbstractAdminTest.loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
|
||||
testRealms.add(realm);
|
||||
oauth.init(adminClient, driver);
|
||||
}
|
||||
|
||||
|
||||
// If scope=openid is missing, IDToken won't be present
|
||||
@Test
|
||||
public void testMissingScopeOpenid() {
|
||||
public void testMissingIDToken() {
|
||||
String loginFormUrl = oauth.getLoginFormUrl();
|
||||
loginFormUrl = ActionURIUtils.removeQueryParamFromURI(loginFormUrl, OAuth2Constants.SCOPE);
|
||||
|
||||
@@ -139,4 +153,60 @@ public class ScopeParameterTest extends AbstractTestRealmKeycloakTest {
|
||||
Assert.assertEquals(accessToken.getPreferredUsername(), "test-user@localhost");
|
||||
|
||||
}
|
||||
|
||||
|
||||
// In OAuth2, it is allowed that redirect_uri is not mandatory as long as client has just 1 redirect_uri configured without wildcard
|
||||
@Test
|
||||
public void testMissingRedirectUri() throws Exception {
|
||||
// OAuth2 login without redirect_uri. It will be allowed.
|
||||
String loginFormUrl = oauth.getLoginFormUrl();
|
||||
loginFormUrl = ActionURIUtils.removeQueryParamFromURI(loginFormUrl, OAuth2Constants.SCOPE);
|
||||
loginFormUrl = ActionURIUtils.removeQueryParamFromURI(loginFormUrl, OAuth2Constants.REDIRECT_URI);
|
||||
|
||||
driver.navigate().to(loginFormUrl);
|
||||
loginPage.assertCurrent();
|
||||
oauth.fillLoginForm("test-user@localhost", "password");
|
||||
events.expectLogin().assertEvent();
|
||||
|
||||
// Client 'more-uris-client' has 2 redirect uris. OAuth2 login without redirect_uri won't be allowed
|
||||
oauth.clientId("more-uris-client");
|
||||
loginFormUrl = oauth.getLoginFormUrl();
|
||||
loginFormUrl = ActionURIUtils.removeQueryParamFromURI(loginFormUrl, OAuth2Constants.SCOPE);
|
||||
loginFormUrl = ActionURIUtils.removeQueryParamFromURI(loginFormUrl, OAuth2Constants.REDIRECT_URI);
|
||||
|
||||
driver.navigate().to(loginFormUrl);
|
||||
errorPage.assertCurrent();
|
||||
Assert.assertEquals("Invalid parameter: redirect_uri", errorPage.getError());
|
||||
events.expectLogin()
|
||||
.error(Errors.INVALID_REDIRECT_URI)
|
||||
.client("more-uris-client")
|
||||
.user(Matchers.nullValue(String.class))
|
||||
.session(Matchers.nullValue(String.class))
|
||||
.removeDetail(Details.REDIRECT_URI)
|
||||
.removeDetail(Details.CODE_ID)
|
||||
.removeDetail(Details.CONSENT)
|
||||
.assertEvent();
|
||||
}
|
||||
|
||||
|
||||
// In OAuth2 (when response_type=token and no scope=openid) we don't treat nonce parameter mandatory
|
||||
@Test
|
||||
public void testMissingNonceInOAuth2ImplicitFlow() throws Exception {
|
||||
oauth.responseType("token");
|
||||
oauth.nonce(null);
|
||||
String loginFormUrl = oauth.getLoginFormUrl();
|
||||
loginFormUrl = ActionURIUtils.removeQueryParamFromURI(loginFormUrl, OAuth2Constants.SCOPE);
|
||||
|
||||
driver.navigate().to(loginFormUrl);
|
||||
loginPage.assertCurrent();
|
||||
oauth.fillLoginForm("test-user@localhost", "password");
|
||||
events.expectLogin().assertEvent();
|
||||
|
||||
OAuthClient.AuthorizationEndpointResponse response = new OAuthClient.AuthorizationEndpointResponse(oauth);
|
||||
Assert.assertNull(response.getError());
|
||||
Assert.assertNull(response.getCode());
|
||||
Assert.assertNull(response.getIdToken());
|
||||
Assert.assertNotNull(response.getAccessToken());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -444,14 +444,10 @@ public class OAuthProofKeyForCodeExchangeTest extends AbstractKeycloakTest {
|
||||
|
||||
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 codeChallenge = Base64Url.encode(sb.toString().getBytes());
|
||||
return codeChallenge;
|
||||
md.update(codeVerifier.getBytes("ISO_8859_1"));
|
||||
byte[] digestBytes = md.digest();
|
||||
String codeChallenge = Base64Url.encode(digestBytes);
|
||||
return codeChallenge;
|
||||
}
|
||||
|
||||
private void expectSuccessfulResponseFromTokenEndpoint(String codeId, String sessionId, String code) throws Exception {
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
package org.keycloak.testsuite.util;
|
||||
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
@@ -175,7 +177,15 @@ public class ClientBuilder {
|
||||
}
|
||||
|
||||
public ClientBuilder authorizationServicesEnabled(boolean enable) {
|
||||
rep.setAuthorizationServicesEnabled(true);
|
||||
rep.setAuthorizationServicesEnabled(enable);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientBuilder protocolMapper(ProtocolMapperRepresentation... mappers) {
|
||||
if (rep.getProtocolMappers() == null) {
|
||||
rep.setProtocolMappers(new ArrayList<>());
|
||||
}
|
||||
rep.getProtocolMappers().addAll(Arrays.asList(mappers));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
|
||||
"connectionsJpa": {
|
||||
"default": {
|
||||
"url": "${keycloak.connectionsJpa.url:jdbc:h2:mem:test}",
|
||||
"url": "${keycloak.connectionsJpa.url:jdbc:h2:mem:test;MVCC=TRUE}",
|
||||
"driver": "${keycloak.connectionsJpa.driver:org.h2.Driver}",
|
||||
"driverDialect": "${keycloak.connectionsJpa.driverDialect:}",
|
||||
"user": "${keycloak.connectionsJpa.user:sa}",
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
<container qualifier="auth-server-undertow" mode="suite" >
|
||||
<configuration>
|
||||
<property name="enabled">${auth.server.undertow}</property>
|
||||
<property name="enabled">${auth.server.undertow} && ! ${auth.server.undertow.crossdc}</property>
|
||||
<property name="bindAddress">localhost</property>
|
||||
<property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow</property>
|
||||
<property name="bindHttpPort">${auth.server.http.port}</property>
|
||||
@@ -169,12 +169,12 @@
|
||||
|
||||
<!-- Cross DC with embedded undertow. Node numbering is [centre #].[node #] -->
|
||||
<group qualifier="auth-server-undertow-cross-dc">
|
||||
<container qualifier="cache-server-cross-dc" mode="suite" >
|
||||
<container qualifier="cache-server-cross-dc-1" mode="suite" >
|
||||
<configuration>
|
||||
<property name="enabled">${auth.server.undertow.crossdc}</property>
|
||||
<property name="adapterImplClass">org.jboss.as.arquillian.container.managed.ManagedDeployableContainer</property>
|
||||
<property name="jbossHome">${cache.server.home}</property>
|
||||
<property name="serverConfig">standalone.xml</property>
|
||||
<property name="serverConfig">clustered.xml</property>
|
||||
<property name="jbossArguments">
|
||||
-Djboss.socket.binding.port-offset=${cache.server.port.offset}
|
||||
-Djboss.default.multicast.address=234.56.78.99
|
||||
@@ -192,30 +192,54 @@
|
||||
</configuration>
|
||||
</container>
|
||||
|
||||
<container qualifier="cache-server-cross-dc-2" mode="suite" >
|
||||
<configuration>
|
||||
<property name="enabled">${auth.server.undertow.crossdc}</property>
|
||||
<property name="adapterImplClass">org.jboss.as.arquillian.container.managed.ManagedDeployableContainer</property>
|
||||
<property name="jbossHome">${cache.server.home}</property>
|
||||
<property name="setupCleanServerBaseDir">true</property>
|
||||
<property name="cleanServerBaseDir">${cache.server.home}/standalone-dc-2</property>
|
||||
<property name="serverConfig">clustered.xml</property>
|
||||
<property name="jbossArguments">
|
||||
-Djboss.socket.binding.port-offset=${cache.server.2.port.offset}
|
||||
-Djboss.default.multicast.address=234.56.78.99
|
||||
-Djboss.node.name=cache-server-dc-2
|
||||
${adapter.test.props}
|
||||
${auth.server.profile}
|
||||
</property>
|
||||
<property name="javaVmArguments">
|
||||
${auth.server.memory.settings}
|
||||
-Djava.net.preferIPv4Stack=true
|
||||
</property>
|
||||
<property name="outputToConsole">${cache.server.console.output}</property>
|
||||
<property name="managementPort">${cache.server.2.management.port}</property>
|
||||
<property name="startupTimeoutInSeconds">${auth.server.jboss.startup.timeout}</property>
|
||||
</configuration>
|
||||
</container>
|
||||
|
||||
<container qualifier="auth-server-balancer-cross-dc" mode="suite" >
|
||||
<configuration>
|
||||
<property name="enabled">${auth.server.undertow.crossdc}</property>
|
||||
<property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.lb.SimpleUndertowLoadBalancerContainer</property>
|
||||
<property name="bindAddress">localhost</property>
|
||||
<property name="bindHttpPort">${auth.server.http.port}</property>
|
||||
<property name="bindHttpPortOffset">5</property>
|
||||
<property name="nodes">auth-server-undertow-cross-dc-0.1=http://localhost:8101,auth-server-undertow-cross-dc-0.2-manual=http://localhost:8102,auth-server-undertow-cross-dc-1.1=http://localhost:8111,auth-server-undertow-cross-dc-1.2-manual=http://localhost:8112</property>
|
||||
</configuration>
|
||||
</container>
|
||||
|
||||
<container qualifier="auth-server-undertow-cross-dc-0.1" mode="suite" >
|
||||
<container qualifier="auth-server-undertow-cross-dc-0_1" mode="suite" >
|
||||
<configuration>
|
||||
<property name="enabled">${auth.server.undertow.crossdc}</property>
|
||||
<property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow</property>
|
||||
<property name="bindAddress">localhost</property>
|
||||
<property name="bindHttpPort">${auth.server.http.port}</property>
|
||||
<property name="bindHttpPortOffset">-79</property>
|
||||
<property name="route">auth-server-undertow-cross-dc-0.1</property>
|
||||
<property name="route">auth-server-undertow-cross-dc-0_1</property>
|
||||
<property name="remoteMode">${undertow.remote}</property>
|
||||
<property name="dataCenter">0</property>
|
||||
<property name="keycloakConfigPropertyOverrides">{
|
||||
"keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.1",
|
||||
"keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-0.1",
|
||||
"keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-0_1",
|
||||
"keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}",
|
||||
"keycloak.connectionsInfinispan.remoteStoreServer": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}",
|
||||
"keycloak.connectionsInfinispan.remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}",
|
||||
@@ -226,19 +250,19 @@
|
||||
}</property>
|
||||
</configuration>
|
||||
</container>
|
||||
<container qualifier="auth-server-undertow-cross-dc-0.2-manual" mode="manual" >
|
||||
<container qualifier="auth-server-undertow-cross-dc-0_2-manual" mode="manual" >
|
||||
<configuration>
|
||||
<property name="enabled">${auth.server.undertow.crossdc}</property>
|
||||
<property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow</property>
|
||||
<property name="bindAddress">localhost</property>
|
||||
<property name="bindHttpPort">${auth.server.http.port}</property>
|
||||
<property name="bindHttpPortOffset">-78</property>
|
||||
<property name="route">auth-server-undertow-cross-dc-0.2</property>
|
||||
<property name="route">auth-server-undertow-cross-dc-0_2-manual</property>
|
||||
<property name="remoteMode">${undertow.remote}</property>
|
||||
<property name="dataCenter">0</property>
|
||||
<property name="keycloakConfigPropertyOverrides">{
|
||||
"keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.1",
|
||||
"keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-0.2",
|
||||
"keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-0_2-manual",
|
||||
"keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}",
|
||||
"keycloak.connectionsInfinispan.remoteStoreServer": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}",
|
||||
"keycloak.connectionsInfinispan.remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}",
|
||||
@@ -250,22 +274,22 @@
|
||||
</configuration>
|
||||
</container>
|
||||
|
||||
<container qualifier="auth-server-undertow-cross-dc-1.1" mode="suite" >
|
||||
<container qualifier="auth-server-undertow-cross-dc-1_1" mode="suite" >
|
||||
<configuration>
|
||||
<property name="enabled">${auth.server.undertow.crossdc}</property>
|
||||
<property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow</property>
|
||||
<property name="bindAddress">localhost</property>
|
||||
<property name="bindHttpPort">${auth.server.http.port}</property>
|
||||
<property name="bindHttpPortOffset">-69</property>
|
||||
<property name="route">auth-server-undertow-cross-dc-1.1</property>
|
||||
<property name="route">auth-server-undertow-cross-dc-1_1</property>
|
||||
<property name="remoteMode">${undertow.remote}</property>
|
||||
<property name="dataCenter">1</property>
|
||||
<property name="keycloakConfigPropertyOverrides">{
|
||||
"keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.2",
|
||||
"keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-1.1",
|
||||
"keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-1_1",
|
||||
"keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}",
|
||||
"keycloak.connectionsInfinispan.remoteStoreServer": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}",
|
||||
"keycloak.connectionsInfinispan.remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}",
|
||||
"keycloak.connectionsInfinispan.remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort.2:11222}",
|
||||
"keycloak.connectionsInfinispan.remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:true}",
|
||||
"keycloak.connectionsJpa.url": "${keycloak.connectionsJpa.url.crossdc:jdbc:h2:mem:test-dc-shared}",
|
||||
"keycloak.connectionsJpa.driver": "${keycloak.connectionsJpa.driver.crossdc:org.h2.Driver}",
|
||||
@@ -273,22 +297,22 @@
|
||||
}</property>
|
||||
</configuration>
|
||||
</container>
|
||||
<container qualifier="auth-server-undertow-cross-dc-1.2-manual" mode="manual" >
|
||||
<container qualifier="auth-server-undertow-cross-dc-1_2-manual" mode="manual" >
|
||||
<configuration>
|
||||
<property name="enabled">${auth.server.undertow.crossdc}</property>
|
||||
<property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow</property>
|
||||
<property name="bindAddress">localhost</property>
|
||||
<property name="bindHttpPort">${auth.server.http.port}</property>
|
||||
<property name="bindHttpPortOffset">-68</property>
|
||||
<property name="route">auth-server-undertow-cross-dc-1.2</property>
|
||||
<property name="route">auth-server-undertow-cross-dc-1_2-manual</property>
|
||||
<property name="remoteMode">${undertow.remote}</property>
|
||||
<property name="dataCenter">1</property>
|
||||
<property name="keycloakConfigPropertyOverrides">{
|
||||
"keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.2",
|
||||
"keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-1.2",
|
||||
"keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-1_2-manual",
|
||||
"keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}",
|
||||
"keycloak.connectionsInfinispan.remoteStoreServer": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}",
|
||||
"keycloak.connectionsInfinispan.remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}",
|
||||
"keycloak.connectionsInfinispan.remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort.2:11222}",
|
||||
"keycloak.connectionsInfinispan.remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:true}",
|
||||
"keycloak.connectionsJpa.url": "${keycloak.connectionsJpa.url.crossdc:jdbc:h2:mem:test-dc-shared}",
|
||||
"keycloak.connectionsJpa.driver": "${keycloak.connectionsJpa.driver.crossdc:org.h2.Driver}",
|
||||
|
||||
@@ -71,6 +71,50 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"name": "Group A",
|
||||
"path": "/Group A",
|
||||
"attributes": {
|
||||
"topAttribute": [
|
||||
"true"
|
||||
]
|
||||
},
|
||||
"subGroups": [
|
||||
{
|
||||
"name": "Group B",
|
||||
"path": "/Group A/Group B",
|
||||
"attributes": {
|
||||
"level2Attribute": [
|
||||
"true"
|
||||
]
|
||||
},
|
||||
"subGroups": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group C",
|
||||
"path": "/Group C",
|
||||
"attributes": {
|
||||
"topAttribute": [
|
||||
"true"
|
||||
]
|
||||
},
|
||||
"subGroups": [
|
||||
{
|
||||
"name": "Group D",
|
||||
"path": "/Group C/Group D",
|
||||
"attributes": {
|
||||
"level2Attribute": [
|
||||
"true"
|
||||
]
|
||||
},
|
||||
"subGroups": []
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"users": [
|
||||
{
|
||||
"username": "wburke",
|
||||
@@ -298,6 +342,14 @@
|
||||
"clients": "[\"broker\",\"admin-cli\"]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Group Policy Test",
|
||||
"type": "group",
|
||||
"config": {
|
||||
"groupsClaim": "groups",
|
||||
"groups": "[{\"path\":\"/Group A\",\"extendChildren\":true},{\"path\":\"/Group A/Group B\",\"extendChildren\":false},{\"path\":\"/Group C/Group D\",\"extendChildren\":true}]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Only Premium User Policy",
|
||||
"description": "Defines that only premium users can do something",
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2017 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.testsuite.console.page.clients.authorization.policy;
|
||||
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public class GroupPolicy implements PolicyTypeUI {
|
||||
|
||||
@Page
|
||||
private GroupPolicyForm form;
|
||||
|
||||
public GroupPolicyForm form() {
|
||||
return form;
|
||||
}
|
||||
|
||||
public GroupPolicyRepresentation toRepresentation() {
|
||||
return form.toRepresentation();
|
||||
}
|
||||
|
||||
public void update(GroupPolicyRepresentation expected) {
|
||||
form().populate(expected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* 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.console.page.clients.authorization.policy;
|
||||
|
||||
import static org.openqa.selenium.By.tagName;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jboss.arquillian.drone.api.annotation.Drone;
|
||||
import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.Logic;
|
||||
import org.keycloak.testsuite.page.Form;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
import org.openqa.selenium.support.ui.Select;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public class GroupPolicyForm extends Form {
|
||||
|
||||
@FindBy(id = "name")
|
||||
private WebElement name;
|
||||
|
||||
@FindBy(id = "description")
|
||||
private WebElement description;
|
||||
|
||||
@FindBy(id = "groupsClaim")
|
||||
private WebElement groupsClaim;
|
||||
|
||||
@FindBy(id = "logic")
|
||||
private Select logic;
|
||||
|
||||
@FindBy(xpath = "//i[contains(@class,'pficon-delete')]")
|
||||
private WebElement deleteButton;
|
||||
|
||||
@FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']")
|
||||
private WebElement confirmDelete;
|
||||
|
||||
@FindBy(id = "selectGroup")
|
||||
private WebElement selectGroupButton;
|
||||
|
||||
@Drone
|
||||
private WebDriver driver;
|
||||
|
||||
public void populate(GroupPolicyRepresentation expected) {
|
||||
setInputValue(name, expected.getName());
|
||||
setInputValue(description, expected.getDescription());
|
||||
setInputValue(groupsClaim, expected.getGroupsClaim());
|
||||
logic.selectByValue(expected.getLogic().name());
|
||||
|
||||
|
||||
for (GroupPolicyRepresentation.GroupDefinition definition : toRepresentation().getGroups()) {
|
||||
boolean isExpected = false;
|
||||
|
||||
for (GroupPolicyRepresentation.GroupDefinition expectedDef : expected.getGroups()) {
|
||||
if (definition.getPath().equals(expectedDef.getPath())) {
|
||||
isExpected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isExpected) {
|
||||
unselect(definition.getPath());
|
||||
}
|
||||
}
|
||||
|
||||
for (GroupPolicyRepresentation.GroupDefinition definition : expected.getGroups()) {
|
||||
String path = definition.getPath();
|
||||
String groupName = path.substring(path.lastIndexOf('/') + 1);
|
||||
WebElement element = driver.findElement(By.xpath("//span[text()='" + groupName + "']"));
|
||||
element.click();
|
||||
selectGroupButton.click();
|
||||
driver.findElements(By.xpath("(//table[@id='selected-groups'])/tbody/tr")).stream()
|
||||
.filter(webElement -> webElement.findElements(tagName("td")).size() > 1)
|
||||
.map(webElement -> webElement.findElements(tagName("td")))
|
||||
.filter(tds -> tds.get(0).getText().equals(definition.getPath()))
|
||||
.forEach(tds -> {
|
||||
if (!tds.get(1).findElement(By.tagName("input")).isSelected()) {
|
||||
if (definition.isExtendChildren()) {
|
||||
tds.get(1).findElement(By.tagName("input")).click();
|
||||
}
|
||||
} else {
|
||||
if (!definition.isExtendChildren() && tds.get(1).findElement(By.tagName("input")).isSelected()) {
|
||||
tds.get(1).findElement(By.tagName("input")).click();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
save();
|
||||
}
|
||||
|
||||
private void unselect(String path) {
|
||||
for (WebElement webElement : driver.findElements(By.xpath("(//table[@id='selected-groups'])/tbody/tr"))) {
|
||||
List<WebElement> tds = webElement.findElements(tagName("td"));
|
||||
|
||||
if (tds.size() > 1) {
|
||||
if (tds.get(0).getText().equals(path)) {
|
||||
tds.get(2).findElement(By.tagName("button")).click();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void delete() {
|
||||
deleteButton.click();
|
||||
confirmDelete.click();
|
||||
}
|
||||
|
||||
public GroupPolicyRepresentation toRepresentation() {
|
||||
GroupPolicyRepresentation representation = new GroupPolicyRepresentation();
|
||||
|
||||
representation.setName(getInputValue(name));
|
||||
representation.setDescription(getInputValue(description));
|
||||
representation.setGroupsClaim(getInputValue(groupsClaim));
|
||||
representation.setLogic(Logic.valueOf(logic.getFirstSelectedOption().getText().toUpperCase()));
|
||||
representation.setGroups(new HashSet<>());
|
||||
|
||||
driver.findElements(By.xpath("(//table[@id='selected-groups'])/tbody/tr")).stream()
|
||||
.filter(webElement -> webElement.findElements(tagName("td")).size() > 1)
|
||||
.forEach(webElement -> {
|
||||
List<WebElement> tds = webElement.findElements(tagName("td"));
|
||||
representation.addGroupPath(tds.get(0).getText(), tds.get(1).findElement(By.tagName("input")).isSelected());
|
||||
});
|
||||
|
||||
return representation;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.AggregatePolicyRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.JSPolicyRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
|
||||
@@ -66,6 +67,9 @@ public class Policies extends Form {
|
||||
@Page
|
||||
private ClientPolicy clientPolicy;
|
||||
|
||||
@Page
|
||||
private GroupPolicy groupPolicy;
|
||||
|
||||
public PoliciesTable policies() {
|
||||
return table;
|
||||
}
|
||||
@@ -103,6 +107,10 @@ public class Policies extends Form {
|
||||
clientPolicy.form().populate((ClientPolicyRepresentation) expected);
|
||||
clientPolicy.form().save();
|
||||
return (P) clientPolicy;
|
||||
} else if ("group".equals(type)) {
|
||||
groupPolicy.form().populate((GroupPolicyRepresentation) expected);
|
||||
groupPolicy.form().save();
|
||||
return (P) groupPolicy;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -130,6 +138,8 @@ public class Policies extends Form {
|
||||
rulePolicy.form().populate((RulePolicyRepresentation) representation);
|
||||
} else if ("client".equals(type)) {
|
||||
clientPolicy.form().populate((ClientPolicyRepresentation) representation);
|
||||
} else if ("group".equals(type)) {
|
||||
groupPolicy.form().populate((GroupPolicyRepresentation) representation);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -158,6 +168,8 @@ public class Policies extends Form {
|
||||
return (P) rulePolicy;
|
||||
} else if ("client".equals(type)) {
|
||||
return (P) clientPolicy;
|
||||
} else if ("group".equals(type)) {
|
||||
return (P) groupPolicy;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,6 +199,8 @@ public class Policies extends Form {
|
||||
rulePolicy.form().delete();
|
||||
} else if ("client".equals(type)) {
|
||||
clientPolicy.form().delete();
|
||||
} else if ("group".equals(type)) {
|
||||
groupPolicy.form().delete();
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* 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.console.authorization;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.admin.client.resource.RolesResource;
|
||||
import org.keycloak.representations.idm.GroupRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.Logic;
|
||||
import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.UserPolicyRepresentation;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.console.page.clients.authorization.policy.GroupPolicy;
|
||||
import org.keycloak.testsuite.console.page.clients.authorization.policy.RolePolicy;
|
||||
import org.keycloak.testsuite.console.page.clients.authorization.policy.UserPolicy;
|
||||
import org.keycloak.testsuite.util.GroupBuilder;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public class GroupPolicyManagementTest extends AbstractAuthorizationSettingsTest {
|
||||
|
||||
@Before
|
||||
public void configureTest() {
|
||||
super.configureTest();
|
||||
RealmResource realmResource = testRealmResource();
|
||||
String groupAId = ApiUtil.getCreatedId(realmResource.groups().add(GroupBuilder.create().name("Group A").build()));
|
||||
String groupBId = ApiUtil.getCreatedId(realmResource.groups().group(groupAId).subGroup(GroupBuilder.create().name("Group B").build()));
|
||||
realmResource.groups().group(groupBId).subGroup(GroupBuilder.create().name("Group D").build());
|
||||
realmResource.groups().group(groupBId).subGroup(GroupBuilder.create().name("Group E").build());
|
||||
realmResource.groups().group(groupAId).subGroup(GroupBuilder.create().name("Group C").build());
|
||||
realmResource.groups().add(GroupBuilder.create().name("Group F").build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdate() throws InterruptedException {
|
||||
authorizationPage.navigateTo();
|
||||
GroupPolicyRepresentation expected = new GroupPolicyRepresentation();
|
||||
|
||||
expected.setName("Test Group Policy");
|
||||
expected.setDescription("description");
|
||||
expected.setGroupsClaim("groups");
|
||||
expected.addGroupPath("/Group A", true);
|
||||
expected.addGroupPath("/Group A/Group B/Group D");
|
||||
expected.addGroupPath("Group F");
|
||||
|
||||
expected = createPolicy(expected);
|
||||
|
||||
String previousName = expected.getName();
|
||||
|
||||
expected.setName("Changed Test Group Policy");
|
||||
expected.setDescription("Changed description");
|
||||
expected.setLogic(Logic.NEGATIVE);
|
||||
|
||||
authorizationPage.navigateTo();
|
||||
authorizationPage.authorizationTabs().policies().update(previousName, expected);
|
||||
assertAlertSuccess();
|
||||
|
||||
authorizationPage.navigateTo();
|
||||
GroupPolicy actual = authorizationPage.authorizationTabs().policies().name(expected.getName());
|
||||
|
||||
assertPolicy(expected, actual);
|
||||
|
||||
expected.getGroups().clear();
|
||||
expected.addGroupPath("/Group A", false);
|
||||
expected.addGroupPath("/Group A/Group B/Group D");
|
||||
|
||||
authorizationPage.navigateTo();
|
||||
authorizationPage.authorizationTabs().policies().update(expected.getName(), expected);
|
||||
assertAlertSuccess();
|
||||
|
||||
authorizationPage.navigateTo();
|
||||
actual = authorizationPage.authorizationTabs().policies().name(expected.getName());
|
||||
|
||||
assertPolicy(expected, actual);
|
||||
|
||||
expected.getGroups().clear();
|
||||
expected.addGroupPath("/Group E");
|
||||
expected.addGroupPath("/Group A/Group B", true);
|
||||
expected.addGroupPath("/Group A/Group C");
|
||||
|
||||
|
||||
authorizationPage.navigateTo();
|
||||
authorizationPage.authorizationTabs().policies().update(expected.getName(), expected);
|
||||
assertAlertSuccess();
|
||||
|
||||
authorizationPage.navigateTo();
|
||||
actual = authorizationPage.authorizationTabs().policies().name(expected.getName());
|
||||
|
||||
assertPolicy(expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDelete() throws InterruptedException {
|
||||
authorizationPage.navigateTo();
|
||||
GroupPolicyRepresentation expected = new GroupPolicyRepresentation();
|
||||
|
||||
expected.setName("Test Delete Group Policy");
|
||||
expected.setDescription("description");
|
||||
expected.setGroupsClaim("groups");
|
||||
expected.addGroupPath("/Group A", true);
|
||||
expected.addGroupPath("/Group A/Group B/Group D");
|
||||
expected.addGroupPath("Group F");
|
||||
|
||||
expected = createPolicy(expected);
|
||||
authorizationPage.navigateTo();
|
||||
authorizationPage.authorizationTabs().policies().delete(expected.getName());
|
||||
assertAlertSuccess();
|
||||
authorizationPage.navigateTo();
|
||||
assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName()));
|
||||
}
|
||||
|
||||
private GroupPolicyRepresentation createPolicy(GroupPolicyRepresentation expected) {
|
||||
GroupPolicy policy = authorizationPage.authorizationTabs().policies().create(expected);
|
||||
assertAlertSuccess();
|
||||
return assertPolicy(expected, policy);
|
||||
}
|
||||
|
||||
private GroupPolicyRepresentation assertPolicy(GroupPolicyRepresentation expected, GroupPolicy policy) {
|
||||
GroupPolicyRepresentation actual = policy.toRepresentation();
|
||||
|
||||
assertEquals(expected.getName(), actual.getName());
|
||||
assertEquals(expected.getDescription(), actual.getDescription());
|
||||
assertEquals(expected.getLogic(), actual.getLogic());
|
||||
|
||||
assertNotNull(actual.getGroups());
|
||||
assertEquals(expected.getGroups().size(), actual.getGroups().size());
|
||||
assertEquals(0, actual.getGroups().stream().filter(actualDefinition -> !expected.getGroups().stream()
|
||||
.filter(groupDefinition -> actualDefinition.getPath().contains(groupDefinition.getPath()) && actualDefinition.isExtendChildren() == groupDefinition.isExtendChildren())
|
||||
.findFirst().isPresent())
|
||||
.count());
|
||||
return actual;
|
||||
}
|
||||
}
|
||||
@@ -73,9 +73,12 @@
|
||||
<cache.server.home>${containers.home}/${cache.server.container}</cache.server.home>
|
||||
<cache.server.port.offset>1010</cache.server.port.offset>
|
||||
<cache.server.management.port>11000</cache.server.management.port>
|
||||
<cache.server.2.port.offset>2010</cache.server.2.port.offset>
|
||||
<cache.server.2.management.port>12000</cache.server.2.management.port>
|
||||
<cache.server.console.output>true</cache.server.console.output>
|
||||
<keycloak.connectionsInfinispan.remoteStoreServer>localhost</keycloak.connectionsInfinispan.remoteStoreServer>
|
||||
<keycloak.connectionsInfinispan.remoteStorePort>12232</keycloak.connectionsInfinispan.remoteStorePort>
|
||||
<keycloak.connectionsInfinispan.remoteStorePort.2>13232</keycloak.connectionsInfinispan.remoteStorePort.2>
|
||||
<keycloak.connectionsJpa.url.crossdc>jdbc:h2:mem:test-dc-shared</keycloak.connectionsJpa.url.crossdc>
|
||||
|
||||
<adapter.test.props/>
|
||||
@@ -176,6 +179,23 @@
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>clean-second-cache-server-arquillian-bug-workaround</id>
|
||||
<phase>process-test-resources</phase>
|
||||
<goals><goal>run</goal></goals>
|
||||
<configuration>
|
||||
<target>
|
||||
<echo>${cache.server.home}/standalone-dc-2</echo>
|
||||
<delete failonerror="false" dir="${cache.server.home}/standalone-dc-2" />
|
||||
<mkdir dir="${cache.server.home}/standalone-dc-2/deployments" />
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
@@ -252,8 +272,11 @@
|
||||
<cache.server.home>${cache.server.home}</cache.server.home>
|
||||
<cache.server.console.output>${cache.server.console.output}</cache.server.console.output>
|
||||
<cache.server.management.port>${cache.server.management.port}</cache.server.management.port>
|
||||
<cache.server.2.port.offset>${cache.server.2.port.offset}</cache.server.2.port.offset>
|
||||
<cache.server.2.management.port>${cache.server.2.management.port}</cache.server.2.management.port>
|
||||
|
||||
<keycloak.connectionsInfinispan.remoteStorePort>${keycloak.connectionsInfinispan.remoteStorePort}</keycloak.connectionsInfinispan.remoteStorePort>
|
||||
<keycloak.connectionsInfinispan.remoteStorePort.2>${keycloak.connectionsInfinispan.remoteStorePort.2}</keycloak.connectionsInfinispan.remoteStorePort.2>
|
||||
<keycloak.connectionsInfinispan.remoteStoreServer>${keycloak.connectionsInfinispan.remoteStoreServer}</keycloak.connectionsInfinispan.remoteStoreServer>
|
||||
|
||||
<keycloak.connectionsJpa.url.crossdc>${keycloak.connectionsJpa.url.crossdc}</keycloak.connectionsJpa.url.crossdc>
|
||||
|
||||
@@ -1209,6 +1209,13 @@ authz-policy-js-code.tooltip=The JavaScript code providing the conditions for th
|
||||
authz-aggregated=Aggregated
|
||||
authz-add-aggregated-policy=Add Aggregated Policy
|
||||
|
||||
# Authz Group Policy Detail
|
||||
authz-add-group-policy=Add Group Policy
|
||||
authz-no-groups-assigned=No groups assigned.
|
||||
authz-policy-group-claim=Groups Claim
|
||||
authz-policy-group-claim.tooltip=A claim to use as the source for user’s group. If the claim is present it must be an array of strings.
|
||||
authz-policy-group-groups.tooltip=Specifies the groups allowed by this policy.
|
||||
|
||||
# Authz Permission List
|
||||
authz-no-permissions-available=No permissions available.
|
||||
|
||||
|
||||
@@ -324,7 +324,29 @@ module.config(['$routeProvider', function ($routeProvider) {
|
||||
}
|
||||
},
|
||||
controller: 'ResourceServerPolicyRoleDetailCtrl'
|
||||
}).when('/realms/:realm/clients/:client/authz/resource-server/policy/js/create', {
|
||||
}).when('/realms/:realm/clients/:client/authz/resource-server/policy/group/create', {
|
||||
templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-group-detail.html',
|
||||
resolve: {
|
||||
realm: function (RealmLoader) {
|
||||
return RealmLoader();
|
||||
},
|
||||
client : function(ClientLoader) {
|
||||
return ClientLoader();
|
||||
}
|
||||
},
|
||||
controller: 'ResourceServerPolicyGroupDetailCtrl'
|
||||
}).when('/realms/:realm/clients/:client/authz/resource-server/policy/group/:id', {
|
||||
templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-group-detail.html',
|
||||
resolve: {
|
||||
realm: function (RealmLoader) {
|
||||
return RealmLoader();
|
||||
},
|
||||
client : function(ClientLoader) {
|
||||
return ClientLoader();
|
||||
}
|
||||
},
|
||||
controller: 'ResourceServerPolicyGroupDetailCtrl'
|
||||
}).when('/realms/:realm/clients/:client/authz/resource-server/policy/js/create', {
|
||||
templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-js-detail.html',
|
||||
resolve: {
|
||||
realm: function (RealmLoader) {
|
||||
|
||||
@@ -1663,6 +1663,119 @@ module.controller('ResourceServerPolicyRoleDetailCtrl', function($scope, $route,
|
||||
}
|
||||
});
|
||||
|
||||
module.controller('ResourceServerPolicyGroupDetailCtrl', function($scope, $route, realm, client, Client, Groups, Group, PolicyController) {
|
||||
PolicyController.onInit({
|
||||
getPolicyType : function() {
|
||||
return "group";
|
||||
},
|
||||
|
||||
onInit : function() {
|
||||
$scope.tree = [];
|
||||
|
||||
Groups.query({realm: $route.current.params.realm}, function(groups) {
|
||||
$scope.groups = groups;
|
||||
$scope.groupList = [
|
||||
{"id" : "realm", "name": "Groups",
|
||||
"subGroups" : groups}
|
||||
];
|
||||
});
|
||||
|
||||
var isLeaf = function(node) {
|
||||
return node.id != "realm" && (!node.subGroups || node.subGroups.length == 0);
|
||||
}
|
||||
|
||||
$scope.getGroupClass = function(node) {
|
||||
if (node.id == "realm") {
|
||||
return 'pficon pficon-users';
|
||||
}
|
||||
if (isLeaf(node)) {
|
||||
return 'normal';
|
||||
}
|
||||
if (node.subGroups.length && node.collapsed) return 'collapsed';
|
||||
if (node.subGroups.length && !node.collapsed) return 'expanded';
|
||||
return 'collapsed';
|
||||
|
||||
}
|
||||
|
||||
$scope.getSelectedClass = function(node) {
|
||||
if (node.selected) {
|
||||
return 'selected';
|
||||
} else if ($scope.cutNode && $scope.cutNode.id == node.id) {
|
||||
return 'cut';
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
$scope.selectGroup = function(group) {
|
||||
for (i = 0; i < $scope.selectedGroups.length; i++) {
|
||||
if ($scope.selectedGroups[i].id == group.id) {
|
||||
return
|
||||
}
|
||||
}
|
||||
$scope.selectedGroups.push({id: group.id, path: group.path});
|
||||
$scope.changed = true;
|
||||
}
|
||||
|
||||
$scope.extendChildren = function(group) {
|
||||
$scope.changed = true;
|
||||
}
|
||||
|
||||
$scope.removeFromList = function(group) {
|
||||
var index = $scope.selectedGroups.indexOf(group);
|
||||
if (index != -1) {
|
||||
$scope.selectedGroups.splice(index, 1);
|
||||
$scope.changed = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onInitCreate : function(policy) {
|
||||
var selectedGroups = [];
|
||||
|
||||
$scope.selectedGroups = angular.copy(selectedGroups);
|
||||
|
||||
$scope.$watch('selectedGroups', function() {
|
||||
if (!angular.equals($scope.selectedGroups, selectedGroups)) {
|
||||
$scope.changed = true;
|
||||
} else {
|
||||
$scope.changed = false;
|
||||
}
|
||||
}, true);
|
||||
},
|
||||
|
||||
onInitUpdate : function(policy) {
|
||||
$scope.selectedGroups = policy.groups;
|
||||
|
||||
angular.forEach($scope.selectedGroups, function(group, index){
|
||||
Group.get({realm: $route.current.params.realm, groupId: group.id}, function (existing) {
|
||||
group.path = existing.path;
|
||||
});
|
||||
});
|
||||
|
||||
$scope.$watch('selectedGroups', function() {
|
||||
if (!$scope.changed) {
|
||||
return;
|
||||
}
|
||||
if (!angular.equals($scope.selectedGroups, selectedGroups)) {
|
||||
$scope.changed = true;
|
||||
} else {
|
||||
$scope.changed = false;
|
||||
}
|
||||
}, true);
|
||||
},
|
||||
|
||||
onUpdate : function() {
|
||||
$scope.policy.groups = $scope.selectedGroups;
|
||||
delete $scope.policy.config;
|
||||
},
|
||||
|
||||
onCreate : function() {
|
||||
$scope.policy.groups = $scope.selectedGroups;
|
||||
delete $scope.policy.config;
|
||||
}
|
||||
}, realm, client, $scope);
|
||||
});
|
||||
|
||||
module.controller('ResourceServerPolicyJSDetailCtrl', function($scope, $route, $location, realm, PolicyController, client) {
|
||||
PolicyController.onInit({
|
||||
getPolicyType : function() {
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
<!--
|
||||
~ * 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.
|
||||
-->
|
||||
|
||||
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
|
||||
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="#/realms/{{realm.realm}}/clients">{{:: 'clients' | translate}}</a></li>
|
||||
<li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li>
|
||||
<li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">{{:: 'authz-authorization' | translate}}</a></li>
|
||||
<li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/policy">{{:: 'authz-policies' | translate}}</a></li>
|
||||
<li data-ng-show="create">{{:: 'authz-add-group-policy' | translate}}</li>
|
||||
<li data-ng-hide="create">{{:: 'groups' | translate}}</li>
|
||||
<li data-ng-hide="create">{{originalPolicy.name}}</li>
|
||||
</ol>
|
||||
|
||||
<h1 data-ng-show="create">{{:: 'authz-add-group-policy' | translate}}</h1>
|
||||
<h1 data-ng-hide="create">{{originalPolicy.name|capitalize}}<i class="pficon pficon-delete clickable" data-ng-show="!create"
|
||||
data-ng-click="remove()"></i></h1>
|
||||
|
||||
<form class="form-horizontal" name="groupPolicyForm" novalidate>
|
||||
<fieldset class="border-top">
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="name">{{:: 'name' | translate}} <span class="required">*</span></label>
|
||||
<div class="col-sm-6">
|
||||
<input class="form-control" type="text" id="name" name="name" data-ng-model="policy.name" autofocus required data-ng-blur="checkNewNameAvailability()">
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'authz-policy-name.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="description">{{:: 'description' | translate}} </label>
|
||||
<div class="col-sm-6">
|
||||
<input class="form-control" type="text" id="description" name="description" data-ng-model="policy.description">
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'authz-policy-description.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="groupsClaim">{{:: 'authz-policy-group-claim' | translate}} <span class="required">*</span></label>
|
||||
<div class="col-sm-6">
|
||||
<input class="form-control" type="text" id="groupsClaim" name="groupsClaim" data-ng-model="policy.groupsClaim" required>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'authz-policy-group-claim.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="selectedGroups">{{:: 'groups' | translate}} <span class="required">*</span></label>
|
||||
<div class="col-md-6">
|
||||
<div tree-id="tree"
|
||||
angular-treeview="true"
|
||||
tree-model="groupList"
|
||||
node-id="id"
|
||||
node-label="name"
|
||||
node-children="subGroups" >
|
||||
</div>
|
||||
<button data-ng-click="selectGroup(tree.currentNode)" id="selectGroup" class="btn btn-primary" data-ng-disabled="tree.currentNode == null">Select</button>
|
||||
<input class="form-control" type="text" id="selectedGroups" name="selectedGroups" data-ng-model="noop" data-ng-required="selectedGroups.length <= 0" autofocus required data-ng-show="false">
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'authz-policy-user-users.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group" data-ng-if="selectedGroups.length > 0">
|
||||
<label class="col-md-2 control-label"></label>
|
||||
<div class="col-md-5">
|
||||
<table class="table table-striped table-bordered" id="selected-groups">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{:: 'path' | translate}}</th>
|
||||
<th class="col-sm-3">Extend to Children</th>
|
||||
<th>{{:: 'actions' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="group in selectedGroups | orderBy:'name' track by $index">
|
||||
<td>{{group.path}}</td>
|
||||
<td>
|
||||
<input type="checkbox" ng-model="group.extendChildren" id="{{role.id}}" data-ng-click="extendChildren()">
|
||||
</td>
|
||||
<td class="kc-action-cell">
|
||||
<button class="btn btn-default btn-block btn-sm" ng-click="removeFromList(group);">{{:: 'remove' | translate}}</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr data-ng-show="!selectedGroups.length">
|
||||
<td class="text-muted" colspan="3">{{:: 'authz-no-groups-assigned' | translate}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group clearfix">
|
||||
<label class="col-md-2 control-label" for="logic">{{:: 'authz-policy-logic' | translate}}</label>
|
||||
|
||||
<div class="col-sm-1">
|
||||
<select class="form-control" id="logic"
|
||||
data-ng-model="policy.logic">
|
||||
<option value="POSITIVE">{{:: 'authz-policy-logic-positive' | translate}}</option>
|
||||
<option value="NEGATIVE">{{:: 'authz-policy-logic-negative' | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<kc-tooltip>{{:: 'authz-policy-logic.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<input type="hidden" data-ng-model="policy.type"/>
|
||||
</fieldset>
|
||||
<div class="form-group" data-ng-show="access.manageAuthorization">
|
||||
<div class="col-md-10 col-md-offset-2">
|
||||
<button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
|
||||
<button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<kc-menu></kc-menu>
|
||||
Reference in New Issue
Block a user