Continue LDAP search if a duplicated user (ModelDuplicateException) is found
Closes #25778 Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
parent
1f772d2957
commit
d679c13040
3 changed files with 126 additions and 39 deletions
|
@ -62,6 +62,7 @@ import org.keycloak.models.RoleModel;
|
||||||
import org.keycloak.models.UserCredentialModel;
|
import org.keycloak.models.UserCredentialModel;
|
||||||
import org.keycloak.models.UserManager;
|
import org.keycloak.models.UserManager;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.UserProvider;
|
||||||
import org.keycloak.models.cache.CachedUserModel;
|
import org.keycloak.models.cache.CachedUserModel;
|
||||||
import org.keycloak.models.credential.PasswordCredentialModel;
|
import org.keycloak.models.credential.PasswordCredentialModel;
|
||||||
import org.keycloak.models.utils.ReadOnlyUserModelDelegate;
|
import org.keycloak.models.utils.ReadOnlyUserModelDelegate;
|
||||||
|
@ -648,37 +649,8 @@ public class LDAPStorageProvider implements UserStorageProvider,
|
||||||
return importUserFromLDAP(session, realm, ldapUser, true);
|
return importUserFromLDAP(session, realm, ldapUser, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected UserModel importUserFromLDAP(KeycloakSession session, RealmModel realm, LDAPObject ldapUser, boolean duplicates) {
|
private void doImportUser(final RealmModel realm, final UserModel user, final LDAPObject ldapUser) {
|
||||||
String ldapUsername = LDAPUtils.getUsername(ldapUser, ldapIdentityStore.getConfig());
|
user.setEnabled(true);
|
||||||
LDAPUtils.checkUuid(ldapUser, ldapIdentityStore.getConfig());
|
|
||||||
|
|
||||||
UserModel imported;
|
|
||||||
if (model.isImportEnabled()) {
|
|
||||||
// Search if there is already an existing user, which means the username might have changed in LDAP without Keycloak knowing about it
|
|
||||||
UserModel existingLocalUser = UserStoragePrivateUtil.userLocalStorage(session)
|
|
||||||
.searchForUserByUserAttributeStream(realm, LDAPConstants.LDAP_ID, ldapUser.getUuid()).findFirst().orElse(null);
|
|
||||||
if(existingLocalUser != null){
|
|
||||||
imported = existingLocalUser;
|
|
||||||
// Need to evict the existing user from cache
|
|
||||||
if (UserStorageUtil.userCache(session) != null) {
|
|
||||||
UserStorageUtil.userCache(session).evict(realm, existingLocalUser);
|
|
||||||
}
|
|
||||||
if (!duplicates) {
|
|
||||||
// if duplicates are not wanted return null
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
imported = UserStoragePrivateUtil.userLocalStorage(session).addUser(realm, ldapUsername);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
InMemoryUserAdapter adapter = new InMemoryUserAdapter(session, realm, new StorageId(model.getId(), ldapUsername).getId());
|
|
||||||
adapter.addDefaults();
|
|
||||||
imported = adapter;
|
|
||||||
}
|
|
||||||
imported.setEnabled(true);
|
|
||||||
|
|
||||||
UserModel finalImported = imported;
|
|
||||||
realm.getComponentsStream(model.getId(), LDAPStorageMapper.class.getName())
|
realm.getComponentsStream(model.getId(), LDAPStorageMapper.class.getName())
|
||||||
.sorted(ldapMappersComparator.sortDesc())
|
.sorted(ldapMappersComparator.sortDesc())
|
||||||
.forEachOrdered(mapperModel -> {
|
.forEachOrdered(mapperModel -> {
|
||||||
|
@ -686,15 +658,15 @@ public class LDAPStorageProvider implements UserStorageProvider,
|
||||||
logger.tracef("Using mapper %s during import user from LDAP", mapperModel);
|
logger.tracef("Using mapper %s during import user from LDAP", mapperModel);
|
||||||
}
|
}
|
||||||
LDAPStorageMapper ldapMapper = mapperManager.getMapper(mapperModel);
|
LDAPStorageMapper ldapMapper = mapperManager.getMapper(mapperModel);
|
||||||
ldapMapper.onImportUserFromLDAP(ldapUser, finalImported, realm, true);
|
ldapMapper.onImportUserFromLDAP(ldapUser, user, realm, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
String userDN = ldapUser.getDn().toString();
|
String userDN = ldapUser.getDn().toString();
|
||||||
if (model.isImportEnabled()) imported.setFederationLink(model.getId());
|
if (model.isImportEnabled()) user.setFederationLink(model.getId());
|
||||||
imported.setSingleAttribute(LDAPConstants.LDAP_ID, ldapUser.getUuid());
|
user.setSingleAttribute(LDAPConstants.LDAP_ID, ldapUser.getUuid());
|
||||||
imported.setSingleAttribute(LDAPConstants.LDAP_ENTRY_DN, userDN);
|
user.setSingleAttribute(LDAPConstants.LDAP_ENTRY_DN, userDN);
|
||||||
if(getLdapIdentityStore().getConfig().isTrustEmail()){
|
if(getLdapIdentityStore().getConfig().isTrustEmail()){
|
||||||
imported.setEmailVerified(true);
|
user.setEmailVerified(true);
|
||||||
}
|
}
|
||||||
if (kerberosConfig.isAllowKerberosAuthentication() && kerberosConfig.getKerberosPrincipalAttribute() != null) {
|
if (kerberosConfig.isAllowKerberosAuthentication() && kerberosConfig.getKerberosPrincipalAttribute() != null) {
|
||||||
String kerberosPrincipal = ldapUser.getAttributeAsString(kerberosConfig.getKerberosPrincipalAttribute());
|
String kerberosPrincipal = ldapUser.getAttributeAsString(kerberosConfig.getKerberosPrincipalAttribute());
|
||||||
|
@ -702,11 +674,56 @@ public class LDAPStorageProvider implements UserStorageProvider,
|
||||||
logger.warnf("Kerberos principal attribute not found on LDAP user [%s]. Configured kerberos principal attribute name is [%s]", ldapUser.getDn(), kerberosConfig.getKerberosPrincipalAttribute());
|
logger.warnf("Kerberos principal attribute not found on LDAP user [%s]. Configured kerberos principal attribute name is [%s]", ldapUser.getDn(), kerberosConfig.getKerberosPrincipalAttribute());
|
||||||
} else {
|
} else {
|
||||||
KerberosPrincipal kerberosPrinc = new KerberosPrincipal(kerberosPrincipal);
|
KerberosPrincipal kerberosPrinc = new KerberosPrincipal(kerberosPrincipal);
|
||||||
imported.setSingleAttribute(KerberosConstants.KERBEROS_PRINCIPAL, kerberosPrinc.toString());
|
user.setSingleAttribute(KerberosConstants.KERBEROS_PRINCIPAL, kerberosPrinc.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.debugf("Imported new user from LDAP to Keycloak DB. Username: [%s], Email: [%s], LDAP_ID: [%s], LDAP Entry DN: [%s]", imported.getUsername(), imported.getEmail(),
|
logger.debugf("Imported new user from LDAP to Keycloak DB. Username: [%s], Email: [%s], LDAP_ID: [%s], LDAP Entry DN: [%s]",
|
||||||
ldapUser.getUuid(), userDN);
|
user.getUsername(), user.getEmail(), ldapUser.getUuid(), userDN);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected UserModel importUserFromLDAP(KeycloakSession session, RealmModel realm, LDAPObject ldapUser, boolean forcedImport) {
|
||||||
|
String ldapUsername = LDAPUtils.getUsername(ldapUser, ldapIdentityStore.getConfig());
|
||||||
|
LDAPUtils.checkUuid(ldapUser, ldapIdentityStore.getConfig());
|
||||||
|
|
||||||
|
UserModel imported = null;
|
||||||
|
UserModel existingLocalUser = null;
|
||||||
|
final UserProvider userProvider = UserStoragePrivateUtil.userLocalStorage(session);
|
||||||
|
try {
|
||||||
|
if (model.isImportEnabled()) {
|
||||||
|
// Search if there is already an existing user, which means the username might have changed in LDAP without Keycloak knowing about it
|
||||||
|
existingLocalUser = userProvider.searchForUserByUserAttributeStream(realm, LDAPConstants.LDAP_ID, ldapUser.getUuid())
|
||||||
|
.findFirst().orElse(null);
|
||||||
|
if (existingLocalUser != null) {
|
||||||
|
imported = existingLocalUser;
|
||||||
|
// Need to evict the existing user from cache
|
||||||
|
if (UserStorageUtil.userCache(session) != null) {
|
||||||
|
UserStorageUtil.userCache(session).evict(realm, existingLocalUser);
|
||||||
|
}
|
||||||
|
if (!forcedImport) {
|
||||||
|
// if import is not forced return null as it was already imported
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
imported = userProvider.addUser(realm, ldapUsername);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
InMemoryUserAdapter adapter = new InMemoryUserAdapter(session, realm, new StorageId(model.getId(), ldapUsername).getId());
|
||||||
|
adapter.addDefaults();
|
||||||
|
imported = adapter;
|
||||||
|
}
|
||||||
|
doImportUser(realm, imported, ldapUser);
|
||||||
|
} catch (ModelDuplicateException e) {
|
||||||
|
logger.warnf(e, "Duplicated user importing from LDAP. LDAP Entry DN: [%s], LDAP_ID: [%s]", ldapUser.getDn(), ldapUser.getUuid());
|
||||||
|
if (!forcedImport && existingLocalUser == null) {
|
||||||
|
// try to continue if import was not forced, delete created db user if necessary
|
||||||
|
if (model.isImportEnabled() && imported != null) {
|
||||||
|
userProvider.removeUser(realm, imported);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
UserModel proxy = proxy(realm, imported, ldapUser, false);
|
UserModel proxy = proxy(realm, imported, ldapUser, false);
|
||||||
return proxy;
|
return proxy;
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,10 +36,13 @@ import org.junit.FixMethodOrder;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runners.MethodSorters;
|
import org.junit.runners.MethodSorters;
|
||||||
import org.keycloak.models.LDAPConstants;
|
import org.keycloak.models.LDAPConstants;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.representations.idm.UserRepresentation;
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
|
import org.keycloak.testsuite.admin.ApiUtil;
|
||||||
import org.keycloak.testsuite.util.LDAPRule;
|
import org.keycloak.testsuite.util.LDAPRule;
|
||||||
import org.keycloak.testsuite.util.LDAPTestUtils;
|
import org.keycloak.testsuite.util.LDAPTestUtils;
|
||||||
|
import org.keycloak.testsuite.util.UserBuilder;
|
||||||
|
|
||||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||||
public class LDAPSearchForUsersPaginationTest extends AbstractLDAPTest {
|
public class LDAPSearchForUsersPaginationTest extends AbstractLDAPTest {
|
||||||
|
@ -180,6 +183,38 @@ public class LDAPSearchForUsersPaginationTest extends AbstractLDAPTest {
|
||||||
Assert.assertEquals(Set.of("john"), usernames);
|
Assert.assertEquals(Set.of("john"), usernames);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testDuplicateEmailInDatabase() {
|
||||||
|
setLDAPEnabled(false);
|
||||||
|
try {
|
||||||
|
// create a local db user with the same email than an a ldap user
|
||||||
|
String userId = ApiUtil.getCreatedId(testRealm().users().create(UserBuilder.create()
|
||||||
|
.username("jdoe").firstName("John").lastName("Doe")
|
||||||
|
.email("john14@email.org")
|
||||||
|
.build()));
|
||||||
|
Assert.assertNotNull("User not created", userId);
|
||||||
|
getCleanup().addUserId(userId);
|
||||||
|
} finally {
|
||||||
|
setLDAPEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<UserRepresentation> search = adminClient.realm(TEST_REALM_NAME).users()
|
||||||
|
.search("john14@email.org", null, null)
|
||||||
|
.stream().collect(Collectors.toList());
|
||||||
|
Assert.assertEquals("Incorrect users found", 1, search.size());
|
||||||
|
Assert.assertEquals("Incorrect User", "jdoe", search.get(0).getUsername());
|
||||||
|
Assert.assertTrue("Duplicated user created", adminClient.realm(TEST_REALM_NAME).users().search("john", true).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setLDAPEnabled(final boolean enabled) {
|
||||||
|
testingClient.server().run((KeycloakSession session) -> {
|
||||||
|
LDAPTestContext ctx = LDAPTestContext.init(session);
|
||||||
|
RealmModel appRealm = ctx.getRealm();
|
||||||
|
|
||||||
|
ctx.getLdapModel().getConfig().putSingle("enabled", Boolean.toString(enabled));
|
||||||
|
appRealm.updateComponent(ctx.getLdapModel());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void assertLDAPSearchMatchesLocalDB(String searchString) {
|
private void assertLDAPSearchMatchesLocalDB(String searchString) {
|
||||||
//this call should import some users into local database
|
//this call should import some users into local database
|
||||||
List<String> importedUsers = adminClient.realm(TEST_REALM_NAME).users().search(searchString, null, null).stream().map(UserRepresentation::getUsername).collect(Collectors.toList());
|
List<String> importedUsers = adminClient.realm(TEST_REALM_NAME).users().search(searchString, null, null).stream().map(UserRepresentation::getUsername).collect(Collectors.toList());
|
||||||
|
|
|
@ -28,12 +28,15 @@ import org.junit.FixMethodOrder;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runners.MethodSorters;
|
import org.junit.runners.MethodSorters;
|
||||||
import org.keycloak.models.LDAPConstants;
|
import org.keycloak.models.LDAPConstants;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.representations.idm.UserRepresentation;
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
|
import org.keycloak.testsuite.admin.ApiUtil;
|
||||||
import org.keycloak.testsuite.federation.ldap.AbstractLDAPTest;
|
import org.keycloak.testsuite.federation.ldap.AbstractLDAPTest;
|
||||||
import org.keycloak.testsuite.federation.ldap.LDAPTestContext;
|
import org.keycloak.testsuite.federation.ldap.LDAPTestContext;
|
||||||
import org.keycloak.testsuite.util.LDAPRule;
|
import org.keycloak.testsuite.util.LDAPRule;
|
||||||
import org.keycloak.testsuite.util.LDAPTestUtils;
|
import org.keycloak.testsuite.util.LDAPTestUtils;
|
||||||
|
import org.keycloak.testsuite.util.UserBuilder;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.hasItem;
|
import static org.hamcrest.Matchers.hasItem;
|
||||||
import static org.hamcrest.Matchers.hasSize;
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
|
@ -192,4 +195,36 @@ public class LDAPSearchForUsersPaginationNoImportTest extends AbstractLDAPTest {
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
Assert.assertEquals(Set.of("john"), usernames);
|
Assert.assertEquals(Set.of("john"), usernames);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testDuplicateEmailInDatabase() {
|
||||||
|
setLDAPEnabled(false);
|
||||||
|
try {
|
||||||
|
// create a local db user with the same email than an a ldap user
|
||||||
|
String userId = ApiUtil.getCreatedId(testRealm().users().create(UserBuilder.create()
|
||||||
|
.username("jdoe").firstName("John").lastName("Doe")
|
||||||
|
.email("john14@email.org")
|
||||||
|
.build()));
|
||||||
|
Assert.assertNotNull("User not created", userId);
|
||||||
|
getCleanup().addUserId(userId);
|
||||||
|
} finally {
|
||||||
|
setLDAPEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<UserRepresentation> search = adminClient.realm(TEST_REALM_NAME).users()
|
||||||
|
.search("john14@email.org", null, null)
|
||||||
|
.stream().collect(Collectors.toList());
|
||||||
|
Assert.assertEquals("User not found", 1, search.size());
|
||||||
|
Assert.assertEquals("Incorrect User", "jdoe", search.get(0).getUsername());
|
||||||
|
Assert.assertTrue("Duplicated user created", adminClient.realm(TEST_REALM_NAME).users().search("john", true).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setLDAPEnabled(final boolean enabled) {
|
||||||
|
testingClient.server().run((KeycloakSession session) -> {
|
||||||
|
LDAPTestContext ctx = LDAPTestContext.init(session);
|
||||||
|
RealmModel appRealm = ctx.getRealm();
|
||||||
|
|
||||||
|
ctx.getLdapModel().getConfig().putSingle("enabled", Boolean.toString(enabled));
|
||||||
|
appRealm.updateComponent(ctx.getLdapModel());
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue