KEYCLOAK-19283 Implemented new identity provider mapper "Advanced claim to group mapper" alongside tests.

This commit is contained in:
bal1imb 2021-09-13 04:32:35 -07:00 committed by Marek Posolda
parent 262cde8f52
commit 661aca4452
16 changed files with 778 additions and 4 deletions

View file

@ -224,6 +224,21 @@ public class BrokeredIdentityContext {
return roles; return roles;
} }
/**
* Obtains the set of groups that were assigned by mappers.
*
* @return a {@link Set} containing the groups.
*/
@SuppressWarnings("unchecked")
private Set<String> getMapperAssignedGroups() {
Set<String> groups = (Set<String>) this.contextData.get(Constants.MAPPER_GRANTED_GROUPS);
if (groups == null) {
groups = new HashSet<>();
this.contextData.put(Constants.MAPPER_GRANTED_GROUPS, groups);
}
return groups;
}
/** /**
* Verifies if a mapper has already granted the specified role. * Verifies if a mapper has already granted the specified role.
* *
@ -234,6 +249,16 @@ public class BrokeredIdentityContext {
return this.getMapperGrantedRoles().contains(roleName); return this.getMapperGrantedRoles().contains(roleName);
} }
/**
* Verifies if a mapper has already assigned the specified group.
*
* @param groupId the id of the group.
* @return {@code true} if a mapper has already assigned the group; {@code false} otherwise.
*/
public boolean hasMapperAssignedGroup(final String groupId) {
return this.getMapperAssignedGroups().contains(groupId);
}
/** /**
* Adds the specified role to the set of roles granted by mappers. * Adds the specified role to the set of roles granted by mappers.
* *
@ -243,6 +268,15 @@ public class BrokeredIdentityContext {
this.getMapperGrantedRoles().add(roleName); this.getMapperGrantedRoles().add(roleName);
} }
/**
* Adds the specified group to the set of groups assigned by mappers.
*
* @param groupId the id of the group.
*/
public void addMapperAssignedGroup(final String groupId) {
this.getMapperAssignedGroups().add(groupId);
}
/** /**
* @deprecated use {@link #setFirstName(String)} and {@link #setLastName(String)} instead * @deprecated use {@link #setFirstName(String)} and {@link #setLastName(String)} instead
* @param name * @param name

View file

@ -23,4 +23,5 @@ package org.keycloak.broker.provider;
*/ */
public interface ConfigConstants { public interface ConfigConstants {
String ROLE = "role"; String ROLE = "role";
String GROUP = "group";
} }

View file

