diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPConfig.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPConfig.java index 2442d30d3d..65181e147b 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPConfig.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPConfig.java @@ -16,7 +16,7 @@ import org.keycloak.models.UserFederationProviderModel; /** * @author Marek Posolda * - * TODO: init properties at startup instead of always compute them + * TODO: init properties at constructor instead of always compute them */ public class LDAPConfig { diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java index 7d3ca02570..ad55bc3534 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java @@ -19,7 +19,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialValueModel; -import org.keycloak.models.UserFederationMapper; +import org.keycloak.mappers.UserFederationMapper; import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationProvider; import org.keycloak.models.UserFederationProviderModel; @@ -114,7 +114,7 @@ public class LDAPFederationProvider implements UserFederationProvider { List federationMappers = realm.getUserFederationMappers(); for (UserFederationMapperModel mapperModel : federationMappers) { LDAPFederationMapper ldapMapper = getMapper(mapperModel); - proxied = ldapMapper.proxy(mapperModel, this, ldapObject, proxied); + proxied = ldapMapper.proxy(mapperModel, this, ldapObject, proxied, realm); } return proxied; @@ -227,7 +227,7 @@ public class LDAPFederationProvider implements UserFederationProvider { /** * @param local - * @return ldapObject corresponding to local user or null if user is no longer in LDAP + * @return ldapUser corresponding to local user or null if user is no longer in LDAP */ protected LDAPObject loadAndValidateUser(RealmModel realm, UserModel local) { LDAPObject ldapUser = loadLDAPUserByUsername(realm, local.getUsername()); @@ -271,7 +271,7 @@ public class LDAPFederationProvider implements UserFederationProvider { List federationMappers = realm.getUserFederationMappers(); for (UserFederationMapperModel mapperModel : federationMappers) { LDAPFederationMapper ldapMapper = getMapper(mapperModel); - ldapMapper.importUserFromLDAP(mapperModel, this, ldapUser, imported, true); + ldapMapper.onImportUserFromLDAP(mapperModel, this, ldapUser, imported, realm, true); } String userDN = ldapUser.getDn().toString(); @@ -314,7 +314,7 @@ public class LDAPFederationProvider implements UserFederationProvider { @Override public void preRemove(RealmModel realm, RoleModel role) { // complete I don't think we have to do anything here - // TODO: requires implementation... Maybe mappers callback + // TODO: requires implementation... Maybe mappers callback to ensure role deletion propagated to LDAP by RoleLDAPFederationMapper } public boolean validPassword(RealmModel realm, UserModel user, String password) { @@ -407,7 +407,7 @@ public class LDAPFederationProvider implements UserFederationProvider { List federationMappers = realm.getUserFederationMappers(); for (UserFederationMapperModel mapperModel : federationMappers) { LDAPFederationMapper ldapMapper = getMapper(mapperModel); - ldapMapper.importUserFromLDAP(mapperModel, this, ldapUser, currentUser, false); + ldapMapper.onImportUserFromLDAP(mapperModel, this, ldapUser, currentUser, realm, false); } logger.debugf("Updated user from LDAP: %s", currentUser.getUsername()); @@ -477,8 +477,7 @@ public class LDAPFederationProvider implements UserFederationProvider { } public LDAPFederationMapper getMapper(UserFederationMapperModel mapperModel) { - LDAPFederationMapper ldapMapper = (LDAPFederationMapper) getSession().getKeycloakSessionFactory() - .getProviderFactory(UserFederationMapper.class, mapperModel.getFederationMapperId()); + LDAPFederationMapper ldapMapper = (LDAPFederationMapper) getSession().getProvider(UserFederationMapper.class, mapperModel.getFederationMapperId()); if (ldapMapper == null) { throw new ModelException("Can't find mapper type with ID: " + mapperModel.getFederationMapperId()); } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java index 1e9918df35..6c5ac93237 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java @@ -37,7 +37,7 @@ public class LDAPUtils { List federationMappers = realm.getUserFederationMappers(); for (UserFederationMapperModel mapperModel : federationMappers) { LDAPFederationMapper ldapMapper = ldapProvider.getMapper(mapperModel); - ldapMapper.registerUserToLDAP(mapperModel, ldapProvider, ldapObject, user); + ldapMapper.onRegisterUserToLDAP(mapperModel, ldapProvider, ldapObject, user, realm); } LDAPUtils.computeAndSetDn(ldapConfig, ldapObject); @@ -161,10 +161,10 @@ public class LDAPUtils { return fullName; } */ - // ldapObject has filled attributes, but doesn't have filled + // ldapUser has filled attributes, but doesn't have filled dn public static void computeAndSetDn(LDAPConfig config, LDAPObject ldapObject) { String rdnLdapAttrName = config.getRdnLdapAttribute(); - String rdnLdapAttrValue = (String) ldapObject.getAttribute(rdnLdapAttrName); + String rdnLdapAttrValue = ldapObject.getAttributeAsString(rdnLdapAttrName); if (rdnLdapAttrValue == null) { throw new ModelException("RDN Attribute [" + rdnLdapAttrName + "] is not filled. Filled attributes: " + ldapObject.getAttributes()); } @@ -176,11 +176,6 @@ public class LDAPUtils { public static String getUsername(LDAPObject ldapUser, LDAPConfig config) { String usernameAttr = config.getUsernameLdapAttribute(); - return (String) ldapUser.getAttribute(usernameAttr); - } - - public static boolean parseBooleanParameter(UserFederationMapperModel mapperModel, String paramName) { - String readOnly = mapperModel.getConfig().get(paramName); - return Boolean.parseBoolean(readOnly); + return ldapUser.getAttributeAsString(usernameAttr); } } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java index 999f133fd7..5d58438b15 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java @@ -1,14 +1,16 @@ package org.keycloak.federation.ldap.idm.model; +import java.util.Deque; import java.util.LinkedList; import java.util.List; +import java.util.Queue; /** * @author Marek Posolda */ public class LDAPDn { - private final List entries = new LinkedList(); + private final Deque entries = new LinkedList(); public static LDAPDn fromString(String dnString) { LDAPDn dn = new LDAPDn(); @@ -43,7 +45,7 @@ public class LDAPDn { * @return string like "uid=joe" from the DN like "uid=joe,dc=something,dc=org" */ public String getFirstRdn() { - Entry firstEntry = entries.get(0); + Entry firstEntry = entries.getFirst(); return firstEntry.attrName + "=" + firstEntry.attrValue; } @@ -51,7 +53,7 @@ public class LDAPDn { * @return string attribute name like "uid" from the DN like "uid=joe,dc=something,dc=org" */ public String getFirstRdnAttrName() { - Entry firstEntry = entries.get(0); + Entry firstEntry = entries.getFirst(); return firstEntry.attrName; } @@ -60,28 +62,15 @@ public class LDAPDn { * @return string like "dc=something,dc=org" from the DN like "uid=joe,dc=something,dc=org" */ public String getParentDn() { - StringBuilder builder = new StringBuilder(); - - int n = 0; - for (Entry rdn : entries) { - n++; - if (n > 2) { - builder.append(","); - } - if (n >= 2) { - builder.append(rdn.attrName).append("=").append(rdn.attrValue); - } - } - - return builder.toString(); + return new LinkedList(entries).remove().toString(); } public void addToHead(String rdnName, String rdnValue) { - entries.add(0, new Entry(rdnName, rdnValue)); + entries.addFirst(new Entry(rdnName, rdnValue)); } public void addToBottom(String rdnName, String rdnValue) { - entries.add(new Entry(rdnName, rdnValue)); + entries.addLast(new Entry(rdnName, rdnValue)); } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java index 61b631d5e0..587a81f1cf 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java @@ -18,7 +18,7 @@ public class LDAPObject { private final List objectClasses = new LinkedList(); private final List readOnlyAttributeNames = new LinkedList(); - private final Map attributes = new HashMap(); + private final Map attributes = new HashMap(); public String getUuid() { return uuid; @@ -61,7 +61,7 @@ public class LDAPObject { this.rdnAttributeName = rdnAttributeName; } - public void setAttribute(String attributeName, Serializable attributeValue) { + public void setAttribute(String attributeName, Object attributeValue) { attributes.put(attributeName, attributeValue); } @@ -70,12 +70,21 @@ public class LDAPObject { } - public Serializable getAttribute(String name) { + public Object getAttribute(String name) { return attributes.get(name); } + public String getAttributeAsString(String name) { + Object attrValue = attributes.get(name); + if (attrValue != null && !(attrValue instanceof String)) { + throw new IllegalStateException("Expected String but attribute was " + attrValue + " of type " + attrValue.getClass().getName()); + } - public Map getAttributes() { + return (String) attrValue; + } + + + public Map getAttributes() { return attributes; } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java index 04e6d6190c..023d143ada 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java @@ -9,6 +9,8 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Set; +import java.util.TreeSet; import javax.naming.NamingEnumeration; import javax.naming.NamingException; @@ -389,10 +391,9 @@ public class LDAPIdentityStore implements IdentityStore { while (ldapAttributes.hasMore()) { Attribute ldapAttribute = ldapAttributes.next(); - Serializable ldapAttributeValue; try { - ldapAttributeValue = (Serializable) ldapAttribute.get(); + ldapAttribute.get(); } catch (NoSuchElementException nsee) { continue; } @@ -400,23 +401,31 @@ public class LDAPIdentityStore implements IdentityStore { String ldapAttributeName = ldapAttribute.getID(); if (ldapAttributeName.toLowerCase().equals(getConfig().getUuidAttributeName().toLowerCase())) { - ldapObject.setUuid(this.operationManager.decodeEntryUUID(ldapAttributeValue)); - } else if (ldapAttributeName.toLowerCase().equals(LDAPConstants.OBJECT_CLASS)) { - List objectClasses = new LinkedList(); + Object uuidValue = ldapAttribute.get(); + ldapObject.setUuid(this.operationManager.decodeEntryUUID(uuidValue)); + } else { + Set attrValues = new TreeSet(); NamingEnumeration enumm = ldapAttribute.getAll(); while (enumm.hasMoreElements()) { String objectClass = enumm.next().toString(); - objectClasses.add(objectClass); - } - ldapObject.setObjectClasses(objectClasses); - } else { - if (logger.isTraceEnabled()) { - logger.tracef("Populating ldap attribute [%s] with value [%s] for DN [%s].", ldapAttributeName, ldapAttributeValue, entryDN); + attrValues.add(objectClass); } - ldapObject.setAttribute(ldapAttributeName, ldapAttributeValue); - if (uppercasedReadOnlyAttrNames.contains(ldapAttributeName.toUpperCase())) { - ldapObject.addReadOnlyAttributeName(ldapAttributeName); + if (ldapAttributeName.toLowerCase().equals(LDAPConstants.OBJECT_CLASS)) { + ldapObject.setObjectClasses(attrValues); + } else { + if (logger.isTraceEnabled()) { + logger.tracef("Populating ldap attribute [%s] with value [%s] for DN [%s].", ldapAttributeName, attrValues.toString(), entryDN); + } + if (attrValues.size() == 1) { + ldapObject.setAttribute(ldapAttributeName, attrValues.iterator().next()); + } else { + ldapObject.setAttribute(ldapAttributeName, attrValues); + } + + if (uppercasedReadOnlyAttrNames.contains(ldapAttributeName.toUpperCase())) { + ldapObject.addReadOnlyAttributeName(ldapAttributeName); + } } } } @@ -487,16 +496,16 @@ public class LDAPIdentityStore implements IdentityStore { protected BasicAttributes extractAttributes(LDAPObject ldapObject, boolean isCreate) { BasicAttributes entryAttributes = new BasicAttributes(); - for (Map.Entry attrEntry : ldapObject.getAttributes().entrySet()) { + for (Map.Entry attrEntry : ldapObject.getAttributes().entrySet()) { String attrName = attrEntry.getKey(); - Serializable attrValue = attrEntry.getValue(); + Object attrValue = attrEntry.getValue(); if (!ldapObject.getReadOnlyAttributeNames().contains(attrName) && (isCreate || !ldapObject.getRdnAttributeName().equals(attrName))) { if (String.class.isInstance(attrValue)) { entryAttributes.put(attrName, attrValue); } else if (Collection.class.isInstance(attrValue)) { BasicAttribute attr = new BasicAttribute(attrName); - Collection valueCollection = (Collection) attr; + Collection valueCollection = (Collection) attrValue; for (String val : valueCollection) { attr.add(val); } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java index 357262e6ec..6daa011692 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java @@ -1,9 +1,6 @@ package org.keycloak.federation.ldap.mappers; -import org.keycloak.Config; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.models.UserFederationMapper; +import org.keycloak.models.UserFederationMapperModel; /** * @author Marek Posolda @@ -15,18 +12,8 @@ public abstract class AbstractLDAPFederationMapper implements LDAPFederationMapp } - @Override - public UserFederationMapper create(KeycloakSession session) { - throw new RuntimeException("UNSUPPORTED METHOD"); - } - - @Override - public void init(Config.Scope config) { - - } - - @Override - public void postInit(KeycloakSessionFactory factory) { - + protected boolean parseBooleanParameter(UserFederationMapperModel mapperModel, String paramName) { + String paramm = mapperModel.getConfig().get(paramName); + return Boolean.parseBoolean(paramm); } } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapperFactory.java new file mode 100644 index 0000000000..6b8f186d4c --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapperFactory.java @@ -0,0 +1,25 @@ +package org.keycloak.federation.ldap.mappers; + +import org.keycloak.Config; +import org.keycloak.mappers.UserFederationMapperFactory; +import org.keycloak.models.KeycloakSessionFactory; + +/** + * @author Marek Posolda + */ +public abstract class AbstractLDAPFederationMapperFactory implements UserFederationMapperFactory { + + @Override + public void init(Config.Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java index 483d825f9d..109b0b0775 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java @@ -11,7 +11,10 @@ import org.keycloak.federation.ldap.idm.query.Condition; import org.keycloak.federation.ldap.idm.query.QueryParameter; import org.keycloak.federation.ldap.idm.query.internal.EqualCondition; import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery; +import org.keycloak.mappers.UserFederationMapper; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.LDAPConstants; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationProvider; import org.keycloak.models.UserModel; @@ -28,24 +31,9 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper { public static final String READ_ONLY = "read.only"; @Override - public String getHelpText() { - return "Some help text - full name mapper - TODO"; - } - - @Override - public List getConfigProperties() { - return null; - } - - @Override - public String getId() { - return "full-name-ldap-mapper"; - } - - @Override - public void importUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapObject, UserModel user, boolean isCreate) { + public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) { String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel); - String fullName = (String) ldapObject.getAttribute(ldapFullNameAttrName); + String fullName = ldapUser.getAttributeAsString(ldapFullNameAttrName); fullName = fullName.trim(); if (fullName != null) { int lastSpaceIndex = fullName.lastIndexOf(" "); @@ -59,22 +47,22 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper { } @Override - public void registerUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapObject, UserModel localUser) { + public void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm) { String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel); String fullName = getFullName(localUser.getFirstName(), localUser.getLastName()); - ldapObject.setAttribute(ldapFullNameAttrName, fullName); + ldapUser.setAttribute(ldapFullNameAttrName, fullName); if (isReadOnly(mapperModel)) { - ldapObject.addReadOnlyAttributeName(ldapFullNameAttrName); + ldapUser.addReadOnlyAttributeName(ldapFullNameAttrName); } } @Override - public UserModel proxy(final UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapObject, UserModel delegate) { + public UserModel proxy(final UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel delegate, RealmModel realm) { if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && !isReadOnly(mapperModel)) { - AbstractTxAwareLDAPUserModelDelegate txDelegate = new AbstractTxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapObject) { + TxAwareLDAPUserModelDelegate txDelegate = new TxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapUser) { @Override public void setFirstName(String firstName) { @@ -97,7 +85,7 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper { ensureTransactionStarted(); String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel); - ldapObject.setAttribute(ldapFullNameAttrName, fullName); + ldapUser.setAttribute(ldapFullNameAttrName, fullName); } }; @@ -127,10 +115,10 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper { lastNameCondition = (EqualCondition) condition; query.getConditions().remove(condition); } else if (param.getName().equals(LDAPConstants.GIVENNAME)) { - // Some previous mapper already converted it + // Some previous mapper already converted it to LDAP name firstNameCondition = (EqualCondition) condition; } else if (param.getName().equals(LDAPConstants.SN)) { - // Some previous mapper already converted it + // Some previous mapper already converted it to LDAP name lastNameCondition = (EqualCondition) condition; } } @@ -169,6 +157,6 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper { } private boolean isReadOnly(UserFederationMapperModel mapperModel) { - return LDAPUtils.parseBooleanParameter(mapperModel, READ_ONLY); + return parseBooleanParameter(mapperModel, READ_ONLY); } } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java new file mode 100644 index 0000000000..682034551f --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java @@ -0,0 +1,33 @@ +package org.keycloak.federation.ldap.mappers; + +import java.util.List; + +import org.keycloak.mappers.UserFederationMapper; +import org.keycloak.models.KeycloakSession; +import org.keycloak.provider.ProviderConfigProperty; + +/** + * @author Marek Posolda + */ +public class FullNameLDAPFederationMapperFactory extends AbstractLDAPFederationMapperFactory { + + @Override + public String getHelpText() { + return "Some help text - full name mapper - TODO"; + } + + @Override + public List getConfigProperties() { + return null; + } + + @Override + public String getId() { + return "full-name-ldap-mapper"; + } + + @Override + public UserFederationMapper create(KeycloakSession session) { + return new FullNameLDAPFederationMapper(); + } +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapper.java index de89e925b7..4676a6b198 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapper.java @@ -3,7 +3,8 @@ package org.keycloak.federation.ldap.mappers; import org.keycloak.federation.ldap.LDAPFederationProvider; import org.keycloak.federation.ldap.idm.model.LDAPObject; import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery; -import org.keycloak.models.UserFederationMapper; +import org.keycloak.models.RealmModel; +import org.keycloak.mappers.UserFederationMapper; import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserModel; @@ -12,17 +13,50 @@ import org.keycloak.models.UserModel; */ public interface LDAPFederationMapper extends UserFederationMapper { - // TODO: rename? - // Called when importing user from federation provider to local keycloak DB. Flag "isCreate" means if we creating new user to Keycloak DB or just update existing user in Keycloak DB - void importUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapObject, UserModel user, boolean isCreate); - // TODO: rename to beforeRegister or something? - // Called when register new user to federation provider - void registerUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapObject, UserModel localUser); + /** + * Called when importing user from LDAP to local keycloak DB. + * + * @param mapperModel + * @param ldapProvider + * @param ldapUser + * @param user + * @param realm + * @param isCreate true if we importing new user from LDAP. False if user already exists in Keycloak, but we are upgrading (syncing) it from LDAP + */ + void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate); - // Called when invoke proxy on federation provider - UserModel proxy(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapObject, UserModel delegate); - // Called before any LDAPIdentityQuery is executed + /** + * Called when register new user to LDAP - just after user was created in Keycloak DB + * + * @param mapperModel + * @param ldapProvider + * @param ldapUser + * @param localUser + * @param realm + */ + void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm); + + + /** + * Called when invoke proxy on LDAP federation provider + * + * @param mapperModel + * @param ldapProvider + * @param ldapUser + * @param delegate + * @param realm + * @return + */ + UserModel proxy(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel delegate, RealmModel realm); + + + /** + * Called before LDAP Identity query for retrieve LDAP users was executed. It allows to change query somehow (add returning attributes from LDAP, change conditions etc) + * + * @param mapperModel + * @param query + */ void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPIdentityQuery query); } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java new file mode 100644 index 0000000000..47f288dba1 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java @@ -0,0 +1,473 @@ +package org.keycloak.federation.ldap.mappers; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import javax.naming.directory.SearchControls; + +import org.jboss.logging.Logger; +import org.keycloak.federation.ldap.LDAPFederationProvider; +import org.keycloak.federation.ldap.idm.model.LDAPDn; +import org.keycloak.federation.ldap.idm.model.LDAPObject; +import org.keycloak.federation.ldap.idm.query.Condition; +import org.keycloak.federation.ldap.idm.query.QueryParameter; +import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery; +import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder; +import org.keycloak.models.ClientModel; +import org.keycloak.models.LDAPConstants; +import org.keycloak.models.ModelException; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleContainerModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserFederationMapperModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.models.utils.UserModelDelegate; + +/** + * Map realm roles or roles of particular client to LDAP roles + * + * @author Marek Posolda + */ +public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper { + + private static final Logger logger = Logger.getLogger(RoleLDAPFederationMapper.class); + + // LDAP DN where are roles of this tree saved. + public static final String ROLES_DN = "roles.dn"; + + // Name of LDAP attribute, which is used in role objects for name and RDN of role. Usually it will be "cn" + public static final String ROLE_NAME_LDAP_ATTRIBUTE = "role.name.ldap.attribute"; + + // Name of LDAP attribute on role, which is used for membership mappings. Usually it will be "member" + public static final String MEMBERSHIP_LDAP_ATTRIBUTE = "membership.ldap.attribute"; + + // Object classes of the role object. + public static final String ROLE_OBJECT_CLASSES = "role.object.classes"; + + // Boolean option. If true, we will map LDAP roles to realm roles. If false, we will map to client roles (client specified by option CLIENT_ID) + public static final String USE_REALM_ROLES_MAPPING = "use.realm.roles.mapping"; + + // ClientId, which we want to map roles. Applicable just if "USE_REALM_ROLES_MAPPING" is false + public static final String CLIENT_ID = "client.id"; + + // See docs for Mode enum + public static final String MODE = "mode"; + + + // List of IDs of UserFederationMapperModels where syncRolesFromLDAP was already called in this KeycloakSession. This is to improve performance + // TODO: Rather address this with caching at LDAPIdentityStore level? + private Set rolesSyncedModels = new TreeSet(); + + @Override + public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) { + syncRolesFromLDAP(mapperModel, ldapProvider, realm); + + Mode mode = getMode(mapperModel); + + // For now, import LDAP role mappings just during create + if (mode == Mode.IMPORT && isCreate) { + + List ldapRoles = getLDAPRoleMappings(mapperModel, ldapProvider, ldapUser); + + // Import role mappings from LDAP into Keycloak DB + String roleNameAttr = getRoleNameLdapAttribute(mapperModel); + for (LDAPObject ldapRole : ldapRoles) { + String roleName = ldapRole.getAttributeAsString(roleNameAttr); + + RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm); + RoleModel role = roleContainer.getRole(roleName); + + // TODO: debug + logger.infof("Granting role [%s] to user [%s] during import from LDAP", roleName, user.getUsername()); + user.grantRole(role); + } + } + } + + @Override + public void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm) { + syncRolesFromLDAP(mapperModel, ldapProvider, realm); + } + + // Sync roles from LDAP tree and create them in local Keycloak DB (if they don't exist here yet) + protected void syncRolesFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm) { + if (!rolesSyncedModels.contains(mapperModel.getId())) { + LDAPIdentityQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider); + + // Send query + List ldapRoles = ldapQuery.getResultList(); + + RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm); + String rolesRdnAttr = getRoleNameLdapAttribute(mapperModel); + for (LDAPObject ldapRole : ldapRoles) { + String roleName = ldapRole.getAttributeAsString(rolesRdnAttr); + + if (roleContainer.getRole(roleName) == null) { + // TODO: rather change to debug + logger.infof("Syncing role [%s] from LDAP to keycloak DB", roleName); + roleContainer.addRole(roleName); + } + } + + rolesSyncedModels.add(mapperModel.getId()); + } + } + + public LDAPIdentityQuery createRoleQuery(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider) { + LDAPIdentityQuery ldapQuery = new LDAPIdentityQuery(ldapProvider); + ldapQuery.setSearchScope(SearchControls.ONELEVEL_SCOPE); + + String rolesDn = getRolesDn(mapperModel); + ldapQuery.addSearchDns(Arrays.asList(rolesDn)); + + Collection roleObjectClasses = getRoleObjectClasses(mapperModel); + ldapQuery.addObjectClasses(roleObjectClasses); + + String rolesRdnAttr = getRoleNameLdapAttribute(mapperModel); + String membershipAttr = getMembershipLdapAttribute(mapperModel); + ldapQuery.addReturningLdapAttribute(rolesRdnAttr); + ldapQuery.addReturningLdapAttribute(membershipAttr); + + return ldapQuery; + } + + protected RoleContainerModel getTargetRoleContainer(UserFederationMapperModel mapperModel, RealmModel realm) { + boolean realmRolesMapping = parseBooleanParameter(mapperModel, USE_REALM_ROLES_MAPPING); + if (realmRolesMapping) { + return realm; + } else { + String clientId = mapperModel.getConfig().get(CLIENT_ID); + if (clientId == null) { + throw new IllegalStateException("Using client roles mapping is requested, but parameter client.id not found!"); + } + ClientModel client = realm.getClientByClientId(clientId); + if (client == null) { + throw new IllegalStateException("Can't found requested client with clientId: " + clientId); + } + return client; + } + } + + protected String getRolesDn(UserFederationMapperModel mapperModel) { + String rolesDn = mapperModel.getConfig().get(ROLES_DN); + if (rolesDn == null) { + throw new IllegalStateException("Roles DN is null! Check your configuration"); + } + return rolesDn; + } + + protected String getRoleNameLdapAttribute(UserFederationMapperModel mapperModel) { + String rolesRdnAttr = mapperModel.getConfig().get(ROLE_NAME_LDAP_ATTRIBUTE); + return rolesRdnAttr!=null ? rolesRdnAttr : LDAPConstants.CN; + } + + protected String getMembershipLdapAttribute(UserFederationMapperModel mapperModel) { + String membershipAttrName = mapperModel.getConfig().get(MEMBERSHIP_LDAP_ATTRIBUTE); + return membershipAttrName!=null ? membershipAttrName : LDAPConstants.MEMBER; + } + + protected Collection getRoleObjectClasses(UserFederationMapperModel mapperModel) { + String objectClasses = mapperModel.getConfig().get(ROLE_OBJECT_CLASSES); + if (objectClasses == null) { + objectClasses = "groupOfNames"; + } + String[] objClasses = objectClasses.split(","); + + // TODO: util method for trim and convert array to collection? + Set trimmed = new HashSet(); + for (String objectClass : objClasses) { + objectClass = objectClass.trim(); + if (objectClass.length() > 0) { + trimmed.add(objectClass); + } + } + return trimmed; + } + + private Mode getMode(UserFederationMapperModel mapperModel) { + String modeString = mapperModel.getConfig().get(MODE); + if (modeString == null || modeString.trim().length() == 0) { + return Mode.LDAP_ONLY; + } + + return Enum.valueOf(Mode.class, modeString.toUpperCase()); + } + + protected LDAPObject createLDAPRole(UserFederationMapperModel mapperModel, String roleName, LDAPFederationProvider ldapProvider) { + LDAPObject ldapObject = new LDAPObject(); + String roleNameAttribute = getRoleNameLdapAttribute(mapperModel); + ldapObject.setRdnAttributeName(roleNameAttribute); + ldapObject.setObjectClasses(getRoleObjectClasses(mapperModel)); + ldapObject.setAttribute(roleNameAttribute, roleName); + + LDAPDn roleDn = LDAPDn.fromString(getRolesDn(mapperModel)); + roleDn.addToHead(roleNameAttribute, roleName); + ldapObject.setDn(roleDn); + + // TODO: debug + logger.infof("Creating role to [%s] to LDAP with DN [%s]", roleName, roleDn.toString()); + ldapProvider.getLdapIdentityStore().add(ldapObject); + return ldapObject; + } + + public void addRoleMappingInLDAP(UserFederationMapperModel mapperModel, String roleName, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) { + LDAPObject ldapRole = loadLDAPRoleByName(mapperModel, ldapProvider, roleName); + if (ldapRole == null) { + ldapRole = createLDAPRole(mapperModel, roleName, ldapProvider); + } + + Set memberships = getExistingMemberships(mapperModel, ldapRole); + memberships.add(ldapUser.getDn().toString()); + ldapRole.setAttribute(getMembershipLdapAttribute(mapperModel), memberships); + + ldapProvider.getLdapIdentityStore().update(ldapRole); + } + + public void deleteRoleMappingInLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, LDAPObject ldapRole) { + Set memberships = getExistingMemberships(mapperModel, ldapRole); + memberships.remove(ldapUser.getDn().toString()); + + // Some membership placeholder needs to be always here as "member" is mandatory attribute on some LDAP servers + if (memberships.size() == 0) { + memberships.add(LDAPConstants.EMPTY_ATTRIBUTE_VALUE); + } + + ldapRole.setAttribute(getMembershipLdapAttribute(mapperModel), memberships); + ldapProvider.getLdapIdentityStore().update(ldapRole); + } + + public LDAPObject loadLDAPRoleByName(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, String roleName) { + LDAPIdentityQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider); + Condition roleNameCondition = new LDAPQueryConditionsBuilder().equal(new QueryParameter(getRoleNameLdapAttribute(mapperModel)), roleName); + ldapQuery.where(roleNameCondition); + return ldapQuery.getFirstResult(); + } + + protected Set getExistingMemberships(UserFederationMapperModel mapperModel, LDAPObject ldapRole) { + String memberAttrName = getMembershipLdapAttribute(mapperModel); + Set memberships = new TreeSet(); + Object existingMemberships = ldapRole.getAttribute(memberAttrName); + + if (existingMemberships != null) { + if (existingMemberships instanceof String) { + String existingMembership = existingMemberships.toString().trim(); + if (existingMemberships != null && existingMembership.length() > 0) { + memberships.add(existingMembership); + } + } else if (existingMemberships instanceof Collection) { + Collection exMemberships = (Collection) existingMemberships; + for (String membership : exMemberships) { + if (membership.trim().length() > 0) { + memberships.add(membership); + } + } + } + } + return memberships; + } + + protected List getLDAPRoleMappings(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) { + LDAPIdentityQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider); + String membershipAttr = getMembershipLdapAttribute(mapperModel); + Condition membershipCondition = new LDAPQueryConditionsBuilder().equal(new QueryParameter(membershipAttr), ldapUser.getDn().toString()); + ldapQuery.where(membershipCondition); + return ldapQuery.getResultList(); + } + + protected Set getLDAPRoleMappingsConverted(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, RoleContainerModel roleContainer) { + List ldapRoles = getLDAPRoleMappings(mapperModel, ldapProvider, ldapUser); + + Set roles = new HashSet(); + String roleNameLdapAttr = getRoleNameLdapAttribute(mapperModel); + for (LDAPObject role : ldapRoles) { + String roleName = role.getAttributeAsString(roleNameLdapAttr); + RoleModel modelRole = roleContainer.getRole(roleName); + if (modelRole == null) { + // Add role to local DB + modelRole = roleContainer.addRole(roleName); + } + roles.add(modelRole); + } + return roles; + } + + @Override + public UserModel proxy(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel delegate, RealmModel realm) { + final Mode mode = getMode(mapperModel); + + // For IMPORT mode, all operations are performed against local DB + if (mode == Mode.IMPORT) { + return delegate; + } else { + return new LDAPRoleMappingsUserDelegate(delegate, mapperModel, ldapProvider, ldapUser, realm, mode); + } + } + + @Override + public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPIdentityQuery query) { + } + + + + public class LDAPRoleMappingsUserDelegate extends UserModelDelegate { + + private final UserFederationMapperModel mapperModel; + private final LDAPFederationProvider ldapProvider; + private final LDAPObject ldapUser; + private final RealmModel realm; + private final Mode mode; + + public LDAPRoleMappingsUserDelegate(UserModel user, UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, + RealmModel realm, Mode mode) { + super(user); + this.mapperModel = mapperModel; + this.ldapProvider = ldapProvider; + this.ldapUser = ldapUser; + this.realm = realm; + this.mode = mode; + } + + @Override + public Set getRealmRoleMappings() { + RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm); + if (roleContainer.equals(realm)) { + Set ldapRoleMappings = getLDAPRoleMappingsConverted(mapperModel, ldapProvider, ldapUser, roleContainer); + + if (mode == Mode.LDAP_ONLY) { + // Use just role mappings from LDAP + return ldapRoleMappings; + } else { + // Merge mappings from both DB and LDAP + Set modelRoleMappings = super.getRealmRoleMappings(); + ldapRoleMappings.addAll(modelRoleMappings); + return ldapRoleMappings; + } + } else { + return super.getRealmRoleMappings(); + } + } + + @Override + public Set getClientRoleMappings(ClientModel client) { + RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm); + if (roleContainer.equals(client)) { + Set ldapRoleMappings = getLDAPRoleMappingsConverted(mapperModel, ldapProvider, ldapUser, roleContainer); + + if (mode == Mode.LDAP_ONLY) { + // Use just role mappings from LDAP + return ldapRoleMappings; + } else { + // Merge mappings from both DB and LDAP + Set modelRoleMappings = super.getClientRoleMappings(client); + ldapRoleMappings.addAll(modelRoleMappings); + return ldapRoleMappings; + } + } else { + return super.getClientRoleMappings(client); + } + } + + @Override + public boolean hasRole(RoleModel role) { + Set roles = getRoleMappings(); + return KeycloakModelUtils.hasRole(roles, role); + } + + @Override + public void grantRole(RoleModel role) { + if (mode == Mode.LDAP_ONLY) { + RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm); + + if (role.getContainer().equals(roleContainer)) { + + // We need to create new role mappings in LDAP + addRoleMappingInLDAP(mapperModel, role.getName(), ldapProvider, ldapUser); + } else { + super.grantRole(role); + } + } else { + super.grantRole(role); + } + } + + @Override + public Set getRoleMappings() { + Set modelRoleMappings = super.getRoleMappings(); + + RoleContainerModel targetRoleContainer = getTargetRoleContainer(mapperModel, realm); + Set ldapRoleMappings = getLDAPRoleMappingsConverted(mapperModel, ldapProvider, ldapUser, targetRoleContainer); + + if (mode == Mode.LDAP_ONLY) { + // For LDAP-only we want to retrieve role mappings of target container just from LDAP + Set modelRolesCopy = new HashSet(modelRoleMappings); + for (RoleModel role : modelRolesCopy) { + if (role.getContainer().equals(targetRoleContainer)) { + modelRoleMappings.remove(role); + } + } + } + + modelRoleMappings.addAll(ldapRoleMappings); + return modelRoleMappings; + } + + @Override + public void deleteRoleMapping(RoleModel role) { + RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm); + if (role.getContainer().equals(roleContainer)) { + + LDAPIdentityQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider); + LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder(); + Condition roleNameCondition = conditionsBuilder.equal(new QueryParameter(getRoleNameLdapAttribute(mapperModel)), role.getName()); + Condition membershipCondition = conditionsBuilder.equal(new QueryParameter(getMembershipLdapAttribute(mapperModel)), ldapUser.getDn().toString()); + ldapQuery.where(roleNameCondition).where(membershipCondition); + LDAPObject ldapRole = ldapQuery.getFirstResult(); + + if (ldapRole == null) { + // Role mapping doesn't exist in LDAP. For LDAP_ONLY mode, we don't need to do anything. For READ_ONLY, delete it in local DB. + if (mode == Mode.READ_ONLY) { + super.deleteRoleMapping(role); + } + } else { + // Role mappings exists in LDAP. For LDAP_ONLY mode, we can just delete it in LDAP. For READ_ONLY we can't delete it -> throw error + if (mode == Mode.READ_ONLY) { + throw new ModelException("Not possible to delete LDAP role mappings as mapper mode is READ_ONLY"); + } else { + // Delete ldap role mappings + deleteRoleMappingInLDAP(mapperModel, ldapProvider, ldapUser, ldapRole); + } + } + } else { + super.deleteRoleMapping(role); + } + } + } + + public enum Mode { + /** + * All role mappings are retrieved from LDAP and saved into LDAP + */ + LDAP_ONLY, + + /** + * Read-only LDAP mode. Role mappings are retrieved from LDAP for particular user just at the time when he is imported and then + * they are saved to local keycloak DB. Then all role mappings are always retrieved from keycloak DB, never from LDAP. + * Creating or deleting of role mapping is propagated only to DB. + * + * This is read-only mode LDAP mode and it's good for performance, but when user is put to some role directly in LDAP, it + * won't be seen by Keycloak + */ + IMPORT, + + /** + * Read-only LDAP mode. Role mappings are retrieved from both LDAP and DB and merged together. New role grants are not saved to LDAP but to DB. + * Deleting role mappings, which is mapped to LDAP, will throw an error. + */ + READ_ONLY + } +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java new file mode 100644 index 0000000000..321a0d5fa8 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java @@ -0,0 +1,33 @@ +package org.keycloak.federation.ldap.mappers; + +import java.util.List; + +import org.keycloak.mappers.UserFederationMapper; +import org.keycloak.models.KeycloakSession; +import org.keycloak.provider.ProviderConfigProperty; + +/** + * @author Marek Posolda + */ +public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMapperFactory { + + @Override + public String getHelpText() { + return "Some help text - role mapper - TODO"; + } + + @Override + public List getConfigProperties() { + return null; + } + + @Override + public String getId() { + return "role-ldap-mapper"; + } + + @Override + public UserFederationMapper create(KeycloakSession session) { + return new RoleLDAPFederationMapper(); + } +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractTxAwareLDAPUserModelDelegate.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/TxAwareLDAPUserModelDelegate.java similarity index 75% rename from federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractTxAwareLDAPUserModelDelegate.java rename to federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/TxAwareLDAPUserModelDelegate.java index 4518f4bebf..ebdba95060 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractTxAwareLDAPUserModelDelegate.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/TxAwareLDAPUserModelDelegate.java @@ -10,18 +10,18 @@ import org.keycloak.models.utils.UserModelDelegate; /** * @author Marek Posolda */ -public abstract class AbstractTxAwareLDAPUserModelDelegate extends UserModelDelegate { +public abstract class TxAwareLDAPUserModelDelegate extends UserModelDelegate { - public static final Logger logger = Logger.getLogger(AbstractTxAwareLDAPUserModelDelegate.class); + public static final Logger logger = Logger.getLogger(TxAwareLDAPUserModelDelegate.class); protected LDAPFederationProvider provider; - protected LDAPObject ldapObject; + protected LDAPObject ldapUser; private final LDAPTransaction transaction; - public AbstractTxAwareLDAPUserModelDelegate(UserModel delegate, LDAPFederationProvider provider, LDAPObject ldapObject) { + public TxAwareLDAPUserModelDelegate(UserModel delegate, LDAPFederationProvider provider, LDAPObject ldapUser) { super(delegate); this.provider = provider; - this.ldapObject = ldapObject; + this.ldapUser = ldapUser; this.transaction = findOrCreateTransaction(); } @@ -30,20 +30,18 @@ public abstract class AbstractTxAwareLDAPUserModelDelegate extends UserModelDele } // Try to find transaction in any delegate. We want to enlist just single transaction per all delegates - protected LDAPTransaction findOrCreateTransaction() { + private LDAPTransaction findOrCreateTransaction() { UserModelDelegate delegate = this; while (true) { UserModel deleg = delegate.getDelegate(); if (!(deleg instanceof UserModelDelegate)) { - // Existing transaction not available. Need to create new return new LDAPTransaction(); } else { delegate = (UserModelDelegate) deleg; } - // Check if it's transaction aware delegate - if (delegate instanceof AbstractTxAwareLDAPUserModelDelegate) { - AbstractTxAwareLDAPUserModelDelegate txDelegate = (AbstractTxAwareLDAPUserModelDelegate) delegate; + if (delegate instanceof TxAwareLDAPUserModelDelegate) { + TxAwareLDAPUserModelDelegate txDelegate = (TxAwareLDAPUserModelDelegate) delegate; return txDelegate.getTransaction(); } } @@ -52,7 +50,7 @@ public abstract class AbstractTxAwareLDAPUserModelDelegate extends UserModelDele protected void ensureTransactionStarted() { if (transaction.state == TransactionState.NOT_STARTED) { if (logger.isTraceEnabled()) { - logger.trace("Starting and enlisting transaction for object " + ldapObject.getDn().toString()); + logger.trace("Starting and enlisting transaction for object " + ldapUser.getDn().toString()); } this.provider.getSession().getTransaction().enlistAfterCompletion(transaction); @@ -81,10 +79,10 @@ public abstract class AbstractTxAwareLDAPUserModelDelegate extends UserModelDele } if (logger.isTraceEnabled()) { - logger.trace("Transaction commit! Updating LDAP attributes for object " + ldapObject.getDn().toString() + ", attributes: " + ldapObject.getAttributes()); + logger.trace("Transaction commit! Updating LDAP attributes for object " + ldapUser.getDn().toString() + ", attributes: " + ldapUser.getAttributes()); } - provider.getLdapIdentityStore().update(ldapObject); + provider.getLdapIdentityStore().update(ldapUser); state = TransactionState.FINISHED; } @@ -94,7 +92,7 @@ public abstract class AbstractTxAwareLDAPUserModelDelegate extends UserModelDele throw new IllegalStateException("Transaction in illegal state for rollback: " + state); } - logger.warn("Transaction rollback! Ignoring LDAP updates for object " + ldapObject.getDn().toString()); + logger.warn("Transaction rollback! Ignoring LDAP updates for object " + ldapUser.getDn().toString()); state = TransactionState.FINISHED; } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java index de0b8349a0..c4c30291dd 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java @@ -11,6 +11,9 @@ import org.keycloak.federation.ldap.idm.model.LDAPObject; import org.keycloak.federation.ldap.idm.query.Condition; import org.keycloak.federation.ldap.idm.query.QueryParameter; import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery; +import org.keycloak.mappers.UserFederationMapper; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationProvider; import org.keycloak.models.UserModel; @@ -43,31 +46,15 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap public static final String USER_MODEL_ATTRIBUTE = "user.model.attribute"; public static final String LDAP_ATTRIBUTE = "ldap.attribute"; - - // TODO: Merge with fullname mapper public static final String READ_ONLY = "read.only"; - @Override - public String getHelpText() { - return "Some help text TODO"; - } @Override - public List getConfigProperties() { - return null; - } - - @Override - public String getId() { - return "user-attribute-ldap-mapper"; - } - - @Override - public void importUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapObject, UserModel user, boolean isCreate) { + public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) { String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE); String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE); - Serializable ldapAttrValue = ldapObject.getAttribute(ldapAttrName); + Object ldapAttrValue = ldapUser.getAttribute(ldapAttrName); if (ldapAttrValue != null) { Property userModelProperty = userModelProperties.get(userModelAttrName); @@ -82,7 +69,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap } @Override - public void registerUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapObject, UserModel localUser) { + public void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm) { String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE); String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE); @@ -97,46 +84,42 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap attrValue = localUser.getAttribute(userModelAttrName); } - ldapObject.setAttribute(ldapAttrName, (Serializable) attrValue); + ldapUser.setAttribute(ldapAttrName, attrValue); if (isReadOnly(mapperModel)) { - ldapObject.addReadOnlyAttributeName(ldapAttrName); + ldapUser.addReadOnlyAttributeName(ldapAttrName); } } @Override - public UserModel proxy(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapObject, UserModel delegate) { + public UserModel proxy(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel delegate, RealmModel realm) { if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && !isReadOnly(mapperModel)) { final String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE); final String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE); - AbstractTxAwareLDAPUserModelDelegate txDelegate = new AbstractTxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapObject) { + TxAwareLDAPUserModelDelegate txDelegate = new TxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapUser) { @Override public void setAttribute(String name, String value) { setLDAPAttribute(name, value); - super.setAttribute(name, value); } @Override public void setEmail(String email) { setLDAPAttribute(UserModel.EMAIL, email); - super.setEmail(email); } @Override public void setLastName(String lastName) { setLDAPAttribute(UserModel.LAST_NAME, lastName); - super.setLastName(lastName); } @Override public void setFirstName(String firstName) { setLDAPAttribute(UserModel.FIRST_NAME, firstName); - super.setFirstName(firstName); } @@ -148,7 +131,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap ensureTransactionStarted(); - ldapObject.setAttribute(ldapAttrName, value); + ldapUser.setAttribute(ldapAttrName, value); } } @@ -181,6 +164,6 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap } private boolean isReadOnly(UserFederationMapperModel mapperModel) { - return LDAPUtils.parseBooleanParameter(mapperModel, READ_ONLY); + return parseBooleanParameter(mapperModel, READ_ONLY); } } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java new file mode 100644 index 0000000000..16d4879449 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java @@ -0,0 +1,33 @@ +package org.keycloak.federation.ldap.mappers; + +import java.util.List; + +import org.keycloak.mappers.UserFederationMapper; +import org.keycloak.models.KeycloakSession; +import org.keycloak.provider.ProviderConfigProperty; + +/** + * @author Marek Posolda + */ +public class UserAttributeLDAPFederationMapperFactory extends AbstractLDAPFederationMapperFactory { + + @Override + public String getHelpText() { + return "Some help text TODO"; + } + + @Override + public List getConfigProperties() { + return null; + } + + @Override + public String getId() { + return "user-attribute-ldap-mapper"; + } + + @Override + public UserFederationMapper create(KeycloakSession session) { + return new UserAttributeLDAPFederationMapper(); + } +} diff --git a/federation/ldap/src/main/resources/META-INF/services/org.keycloak.models.UserFederationMapper b/federation/ldap/src/main/resources/META-INF/services/org.keycloak.mappers.UserFederationMapperFactory similarity index 58% rename from federation/ldap/src/main/resources/META-INF/services/org.keycloak.models.UserFederationMapper rename to federation/ldap/src/main/resources/META-INF/services/org.keycloak.mappers.UserFederationMapperFactory index 9d85b2075b..d9e36311cb 100644 --- a/federation/ldap/src/main/resources/META-INF/services/org.keycloak.models.UserFederationMapper +++ b/federation/ldap/src/main/resources/META-INF/services/org.keycloak.mappers.UserFederationMapperFactory @@ -1,2 +1,3 @@ -org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapper -org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapper \ No newline at end of file +org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapperFactory +org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapperFactory +org.keycloak.federation.ldap.mappers.RoleLDAPFederationMapperFactory \ No newline at end of file diff --git a/model/api/src/main/java/org/keycloak/mappers/UserFederationMapper.java b/model/api/src/main/java/org/keycloak/mappers/UserFederationMapper.java new file mode 100644 index 0000000000..509ae64df6 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/mappers/UserFederationMapper.java @@ -0,0 +1,10 @@ +package org.keycloak.mappers; + +import org.keycloak.provider.Provider; + +/** + * @author Marek Posolda + */ +public interface UserFederationMapper extends Provider { + +} diff --git a/model/api/src/main/java/org/keycloak/mappers/UserFederationMapperFactory.java b/model/api/src/main/java/org/keycloak/mappers/UserFederationMapperFactory.java new file mode 100644 index 0000000000..386ce1307c --- /dev/null +++ b/model/api/src/main/java/org/keycloak/mappers/UserFederationMapperFactory.java @@ -0,0 +1,10 @@ +package org.keycloak.mappers; + +import org.keycloak.provider.ConfiguredProvider; +import org.keycloak.provider.ProviderFactory; + +/** + * @author Marek Posolda + */ +public interface UserFederationMapperFactory extends ProviderFactory, ConfiguredProvider { +} diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationMapperSpi.java b/model/api/src/main/java/org/keycloak/mappers/UserFederationMapperSpi.java similarity index 87% rename from model/api/src/main/java/org/keycloak/models/UserFederationMapperSpi.java rename to model/api/src/main/java/org/keycloak/mappers/UserFederationMapperSpi.java index 72c1c428bb..b67a23a850 100644 --- a/model/api/src/main/java/org/keycloak/models/UserFederationMapperSpi.java +++ b/model/api/src/main/java/org/keycloak/mappers/UserFederationMapperSpi.java @@ -1,4 +1,4 @@ -package org.keycloak.models; +package org.keycloak.mappers; import org.keycloak.provider.Provider; import org.keycloak.provider.ProviderFactory; @@ -21,6 +21,6 @@ public class UserFederationMapperSpi implements Spi { @Override public Class getProviderFactoryClass() { - return UserFederationMapper.class; + return UserFederationMapperFactory.class; } } diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationMapper.java b/model/api/src/main/java/org/keycloak/models/UserFederationMapper.java deleted file mode 100644 index 70c99b3286..0000000000 --- a/model/api/src/main/java/org/keycloak/models/UserFederationMapper.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.keycloak.models; - -import org.keycloak.provider.ConfiguredProvider; -import org.keycloak.provider.Provider; -import org.keycloak.provider.ProviderFactory; - -/** - * @author Marek Posolda - */ -public interface UserFederationMapper extends Provider, ProviderFactory, ConfiguredProvider { - -} diff --git a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java index 44f14c22eb..64625c5c5e 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java +++ b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java @@ -252,4 +252,19 @@ public final class KeycloakModelUtils { public static String getMasterRealmAdminApplicationClientId(RealmModel realm) { return realm.getName() + "-realm"; } + + /** + * + * @param roles + * @param targetRole + * @return true if targetRole is in roles (directly or indirectly via composite role) + */ + public static boolean hasRole(Set roles, RoleModel targetRole) { + if (roles.contains(targetRole)) return true; + + for (RoleModel mapping : roles) { + if (mapping.hasRole(targetRole)) return true; + } + return false; + } } diff --git a/model/api/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/model/api/src/main/resources/META-INF/services/org.keycloak.provider.Spi index d830551797..37844e06af 100755 --- a/model/api/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/model/api/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -1,5 +1,5 @@ org.keycloak.models.UserFederationSpi -org.keycloak.models.UserFederationMapperSpi +org.keycloak.mappers.UserFederationMapperSpi org.keycloak.models.RealmSpi org.keycloak.models.UserSessionSpi org.keycloak.models.UserSpi diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java index eb4277f621..b39fc6de7c 100755 --- a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java +++ b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java @@ -33,6 +33,7 @@ import org.keycloak.models.entities.CredentialEntity; import org.keycloak.models.entities.FederatedIdentityEntity; import org.keycloak.models.entities.RoleEntity; import org.keycloak.models.entities.UserEntity; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.Pbkdf2PasswordEncoder; import org.keycloak.util.Time; @@ -394,12 +395,7 @@ public class UserAdapter implements UserModel, Comparable { @Override public boolean hasRole(RoleModel role) { Set roles = getRoleMappings(); - if (roles.contains(role)) return true; - - for (RoleModel mapping : roles) { - if (mapping.hasRole(role)) return true; - } - return false; + return KeycloakModelUtils.hasRole(roles, role); } @Override diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java index eeeeedfcf1..d62d8d8001 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java @@ -952,6 +952,7 @@ public class RealmAdapter implements RealmModel { return null; } + public static String LDAP_MODE = "LDAP_ONLY"; @Override public List getUserFederationMappers() { @@ -992,6 +993,17 @@ public class RealmAdapter implements RealmModel { "user.model.attribute", LDAPConstants.MODIFY_TIMESTAMP, "ldap.attribute", LDAPConstants.MODIFY_TIMESTAMP, "read.only", "true")); + + mappers.add(createMapperModel("realmRoleMpr", "realmRoleMapper", "role-ldap-mapper", + "roles.dn", "ou=RealmRoles,dc=keycloak,dc=org", + "use.realm.roles.mapping", "true", + "mode", LDAP_MODE)); + mappers.add(createMapperModel("financeRoleMpr", "financeRoleMapper", "role-ldap-mapper", + "roles.dn", "ou=FinanceRoles,dc=keycloak,dc=org", + "use.realm.roles.mapping", "false", + "client.id", "finance", + "mode", LDAP_MODE)); + return mappers; } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java index 7c9086bf3b..977a4f5175 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java @@ -404,12 +404,7 @@ public class UserAdapter implements UserModel { @Override public boolean hasRole(RoleModel role) { Set roles = getRoleMappings(); - if (roles.contains(role)) return true; - - for (RoleModel mapping : roles) { - if (mapping.hasRole(role)) return true; - } - return false; + return KeycloakModelUtils.hasRole(roles, role); } protected TypedQuery getUserRoleMappingEntityTypedQuery(RoleModel role) { diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java index f7895f2992..813faab887 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java @@ -23,6 +23,7 @@ import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity; import org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity; import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity; import org.keycloak.models.mongo.utils.MongoModelUtils; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.Pbkdf2PasswordEncoder; import org.keycloak.util.Time; @@ -374,12 +375,7 @@ public class UserAdapter extends AbstractMongoAdapter implement @Override public boolean hasRole(RoleModel role) { Set roles = getRoleMappings(); - if (roles.contains(role)) return true; - - for (RoleModel mapping : roles) { - if (mapping.hasRole(role)) return true; - } - return false; + return KeycloakModelUtils.hasRole(roles, role); } @Override diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java index e680130347..966260a559 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java @@ -13,6 +13,7 @@ import org.keycloak.federation.ldap.LDAPFederationProvider; import org.keycloak.federation.ldap.LDAPFederationProviderFactory; import org.keycloak.federation.ldap.LDAPUtils; import org.keycloak.federation.ldap.idm.model.LDAPObject; +import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.LDAPConstants; import org.keycloak.models.ModelReadOnlyException; @@ -65,6 +66,9 @@ public class FederationProvidersIntegrationTest { LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel); LDAPUtils.removeAllUsers(ldapFedProvider, appRealm); + // Add sample application TODO: REmove this!!!! It's just temporarily needed in SyncProvidersTest until model for federation mappers is implemented + ClientModel finance = appRealm.addClient("finance"); + LDAPObject john = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", "1234"); ldapFedProvider.getLdapIdentityStore().updatePassword(john, "Password1"); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java new file mode 100644 index 0000000000..bdc3574ae8 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java @@ -0,0 +1,367 @@ +package org.keycloak.testsuite.federation; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +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.federation.ldap.LDAPFederationProvider; +import org.keycloak.federation.ldap.LDAPFederationProviderFactory; +import org.keycloak.federation.ldap.LDAPUtils; +import org.keycloak.federation.ldap.idm.model.LDAPObject; +import org.keycloak.federation.ldap.idm.query.Condition; +import org.keycloak.federation.ldap.idm.query.QueryParameter; +import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery; +import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder; +import org.keycloak.federation.ldap.mappers.RoleLDAPFederationMapper; +import org.keycloak.models.AccountRoles; +import org.keycloak.models.ClientModel; +import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.LDAPConstants; +import org.keycloak.models.ModelException; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserFederationMapperModel; +import org.keycloak.models.UserFederationProvider; +import org.keycloak.models.UserFederationProviderModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.cache.RealmAdapter; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.OAuthClient; +import org.keycloak.testsuite.pages.AccountPasswordPage; +import org.keycloak.testsuite.pages.AccountUpdateProfilePage; +import org.keycloak.testsuite.pages.AppPage; +import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.pages.RegisterPage; +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 Marek Posolda + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class LDAPRoleMappingsTest { + + 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) { + FederationTestUtils.addLocalUser(manager.getSession(), appRealm, "mary", "mary@test.com", "password-app"); + + Map ldapConfig = ldapRule.getConfig(); + ldapConfig.put(LDAPConstants.SYNC_REGISTRATIONS, "true"); + ldapConfig.put(LDAPConstants.EDIT_MODE, UserFederationProvider.EditMode.WRITABLE.toString()); + + ldapModel = appRealm.addUserFederationProvider(LDAPFederationProviderFactory.PROVIDER_NAME, ldapConfig, 0, "test-ldap", -1, -1, 0); + + // Delete all LDAP users and add some new for testing + LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel); + LDAPUtils.removeAllUsers(ldapFedProvider, appRealm); + + // Add sample application + ClientModel finance = appRealm.addClient("finance"); + + LDAPObject john = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", "1234"); + ldapFedProvider.getLdapIdentityStore().updatePassword(john, "Password1"); + + LDAPObject mary = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "marykeycloak", "Mary", "Kelly", "mary@email.org", "5678"); + ldapFedProvider.getLdapIdentityStore().updatePassword(mary, "Password1"); + + LDAPObject rob = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "robkeycloak", "Rob", "Brown", "rob@email.org", "8910"); + ldapFedProvider.getLdapIdentityStore().updatePassword(rob, "Password1"); + + } + }) { + + @Override + protected void after() { + // Need to cleanup some LDAP objects after the test + update(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + RoleLDAPFederationMapper roleMapper = new RoleLDAPFederationMapper(); + UserFederationMapperModel roleMapperModel = findRoleMapperModel(appRealm); + LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel); + + LDAPObject ldapRole = roleMapper.loadLDAPRoleByName(roleMapperModel, ldapProvider, "realmRole3"); + if (ldapRole != null) { + ldapProvider.getLdapIdentityStore().remove(ldapRole); + } + } + + }); + + super.after(); + } + + }; + + @ClassRule + public static TestRule chain = RuleChain + .outerRule(ldapRule) + .around(keycloakRule); + + @Rule + public WebRule webRule = new WebRule(this); + + @WebResource + protected OAuthClient oauth; + + @WebResource + protected WebDriver driver; + + @WebResource + protected AppPage appPage; + + @WebResource + protected RegisterPage registerPage; + + @WebResource + protected LoginPage loginPage; + + @WebResource + protected AccountUpdateProfilePage profilePage; + + @WebResource + protected AccountPasswordPage changePasswordPage; + + @Test + public void test01_ldapOnlyRoleMappings() { + // TODO: Remove me!!! + RealmAdapter.LDAP_MODE = "LDAP_ONLY"; + + KeycloakSession session = keycloakRule.startSession(); + try { + RealmModel appRealm = session.realms().getRealmByName("test"); + UserModel john = session.users().getUserByUsername("johnkeycloak", appRealm); + UserModel mary = session.users().getUserByUsername("marykeycloak", appRealm); + + // 1 - Grant some roles in LDAP + + // This role should already exists as it was imported from LDAP + RoleModel realmRole1 = appRealm.getRole("realmRole1"); + john.grantRole(realmRole1); + + // This role should already exists as it was imported from LDAP + RoleModel realmRole2 = appRealm.getRole("realmRole2"); + mary.grantRole(realmRole2); + + RoleModel realmRole3 = appRealm.addRole("realmRole3"); + john.grantRole(realmRole3); + mary.grantRole(realmRole3); + + ClientModel accountApp = appRealm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID); + ClientModel financeApp = appRealm.getClientByClientId("finance"); + + RoleModel manageAccountRole = accountApp.getRole(AccountRoles.MANAGE_ACCOUNT); + RoleModel financeRole1 = financeApp.getRole("financeRole1"); + john.grantRole(financeRole1); + + // 2 - Check that role mappings are not in local Keycloak DB (They are in LDAP). + + UserModel johnDb = session.userStorage().getUserByUsername("johnkeycloak", appRealm); + Set johnDbRoles = johnDb.getRoleMappings(); + Assert.assertFalse(johnDbRoles.contains(realmRole1)); + Assert.assertFalse(johnDbRoles.contains(realmRole2)); + Assert.assertFalse(johnDbRoles.contains(realmRole3)); + Assert.assertFalse(johnDbRoles.contains(financeRole1)); + Assert.assertTrue(johnDbRoles.contains(manageAccountRole)); + + // 3 - Check that role mappings are in LDAP and hence available through federation + + Set johnRoles = john.getRoleMappings(); + Assert.assertTrue(johnRoles.contains(realmRole1)); + Assert.assertFalse(johnRoles.contains(realmRole2)); + Assert.assertTrue(johnRoles.contains(realmRole3)); + Assert.assertTrue(johnRoles.contains(financeRole1)); + Assert.assertTrue(johnRoles.contains(manageAccountRole)); + + Set johnRealmRoles = john.getRealmRoleMappings(); + Assert.assertEquals(2, johnRealmRoles.size()); + Assert.assertTrue(johnRealmRoles.contains(realmRole1)); + Assert.assertTrue(johnRealmRoles.contains(realmRole3)); + + // account roles are not mapped in LDAP. Those are in Keycloak DB + Set johnAccountRoles = john.getClientRoleMappings(accountApp); + Assert.assertTrue(johnAccountRoles.contains(manageAccountRole)); + + Set johnFinanceRoles = john.getClientRoleMappings(financeApp); + Assert.assertEquals(1, johnFinanceRoles.size()); + Assert.assertTrue(johnFinanceRoles.contains(financeRole1)); + + // 4 - Delete some role mappings and check they are deleted + + john.deleteRoleMapping(realmRole3); + john.deleteRoleMapping(realmRole1); + john.deleteRoleMapping(financeRole1); + john.deleteRoleMapping(manageAccountRole); + + johnRoles = john.getRoleMappings(); + Assert.assertFalse(johnRoles.contains(realmRole1)); + Assert.assertFalse(johnRoles.contains(realmRole2)); + Assert.assertFalse(johnRoles.contains(realmRole3)); + Assert.assertFalse(johnRoles.contains(financeRole1)); + Assert.assertFalse(johnRoles.contains(manageAccountRole)); + + // Cleanup + mary.deleteRoleMapping(realmRole2); + mary.deleteRoleMapping(realmRole3); + john.grantRole(manageAccountRole); + } finally { + keycloakRule.stopSession(session, false); + } + } + + @Test + public void test02_readOnlyRoleMappings() { + // TODO: Remove me!!! + RealmAdapter.LDAP_MODE = "READ_ONLY"; + + KeycloakSession session = keycloakRule.startSession(); + try { + RealmModel appRealm = session.realms().getRealmByName("test"); + UserModel mary = session.users().getUserByUsername("marykeycloak", appRealm); + + RoleModel realmRole1 = appRealm.getRole("realmRole1"); + RoleModel realmRole2 = appRealm.getRole("realmRole2"); + RoleModel realmRole3 = appRealm.getRole("realmRole3"); + if (realmRole3 == null) { + realmRole3 = appRealm.addRole("realmRole3"); + } + + // Add some role mappings directly into LDAP + RoleLDAPFederationMapper roleMapper = new RoleLDAPFederationMapper(); + UserFederationMapperModel roleMapperModel = findRoleMapperModel(appRealm); + LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel); + LDAPObject maryLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "marykeycloak"); + roleMapper.addRoleMappingInLDAP(roleMapperModel, "realmRole1", ldapProvider, maryLdap); + roleMapper.addRoleMappingInLDAP(roleMapperModel, "realmRole2", ldapProvider, maryLdap); + + // Add some role to model + mary.grantRole(realmRole3); + + // Assert that mary has both LDAP and DB mapped roles + Set maryRoles = mary.getRealmRoleMappings(); + Assert.assertTrue(maryRoles.contains(realmRole1)); + Assert.assertTrue(maryRoles.contains(realmRole2)); + Assert.assertTrue(maryRoles.contains(realmRole3)); + + // Assert that access through DB will have just DB mapped role + UserModel maryDB = session.userStorage().getUserByUsername("marykeycloak", appRealm); + Set maryDBRoles = maryDB.getRealmRoleMappings(); + Assert.assertFalse(maryDBRoles.contains(realmRole1)); + Assert.assertFalse(maryDBRoles.contains(realmRole2)); + Assert.assertTrue(maryDBRoles.contains(realmRole3)); + + mary.deleteRoleMapping(realmRole3); + try { + mary.deleteRoleMapping(realmRole1); + Assert.fail("It wasn't expected to successfully delete LDAP role mappings in READ_ONLY mode"); + } catch (ModelException expected) { + } + + // Delete role mappings directly in LDAP + deleteRoleMappingsInLDAP(roleMapperModel, roleMapper, ldapProvider, maryLdap, "realmRole1"); + deleteRoleMappingsInLDAP(roleMapperModel, roleMapper, ldapProvider, maryLdap, "realmRole2"); + + // Assert role mappings is not available + maryRoles = mary.getRealmRoleMappings(); + Assert.assertFalse(maryRoles.contains(realmRole1)); + Assert.assertFalse(maryRoles.contains(realmRole2)); + Assert.assertFalse(maryRoles.contains(realmRole3)); + } finally { + keycloakRule.stopSession(session, false); + } + } + + @Test + public void test03_importRoleMappings() { + // TODO: Remove me!!! + RealmAdapter.LDAP_MODE = "IMPORT"; + + KeycloakSession session = keycloakRule.startSession(); + try { + RealmModel appRealm = session.realms().getRealmByName("test"); + + // Add some role mappings directly in LDAP + RoleLDAPFederationMapper roleMapper = new RoleLDAPFederationMapper(); + UserFederationMapperModel roleMapperModel = findRoleMapperModel(appRealm); + LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel); + LDAPObject robLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "robkeycloak"); + roleMapper.addRoleMappingInLDAP(roleMapperModel, "realmRole1", ldapProvider, robLdap); + roleMapper.addRoleMappingInLDAP(roleMapperModel, "realmRole2", ldapProvider, robLdap); + + // Get user and check that he has requested roles from LDAP + UserModel rob = session.users().getUserByUsername("robkeycloak", appRealm); + RoleModel realmRole1 = appRealm.getRole("realmRole1"); + RoleModel realmRole2 = appRealm.getRole("realmRole2"); + RoleModel realmRole3 = appRealm.getRole("realmRole3"); + if (realmRole3 == null) { + realmRole3 = appRealm.addRole("realmRole3"); + } + Set robRoles = rob.getRealmRoleMappings(); + Assert.assertTrue(robRoles.contains(realmRole1)); + Assert.assertTrue(robRoles.contains(realmRole2)); + Assert.assertFalse(robRoles.contains(realmRole3)); + + // Add some role mappings in model and check that user has it + rob.grantRole(realmRole3); + robRoles = rob.getRealmRoleMappings(); + Assert.assertTrue(robRoles.contains(realmRole3)); + + // Delete some role mappings in LDAP and check that it doesn't have any effect and user still has role + deleteRoleMappingsInLDAP(roleMapperModel, roleMapper, ldapProvider, robLdap, "realmRole1"); + deleteRoleMappingsInLDAP(roleMapperModel, roleMapper, ldapProvider, robLdap, "realmRole2"); + robRoles = rob.getRealmRoleMappings(); + Assert.assertTrue(robRoles.contains(realmRole1)); + Assert.assertTrue(robRoles.contains(realmRole2)); + + // Delete role mappings through model and verifies that user doesn't have them anymore + rob.deleteRoleMapping(realmRole1); + rob.deleteRoleMapping(realmRole2); + rob.deleteRoleMapping(realmRole3); + robRoles = rob.getRealmRoleMappings(); + Assert.assertFalse(robRoles.contains(realmRole1)); + Assert.assertFalse(robRoles.contains(realmRole2)); + Assert.assertFalse(robRoles.contains(realmRole3)); + } finally { + keycloakRule.stopSession(session, false); + } + } + + private static UserFederationMapperModel findRoleMapperModel(RealmModel appRealm) { + List fedMappers = appRealm.getUserFederationMappers(); + for (UserFederationMapperModel mapper : fedMappers) { + if ("realmRoleMapper".equals(mapper.getName())) { + return mapper; + } + } + + throw new IllegalStateException("Mapper 'realmRoleMapper' not found"); + } + + private void deleteRoleMappingsInLDAP(UserFederationMapperModel roleMapperModel, RoleLDAPFederationMapper roleMapper, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, String roleName) { + LDAPIdentityQuery ldapQuery = roleMapper.createRoleQuery(roleMapperModel, ldapProvider); + LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder(); + Condition roleNameCondition = conditionsBuilder.equal(new QueryParameter(LDAPConstants.CN), roleName); + ldapQuery.where(roleNameCondition); + LDAPObject ldapRole1 = ldapQuery.getFirstResult(); + roleMapper.deleteRoleMappingInLDAP(roleMapperModel, ldapProvider, ldapUser, ldapRole1); + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java index c44a709178..71a37b77cf 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java @@ -11,6 +11,7 @@ import org.keycloak.federation.ldap.LDAPFederationProvider; import org.keycloak.federation.ldap.LDAPFederationProviderFactory; import org.keycloak.federation.ldap.LDAPUtils; import org.keycloak.federation.ldap.idm.model.LDAPObject; +import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.LDAPConstants; @@ -60,6 +61,9 @@ public class SyncProvidersTest { LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel); LDAPUtils.removeAllUsers(ldapFedProvider, appRealm); + // Add sample application TODO: REmove this!!!! It's just temporarily needed in SyncProvidersTest until model for federation mappers is implemented + ClientModel finance = appRealm.addClient("finance"); + for (int i=1 ; i<=5 ; i++) { LDAPObject ldapUser = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "user" + i, "User" + i + "FN", "User" + i + "LN", "user" + i + "@email.org", "12" + i); ldapFedProvider.getLdapIdentityStore().updatePassword(ldapUser, "Password1"); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/LDAPConfiguration.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/LDAPTestConfiguration.java similarity index 91% rename from testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/LDAPConfiguration.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/LDAPTestConfiguration.java index dd8e26aa4f..c00ca33773 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/LDAPConfiguration.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/ldap/LDAPTestConfiguration.java @@ -15,9 +15,9 @@ import org.keycloak.models.UserFederationProvider; /** * @author Marek Posolda */ -public class LDAPConfiguration { +public class LDAPTestConfiguration { - private static final Logger log = Logger.getLogger(LDAPConfiguration.class); + private static final Logger log = Logger.getLogger(LDAPTestConfiguration.class); private String connectionPropertiesLocation; private boolean startEmbeddedLdapLerver = true; @@ -69,7 +69,7 @@ public class LDAPConfiguration { DEFAULT_VALUES.put(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION, "false"); DEFAULT_VALUES.put(KerberosConstants.KERBEROS_REALM, "KEYCLOAK.ORG"); DEFAULT_VALUES.put(KerberosConstants.SERVER_PRINCIPAL, "HTTP/localhost@KEYCLOAK.ORG"); - URL keytabUrl = LDAPConfiguration.class.getResource("/kerberos/http.keytab"); + URL keytabUrl = LDAPTestConfiguration.class.getResource("/kerberos/http.keytab"); String keyTabPath = new File(keytabUrl.getFile()).getAbsolutePath(); DEFAULT_VALUES.put(KerberosConstants.KEYTAB, keyTabPath); DEFAULT_VALUES.put(KerberosConstants.DEBUG, "true"); @@ -78,11 +78,11 @@ public class LDAPConfiguration { DEFAULT_VALUES.put(KerberosConstants.USE_KERBEROS_FOR_PASSWORD_AUTHENTICATION, "false"); } - public static LDAPConfiguration readConfiguration(String connectionPropertiesLocation) { - LDAPConfiguration ldapConfiguration = new LDAPConfiguration(); - ldapConfiguration.setConnectionPropertiesLocation(connectionPropertiesLocation); - ldapConfiguration.loadConnectionProperties(); - return ldapConfiguration; + public static LDAPTestConfiguration readConfiguration(String connectionPropertiesLocation) { + LDAPTestConfiguration ldapTestConfiguration = new LDAPTestConfiguration(); + ldapTestConfiguration.setConnectionPropertiesLocation(connectionPropertiesLocation); + ldapTestConfiguration.loadConnectionProperties(); + return ldapTestConfiguration; } protected void loadConnectionProperties() { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KerberosRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KerberosRule.java index d2f0ed7ecd..9026557313 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KerberosRule.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KerberosRule.java @@ -5,7 +5,7 @@ import java.net.URL; import org.jboss.logging.Logger; import org.keycloak.testsuite.ldap.EmbeddedServersFactory; -import org.keycloak.testsuite.ldap.LDAPConfiguration; +import org.keycloak.testsuite.ldap.LDAPTestConfiguration; import org.keycloak.testsuite.ldap.LDAPEmbeddedServer; /** @@ -21,7 +21,7 @@ public class KerberosRule extends LDAPRule { this.configLocation = configLocation; // Global kerberos configuration - URL krb5ConfURL = LDAPConfiguration.class.getResource("/kerberos/test-krb5.conf"); + URL krb5ConfURL = LDAPTestConfiguration.class.getResource("/kerberos/test-krb5.conf"); String krb5ConfPath = new File(krb5ConfURL.getFile()).getAbsolutePath(); log.info("Krb5.conf file location is: " + krb5ConfPath); System.setProperty("java.security.krb5.conf", krb5ConfPath); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java index 290341cb4b..1bf9b8e69c 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java @@ -4,7 +4,7 @@ import java.util.Map; import org.junit.rules.ExternalResource; import org.keycloak.testsuite.ldap.EmbeddedServersFactory; -import org.keycloak.testsuite.ldap.LDAPConfiguration; +import org.keycloak.testsuite.ldap.LDAPTestConfiguration; import org.keycloak.testsuite.ldap.LDAPEmbeddedServer; /** @@ -14,15 +14,15 @@ public class LDAPRule extends ExternalResource { public static final String LDAP_CONNECTION_PROPERTIES_LOCATION = "ldap/ldap-connection.properties"; - protected LDAPConfiguration ldapConfiguration; + protected LDAPTestConfiguration ldapTestConfiguration; protected LDAPEmbeddedServer ldapEmbeddedServer; @Override protected void before() throws Throwable { String connectionPropsLocation = getConnectionPropertiesLocation(); - ldapConfiguration = LDAPConfiguration.readConfiguration(connectionPropsLocation); + ldapTestConfiguration = LDAPTestConfiguration.readConfiguration(connectionPropsLocation); - if (ldapConfiguration.isStartEmbeddedLdapLerver()) { + if (ldapTestConfiguration.isStartEmbeddedLdapLerver()) { EmbeddedServersFactory factory = EmbeddedServersFactory.readConfiguration(); ldapEmbeddedServer = createServer(factory); ldapEmbeddedServer.init(); @@ -36,7 +36,7 @@ public class LDAPRule extends ExternalResource { if (ldapEmbeddedServer != null) { ldapEmbeddedServer.stop(); ldapEmbeddedServer = null; - ldapConfiguration = null; + ldapTestConfiguration = null; } } catch (Exception e) { throw new RuntimeException("Error tearDown Embedded LDAP server.", e); @@ -52,6 +52,6 @@ public class LDAPRule extends ExternalResource { } public Map getConfig() { - return ldapConfiguration.getLDAPConfig(); + return ldapTestConfiguration.getLDAPConfig(); } } diff --git a/testsuite/integration/src/test/resources/ldap/users.ldif b/testsuite/integration/src/test/resources/ldap/users.ldif index b41d9f5b9d..b1777d7cd7 100644 --- a/testsuite/integration/src/test/resources/ldap/users.ldif +++ b/testsuite/integration/src/test/resources/ldap/users.ldif @@ -9,13 +9,31 @@ objectclass: top objectclass: organizationalUnit ou: People -dn: ou=Roles,dc=keycloak,dc=org +dn: ou=RealmRoles,dc=keycloak,dc=org objectclass: top objectclass: organizationalUnit -ou: Roles +ou: RealmRoles -dn: ou=Groups,dc=keycloak,dc=org +dn: cn=realmRole1,ou=RealmRoles,dc=keycloak,dc=org +objectclass: top +objectclass: groupOfNames +cn: realmRole1 +member: + +dn: cn=realmRole2,ou=RealmRoles,dc=keycloak,dc=org +objectclass: top +objectclass: groupOfNames +cn: realmRole2 +member: + +dn: ou=FinanceRoles,dc=keycloak,dc=org objectclass: top objectclass: organizationalUnit -ou: Groups +ou: FinanceRoles + +dn: cn=financeRole1,ou=FinanceRoles,dc=keycloak,dc=org +objectclass: top +objectclass: groupOfNames +cn: financeRole1 +member: