keycloak-aplcache
Changes
broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java 2(+1 -1)
connections/mongo/src/main/java/org/keycloak/connections/mongo/api/types/MapperContext.java 7(+4 -3)
connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java 3(+2 -1)
connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBListMapper.java 2(+1 -1)
connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBListToSetMapper.java 2(+1 -1)
connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBObjectMapper.java 13(+9 -4)
federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java 6(+3 -3)
federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/LDAPQuery.java 29(+15 -14)
federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java 62(+25 -37)
federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java 4(+2 -2)
federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java 43(+27 -16)
federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java 14(+9 -5)
federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapper.java 4(+2 -2)
federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java 55(+25 -30)
federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java 204(+173 -31)
federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java 4(+4 -0)
forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountBean.java 20(+19 -1)
forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js 27(+26 -1)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java 5(+3 -2)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java 24(+19 -5)
saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java 4(+2 -2)
testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java 4(+2 -2)
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java 95(+95 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java 21(+12 -9)
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPExampleServlet.java 58(+58 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPMultipleAttributesTest.java 193(+193 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java 4(+2 -2)
Details
diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java
index 9f5085e..6d3f8db 100755
--- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java
@@ -110,7 +110,7 @@ public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityPr
String value = getJsonValue(mapperModel, context);
if (value != null) {
- user.setAttribute(attribute, value);
+ user.setSingleAttribute(attribute, value);
}
}
diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java
index 61500a7..d9cb079 100755
--- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java
@@ -3,16 +3,14 @@ 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.IdentityBrokerException;
-import org.keycloak.models.ClientModel;
import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
-import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.provider.ProviderConfigProperty;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
/**
@@ -76,7 +74,7 @@ public class UserAttributeMapper extends AbstractClaimMapper {
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
Object value = getClaimValue(mapperModel, context);
if (value != null) {
- user.setAttribute(attribute, value.toString());
+ user.setSingleAttribute(attribute, value.toString());
}
}
@@ -84,9 +82,9 @@ public class UserAttributeMapper extends AbstractClaimMapper {
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
Object value = getClaimValue(mapperModel, context);
- String current = user.getAttribute(attribute);
+ String current = user.getFirstAttribute(attribute);
if (value != null && !value.equals(current)) {
- user.setAttribute(attribute, value.toString());
+ user.setSingleAttribute(attribute, value.toString());
} else if (value == null) {
user.removeAttribute(attribute);
}
diff --git a/broker/saml/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeMapper.java b/broker/saml/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeMapper.java
index b5f9770..a1feac8 100755
--- a/broker/saml/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeMapper.java
+++ b/broker/saml/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeMapper.java
@@ -2,17 +2,14 @@ package org.keycloak.broker.saml.mappers;
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
import org.keycloak.broker.provider.BrokeredIdentityContext;
-import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.saml.SAMLEndpoint;
import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
import org.keycloak.dom.saml.v2.assertion.AssertionType;
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
import org.keycloak.dom.saml.v2.assertion.AttributeType;
-import org.keycloak.models.ClientModel;
import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
-import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.provider.ProviderConfigProperty;
@@ -87,7 +84,7 @@ public class UserAttributeMapper extends AbstractIdentityProviderMapper {
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
Object value = getAttribute(mapperModel, context);
if (value != null) {
- user.setAttribute(attribute, value.toString());
+ user.setSingleAttribute(attribute, value.toString());
}
}
@@ -115,9 +112,9 @@ public class UserAttributeMapper extends AbstractIdentityProviderMapper {
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
Object value = getAttribute(mapperModel, context);
- String current = user.getAttribute(attribute);
+ String current = user.getFirstAttribute(attribute);
if (value != null && !value.equals(current)) {
- user.setAttribute(attribute, value.toString());
+ user.setSingleAttribute(attribute, value.toString());
} else if (value == null) {
user.removeAttribute(attribute);
}
diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml
index ff0311d..f5f0195 100755
--- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml
+++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml
@@ -2,6 +2,7 @@
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet author="bburke@redhat.com" id="1.4.0">
<delete tableName="CLIENT_SESSION_AUTH_STATUS"/>
+ <delete tableName="CLIENT_SESSION_ROLE"/>
<delete tableName="CLIENT_SESSION_PROT_MAPPER"/>
<delete tableName="CLIENT_SESSION_NOTE"/>
<delete tableName="CLIENT_SESSION"/>
@@ -22,6 +23,12 @@
<constraints nullable="true"/>
</column>
</addColumn>
+ <addColumn tableName="USER_ATTRIBUTE">
+ <column name="ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ </addColumn>
+
<dropColumn tableName="AUTHENTICATOR" columnName="PROVIDER_ID"/>
<renameTable oldTableName="AUTHENTICATOR_CONFIG" newTableName="AUTHENTICATOR_CONFIG_ENTRY"/>
<renameTable oldTableName="AUTHENTICATOR" newTableName="AUTHENTICATOR_CONFIG"/>
@@ -110,6 +117,8 @@
</column>
</createTable>
+ <dropPrimaryKey constraintName="CONSTRAINT_6" tableName="USER_ATTRIBUTE"/>
+ <addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_USER_ATTRIBUTE_PK" tableName="USER_ATTRIBUTE"/>
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_REQ_ACT_PRV_PK" tableName="REQUIRED_ACTION_PROVIDER"/>
<addPrimaryKey columnNames="REQUIRED_ACTION_ID, NAME" constraintName="CONSTRAINT_REQ_ACT_CFG_PK" tableName="REQUIRED_ACTION_CONFIG"/>
<addPrimaryKey columnNames="CLIENT_SESSION, NAME" constraintName="CONSTR_CL_USR_SES_NOTE" tableName="CLIENT_USER_SESSION_NOTE"/>
diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/api/types/MapperContext.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/api/types/MapperContext.java
index 094f506..30bd9f2 100644
--- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/api/types/MapperContext.java
+++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/api/types/MapperContext.java
@@ -1,5 +1,6 @@
package org.keycloak.connections.mongo.api.types;
+import java.lang.reflect.Type;
import java.util.List;
/**
@@ -14,9 +15,9 @@ public class MapperContext<T, S> {
private final Class<? extends S> expectedReturnType;
// in case that expected return type is generic type (like "List<String>"), then genericTypes could contain list of expected generic arguments
- private final List<Class<?>> genericTypes;
+ private final List<Type> genericTypes;
- public MapperContext(T objectToConvert, Class<? extends S> expectedReturnType, List<Class<?>> genericTypes) {
+ public MapperContext(T objectToConvert, Class<? extends S> expectedReturnType, List<Type> genericTypes) {
this.objectToConvert = objectToConvert;
this.expectedReturnType = expectedReturnType;
this.genericTypes = genericTypes;
@@ -30,7 +31,7 @@ public class MapperContext<T, S> {
return expectedReturnType;
}
- public List<Class<?>> getGenericTypes() {
+ public List<Type> getGenericTypes() {
return genericTypes;
}
}
diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java
index dab99e0..36be680 100755
--- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java
+++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java
@@ -46,7 +46,8 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
"org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity",
"org.keycloak.models.entities.AuthenticationExecutionEntity",
"org.keycloak.models.entities.AuthenticationFlowEntity",
- "org.keycloak.models.entities.AuthenticatorEntity",
+ "org.keycloak.models.entities.AuthenticatorConfigEntity",
+ "org.keycloak.models.entities.RequiredActionProviderEntity",
};
private static final Logger logger = Logger.getLogger(DefaultMongoConnectionFactoryProvider.class);
diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBListMapper.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBListMapper.java
index cc229c6..d435e1e 100755
--- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBListMapper.java
+++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBListMapper.java
@@ -24,7 +24,7 @@ public class BasicDBListMapper implements Mapper<BasicDBList, List> {
public List convertObject(MapperContext<BasicDBList, List> context) {
BasicDBList dbList = context.getObjectToConvert();
ArrayList<Object> appObjects = new ArrayList<Object>();
- Class<?> expectedListElementType = context.getGenericTypes().get(0);
+ Class<?> expectedListElementType = (Class<?>) context.getGenericTypes().get(0);
for (Object dbObject : dbList) {
MapperContext<Object, Object> newContext = new MapperContext<Object, Object>(dbObject, expectedListElementType, null);
diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBListToSetMapper.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBListToSetMapper.java
index d43781a..eea2ee9 100644
--- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBListToSetMapper.java
+++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBListToSetMapper.java
@@ -23,7 +23,7 @@ public class BasicDBListToSetMapper implements Mapper<BasicDBList, Set> {
public Set convertObject(MapperContext<BasicDBList, Set> context) {
BasicDBList dbList = context.getObjectToConvert();
Set<Object> appObjects = new HashSet<Object>();
- Class<?> expectedListElementType = context.getGenericTypes().get(0);
+ Class<?> expectedListElementType = (Class<?>) context.getGenericTypes().get(0);
for (Object dbObject : dbList) {
MapperContext<Object, Object> newContext = new MapperContext<Object, Object>(dbObject, expectedListElementType, null);
diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBObjectMapper.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBObjectMapper.java
index 102cf21..e592df9 100644
--- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBObjectMapper.java
+++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBObjectMapper.java
@@ -14,6 +14,7 @@ import org.keycloak.util.reflections.Types;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
/**
@@ -87,10 +88,14 @@ public class BasicDBObjectMapper<S> implements Mapper<BasicDBObject, S> {
ParameterizedType parameterized = (ParameterizedType) type;
Type[] genericTypeArguments = parameterized.getActualTypeArguments();
- List<Class<?>> genericTypes = new ArrayList<Class<?>>();
- for (Type genericType : genericTypeArguments) {
- genericTypes.add((Class<?>)genericType);
- }
+ List<Type> genericTypes = Arrays.asList(genericTypeArguments);
+ /*for (Type genericType : genericTypeArguments) {
+ if (genericType instanceof Class) {
+ genericTypes.add((Class<?>) genericType);
+ } else {
+ System.out.println("foo");
+ }
+ }*/
Class<?> expectedReturnType = (Class<?>)parameterized.getRawType();
context = new MapperContext<Object, Object>(valueFromDB, expectedReturnType, genericTypes);
diff --git a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java
index 747b64c..8607264 100755
--- a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java
@@ -1,10 +1,14 @@
package org.keycloak.representations.idm;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import org.codehaus.jackson.annotate.JsonIgnore;
+import org.keycloak.util.MultivaluedHashMap;
+
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
@@ -21,7 +25,9 @@ public class UserRepresentation {
protected String lastName;
protected String email;
protected String federationLink;
- protected Map<String, String> attributes;
+
+ // Currently there is Map<String, List<String>> but for backwards compatibility, we also need to support Map<String, String>
+ protected Map<String, Object> attributes;
protected List<CredentialRepresentation> credentials;
protected List<String> requiredActions;
protected List<FederatedIdentityRepresentation> federatedIdentities;
@@ -106,17 +112,23 @@ public class UserRepresentation {
this.emailVerified = emailVerified;
}
- public Map<String, String> getAttributes() {
+ public Map<String, Object> getAttributes() {
return attributes;
}
- public void setAttributes(Map<String, String> attributes) {
+ // This method can be removed once we can remove backwards compatibility with Keycloak 1.3 (then getAttributes() can be changed to return Map<String, List<String>> )
+ @JsonIgnore
+ public Map<String, List<String>> getAttributesAsListValues() {
+ return (Map) attributes;
+ }
+
+ public void setAttributes(Map<String, Object> attributes) {
this.attributes = attributes;
}
- public UserRepresentation attribute(String name, String value) {
- if (this.attributes == null) attributes = new HashMap<String, String>();
- attributes.put(name, value);
+ public UserRepresentation singleAttribute(String name, String value) {
+ if (this.attributes == null) attributes = new HashMap<>();
+ attributes.put(name, Arrays.asList(value));
return this;
}
diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java
index c43b58b..6c39849 100644
--- a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java
+++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java
@@ -110,7 +110,7 @@ public class KerberosFederationProvider implements UserFederationProvider {
// KerberosUsernamePasswordAuthenticator.isUserAvailable is an overhead, so avoid it for now
String kerberosPrincipal = local.getUsername() + "@" + kerberosConfig.getKerberosRealm();
- return kerberosPrincipal.equals(local.getAttribute(KERBEROS_PRINCIPAL));
+ return kerberosPrincipal.equals(local.getFirstAttribute(KERBEROS_PRINCIPAL));
}
@Override
@@ -229,7 +229,7 @@ public class KerberosFederationProvider implements UserFederationProvider {
return proxied;
} else {
logger.warn("User with username " + username + " already exists and is linked to provider [" + model.getDisplayName() +
- "] but kerberos principal is not correct. Kerberos principal on user is: " + user.getAttribute(KERBEROS_PRINCIPAL));
+ "] but kerberos principal is not correct. Kerberos principal on user is: " + user.getFirstAttribute(KERBEROS_PRINCIPAL));
logger.warn("Will re-create user");
session.userStorage().removeUser(realm, user);
}
@@ -249,7 +249,7 @@ public class KerberosFederationProvider implements UserFederationProvider {
user.setEnabled(true);
user.setEmail(email);
user.setFederationLink(model.getId());
- user.setAttribute(KERBEROS_PRINCIPAL, username + "@" + kerberosConfig.getKerberosRealm());
+ user.setSingleAttribute(KERBEROS_PRINCIPAL, username + "@" + kerberosConfig.getKerberosRealm());
if (kerberosConfig.isUpdateProfileFirstLogin()) {
user.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE);
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java
index b7e6c0e..81f058d 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java
@@ -2,22 +2,36 @@ package org.keycloak.federation.ldap.idm.model;
import java.util.Collection;
import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.Set;
+
+import org.jboss.logging.Logger;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LDAPObject {
+ private static final Logger logger = Logger.getLogger(LDAPObject.class);
+
private String uuid;
private LDAPDn dn;
private String rdnAttributeName;
- private final List<String> objectClasses = new LinkedList<String>();
- private final List<String> readOnlyAttributeNames = new LinkedList<String>();
- private final Map<String, Object> attributes = new HashMap<String, Object>();
+ private final List<String> objectClasses = new LinkedList<>();
+
+ // NOTE: names of read-only attributes are lower-cased to avoid case sensitivity issues
+ private final List<String> readOnlyAttributeNames = new LinkedList<>();
+
+ private final Map<String, Set<String>> attributes = new HashMap<>();
+
+ // Copy of "attributes" containing lower-cased keys
+ private final Map<String, Set<String>> lowerCasedAttributes = new HashMap<>();
+
public String getUuid() {
return uuid;
@@ -49,7 +63,7 @@ public class LDAPObject {
}
public void addReadOnlyAttributeName(String readOnlyAttribute) {
- readOnlyAttributeNames.add(readOnlyAttribute);
+ readOnlyAttributeNames.add(readOnlyAttribute.toLowerCase());
}
public String getRdnAttributeName() {
@@ -60,30 +74,37 @@ public class LDAPObject {
this.rdnAttributeName = rdnAttributeName;
}
- public void setAttribute(String attributeName, Object attributeValue) {
- attributes.put(attributeName, attributeValue);
+ public void setSingleAttribute(String attributeName, String attributeValue) {
+ Set<String> asSet = new LinkedHashSet<>();
+ asSet.add(attributeValue);
+ setAttribute(attributeName, asSet);
}
- public void removeAttribute(String name) {
- attributes.remove(name);
- }
-
-
- public Object getAttribute(String name) {
- return attributes.get(name);
+ public void setAttribute(String attributeName, Set<String> attributeValue) {
+ attributes.put(attributeName, attributeValue);
+ lowerCasedAttributes.put(attributeName.toLowerCase(), attributeValue);
}
+ // Case-insensitive
public String getAttributeAsString(String name) {
- Object attrValue = attributes.get(name);
- if (attrValue != null && !(attrValue instanceof String)) {
- throw new IllegalStateException("Expected String but attribute was " + attrValue + " of type " + attrValue.getClass().getName());
+ Set<String> attrValue = lowerCasedAttributes.get(name.toLowerCase());
+ if (attrValue == null || attrValue.size() == 0) {
+ return null;
+ } else if (attrValue.size() > 1) {
+ logger.warnf("Expected String but attribute '%s' has more values '%s' on object '%s' . Returning just first value", name, attrValue, dn);
}
- return (String) attrValue;
+ return attrValue.iterator().next();
+ }
+
+ // Case-insensitive. Return null if there is not value of attribute with given name or set with all values otherwise
+ public Set<String> getAttributeAsSet(String name) {
+ Set<String> values = lowerCasedAttributes.get(name.toLowerCase());
+ return (values == null) ? null : new LinkedHashSet<>(values);
}
- public Map<String, Object> getAttributes() {
+ public Map<String, Set<String>> getAttributes() {
return attributes;
}
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/IdentityStore.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/IdentityStore.java
index 01d85a6..23c6d99 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/IdentityStore.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/IdentityStore.java
@@ -4,7 +4,7 @@ import java.util.List;
import org.keycloak.federation.ldap.LDAPConfig;
import org.keycloak.federation.ldap.idm.model.LDAPObject;
-import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
/**
* IdentityStore representation providing minimal SPI
@@ -48,9 +48,9 @@ public interface IdentityStore {
// Identity query
- List<LDAPObject> fetchQueryResults(LDAPIdentityQuery LDAPIdentityQuery);
+ List<LDAPObject> fetchQueryResults(LDAPQuery LDAPQuery);
- int countQueryResults(LDAPIdentityQuery LDAPIdentityQuery);
+ int countQueryResults(LDAPQuery LDAPQuery);
// // Relationship query
//
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java
index 3dbfd0a..67e92c7 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java
@@ -4,11 +4,11 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
-import java.util.TreeSet;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
@@ -28,7 +28,7 @@ import org.keycloak.federation.ldap.idm.model.LDAPObject;
import org.keycloak.federation.ldap.idm.query.Condition;
import org.keycloak.federation.ldap.idm.query.QueryParameter;
import org.keycloak.federation.ldap.idm.query.internal.BetweenCondition;
-import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.federation.ldap.idm.query.internal.EqualCondition;
import org.keycloak.federation.ldap.idm.query.internal.GreaterThanCondition;
import org.keycloak.federation.ldap.idm.query.internal.InCondition;
@@ -108,7 +108,7 @@ public class LDAPIdentityStore implements IdentityStore {
@Override
- public List<LDAPObject> fetchQueryResults(LDAPIdentityQuery identityQuery) {
+ public List<LDAPObject> fetchQueryResults(LDAPQuery identityQuery) {
if (identityQuery.getSorting() != null && !identityQuery.getSorting().isEmpty()) {
throw new ModelException("LDAP Identity Store does not yet support sorted queries.");
}
@@ -160,7 +160,7 @@ public class LDAPIdentityStore implements IdentityStore {
}
@Override
- public int countQueryResults(LDAPIdentityQuery identityQuery) {
+ public int countQueryResults(LDAPQuery identityQuery) {
int limit = identityQuery.getLimit();
int offset = identityQuery.getOffset();
@@ -247,7 +247,7 @@ public class LDAPIdentityStore implements IdentityStore {
// ************ END CREDENTIALS AND USER SPECIFIC STUFF
- protected StringBuilder createIdentityTypeSearchFilter(final LDAPIdentityQuery identityQuery) {
+ protected StringBuilder createIdentityTypeSearchFilter(final LDAPQuery identityQuery) {
StringBuilder filter = new StringBuilder();
for (Condition condition : identityQuery.getConditions()) {
@@ -382,12 +382,6 @@ public class LDAPIdentityStore implements IdentityStore {
NamingEnumeration<? extends Attribute> ldapAttributes = attributes.getAll();
- // Exact name of attributes might be different
- List<String> uppercasedReadOnlyAttrNames = new ArrayList<>();
- for (String readonlyAttr : readOnlyAttrNames) {
- uppercasedReadOnlyAttrNames.add(readonlyAttr.toUpperCase());
- }
-
while (ldapAttributes.hasMore()) {
Attribute ldapAttribute = ldapAttributes.next();
@@ -403,23 +397,20 @@ public class LDAPIdentityStore implements IdentityStore {
Object uuidValue = ldapAttribute.get();
ldapObject.setUuid(this.operationManager.decodeEntryUUID(uuidValue));
} else {
- Set<String> attrValues = new TreeSet<>();
+ Set<String> attrValues = new LinkedHashSet<>();
NamingEnumeration<?> enumm = ldapAttribute.getAll();
while (enumm.hasMoreElements()) {
- String attrVal = enumm.next().toString();
+ String attrVal = enumm.next().toString().trim();
attrValues.add(attrVal);
}
if (ldapAttributeName.equalsIgnoreCase(LDAPConstants.OBJECT_CLASS)) {
ldapObject.setObjectClasses(attrValues);
} else {
- if (attrValues.size() == 1) {
- ldapObject.setAttribute(ldapAttributeName, attrValues.iterator().next());
- } else {
- ldapObject.setAttribute(ldapAttributeName, attrValues);
- }
+ ldapObject.setAttribute(ldapAttributeName, attrValues);
- if (uppercasedReadOnlyAttrNames.contains(ldapAttributeName.toUpperCase())) {
+ // readOnlyAttrNames are lower-cased
+ if (readOnlyAttrNames.contains(ldapAttributeName.toLowerCase())) {
ldapObject.addReadOnlyAttributeName(ldapAttributeName);
}
}
@@ -440,28 +431,25 @@ public class LDAPIdentityStore implements IdentityStore {
protected BasicAttributes extractAttributes(LDAPObject ldapObject, boolean isCreate) {
BasicAttributes entryAttributes = new BasicAttributes();
- for (Map.Entry<String, Object> attrEntry : ldapObject.getAttributes().entrySet()) {
+ for (Map.Entry<String, Set<String>> attrEntry : ldapObject.getAttributes().entrySet()) {
String attrName = attrEntry.getKey();
- Object attrValue = attrEntry.getValue();
- if (!ldapObject.getReadOnlyAttributeNames().contains(attrName) && (isCreate || !ldapObject.getRdnAttributeName().equalsIgnoreCase(attrName))) {
-
- if (String.class.isInstance(attrValue)) {
- if (attrValue.toString().trim().length() == 0) {
- attrValue = LDAPConstants.EMPTY_ATTRIBUTE_VALUE;
- }
- entryAttributes.put(attrName, attrValue);
- } else if (Collection.class.isInstance(attrValue)) {
- BasicAttribute attr = new BasicAttribute(attrName);
- Collection<String> valueCollection = (Collection<String>) attrValue;
- for (String val : valueCollection) {
+ Set<String> attrValue = attrEntry.getValue();
+
+ // ldapObject.getReadOnlyAttributeNames() are lower-cased
+ if (!ldapObject.getReadOnlyAttributeNames().contains(attrName.toLowerCase()) && (isCreate || !ldapObject.getRdnAttributeName().equalsIgnoreCase(attrName))) {
+ BasicAttribute attr = new BasicAttribute(attrName);
+ if (attrValue == null) {
+ // Adding empty value as we don't know if attribute is mandatory in LDAP
+ attr.add(LDAPConstants.EMPTY_ATTRIBUTE_VALUE);
+ } else {
+ for (String val : attrValue) {
+ if (val == null || val.toString().trim().length() == 0) {
+ val = LDAPConstants.EMPTY_ATTRIBUTE_VALUE;
+ }
attr.add(val);
}
- entryAttributes.put(attr);
- } else if (attrValue == null || attrValue.toString().trim().length() == 0) {
- entryAttributes.put(attrName, LDAPConstants.EMPTY_ATTRIBUTE_VALUE);
- } else {
- throw new ModelException("Unexpected type of value of argument " + attrName + ". Value is " + attrValue);
}
+ entryAttributes.put(attr);
}
}
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java
index 8d934f3..18b8a86 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java
@@ -31,7 +31,7 @@ import javax.naming.ldap.PagedResultsResponseControl;
import org.jboss.logging.Logger;
import org.keycloak.federation.ldap.LDAPConfig;
-import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
@@ -165,7 +165,7 @@ public class LDAPOperationManager {
}
}
- public List<SearchResult> searchPaginated(final String baseDN, final String filter, final LDAPIdentityQuery identityQuery) throws NamingException {
+ public List<SearchResult> searchPaginated(final String baseDN, final String filter, final LDAPQuery identityQuery) throws NamingException {
final List<SearchResult> result = new ArrayList<SearchResult>();
final SearchControls cons = getSearchControls(identityQuery.getReturningLdapAttributes(), identityQuery.getSearchScope());
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java
index 7ad05d3..66665bf 100755
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java
@@ -6,7 +6,7 @@ import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator;
import org.keycloak.federation.ldap.idm.model.LDAPObject;
import org.keycloak.federation.ldap.idm.query.Condition;
import org.keycloak.federation.ldap.idm.query.QueryParameter;
-import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.federation.ldap.kerberos.LDAPProviderKerberosConfig;
@@ -51,7 +51,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
protected EditMode editMode;
protected LDAPProviderKerberosConfig kerberosConfig;
- protected final Set<String> supportedCredentialTypes = new HashSet<String>();
+ protected final Set<String> supportedCredentialTypes = new HashSet<>();
public LDAPFederationProvider(LDAPFederationProviderFactory factory, KeycloakSession session, UserFederationProviderModel model, LDAPIdentityStore ldapIdentityStore) {
this.factory = factory;
@@ -145,8 +145,8 @@ public class LDAPFederationProvider implements UserFederationProvider {
if (!synchronizeRegistrations()) throw new IllegalStateException("Registration is not supported by this ldap server");
LDAPObject ldapObject = LDAPUtils.addUserToLDAP(this, realm, user);
- user.setAttribute(LDAPConstants.LDAP_ID, ldapObject.getUuid());
- user.setAttribute(LDAPConstants.LDAP_ENTRY_DN, ldapObject.getDn().toString());
+ user.setSingleAttribute(LDAPConstants.LDAP_ID, ldapObject.getUuid());
+ user.setSingleAttribute(LDAPConstants.LDAP_ENTRY_DN, ldapObject.getDn().toString());
return proxy(realm, user, ldapObject);
}
@@ -202,7 +202,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
}
if (attributes.containsKey(FIRST_NAME) || attributes.containsKey(LAST_NAME)) {
- LDAPIdentityQuery ldapQuery = LDAPUtils.createQueryForUserSearch(this, realm);
+ LDAPQuery ldapQuery = LDAPUtils.createQueryForUserSearch(this, realm);
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
// Mapper should replace parameter with correct LDAP mapped attributes
@@ -229,10 +229,10 @@ public class LDAPFederationProvider implements UserFederationProvider {
if (ldapUser == null) {
return null;
}
- if (ldapUser.getUuid().equals(local.getAttribute(LDAPConstants.LDAP_ID))) {
+ if (ldapUser.getUuid().equals(local.getFirstAttribute(LDAPConstants.LDAP_ID))) {
return ldapUser;
} else {
- logger.warnf("LDAP User invalid. ID doesn't match. ID from LDAP [%s], ID from local DB: [%s]", ldapUser.getUuid(), local.getAttribute(LDAPConstants.LDAP_ID));
+ logger.warnf("LDAP User invalid. ID doesn't match. ID from LDAP [%s], LDAP ID from local DB: [%s]", ldapUser.getUuid(), local.getFirstAttribute(LDAPConstants.LDAP_ID));
return null;
}
}
@@ -271,8 +271,8 @@ public class LDAPFederationProvider implements UserFederationProvider {
String userDN = ldapUser.getDn().toString();
imported.setFederationLink(model.getId());
- imported.setAttribute(LDAPConstants.LDAP_ID, ldapUser.getUuid());
- imported.setAttribute(LDAPConstants.LDAP_ENTRY_DN, userDN);
+ imported.setSingleAttribute(LDAPConstants.LDAP_ID, ldapUser.getUuid());
+ imported.setSingleAttribute(LDAPConstants.LDAP_ENTRY_DN, userDN);
logger.debugf("Imported new user from LDAP to Keycloak DB. Username: [%s], Email: [%s], LDAP_ID: [%s], LDAP Entry DN: [%s]", imported.getUsername(), imported.getEmail(),
ldapUser.getUuid(), userDN);
@@ -280,7 +280,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
}
protected LDAPObject queryByEmail(RealmModel realm, String email) {
- LDAPIdentityQuery ldapQuery = LDAPUtils.createQueryForUserSearch(this, realm);
+ LDAPQuery ldapQuery = LDAPUtils.createQueryForUserSearch(this, realm);
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
// Mapper should replace "email" in parameter name with correct LDAP mapped attribute
@@ -395,7 +395,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
importUserFromLDAP(realm, ldapUser);
syncResult.increaseAdded();
} else {
- if ((fedModel.getId().equals(currentUser.getFederationLink())) && (ldapUser.getUuid().equals(currentUser.getAttribute(LDAPConstants.LDAP_ID)))) {
+ if ((fedModel.getId().equals(currentUser.getFederationLink())) && (ldapUser.getUuid().equals(currentUser.getFirstAttribute(LDAPConstants.LDAP_ID)))) {
// Update keycloak user
Set<UserFederationMapperModel> federationMappers = realm.getUserFederationMappersByFederationProvider(model.getId());
@@ -435,7 +435,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
return proxy(realm, user, ldapObject);
} else {
logger.warnf("User with username [%s] aready exists and is linked to provider [%s] but is not valid. Stale LDAP_ID on local user is: %s",
- username, model.getDisplayName(), user.getAttribute(LDAPConstants.LDAP_ID));
+ username, model.getDisplayName(), user.getFirstAttribute(LDAPConstants.LDAP_ID));
logger.warn("Will re-create user");
session.userStorage().removeUser(realm, user);
}
@@ -448,7 +448,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
}
public LDAPObject loadLDAPUserByUsername(RealmModel realm, String username) {
- LDAPIdentityQuery ldapQuery = LDAPUtils.createQueryForUserSearch(this, realm);
+ LDAPQuery ldapQuery = LDAPUtils.createQueryForUserSearch(this, realm);
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
String usernameMappedAttribute = this.ldapIdentityStore.getConfig().getUsernameLdapAttribute();
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java
index 876a96d..2b60ff8 100755
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java
@@ -9,7 +9,7 @@ import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator;
import org.keycloak.federation.ldap.idm.model.LDAPObject;
import org.keycloak.federation.ldap.idm.query.Condition;
import org.keycloak.federation.ldap.idm.query.QueryParameter;
-import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapper;
@@ -84,14 +84,17 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
boolean activeDirectory = ldapConfig.isActiveDirectory();
UserFederationProvider.EditMode editMode = ldapConfig.getEditMode();
- String readOnly = String.valueOf(editMode==UserFederationProvider.EditMode.READ_ONLY || editMode== UserFederationProvider.EditMode.UNSYNCED);
+ String readOnly = String.valueOf(editMode == UserFederationProvider.EditMode.READ_ONLY || editMode == UserFederationProvider.EditMode.UNSYNCED);
String usernameLdapAttribute = ldapConfig.getUsernameLdapAttribute();
+ String alwaysReadValueFromLDAP = String.valueOf(editMode==UserFederationProvider.EditMode.READ_ONLY || editMode== UserFederationProvider.EditMode.WRITABLE);
+
UserFederationMapperModel mapperModel;
mapperModel = KeycloakModelUtils.createUserFederationMapperModel("username", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.USERNAME,
UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, usernameLdapAttribute,
- UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
+ UserAttributeLDAPFederationMapper.READ_ONLY, readOnly,
+ UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false");
realm.addUserFederationMapper(mapperModel);
// CN is typically used as RDN for Active Directory deployments
@@ -103,7 +106,8 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
mapperModel = KeycloakModelUtils.createUserFederationMapperModel("first name", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.FIRST_NAME,
UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.GIVENNAME,
- UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
+ UserAttributeLDAPFederationMapper.READ_ONLY, readOnly,
+ UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP);
realm.addUserFederationMapper(mapperModel);
} else {
@@ -113,13 +117,15 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
mapperModel = KeycloakModelUtils.createUserFederationMapperModel("first name", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.FIRST_NAME,
UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.GIVENNAME,
- UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
+ UserAttributeLDAPFederationMapper.READ_ONLY, readOnly,
+ UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP);
realm.addUserFederationMapper(mapperModel);
mapperModel = KeycloakModelUtils.createUserFederationMapperModel("username-cn", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.USERNAME,
UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.CN,
- UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
+ UserAttributeLDAPFederationMapper.READ_ONLY, readOnly,
+ UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false");
realm.addUserFederationMapper(mapperModel);
} else {
@@ -134,20 +140,23 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
mapperModel = KeycloakModelUtils.createUserFederationMapperModel("first name", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.FIRST_NAME,
UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.CN,
- UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
+ UserAttributeLDAPFederationMapper.READ_ONLY, readOnly,
+ UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP);
realm.addUserFederationMapper(mapperModel);
}
mapperModel = KeycloakModelUtils.createUserFederationMapperModel("last name", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.LAST_NAME,
UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.SN,
- UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
+ UserAttributeLDAPFederationMapper.READ_ONLY, readOnly,
+ UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP);
realm.addUserFederationMapper(mapperModel);
mapperModel = KeycloakModelUtils.createUserFederationMapperModel("email", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.EMAIL,
UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.EMAIL,
- UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
+ UserAttributeLDAPFederationMapper.READ_ONLY, readOnly,
+ UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP);
realm.addUserFederationMapper(mapperModel);
String createTimestampLdapAttrName = activeDirectory ? "whenCreated" : LDAPConstants.CREATE_TIMESTAMP;
@@ -157,14 +166,16 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
mapperModel = KeycloakModelUtils.createUserFederationMapperModel("creation date", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, LDAPConstants.CREATE_TIMESTAMP,
UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, createTimestampLdapAttrName,
- UserAttributeLDAPFederationMapper.READ_ONLY, "true");
+ UserAttributeLDAPFederationMapper.READ_ONLY, "true",
+ UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP);
realm.addUserFederationMapper(mapperModel);
// map modifyTimeStamp as read-only
mapperModel = KeycloakModelUtils.createUserFederationMapperModel("modify date", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, LDAPConstants.MODIFY_TIMESTAMP,
UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, modifyTimestampLdapAttrName,
- UserAttributeLDAPFederationMapper.READ_ONLY, "true");
+ UserAttributeLDAPFederationMapper.READ_ONLY, "true",
+ UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP);
realm.addUserFederationMapper(mapperModel);
}
@@ -173,7 +184,7 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
public UserFederationSyncResult syncAllUsers(KeycloakSessionFactory sessionFactory, final String realmId, final UserFederationProviderModel model) {
logger.infof("Sync all users from LDAP to local store: realm: %s, federation provider: %s", realmId, model.getDisplayName());
- LDAPIdentityQuery userQuery = createQuery(sessionFactory, realmId, model);
+ LDAPQuery userQuery = createQuery(sessionFactory, realmId, model);
UserFederationSyncResult syncResult = syncImpl(sessionFactory, userQuery, realmId, model);
// TODO: Remove all existing keycloak users, which have federation links, but are not in LDAP. Perhaps don't check users, which were just added or updated during this sync?
@@ -192,7 +203,7 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
Condition modifyCondition = conditionsBuilder.greaterThanOrEqualTo(new QueryParameter(LDAPConstants.MODIFY_TIMESTAMP), lastSync);
Condition orCondition = conditionsBuilder.orCondition(createCondition, modifyCondition);
- LDAPIdentityQuery userQuery = createQuery(sessionFactory, realmId, model);
+ LDAPQuery userQuery = createQuery(sessionFactory, realmId, model);
userQuery.where(orCondition);
UserFederationSyncResult result = syncImpl(sessionFactory, userQuery, realmId, model);
@@ -200,7 +211,7 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
return result;
}
- protected UserFederationSyncResult syncImpl(KeycloakSessionFactory sessionFactory, LDAPIdentityQuery userQuery, final String realmId, final UserFederationProviderModel fedModel) {
+ protected UserFederationSyncResult syncImpl(KeycloakSessionFactory sessionFactory, LDAPQuery userQuery, final String realmId, final UserFederationProviderModel fedModel) {
final UserFederationSyncResult syncResult = new UserFederationSyncResult();
@@ -243,9 +254,9 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
return syncResult;
}
- private LDAPIdentityQuery createQuery(KeycloakSessionFactory sessionFactory, final String realmId, final UserFederationProviderModel model) {
+ private LDAPQuery createQuery(KeycloakSessionFactory sessionFactory, final String realmId, final UserFederationProviderModel model) {
class QueryHolder {
- LDAPIdentityQuery query;
+ LDAPQuery query;
}
final QueryHolder queryHolder = new QueryHolder();
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java
index 396d997..c731330 100755
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java
@@ -1,11 +1,10 @@
package org.keycloak.federation.ldap;
-import java.util.List;
import java.util.Set;
import org.keycloak.federation.ldap.idm.model.LDAPDn;
import org.keycloak.federation.ldap.idm.model.LDAPObject;
-import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.federation.ldap.mappers.LDAPFederationMapper;
import org.keycloak.models.ModelException;
@@ -45,8 +44,8 @@ public class LDAPUtils {
return ldapUser;
}
- public static LDAPIdentityQuery createQueryForUserSearch(LDAPFederationProvider ldapProvider, RealmModel realm) {
- LDAPIdentityQuery ldapQuery = new LDAPIdentityQuery(ldapProvider);
+ public static LDAPQuery createQueryForUserSearch(LDAPFederationProvider ldapProvider, RealmModel realm) {
+ LDAPQuery ldapQuery = new LDAPQuery(ldapProvider);
LDAPConfig config = ldapProvider.getLdapIdentityStore().getConfig();
ldapQuery.setSearchScope(config.getSearchScope());
ldapQuery.setSearchDn(config.getUsersDn());
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java
index 7466778..a29581d 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java
@@ -8,7 +8,7 @@ import org.keycloak.federation.ldap.idm.model.LDAPObject;
import org.keycloak.federation.ldap.idm.query.Condition;
import org.keycloak.federation.ldap.idm.query.QueryParameter;
import org.keycloak.federation.ldap.idm.query.internal.EqualCondition;
-import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserFederationMapperModel;
@@ -29,8 +29,12 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) {
String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel);
String fullName = ldapUser.getAttributeAsString(ldapFullNameAttrName);
+ if (fullName == null) {
+ return;
+ }
+
fullName = fullName.trim();
- if (fullName != null && !fullName.trim().isEmpty()) {
+ if (!fullName.isEmpty()) {
int lastSpaceIndex = fullName.lastIndexOf(" ");
if (lastSpaceIndex == -1) {
user.setLastName(fullName);
@@ -45,7 +49,7 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
public void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm) {
String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel);
String fullName = getFullName(localUser.getFirstName(), localUser.getLastName());
- ldapUser.setAttribute(ldapFullNameAttrName, fullName);
+ ldapUser.setSingleAttribute(ldapFullNameAttrName, fullName);
if (isReadOnly(mapperModel)) {
ldapUser.addReadOnlyAttributeName(ldapFullNameAttrName);
@@ -80,7 +84,7 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
ensureTransactionStarted();
String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel);
- ldapUser.setAttribute(ldapFullNameAttrName, fullName);
+ ldapUser.setSingleAttribute(ldapFullNameAttrName, fullName);
}
};
@@ -92,7 +96,7 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
}
@Override
- public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPIdentityQuery query) {
+ public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) {
String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel);
query.addReturningLdapAttribute(ldapFullNameAttrName);
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapper.java
index 4676a6b..8d5ab4e 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapper.java
@@ -2,7 +2,7 @@ package org.keycloak.federation.ldap.mappers;
import org.keycloak.federation.ldap.LDAPFederationProvider;
import org.keycloak.federation.ldap.idm.model.LDAPObject;
-import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.models.RealmModel;
import org.keycloak.mappers.UserFederationMapper;
import org.keycloak.models.UserFederationMapperModel;
@@ -58,5 +58,5 @@ public interface LDAPFederationMapper extends UserFederationMapper {
* @param mapperModel
* @param query
*/
- void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPIdentityQuery query);
+ void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query);
}
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java
index 165309c..4565f88 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java
@@ -12,7 +12,7 @@ import org.keycloak.federation.ldap.idm.model.LDAPDn;
import org.keycloak.federation.ldap.idm.model.LDAPObject;
import org.keycloak.federation.ldap.idm.query.Condition;
import org.keycloak.federation.ldap.idm.query.QueryParameter;
-import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
import org.keycloak.models.ClientModel;
import org.keycloak.models.LDAPConstants;
@@ -58,7 +58,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
// List of IDs of UserFederationMapperModels where syncRolesFromLDAP was already called in this KeycloakSession. This is to improve performance
// TODO: Rather address this with caching at LDAPIdentityStore level?
- private Set<String> rolesSyncedModels = new TreeSet<String>();
+ private Set<String> rolesSyncedModels = new TreeSet<>();
@Override
public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) {
@@ -95,7 +95,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
if (!rolesSyncedModels.contains(mapperModel.getId())) {
logger.debugf("Syncing roles from LDAP into Keycloak DB. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getDisplayName());
- LDAPIdentityQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
+ LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
// Send query
List<LDAPObject> ldapRoles = ldapQuery.getResultList();
@@ -115,8 +115,8 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
}
}
- public LDAPIdentityQuery createRoleQuery(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider) {
- LDAPIdentityQuery ldapQuery = new LDAPIdentityQuery(ldapProvider);
+ public LDAPQuery createRoleQuery(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider) {
+ LDAPQuery ldapQuery = new LDAPQuery(ldapProvider);
// For now, use same search scope, which is configured "globally" and used for user's search.
ldapQuery.setSearchScope(ldapProvider.getLdapIdentityStore().getConfig().getSearchScope());
@@ -178,7 +178,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
}
String[] objClasses = objectClasses.split(",");
- Set<String> trimmed = new HashSet<String>();
+ Set<String> trimmed = new HashSet<>();
for (String objectClass : objClasses) {
objectClass = objectClass.trim();
if (objectClass.length() > 0) {
@@ -202,7 +202,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
String roleNameAttribute = getRoleNameLdapAttribute(mapperModel);
ldapObject.setRdnAttributeName(roleNameAttribute);
ldapObject.setObjectClasses(getRoleObjectClasses(mapperModel, ldapProvider));
- ldapObject.setAttribute(roleNameAttribute, roleName);
+ ldapObject.setSingleAttribute(roleNameAttribute, roleName);
LDAPDn roleDn = LDAPDn.fromString(getRolesDn(mapperModel));
roleDn.addFirst(roleNameAttribute, roleName);
@@ -220,6 +220,15 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
}
Set<String> memberships = getExistingMemberships(mapperModel, ldapRole);
+
+ // Remove membership placeholder if present
+ for (String membership : memberships) {
+ if (membership.trim().length() == 0) {
+ memberships.remove(membership);
+ break;
+ }
+ }
+
memberships.add(ldapUser.getDn().toString());
ldapRole.setAttribute(getMembershipLdapAttribute(mapperModel), memberships);
@@ -240,7 +249,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
}
public LDAPObject loadLDAPRoleByName(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, String roleName) {
- LDAPIdentityQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
+ LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
Condition roleNameCondition = new LDAPQueryConditionsBuilder().equal(new QueryParameter(getRoleNameLdapAttribute(mapperModel)), roleName);
ldapQuery.where(roleNameCondition);
return ldapQuery.getFirstResult();
@@ -248,29 +257,15 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
protected Set<String> getExistingMemberships(UserFederationMapperModel mapperModel, LDAPObject ldapRole) {
String memberAttrName = getMembershipLdapAttribute(mapperModel);
- Set<String> memberships = new TreeSet<String>();
- Object existingMemberships = ldapRole.getAttribute(memberAttrName);
-
- if (existingMemberships != null) {
- if (existingMemberships instanceof String) {
- String existingMembership = existingMemberships.toString().trim();
- if (existingMemberships != null && existingMembership.length() > 0) {
- memberships.add(existingMembership);
- }
- } else if (existingMemberships instanceof Collection) {
- Collection<String> exMemberships = (Collection<String>) existingMemberships;
- for (String membership : exMemberships) {
- if (membership.trim().length() > 0) {
- memberships.add(membership);
- }
- }
- }
+ Set<String> memberships = ldapRole.getAttributeAsSet(memberAttrName);
+ if (memberships == null) {
+ memberships = new HashSet<>();
}
return memberships;
}
protected List<LDAPObject> getLDAPRoleMappings(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) {
- LDAPIdentityQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
+ LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
String membershipAttr = getMembershipLdapAttribute(mapperModel);
Condition membershipCondition = new LDAPQueryConditionsBuilder().equal(new QueryParameter(membershipAttr), ldapUser.getDn().toString());
ldapQuery.where(membershipCondition);
@@ -290,7 +285,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
}
@Override
- public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPIdentityQuery query) {
+ public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) {
}
@@ -389,7 +384,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
if (mode == Mode.LDAP_ONLY) {
// For LDAP-only we want to retrieve role mappings of target container just from LDAP
- Set<RoleModel> modelRolesCopy = new HashSet<RoleModel>(modelRoleMappings);
+ Set<RoleModel> modelRolesCopy = new HashSet<>(modelRoleMappings);
for (RoleModel role : modelRolesCopy) {
if (role.getContainer().equals(targetRoleContainer)) {
modelRoleMappings.remove(role);
@@ -408,7 +403,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
List<LDAPObject> ldapRoles = getLDAPRoleMappings(mapperModel, ldapProvider, ldapUser);
- Set<RoleModel> roles = new HashSet<RoleModel>();
+ Set<RoleModel> roles = new HashSet<>();
String roleNameLdapAttr = getRoleNameLdapAttribute(mapperModel);
for (LDAPObject role : ldapRoles) {
String roleName = role.getAttributeAsString(roleNameLdapAttr);
@@ -430,7 +425,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
if (role.getContainer().equals(roleContainer)) {
- LDAPIdentityQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
+ LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
Condition roleNameCondition = conditionsBuilder.equal(new QueryParameter(getRoleNameLdapAttribute(mapperModel)), role.getName());
Condition membershipCondition = conditionsBuilder.equal(new QueryParameter(getMembershipLdapAttribute(mapperModel)), ldapUser.getDn().toString());
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java
index dd139b0..24bf450 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java
@@ -1,17 +1,25 @@
package org.keycloak.federation.ldap.mappers;
import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
import java.util.Map;
+import java.util.Set;
+import org.jboss.logging.Logger;
import org.keycloak.federation.ldap.LDAPFederationProvider;
import org.keycloak.federation.ldap.idm.model.LDAPObject;
import org.keycloak.federation.ldap.idm.query.Condition;
import org.keycloak.federation.ldap.idm.query.QueryParameter;
-import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserFederationMapperModel;
import org.keycloak.models.UserFederationProvider;
import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.UserModelDelegate;
import org.keycloak.models.utils.reflection.Property;
import org.keycloak.models.utils.reflection.PropertyCriteria;
import org.keycloak.models.utils.reflection.PropertyQueries;
@@ -21,10 +29,12 @@ import org.keycloak.models.utils.reflection.PropertyQueries;
*/
public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMapper {
+ private static final Logger logger = Logger.getLogger(UserAttributeLDAPFederationMapper.class);
+
private static final Map<String, Property<Object>> userModelProperties;
static {
- userModelProperties = PropertyQueries.createQuery(UserModel.class).addCriteria(new PropertyCriteria() {
+ Map<String, Property<Object>> userModelProps = PropertyQueries.createQuery(UserModel.class).addCriteria(new PropertyCriteria() {
@Override
public boolean methodMatches(Method m) {
@@ -36,11 +46,18 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
}
}).getResultList();
+
+ // Convert to be keyed by lower-cased attribute names
+ userModelProperties = new HashMap<>();
+ for (Map.Entry<String, Property<Object>> entry : userModelProps.entrySet()) {
+ userModelProperties.put(entry.getKey().toLowerCase(), entry.getValue());
+ }
}
public static final String USER_MODEL_ATTRIBUTE = "user.model.attribute";
public static final String LDAP_ATTRIBUTE = "ldap.attribute";
public static final String READ_ONLY = "read.only";
+ public static final String ALWAYS_READ_VALUE_FROM_LDAP = "always.read.value.from.ldap";
@Override
@@ -48,16 +65,21 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
- Object ldapAttrValue = ldapUser.getAttribute(ldapAttrName);
- if (ldapAttrValue != null && !ldapAttrValue.toString().trim().isEmpty()) {
- Property<Object> userModelProperty = userModelProperties.get(userModelAttrName);
+ Property<Object> userModelProperty = userModelProperties.get(userModelAttrName.toLowerCase());
- if (userModelProperty != null) {
- // we have java property on UserModel
- userModelProperty.setValue(user, ldapAttrValue);
+ if (userModelProperty != null) {
+
+ // we have java property on UserModel
+ String ldapAttrValue = ldapUser.getAttributeAsString(ldapAttrName);
+ setPropertyOnUserModel(userModelProperty, user, ldapAttrValue);
+ } else {
+
+ // we don't have java property. Let's set attribute
+ Set<String> ldapAttrValue = ldapUser.getAttributeAsSet(ldapAttrName);
+ if (ldapAttrValue != null) {
+ user.setAttribute(userModelAttrName, new ArrayList<>(ldapAttrValue));
} else {
- // we don't have java property. Let's just setAttribute
- user.setAttribute(userModelAttrName, (String) ldapAttrValue);
+ user.removeAttribute(userModelAttrName);
}
}
}
@@ -67,36 +89,58 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
- Property<Object> userModelProperty = userModelProperties.get(userModelAttrName);
+ Property<Object> userModelProperty = userModelProperties.get(userModelAttrName.toLowerCase());
- Object attrValue;
if (userModelProperty != null) {
- // we have java property on UserModel
- attrValue = userModelProperty.getValue(localUser);
+
+ // we have java property on UserModel. Assuming we support just properties of simple types
+ Object attrValue = userModelProperty.getValue(localUser);
+ String valueAsString = (attrValue == null) ? null : attrValue.toString();
+ ldapUser.setSingleAttribute(ldapAttrName, valueAsString);
} else {
- // we don't have java property. Let's just setAttribute
- attrValue = localUser.getAttribute(userModelAttrName);
+
+ // we don't have java property. Let's set attribute
+ List<String> attrValues = localUser.getAttribute(userModelAttrName);
+
+ if (attrValues.size() == 0) {
+ ldapUser.setAttribute(ldapAttrName, null);
+ } else {
+ ldapUser.setAttribute(ldapAttrName, new LinkedHashSet<>(attrValues));
+ }
}
- ldapUser.setAttribute(ldapAttrName, attrValue);
if (isReadOnly(mapperModel)) {
ldapUser.addReadOnlyAttributeName(ldapAttrName);
}
}
@Override
- public UserModel proxy(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel delegate, RealmModel realm) {
- if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && !isReadOnly(mapperModel)) {
+ public UserModel proxy(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, final LDAPObject ldapUser, UserModel delegate, RealmModel realm) {
+ final String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
+ final String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
+ boolean isAlwaysReadValueFromLDAP = parseBooleanParameter(mapperModel, ALWAYS_READ_VALUE_FROM_LDAP);
- final String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
- final String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
+ // For writable mode, we want to propagate writing of attribute to LDAP as well
+ if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && !isReadOnly(mapperModel)) {
- TxAwareLDAPUserModelDelegate txDelegate = new TxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapUser) {
+ delegate = new TxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapUser) {
@Override
- public void setAttribute(String name, String value) {
+ public void setSingleAttribute(String name, String value) {
setLDAPAttribute(name, value);
- super.setAttribute(name, value);
+ super.setSingleAttribute(name, value);
+ }
+
+ @Override
+ public void setAttribute(String name, List<String> values) {
+ setLDAPAttribute(name, values);
+ super.setAttribute(name, values);
+ }
+
+ @Override
+ public void removeAttribute(String name) {
+ setLDAPAttribute(name, null);
+ super.removeAttribute(name);
}
@Override
@@ -117,28 +161,108 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
super.setFirstName(firstName);
}
- protected void setLDAPAttribute(String modelAttrName, String value) {
+ protected void setLDAPAttribute(String modelAttrName, Object value) {
if (modelAttrName.equalsIgnoreCase(userModelAttrName)) {
if (logger.isTraceEnabled()) {
- logger.tracef("Pushing user attribute to LDAP. Model attribute name: %s, LDAP attribute name: %s, Attribute value: %s", modelAttrName, ldapAttrName, value);
+ logger.tracef("Pushing user attribute to LDAP. username: %s, Model attribute name: %s, LDAP attribute name: %s, Attribute value: %s", getUsername(), modelAttrName, ldapAttrName, value);
}
ensureTransactionStarted();
- ldapUser.setAttribute(ldapAttrName, value);
+ if (value == null) {
+ ldapUser.setAttribute(ldapAttrName, null);
+ } else if (value instanceof String) {
+ ldapUser.setSingleAttribute(ldapAttrName, (String) value);
+ } else {
+ List<String> asList = (List<String>) value;
+ ldapUser.setAttribute(ldapAttrName, new LinkedHashSet<>(asList));
+ }
}
}
};
- return txDelegate;
- } else {
- return delegate;
}
+
+ // We prefer to read attribute value from LDAP instead of from local Keycloak DB
+ if (isAlwaysReadValueFromLDAP) {
+
+ delegate = new UserModelDelegate(delegate) {
+
+ @Override
+ public String getFirstAttribute(String name) {
+ if (name.equalsIgnoreCase(userModelAttrName)) {
+ return ldapUser.getAttributeAsString(ldapAttrName);
+ } else {
+ return super.getFirstAttribute(name);
+ }
+ }
+
+ @Override
+ public List<String> getAttribute(String name) {
+ if (name.equalsIgnoreCase(userModelAttrName)) {
+ Collection<String> ldapAttrValue = ldapUser.getAttributeAsSet(ldapAttrName);
+ if (ldapAttrValue == null) {
+ return null;
+ } else {
+ return new ArrayList<>(ldapAttrValue);
+ }
+ } else {
+ return super.getAttribute(name);
+ }
+ }
+
+ @Override
+ public Map<String, List<String>> getAttributes() {
+ Map<String, List<String>> attrs = new HashMap<>(super.getAttributes());
+
+ // Ignore UserModel properties
+ if (userModelProperties.get(userModelAttrName.toLowerCase()) != null) {
+ return attrs;
+ }
+
+ Set<String> allLdapAttrValues = ldapUser.getAttributeAsSet(ldapAttrName);
+ if (allLdapAttrValues != null) {
+ attrs.put(userModelAttrName, new ArrayList<>(allLdapAttrValues));
+ }
+ return attrs;
+ }
+
+ @Override
+ public String getEmail() {
+ if (UserModel.EMAIL.equalsIgnoreCase(userModelAttrName)) {
+ return ldapUser.getAttributeAsString(ldapAttrName);
+ } else {
+ return super.getEmail();
+ }
+ }
+
+ @Override
+ public String getLastName() {
+ if (UserModel.LAST_NAME.equalsIgnoreCase(userModelAttrName)) {
+ return ldapUser.getAttributeAsString(ldapAttrName);
+ } else {
+ return super.getLastName();
+ }
+ }
+
+ @Override
+ public String getFirstName() {
+ if (UserModel.FIRST_NAME.equalsIgnoreCase(userModelAttrName)) {
+ return ldapUser.getAttributeAsString(ldapAttrName);
+ } else {
+ return super.getFirstName();
+ }
+ }
+
+ };
+ }
+
+ return delegate;
}
@Override
- public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPIdentityQuery query) {
+ public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) {
String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
@@ -160,4 +284,22 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
private boolean isReadOnly(UserFederationMapperModel mapperModel) {
return parseBooleanParameter(mapperModel, READ_ONLY);
}
+
+
+ protected void setPropertyOnUserModel(Property<Object> userModelProperty, UserModel user, String ldapAttrValue) {
+ if (ldapAttrValue == null) {
+ userModelProperty.setValue(user, null);
+ } else {
+ Class<Object> clazz = userModelProperty.getJavaClass();
+
+ if (String.class.equals(clazz)) {
+ userModelProperty.setValue(user, ldapAttrValue);
+ } else if (Boolean.class.equals(clazz) || boolean.class.equals(clazz)) {
+ Boolean boolVal = Boolean.valueOf(ldapAttrValue);
+ userModelProperty.setValue(user, boolVal);
+ } else {
+ logger.warnf("Don't know how to set the property '%s' on user '%s' . Value of LDAP attribute is '%s' ", userModelProperty.getName(), user.getUsername(), ldapAttrValue.toString());
+ }
+ }
+ }
}
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java
index 90dd21a..1b1b44d 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java
@@ -30,6 +30,10 @@ public class UserAttributeLDAPFederationMapperFactory extends AbstractLDAPFedera
ProviderConfigProperty readOnly = createConfigProperty(UserAttributeLDAPFederationMapper.READ_ONLY, "Read Only",
"Read-only attribute is imported from LDAP to Keycloak DB, but it's not saved back to LDAP when user is updated in Keycloak.", ProviderConfigProperty.BOOLEAN_TYPE, "false");
configProperties.add(readOnly);
+
+ ProviderConfigProperty alwaysReadValueFromLDAP = createConfigProperty(UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "Always read value from LDAP",
+ "If on, then during reading of the user will be value of attribute from LDAP always used instead of the value from Keycloak DB", ProviderConfigProperty.BOOLEAN_TYPE, "false");
+ configProperties.add(alwaysReadValueFromLDAP);
}
@Override
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountBean.java
index 2032370..c152e11 100755
--- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountBean.java
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountBean.java
@@ -1,9 +1,12 @@
package org.keycloak.account.freemarker.model;
+import org.jboss.logging.Logger;
import org.keycloak.models.UserModel;
+import org.keycloak.util.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
/**
@@ -11,14 +14,29 @@ import java.util.Map;
*/
public class AccountBean {
+ private static final Logger logger = Logger.getLogger(AccountBean.class);
+
private final UserModel user;
private final MultivaluedMap<String, String> profileFormData;
+
+ // TODO: More proper multi-value attribute support
private final Map<String, String> attributes = new HashMap<>();
public AccountBean(UserModel user, MultivaluedMap<String, String> profileFormData) {
this.user = user;
this.profileFormData = profileFormData;
- attributes.putAll(user.getAttributes());
+
+ for (Map.Entry<String, List<String>> attr : user.getAttributes().entrySet()) {
+ List<String> attrValue = attr.getValue();
+ if (attrValue.size() > 0) {
+ attributes.put(attr.getKey(), attrValue.get(0));
+ }
+
+ if (attrValue.size() > 1) {
+ logger.warnf("There are more values for attribute '%s' of user '%s' . Will display just first value", attr.getKey(), user.getUsername());
+ }
+ }
+
if (profileFormData != null) {
for (String key : profileFormData.keySet()) {
if (key.startsWith("user.attributes.")) {
diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/LocaleHelper.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/LocaleHelper.java
index 0029dfd..fc249d5 100644
--- a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/LocaleHelper.java
+++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/LocaleHelper.java
@@ -35,7 +35,7 @@ public class LocaleHelper {
Locale locale = findLocale(realm.getSupportedLocales(), localeString);
if(locale != null){
if(user != null){
- user.setAttribute(UserModel.LOCALE, locale.toLanguageTag());
+ user.setSingleAttribute(UserModel.LOCALE, locale.toLanguageTag());
}
return locale;
}else{
@@ -48,8 +48,8 @@ public class LocaleHelper {
String localeString = httpHeaders.getCookies().get(LOCALE_COOKIE).getValue();
Locale locale = findLocale(realm.getSupportedLocales(), localeString);
if(locale != null){
- if(user != null && user.getAttribute(UserModel.LOCALE) == null){
- user.setAttribute(UserModel.LOCALE, locale.toLanguageTag());
+ if(user != null && user.getFirstAttribute(UserModel.LOCALE) == null){
+ user.setSingleAttribute(UserModel.LOCALE, locale.toLanguageTag());
}
return locale;
}else{
@@ -59,7 +59,7 @@ public class LocaleHelper {
//2. User profile
if(user != null && user.getAttributes().containsKey(UserModel.LOCALE)){
- String localeString = user.getAttribute(UserModel.LOCALE);
+ String localeString = user.getFirstAttribute(UserModel.LOCALE);
Locale locale = findLocale(realm.getSupportedLocales(), localeString);
if(locale != null){
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
index 2ff1273..f405994 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
@@ -206,6 +206,8 @@ module.controller('UserDetailCtrl', function($scope, realm, user, User, UserFede
if (!user.attributes) {
user.attributes = {}
}
+ convertAttributeValuesToString(user);
+
$scope.user = angular.copy(user);
if(user.federationLink) {
console.log("federationLink is not null");
@@ -252,13 +254,15 @@ module.controller('UserDetailCtrl', function($scope, realm, user, User, UserFede
}, true);
$scope.save = function() {
+ convertAttributeValuesToLists();
+
if ($scope.create) {
User.save({
realm: realm.realm
}, $scope.user, function (data, headers) {
$scope.changed = false;
+ convertAttributeValuesToString($scope.user);
user = angular.copy($scope.user);
-
var l = headers().location;
console.debug("Location == " + l);
@@ -275,12 +279,33 @@ module.controller('UserDetailCtrl', function($scope, realm, user, User, UserFede
userId: $scope.user.id
}, $scope.user, function () {
$scope.changed = false;
+ convertAttributeValuesToString($scope.user);
user = angular.copy($scope.user);
Notifications.success("Your changes have been saved to the user.");
});
}
};
+ function convertAttributeValuesToLists() {
+ var attrs = $scope.user.attributes;
+ for (var attribute in attrs) {
+ if (typeof attrs[attribute] === "string") {
+ var attrVals = attrs[attribute].split("##");
+ attrs[attribute] = attrVals;
+ }
+ }
+ }
+
+ function convertAttributeValuesToString(user) {
+ var attrs = user.attributes;
+ for (var attribute in attrs) {
+ if (typeof attrs[attribute] === "object") {
+ var attrVals = attrs[attribute].join("##");
+ attrs[attribute] = attrVals;
+ }
+ }
+ }
+
$scope.reset = function() {
$scope.user = angular.copy(user);
$scope.changed = false;
diff --git a/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java b/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java
index 064697b..d21d203 100755
--- a/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java
+++ b/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java
@@ -1,7 +1,5 @@
package org.keycloak.models.entities;
-import org.keycloak.models.UserModel;
-
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -23,7 +21,7 @@ public class UserEntity extends AbstractIdentifiableEntity {
private List<String> roleIds;
- private Map<String, String> attributes;
+ private Map<String, List<String>> attributes;
private List<String> requiredActions;
private List<CredentialEntity> credentials = new ArrayList<CredentialEntity>();
private List<FederatedIdentityEntity> federatedIdentities;
@@ -101,11 +99,11 @@ public class UserEntity extends AbstractIdentifiableEntity {
this.roleIds = roleIds;
}
- public Map<String, String> getAttributes() {
+ public Map<String, List<String>> getAttributes() {
return attributes;
}
- public void setAttributes(Map<String, String> attributes) {
+ public void setAttributes(Map<String, List<String>> attributes) {
this.attributes = attributes;
}
diff --git a/model/api/src/main/java/org/keycloak/models/LDAPConstants.java b/model/api/src/main/java/org/keycloak/models/LDAPConstants.java
index 0d97664..1a32097 100644
--- a/model/api/src/main/java/org/keycloak/models/LDAPConstants.java
+++ b/model/api/src/main/java/org/keycloak/models/LDAPConstants.java
@@ -60,6 +60,7 @@ public class LDAPConstants {
public static final String SAM_ACCOUNT_NAME = "sAMAccountName";
public static final String EMAIL = "mail";
public static final String POSTAL_CODE = "postalCode";
+ public static final String STREET = "street";
public static final String MEMBER = "member";
public static final String MEMBER_OF = "memberOf";
public static final String OBJECT_CLASS = "objectclass";
diff --git a/model/api/src/main/java/org/keycloak/models/UserModel.java b/model/api/src/main/java/org/keycloak/models/UserModel.java
index 645250e..dea9e7b 100755
--- a/model/api/src/main/java/org/keycloak/models/UserModel.java
+++ b/model/api/src/main/java/org/keycloak/models/UserModel.java
@@ -27,13 +27,31 @@ public interface UserModel {
void setEnabled(boolean enabled);
- void setAttribute(String name, String value);
+ /**
+ * Set single value of specified attribute. Remove all other existing values
+ *
+ * @param name
+ * @param value
+ */
+ void setSingleAttribute(String name, String value);
+
+ void setAttribute(String name, List<String> values);
void removeAttribute(String name);
- String getAttribute(String name);
+ /**
+ * @param name
+ * @return null if there is not any value of specified attribute or first value otherwise. Don't throw exception if there are more values of the attribute
+ */
+ String getFirstAttribute(String name);
+
+ /**
+ * @param name
+ * @return list of all attribute values or empty list if there are not any values. Never return null
+ */
+ List<String> getAttribute(String name);
- Map<String, String> getAttributes();
+ Map<String, List<String>> getAttributes();
Set<String> getRequiredActions();
diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index 467040f..3bf412c 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -31,6 +31,7 @@ import org.keycloak.representations.idm.UserFederationMapperRepresentation;
import org.keycloak.representations.idm.UserFederationProviderRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation;
+import org.keycloak.util.MultivaluedHashMap;
import org.keycloak.util.Time;
import java.util.ArrayList;
@@ -67,7 +68,7 @@ public class ModelToRepresentation {
rep.setRequiredActions(reqActions);
if (user.getAttributes() != null && !user.getAttributes().isEmpty()) {
- Map<String, String> attrs = new HashMap<String, String>();
+ Map<String, Object> attrs = new HashMap<>();
attrs.putAll(user.getAttributes());
rep.setAttributes(attrs);
}
diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index 245e6c6..217da67 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -43,6 +43,7 @@ import org.keycloak.util.UriUtils;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
@@ -805,8 +806,17 @@ public class RepresentationToModel {
user.setFederationLink(userRep.getFederationLink());
user.setTotp(userRep.isTotp());
if (userRep.getAttributes() != null) {
- for (Map.Entry<String, String> entry : userRep.getAttributes().entrySet()) {
- user.setAttribute(entry.getKey(), entry.getValue());
+ for (Map.Entry<String, Object> entry : userRep.getAttributes().entrySet()) {
+ Object value = entry.getValue();
+
+ if (value instanceof Collection) {
+ Collection<String> colVal = (Collection<String>) value;
+ user.setAttribute(entry.getKey(), new ArrayList<>(colVal));
+ } else if (value instanceof String) {
+ // TODO: This is here just for backwards compatibility with KC 1.3 and earlier
+ String stringVal = (String) value;
+ user.setSingleAttribute(entry.getKey(), stringVal);
+ }
}
}
if (userRep.getRequiredActions() != null) {
diff --git a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java
index 7123c3e..699a38e 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java
@@ -53,8 +53,13 @@ public class UserModelDelegate implements UserModel {
}
@Override
- public void setAttribute(String name, String value) {
- delegate.setAttribute(name, value);
+ public void setSingleAttribute(String name, String value) {
+ delegate.setSingleAttribute(name, value);
+ }
+
+ @Override
+ public void setAttribute(String name, List<String> values) {
+ delegate.setAttribute(name, values);
}
@Override
@@ -63,12 +68,17 @@ public class UserModelDelegate implements UserModel {
}
@Override
- public String getAttribute(String name) {
+ public String getFirstAttribute(String name) {
+ return delegate.getFirstAttribute(name);
+ }
+
+ @Override
+ public List<String> getAttribute(String name) {
return delegate.getAttribute(name);
}
@Override
- public Map<String, String> getAttributes() {
+ public Map<String, List<String>> getAttributes() {
return delegate.getAttributes();
}
diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
index 39024c1..5c33375 100755
--- a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
+++ b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
@@ -158,12 +158,23 @@ public class UserAdapter implements UserModel, Comparable {
}
@Override
- public void setAttribute(String name, String value) {
+ public void setSingleAttribute(String name, String value) {
if (user.getAttributes() == null) {
- user.setAttributes(new HashMap<String, String>());
+ user.setAttributes(new HashMap<String, List<String>>());
}
- user.getAttributes().put(name, value);
+ List<String> attrValues = new ArrayList<>();
+ attrValues.add(value);
+ user.getAttributes().put(name, attrValues);
+ }
+
+ @Override
+ public void setAttribute(String name, List<String> values) {
+ if (user.getAttributes() == null) {
+ user.setAttributes(new HashMap<String, List<String>>());
+ }
+
+ user.getAttributes().put(name, values);
}
@Override
@@ -174,13 +185,23 @@ public class UserAdapter implements UserModel, Comparable {
}
@Override
- public String getAttribute(String name) {
- return user.getAttributes()==null ? null : user.getAttributes().get(name);
+ public String getFirstAttribute(String name) {
+ if (user.getAttributes()==null) return null;
+
+ List<String> attrValues = user.getAttributes().get(name);
+ return (attrValues==null || attrValues.isEmpty()) ? null : attrValues.get(0);
+ }
+
+ @Override
+ public List<String> getAttribute(String name) {
+ if (user.getAttributes()==null) return Collections.<String>emptyList();
+ List<String> attrValues = user.getAttributes().get(name);
+ return (attrValues == null) ? Collections.<String>emptyList() : Collections.unmodifiableList(attrValues);
}
@Override
- public Map<String, String> getAttributes() {
- return user.getAttributes()==null ? Collections.<String, String>emptyMap() : Collections.unmodifiableMap(user.getAttributes());
+ public Map<String, List<String>> getAttributes() {
+ return user.getAttributes()==null ? Collections.<String, List<String>>emptyMap() : Collections.unmodifiableMap((Map)user.getAttributes());
}
@Override
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java
index bbef81b..d7824cc 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java
@@ -4,6 +4,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel;
+import org.keycloak.util.MultivaluedHashMap;
import java.io.Serializable;
import java.util.HashMap;
@@ -29,7 +30,7 @@ public class CachedUser implements Serializable {
private boolean enabled;
private boolean totp;
private String federationLink;
- private Map<String, String> attributes = new HashMap<>();
+ private MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
private Set<String> requiredActions = new HashSet<>();
private Set<String> roleMappings = new HashSet<String>();
@@ -93,7 +94,7 @@ public class CachedUser implements Serializable {
return totp;
}
- public Map<String, String> getAttributes() {
+ public MultivaluedHashMap<String, String> getAttributes() {
return attributes;
}
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java
index aa80a25..fb516c6 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java
@@ -11,6 +11,7 @@ import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.cache.entities.CachedUser;
+import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -78,9 +79,15 @@ public class UserAdapter implements UserModel {
}
@Override
- public void setAttribute(String name, String value) {
+ public void setSingleAttribute(String name, String value) {
getDelegateForUpdate();
- updated.setAttribute(name, value);
+ updated.setSingleAttribute(name, value);
+ }
+
+ @Override
+ public void setAttribute(String name, List<String> values) {
+ getDelegateForUpdate();
+ updated.setAttribute(name, values);
}
@Override
@@ -90,13 +97,20 @@ public class UserAdapter implements UserModel {
}
@Override
- public String getAttribute(String name) {
+ public String getFirstAttribute(String name) {
+ if (updated != null) return updated.getFirstAttribute(name);
+ return cached.getAttributes().getFirst(name);
+ }
+
+ @Override
+ public List<String> getAttribute(String name) {
if (updated != null) return updated.getAttribute(name);
- return cached.getAttributes().get(name);
+ List<String> result = cached.getAttributes().get(name);
+ return (result == null) ? Collections.<String>emptyList() : result;
}
@Override
- public Map<String, String> getAttributes() {
+ public Map<String, List<String>> getAttributes() {
if (updated != null) return updated.getAttributes();
return cached.getAttributes();
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserAttributeEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserAttributeEntity.java
index 2454f96..51352b4 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserAttributeEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserAttributeEntity.java
@@ -1,6 +1,8 @@
package org.keycloak.models.jpa.entities;
+import javax.persistence.CollectionTable;
import javax.persistence.Column;
+import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
@@ -11,6 +13,8 @@ import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
import java.io.Serializable;
+import java.util.HashSet;
+import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -22,20 +26,29 @@ import java.io.Serializable;
})
@Table(name="USER_ATTRIBUTE")
@Entity
-@IdClass(UserAttributeEntity.Key.class)
public class UserAttributeEntity {
@Id
+ @Column(name="ID", length = 36)
+ protected String id;
+
@ManyToOne(fetch= FetchType.LAZY)
@JoinColumn(name = "USER_ID")
protected UserEntity user;
- @Id
@Column(name = "NAME")
protected String name;
@Column(name = "VALUE")
protected String value;
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
public String getName() {
return name;
}
@@ -60,47 +73,4 @@ public class UserAttributeEntity {
this.user = user;
}
- public static class Key implements Serializable {
-
- protected UserEntity user;
-
- protected String name;
-
- public Key() {
- }
-
- public Key(UserEntity user, String name) {
- this.user = user;
- this.name = name;
- }
-
- public UserEntity getUser() {
- return user;
- }
-
- public String getName() {
- return name;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- Key key = (Key) o;
-
- if (name != null ? !name.equals(key.name) : key.name != null) return false;
- if (user != null ? !user.getId().equals(key.user != null ? key.user.getId() : null) : key.user != null) return false;
-
- return true;
- }
-
- @Override
- public int hashCode() {
- int result = user != null ? user.getId().hashCode() : 0;
- result = 31 * result + (name != null ? name.hashCode() : 0);
- return result;
- }
- }
-
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
index 670f5f0..3719b9d 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
@@ -22,6 +22,7 @@ import org.keycloak.models.jpa.entities.UserRequiredActionEntity;
import org.keycloak.models.jpa.entities.UserRoleMappingEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
+import org.keycloak.util.MultivaluedHashMap;
import org.keycloak.util.Time;
import javax.persistence.EntityManager;
@@ -92,14 +93,46 @@ public class UserAdapter implements UserModel {
}
@Override
- public void setAttribute(String name, String value) {
+ public void setSingleAttribute(String name, String value) {
+ boolean found = false;
+ List<UserAttributeEntity> toRemove = new ArrayList<>();
for (UserAttributeEntity attr : user.getAttributes()) {
if (attr.getName().equals(name)) {
- attr.setValue(value);
- return;
+ if (!found) {
+ attr.setValue(value);
+ found = true;
+ } else {
+ toRemove.add(attr);
+ }
}
}
+
+ for (UserAttributeEntity attr : toRemove) {
+ em.remove(attr);
+ user.getAttributes().remove(attr);
+ }
+
+ if (found) {
+ return;
+ }
+
+ persistAttributeValue(name, value);
+ }
+
+ @Override
+ public void setAttribute(String name, List<String> values) {
+ // Remove all existing
+ removeAttribute(name);
+
+ // Put all new
+ for (String value : values) {
+ persistAttributeValue(name, value);
+ }
+ }
+
+ private void persistAttributeValue(String name, String value) {
UserAttributeEntity attr = new UserAttributeEntity();
+ attr.setId(KeycloakModelUtils.generateId());
attr.setName(name);
attr.setValue(value);
attr.setUser(user);
@@ -120,7 +153,7 @@ public class UserAdapter implements UserModel {
}
@Override
- public String getAttribute(String name) {
+ public String getFirstAttribute(String name) {
for (UserAttributeEntity attr : user.getAttributes()) {
if (attr.getName().equals(name)) {
return attr.getValue();
@@ -130,10 +163,21 @@ public class UserAdapter implements UserModel {
}
@Override
- public Map<String, String> getAttributes() {
- Map<String, String> result = new HashMap<String, String>();
+ public List<String> getAttribute(String name) {
+ List<String> result = new ArrayList<>();
+ for (UserAttributeEntity attr : user.getAttributes()) {
+ if (attr.getName().equals(name)) {
+ result.add(attr.getValue());
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public Map<String, List<String>> getAttributes() {
+ MultivaluedHashMap<String, String> result = new MultivaluedHashMap<>();
for (UserAttributeEntity attr : user.getAttributes()) {
- result.put(attr.getName(), attr.getValue());
+ result.add(attr.getName(), attr.getValue());
}
return result;
}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
index 79a6260..dc858ef 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
@@ -19,7 +19,6 @@ import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.entities.CredentialEntity;
import org.keycloak.models.entities.UserConsentEntity;
-import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity;
import org.keycloak.models.mongo.utils.MongoModelUtils;
@@ -127,12 +126,24 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
}
@Override
- public void setAttribute(String name, String value) {
+ public void setSingleAttribute(String name, String value) {
if (user.getAttributes() == null) {
- user.setAttributes(new HashMap<String, String>());
+ user.setAttributes(new HashMap<String, List<String>>());
}
- user.getAttributes().put(name, value);
+ List<String> attrValues = new ArrayList<>();
+ attrValues.add(value);
+ user.getAttributes().put(name, attrValues);
+ updateUser();
+ }
+
+ @Override
+ public void setAttribute(String name, List<String> values) {
+ if (user.getAttributes() == null) {
+ user.setAttributes(new HashMap<String, List<String>>());
+ }
+
+ user.getAttributes().put(name, values);
updateUser();
}
@@ -145,13 +156,23 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
}
@Override
- public String getAttribute(String name) {
- return user.getAttributes()==null ? null : user.getAttributes().get(name);
+ public String getFirstAttribute(String name) {
+ if (user.getAttributes()==null) return null;
+
+ List<String> attrValues = user.getAttributes().get(name);
+ return (attrValues==null || attrValues.isEmpty()) ? null : attrValues.get(0);
+ }
+
+ @Override
+ public List<String> getAttribute(String name) {
+ if (user.getAttributes()==null) return Collections.<String>emptyList();
+ List<String> attrValues = user.getAttributes().get(name);
+ return (attrValues == null) ? Collections.<String>emptyList() : Collections.unmodifiableList(attrValues);
}
@Override
- public Map<String, String> getAttributes() {
- return user.getAttributes()==null ? Collections.<String, String>emptyMap() : Collections.unmodifiableMap(user.getAttributes());
+ public Map<String, List<String>> getAttributes() {
+ return user.getAttributes()==null ? Collections.<String, List<String>>emptyMap() : Collections.unmodifiableMap((Map)user.getAttributes());
}
public MongoUserEntity getUser() {
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java
index 671abb2..b5f59ad 100755
--- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java
+++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java
@@ -13,7 +13,7 @@ import java.util.ArrayList;
import java.util.List;
/**
- * Mappings UserModel property (the property name of a getter method) to an AttributeStatement.
+ * Mappings UserModel attribute (not property name of a getter method) to an AttributeStatement.
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
@@ -62,7 +62,7 @@ public class UserAttributeStatementMapper extends AbstractSAMLProtocolMapper imp
public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
UserModel user = userSession.getUser();
String attributeName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE);
- String attributeValue = user.getAttribute(attributeName);
+ String attributeValue = user.getFirstAttribute(attributeName);
if (attributeValue == null) return;
AttributeStatementHelper.addAttribute(attributeStatement, mappingModel, attributeValue);
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
index d5630bd..6dcef81 100755
--- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
+++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
@@ -238,11 +238,11 @@ public class SamlProtocol implements LoginProtocol {
// generate a persistent user id specifically for each client.
UserModel user = userSession.getUser();
String name = SAML_PERSISTENT_NAME_ID_FOR + "." + clientSession.getClient().getClientId();
- String samlPersistentId = user.getAttribute(name);
+ String samlPersistentId = user.getFirstAttribute(name);
if (samlPersistentId != null) return samlPersistentId;
// "G-" stands for "generated"
samlPersistentId = "G-" + UUID.randomUUID().toString();
- user.setAttribute(name, samlPersistentId);
+ user.setSingleAttribute(name, samlPersistentId);
return samlPersistentId;
} else if(nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get())){
// TODO: Support for persistent NameID (pseudo-random identifier persisted in user object)
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AddressMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AddressMapper.java
index 5ebff0d..64fc4cd 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AddressMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AddressMapper.java
@@ -118,11 +118,11 @@ public class AddressMapper extends AbstractOIDCProtocolMapper implements OIDCAcc
protected void setClaim(IDToken token, UserSessionModel userSession) {
UserModel user = userSession.getUser();
AddressClaimSet addressSet = new AddressClaimSet();
- addressSet.setStreetAddress(user.getAttribute("street"));
- addressSet.setLocality(user.getAttribute("locality"));
- addressSet.setRegion(user.getAttribute("region"));
- addressSet.setPostalCode(user.getAttribute("postal_code"));
- addressSet.setCountry(user.getAttribute("country"));
+ addressSet.setStreetAddress(user.getFirstAttribute("street"));
+ addressSet.setLocality(user.getFirstAttribute("locality"));
+ addressSet.setRegion(user.getFirstAttribute("region"));
+ addressSet.setPostalCode(user.getFirstAttribute("postal_code"));
+ addressSet.setCountry(user.getFirstAttribute("country"));
token.getOtherClaims().put("address", addressSet);
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java
index bae8c08..9e53b22 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java
@@ -1,5 +1,6 @@
package org.keycloak.protocol.oidc.mappers;
+import org.jboss.logging.Logger;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.ProtocolMapper;
@@ -19,6 +20,8 @@ import java.util.Map;
* @version $Revision: 1 $
*/
public class OIDCAttributeMapperHelper {
+ private static final Logger logger = Logger.getLogger(OIDCAttributeMapperHelper.class);
+
public static final String TOKEN_CLAIM_NAME = "claim.name";
public static final String TOKEN_CLAIM_NAME_LABEL = "Token Claim Name";
public static final String JSON_TYPE = "Claim JSON Type";
@@ -31,6 +34,26 @@ public class OIDCAttributeMapperHelper {
public static Object mapAttributeValue(ProtocolMapperModel mappingModel, Object attributeValue) {
if (attributeValue == null) return null;
+
+ if (attributeValue instanceof List) {
+ List<Object> valueAsList = (List<Object>) attributeValue;
+ if (valueAsList.size() == 0) return null;
+
+ if (isMultivalued(mappingModel)) {
+ List<Object> result = new ArrayList<>();
+ for (Object valueItem : valueAsList) {
+ result.add(mapAttributeValue(mappingModel, valueItem));
+ }
+ return result;
+ } else {
+ if (valueAsList.size() > 1) {
+ logger.warnf("Multiple values found '%s' for protocol mapper '%s' but expected just single value", attributeValue.toString(), mappingModel.getName());
+ }
+
+ attributeValue = valueAsList.get(0);
+ }
+ }
+
String type = mappingModel.getConfig().get(JSON_TYPE);
if (type == null) return attributeValue;
if (type.equals("boolean")) {
@@ -53,8 +76,9 @@ public class OIDCAttributeMapperHelper {
}
public static void mapClaim(IDToken token, ProtocolMapperModel mappingModel, Object attributeValue) {
- if (attributeValue == null) return;
attributeValue = mapAttributeValue(mappingModel, attributeValue);
+ if (attributeValue == null) return;
+
String protocolClaim = mappingModel.getConfig().get(TOKEN_CLAIM_NAME);
String[] split = protocolClaim.split("\\.");
Map<String, Object> jsonObject = token.getOtherClaims();
@@ -102,6 +126,11 @@ public class OIDCAttributeMapperHelper {
return "true".equals(mappingModel.getConfig().get(INCLUDE_IN_ACCESS_TOKEN));
}
+
+ public static boolean isMultivalued(ProtocolMapperModel mappingModel) {
+ return "true".equals(mappingModel.getConfig().get(ProtocolMapperUtils.MULTIVALUED));
+ }
+
public static void addAttributeConfig(List<ProviderConfigProperty> configProperties) {
ProviderConfigProperty property;
property = new ProviderConfigProperty();
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java
index b854351..b4c9c76 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java
@@ -1,9 +1,9 @@
package org.keycloak.protocol.oidc.mappers;
+import org.jboss.logging.Logger;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
-import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.ProtocolMapperUtils;
@@ -36,6 +36,13 @@ public class UserAttributeMapper extends AbstractOIDCProtocolMapper implements O
configProperties.add(property);
OIDCAttributeMapperHelper.addAttributeConfig(configProperties);
+ property = new ProviderConfigProperty();
+ property.setName(ProtocolMapperUtils.MULTIVALUED);
+ property.setLabel(ProtocolMapperUtils.MULTIVALUED_LABEL);
+ property.setHelpText(ProtocolMapperUtils.MULTIVALUED_HELP_TEXT);
+ property.setType(ProviderConfigProperty.BOOLEAN_TYPE);
+ configProperties.add(property);
+
}
public static final String PROVIDER_ID = "oidc-usermodel-attribute-mapper";
@@ -77,7 +84,7 @@ public class UserAttributeMapper extends AbstractOIDCProtocolMapper implements O
protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
UserModel user = userSession.getUser();
String attributeName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE);
- String attributeValue = user.getAttribute(attributeName);
+ List<String> attributeValue = user.getAttribute(attributeName);
if (attributeValue == null) return;
OIDCAttributeMapperHelper.mapClaim(token, mappingModel, attributeValue);
}
@@ -93,12 +100,18 @@ public class UserAttributeMapper extends AbstractOIDCProtocolMapper implements O
String userAttribute,
String tokenClaimName, String claimType,
boolean consentRequired, String consentText,
- boolean accessToken, boolean idToken) {
- return OIDCAttributeMapperHelper.createClaimMapper(name, userAttribute,
+ boolean accessToken, boolean idToken, boolean multivalued) {
+ ProtocolMapperModel mapper = OIDCAttributeMapperHelper.createClaimMapper(name, userAttribute,
tokenClaimName, claimType,
consentRequired, consentText,
accessToken, idToken,
PROVIDER_ID);
+
+ if (multivalued) {
+ mapper.getConfig().put(ProtocolMapperUtils.MULTIVALUED, "true");
+ }
+
+ return mapper;
}
diff --git a/services/src/main/java/org/keycloak/protocol/ProtocolMapperUtils.java b/services/src/main/java/org/keycloak/protocol/ProtocolMapperUtils.java
index b91b754..457084a 100755
--- a/services/src/main/java/org/keycloak/protocol/ProtocolMapperUtils.java
+++ b/services/src/main/java/org/keycloak/protocol/ProtocolMapperUtils.java
@@ -15,12 +15,15 @@ import java.util.List;
public class ProtocolMapperUtils {
public static final String USER_ATTRIBUTE = "user.attribute";
public static final String USER_SESSION_NOTE = "user.session.note";
+ public static final String MULTIVALUED = "multivalued";
public static final String USER_MODEL_PROPERTY_LABEL = "User Property";
public static final String USER_MODEL_PROPERTY_HELP_TEXT = "Name of the property method in the UserModel interface. For example, a value of 'email' would reference the UserModel.getEmail() method.";
public static final String USER_MODEL_ATTRIBUTE_LABEL = "User Attribute";
public static final String USER_MODEL_ATTRIBUTE_HELP_TEXT = "Name of stored user attribute which is the name of an attribute within the UserModel.attribute map.";
public static final String USER_SESSION_MODEL_NOTE_LABEL = "User Session Note";
public static final String USER_SESSION_MODEL_NOTE_HELP_TEXT = "Name of stored user session note within the UserSessionModel.note map.";
+ public static final String MULTIVALUED_LABEL = "Multivalued";
+ public static final String MULTIVALUED_HELP_TEXT = "Indicates if attribute supports multiple values. If true, then the list of all values of this attribute will be set as claim. If false, then just first value will be set as claim";
public static String getUserModelValue(UserModel user, String propertyName) {
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
index b27e453..b6f1fb1 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
@@ -5,7 +5,6 @@ import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.BadRequestException;
import org.jboss.resteasy.spi.NotFoundException;
import org.keycloak.ClientConnection;
-import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailProvider;
@@ -228,8 +227,8 @@ public class UsersResource {
}
}
- if (rep.getAttributes() != null) {
- for (Map.Entry<String, String> attr : rep.getAttributes().entrySet()) {
+ if (rep.getAttributesAsListValues() != null) {
+ for (Map.Entry<String, List<String>> attr : rep.getAttributesAsListValues().entrySet()) {
user.setAttribute(attr.getKey(), attr.getValue());
}
diff --git a/services/src/main/java/org/keycloak/services/resources/AttributeFormDataProcessor.java b/services/src/main/java/org/keycloak/services/resources/AttributeFormDataProcessor.java
index e426b10..10ce8db 100755
--- a/services/src/main/java/org/keycloak/services/resources/AttributeFormDataProcessor.java
+++ b/services/src/main/java/org/keycloak/services/resources/AttributeFormDataProcessor.java
@@ -1,5 +1,8 @@
package org.keycloak.services.resources;
+import java.util.ArrayList;
+import java.util.List;
+
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
@@ -21,8 +24,26 @@ public class AttributeFormDataProcessor {
for (String key : formData.keySet()) {
if (!key.startsWith("user.attributes.")) continue;
String attribute = key.substring("user.attributes.".length());
- user.setAttribute(attribute, formData.getFirst(key));
+
+ // Need to handle case when attribute has multiple values, but in UI was displayed just first value
+ List<String> modelValue = new ArrayList<>(user.getAttribute(attribute));
+
+ int index = 0;
+ for (String value : formData.get(key)) {
+ addOrSetValue(modelValue, index, value);
+ index++;
+ }
+
+ user.setAttribute(attribute, modelValue);
}
}
+
+ private static void addOrSetValue(List<String> list, int index, String value) {
+ if (list.size() > index) {
+ list.set(index, value);
+ } else {
+ list.add(value);
+ }
+ }
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java
index 526813b..dd572d7 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java
@@ -6,6 +6,7 @@ import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
+import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.ClassRule;
import org.junit.Rule;
@@ -51,8 +52,8 @@ public class ProfileTest {
UserModel user = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm);
user.setFirstName("First");
user.setLastName("Last");
- user.setAttribute("key1", "value1");
- user.setAttribute("key2", "value2");
+ user.setSingleAttribute("key1", "value1");
+ user.setSingleAttribute("key2", "value2");
ClientModel accountApp = appRealm.getClientByClientId(org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
@@ -114,8 +115,12 @@ public class ProfileTest {
assertEquals("Last", profile.getString("lastName"));
JSONObject attributes = profile.getJSONObject("attributes");
- assertEquals("value1", attributes.getString("key1"));
- assertEquals("value2", attributes.getString("key2"));
+ JSONArray attrValue = attributes.getJSONArray("key1");
+ assertEquals(1, attrValue.length());
+ assertEquals("value1", attrValue.get(0));
+ attrValue = attributes.getJSONArray("key2");
+ assertEquals(1, attrValue.length());
+ assertEquals("value2", attrValue.get(0));
}
@Test
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java
index 42dd464..ab644cd 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java
@@ -12,6 +12,8 @@ import org.keycloak.representations.idm.UserRepresentation;
import javax.ws.rs.ClientErrorException;
import javax.ws.rs.core.Response;
+
+import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
@@ -271,8 +273,8 @@ public class UserTest extends AbstractClientTest {
public void attributes() {
UserRepresentation user1 = new UserRepresentation();
user1.setUsername("user1");
- user1.attribute("attr1", "value1user1");
- user1.attribute("attr2", "value2user1");
+ user1.singleAttribute("attr1", "value1user1");
+ user1.singleAttribute("attr2", "value2user1");
Response response = realm.users().create(user1);
String user1Id = ApiUtil.getCreatedId(response);
@@ -280,40 +282,45 @@ public class UserTest extends AbstractClientTest {
UserRepresentation user2 = new UserRepresentation();
user2.setUsername("user2");
- user2.attribute("attr1", "value1user2");
- user2.attribute("attr2", "value2user2");
+ user2.singleAttribute("attr1", "value1user2");
+ List<String> vals = new ArrayList<>();
+ vals.add("value2user2");
+ vals.add("value2user2_2");
+ user2.getAttributesAsListValues().put("attr2", vals);
response = realm.users().create(user2);
String user2Id = ApiUtil.getCreatedId(response);
response.close();
user1 = realm.users().get(user1Id).toRepresentation();
- assertEquals(2, user1.getAttributes().size());
- assertEquals("value1user1", user1.getAttributes().get("attr1"));
- assertEquals("value2user1", user1.getAttributes().get("attr2"));
+ assertEquals(2, user1.getAttributesAsListValues().size());
+ assertAttributeValue("value1user1", user1.getAttributesAsListValues().get("attr1"));
+ assertAttributeValue("value2user1", user1.getAttributesAsListValues().get("attr2"));
user2 = realm.users().get(user2Id).toRepresentation();
- assertEquals(2, user2.getAttributes().size());
- assertEquals("value1user2", user2.getAttributes().get("attr1"));
- assertEquals("value2user2", user2.getAttributes().get("attr2"));
+ assertEquals(2, user2.getAttributesAsListValues().size());
+ assertAttributeValue("value1user2", user2.getAttributesAsListValues().get("attr1"));
+ vals = user2.getAttributesAsListValues().get("attr2");
+ assertEquals(2, vals.size());
+ assertTrue(vals.contains("value2user2") && vals.contains("value2user2_2"));
- user1.attribute("attr1", "value3user1");
- user1.attribute("attr3", "value4user1");
+ user1.singleAttribute("attr1", "value3user1");
+ user1.singleAttribute("attr3", "value4user1");
realm.users().get(user1Id).update(user1);
user1 = realm.users().get(user1Id).toRepresentation();
- assertEquals(3, user1.getAttributes().size());
- assertEquals("value3user1", user1.getAttributes().get("attr1"));
- assertEquals("value2user1", user1.getAttributes().get("attr2"));
- assertEquals("value4user1", user1.getAttributes().get("attr3"));
+ assertEquals(3, user1.getAttributesAsListValues().size());
+ assertAttributeValue("value3user1", user1.getAttributesAsListValues().get("attr1"));
+ assertAttributeValue("value2user1", user1.getAttributesAsListValues().get("attr2"));
+ assertAttributeValue("value4user1", user1.getAttributesAsListValues().get("attr3"));
user1.getAttributes().remove("attr1");
realm.users().get(user1Id).update(user1);
user1 = realm.users().get(user1Id).toRepresentation();
- assertEquals(2, user1.getAttributes().size());
- assertEquals("value2user1", user1.getAttributes().get("attr2"));
- assertEquals("value4user1", user1.getAttributes().get("attr3"));
+ assertEquals(2, user1.getAttributesAsListValues().size());
+ assertAttributeValue("value2user1", user1.getAttributesAsListValues().get("attr2"));
+ assertAttributeValue("value4user1", user1.getAttributesAsListValues().get("attr3"));
user1.getAttributes().clear();
realm.users().get(user1Id).update(user1);
@@ -322,6 +329,11 @@ public class UserTest extends AbstractClientTest {
assertNull(user1.getAttributes());
}
+ private void assertAttributeValue(String expectedValue, List<String> attrValues) {
+ assertEquals(1, attrValues.size());
+ assertEquals(expectedValue, attrValues.get(0));
+ }
+
@Test
public void sendResetPasswordEmail() {
UserRepresentation userRep = new UserRepresentation();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
index 3f4ffa5..ba6d44b 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
@@ -141,7 +141,7 @@ public abstract class AbstractIdentityProviderTest {
identityProviderModel.setUpdateProfileFirstLoginMode(IdentityProviderRepresentation.UPFLM_ON);
UserModel user = assertSuccessfulAuthentication(identityProviderModel, "test-user", "new@email.com", true);
- Assert.assertEquals("617-666-7777", user.getAttribute("mobile"));
+ Assert.assertEquals("617-666-7777", user.getFirstAttribute("mobile"));
}
@Test
@@ -304,7 +304,7 @@ public abstract class AbstractIdentityProviderTest {
identityProviderModel.setTrustEmail(true);
UserModel user = assertSuccessfulAuthenticationWithEmailVerification(identityProviderModel, "test-user", "new@email.com", true);
- Assert.assertEquals("617-666-7777", user.getAttribute("mobile"));
+ Assert.assertEquals("617-666-7777", user.getFirstAttribute("mobile"));
} finally {
identityProviderModel.setTrustEmail(false);
getRealm().setVerifyEmail(false);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java
index 4ed0d28..c4a028c 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java
@@ -263,6 +263,101 @@ public class FederationProvidersIntegrationTest {
}
@Test
+ public void testCaseSensitiveAttributeName() {
+ KeycloakSession session = keycloakRule.startSession();
+
+ try {
+ RealmModel appRealm = new RealmManager(session).getRealmByName("test");
+
+ LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
+ LDAPObject johnZip = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnzip", "John", "Zip", "johnzip@email.org", "12398");
+
+ // Remove default zipcode mapper and add the mapper for "POstalCode" to test case sensitivity
+ UserFederationMapperModel currentZipMapper = appRealm.getUserFederationMapperByName(ldapModel.getId(), "zipCodeMapper");
+ appRealm.removeUserFederationMapper(currentZipMapper);
+ FederationTestUtils.addUserAttributeMapper(appRealm, ldapModel, "zipCodeMapper-cs", "postal_code", "POstalCode");
+
+ // Fetch user from LDAP and check that postalCode is filled
+ UserModel user = session.users().getUserByUsername("johnzip", appRealm);
+ String postalCode = user.getFirstAttribute("postal_code");
+ Assert.assertEquals("12398", postalCode);
+
+ } finally {
+ keycloakRule.stopSession(session, false);
+ }
+ }
+
+ @Test
+ public void testDirectLDAPUpdate() {
+ KeycloakSession session = keycloakRule.startSession();
+
+ try {
+ RealmModel appRealm = new RealmManager(session).getRealmByName("test");
+
+ LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
+ LDAPObject johnDirect = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johndirect", "John", "Direct", "johndirect@email.org", "12399");
+
+ // Fetch user from LDAP and check that postalCode is filled
+ UserModel user = session.users().getUserByUsername("johndirect", appRealm);
+ String postalCode = user.getFirstAttribute("postal_code");
+ Assert.assertEquals("12399", postalCode);
+
+ // Directly update user in LDAP
+ johnDirect.setSingleAttribute(LDAPConstants.POSTAL_CODE, "12400");
+ johnDirect.setSingleAttribute(LDAPConstants.SN, "DirectLDAPUpdated");
+ ldapFedProvider.getLdapIdentityStore().update(johnDirect);
+
+ // Verify that postalCode is still the same as we read it's value from Keycloak DB
+ user = session.users().getUserByUsername("johndirect", appRealm);
+ postalCode = user.getFirstAttribute("postal_code");
+ Assert.assertEquals("12399", postalCode);
+
+ // Check user.getAttributes()
+ postalCode = user.getAttributes().get("postal_code").get(0);
+ Assert.assertEquals("12399", postalCode);
+
+ // LastName is new as lastName mapper will read the value from LDAP
+ String lastName = user.getLastName();
+ Assert.assertEquals("DirectLDAPUpdated", lastName);
+ } finally {
+ keycloakRule.stopSession(session, true);
+ }
+
+ session = keycloakRule.startSession();
+ try {
+ RealmModel appRealm = new RealmManager(session).getRealmByName("test");
+
+ // Update postalCode mapper to always read the value from LDAP
+ UserFederationMapperModel zipMapper = appRealm.getUserFederationMapperByName(ldapModel.getId(), "zipCodeMapper");
+ zipMapper.getConfig().put(UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "true");
+ appRealm.updateUserFederationMapper(zipMapper);
+
+ // Update lastName mapper to read the value from Keycloak DB
+ UserFederationMapperModel lastNameMapper = appRealm.getUserFederationMapperByName(ldapModel.getId(), "last name");
+ lastNameMapper.getConfig().put(UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false");
+ appRealm.updateUserFederationMapper(lastNameMapper);
+
+ // Verify that postalCode is read from LDAP now
+ UserModel user = session.users().getUserByUsername("johndirect", appRealm);
+ String postalCode = user.getFirstAttribute("postal_code");
+ Assert.assertEquals("12400", postalCode);
+
+ // Check user.getAttributes()
+ postalCode = user.getAttributes().get("postal_code").get(0);
+ Assert.assertEquals("12400", postalCode);
+
+ Assert.assertFalse(user.getAttributes().containsKey(UserModel.LAST_NAME));
+
+ // lastName is read from Keycloak DB now
+ String lastName = user.getLastName();
+ Assert.assertEquals("Direct", lastName);
+
+ } finally {
+ keycloakRule.stopSession(session, false);
+ }
+ }
+
+ @Test
public void testFullNameMapper() {
KeycloakSession session = keycloakRule.startSession();
UserFederationMapperModel firstNameMapper = null;
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java
index 540ae64..56c4a70 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java
@@ -1,5 +1,7 @@
package org.keycloak.testsuite.federation;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import org.junit.Assert;
@@ -7,7 +9,7 @@ import org.keycloak.federation.ldap.LDAPFederationProvider;
import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
import org.keycloak.federation.ldap.LDAPUtils;
import org.keycloak.federation.ldap.idm.model.LDAPObject;
-import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.federation.ldap.mappers.RoleLDAPFederationMapper;
import org.keycloak.federation.ldap.mappers.RoleLDAPFederationMapperFactory;
@@ -69,11 +71,11 @@ class FederationTestUtils {
}
@Override
- public String getAttribute(String name) {
+ public List<String> getAttribute(String name) {
if ("postal_code".equals(name)) {
- return postalCode;
+ return Arrays.asList(postalCode);
} else {
- return null;
+ return Collections.emptyList();
}
}
};
@@ -91,18 +93,19 @@ class FederationTestUtils {
Assert.assertEquals(expectedFirstName, user.getFirstName());
Assert.assertEquals(expectedLastName, user.getLastName());
Assert.assertEquals(expectedEmail, user.getEmail());
- Assert.assertEquals(expectedPostalCode, user.getAttribute("postal_code"));
+ Assert.assertEquals(expectedPostalCode, user.getFirstAttribute("postal_code"));
}
public static void addZipCodeLDAPMapper(RealmModel realm, UserFederationProviderModel providerModel) {
- addUserAttributeMapper(realm, providerModel, "zipCodeMapper", "postal_code", LDAPConstants.POSTAL_CODE);
+ addUserAttributeMapper(realm, providerModel, "zipCodeMapper", "postal_code", LDAPConstants.POSTAL_CODE);
}
public static void addUserAttributeMapper(RealmModel realm, UserFederationProviderModel providerModel, String mapperName, String userModelAttributeName, String ldapAttributeName) {
UserFederationMapperModel mapperModel = KeycloakModelUtils.createUserFederationMapperModel(mapperName, providerModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, userModelAttributeName,
UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, ldapAttributeName,
- UserAttributeLDAPFederationMapper.READ_ONLY, "false");
+ UserAttributeLDAPFederationMapper.READ_ONLY, "false",
+ UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false");
realm.addUserFederationMapper(mapperModel);
}
@@ -137,7 +140,7 @@ class FederationTestUtils {
public static void removeAllLDAPUsers(LDAPFederationProvider ldapProvider, RealmModel realm) {
LDAPIdentityStore ldapStore = ldapProvider.getLdapIdentityStore();
- LDAPIdentityQuery ldapQuery = LDAPUtils.createQueryForUserSearch(ldapProvider, realm);
+ LDAPQuery ldapQuery = LDAPUtils.createQueryForUserSearch(ldapProvider, realm);
List<LDAPObject> allUsers = ldapQuery.getResultList();
for (LDAPObject ldapUser : allUsers) {
@@ -148,7 +151,7 @@ class FederationTestUtils {
public static void removeAllLDAPRoles(KeycloakSession session, RealmModel appRealm, UserFederationProviderModel ldapModel, String mapperName) {
UserFederationMapperModel mapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), mapperName);
LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
- LDAPIdentityQuery roleQuery = new RoleLDAPFederationMapper().createRoleQuery(mapperModel, ldapProvider);
+ LDAPQuery roleQuery = new RoleLDAPFederationMapper().createRoleQuery(mapperModel, ldapProvider);
List<LDAPObject> ldapRoles = roleQuery.getResultList();
for (LDAPObject ldapRole : ldapRoles) {
ldapProvider.getLdapIdentityStore().remove(ldapRole);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPExampleServlet.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPExampleServlet.java
new file mode 100644
index 0000000..a629ddb
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPExampleServlet.java
@@ -0,0 +1,58 @@
+package org.keycloak.testsuite.federation;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.keycloak.KeycloakSecurityContext;
+import org.keycloak.representations.IDToken;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class LDAPExampleServlet extends HttpServlet {
+
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ KeycloakSecurityContext securityContext = (KeycloakSecurityContext) req.getAttribute(KeycloakSecurityContext.class.getName());
+ IDToken idToken = securityContext.getIdToken();
+
+ PrintWriter out = resp.getWriter();
+ out.println("<html><head><title>LDAP Portal</title></head><body>");
+ out.println("<table border><tr><th>Attribute name</th><th>Attribute values</th></tr>");
+
+ out.printf("<tr><td>%s</td><td>%s</td></tr>", "preferred_username", idToken.getPreferredUsername());
+ out.println();
+ out.printf("<tr><td>%s</td><td>%s</td></tr>", "name", idToken.getName());
+ out.println();
+ out.printf("<tr><td>%s</td><td>%s</td></tr>", "email", idToken.getEmail());
+ out.println();
+
+ for (Map.Entry<String, Object> claim : idToken.getOtherClaims().entrySet()) {
+ Object value = claim.getValue();
+
+ if (value instanceof List) {
+ List<String> asList = (List<String>) value;
+ StringBuilder result = new StringBuilder();
+ for (String item : asList) {
+ result.append(item + "<br>");
+ }
+ value = result.toString();
+ }
+
+ out.printf("<tr><td>%s</td><td>%s</td></tr>", claim.getKey(), value);
+ out.println();
+ }
+
+ out.println("</table></body></html>");
+ out.flush();
+ }
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPMultipleAttributesTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPMultipleAttributesTest.java
new file mode 100644
index 0000000..e1e9aff
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPMultipleAttributesTest.java
@@ -0,0 +1,193 @@
+package org.keycloak.testsuite.federation;
+
+import java.net.URL;
+import java.util.List;
+import java.util.Map;
+
+import javax.ws.rs.core.UriBuilder;
+
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.FixMethodOrder;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+import org.junit.runners.MethodSorters;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.federation.ldap.LDAPFederationProvider;
+import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserFederationProvider;
+import org.keycloak.models.UserFederationProviderModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
+import org.keycloak.protocol.oidc.mappers.UserAttributeMapper;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.testsuite.OAuthClient;
+import org.keycloak.testsuite.adapter.AdapterTest;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.testsuite.rule.LDAPRule;
+import org.keycloak.testsuite.rule.WebResource;
+import org.keycloak.testsuite.rule.WebRule;
+import org.openqa.selenium.WebDriver;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class LDAPMultipleAttributesTest {
+
+ protected String APP_SERVER_BASE_URL = "http://localhost:8081";
+ protected String LOGIN_URL = OIDCLoginProtocolService.authUrl(UriBuilder.fromUri(APP_SERVER_BASE_URL + "/auth")).build("test").toString();
+
+ private static LDAPRule ldapRule = new LDAPRule();
+
+ private static UserFederationProviderModel ldapModel = null;
+
+ private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
+
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ Map<String,String> ldapConfig = ldapRule.getConfig();
+ ldapConfig.put(LDAPConstants.EDIT_MODE, UserFederationProvider.EditMode.WRITABLE.toString());
+
+ ldapModel = appRealm.addUserFederationProvider(LDAPFederationProviderFactory.PROVIDER_NAME, ldapConfig, 0, "test-ldap", -1, -1, 0);
+ FederationTestUtils.addZipCodeLDAPMapper(appRealm, ldapModel);
+ FederationTestUtils.addUserAttributeMapper(appRealm, ldapModel, "streetMapper", "street", LDAPConstants.STREET);
+
+ // Create ldap-portal client
+ ClientModel ldapClient = appRealm.addClient("ldap-portal");
+ ldapClient.addRedirectUri("/ldap-portal");
+ ldapClient.addRedirectUri("/ldap-portal/*");
+ ldapClient.setManagementUrl("/ldap-portal");
+ ldapClient.addProtocolMapper(UserAttributeMapper.createClaimMapper("postalCode", "postal_code", "postal_code", "String", true, "", true, true, true));
+ ldapClient.addProtocolMapper(UserAttributeMapper.createClaimMapper("street", "street", "street", "String", true, "", true, true, false));
+ ldapClient.addScopeMapping(appRealm.getRole("user"));
+ ldapClient.setSecret("password");
+
+ // Deploy ldap-portal client
+ URL url = getClass().getResource("/ldap/ldap-app-keycloak.json");
+ keycloakRule.createApplicationDeployment()
+ .name("ldap-portal").contextPath("/ldap-portal")
+ .servletClass(LDAPExampleServlet.class).adapterConfigPath(url.getPath())
+ .role("user").deployApplication();
+ }
+ });
+
+ @ClassRule
+ public static TestRule chain = RuleChain
+ .outerRule(ldapRule)
+ .around(keycloakRule);
+
+ @Rule
+ public WebRule webRule = new WebRule(this);
+
+ @WebResource
+ protected WebDriver driver;
+
+ @WebResource
+ protected OAuthClient oauth;
+
+ @WebResource
+ protected LoginPage loginPage;
+
+ @Test
+ public void testModel() {
+ KeycloakSession session = keycloakRule.startSession();
+ try {
+ RealmModel appRealm = session.realms().getRealmByName("test");
+ LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
+
+ FederationTestUtils.assertUserImported(session.users(), appRealm, "jbrown", "James", "Brown", "jbrown@keycloak.org", "88441");
+
+ UserModel user = session.users().getUserByUsername("bwilson", appRealm);
+ Assert.assertEquals("bwilson@keycloak.org", user.getEmail());
+ Assert.assertEquals("Bruce", user.getFirstName());
+
+ // There are 2 lastnames in ldif
+ Assert.assertTrue("Wilson".equals(user.getLastName()) || "Schneider".equals(user.getLastName()));
+
+ // Actually there are 2 postalCodes
+ List<String> postalCodes = user.getAttribute("postal_code");
+ assertPostalCodes(postalCodes, "88441", "77332");
+
+ postalCodes.remove("77332");
+ user.setAttribute("postal_code", postalCodes);
+
+ } finally {
+ keycloakRule.stopSession(session, true);
+ }
+
+ session = keycloakRule.startSession();
+ try {
+ RealmModel appRealm = session.realms().getRealmByName("test");
+ UserModel user = session.users().getUserByUsername("bwilson", appRealm);
+ List<String> postalCodes = user.getAttribute("postal_code");
+ assertPostalCodes(postalCodes, "88441");
+
+ postalCodes.add("77332");
+ user.setAttribute("postal_code", postalCodes);
+ assertPostalCodes(user.getAttribute("postal_code"), "88441", "77332");
+ } finally {
+ keycloakRule.stopSession(session, true);
+ }
+ }
+
+ private void assertPostalCodes(List<String> postalCodes, String... expectedPostalCodes) {
+ if (expectedPostalCodes == null && postalCodes.isEmpty()) {
+ return;
+ }
+
+
+ Assert.assertEquals(expectedPostalCodes.length, postalCodes.size());
+ for (String expected : expectedPostalCodes) {
+ if (!postalCodes.contains(expected)) {
+ Assert.fail("postalCode '" + expected + "' not in postalCodes: " + postalCodes);
+ }
+ }
+ }
+
+ @Test
+ public void ldapPortalEndToEndTest() {
+ // Login as bwilson
+ driver.navigate().to(APP_SERVER_BASE_URL + "/ldap-portal");
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+ loginPage.login("bwilson", "password");
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(APP_SERVER_BASE_URL + "/ldap-portal"));
+ String pageSource = driver.getPageSource();
+ System.out.println(pageSource);
+ Assert.assertTrue(pageSource.contains("bwilson") && pageSource.contains("Bruce"));
+ Assert.assertTrue(pageSource.contains("street") && pageSource.contains("Elm 5"));
+ Assert.assertTrue(pageSource.contains("postal_code") && pageSource.contains("88441") && pageSource.contains("77332"));
+
+ // Logout
+ String logoutUri = OIDCLoginProtocolService.logoutUrl(UriBuilder.fromUri(APP_SERVER_BASE_URL + "/auth"))
+ .queryParam(OAuth2Constants.REDIRECT_URI, APP_SERVER_BASE_URL + "/ldap-portal").build("test").toString();
+ driver.navigate().to(logoutUri);
+
+ // Login as jbrown
+ driver.navigate().to(APP_SERVER_BASE_URL + "/ldap-portal");
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+ loginPage.login("jbrown", "password");
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(APP_SERVER_BASE_URL + "/ldap-portal"));
+ pageSource = driver.getPageSource();
+ System.out.println(pageSource);
+ Assert.assertTrue(pageSource.contains("jbrown") && pageSource.contains("James Brown"));
+ Assert.assertFalse(pageSource.contains("street"));
+ Assert.assertTrue(pageSource.contains("postal_code") && pageSource.contains("88441"));
+ Assert.assertFalse(pageSource.contains("77332"));
+
+ // Logout
+ driver.navigate().to(logoutUri);
+ }
+
+
+
+}
+
+
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java
index f8a6944..4802105 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java
@@ -16,7 +16,7 @@ import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
import org.keycloak.federation.ldap.idm.model.LDAPObject;
import org.keycloak.federation.ldap.idm.query.Condition;
import org.keycloak.federation.ldap.idm.query.QueryParameter;
-import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
import org.keycloak.federation.ldap.mappers.RoleLDAPFederationMapper;
import org.keycloak.models.AccountRoles;
@@ -329,7 +329,7 @@ public class LDAPRoleMappingsTest {
}
private void deleteRoleMappingsInLDAP(UserFederationMapperModel roleMapperModel, RoleLDAPFederationMapper roleMapper, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, String roleName) {
- LDAPIdentityQuery ldapQuery = roleMapper.createRoleQuery(roleMapperModel, ldapProvider);
+ LDAPQuery ldapQuery = roleMapper.createRoleQuery(roleMapperModel, ldapProvider);
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
Condition roleNameCondition = conditionsBuilder.equal(new QueryParameter(LDAPConstants.CN), roleName);
ldapQuery.where(roleNameCondition);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java
index bab44c1..b463bcd 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java
@@ -126,8 +126,8 @@ public class SyncProvidersTest {
FederationTestUtils.addLDAPUser(ldapFedProvider, testRealm, "user6", "User6FN", "User6LN", "user6@email.org", "126");
LDAPObject ldapUser5 = ldapFedProvider.loadLDAPUserByUsername(testRealm, "user5");
// NOTE: Changing LDAP attributes directly here
- ldapUser5.setAttribute(LDAPConstants.EMAIL, "user5Updated@email.org");
- ldapUser5.setAttribute(LDAPConstants.POSTAL_CODE, "521");
+ ldapUser5.setSingleAttribute(LDAPConstants.EMAIL, "user5Updated@email.org");
+ ldapUser5.setSingleAttribute(LDAPConstants.POSTAL_CODE, "521");
ldapFedProvider.getLdapIdentityStore().update(ldapUser5);
// Assert still old users in local provider
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/EmailTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/EmailTest.java
index b8e2453..af6750f 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/EmailTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/EmailTest.java
@@ -54,7 +54,7 @@ public class EmailTest {
UserModel user = manager.getSession().users().addUser(appRealm, "login-test");
user.setEmail("login@test.com");
user.setEnabled(true);
- user.setAttribute(UserModel.LOCALE, "de");
+ user.setSingleAttribute(UserModel.LOCALE, "de");
UserCredentialModel creds = new UserCredentialModel();
creds.setType(CredentialRepresentation.PASSWORD);
@@ -91,7 +91,7 @@ public class EmailTest {
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
- manager.getSession().users().getUserByUsername("login-test", appRealm).setAttribute(UserModel.LOCALE, "en");
+ manager.getSession().users().getUserByUsername("login-test", appRealm).setSingleAttribute(UserModel.LOCALE, "en");
}
});
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java
index 8640d8e..57b4ce9 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java
@@ -25,7 +25,6 @@ import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -159,7 +158,7 @@ public class AdapterTest extends AbstractModelTest {
test1CreateRealm();
UserModel user = realmManager.getSession().users().addUser(realmModel, "bburke");
- user.setAttribute("attr1", "val1");
+ user.setSingleAttribute("attr1", "val1");
user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
RoleModel testRole = realmModel.addRole("test");
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
index 276698d..e28d2b3 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
@@ -140,6 +140,22 @@ public class ImportTest extends AbstractModelTest {
Assert.assertEquals(1, appRoles.size());
Assert.assertEquals("app-admin", appRoles.iterator().next().getName());
+ // Test attributes
+ Map<String, List<String>> attrs = wburke.getAttributes();
+ Assert.assertEquals(1, attrs.size());
+ List<String> attrVals = attrs.get("email");
+ Assert.assertEquals(1, attrVals.size());
+ Assert.assertEquals("bburke@redhat.com", attrVals.get(0));
+
+ attrs = admin.getAttributes();
+ Assert.assertEquals(2, attrs.size());
+ attrVals = attrs.get("key1");
+ Assert.assertEquals(1, attrVals.size());
+ Assert.assertEquals("val1", attrVals.get(0));
+ attrVals = attrs.get("key2");
+ Assert.assertEquals(2, attrVals.size());
+ Assert.assertTrue(attrVals.contains("val21") && attrVals.contains("val22"));
+
// Test client
ClientModel oauthClient = realm.getClientByClientId("oauthclient");
Assert.assertEquals("clientpassword", oauthClient.getSecret());
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java
index d9e1aa5..ff74293 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java
@@ -8,6 +8,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@@ -137,6 +138,61 @@ public class UserModelTest extends AbstractModelTest {
Assert.assertTrue(user.getRequiredActions().isEmpty());
}
+ @Test
+ public void testUserMultipleAttributes() throws Exception {
+ RealmModel realm = realmManager.createRealm("original");
+ UserModel user = session.users().addUser(realm, "user");
+
+ user.setSingleAttribute("key1", "value1");
+ List<String> attrVals = new ArrayList<>(Arrays.asList( "val21", "val22" ));
+ user.setAttribute("key2", attrVals);
+
+ commit();
+
+ // Test read attributes
+ realm = realmManager.getRealmByName("original");
+ user = session.users().getUserByUsername("user", realm);
+
+ attrVals = user.getAttribute("key1");
+ Assert.assertEquals(1, attrVals.size());
+ Assert.assertEquals("value1", attrVals.get(0));
+ Assert.assertEquals("value1", user.getFirstAttribute("key1"));
+
+ attrVals = user.getAttribute("key2");
+ Assert.assertEquals(2, attrVals.size());
+ Assert.assertTrue(attrVals.contains("val21"));
+ Assert.assertTrue(attrVals.contains("val22"));
+
+ attrVals = user.getAttribute("key3");
+ Assert.assertTrue(attrVals.isEmpty());
+ Assert.assertNull(user.getFirstAttribute("key3"));
+
+ Map<String, List<String>> allAttrVals = user.getAttributes();
+ Assert.assertEquals(2, allAttrVals.size());
+ Assert.assertEquals(allAttrVals.get("key1"), user.getAttribute("key1"));
+ Assert.assertEquals(allAttrVals.get("key2"), user.getAttribute("key2"));
+
+ // Test searching
+ Map<String, String> attributes = new HashMap<String, String>();
+ attributes.put("key2", "val22");
+ List<UserModel> users = session.users().searchForUserByAttributes(attributes, realm);
+ Assert.assertEquals(1, users.size());
+ Assert.assertEquals(users.get(0), user);
+
+ // Test remove and rewrite attribute
+ user.removeAttribute("key1");
+ user.setSingleAttribute("key2", "val23");
+
+ commit();
+
+ realm = realmManager.getRealmByName("original");
+ user = session.users().getUserByUsername("user", realm);
+ Assert.assertNull(user.getFirstAttribute("key1"));
+ attrVals = user.getAttribute("key2");
+ Assert.assertEquals(1, attrVals.size());
+ Assert.assertEquals("val23", attrVals.get(0));
+ }
+
public static void assertEquals(UserModel expected, UserModel actual) {
Assert.assertEquals(expected.getUsername(), actual.getUsername());
Assert.assertEquals(expected.getFirstName(), actual.getFirstName());
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
index 0920be5..89d353b 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
@@ -69,7 +69,9 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.io.IOException;
import java.net.URI;
+import java.util.Arrays;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import static org.hamcrest.Matchers.*;
@@ -616,19 +618,22 @@ public class AccessTokenTest {
KeycloakSession session = keycloakRule.startSession();
RealmModel realm = session.realms().getRealmByName("test");
UserModel user = session.users().getUserByUsername("test-user@localhost", realm);
- user.setAttribute("street", "5 Yawkey Way");
- user.setAttribute("locality", "Boston");
- user.setAttribute("region", "MA");
- user.setAttribute("postal_code", "02115");
- user.setAttribute("country", "USA");
- user.setAttribute("phone", "617-777-6666");
+ user.setSingleAttribute("street", "5 Yawkey Way");
+ user.setSingleAttribute("locality", "Boston");
+ user.setSingleAttribute("region", "MA");
+ user.setSingleAttribute("postal_code", "02115");
+ user.setSingleAttribute("country", "USA");
+ user.setSingleAttribute("phone", "617-777-6666");
+ List<String> departments = Arrays.asList("finance", "development");
+ user.setAttribute("departments", departments);
ClientModel app = realm.getClientByClientId("test-app");
ProtocolMapperModel mapper = AddressMapper.createAddressMapper(true, true);
app.addProtocolMapper(mapper);
app.addProtocolMapper(HardcodedClaim.create("hard", "hard", "coded", "String", false, null, true, true));
app.addProtocolMapper(HardcodedClaim.create("hard-nested", "nested.hard", "coded-nested", "String", false, null, true, true));
- app.addProtocolMapper(UserAttributeMapper.createClaimMapper("custom phone", "phone", "home_phone", "String", true, "", true, true));
- app.addProtocolMapper(UserAttributeMapper.createClaimMapper("nested phone", "phone", "home.phone", "String", true, "", true, true));
+ app.addProtocolMapper(UserAttributeMapper.createClaimMapper("custom phone", "phone", "home_phone", "String", true, "", true, true, false));
+ app.addProtocolMapper(UserAttributeMapper.createClaimMapper("nested phone", "phone", "home.phone", "String", true, "", true, true, false));
+ app.addProtocolMapper(UserAttributeMapper.createClaimMapper("departments", "departments", "department", "String", true, "", true, true, true));
app.addProtocolMapper(HardcodedRole.create("hard-realm", "hardcoded"));
app.addProtocolMapper(HardcodedRole.create("hard-app", "app.hardcoded"));
app.addProtocolMapper(RoleNameMapper.create("rename-app-role", "test-app.customer-user", "realm-user"));
@@ -655,6 +660,9 @@ public class AccessTokenTest {
Assert.assertEquals("coded-nested", nested.get("hard"));
nested = (Map)idToken.getOtherClaims().get("home");
Assert.assertEquals("617-777-6666", nested.get("phone"));
+ List<String> departments = (List<String>)idToken.getOtherClaims().get("department");
+ Assert.assertEquals(2, departments.size());
+ Assert.assertTrue(departments.contains("finance") && departments.contains("development"));
AccessToken accessToken = getAccessToken(tokenResponse);
Assert.assertEquals(accessToken.getName(), "Tom Brady");
@@ -671,6 +679,9 @@ public class AccessTokenTest {
Assert.assertEquals("coded-nested", nested.get("hard"));
nested = (Map)accessToken.getOtherClaims().get("home");
Assert.assertEquals("617-777-6666", nested.get("phone"));
+ departments = (List<String>)idToken.getOtherClaims().get("department");
+ Assert.assertEquals(2, departments.size());
+ Assert.assertTrue(departments.contains("finance") && departments.contains("development"));
Assert.assertTrue(accessToken.getRealmAccess().getRoles().contains("hardcoded"));
Assert.assertTrue(accessToken.getRealmAccess().getRoles().contains("realm-user"));
Assert.assertFalse(accessToken.getResourceAccess("test-app").getRoles().contains("customer-user"));
diff --git a/testsuite/integration/src/test/resources/ldap/ldap-app-keycloak.json b/testsuite/integration/src/test/resources/ldap/ldap-app-keycloak.json
new file mode 100644
index 0000000..e082558
--- /dev/null
+++ b/testsuite/integration/src/test/resources/ldap/ldap-app-keycloak.json
@@ -0,0 +1,10 @@
+{
+ "realm": "test",
+ "resource": "ldap-portal",
+ "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "auth-server-url": "http://localhost:8081/auth",
+ "ssl-required" : "external",
+ "credentials": {
+ "secret": "password"
+ }
+}
\ No newline at end of file
diff --git a/testsuite/integration/src/test/resources/ldap/users.ldif b/testsuite/integration/src/test/resources/ldap/users.ldif
index 9450126..4d6d87e 100644
--- a/testsuite/integration/src/test/resources/ldap/users.ldif
+++ b/testsuite/integration/src/test/resources/ldap/users.ldif
@@ -19,4 +19,29 @@ objectclass: top
objectclass: organizationalUnit
ou: FinanceRoles
+dn: uid=jbrown,ou=People,dc=keycloak,dc=org
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+objectclass: inetOrgPerson
+uid: jbrown
+cn: James
+sn: Brown
+mail: jbrown@keycloak.org
+postalCode: 88441
+userPassword: password
+dn: uid=bwilson,ou=People,dc=keycloak,dc=org
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+objectclass: inetOrgPerson
+uid: bwilson
+cn: Bruce
+sn: Wilson
+sn: Schneider
+mail: bwilson@keycloak.org
+postalCode: 88441
+postalCode: 77332
+street: Elm 5
+userPassword: password
diff --git a/testsuite/integration/src/test/resources/model/testrealm.json b/testsuite/integration/src/test/resources/model/testrealm.json
index 0e41313..00336c8 100755
--- a/testsuite/integration/src/test/resources/model/testrealm.json
+++ b/testsuite/integration/src/test/resources/model/testrealm.json
@@ -82,6 +82,15 @@
{
"username": "admin",
"enabled": true,
+ "attributes": {
+ "key1": [
+ "val1"
+ ],
+ "key2": [
+ "val21",
+ "val22"
+ ]
+ },
"credentials": [
{
"type": "password",