@ -99,6 +99,9 @@ public final class Constants {
// Roles already granted by a mapper when updating brokered users. // Roles already granted by a mapper when updating brokered users.
public static final String MAPPER_GRANTED_ROLES = "MAPPER_GRANTED_ROLES"; public static final String MAPPER_GRANTED_ROLES = "MAPPER_GRANTED_ROLES";
// Groups already assigned by a mapper when updating brokered users.
public static final String MAPPER_GRANTED_GROUPS = "MAPPER_GRANTED_GROUPS";
// Indication to admin-rest-endpoint that realm keys should be re-generated // Indication to admin-rest-endpoint that realm keys should be re-generated
public static final String GENERATE = "GENERATE"; public static final String GENERATE = "GENERATE";

View file

@ -38,6 +38,7 @@ public class ProviderConfigProperty {
public static final String SCRIPT_TYPE="Script"; public static final String SCRIPT_TYPE="Script";
public static final String FILE_TYPE="File"; public static final String FILE_TYPE="File";
public static final String ROLE_TYPE="Role"; public static final String ROLE_TYPE="Role";
public static final String GROUP_TYPE="Group";
/** /**
* Possibility to configure single String value, which needs to be chosen from the list of predefined values (HTML select) * Possibility to configure single String value, which needs to be chosen from the list of predefined values (HTML select)

View file

@ -0,0 +1,88 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.broker.oidc.mappers;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.ConfigConstants;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.models.GroupModel;
import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
public abstract class AbstractClaimToGroupMapper extends AbstractClaimMapper {
@Override
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user,
IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
GroupModel group = this.getGroup(realm, mapperModel);
if (applies(mapperModel, context)) {
user.joinGroup(group);
}
}
@Override
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user,
IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
GroupModel group = this.getGroup(realm, mapperModel);
String groupId = mapperModel.getConfig().get(ConfigConstants.GROUP);
if (!context.hasMapperAssignedGroup(groupId)) {
if (applies(mapperModel, context)) {
context.addMapperAssignedGroup(groupId);
user.joinGroup(group);
} else {
user.leaveGroup(group);
}
}
}
/**
* This method must be implemented by subclasses and they must return {@code true} if their mapping can be applied
* (i.e. user has the OIDC claim that should be mapped) or {@code false} otherwise.
*
* @param mapperModel a reference to the {@link IdentityProviderMapperModel}.
* @param context a reference to the {@link BrokeredIdentityContext}.
* @return {@code true} if the mapping can be applied or {@code false} otherwise.*
*/
protected abstract boolean applies(final IdentityProviderMapperModel mapperModel,
final BrokeredIdentityContext context);
/**
* Obtains the {@link GroupModel} corresponding the group configured in the specified
* {@link IdentityProviderMapperModel}.
* If the group doesn't exist, this method throws an {@link IdentityBrokerException}.
*
* @param realm a reference to the realm.
* @param mapperModel a reference to the {@link IdentityProviderMapperModel} containing the configured group.
* @return the {@link GroupModel}
* @throws IdentityBrokerException if the group doesn't exist.
*/
private GroupModel getGroup(final RealmModel realm, final IdentityProviderMapperModel mapperModel) {
GroupModel group = KeycloakModelUtils.findGroupByPath(realm, mapperModel.getConfig().get(ConfigConstants.GROUP));
if (group == null) {
throw new IdentityBrokerException("Unable to find group: " + group.getId());
}
return group;
}
}

View file

@ -0,0 +1,120 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.broker.oidc.mappers;
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.ConfigConstants;
import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.IdentityProviderSyncMode;
import org.keycloak.provider.ProviderConfigProperty;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.keycloak.utils.RegexUtils.valueMatchesRegex;
public class AdvancedClaimToGroupMapper extends AbstractClaimToGroupMapper {
public static final String CLAIM_PROPERTY_NAME = "claims";
public static final String ARE_CLAIM_VALUES_REGEX_PROPERTY_NAME = "are.claim.values.regex";
public static final String[] COMPATIBLE_PROVIDERS = {KeycloakOIDCIdentityProviderFactory.PROVIDER_ID, OIDCIdentityProviderFactory.PROVIDER_ID};
private static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
static {
ProviderConfigProperty claimsProperty = new ProviderConfigProperty();
claimsProperty.setName(CLAIM_PROPERTY_NAME);
claimsProperty.setLabel("Claims");
claimsProperty.setHelpText("Name and value of the claims to search for in token. You can reference nested claims using a '.', i.e. 'address.locality'. To use dot (.) literally, escape it with backslash (\\.)");
claimsProperty.setType(ProviderConfigProperty.MAP_TYPE);
configProperties.add(claimsProperty);
ProviderConfigProperty isClaimValueRegexProperty = new ProviderConfigProperty();
isClaimValueRegexProperty.setName(ARE_CLAIM_VALUES_REGEX_PROPERTY_NAME);
isClaimValueRegexProperty.setLabel("Regex Claim Values");
isClaimValueRegexProperty.setHelpText("If enabled claim values are interpreted as regular expressions.");
isClaimValueRegexProperty.setType(ProviderConfigProperty.BOOLEAN_TYPE);
configProperties.add(isClaimValueRegexProperty);
ProviderConfigProperty groupProperty = new ProviderConfigProperty();
groupProperty.setName(ConfigConstants.GROUP);
groupProperty.setLabel("Group");
groupProperty.setHelpText("Group to assign the user to if claim is present.");
groupProperty.setType(ProviderConfigProperty.GROUP_TYPE);
configProperties.add(groupProperty);
}
public static final String PROVIDER_ID = "oidc-advanced-group-idp-mapper";
@Override
public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) {
return true;
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String[] getCompatibleProviders() {
return COMPATIBLE_PROVIDERS;
}
@Override
public String getDisplayCategory() {
return "Group Importer";
}
@Override
public String getDisplayType() {
return "Advanced Claim to Group";
}
@Override
public String getHelpText() {
return "If all claims exists, assign the user to the specified group.";
}
@Override
protected boolean applies(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
Map<String, String> claims = mapperModel.getConfigMap(CLAIM_PROPERTY_NAME);
boolean areClaimValuesRegex = Boolean.parseBoolean(mapperModel.getConfig().get(ARE_CLAIM_VALUES_REGEX_PROPERTY_NAME));
for (Map.Entry<String, String> claim : claims.entrySet()) {
Object value = getClaimValue(context, claim.getKey());
boolean claimValuesMismatch = !(areClaimValuesRegex ? valueMatchesRegex(claim.getValue(), value) : valueEquals(claim.getValue(), value));
if (claimValuesMismatch) {
return false;
}
}
return true;
}
}

View file

@ -20,6 +20,7 @@ org.keycloak.broker.provider.HardcodedAttributeMapper
org.keycloak.broker.provider.HardcodedUserSessionAttributeMapper org.keycloak.broker.provider.HardcodedUserSessionAttributeMapper
org.keycloak.broker.oidc.mappers.ClaimToRoleMapper org.keycloak.broker.oidc.mappers.ClaimToRoleMapper
org.keycloak.broker.oidc.mappers.AdvancedClaimToRoleMapper org.keycloak.broker.oidc.mappers.AdvancedClaimToRoleMapper
org.keycloak.broker.oidc.mappers.AdvancedClaimToGroupMapper
org.keycloak.broker.oidc.mappers.ExternalKeycloakRoleToRoleMapper org.keycloak.broker.oidc.mappers.ExternalKeycloakRoleToRoleMapper
org.keycloak.broker.oidc.mappers.UserAttributeMapper org.keycloak.broker.oidc.mappers.UserAttributeMapper
org.keycloak.broker.oidc.mappers.UsernameTemplateMapper org.keycloak.broker.oidc.mappers.UsernameTemplateMapper

View file

@ -562,12 +562,12 @@ public class IdentityProviderTest extends AbstractAdminTest {
create(createRep("keycloak-oidc", "keycloak-oidc")); create(createRep("keycloak-oidc", "keycloak-oidc"));
provider = realm.identityProviders().get("keycloak-oidc"); provider = realm.identityProviders().get("keycloak-oidc");
mapperTypes = provider.getMapperTypes(); mapperTypes = provider.getMapperTypes();
assertMapperTypes(mapperTypes, "keycloak-oidc-role-to-role-idp-mapper", "oidc-user-attribute-idp-mapper", "oidc-role-idp-mapper", "oidc-username-idp-mapper", "oidc-advanced-role-idp-mapper"); assertMapperTypes(mapperTypes, "keycloak-oidc-role-to-role-idp-mapper", "oidc-user-attribute-idp-mapper", "oidc-role-idp-mapper", "oidc-username-idp-mapper", "oidc-advanced-group-idp-mapper", "oidc-advanced-role-idp-mapper");
create(createRep("oidc", "oidc")); create(createRep("oidc", "oidc"));
provider = realm.identityProviders().get("oidc"); provider = realm.identityProviders().get("oidc");
mapperTypes = provider.getMapperTypes(); mapperTypes = provider.getMapperTypes();
assertMapperTypes(mapperTypes, "oidc-user-attribute-idp-mapper", "oidc-role-idp-mapper", "oidc-username-idp-mapper", "oidc-advanced-role-idp-mapper"); assertMapperTypes(mapperTypes, "oidc-user-attribute-idp-mapper", "oidc-role-idp-mapper", "oidc-username-idp-mapper", "oidc-advanced-group-idp-mapper", "oidc-advanced-role-idp-mapper");
create(createRep("saml", "saml")); create(createRep("saml", "saml"));
provider = realm.identityProviders().get("saml"); provider = realm.identityProviders().get("saml");

View file

@ -0,0 +1,186 @@
package org.keycloak.testsuite.broker;
import static org.keycloak.models.IdentityProviderMapperSyncMode.FORCE;
import static org.keycloak.models.IdentityProviderMapperSyncMode.IMPORT;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.models.IdentityProviderMapperSyncMode;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.List;
public abstract class AbstractAdvancedGroupMapperTest extends AbstractGroupMapperTest {
private static final String CLAIMS_OR_ATTRIBUTES = "[\n" +
" {\n" +
" \"key\": \"" + KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME + "\",\n" +
" \"value\": \"value 1\"\n" +
" },\n" +
" {\n" +
" \"key\": \"" + KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2 + "\",\n" +
" \"value\": \"value 2\"\n" +
" }\n" +
"]";
private static final String CLAIMS_OR_ATTRIBUTES_REGEX = "[\n" +
" {\n" +
" \"key\": \"" + KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME + "\",\n" +
" \"value\": \"va.*\"\n" +
" },\n" +
" {\n" +
" \"key\": \"" + KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2 + "\",\n" +
" \"value\": \"value 2\"\n" +
" }\n" +
"]";
private String newValueForAttribute2 = "";
@Before
public void addMapperTestGroupToConsumerRealm() {
GroupRepresentation mapperTestGroup = new GroupRepresentation();
mapperTestGroup.setName(MAPPER_TEST_GROUP_NAME);
mapperTestGroup.setPath(MAPPER_TEST_GROUP_PATH);
adminClient.realm(bc.consumerRealmName()).groups().add(mapperTestGroup);
}
@Test
public void allValuesMatch() {
createAdvancedGroupMapper(CLAIMS_OR_ATTRIBUTES, false);
createUserInProviderRealm(ImmutableMap.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>builder().add("value 2").build())
.build());
logInAsUserInIDPForFirstTime();
UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail());
assertThatUserHasBeenAssignedToGroup(user);
}
@Test
public void valuesMismatch() {
createAdvancedGroupMapper(CLAIMS_OR_ATTRIBUTES, false);
createUserInProviderRealm(ImmutableMap.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>builder().add("value mismatch").build())
.build());
logInAsUserInIDPForFirstTime();
UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail());
assertThatUserHasNotBeenAssignedToGroup(user);
}
@Test
public void valuesMatchIfNoClaimsSpecified() {
createAdvancedGroupMapper("[]", false);
createUserInProviderRealm(ImmutableMap.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("some value").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>builder().add("some value").build())
.build());
logInAsUserInIDPForFirstTime();
UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail());
assertThatUserHasBeenAssignedToGroup(user);
}
@Test
public void allValuesMatchRegex() {
createAdvancedGroupMapper(CLAIMS_OR_ATTRIBUTES_REGEX, true);
createUserInProviderRealm(ImmutableMap.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>builder().add("value 2").build())
.build());
logInAsUserInIDPForFirstTime();
UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail());
assertThatUserHasBeenAssignedToGroup(user);
}
@Test
public void valuesMismatchRegex() {
createAdvancedGroupMapper(CLAIMS_OR_ATTRIBUTES_REGEX, true);
createUserInProviderRealm(ImmutableMap.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("mismatch").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>builder().add("value 2").build())
.build());
logInAsUserInIDPForFirstTime();
UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail());
assertThatUserHasNotBeenAssignedToGroup(user);
}
@Test
public void updateBrokeredUserMismatchLeavesGroup() {
newValueForAttribute2 = "value mismatch";
UserRepresentation user = createMapperAndLoginAsUserTwiceWithMapper(FORCE, false);
assertThatUserHasNotBeenAssignedToGroup(user);
}
@Test
public void updateBrokeredUserMismatchDoesNotLeaveGroupInImportMode() {
newValueForAttribute2 = "value mismatch";
UserRepresentation user = createMapperAndLoginAsUserTwiceWithMapper(IMPORT, false);
assertThatUserHasBeenAssignedToGroup(user);
}
@Test
public void updateBrokeredUserMatchDoesntLeaveGroup() {
newValueForAttribute2 = "value 2";
UserRepresentation user = createMapperAndLoginAsUserTwiceWithMapper(FORCE, false);
assertThatUserHasBeenAssignedToGroup(user);
}
@Test
public void updateBrokeredUserIsAssignedToGroupInForceModeWhenCreatingTheMapperAfterFirstLogin() {
newValueForAttribute2 = "value 2";
UserRepresentation user = createMapperAndLoginAsUserTwiceWithMapper(FORCE, true);
assertThatUserHasBeenAssignedToGroup(user);
}
public UserRepresentation createMapperAndLoginAsUserTwiceWithMapper(IdentityProviderMapperSyncMode syncMode, boolean createAfterFirstLogin) {
return loginAsUserTwiceWithMapper(syncMode, createAfterFirstLogin, ImmutableMap.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>builder().add("value 2").build())
.build());
}
@Override
protected void updateUser() {
UserRepresentation user = findUser(bc.providerRealmName(), bc.getUserLogin(), bc.getUserEmail());
ImmutableMap<String, List<String>> matchingAttributes = ImmutableMap.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>builder().add(newValueForAttribute2).build())
.put("some.other.attribute", ImmutableList.<String>builder().add("some value").build())
.build();
user.setAttributes(matchingAttributes);
adminClient.realm(bc.providerRealmName()).users().get(user.getId()).update(user);
}
@Override
protected void createMapperInIdp(IdentityProviderRepresentation idp, IdentityProviderMapperSyncMode syncMode) {
createMapperInIdp(idp, CLAIMS_OR_ATTRIBUTES, false, syncMode);
}
protected void createAdvancedGroupMapper(String claimsOrAttributeRepresentation, boolean areClaimsOrAttributeValuesRegexes) {
IdentityProviderRepresentation idp = setupIdentityProvider();
createMapperInIdp(idp, claimsOrAttributeRepresentation, areClaimsOrAttributeValuesRegexes, IMPORT);
}
abstract protected void createMapperInIdp(
IdentityProviderRepresentation idp, String claimsOrAttributeRepresentation, boolean areClaimsOrAttributeValuesRegexes, IdentityProviderMapperSyncMode syncMode);
}

