keycloak-aplcache
Changes
federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java 43(+26 -17)
federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java 21(+4 -17)
federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapperFactory.java 25(+25 -0)
federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java 40(+14 -26)
federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java 33(+33 -0)
federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapper.java 54(+44 -10)
federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java 473(+473 -0)
federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java 33(+33 -0)
federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/TxAwareLDAPUserModelDelegate.java 26(+12 -14)
federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java 41(+12 -29)
federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java 33(+33 -0)
federation/ldap/src/main/resources/META-INF/services/org.keycloak.mappers.UserFederationMapperFactory 3(+3 -0)
federation/ldap/src/main/resources/META-INF/services/org.keycloak.models.UserFederationMapper 2(+0 -2)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java 12(+12 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java 4(+4 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java 367(+367 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java 4(+4 -0)
Details
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 999f133..5d58438 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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LDAPDn {
- private final List<Entry> entries = new LinkedList<Entry>();
+ private final Deque<Entry> entries = new LinkedList<Entry>();
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<Entry>(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 61b631d..587a81f 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<String> objectClasses = new LinkedList<String>();
private final List<String> readOnlyAttributeNames = new LinkedList<String>();
- private final Map<String, Serializable> attributes = new HashMap<String, Serializable>();
+ private final Map<String, Object> attributes = new HashMap<String, Object>();
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());
+ }
+
+ return (String) attrValue;
+ }
+
- public Map<String, Serializable> getAttributes() {
+ public Map<String, Object> 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 04e6d61..023d143 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<String> objectClasses = new LinkedList<String>();
+ Object uuidValue = ldapAttribute.get();
+ ldapObject.setUuid(this.operationManager.decodeEntryUUID(uuidValue));
+ } else {
+ Set<String> attrValues = new TreeSet<String>();
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<String, Serializable> attrEntry : ldapObject.getAttributes().entrySet()) {
+ for (Map.Entry<String, Object> 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<String> valueCollection = (Collection<String>) attr;
+ Collection<String> valueCollection = (Collection<String>) attrValue;
for (String val : valueCollection) {
attr.add(val);
}
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 2442d30..65181e1 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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*
- * 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 7d3ca02..ad55bc3 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<UserFederationMapperModel> 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<UserFederationMapperModel> 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<UserFederationMapperModel> 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 1e9918d..6c5ac93 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<UserFederationMapperModel> 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/mappers/AbstractLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java
index 357262e..6daa011 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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -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 0000000..6b8f186
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+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 483d825..109b0b0 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<ProviderConfigProperty> 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 0000000..6820345
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class FullNameLDAPFederationMapperFactory extends AbstractLDAPFederationMapperFactory {
+
+ @Override
+ public String getHelpText() {
+ return "Some help text - full name mapper - TODO";
+ }
+
+ @Override
+ public List<ProviderConfigProperty> 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 de89e92..4676a6b 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 0000000..47f288d
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+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<String> rolesSyncedModels = new TreeSet<String>();
+
+ @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<LDAPObject> 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<LDAPObject> 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<String> 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<String> 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<String> trimmed = new HashSet<String>();
+ 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<String> 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<String> 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<String> getExistingMemberships(UserFederationMapperModel mapperModel, LDAPObject ldapRole) {
+ String memberAttrName = getMembershipLdapAttribute(mapperModel);
+ Set<String> memberships = new TreeSet<String>();
+ Object existingMemberships = ldapRole.getAttribute(memberAttrName);
+
+ if (existingMemberships != null) {
+ if (existingMemberships instanceof String) {
+ String existingMembership = existingMemberships.toString().trim();
+ if (existingMemberships != null && existingMembership.length() > 0) {
+ memberships.add(existingMembership);
+ }
+ } else if (existingMemberships instanceof Collection) {
+ Collection<String> exMemberships = (Collection<String>) existingMemberships;
+ for (String membership : exMemberships) {
+ if (membership.trim().length() > 0) {
+ memberships.add(membership);
+ }
+ }
+ }
+ }
+ return memberships;
+ }
+
+ protected List<LDAPObject> 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<RoleModel> getLDAPRoleMappingsConverted(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, RoleContainerModel roleContainer) {
+ List<LDAPObject> ldapRoles = getLDAPRoleMappings(mapperModel, ldapProvider, ldapUser);
+
+ Set<RoleModel> roles = new HashSet<RoleModel>();
+ 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<RoleModel> getRealmRoleMappings() {
+ RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
+ if (roleContainer.equals(realm)) {
+ Set<RoleModel> 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<RoleModel> modelRoleMappings = super.getRealmRoleMappings();
+ ldapRoleMappings.addAll(modelRoleMappings);
+ return ldapRoleMappings;
+ }
+ } else {
+ return super.getRealmRoleMappings();
+ }
+ }
+
+ @Override
+ public Set<RoleModel> getClientRoleMappings(ClientModel client) {
+ RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
+ if (roleContainer.equals(client)) {
+ Set<RoleModel> 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<RoleModel> modelRoleMappings = super.getClientRoleMappings(client);
+ ldapRoleMappings.addAll(modelRoleMappings);
+ return ldapRoleMappings;
+ }
+ } else {
+ return super.getClientRoleMappings(client);
+ }
+ }
+
+ @Override
+ public boolean hasRole(RoleModel role) {
+ Set<RoleModel> 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<RoleModel> getRoleMappings() {
+ Set<RoleModel> modelRoleMappings = super.getRoleMappings();
+
+ RoleContainerModel targetRoleContainer = getTargetRoleContainer(mapperModel, realm);
+ Set<RoleModel> 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<RoleModel> modelRolesCopy = new HashSet<RoleModel>(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 0000000..321a0d5
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMapperFactory {
+
+ @Override
+ public String getHelpText() {
+ return "Some help text - role mapper - TODO";
+ }
+
+ @Override
+ public List<ProviderConfigProperty> 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/UserAttributeLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java
index de0b834..c4c3029 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<ProviderConfigProperty> 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<Object> 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 0000000..16d4879
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserAttributeLDAPFederationMapperFactory extends AbstractLDAPFederationMapperFactory {
+
+ @Override
+ public String getHelpText() {
+ return "Some help text TODO";
+ }
+
+ @Override
+ public List<ProviderConfigProperty> 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.mappers.UserFederationMapperFactory b/federation/ldap/src/main/resources/META-INF/services/org.keycloak.mappers.UserFederationMapperFactory
new file mode 100644
index 0000000..d9e3631
--- /dev/null
+++ b/federation/ldap/src/main/resources/META-INF/services/org.keycloak.mappers.UserFederationMapperFactory
@@ -0,0 +1,3 @@
+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 0000000..509ae64
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface UserFederationMapper extends Provider {
+
+}
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 44f14c2..64625c5 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<RoleModel> 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 d830551..37844e0 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 eb4277f..b39fc6d 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<RoleModel> 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 eeeeedf..d62d8d8 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<UserFederationMapperModel> 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 7c9086b..977a4f5 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<RoleModel> 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<UserRoleMappingEntity> 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 f7895f2..813faab 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<MongoUserEntity> implement
@Override
public boolean hasRole(RoleModel role) {
Set<RoleModel> 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 e680130..966260a 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 0000000..bdc3574
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@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<String,String> 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<RoleModel> 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<RoleModel> 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<RoleModel> 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<RoleModel> johnAccountRoles = john.getClientRoleMappings(accountApp);
+ Assert.assertTrue(johnAccountRoles.contains(manageAccountRole));
+
+ Set<RoleModel> 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<RoleModel> 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<RoleModel> 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<RoleModel> 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<UserFederationMapperModel> 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 c44a709..71a37b7 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/rule/KerberosRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KerberosRule.java
index d2f0ed7..9026557 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 290341c..1bf9b8e 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<String, String> 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 b41d9f5..b1777d7 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: