Merge pull request #1417 from mposolda/master

KEYCLOAK-1487 multivalued attributes support
This commit is contained in:
Marek Posolda 2015-06-30 14:56:45 +02:00
commit 2d7f232c3d
62 changed files with 1056 additions and 364 deletions

View file

@ -110,7 +110,7 @@ public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityPr
String value = getJsonValue(mapperModel, context); String value = getJsonValue(mapperModel, context);
if (value != null) { if (value != null) {
user.setAttribute(attribute, value); user.setSingleAttribute(attribute, value);
} }
} }

View file

@ -3,16 +3,14 @@ package org.keycloak.broker.oidc.mappers;
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory; import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory; import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
import org.keycloak.broker.provider.BrokeredIdentityContext; 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.IdentityProviderMapperModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.List; import java.util.List;
/** /**
@ -76,7 +74,7 @@ public class UserAttributeMapper extends AbstractClaimMapper {
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE); String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
Object value = getClaimValue(mapperModel, context); Object value = getClaimValue(mapperModel, context);
if (value != null) { 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) { public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE); String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
Object value = getClaimValue(mapperModel, context); Object value = getClaimValue(mapperModel, context);
String current = user.getAttribute(attribute); String current = user.getFirstAttribute(attribute);
if (value != null && !value.equals(current)) { if (value != null && !value.equals(current)) {
user.setAttribute(attribute, value.toString()); user.setSingleAttribute(attribute, value.toString());
} else if (value == null) { } else if (value == null) {
user.removeAttribute(attribute); user.removeAttribute(attribute);
} }

View file

@ -2,17 +2,14 @@ package org.keycloak.broker.saml.mappers;
import org.keycloak.broker.provider.AbstractIdentityProviderMapper; import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.saml.SAMLEndpoint; import org.keycloak.broker.saml.SAMLEndpoint;
import org.keycloak.broker.saml.SAMLIdentityProviderFactory; import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
import org.keycloak.dom.saml.v2.assertion.AssertionType; import org.keycloak.dom.saml.v2.assertion.AssertionType;
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
import org.keycloak.dom.saml.v2.assertion.AttributeType; import org.keycloak.dom.saml.v2.assertion.AttributeType;
import org.keycloak.models.ClientModel;
import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
@ -87,7 +84,7 @@ public class UserAttributeMapper extends AbstractIdentityProviderMapper {
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE); String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
Object value = getAttribute(mapperModel, context); Object value = getAttribute(mapperModel, context);
if (value != null) { 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) { public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE); String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
Object value = getAttribute(mapperModel, context); Object value = getAttribute(mapperModel, context);
String current = user.getAttribute(attribute); String current = user.getFirstAttribute(attribute);
if (value != null && !value.equals(current)) { if (value != null && !value.equals(current)) {
user.setAttribute(attribute, value.toString()); user.setSingleAttribute(attribute, value.toString());
} else if (value == null) { } else if (value == null) {
user.removeAttribute(attribute); user.removeAttribute(attribute);
} }

View file

@ -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"> <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"> <changeSet author="bburke@redhat.com" id="1.4.0">
<delete tableName="CLIENT_SESSION_AUTH_STATUS"/> <delete tableName="CLIENT_SESSION_AUTH_STATUS"/>
<delete tableName="CLIENT_SESSION_ROLE"/>
<delete tableName="CLIENT_SESSION_PROT_MAPPER"/> <delete tableName="CLIENT_SESSION_PROT_MAPPER"/>
<delete tableName="CLIENT_SESSION_NOTE"/> <delete tableName="CLIENT_SESSION_NOTE"/>
<delete tableName="CLIENT_SESSION"/> <delete tableName="CLIENT_SESSION"/>
@ -22,6 +23,12 @@
<constraints nullable="true"/> <constraints nullable="true"/>
</column> </column>
</addColumn> </addColumn>
<addColumn tableName="USER_ATTRIBUTE">
<column name="ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</addColumn>
<dropColumn tableName="AUTHENTICATOR" columnName="PROVIDER_ID"/> <dropColumn tableName="AUTHENTICATOR" columnName="PROVIDER_ID"/>
<renameTable oldTableName="AUTHENTICATOR_CONFIG" newTableName="AUTHENTICATOR_CONFIG_ENTRY"/> <renameTable oldTableName="AUTHENTICATOR_CONFIG" newTableName="AUTHENTICATOR_CONFIG_ENTRY"/>
<renameTable oldTableName="AUTHENTICATOR" newTableName="AUTHENTICATOR_CONFIG"/> <renameTable oldTableName="AUTHENTICATOR" newTableName="AUTHENTICATOR_CONFIG"/>
@ -110,6 +117,8 @@
</column> </column>
</createTable> </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="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="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"/> <addPrimaryKey columnNames="CLIENT_SESSION, NAME" constraintName="CONSTR_CL_USR_SES_NOTE" tableName="CLIENT_USER_SESSION_NOTE"/>

View file

@ -1,5 +1,6 @@
package org.keycloak.connections.mongo.api.types; package org.keycloak.connections.mongo.api.types;
import java.lang.reflect.Type;
import java.util.List; import java.util.List;
/** /**
@ -14,9 +15,9 @@ public class MapperContext<T, S> {
private final Class<? extends S> expectedReturnType; 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 // 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.objectToConvert = objectToConvert;
this.expectedReturnType = expectedReturnType; this.expectedReturnType = expectedReturnType;
this.genericTypes = genericTypes; this.genericTypes = genericTypes;
@ -30,7 +31,7 @@ public class MapperContext<T, S> {
return expectedReturnType; return expectedReturnType;
} }
public List<Class<?>> getGenericTypes() { public List<Type> getGenericTypes() {
return genericTypes; return genericTypes;
} }
} }

View file

@ -24,7 +24,7 @@ public class BasicDBListMapper implements Mapper<BasicDBList, List> {
public List convertObject(MapperContext<BasicDBList, List> context) { public List convertObject(MapperContext<BasicDBList, List> context) {
BasicDBList dbList = context.getObjectToConvert(); BasicDBList dbList = context.getObjectToConvert();
ArrayList<Object> appObjects = new ArrayList<Object>(); ArrayList<Object> appObjects = new ArrayList<Object>();
Class<?> expectedListElementType = context.getGenericTypes().get(0); Class<?> expectedListElementType = (Class<?>) context.getGenericTypes().get(0);
for (Object dbObject : dbList) { for (Object dbObject : dbList) {
MapperContext<Object, Object> newContext = new MapperContext<Object, Object>(dbObject, expectedListElementType, null); MapperContext<Object, Object> newContext = new MapperContext<Object, Object>(dbObject, expectedListElementType, null);

View file

@ -23,7 +23,7 @@ public class BasicDBListToSetMapper implements Mapper<BasicDBList, Set> {
public Set convertObject(MapperContext<BasicDBList, Set> context) { public Set convertObject(MapperContext<BasicDBList, Set> context) {
BasicDBList dbList = context.getObjectToConvert(); BasicDBList dbList = context.getObjectToConvert();
Set<Object> appObjects = new HashSet<Object>(); Set<Object> appObjects = new HashSet<Object>();
Class<?> expectedListElementType = context.getGenericTypes().get(0); Class<?> expectedListElementType = (Class<?>) context.getGenericTypes().get(0);
for (Object dbObject : dbList) { for (Object dbObject : dbList) {
MapperContext<Object, Object> newContext = new MapperContext<Object, Object>(dbObject, expectedListElementType, null); MapperContext<Object, Object> newContext = new MapperContext<Object, Object>(dbObject, expectedListElementType, null);

View file

@ -14,6 +14,7 @@ import org.keycloak.util.reflections.Types;
import java.lang.reflect.ParameterizedType; import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
/** /**
@ -87,10 +88,14 @@ public class BasicDBObjectMapper<S> implements Mapper<BasicDBObject, S> {
ParameterizedType parameterized = (ParameterizedType) type; ParameterizedType parameterized = (ParameterizedType) type;
Type[] genericTypeArguments = parameterized.getActualTypeArguments(); Type[] genericTypeArguments = parameterized.getActualTypeArguments();
List<Class<?>> genericTypes = new ArrayList<Class<?>>(); List<Type> genericTypes = Arrays.asList(genericTypeArguments);
for (Type genericType : genericTypeArguments) { /*for (Type genericType : genericTypeArguments) {
genericTypes.add((Class<?>)genericType); if (genericType instanceof Class) {
} genericTypes.add((Class<?>) genericType);
} else {
System.out.println("foo");
}
}*/
Class<?> expectedReturnType = (Class<?>)parameterized.getRawType(); Class<?> expectedReturnType = (Class<?>)parameterized.getRawType();
context = new MapperContext<Object, Object>(valueFromDB, expectedReturnType, genericTypes); context = new MapperContext<Object, Object>(valueFromDB, expectedReturnType, genericTypes);

View file

@ -1,10 +1,14 @@
package org.keycloak.representations.idm; package org.keycloak.representations.idm;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; 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> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
@ -21,7 +25,9 @@ public class UserRepresentation {
protected String lastName; protected String lastName;
protected String email; protected String email;
protected String federationLink; 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<CredentialRepresentation> credentials;
protected List<String> requiredActions; protected List<String> requiredActions;
protected List<FederatedIdentityRepresentation> federatedIdentities; protected List<FederatedIdentityRepresentation> federatedIdentities;
@ -106,17 +112,23 @@ public class UserRepresentation {
this.emailVerified = emailVerified; this.emailVerified = emailVerified;
} }
public Map<String, String> getAttributes() { public Map<String, Object> getAttributes() {
return attributes; 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; this.attributes = attributes;
} }
public UserRepresentation attribute(String name, String value) { public UserRepresentation singleAttribute(String name, String value) {
if (this.attributes == null) attributes = new HashMap<String, String>(); if (this.attributes == null) attributes = new HashMap<>();
attributes.put(name, value); attributes.put(name, Arrays.asList(value));
return this; return this;
} }

View file