View file

@ -0,0 +1,75 @@
package org.keycloak.testsuite.broker;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.keycloak.testsuite.broker.BrokerTestTools.getConsumerRoot;
import org.keycloak.models.IdentityProviderMapperSyncMode;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public abstract class AbstractGroupMapperTest extends AbstractIdentityProviderMapperTest {
public static final String MAPPER_TEST_GROUP_NAME = "mapper-test";
public static final String MAPPER_TEST_GROUP_PATH = "/" + MAPPER_TEST_GROUP_NAME;
protected abstract void createMapperInIdp(
IdentityProviderRepresentation idp, IdentityProviderMapperSyncMode syncMode);
protected void updateUser() {
}
protected UserRepresentation loginAsUserTwiceWithMapper(
IdentityProviderMapperSyncMode syncMode, boolean createAfterFirstLogin,
Map<String, List<String>> userConfig) {
final IdentityProviderRepresentation idp = setupIdentityProvider();
if (!createAfterFirstLogin) {
createMapperInIdp(idp, syncMode);
}
createUserInProviderRealm(userConfig);
logInAsUserInIDPForFirstTime();
UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail());
if (!createAfterFirstLogin) {
assertThatUserHasBeenAssignedToGroup(user);
} else {
assertThatUserHasNotBeenAssignedToGroup(user);
}
if (createAfterFirstLogin) {
createMapperInIdp(idp, syncMode);
}
logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());
updateUser();
logInAsUserInIDP();
user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail());
return user;
}
protected void assertThatUserHasBeenAssignedToGroup(UserRepresentation user) {
List<String> groupNames = new ArrayList<>();
realm.users().get(user.getId()).groups().forEach(group -> {
groupNames.add(group.getName());
});
assertTrue(groupNames.contains(MAPPER_TEST_GROUP_NAME));
}
protected void assertThatUserHasNotBeenAssignedToGroup(UserRepresentation user) {
List<String> groupNames = new ArrayList<>();
realm.users().get(user.getId()).groups().forEach(group -> {
groupNames.add(group.getName());
});
assertFalse(groupNames.contains(MAPPER_TEST_GROUP_NAME));
}
}

