ldap port

This commit is contained in:
Bill Burke 2016-11-04 21:25:47 -04:00
parent ccaac40863
commit c75dcb90c2
96 changed files with 10837 additions and 141 deletions

View file

@ -1,90 +0,0 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.representations.idm;
import java.util.Map;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class StorageProviderRepresentation {
private String id;
private String displayName;
private String providerName;
private Map<String, String> config;
private int priority;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public String getProviderName() {
return providerName;
}
public void setProviderName(String providerName) {
this.providerName = providerName;
}
public Map<String, String> getConfig() {
return config;
}
public void setConfig(Map<String, String> config) {
this.config = config;
}
public int getPriority() {
return priority;
}
public void setPriority(int priority) {
this.priority = priority;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
StorageProviderRepresentation that = (StorageProviderRepresentation) o;
if (!id.equals(that.id)) return false;
return true;
}
@Override
public int hashCode() {
return id.hashCode();
}
}

View file

@ -18,6 +18,7 @@
package org.keycloak.federation.kerberos;
import org.keycloak.common.constants.KerberosConstants;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.UserFederationProviderModel;
import java.util.Map;
@ -29,31 +30,41 @@ import java.util.Map;
*/
public abstract class CommonKerberosConfig {
private final UserFederationProviderModel providerModel;
protected UserFederationProviderModel providerModel;
protected ComponentModel componentModel;
public CommonKerberosConfig(UserFederationProviderModel userFederationProvider) {
this.providerModel = userFederationProvider;
}
public CommonKerberosConfig(ComponentModel componentModel) {
this.componentModel = componentModel;
}
// Should be always true for KerberosFederationProvider
public boolean isAllowKerberosAuthentication() {
return Boolean.valueOf(getConfig().get(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION));
if (providerModel != null) return Boolean.valueOf(getConfig().get(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION));
else return Boolean.valueOf(componentModel.getConfig().getFirst(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION));
}
public String getKerberosRealm() {
return getConfig().get(KerberosConstants.KERBEROS_REALM);
if (providerModel != null) return getConfig().get(KerberosConstants.KERBEROS_REALM);
else return componentModel.getConfig().getFirst(KerberosConstants.KERBEROS_REALM);
}
public String getServerPrincipal() {
return getConfig().get(KerberosConstants.SERVER_PRINCIPAL);
if (providerModel != null) return getConfig().get(KerberosConstants.SERVER_PRINCIPAL);
else return componentModel.getConfig().getFirst(KerberosConstants.SERVER_PRINCIPAL);
}
public String getKeyTab() {
return getConfig().get(KerberosConstants.KEYTAB);
if (providerModel != null) return getConfig().get(KerberosConstants.KEYTAB);
else return componentModel.getConfig().getFirst(KerberosConstants.KEYTAB);
}
public boolean isDebug() {
return Boolean.valueOf(getConfig().get(KerberosConstants.DEBUG));
if (providerModel != null) return Boolean.valueOf(getConfig().get(KerberosConstants.DEBUG));
else return Boolean.valueOf(componentModel.getConfig().getFirst(KerberosConstants.DEBUG));
}
protected Map<String, String> getConfig() {

102
federation/ldap2/pom.xml Executable file
View file

@ -0,0 +1,102 @@
<!--
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>2.4.0.CR1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-ldap-storage</artifactId>
<name>Keycloak LDAP UserStoreProvider</name>
<description />
<properties>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
</properties>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-kerberos-federation</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>
<scope>provided</scope>
<exclusions>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.transaction</groupId>
<artifactId>jboss-transaction-api_1.2_spec</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,182 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.models.LDAPConstants;
import javax.naming.directory.SearchControls;
import java.util.Collection;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*
*/
public class LDAPConfig {
private final MultivaluedHashMap<String, String> config;
public LDAPConfig(MultivaluedHashMap<String, String> config) {
this.config = config;
}
public String getConnectionUrl() {
return config.getFirst(LDAPConstants.CONNECTION_URL);
}
public String getFactoryName() {
// hardcoded for now
return "com.sun.jndi.ldap.LdapCtxFactory";
}
public String getAuthType() {
String value = config.getFirst(LDAPConstants.AUTH_TYPE);
if (value == null) {
return LDAPConstants.AUTH_TYPE_SIMPLE;
} else {
return value;
}
}
public String getUseTruststoreSpi() {
return config.getFirst(LDAPConstants.USE_TRUSTSTORE_SPI);
}
public String getUsersDn() {
String usersDn = config.getFirst(LDAPConstants.USERS_DN);
if (usersDn == null) {
// Just for the backwards compatibility 1.2 -> 1.3 . Should be removed later.
usersDn = config.getFirst("userDnSuffix");
}
return usersDn;
}
public Collection<String> getUserObjectClasses() {
String objClassesCfg = config.getFirst(LDAPConstants.USER_OBJECT_CLASSES);
String objClassesStr = (objClassesCfg != null && objClassesCfg.length() > 0) ? objClassesCfg.trim() : "inetOrgPerson,organizationalPerson";
String[] objectClasses = objClassesStr.split(",");
// Trim them
Set<String> userObjClasses = new HashSet<>();
for (int i=0 ; i<objectClasses.length ; i++) {
userObjClasses.add(objectClasses[i].trim());
}
return userObjClasses;
}
public String getBindDN() {
return config.getFirst(LDAPConstants.BIND_DN);
}
public String getBindCredential() {
return config.getFirst(LDAPConstants.BIND_CREDENTIAL);
}
public String getVendor() {
return config.getFirst(LDAPConstants.VENDOR);
}
public boolean isActiveDirectory() {
String vendor = getVendor();
return vendor != null && vendor.equals(LDAPConstants.VENDOR_ACTIVE_DIRECTORY);
}
public String getConnectionPooling() {
return config.getFirst(LDAPConstants.CONNECTION_POOLING);
}
public Properties getAdditionalConnectionProperties() {
// not supported for now
return null;
}
public int getSearchScope() {
String searchScope = config.getFirst(LDAPConstants.SEARCH_SCOPE);
return searchScope == null ? SearchControls.SUBTREE_SCOPE : Integer.parseInt(searchScope);
}
public String getUuidLDAPAttributeName() {
String uuidAttrName = config.getFirst(LDAPConstants.UUID_LDAP_ATTRIBUTE);
if (uuidAttrName == null) {
// Differences of unique attribute among various vendors
String vendor = getVendor();
uuidAttrName = LDAPConstants.getUuidAttributeName(vendor);
}
return uuidAttrName;
}
public boolean isPagination() {
String pagination = config.getFirst(LDAPConstants.PAGINATION);
return Boolean.parseBoolean(pagination);
}
public int getBatchSizeForSync() {
String pageSizeConfig = config.getFirst(LDAPConstants.BATCH_SIZE_FOR_SYNC);
return pageSizeConfig!=null ? Integer.parseInt(pageSizeConfig) : LDAPConstants.DEFAULT_BATCH_SIZE_FOR_SYNC;
}
public String getUsernameLdapAttribute() {
String username = config.getFirst(LDAPConstants.USERNAME_LDAP_ATTRIBUTE);
if (username == null) {
username = isActiveDirectory() ? LDAPConstants.CN : LDAPConstants.UID;
}
return username;
}
public String getRdnLdapAttribute() {
String rdn = config.getFirst(LDAPConstants.RDN_LDAP_ATTRIBUTE);
if (rdn == null) {
rdn = getUsernameLdapAttribute();
if (rdn.equalsIgnoreCase(LDAPConstants.SAM_ACCOUNT_NAME)) {
// Just for the backwards compatibility 1.2 -> 1.3 . Should be removed later.
rdn = LDAPConstants.CN;
}
}
return rdn;
}
public String getCustomUserSearchFilter() {
String customFilter = config.getFirst(LDAPConstants.CUSTOM_USER_SEARCH_FILTER);
if (customFilter != null) {
customFilter = customFilter.trim();
if (customFilter.length() > 0) {
return customFilter;
}
}
return null;
}
public LDAPStorageProviderFactory.EditMode getEditMode() {
String editModeString = config.getFirst(LDAPConstants.EDIT_MODE);
if (editModeString == null) {
return LDAPStorageProviderFactory.EditMode.READ_ONLY;
} else {
return LDAPStorageProviderFactory.EditMode.valueOf(editModeString);
}
}
}

View file

@ -0,0 +1,95 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap;
import org.jboss.logging.Logger;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.LDAPConstants;
import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LDAPIdentityStoreRegistry {
private static final Logger logger = Logger.getLogger(LDAPIdentityStoreRegistry.class);
private Map<String, LDAPIdentityStoreContext> ldapStores = new ConcurrentHashMap<String, LDAPIdentityStoreContext>();
public LDAPIdentityStore getLdapStore(ComponentModel model) {
LDAPIdentityStoreContext context = ldapStores.get(model.getId());
// Ldap config might have changed for the realm. In this case, we must re-initialize
MultivaluedHashMap<String, String> config = model.getConfig();
if (context == null || !config.equals(context.config)) {
logLDAPConfig(model.getName(), config);
LDAPIdentityStore store = createLdapIdentityStore(config);
context = new LDAPIdentityStoreContext(config, store);
ldapStores.put(model.getId(), context);
}
return context.store;
}
// Don't log LDAP password
private void logLDAPConfig(String fedProviderDisplayName, MultivaluedHashMap<String, String> ldapConfig) {
MultivaluedHashMap<String, String> copy = new MultivaluedHashMap<String, String>(ldapConfig);
copy.remove(LDAPConstants.BIND_CREDENTIAL);
logger.infof("Creating new LDAP based partition manager for the Federation provider: " + fedProviderDisplayName + ", LDAP Configuration: " + copy);
}
/**
* @param ldapConfig from realm
* @return PartitionManager instance based on LDAP store
*/
public static LDAPIdentityStore createLdapIdentityStore(MultivaluedHashMap<String, String> ldapConfig) {
LDAPConfig cfg = new LDAPConfig(ldapConfig);
checkSystemProperty("com.sun.jndi.ldap.connect.pool.authentication", "none simple");
checkSystemProperty("com.sun.jndi.ldap.connect.pool.initsize", "1");
checkSystemProperty("com.sun.jndi.ldap.connect.pool.maxsize", "1000");
checkSystemProperty("com.sun.jndi.ldap.connect.pool.prefsize", "5");
checkSystemProperty("com.sun.jndi.ldap.connect.pool.timeout", "300000");
checkSystemProperty("com.sun.jndi.ldap.connect.pool.protocol", "plain");
checkSystemProperty("com.sun.jndi.ldap.connect.pool.debug", "off");
return new LDAPIdentityStore(cfg);
}
private static void checkSystemProperty(String name, String defaultValue) {
if (System.getProperty(name) == null) {
System.setProperty(name, defaultValue);
}
}
private class LDAPIdentityStoreContext {
private LDAPIdentityStoreContext(MultivaluedHashMap<String, String> config, LDAPIdentityStore store) {
this.config = config;
this.store = store;
}
private MultivaluedHashMap<String, String> config;
private LDAPIdentityStore store;
}
}

View file

@ -0,0 +1,645 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap;
import org.jboss.logging.Logger;
import org.keycloak.common.constants.KerberosConstants;
import org.keycloak.component.ComponentModel;
import org.keycloak.credential.CredentialAuthentication;
import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialInputUpdater;
import org.keycloak.credential.CredentialInputValidator;
import org.keycloak.credential.CredentialModel;
import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator;
import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator;
import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException;
import org.keycloak.models.ModelReadOnlyException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.managers.UserManager;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.Condition;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.storage.ldap.kerberos.LDAPProviderKerberosConfig;
import org.keycloak.storage.ldap.mappers.LDAPMappersComparator;
import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.PasswordUpdated;
import org.keycloak.storage.user.ImportedUserValidation;
import org.keycloak.storage.user.UserLookupProvider;
import org.keycloak.storage.user.UserQueryProvider;
import org.keycloak.storage.user.UserRegistrationProvider;
import javax.naming.AuthenticationException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class LDAPStorageProvider implements UserStorageProvider,
CredentialInputValidator,
CredentialInputUpdater,
CredentialAuthentication,
UserLookupProvider,
UserRegistrationProvider,
UserQueryProvider,
ImportedUserValidation {
private static final Logger logger = Logger.getLogger(LDAPStorageProvider.class);
protected LDAPStorageProviderFactory factory;
protected KeycloakSession session;
protected ComponentModel model;
protected LDAPIdentityStore ldapIdentityStore;
protected LDAPStorageProviderFactory.EditMode editMode;
protected LDAPProviderKerberosConfig kerberosConfig;
protected PasswordUpdated updater;
protected final Set<String> supportedCredentialTypes = new HashSet<>();
public LDAPStorageProvider(LDAPStorageProviderFactory factory, KeycloakSession session, ComponentModel model, LDAPIdentityStore ldapIdentityStore) {
this.factory = factory;
this.session = session;
this.model = model;
this.ldapIdentityStore = ldapIdentityStore;
this.kerberosConfig = new LDAPProviderKerberosConfig(model);
this.editMode = ldapIdentityStore.getConfig().getEditMode();
supportedCredentialTypes.add(UserCredentialModel.PASSWORD);
if (kerberosConfig.isAllowKerberosAuthentication()) {
supportedCredentialTypes.add(UserCredentialModel.KERBEROS);
}
}
public void setUpdater(PasswordUpdated updater) {
this.updater = updater;
}
public KeycloakSession getSession() {
return session;
}
public LDAPIdentityStore getLdapIdentityStore() {
return this.ldapIdentityStore;
}
public LDAPStorageProviderFactory.EditMode getEditMode() {
return editMode;
}
public ComponentModel getModel() {
return model;
}
@Override
public UserModel validate(RealmModel realm, UserModel local) {
LDAPObject ldapObject = loadAndValidateUser(realm, local);
if (ldapObject == null) {
return null;
}
return proxy(realm, local, ldapObject);
}
protected UserModel proxy(RealmModel realm, UserModel local, LDAPObject ldapObject) {
UserModel proxied = local;
switch (editMode) {
case READ_ONLY:
proxied = new ReadonlyLDAPUserModelDelegate(local, this);
break;
case WRITABLE:
proxied = new WritableLDAPUserModelDelegate(local, this, ldapObject);
break;
case UNSYNCED:
proxied = new UnsyncedLDAPUserModelDelegate(local, this);
}
List<ComponentModel> mappers = realm.getComponents(model.getId(), LDAPStorageMapper.class.getName());
List<ComponentModel> sortedMappers = sortMappersAsc(mappers);
for (ComponentModel mapperModel : sortedMappers) {
LDAPStorageMapper ldapMapper = getMapper(mapperModel);
proxied = ldapMapper.proxy(mapperModel, this, ldapObject, proxied, realm);
}
return proxied;
}
@Override
public boolean supportsCredentialAuthenticationFor(String type) {
return type.equals(CredentialModel.KERBEROS) && kerberosConfig.isAllowKerberosAuthentication();
}
@Override
public List<UserModel> searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm) {
return Collections.EMPTY_LIST;
}
@Override
public void grantToAllUsers(RealmModel realm, RoleModel role) {
}
public boolean synchronizeRegistrations() {
return "true".equalsIgnoreCase(model.getConfig().getFirst(LDAPConstants.SYNC_REGISTRATIONS)) && editMode == LDAPStorageProviderFactory.EditMode.WRITABLE;
}
@Override
public UserModel addUser(RealmModel realm, String username) {
if (editMode == LDAPStorageProviderFactory.EditMode.READ_ONLY || editMode == LDAPStorageProviderFactory.EditMode.UNSYNCED) throw new IllegalStateException("Registration is not supported by this ldap server");
if (!synchronizeRegistrations()) throw new IllegalStateException("Registration is not supported by this ldap server");
UserModel user = session.userLocalStorage().addUser(realm, username);
user.setFederationLink(model.getId());
LDAPObject ldapUser = LDAPUtils.addUserToLDAP(this, realm, user);
LDAPUtils.checkUuid(ldapUser, ldapIdentityStore.getConfig());
user.setSingleAttribute(LDAPConstants.LDAP_ID, ldapUser.getUuid());
user.setSingleAttribute(LDAPConstants.LDAP_ENTRY_DN, ldapUser.getDn().toString());
return proxy(realm, user, ldapUser);
}
@Override
public boolean removeUser(RealmModel realm, UserModel user) {
if (editMode == LDAPStorageProviderFactory.EditMode.READ_ONLY || editMode == LDAPStorageProviderFactory.EditMode.UNSYNCED) {
logger.warnf("User '%s' can't be deleted in LDAP as editMode is '%s'. Deleting user just from Keycloak DB, but he will be re-imported from LDAP again once searched in Keycloak", user.getUsername(), editMode.toString());
return true;
}
LDAPObject ldapObject = loadAndValidateUser(realm, user);
if (ldapObject == null) {
logger.warnf("User '%s' can't be deleted from LDAP as it doesn't exist here", user.getUsername());
return false;
}
ldapIdentityStore.remove(ldapObject);
return true;
}
@Override
public UserModel getUserById(String id, RealmModel realm) {
StorageId storageId = new StorageId(id);
return getUserByUsername(storageId.getExternalId(), realm);
}
@Override
public int getUsersCount(RealmModel realm) {
return 0;
}
@Override
public List<UserModel> getUsers(RealmModel realm) {
return Collections.EMPTY_LIST;
}
@Override
public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults) {
return Collections.EMPTY_LIST;
}
@Override
public List<UserModel> searchForUser(String search, RealmModel realm) {
return searchForUser(search, realm, 0, Integer.MAX_VALUE - 1);
}
@Override
public List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults) {
Map<String, String> attributes = new HashMap<String, String>();
int spaceIndex = search.lastIndexOf(' ');
if (spaceIndex > -1) {
String firstName = search.substring(0, spaceIndex).trim();
String lastName = search.substring(spaceIndex).trim();
attributes.put(UserModel.FIRST_NAME, firstName);
attributes.put(UserModel.LAST_NAME, lastName);
} else if (search.indexOf('@') > -1) {
attributes.put(UserModel.USERNAME, search.trim().toLowerCase());
attributes.put(UserModel.EMAIL, search.trim().toLowerCase());
} else {
attributes.put(UserModel.LAST_NAME, search.trim());
attributes.put(UserModel.USERNAME, search.trim().toLowerCase());
}
return searchForUser(attributes, realm, firstResult, maxResults);
}
@Override
public List<UserModel> searchForUser(Map<String, String> params, RealmModel realm) {
return searchForUser(params, realm, 0, Integer.MAX_VALUE - 1);
}
@Override
public List<UserModel> searchForUser(Map<String, String> params, RealmModel realm, int firstResult, int maxResults) {
List<UserModel> searchResults =new LinkedList<UserModel>();
List<LDAPObject> ldapUsers = searchLDAP(realm, params, maxResults + firstResult);
int counter = 0;
for (LDAPObject ldapUser : ldapUsers) {
if (counter++ < firstResult) continue;
String ldapUsername = LDAPUtils.getUsername(ldapUser, this.ldapIdentityStore.getConfig());
if (session.userLocalStorage().getUserByUsername(ldapUsername, realm) == null) {
UserModel imported = importUserFromLDAP(session, realm, ldapUser);
searchResults.add(imported);
}
}
return searchResults;
}
@Override
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group) {
return getGroupMembers(realm, group, 0, Integer.MAX_VALUE - 1);
}
@Override
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
List<ComponentModel> mappers = realm.getComponents(model.getId(), LDAPStorageMapper.class.getName());
List<ComponentModel> sortedMappers = sortMappersAsc(mappers);
for (ComponentModel mapperModel : sortedMappers) {
LDAPStorageMapper ldapMapper = getMapper(mapperModel);
List<UserModel> users = ldapMapper.getGroupMembers(mapperModel, this, realm, group, firstResult, maxResults);
// Sufficient for now
if (users.size() > 0) {
return users;
}
}
return Collections.emptyList();
}
public List<UserModel> loadUsersByUsernames(List<String> usernames, RealmModel realm) {
List<UserModel> result = new ArrayList<>();
for (String username : usernames) {
UserModel kcUser = session.users().getUserByUsername(username, realm);
if (kcUser == null) {
logger.warnf("User '%s' referenced by membership wasn't found in LDAP", username);
} else if (!model.getId().equals(kcUser.getFederationLink())) {
logger.warnf("Incorrect federation provider of user '%s'", kcUser.getUsername());
} else {
result.add(kcUser);
}
}
return result;
}
protected List<LDAPObject> searchLDAP(RealmModel realm, Map<String, String> attributes, int maxResults) {
List<LDAPObject> results = new ArrayList<LDAPObject>();
if (attributes.containsKey(UserModel.USERNAME)) {
LDAPObject user = loadLDAPUserByUsername(realm, attributes.get(UserModel.USERNAME));
if (user != null) {
results.add(user);
}
}
if (attributes.containsKey(UserModel.EMAIL)) {
LDAPObject user = queryByEmail(realm, attributes.get(UserModel.EMAIL));
if (user != null) {
results.add(user);
}
}
if (attributes.containsKey(UserModel.FIRST_NAME) || attributes.containsKey(UserModel.LAST_NAME)) {
LDAPQuery ldapQuery = LDAPUtils.createQueryForUserSearch(this, realm);
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
// Mapper should replace parameter with correct LDAP mapped attributes
if (attributes.containsKey(UserModel.FIRST_NAME)) {
ldapQuery.addWhereCondition(conditionsBuilder.equal(UserModel.FIRST_NAME, attributes.get(UserModel.FIRST_NAME)));
}
if (attributes.containsKey(UserModel.LAST_NAME)) {
ldapQuery.addWhereCondition(conditionsBuilder.equal(UserModel.LAST_NAME, attributes.get(UserModel.LAST_NAME)));
}
List<LDAPObject> ldapObjects = ldapQuery.getResultList();
results.addAll(ldapObjects);
}
return results;
}
/**
* @param local
* @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());
if (ldapUser == null) {
return null;
}
LDAPUtils.checkUuid(ldapUser, ldapIdentityStore.getConfig());
if (ldapUser.getUuid().equals(local.getFirstAttribute(LDAPConstants.LDAP_ID))) {
return ldapUser;
} else {
logger.warnf("LDAP User invalid. ID doesn't match. ID from LDAP [%s], LDAP ID from local DB: [%s]", ldapUser.getUuid(), local.getFirstAttribute(LDAPConstants.LDAP_ID));
return null;
}
}
@Override
public UserModel getUserByUsername(String username, RealmModel realm) {
LDAPObject ldapUser = loadLDAPUserByUsername(realm, username);
if (ldapUser == null) {
return null;
}
return importUserFromLDAP(session, realm, ldapUser);
}
protected UserModel importUserFromLDAP(KeycloakSession session, RealmModel realm, LDAPObject ldapUser) {
String ldapUsername = LDAPUtils.getUsername(ldapUser, ldapIdentityStore.getConfig());
LDAPUtils.checkUuid(ldapUser, ldapIdentityStore.getConfig());
UserModel imported = session.userLocalStorage().addUser(realm, ldapUsername);
imported.setEnabled(true);
List<ComponentModel> mappers = realm.getComponents(model.getId(), LDAPStorageMapper.class.getName());
List<ComponentModel> sortedMappers = sortMappersDesc(mappers);
for (ComponentModel mapperModel : sortedMappers) {
if (logger.isTraceEnabled()) {
logger.tracef("Using mapper %s during import user from LDAP", mapperModel);
}
LDAPStorageMapper ldapMapper = getMapper(mapperModel);
ldapMapper.onImportUserFromLDAP(mapperModel, this, ldapUser, imported, realm, true);
}
String userDN = ldapUser.getDn().toString();
imported.setFederationLink(model.getId());
imported.setSingleAttribute(LDAPConstants.LDAP_ID, ldapUser.getUuid());
imported.setSingleAttribute(LDAPConstants.LDAP_ENTRY_DN, userDN);
logger.debugf("Imported new user from LDAP to Keycloak DB. Username: [%s], Email: [%s], LDAP_ID: [%s], LDAP Entry DN: [%s]", imported.getUsername(), imported.getEmail(),
ldapUser.getUuid(), userDN);
return proxy(realm, imported, ldapUser);
}
protected LDAPObject queryByEmail(RealmModel realm, String email) {
LDAPQuery ldapQuery = LDAPUtils.createQueryForUserSearch(this, realm);
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
// Mapper should replace "email" in parameter name with correct LDAP mapped attribute
Condition emailCondition = conditionsBuilder.equal(UserModel.EMAIL, email);
ldapQuery.addWhereCondition(emailCondition);
return ldapQuery.getFirstResult();
}
@Override
public UserModel getUserByEmail(String email, RealmModel realm) {
LDAPObject ldapUser = queryByEmail(realm, email);
if (ldapUser == null) {
return null;
}
// Check here if user already exists
String ldapUsername = LDAPUtils.getUsername(ldapUser, ldapIdentityStore.getConfig());
if (session.userLocalStorage().getUserByUsername(ldapUsername, realm) != null) {
throw new ModelDuplicateException("User with username '" + ldapUsername + "' already exists in Keycloak. It conflicts with LDAP user with email '" + email + "'");
}
return importUserFromLDAP(session, realm, ldapUser);
}
@Override
public void preRemove(RealmModel realm) {
// complete Don't think we have to do anything
}
@Override
public void preRemove(RealmModel realm, RoleModel role) {
// TODO: Maybe mappers callback to ensure role deletion propagated to LDAP by RoleLDAPFederationMapper?
}
@Override
public void preRemove(RealmModel realm, GroupModel group) {
}
public boolean validPassword(RealmModel realm, UserModel user, String password) {
if (kerberosConfig.isAllowKerberosAuthentication() && kerberosConfig.isUseKerberosForPasswordAuthentication()) {
// Use Kerberos JAAS (Krb5LoginModule)
KerberosUsernamePasswordAuthenticator authenticator = factory.createKerberosUsernamePasswordAuthenticator(kerberosConfig);
return authenticator.validUser(user.getUsername(), password);
} else {
// Use Naming LDAP API
LDAPObject ldapUser = loadAndValidateUser(realm, user);
try {
ldapIdentityStore.validatePassword(ldapUser, password);
return true;
} catch (AuthenticationException ae) {
boolean processed = false;
List<ComponentModel> mappers = realm.getComponents(model.getId(), LDAPStorageMapper.class.getName());
List<ComponentModel> sortedMappers = sortMappersDesc(mappers);
for (ComponentModel mapperModel : sortedMappers) {
if (logger.isTraceEnabled()) {
logger.tracef("Using mapper %s during import user from LDAP", mapperModel);
}
LDAPStorageMapper ldapMapper = getMapper(mapperModel);
processed = processed || ldapMapper.onAuthenticationFailure(mapperModel, this, ldapUser, user, ae, realm);
}
return processed;
}
}
}
@Override
public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
if (!CredentialModel.PASSWORD.equals(input.getType()) || ! (input instanceof UserCredentialModel)) return false;
if (editMode == LDAPStorageProviderFactory.EditMode.READ_ONLY) {
throw new ModelReadOnlyException("Federated storage is not writable");
} else if (editMode == LDAPStorageProviderFactory.EditMode.WRITABLE) {
LDAPIdentityStore ldapIdentityStore = getLdapIdentityStore();
UserCredentialModel cred = (UserCredentialModel)input;
String password = cred.getValue();
LDAPObject ldapUser = loadAndValidateUser(realm, user);
ldapIdentityStore.updatePassword(ldapUser, password);
if (updater != null) updater.passwordUpdated(user, ldapUser, input);
return true;
} else {
return false;
}
}
@Override
public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {
}
@Override
public Set<String> getDisableableCredentialTypes(RealmModel realm, UserModel user) {
return Collections.EMPTY_SET;
}
public Set<String> getSupportedCredentialTypes() {
return new HashSet<String>(this.supportedCredentialTypes);
}
@Override
public boolean supportsCredentialType(String credentialType) {
return getSupportedCredentialTypes().contains(credentialType);
}
@Override
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
return getSupportedCredentialTypes().contains(credentialType);
}
@Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
if (!(input instanceof UserCredentialModel)) return false;
if (input.getType().equals(UserCredentialModel.PASSWORD) && !session.userCredentialManager().isConfiguredLocally(realm, user, UserCredentialModel.PASSWORD)) {
return validPassword(realm, user, ((UserCredentialModel)input).getValue());
} else {
return false; // invalid cred type
}
}
@Override
public CredentialValidationOutput authenticate(RealmModel realm, CredentialInput cred) {
if (!(cred instanceof UserCredentialModel)) CredentialValidationOutput.failed();
UserCredentialModel credential = (UserCredentialModel)cred;
if (credential.getType().equals(UserCredentialModel.KERBEROS)) {
if (kerberosConfig.isAllowKerberosAuthentication()) {
String spnegoToken = credential.getValue();
SPNEGOAuthenticator spnegoAuthenticator = factory.createSPNEGOAuthenticator(spnegoToken, kerberosConfig);
spnegoAuthenticator.authenticate();
Map<String, String> state = new HashMap<String, String>();
if (spnegoAuthenticator.isAuthenticated()) {
// TODO: This assumes that LDAP "uid" is equal to kerberos principal name. Like uid "hnelson" and kerberos principal "hnelson@KEYCLOAK.ORG".
// Check if it's correct or if LDAP attribute for mapping kerberos principal should be available (For ApacheDS it seems to be attribute "krb5PrincipalName" but on MSAD it's likely different)
String username = spnegoAuthenticator.getAuthenticatedUsername();
UserModel user = findOrCreateAuthenticatedUser(realm, username);
if (user == null) {
logger.warnf("Kerberos/SPNEGO authentication succeeded with username [%s], but couldn't find or create user with federation provider [%s]", username, model.getName());
return CredentialValidationOutput.failed();
} else {
String delegationCredential = spnegoAuthenticator.getSerializedDelegationCredential();
if (delegationCredential != null) {
state.put(KerberosConstants.GSS_DELEGATION_CREDENTIAL, delegationCredential);
}
return new CredentialValidationOutput(user, CredentialValidationOutput.Status.AUTHENTICATED, state);
}
} else {
state.put(KerberosConstants.RESPONSE_TOKEN, spnegoAuthenticator.getResponseToken());
return new CredentialValidationOutput(null, CredentialValidationOutput.Status.CONTINUE, state);
}
}
}
return CredentialValidationOutput.failed();
}
@Override
public void close() {
}
/**
* Called after successful kerberos authentication
*
* @param realm realm
* @param username username without realm prefix
* @return finded or newly created user
*/
protected UserModel findOrCreateAuthenticatedUser(RealmModel realm, String username) {
UserModel user = session.userLocalStorage().getUserByUsername(username, realm);
if (user != null) {
logger.debugf("Kerberos authenticated user [%s] found in Keycloak storage", username);
if (!model.getId().equals(user.getFederationLink())) {
logger.warnf("User with username [%s] already exists, but is not linked to provider [%s]", username, model.getName());
return null;
} else {
LDAPObject ldapObject = loadAndValidateUser(realm, user);
if (ldapObject != null) {
return proxy(realm, user, ldapObject);
} else {
logger.warnf("User with username [%s] aready exists and is linked to provider [%s] but is not valid. Stale LDAP_ID on local user is: %s",
username, model.getName(), user.getFirstAttribute(LDAPConstants.LDAP_ID));
logger.warn("Will re-create user");
session.getUserCache().evict(realm, user);
new UserManager(session).removeUser(realm, user, session.userLocalStorage());
}
}
}
// Creating user to local storage
logger.debugf("Kerberos authenticated user [%s] not in Keycloak storage. Creating him", username);
return getUserByUsername(username, realm);
}
public LDAPObject loadLDAPUserByUsername(RealmModel realm, String username) {
LDAPQuery ldapQuery = LDAPUtils.createQueryForUserSearch(this, realm);
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
String usernameMappedAttribute = this.ldapIdentityStore.getConfig().getUsernameLdapAttribute();
Condition usernameCondition = conditionsBuilder.equal(usernameMappedAttribute, username);
ldapQuery.addWhereCondition(usernameCondition);
LDAPObject ldapUser = ldapQuery.getFirstResult();
if (ldapUser == null) {
return null;
}
return ldapUser;
}
public LDAPStorageMapper getMapper(ComponentModel mapperModel) {
LDAPStorageMapper ldapMapper = (LDAPStorageMapper) getSession().getProvider(LDAPStorageMapper.class, mapperModel);
if (ldapMapper == null) {
throw new ModelException("Can't find mapper type with ID: " + mapperModel.getProviderId());
}
return ldapMapper;
}
public List<ComponentModel> sortMappersAsc(Collection<ComponentModel> mappers) {
return LDAPMappersComparator.sortAsc(getLdapIdentityStore().getConfig(), mappers);
}
protected List<ComponentModel> sortMappersDesc(Collection<ComponentModel> mappers) {
return LDAPMappersComparator.sortDesc(getLdapIdentityStore().getConfig(), mappers);
}
}

View file

@ -0,0 +1,437 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.federation.kerberos.CommonKerberosConfig;
import org.keycloak.federation.kerberos.impl.KerberosServerSubjectAuthenticator;
import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator;
import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderFactory;
import org.keycloak.storage.UserStorageProviderModel;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.Condition;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapperFactory;
import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapperFactory;
import org.keycloak.storage.ldap.mappers.msad.MSADUserAccountControlStorageMapperFactory;
import org.keycloak.storage.user.ImportSynchronization;
import org.keycloak.storage.user.SynchronizationResult;
import java.util.Date;
import java.util.List;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LDAPStorageProvider>, ImportSynchronization {
/**
* Optional type that can be by implementations to describe edit mode of federation storage
*
*/
public enum EditMode {
/**
* federation storage is read-only
*/
READ_ONLY,
/**
* federation storage is writable
*
*/
WRITABLE,
/**
* updates to user are stored locally and not synced with federation storage.
*
*/
UNSYNCED
}
private static final Logger logger = Logger.getLogger(LDAPStorageProviderFactory.class);
public static final String PROVIDER_NAME = "ldap2";//LDAPConstants.LDAP_PROVIDER;
private LDAPIdentityStoreRegistry ldapStoreRegistry;
@Override
public LDAPStorageProvider create(KeycloakSession session, ComponentModel model) {
LDAPIdentityStore ldapIdentityStore = this.ldapStoreRegistry.getLdapStore(model);
return new LDAPStorageProvider(this, session, model, ldapIdentityStore);
}
@Override
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
LDAPConfig cfg = new LDAPConfig(config.getConfig());
String customFilter = cfg.getCustomUserSearchFilter();
LDAPUtils.validateCustomLdapFilter(customFilter);
}
@Override
public void init(Config.Scope config) {
this.ldapStoreRegistry = new LDAPIdentityStoreRegistry();
}
@Override
public void close() {
this.ldapStoreRegistry = null;
}
@Override
public String getId() {
return PROVIDER_NAME;
}
// Best effort to create appropriate mappers according to our LDAP config
@Override
public void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) {
LDAPConfig ldapConfig = new LDAPConfig(model.getConfig());
boolean activeDirectory = ldapConfig.isActiveDirectory();
EditMode editMode = ldapConfig.getEditMode();
String readOnly = String.valueOf(editMode == EditMode.READ_ONLY || editMode == EditMode.UNSYNCED);
String usernameLdapAttribute = ldapConfig.getUsernameLdapAttribute();
String alwaysReadValueFromLDAP = String.valueOf(editMode==EditMode.READ_ONLY || editMode== EditMode.WRITABLE);
ComponentModel mapperModel;
mapperModel = KeycloakModelUtils.createComponentModel("username", model.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID, LDAPStorageMapper.class.getName(),
UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, UserModel.USERNAME,
UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, usernameLdapAttribute,
UserAttributeLDAPStorageMapper.READ_ONLY, readOnly,
UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false",
UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "true");
realm.addComponentModel(mapperModel);
// CN is typically used as RDN for Active Directory deployments
if (ldapConfig.getRdnLdapAttribute().equalsIgnoreCase(LDAPConstants.CN)) {
if (usernameLdapAttribute.equalsIgnoreCase(LDAPConstants.CN)) {
// For AD deployments with "cn" as username, we will map "givenName" to first name
mapperModel = KeycloakModelUtils.createComponentModel("first name", model.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, UserModel.FIRST_NAME,
UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, LDAPConstants.GIVENNAME,
UserAttributeLDAPStorageMapper.READ_ONLY, readOnly,
UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "true");
realm.addComponentModel(mapperModel);
} else {
if (editMode == EditMode.WRITABLE) {
// For AD deployments with "sAMAccountName" as username and writable, we need to map "cn" as username as well (this is needed so we can register new users from KC into LDAP) and we will map "givenName" to first name.
mapperModel = KeycloakModelUtils.createComponentModel("first name", model.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, UserModel.FIRST_NAME,
UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, LDAPConstants.GIVENNAME,
UserAttributeLDAPStorageMapper.READ_ONLY, readOnly,
UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "true");
realm.addComponentModel(mapperModel);
mapperModel = KeycloakModelUtils.createComponentModel("username-cn", model.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, UserModel.USERNAME,
UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, LDAPConstants.CN,
UserAttributeLDAPStorageMapper.READ_ONLY, readOnly,
UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false",
UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "true");
realm.addComponentModel(mapperModel);
} else {
// For read-only LDAP, we map "cn" as full name
mapperModel = KeycloakModelUtils.createComponentModel("full name", model.getId(), FullNameLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
FullNameLDAPStorageMapper.LDAP_FULL_NAME_ATTRIBUTE, LDAPConstants.CN,
FullNameLDAPStorageMapper.READ_ONLY, readOnly,
FullNameLDAPStorageMapper.WRITE_ONLY, "false");
realm.addComponentModel(mapperModel);
}
}
} else {
mapperModel = KeycloakModelUtils.createComponentModel("first name", model.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, UserModel.FIRST_NAME,
UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, LDAPConstants.CN,
UserAttributeLDAPStorageMapper.READ_ONLY, readOnly,
UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "true");
realm.addComponentModel(mapperModel);
}
mapperModel = KeycloakModelUtils.createComponentModel("last name", model.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, UserModel.LAST_NAME,
UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, LDAPConstants.SN,
UserAttributeLDAPStorageMapper.READ_ONLY, readOnly,
UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "true");
realm.addComponentModel(mapperModel);
mapperModel = KeycloakModelUtils.createComponentModel("email", model.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, UserModel.EMAIL,
UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, LDAPConstants.EMAIL,
UserAttributeLDAPStorageMapper.READ_ONLY, readOnly,
UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false",
UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "false");
realm.addComponentModel(mapperModel);
String createTimestampLdapAttrName = activeDirectory ? "whenCreated" : LDAPConstants.CREATE_TIMESTAMP;
String modifyTimestampLdapAttrName = activeDirectory ? "whenChanged" : LDAPConstants.MODIFY_TIMESTAMP;
// map createTimeStamp as read-only
mapperModel = KeycloakModelUtils.createComponentModel("creation date", model.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, LDAPConstants.CREATE_TIMESTAMP,
UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, createTimestampLdapAttrName,
UserAttributeLDAPStorageMapper.READ_ONLY, "true",
UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "false");
realm.addComponentModel(mapperModel);
// map modifyTimeStamp as read-only
mapperModel = KeycloakModelUtils.createComponentModel("modify date", model.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, LDAPConstants.MODIFY_TIMESTAMP,
UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, modifyTimestampLdapAttrName,
UserAttributeLDAPStorageMapper.READ_ONLY, "true",
UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "false");
realm.addComponentModel(mapperModel);
// MSAD specific mapper for account state propagation
if (activeDirectory) {
mapperModel = KeycloakModelUtils.createComponentModel("MSAD account controls", model.getId(), MSADUserAccountControlStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName());
realm.addComponentModel(mapperModel);
}
}
@Override
public SynchronizationResult sync(KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model) {
syncMappers(sessionFactory, realmId, model);
logger.infof("Sync all users from LDAP to local store: realm: %s, federation provider: %s", realmId, model.getName());
LDAPQuery userQuery = createQuery(sessionFactory, realmId, model);
SynchronizationResult syncResult = syncImpl(sessionFactory, userQuery, realmId, model);
// TODO: Remove all existing keycloak users, which have federation links, but are not in LDAP. Perhaps don't check users, which were just added or updated during this sync?
logger.infof("Sync all users finished: %s", syncResult.getStatus());
return syncResult;
}
@Override
public SynchronizationResult syncSince(Date lastSync, KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model) {
syncMappers(sessionFactory, realmId, model);
logger.infof("Sync changed users from LDAP to local store: realm: %s, federation provider: %s, last sync time: " + lastSync, realmId, model.getName());
// Sync newly created and updated users
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
Condition createCondition = conditionsBuilder.greaterThanOrEqualTo(LDAPConstants.CREATE_TIMESTAMP, lastSync);
Condition modifyCondition = conditionsBuilder.greaterThanOrEqualTo(LDAPConstants.MODIFY_TIMESTAMP, lastSync);
Condition orCondition = conditionsBuilder.orCondition(createCondition, modifyCondition);
LDAPQuery userQuery = createQuery(sessionFactory, realmId, model);
userQuery.addWhereCondition(orCondition);
SynchronizationResult result = syncImpl(sessionFactory, userQuery, realmId, model);
logger.infof("Sync changed users finished: %s", result.getStatus());
return result;
}
protected void syncMappers(KeycloakSessionFactory sessionFactory, final String realmId, final ComponentModel model) {
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
LDAPStorageProvider ldapProvider = (LDAPStorageProvider)session.getProvider(UserStorageProvider.class, model);
RealmModel realm = session.realms().getRealm(realmId);
List<ComponentModel> mappers = realm.getComponents(model.getId(), LDAPStorageMapper.class.getName());
for (ComponentModel mapperModel : mappers) {
LDAPStorageMapper ldapMapper = session.getProvider(LDAPStorageMapper.class, mapperModel);
SynchronizationResult syncResult = ldapMapper.syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm);
if (syncResult.getAdded() > 0 || syncResult.getUpdated() > 0 || syncResult.getRemoved() > 0 || syncResult.getFailed() > 0) {
logger.infof("Sync of federation mapper '%s' finished. Status: %s", mapperModel.getName(), syncResult.toString());
}
}
}
});
}
protected SynchronizationResult syncImpl(KeycloakSessionFactory sessionFactory, LDAPQuery userQuery, final String realmId, final ComponentModel fedModel) {
final SynchronizationResult syncResult = new SynchronizationResult();
LDAPConfig ldapConfig = new LDAPConfig(fedModel.getConfig());
boolean pagination = ldapConfig.isPagination();
if (pagination) {
int pageSize = ldapConfig.getBatchSizeForSync();
boolean nextPage = true;
while (nextPage) {
userQuery.setLimit(pageSize);
final List<LDAPObject> users = userQuery.getResultList();
nextPage = userQuery.getPaginationContext() != null;
SynchronizationResult currentPageSync = importLdapUsers(sessionFactory, realmId, fedModel, users);
syncResult.add(currentPageSync);
}
} else {
// LDAP pagination not available. Do everything in single transaction
final List<LDAPObject> users = userQuery.getResultList();
SynchronizationResult currentSync = importLdapUsers(sessionFactory, realmId, fedModel, users);
syncResult.add(currentSync);
}
return syncResult;
}
private LDAPQuery createQuery(KeycloakSessionFactory sessionFactory, final String realmId, final ComponentModel model) {
class QueryHolder {
LDAPQuery query;
}
final QueryHolder queryHolder = new QueryHolder();
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
LDAPStorageProvider ldapFedProvider = (LDAPStorageProvider)session.getProvider(UserStorageProvider.class, model);
RealmModel realm = session.realms().getRealm(realmId);
queryHolder.query = LDAPUtils.createQueryForUserSearch(ldapFedProvider, realm);
}
});
return queryHolder.query;
}
protected SynchronizationResult importLdapUsers(KeycloakSessionFactory sessionFactory, final String realmId, final ComponentModel fedModel, List<LDAPObject> ldapUsers) {
final SynchronizationResult syncResult = new SynchronizationResult();
class BooleanHolder {
private boolean value = true;
}
final BooleanHolder exists = new BooleanHolder();
for (final LDAPObject ldapUser : ldapUsers) {
try {
// Process each user in it's own transaction to avoid global fail
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
LDAPStorageProvider ldapFedProvider = (LDAPStorageProvider)session.getProvider(UserStorageProvider.class, fedModel);
RealmModel currentRealm = session.realms().getRealm(realmId);
String username = LDAPUtils.getUsername(ldapUser, ldapFedProvider.getLdapIdentityStore().getConfig());
exists.value = true;
LDAPUtils.checkUuid(ldapUser, ldapFedProvider.getLdapIdentityStore().getConfig());
UserModel currentUser = session.userLocalStorage().getUserByUsername(username, currentRealm);
if (currentUser == null) {
// Add new user to Keycloak
exists.value = false;
ldapFedProvider.importUserFromLDAP(session, currentRealm, ldapUser);
syncResult.increaseAdded();
} else {
if ((fedModel.getId().equals(currentUser.getFederationLink())) && (ldapUser.getUuid().equals(currentUser.getFirstAttribute(LDAPConstants.LDAP_ID)))) {
// Update keycloak user
List<ComponentModel> federationMappers = currentRealm.getComponents(fedModel.getId(), LDAPStorageMapper.class.getName());
List<ComponentModel> sortedMappers = ldapFedProvider.sortMappersDesc(federationMappers);
for (ComponentModel mapperModel : sortedMappers) {
LDAPStorageMapper ldapMapper = ldapFedProvider.getMapper(mapperModel);
ldapMapper.onImportUserFromLDAP(mapperModel, ldapFedProvider, ldapUser, currentUser, currentRealm, false);
}
logger.debugf("Updated user from LDAP: %s", currentUser.getUsername());
syncResult.increaseUpdated();
} else {
logger.warnf("User '%s' is not updated during sync as he already exists in Keycloak database but is not linked to federation provider '%s'", username, fedModel.getName());
syncResult.increaseFailed();
}
}
}
});
} catch (ModelException me) {
logger.error("Failed during import user from LDAP", me);
syncResult.increaseFailed();
// Remove user if we already added him during this transaction
if (!exists.value) {
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
LDAPStorageProvider ldapFedProvider = (LDAPStorageProvider)session.getProvider(UserStorageProvider.class, fedModel);
RealmModel currentRealm = session.realms().getRealm(realmId);
String username = null;
try {
username = LDAPUtils.getUsername(ldapUser, ldapFedProvider.getLdapIdentityStore().getConfig());
} catch (ModelException ignore) {
}
if (username != null) {
UserModel existing = session.userLocalStorage().getUserByUsername(username, currentRealm);
if (existing != null) {
session.getUserCache().evict(currentRealm, existing);
session.userLocalStorage().removeUser(currentRealm, existing);
}
}
}
});
}
}
}
return syncResult;
}
protected SPNEGOAuthenticator createSPNEGOAuthenticator(String spnegoToken, CommonKerberosConfig kerberosConfig) {
KerberosServerSubjectAuthenticator kerberosAuth = createKerberosSubjectAuthenticator(kerberosConfig);
return new SPNEGOAuthenticator(kerberosConfig, kerberosAuth, spnegoToken);
}
protected KerberosServerSubjectAuthenticator createKerberosSubjectAuthenticator(CommonKerberosConfig kerberosConfig) {
return new KerberosServerSubjectAuthenticator(kerberosConfig);
}
protected KerberosUsernamePasswordAuthenticator createKerberosUsernamePasswordAuthenticator(CommonKerberosConfig kerberosConfig) {
return new KerberosUsernamePasswordAuthenticator(kerberosConfig);
}
}

View file

@ -0,0 +1,286 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.mappers.FederationConfigValidationException;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.storage.ldap.idm.model.LDAPDn;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.Condition;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.membership.MembershipType;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Allow to directly call some operations against LDAPIdentityStore.
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LDAPUtils {
/**
* @param ldapProvider
* @param realm
* @param user
* @return newly created LDAPObject with all the attributes, uuid and DN properly set
*/
public static LDAPObject addUserToLDAP(LDAPStorageProvider ldapProvider, RealmModel realm, UserModel user) {
LDAPObject ldapUser = new LDAPObject();
LDAPIdentityStore ldapStore = ldapProvider.getLdapIdentityStore();
LDAPConfig ldapConfig = ldapStore.getConfig();
ldapUser.setRdnAttributeName(ldapConfig.getRdnLdapAttribute());
ldapUser.setObjectClasses(ldapConfig.getUserObjectClasses());
List<ComponentModel> federationMappers = realm.getComponents(ldapProvider.getModel().getId(), LDAPStorageMapper.class.getName());
List<ComponentModel> sortedMappers = ldapProvider.sortMappersAsc(federationMappers);
for (ComponentModel mapperModel : sortedMappers) {
LDAPStorageMapper ldapMapper = ldapProvider.getMapper(mapperModel);
ldapMapper.onRegisterUserToLDAP(mapperModel, ldapProvider, ldapUser, user, realm);
}
LDAPUtils.computeAndSetDn(ldapConfig, ldapUser);
ldapStore.add(ldapUser);
return ldapUser;
}
public static LDAPQuery createQueryForUserSearch(LDAPStorageProvider ldapProvider, RealmModel realm) {
LDAPQuery ldapQuery = new LDAPQuery(ldapProvider);
LDAPConfig config = ldapProvider.getLdapIdentityStore().getConfig();
ldapQuery.setSearchScope(config.getSearchScope());
ldapQuery.setSearchDn(config.getUsersDn());
ldapQuery.addObjectClasses(config.getUserObjectClasses());
String customFilter = config.getCustomUserSearchFilter();
if (customFilter != null) {
Condition customFilterCondition = new LDAPQueryConditionsBuilder().addCustomLDAPFilter(customFilter);
ldapQuery.addWhereCondition(customFilterCondition);
}
List<ComponentModel> mapperModels = realm.getComponents(ldapProvider.getModel().getId(), LDAPStorageMapper.class.getName());
ldapQuery.addMappers(mapperModels);
return ldapQuery;
}
// ldapUser has filled attributes, but doesn't have filled dn.
private static void computeAndSetDn(LDAPConfig config, LDAPObject ldapUser) {
String rdnLdapAttrName = config.getRdnLdapAttribute();
String rdnLdapAttrValue = ldapUser.getAttributeAsString(rdnLdapAttrName);
if (rdnLdapAttrValue == null) {
throw new ModelException("RDN Attribute [" + rdnLdapAttrName + "] is not filled. Filled attributes: " + ldapUser.getAttributes());
}
LDAPDn dn = LDAPDn.fromString(config.getUsersDn());
dn.addFirst(rdnLdapAttrName, rdnLdapAttrValue);
ldapUser.setDn(dn);
}
public static String getUsername(LDAPObject ldapUser, LDAPConfig config) {
String usernameAttr = config.getUsernameLdapAttribute();
String ldapUsername = ldapUser.getAttributeAsString(usernameAttr);
if (ldapUsername == null) {
throw new ModelException("User returned from LDAP has null username! Check configuration of your LDAP mappings. Mapped username LDAP attribute: " +
config.getUsernameLdapAttribute() + ", user DN: " + ldapUser.getDn() + ", attributes from LDAP: " + ldapUser.getAttributes());
}
return ldapUsername;
}
public static void checkUuid(LDAPObject ldapUser, LDAPConfig config) {
if (ldapUser.getUuid() == null) {
throw new ModelException("User returned from LDAP has null uuid! Check configuration of your LDAP settings. UUID Attribute must be unique among your LDAP records and available on all the LDAP user records. " +
"If your LDAP server really doesn't support the notion of UUID, you can use any other attribute, which is supposed to be unique among LDAP users in tree. For example 'uid' or 'entryDN' . " +
"Mapped UUID LDAP attribute: " + config.getUuidLDAPAttributeName() + ", user DN: " + ldapUser.getDn());
}
}
// roles & groups
public static LDAPObject createLDAPGroup(LDAPStorageProvider ldapProvider, String groupName, String groupNameAttribute, Collection<String> objectClasses,
String parentDn, Map<String, Set<String>> additionalAttributes) {
LDAPObject ldapObject = new LDAPObject();
ldapObject.setRdnAttributeName(groupNameAttribute);
ldapObject.setObjectClasses(objectClasses);
ldapObject.setSingleAttribute(groupNameAttribute, groupName);
LDAPDn roleDn = LDAPDn.fromString(parentDn);
roleDn.addFirst(groupNameAttribute, groupName);
ldapObject.setDn(roleDn);
for (Map.Entry<String, Set<String>> attrEntry : additionalAttributes.entrySet()) {
ldapObject.setAttribute(attrEntry.getKey(), attrEntry.getValue());
}
ldapProvider.getLdapIdentityStore().add(ldapObject);
return ldapObject;
}
/**
* Add ldapChild as member of ldapParent and save ldapParent to LDAP.
*
* @param ldapProvider
* @param membershipType how is 'member' attribute saved (full DN or just uid)
* @param memberAttrName usually 'member'
* @param ldapParent role or group
* @param ldapChild usually user (or child group or child role)
* @param sendLDAPUpdateRequest if true, the method will send LDAP update request too. Otherwise it will skip it
*/
public static void addMember(LDAPStorageProvider ldapProvider, MembershipType membershipType, String memberAttrName, LDAPObject ldapParent, LDAPObject ldapChild, boolean sendLDAPUpdateRequest) {
Set<String> memberships = getExistingMemberships(memberAttrName, ldapParent);
// Remove membership placeholder if present
if (membershipType == MembershipType.DN) {
for (String membership : memberships) {
if (LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE.equals(membership)) {
memberships.remove(membership);
break;
}
}
}
String membership = getMemberValueOfChildObject(ldapChild, membershipType);
memberships.add(membership);
ldapParent.setAttribute(memberAttrName, memberships);
if (sendLDAPUpdateRequest) {
ldapProvider.getLdapIdentityStore().update(ldapParent);
}
}
/**
* Remove ldapChild as member of ldapParent and save ldapParent to LDAP.
*
* @param ldapProvider
* @param membershipType how is 'member' attribute saved (full DN or just uid)
* @param memberAttrName usually 'member'
* @param ldapParent role or group
* @param ldapChild usually user (or child group or child role)
* @param sendLDAPUpdateRequest if true, the method will send LDAP update request too. Otherwise it will skip it
*/
public static void deleteMember(LDAPStorageProvider ldapProvider, MembershipType membershipType, String memberAttrName, LDAPObject ldapParent, LDAPObject ldapChild, boolean sendLDAPUpdateRequest) {
Set<String> memberships = getExistingMemberships(memberAttrName, ldapParent);
String userMembership = getMemberValueOfChildObject(ldapChild, membershipType);
memberships.remove(userMembership);
// Some membership placeholder needs to be always here as "member" is mandatory attribute on some LDAP servers. But not on active directory! (Placeholder, which not matches any real object is not allowed here)
if (memberships.size() == 0 && membershipType== MembershipType.DN && !ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory()) {
memberships.add(LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE);
}
ldapParent.setAttribute(memberAttrName, memberships);
ldapProvider.getLdapIdentityStore().update(ldapParent);
}
/**
* Return all existing memberships (values of attribute 'member' ) from the given ldapRole or ldapGroup
*
* @param memberAttrName usually 'member'
* @param ldapRole
* @return
*/
public static Set<String> getExistingMemberships(String memberAttrName, LDAPObject ldapRole) {
Set<String> memberships = ldapRole.getAttributeAsSet(memberAttrName);
if (memberships == null) {
memberships = new HashSet<>();
}
return memberships;
}
/**
* Get value to be used as attribute 'member' in some parent ldapObject
*/
public static String getMemberValueOfChildObject(LDAPObject ldapUser, MembershipType membershipType) {
return membershipType == MembershipType.DN ? ldapUser.getDn().toString() : ldapUser.getAttributeAsString(ldapUser.getRdnAttributeName());
}
/**
* Load all LDAP objects corresponding to given query. We will load them paginated, so we allow to bypass the limitation of 1000
* maximum loaded objects in single query in MSAD
*
* @param ldapQuery
* @param ldapProvider
* @return
*/
public static List<LDAPObject> loadAllLDAPObjects(LDAPQuery ldapQuery, LDAPStorageProvider ldapProvider) {
LDAPConfig ldapConfig = ldapProvider.getLdapIdentityStore().getConfig();
boolean pagination = ldapConfig.isPagination();
if (pagination) {
// For now reuse globally configured batch size in LDAP provider page
int pageSize = ldapConfig.getBatchSizeForSync();
List<LDAPObject> result = new LinkedList<>();
boolean nextPage = true;
while (nextPage) {
ldapQuery.setLimit(pageSize);
final List<LDAPObject> currentPageGroups = ldapQuery.getResultList();
result.addAll(currentPageGroups);
nextPage = ldapQuery.getPaginationContext() != null;
}
return result;
} else {
// LDAP pagination not available. Do everything in single transaction
return ldapQuery.getResultList();
}
}
/**
* Validate configured customFilter matches the requested format
*
* @param customFilter
* @throws FederationConfigValidationException
*/
public static void validateCustomLdapFilter(String customFilter) throws ComponentValidationException {
if (customFilter != null) {
customFilter = customFilter.trim();
if (customFilter.isEmpty()) {
return;
}
if (!customFilter.startsWith("(") || !customFilter.endsWith(")")) {
throw new ComponentValidationException("ldapErrorInvalidCustomFilter");
}
}
}
}

View file

@ -0,0 +1,57 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap;
import org.keycloak.models.ModelReadOnlyException;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.UserModelDelegate;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ReadonlyLDAPUserModelDelegate extends UserModelDelegate implements UserModel {
protected LDAPStorageProvider provider;
public ReadonlyLDAPUserModelDelegate(UserModel delegate, LDAPStorageProvider provider) {
super(delegate);
this.provider = provider;
}
@Override
public void setUsername(String username) {
throw new ModelReadOnlyException("Federated storage is not writable");
}
@Override
public void setLastName(String lastName) {
throw new ModelReadOnlyException("Federated storage is not writable");
}
@Override
public void setFirstName(String first) {
throw new ModelReadOnlyException("Federated storage is not writable");
}
@Override
public void setEmail(String email) {
throw new ModelReadOnlyException("Federated storage is not writable");
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap;
import org.jboss.logging.Logger;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.UserModelDelegate;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class UnsyncedLDAPUserModelDelegate extends UserModelDelegate implements UserModel {
private static final Logger logger = Logger.getLogger(UnsyncedLDAPUserModelDelegate.class);
protected LDAPStorageProvider provider;
public UnsyncedLDAPUserModelDelegate(UserModel delegate, LDAPStorageProvider provider) {
super(delegate);
this.provider = provider;
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap;
import org.jboss.logging.Logger;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.UserModelDelegate;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class WritableLDAPUserModelDelegate extends UserModelDelegate implements UserModel {
private static final Logger logger = Logger.getLogger(WritableLDAPUserModelDelegate.class);
protected LDAPStorageProvider provider;
protected LDAPObject ldapObject;
public WritableLDAPUserModelDelegate(UserModel delegate, LDAPStorageProvider provider, LDAPObject ldapObject) {
super(delegate);
this.provider = provider;
this.ldapObject = ldapObject;
}
}

View file

@ -0,0 +1,153 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.idm.model;
import javax.naming.ldap.Rdn;
import java.util.Collection;
import java.util.Deque;
import java.util.LinkedList;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LDAPDn {
private final Deque<Entry> entries = new LinkedList<>();
public static LDAPDn fromString(String dnString) {
LDAPDn dn = new LDAPDn();
// In certain OpenLDAP implementations the uniqueMember attribute is mandatory
// Thus, if a new group is created, it will contain an empty uniqueMember attribute
// Later on, when adding members, this empty attribute will be kept
// Keycloak must be able to process it, properly, w/o throwing an ArrayIndexOutOfBoundsException
if(dnString.trim().isEmpty())
return dn;
String[] rdns = dnString.split("(?<!\\\\),");
for (String entryStr : rdns) {
String[] rdn = entryStr.split("(?<!\\\\)=");
dn.addLast(rdn[0].trim(), rdn[1].trim());
}
return dn;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof LDAPDn)) {
return false;
}
return toString().equals(obj.toString());
}
@Override
public int hashCode() {
return toString().hashCode();
}
@Override
public String toString() {
return toString(entries);
}
private static String toString(Collection<Entry> entries) {
StringBuilder builder = new StringBuilder();
boolean first = true;
for (Entry rdn : entries) {
if (first) {
first = false;
} else {
builder.append(",");
}
builder.append(rdn.attrName).append("=").append(rdn.attrValue);
}
return builder.toString();
}
/**
* @return string like "uid=joe" from the DN like "uid=joe,dc=something,dc=org"
*/
public String getFirstRdn() {
Entry firstEntry = entries.getFirst();
return firstEntry.attrName + "=" + firstEntry.attrValue;
}
/**
* @return string attribute name like "uid" from the DN like "uid=joe,dc=something,dc=org"
*/
public String getFirstRdnAttrName() {
Entry firstEntry = entries.getFirst();
return firstEntry.attrName;
}
/**
* @return string attribute value like "joe" from the DN like "uid=joe,dc=something,dc=org"
*/
public String getFirstRdnAttrValue() {
Entry firstEntry = entries.getFirst();
return firstEntry.attrValue;
}
/**
*
* @return string like "dc=something,dc=org" from the DN like "uid=joe,dc=something,dc=org"
*/
public String getParentDn() {
LinkedList<Entry> parentDnEntries = new LinkedList<>(entries);
parentDnEntries.remove();
return toString(parentDnEntries);
}
public boolean isDescendantOf(LDAPDn expectedParentDn) {
int parentEntriesCount = expectedParentDn.entries.size();
Deque<Entry> myEntries = new LinkedList<>(this.entries);
boolean someRemoved = false;
while (myEntries.size() > parentEntriesCount) {
myEntries.removeFirst();
someRemoved = true;
}
String myEntriesParentStr = toString(myEntries).toLowerCase();
String expectedParentDnStr = expectedParentDn.toString().toLowerCase();
return someRemoved && myEntriesParentStr.equals(expectedParentDnStr);
}
public void addFirst(String rdnName, String rdnValue) {
rdnValue = Rdn.escapeValue(rdnValue);
entries.addFirst(new Entry(rdnName, rdnValue));
}
private void addLast(String rdnName, String rdnValue) {
entries.addLast(new Entry(rdnName, rdnValue));
}
private static class Entry {
private final String attrName;
private final String attrValue;
private Entry(String attrName, String attrValue) {
this.attrName = attrName;
this.attrValue = attrValue;
}
}
}

View file

@ -0,0 +1,157 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.idm.model;
import org.jboss.logging.Logger;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LDAPObject {
private static final Logger logger = Logger.getLogger(LDAPObject.class);
private String uuid;
private LDAPDn dn;
private String rdnAttributeName;
private final List<String> objectClasses = new LinkedList<>();
// NOTE: names of read-only attributes are lower-cased to avoid case sensitivity issues
private final List<String> readOnlyAttributeNames = new LinkedList<>();
private final Map<String, Set<String>> attributes = new HashMap<>();
// Copy of "attributes" containing lower-cased keys
private final Map<String, Set<String>> lowerCasedAttributes = new HashMap<>();
public String getUuid() {
return uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
public LDAPDn getDn() {
return dn;
}
public void setDn(LDAPDn dn) {
this.dn = dn;
}
public List<String> getObjectClasses() {
return objectClasses;
}
public void setObjectClasses(Collection<String> objectClasses) {
this.objectClasses.clear();
this.objectClasses.addAll(objectClasses);
}
public List<String> getReadOnlyAttributeNames() {
return readOnlyAttributeNames;
}
public void addReadOnlyAttributeName(String readOnlyAttribute) {
readOnlyAttributeNames.add(readOnlyAttribute.toLowerCase());
}
public void removeReadOnlyAttributeName(String readOnlyAttribute) {
readOnlyAttributeNames.remove(readOnlyAttribute.toLowerCase());
}
public String getRdnAttributeName() {
return rdnAttributeName;
}
public void setRdnAttributeName(String rdnAttributeName) {
this.rdnAttributeName = rdnAttributeName;
}
public void setSingleAttribute(String attributeName, String attributeValue) {
Set<String> asSet = new LinkedHashSet<>();
asSet.add(attributeValue);
setAttribute(attributeName, asSet);
}
public void setAttribute(String attributeName, Set<String> attributeValue) {
attributes.put(attributeName, attributeValue);
lowerCasedAttributes.put(attributeName.toLowerCase(), attributeValue);
}
// Case-insensitive
public String getAttributeAsString(String name) {
Set<String> attrValue = lowerCasedAttributes.get(name.toLowerCase());
if (attrValue == null || attrValue.size() == 0) {
return null;
} else if (attrValue.size() > 1) {
logger.warnf("Expected String but attribute '%s' has more values '%s' on object '%s' . Returning just first value", name, attrValue, dn);
}
return attrValue.iterator().next();
}
// Case-insensitive. Return null if there is not value of attribute with given name or set with all values otherwise
public Set<String> getAttributeAsSet(String name) {
Set<String> values = lowerCasedAttributes.get(name.toLowerCase());
return (values == null) ? null : new LinkedHashSet<>(values);
}
public Map<String, Set<String>> getAttributes() {
return attributes;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (!getClass().isInstance(obj)) {
return false;
}
LDAPObject other = (LDAPObject) obj;
return getUuid() != null && other.getUuid() != null && getUuid().equals(other.getUuid());
}
@Override
public int hashCode() {
int result = getUuid() != null ? getUuid().hashCode() : 0;
result = 31 * result + (getUuid() != null ? getUuid().hashCode() : 0);
return result;
}
@Override
public String toString() {
return "LDAP Object [ dn: " + dn + " , uuid: " + uuid + ", attributes: " + attributes + ", readOnly attribute names: " + readOnlyAttributeNames + " ]";
}
}

View file

@ -0,0 +1,42 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.idm.query;
/**
* <p>A {@link Condition} is used to specify how a specific query parameter
* is defined in order to filter query results.</p>
*
* @author Pedro Igor
*/
public interface Condition {
String getParameterName();
void setParameterName(String parameterName);
/**
* Will change the parameter name if it is "modelParamName" to "ldapParamName" . Implementation can apply this to subconditions as well.
*
* It is used to update LDAP queries, which were created with model parameter name ( for example "firstName" ) and rewrite them to use real
* LDAP mapped attribute (for example "givenName" )
*/
void updateParameterName(String modelParamName, String ldapParamName);
void applyCondition(StringBuilder filter);
}

View file

@ -0,0 +1,40 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.idm.query;
/**
* @author Pedro Igor
*/
public class Sort {
private final String paramName;
private final boolean asc;
public Sort(String paramName, boolean asc) {
this.paramName = paramName;
this.asc = asc;
}
public String getParameter() {
return this.paramName;
}
public boolean isAscending() {
return asc;
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.idm.query.internal;
import org.keycloak.storage.ldap.idm.store.ldap.LDAPUtil;
import java.util.Date;
/**
* @author Pedro Igor
*/
class BetweenCondition extends NamedParameterCondition {
private final Comparable x;
private final Comparable y;
public BetweenCondition(String name, Comparable x, Comparable y) {
super(name);
this.x = x;
this.y = y;
}
@Override
public void applyCondition(StringBuilder filter) {
Comparable x = this.x;
Comparable y = this.y;
if (Date.class.isInstance(x)) {
x = LDAPUtil.formatDate((Date) x);
}
if (Date.class.isInstance(y)) {
y = LDAPUtil.formatDate((Date) y);
}
filter.append("(").append(x).append("<=").append(getParameterName()).append("<=").append(y).append(")");
}
}

View file

@ -0,0 +1,51 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.idm.query.internal;
import org.keycloak.storage.ldap.idm.query.Condition;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
class CustomLDAPFilter implements Condition {
private final String customFilter;
public CustomLDAPFilter(String customFilter) {
this.customFilter = customFilter;
}
@Override
public String getParameterName() {
return null;
}
@Override
public void setParameterName(String parameterName) {
}
@Override
public void updateParameterName(String modelParamName, String ldapParamName) {
}
@Override
public void applyCondition(StringBuilder filter) {
filter.append(customFilter);
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.idm.query.internal;
import org.keycloak.models.LDAPConstants;
import org.keycloak.storage.ldap.idm.store.ldap.LDAPUtil;
import java.util.Date;
/**
* @author Pedro Igor
*/
public class EqualCondition extends NamedParameterCondition {
private final Object value;
public EqualCondition(String name, Object value) {
super(name);
this.value = value;
}
public Object getValue() {
return this.value;
}
@Override
public void applyCondition(StringBuilder filter) {
Object parameterValue = value;
if (Date.class.isInstance(value)) {
parameterValue = LDAPUtil.formatDate((Date) parameterValue);
}
filter.append("(").append(getParameterName()).append(LDAPConstants.EQUAL).append(parameterValue).append(")");
}
@Override
public String toString() {
return "EqualCondition{" +
"paramName=" + getParameterName() +
", value=" + value +
'}';
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.idm.query.internal;
import org.keycloak.storage.ldap.idm.store.ldap.LDAPUtil;
import java.util.Date;
/**
* @author Pedro Igor
*/
class GreaterThanCondition extends NamedParameterCondition {
private final boolean orEqual;
private final Comparable value;
public GreaterThanCondition(String name, Comparable value, boolean orEqual) {
super(name);
this.value = value;
this.orEqual = orEqual;
}
@Override
public void applyCondition(StringBuilder filter) {
Comparable parameterValue = value;
if (Date.class.isInstance(parameterValue)) {
parameterValue = LDAPUtil.formatDate((Date) parameterValue);
}
if (orEqual) {
filter.append("(").append(getParameterName()).append(">=").append(parameterValue).append(")");
} else {
filter.append("(").append(getParameterName()).append(">").append(parameterValue).append(")");
}
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.idm.query.internal;
import org.keycloak.models.LDAPConstants;
/**
* @author Pedro Igor
*/
class InCondition extends NamedParameterCondition {
private final Object[] valuesToCompare;
public InCondition(String name, Object[] valuesToCompare) {
super(name);
this.valuesToCompare = valuesToCompare;
}
@Override
public void applyCondition(StringBuilder filter) {
filter.append("(&(");
for (int i = 0; i< valuesToCompare.length; i++) {
Object value = valuesToCompare[i];
filter.append("(").append(getParameterName()).append(LDAPConstants.EQUAL).append(value).append(")");
}
filter.append("))");
}
}

View file

@ -0,0 +1,213 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.idm.query.internal;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.Condition;
import org.keycloak.storage.ldap.idm.query.Sort;
import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
import javax.naming.directory.SearchControls;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import static java.util.Collections.unmodifiableSet;
/**
* Default IdentityQuery implementation.
*
*
* @author Shane Bryzak
*/
public class LDAPQuery {
private final LDAPStorageProvider ldapFedProvider;
private int offset;
private int limit;
private byte[] paginationContext;
private String searchDn;
private final Set<Condition> conditions = new LinkedHashSet<Condition>();
private final Set<Sort> ordering = new LinkedHashSet<Sort>();
private final Set<String> returningLdapAttributes = new LinkedHashSet<String>();
// Contains just those returningLdapAttributes, which are read-only. They will be marked as read-only in returned LDAPObject instances as well
// NOTE: names of attributes are lower-cased to avoid case sensitivity issues (LDAP searching is usually case-insensitive, so we want to be as well)
private final Set<String> returningReadOnlyLdapAttributes = new LinkedHashSet<String>();
private final Set<String> objectClasses = new LinkedHashSet<String>();
private final List<ComponentModel> mappers = new ArrayList<>();
private int searchScope = SearchControls.SUBTREE_SCOPE;
public LDAPQuery(LDAPStorageProvider ldapProvider) {
this.ldapFedProvider = ldapProvider;
}
public LDAPQuery addWhereCondition(Condition... condition) {
this.conditions.addAll(Arrays.asList(condition));
return this;
}
public LDAPQuery sortBy(Sort... sorts) {
this.ordering.addAll(Arrays.asList(sorts));
return this;
}
public LDAPQuery setSearchDn(String searchDn) {
this.searchDn = searchDn;
return this;
}
public LDAPQuery addObjectClasses(Collection<String> objectClasses) {
this.objectClasses.addAll(objectClasses);
return this;
}
public LDAPQuery addReturningLdapAttribute(String ldapAttributeName) {
this.returningLdapAttributes.add(ldapAttributeName);
return this;
}
public LDAPQuery addReturningReadOnlyLdapAttribute(String ldapAttributeName) {
this.returningReadOnlyLdapAttributes.add(ldapAttributeName.toLowerCase());
return this;
}
public LDAPQuery addMappers(Collection<ComponentModel> mappers) {
this.mappers.addAll(mappers);
return this;
}
public LDAPQuery setSearchScope(int searchScope) {
this.searchScope = searchScope;
return this;
}
public Set<Sort> getSorting() {
return unmodifiableSet(this.ordering);
}
public String getSearchDn() {
return this.searchDn;
}
public Set<String> getObjectClasses() {
return unmodifiableSet(this.objectClasses);
}
public Set<String> getReturningLdapAttributes() {
return unmodifiableSet(this.returningLdapAttributes);
}
public Set<String> getReturningReadOnlyLdapAttributes() {
return unmodifiableSet(this.returningReadOnlyLdapAttributes);
}
public List<ComponentModel> getMappers() {
return mappers;
}
public int getSearchScope() {
return searchScope;
}
public int getLimit() {
return limit;
}
public int getOffset() {
return offset;
}
public byte[] getPaginationContext() {
return paginationContext;
}
public List<LDAPObject> getResultList() {
// Apply mappers now
List<ComponentModel> sortedMappers = ldapFedProvider.sortMappersAsc(mappers);
for (ComponentModel mapperModel : sortedMappers) {
LDAPStorageMapper fedMapper = ldapFedProvider.getMapper(mapperModel);
fedMapper.beforeLDAPQuery(mapperModel, this);
}
List<LDAPObject> result = new ArrayList<LDAPObject>();
try {
for (LDAPObject ldapObject : ldapFedProvider.getLdapIdentityStore().fetchQueryResults(this)) {
result.add(ldapObject);
}
} catch (Exception e) {
throw new ModelException("LDAP Query failed", e);
}
return result;
}
public LDAPObject getFirstResult() {
List<LDAPObject> results = getResultList();
if (results.isEmpty()) {
return null;
} else if (results.size() == 1) {
return results.get(0);
} else {
throw new ModelDuplicateException("Error - multiple LDAP objects found but expected just one");
}
}
public int getResultCount() {
return ldapFedProvider.getLdapIdentityStore().countQueryResults(this);
}
public LDAPQuery setOffset(int offset) {
this.offset = offset;
return this;
}
public LDAPQuery setLimit(int limit) {
this.limit = limit;
return this;
}
public LDAPQuery setPaginationContext(byte[] paginationContext) {
this.paginationContext = paginationContext;
return this;
}
public Set<Condition> getConditions() {
return this.conditions;
}
public LDAPStorageProvider getLdapProvider() {
return ldapFedProvider;
}
}

View file

@ -0,0 +1,84 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.idm.query.internal;
import org.keycloak.models.ModelException;
import org.keycloak.storage.ldap.idm.query.Condition;
import org.keycloak.storage.ldap.idm.query.Sort;
/**
* @author Pedro Igor
*/
public class LDAPQueryConditionsBuilder {
public Condition equal(String parameter, Object value) {
return new EqualCondition(parameter, value);
}
public Condition greaterThan(String paramName, Object x) {
throwExceptionIfNotComparable(x);
return new GreaterThanCondition(paramName, (Comparable) x, false);
}
public Condition greaterThanOrEqualTo(String paramName, Object x) {
throwExceptionIfNotComparable(x);
return new GreaterThanCondition(paramName, (Comparable) x, true);
}
public Condition lessThan(String paramName, Comparable x) {
return new LessThanCondition(paramName, x, false);
}
public Condition lessThanOrEqualTo(String paramName, Comparable x) {
return new LessThanCondition(paramName, x, true);
}
public Condition between(String paramName, Comparable x, Comparable y) {
return new BetweenCondition(paramName, x, y);
}
public Condition orCondition(Condition... conditions) {
if (conditions == null || conditions.length == 0) {
throw new ModelException("At least one condition should be provided to OR query");
}
return new OrCondition(conditions);
}
public Condition addCustomLDAPFilter(String filter) {
filter = filter.trim();
return new CustomLDAPFilter(filter);
}
public Condition in(String paramName, Object... x) {
return new InCondition(paramName, x);
}
public Sort asc(String paramName) {
return new Sort(paramName, true);
}
public Sort desc(String paramName) {
return new Sort(paramName, false);
}
private void throwExceptionIfNotComparable(Object x) {
if (!Comparable.class.isInstance(x)) {
throw new ModelException("Query parameter value [" + x + "] must be " + Comparable.class + ".");
}
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.idm.query.internal;
import org.keycloak.storage.ldap.idm.store.ldap.LDAPUtil;
import java.util.Date;
/**
* @author Pedro Igor
*/
class LessThanCondition extends NamedParameterCondition {
private final boolean orEqual;
private final Comparable value;
public LessThanCondition(String name, Comparable value, boolean orEqual) {
super(name);
this.value = value;
this.orEqual = orEqual;
}
@Override
public void applyCondition(StringBuilder filter) {
Comparable parameterValue = value;
if (Date.class.isInstance(parameterValue)) {
parameterValue = LDAPUtil.formatDate((Date) parameterValue);
}
if (orEqual) {
filter.append("(").append(getParameterName()).append("<=").append(parameterValue).append(")");
} else {
filter.append("(").append(getParameterName()).append("<").append(parameterValue).append(")");
}
}
}

View file

@ -0,0 +1,50 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.idm.query.internal;
import org.keycloak.storage.ldap.idm.query.Condition;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public abstract class NamedParameterCondition implements Condition {
private String parameterName;
public NamedParameterCondition(String parameterName) {
this.parameterName = parameterName;
}
@Override
public String getParameterName() {
return parameterName;
}
@Override
public void setParameterName(String parameterName) {
this.parameterName = parameterName;
}
@Override
public void updateParameterName(String modelParamName, String ldapParamName) {
if (parameterName.equalsIgnoreCase(modelParamName)) {
this.parameterName = ldapParamName;
}
}
}

View file

@ -0,0 +1,59 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.idm.query.internal;
import org.keycloak.storage.ldap.idm.query.Condition;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
class OrCondition implements Condition {
private final Condition[] innerConditions;
public OrCondition(Condition... innerConditions) {
this.innerConditions = innerConditions;
}
@Override
public String getParameterName() {
return null;
}
@Override
public void setParameterName(String parameterName) {
}
@Override
public void updateParameterName(String modelParamName, String ldapParamName) {
for (Condition innerCondition : innerConditions) {
innerCondition.updateParameterName(modelParamName, ldapParamName);
}
}
@Override
public void applyCondition(StringBuilder filter) {
filter.append("(|");
for (Condition innerCondition : innerConditions) {
innerCondition.applyCondition(filter);
}
filter.append(")");
}
}

View file

@ -0,0 +1,98 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.idm.store;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import javax.naming.AuthenticationException;
import java.util.List;
/**
* IdentityStore representation providing minimal SPI
*
* TODO: Rather remove this abstraction
*
* @author Boleslaw Dawidowicz
* @author Shane Bryzak
*/
public interface IdentityStore {
/**
* Returns the configuration for this IdentityStore instance
*
* @return
*/
LDAPConfig getConfig();
// General
/**
* Persists the specified IdentityType
*
* @param ldapObject
*/
void add(LDAPObject ldapObject);
/**
* Updates the specified IdentityType
*
* @param ldapObject
*/
void update(LDAPObject ldapObject);
/**
* Removes the specified IdentityType
*
* @param ldapObject
*/
void remove(LDAPObject ldapObject);
// Identity query
List<LDAPObject> fetchQueryResults(LDAPQuery LDAPQuery);
int countQueryResults(LDAPQuery LDAPQuery);
// // Relationship query
//
// <V extends Relationship> List<V> fetchQueryResults(RelationshipQuery<V> query);
//
// <V extends Relationship> int countQueryResults(RelationshipQuery<V> query);
// Credentials
/**
* Validates the specified credentials.
*
* @param user Keycloak user
* @param password Ldap password
* @throws AuthenticationException if authentication is not successful
*/
void validatePassword(LDAPObject user, String password) throws AuthenticationException;
/**
* Updates the specified credential value.
*
* @param user Keycloak user
* @param password Ldap password
*/
void updatePassword(LDAPObject user, String password);
}

View file

@ -0,0 +1,423 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.idm.store.ldap;
import org.jboss.logging.Logger;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.idm.model.LDAPDn;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.Condition;
import org.keycloak.storage.ldap.idm.query.internal.EqualCondition;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.ldap.idm.store.IdentityStore;
import javax.naming.AuthenticationException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.TreeSet;
/**
* An IdentityStore implementation backed by an LDAP directory
*
* @author Shane Bryzak
* @author Anil Saldhana
* @author <a href="mailto:psilva@redhat.com">Pedro Silva</a>
*/
public class LDAPIdentityStore implements IdentityStore {
private static final Logger logger = Logger.getLogger(LDAPIdentityStore.class);
private final LDAPConfig config;
private final LDAPOperationManager operationManager;
public LDAPIdentityStore(LDAPConfig config) {
this.config = config;
try {
this.operationManager = new LDAPOperationManager(config);
} catch (NamingException e) {
throw new ModelException("Couldn't init operation manager", e);
}
}
@Override
public LDAPConfig getConfig() {
return this.config;
}
@Override
public void add(LDAPObject ldapObject) {
// id will be assigned by the ldap server
if (ldapObject.getUuid() != null) {
throw new ModelException("Can't add object with already assigned uuid");
}
String entryDN = ldapObject.getDn().toString();
BasicAttributes ldapAttributes = extractAttributes(ldapObject, true);
this.operationManager.createSubContext(entryDN, ldapAttributes);
ldapObject.setUuid(getEntryIdentifier(ldapObject));
if (logger.isDebugEnabled()) {
logger.debugf("Type with identifier [%s] and dn [%s] successfully added to LDAP store.", ldapObject.getUuid(), entryDN);
}
}
@Override
public void update(LDAPObject ldapObject) {
BasicAttributes updatedAttributes = extractAttributes(ldapObject, false);
NamingEnumeration<Attribute> attributes = updatedAttributes.getAll();
String entryDn = ldapObject.getDn().toString();
this.operationManager.modifyAttributes(entryDn, attributes);
if (logger.isDebugEnabled()) {
logger.debugf("Type with identifier [%s] and DN [%s] successfully updated to LDAP store.", ldapObject.getUuid(), entryDn);
}
}
@Override
public void remove(LDAPObject ldapObject) {
this.operationManager.removeEntry(ldapObject.getDn().toString());
if (logger.isDebugEnabled()) {
logger.debugf("Type with identifier [%s] and DN [%s] successfully removed from LDAP store.", ldapObject.getUuid(), ldapObject.getDn().toString());
}
}
@Override
public List<LDAPObject> fetchQueryResults(LDAPQuery identityQuery) {
if (identityQuery.getSorting() != null && !identityQuery.getSorting().isEmpty()) {
throw new ModelException("LDAP Identity Store does not yet support sorted queries.");
}
List<LDAPObject> results = new ArrayList<>();
try {
String baseDN = identityQuery.getSearchDn();
for (Condition condition : identityQuery.getConditions()) {
// Check if we are searching by ID
String uuidAttrName = getConfig().getUuidLDAPAttributeName();
if (condition instanceof EqualCondition) {
EqualCondition equalCondition = (EqualCondition) condition;
if (equalCondition.getParameterName().equalsIgnoreCase(uuidAttrName)) {
SearchResult search = this.operationManager
.lookupById(baseDN, equalCondition.getValue().toString(), identityQuery.getReturningLdapAttributes());
if (search != null) {
results.add(populateAttributedType(search, identityQuery));
}
return results;
}
}
}
StringBuilder filter = createIdentityTypeSearchFilter(identityQuery);
List<SearchResult> search;
if (getConfig().isPagination() && identityQuery.getLimit() > 0) {
search = this.operationManager.searchPaginated(baseDN, filter.toString(), identityQuery);
} else {
search = this.operationManager.search(baseDN, filter.toString(), identityQuery.getReturningLdapAttributes(), identityQuery.getSearchScope());
}
for (SearchResult result : search) {
if (!result.getNameInNamespace().equalsIgnoreCase(baseDN)) {
results.add(populateAttributedType(result, identityQuery));
}
}
} catch (Exception e) {
throw new ModelException("Querying of LDAP failed " + identityQuery, e);
}
return results;
}
@Override
public int countQueryResults(LDAPQuery identityQuery) {
int limit = identityQuery.getLimit();
int offset = identityQuery.getOffset();
identityQuery.setLimit(0);
identityQuery.setOffset(0);
int resultCount = identityQuery.getResultList().size();
identityQuery.setLimit(limit);
identityQuery.setOffset(offset);
return resultCount;
}
// *************** CREDENTIALS AND USER SPECIFIC STUFF
@Override
public void validatePassword(LDAPObject user, String password) throws AuthenticationException {
String userDN = user.getDn().toString();
if (logger.isTraceEnabled()) {
logger.tracef("Using DN [%s] for authentication of user", userDN);
}
operationManager.authenticate(userDN, password);
}
@Override
public void updatePassword(LDAPObject user, String password) {
String userDN = user.getDn().toString();
if (logger.isDebugEnabled()) {
logger.debugf("Using DN [%s] for updating LDAP password of user", userDN);
}
if (getConfig().isActiveDirectory()) {
updateADPassword(userDN, password);
} else {
ModificationItem[] mods = new ModificationItem[1];
try {
BasicAttribute mod0 = new BasicAttribute(LDAPConstants.USER_PASSWORD_ATTRIBUTE, password);
mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, mod0);
operationManager.modifyAttribute(userDN, mod0);
} catch (ModelException me) {
throw me;
} catch (Exception e) {
throw new ModelException("Error updating password.", e);
}
}
}
private void updateADPassword(String userDN, String password) {
try {
// Replace the "unicdodePwd" attribute with a new value
// Password must be both Unicode and a quoted string
String newQuotedPassword = "\"" + password + "\"";
byte[] newUnicodePassword = newQuotedPassword.getBytes("UTF-16LE");
BasicAttribute unicodePwd = new BasicAttribute("unicodePwd", newUnicodePassword);
List<ModificationItem> modItems = new ArrayList<ModificationItem>();
modItems.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, unicodePwd));
operationManager.modifyAttributes(userDN, modItems.toArray(new ModificationItem[] {}));
} catch (ModelException me) {
throw me;
} catch (Exception e) {
throw new ModelException(e);
}
}
// ************ END CREDENTIALS AND USER SPECIFIC STUFF
protected StringBuilder createIdentityTypeSearchFilter(final LDAPQuery identityQuery) {
StringBuilder filter = new StringBuilder();
for (Condition condition : identityQuery.getConditions()) {
condition.applyCondition(filter);
}
filter.insert(0, "(&");
filter.append(getObjectClassesFilter(identityQuery.getObjectClasses()));
filter.append(")");
if (logger.isTraceEnabled()) {
logger.tracef("Using filter for LDAP search: %s . Searching in DN: %s", filter, identityQuery.getSearchDn());
}
return filter;
}
private StringBuilder getObjectClassesFilter(Collection<String> objectClasses) {
StringBuilder builder = new StringBuilder();
if (!objectClasses.isEmpty()) {
for (String objectClass : objectClasses) {
builder.append("(").append(LDAPConstants.OBJECT_CLASS).append(LDAPConstants.EQUAL).append(objectClass).append(")");
}
} else {
builder.append("(").append(LDAPConstants.OBJECT_CLASS).append(LDAPConstants.EQUAL).append("*").append(")");
}
return builder;
}
private LDAPObject populateAttributedType(SearchResult searchResult, LDAPQuery ldapQuery) {
Set<String> readOnlyAttrNames = ldapQuery.getReturningReadOnlyLdapAttributes();
Set<String> lowerCasedAttrNames = new TreeSet<>();
for (String attrName : ldapQuery.getReturningLdapAttributes()) {
lowerCasedAttrNames.add(attrName.toLowerCase());
}
try {
String entryDN = searchResult.getNameInNamespace();
Attributes attributes = searchResult.getAttributes();
LDAPObject ldapObject = new LDAPObject();
LDAPDn dn = LDAPDn.fromString(entryDN);
ldapObject.setDn(dn);
ldapObject.setRdnAttributeName(dn.getFirstRdnAttrName());
NamingEnumeration<? extends Attribute> ldapAttributes = attributes.getAll();
while (ldapAttributes.hasMore()) {
Attribute ldapAttribute = ldapAttributes.next();
try {
ldapAttribute.get();
} catch (NoSuchElementException nsee) {
continue;
}
String ldapAttributeName = ldapAttribute.getID();
if (ldapAttributeName.equalsIgnoreCase(getConfig().getUuidLDAPAttributeName())) {
Object uuidValue = ldapAttribute.get();
ldapObject.setUuid(this.operationManager.decodeEntryUUID(uuidValue));
}
// Note: UUID is normally not populated here. It's populated just in case that it's used for name of other attribute as well
if (!ldapAttributeName.equalsIgnoreCase(getConfig().getUuidLDAPAttributeName()) || (lowerCasedAttrNames.contains(ldapAttributeName.toLowerCase()))) {
Set<String> attrValues = new LinkedHashSet<>();
NamingEnumeration<?> enumm = ldapAttribute.getAll();
while (enumm.hasMoreElements()) {
String attrVal = enumm.next().toString().trim();
attrValues.add(attrVal);
}
if (ldapAttributeName.equalsIgnoreCase(LDAPConstants.OBJECT_CLASS)) {
ldapObject.setObjectClasses(attrValues);
} else {
ldapObject.setAttribute(ldapAttributeName, attrValues);
// readOnlyAttrNames are lower-cased
if (readOnlyAttrNames.contains(ldapAttributeName.toLowerCase())) {
ldapObject.addReadOnlyAttributeName(ldapAttributeName);
}
}
}
}
if (logger.isTraceEnabled()) {
logger.tracef("Found ldap object and populated with the attributes. LDAP Object: %s", ldapObject.toString());
}
return ldapObject;
} catch (Exception e) {
throw new ModelException("Could not populate attribute type " + searchResult.getNameInNamespace() + ".", e);
}
}
protected BasicAttributes extractAttributes(LDAPObject ldapObject, boolean isCreate) {
BasicAttributes entryAttributes = new BasicAttributes();
for (Map.Entry<String, Set<String>> attrEntry : ldapObject.getAttributes().entrySet()) {
String attrName = attrEntry.getKey();
Set<String> attrValue = attrEntry.getValue();
// ldapObject.getReadOnlyAttributeNames() are lower-cased
if (!ldapObject.getReadOnlyAttributeNames().contains(attrName.toLowerCase()) && (isCreate || !ldapObject.getRdnAttributeName().equalsIgnoreCase(attrName))) {
if (attrValue == null) {
// Shouldn't happen
logger.warnf("Attribute '%s' is null on LDAP object '%s' . Using empty value to be saved to LDAP", attrName, ldapObject.getDn().toString());
attrValue = Collections.emptySet();
}
// Ignore empty attributes during create
if (isCreate && attrValue.isEmpty()) {
continue;
}
BasicAttribute attr = new BasicAttribute(attrName);
for (String val : attrValue) {
if (val == null || val.toString().trim().length() == 0) {
val = LDAPConstants.EMPTY_ATTRIBUTE_VALUE;
}
attr.add(val);
}
entryAttributes.put(attr);
}
}
// Don't extract object classes for update
if (isCreate) {
BasicAttribute objectClassAttribute = new BasicAttribute(LDAPConstants.OBJECT_CLASS);
for (String objectClassValue : ldapObject.getObjectClasses()) {
objectClassAttribute.add(objectClassValue);
if (objectClassValue.equalsIgnoreCase(LDAPConstants.GROUP_OF_NAMES)
|| objectClassValue.equalsIgnoreCase(LDAPConstants.GROUP_OF_ENTRIES)
|| objectClassValue.equalsIgnoreCase(LDAPConstants.GROUP_OF_UNIQUE_NAMES)) {
entryAttributes.put(LDAPConstants.MEMBER, LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE);
}
}
entryAttributes.put(objectClassAttribute);
}
return entryAttributes;
}
protected String getEntryIdentifier(final LDAPObject ldapObject) {
try {
// we need this to retrieve the entry's identifier from the ldap server
String uuidAttrName = getConfig().getUuidLDAPAttributeName();
List<SearchResult> search = this.operationManager.search(ldapObject.getDn().toString(), "(" + ldapObject.getDn().getFirstRdn() + ")", Arrays.asList(uuidAttrName), SearchControls.OBJECT_SCOPE);
Attribute id = search.get(0).getAttributes().get(getConfig().getUuidLDAPAttributeName());
if (id == null) {
throw new ModelException("Could not retrieve identifier for entry [" + ldapObject.getDn().toString() + "].");
}
return this.operationManager.decodeEntryUUID(id.get());
} catch (NamingException ne) {
throw new ModelException("Could not retrieve identifier for entry [" + ldapObject.getDn().toString() + "].");
}
}
}

View file

@ -0,0 +1,562 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.idm.store.ldap;
import org.jboss.logging.Logger;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import javax.naming.AuthenticationException;
import javax.naming.Binding;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.Control;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.PagedResultsControl;
import javax.naming.ldap.PagedResultsResponseControl;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
/**
* <p>This class provides a set of operations to manage LDAP trees.</p>
*
* @author Anil Saldhana
* @author <a href="mailto:psilva@redhat.com">Pedro Silva</a>
*/
public class LDAPOperationManager {
private static final Logger logger = Logger.getLogger(LDAPOperationManager.class);
private final LDAPConfig config;
private final Map<String, Object> connectionProperties;
public LDAPOperationManager(LDAPConfig config) throws NamingException {
this.config = config;
this.connectionProperties = Collections.unmodifiableMap(createConnectionProperties());
}
/**
* <p>
* Modifies the given {@link javax.naming.directory.Attribute} instance using the given DN. This method performs a REPLACE_ATTRIBUTE
* operation.
* </p>
*
* @param dn
* @param attribute
*/
public void modifyAttribute(String dn, Attribute attribute) {
ModificationItem[] mods = new ModificationItem[]{new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attribute)};
modifyAttributes(dn, mods);
}
/**
* <p>
* Modifies the given {@link Attribute} instances using the given DN. This method performs a REPLACE_ATTRIBUTE
* operation.
* </p>
*
* @param dn
* @param attributes
*/
public void modifyAttributes(String dn, NamingEnumeration<Attribute> attributes) {
try {
List<ModificationItem> modItems = new ArrayList<ModificationItem>();
while (attributes.hasMore()) {
ModificationItem modItem = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attributes.next());
modItems.add(modItem);
}
modifyAttributes(dn, modItems.toArray(new ModificationItem[] {}));
} catch (NamingException ne) {
throw new ModelException("Could not modify attributes on entry from DN [" + dn + "]", ne);
}
}
/**
* <p>
* Removes the given {@link Attribute} instance using the given DN. This method performs a REMOVE_ATTRIBUTE
* operation.
* </p>
*
* @param dn
* @param attribute
*/
public void removeAttribute(String dn, Attribute attribute) {
ModificationItem[] mods = new ModificationItem[]{new ModificationItem(DirContext.REMOVE_ATTRIBUTE, attribute)};
modifyAttributes(dn, mods);
}
/**
* <p>
* Adds the given {@link Attribute} instance using the given DN. This method performs a ADD_ATTRIBUTE operation.
* </p>
*
* @param dn
* @param attribute
*/
public void addAttribute(String dn, Attribute attribute) {
ModificationItem[] mods = new ModificationItem[]{new ModificationItem(DirContext.ADD_ATTRIBUTE, attribute)};
modifyAttributes(dn, mods);
}
/**
* <p>
* Removes the object from the LDAP tree
* </p>
*/
public void removeEntry(final String entryDn) {
try {
execute(new LdapOperation<SearchResult>() {
@Override
public SearchResult execute(LdapContext context) throws NamingException {
if (logger.isTraceEnabled()) {
logger.tracef("Removing entry with DN [%s]", entryDn);
}
destroySubcontext(context, entryDn);
return null;
}
});
} catch (NamingException e) {
throw new ModelException("Could not remove entry from DN [" + entryDn + "]", e);
}
}
public List<SearchResult> search(final String baseDN, final String filter, Collection<String> returningAttributes, int searchScope) throws NamingException {
final List<SearchResult> result = new ArrayList<SearchResult>();
final SearchControls cons = getSearchControls(returningAttributes, searchScope);
try {
return execute(new LdapOperation<List<SearchResult>>() {
@Override
public List<SearchResult> execute(LdapContext context) throws NamingException {
NamingEnumeration<SearchResult> search = context.search(baseDN, filter, cons);
while (search.hasMoreElements()) {
result.add(search.nextElement());
}
search.close();
return result;
}
});
} catch (NamingException e) {
logger.errorf(e, "Could not query server using DN [%s] and filter [%s]", baseDN, filter);
throw e;
}
}
public List<SearchResult> searchPaginated(final String baseDN, final String filter, final LDAPQuery identityQuery) throws NamingException {
final List<SearchResult> result = new ArrayList<SearchResult>();
final SearchControls cons = getSearchControls(identityQuery.getReturningLdapAttributes(), identityQuery.getSearchScope());
try {
return execute(new LdapOperation<List<SearchResult>>() {
@Override
public List<SearchResult> execute(LdapContext context) throws NamingException {
try {
byte[] cookie = identityQuery.getPaginationContext();
PagedResultsControl pagedControls = new PagedResultsControl(identityQuery.getLimit(), cookie, Control.CRITICAL);
context.setRequestControls(new Control[] { pagedControls });
NamingEnumeration<SearchResult> search = context.search(baseDN, filter, cons);
while (search.hasMoreElements()) {
result.add(search.nextElement());
}
search.close();
Control[] responseControls = context.getResponseControls();
if (responseControls != null) {
for (Control respControl : responseControls) {
if (respControl instanceof PagedResultsResponseControl) {
PagedResultsResponseControl prrc = (PagedResultsResponseControl)respControl;
cookie = prrc.getCookie();
identityQuery.setPaginationContext(cookie);
}
}
}
return result;
} catch (IOException ioe) {
logger.errorf(ioe, "Could not query server with paginated query using DN [%s], filter [%s]", baseDN, filter);
throw new NamingException(ioe.getMessage());
}
}
});
} catch (NamingException e) {
logger.errorf(e, "Could not query server using DN [%s] and filter [%s]", baseDN, filter);
throw e;
}
}
private SearchControls getSearchControls(Collection<String> returningAttributes, int searchScope) {
final SearchControls cons = new SearchControls();
cons.setSearchScope(searchScope);
cons.setReturningObjFlag(false);
returningAttributes = getReturningAttributes(returningAttributes);
cons.setReturningAttributes(returningAttributes.toArray(new String[returningAttributes.size()]));
return cons;
}
public String getFilterById(String id) {
String filter = null;
if (this.config.isActiveDirectory()) {
final String strObjectGUID = "<GUID=" + id + ">";
try {
Attributes attributes = execute(new LdapOperation<Attributes>() {
@Override
public Attributes execute(LdapContext context) throws NamingException {
return context.getAttributes(strObjectGUID);
}
});
byte[] objectGUID = (byte[]) attributes.get(LDAPConstants.OBJECT_GUID).get();
filter = "(&(objectClass=*)(" + getUuidAttributeName() + LDAPConstants.EQUAL + LDAPUtil.convertObjectGUIToByteString(objectGUID) + "))";
} catch (NamingException ne) {
filter = null;
}
}
if (filter == null) {
filter = "(&(objectClass=*)(" + getUuidAttributeName() + LDAPConstants.EQUAL + id + "))";
}
return filter;
}
public SearchResult lookupById(final String baseDN, final String id, final Collection<String> returningAttributes) {
final String filter = getFilterById(id);
try {
final SearchControls cons = getSearchControls(returningAttributes, this.config.getSearchScope());
return execute(new LdapOperation<SearchResult>() {
@Override
public SearchResult execute(LdapContext context) throws NamingException {
NamingEnumeration<SearchResult> search = context.search(baseDN, filter, cons);
try {
if (search.hasMoreElements()) {
return search.next();
}
} finally {
if (search != null) {
search.close();
}
}
return null;
}
});
} catch (NamingException e) {
throw new ModelException("Could not query server using DN [" + baseDN + "] and filter [" + filter + "]", e);
}
}
/**
* <p>
* Destroys a subcontext with the given DN from the LDAP tree.
* </p>
*
* @param dn
*/
private void destroySubcontext(LdapContext context, final String dn) {
try {
NamingEnumeration<Binding> enumeration = null;
try {
enumeration = context.listBindings(dn);
while (enumeration.hasMore()) {
Binding binding = enumeration.next();
String name = binding.getNameInNamespace();
destroySubcontext(context, name);
}
context.unbind(dn);
} finally {
try {
enumeration.close();
} catch (Exception e) {
}
}
} catch (Exception e) {
throw new ModelException("Could not unbind DN [" + dn + "]", e);
}
}
/**
* <p>
* Performs a simple authentication using the given DN and password to bind to the authentication context.
* </p>
*
* @param dn
* @param password
* @throws AuthenticationException if authentication is not successful
*
*/
public void authenticate(String dn, String password) throws AuthenticationException {
InitialContext authCtx = null;
try {
if (password == null || password.isEmpty()) {
throw new AuthenticationException("Empty password used");
}
Hashtable<String, Object> env = new Hashtable<String, Object>(this.connectionProperties);
env.put(Context.SECURITY_AUTHENTICATION, LDAPConstants.AUTH_TYPE_SIMPLE);
env.put(Context.SECURITY_PRINCIPAL, dn);
env.put(Context.SECURITY_CREDENTIALS, password);
// Never use connection pool to prevent password caching
env.put("com.sun.jndi.ldap.connect.pool", "false");
authCtx = new InitialLdapContext(env, null);
} catch (AuthenticationException ae) {
if (logger.isDebugEnabled()) {
logger.debugf(ae, "Authentication failed for DN [%s]", dn);
}
throw ae;
} catch (Exception e) {
logger.errorf(e, "Unexpected exception when validating password of DN [%s]", dn);
throw new AuthenticationException("Unexpected exception when validating password of user");
} finally {
if (authCtx != null) {
try {
authCtx.close();
} catch (NamingException e) {
}
}
}
}
public void modifyAttributes(final String dn, final ModificationItem[] mods) {
try {
if (logger.isTraceEnabled()) {
logger.tracef("Modifying attributes for entry [%s]: [", dn);
for (ModificationItem item : mods) {
Object values;
if (item.getAttribute().size() > 0) {
values = item.getAttribute().get();
} else {
values = "No values";
}
logger.tracef(" Op [%s]: %s = %s", item.getModificationOp(), item.getAttribute().getID(), values);
}
logger.tracef("]");
}
execute(new LdapOperation<Void>() {
@Override
public Void execute(LdapContext context) throws NamingException {
context.modifyAttributes(dn, mods);
return null;
}
});
} catch (NamingException e) {
throw new ModelException("Could not modify attribute for DN [" + dn + "]", e);
}
}
public void createSubContext(final String name, final Attributes attributes) {
try {
if (logger.isTraceEnabled()) {
logger.tracef("Creating entry [%s] with attributes: [", name);
NamingEnumeration<? extends Attribute> all = attributes.getAll();
while (all.hasMore()) {
Attribute attribute = all.next();
logger.tracef(" %s = %s", attribute.getID(), attribute.get());
}
logger.tracef("]");
}
execute(new LdapOperation<Void>() {
@Override
public Void execute(LdapContext context) throws NamingException {
DirContext subcontext = context.createSubcontext(name, attributes);
subcontext.close();
return null;
}
});
} catch (NamingException e) {
throw new ModelException("Error creating subcontext [" + name + "]", e);
}
}
private String getUuidAttributeName() {
return this.config.getUuidLDAPAttributeName();
}
public Attributes getAttributes(final String entryUUID, final String baseDN, Set<String> returningAttributes) {
SearchResult search = lookupById(baseDN, entryUUID, returningAttributes);
if (search == null) {
throw new ModelException("Couldn't find item with ID [" + entryUUID + " under base DN [" + baseDN + "]");
}
return search.getAttributes();
}
public String decodeEntryUUID(final Object entryUUID) {
String id;
if (this.config.isActiveDirectory() && entryUUID instanceof byte[]) {
id = LDAPUtil.decodeObjectGUID((byte[]) entryUUID);
} else {
id = entryUUID.toString();
}
return id;
}
private LdapContext createLdapContext() throws NamingException {
return new InitialLdapContext(new Hashtable<Object, Object>(this.connectionProperties), null);
}
private Map<String, Object> createConnectionProperties() {
HashMap<String, Object> env = new HashMap<String, Object>();
String authType = this.config.getAuthType();
env.put(Context.INITIAL_CONTEXT_FACTORY, this.config.getFactoryName());
env.put(Context.SECURITY_AUTHENTICATION, authType);
String bindDN = this.config.getBindDN();
char[] bindCredential = null;
if (this.config.getBindCredential() != null) {
bindCredential = this.config.getBindCredential().toCharArray();
}
if (!LDAPConstants.AUTH_TYPE_NONE.equals(authType)) {
env.put(Context.SECURITY_PRINCIPAL, bindDN);
env.put(Context.SECURITY_CREDENTIALS, bindCredential);
}
String url = this.config.getConnectionUrl();
if (url != null) {
env.put(Context.PROVIDER_URL, url);
} else {
logger.warn("LDAP URL is null. LDAPOperationManager won't work correctly");
}
String useTruststoreSpi = this.config.getUseTruststoreSpi();
LDAPConstants.setTruststoreSpiIfNeeded(useTruststoreSpi, url, env);
String connectionPooling = this.config.getConnectionPooling();
if (connectionPooling != null) {
env.put("com.sun.jndi.ldap.connect.pool", connectionPooling);
}
// Just dump the additional properties
Properties additionalProperties = this.config.getAdditionalConnectionProperties();
if (additionalProperties != null) {
for (Object key : additionalProperties.keySet()) {
env.put(key.toString(), additionalProperties.getProperty(key.toString()));
}
}
if (config.isActiveDirectory()) {
env.put("java.naming.ldap.attributes.binary", LDAPConstants.OBJECT_GUID);
}
if (logger.isDebugEnabled()) {
logger.debugf("Creating LdapContext using properties: [%s]", env);
}
return env;
}
private <R> R execute(LdapOperation<R> operation) throws NamingException {
LdapContext context = null;
try {
context = createLdapContext();
return operation.execute(context);
} catch (NamingException ne) {
throw ne;
} finally {
if (context != null) {
try {
context.close();
} catch (NamingException ne) {
logger.error("Could not close Ldap context.", ne);
}
}
}
}
private interface LdapOperation<R> {
R execute(LdapContext context) throws NamingException;
}
private Set<String> getReturningAttributes(final Collection<String> returningAttributes) {
Set<String> result = new HashSet<String>();
result.addAll(returningAttributes);
result.add(getUuidAttributeName());
result.add(LDAPConstants.OBJECT_CLASS);
return result;
}
}

View file

@ -0,0 +1,175 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.idm.store.ldap;
import org.keycloak.models.ModelException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
/**
* <p>Utility class for working with LDAP.</p>
*
* @author Pedro Igor
*/
public class LDAPUtil {
/**
* <p>Formats the given date.</p>
*
* @param date The Date to format.
*
* @return A String representing the formatted date.
*/
public static final String formatDate(Date date) {
if (date == null) {
throw new IllegalArgumentException("You must provide a date.");
}
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss'.0Z'");
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
return dateFormat.format(date);
}
/**
* <p>
* Parses dates/time stamps stored in LDAP. Some possible values:
* </p>
* <ul>
* <li>20020228150820</li>
* <li>20030228150820Z</li>
* <li>20050228150820.12</li>
* <li>20060711011740.0Z</li>
* </ul>
*
* @param date The date string to parse from.
*
* @return the Date.
*/
public static final Date parseDate(String date) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
try {
if (date.endsWith("Z")) {
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
} else {
dateFormat.setTimeZone(TimeZone.getDefault());
}
return dateFormat.parse(date);
} catch (Exception e) {
throw new ModelException("Error converting ldap date.", e);
}
}
/**
* <p>Creates a byte-based {@link String} representation of a raw byte array representing the value of the
* <code>objectGUID</code> attribute retrieved from Active Directory.</p>
*
* <p>The returned string is useful to perform queries on AD based on the <code>objectGUID</code> value. Eg.:</p>
*
* <p>
* String filter = "(&(objectClass=*)(objectGUID" + EQUAL + convertObjectGUIToByteString(objectGUID) + "))";
* </p>
*
* @param objectGUID A raw byte array representing the value of the <code>objectGUID</code> attribute retrieved from
* Active Directory.
*
* @return A byte-based String representation in the form of \[0]\[1]\[2]\[3]\[4]\[5]\[6]\[7]\[8]\[9]\[10]\[11]\[12]\[13]\[14]\[15]
*/
public static String convertObjectGUIToByteString(byte[] objectGUID) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < objectGUID.length; i++) {
String transformed = prefixZeros((int) objectGUID[i] & 0xFF);
result.append("\\");
result.append(transformed);
}
return result.toString();
}
/**
* <p>Decode a raw byte array representing the value of the <code>objectGUID</code> attribute retrieved from Active
* Directory.</p>
*
* <p>The returned string is useful to directly bind an entry. Eg.:</p>
*
* <p>
* String bindingString = decodeObjectGUID(objectGUID);
* <br/>
* Attributes attributes = ctx.getAttributes(bindingString);
* </p>
*
* @param objectGUID A raw byte array representing the value of the <code>objectGUID</code> attribute retrieved from
* Active Directory.
*
* @return A string representing the decoded value in the form of [3][2][1][0]-[5][4]-[7][6]-[8][9]-[10][11][12][13][14][15].
*/
public static String decodeObjectGUID(byte[] objectGUID) {
StringBuilder displayStr = new StringBuilder();
displayStr.append(convertToDashedString(objectGUID));
return displayStr.toString();
}
private static String convertToDashedString(byte[] objectGUID) {
StringBuilder displayStr = new StringBuilder();
displayStr.append(prefixZeros((int) objectGUID[3] & 0xFF));
displayStr.append(prefixZeros((int) objectGUID[2] & 0xFF));
displayStr.append(prefixZeros((int) objectGUID[1] & 0xFF));
displayStr.append(prefixZeros((int) objectGUID[0] & 0xFF));
displayStr.append("-");
displayStr.append(prefixZeros((int) objectGUID[5] & 0xFF));
displayStr.append(prefixZeros((int) objectGUID[4] & 0xFF));
displayStr.append("-");
displayStr.append(prefixZeros((int) objectGUID[7] & 0xFF));
displayStr.append(prefixZeros((int) objectGUID[6] & 0xFF));
displayStr.append("-");
displayStr.append(prefixZeros((int) objectGUID[8] & 0xFF));
displayStr.append(prefixZeros((int) objectGUID[9] & 0xFF));
displayStr.append("-");
displayStr.append(prefixZeros((int) objectGUID[10] & 0xFF));
displayStr.append(prefixZeros((int) objectGUID[11] & 0xFF));
displayStr.append(prefixZeros((int) objectGUID[12] & 0xFF));
displayStr.append(prefixZeros((int) objectGUID[13] & 0xFF));
displayStr.append(prefixZeros((int) objectGUID[14] & 0xFF));
displayStr.append(prefixZeros((int) objectGUID[15] & 0xFF));
return displayStr.toString();
}
private static String prefixZeros(int value) {
if (value <= 0xF) {
StringBuilder sb = new StringBuilder("0");
sb.append(Integer.toHexString(value));
return sb.toString();
} else {
return Integer.toHexString(value);
}
}
}

View file

@ -0,0 +1,39 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.kerberos;
import org.keycloak.common.constants.KerberosConstants;
import org.keycloak.component.ComponentModel;
import org.keycloak.federation.kerberos.CommonKerberosConfig;
import org.keycloak.storage.ldap.LDAPStorageProvider;
/**
* Configuration specific to {@link LDAPStorageProvider}
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LDAPProviderKerberosConfig extends CommonKerberosConfig {
public LDAPProviderKerberosConfig(ComponentModel componentModel) {
super(componentModel);
}
public boolean isUseKerberosForPasswordAuthentication() {
return Boolean.valueOf(componentModel.getConfig().getFirst(KerberosConstants.USE_KERBEROS_FOR_PASSWORD_AUTHENTICATION));
}
}

View file

@ -0,0 +1,106 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.user.SynchronizationResult;
import javax.naming.AuthenticationException;
import java.util.Collections;
import java.util.List;
/**
* Stateful per-request object
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public abstract class AbstractLDAPStorageMapper {
protected final ComponentModel mapperModel;
protected final LDAPStorageProvider ldapProvider;
protected final RealmModel realm;
public AbstractLDAPStorageMapper(ComponentModel mapperModel, LDAPStorageProvider ldapProvider, RealmModel realm) {
this.mapperModel = mapperModel;
this.ldapProvider = ldapProvider;
this.realm = realm;
}
/**
* @see LDAPStorageMapper#syncDataFromFederationProviderToKeycloak(ComponentModel, LDAPStorageProvider, KeycloakSession, RealmModel)
*/
public SynchronizationResult syncDataFromFederationProviderToKeycloak() {
return new SynchronizationResult();
}
/**
* @see LDAPStorageMapper#syncDataFromKeycloakToFederationProvider(ComponentModel, LDAPStorageProvider, KeycloakSession, RealmModel)
*/
public SynchronizationResult syncDataFromKeycloakToFederationProvider() {
return new SynchronizationResult();
}
/**
* @see LDAPStorageMapper#beforeLDAPQuery(ComponentModel, LDAPQuery)
*/
public abstract void beforeLDAPQuery(LDAPQuery query);
/**
* @see LDAPStorageMapper#proxy(ComponentModel, LDAPStorageProvider, LDAPObject, UserModel, RealmModel)
*/
public abstract UserModel proxy(LDAPObject ldapUser, UserModel delegate);
/**
* @see LDAPStorageMapper#onRegisterUserToLDAP(ComponentModel, LDAPStorageProvider, LDAPObject, UserModel, RealmModel)
*/
public abstract void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser);
/**
* @see LDAPStorageMapper#onImportUserFromLDAP(ComponentModel, LDAPStorageProvider, LDAPObject, UserModel, RealmModel, boolean)
*/
public abstract void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate);
public List<UserModel> getGroupMembers(GroupModel group, int firstResult, int maxResults) {
return Collections.emptyList();
}
public boolean onAuthenticationFailure(LDAPObject ldapUser, UserModel user, AuthenticationException ldapException) {
return false;
}
public static boolean parseBooleanParameter(ComponentModel mapperModel, String paramName) {
String paramm = mapperModel.getConfig().getFirst(paramName);
return Boolean.parseBoolean(paramm);
}
public LDAPStorageProvider getLdapProvider() {
return ldapProvider;
}
public RealmModel getRealm() {
return realm;
}
}

View file

@ -0,0 +1,89 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers;
import org.keycloak.Config;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import java.util.List;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public abstract class AbstractLDAPStorageMapperFactory implements LDAPStorageMapperFactory<LDAPStorageMapper> {
// Used to map attributes from LDAP to UserModel attributes
public static final String ATTRIBUTE_MAPPER_CATEGORY = "Attribute Mapper";
// Used to map roles from LDAP to UserModel users
public static final String ROLE_MAPPER_CATEGORY = "Role Mapper";
// Used to map group from LDAP to UserModel users
public static final String GROUP_MAPPER_CATEGORY = "Group Mapper";
@Override
public void init(Config.Scope config) {
}
@Override
public LDAPStorageMapper create(KeycloakSession session, ComponentModel model) {
return new LDAPStorageMapperBridge(this); }
// Used just by LDAPFederationMapperBridge.
protected abstract AbstractLDAPStorageMapper createMapper(ComponentModel mapperModel, LDAPStorageProvider federationProvider, RealmModel realm);
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public UserFederationMapperSyncConfigRepresentation getSyncConfig() {
return new UserFederationMapperSyncConfigRepresentation(false, null, false, null);
}
@Override
public void close() {
}
public static ProviderConfigProperty createConfigProperty(String name, String label, String helpText, String type, List<String> options) {
ProviderConfigProperty configProperty = new ProviderConfigProperty();
configProperty.setName(name);
configProperty.setLabel(label);
configProperty.setHelpText(helpText);
configProperty.setType(type);
configProperty.setOptions(options);
return configProperty;
}
protected void checkMandatoryConfigAttribute(String name, String displayName, ComponentModel mapperModel) throws ComponentValidationException {
String attrConfigValue = mapperModel.getConfig().getFirst(name);
if (attrConfigValue == null || attrConfigValue.trim().isEmpty()) {
throw new ComponentValidationException("Missing configuration for '" + displayName + "'");
}
}
}

View file

@ -0,0 +1,195 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.Condition;
import org.keycloak.storage.ldap.idm.query.internal.EqualCondition;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import java.util.HashSet;
import java.util.Set;
/**
* Mapper useful for the LDAP deployments when some attribute (usually CN) is mapped to full name of user
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class FullNameLDAPStorageMapper extends AbstractLDAPStorageMapper {
public static final String LDAP_FULL_NAME_ATTRIBUTE = "ldap.full.name.attribute";
public static final String READ_ONLY = "read.only";
public static final String WRITE_ONLY = "write.only";
public FullNameLDAPStorageMapper(ComponentModel mapperModel, LDAPStorageProvider ldapProvider, RealmModel realm) {
super(mapperModel, ldapProvider, realm);
}
@Override
public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate) {
if (isWriteOnly()) {
return;
}
String ldapFullNameAttrName = getLdapFullNameAttrName();
String fullName = ldapUser.getAttributeAsString(ldapFullNameAttrName);
if (fullName == null) {
return;
}
fullName = fullName.trim();
if (!fullName.isEmpty()) {
int lastSpaceIndex = fullName.lastIndexOf(" ");
if (lastSpaceIndex == -1) {
user.setLastName(fullName);
} else {
user.setFirstName(fullName.substring(0, lastSpaceIndex));
user.setLastName(fullName.substring(lastSpaceIndex + 1));
}
}
}
@Override
public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser) {
String ldapFullNameAttrName = getLdapFullNameAttrName();
String fullName = getFullName(localUser.getFirstName(), localUser.getLastName());
ldapUser.setSingleAttribute(ldapFullNameAttrName, fullName);
if (isReadOnly()) {
ldapUser.addReadOnlyAttributeName(ldapFullNameAttrName);
}
}
@Override
public UserModel proxy(LDAPObject ldapUser, UserModel delegate) {
if (ldapProvider.getEditMode() == LDAPStorageProviderFactory.EditMode.WRITABLE && !isReadOnly()) {
TxAwareLDAPUserModelDelegate txDelegate = new TxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapUser) {
@Override
public void setFirstName(String firstName) {
super.setFirstName(firstName);
setFullNameToLDAPObject();
}
@Override
public void setLastName(String lastName) {
super.setLastName(lastName);
setFullNameToLDAPObject();
}
private void setFullNameToLDAPObject() {
String fullName = getFullName(getFirstName(), getLastName());
if (logger.isTraceEnabled()) {
logger.tracef("Pushing full name attribute to LDAP. Full name: %s", fullName);
}
ensureTransactionStarted();
String ldapFullNameAttrName = getLdapFullNameAttrName();
ldapUser.setSingleAttribute(ldapFullNameAttrName, fullName);
}
};
return txDelegate;
} else {
return delegate;
}
}
@Override
public void beforeLDAPQuery(LDAPQuery query) {
if (isWriteOnly()) {
return;
}
String ldapFullNameAttrName = getLdapFullNameAttrName();
query.addReturningLdapAttribute(ldapFullNameAttrName);
// Change conditions and compute condition for fullName from the conditions for firstName and lastName. Right now just "equal" condition is supported
EqualCondition firstNameCondition = null;
EqualCondition lastNameCondition = null;
Set<Condition> conditionsCopy = new HashSet<Condition>(query.getConditions());
for (Condition condition : conditionsCopy) {
String paramName = condition.getParameterName();
if (paramName != null) {
if (paramName.equals(UserModel.FIRST_NAME)) {
firstNameCondition = (EqualCondition) condition;
query.getConditions().remove(condition);
} else if (paramName.equals(UserModel.LAST_NAME)) {
lastNameCondition = (EqualCondition) condition;
query.getConditions().remove(condition);
} else if (paramName.equals(LDAPConstants.GIVENNAME)) {
// Some previous mapper already converted it to LDAP name
firstNameCondition = (EqualCondition) condition;
} else if (paramName.equals(LDAPConstants.SN)) {
// Some previous mapper already converted it to LDAP name
lastNameCondition = (EqualCondition) condition;
}
}
}
String fullName = null;
if (firstNameCondition != null && lastNameCondition != null) {
fullName = firstNameCondition.getValue() + " " + lastNameCondition.getValue();
} else if (firstNameCondition != null) {
fullName = (String) firstNameCondition.getValue();
} else if (lastNameCondition != null) {
fullName = (String) lastNameCondition.getValue();
} else {
return;
}
EqualCondition fullNameCondition = new EqualCondition(ldapFullNameAttrName, fullName);
query.addWhereCondition(fullNameCondition);
}
protected String getLdapFullNameAttrName() {
String ldapFullNameAttrName = mapperModel.getConfig().getFirst(LDAP_FULL_NAME_ATTRIBUTE);
return ldapFullNameAttrName == null ? LDAPConstants.CN : ldapFullNameAttrName;
}
protected String getFullName(String firstName, String lastName) {
if (firstName != null && lastName != null) {
return firstName + " " + lastName;
} else if (firstName != null) {
return firstName;
} else if (lastName != null) {
return lastName;
} else {
return LDAPConstants.EMPTY_ATTRIBUTE_VALUE;
}
}
private boolean isReadOnly() {
return parseBooleanParameter(mapperModel, READ_ONLY);
}
private boolean isWriteOnly() {
return parseBooleanParameter(mapperModel, WRITE_ONLY);
}
}

View file

@ -0,0 +1,124 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserFederationProvider;
import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class FullNameLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFactory {
public static final String PROVIDER_ID = "full-name-ldap-mapper";
protected static final List<ProviderConfigProperty> configProperties;
static {
configProperties = getConfigProps(null);
}
private static List<ProviderConfigProperty> getConfigProps(ComponentModel parent) {
boolean readOnly = false;
if (parent != null) {
LDAPConfig config = new LDAPConfig(parent.getConfig());
readOnly = config.getEditMode() != LDAPStorageProviderFactory.EditMode.WRITABLE;
}
return ProviderConfigurationBuilder.create()
.property().name(FullNameLDAPStorageMapper.LDAP_FULL_NAME_ATTRIBUTE)
.label("LDAP Full Name Attribute")
.helpText("Name of LDAP attribute, which contains fullName of user. Usually it will be 'cn' ")
.type(ProviderConfigProperty.STRING_TYPE)
.defaultValue(LDAPConstants.CN)
.add()
.property().name(FullNameLDAPStorageMapper.READ_ONLY)
.label("Read Only")
.helpText("For Read-only is data imported from LDAP to Keycloak DB, but it's not saved back to LDAP when user is updated in Keycloak.")
.type(ProviderConfigProperty.BOOLEAN_TYPE)
.defaultValue(String.valueOf(readOnly))
.add()
.property().name(FullNameLDAPStorageMapper.WRITE_ONLY)
.label("Write Only")
.helpText("For Write-only is data propagated to LDAP when user is created or updated in Keycloak. But this mapper is not used to propagate data from LDAP back into Keycloak. " +
"This setting is useful if you configured separate firstName and lastName attribute mappers and you want to use those to read attribute from LDAP into Keycloak")
.type(ProviderConfigProperty.BOOLEAN_TYPE)
.defaultValue(String.valueOf(!readOnly))
.add()
.build();
}
@Override
public String getHelpText() {
return "Used to map full-name of user from single attribute in LDAP (usually 'cn' attribute) to firstName and lastName attributes of UserModel in Keycloak DB";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
checkMandatoryConfigAttribute(FullNameLDAPStorageMapper.LDAP_FULL_NAME_ATTRIBUTE, "LDAP Full Name Attribute", config);
boolean readOnly = AbstractLDAPStorageMapper.parseBooleanParameter(config, FullNameLDAPStorageMapper.READ_ONLY);
boolean writeOnly = AbstractLDAPStorageMapper.parseBooleanParameter(config, FullNameLDAPStorageMapper.WRITE_ONLY);
ComponentModel parent = realm.getComponent(config.getParentId());
if (parent == null) {
throw new ComponentValidationException("can't find parent component model");
}
LDAPConfig cfg = new LDAPConfig(parent.getConfig());
LDAPStorageProviderFactory.EditMode editMode = cfg.getEditMode();
if (writeOnly && cfg.getEditMode() != LDAPStorageProviderFactory.EditMode.WRITABLE) {
throw new ComponentValidationException("ldapErrorCantWriteOnlyForReadOnlyLdap");
}
if (writeOnly && readOnly) {
throw new ComponentValidationException("ldapErrorCantWriteOnlyAndReadOnly");
}
}
@Override
protected AbstractLDAPStorageMapper createMapper(ComponentModel mapperModel, LDAPStorageProvider federationProvider, RealmModel realm) {
return new FullNameLDAPStorageMapper(mapperModel, federationProvider, realm);
}
}

View file

@ -0,0 +1,126 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers;
import org.jboss.logging.Logger;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.UserModelDelegate;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import java.util.Set;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class HardcodedLDAPRoleStorageMapper extends AbstractLDAPStorageMapper {
private static final Logger logger = Logger.getLogger(HardcodedLDAPRoleStorageMapper.class);
public static final String ROLE = "role";
public HardcodedLDAPRoleStorageMapper(ComponentModel mapperModel, LDAPStorageProvider ldapProvider, RealmModel realm) {
super(mapperModel, ldapProvider, realm);
}
@Override
public void beforeLDAPQuery(LDAPQuery query) {
}
@Override
public UserModel proxy(LDAPObject ldapUser, UserModel delegate) {
return new UserModelDelegate(delegate) {
@Override
public Set<RoleModel> getRealmRoleMappings() {
Set<RoleModel> roles = super.getRealmRoleMappings();
RoleModel role = getRole();
if (role != null && role.getContainer().equals(realm)) {
roles.add(role);
}
return roles;
}
@Override
public Set<RoleModel> getClientRoleMappings(ClientModel app) {
Set<RoleModel> roles = super.getClientRoleMappings(app);
RoleModel role = getRole();
if (role != null && role.getContainer().equals(app)) {
roles.add(role);
}
return roles;
}
@Override
public boolean hasRole(RoleModel role) {
return super.hasRole(role) || role.equals(getRole());
}
@Override
public Set<RoleModel> getRoleMappings() {
Set<RoleModel> roles = super.getRoleMappings();
RoleModel role = getRole();
if (role != null) {
roles.add(role);
}
return roles;
}
@Override
public void deleteRoleMapping(RoleModel role) {
if (role.equals(getRole())) {
throw new ModelException("Not possible to delete role. It's hardcoded by LDAP mapper");
} else {
super.deleteRoleMapping(role);
}
}
};
}
@Override
public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser) {
}
@Override
public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate) {
}
private RoleModel getRole() {
String roleName = mapperModel.getConfig().getFirst(HardcodedLDAPRoleStorageMapper.ROLE);
RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName);
if (role == null) {
logger.warnf("Hardcoded role '%s' configured in mapper '%s' is not available anymore");
}
return role;
}
}

View file

@ -0,0 +1,78 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import java.util.ArrayList;
import java.util.List;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class HardcodedLDAPRoleStorageMapperFactory extends AbstractLDAPStorageMapperFactory {
public static final String PROVIDER_ID = "hardcoded-ldap-role-mapper";
protected static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
static {
ProviderConfigProperty roleAttr = createConfigProperty(HardcodedLDAPRoleStorageMapper.ROLE, "Role",
"Role to grant to user. Click 'Select Role' button to browse roles, or just type it in the textbox. To reference an application role the syntax is appname.approle, i.e. myapp.myrole",
ProviderConfigProperty.ROLE_TYPE, null);
configProperties.add(roleAttr);
}
@Override
public String getHelpText() {
return "When user is imported from LDAP, he will be automatically added into this configured role.";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
String roleName = config.getConfig().getFirst(HardcodedLDAPRoleStorageMapper.ROLE);
if (roleName == null) {
throw new ComponentValidationException("Role can't be null");
}
RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName);
if (role == null) {
throw new ComponentValidationException("There is no role corresponding to configured value");
}
}
@Override
protected AbstractLDAPStorageMapper createMapper(ComponentModel mapperModel, LDAPStorageProvider federationProvider, RealmModel realm) {
return new HardcodedLDAPRoleStorageMapper(mapperModel, federationProvider, realm);
}
}

View file

@ -0,0 +1,114 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.UserModel;
import org.keycloak.storage.ldap.LDAPConfig;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* TODO: Possibly add "priority" instead of hardcoding behaviour
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LDAPMappersComparator {
public static List<ComponentModel> sortAsc(LDAPConfig ldapConfig, Collection<ComponentModel> mappers) {
Comparator<ComponentModel> comparator = new ImportantFirstComparator(ldapConfig);
List<ComponentModel> result = new ArrayList<>(mappers);
Collections.sort(result, comparator);
return result;
}
public static List<ComponentModel> sortDesc(LDAPConfig ldapConfig, Collection<ComponentModel> mappers) {
Comparator<ComponentModel> comparator = new ImportantFirstComparator(ldapConfig).reversed();
List<ComponentModel> result = new ArrayList<>(mappers);
Collections.sort(result, comparator);
return result;
}
private static class ImportantFirstComparator implements Comparator<ComponentModel> {
private final LDAPConfig ldapConfig;
public ImportantFirstComparator(LDAPConfig ldapConfig) {
this.ldapConfig = ldapConfig;
}
@Override
public int compare(ComponentModel o1, ComponentModel o2) {
// UserAttributeLDAPFederationMapper first
boolean isO1AttrMapper = o1.getProviderType().equals(UserAttributeLDAPStorageMapperFactory.PROVIDER_ID);
boolean isO2AttrMapper = o2.getProviderType().equals(UserAttributeLDAPStorageMapperFactory.PROVIDER_ID);
if (!isO1AttrMapper) {
if (isO2AttrMapper) {
return 1;
} else {
return 0;
}
} else if (!isO2AttrMapper) {
return -1;
}
// Mapper for "username" attribute first
String model1 = o1.getConfig().getFirst(UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE);
String model2 = o2.getConfig().getFirst(UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE);
boolean isO1UsernameMapper = model1 != null && model1.equalsIgnoreCase(UserModel.USERNAME);
boolean isO2UsernameMapper = model2 != null && model2.equalsIgnoreCase(UserModel.USERNAME);
if (!isO1UsernameMapper) {
if (isO2UsernameMapper) {
return 1;
} else {
return 0;
}
} else if (!isO2UsernameMapper) {
return -1;
}
// The username mapper corresponding to the same like configured username for federationProvider is first
String o1LdapAttr = o1.getConfig().getFirst(UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE);
String o2LdapAttr = o2.getConfig().getFirst(UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE);
boolean isO1LdapAttr = o1LdapAttr != null && ldapConfig.getUsernameLdapAttribute().equalsIgnoreCase(o1LdapAttr);
boolean isO2LdapAttr = o2LdapAttr != null && ldapConfig.getUsernameLdapAttribute().equalsIgnoreCase(o2LdapAttr);
if (!isO1LdapAttr) {
if (isO2LdapAttr) {
return 1;
} else {
return 0;
}
} else if (!isO2LdapAttr) {
return -1;
}
return 0;
}
}
}

View file

@ -0,0 +1,125 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.provider.Provider;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.user.SynchronizationResult;
import javax.naming.AuthenticationException;
import java.util.List;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface LDAPStorageMapper extends Provider {
/**
* Sync data from federated storage to Keycloak. It's useful just if mapper needs some data preloaded from federated storage (For example
* load roles from federated provider and sync them to Keycloak database)
*
* Applicable just if sync is supported
*
* @param mapperModel
* @param provider
* @param session
* @param realm
*/
SynchronizationResult syncDataFromFederationProviderToKeycloak(ComponentModel mapperModel, LDAPStorageProvider provider, KeycloakSession session, RealmModel realm);
/**
* Sync data from Keycloak back to federated storage
*
* @param mapperModel
* @param provider
* @param session
* @param realm
*/
SynchronizationResult syncDataFromKeycloakToFederationProvider(ComponentModel mapperModel, LDAPStorageProvider provider, KeycloakSession session, RealmModel realm);
/**
* Return empty list if doesn't support storing of groups
*/
List<UserModel> getGroupMembers(ComponentModel mapperModel, LDAPStorageProvider provider, RealmModel realm, GroupModel group, int firstResult, int maxResults);
/**
* 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(ComponentModel mapperModel, LDAPStorageProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate);
/**
* 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(ComponentModel mapperModel, LDAPStorageProvider 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(ComponentModel mapperModel, LDAPStorageProvider 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(ComponentModel mapperModel, LDAPQuery query);
/**
* Called when LDAP authentication of specified user fails. If any mapper returns true from this method, AuthenticationException won't be rethrown!
*
* @param mapperModel
* @param ldapProvider
* @param realm
* @param user
* @param ldapUser
* @param ldapException
* @return true if mapper processed the AuthenticationException and did some actions based on that. In that case, AuthenticationException won't be rethrown!
*/
boolean onAuthenticationFailure(ComponentModel mapperModel, LDAPStorageProvider ldapProvider, LDAPObject ldapUser, UserModel user, AuthenticationException ldapException, RealmModel realm);
}

View file

@ -0,0 +1,98 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.user.SynchronizationResult;
import javax.naming.AuthenticationException;
import java.util.List;
/**
* Sufficient if mapper implementation is stateless and doesn't need to "close" any state
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LDAPStorageMapperBridge implements LDAPStorageMapper {
private final AbstractLDAPStorageMapperFactory factory;
public LDAPStorageMapperBridge(AbstractLDAPStorageMapperFactory factory) {
this.factory = factory;
}
// Sync groups from LDAP to Keycloak DB
@Override
public SynchronizationResult syncDataFromFederationProviderToKeycloak(ComponentModel mapperModel, LDAPStorageProvider federationProvider, KeycloakSession session, RealmModel realm) {
return getDelegate(mapperModel, federationProvider, realm).syncDataFromFederationProviderToKeycloak();
}
@Override
public SynchronizationResult syncDataFromKeycloakToFederationProvider(ComponentModel mapperModel, LDAPStorageProvider federationProvider, KeycloakSession session, RealmModel realm) {
return getDelegate(mapperModel, federationProvider, realm).syncDataFromKeycloakToFederationProvider();
}
@Override
public void onImportUserFromLDAP(ComponentModel mapperModel, LDAPStorageProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) {
getDelegate(mapperModel, ldapProvider, realm).onImportUserFromLDAP(ldapUser, user, isCreate);
}
@Override
public void onRegisterUserToLDAP(ComponentModel mapperModel, LDAPStorageProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm) {
getDelegate(mapperModel, ldapProvider, realm).onRegisterUserToLDAP(ldapUser, localUser);
}
@Override
public UserModel proxy(ComponentModel mapperModel, LDAPStorageProvider ldapProvider, LDAPObject ldapUser, UserModel delegate, RealmModel realm) {
return getDelegate(mapperModel, ldapProvider, realm).proxy(ldapUser, delegate);
}
@Override
public void beforeLDAPQuery(ComponentModel mapperModel, LDAPQuery query) {
// Improve if needed
getDelegate(mapperModel, query.getLdapProvider(), null).beforeLDAPQuery(query);
}
@Override
public List<UserModel> getGroupMembers(ComponentModel mapperModel, LDAPStorageProvider ldapProvider, RealmModel realm, GroupModel group, int firstResult, int maxResults) {
return getDelegate(mapperModel, ldapProvider, realm).getGroupMembers(group, firstResult, maxResults);
}
@Override
public boolean onAuthenticationFailure(ComponentModel mapperModel, LDAPStorageProvider ldapProvider, LDAPObject ldapUser, UserModel user, AuthenticationException ldapException, RealmModel realm) {
return getDelegate(mapperModel, ldapProvider, realm).onAuthenticationFailure(ldapUser, user, ldapException);
}
private AbstractLDAPStorageMapper getDelegate(ComponentModel mapperModel, LDAPStorageProvider federationProvider, RealmModel realm) {
LDAPStorageProvider ldapProvider = (LDAPStorageProvider) federationProvider;
return factory.createMapper(mapperModel, ldapProvider, realm);
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,99 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers;
import org.keycloak.Config;
import org.keycloak.component.ComponentFactory;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.component.SubComponentFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation;
import java.util.Collections;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface LDAPStorageMapperFactory<T extends LDAPStorageMapper> extends SubComponentFactory<T, LDAPStorageMapper> {
/**
* called per Keycloak transaction.
*
* @param session
* @param model
* @return
*/
T create(KeycloakSession session, ComponentModel model);
UserFederationMapperSyncConfigRepresentation getSyncConfig();
/**
* This is the name of the provider and will be showed in the admin console as an option.
*
* @return
*/
@Override
String getId();
@Override
default void init(Config.Scope config) {
}
@Override
default void postInit(KeycloakSessionFactory factory) {
}
@Override
default void close() {
}
@Override
default String getHelpText() {
return "";
}
@Override
default List<ProviderConfigProperty> getConfigProperties() {
return Collections.EMPTY_LIST;
}
@Override
default void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
}
/**
* Called when UserStorageProviderModel is created. This allows you to do initialization of any additional configuration
* you need to add. For example, you may be introspecting a database or ldap schema to automatically create mappings.
*
* @param session
* @param realm
* @param model
*/
@Override
default void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) {
}
}

View file

@ -0,0 +1,50 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers;
import org.keycloak.credential.CredentialProvider;
import org.keycloak.credential.CredentialProviderFactory;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
* @author <a href="mailto:me@tsudot.com">Kunal Kerkar</a>
*/
public class LDAPStorageMapperSpi implements Spi {
@Override
public boolean isInternal() {
return false;
}
@Override
public String getName() {
return "ldap-mapper";
}
@Override
public Class<? extends Provider> getProviderClass() {
return LDAPStorageMapper.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return LDAPStorageMapperFactory.class;
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers;
import org.keycloak.credential.CredentialInput;
import org.keycloak.models.UserModel;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface PasswordUpdated {
void passwordUpdated(UserModel user, LDAPObject ldapUser, CredentialInput input);
}

View file

@ -0,0 +1,136 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakTransaction;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.UserModelDelegate;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public abstract class TxAwareLDAPUserModelDelegate extends UserModelDelegate {
public static final Logger logger = Logger.getLogger(TxAwareLDAPUserModelDelegate.class);
protected LDAPStorageProvider provider;
protected LDAPObject ldapUser;
private final LDAPTransaction transaction;
public TxAwareLDAPUserModelDelegate(UserModel delegate, LDAPStorageProvider provider, LDAPObject ldapUser) {
super(delegate);
this.provider = provider;
this.ldapUser = ldapUser;
this.transaction = findOrCreateTransaction();
}
public LDAPTransaction getTransaction() {
return transaction;
}
// Try to find transaction in any delegate. We want to enlist just single transaction per all delegates
private LDAPTransaction findOrCreateTransaction() {
UserModelDelegate delegate = this;
while (true) {
UserModel deleg = delegate.getDelegate();
if (!(deleg instanceof UserModelDelegate)) {
return new LDAPTransaction();
} else {
delegate = (UserModelDelegate) deleg;
}
if (delegate instanceof TxAwareLDAPUserModelDelegate) {
TxAwareLDAPUserModelDelegate txDelegate = (TxAwareLDAPUserModelDelegate) delegate;
return txDelegate.getTransaction();
}
}
}
protected void ensureTransactionStarted() {
if (transaction.state == TransactionState.NOT_STARTED) {
if (logger.isTraceEnabled()) {
logger.trace("Starting and enlisting transaction for object " + ldapUser.getDn().toString());
}
this.provider.getSession().getTransactionManager().enlistAfterCompletion(transaction);
}
}
protected class LDAPTransaction implements KeycloakTransaction {
protected TransactionState state = TransactionState.NOT_STARTED;
@Override
public void begin() {
if (state != TransactionState.NOT_STARTED) {
throw new IllegalStateException("Transaction already started");
}
state = TransactionState.STARTED;
}
@Override
public void commit() {
if (state != TransactionState.STARTED) {
throw new IllegalStateException("Transaction in illegal state for commit: " + state);
}
if (logger.isTraceEnabled()) {
logger.trace("Transaction commit! Updating LDAP attributes for object " + ldapUser.getDn().toString() + ", attributes: " + ldapUser.getAttributes());
}
provider.getLdapIdentityStore().update(ldapUser);
state = TransactionState.FINISHED;
}
@Override
public void rollback() {
if (state != TransactionState.STARTED && state != TransactionState.ROLLBACK_ONLY) {
throw new IllegalStateException("Transaction in illegal state for rollback: " + state);
}
logger.warn("Transaction rollback! Ignoring LDAP updates for object " + ldapUser.getDn().toString());
state = TransactionState.FINISHED;
}
@Override
public void setRollbackOnly() {
state = TransactionState.ROLLBACK_ONLY;
}
@Override
public boolean getRollbackOnly() {
return state == TransactionState.ROLLBACK_ONLY;
}
@Override
public boolean isActive() {
return state == TransactionState.STARTED || state == TransactionState.ROLLBACK_ONLY;
}
}
protected enum TransactionState {
NOT_STARTED, STARTED, ROLLBACK_ONLY, FINISHED
}
}

View file

@ -0,0 +1,369 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers;
import org.jboss.logging.Logger;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.UserModelDelegate;
import org.keycloak.models.utils.reflection.Property;
import org.keycloak.models.utils.reflection.PropertyCriteria;
import org.keycloak.models.utils.reflection.PropertyQueries;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.Condition;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
private static final Logger logger = Logger.getLogger(UserAttributeLDAPStorageMapper.class);
private static final Map<String, Property<Object>> userModelProperties;
static {
Map<String, Property<Object>> userModelProps = PropertyQueries.createQuery(UserModel.class).addCriteria(new PropertyCriteria() {
@Override
public boolean methodMatches(Method m) {
if ((m.getName().startsWith("get") || m.getName().startsWith("is")) && m.getParameterTypes().length > 0) {
return false;
}
return true;
}
}).getResultList();
// Convert to be keyed by lower-cased attribute names
userModelProperties = new HashMap<>();
for (Map.Entry<String, Property<Object>> entry : userModelProps.entrySet()) {
userModelProperties.put(entry.getKey().toLowerCase(), entry.getValue());
}
}
public static final String USER_MODEL_ATTRIBUTE = "user.model.attribute";
public static final String LDAP_ATTRIBUTE = "ldap.attribute";
public static final String READ_ONLY = "read.only";
public static final String ALWAYS_READ_VALUE_FROM_LDAP = "always.read.value.from.ldap";
public static final String IS_MANDATORY_IN_LDAP = "is.mandatory.in.ldap";
public UserAttributeLDAPStorageMapper(ComponentModel mapperModel, LDAPStorageProvider ldapProvider, RealmModel realm) {
super(mapperModel, ldapProvider, realm);
}
@Override
public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate) {
String userModelAttrName = mapperModel.getConfig().getFirst(USER_MODEL_ATTRIBUTE);
String ldapAttrName = mapperModel.getConfig().getFirst(LDAP_ATTRIBUTE);
Property<Object> userModelProperty = userModelProperties.get(userModelAttrName.toLowerCase());
if (userModelProperty != null) {
// we have java property on UserModel
String ldapAttrValue = ldapUser.getAttributeAsString(ldapAttrName);
checkDuplicateEmail(userModelAttrName, ldapAttrValue, realm, ldapProvider.getSession(), user);
setPropertyOnUserModel(userModelProperty, user, ldapAttrValue);
} else {
// we don't have java property. Let's set attribute
Set<String> ldapAttrValue = ldapUser.getAttributeAsSet(ldapAttrName);
if (ldapAttrValue != null) {
user.setAttribute(userModelAttrName, new ArrayList<>(ldapAttrValue));
} else {
user.removeAttribute(userModelAttrName);
}
}
}
@Override
public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser) {
String userModelAttrName = mapperModel.getConfig().getFirst(USER_MODEL_ATTRIBUTE);
String ldapAttrName = mapperModel.getConfig().getFirst(LDAP_ATTRIBUTE);
boolean isMandatoryInLdap = parseBooleanParameter(mapperModel, IS_MANDATORY_IN_LDAP);
Property<Object> userModelProperty = userModelProperties.get(userModelAttrName.toLowerCase());
if (userModelProperty != null) {
// we have java property on UserModel. Assuming we support just properties of simple types
Object attrValue = userModelProperty.getValue(localUser);
if (attrValue == null) {
if (isMandatoryInLdap) {
ldapUser.setSingleAttribute(ldapAttrName, LDAPConstants.EMPTY_ATTRIBUTE_VALUE);
} else {
ldapUser.setAttribute(ldapAttrName, new LinkedHashSet<String>());
}
} else {
ldapUser.setSingleAttribute(ldapAttrName, attrValue.toString());
}
} else {
// we don't have java property. Let's set attribute
List<String> attrValues = localUser.getAttribute(userModelAttrName);
if (attrValues.size() == 0) {
if (isMandatoryInLdap) {
ldapUser.setSingleAttribute(ldapAttrName, LDAPConstants.EMPTY_ATTRIBUTE_VALUE);
} else {
ldapUser.setAttribute(ldapAttrName, new LinkedHashSet<String>());
}
} else {
ldapUser.setAttribute(ldapAttrName, new LinkedHashSet<>(attrValues));
}
}
if (isReadOnly()) {
ldapUser.addReadOnlyAttributeName(ldapAttrName);
}
}
// throw ModelDuplicateException if there is different user in model with same email
protected void checkDuplicateEmail(String userModelAttrName, String email, RealmModel realm, KeycloakSession session, UserModel user) {
if (UserModel.EMAIL.equalsIgnoreCase(userModelAttrName)) {
// lowercase before search
email = KeycloakModelUtils.toLowerCaseSafe(email);
UserModel that = session.userLocalStorage().getUserByEmail(email, realm);
if (that != null && !that.getId().equals(user.getId())) {
session.getTransactionManager().setRollbackOnly();
String exceptionMessage = String.format("Can't import user '%s' from LDAP because email '%s' already exists in Keycloak. Existing user with this email is '%s'", user.getUsername(), email, that.getUsername());
throw new ModelDuplicateException(exceptionMessage, UserModel.EMAIL);
}
}
}
@Override
public UserModel proxy(final LDAPObject ldapUser, UserModel delegate) {
final String userModelAttrName = mapperModel.getConfig().getFirst(USER_MODEL_ATTRIBUTE);
final String ldapAttrName = mapperModel.getConfig().getFirst(LDAP_ATTRIBUTE);
boolean isAlwaysReadValueFromLDAP = parseBooleanParameter(mapperModel, ALWAYS_READ_VALUE_FROM_LDAP);
final boolean isMandatoryInLdap = parseBooleanParameter(mapperModel, IS_MANDATORY_IN_LDAP);
// For writable mode, we want to propagate writing of attribute to LDAP as well
if (ldapProvider.getEditMode() == LDAPStorageProviderFactory.EditMode.WRITABLE && !isReadOnly()) {
delegate = new TxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapUser) {
@Override
public void setSingleAttribute(String name, String value) {
setLDAPAttribute(name, value);
super.setSingleAttribute(name, value);
}
@Override
public void setAttribute(String name, List<String> values) {
setLDAPAttribute(name, values);
super.setAttribute(name, values);
}
@Override
public void removeAttribute(String name) {
setLDAPAttribute(name, null);
super.removeAttribute(name);
}
@Override
public void setEmail(String email) {
checkDuplicateEmail(userModelAttrName, email, realm, ldapProvider.getSession(), this);
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);
}
protected void setLDAPAttribute(String modelAttrName, Object value) {
if (modelAttrName.equalsIgnoreCase(userModelAttrName)) {
if (logger.isTraceEnabled()) {
logger.tracef("Pushing user attribute to LDAP. username: %s, Model attribute name: %s, LDAP attribute name: %s, Attribute value: %s", getUsername(), modelAttrName, ldapAttrName, value);
}
ensureTransactionStarted();
if (value == null) {
if (isMandatoryInLdap) {
ldapUser.setSingleAttribute(ldapAttrName, LDAPConstants.EMPTY_ATTRIBUTE_VALUE);
} else {
ldapUser.setAttribute(ldapAttrName, new LinkedHashSet<String>());
}
} else if (value instanceof String) {
ldapUser.setSingleAttribute(ldapAttrName, (String) value);
} else {
List<String> asList = (List<String>) value;
if (asList.isEmpty() && isMandatoryInLdap) {
ldapUser.setSingleAttribute(ldapAttrName, LDAPConstants.EMPTY_ATTRIBUTE_VALUE);
} else {
ldapUser.setAttribute(ldapAttrName, new LinkedHashSet<>(asList));
}
}
}
}
};
}
// We prefer to read attribute value from LDAP instead of from local Keycloak DB
if (isAlwaysReadValueFromLDAP) {
delegate = new UserModelDelegate(delegate) {
@Override
public String getFirstAttribute(String name) {
if (name.equalsIgnoreCase(userModelAttrName)) {
return ldapUser.getAttributeAsString(ldapAttrName);
} else {
return super.getFirstAttribute(name);
}
}
@Override
public List<String> getAttribute(String name) {
if (name.equalsIgnoreCase(userModelAttrName)) {
Collection<String> ldapAttrValue = ldapUser.getAttributeAsSet(ldapAttrName);
if (ldapAttrValue == null) {
return Collections.emptyList();
} else {
return new ArrayList<>(ldapAttrValue);
}
} else {
return super.getAttribute(name);
}
}
@Override
public Map<String, List<String>> getAttributes() {
Map<String, List<String>> attrs = new HashMap<>(super.getAttributes());
// Ignore UserModel properties
if (userModelProperties.get(userModelAttrName.toLowerCase()) != null) {
return attrs;
}
Set<String> allLdapAttrValues = ldapUser.getAttributeAsSet(ldapAttrName);
if (allLdapAttrValues != null) {
attrs.put(userModelAttrName, new ArrayList<>(allLdapAttrValues));
}
return attrs;
}
@Override
public String getEmail() {
if (UserModel.EMAIL.equalsIgnoreCase(userModelAttrName)) {
return ldapUser.getAttributeAsString(ldapAttrName);
} else {
return super.getEmail();
}
}
@Override
public String getLastName() {
if (UserModel.LAST_NAME.equalsIgnoreCase(userModelAttrName)) {
return ldapUser.getAttributeAsString(ldapAttrName);
} else {
return super.getLastName();
}
}
@Override
public String getFirstName() {
if (UserModel.FIRST_NAME.equalsIgnoreCase(userModelAttrName)) {
return ldapUser.getAttributeAsString(ldapAttrName);
} else {
return super.getFirstName();
}
}
};
}
return delegate;
}
@Override
public void beforeLDAPQuery(LDAPQuery query) {
String userModelAttrName = mapperModel.getConfig().getFirst(USER_MODEL_ATTRIBUTE);
String ldapAttrName = mapperModel.getConfig().getFirst(LDAP_ATTRIBUTE);
// Add mapped attribute to returning ldap attributes
query.addReturningLdapAttribute(ldapAttrName);
if (isReadOnly()) {
query.addReturningReadOnlyLdapAttribute(ldapAttrName);
}
// Change conditions and use ldapAttribute instead of userModel
for (Condition condition : query.getConditions()) {
condition.updateParameterName(userModelAttrName, ldapAttrName);
}
}
private boolean isReadOnly() {
return parseBooleanParameter(mapperModel, READ_ONLY);
}
protected void setPropertyOnUserModel(Property<Object> userModelProperty, UserModel user, String ldapAttrValue) {
if (ldapAttrValue == null) {
userModelProperty.setValue(user, null);
} else {
Class<Object> clazz = userModelProperty.getJavaClass();
if (String.class.equals(clazz)) {
userModelProperty.setValue(user, ldapAttrValue);
} else if (Boolean.class.equals(clazz) || boolean.class.equals(clazz)) {
Boolean boolVal = Boolean.valueOf(ldapAttrValue);
userModelProperty.setValue(user, boolVal);
} else {
logger.warnf("Don't know how to set the property '%s' on user '%s' . Value of LDAP attribute is '%s' ", userModelProperty.getName(), user.getUsername(), ldapAttrValue.toString());
}
}
}
}

View file

@ -0,0 +1,110 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserFederationProvider;
import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class UserAttributeLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFactory {
public static final String PROVIDER_ID = "user-attribute-ldap-mapper";
protected static final List<ProviderConfigProperty> configProperties;
static {
List<ProviderConfigProperty> props = getConfigProps(null);
configProperties = props;
}
private static List<ProviderConfigProperty> getConfigProps(ComponentModel parent) {
String readOnly = "false";
if (parent != null) {
LDAPConfig ldapConfig = new LDAPConfig(parent.getConfig());
readOnly = ldapConfig.getEditMode() == LDAPStorageProviderFactory.EditMode.WRITABLE ? "false" : "true";
}
return ProviderConfigurationBuilder.create()
.property().name(UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE)
.label("User Model Attribute")
.helpText("Name of mapped UserModel property or UserModel attribute in Keycloak DB. For example 'firstName', 'lastName, 'email', 'street' etc.")
.type(ProviderConfigProperty.STRING_TYPE)
.add()
.property().name(UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE).label("LDAP Attribute").helpText("Name of mapped attribute on LDAP object. For example 'cn', 'sn, 'mail', 'street' etc.")
.type(ProviderConfigProperty.STRING_TYPE)
.add()
.property().name(UserAttributeLDAPStorageMapper.READ_ONLY).label("Read Only")
.helpText("Read-only attribute is imported from LDAP to Keycloak DB, but it's not saved back to LDAP when user is updated in Keycloak.")
.type(ProviderConfigProperty.BOOLEAN_TYPE)
.defaultValue(readOnly)
.add()
.property().name(UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP).label("Always Read Value From LDAP")
.helpText("If on, then during reading of the user will be value of attribute from LDAP always used instead of the value from Keycloak DB")
.type(ProviderConfigProperty.BOOLEAN_TYPE).defaultValue("false").add()
.property().name(UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP).label("Is Mandatory In LDAP")
.helpText("If true, attribute is mandatory in LDAP. Hence if there is no value in Keycloak DB, the empty value will be set to be propagated to LDAP")
.type(ProviderConfigProperty.BOOLEAN_TYPE)
.defaultValue("false").add()
.build();
}
@Override
public String getHelpText() {
return "Used to map single attribute from LDAP user to attribute of UserModel in Keycloak DB";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
checkMandatoryConfigAttribute(UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, "User Model Attribute", config);
checkMandatoryConfigAttribute(UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, "LDAP Attribute", config);
}
@Override
protected AbstractLDAPStorageMapper createMapper(ComponentModel mapperModel, LDAPStorageProvider federationProvider, RealmModel realm) {
return new UserAttributeLDAPStorageMapper(mapperModel, federationProvider, realm);
}
@Override
public List<ProviderConfigProperty> getConfigProperties(RealmModel realm, ComponentModel parent) {
return getConfigProps(parent);
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers.membership;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
/**
* Mapper related to mapping of LDAP groups to keycloak model objects (either keycloak roles or keycloak groups)
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface CommonLDAPGroupMapper {
LDAPQuery createLDAPGroupQuery();
CommonLDAPGroupMapperConfig getConfig();
}

View file

@ -0,0 +1,87 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers.membership;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
import java.util.HashSet;
import java.util.Set;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public abstract class CommonLDAPGroupMapperConfig {
// 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";
// See docs for MembershipType enum
public static final String MEMBERSHIP_ATTRIBUTE_TYPE = "membership.attribute.type";
// See docs for Mode enum
public static final String MODE = "mode";
// See docs for UserRolesRetriever enum
public static final String USER_ROLES_RETRIEVE_STRATEGY = "user.roles.retrieve.strategy";
protected final ComponentModel mapperModel;
public CommonLDAPGroupMapperConfig(ComponentModel mapperModel) {
this.mapperModel = mapperModel;
}
public String getMembershipLdapAttribute() {
String membershipAttrName = mapperModel.getConfig().getFirst(MEMBERSHIP_LDAP_ATTRIBUTE);
return membershipAttrName!=null ? membershipAttrName : LDAPConstants.MEMBER;
}
public MembershipType getMembershipTypeLdapAttribute() {
String membershipType = mapperModel.getConfig().getFirst(MEMBERSHIP_ATTRIBUTE_TYPE);
return (membershipType!=null && !membershipType.isEmpty()) ? Enum.valueOf(MembershipType.class, membershipType) : MembershipType.DN;
}
public LDAPGroupMapperMode getMode() {
String modeString = mapperModel.getConfig().getFirst(MODE);
if (modeString == null || modeString.isEmpty()) {
throw new ModelException("Mode is missing! Check your configuration");
}
return Enum.valueOf(LDAPGroupMapperMode.class, modeString.toUpperCase());
}
protected Set<String> getConfigValues(String str) {
String[] objClasses = str.split(",");
Set<String> trimmed = new HashSet<>();
for (String objectClass : objClasses) {
objectClass = objectClass.trim();
if (objectClass.length() > 0) {
trimmed.add(objectClass);
}
}
return trimmed;
}
public abstract String getLDAPGroupsDn();
public abstract String getLDAPGroupNameLdapAttribute();
}

View file

@ -0,0 +1,46 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers.membership;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public enum LDAPGroupMapperMode {
/**
* 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
}

View file

@ -0,0 +1,157 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers.membership;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.LDAPUtils;
import org.keycloak.storage.ldap.idm.model.LDAPDn;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.Condition;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
import org.keycloak.storage.ldap.mappers.membership.group.GroupLDAPStorageMapper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public enum MembershipType {
/**
* Used if LDAP role has it's members declared in form of their full DN. For example ( "member: uid=john,ou=users,dc=example,dc=com" )
*/
DN {
@Override
public Set<LDAPDn> getLDAPSubgroups(GroupLDAPStorageMapper groupMapper, LDAPObject ldapGroup) {
CommonLDAPGroupMapperConfig config = groupMapper.getConfig();
return getLDAPMembersWithParent(ldapGroup, config.getMembershipLdapAttribute(), LDAPDn.fromString(config.getLDAPGroupsDn()));
}
// Get just those members of specified group, which are descendants of "requiredParentDn"
protected Set<LDAPDn> getLDAPMembersWithParent(LDAPObject ldapGroup, String membershipLdapAttribute, LDAPDn requiredParentDn) {
Set<String> allMemberships = LDAPUtils.getExistingMemberships(membershipLdapAttribute, ldapGroup);
// Filter and keep just groups
Set<LDAPDn> result = new HashSet<>();
for (String membership : allMemberships) {
LDAPDn childDn = LDAPDn.fromString(membership);
if (childDn.isDescendantOf(requiredParentDn)) {
result.add(childDn);
}
}
return result;
}
@Override
public List<UserModel> getGroupMembers(GroupLDAPStorageMapper groupMapper, LDAPObject ldapGroup, int firstResult, int maxResults) {
RealmModel realm = groupMapper.getRealm();
LDAPStorageProvider ldapProvider = groupMapper.getLdapProvider();
CommonLDAPGroupMapperConfig config = groupMapper.getConfig();
LDAPDn usersDn = LDAPDn.fromString(ldapProvider.getLdapIdentityStore().getConfig().getUsersDn());
Set<LDAPDn> userDns = getLDAPMembersWithParent(ldapGroup, config.getMembershipLdapAttribute(), usersDn);
if (userDns == null) {
return Collections.emptyList();
}
if (userDns.size() <= firstResult) {
return Collections.emptyList();
}
List<LDAPDn> dns = new ArrayList<>(userDns);
int max = Math.min(dns.size(), firstResult + maxResults);
dns = dns.subList(firstResult, max);
// If usernameAttrName is same like DN, we can just retrieve usernames from DNs
List<String> usernames = new LinkedList<>();
LDAPConfig ldapConfig = ldapProvider.getLdapIdentityStore().getConfig();
if (ldapConfig.getUsernameLdapAttribute().equals(ldapConfig.getRdnLdapAttribute())) {
for (LDAPDn userDn : dns) {
String username = userDn.getFirstRdnAttrValue();
usernames.add(username);
}
} else {
LDAPQuery query = LDAPUtils.createQueryForUserSearch(ldapProvider, realm);
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
Condition[] orSubconditions = new Condition[dns.size()];
int index = 0;
for (LDAPDn userDn : dns) {
Condition condition = conditionsBuilder.equal(userDn.getFirstRdnAttrName(), userDn.getFirstRdnAttrValue());
orSubconditions[index] = condition;
index++;
}
Condition orCondition = conditionsBuilder.orCondition(orSubconditions);
query.addWhereCondition(orCondition);
List<LDAPObject> ldapUsers = query.getResultList();
for (LDAPObject ldapUser : ldapUsers) {
String username = LDAPUtils.getUsername(ldapUser, ldapConfig);
usernames.add(username);
}
}
// We have dns of users, who are members of our group. Load them now
return ldapProvider.loadUsersByUsernames(usernames, realm);
}
},
/**
* Used if LDAP role has it's members declared in form of pure user uids. For example ( "memberUid: john" )
*/
UID {
// Group inheritance not supported for this config
@Override
public Set<LDAPDn> getLDAPSubgroups(GroupLDAPStorageMapper groupMapper, LDAPObject ldapGroup) {
return Collections.emptySet();
}
@Override
public List<UserModel> getGroupMembers(GroupLDAPStorageMapper groupMapper, LDAPObject ldapGroup, int firstResult, int maxResults) {
String memberAttrName = groupMapper.getConfig().getMembershipLdapAttribute();
Set<String> memberUids = LDAPUtils.getExistingMemberships(memberAttrName, ldapGroup);
if (memberUids == null || memberUids.size() <= firstResult) {
return Collections.emptyList();
}
List<String> uids = new ArrayList<>(memberUids);
int max = Math.min(memberUids.size(), firstResult + maxResults);
uids = uids.subList(firstResult, max);
return groupMapper.getLdapProvider().loadUsersByUsernames(uids, groupMapper.getRealm());
}
};
public abstract Set<LDAPDn> getLDAPSubgroups(GroupLDAPStorageMapper groupMapper, LDAPObject ldapGroup);
public abstract List<UserModel> getGroupMembers(GroupLDAPStorageMapper groupMapper, LDAPObject ldapGroup, int firstResult, int maxResults);
}

View file

@ -0,0 +1,128 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers.membership;
import org.keycloak.models.LDAPConstants;
import org.keycloak.storage.ldap.LDAPUtils;
import org.keycloak.storage.ldap.idm.model.LDAPDn;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.Condition;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
/**
* Strategy for how to retrieve LDAP roles of user
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface UserRolesRetrieveStrategy {
List<LDAPObject> getLDAPRoleMappings(CommonLDAPGroupMapper roleOrGroupMapper, LDAPObject ldapUser);
void beforeUserLDAPQuery(LDAPQuery query);
// Impl subclasses
/**
* Roles of user will be retrieved by sending LDAP query to retrieve all roles where "member" is our user
*/
class LoadRolesByMember implements UserRolesRetrieveStrategy {
@Override
public List<LDAPObject> getLDAPRoleMappings(CommonLDAPGroupMapper roleOrGroupMapper, LDAPObject ldapUser) {
LDAPQuery ldapQuery = roleOrGroupMapper.createLDAPGroupQuery();
String membershipAttr = roleOrGroupMapper.getConfig().getMembershipLdapAttribute();
String userMembership = LDAPUtils.getMemberValueOfChildObject(ldapUser, roleOrGroupMapper.getConfig().getMembershipTypeLdapAttribute());
Condition membershipCondition = getMembershipCondition(membershipAttr, userMembership);
ldapQuery.addWhereCondition(membershipCondition);
return ldapQuery.getResultList();
}
@Override
public void beforeUserLDAPQuery(LDAPQuery query) {
}
protected Condition getMembershipCondition(String membershipAttr, String userMembership) {
return new LDAPQueryConditionsBuilder().equal(membershipAttr, userMembership);
}
};
/**
* Roles of user will be retrieved from "memberOf" attribute of our user
*/
class GetRolesFromUserMemberOfAttribute implements UserRolesRetrieveStrategy {
@Override
public List<LDAPObject> getLDAPRoleMappings(CommonLDAPGroupMapper roleOrGroupMapper, LDAPObject ldapUser) {
Set<String> memberOfValues = ldapUser.getAttributeAsSet(LDAPConstants.MEMBER_OF);
if (memberOfValues == null) {
return Collections.emptyList();
}
List<LDAPObject> roles = new LinkedList<>();
LDAPDn parentDn = LDAPDn.fromString(roleOrGroupMapper.getConfig().getLDAPGroupsDn());
for (String roleDn : memberOfValues) {
LDAPDn roleDN = LDAPDn.fromString(roleDn);
if (roleDN.isDescendantOf(parentDn)) {
LDAPObject role = new LDAPObject();
role.setDn(roleDN);
String firstDN = roleDN.getFirstRdnAttrName();
if (firstDN.equalsIgnoreCase(roleOrGroupMapper.getConfig().getLDAPGroupNameLdapAttribute())) {
role.setRdnAttributeName(firstDN);
role.setSingleAttribute(firstDN, roleDN.getFirstRdnAttrValue());
roles.add(role);
}
}
}
return roles;
}
@Override
public void beforeUserLDAPQuery(LDAPQuery query) {
query.addReturningLdapAttribute(LDAPConstants.MEMBER_OF);
query.addReturningReadOnlyLdapAttribute(LDAPConstants.MEMBER_OF);
}
};
/**
* Extension specific to Active Directory. Roles of user will be retrieved by sending LDAP query to retrieve all roles where "member" is our user.
* The query will be able to retrieve memberships recursively with usage of AD specific extension LDAP_MATCHING_RULE_IN_CHAIN, so likely doesn't work on other LDAP servers
*/
class LoadRolesByMemberRecursively extends LoadRolesByMember {
protected Condition getMembershipCondition(String membershipAttr, String userMembership) {
return new LDAPQueryConditionsBuilder().equal(membershipAttr + LDAPConstants.LDAP_MATCHING_RULE_IN_CHAIN, userMembership);
}
};
}

View file

@ -0,0 +1,648 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers.membership.group;
import org.jboss.logging.Logger;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.UserModelDelegate;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.LDAPUtils;
import org.keycloak.storage.ldap.idm.model.LDAPDn;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.Condition;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.membership.CommonLDAPGroupMapper;
import org.keycloak.storage.ldap.mappers.membership.CommonLDAPGroupMapperConfig;
import org.keycloak.storage.ldap.mappers.membership.LDAPGroupMapperMode;
import org.keycloak.storage.ldap.mappers.membership.MembershipType;
import org.keycloak.storage.ldap.mappers.membership.UserRolesRetrieveStrategy;
import org.keycloak.storage.user.SynchronizationResult;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements CommonLDAPGroupMapper {
private static final Logger logger = Logger.getLogger(GroupLDAPStorageMapper.class);
private final GroupMapperConfig config;
private final GroupLDAPStorageMapperFactory factory;
// Flag to avoid syncing multiple times per transaction
private boolean syncFromLDAPPerformedInThisTransaction = false;
public GroupLDAPStorageMapper(ComponentModel mapperModel, LDAPStorageProvider ldapProvider, RealmModel realm, GroupLDAPStorageMapperFactory factory) {
super(mapperModel, ldapProvider, realm);
this.config = new GroupMapperConfig(mapperModel);
this.factory = factory;
}
// CommonLDAPGroupMapper interface
@Override
public LDAPQuery createLDAPGroupQuery() {
return createGroupQuery();
}
@Override
public CommonLDAPGroupMapperConfig getConfig() {
return config;
}
// LDAP Group CRUD operations
public LDAPQuery createGroupQuery() {
LDAPQuery ldapQuery = new LDAPQuery(ldapProvider);
// For now, use same search scope, which is configured "globally" and used for user's search.
ldapQuery.setSearchScope(ldapProvider.getLdapIdentityStore().getConfig().getSearchScope());
String groupsDn = config.getGroupsDn();
ldapQuery.setSearchDn(groupsDn);
Collection<String> groupObjectClasses = config.getGroupObjectClasses(ldapProvider);
ldapQuery.addObjectClasses(groupObjectClasses);
String customFilter = config.getCustomLdapFilter();
if (customFilter != null && customFilter.trim().length() > 0) {
Condition customFilterCondition = new LDAPQueryConditionsBuilder().addCustomLDAPFilter(customFilter);
ldapQuery.addWhereCondition(customFilterCondition);
}
ldapQuery.addReturningLdapAttribute(config.getGroupNameLdapAttribute());
ldapQuery.addReturningLdapAttribute(config.getMembershipLdapAttribute());
for (String groupAttr : config.getGroupAttributes()) {
ldapQuery.addReturningLdapAttribute(groupAttr);
}
return ldapQuery;
}
public LDAPObject createLDAPGroup(String groupName, Map<String, Set<String>> additionalAttributes) {
LDAPObject ldapGroup = LDAPUtils.createLDAPGroup(ldapProvider, groupName, config.getGroupNameLdapAttribute(), config.getGroupObjectClasses(ldapProvider),
config.getGroupsDn(), additionalAttributes);
logger.debugf("Creating group [%s] to LDAP with DN [%s]", groupName, ldapGroup.getDn().toString());
return ldapGroup;
}
public LDAPObject loadLDAPGroupByName(String groupName) {
LDAPQuery ldapQuery = createGroupQuery();
Condition roleNameCondition = new LDAPQueryConditionsBuilder().equal(config.getGroupNameLdapAttribute(), groupName);
ldapQuery.addWhereCondition(roleNameCondition);
return ldapQuery.getFirstResult();
}
protected Set<LDAPDn> getLDAPSubgroups(LDAPObject ldapGroup) {
MembershipType membershipType = config.getMembershipTypeLdapAttribute();
return membershipType.getLDAPSubgroups(this, ldapGroup);
}
// Sync from Ldap to KC
@Override
public SynchronizationResult syncDataFromFederationProviderToKeycloak() {
SynchronizationResult syncResult = new SynchronizationResult() {
@Override
public String getStatus() {
return String.format("%d imported groups, %d updated groups, %d removed groups", getAdded(), getUpdated(), getRemoved());
}
};
logger.debugf("Syncing groups from LDAP into Keycloak DB. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getName());
// Get all LDAP groups
List<LDAPObject> ldapGroups = getAllLDAPGroups();
// Convert to internal format
Map<String, LDAPObject> ldapGroupsMap = new HashMap<>();
List<GroupTreeResolver.Group> ldapGroupsRep = new LinkedList<>();
String groupsRdnAttr = config.getGroupNameLdapAttribute();
for (LDAPObject ldapGroup : ldapGroups) {
String groupName = ldapGroup.getAttributeAsString(groupsRdnAttr);
Set<String> subgroupNames = new HashSet<>();
for (LDAPDn groupDn : getLDAPSubgroups(ldapGroup)) {
subgroupNames.add(groupDn.getFirstRdnAttrValue());
}
ldapGroupsRep.add(new GroupTreeResolver.Group(groupName, subgroupNames));
ldapGroupsMap.put(groupName, ldapGroup);
}
// Now we have list of LDAP groups. Let's form the tree (if needed)
if (config.isPreserveGroupsInheritance()) {
try {
List<GroupTreeResolver.GroupTreeEntry> groupTrees = new GroupTreeResolver().resolveGroupTree(ldapGroupsRep);
updateKeycloakGroupTree(groupTrees, ldapGroupsMap, syncResult);
} catch (GroupTreeResolver.GroupTreeResolveException gre) {
throw new ModelException("Couldn't resolve groups from LDAP. Fix LDAP or skip preserve inheritance. Details: " + gre.getMessage(), gre);
}
} else {
Set<String> visitedGroupIds = new HashSet<>();
// Just add flat structure of groups with all groups at top-level
for (Map.Entry<String, LDAPObject> groupEntry : ldapGroupsMap.entrySet()) {
String groupName = groupEntry.getKey();
GroupModel kcExistingGroup = KeycloakModelUtils.findGroupByPath(realm, "/" + groupName);
if (kcExistingGroup != null) {
updateAttributesOfKCGroup(kcExistingGroup, groupEntry.getValue());
syncResult.increaseUpdated();
visitedGroupIds.add(kcExistingGroup.getId());
} else {
GroupModel kcGroup = realm.createGroup(groupName);
updateAttributesOfKCGroup(kcGroup, groupEntry.getValue());
realm.moveGroup(kcGroup, null);
syncResult.increaseAdded();
visitedGroupIds.add(kcGroup.getId());
}
}
// Possibly remove keycloak groups, which doesn't exists in LDAP
if (config.isDropNonExistingGroupsDuringSync()) {
dropNonExistingKcGroups(syncResult, visitedGroupIds);
}
}
syncFromLDAPPerformedInThisTransaction = true;
return syncResult;
}
private void updateKeycloakGroupTree(List<GroupTreeResolver.GroupTreeEntry> groupTrees, Map<String, LDAPObject> ldapGroups, SynchronizationResult syncResult) {
Set<String> visitedGroupIds = new HashSet<>();
for (GroupTreeResolver.GroupTreeEntry groupEntry : groupTrees) {
updateKeycloakGroupTreeEntry(groupEntry, ldapGroups, null, syncResult, visitedGroupIds);
}
// Possibly remove keycloak groups, which doesn't exists in LDAP
if (config.isDropNonExistingGroupsDuringSync()) {
dropNonExistingKcGroups(syncResult, visitedGroupIds);
}
}
private void updateKeycloakGroupTreeEntry(GroupTreeResolver.GroupTreeEntry groupTreeEntry, Map<String, LDAPObject> ldapGroups, GroupModel kcParent, SynchronizationResult syncResult, Set<String> visitedGroupIds) {
String groupName = groupTreeEntry.getGroupName();
// Check if group already exists
GroupModel kcGroup = null;
Collection<GroupModel> subgroups = kcParent == null ? realm.getTopLevelGroups() : kcParent.getSubGroups();
for (GroupModel group : subgroups) {
if (group.getName().equals(groupName)) {
kcGroup = group;
break;
}
}
if (kcGroup != null) {
logger.debugf("Updated Keycloak group '%s' from LDAP", kcGroup.getName());
updateAttributesOfKCGroup(kcGroup, ldapGroups.get(kcGroup.getName()));
syncResult.increaseUpdated();
} else {
kcGroup = realm.createGroup(groupTreeEntry.getGroupName());
if (kcParent == null) {
realm.moveGroup(kcGroup, null);
logger.debugf("Imported top-level group '%s' from LDAP", kcGroup.getName());
} else {
realm.moveGroup(kcGroup, kcParent);
logger.debugf("Imported group '%s' from LDAP as child of group '%s'", kcGroup.getName(), kcParent.getName());
}
updateAttributesOfKCGroup(kcGroup, ldapGroups.get(kcGroup.getName()));
syncResult.increaseAdded();
}
visitedGroupIds.add(kcGroup.getId());
for (GroupTreeResolver.GroupTreeEntry childEntry : groupTreeEntry.getChildren()) {
updateKeycloakGroupTreeEntry(childEntry, ldapGroups, kcGroup, syncResult, visitedGroupIds);
}
}
private void dropNonExistingKcGroups(SynchronizationResult syncResult, Set<String> visitedGroupIds) {
// Remove keycloak groups, which doesn't exists in LDAP
List<GroupModel> allGroups = realm.getGroups();
for (GroupModel kcGroup : allGroups) {
if (!visitedGroupIds.contains(kcGroup.getId())) {
logger.debugf("Removing Keycloak group '%s', which doesn't exist in LDAP", kcGroup.getName());
realm.removeGroup(kcGroup);
syncResult.increaseRemoved();
}
}
}
private void updateAttributesOfKCGroup(GroupModel kcGroup, LDAPObject ldapGroup) {
Collection<String> groupAttributes = config.getGroupAttributes();
for (String attrName : groupAttributes) {
Set<String> attrValues = ldapGroup.getAttributeAsSet(attrName);
if (attrValues==null) {
kcGroup.removeAttribute(attrName);
} else {
kcGroup.setAttribute(attrName, new LinkedList<>(attrValues));
}
}
}
protected GroupModel findKcGroupByLDAPGroup(LDAPObject ldapGroup) {
String groupNameAttr = config.getGroupNameLdapAttribute();
String groupName = ldapGroup.getAttributeAsString(groupNameAttr);
if (config.isPreserveGroupsInheritance()) {
// Override if better effectivity or different algorithm is needed
List<GroupModel> groups = realm.getGroups();
for (GroupModel group : groups) {
if (group.getName().equals(groupName)) {
return group;
}
}
return null;
} else {
// Without preserved inheritance, it's always top-level group
return KeycloakModelUtils.findGroupByPath(realm, "/" + groupName);
}
}
protected GroupModel findKcGroupOrSyncFromLDAP(LDAPObject ldapGroup, UserModel user) {
GroupModel kcGroup = findKcGroupByLDAPGroup(ldapGroup);
if (kcGroup == null) {
if (config.isPreserveGroupsInheritance()) {
// Better to sync all groups from LDAP with preserved inheritance
if (!syncFromLDAPPerformedInThisTransaction) {
syncDataFromFederationProviderToKeycloak();
kcGroup = findKcGroupByLDAPGroup(ldapGroup);
}
} else {
String groupNameAttr = config.getGroupNameLdapAttribute();
String groupName = ldapGroup.getAttributeAsString(groupNameAttr);
kcGroup = realm.createGroup(groupName);
updateAttributesOfKCGroup(kcGroup, ldapGroup);
realm.moveGroup(kcGroup, null);
}
// Could theoretically happen on some LDAP servers if 'memberof' style is used and 'memberof' attribute of user references non-existing group
if (kcGroup == null) {
String groupName = ldapGroup.getAttributeAsString(config.getGroupNameLdapAttribute());
logger.warnf("User '%s' is member of group '%s', which doesn't exists in LDAP", user.getUsername(), groupName);
}
}
return kcGroup;
}
// Send LDAP query to retrieve all groups
protected List<LDAPObject> getAllLDAPGroups() {
LDAPQuery ldapGroupQuery = createGroupQuery();
return LDAPUtils.loadAllLDAPObjects(ldapGroupQuery, ldapProvider);
}
// Sync from Keycloak to LDAP
public SynchronizationResult syncDataFromKeycloakToFederationProvider() {
SynchronizationResult syncResult = new SynchronizationResult() {
@Override
public String getStatus() {
return String.format("%d groups imported to LDAP, %d groups updated to LDAP, %d groups removed from LDAP", getAdded(), getUpdated(), getRemoved());
}
};
if (config.getMode() != LDAPGroupMapperMode.LDAP_ONLY) {
logger.warnf("Ignored sync for federation mapper '%s' as it's mode is '%s'", mapperModel.getName(), config.getMode().toString());
return syncResult;
}
logger.debugf("Syncing groups from Keycloak into LDAP. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getName());
// Query existing LDAP groups
LDAPQuery ldapQuery = createGroupQuery();
List<LDAPObject> ldapGroups = ldapQuery.getResultList();
// Convert them to Map<String, LDAPObject>
Map<String, LDAPObject> ldapGroupsMap = new HashMap<>();
String groupsRdnAttr = config.getGroupNameLdapAttribute();
for (LDAPObject ldapGroup : ldapGroups) {
String groupName = ldapGroup.getAttributeAsString(groupsRdnAttr);
ldapGroupsMap.put(groupName, ldapGroup);
}
// Map to track all LDAP groups also exists in Keycloak
Set<String> ldapGroupNames = new HashSet<>();
// Create or update KC groups to LDAP including their attributes
for (GroupModel kcGroup : realm.getTopLevelGroups()) {
processLdapGroupSyncToLDAP(kcGroup, ldapGroupsMap, ldapGroupNames, syncResult);
}
// If dropNonExisting, then drop all groups, which doesn't exist in KC from LDAP as well
if (config.isDropNonExistingGroupsDuringSync()) {
Set<String> copy = new HashSet<>(ldapGroupsMap.keySet());
for (String groupName : copy) {
if (!ldapGroupNames.contains(groupName)) {
LDAPObject ldapGroup = ldapGroupsMap.remove(groupName);
ldapProvider.getLdapIdentityStore().remove(ldapGroup);
syncResult.increaseRemoved();
}
}
}
// Finally process memberships,
if (config.isPreserveGroupsInheritance()) {
for (GroupModel kcGroup : realm.getTopLevelGroups()) {
processLdapGroupMembershipsSyncToLDAP(kcGroup, ldapGroupsMap);
}
}
return syncResult;
}
// For given kcGroup check if it exists in LDAP (map) by name
// If not, create it in LDAP including attributes. Otherwise update attributes in LDAP.
// Process this recursively for all subgroups of KC group
private void processLdapGroupSyncToLDAP(GroupModel kcGroup, Map<String, LDAPObject> ldapGroupsMap, Set<String> ldapGroupNames, SynchronizationResult syncResult) {
String groupName = kcGroup.getName();
// extract group attributes to be updated to LDAP
Map<String, Set<String>> supportedLdapAttributes = new HashMap<>();
for (String attrName : config.getGroupAttributes()) {
List<String> kcAttrValues = kcGroup.getAttribute(attrName);
Set<String> attrValues2 = (kcAttrValues == null || kcAttrValues.isEmpty()) ? null : new HashSet<>(kcAttrValues);
supportedLdapAttributes.put(attrName, attrValues2);
}
LDAPObject ldapGroup = ldapGroupsMap.get(groupName);
if (ldapGroup == null) {
ldapGroup = createLDAPGroup(groupName, supportedLdapAttributes);
syncResult.increaseAdded();
} else {
for (Map.Entry<String, Set<String>> attrEntry : supportedLdapAttributes.entrySet()) {
ldapGroup.setAttribute(attrEntry.getKey(), attrEntry.getValue());
}
ldapProvider.getLdapIdentityStore().update(ldapGroup);
syncResult.increaseUpdated();
}
ldapGroupsMap.put(groupName, ldapGroup);
ldapGroupNames.add(groupName);
// process KC subgroups
for (GroupModel kcSubgroup : kcGroup.getSubGroups()) {
processLdapGroupSyncToLDAP(kcSubgroup, ldapGroupsMap, ldapGroupNames, syncResult);
}
}
// Sync memberships update. Update memberships of group in LDAP based on subgroups from KC. Do it recursively
private void processLdapGroupMembershipsSyncToLDAP(GroupModel kcGroup, Map<String, LDAPObject> ldapGroupsMap) {
LDAPObject ldapGroup = ldapGroupsMap.get(kcGroup.getName());
Set<LDAPDn> toRemoveSubgroupsDNs = getLDAPSubgroups(ldapGroup);
// Add LDAP subgroups, which are KC subgroups
Set<GroupModel> kcSubgroups = kcGroup.getSubGroups();
for (GroupModel kcSubgroup : kcSubgroups) {
LDAPObject ldapSubgroup = ldapGroupsMap.get(kcSubgroup.getName());
LDAPUtils.addMember(ldapProvider, MembershipType.DN, config.getMembershipLdapAttribute(), ldapGroup, ldapSubgroup, false);
toRemoveSubgroupsDNs.remove(ldapSubgroup.getDn());
}
// Remove LDAP subgroups, which are not members in KC anymore
for (LDAPDn toRemoveDN : toRemoveSubgroupsDNs) {
LDAPObject fakeGroup = new LDAPObject();
fakeGroup.setDn(toRemoveDN);
LDAPUtils.deleteMember(ldapProvider, MembershipType.DN, config.getMembershipLdapAttribute(), ldapGroup, fakeGroup, false);
}
// Update group to LDAP
if (!kcGroup.getSubGroups().isEmpty() || !toRemoveSubgroupsDNs.isEmpty()) {
ldapProvider.getLdapIdentityStore().update(ldapGroup);
}
for (GroupModel kcSubgroup : kcGroup.getSubGroups()) {
processLdapGroupMembershipsSyncToLDAP(kcSubgroup, ldapGroupsMap);
}
}
// group-user membership operations
@Override
public List<UserModel> getGroupMembers(GroupModel kcGroup, int firstResult, int maxResults) {
LDAPObject ldapGroup = loadLDAPGroupByName(kcGroup.getName());
if (ldapGroup == null) {
return Collections.emptyList();
}
MembershipType membershipType = config.getMembershipTypeLdapAttribute();
return membershipType.getGroupMembers(this, ldapGroup, firstResult, maxResults);
}
public void addGroupMappingInLDAP(String groupName, LDAPObject ldapUser) {
LDAPObject ldapGroup = loadLDAPGroupByName(groupName);
if (ldapGroup == null) {
syncDataFromKeycloakToFederationProvider();
ldapGroup = loadLDAPGroupByName(groupName);
}
LDAPUtils.addMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), ldapGroup, ldapUser, true);
}
public void deleteGroupMappingInLDAP(LDAPObject ldapUser, LDAPObject ldapGroup) {
LDAPUtils.deleteMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), ldapGroup, ldapUser, true);
}
protected List<LDAPObject> getLDAPGroupMappings(LDAPObject ldapUser) {
String strategyKey = config.getUserGroupsRetrieveStrategy();
UserRolesRetrieveStrategy strategy = factory.getUserGroupsRetrieveStrategy(strategyKey);
return strategy.getLDAPRoleMappings(this, ldapUser);
}
public void beforeLDAPQuery(LDAPQuery query) {
String strategyKey = config.getUserGroupsRetrieveStrategy();
UserRolesRetrieveStrategy strategy = factory.getUserGroupsRetrieveStrategy(strategyKey);
strategy.beforeUserLDAPQuery(query);
}
public UserModel proxy(LDAPObject ldapUser, UserModel delegate) {
final LDAPGroupMapperMode mode = config.getMode();
// For IMPORT mode, all operations are performed against local DB
if (mode == LDAPGroupMapperMode.IMPORT) {
return delegate;
} else {
return new LDAPGroupMappingsUserDelegate(delegate, ldapUser);
}
}
public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser) {
}
public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate) {
LDAPGroupMapperMode mode = config.getMode();
// For now, import LDAP group mappings just during create
if (mode == LDAPGroupMapperMode.IMPORT && isCreate) {
List<LDAPObject> ldapGroups = getLDAPGroupMappings(ldapUser);
// Import role mappings from LDAP into Keycloak DB
for (LDAPObject ldapGroup : ldapGroups) {
GroupModel kcGroup = findKcGroupOrSyncFromLDAP(ldapGroup, user);
if (kcGroup != null) {
logger.debugf("User '%s' joins group '%s' during import from LDAP", user.getUsername(), kcGroup.getName());
user.joinGroup(kcGroup);
}
}
}
}
public class LDAPGroupMappingsUserDelegate extends UserModelDelegate {
private final LDAPObject ldapUser;
// Avoid loading group mappings from LDAP more times per-request
private Set<GroupModel> cachedLDAPGroupMappings;
public LDAPGroupMappingsUserDelegate(UserModel user, LDAPObject ldapUser) {
super(user);
this.ldapUser = ldapUser;
}
@Override
public boolean hasRole(RoleModel role) {
return super.hasRole(role) || KeycloakModelUtils.hasRoleFromGroup(getGroups(), role, true);
}
@Override
public Set<GroupModel> getGroups() {
Set<GroupModel> ldapGroupMappings = getLDAPGroupMappingsConverted();
if (config.getMode() == LDAPGroupMapperMode.LDAP_ONLY) {
// Use just group mappings from LDAP
return ldapGroupMappings;
} else {
// Merge mappings from both DB and LDAP
Set<GroupModel> modelGroupMappings = super.getGroups();
ldapGroupMappings.addAll(modelGroupMappings);
return ldapGroupMappings;
}
}
@Override
public void joinGroup(GroupModel group) {
if (config.getMode() == LDAPGroupMapperMode.LDAP_ONLY) {
// We need to create new role mappings in LDAP
cachedLDAPGroupMappings = null;
addGroupMappingInLDAP(group.getName(), ldapUser);
} else {
super.joinGroup(group);
}
}
@Override
public void leaveGroup(GroupModel group) {
LDAPQuery ldapQuery = createGroupQuery();
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
Condition roleNameCondition = conditionsBuilder.equal(config.getGroupNameLdapAttribute(), group.getName());
String membershipUserAttr = LDAPUtils.getMemberValueOfChildObject(ldapUser, config.getMembershipTypeLdapAttribute());
Condition membershipCondition = conditionsBuilder.equal(config.getMembershipLdapAttribute(), membershipUserAttr);
ldapQuery.addWhereCondition(roleNameCondition).addWhereCondition(membershipCondition);
LDAPObject ldapGroup = ldapQuery.getFirstResult();
if (ldapGroup == null) {
// Group 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 (config.getMode() == LDAPGroupMapperMode.READ_ONLY) {
super.leaveGroup(group);
}
} else {
// Group 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 (config.getMode() == LDAPGroupMapperMode.READ_ONLY) {
throw new ModelException("Not possible to delete LDAP group mappings as mapper mode is READ_ONLY");
} else {
// Delete ldap role mappings
cachedLDAPGroupMappings = null;
deleteGroupMappingInLDAP(ldapUser, ldapGroup);
}
}
}
@Override
public boolean isMemberOf(GroupModel group) {
Set<GroupModel> ldapGroupMappings = getGroups();
return ldapGroupMappings.contains(group);
}
protected Set<GroupModel> getLDAPGroupMappingsConverted() {
if (cachedLDAPGroupMappings != null) {
return new HashSet<>(cachedLDAPGroupMappings);
}
List<LDAPObject> ldapGroups = getLDAPGroupMappings(ldapUser);
Set<GroupModel> result = new HashSet<>();
for (LDAPObject ldapGroup : ldapGroups) {
GroupModel kcGroup = findKcGroupOrSyncFromLDAP(ldapGroup, this);
if (kcGroup != null) {
result.add(kcGroup);
}
}
cachedLDAPGroupMappings = new HashSet<>(result);
return result;
}
}
}

View file

@ -0,0 +1,206 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers.membership.group;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserFederationProvider;
import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
import org.keycloak.storage.ldap.LDAPUtils;
import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapperFactory;
import org.keycloak.storage.ldap.mappers.membership.CommonLDAPGroupMapperConfig;
import org.keycloak.storage.ldap.mappers.membership.LDAPGroupMapperMode;
import org.keycloak.storage.ldap.mappers.membership.MembershipType;
import org.keycloak.storage.ldap.mappers.membership.UserRolesRetrieveStrategy;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class GroupLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFactory {
public static final String PROVIDER_ID = "group-ldap-mapper";
protected static final List<ProviderConfigProperty> configProperties;
protected static final Map<String, UserRolesRetrieveStrategy> userGroupsStrategies = new LinkedHashMap<>();
protected static final List<String> MEMBERSHIP_TYPES = new LinkedList<>();
protected static final List<String> MODES = new LinkedList<>();
protected static final List<String> ROLE_RETRIEVERS;
// TODO: Merge with RoleLDAPFederationMapperFactory as there are lot of similar properties
static {
userGroupsStrategies.put(GroupMapperConfig.LOAD_GROUPS_BY_MEMBER_ATTRIBUTE, new UserRolesRetrieveStrategy.LoadRolesByMember());
userGroupsStrategies.put(GroupMapperConfig.GET_GROUPS_FROM_USER_MEMBEROF_ATTRIBUTE, new UserRolesRetrieveStrategy.GetRolesFromUserMemberOfAttribute());
userGroupsStrategies.put(GroupMapperConfig.LOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY, new UserRolesRetrieveStrategy.LoadRolesByMemberRecursively());
for (MembershipType membershipType : MembershipType.values()) {
MEMBERSHIP_TYPES.add(membershipType.toString());
}
for (LDAPGroupMapperMode mode : LDAPGroupMapperMode.values()) {
MODES.add(mode.toString());
}
ROLE_RETRIEVERS = new LinkedList<>(userGroupsStrategies.keySet());
List<ProviderConfigProperty> config = getProps(null);
configProperties = config;
}
private static List<ProviderConfigProperty> getProps(ComponentModel parent) {
String roleObjectClasses = LDAPConstants.GROUP_OF_NAMES;
String mode = LDAPGroupMapperMode.LDAP_ONLY.toString();
if (parent != null) {
LDAPConfig config = new LDAPConfig(parent.getConfig());
roleObjectClasses = config.isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES;
mode = config.getEditMode() == LDAPStorageProviderFactory.EditMode.WRITABLE ? LDAPGroupMapperMode.LDAP_ONLY.toString() : LDAPGroupMapperMode.READ_ONLY.toString();
}
return ProviderConfigurationBuilder.create()
.property().name(GroupMapperConfig.GROUPS_DN)
.label("LDAP Groups DN")
.helpText("LDAP DN where are groups of this tree saved. For example 'ou=groups,dc=example,dc=org' ")
.type(ProviderConfigProperty.STRING_TYPE)
.add()
.property().name(GroupMapperConfig.GROUP_NAME_LDAP_ATTRIBUTE)
.label("Group Name LDAP Attribute")
.helpText("Name of LDAP attribute, which is used in group objects for name and RDN of group. Usually it will be 'cn' . In this case typical group/role object may have DN like 'cn=Group1,ou=groups,dc=example,dc=org' ")
.type(ProviderConfigProperty.STRING_TYPE)
.defaultValue(LDAPConstants.CN)
.add()
.property().name(GroupMapperConfig.GROUP_OBJECT_CLASSES)
.label("Group Object Classes")
.helpText("Object class (or classes) of the group object. It's divided by comma if more classes needed. In typical LDAP deployment it could be 'groupOfNames' . In Active Directory it's usually 'group' ")
.type(ProviderConfigProperty.STRING_TYPE)
.defaultValue(roleObjectClasses)
.add()
.property().name(GroupMapperConfig.PRESERVE_GROUP_INHERITANCE)
.label("Preserve Group Inheritance")
.helpText("Flag whether group inheritance from LDAP should be propagated to Keycloak. If false, then all LDAP groups will be mapped as flat top-level groups in Keycloak. Otherwise group inheritance is " +
"preserved into Keycloak, but the group sync might fail if LDAP structure contains recursions or multiple parent groups per child groups")
.type(ProviderConfigProperty.BOOLEAN_TYPE)
.defaultValue("true")
.add()
.property().name(GroupMapperConfig.MEMBERSHIP_LDAP_ATTRIBUTE)
.label("Membership LDAP Attribute")
.helpText("Name of LDAP attribute on group, which is used for membership mappings. Usually it will be 'member' ")
.type(ProviderConfigProperty.STRING_TYPE)
.defaultValue(LDAPConstants.MEMBER)
.add()
.property().name(GroupMapperConfig.MEMBERSHIP_ATTRIBUTE_TYPE)
.label("Membership Attribute Type")
.helpText("DN means that LDAP group has it's members declared in form of their full DN. For example 'member: uid=john,ou=users,dc=example,dc=com' . " +
"UID means that LDAP group has it's members declared in form of pure user uids. For example 'memberUid: john' .")
.type(ProviderConfigProperty.LIST_TYPE)
.options(MEMBERSHIP_TYPES)
.defaultValue(MembershipType.DN.toString())
.add()
.property().name(GroupMapperConfig.GROUPS_LDAP_FILTER)
.label("LDAP Filter")
.helpText("LDAP Filter adds additional custom filter to the whole query for retrieve LDAP groups. Leave this empty if no additional filtering is needed and you want to retrieve all groups from LDAP. Otherwise make sure that filter starts with '(' and ends with ')'")
.type(ProviderConfigProperty.STRING_TYPE)
.add()
.property().name(GroupMapperConfig.MODE)
.label("Mode")
.helpText("LDAP_ONLY means that all group mappings of users are retrieved from LDAP and saved into LDAP. READ_ONLY is Read-only LDAP mode where group mappings are " +
"retrieved from both LDAP and DB and merged together. New group joins are not saved to LDAP but to DB. IMPORT is Read-only LDAP mode where group mappings are " +
"retrieved from LDAP just at the time when user is imported from LDAP and then " +
"they are saved to local keycloak DB.")
.type(ProviderConfigProperty.LIST_TYPE)
.options(MODES)
.defaultValue(mode)
.add()
.property().name(GroupMapperConfig.USER_ROLES_RETRIEVE_STRATEGY)
.label("User Groups Retrieve Strategy")
.helpText("Specify how to retrieve groups of user. LOAD_GROUPS_BY_MEMBER_ATTRIBUTE means that roles of user will be retrieved by sending LDAP query to retrieve all groups where 'member' is our user. " +
"GET_GROUPS_FROM_USER_MEMBEROF_ATTRIBUTE means that groups of user will be retrieved from 'memberOf' attribute of our user. " +
"LOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY is applicable just in Active Directory and it means that groups of user will be retrieved recursively with usage of LDAP_MATCHING_RULE_IN_CHAIN Ldap extension.")
.type(ProviderConfigProperty.LIST_TYPE)
.options(ROLE_RETRIEVERS)
.defaultValue(GroupMapperConfig.LOAD_GROUPS_BY_MEMBER_ATTRIBUTE)
.add()
.property().name(GroupMapperConfig.MAPPED_GROUP_ATTRIBUTES)
.label("Mapped Group Attributes")
.helpText("List of names of attributes divided by comma. This points to the list of attributes on LDAP group, which will be mapped as attributes of Group in Keycloak. " +
"Leave this empty if no additional group attributes are required to be mapped in Keycloak. ")
.type(ProviderConfigProperty.STRING_TYPE)
.add()
.property().name(GroupMapperConfig.DROP_NON_EXISTING_GROUPS_DURING_SYNC)
.label("Drop non-existing groups during sync")
.helpText("If this flag is true, then during sync of groups from LDAP to Keycloak, we will keep just those Keycloak groups, which still exists in LDAP. Rest will be deleted")
.type(ProviderConfigProperty.BOOLEAN_TYPE)
.defaultValue("false")
.add()
.build();
}
@Override
public String getHelpText() {
return "Used to map group mappings of groups from some LDAP DN to Keycloak group mappings";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public UserFederationMapperSyncConfigRepresentation getSyncConfig() {
return new UserFederationMapperSyncConfigRepresentation(true, "sync-ldap-groups-to-keycloak", true, "sync-keycloak-groups-to-ldap");
}
@Override
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
checkMandatoryConfigAttribute(GroupMapperConfig.GROUPS_DN, "LDAP Groups DN", config);
checkMandatoryConfigAttribute(GroupMapperConfig.MODE, "Mode", config);
String mt = config.getConfig().getFirst(CommonLDAPGroupMapperConfig.MEMBERSHIP_ATTRIBUTE_TYPE);
MembershipType membershipType = mt==null ? MembershipType.DN : Enum.valueOf(MembershipType.class, mt);
boolean preserveGroupInheritance = Boolean.parseBoolean(config.getConfig().getFirst(GroupMapperConfig.PRESERVE_GROUP_INHERITANCE));
if (preserveGroupInheritance && membershipType != MembershipType.DN) {
throw new ComponentValidationException("ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType");
}
LDAPUtils.validateCustomLdapFilter(config.getConfig().getFirst(GroupMapperConfig.GROUPS_LDAP_FILTER));
}
@Override
protected AbstractLDAPStorageMapper createMapper(ComponentModel mapperModel, LDAPStorageProvider federationProvider, RealmModel realm) {
return new GroupLDAPStorageMapper(mapperModel, federationProvider, realm, this);
}
protected UserRolesRetrieveStrategy getUserGroupsRetrieveStrategy(String strategyKey) {
return userGroupsStrategies.get(strategyKey);
}
}

View file

@ -0,0 +1,125 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers.membership.group;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.membership.CommonLDAPGroupMapperConfig;
import java.util.Collection;
import java.util.Collections;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class GroupMapperConfig extends CommonLDAPGroupMapperConfig {
// LDAP DN where are groups of this tree saved.
public static final String GROUPS_DN = "groups.dn";
// Name of LDAP attribute, which is used in group objects for name and RDN of group. Usually it will be "cn"
public static final String GROUP_NAME_LDAP_ATTRIBUTE = "group.name.ldap.attribute";
// Object classes of the group object.
public static final String GROUP_OBJECT_CLASSES = "group.object.classes";
// Flag whether group inheritance from LDAP should be propagated to Keycloak group inheritance.
public static final String PRESERVE_GROUP_INHERITANCE = "preserve.group.inheritance";
// Customized LDAP filter which is added to the whole LDAP query
public static final String GROUPS_LDAP_FILTER = "groups.ldap.filter";
// Name of attributes of the LDAP group object, which will be mapped as attributes of Group in Keycloak
public static final String MAPPED_GROUP_ATTRIBUTES = "mapped.group.attributes";
// During sync of groups from LDAP to Keycloak, we will keep just those Keycloak groups, which still exists in LDAP. Rest will be deleted
public static final String DROP_NON_EXISTING_GROUPS_DURING_SYNC = "drop.non.existing.groups.during.sync";
// See UserRolesRetrieveStrategy
public static final String LOAD_GROUPS_BY_MEMBER_ATTRIBUTE = "LOAD_GROUPS_BY_MEMBER_ATTRIBUTE";
public static final String GET_GROUPS_FROM_USER_MEMBEROF_ATTRIBUTE = "GET_GROUPS_FROM_USER_MEMBEROF_ATTRIBUTE";
public static final String LOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY = "LOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY";
public GroupMapperConfig(ComponentModel mapperModel) {
super(mapperModel);
}
public String getGroupsDn() {
String groupsDn = mapperModel.getConfig().getFirst(GROUPS_DN);
if (groupsDn == null) {
throw new ModelException("Groups DN is null! Check your configuration");
}
return groupsDn;
}
@Override
public String getLDAPGroupsDn() {
return getGroupsDn();
}
public String getGroupNameLdapAttribute() {
String rolesRdnAttr = mapperModel.getConfig().getFirst(GROUP_NAME_LDAP_ATTRIBUTE);
return rolesRdnAttr!=null ? rolesRdnAttr : LDAPConstants.CN;
}
@Override
public String getLDAPGroupNameLdapAttribute() {
return getGroupNameLdapAttribute();
}
public boolean isPreserveGroupsInheritance() {
return AbstractLDAPStorageMapper.parseBooleanParameter(mapperModel, PRESERVE_GROUP_INHERITANCE);
}
public String getMembershipLdapAttribute() {
String membershipAttrName = mapperModel.getConfig().getFirst(MEMBERSHIP_LDAP_ATTRIBUTE);
return membershipAttrName!=null ? membershipAttrName : LDAPConstants.MEMBER;
}
public Collection<String> getGroupObjectClasses(LDAPStorageProvider ldapProvider) {
String objectClasses = mapperModel.getConfig().getFirst(GROUP_OBJECT_CLASSES);
if (objectClasses == null) {
// For Active directory, the default is 'group' . For other servers 'groupOfNames'
objectClasses = ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES;
}
return getConfigValues(objectClasses);
}
public Collection<String> getGroupAttributes() {
String groupAttrs = mapperModel.getConfig().getFirst(MAPPED_GROUP_ATTRIBUTES);
return (groupAttrs == null) ? Collections.<String>emptySet() : getConfigValues(groupAttrs);
}
public String getCustomLdapFilter() {
return mapperModel.getConfig().getFirst(GROUPS_LDAP_FILTER);
}
public boolean isDropNonExistingGroupsDuringSync() {
return AbstractLDAPStorageMapper.parseBooleanParameter(mapperModel, DROP_NON_EXISTING_GROUPS_DURING_SYNC);
}
public String getUserGroupsRetrieveStrategy() {
String strategyString = mapperModel.getConfig().getFirst(USER_ROLES_RETRIEVE_STRATEGY);
return strategyString!=null ? strategyString : LOAD_GROUPS_BY_MEMBER_ATTRIBUTE;
}
}

View file

@ -0,0 +1,204 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers.membership.group;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class GroupTreeResolver {
/**
* Fully resolves list of group trees to be used in Keycloak. The input is group info (usually from LDAP) where each "Group" object contains
* just it's name and direct children.
*
* The operation also performs validation as rules for LDAP are less strict than for Keycloak (In LDAP, the recursion is possible and multiple parents of single group is also allowed)
*
* @param groups
* @return
* @throws GroupTreeResolveException
*/
public List<GroupTreeEntry> resolveGroupTree(List<Group> groups) throws GroupTreeResolveException {
// 1- Get parents of each group
Map<String, List<String>> parentsTree = getParentsTree(groups);
// 2 - Get rootGroups (groups without parent) and check if there is no group with multiple parents
List<String> rootGroups = new LinkedList<>();
for (Map.Entry<String, List<String>> group : parentsTree.entrySet()) {
int parentCount = group.getValue().size();
if (parentCount == 0) {
rootGroups.add(group.getKey());
} else if (parentCount > 1) {
throw new GroupTreeResolveException("Group '" + group.getKey() + "' detected to have multiple parents. This is not allowed in Keycloak. Parents are: " + group.getValue());
}
}
// 3 - Just convert to map for easier retrieval
Map<String, Group> asMap = new TreeMap<>();
for (Group group : groups) {
asMap.put(group.getGroupName(), group);
}
// 4 - Now we have rootGroups. Let's resolve them
List<GroupTreeEntry> finalResult = new LinkedList<>();
Set<String> visitedGroups = new TreeSet<>();
for (String rootGroupName : rootGroups) {
List<String> subtree = new LinkedList<>();
subtree.add(rootGroupName);
GroupTreeEntry groupTree = resolveGroupTree(rootGroupName, asMap, visitedGroups, subtree);
finalResult.add(groupTree);
}
// 5 - Check recursion
if (visitedGroups.size() != asMap.size()) {
// Recursion detected. Try to find where it is
for (Map.Entry<String, Group> entry : asMap.entrySet()) {
String groupName = entry.getKey();
if (!visitedGroups.contains(groupName)) {
List<String> subtree = new LinkedList<>();
subtree.add(groupName);
Set<String> newVisitedGroups = new TreeSet<>();
resolveGroupTree(groupName, asMap, newVisitedGroups, subtree);
visitedGroups.addAll(newVisitedGroups);
}
}
// Shouldn't happen
throw new GroupTreeResolveException("Illegal state: Recursion detected, but wasn't able to find it");
}
return finalResult;
}
private Map<String, List<String>> getParentsTree(List<Group> groups) throws GroupTreeResolveException {
Map<String, List<String>> result = new TreeMap<>();
for (Group group : groups) {
result.put(group.getGroupName(), new LinkedList<String>());
}
for (Group group : groups) {
for (String child : group.getChildrenNames()) {
List<String> list = result.get(child);
if (list == null) {
throw new GroupTreeResolveException("Group '" + child + "' referenced as member of group '" + group.getGroupName() + "' doesn't exists");
}
list.add(group.getGroupName());
}
}
return result;
}
private GroupTreeEntry resolveGroupTree(String groupName, Map<String, Group> asMap, Set<String> visitedGroups, List<String> currentSubtree) throws GroupTreeResolveException {
if (visitedGroups.contains(groupName)) {
throw new GroupTreeResolveException("Recursion detected when trying to resolve group '" + groupName + "'. Whole recursion path: " + currentSubtree);
}
visitedGroups.add(groupName);
Group group = asMap.get(groupName);
List<GroupTreeEntry> children = new LinkedList<>();
GroupTreeEntry result = new GroupTreeEntry(group.getGroupName(), children);
for (String childrenName : group.getChildrenNames()) {
List<String> subtreeCopy = new LinkedList<>(currentSubtree);
subtreeCopy.add(childrenName);
GroupTreeEntry childEntry = resolveGroupTree(childrenName, asMap, visitedGroups, subtreeCopy);
children.add(childEntry);
}
return result;
}
// static classes
public static class GroupTreeResolveException extends Exception {
public GroupTreeResolveException(String message) {
super(message);
}
}
public static class Group {
private final String groupName;
private final List<String> childrenNames;
public Group(String groupName, String... childrenNames) {
this(groupName, Arrays.asList(childrenNames));
}
public Group(String groupName, Collection<String> childrenNames) {
this.groupName = groupName;
this.childrenNames = new LinkedList<>(childrenNames);
}
public String getGroupName() {
return groupName;
}
public List<String> getChildrenNames() {
return childrenNames;
}
}
public static class GroupTreeEntry {
private final String groupName;
private final List<GroupTreeEntry> children;
public GroupTreeEntry(String groupName, List<GroupTreeEntry> children) {
this.groupName = groupName;
this.children = children;
}
public String getGroupName() {
return groupName;
}
public List<GroupTreeEntry> getChildren() {
return children;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder("{ " + groupName + " -> [ ");
for (GroupTreeEntry child : children) {
builder.append(child.toString());
}
builder.append(" ]}");
return builder.toString();
}
}
}

View file

@ -0,0 +1,450 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers.membership.role;
import org.jboss.logging.Logger;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.UserModelDelegate;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.LDAPUtils;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.Condition;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.membership.CommonLDAPGroupMapper;
import org.keycloak.storage.ldap.mappers.membership.CommonLDAPGroupMapperConfig;
import org.keycloak.storage.ldap.mappers.membership.LDAPGroupMapperMode;
import org.keycloak.storage.ldap.mappers.membership.UserRolesRetrieveStrategy;
import org.keycloak.storage.user.SynchronizationResult;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Map realm roles or roles of particular client to LDAP groups
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class RoleLDAPStorageMapper extends AbstractLDAPStorageMapper implements CommonLDAPGroupMapper {
private static final Logger logger = Logger.getLogger(RoleLDAPStorageMapper.class);
private final RoleMapperConfig config;
private final RoleLDAPStorageMapperFactory factory;
public RoleLDAPStorageMapper(ComponentModel mapperModel, LDAPStorageProvider ldapProvider, RealmModel realm, RoleLDAPStorageMapperFactory factory) {
super(mapperModel, ldapProvider, realm);
this.config = new RoleMapperConfig(mapperModel);
this.factory = factory;
}
@Override
public LDAPQuery createLDAPGroupQuery() {
return createRoleQuery();
}
@Override
public CommonLDAPGroupMapperConfig getConfig() {
return config;
}
@Override
public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate) {
LDAPGroupMapperMode mode = config.getMode();
// For now, import LDAP role mappings just during create
if (mode == LDAPGroupMapperMode.IMPORT && isCreate) {
List<LDAPObject> ldapRoles = getLDAPRoleMappings(ldapUser);
// Import role mappings from LDAP into Keycloak DB
String roleNameAttr = config.getRoleNameLdapAttribute();
for (LDAPObject ldapRole : ldapRoles) {
String roleName = ldapRole.getAttributeAsString(roleNameAttr);
RoleContainerModel roleContainer = getTargetRoleContainer();
RoleModel role = roleContainer.getRole(roleName);
if (role == null) {
role = roleContainer.addRole(roleName);
}
logger.debugf("Granting role [%s] to user [%s] during import from LDAP", roleName, user.getUsername());
user.grantRole(role);
}
}
}
@Override
public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser) {
}
// Sync roles from LDAP to Keycloak DB
@Override
public SynchronizationResult syncDataFromFederationProviderToKeycloak() {
SynchronizationResult syncResult = new SynchronizationResult() {
@Override
public String getStatus() {
return String.format("%d imported roles, %d roles already exists in Keycloak", getAdded(), getUpdated());
}
};
logger.debugf("Syncing roles from LDAP into Keycloak DB. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getName());
// Send LDAP query to load all roles
LDAPQuery ldapRoleQuery = createRoleQuery();
List<LDAPObject> ldapRoles = LDAPUtils.loadAllLDAPObjects(ldapRoleQuery, ldapProvider);
RoleContainerModel roleContainer = getTargetRoleContainer();
String rolesRdnAttr = config.getRoleNameLdapAttribute();
for (LDAPObject ldapRole : ldapRoles) {
String roleName = ldapRole.getAttributeAsString(rolesRdnAttr);
if (roleContainer.getRole(roleName) == null) {
logger.debugf("Syncing role [%s] from LDAP to keycloak DB", roleName);
roleContainer.addRole(roleName);
syncResult.increaseAdded();
} else {
syncResult.increaseUpdated();
}
}
return syncResult;
}
// Sync roles from Keycloak back to LDAP
@Override
public SynchronizationResult syncDataFromKeycloakToFederationProvider() {
SynchronizationResult syncResult = new SynchronizationResult() {
@Override
public String getStatus() {
return String.format("%d roles imported to LDAP, %d roles already existed in LDAP", getAdded(), getUpdated());
}
};
if (config.getMode() != LDAPGroupMapperMode.LDAP_ONLY) {
logger.warnf("Ignored sync for federation mapper '%s' as it's mode is '%s'", mapperModel.getName(), config.getMode().toString());
return syncResult;
}
logger.debugf("Syncing roles from Keycloak into LDAP. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getName());
// Send LDAP query to see which roles exists there
LDAPQuery ldapQuery = createRoleQuery();
List<LDAPObject> ldapRoles = ldapQuery.getResultList();
Set<String> ldapRoleNames = new HashSet<>();
String rolesRdnAttr = config.getRoleNameLdapAttribute();
for (LDAPObject ldapRole : ldapRoles) {
String roleName = ldapRole.getAttributeAsString(rolesRdnAttr);
ldapRoleNames.add(roleName);
}
RoleContainerModel roleContainer = getTargetRoleContainer();
Set<RoleModel> keycloakRoles = roleContainer.getRoles();
for (RoleModel keycloakRole : keycloakRoles) {
String roleName = keycloakRole.getName();
if (ldapRoleNames.contains(roleName)) {
syncResult.increaseUpdated();
} else {
logger.debugf("Syncing role [%s] from Keycloak to LDAP", roleName);
createLDAPRole(roleName);
syncResult.increaseAdded();
}
}
return syncResult;
}
// TODO: Possible to merge with GroupMapper and move to common class
public LDAPQuery createRoleQuery() {
LDAPQuery ldapQuery = new LDAPQuery(ldapProvider);
// For now, use same search scope, which is configured "globally" and used for user's search.
ldapQuery.setSearchScope(ldapProvider.getLdapIdentityStore().getConfig().getSearchScope());
String rolesDn = config.getRolesDn();
ldapQuery.setSearchDn(rolesDn);
Collection<String> roleObjectClasses = config.getRoleObjectClasses(ldapProvider);
ldapQuery.addObjectClasses(roleObjectClasses);
String rolesRdnAttr = config.getRoleNameLdapAttribute();
String customFilter = config.getCustomLdapFilter();
if (customFilter != null && customFilter.trim().length() > 0) {
Condition customFilterCondition = new LDAPQueryConditionsBuilder().addCustomLDAPFilter(customFilter);
ldapQuery.addWhereCondition(customFilterCondition);
}
String membershipAttr = config.getMembershipLdapAttribute();
ldapQuery.addReturningLdapAttribute(rolesRdnAttr);
ldapQuery.addReturningLdapAttribute(membershipAttr);
return ldapQuery;
}
protected RoleContainerModel getTargetRoleContainer() {
boolean realmRolesMapping = config.isRealmRolesMapping();
if (realmRolesMapping) {
return realm;
} else {
String clientId = config.getClientId();
if (clientId == null) {
throw new ModelException("Using client roles mapping is requested, but parameter client.id not found!");
}
ClientModel client = realm.getClientByClientId(clientId);
if (client == null) {
throw new ModelException("Can't found requested client with clientId: " + clientId);
}
return client;
}
}
public LDAPObject createLDAPRole(String roleName) {
LDAPObject ldapRole = LDAPUtils.createLDAPGroup(ldapProvider, roleName, config.getRoleNameLdapAttribute(), config.getRoleObjectClasses(ldapProvider),
config.getRolesDn(), Collections.<String, Set<String>>emptyMap());
logger.debugf("Creating role [%s] to LDAP with DN [%s]", roleName, ldapRole.getDn().toString());
return ldapRole;
}
public void addRoleMappingInLDAP(String roleName, LDAPObject ldapUser) {
LDAPObject ldapRole = loadLDAPRoleByName(roleName);
if (ldapRole == null) {
ldapRole = createLDAPRole(roleName);
}
LDAPUtils.addMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), ldapRole, ldapUser, true);
}
public void deleteRoleMappingInLDAP(LDAPObject ldapUser, LDAPObject ldapRole) {
LDAPUtils.deleteMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), ldapRole, ldapUser, true);
}
public LDAPObject loadLDAPRoleByName(String roleName) {
LDAPQuery ldapQuery = createRoleQuery();
Condition roleNameCondition = new LDAPQueryConditionsBuilder().equal(config.getRoleNameLdapAttribute(), roleName);
ldapQuery.addWhereCondition(roleNameCondition);
return ldapQuery.getFirstResult();
}
protected List<LDAPObject> getLDAPRoleMappings(LDAPObject ldapUser) {
String strategyKey = config.getUserRolesRetrieveStrategy();
UserRolesRetrieveStrategy strategy = factory.getUserRolesRetrieveStrategy(strategyKey);
return strategy.getLDAPRoleMappings(this, ldapUser);
}
@Override
public UserModel proxy(LDAPObject ldapUser, UserModel delegate) {
final LDAPGroupMapperMode mode = config.getMode();
// For IMPORT mode, all operations are performed against local DB
if (mode == LDAPGroupMapperMode.IMPORT) {
return delegate;
} else {
return new LDAPRoleMappingsUserDelegate(delegate, ldapUser);
}
}
@Override
public void beforeLDAPQuery(LDAPQuery query) {
String strategyKey = config.getUserRolesRetrieveStrategy();
UserRolesRetrieveStrategy strategy = factory.getUserRolesRetrieveStrategy(strategyKey);
strategy.beforeUserLDAPQuery(query);
}
public class LDAPRoleMappingsUserDelegate extends UserModelDelegate {
private final LDAPObject ldapUser;
private final RoleContainerModel roleContainer;
// Avoid loading role mappings from LDAP more times per-request
private Set<RoleModel> cachedLDAPRoleMappings;
public LDAPRoleMappingsUserDelegate(UserModel user, LDAPObject ldapUser) {
super(user);
this.ldapUser = ldapUser;
this.roleContainer = getTargetRoleContainer();
}
@Override
public Set<RoleModel> getRealmRoleMappings() {
if (roleContainer.equals(realm)) {
Set<RoleModel> ldapRoleMappings = getLDAPRoleMappingsConverted();
if (config.getMode() == LDAPGroupMapperMode.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) {
if (roleContainer.equals(client)) {
Set<RoleModel> ldapRoleMappings = getLDAPRoleMappingsConverted();
if (config.getMode() == LDAPGroupMapperMode.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)
|| KeycloakModelUtils.hasRoleFromGroup(getGroups(), role, true);
}
@Override
public void grantRole(RoleModel role) {
if (config.getMode() == LDAPGroupMapperMode.LDAP_ONLY) {
if (role.getContainer().equals(roleContainer)) {
// We need to create new role mappings in LDAP
cachedLDAPRoleMappings = null;
addRoleMappingInLDAP(role.getName(), ldapUser);
} else {
super.grantRole(role);
}
} else {
super.grantRole(role);
}
}
@Override
public Set<RoleModel> getRoleMappings() {
Set<RoleModel> modelRoleMappings = super.getRoleMappings();
Set<RoleModel> ldapRoleMappings = getLDAPRoleMappingsConverted();
if (config.getMode() == LDAPGroupMapperMode.LDAP_ONLY) {
// For LDAP-only we want to retrieve role mappings of target container just from LDAP
Set<RoleModel> modelRolesCopy = new HashSet<>(modelRoleMappings);
for (RoleModel role : modelRolesCopy) {
if (role.getContainer().equals(roleContainer)) {
modelRoleMappings.remove(role);
}
}
}
modelRoleMappings.addAll(ldapRoleMappings);
return modelRoleMappings;
}
protected Set<RoleModel> getLDAPRoleMappingsConverted() {
if (cachedLDAPRoleMappings != null) {
return new HashSet<>(cachedLDAPRoleMappings);
}
List<LDAPObject> ldapRoles = getLDAPRoleMappings(ldapUser);
Set<RoleModel> roles = new HashSet<>();
String roleNameLdapAttr = config.getRoleNameLdapAttribute();
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);
}
cachedLDAPRoleMappings = new HashSet<>(roles);
return roles;
}
@Override
public void deleteRoleMapping(RoleModel role) {
if (role.getContainer().equals(roleContainer)) {
LDAPQuery ldapQuery = createRoleQuery();
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
Condition roleNameCondition = conditionsBuilder.equal(config.getRoleNameLdapAttribute(), role.getName());
String membershipUserAttr = LDAPUtils.getMemberValueOfChildObject(ldapUser, config.getMembershipTypeLdapAttribute());
Condition membershipCondition = conditionsBuilder.equal(config.getMembershipLdapAttribute(), membershipUserAttr);
ldapQuery.addWhereCondition(roleNameCondition).addWhereCondition(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 (config.getMode() == LDAPGroupMapperMode.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 (config.getMode() == LDAPGroupMapperMode.READ_ONLY) {
throw new ModelException("Not possible to delete LDAP role mappings as mapper mode is READ_ONLY");
} else {
// Delete ldap role mappings
cachedLDAPRoleMappings = null;
deleteRoleMappingInLDAP(ldapUser, ldapRole);
}
}
} else {
super.deleteRoleMapping(role);
}
}
}
}

View file

@ -0,0 +1,200 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers.membership.role;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserFederationProvider;
import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
import org.keycloak.storage.ldap.LDAPUtils;
import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapperFactory;
import org.keycloak.storage.ldap.mappers.membership.LDAPGroupMapperMode;
import org.keycloak.storage.ldap.mappers.membership.MembershipType;
import org.keycloak.storage.ldap.mappers.membership.UserRolesRetrieveStrategy;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class RoleLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFactory {
public static final String PROVIDER_ID = "role-ldap-mapper";
protected static final List<ProviderConfigProperty> configProperties;
protected static final Map<String, UserRolesRetrieveStrategy> userRolesStrategies = new LinkedHashMap<>();
protected static final List<String> MEMBERSHIP_TYPES = new LinkedList<>();
protected static final List<String> MODES = new LinkedList<>();
protected static final List<String> roleRetrievers;
static {
userRolesStrategies.put(RoleMapperConfig.LOAD_ROLES_BY_MEMBER_ATTRIBUTE, new UserRolesRetrieveStrategy.LoadRolesByMember());
userRolesStrategies.put(RoleMapperConfig.GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE, new UserRolesRetrieveStrategy.GetRolesFromUserMemberOfAttribute());
userRolesStrategies.put(RoleMapperConfig.LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY, new UserRolesRetrieveStrategy.LoadRolesByMemberRecursively());
for (MembershipType membershipType : MembershipType.values()) {
MEMBERSHIP_TYPES.add(membershipType.toString());
}
for (LDAPGroupMapperMode mode : LDAPGroupMapperMode.values()) {
MODES.add(mode.toString());
}
roleRetrievers = new LinkedList<>(userRolesStrategies.keySet());
List<ProviderConfigProperty> config = getProps(null);
configProperties = config;
}
private static List<ProviderConfigProperty> getProps(ComponentModel parent) {
String roleObjectClasses = LDAPConstants.GROUP_OF_NAMES;
String mode = LDAPGroupMapperMode.LDAP_ONLY.toString();
if (parent != null) {
LDAPConfig config = new LDAPConfig(parent.getConfig());
roleObjectClasses = config.isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES;
mode = config.getEditMode() == LDAPStorageProviderFactory.EditMode.WRITABLE ? LDAPGroupMapperMode.LDAP_ONLY.toString() : LDAPGroupMapperMode.READ_ONLY.toString();
}
return ProviderConfigurationBuilder.create()
.property().name(RoleMapperConfig.ROLES_DN)
.label("LDAP Roles DN")
.helpText("LDAP DN where are roles of this tree saved. For example 'ou=finance,dc=example,dc=org' ")
.type(ProviderConfigProperty.STRING_TYPE)
.add()
.property().name(RoleMapperConfig.ROLE_NAME_LDAP_ATTRIBUTE)
.label("Role Name LDAP Attribute")
.helpText("Name of LDAP attribute, which is used in role objects for name and RDN of role. Usually it will be 'cn' . In this case typical group/role object may have DN like 'cn=role1,ou=finance,dc=example,dc=org' ")
.type(ProviderConfigProperty.STRING_TYPE)
.defaultValue(LDAPConstants.CN)
.add()
.property().name(RoleMapperConfig.ROLE_OBJECT_CLASSES)
.label("Role Object Classes")
.helpText("Object class (or classes) of the role object. It's divided by comma if more classes needed. In typical LDAP deployment it could be 'groupOfNames' . In Active Directory it's usually 'group' ")
.type(ProviderConfigProperty.STRING_TYPE)
.defaultValue(roleObjectClasses)
.add()
.property().name(RoleMapperConfig.MEMBERSHIP_LDAP_ATTRIBUTE)
.label("Membership LDAP Attribute")
.helpText("Name of LDAP attribute on role, which is used for membership mappings. Usually it will be 'member' ")
.type(ProviderConfigProperty.STRING_TYPE)
.defaultValue(LDAPConstants.MEMBER)
.add()
.property().name(RoleMapperConfig.MEMBERSHIP_ATTRIBUTE_TYPE)
.label("Membership Attribute Type")
.helpText("DN means that LDAP role has it's members declared in form of their full DN. For example 'member: uid=john,ou=users,dc=example,dc=com' . " +
"UID means that LDAP role has it's members declared in form of pure user uids. For example 'memberUid: john' .")
.type(ProviderConfigProperty.LIST_TYPE)
.options(MEMBERSHIP_TYPES)
.defaultValue(MembershipType.DN.toString())
.add()
.property().name(RoleMapperConfig.ROLES_LDAP_FILTER)
.label("LDAP Filter")
.helpText("LDAP Filter adds additional custom filter to the whole query for retrieve LDAP roles. Leave this empty if no additional filtering is needed and you want to retrieve all roles from LDAP. Otherwise make sure that filter starts with '(' and ends with ')'")
.type(ProviderConfigProperty.STRING_TYPE)
.add()
.property().name(RoleMapperConfig.MODE)
.label("Mode")
.helpText("LDAP_ONLY means that all role mappings are retrieved from LDAP and saved into LDAP. READ_ONLY is Read-only LDAP mode where role mappings are " +
"retrieved from both LDAP and DB and merged together. New role grants are not saved to LDAP but to DB. IMPORT is Read-only LDAP mode where role mappings are retrieved from LDAP just at the time when user is imported from LDAP and then " +
"they are saved to local keycloak DB.")
.type(ProviderConfigProperty.LIST_TYPE)
.options(MODES)
.defaultValue(mode)
.add()
.property().name(RoleMapperConfig.USER_ROLES_RETRIEVE_STRATEGY)
.label("User Roles Retrieve Strategy")
.helpText("Specify how to retrieve roles of user. LOAD_ROLES_BY_MEMBER_ATTRIBUTE means that roles of user will be retrieved by sending LDAP query to retrieve all roles where 'member' is our user. " +
"GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE means that roles of user will be retrieved from 'memberOf' attribute of our user. " +
"LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY is applicable just in Active Directory and it means that roles of user will be retrieved recursively with usage of LDAP_MATCHING_RULE_IN_CHAIN Ldap extension.")
.type(ProviderConfigProperty.LIST_TYPE)
.options(roleRetrievers)
.defaultValue(RoleMapperConfig.LOAD_ROLES_BY_MEMBER_ATTRIBUTE)
.add()
.property().name(RoleMapperConfig.USE_REALM_ROLES_MAPPING)
.label("Use Realm Roles Mapping")
.helpText("If true, then LDAP role mappings will be mapped to realm role mappings in Keycloak. Otherwise it will be mapped to client role mappings")
.type(ProviderConfigProperty.BOOLEAN_TYPE)
.defaultValue("true")
.add()
.property().name(RoleMapperConfig.CLIENT_ID)
.label("Client ID")
.helpText("Client ID of client to which LDAP role mappings will be mapped. Applicable just if 'Use Realm Roles Mapping' is false")
.type(ProviderConfigProperty.CLIENT_LIST_TYPE)
.add()
.build();
}
@Override
public String getHelpText() {
return "Used to map role mappings of roles from some LDAP DN to Keycloak role mappings of either realm roles or client roles of particular client";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public UserFederationMapperSyncConfigRepresentation getSyncConfig() {
return new UserFederationMapperSyncConfigRepresentation(true, "sync-ldap-roles-to-keycloak", true, "sync-keycloak-roles-to-ldap");
}
@Override
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
checkMandatoryConfigAttribute(RoleMapperConfig.ROLES_DN, "LDAP Roles DN", config);
checkMandatoryConfigAttribute(RoleMapperConfig.MODE, "Mode", config);
String realmMappings = config.getConfig().getFirst(RoleMapperConfig.USE_REALM_ROLES_MAPPING);
boolean useRealmMappings = Boolean.parseBoolean(realmMappings);
if (!useRealmMappings) {
String clientId = config.getConfig().getFirst(RoleMapperConfig.CLIENT_ID);
if (clientId == null || clientId.trim().isEmpty()) {
throw new ComponentValidationException("ldapErrorMissingClientId");
}
}
LDAPUtils.validateCustomLdapFilter(config.getConfig().getFirst(RoleMapperConfig.ROLES_LDAP_FILTER));
}
@Override
protected AbstractLDAPStorageMapper createMapper(ComponentModel mapperModel, LDAPStorageProvider federationProvider, RealmModel realm) {
return new RoleLDAPStorageMapper(mapperModel, federationProvider, realm, this);
}
protected UserRolesRetrieveStrategy getUserRolesRetrieveStrategy(String strategyKey) {
return userRolesStrategies.get(strategyKey);
}
}

View file

@ -0,0 +1,113 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers.membership.role;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.mappers.membership.CommonLDAPGroupMapperConfig;
import java.util.Collection;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class RoleMapperConfig extends CommonLDAPGroupMapperConfig {
// 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";
// 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";
// Customized LDAP filter which is added to the whole LDAP query
public static final String ROLES_LDAP_FILTER = "roles.ldap.filter";
// See UserRolesRetrieveStrategy
public static final String LOAD_ROLES_BY_MEMBER_ATTRIBUTE = "LOAD_ROLES_BY_MEMBER_ATTRIBUTE";
public static final String GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE = "GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE";
public static final String LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY = "LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY";
public RoleMapperConfig(ComponentModel mapperModel) {
super(mapperModel);
}
public String getRolesDn() {
String rolesDn = mapperModel.getConfig().getFirst(ROLES_DN);
if (rolesDn == null) {
throw new ModelException("Roles DN is null! Check your configuration");
}
return rolesDn;
}
@Override
public String getLDAPGroupsDn() {
return getRolesDn();
}
public String getRoleNameLdapAttribute() {
String rolesRdnAttr = mapperModel.getConfig().getFirst(ROLE_NAME_LDAP_ATTRIBUTE);
return rolesRdnAttr!=null ? rolesRdnAttr : LDAPConstants.CN;
}
@Override
public String getLDAPGroupNameLdapAttribute() {
return getRoleNameLdapAttribute();
}
public Collection<String> getRoleObjectClasses(LDAPStorageProvider ldapProvider) {
String objectClasses = mapperModel.getConfig().getFirst(ROLE_OBJECT_CLASSES);
if (objectClasses == null) {
// For Active directory, the default is 'group' . For other servers 'groupOfNames'
objectClasses = ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES;
}
return getConfigValues(objectClasses);
}
public String getCustomLdapFilter() {
return mapperModel.getConfig().getFirst(ROLES_LDAP_FILTER);
}
public boolean isRealmRolesMapping() {
String realmRolesMapping = mapperModel.getConfig().getFirst(USE_REALM_ROLES_MAPPING);
return realmRolesMapping==null || Boolean.parseBoolean(realmRolesMapping);
}
public String getClientId() {
return mapperModel.getConfig().getFirst(CLIENT_ID);
}
public String getUserRolesRetrieveStrategy() {
String strategyString = mapperModel.getConfig().getFirst(USER_ROLES_RETRIEVE_STRATEGY);
return strategyString!=null ? strategyString : LOAD_ROLES_BY_MEMBER_ATTRIBUTE;
}
}

View file

@ -0,0 +1,289 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers.msad;
import org.jboss.logging.Logger;
import org.keycloak.component.ComponentModel;
import org.keycloak.credential.CredentialInput;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.UserModelDelegate;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.PasswordUpdated;
import javax.naming.AuthenticationException;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Mapper specific to MSAD. It's able to read the userAccountControl and pwdLastSet attributes and set actions in Keycloak based on that.
* It's also able to handle exception code from LDAP user authentication (See http://www-01.ibm.com/support/docview.wss?uid=swg21290631 )
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class MSADUserAccountControlStorageMapper extends AbstractLDAPStorageMapper implements PasswordUpdated {
private static final Logger logger = Logger.getLogger(MSADUserAccountControlStorageMapper.class);
private static final Pattern AUTH_EXCEPTION_REGEX = Pattern.compile(".*AcceptSecurityContext error, data ([0-9a-f]*), v.*");
private static final Pattern AUTH_INVALID_NEW_PASSWORD = Pattern.compile(".*error code ([0-9a-f]+) .*WILL_NOT_PERFORM.*");
public MSADUserAccountControlStorageMapper(ComponentModel mapperModel, LDAPStorageProvider ldapProvider, RealmModel realm) {
super(mapperModel, ldapProvider, realm);
ldapProvider.setUpdater(this);
}
@Override
public void beforeLDAPQuery(LDAPQuery query) {
query.addReturningLdapAttribute(LDAPConstants.PWD_LAST_SET);
query.addReturningLdapAttribute(LDAPConstants.USER_ACCOUNT_CONTROL);
// This needs to be read-only and can be set to writable just on demand
query.addReturningReadOnlyLdapAttribute(LDAPConstants.PWD_LAST_SET);
if (ldapProvider.getEditMode() != LDAPStorageProviderFactory.EditMode.WRITABLE) {
query.addReturningReadOnlyLdapAttribute(LDAPConstants.USER_ACCOUNT_CONTROL);
}
}
@Override
public void passwordUpdated(UserModel user, LDAPObject ldapUser, CredentialInput input) {
logger.debugf("Going to update userAccountControl for ldap user '%s' after successful password update", ldapUser.getDn().toString());
// Normally it's read-only
ldapUser.removeReadOnlyAttributeName(LDAPConstants.PWD_LAST_SET);
ldapUser.setSingleAttribute(LDAPConstants.PWD_LAST_SET, "-1");
UserAccountControl control = getUserAccountControl(ldapUser);
control.remove(UserAccountControl.PASSWD_NOTREQD);
control.remove(UserAccountControl.PASSWORD_EXPIRED);
if (user.isEnabled()) {
control.remove(UserAccountControl.ACCOUNTDISABLE);
}
updateUserAccountControl(ldapUser, control);
}
@Override
public UserModel proxy(LDAPObject ldapUser, UserModel delegate) {
return new MSADUserModelDelegate(delegate, ldapUser);
}
@Override
public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser) {
}
@Override
public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate) {
}
@Override
public boolean onAuthenticationFailure(LDAPObject ldapUser, UserModel user, AuthenticationException ldapException) {
String exceptionMessage = ldapException.getMessage();
Matcher m = AUTH_EXCEPTION_REGEX.matcher(exceptionMessage);
if (m.matches()) {
String errorCode = m.group(1);
return processAuthErrorCode(errorCode, user);
} else {
return false;
}
}
protected boolean processAuthErrorCode(String errorCode, UserModel user) {
logger.debugf("MSAD Error code is '%s' after failed LDAP login of user '%s'", errorCode, user.getUsername());
if (ldapProvider.getEditMode() == LDAPStorageProviderFactory.EditMode.WRITABLE) {
if (errorCode.equals("532") || errorCode.equals("773")) {
// User needs to change his MSAD password. Allow him to login, but add UPDATE_PASSWORD required action
user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
return true;
} else if (errorCode.equals("533")) {
// User is disabled in MSAD. Set him to disabled in KC as well
user.setEnabled(false);
return true;
} else if (errorCode.equals("775")) {
logger.warnf("Locked user '%s' attempt to login", user.getUsername());
}
}
return false;
}
protected ModelException processFailedPasswordUpdateException(ModelException e) {
if (e.getCause() == null || e.getCause().getMessage() == null) {
return e;
}
String exceptionMessage = e.getCause().getMessage().replace('\n', ' ');
Matcher m = AUTH_INVALID_NEW_PASSWORD.matcher(exceptionMessage);
if (m.matches()) {
String errorCode = m.group(1);
if (errorCode.equals("53")) {
ModelException me = new ModelException("invalidPasswordRegexPatternMessage", e);
me.setParameters(new Object[]{"passwordConstraintViolation"});
return me;
}
}
return e;
}
protected UserAccountControl getUserAccountControl(LDAPObject ldapUser) {
String userAccountControl = ldapUser.getAttributeAsString(LDAPConstants.USER_ACCOUNT_CONTROL);
long longValue = userAccountControl == null ? 0 : Long.parseLong(userAccountControl);
return new UserAccountControl(longValue);
}
// Update user in LDAP
protected void updateUserAccountControl(LDAPObject ldapUser, UserAccountControl accountControl) {
String userAccountControlValue = String.valueOf(accountControl.getValue());
logger.debugf("Updating userAccountControl of user '%s' to value '%s'", ldapUser.getDn().toString(), userAccountControlValue);
ldapUser.setSingleAttribute(LDAPConstants.USER_ACCOUNT_CONTROL, userAccountControlValue);
ldapProvider.getLdapIdentityStore().update(ldapUser);
}
public class MSADUserModelDelegate extends UserModelDelegate {
private final LDAPObject ldapUser;
public MSADUserModelDelegate(UserModel delegate, LDAPObject ldapUser) {
super(delegate);
this.ldapUser = ldapUser;
}
@Override
public boolean isEnabled() {
boolean kcEnabled = super.isEnabled();
if (getPwdLastSet() > 0) {
// Merge KC and MSAD
return kcEnabled && !getUserAccountControl(ldapUser).has(UserAccountControl.ACCOUNTDISABLE);
} else {
// If new MSAD user is created and pwdLastSet is still 0, MSAD account is in disabled state. So read just from Keycloak DB. User is not able to login via MSAD anyway
return kcEnabled;
}
}
@Override
public void setEnabled(boolean enabled) {
// Always update DB
super.setEnabled(enabled);
if (ldapProvider.getEditMode() == LDAPStorageProviderFactory.EditMode.WRITABLE && getPwdLastSet() > 0) {
logger.debugf("Going to propagate enabled=%s for ldapUser '%s' to MSAD", enabled, ldapUser.getDn().toString());
UserAccountControl control = getUserAccountControl(ldapUser);
if (enabled) {
control.remove(UserAccountControl.ACCOUNTDISABLE);
} else {
control.add(UserAccountControl.ACCOUNTDISABLE);
}
updateUserAccountControl(ldapUser, control);
}
}
@Override
public void addRequiredAction(RequiredAction action) {
String actionName = action.name();
addRequiredAction(actionName);
}
@Override
public void addRequiredAction(String action) {
// Always update DB
super.addRequiredAction(action);
if (ldapProvider.getEditMode() == LDAPStorageProviderFactory.EditMode.WRITABLE && RequiredAction.UPDATE_PASSWORD.toString().equals(action)) {
logger.debugf("Going to propagate required action UPDATE_PASSWORD to MSAD for ldap user '%s' ", ldapUser.getDn().toString());
// Normally it's read-only
ldapUser.removeReadOnlyAttributeName(LDAPConstants.PWD_LAST_SET);
ldapUser.setSingleAttribute(LDAPConstants.PWD_LAST_SET, "0");
ldapProvider.getLdapIdentityStore().update(ldapUser);
}
}
@Override
public void removeRequiredAction(RequiredAction action) {
String actionName = action.name();
removeRequiredAction(actionName);
}
@Override
public void removeRequiredAction(String action) {
// Always update DB
super.removeRequiredAction(action);
if (ldapProvider.getEditMode() == LDAPStorageProviderFactory.EditMode.WRITABLE && RequiredAction.UPDATE_PASSWORD.toString().equals(action)) {
// Don't set pwdLastSet in MSAD when it is new user
UserAccountControl accountControl = getUserAccountControl(ldapUser);
if (accountControl.getValue() != 0 && !accountControl.has(UserAccountControl.PASSWD_NOTREQD)) {
logger.debugf("Going to remove required action UPDATE_PASSWORD from MSAD for ldap user '%s' ", ldapUser.getDn().toString());
// Normally it's read-only
ldapUser.removeReadOnlyAttributeName(LDAPConstants.PWD_LAST_SET);
ldapUser.setSingleAttribute(LDAPConstants.PWD_LAST_SET, "-1");
ldapProvider.getLdapIdentityStore().update(ldapUser);
}
}
}
@Override
public Set<String> getRequiredActions() {
Set<String> requiredActions = super.getRequiredActions();
if (ldapProvider.getEditMode() == LDAPStorageProviderFactory.EditMode.WRITABLE) {
if (getPwdLastSet() == 0 || getUserAccountControl(ldapUser).has(UserAccountControl.PASSWORD_EXPIRED)) {
requiredActions = new HashSet<>(requiredActions);
requiredActions.add(RequiredAction.UPDATE_PASSWORD.toString());
return requiredActions;
}
}
return requiredActions;
}
protected long getPwdLastSet() {
String pwdLastSet = ldapUser.getAttributeAsString(LDAPConstants.PWD_LAST_SET);
return pwdLastSet == null ? 0 : Long.parseLong(pwdLastSet);
}
}
}

View file

@ -0,0 +1,62 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers.msad;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapperFactory;
import java.util.ArrayList;
import java.util.List;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class MSADUserAccountControlStorageMapperFactory extends AbstractLDAPStorageMapperFactory {
public static final String PROVIDER_ID = LDAPConstants.MSAD_USER_ACCOUNT_CONTROL_MAPPER;
protected static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
static {
}
@Override
public String getHelpText() {
return "Mapper specific to MSAD. It's able to integrate the MSAD user account state into Keycloak account state (account enabled, password is expired etc). It's using userAccountControl and pwdLastSet MSAD attributes for that. " +
"For example if pwdLastSet is 0, the Keycloak user is required to update password, if userAccountControl is 514 (disabled account) the Keycloak user is disabled as well etc. Mapper is also able to handle exception code from LDAP user authentication.";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
protected AbstractLDAPStorageMapper createMapper(ComponentModel mapperModel, LDAPStorageProvider federationProvider, RealmModel realm) {
return new MSADUserAccountControlStorageMapper(mapperModel, federationProvider, realm);
}
}

View file

@ -0,0 +1,75 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.mappers.msad;
/**
* See https://support.microsoft.com/en-us/kb/305144
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class UserAccountControl {
public static final long SCRIPT = 0x0001L;
public static final long ACCOUNTDISABLE = 0x0002L;
public static final long HOMEDIR_REQUIRED = 0x0008L;
public static final long LOCKOUT = 0x0010L;
public static final long PASSWD_NOTREQD = 0x0020L;
public static final long PASSWD_CANT_CHANGE = 0x0040L;
public static final long ENCRYPTED_TEXT_PWD_ALLOWED = 0x0080L;
public static final long TEMP_DUPLICATE_ACCOUNT = 0x0100L;
public static final long NORMAL_ACCOUNT = 0x0200L;
public static final long INTERDOMAIN_TRUST_ACCOUNT = 0x0800L;
public static final long WORKSTATION_TRUST_ACCOUNT = 0x1000L;
public static final long SERVER_TRUST_ACCOUNT = 0x2000L;
public static final long DONT_EXPIRE_PASSWORD = 0x10000L;
public static final long MNS_LOGON_ACCOUNT = 0x20000L;
public static final long SMARTCARD_REQUIRED = 0x40000L;
public static final long TRUSTED_FOR_DELEGATION = 0x80000L;
public static final long NOT_DELEGATED = 0x100000L;
public static final long USE_DES_KEY_ONLY = 0x200000L;
public static final long DONT_REQ_PREAUTH = 0x400000L;
public static final long PASSWORD_EXPIRED = 0x800000L;
public static final long TRUSTED_TO_AUTH_FOR_DELEGATION = 0x1000000L;
public static final long PARTIAL_SECRETS_ACCOUNT = 0x04000000L;
private long value;
public UserAccountControl(long value) {
this.value = value;
}
public boolean has(long feature) {
return (this.value & feature) > 0;
}
public void add(long feature) {
if (!has(feature)) {
this.value += feature;
}
}
public void remove(long feature) {
if (has(feature)) {
this.value -= feature;
}
}
public long getValue() {
return value;
}
}

View file

@ -0,0 +1,18 @@
#
# Copyright 2016 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
org.keycloak.storage.ldap.mappers.LDAPStorageMapperSpi

View file

@ -0,0 +1 @@
org.keycloak.storage.ldap.LDAPStorageProviderFactory

View file

@ -0,0 +1,6 @@
org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapperFactory
org.keycloak.storage.ldap.mappers.HardcodedLDAPRoleStorageMapperFactory
org.keycloak.storage.ldap.mappers.membership.group.GroupLDAPStorageMapperFactory
org.keycloak.storage.ldap.mappers.membership.role.RoleLDAPStorageMapperFactory
org.keycloak.storage.ldap.mappers.msad.MSADUserAccountControlStorageMapperFactory
org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapperFactory

View file

@ -0,0 +1,125 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.idm.model;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.storage.ldap.mappers.membership.group.GroupTreeResolver;
import java.util.Arrays;
import java.util.List;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class GroupTreeResolverTest {
@Test
public void testGroupResolvingCorrect() throws GroupTreeResolver.GroupTreeResolveException {
GroupTreeResolver.Group group1 = new GroupTreeResolver.Group("group1", "group2", "group3");
GroupTreeResolver.Group group2 = new GroupTreeResolver.Group("group2", "group4", "group5");
GroupTreeResolver.Group group3 = new GroupTreeResolver.Group("group3", "group6");
GroupTreeResolver.Group group4 = new GroupTreeResolver.Group("group4");
GroupTreeResolver.Group group5 = new GroupTreeResolver.Group("group5");
GroupTreeResolver.Group group6 = new GroupTreeResolver.Group("group6", "group7");
GroupTreeResolver.Group group7 = new GroupTreeResolver.Group("group7");
List<GroupTreeResolver.Group> groups = Arrays.asList(group1, group2, group3, group4, group5, group6, group7);
GroupTreeResolver resolver = new GroupTreeResolver();
List<GroupTreeResolver.GroupTreeEntry> groupTree = resolver.resolveGroupTree(groups);
Assert.assertEquals(1, groupTree.size());
Assert.assertEquals("{ group1 -> [ { group2 -> [ { group4 -> [ ]}{ group5 -> [ ]} ]}{ group3 -> [ { group6 -> [ { group7 -> [ ]} ]} ]} ]}", groupTree.get(0).toString());
}
@Test
public void testGroupResolvingCorrect2_multipleRootGroups() throws GroupTreeResolver.GroupTreeResolveException {
GroupTreeResolver.Group group1 = new GroupTreeResolver.Group("group1", "group8");
GroupTreeResolver.Group group2 = new GroupTreeResolver.Group("group2");
GroupTreeResolver.Group group3 = new GroupTreeResolver.Group("group3", "group2");
GroupTreeResolver.Group group4 = new GroupTreeResolver.Group("group4", "group1", "group5");
GroupTreeResolver.Group group5 = new GroupTreeResolver.Group("group5", "group6", "group7");
GroupTreeResolver.Group group6 = new GroupTreeResolver.Group("group6");
GroupTreeResolver.Group group7 = new GroupTreeResolver.Group("group7");
GroupTreeResolver.Group group8 = new GroupTreeResolver.Group("group8", "group9");
GroupTreeResolver.Group group9 = new GroupTreeResolver.Group("group9");
List<GroupTreeResolver.Group> groups = Arrays.asList(group1, group2, group3, group4, group5, group6, group7, group8, group9);
GroupTreeResolver resolver = new GroupTreeResolver();
List<GroupTreeResolver.GroupTreeEntry> groupTree = resolver.resolveGroupTree(groups);
Assert.assertEquals(2, groupTree.size());
Assert.assertEquals("{ group3 -> [ { group2 -> [ ]} ]}", groupTree.get(0).toString());
Assert.assertEquals("{ group4 -> [ { group1 -> [ { group8 -> [ { group9 -> [ ]} ]} ]}{ group5 -> [ { group6 -> [ ]}{ group7 -> [ ]} ]} ]}", groupTree.get(1).toString());
}
@Test
public void testGroupResolvingRecursion() {
GroupTreeResolver.Group group1 = new GroupTreeResolver.Group("group1", "group2", "group3");
GroupTreeResolver.Group group2 = new GroupTreeResolver.Group("group2");
GroupTreeResolver.Group group3 = new GroupTreeResolver.Group("group3", "group4");
GroupTreeResolver.Group group4 = new GroupTreeResolver.Group("group4", "group5");
GroupTreeResolver.Group group5 = new GroupTreeResolver.Group("group5", "group1");
GroupTreeResolver.Group group6 = new GroupTreeResolver.Group("group6", "group7");
GroupTreeResolver.Group group7 = new GroupTreeResolver.Group("group7");
List<GroupTreeResolver.Group> groups = Arrays.asList(group1, group2, group3, group4, group5, group6, group7);
GroupTreeResolver resolver = new GroupTreeResolver();
try {
resolver.resolveGroupTree(groups);
Assert.fail("Exception expected because of recursion");
} catch (GroupTreeResolver.GroupTreeResolveException gre) {
Assert.assertTrue(gre.getMessage().startsWith("Recursion detected"));
}
}
@Test
public void testGroupResolvingMultipleParents() {
GroupTreeResolver.Group group1 = new GroupTreeResolver.Group("group1", "group2");
GroupTreeResolver.Group group2 = new GroupTreeResolver.Group("group2");
GroupTreeResolver.Group group3 = new GroupTreeResolver.Group("group3", "group2");
GroupTreeResolver.Group group4 = new GroupTreeResolver.Group("group4", "group1", "group5");
GroupTreeResolver.Group group5 = new GroupTreeResolver.Group("group5", "group4");
List<GroupTreeResolver.Group> groups = Arrays.asList(group1, group2, group3, group4, group5);
GroupTreeResolver resolver = new GroupTreeResolver();
try {
resolver.resolveGroupTree(groups);
Assert.fail("Exception expected because of some groups have multiple parents");
} catch (GroupTreeResolver.GroupTreeResolveException gre) {
Assert.assertTrue(gre.getMessage().contains("detected to have multiple parents"));
}
}
@Test
public void testGroupResolvingMissingGroup() {
GroupTreeResolver.Group group1 = new GroupTreeResolver.Group("group1", "group2");
GroupTreeResolver.Group group2 = new GroupTreeResolver.Group("group2", "group3");
GroupTreeResolver.Group group4 = new GroupTreeResolver.Group("group4");
List<GroupTreeResolver.Group> groups = Arrays.asList(group1, group2, group4);
GroupTreeResolver resolver = new GroupTreeResolver();
try {
resolver.resolveGroupTree(groups);
Assert.fail("Exception expected because of missing referenced group");
} catch (GroupTreeResolver.GroupTreeResolveException gre) {
Assert.assertEquals("Group 'group3' referenced as member of group 'group2' doesn't exists", gre.getMessage());
}
}
}

View file

@ -0,0 +1,49 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.idm.model;
import org.junit.Assert;
import org.junit.Test;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LDAPDnTest {
@Test
public void testDn() throws Exception {
LDAPDn dn = LDAPDn.fromString("dc=keycloak, dc=org");
dn.addFirst("ou", "People");
Assert.assertEquals("ou=People,dc=keycloak,dc=org", dn.toString());
dn.addFirst("uid", "Johny,Depp+Pepp");
Assert.assertEquals("uid=Johny\\,Depp\\+Pepp,ou=People,dc=keycloak,dc=org", dn.toString());
Assert.assertEquals(LDAPDn.fromString("uid=Johny\\,Depp\\+Pepp,ou=People,dc=keycloak,dc=org"), dn);
Assert.assertEquals("ou=People,dc=keycloak,dc=org", dn.getParentDn());
Assert.assertTrue(dn.isDescendantOf(LDAPDn.fromString("dc=keycloak, dc=org")));
Assert.assertTrue(dn.isDescendantOf(LDAPDn.fromString("dc=org")));
Assert.assertTrue(dn.isDescendantOf(LDAPDn.fromString("DC=keycloak, DC=org")));
Assert.assertFalse(dn.isDescendantOf(LDAPDn.fromString("dc=keycloakk, dc=org")));
Assert.assertFalse(dn.isDescendantOf(dn));
Assert.assertEquals("uid", dn.getFirstRdnAttrName());
Assert.assertEquals("Johny\\,Depp\\+Pepp", dn.getFirstRdnAttrValue());
}
}

View file

@ -0,0 +1,116 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.idm.model;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapperFactory;
import org.keycloak.storage.ldap.mappers.LDAPMappersComparator;
import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapperFactory;
import java.util.LinkedList;
import java.util.List;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LDAPMappersComparatorTest {
@Test
public void testCompareWithCNUsername() {
MultivaluedHashMap<String, String> cfg = new MultivaluedHashMap<>();
cfg.add(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, LDAPConstants.CN);
LDAPConfig config = new LDAPConfig(cfg);
List<ComponentModel> sorted = LDAPMappersComparator.sortAsc(config, getMappers());
assertOrder(sorted, "username-cn", "sAMAccountName", "first name", "full name");
sorted = LDAPMappersComparator.sortDesc(config, getMappers());
assertOrder(sorted, "full name", "first name", "sAMAccountName", "username-cn");
}
@Test
public void testCompareWithSAMAccountNameUsername() {
MultivaluedHashMap<String, String> cfg = new MultivaluedHashMap<>();
cfg.add(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, LDAPConstants.SAM_ACCOUNT_NAME);
LDAPConfig config = new LDAPConfig(cfg);
List<ComponentModel> sorted = LDAPMappersComparator.sortAsc(config, getMappers());
assertOrder(sorted, "sAMAccountName", "username-cn", "first name", "full name");
sorted = LDAPMappersComparator.sortDesc(config, getMappers());
assertOrder(sorted, "full name", "first name", "username-cn", "sAMAccountName");
}
private void assertOrder(List<ComponentModel> result, String... names) {
Assert.assertEquals(result.size(), names.length);
for (int i=0 ; i<names.length ; i++) {
Assert.assertEquals(names[i], result.get(i).getName());
}
}
private List<ComponentModel> getMappers() {
List<ComponentModel> result = new LinkedList<>();
ComponentModel mapperModel = KeycloakModelUtils.createComponentModel("first name", "fed-provider", UserAttributeLDAPStorageMapperFactory.PROVIDER_ID, LDAPStorageMapper.class.getName(),
UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, UserModel.FIRST_NAME,
UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, LDAPConstants.GIVENNAME,
UserAttributeLDAPStorageMapper.READ_ONLY, "true",
UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, "true",
UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "true");
mapperModel.setId("idd1");
result.add(mapperModel);
mapperModel = KeycloakModelUtils.createComponentModel("username-cn", "fed-provider", UserAttributeLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, UserModel.USERNAME,
UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, LDAPConstants.CN,
UserAttributeLDAPStorageMapper.READ_ONLY, "true",
UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false",
UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "true");
mapperModel.setId("idd2");
result.add(mapperModel);
mapperModel = KeycloakModelUtils.createComponentModel("full name", "fed-provider", FullNameLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
FullNameLDAPStorageMapper.LDAP_FULL_NAME_ATTRIBUTE, LDAPConstants.CN,
UserAttributeLDAPStorageMapper.READ_ONLY, "true");
mapperModel.setId("idd3");
result.add(mapperModel);
mapperModel = KeycloakModelUtils.createComponentModel("sAMAccountName", "fed-provider", UserAttributeLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, UserModel.USERNAME,
UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, LDAPConstants.SAM_ACCOUNT_NAME,
UserAttributeLDAPStorageMapper.READ_ONLY, "false",
UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false",
UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "true");
mapperModel.setId("idd4");
result.add(mapperModel);
return result;
}
}

View file

@ -33,8 +33,9 @@
<description />
<modules>
<module>ldap</module>
<module>kerberos</module>
<module>ldap</module>
<module>ldap2</module>
<module>sssd</module>
</modules>

View file

@ -48,6 +48,7 @@ import org.keycloak.models.jpa.entities.UserConsentRoleEntity;
import org.keycloak.models.jpa.entities.UserEntity;
import org.keycloak.models.utils.DefaultRoles;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.storage.UserStorageProvider;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
@ -395,33 +396,38 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
@Override
public void preRemove(RealmModel realm, UserFederationProviderModel link) {
String linkId = link.getId();
removeUserDataByLink(realm, linkId);
}
public void removeUserDataByLink(RealmModel realm, String linkId) {
int num = em.createNamedQuery("deleteUserRoleMappingsByRealmAndLink")
.setParameter("realmId", realm.getId())
.setParameter("link", link.getId())
.setParameter("link", linkId)
.executeUpdate();
num = em.createNamedQuery("deleteUserRequiredActionsByRealmAndLink")
.setParameter("realmId", realm.getId())
.setParameter("link", link.getId())
.setParameter("link", linkId)
.executeUpdate();
num = em.createNamedQuery("deleteFederatedIdentityByRealmAndLink")
.setParameter("realmId", realm.getId())
.setParameter("link", link.getId())
.setParameter("link", linkId)
.executeUpdate();
num = em.createNamedQuery("deleteCredentialAttributeByRealmAndLink")
.setParameter("realmId", realm.getId())
.setParameter("link", link.getId())
.setParameter("link", linkId)
.executeUpdate();
num = em.createNamedQuery("deleteCredentialsByRealmAndLink")
.setParameter("realmId", realm.getId())
.setParameter("link", link.getId())
.setParameter("link", linkId)
.executeUpdate();
num = em.createNamedQuery("deleteUserAttributesByRealmAndLink")
.setParameter("realmId", realm.getId())
.setParameter("link", link.getId())
.setParameter("link", linkId)
.executeUpdate();
num = em.createNamedQuery("deleteUsersByRealmAndLink")
.setParameter("realmId", realm.getId())
.setParameter("link", link.getId())
.setParameter("link", linkId)
.executeUpdate();
}
@ -718,6 +724,8 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
@Override
public void preRemove(RealmModel realm, ComponentModel component) {
if (!component.getProviderType().equals(UserStorageProvider.class.getName())) return;
removeUserDataByLink(realm, component.getId());
}

View file

@ -2036,7 +2036,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
throw new IllegalArgumentException("Invalid component type");
}
componentFactory.validateConfiguration(session, model);
componentFactory.validateConfiguration(session, this, model);
ComponentEntity c = new ComponentEntity();
if (model.getId() == null) {
@ -2046,6 +2046,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
}
c.setName(model.getName());
c.setParentId(model.getParentId());
if (model.getParentId() == null) c.setParentId(this.getId());
c.setProviderType(model.getProviderType());
c.setProviderId(model.getProviderId());
c.setSubType(model.getSubType());
@ -2077,7 +2078,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
@Override
public void updateComponent(ComponentModel component) {
ComponentUtil.getComponentFactory(session, component).validateConfiguration(session, component);
ComponentUtil.getComponentFactory(session, component).validateConfiguration(session, this, component);
ComponentEntity c = em.find(ComponentEntity.class, component.getId());
if (c == null) return;
@ -2098,6 +2099,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
ComponentEntity c = em.find(ComponentEntity.class, component.getId());
if (c == null) return;
session.users().preRemove(this, component);
removeComponents(component.getId());
em.createNamedQuery("deleteComponentConfigByComponent").setParameter("component", c).executeUpdate();
em.remove(c);
}

View file

@ -1954,7 +1954,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
@Override
public ComponentModel addComponentModel(ComponentModel model) {
ComponentUtil.getComponentFactory(session, model).validateConfiguration(session, model);
ComponentUtil.getComponentFactory(session, model).validateConfiguration(session, this, model);
ComponentEntity entity = new ComponentEntity();
if (model.getId() == null) {
@ -1964,6 +1964,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
}
updateComponentEntity(entity, model);
model.setId(entity.getId());
if (model.getParentId() == null) entity.setParentId(this.getId());
realm.getComponentEntities().add(entity);
updateRealm();
ComponentUtil.notifyCreated(session, this, model);
@ -1972,7 +1973,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
@Override
public void updateComponent(ComponentModel model) {
ComponentUtil.getComponentFactory(session, model).validateConfiguration(session, model);
ComponentUtil.getComponentFactory(session, model).validateConfiguration(session, this, model);
for (ComponentEntity entity : realm.getComponentEntities()) {
if (entity.getId().equals(model.getId())) {
@ -1999,6 +2000,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
while(it.hasNext()) {
if (it.next().getId().equals(component.getId())) {
session.users().preRemove(this, component);
removeComponents(component.getId());
it.remove();
break;
}

View file

@ -705,6 +705,11 @@
<artifactId>keycloak-ldap-federation</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-ldap-storage</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-dependencies-server-min</artifactId>

View file

@ -38,7 +38,7 @@ public interface ComponentFactory<CreatedType, ProviderType extends Provider> ex
return null;
}
void validateConfiguration(KeycloakSession session, ComponentModel model) throws ComponentValidationException;
void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException;
default
void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) {

View file

@ -48,7 +48,7 @@ public class ComponentModel implements Serializable {
this.providerType = copy.providerType;
this.parentId = copy.parentId;
this.subType = copy.subType;
this.config = copy.config;
this.config.addAll(copy.config);
}

View file

@ -0,0 +1,40 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.component;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ConfiguredProvider;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderFactory;
import java.util.Collections;
import java.util.List;
/**
* Useful when you want to describe config properties that are effected by the parent ComponentModel
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface SubComponentFactory<CreatedType, ProviderType extends Provider> extends ComponentFactory<CreatedType, ProviderType> {
default
List<ProviderConfigProperty> getConfigProperties(RealmModel realm, ComponentModel parent) {
return getConfigProperties();
}
}

View file

@ -372,6 +372,29 @@ public final class KeycloakModelUtils {
}
return null;
}
public static ComponentModel createComponentModel(String name, String parentId, String providerId, String providerType, String... config) {
ComponentModel mapperModel = new ComponentModel();
mapperModel.setParentId(parentId);
mapperModel.setName(name);
mapperModel.setProviderId(providerId);
mapperModel.setProviderType(providerType);
String key = null;
for (String configEntry : config) {
if (key == null) {
key = configEntry;
} else {
mapperModel.getConfig().add(key, configEntry);
key = null;
}
}
if (key != null) {
throw new IllegalStateException("Invalid count of arguments for config. Maybe mistake?");
}
return mapperModel;
}
public static UserFederationMapperModel createUserFederationMapperModel(String name, String federationProviderId, String mapperType, String... config) {

View file

@ -102,6 +102,12 @@ public class ProviderConfigurationBuilder {
return this;
}
public ProviderConfigPropertyBuilder options(List<String> options) {
this.options = options;
return this;
}
public ProviderConfigPropertyBuilder secret(boolean secret) {
this.secret = secret;
return this;

View file

@ -81,7 +81,7 @@ public interface UserStorageProviderFactory<T extends UserStorageProvider> exten
}
@Override
default void validateConfiguration(KeycloakSession session, ComponentModel config) throws ComponentValidationException {
default void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
}

View file

@ -20,6 +20,7 @@ package org.keycloak.keys;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ConfigurationValidationHelper;
import org.keycloak.provider.ProviderConfigurationBuilder;
@ -36,7 +37,7 @@ public abstract class AbstractRsaKeyProviderFactory implements KeyProviderFactor
}
@Override
public void validateConfiguration(KeycloakSession session, ComponentModel model) throws ComponentValidationException {
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
ConfigurationValidationHelper.check(model)
.checkLong(Attributes.PRIORITY_PROPERTY, false)
.checkBoolean(Attributes.ENABLED_PROPERTY, false)

View file

@ -57,8 +57,8 @@ public class GeneratedRsaKeyProviderFactory extends AbstractRsaKeyProviderFactor
}
@Override
public void validateConfiguration(KeycloakSession session, ComponentModel model) throws ComponentValidationException {
super.validateConfiguration(session, model);
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
super.validateConfiguration(session, realm, model);
ConfigurationValidationHelper.check(model)
.checkInt(Attributes.KEY_SIZE_PROPERTY, false);
@ -75,7 +75,6 @@ public class GeneratedRsaKeyProviderFactory extends AbstractRsaKeyProviderFactor
}
if (!(model.contains(Attributes.PRIVATE_KEY_KEY) && model.contains(Attributes.CERTIFICATE_KEY))) {
RealmModel realm = session.realms().getRealm(model.getParentId());
generateKeys(realm, model, size);
logger.debugv("Generated keys for {0}", realm.getName());
@ -83,7 +82,6 @@ public class GeneratedRsaKeyProviderFactory extends AbstractRsaKeyProviderFactor
PrivateKey privateKey = PemUtils.decodePrivateKey(model.get(Attributes.PRIVATE_KEY_KEY));
int currentSize = ((RSAPrivateKey) privateKey).getModulus().bitLength();
if (currentSize != size) {
RealmModel realm = session.realms().getRealm(model.getParentId());
generateKeys(realm, model, size);
logger.debugv("Key size changed, generating new keys for {0}", realm.getName());

View file

@ -22,6 +22,7 @@ import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ConfigurationValidationHelper;
import org.keycloak.provider.ProviderConfigProperty;
@ -63,8 +64,8 @@ public class JavaKeystoreKeyProviderFactory extends AbstractRsaKeyProviderFactor
}
@Override
public void validateConfiguration(KeycloakSession session, ComponentModel model) throws ComponentValidationException {
super.validateConfiguration(session, model);
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
super.validateConfiguration(session, realm, model);
ConfigurationValidationHelper.check(model)
.checkSingle(KEYSTORE_PROPERTY, true)

View file

@ -56,8 +56,8 @@ public class RsaKeyProviderFactory extends AbstractRsaKeyProviderFactory {
}
@Override
public void validateConfiguration(KeycloakSession session, ComponentModel model) throws ComponentValidationException {
super.validateConfiguration(session, model);
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
super.validateConfiguration(session, realm, model);
ConfigurationValidationHelper.check(model)
.checkSingle(Attributes.PRIVATE_KEY_PROPERTY, true)
@ -89,7 +89,6 @@ public class RsaKeyProviderFactory extends AbstractRsaKeyProviderFactory {
}
} else {
try {
RealmModel realm = session.realms().getRealm(model.getParentId());
Certificate certificate = CertificateUtils.generateV1SelfSignedCertificate(keyPair, realm.getName());
model.put(Attributes.CERTIFICATE_KEY, PemUtils.encodeCertificate(certificate));
} catch (Throwable t) {

View file

@ -24,6 +24,7 @@ import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ProviderConfigProperty;
/**
@ -47,7 +48,7 @@ public abstract class AbstractClientRegistrationPolicyFactory implements ClientR
}
@Override
public void validateConfiguration(KeycloakSession session, ComponentModel config) throws ComponentValidationException {
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
}
@Override

View file

@ -23,6 +23,7 @@ import java.util.List;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ConfigurationValidationHelper;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.services.clientregistration.policy.AbstractClientRegistrationPolicyFactory;
@ -72,7 +73,7 @@ public class MaxClientsClientRegistrationPolicyFactory extends AbstractClientReg
}
@Override
public void validateConfiguration(KeycloakSession session, ComponentModel config) throws ComponentValidationException {
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
ConfigurationValidationHelper.check(config)
.checkInt(MAX_CLIENTS_PROPERTY, true);
}

View file

@ -23,6 +23,7 @@ import java.util.List;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ConfigurationValidationHelper;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.services.clientregistration.policy.AbstractClientRegistrationPolicyFactory;
@ -70,7 +71,7 @@ public class TrustedHostClientRegistrationPolicyFactory extends AbstractClientRe
}
@Override
public void validateConfiguration(KeycloakSession session, ComponentModel config) throws ComponentValidationException {
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
ConfigurationValidationHelper.check(config)
.checkBoolean(HOST_SENDING_REGISTRATION_REQUEST_MUST_MATCH_PROPERTY, true)
.checkBoolean(CLIENT_URIS_MUST_MATCH_PROPERTY, true);

View file

@ -22,13 +22,19 @@ import org.jboss.resteasy.spi.NotFoundException;
import org.keycloak.common.ClientConnection;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.component.SubComponentFactory;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.models.utils.StripSecretsUtils;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.ComponentTypeRepresentation;
import org.keycloak.representations.idm.ConfigPropertyRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.ErrorResponseException;
@ -197,4 +203,32 @@ public class ComponentResource {
return ErrorResponse.error(message, Response.Status.BAD_REQUEST);
}
@GET
@Path("{id}/sub-component-config")
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public List<ConfigPropertyRepresentation> getSubcomponentConfig(@PathParam("id") String id, @QueryParam("type") String providerType, @QueryParam("id") String providerId) {
auth.requireView();
ComponentModel parent = realm.getComponent(id);
if (parent == null) {
throw new NotFoundException("Could not find component");
}
Class<? extends Provider> providerClass = null;
try {
providerClass = (Class<? extends Provider>)Class.forName(providerType);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
ProviderFactory factory = session.getKeycloakSessionFactory().getProviderFactory(providerClass, providerId);
if (factory == null) {
throw new NotFoundException("Could not find subcomponent factory");
}
if (!(factory instanceof SubComponentFactory)) return Collections.EMPTY_LIST;
List<ProviderConfigProperty> props = ((SubComponentFactory)factory).getConfigProperties(realm, parent);
return ModelToRepresentation.toRepresentation(props);
}
}

View file

@ -22,6 +22,7 @@ import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ConfigurationValidationHelper;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
@ -50,7 +51,7 @@ public class TestImplProviderFactory implements TestProviderFactory {
}
@Override
public void validateConfiguration(KeycloakSession session, ComponentModel model) throws ComponentValidationException {
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
ConfigurationValidationHelper.check(model)
.checkRequired("required", "Required")
.checkInt("number", "Number", false);

View file

@ -126,6 +126,10 @@
<groupId>org.keycloak</groupId>
<artifactId>keycloak-ldap-federation</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-ldap-storage</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-kerberos-federation</artifactId>

View file

@ -0,0 +1,63 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.federation.storage.ldap;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.representations.IDToken;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LDAPExampleServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
KeycloakSecurityContext securityContext = (KeycloakSecurityContext) req.getAttribute(KeycloakSecurityContext.class.getName());
IDToken idToken = securityContext.getIdToken();
PrintWriter out = resp.getWriter();
out.println("<html><head><title>LDAP Portal</title></head><body>");
out.println("<table border><tr><th>Attribute name</th><th>Attribute values</th></tr>");
out.printf("<tr><td>%s</td><td>%s</td></tr>", "preferred_username", idToken.getPreferredUsername());
out.println();
out.printf("<tr><td>%s</td><td>%s</td></tr>", "name", idToken.getName());
out.println();
out.printf("<tr><td>%s</td><td>%s</td></tr>", "email", idToken.getEmail());
out.println();
for (Map.Entry<String, Object> claim : idToken.getOtherClaims().entrySet()) {
String value = claim.getValue().toString();
out.printf("<tr><td>%s</td><td>%s</td></tr>", claim.getKey(), value);
out.println();
}
out.println("</table></body></html>");
out.flush();
}
}

View file

@ -0,0 +1,868 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.federation.storage.ldap;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.FixMethodOrder;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;
import org.junit.runners.MethodSorters;
import org.keycloak.OAuth2Constants;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.ComponentModel;
import org.keycloak.credential.CredentialModel;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderModel;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
import org.keycloak.models.ModelReadOnlyException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.AccessToken;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapperFactory;
import org.keycloak.storage.ldap.mappers.HardcodedLDAPRoleStorageMapper;
import org.keycloak.storage.ldap.mappers.HardcodedLDAPRoleStorageMapperFactory;
import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapper;
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;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class LDAPProvidersIntegrationTest {
private static LDAPRule ldapRule = new LDAPRule();
private static ComponentModel ldapModel = null;
private static MultivaluedHashMap<String, String> getLdapRuleConfig() {
MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
Map<String,String> ldapConfig = ldapRule.getConfig();
for (Map.Entry<String, String> entry : ldapConfig.entrySet()) {
config.add(entry.getKey(), entry.getValue());
}
return config;
}
private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
LDAPTestUtils.addLocalUser(manager.getSession(), appRealm, "marykeycloak", "mary@test.com", "password-app");
MultivaluedHashMap<String,String> ldapConfig = getLdapRuleConfig();
ldapConfig.putSingle(LDAPConstants.SYNC_REGISTRATIONS, "true");
ldapConfig.putSingle(LDAPConstants.EDIT_MODE, LDAPStorageProviderFactory.EditMode.WRITABLE.toString());
UserStorageProviderModel model = new UserStorageProviderModel();
model.setLastSync(0);
model.setChangedSyncPeriod(-1);
model.setFullSyncPeriod(-1);
model.setName("test-ldap");
model.setPriority(0);
model.setProviderId(LDAPStorageProviderFactory.PROVIDER_NAME);
model.setConfig(ldapConfig);
ldapModel = appRealm.addComponentModel(model);
LDAPTestUtils.addZipCodeLDAPMapper(appRealm, ldapModel);
// Delete all LDAP users and add some new for testing
LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
LDAPTestUtils.removeAllLDAPUsers(ldapFedProvider, appRealm);
LDAPObject john = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234");
LDAPTestUtils.updateLDAPPassword(ldapFedProvider, john, "Password1");
LDAPObject existing = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "existing", "Existing", "Foo", "existing@email.org", null, "5678");
appRealm.getClientByClientId("test-app").setDirectAccessGrantsEnabled(true);
}
});
@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
// @Ignore
// public void runit() throws Exception {
// Thread.sleep(10000000);
//
// }
@Test
public void caseInSensitiveImport() {
KeycloakSession session = keycloakRule.startSession();
try {
RealmManager manager = new RealmManager(session);
RealmModel appRealm = manager.getRealm("test");
LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
LDAPObject jbrown2 = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "JBrown2", "John", "Brown2", "jbrown2@email.org", null, "1234");
LDAPTestUtils.updateLDAPPassword(ldapFedProvider, jbrown2, "Password1");
LDAPObject jbrown3 = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "jbrown3", "John", "Brown3", "JBrown3@email.org", null, "1234");
LDAPTestUtils.updateLDAPPassword(ldapFedProvider, jbrown3, "Password1");
} finally {
keycloakRule.stopSession(session, true);
}
loginSuccessAndLogout("jbrown2", "Password1");
loginSuccessAndLogout("JBrown2", "Password1");
loginSuccessAndLogout("jbrown2@email.org", "Password1");
loginSuccessAndLogout("JBrown2@email.org", "Password1");
loginSuccessAndLogout("jbrown3", "Password1");
loginSuccessAndLogout("JBrown3", "Password1");
loginSuccessAndLogout("jbrown3@email.org", "Password1");
loginSuccessAndLogout("JBrown3@email.org", "Password1");
}
private void loginSuccessAndLogout(String username, String password) {
loginPage.open();
loginPage.login(username, password);
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
oauth.openLogout();
}
@Test
public void caseInsensitiveSearch() {
KeycloakSession session = keycloakRule.startSession();
try {
RealmManager manager = new RealmManager(session);
RealmModel appRealm = manager.getRealm("test");
LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
LDAPObject jbrown4 = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "JBrown4", "John", "Brown4", "jbrown4@email.org", null, "1234");
LDAPTestUtils.updateLDAPPassword(ldapFedProvider, jbrown4, "Password1");
LDAPObject jbrown5 = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "jbrown5", "John", "Brown5", "JBrown5@Email.org", null, "1234");
LDAPTestUtils.updateLDAPPassword(ldapFedProvider, jbrown5, "Password1");
} finally {
keycloakRule.stopSession(session, true);
}
session = keycloakRule.startSession();
try {
RealmManager manager = new RealmManager(session);
RealmModel appRealm = manager.getRealm("test");
// search by username
List<UserModel> users = session.users().searchForUser("JBROwn4", appRealm);
Assert.assertEquals(1, users.size());
UserModel user4 = users.get(0);
Assert.assertEquals("jbrown4", user4.getUsername());
Assert.assertEquals("jbrown4@email.org", user4.getEmail());
// search by email
users = session.users().searchForUser("JBROwn5@eMAil.org", appRealm);
Assert.assertEquals(1, users.size());
UserModel user5 = users.get(0);
Assert.assertEquals("jbrown5", user5.getUsername());
Assert.assertEquals("jbrown5@email.org", user5.getEmail());
} finally {
keycloakRule.stopSession(session, true);
}
}
@Test
public void deleteFederationLink() {
loginLdap();
{
KeycloakSession session = keycloakRule.startSession();
try {
RealmManager manager = new RealmManager(session);
RealmModel appRealm = manager.getRealm("test");
appRealm.removeComponent(ldapModel);
Assert.assertEquals(0, appRealm.getComponents(appRealm.getId(), UserStorageProvider.class.getName()).size());
} finally {
keycloakRule.stopSession(session, true);
}
}
loginPage.open();
loginPage.login("johnkeycloak", "Password1");
loginPage.assertCurrent();
Assert.assertEquals("Invalid username or password.", loginPage.getError());
{
KeycloakSession session = keycloakRule.startSession();
try {
RealmManager manager = new RealmManager(session);
RealmModel appRealm = manager.getRealm("test");
ldapModel.setId(null);
ldapModel = appRealm.addComponentModel(ldapModel);
LDAPTestUtils.addZipCodeLDAPMapper(appRealm, ldapModel);
} finally {
keycloakRule.stopSession(session, true);
}
}
loginLdap();
}
@Test
public void loginClassic() {
loginPage.open();
loginPage.login("marykeycloak", "password-app");
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
}
@Test
public void loginLdap() {
loginPage.open();
loginPage.login("johnkeycloak", "Password1");
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
profilePage.open();
Assert.assertEquals("John", profilePage.getFirstName());
Assert.assertEquals("Doe", profilePage.getLastName());
Assert.assertEquals("john@email.org", profilePage.getEmail());
}
@Test
public void loginLdapWithDirectGrant() throws Exception {
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "johnkeycloak", "Password1");
assertEquals(200, response.getStatusCode());
AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
response = oauth.doGrantAccessTokenRequest("password", "johnkeycloak", "");
assertEquals(401, response.getStatusCode());
}
@Test
public void loginLdapWithEmail() {
loginPage.open();
loginPage.login("john@email.org", "Password1");
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
}
@Test
public void loginLdapWithoutPassword() {
loginPage.open();
loginPage.login("john@email.org", "");
Assert.assertEquals("Invalid username or password.", loginPage.getError());
}
@Test
public void passwordChangeLdap() throws Exception {
changePasswordPage.open();
loginPage.login("johnkeycloak", "Password1");
changePasswordPage.changePassword("Password1", "New-password1", "New-password1");
Assert.assertEquals("Your password has been updated.", profilePage.getSuccess());
changePasswordPage.logout();
loginPage.open();
loginPage.login("johnkeycloak", "Bad-password1");
Assert.assertEquals("Invalid username or password.", loginPage.getError());
loginPage.open();
loginPage.login("johnkeycloak", "New-password1");
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
// Change password back to previous value
changePasswordPage.open();
changePasswordPage.changePassword("New-password1", "Password1", "Password1");
Assert.assertEquals("Your password has been updated.", profilePage.getSuccess());
}
@Test
public void registerExistingLdapUser() {
loginPage.open();
loginPage.clickRegister();
registerPage.assertCurrent();
// check existing username
registerPage.register("firstName", "lastName", "email@mail.cz", "existing", "Password1", "Password1");
registerPage.assertCurrent();
Assert.assertEquals("Username already exists.", registerPage.getError());
// Check existing email
registerPage.register("firstName", "lastName", "existing@email.org", "nonExisting", "Password1", "Password1");
registerPage.assertCurrent();
Assert.assertEquals("Email already exists.", registerPage.getError());
}
@Test
public void registerUserLdapSuccess() {
loginPage.open();
loginPage.clickRegister();
registerPage.assertCurrent();
registerPage.register("firstName", "lastName", "email2@check.cz", "registerUserSuccess2", "Password1", "Password1");
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
KeycloakSession session = keycloakRule.startSession();
try {
RealmModel appRealm = session.realms().getRealmByName("test");
UserModel user = session.users().getUserByUsername("registerUserSuccess2", appRealm);
Assert.assertNotNull(user);
Assert.assertNotNull(user.getFederationLink());
Assert.assertEquals(user.getFederationLink(), ldapModel.getId());
} finally {
keycloakRule.stopSession(session, false);
}
}
@Test
public void testCaseSensitiveAttributeName() {
KeycloakSession session = keycloakRule.startSession();
try {
RealmModel appRealm = new RealmManager(session).getRealmByName("test");
LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
LDAPObject johnZip = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnzip", "John", "Zip", "johnzip@email.org", null, "12398");
// Remove default zipcode mapper and add the mapper for "POstalCode" to test case sensitivity
ComponentModel currentZipMapper = LDAPTestUtils.getComponentByName(appRealm, ldapModel, "zipCodeMapper");
appRealm.removeComponent(currentZipMapper);
LDAPTestUtils.addUserAttributeMapper(appRealm, ldapModel, "zipCodeMapper-cs", "postal_code", "POstalCode");
// Fetch user from LDAP and check that postalCode is filled
UserModel user = session.users().getUserByUsername("johnzip", appRealm);
String postalCode = user.getFirstAttribute("postal_code");
Assert.assertEquals("12398", postalCode);
} finally {
keycloakRule.stopSession(session, false);
}
}
@Test
public void testCommaInUsername() {
KeycloakSession session = keycloakRule.startSession();
boolean skip = false;
try {
RealmModel appRealm = new RealmManager(session).getRealmByName("test");
LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
// Workaround as comma is not allowed in sAMAccountName on active directory. So we will skip the test for this configuration
LDAPConfig config = ldapFedProvider.getLdapIdentityStore().getConfig();
if (config.isActiveDirectory() && config.getUsernameLdapAttribute().equals(LDAPConstants.SAM_ACCOUNT_NAME)) {
skip = true;
}
if (!skip) {
LDAPObject johnComma = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "john,comma", "John", "Comma", "johncomma@email.org", null, "12387");
LDAPTestUtils.updateLDAPPassword(ldapFedProvider, johnComma, "Password1");
LDAPObject johnPlus = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "john+plus,comma", "John", "Plus", "johnplus@email.org", null, "12387");
LDAPTestUtils.updateLDAPPassword(ldapFedProvider, johnPlus, "Password1");
}
} finally {
keycloakRule.stopSession(session, false);
}
if (!skip) {
// Try to import the user with comma in username into Keycloak
loginSuccessAndLogout("john,comma", "Password1");
loginSuccessAndLogout("john+plus,comma", "Password1");
}
}
@Test
public void testDirectLDAPUpdate() {
KeycloakSession session = keycloakRule.startSession();
try {
RealmModel appRealm = new RealmManager(session).getRealmByName("test");
LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
LDAPObject johnDirect = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johndirect", "John", "Direct", "johndirect@email.org", null, "12399");
// Fetch user from LDAP and check that postalCode is filled
UserModel user = session.users().getUserByUsername("johndirect", appRealm);
String postalCode = user.getFirstAttribute("postal_code");
Assert.assertEquals("12399", postalCode);
// Directly update user in LDAP
johnDirect.setSingleAttribute(LDAPConstants.POSTAL_CODE, "12400");
johnDirect.setSingleAttribute(LDAPConstants.SN, "DirectLDAPUpdated");
ldapFedProvider.getLdapIdentityStore().update(johnDirect);
} finally {
keycloakRule.stopSession(session, true);
}
session = keycloakRule.startSession();
try {
RealmModel appRealm = new RealmManager(session).getRealmByName("test");
UserModel user = session.users().getUserByUsername("johndirect", appRealm);
// Verify that postalCode is still the same as we read it's value from Keycloak DB
user = session.users().getUserByUsername("johndirect", appRealm);
String postalCode = user.getFirstAttribute("postal_code");
Assert.assertEquals("12399", postalCode);
// Check user.getAttributes()
postalCode = user.getAttributes().get("postal_code").get(0);
Assert.assertEquals("12399", postalCode);
// LastName is new as lastName mapper will read the value from LDAP
String lastName = user.getLastName();
Assert.assertEquals("DirectLDAPUpdated", lastName);
} finally {
keycloakRule.stopSession(session, true);
}
session = keycloakRule.startSession();
try {
RealmModel appRealm = new RealmManager(session).getRealmByName("test");
// Update postalCode mapper to always read the value from LDAP
ComponentModel zipMapper = LDAPTestUtils.getComponentByName(appRealm, ldapModel, "zipCodeMapper");
zipMapper.getConfig().putSingle(UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, "true");
appRealm.updateComponent(zipMapper);
// Update lastName mapper to read the value from Keycloak DB
ComponentModel lastNameMapper = LDAPTestUtils.getComponentByName(appRealm, ldapModel, "last name");
lastNameMapper.getConfig().putSingle(UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false");
appRealm.updateComponent(lastNameMapper);
// Verify that postalCode is read from LDAP now
UserModel user = session.users().getUserByUsername("johndirect", appRealm);
String postalCode = user.getFirstAttribute("postal_code");
Assert.assertEquals("12400", postalCode);
// Check user.getAttributes()
postalCode = user.getAttributes().get("postal_code").get(0);
Assert.assertEquals("12400", postalCode);
Assert.assertFalse(user.getAttributes().containsKey(UserModel.LAST_NAME));
// lastName is read from Keycloak DB now
String lastName = user.getLastName();
Assert.assertEquals("Direct", lastName);
} finally {
keycloakRule.stopSession(session, false);
}
}
// TODO: Rather separate test for fullNameMapper to better test all the possibilities
@Test
public void testFullNameMapper() {
KeycloakSession session = keycloakRule.startSession();
ComponentModel firstNameMapper = null;
try {
RealmModel appRealm = new RealmManager(session).getRealmByName("test");
// assert that user "fullnameUser" is not in local DB
Assert.assertNull(session.users().getUserByUsername("fullname", appRealm));
// Add the user with some fullName into LDAP directly. Ensure that fullName is saved into "cn" attribute in LDAP (currently mapped to model firstName)
LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "fullname", "James Dee", "Dee", "fullname@email.org", null, "4578");
// add fullname mapper to the provider and remove "firstNameMapper". For this test, we will simply map full name to the LDAP attribute, which was before firstName ( "givenName" on active directory, "cn" on other LDAP servers)
firstNameMapper = LDAPTestUtils.getComponentByName(appRealm, ldapModel, "first name");
String ldapFirstNameAttributeName = firstNameMapper.getConfig().getFirst(UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE);
appRealm.removeComponent(firstNameMapper);
ComponentModel fullNameMapperModel = KeycloakModelUtils.createComponentModel("full name", ldapModel.getId(), FullNameLDAPStorageMapperFactory.PROVIDER_ID, LDAPStorageMapper.class.getName(),
FullNameLDAPStorageMapper.LDAP_FULL_NAME_ATTRIBUTE, ldapFirstNameAttributeName,
FullNameLDAPStorageMapper.READ_ONLY, "false");
appRealm.addComponentModel(fullNameMapperModel);
} finally {
keycloakRule.stopSession(session, true);
}
session = keycloakRule.startSession();
try {
RealmModel appRealm = new RealmManager(session).getRealmByName("test");
// Assert user is successfully imported in Keycloak DB now with correct firstName and lastName
LDAPTestUtils.assertUserImported(session.users(), appRealm, "fullname", "James", "Dee", "fullname@email.org", "4578");
// change mapper to writeOnly
ComponentModel fullNameMapperModel = LDAPTestUtils.getComponentByName(appRealm, ldapModel, "full name");
fullNameMapperModel.getConfig().putSingle(FullNameLDAPStorageMapper.WRITE_ONLY, "true");
appRealm.updateComponent(fullNameMapperModel);
} finally {
keycloakRule.stopSession(session, true);
}
// Assert changing user in Keycloak will change him in LDAP too...
session = keycloakRule.startSession();
try {
RealmModel appRealm = new RealmManager(session).getRealmByName("test");
UserModel fullnameUser = session.users().getUserByUsername("fullname", appRealm);
fullnameUser.setFirstName("James2");
fullnameUser.setLastName("Dee2");
} finally {
keycloakRule.stopSession(session, true);
}
// Assert changed user available in Keycloak
session = keycloakRule.startSession();
try {
RealmModel appRealm = new RealmManager(session).getRealmByName("test");
// Assert user is successfully imported in Keycloak DB now with correct firstName and lastName
LDAPTestUtils.assertUserImported(session.users(), appRealm, "fullname", "James2", "Dee2", "fullname@email.org", "4578");
// Remove "fullnameUser" to assert he is removed from LDAP. Revert mappers to previous state
UserModel fullnameUser = session.users().getUserByUsername("fullname", appRealm);
session.users().removeUser(appRealm, fullnameUser);
// Revert mappers
ComponentModel fullNameMapperModel = LDAPTestUtils.getComponentByName(appRealm, ldapModel, "full name");
appRealm.removeComponent(fullNameMapperModel);
firstNameMapper.setId(null);
appRealm.addComponentModel(firstNameMapper);
} finally {
keycloakRule.stopSession(session, true);
}
}
@Test
public void testHardcodedRoleMapper() {
KeycloakSession session = keycloakRule.startSession();
ComponentModel firstNameMapper = null;
try {
RealmModel appRealm = new RealmManager(session).getRealmByName("test");
RoleModel hardcodedRole = appRealm.addRole("hardcoded-role");
// assert that user "johnkeycloak" doesn't have hardcoded role
UserModel john = session.users().getUserByUsername("johnkeycloak", appRealm);
Assert.assertFalse(john.hasRole(hardcodedRole));
ComponentModel hardcodedMapperModel = KeycloakModelUtils.createComponentModel("hardcoded role", ldapModel.getId(), HardcodedLDAPRoleStorageMapperFactory.PROVIDER_ID, LDAPStorageMapper.class.getName(),
HardcodedLDAPRoleStorageMapper.ROLE, "hardcoded-role");
appRealm.addComponentModel(hardcodedMapperModel);
} finally {
keycloakRule.stopSession(session, true);
}
session = keycloakRule.startSession();
try {
RealmModel appRealm = new RealmManager(session).getRealmByName("test");
RoleModel hardcodedRole = appRealm.getRole("hardcoded-role");
// Assert user is successfully imported in Keycloak DB now with correct firstName and lastName
UserModel john = session.users().getUserByUsername("johnkeycloak", appRealm);
Assert.assertTrue(john.hasRole(hardcodedRole));
// Can't remove user from hardcoded role
try {
john.deleteRoleMapping(hardcodedRole);
Assert.fail("Didn't expected to remove role mapping");
} catch (ModelException expected) {
}
// Revert mappers
ComponentModel hardcodedMapperModel = LDAPTestUtils.getComponentByName(appRealm, ldapModel, "hardcoded role");
appRealm.removeComponent(hardcodedMapperModel);
} finally {
keycloakRule.stopSession(session, true);
}
}
@Test
public void testImportExistingUserFromLDAP() throws Exception {
// Add LDAP user with same email like existing model user
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "marykeycloak", "Mary1", "Kelly1", "mary1@email.org", null, "123");
LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "mary-duplicatemail", "Mary2", "Kelly2", "mary@test.com", null, "123");
LDAPObject marynoemail = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "marynoemail", "Mary1", "Kelly1", null, null, "123");
LDAPTestUtils.updateLDAPPassword(ldapFedProvider, marynoemail, "Password1");
}
});
// Try to import the duplicated LDAP user into Keycloak
loginPage.open();
loginPage.login("mary-duplicatemail", "password");
Assert.assertEquals("Email already exists.", loginPage.getError());
loginPage.login("mary1@email.org", "password");
Assert.assertEquals("Username already exists.", loginPage.getError());
loginSuccessAndLogout("marynoemail", "Password1");
}
@Test
public void testReadonly() {
KeycloakSession session = keycloakRule.startSession();
try {
RealmModel appRealm = session.realms().getRealmByName("test");
UserStorageProviderModel model = new UserStorageProviderModel(ldapModel);
model.getConfig().putSingle(LDAPConstants.EDIT_MODE, LDAPStorageProviderFactory.EditMode.READ_ONLY.toString());
appRealm.updateComponent(model);
UserModel user = session.users().getUserByUsername("johnkeycloak", appRealm);
Assert.assertNotNull(user);
Assert.assertNotNull(user.getFederationLink());
Assert.assertEquals(user.getFederationLink(), ldapModel.getId());
try {
user.setEmail("error@error.com");
Assert.fail("should fail");
} catch (ModelReadOnlyException e) {
}
try {
user.setLastName("Berk");
Assert.fail("should fail");
} catch (ModelReadOnlyException e) {
}
try {
user.setFirstName("Bilbo");
Assert.fail("should fail");
} catch (ModelReadOnlyException e) {
}
try {
UserCredentialModel cred = UserCredentialModel.password("PoopyPoop1");
session.userCredentialManager().updateCredential(appRealm, user, cred);
Assert.fail("should fail");
} catch (ModelReadOnlyException e) {
}
Assert.assertTrue(session.users().removeUser(appRealm, user));
} finally {
keycloakRule.stopSession(session, false);
}
session = keycloakRule.startSession();
try {
RealmModel appRealm = session.realms().getRealmByName("test");
Assert.assertEquals(LDAPStorageProviderFactory.EditMode.WRITABLE.toString(), appRealm.getComponent(ldapModel.getId()).getConfig().getFirst(LDAPConstants.EDIT_MODE));
} finally {
keycloakRule.stopSession(session, false);
}
}
@Test
public void testRemoveFederatedUser() {
KeycloakSession session = keycloakRule.startSession();
try {
RealmModel appRealm = session.realms().getRealmByName("test");
UserModel user = session.users().getUserByUsername("registerUserSuccess2", appRealm);
Assert.assertNotNull(user);
Assert.assertNotNull(user.getFederationLink());
Assert.assertEquals(user.getFederationLink(), ldapModel.getId());
Assert.assertTrue(session.users().removeUser(appRealm, user));
Assert.assertNull(session.users().getUserByUsername("registerUserSuccess2", appRealm));
} finally {
keycloakRule.stopSession(session, true);
}
}
@Test
public void testSearch() {
KeycloakSession session = keycloakRule.startSession();
try {
RealmModel appRealm = session.realms().getRealmByName("test");
LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
LDAPTestUtils.addLDAPUser(ldapProvider, appRealm, "username1", "John1", "Doel1", "user1@email.org", null, "121");
LDAPTestUtils.addLDAPUser(ldapProvider, appRealm, "username2", "John2", "Doel2", "user2@email.org", null, "122");
LDAPTestUtils.addLDAPUser(ldapProvider, appRealm, "username3", "John3", "Doel3", "user3@email.org", null, "123");
LDAPTestUtils.addLDAPUser(ldapProvider, appRealm, "username4", "John4", "Doel4", "user4@email.org", null, "124");
// Users are not at local store at this moment
Assert.assertNull(session.userStorage().getUserByUsername("username1", appRealm));
Assert.assertNull(session.userStorage().getUserByUsername("username2", appRealm));
Assert.assertNull(session.userStorage().getUserByUsername("username3", appRealm));
Assert.assertNull(session.userStorage().getUserByUsername("username4", appRealm));
// search by username
session.users().searchForUser("username1", appRealm);
LDAPTestUtils.assertUserImported(session.userStorage(), appRealm, "username1", "John1", "Doel1", "user1@email.org", "121");
// search by email
session.users().searchForUser("user2@email.org", appRealm);
LDAPTestUtils.assertUserImported(session.userStorage(), appRealm, "username2", "John2", "Doel2", "user2@email.org", "122");
// search by lastName
session.users().searchForUser("Doel3", appRealm);
LDAPTestUtils.assertUserImported(session.userStorage(), appRealm, "username3", "John3", "Doel3", "user3@email.org", "123");
// search by firstName + lastName
session.users().searchForUser("John4 Doel4", appRealm);
LDAPTestUtils.assertUserImported(session.userStorage(), appRealm, "username4", "John4", "Doel4", "user4@email.org", "124");
} finally {
keycloakRule.stopSession(session, true);
}
}
@Test
public void testSearchWithCustomLDAPFilter() {
// Add custom filter for searching users
KeycloakSession session = keycloakRule.startSession();
try {
RealmModel appRealm = session.realms().getRealmByName("test");
ldapModel.getConfig().putSingle(LDAPConstants.CUSTOM_USER_SEARCH_FILTER, "(|(mail=user5@email.org)(mail=user6@email.org))");
appRealm.updateComponent(ldapModel);
} finally {
keycloakRule.stopSession(session, true);
}
session = keycloakRule.startSession();
try {
LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
RealmModel appRealm = session.realms().getRealmByName("test");
LDAPTestUtils.addLDAPUser(ldapProvider, appRealm, "username5", "John5", "Doel5", "user5@email.org", null, "125");
LDAPTestUtils.addLDAPUser(ldapProvider, appRealm, "username6", "John6", "Doel6", "user6@email.org", null, "126");
LDAPTestUtils.addLDAPUser(ldapProvider, appRealm, "username7", "John7", "Doel7", "user7@email.org", null, "127");
// search by email
session.users().searchForUser("user5@email.org", appRealm);
LDAPTestUtils.assertUserImported(session.userStorage(), appRealm, "username5", "John5", "Doel5", "user5@email.org", "125");
session.users().searchForUser("John6 Doel6", appRealm);
LDAPTestUtils.assertUserImported(session.userStorage(), appRealm, "username6", "John6", "Doel6", "user6@email.org", "126");
session.users().searchForUser("user7@email.org", appRealm);
session.users().searchForUser("John7 Doel7", appRealm);
Assert.assertNull(session.userStorage().getUserByUsername("username7", appRealm));
// Remove custom filter
ldapModel.getConfig().remove(LDAPConstants.CUSTOM_USER_SEARCH_FILTER);
appRealm.updateComponent(ldapModel);
} finally {
keycloakRule.stopSession(session, true);
}
}
@Test
public void testUnsynced() throws Exception {
KeycloakSession session = keycloakRule.startSession();
try {
RealmModel appRealm = session.realms().getRealmByName("test");
UserStorageProviderModel model = new UserStorageProviderModel(ldapModel);
model.getConfig().putSingle(LDAPConstants.EDIT_MODE, LDAPStorageProviderFactory.EditMode.UNSYNCED.toString());
appRealm.updateComponent(model);
UserModel user = session.users().getUserByUsername("johnkeycloak", appRealm);
Assert.assertNotNull(user);
Assert.assertNotNull(user.getFederationLink());
Assert.assertEquals(user.getFederationLink(), ldapModel.getId());
UserCredentialModel cred = UserCredentialModel.password("Candycand1");
session.userCredentialManager().updateCredential(appRealm, user, cred);
CredentialModel userCredentialValueModel = session.userCredentialManager().getStoredCredentialsByType(appRealm, user, CredentialModel.PASSWORD).get(0);
Assert.assertEquals(UserCredentialModel.PASSWORD, userCredentialValueModel.getType());
Assert.assertTrue(session.userCredentialManager().isValid(appRealm, user, cred));
// LDAP password is still unchanged
LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, model);
LDAPObject ldapUser = ldapProvider.loadLDAPUserByUsername(appRealm, "johnkeycloak");
ldapProvider.getLdapIdentityStore().validatePassword(ldapUser, "Password1");
// User is deleted just locally
Assert.assertTrue(session.users().removeUser(appRealm, user));
// Assert user not available locally, but will be reimported from LDAP once searched
Assert.assertNull(session.userStorage().getUserByUsername("johnkeycloak", appRealm));
Assert.assertNotNull(session.users().getUserByUsername("johnkeycloak", appRealm));
} finally {
keycloakRule.stopSession(session, false);
}
session = keycloakRule.startSession();
try {
RealmModel appRealm = session.realms().getRealmByName("test");
Assert.assertEquals(LDAPStorageProviderFactory.EditMode.WRITABLE.toString(), appRealm.getComponent(ldapModel.getId()).getConfig().getFirst(LDAPConstants.EDIT_MODE));
} finally {
keycloakRule.stopSession(session, false);
}
}
}

View file

@ -0,0 +1,149 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.federation.storage.ldap;
import org.jboss.logging.Logger;
import org.keycloak.common.constants.KerberosConstants;
import org.keycloak.models.LDAPConstants;
import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LDAPTestConfiguration {
private static final Logger log = Logger.getLogger(LDAPTestConfiguration.class);
private String connectionPropertiesLocation;
private int sleepTime;
private boolean startEmbeddedLdapLerver = true;
private Map<String, String> config;
protected static final Map<String, String> PROP_MAPPINGS = new HashMap<String, String>();
protected static final Map<String, String> DEFAULT_VALUES = new HashMap<String, String>();
static {
PROP_MAPPINGS.put(LDAPConstants.CONNECTION_URL, "idm.test.ldap.connection.url");
PROP_MAPPINGS.put(LDAPConstants.BASE_DN, "idm.test.ldap.base.dn");
PROP_MAPPINGS.put(LDAPConstants.USERS_DN, "idm.test.ldap.user.dn.suffix");
PROP_MAPPINGS.put(LDAPConstants.BIND_DN, "idm.test.ldap.bind.dn");
PROP_MAPPINGS.put(LDAPConstants.BIND_CREDENTIAL, "idm.test.ldap.bind.credential");
PROP_MAPPINGS.put(LDAPConstants.VENDOR, "idm.test.ldap.vendor");
PROP_MAPPINGS.put(LDAPConstants.CONNECTION_POOLING, "idm.test.ldap.connection.pooling");
PROP_MAPPINGS.put(LDAPConstants.PAGINATION, "idm.test.ldap.pagination");
PROP_MAPPINGS.put(LDAPConstants.BATCH_SIZE_FOR_SYNC, "idm.test.ldap.batch.size.for.sync");
PROP_MAPPINGS.put(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, "idm.test.ldap.username.ldap.attribute");
PROP_MAPPINGS.put(LDAPConstants.RDN_LDAP_ATTRIBUTE, "idm.test.ldap.rdn.ldap.attribute");
PROP_MAPPINGS.put(LDAPConstants.USER_OBJECT_CLASSES, "idm.test.ldap.user.object.classes");
PROP_MAPPINGS.put(LDAPConstants.EDIT_MODE, "idm.test.ldap.edit.mode");
PROP_MAPPINGS.put(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION, "idm.test.kerberos.allow.kerberos.authentication");
PROP_MAPPINGS.put(KerberosConstants.KERBEROS_REALM, "idm.test.kerberos.realm");
PROP_MAPPINGS.put(KerberosConstants.SERVER_PRINCIPAL, "idm.test.kerberos.server.principal");
PROP_MAPPINGS.put(KerberosConstants.KEYTAB, "idm.test.kerberos.keytab");
PROP_MAPPINGS.put(KerberosConstants.DEBUG, "idm.test.kerberos.debug");
PROP_MAPPINGS.put(KerberosConstants.ALLOW_PASSWORD_AUTHENTICATION, "idm.test.kerberos.allow.password.authentication");
PROP_MAPPINGS.put(KerberosConstants.UPDATE_PROFILE_FIRST_LOGIN, "idm.test.kerberos.update.profile.first.login");
PROP_MAPPINGS.put(KerberosConstants.USE_KERBEROS_FOR_PASSWORD_AUTHENTICATION, "idm.test.kerberos.use.kerberos.for.password.authentication");
DEFAULT_VALUES.put(LDAPConstants.CONNECTION_URL, "ldap://localhost:10389");
DEFAULT_VALUES.put(LDAPConstants.BASE_DN, "dc=keycloak,dc=org");
DEFAULT_VALUES.put(LDAPConstants.USERS_DN, "ou=People,dc=keycloak,dc=org");
DEFAULT_VALUES.put(LDAPConstants.BIND_DN, "uid=admin,ou=system");
DEFAULT_VALUES.put(LDAPConstants.BIND_CREDENTIAL, "secret");
DEFAULT_VALUES.put(LDAPConstants.VENDOR, LDAPConstants.VENDOR_OTHER);
DEFAULT_VALUES.put(LDAPConstants.CONNECTION_POOLING, "true");
DEFAULT_VALUES.put(LDAPConstants.PAGINATION, "true");
DEFAULT_VALUES.put(LDAPConstants.BATCH_SIZE_FOR_SYNC, String.valueOf(LDAPConstants.DEFAULT_BATCH_SIZE_FOR_SYNC));
DEFAULT_VALUES.put(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, null);
DEFAULT_VALUES.put(LDAPConstants.USER_OBJECT_CLASSES, null);
DEFAULT_VALUES.put(LDAPConstants.EDIT_MODE, LDAPStorageProviderFactory.EditMode.READ_ONLY.toString());
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 = 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");
DEFAULT_VALUES.put(KerberosConstants.ALLOW_PASSWORD_AUTHENTICATION, "true");
DEFAULT_VALUES.put(KerberosConstants.UPDATE_PROFILE_FIRST_LOGIN, "true");
DEFAULT_VALUES.put(KerberosConstants.USE_KERBEROS_FOR_PASSWORD_AUTHENTICATION, "false");
}
public static LDAPTestConfiguration readConfiguration(String connectionPropertiesLocation) {
LDAPTestConfiguration ldapTestConfiguration = new LDAPTestConfiguration();
ldapTestConfiguration.setConnectionPropertiesLocation(connectionPropertiesLocation);
ldapTestConfiguration.loadConnectionProperties();
return ldapTestConfiguration;
}
protected void loadConnectionProperties() {
Properties p = new Properties();
try {
log.info("Reading LDAP configuration from: " + connectionPropertiesLocation);
InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(connectionPropertiesLocation);
p.load(is);
}
catch (Exception e) {
throw new RuntimeException(e);
}
config = new HashMap<String, String>();
for (Map.Entry<String, String> property : PROP_MAPPINGS.entrySet()) {
String propertyName = property.getKey();
String configName = property.getValue();
String value = (String) p.get(configName);
if (value == null) {
value = DEFAULT_VALUES.get(propertyName);
}
config.put(propertyName, value);
}
startEmbeddedLdapLerver = Boolean.parseBoolean(p.getProperty("idm.test.ldap.start.embedded.ldap.server", "true"));
sleepTime = Integer.parseInt(p.getProperty("idm.test.ldap.sleepTime", "1000"));
log.info("Start embedded server: " + startEmbeddedLdapLerver);
log.info("Read config: " + config);
}
public Map<String,String> getLDAPConfig() {
return config;
}
public void setConnectionPropertiesLocation(String connectionPropertiesLocation) {
this.connectionPropertiesLocation = connectionPropertiesLocation;
}
public boolean isStartEmbeddedLdapLerver() {
return startEmbeddedLdapLerver;
}
public int getSleepTime() {
return sleepTime;
}
}

View file

@ -0,0 +1,296 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.federation.storage.ldap;
import org.junit.Assert;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.UserModelDelegate;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
import org.keycloak.storage.ldap.LDAPUtils;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapperFactory;
import org.keycloak.storage.ldap.mappers.membership.LDAPGroupMapperMode;
import org.keycloak.storage.ldap.mappers.membership.group.GroupLDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.membership.group.GroupLDAPStorageMapperFactory;
import org.keycloak.storage.ldap.mappers.membership.group.GroupMapperConfig;
import org.keycloak.storage.ldap.mappers.membership.role.RoleLDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.membership.role.RoleLDAPStorageMapperFactory;
import org.keycloak.storage.ldap.mappers.membership.role.RoleMapperConfig;
import org.keycloak.storage.user.SynchronizationResult;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LDAPTestUtils {
public static UserModel addLocalUser(KeycloakSession session, RealmModel realm, String username, String email, String password) {
UserModel user = session.userStorage().addUser(realm, username);
user.setEmail(email);
user.setEnabled(true);
UserCredentialModel creds = new UserCredentialModel();
creds.setType(CredentialRepresentation.PASSWORD);
creds.setValue(password);
session.userCredentialManager().updateCredential(realm, user, creds);
return user;
}
public static LDAPObject addLDAPUser(LDAPStorageProvider ldapProvider, RealmModel realm, final String username,
final String firstName, final String lastName, final String email, final String street, final String... postalCode) {
UserModel helperUser = new UserModelDelegate(null) {
@Override
public String getUsername() {
return username;
}
@Override
public String getFirstName() {
return firstName;
}
@Override
public String getLastName() {
return lastName;
}
@Override
public String getEmail() {
return email;
}
@Override
public List<String> getAttribute(String name) {
if ("postal_code".equals(name) && postalCode != null && postalCode.length > 0) {
return Arrays.asList(postalCode);
} else if ("street".equals(name) && street != null) {
return Collections.singletonList(street);
} else {
return Collections.emptyList();
}
}
};
return LDAPUtils.addUserToLDAP(ldapProvider, realm, helperUser);
}
public static void updateLDAPPassword(LDAPStorageProvider ldapProvider, LDAPObject ldapUser, String password) {
ldapProvider.getLdapIdentityStore().updatePassword(ldapUser, password);
// Enable MSAD user through userAccountControls
if (ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory()) {
ldapUser.setSingleAttribute(LDAPConstants.USER_ACCOUNT_CONTROL, "512");
ldapProvider.getLdapIdentityStore().update(ldapUser);
}
}
public static LDAPStorageProvider getLdapProvider(KeycloakSession keycloakSession, ComponentModel ldapFedModel) {
return (LDAPStorageProvider)keycloakSession.getProvider(UserStorageProvider.class, ldapFedModel);
}
public static void assertUserImported(UserProvider userProvider, RealmModel realm, String username, String expectedFirstName, String expectedLastName, String expectedEmail, String expectedPostalCode) {
UserModel user = userProvider.getUserByUsername(username, realm);
Assert.assertNotNull(user);
Assert.assertEquals(expectedFirstName, user.getFirstName());
Assert.assertEquals(expectedLastName, user.getLastName());
Assert.assertEquals(expectedEmail, user.getEmail());
Assert.assertEquals(expectedPostalCode, user.getFirstAttribute("postal_code"));
}
// CRUD model mappers
public static void addZipCodeLDAPMapper(RealmModel realm, ComponentModel providerModel) {
addUserAttributeMapper(realm, providerModel, "zipCodeMapper", "postal_code", LDAPConstants.POSTAL_CODE);
}
public static ComponentModel addUserAttributeMapper(RealmModel realm, ComponentModel providerModel, String mapperName, String userModelAttributeName, String ldapAttributeName) {
ComponentModel mapperModel = KeycloakModelUtils.createComponentModel(mapperName, providerModel.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID, LDAPStorageMapper.class.getName(),
UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, userModelAttributeName,
UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, ldapAttributeName,
UserAttributeLDAPStorageMapper.READ_ONLY, "false",
UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false",
UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "false");
return realm.addComponentModel(mapperModel);
}
public static void addOrUpdateRoleLDAPMappers(RealmModel realm, ComponentModel providerModel, LDAPGroupMapperMode mode) {
ComponentModel mapperModel = getComponentByName(realm, providerModel, "realmRolesMapper");
if (mapperModel != null) {
mapperModel.getConfig().putSingle(RoleMapperConfig.MODE, mode.toString());
realm.updateComponent(mapperModel);
} else {
String baseDn = providerModel.getConfig().getFirst(LDAPConstants.BASE_DN);
mapperModel = KeycloakModelUtils.createComponentModel("realmRolesMapper", providerModel.getId(), RoleLDAPStorageMapperFactory.PROVIDER_ID, LDAPStorageMapper.class.getName(),
RoleMapperConfig.ROLES_DN, "ou=RealmRoles," + baseDn,
RoleMapperConfig.USE_REALM_ROLES_MAPPING, "true",
RoleMapperConfig.MODE, mode.toString());
realm.addComponentModel(mapperModel);
}
mapperModel = getComponentByName(realm, providerModel, "financeRolesMapper");
if (mapperModel != null) {
mapperModel.getConfig().putSingle(RoleMapperConfig.MODE, mode.toString());
realm.updateComponent(mapperModel);
} else {
String baseDn = providerModel.getConfig().getFirst(LDAPConstants.BASE_DN);
mapperModel = KeycloakModelUtils.createComponentModel("financeRolesMapper", providerModel.getId(), RoleLDAPStorageMapperFactory.PROVIDER_ID, LDAPStorageMapper.class.getName(),
RoleMapperConfig.ROLES_DN, "ou=FinanceRoles," + baseDn,
RoleMapperConfig.USE_REALM_ROLES_MAPPING, "false",
RoleMapperConfig.CLIENT_ID, "finance",
RoleMapperConfig.MODE, mode.toString());
realm.addComponentModel(mapperModel);
}
}
public static ComponentModel getComponentByName(RealmModel realm, ComponentModel providerModel, String name) {
List<ComponentModel> components = realm.getComponents(providerModel.getId(), LDAPStorageMapper.class.getName());
for (ComponentModel component : components) {
if (component.getName().equals(name)) {
return component;
}
}
return null;
}
public static void addOrUpdateGroupMapper(RealmModel realm, ComponentModel providerModel, LDAPGroupMapperMode mode, String descriptionAttrName, String... otherConfigOptions) {
ComponentModel mapperModel = getComponentByName(realm, providerModel, "groupsMapper");
if (mapperModel != null) {
mapperModel.getConfig().putSingle(GroupMapperConfig.MODE, mode.toString());
updateGroupMapperConfigOptions(mapperModel, otherConfigOptions);
realm.updateComponent(mapperModel);
} else {
String baseDn = providerModel.getConfig().getFirst(LDAPConstants.BASE_DN);
mapperModel = KeycloakModelUtils.createComponentModel("groupsMapper", providerModel.getId(), GroupLDAPStorageMapperFactory.PROVIDER_ID, LDAPStorageMapper.class.getName(),
GroupMapperConfig.GROUPS_DN, "ou=Groups," + baseDn,
GroupMapperConfig.MAPPED_GROUP_ATTRIBUTES, descriptionAttrName,
GroupMapperConfig.PRESERVE_GROUP_INHERITANCE, "true",
GroupMapperConfig.MODE, mode.toString());
updateGroupMapperConfigOptions(mapperModel, otherConfigOptions);
realm.addComponentModel(mapperModel);
}
}
public static void updateGroupMapperConfigOptions(ComponentModel mapperModel, String... configOptions) {
for (int i=0 ; i<configOptions.length ; i+=2) {
String cfgName = configOptions[i];
String cfgValue = configOptions[i+1];
mapperModel.getConfig().putSingle(cfgName, cfgValue);
}
}
// End CRUD model mappers
public static void syncRolesFromLDAP(RealmModel realm, LDAPStorageProvider ldapProvider, ComponentModel providerModel) {
ComponentModel mapperModel = getComponentByName(realm, providerModel, "realmRolesMapper");
RoleLDAPStorageMapper roleMapper = getRoleMapper(mapperModel, ldapProvider, realm);
roleMapper.syncDataFromFederationProviderToKeycloak();
mapperModel = getComponentByName(realm, providerModel, "financeRolesMapper");
roleMapper = getRoleMapper(mapperModel, ldapProvider, realm);
roleMapper.syncDataFromFederationProviderToKeycloak();
}
public static void removeAllLDAPUsers(LDAPStorageProvider ldapProvider, RealmModel realm) {
LDAPIdentityStore ldapStore = ldapProvider.getLdapIdentityStore();
LDAPQuery ldapQuery = LDAPUtils.createQueryForUserSearch(ldapProvider, realm);
List<LDAPObject> allUsers = ldapQuery.getResultList();
for (LDAPObject ldapUser : allUsers) {
ldapStore.remove(ldapUser);
}
}
public static void removeAllLDAPRoles(KeycloakSession session, RealmModel appRealm, ComponentModel ldapModel, String mapperName) {
ComponentModel mapperModel = getComponentByName(appRealm, ldapModel, mapperName);
LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
LDAPQuery roleQuery = getRoleMapper(mapperModel, ldapProvider, appRealm).createRoleQuery();
List<LDAPObject> ldapRoles = roleQuery.getResultList();
for (LDAPObject ldapRole : ldapRoles) {
ldapProvider.getLdapIdentityStore().remove(ldapRole);
}
}
public static void removeAllLDAPGroups(KeycloakSession session, RealmModel appRealm, ComponentModel ldapModel, String mapperName) {
ComponentModel mapperModel = getComponentByName(appRealm, ldapModel, mapperName);
LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
LDAPQuery roleQuery = getGroupMapper(mapperModel, ldapProvider, appRealm).createGroupQuery();
List<LDAPObject> ldapRoles = roleQuery.getResultList();
for (LDAPObject ldapRole : ldapRoles) {
ldapProvider.getLdapIdentityStore().remove(ldapRole);
}
}
public static void createLDAPRole(KeycloakSession session, RealmModel appRealm, ComponentModel ldapModel, String mapperName, String roleName) {
ComponentModel mapperModel = getComponentByName(appRealm, ldapModel, mapperName);
LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
getRoleMapper(mapperModel, ldapProvider, appRealm).createLDAPRole(roleName);
}
public static LDAPObject createLDAPGroup(KeycloakSession session, RealmModel appRealm, ComponentModel ldapModel, String groupName, String... additionalAttrs) {
ComponentModel mapperModel = getComponentByName(appRealm, ldapModel, "groupsMapper");
LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
Map<String, Set<String>> additAttrs = new HashMap<>();
for (int i=0 ; i<additionalAttrs.length ; i+=2) {
String attrName = additionalAttrs[i];
String attrValue = additionalAttrs[i+1];
additAttrs.put(attrName, Collections.singleton(attrValue));
}
return getGroupMapper(mapperModel, ldapProvider, appRealm).createLDAPGroup(groupName, additAttrs);
}
public static GroupLDAPStorageMapper getGroupMapper(ComponentModel mapperModel, LDAPStorageProvider ldapProvider, RealmModel realm) {
return new GroupLDAPStorageMapper(mapperModel, ldapProvider, realm, new GroupLDAPStorageMapperFactory());
}
public static RoleLDAPStorageMapper getRoleMapper(ComponentModel mapperModel, LDAPStorageProvider ldapProvider, RealmModel realm) {
return new RoleLDAPStorageMapper(mapperModel, ldapProvider, realm, new RoleLDAPStorageMapperFactory());
}
public static void assertSyncEquals(SynchronizationResult syncResult, int expectedAdded, int expectedUpdated, int expectedRemoved, int expectedFailed) {
Assert.assertEquals(expectedAdded, syncResult.getAdded());
Assert.assertEquals(expectedUpdated, syncResult.getUpdated());
Assert.assertEquals(expectedRemoved, syncResult.getRemoved());
Assert.assertEquals(expectedFailed, syncResult.getFailed());
}
}

View file

@ -761,13 +761,13 @@ module.controller('GenericUserStorageCtrl', function($scope, $location, Notifica
if (providerFactory.metadata.synchronizable) {
instance.config['fullSyncPeriod'] = ['-1'];
instance.config['changedSyncPeriod'] = ['-1'];
instance.config['cachePolicy'] = ['DEFAULT'];
instance.config['evictionDay'] = [''];
instance.config['evictionHour'] = [''];
instance.config['evictionMinute'] = [''];
instance.config['maxLifespan'] = [''];
}
instance.config['cachePolicy'] = ['DEFAULT'];
instance.config['evictionDay'] = [''];
instance.config['evictionHour'] = [''];
instance.config['evictionMinute'] = [''];
instance.config['maxLifespan'] = [''];
if (providerFactory.properties) {
for (var i = 0; i < providerFactory.properties.length; i++) {
@ -816,17 +816,10 @@ module.controller('GenericUserStorageCtrl', function($scope, $location, Notifica
instance.config['maxLifespan'] = [''];
}
/*
console.log('Manage instance');
console.log(instance.name);
console.log(instance.providerId);
console.log(instance.providerType);
console.log(instance.parentId);
for (var k in instance.config) {
console.log('config[' + k + "] =");
if (!instance.config['priority']) {
instance.config['priority'] = ['0'];
}
*/
}
if (providerFactory.metadata.synchronizable) {
if (instance.config && instance.config['importEnabled']) {

View file

@ -62,7 +62,7 @@
<kc-tooltip>{{:: 'credentials.disable.tooltip' | translate}}</kc-tooltip>
</div>
</fieldset>
<fieldset class="border-top" data-ng-show="user.email">
<legend><span class="text">{{:: 'credential-reset-actions' | translate}}</span></legend>
<div class="form-group clearfix">