@ -110,7 +110,7 @@ public class KerberosFederationProvider implements UserFederationProvider {
// KerberosUsernamePasswordAuthenticator.isUserAvailable is an overhead, so avoid it for now // KerberosUsernamePasswordAuthenticator.isUserAvailable is an overhead, so avoid it for now
String kerberosPrincipal = local.getUsername() + "@" + kerberosConfig.getKerberosRealm(); String kerberosPrincipal = local.getUsername() + "@" + kerberosConfig.getKerberosRealm();
return kerberosPrincipal.equals(local.getAttribute(KERBEROS_PRINCIPAL)); return kerberosPrincipal.equals(local.getFirstAttribute(KERBEROS_PRINCIPAL));
} }
@Override @Override
@ -229,7 +229,7 @@ public class KerberosFederationProvider implements UserFederationProvider {
return proxied; return proxied;
} else { } else {
logger.warn("User with username " + username + " already exists and is linked to provider [" + model.getDisplayName() + 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"); logger.warn("Will re-create user");
session.userStorage().removeUser(realm, user); session.userStorage().removeUser(realm, user);
} }
@ -249,7 +249,7 @@ public class KerberosFederationProvider implements UserFederationProvider {
user.setEnabled(true); user.setEnabled(true);
user.setEmail(email); user.setEmail(email);
user.setFederationLink(model.getId()); user.setFederationLink(model.getId());
user.setAttribute(KERBEROS_PRINCIPAL, username + "@" + kerberosConfig.getKerberosRealm()); user.setSingleAttribute(KERBEROS_PRINCIPAL, username + "@" + kerberosConfig.getKerberosRealm());
if (kerberosConfig.isUpdateProfileFirstLogin()) { if (kerberosConfig.isUpdateProfileFirstLogin()) {
user.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE); user.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE);

View file

@ -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.model.LDAPObject;
import org.keycloak.federation.ldap.idm.query.Condition; import org.keycloak.federation.ldap.idm.query.Condition;
import org.keycloak.federation.ldap.idm.query.QueryParameter; 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.query.internal.LDAPQueryConditionsBuilder;
import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore; import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.federation.ldap.kerberos.LDAPProviderKerberosConfig; import org.keycloak.federation.ldap.kerberos.LDAPProviderKerberosConfig;
@ -51,7 +51,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
protected EditMode editMode; protected EditMode editMode;
protected LDAPProviderKerberosConfig kerberosConfig; 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) { public LDAPFederationProvider(LDAPFederationProviderFactory factory, KeycloakSession session, UserFederationProviderModel model, LDAPIdentityStore ldapIdentityStore) {
this.factory = factory; 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"); if (!synchronizeRegistrations()) throw new IllegalStateException("Registration is not supported by this ldap server");
LDAPObject ldapObject = LDAPUtils.addUserToLDAP(this, realm, user); LDAPObject ldapObject = LDAPUtils.addUserToLDAP(this, realm, user);
user.setAttribute(LDAPConstants.LDAP_ID, ldapObject.getUuid()); user.setSingleAttribute(LDAPConstants.LDAP_ID, ldapObject.getUuid());
user.setAttribute(LDAPConstants.LDAP_ENTRY_DN, ldapObject.getDn().toString()); user.setSingleAttribute(LDAPConstants.LDAP_ENTRY_DN, ldapObject.getDn().toString());
return proxy(realm, user, ldapObject); return proxy(realm, user, ldapObject);
} }
@ -202,7 +202,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
} }
if (attributes.containsKey(FIRST_NAME) || attributes.containsKey(LAST_NAME)) { 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(); LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
// Mapper should replace parameter with correct LDAP mapped attributes // Mapper should replace parameter with correct LDAP mapped attributes
@ -229,10 +229,10 @@ public class LDAPFederationProvider implements UserFederationProvider {
if (ldapUser == null) { if (ldapUser == null) {
return null; return null;
} }
if (ldapUser.getUuid().equals(local.getAttribute(LDAPConstants.LDAP_ID))) { if (ldapUser.getUuid().equals(local.getFirstAttribute(LDAPConstants.LDAP_ID))) {
return ldapUser; return ldapUser;
} else { } else {
logger.warnf("LDAP User invalid. ID doesn't match. ID from LDAP [%s], LDAP 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; return null;
} }
} }
@ -271,8 +271,8 @@ public class LDAPFederationProvider implements UserFederationProvider {
String userDN = ldapUser.getDn().toString(); String userDN = ldapUser.getDn().toString();
imported.setFederationLink(model.getId()); imported.setFederationLink(model.getId());
imported.setAttribute(LDAPConstants.LDAP_ID, ldapUser.getUuid()); imported.setSingleAttribute(LDAPConstants.LDAP_ID, ldapUser.getUuid());
imported.setAttribute(LDAPConstants.LDAP_ENTRY_DN, userDN); 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(), 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); ldapUser.getUuid(), userDN);
@ -280,7 +280,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
} }
protected LDAPObject queryByEmail(RealmModel realm, String email) { protected LDAPObject queryByEmail(RealmModel realm, String email) {
LDAPIdentityQuery ldapQuery = LDAPUtils.createQueryForUserSearch(this, realm); LDAPQuery ldapQuery = LDAPUtils.createQueryForUserSearch(this, realm);
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder(); LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
// Mapper should replace "email" in parameter name with correct LDAP mapped attribute // Mapper should replace "email" in parameter name with correct LDAP mapped attribute
@ -395,7 +395,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
importUserFromLDAP(realm, ldapUser); importUserFromLDAP(realm, ldapUser);
syncResult.increaseAdded(); syncResult.increaseAdded();
} else { } 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 // Update keycloak user
Set<UserFederationMapperModel> federationMappers = realm.getUserFederationMappersByFederationProvider(model.getId()); Set<UserFederationMapperModel> federationMappers = realm.getUserFederationMappersByFederationProvider(model.getId());
@ -435,7 +435,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
return proxy(realm, user, ldapObject); return proxy(realm, user, ldapObject);
} else { } 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", 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"); logger.warn("Will re-create user");
session.userStorage().removeUser(realm, user); session.userStorage().removeUser(realm, user);
} }
@ -448,7 +448,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
} }
public LDAPObject loadLDAPUserByUsername(RealmModel realm, String username) { public LDAPObject loadLDAPUserByUsername(RealmModel realm, String username) {
LDAPIdentityQuery ldapQuery = LDAPUtils.createQueryForUserSearch(this, realm); LDAPQuery ldapQuery = LDAPUtils.createQueryForUserSearch(this, realm);
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder(); LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
String usernameMappedAttribute = this.ldapIdentityStore.getConfig().getUsernameLdapAttribute(); String usernameMappedAttribute = this.ldapIdentityStore.getConfig().getUsernameLdapAttribute();

View file

@ -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.model.LDAPObject;
import org.keycloak.federation.ldap.idm.query.Condition; import org.keycloak.federation.ldap.idm.query.Condition;
import org.keycloak.federation.ldap.idm.query.QueryParameter; 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.query.internal.LDAPQueryConditionsBuilder;
import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore; import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapper; import org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapper;
@ -184,7 +184,7 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
public UserFederationSyncResult syncAllUsers(KeycloakSessionFactory sessionFactory, final String realmId, final UserFederationProviderModel model) { 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()); 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); 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? // 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?
@ -203,7 +203,7 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
Condition modifyCondition = conditionsBuilder.greaterThanOrEqualTo(new QueryParameter(LDAPConstants.MODIFY_TIMESTAMP), lastSync); Condition modifyCondition = conditionsBuilder.greaterThanOrEqualTo(new QueryParameter(LDAPConstants.MODIFY_TIMESTAMP), lastSync);
Condition orCondition = conditionsBuilder.orCondition(createCondition, modifyCondition); Condition orCondition = conditionsBuilder.orCondition(createCondition, modifyCondition);
LDAPIdentityQuery userQuery = createQuery(sessionFactory, realmId, model); LDAPQuery userQuery = createQuery(sessionFactory, realmId, model);
userQuery.where(orCondition); userQuery.where(orCondition);
UserFederationSyncResult result = syncImpl(sessionFactory, userQuery, realmId, model); UserFederationSyncResult result = syncImpl(sessionFactory, userQuery, realmId, model);
@ -211,7 +211,7 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
return result; 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(); final UserFederationSyncResult syncResult = new UserFederationSyncResult();
@ -254,9 +254,9 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
return syncResult; 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 { class QueryHolder {
LDAPIdentityQuery query; LDAPQuery query;
} }
final QueryHolder queryHolder = new QueryHolder(); final QueryHolder queryHolder = new QueryHolder();

View file

@ -4,7 +4,7 @@ import java.util.Set;
import org.keycloak.federation.ldap.idm.model.LDAPDn; import org.keycloak.federation.ldap.idm.model.LDAPDn;
import org.keycloak.federation.ldap.idm.model.LDAPObject; 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.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.federation.ldap.mappers.LDAPFederationMapper; import org.keycloak.federation.ldap.mappers.LDAPFederationMapper;
import org.keycloak.models.ModelException; import org.keycloak.models.ModelException;
@ -44,8 +44,8 @@ public class LDAPUtils {
return ldapUser; return ldapUser;
} }
public static LDAPIdentityQuery createQueryForUserSearch(LDAPFederationProvider ldapProvider, RealmModel realm) { public static LDAPQuery createQueryForUserSearch(LDAPFederationProvider ldapProvider, RealmModel realm) {
LDAPIdentityQuery ldapQuery = new LDAPIdentityQuery(ldapProvider); LDAPQuery ldapQuery = new LDAPQuery(ldapProvider);
LDAPConfig config = ldapProvider.getLdapIdentityStore().getConfig(); LDAPConfig config = ldapProvider.getLdapIdentityStore().getConfig();
ldapQuery.setSearchScope(config.getSearchScope()); ldapQuery.setSearchScope(config.getSearchScope());
ldapQuery.setSearchDn(config.getUsersDn()); ldapQuery.setSearchDn(config.getUsersDn());
@ -60,7 +60,7 @@ public class LDAPUtils {
// ldapUser has filled attributes, but doesn't have filled dn. // ldapUser has filled attributes, but doesn't have filled dn.
private static void computeAndSetDn(LDAPConfig config, LDAPObject ldapUser) { private static void computeAndSetDn(LDAPConfig config, LDAPObject ldapUser) {
String rdnLdapAttrName = config.getRdnLdapAttribute(); String rdnLdapAttrName = config.getRdnLdapAttribute();
String rdnLdapAttrValue = ldapUser.getAttributeAsStringCaseInsensitive(rdnLdapAttrName); String rdnLdapAttrValue = ldapUser.getAttributeAsString(rdnLdapAttrName);
if (rdnLdapAttrValue == null) { if (rdnLdapAttrValue == null) {
throw new ModelException("RDN Attribute [" + rdnLdapAttrName + "] is not filled. Filled attributes: " + ldapUser.getAttributes()); throw new ModelException("RDN Attribute [" + rdnLdapAttrName + "] is not filled. Filled attributes: " + ldapUser.getAttributes());
} }
@ -72,6 +72,6 @@ public class LDAPUtils {
public static String getUsername(LDAPObject ldapUser, LDAPConfig config) { public static String getUsername(LDAPObject ldapUser, LDAPConfig config) {
String usernameAttr = config.getUsernameLdapAttribute(); String usernameAttr = config.getUsernameLdapAttribute();
return ldapUser.getAttributeAsStringCaseInsensitive(usernameAttr); return ldapUser.getAttributeAsString(usernameAttr);
} }
} }

View file

@ -2,9 +2,12 @@ package org.keycloak.federation.ldap.idm.model;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
@ -24,10 +27,10 @@ public class LDAPObject {
// NOTE: names of read-only attributes are lower-cased to avoid case sensitivity issues // NOTE: names of read-only attributes are lower-cased to avoid case sensitivity issues
private final List<String> readOnlyAttributeNames = new LinkedList<>(); private final List<String> readOnlyAttributeNames = new LinkedList<>();
private final Map<String, Object> attributes = new HashMap<>(); private final Map<String, Set<String>> attributes = new HashMap<>();
// Copy of "attributes" containing lower-cased keys // Copy of "attributes" containing lower-cased keys
private final Map<String, Object> lowerCasedAttributes = new HashMap<>(); private final Map<String, Set<String>> lowerCasedAttributes = new HashMap<>();
public String getUuid() { public String getUuid() {
@ -71,32 +74,37 @@ public class LDAPObject {
this.rdnAttributeName = rdnAttributeName; this.rdnAttributeName = rdnAttributeName;
} }
public void setAttribute(String attributeName, Object attributeValue) { public void setSingleAttribute(String attributeName, String attributeValue) {
Set<String> asSet = new LinkedHashSet<>();
asSet.add(attributeValue);
setAttribute(attributeName, asSet);
}
public void setAttribute(String attributeName, Set<String> attributeValue) {
attributes.put(attributeName, attributeValue); attributes.put(attributeName, attributeValue);
lowerCasedAttributes.put(attributeName.toLowerCase(), attributeValue); lowerCasedAttributes.put(attributeName.toLowerCase(), attributeValue);
} }
public Object getAttributeCaseInsensitive(String name) { // Case-insensitive
return lowerCasedAttributes.get(name.toLowerCase()); public String getAttributeAsString(String name) {
} Set<String> attrValue = lowerCasedAttributes.get(name.toLowerCase());
if (attrValue == null || attrValue.size() == 0) {
public String getAttributeAsStringCaseInsensitive(String name) { return null;
Object attrValue = lowerCasedAttributes.get(name.toLowerCase()); } else if (attrValue.size() > 1) {
if (attrValue != null && !(attrValue instanceof String)) { logger.warnf("Expected String but attribute '%s' has more values '%s' on object '%s' . Returning just first value", name, attrValue, dn);
logger.warnf("Expected String but attribute '%s' has value '%s' of type '%s' ", name, attrValue, attrValue.getClass().getName());
if (attrValue instanceof Collection) {
Collection<String> attrValues = (Collection<String>) attrValue;
attrValue = attrValues.iterator().next();
logger.warnf("Returning just first founded value '%s' from the collection", attrValue);
}
} }
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; return attributes;
} }

View file

@ -26,7 +26,7 @@ import static java.util.Collections.unmodifiableSet;
* *
* @author Shane Bryzak * @author Shane Bryzak
*/ */
public class LDAPIdentityQuery { public class LDAPQuery {
private final LDAPFederationProvider ldapFedProvider; private final LDAPFederationProvider ldapFedProvider;
@ -48,46 +48,46 @@ public class LDAPIdentityQuery {
private int searchScope = SearchControls.SUBTREE_SCOPE; private int searchScope = SearchControls.SUBTREE_SCOPE;
public LDAPIdentityQuery(LDAPFederationProvider ldapProvider) { public LDAPQuery(LDAPFederationProvider ldapProvider) {
this.ldapFedProvider = ldapProvider; this.ldapFedProvider = ldapProvider;
} }
public LDAPIdentityQuery where(Condition... condition) { public LDAPQuery where(Condition... condition) {
this.conditions.addAll(Arrays.asList(condition)); this.conditions.addAll(Arrays.asList(condition));
return this; return this;
} }
public LDAPIdentityQuery sortBy(Sort... sorts) { public LDAPQuery sortBy(Sort... sorts) {
this.ordering.addAll(Arrays.asList(sorts)); this.ordering.addAll(Arrays.asList(sorts));
return this; return this;
} }
public LDAPIdentityQuery setSearchDn(String searchDn) { public LDAPQuery setSearchDn(String searchDn) {
this.searchDn = searchDn; this.searchDn = searchDn;
return this; return this;
} }
public LDAPIdentityQuery addObjectClasses(Collection<String> objectClasses) { public LDAPQuery addObjectClasses(Collection<String> objectClasses) {
this.objectClasses.addAll(objectClasses); this.objectClasses.addAll(objectClasses);
return this; return this;
} }
public LDAPIdentityQuery addReturningLdapAttribute(String ldapAttributeName) { public LDAPQuery addReturningLdapAttribute(String ldapAttributeName) {
this.returningLdapAttributes.add(ldapAttributeName); this.returningLdapAttributes.add(ldapAttributeName);
return this; return this;
} }
public LDAPIdentityQuery addReturningReadOnlyLdapAttribute(String ldapAttributeName) { public LDAPQuery addReturningReadOnlyLdapAttribute(String ldapAttributeName) {
this.returningReadOnlyLdapAttributes.add(ldapAttributeName.toLowerCase()); this.returningReadOnlyLdapAttributes.add(ldapAttributeName.toLowerCase());
return this; return this;
} }
public LDAPIdentityQuery addMappers(Collection<UserFederationMapperModel> mappers) { public LDAPQuery addMappers(Collection<UserFederationMapperModel> mappers) {
this.mappers.addAll(mappers); this.mappers.addAll(mappers);
return this; return this;
} }
public LDAPIdentityQuery setSearchScope(int searchScope) { public LDAPQuery setSearchScope(int searchScope) {
this.searchScope = searchScope; this.searchScope = searchScope;
return this; return this;
} }
@ -170,17 +170,17 @@ public class LDAPIdentityQuery {
return ldapFedProvider.getLdapIdentityStore().countQueryResults(this); return ldapFedProvider.getLdapIdentityStore().countQueryResults(this);
} }
public LDAPIdentityQuery setOffset(int offset) { public LDAPQuery setOffset(int offset) {
this.offset = offset; this.offset = offset;
return this; return this;
} }
public LDAPIdentityQuery setLimit(int limit) { public LDAPQuery setLimit(int limit) {
this.limit = limit; this.limit = limit;
return this; return this;
} }
public LDAPIdentityQuery setPaginationContext(byte[] paginationContext) { public LDAPQuery setPaginationContext(byte[] paginationContext) {
this.paginationContext = paginationContext; this.paginationContext = paginationContext;
return this; return this;
} }

View file

@ -4,7 +4,7 @@ import java.util.List;
import org.keycloak.federation.ldap.LDAPConfig; import org.keycloak.federation.ldap.LDAPConfig;
import org.keycloak.federation.ldap.idm.model.LDAPObject; 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 * IdentityStore representation providing minimal SPI
@ -48,9 +48,9 @@ public interface IdentityStore {
// Identity query // Identity query
List<LDAPObject> fetchQueryResults(LDAPIdentityQuery LDAPIdentityQuery); List<LDAPObject> fetchQueryResults(LDAPQuery LDAPQuery);
int countQueryResults(LDAPIdentityQuery LDAPIdentityQuery); int countQueryResults(LDAPQuery LDAPQuery);
// // Relationship query // // Relationship query
// //

View file

@ -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.Condition;
import org.keycloak.federation.ldap.idm.query.QueryParameter; 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.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.EqualCondition;
import org.keycloak.federation.ldap.idm.query.internal.GreaterThanCondition; import org.keycloak.federation.ldap.idm.query.internal.GreaterThanCondition;
import org.keycloak.federation.ldap.idm.query.internal.InCondition; import org.keycloak.federation.ldap.idm.query.internal.InCondition;
@ -108,7 +108,7 @@ public class LDAPIdentityStore implements IdentityStore {
@Override @Override
public List<LDAPObject> fetchQueryResults(LDAPIdentityQuery identityQuery) { public List<LDAPObject> fetchQueryResults(LDAPQuery identityQuery) {
if (identityQuery.getSorting() != null && !identityQuery.getSorting().isEmpty()) { if (identityQuery.getSorting() != null && !identityQuery.getSorting().isEmpty()) {
throw new ModelException("LDAP Identity Store does not yet support sorted queries."); throw new ModelException("LDAP Identity Store does not yet support sorted queries.");
} }
@ -160,7 +160,7 @@ public class LDAPIdentityStore implements IdentityStore {
} }
@Override @Override
public int countQueryResults(LDAPIdentityQuery identityQuery) { public int countQueryResults(LDAPQuery identityQuery) {
int limit = identityQuery.getLimit(); int limit = identityQuery.getLimit();
int offset = identityQuery.getOffset(); int offset = identityQuery.getOffset();
@ -247,7 +247,7 @@ public class LDAPIdentityStore implements IdentityStore {
// ************ END CREDENTIALS AND USER SPECIFIC STUFF // ************ END CREDENTIALS AND USER SPECIFIC STUFF
protected StringBuilder createIdentityTypeSearchFilter(final LDAPIdentityQuery identityQuery) { protected StringBuilder createIdentityTypeSearchFilter(final LDAPQuery identityQuery) {
StringBuilder filter = new StringBuilder(); StringBuilder filter = new StringBuilder();
for (Condition condition : identityQuery.getConditions()) { for (Condition condition : identityQuery.getConditions()) {
@ -400,18 +400,14 @@ public class LDAPIdentityStore implements IdentityStore {
Set<String> attrValues = new LinkedHashSet<>(); Set<String> attrValues = new LinkedHashSet<>();
NamingEnumeration<?> enumm = ldapAttribute.getAll(); NamingEnumeration<?> enumm = ldapAttribute.getAll();
while (enumm.hasMoreElements()) { while (enumm.hasMoreElements()) {
String attrVal = enumm.next().toString(); String attrVal = enumm.next().toString().trim();
attrValues.add(attrVal); attrValues.add(attrVal);
} }
if (ldapAttributeName.equalsIgnoreCase(LDAPConstants.OBJECT_CLASS)) { if (ldapAttributeName.equalsIgnoreCase(LDAPConstants.OBJECT_CLASS)) {
ldapObject.setObjectClasses(attrValues); ldapObject.setObjectClasses(attrValues);
} else { } else {
if (attrValues.size() == 1) { ldapObject.setAttribute(ldapAttributeName, attrValues);
ldapObject.setAttribute(ldapAttributeName, attrValues.iterator().next());
} else {
ldapObject.setAttribute(ldapAttributeName, attrValues);
}
// readOnlyAttrNames are lower-cased // readOnlyAttrNames are lower-cased
if (readOnlyAttrNames.contains(ldapAttributeName.toLowerCase())) { if (readOnlyAttrNames.contains(ldapAttributeName.toLowerCase())) {
@ -435,30 +431,25 @@ public class LDAPIdentityStore implements IdentityStore {
protected BasicAttributes extractAttributes(LDAPObject ldapObject, boolean isCreate) { protected BasicAttributes extractAttributes(LDAPObject ldapObject, boolean isCreate) {
BasicAttributes entryAttributes = new BasicAttributes(); 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(); String attrName = attrEntry.getKey();
Object attrValue = attrEntry.getValue(); Set<String> attrValue = attrEntry.getValue();
// ldapObject.getReadOnlyAttributeNames() are lower-cased // ldapObject.getReadOnlyAttributeNames() are lower-cased
if (!ldapObject.getReadOnlyAttributeNames().contains(attrName.toLowerCase()) && (isCreate || !ldapObject.getRdnAttributeName().equalsIgnoreCase(attrName))) { if (!ldapObject.getReadOnlyAttributeNames().contains(attrName.toLowerCase()) && (isCreate || !ldapObject.getRdnAttributeName().equalsIgnoreCase(attrName))) {
BasicAttribute attr = new BasicAttribute(attrName);
if (String.class.isInstance(attrValue)) { if (attrValue == null) {
if (attrValue.toString().trim().length() == 0) { // Adding empty value as we don't know if attribute is mandatory in LDAP
attrValue = LDAPConstants.EMPTY_ATTRIBUTE_VALUE; attr.add(LDAPConstants.EMPTY_ATTRIBUTE_VALUE);
} } else {
entryAttributes.put(attrName, attrValue); for (String val : attrValue) {
} else if (Collection.class.isInstance(attrValue)) { if (val == null || val.toString().trim().length() == 0) {
BasicAttribute attr = new BasicAttribute(attrName); val = LDAPConstants.EMPTY_ATTRIBUTE_VALUE;
Collection<String> valueCollection = (Collection<String>) attrValue; }
for (String val : valueCollection) {
attr.add(val); 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);
} }
} }

View file

@ -31,7 +31,7 @@ import javax.naming.ldap.PagedResultsResponseControl;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.federation.ldap.LDAPConfig; 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.LDAPConstants;
import org.keycloak.models.ModelException; 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 List<SearchResult> result = new ArrayList<SearchResult>();
final SearchControls cons = getSearchControls(identityQuery.getReturningLdapAttributes(), identityQuery.getSearchScope()); final SearchControls cons = getSearchControls(identityQuery.getReturningLdapAttributes(), identityQuery.getSearchScope());

View file

@ -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.Condition;
import org.keycloak.federation.ldap.idm.query.QueryParameter; 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.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.LDAPConstants;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationMapperModel;
@ -28,9 +28,13 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
@Override @Override
public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) { public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) {
String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel); String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel);
String fullName = ldapUser.getAttributeAsStringCaseInsensitive(ldapFullNameAttrName); String fullName = ldapUser.getAttributeAsString(ldapFullNameAttrName);
if (fullName == null) {
return;
}
fullName = fullName.trim(); fullName = fullName.trim();
if (fullName != null && !fullName.trim().isEmpty()) { if (!fullName.isEmpty()) {
int lastSpaceIndex = fullName.lastIndexOf(" "); int lastSpaceIndex = fullName.lastIndexOf(" ");
if (lastSpaceIndex == -1) { if (lastSpaceIndex == -1) {
user.setLastName(fullName); 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) { public void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm) {
String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel); String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel);
String fullName = getFullName(localUser.getFirstName(), localUser.getLastName()); String fullName = getFullName(localUser.getFirstName(), localUser.getLastName());
ldapUser.setAttribute(ldapFullNameAttrName, fullName); ldapUser.setSingleAttribute(ldapFullNameAttrName, fullName);
if (isReadOnly(mapperModel)) { if (isReadOnly(mapperModel)) {
ldapUser.addReadOnlyAttributeName(ldapFullNameAttrName); ldapUser.addReadOnlyAttributeName(ldapFullNameAttrName);
@ -80,7 +84,7 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
ensureTransactionStarted(); ensureTransactionStarted();
String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel); String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel);
ldapUser.setAttribute(ldapFullNameAttrName, fullName); ldapUser.setSingleAttribute(ldapFullNameAttrName, fullName);
} }
}; };
@ -92,7 +96,7 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
} }
@Override @Override
public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPIdentityQuery query) { public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) {
String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel); String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel);
query.addReturningLdapAttribute(ldapFullNameAttrName); query.addReturningLdapAttribute(ldapFullNameAttrName);

View file

@ -2,7 +2,7 @@ package org.keycloak.federation.ldap.mappers;
import org.keycloak.federation.ldap.LDAPFederationProvider; import org.keycloak.federation.ldap.LDAPFederationProvider;
import org.keycloak.federation.ldap.idm.model.LDAPObject; 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.models.RealmModel;
import org.keycloak.mappers.UserFederationMapper; import org.keycloak.mappers.UserFederationMapper;
import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationMapperModel;
@ -58,5 +58,5 @@ public interface LDAPFederationMapper extends UserFederationMapper {
* @param mapperModel * @param mapperModel
* @param query * @param query
*/ */
void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPIdentityQuery query); void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query);
} }

View file

@ -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.model.LDAPObject;
import org.keycloak.federation.ldap.idm.query.Condition; import org.keycloak.federation.ldap.idm.query.Condition;
import org.keycloak.federation.ldap.idm.query.QueryParameter; 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.query.internal.LDAPQueryConditionsBuilder;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.LDAPConstants; 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 // 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? // TODO: Rather address this with caching at LDAPIdentityStore level?
private Set<String> rolesSyncedModels = new TreeSet<String>(); private Set<String> rolesSyncedModels = new TreeSet<>();
@Override @Override
public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) { public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) {
@ -74,7 +74,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
// Import role mappings from LDAP into Keycloak DB // Import role mappings from LDAP into Keycloak DB
String roleNameAttr = getRoleNameLdapAttribute(mapperModel); String roleNameAttr = getRoleNameLdapAttribute(mapperModel);
for (LDAPObject ldapRole : ldapRoles) { for (LDAPObject ldapRole : ldapRoles) {
String roleName = ldapRole.getAttributeAsStringCaseInsensitive(roleNameAttr); String roleName = ldapRole.getAttributeAsString(roleNameAttr);
RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm); RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
RoleModel role = roleContainer.getRole(roleName); RoleModel role = roleContainer.getRole(roleName);
@ -95,7 +95,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
if (!rolesSyncedModels.contains(mapperModel.getId())) { 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()); 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 // Send query
List<LDAPObject> ldapRoles = ldapQuery.getResultList(); List<LDAPObject> ldapRoles = ldapQuery.getResultList();
@ -103,7 +103,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm); RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
String rolesRdnAttr = getRoleNameLdapAttribute(mapperModel); String rolesRdnAttr = getRoleNameLdapAttribute(mapperModel);
for (LDAPObject ldapRole : ldapRoles) { for (LDAPObject ldapRole : ldapRoles) {
String roleName = ldapRole.getAttributeAsStringCaseInsensitive(rolesRdnAttr); String roleName = ldapRole.getAttributeAsString(rolesRdnAttr);
if (roleContainer.getRole(roleName) == null) { if (roleContainer.getRole(roleName) == null) {
logger.infof("Syncing role [%s] from LDAP to keycloak DB", roleName); logger.infof("Syncing role [%s] from LDAP to keycloak DB", roleName);
@ -115,8 +115,8 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
} }
} }
public LDAPIdentityQuery createRoleQuery(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider) { public LDAPQuery createRoleQuery(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider) {
LDAPIdentityQuery ldapQuery = new LDAPIdentityQuery(ldapProvider); LDAPQuery ldapQuery = new LDAPQuery(ldapProvider);
// For now, use same search scope, which is configured "globally" and used for user's search. // For now, use same search scope, which is configured "globally" and used for user's search.
ldapQuery.setSearchScope(ldapProvider.getLdapIdentityStore().getConfig().getSearchScope()); ldapQuery.setSearchScope(ldapProvider.getLdapIdentityStore().getConfig().getSearchScope());
@ -178,7 +178,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
} }
String[] objClasses = objectClasses.split(","); String[] objClasses = objectClasses.split(",");
Set<String> trimmed = new HashSet<String>(); Set<String> trimmed = new HashSet<>();
for (String objectClass : objClasses) { for (String objectClass : objClasses) {
objectClass = objectClass.trim(); objectClass = objectClass.trim();
if (objectClass.length() > 0) { if (objectClass.length() > 0) {
@ -202,7 +202,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
String roleNameAttribute = getRoleNameLdapAttribute(mapperModel); String roleNameAttribute = getRoleNameLdapAttribute(mapperModel);
ldapObject.setRdnAttributeName(roleNameAttribute); ldapObject.setRdnAttributeName(roleNameAttribute);
ldapObject.setObjectClasses(getRoleObjectClasses(mapperModel, ldapProvider)); ldapObject.setObjectClasses(getRoleObjectClasses(mapperModel, ldapProvider));
ldapObject.setAttribute(roleNameAttribute, roleName); ldapObject.setSingleAttribute(roleNameAttribute, roleName);
LDAPDn roleDn = LDAPDn.fromString(getRolesDn(mapperModel)); LDAPDn roleDn = LDAPDn.fromString(getRolesDn(mapperModel));
roleDn.addFirst(roleNameAttribute, roleName); roleDn.addFirst(roleNameAttribute, roleName);
@ -220,6 +220,15 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
} }
Set<String> memberships = getExistingMemberships(mapperModel, ldapRole); 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()); memberships.add(ldapUser.getDn().toString());
ldapRole.setAttribute(getMembershipLdapAttribute(mapperModel), memberships); ldapRole.setAttribute(getMembershipLdapAttribute(mapperModel), memberships);
@ -240,7 +249,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
} }
public LDAPObject loadLDAPRoleByName(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, String roleName) { 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); Condition roleNameCondition = new LDAPQueryConditionsBuilder().equal(new QueryParameter(getRoleNameLdapAttribute(mapperModel)), roleName);
ldapQuery.where(roleNameCondition); ldapQuery.where(roleNameCondition);
return ldapQuery.getFirstResult(); return ldapQuery.getFirstResult();
@ -248,29 +257,15 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
protected Set<String> getExistingMemberships(UserFederationMapperModel mapperModel, LDAPObject ldapRole) { protected Set<String> getExistingMemberships(UserFederationMapperModel mapperModel, LDAPObject ldapRole) {
String memberAttrName = getMembershipLdapAttribute(mapperModel); String memberAttrName = getMembershipLdapAttribute(mapperModel);
Set<String> memberships = new TreeSet<String>(); Set<String> memberships = ldapRole.getAttributeAsSet(memberAttrName);
Object existingMemberships = ldapRole.getAttributeCaseInsensitive(memberAttrName); if (memberships == null) {
memberships = new HashSet<>();
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);
}
}
}
} }
return memberships; return memberships;
} }
protected List<LDAPObject> getLDAPRoleMappings(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) { protected List<LDAPObject> getLDAPRoleMappings(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) {
LDAPIdentityQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider); LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
String membershipAttr = getMembershipLdapAttribute(mapperModel); String membershipAttr = getMembershipLdapAttribute(mapperModel);
Condition membershipCondition = new LDAPQueryConditionsBuilder().equal(new QueryParameter(membershipAttr), ldapUser.getDn().toString()); Condition membershipCondition = new LDAPQueryConditionsBuilder().equal(new QueryParameter(membershipAttr), ldapUser.getDn().toString());
ldapQuery.where(membershipCondition); ldapQuery.where(membershipCondition);
@ -290,7 +285,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
} }
@Override @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) { if (mode == Mode.LDAP_ONLY) {
// For LDAP-only we want to retrieve role mappings of target container just from LDAP // 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) { for (RoleModel role : modelRolesCopy) {
if (role.getContainer().equals(targetRoleContainer)) { if (role.getContainer().equals(targetRoleContainer)) {
modelRoleMappings.remove(role); modelRoleMappings.remove(role);
@ -408,10 +403,10 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
List<LDAPObject> ldapRoles = getLDAPRoleMappings(mapperModel, ldapProvider, ldapUser); List<LDAPObject> ldapRoles = getLDAPRoleMappings(mapperModel, ldapProvider, ldapUser);
Set<RoleModel> roles = new HashSet<RoleModel>(); Set<RoleModel> roles = new HashSet<>();
String roleNameLdapAttr = getRoleNameLdapAttribute(mapperModel); String roleNameLdapAttr = getRoleNameLdapAttribute(mapperModel);
for (LDAPObject role : ldapRoles) { for (LDAPObject role : ldapRoles) {
String roleName = role.getAttributeAsStringCaseInsensitive(roleNameLdapAttr); String roleName = role.getAttributeAsString(roleNameLdapAttr);
RoleModel modelRole = roleContainer.getRole(roleName); RoleModel modelRole = roleContainer.getRole(roleName);
if (modelRole == null) { if (modelRole == null) {
// Add role to local DB // Add role to local DB
@ -430,7 +425,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm); RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
if (role.getContainer().equals(roleContainer)) { if (role.getContainer().equals(roleContainer)) {
LDAPIdentityQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider); LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder(); LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
Condition roleNameCondition = conditionsBuilder.equal(new QueryParameter(getRoleNameLdapAttribute(mapperModel)), role.getName()); Condition roleNameCondition = conditionsBuilder.equal(new QueryParameter(getRoleNameLdapAttribute(mapperModel)), role.getName());
Condition membershipCondition = conditionsBuilder.equal(new QueryParameter(getMembershipLdapAttribute(mapperModel)), ldapUser.getDn().toString()); Condition membershipCondition = conditionsBuilder.equal(new QueryParameter(getMembershipLdapAttribute(mapperModel)), ldapUser.getDn().toString());

View file

@ -1,14 +1,20 @@
package org.keycloak.federation.ldap.mappers; package org.keycloak.federation.ldap.mappers;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import org.jboss.logging.Logger;
import org.keycloak.federation.ldap.LDAPFederationProvider; import org.keycloak.federation.ldap.LDAPFederationProvider;
import org.keycloak.federation.ldap.idm.model.LDAPObject; import org.keycloak.federation.ldap.idm.model.LDAPObject;
import org.keycloak.federation.ldap.idm.query.Condition; import org.keycloak.federation.ldap.idm.query.Condition;
import org.keycloak.federation.ldap.idm.query.QueryParameter; 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.RealmModel;
import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationMapperModel;
import org.keycloak.models.UserFederationProvider; import org.keycloak.models.UserFederationProvider;
@ -23,10 +29,12 @@ import org.keycloak.models.utils.reflection.PropertyQueries;
*/ */
public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMapper { public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMapper {
private static final Logger logger = Logger.getLogger(UserAttributeLDAPFederationMapper.class);
private static final Map<String, Property<Object>> userModelProperties; private static final Map<String, Property<Object>> userModelProperties;
static { static {
userModelProperties = PropertyQueries.createQuery(UserModel.class).addCriteria(new PropertyCriteria() { Map<String, Property<Object>> userModelProps = PropertyQueries.createQuery(UserModel.class).addCriteria(new PropertyCriteria() {
@Override @Override
public boolean methodMatches(Method m) { public boolean methodMatches(Method m) {
@ -38,6 +46,12 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
} }
}).getResultList(); }).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 USER_MODEL_ATTRIBUTE = "user.model.attribute";
@ -51,16 +65,21 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE); String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE); String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
Object ldapAttrValue = ldapUser.getAttributeCaseInsensitive(ldapAttrName); Property<Object> userModelProperty = userModelProperties.get(userModelAttrName.toLowerCase());
if (ldapAttrValue != null && !ldapAttrValue.toString().trim().isEmpty()) {
Property<Object> userModelProperty = userModelProperties.get(userModelAttrName);
if (userModelProperty != null) { if (userModelProperty != null) {
// we have java property on UserModel
userModelProperty.setValue(user, ldapAttrValue); // 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 { } else {
// we don't have java property. Let's just setAttribute user.removeAttribute(userModelAttrName);
user.setAttribute(userModelAttrName, (String) ldapAttrValue);
} }
} }
} }
@ -70,18 +89,26 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE); String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
String ldapAttrName = mapperModel.getConfig().get(LDAP_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) { 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 { } 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)) { if (isReadOnly(mapperModel)) {
ldapUser.addReadOnlyAttributeName(ldapAttrName); ldapUser.addReadOnlyAttributeName(ldapAttrName);
} }
@ -99,9 +126,21 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
delegate = new TxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapUser) { delegate = new TxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapUser) {
@Override @Override
public void setAttribute(String name, String value) { public void setSingleAttribute(String name, String value) {
setLDAPAttribute(name, 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 @Override
@ -122,15 +161,22 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
super.setFirstName(firstName); super.setFirstName(firstName);
} }
protected void setLDAPAttribute(String modelAttrName, String value) { protected void setLDAPAttribute(String modelAttrName, Object value) {
if (modelAttrName.equalsIgnoreCase(userModelAttrName)) { if (modelAttrName.equalsIgnoreCase(userModelAttrName)) {
if (logger.isTraceEnabled()) { 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(); 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));
}
} }
} }
@ -144,32 +190,48 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
delegate = new UserModelDelegate(delegate) { delegate = new UserModelDelegate(delegate) {
@Override @Override
public String getAttribute(String name) { public String getFirstAttribute(String name) {
if (name.equalsIgnoreCase(userModelAttrName)) { if (name.equalsIgnoreCase(userModelAttrName)) {
// TODO: Support different types than strings as well... return ldapUser.getAttributeAsString(ldapAttrName);
return ldapUser.getAttributeAsStringCaseInsensitive(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 { } else {
return super.getAttribute(name); return super.getAttribute(name);
} }
} }
@Override @Override
public Map<String, String> getAttributes() { public Map<String, List<String>> getAttributes() {
Map<String, String> attrs = new HashMap<>(super.getAttributes()); Map<String, List<String>> attrs = new HashMap<>(super.getAttributes());
// Ignore properties // Ignore UserModel properties
if (UserModel.EMAIL.equalsIgnoreCase(userModelAttrName) || UserModel.FIRST_NAME.equalsIgnoreCase(userModelAttrName) || UserModel.LAST_NAME.equalsIgnoreCase(userModelAttrName)) { if (userModelProperties.get(userModelAttrName.toLowerCase()) != null) {
return attrs; return attrs;
} }
attrs.put(userModelAttrName, ldapUser.getAttributeAsStringCaseInsensitive(ldapAttrName)); Set<String> allLdapAttrValues = ldapUser.getAttributeAsSet(ldapAttrName);
if (allLdapAttrValues != null) {
attrs.put(userModelAttrName, new ArrayList<>(allLdapAttrValues));
}
return attrs; return attrs;
} }
@Override @Override
public String getEmail() { public String getEmail() {
if (UserModel.EMAIL.equalsIgnoreCase(userModelAttrName)) { if (UserModel.EMAIL.equalsIgnoreCase(userModelAttrName)) {
return ldapUser.getAttributeAsStringCaseInsensitive(ldapAttrName); return ldapUser.getAttributeAsString(ldapAttrName);
} else { } else {
return super.getEmail(); return super.getEmail();
} }
@ -178,7 +240,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
@Override @Override
public String getLastName() { public String getLastName() {
if (UserModel.LAST_NAME.equalsIgnoreCase(userModelAttrName)) { if (UserModel.LAST_NAME.equalsIgnoreCase(userModelAttrName)) {
return ldapUser.getAttributeAsStringCaseInsensitive(ldapAttrName); return ldapUser.getAttributeAsString(ldapAttrName);
} else { } else {
return super.getLastName(); return super.getLastName();
} }
@ -187,7 +249,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
@Override @Override
public String getFirstName() { public String getFirstName() {
if (UserModel.FIRST_NAME.equalsIgnoreCase(userModelAttrName)) { if (UserModel.FIRST_NAME.equalsIgnoreCase(userModelAttrName)) {
return ldapUser.getAttributeAsStringCaseInsensitive(ldapAttrName); return ldapUser.getAttributeAsString(ldapAttrName);
} else { } else {
return super.getFirstName(); return super.getFirstName();
} }
@ -200,7 +262,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
} }
@Override @Override
public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPIdentityQuery query) { public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) {
String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE); String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE); String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
@ -222,4 +284,22 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
private boolean isReadOnly(UserFederationMapperModel mapperModel) { private boolean isReadOnly(UserFederationMapperModel mapperModel) {
return parseBooleanParameter(mapperModel, READ_ONLY); 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());
}
}
}
} }

View file

@ -1,9 +1,12 @@
package org.keycloak.account.freemarker.model; package org.keycloak.account.freemarker.model;
import org.jboss.logging.Logger;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.util.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
@ -11,14 +14,29 @@ import java.util.Map;
*/ */
public class AccountBean { public class AccountBean {
private static final Logger logger = Logger.getLogger(AccountBean.class);
private final UserModel user; private final UserModel user;
private final MultivaluedMap<String, String> profileFormData; private final MultivaluedMap<String, String> profileFormData;
// TODO: More proper multi-value attribute support
private final Map<String, String> attributes = new HashMap<>(); private final Map<String, String> attributes = new HashMap<>();
public AccountBean(UserModel user, MultivaluedMap<String, String> profileFormData) { public AccountBean(UserModel user, MultivaluedMap<String, String> profileFormData) {
this.user = user; this.user = user;
this.profileFormData = profileFormData; 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) { if (profileFormData != null) {
for (String key : profileFormData.keySet()) { for (String key : profileFormData.keySet()) {
if (key.startsWith("user.attributes.")) { if (key.startsWith("user.attributes.")) {

View file

@ -35,7 +35,7 @@ public class LocaleHelper {
Locale locale = findLocale(realm.getSupportedLocales(), localeString); Locale locale = findLocale(realm.getSupportedLocales(), localeString);
if(locale != null){ if(locale != null){
if(user != null){ if(user != null){
user.setAttribute(UserModel.LOCALE, locale.toLanguageTag()); user.setSingleAttribute(UserModel.LOCALE, locale.toLanguageTag());
} }
return locale; return locale;
}else{ }else{
@ -48,8 +48,8 @@ public class LocaleHelper {
String localeString = httpHeaders.getCookies().get(LOCALE_COOKIE).getValue(); String localeString = httpHeaders.getCookies().get(LOCALE_COOKIE).getValue();
Locale locale = findLocale(realm.getSupportedLocales(), localeString); Locale locale = findLocale(realm.getSupportedLocales(), localeString);
if(locale != null){ if(locale != null){
if(user != null && user.getAttribute(UserModel.LOCALE) == null){ if(user != null && user.getFirstAttribute(UserModel.LOCALE) == null){
user.setAttribute(UserModel.LOCALE, locale.toLanguageTag()); user.setSingleAttribute(UserModel.LOCALE, locale.toLanguageTag());
} }
return locale; return locale;
}else{ }else{
@ -59,7 +59,7 @@ public class LocaleHelper {
//2. User profile //2. User profile
if(user != null && user.getAttributes().containsKey(UserModel.LOCALE)){ 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); Locale locale = findLocale(realm.getSupportedLocales(), localeString);
if(locale != null){ if(locale != null){

View file

@ -206,6 +206,8 @@ module.controller('UserDetailCtrl', function($scope, realm, user, User, UserFede
if (!user.attributes) { if (!user.attributes) {
user.attributes = {} user.attributes = {}
} }
convertAttributeValuesToString(user);
$scope.user = angular.copy(user); $scope.user = angular.copy(user);
if(user.federationLink) { if(user.federationLink) {
console.log("federationLink is not null"); console.log("federationLink is not null");
@ -252,13 +254,15 @@ module.controller('UserDetailCtrl', function($scope, realm, user, User, UserFede
}, true); }, true);
$scope.save = function() { $scope.save = function() {
convertAttributeValuesToLists();
if ($scope.create) { if ($scope.create) {
User.save({ User.save({
realm: realm.realm realm: realm.realm
}, $scope.user, function (data, headers) { }, $scope.user, function (data, headers) {
$scope.changed = false; $scope.changed = false;
convertAttributeValuesToString($scope.user);
user = angular.copy($scope.user); user = angular.copy($scope.user);
var l = headers().location; var l = headers().location;
console.debug("Location == " + l); console.debug("Location == " + l);
@ -275,12 +279,33 @@ module.controller('UserDetailCtrl', function($scope, realm, user, User, UserFede
userId: $scope.user.id userId: $scope.user.id
}, $scope.user, function () { }, $scope.user, function () {
$scope.changed = false; $scope.changed = false;
convertAttributeValuesToString($scope.user);
user = angular.copy($scope.user); user = angular.copy($scope.user);
Notifications.success("Your changes have been saved to the 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.reset = function() {
$scope.user = angular.copy(user); $scope.user = angular.copy(user);
$scope.changed = false; $scope.changed = false;

View file

@ -60,6 +60,7 @@ public class LDAPConstants {
public static final String SAM_ACCOUNT_NAME = "sAMAccountName"; public static final String SAM_ACCOUNT_NAME = "sAMAccountName";
public static final String EMAIL = "mail"; public static final String EMAIL = "mail";
public static final String POSTAL_CODE = "postalCode"; public static final String POSTAL_CODE = "postalCode";
public static final String STREET = "street";
public static final String MEMBER = "member"; public static final String MEMBER = "member";
public static final String MEMBER_OF = "memberOf"; public static final String MEMBER_OF = "memberOf";
public static final String OBJECT_CLASS = "objectclass"; public static final String OBJECT_CLASS = "objectclass";

View file

@ -27,13 +27,31 @@ public interface UserModel {
void setEnabled(boolean enabled); 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); 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);
Map<String, String> getAttributes(); /**
* @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, List<String>> getAttributes();
Set<String> getRequiredActions(); Set<String> getRequiredActions();

View file

@ -1,7 +1,5 @@
package org.keycloak.models.entities; package org.keycloak.models.entities;
import org.keycloak.models.UserModel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -23,7 +21,7 @@ public class UserEntity extends AbstractIdentifiableEntity {
private List<String> roleIds; private List<String> roleIds;
private Map<String, String> attributes; private Map<String, List<String>> attributes;
private List<String> requiredActions; private List<String> requiredActions;
private List<CredentialEntity> credentials = new ArrayList<CredentialEntity>(); private List<CredentialEntity> credentials = new ArrayList<CredentialEntity>();
private List<FederatedIdentityEntity> federatedIdentities; private List<FederatedIdentityEntity> federatedIdentities;
@ -101,11 +99,11 @@ public class UserEntity extends AbstractIdentifiableEntity {
this.roleIds = roleIds; this.roleIds = roleIds;
} }
public Map<String, String> getAttributes() { public Map<String, List<String>> getAttributes() {
return attributes; return attributes;
} }
public void setAttributes(Map<String, String> attributes) { public void setAttributes(Map<String, List<String>> attributes) {
this.attributes = attributes; this.attributes = attributes;
} }

View file

@ -31,6 +31,7 @@ import org.keycloak.representations.idm.UserFederationMapperRepresentation;
import org.keycloak.representations.idm.UserFederationProviderRepresentation; import org.keycloak.representations.idm.UserFederationProviderRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation; import org.keycloak.representations.idm.UserSessionRepresentation;
import org.keycloak.util.MultivaluedHashMap;
import org.keycloak.util.Time; import org.keycloak.util.Time;
import java.util.ArrayList; import java.util.ArrayList;
@ -67,7 +68,7 @@ public class ModelToRepresentation {
rep.setRequiredActions(reqActions); rep.setRequiredActions(reqActions);
if (user.getAttributes() != null && !user.getAttributes().isEmpty()) { 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()); attrs.putAll(user.getAttributes());
rep.setAttributes(attrs); rep.setAttributes(attrs);
} }

View file

@ -43,6 +43,7 @@ import org.keycloak.util.UriUtils;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
@ -805,8 +806,17 @@ public class RepresentationToModel {
user.setFederationLink(userRep.getFederationLink()); user.setFederationLink(userRep.getFederationLink());
user.setTotp(userRep.isTotp()); user.setTotp(userRep.isTotp());
if (userRep.getAttributes() != null) { if (userRep.getAttributes() != null) {
for (Map.Entry<String, String> entry : userRep.getAttributes().entrySet()) { for (Map.Entry<String, Object> entry : userRep.getAttributes().entrySet()) {
user.setAttribute(entry.getKey(), entry.getValue()); 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) { if (userRep.getRequiredActions() != null) {

View file

@ -53,8 +53,13 @@ public class UserModelDelegate implements UserModel {
} }
@Override @Override
public void setAttribute(String name, String value) { public void setSingleAttribute(String name, String value) {
delegate.setAttribute(name, value); delegate.setSingleAttribute(name, value);
}
@Override
public void setAttribute(String name, List<String> values) {
delegate.setAttribute(name, values);
} }
@Override @Override
@ -63,12 +68,17 @@ public class UserModelDelegate implements UserModel {
} }
@Override @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); return delegate.getAttribute(name);
} }
@Override @Override
public Map<String, String> getAttributes() { public Map<String, List<String>> getAttributes() {
return delegate.getAttributes(); return delegate.getAttributes();
} }

View file

@ -158,12 +158,23 @@ public class UserAdapter implements UserModel, Comparable {
} }
@Override @Override
public void setAttribute(String name, String value) { public void setSingleAttribute(String name, String value) {
if (user.getAttributes() == null) { 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 @Override
@ -174,13 +185,23 @@ public class UserAdapter implements UserModel, Comparable {
} }
@Override @Override
public String getAttribute(String name) { public String getFirstAttribute(String name) {
return user.getAttributes()==null ? null : user.getAttributes().get(name); if (user.getAttributes()==null) return null;
List<String> attrValues = user.getAttributes().get(name);
return (attrValues==null || attrValues.isEmpty()) ? null : attrValues.get(0);
} }
@Override @Override
public Map<String, String> getAttributes() { public List<String> getAttribute(String name) {
return user.getAttributes()==null ? Collections.<String, String>emptyMap() : Collections.unmodifiableMap(user.getAttributes()); 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, List<String>> getAttributes() {
return user.getAttributes()==null ? Collections.<String, List<String>>emptyMap() : Collections.unmodifiableMap((Map)user.getAttributes());
} }
@Override @Override

View file

@ -11,6 +11,7 @@ import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.cache.entities.CachedUser; import org.keycloak.models.cache.entities.CachedUser;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -78,9 +79,15 @@ public class UserAdapter implements UserModel {
} }
@Override @Override
public void setAttribute(String name, String value) { public void setSingleAttribute(String name, String value) {
getDelegateForUpdate(); getDelegateForUpdate();
updated.setAttribute(name, value); updated.setSingleAttribute(name, value);
}
@Override
public void setAttribute(String name, List<String> values) {
getDelegateForUpdate();
updated.setAttribute(name, values);
} }
@Override @Override
@ -90,13 +97,20 @@ public class UserAdapter implements UserModel {
} }
@Override @Override
public String getAttribute(String name) { public String getFirstAttribute(String name) {
if (updated != null) return updated.getAttribute(name); if (updated != null) return updated.getFirstAttribute(name);
return cached.getAttributes().get(name); return cached.getAttributes().getFirst(name);
} }
@Override @Override
public Map<String, String> getAttributes() { public List<String> getAttribute(String name) {
if (updated != null) return updated.getAttribute(name);
List<String> result = cached.getAttributes().get(name);
return (result == null) ? Collections.<String>emptyList() : result;
}
@Override
public Map<String, List<String>> getAttributes() {
if (updated != null) return updated.getAttributes(); if (updated != null) return updated.getAttributes();
return cached.getAttributes(); return cached.getAttributes();
} }

View file

@ -4,6 +4,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.util.MultivaluedHashMap;
import java.io.Serializable; import java.io.Serializable;
import java.util.HashMap; import java.util.HashMap;
@ -29,7 +30,7 @@ public class CachedUser implements Serializable {
private boolean enabled; private boolean enabled;
private boolean totp; private boolean totp;
private String federationLink; 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> requiredActions = new HashSet<>();
private Set<String> roleMappings = new HashSet<String>(); private Set<String> roleMappings = new HashSet<String>();
@ -93,7 +94,7 @@ public class CachedUser implements Serializable {
return totp; return totp;
} }
public Map<String, String> getAttributes() { public MultivaluedHashMap<String, String> getAttributes() {
return attributes; return attributes;
} }

View file

@ -22,6 +22,7 @@ import org.keycloak.models.jpa.entities.UserRequiredActionEntity;
import org.keycloak.models.jpa.entities.UserRoleMappingEntity; import org.keycloak.models.jpa.entities.UserRoleMappingEntity;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.Pbkdf2PasswordEncoder; import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
import org.keycloak.util.MultivaluedHashMap;
import org.keycloak.util.Time; import org.keycloak.util.Time;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
@ -92,14 +93,46 @@ public class UserAdapter implements UserModel {
} }
@Override @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()) { for (UserAttributeEntity attr : user.getAttributes()) {
if (attr.getName().equals(name)) { if (attr.getName().equals(name)) {
attr.setValue(value); if (!found) {
return; 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(); UserAttributeEntity attr = new UserAttributeEntity();
attr.setId(KeycloakModelUtils.generateId());
attr.setName(name); attr.setName(name);
attr.setValue(value); attr.setValue(value);
attr.setUser(user); attr.setUser(user);
@ -120,7 +153,7 @@ public class UserAdapter implements UserModel {
} }
@Override @Override
public String getAttribute(String name) { public String getFirstAttribute(String name) {
for (UserAttributeEntity attr : user.getAttributes()) { for (UserAttributeEntity attr : user.getAttributes()) {
if (attr.getName().equals(name)) { if (attr.getName().equals(name)) {
return attr.getValue(); return attr.getValue();
@ -130,10 +163,21 @@ public class UserAdapter implements UserModel {
} }
@Override @Override
public Map<String, String> getAttributes() { public List<String> getAttribute(String name) {
Map<String, String> result = new HashMap<String, String>(); List<String> result = new ArrayList<>();
for (UserAttributeEntity attr : user.getAttributes()) { for (UserAttributeEntity attr : user.getAttributes()) {
result.put(attr.getName(), attr.getValue()); 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.add(attr.getName(), attr.getValue());
} }
return result; return result;
} }

View file

@ -1,6 +1,8 @@
package org.keycloak.models.jpa.entities; package org.keycloak.models.jpa.entities;
import javax.persistence.CollectionTable;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.FetchType; import javax.persistence.FetchType;
import javax.persistence.Id; import javax.persistence.Id;
@ -11,6 +13,8 @@ import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery; import javax.persistence.NamedQuery;
import javax.persistence.Table; import javax.persistence.Table;
import java.io.Serializable; import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -22,20 +26,29 @@ import java.io.Serializable;
}) })
@Table(name="USER_ATTRIBUTE") @Table(name="USER_ATTRIBUTE")
@Entity @Entity
@IdClass(UserAttributeEntity.Key.class)
public class UserAttributeEntity { public class UserAttributeEntity {
@Id @Id
@Column(name="ID", length = 36)
protected String id;
@ManyToOne(fetch= FetchType.LAZY) @ManyToOne(fetch= FetchType.LAZY)
@JoinColumn(name = "USER_ID") @JoinColumn(name = "USER_ID")
protected UserEntity user; protected UserEntity user;
@Id
@Column(name = "NAME") @Column(name = "NAME")
protected String name; protected String name;
@Column(name = "VALUE") @Column(name = "VALUE")
protected String value; protected String value;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() { public String getName() {
return name; return name;
} }
@ -60,47 +73,4 @@ public class UserAttributeEntity {
this.user = user; 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;
}
}
} }

View file

@ -19,7 +19,6 @@ import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.entities.CredentialEntity; import org.keycloak.models.entities.CredentialEntity;
import org.keycloak.models.entities.UserConsentEntity; 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.MongoUserConsentEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity; import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity;
import org.keycloak.models.mongo.utils.MongoModelUtils; import org.keycloak.models.mongo.utils.MongoModelUtils;
@ -127,12 +126,24 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
} }
@Override @Override
public void setAttribute(String name, String value) { public void setSingleAttribute(String name, String value) {
if (user.getAttributes() == null) { 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(); updateUser();
} }
@ -145,13 +156,23 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
} }
@Override @Override
public String getAttribute(String name) { public String getFirstAttribute(String name) {
return user.getAttributes()==null ? null : user.getAttributes().get(name); if (user.getAttributes()==null) return null;
List<String> attrValues = user.getAttributes().get(name);
return (attrValues==null || attrValues.isEmpty()) ? null : attrValues.get(0);
} }
@Override @Override
public Map<String, String> getAttributes() { public List<String> getAttribute(String name) {
return user.getAttributes()==null ? Collections.<String, String>emptyMap() : Collections.unmodifiableMap(user.getAttributes()); 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, List<String>> getAttributes() {
return user.getAttributes()==null ? Collections.<String, List<String>>emptyMap() : Collections.unmodifiableMap((Map)user.getAttributes());
} }
public MongoUserEntity getUser() { public MongoUserEntity getUser() {

View file

@ -238,11 +238,11 @@ public class SamlProtocol implements LoginProtocol {
// generate a persistent user id specifically for each client. // generate a persistent user id specifically for each client.
UserModel user = userSession.getUser(); UserModel user = userSession.getUser();
String name = SAML_PERSISTENT_NAME_ID_FOR + "." + clientSession.getClient().getClientId(); 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; if (samlPersistentId != null) return samlPersistentId;
// "G-" stands for "generated" // "G-" stands for "generated"
samlPersistentId = "G-" + UUID.randomUUID().toString(); samlPersistentId = "G-" + UUID.randomUUID().toString();
user.setAttribute(name, samlPersistentId); user.setSingleAttribute(name, samlPersistentId);
return samlPersistentId; return samlPersistentId;
} else if(nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get())){ } else if(nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get())){
// TODO: Support for persistent NameID (pseudo-random identifier persisted in user object) // TODO: Support for persistent NameID (pseudo-random identifier persisted in user object)

View file

@ -13,7 +13,7 @@ import java.util.ArrayList;
import java.util.List; 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> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @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) { public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
UserModel user = userSession.getUser(); UserModel user = userSession.getUser();
String attributeName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE); String attributeName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE);
String attributeValue = user.getAttribute(attributeName); String attributeValue = user.getFirstAttribute(attributeName);
if (attributeValue == null) return; if (attributeValue == null) return;
AttributeStatementHelper.addAttribute(attributeStatement, mappingModel, attributeValue); AttributeStatementHelper.addAttribute(attributeStatement, mappingModel, attributeValue);

View file

@ -15,12 +15,15 @@ import java.util.List;
public class ProtocolMapperUtils { public class ProtocolMapperUtils {
public static final String USER_ATTRIBUTE = "user.attribute"; public static final String USER_ATTRIBUTE = "user.attribute";
public static final String USER_SESSION_NOTE = "user.session.note"; 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_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_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_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_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_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 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) { public static String getUserModelValue(UserModel user, String propertyName) {

View file

@ -118,11 +118,11 @@ public class AddressMapper extends AbstractOIDCProtocolMapper implements OIDCAcc
protected void setClaim(IDToken token, UserSessionModel userSession) { protected void setClaim(IDToken token, UserSessionModel userSession) {
UserModel user = userSession.getUser(); UserModel user = userSession.getUser();
AddressClaimSet addressSet = new AddressClaimSet(); AddressClaimSet addressSet = new AddressClaimSet();
addressSet.setStreetAddress(user.getAttribute("street")); addressSet.setStreetAddress(user.getFirstAttribute("street"));
addressSet.setLocality(user.getAttribute("locality")); addressSet.setLocality(user.getFirstAttribute("locality"));
addressSet.setRegion(user.getAttribute("region")); addressSet.setRegion(user.getFirstAttribute("region"));
addressSet.setPostalCode(user.getAttribute("postal_code")); addressSet.setPostalCode(user.getFirstAttribute("postal_code"));
addressSet.setCountry(user.getAttribute("country")); addressSet.setCountry(user.getFirstAttribute("country"));
token.getOtherClaims().put("address", addressSet); token.getOtherClaims().put("address", addressSet);
} }

View file

@ -1,5 +1,6 @@
package org.keycloak.protocol.oidc.mappers; package org.keycloak.protocol.oidc.mappers;
import org.jboss.logging.Logger;
import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.protocol.ProtocolMapper; import org.keycloak.protocol.ProtocolMapper;
@ -19,6 +20,8 @@ import java.util.Map;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class OIDCAttributeMapperHelper { 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 = "claim.name";
public static final String TOKEN_CLAIM_NAME_LABEL = "Token Claim Name"; public static final String TOKEN_CLAIM_NAME_LABEL = "Token Claim Name";
public static final String JSON_TYPE = "Claim JSON Type"; public static final String JSON_TYPE = "Claim JSON Type";
@ -31,6 +34,26 @@ public class OIDCAttributeMapperHelper {
public static Object mapAttributeValue(ProtocolMapperModel mappingModel, Object attributeValue) { public static Object mapAttributeValue(ProtocolMapperModel mappingModel, Object attributeValue) {
if (attributeValue == null) return null; 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); String type = mappingModel.getConfig().get(JSON_TYPE);
if (type == null) return attributeValue; if (type == null) return attributeValue;
if (type.equals("boolean")) { if (type.equals("boolean")) {
@ -53,8 +76,9 @@ public class OIDCAttributeMapperHelper {
} }
public static void mapClaim(IDToken token, ProtocolMapperModel mappingModel, Object attributeValue) { public static void mapClaim(IDToken token, ProtocolMapperModel mappingModel, Object attributeValue) {
if (attributeValue == null) return;
attributeValue = mapAttributeValue(mappingModel, attributeValue); attributeValue = mapAttributeValue(mappingModel, attributeValue);
if (attributeValue == null) return;
String protocolClaim = mappingModel.getConfig().get(TOKEN_CLAIM_NAME); String protocolClaim = mappingModel.getConfig().get(TOKEN_CLAIM_NAME);
String[] split = protocolClaim.split("\\."); String[] split = protocolClaim.split("\\.");
Map<String, Object> jsonObject = token.getOtherClaims(); Map<String, Object> jsonObject = token.getOtherClaims();
@ -102,6 +126,11 @@ public class OIDCAttributeMapperHelper {
return "true".equals(mappingModel.getConfig().get(INCLUDE_IN_ACCESS_TOKEN)); 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) { public static void addAttributeConfig(List<ProviderConfigProperty> configProperties) {
ProviderConfigProperty property; ProviderConfigProperty property;
property = new ProviderConfigProperty(); property = new ProviderConfigProperty();

View file

@ -1,9 +1,9 @@
package org.keycloak.protocol.oidc.mappers; package org.keycloak.protocol.oidc.mappers;
import org.jboss.logging.Logger;
import org.keycloak.models.ClientSessionModel; import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.ProtocolMapperUtils; import org.keycloak.protocol.ProtocolMapperUtils;
@ -36,6 +36,13 @@ public class UserAttributeMapper extends AbstractOIDCProtocolMapper implements O
configProperties.add(property); configProperties.add(property);
OIDCAttributeMapperHelper.addAttributeConfig(configProperties); 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"; 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) { protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
UserModel user = userSession.getUser(); UserModel user = userSession.getUser();
String attributeName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE); String attributeName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE);
String attributeValue = user.getAttribute(attributeName); List<String> attributeValue = user.getAttribute(attributeName);
if (attributeValue == null) return; if (attributeValue == null) return;
OIDCAttributeMapperHelper.mapClaim(token, mappingModel, attributeValue); OIDCAttributeMapperHelper.mapClaim(token, mappingModel, attributeValue);
} }
@ -93,12 +100,18 @@ public class UserAttributeMapper extends AbstractOIDCProtocolMapper implements O
String userAttribute, String userAttribute,
String tokenClaimName, String claimType, String tokenClaimName, String claimType,
boolean consentRequired, String consentText, boolean consentRequired, String consentText,
boolean accessToken, boolean idToken) { boolean accessToken, boolean idToken, boolean multivalued) {
return OIDCAttributeMapperHelper.createClaimMapper(name, userAttribute, ProtocolMapperModel mapper = OIDCAttributeMapperHelper.createClaimMapper(name, userAttribute,
tokenClaimName, claimType, tokenClaimName, claimType,
consentRequired, consentText, consentRequired, consentText,
accessToken, idToken, accessToken, idToken,
PROVIDER_ID); PROVIDER_ID);
if (multivalued) {
mapper.getConfig().put(ProtocolMapperUtils.MULTIVALUED, "true");
}
return mapper;
} }

View file

@ -1,5 +1,8 @@
package org.keycloak.services.resources; package org.keycloak.services.resources;
import java.util.ArrayList;
import java.util.List;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
@ -21,8 +24,26 @@ public class AttributeFormDataProcessor {
for (String key : formData.keySet()) { for (String key : formData.keySet()) {
if (!key.startsWith("user.attributes.")) continue; if (!key.startsWith("user.attributes.")) continue;
String attribute = key.substring("user.attributes.".length()); 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);
}
}
} }

View file

@ -5,7 +5,6 @@ import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.BadRequestException; import org.jboss.resteasy.spi.BadRequestException;
import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.NotFoundException;
import org.keycloak.ClientConnection; import org.keycloak.ClientConnection;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.email.EmailException; import org.keycloak.email.EmailException;
import org.keycloak.email.EmailProvider; import org.keycloak.email.EmailProvider;
@ -228,8 +227,8 @@ public class UsersResource {
} }
} }
if (rep.getAttributes() != null) { if (rep.getAttributesAsListValues() != null) {
for (Map.Entry<String, String> attr : rep.getAttributes().entrySet()) { for (Map.Entry<String, List<String>> attr : rep.getAttributesAsListValues().entrySet()) {
user.setAttribute(attr.getKey(), attr.getValue()); user.setAttribute(attr.getKey(), attr.getValue());
} }

View file

@ -6,6 +6,7 @@ import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient; import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import org.junit.ClassRule; import org.junit.ClassRule;
import org.junit.Rule; import org.junit.Rule;
@ -51,8 +52,8 @@ public class ProfileTest {
UserModel user = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm); UserModel user = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm);
user.setFirstName("First"); user.setFirstName("First");
user.setLastName("Last"); user.setLastName("Last");
user.setAttribute("key1", "value1"); user.setSingleAttribute("key1", "value1");
user.setAttribute("key2", "value2"); user.setSingleAttribute("key2", "value2");
ClientModel accountApp = appRealm.getClientByClientId(org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_CLIENT_ID); ClientModel accountApp = appRealm.getClientByClientId(org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
@ -114,8 +115,12 @@ public class ProfileTest {
assertEquals("Last", profile.getString("lastName")); assertEquals("Last", profile.getString("lastName"));
JSONObject attributes = profile.getJSONObject("attributes"); JSONObject attributes = profile.getJSONObject("attributes");
assertEquals("value1", attributes.getString("key1")); JSONArray attrValue = attributes.getJSONArray("key1");
assertEquals("value2", attributes.getString("key2")); assertEquals(1, attrValue.length());
assertEquals("value1", attrValue.get(0));
attrValue = attributes.getJSONArray("key2");
assertEquals(1, attrValue.length());
assertEquals("value2", attrValue.get(0));
} }
@Test @Test

View file

@ -12,6 +12,8 @@ import org.keycloak.representations.idm.UserRepresentation;
import javax.ws.rs.ClientErrorException; import javax.ws.rs.ClientErrorException;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@ -271,8 +273,8 @@ public class UserTest extends AbstractClientTest {
public void attributes() { public void attributes() {
UserRepresentation user1 = new UserRepresentation(); UserRepresentation user1 = new UserRepresentation();
user1.setUsername("user1"); user1.setUsername("user1");
user1.attribute("attr1", "value1user1"); user1.singleAttribute("attr1", "value1user1");
user1.attribute("attr2", "value2user1"); user1.singleAttribute("attr2", "value2user1");
Response response = realm.users().create(user1); Response response = realm.users().create(user1);
String user1Id = ApiUtil.getCreatedId(response); String user1Id = ApiUtil.getCreatedId(response);
@ -280,40 +282,45 @@ public class UserTest extends AbstractClientTest {
UserRepresentation user2 = new UserRepresentation(); UserRepresentation user2 = new UserRepresentation();
user2.setUsername("user2"); user2.setUsername("user2");
user2.attribute("attr1", "value1user2"); user2.singleAttribute("attr1", "value1user2");
user2.attribute("attr2", "value2user2"); List<String> vals = new ArrayList<>();
vals.add("value2user2");
vals.add("value2user2_2");
user2.getAttributesAsListValues().put("attr2", vals);
response = realm.users().create(user2); response = realm.users().create(user2);
String user2Id = ApiUtil.getCreatedId(response); String user2Id = ApiUtil.getCreatedId(response);
response.close(); response.close();
user1 = realm.users().get(user1Id).toRepresentation(); user1 = realm.users().get(user1Id).toRepresentation();
assertEquals(2, user1.getAttributes().size()); assertEquals(2, user1.getAttributesAsListValues().size());
assertEquals("value1user1", user1.getAttributes().get("attr1")); assertAttributeValue("value1user1", user1.getAttributesAsListValues().get("attr1"));
assertEquals("value2user1", user1.getAttributes().get("attr2")); assertAttributeValue("value2user1", user1.getAttributesAsListValues().get("attr2"));
user2 = realm.users().get(user2Id).toRepresentation(); user2 = realm.users().get(user2Id).toRepresentation();
assertEquals(2, user2.getAttributes().size()); assertEquals(2, user2.getAttributesAsListValues().size());
assertEquals("value1user2", user2.getAttributes().get("attr1")); assertAttributeValue("value1user2", user2.getAttributesAsListValues().get("attr1"));
assertEquals("value2user2", user2.getAttributes().get("attr2")); vals = user2.getAttributesAsListValues().get("attr2");
assertEquals(2, vals.size());
assertTrue(vals.contains("value2user2") && vals.contains("value2user2_2"));
user1.attribute("attr1", "value3user1"); user1.singleAttribute("attr1", "value3user1");
user1.attribute("attr3", "value4user1"); user1.singleAttribute("attr3", "value4user1");
realm.users().get(user1Id).update(user1); realm.users().get(user1Id).update(user1);
user1 = realm.users().get(user1Id).toRepresentation(); user1 = realm.users().get(user1Id).toRepresentation();
assertEquals(3, user1.getAttributes().size()); assertEquals(3, user1.getAttributesAsListValues().size());
assertEquals("value3user1", user1.getAttributes().get("attr1")); assertAttributeValue("value3user1", user1.getAttributesAsListValues().get("attr1"));
assertEquals("value2user1", user1.getAttributes().get("attr2")); assertAttributeValue("value2user1", user1.getAttributesAsListValues().get("attr2"));
assertEquals("value4user1", user1.getAttributes().get("attr3")); assertAttributeValue("value4user1", user1.getAttributesAsListValues().get("attr3"));
user1.getAttributes().remove("attr1"); user1.getAttributes().remove("attr1");
realm.users().get(user1Id).update(user1); realm.users().get(user1Id).update(user1);
user1 = realm.users().get(user1Id).toRepresentation(); user1 = realm.users().get(user1Id).toRepresentation();
assertEquals(2, user1.getAttributes().size()); assertEquals(2, user1.getAttributesAsListValues().size());
assertEquals("value2user1", user1.getAttributes().get("attr2")); assertAttributeValue("value2user1", user1.getAttributesAsListValues().get("attr2"));
assertEquals("value4user1", user1.getAttributes().get("attr3")); assertAttributeValue("value4user1", user1.getAttributesAsListValues().get("attr3"));
user1.getAttributes().clear(); user1.getAttributes().clear();
realm.users().get(user1Id).update(user1); realm.users().get(user1Id).update(user1);
@ -322,6 +329,11 @@ public class UserTest extends AbstractClientTest {
assertNull(user1.getAttributes()); assertNull(user1.getAttributes());
} }
private void assertAttributeValue(String expectedValue, List<String> attrValues) {
assertEquals(1, attrValues.size());
assertEquals(expectedValue, attrValues.get(0));
}
@Test @Test
public void sendResetPasswordEmail() { public void sendResetPasswordEmail() {
UserRepresentation userRep = new UserRepresentation(); UserRepresentation userRep = new UserRepresentation();

View file

@ -141,7 +141,7 @@ public abstract class AbstractIdentityProviderTest {
identityProviderModel.setUpdateProfileFirstLoginMode(IdentityProviderRepresentation.UPFLM_ON); identityProviderModel.setUpdateProfileFirstLoginMode(IdentityProviderRepresentation.UPFLM_ON);
UserModel user = assertSuccessfulAuthentication(identityProviderModel, "test-user", "new@email.com", true); 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 @Test
@ -304,7 +304,7 @@ public abstract class AbstractIdentityProviderTest {
identityProviderModel.setTrustEmail(true); identityProviderModel.setTrustEmail(true);
UserModel user = assertSuccessfulAuthenticationWithEmailVerification(identityProviderModel, "test-user", "new@email.com", 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 { } finally {
identityProviderModel.setTrustEmail(false); identityProviderModel.setTrustEmail(false);
getRealm().setVerifyEmail(false); getRealm().setVerifyEmail(false);

View file

@ -279,7 +279,7 @@ public class FederationProvidersIntegrationTest {
// Fetch user from LDAP and check that postalCode is filled // Fetch user from LDAP and check that postalCode is filled
UserModel user = session.users().getUserByUsername("johnzip", appRealm); UserModel user = session.users().getUserByUsername("johnzip", appRealm);
String postalCode = user.getAttribute("postal_code"); String postalCode = user.getFirstAttribute("postal_code");
Assert.assertEquals("12398", postalCode); Assert.assertEquals("12398", postalCode);
} finally { } finally {
@ -299,21 +299,21 @@ public class FederationProvidersIntegrationTest {
// Fetch user from LDAP and check that postalCode is filled // Fetch user from LDAP and check that postalCode is filled
UserModel user = session.users().getUserByUsername("johndirect", appRealm); UserModel user = session.users().getUserByUsername("johndirect", appRealm);
String postalCode = user.getAttribute("postal_code"); String postalCode = user.getFirstAttribute("postal_code");
Assert.assertEquals("12399", postalCode); Assert.assertEquals("12399", postalCode);
// Directly update user in LDAP // Directly update user in LDAP
johnDirect.setAttribute(LDAPConstants.POSTAL_CODE, "12400"); johnDirect.setSingleAttribute(LDAPConstants.POSTAL_CODE, "12400");
johnDirect.setAttribute(LDAPConstants.SN, "DirectLDAPUpdated"); johnDirect.setSingleAttribute(LDAPConstants.SN, "DirectLDAPUpdated");
ldapFedProvider.getLdapIdentityStore().update(johnDirect); ldapFedProvider.getLdapIdentityStore().update(johnDirect);
// Verify that postalCode is still the same as we read it's value from Keycloak DB // Verify that postalCode is still the same as we read it's value from Keycloak DB
user = session.users().getUserByUsername("johndirect", appRealm); user = session.users().getUserByUsername("johndirect", appRealm);
postalCode = user.getAttribute("postal_code"); postalCode = user.getFirstAttribute("postal_code");
Assert.assertEquals("12399", postalCode); Assert.assertEquals("12399", postalCode);
// Check user.getAttributes() // Check user.getAttributes()
postalCode = user.getAttributes().get("postal_code"); postalCode = user.getAttributes().get("postal_code").get(0);
Assert.assertEquals("12399", postalCode); Assert.assertEquals("12399", postalCode);
// LastName is new as lastName mapper will read the value from LDAP // LastName is new as lastName mapper will read the value from LDAP
@ -339,11 +339,11 @@ public class FederationProvidersIntegrationTest {
// Verify that postalCode is read from LDAP now // Verify that postalCode is read from LDAP now
UserModel user = session.users().getUserByUsername("johndirect", appRealm); UserModel user = session.users().getUserByUsername("johndirect", appRealm);
String postalCode = user.getAttribute("postal_code"); String postalCode = user.getFirstAttribute("postal_code");
Assert.assertEquals("12400", postalCode); Assert.assertEquals("12400", postalCode);
// Check user.getAttributes() // Check user.getAttributes()
postalCode = user.getAttributes().get("postal_code"); postalCode = user.getAttributes().get("postal_code").get(0);
Assert.assertEquals("12400", postalCode); Assert.assertEquals("12400", postalCode);
Assert.assertFalse(user.getAttributes().containsKey(UserModel.LAST_NAME)); Assert.assertFalse(user.getAttributes().containsKey(UserModel.LAST_NAME));

View file

@ -1,5 +1,7 @@
package org.keycloak.testsuite.federation; package org.keycloak.testsuite.federation;
import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import org.junit.Assert; 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.LDAPFederationProviderFactory;
import org.keycloak.federation.ldap.LDAPUtils; import org.keycloak.federation.ldap.LDAPUtils;
import org.keycloak.federation.ldap.idm.model.LDAPObject; 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.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.federation.ldap.mappers.RoleLDAPFederationMapper; import org.keycloak.federation.ldap.mappers.RoleLDAPFederationMapper;
import org.keycloak.federation.ldap.mappers.RoleLDAPFederationMapperFactory; import org.keycloak.federation.ldap.mappers.RoleLDAPFederationMapperFactory;
@ -69,11 +71,11 @@ class FederationTestUtils {
} }
@Override @Override
public String getAttribute(String name) { public List<String> getAttribute(String name) {
if ("postal_code".equals(name)) { if ("postal_code".equals(name)) {
return postalCode; return Arrays.asList(postalCode);
} else { } else {
return null; return Collections.emptyList();
} }
} }
}; };
@ -91,7 +93,7 @@ class FederationTestUtils {
Assert.assertEquals(expectedFirstName, user.getFirstName()); Assert.assertEquals(expectedFirstName, user.getFirstName());
Assert.assertEquals(expectedLastName, user.getLastName()); Assert.assertEquals(expectedLastName, user.getLastName());
Assert.assertEquals(expectedEmail, user.getEmail()); 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) { public static void addZipCodeLDAPMapper(RealmModel realm, UserFederationProviderModel providerModel) {
@ -138,7 +140,7 @@ class FederationTestUtils {
public static void removeAllLDAPUsers(LDAPFederationProvider ldapProvider, RealmModel realm) { public static void removeAllLDAPUsers(LDAPFederationProvider ldapProvider, RealmModel realm) {
LDAPIdentityStore ldapStore = ldapProvider.getLdapIdentityStore(); LDAPIdentityStore ldapStore = ldapProvider.getLdapIdentityStore();
LDAPIdentityQuery ldapQuery = LDAPUtils.createQueryForUserSearch(ldapProvider, realm); LDAPQuery ldapQuery = LDAPUtils.createQueryForUserSearch(ldapProvider, realm);
List<LDAPObject> allUsers = ldapQuery.getResultList(); List<LDAPObject> allUsers = ldapQuery.getResultList();
for (LDAPObject ldapUser : allUsers) { for (LDAPObject ldapUser : allUsers) {
@ -149,7 +151,7 @@ class FederationTestUtils {
public static void removeAllLDAPRoles(KeycloakSession session, RealmModel appRealm, UserFederationProviderModel ldapModel, String mapperName) { public static void removeAllLDAPRoles(KeycloakSession session, RealmModel appRealm, UserFederationProviderModel ldapModel, String mapperName) {
UserFederationMapperModel mapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), mapperName); UserFederationMapperModel mapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), mapperName);
LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel); 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(); List<LDAPObject> ldapRoles = roleQuery.getResultList();
for (LDAPObject ldapRole : ldapRoles) { for (LDAPObject ldapRole : ldapRoles) {
ldapProvider.getLdapIdentityStore().remove(ldapRole); ldapProvider.getLdapIdentityStore().remove(ldapRole);

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -16,7 +16,7 @@ import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
import org.keycloak.federation.ldap.idm.model.LDAPObject; import org.keycloak.federation.ldap.idm.model.LDAPObject;
import org.keycloak.federation.ldap.idm.query.Condition; import org.keycloak.federation.ldap.idm.query.Condition;
import org.keycloak.federation.ldap.idm.query.QueryParameter; 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.query.internal.LDAPQueryConditionsBuilder;
import org.keycloak.federation.ldap.mappers.RoleLDAPFederationMapper; import org.keycloak.federation.ldap.mappers.RoleLDAPFederationMapper;
import org.keycloak.models.AccountRoles; 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) { 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(); LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
Condition roleNameCondition = conditionsBuilder.equal(new QueryParameter(LDAPConstants.CN), roleName); Condition roleNameCondition = conditionsBuilder.equal(new QueryParameter(LDAPConstants.CN), roleName);
ldapQuery.where(roleNameCondition); ldapQuery.where(roleNameCondition);

View file

@ -126,8 +126,8 @@ public class SyncProvidersTest {
FederationTestUtils.addLDAPUser(ldapFedProvider, testRealm, "user6", "User6FN", "User6LN", "user6@email.org", "126"); FederationTestUtils.addLDAPUser(ldapFedProvider, testRealm, "user6", "User6FN", "User6LN", "user6@email.org", "126");
LDAPObject ldapUser5 = ldapFedProvider.loadLDAPUserByUsername(testRealm, "user5"); LDAPObject ldapUser5 = ldapFedProvider.loadLDAPUserByUsername(testRealm, "user5");
// NOTE: Changing LDAP attributes directly here // NOTE: Changing LDAP attributes directly here
ldapUser5.setAttribute(LDAPConstants.EMAIL, "user5Updated@email.org"); ldapUser5.setSingleAttribute(LDAPConstants.EMAIL, "user5Updated@email.org");
ldapUser5.setAttribute(LDAPConstants.POSTAL_CODE, "521"); ldapUser5.setSingleAttribute(LDAPConstants.POSTAL_CODE, "521");
ldapFedProvider.getLdapIdentityStore().update(ldapUser5); ldapFedProvider.getLdapIdentityStore().update(ldapUser5);
// Assert still old users in local provider // Assert still old users in local provider

View file

@ -54,7 +54,7 @@ public class EmailTest {
UserModel user = manager.getSession().users().addUser(appRealm, "login-test"); UserModel user = manager.getSession().users().addUser(appRealm, "login-test");
user.setEmail("login@test.com"); user.setEmail("login@test.com");
user.setEnabled(true); user.setEnabled(true);
user.setAttribute(UserModel.LOCALE, "de"); user.setSingleAttribute(UserModel.LOCALE, "de");
UserCredentialModel creds = new UserCredentialModel(); UserCredentialModel creds = new UserCredentialModel();
creds.setType(CredentialRepresentation.PASSWORD); creds.setType(CredentialRepresentation.PASSWORD);
@ -91,7 +91,7 @@ public class EmailTest {
keycloakRule.update(new KeycloakRule.KeycloakSetup() { keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@Override @Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { 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");
} }
}); });

View file

@ -25,7 +25,6 @@ import java.security.NoSuchAlgorithmException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -159,7 +158,7 @@ public class AdapterTest extends AbstractModelTest {
test1CreateRealm(); test1CreateRealm();
UserModel user = realmManager.getSession().users().addUser(realmModel, "bburke"); UserModel user = realmManager.getSession().users().addUser(realmModel, "bburke");
user.setAttribute("attr1", "val1"); user.setSingleAttribute("attr1", "val1");
user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
RoleModel testRole = realmModel.addRole("test"); RoleModel testRole = realmModel.addRole("test");

View file

@ -140,6 +140,22 @@ public class ImportTest extends AbstractModelTest {
Assert.assertEquals(1, appRoles.size()); Assert.assertEquals(1, appRoles.size());
Assert.assertEquals("app-admin", appRoles.iterator().next().getName()); 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 // Test client
ClientModel oauthClient = realm.getClientByClientId("oauthclient"); ClientModel oauthClient = realm.getClientByClientId("oauthclient");
Assert.assertEquals("clientpassword", oauthClient.getSecret()); Assert.assertEquals("clientpassword", oauthClient.getSecret());

View file

@ -8,6 +8,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction; import org.keycloak.models.UserModel.RequiredAction;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -137,6 +138,61 @@ public class UserModelTest extends AbstractModelTest {
Assert.assertTrue(user.getRequiredActions().isEmpty()); 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) { public static void assertEquals(UserModel expected, UserModel actual) {
Assert.assertEquals(expected.getUsername(), actual.getUsername()); Assert.assertEquals(expected.getUsername(), actual.getUsername());
Assert.assertEquals(expected.getFirstName(), actual.getFirstName()); Assert.assertEquals(expected.getFirstName(), actual.getFirstName());

View file

@ -69,7 +69,9 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
@ -616,19 +618,22 @@ public class AccessTokenTest {
KeycloakSession session = keycloakRule.startSession(); KeycloakSession session = keycloakRule.startSession();
RealmModel realm = session.realms().getRealmByName("test"); RealmModel realm = session.realms().getRealmByName("test");
UserModel user = session.users().getUserByUsername("test-user@localhost", realm); UserModel user = session.users().getUserByUsername("test-user@localhost", realm);
user.setAttribute("street", "5 Yawkey Way"); user.setSingleAttribute("street", "5 Yawkey Way");
user.setAttribute("locality", "Boston"); user.setSingleAttribute("locality", "Boston");
user.setAttribute("region", "MA"); user.setSingleAttribute("region", "MA");
user.setAttribute("postal_code", "02115"); user.setSingleAttribute("postal_code", "02115");
user.setAttribute("country", "USA"); user.setSingleAttribute("country", "USA");
user.setAttribute("phone", "617-777-6666"); user.setSingleAttribute("phone", "617-777-6666");
List<String> departments = Arrays.asList("finance", "development");
user.setAttribute("departments", departments);
ClientModel app = realm.getClientByClientId("test-app"); ClientModel app = realm.getClientByClientId("test-app");
ProtocolMapperModel mapper = AddressMapper.createAddressMapper(true, true); ProtocolMapperModel mapper = AddressMapper.createAddressMapper(true, true);
app.addProtocolMapper(mapper); app.addProtocolMapper(mapper);
app.addProtocolMapper(HardcodedClaim.create("hard", "hard", "coded", "String", false, null, true, true)); 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(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("custom phone", "phone", "home_phone", "String", true, "", true, true, false));
app.addProtocolMapper(UserAttributeMapper.createClaimMapper("nested phone", "phone", "home.phone", "String", true, "", true, true)); 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-realm", "hardcoded"));
app.addProtocolMapper(HardcodedRole.create("hard-app", "app.hardcoded")); app.addProtocolMapper(HardcodedRole.create("hard-app", "app.hardcoded"));
app.addProtocolMapper(RoleNameMapper.create("rename-app-role", "test-app.customer-user", "realm-user")); 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")); Assert.assertEquals("coded-nested", nested.get("hard"));
nested = (Map)idToken.getOtherClaims().get("home"); nested = (Map)idToken.getOtherClaims().get("home");
Assert.assertEquals("617-777-6666", nested.get("phone")); 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); AccessToken accessToken = getAccessToken(tokenResponse);
Assert.assertEquals(accessToken.getName(), "Tom Brady"); Assert.assertEquals(accessToken.getName(), "Tom Brady");
@ -671,6 +679,9 @@ public class AccessTokenTest {
Assert.assertEquals("coded-nested", nested.get("hard")); Assert.assertEquals("coded-nested", nested.get("hard"));
nested = (Map)accessToken.getOtherClaims().get("home"); nested = (Map)accessToken.getOtherClaims().get("home");
Assert.assertEquals("617-777-6666", nested.get("phone")); 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("hardcoded"));
Assert.assertTrue(accessToken.getRealmAccess().getRoles().contains("realm-user")); Assert.assertTrue(accessToken.getRealmAccess().getRoles().contains("realm-user"));
Assert.assertFalse(accessToken.getResourceAccess("test-app").getRoles().contains("customer-user")); Assert.assertFalse(accessToken.getResourceAccess("test-app").getRoles().contains("customer-user"));

View file

@ -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"
}
}

View file

@ -29,6 +29,7 @@ cn: James
sn: Brown sn: Brown
mail: jbrown@keycloak.org mail: jbrown@keycloak.org
postalCode: 88441 postalCode: 88441
userPassword: password
dn: uid=bwilson,ou=People,dc=keycloak,dc=org dn: uid=bwilson,ou=People,dc=keycloak,dc=org
objectclass: top objectclass: top
@ -38,6 +39,9 @@ objectclass: inetOrgPerson
uid: bwilson uid: bwilson
cn: Bruce cn: Bruce
sn: Wilson sn: Wilson
sn: Schneider
mail: bwilson@keycloak.org mail: bwilson@keycloak.org
postalCode: 88441 postalCode: 88441
postalCode: 77332 postalCode: 77332
street: Elm 5
userPassword: password

View file

@ -82,6 +82,15 @@
{ {
"username": "admin", "username": "admin",
"enabled": true, "enabled": true,
"attributes": {
"key1": [
"val1"
],
"key2": [
"val21",
"val22"
]
},
"credentials": [ "credentials": [
{ {
"type": "password", "type": "password",