View file

@ -0,0 +1,37 @@
package org.keycloak.testsuite.broker;
import org.keycloak.admin.client.resource.IdentityProviderResource;
import org.keycloak.broker.oidc.mappers.AdvancedClaimToGroupMapper;
import org.keycloak.broker.provider.ConfigConstants;
import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.IdentityProviderMapperSyncMode;
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import com.google.common.collect.ImmutableMap;
public class OidcAdvancedClaimToGroupMapperTest extends AbstractAdvancedGroupMapperTest {
@Override
protected BrokerConfiguration getBrokerConfiguration() {
return new KcOidcBrokerConfiguration();
}
@Override
protected void createMapperInIdp(IdentityProviderRepresentation idp, String claimsOrAttributeRepresentation,
boolean areClaimsOrAttributeValuesRegexes, IdentityProviderMapperSyncMode syncMode) {
IdentityProviderMapperRepresentation advancedClaimToGroupMapper = new IdentityProviderMapperRepresentation();
advancedClaimToGroupMapper.setName("advanced-claim-to-group-mapper");
advancedClaimToGroupMapper.setIdentityProviderMapper(AdvancedClaimToGroupMapper.PROVIDER_ID);
advancedClaimToGroupMapper.setConfig(ImmutableMap.<String, String> builder()
.put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString())
.put(AdvancedClaimToGroupMapper.CLAIM_PROPERTY_NAME, claimsOrAttributeRepresentation)
.put(AdvancedClaimToGroupMapper.ARE_CLAIM_VALUES_REGEX_PROPERTY_NAME,
areClaimsOrAttributeValuesRegexes ? "true" : "false")
.put(ConfigConstants.GROUP, MAPPER_TEST_GROUP_PATH)
.build());
IdentityProviderResource idpResource = realm.identityProviders().get(idp.getAlias());
advancedClaimToGroupMapper.setIdentityProviderAlias(bc.getIDPAlias());
idpResource.addMapper(advancedClaimToGroupMapper).close();
}
}

View file

@ -253,6 +253,8 @@ aggregate.attrs.label=Aggregate attribute values
aggregate.attrs.tooltip=Indicates if attribute values should be aggregated with the group attributes. If using OpenID Connect mapper the multivalued option needs to be enabled too in order to get all the values. Duplicated values are discarded and the order of values is not guaranteed with this option. aggregate.attrs.tooltip=Indicates if attribute values should be aggregated with the group attributes. If using OpenID Connect mapper the multivalued option needs to be enabled too in order to get all the values. Duplicated values are discarded and the order of values is not guaranteed with this option.
selectRole.label=Select Role selectRole.label=Select Role
selectRole.tooltip=Enter role in the textbox to the left, or click this button to browse and select the role you want. selectRole.tooltip=Enter role in the textbox to the left, or click this button to browse and select the role you want.
selectGroup.label=Select Group
selectGroup.tooltip=Enter group in the textbox to the left, or click this button to browse and select the group you want.
tokenClaimName.label=Token Claim Name tokenClaimName.label=Token Claim Name
tokenClaimName.tooltip=Name of the claim to insert into the token. This can be a fully qualified name like 'address.street'. In this case, a nested json object will be created. To prevent nesting and use dot literally, escape the dot with backslash (\\.). tokenClaimName.tooltip=Name of the claim to insert into the token. This can be a fully qualified name like 'address.street'. In this case, a nested json object will be created. To prevent nesting and use dot literally, escape the dot with backslash (\\.).
jsonType.label=Claim JSON Type jsonType.label=Claim JSON Type
@ -1061,10 +1063,12 @@ export-clients=Export clients
action=Action action=Action
role-selector=Role Selector role-selector=Role Selector
group-selector=Group Selector
realm-roles.tooltip=Realm roles that can be selected. realm-roles.tooltip=Realm roles that can be selected.
select-a-role=Select a role select-a-role=Select a role
select-realm-role=Select realm role select-realm-role=Select realm role
select-group=Select group
client-roles.tooltip=Client roles that can be selected. client-roles.tooltip=Client roles that can be selected.
select-client-role=Select client role select-client-role=Select client role

View file

@ -3048,6 +3048,154 @@ module.controller('RoleSelectorModalCtrl', function($scope, realm, config, confi
}) })
}); });
module.controller('GroupSelectorModalCtrl', function($scope, $q, realm, config, configName, GroupsCount, Groups, Group, GroupChildren, Notifications, Dialog, ComponentUtils, $modalInstance, $translate) {
$scope.realm = realm;
$scope.groupList = [
{
"id" : "realm",
"name": $translate.instant('groups'),
"subGroups" : []
}
];
$scope.groupSelector = {
searchCriteria: undefined,
currentPage: 1,
pageSize: 20,
numberOfPages: 1
};
$scope.groupSelector.currentPageInput = $scope.groupSelector.currentPage;
var refreshGroups = function (search) {
console.log('refreshGroups');
$scope.groupSelector.currentPageInput = $scope.groupSelector.currentPage;
var first = ($scope.groupSelector.currentPage * $scope.groupSelector.pageSize) - $scope.groupSelector.pageSize;
console.log('first:' + first);
var queryParams = {
realm : realm.realm,
first : first,
max : $scope.groupSelector.pageSize
};
var countParams = {
realm : realm.realm,
top : 'true'
};
if(angular.isDefined(search) && search !== '') {
queryParams.search = search;
countParams.search = search;
}
var promiseGetGroups = $q.defer();
Groups.query(queryParams, function(entry) {
promiseGetGroups.resolve(entry);
}, function() {
promiseGetGroups.reject($translate.instant('group.fetch.fail', {params: queryParams}));
});
promiseGetGroups.promise.then(function(groups) {
$scope.groupList = [
{
"id" : "realm",
"name": $translate.instant('groups'),
"subGroups": ComponentUtils.sortGroups('name', groups)
}
];
if (angular.isDefined(search) && search !== '') {
// Add highlight for concrete text match
setTimeout(function () {
document.querySelectorAll('span').forEach(function (element) {
if (element.textContent.indexOf(search) != -1) {
angular.element(element).addClass('highlight');
}
});
}, 500);
}
}, function (failed) {
Notifications.error(failed);
});
var promiseCount = $q.defer();
console.log('countParams: realm[' + countParams.realm);
GroupsCount.query(countParams, function(entry) {
promiseCount.resolve(entry);
}, function() {
promiseCount.reject($translate.instant('group.fetch.fail', {params: countParams}));
});
promiseCount.promise.then(function(entry) {
if(angular.isDefined(entry.count) && entry.count > $scope.groupSelector.pageSize) {
$scope.groupSelector.numberOfPages = Math.ceil(entry.count/$scope.groupSelector.pageSize);
} else {
$scope.groupSelector.numberOfPages = 1;
}
}, function (failed) {
Notifications.error(failed);
});
};
refreshGroups();
$scope.$watch('groupSelector.currentPage', function(newValue, oldValue) {
if(parseInt(newValue, 10) !== oldValue) {
refreshGroups($scope.groupSelector.searchCriteria);
}
});
$scope.clearSearch = function() {
$scope.groupSelector.searchCriteria = '';
if (parseInt($scope.groupSelector.currentPage, 10) === 1) {
refreshGroups();
} else {
$scope.groupSelector.currentPage = 1;
}
};
$scope.searchGroup = function() {
if (parseInt($scope.groupSelector.currentPage, 10) === 1) {
refreshGroups($scope.groupSelector.searchCriteria);
} else {
$scope.groupSelector.currentPage = 1;
}
};
$scope.selectGroup = function(selected) {
if(!selected || selected.id === "realm") return;
config[configName] = selected.path;
$modalInstance.close();
}
$scope.edit = $scope.selectGroup;
$scope.cancel = function() {
$modalInstance.dismiss();
}
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';
}
return undefined;
}
});
module.controller('ProviderConfigCtrl', function ($modal, $scope, $route, ComponentUtils, Client) { module.controller('ProviderConfigCtrl', function ($modal, $scope, $route, ComponentUtils, Client) {
clientSelectControl($scope, $route.current.params.realm, Client); clientSelectControl($scope, $route.current.params.realm, Client);
$scope.fileNames = {}; $scope.fileNames = {};
@ -3091,6 +3239,24 @@ module.controller('ProviderConfigCtrl', function ($modal, $scope, $route, Compon
}) })
} }
$scope.openGroupSelector = function (configName, config) {
$modal.open({
templateUrl: resourceUrl + '/partials/modal/group-selector.html',
controller: 'GroupSelectorModalCtrl',
resolve: {
realm: function () {
return $scope.realm;
},
config: function () {
return config;
},
configName: function () {
return configName;
}
}
})
}
$scope.changeClient = function(configName, config, client, multivalued) { $scope.changeClient = function(configName, config, client, multivalued) {
if (!client || !client.id) { if (!client || !client.id) {
config[configName] = null; config[configName] = null;

View file

@ -0,0 +1,50 @@
<div class="modal-header">
<button type="button" class="close" ng-click="cancel()">
<span class="pficon pficon-close"></span>
</button>
<h4 class="modal-title">{{:: 'group-selector' | translate}}</h4>
</div>
<div style="padding: 0 15px 15px 15px;">
<form>
<table class="table table-striped table-bordered" style="margin-bottom: 0">
<thead>
<tr>
<th class="kc-table-actions" colspan="5">
<div class="form-inline">
<div class="pull-left">
<div class="input-group">
<input type="text" placeholder="{{:: 'search.placeholder' | translate}}" ng-model="groupSelector.searchCriteria" class="form-control search" onkeydown="if (event.keyCode == 13) document.getElementById('groupSearch').click()">
<div class="input-group-addon">
<i class="fa fa-search" id="groupSearch" ng-click="searchGroup()"></i>
</div>
</div>
</div>
<button id="viewAllGroups" class="btn btn-default" ng-click="clearSearch()">{{:: 'view-all-groups' | translate}}</button>
<div class="pull-right">
<button id="selectGroup" class="btn btn-default" ng-click="selectGroup(tree.currentNode)">{{:: 'selectGroup.label' | translate}}</button>
</div>
</div>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div
tree-id="tree"
angular-treeview="true"
tree-model="groupList"
node-id="id"
node-label="name"
node-children="subGroups" >
</div>
</td>
</tr>
</tbody>
</table>
<div style="margin-bottom: 50px">
<kc-paging current-page="groupSelector.currentPage" number-of-pages="groupSelector.numberOfPages" current-page-input="groupSelector.currentPageInput"></kc-paging>
</div>
</form>
</div>

View file

@ -29,6 +29,16 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6" data-ng-if="option.type == 'Group'">
<div class="row">
<div class="col-md-8">
<input class="form-control" type="text" data-ng-model="config[ option.name ]" >
</div>
<div class="col-md-2">
<button type="button" data-ng-click="openGroupSelector(option.name, config)" class="btn btn-default" tooltip-placement="top" tooltip-trigger="mouseover mouseout" tooltip="{{:: 'selectGroup.tooltip' | translate}}">{{:: 'selectGroup.label' | translate}}</button>
</div>
</div>
</div>
<div class="col-md-4" data-ng-if="option.type == 'ClientList'"> <div class="col-md-4" data-ng-if="option.type == 'ClientList'">
<input type="hidden" ui-select2="clientsUiSelect" id="clients" data-ng-init="initSelectedClient(option.name, config)" data-ng-model="selectedClient" data-ng-change="changeClient(option.name, config, selectedClient, false);" data-placeholder="{{:: 'selectOne' | translate}}..."> <input type="hidden" ui-select2="clientsUiSelect" id="clients" data-ng-init="initSelectedClient(option.name, config)" data-ng-model="selectedClient" data-ng-change="changeClient(option.name, config, selectedClient, false);" data-placeholder="{{:: 'selectOne' | translate}}...">
</input> </input>

View file

@ -45,7 +45,6 @@
var nodeChildren = attrs.nodeChildren || 'children'; var nodeChildren = attrs.nodeChildren || 'children';
//tree template //tree template
var template = var template =
'<ul>' + '<ul>' +
'<li data-ng-repeat="node in ' + treeModel + '">' + '<li data-ng-repeat="node in ' + treeModel + '">' +
@ -55,7 +54,6 @@
'</li>' + '</li>' +
'</ul>'; '</ul>';
//check tree id, tree model //check tree id, tree model
if( treeId && treeModel ) { if( treeId && treeModel ) {
//root node //root node