LDAP Import: KERBEROS_PRINCIPAL not updated when UserPrincipal changes and user already exists
Closes #32266 Signed-off-by: Martin Kanis <mkanis@redhat.com>
This commit is contained in:
parent
2f1307a162
commit
0ebf862b63
7 changed files with 213 additions and 3 deletions
|
@ -101,7 +101,6 @@ import org.keycloak.userprofile.UserProfileMetadata;
|
||||||
import org.keycloak.userprofile.UserProfileUtil;
|
import org.keycloak.userprofile.UserProfileUtil;
|
||||||
|
|
||||||
import org.keycloak.utils.StreamsUtil;
|
import org.keycloak.utils.StreamsUtil;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
|
|
@ -54,6 +54,7 @@ import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapper;
|
||||||
import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapperFactory;
|
import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapperFactory;
|
||||||
import org.keycloak.storage.ldap.mappers.HardcodedLDAPAttributeMapper;
|
import org.keycloak.storage.ldap.mappers.HardcodedLDAPAttributeMapper;
|
||||||
import org.keycloak.storage.ldap.mappers.HardcodedLDAPAttributeMapperFactory;
|
import org.keycloak.storage.ldap.mappers.HardcodedLDAPAttributeMapperFactory;
|
||||||
|
import org.keycloak.storage.ldap.mappers.KerberosPrincipalAttributeMapperFactory;
|
||||||
import org.keycloak.storage.ldap.mappers.LDAPConfigDecorator;
|
import org.keycloak.storage.ldap.mappers.LDAPConfigDecorator;
|
||||||
import org.keycloak.storage.ldap.mappers.LDAPMappersComparator;
|
import org.keycloak.storage.ldap.mappers.LDAPMappersComparator;
|
||||||
import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
|
import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
|
||||||
|
@ -451,6 +452,11 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LD
|
||||||
realm.updateComponent(model);
|
realm.updateComponent(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (kerberosConfig.getKerberosPrincipalAttribute() != null) {
|
||||||
|
mapperModel = KeycloakModelUtils.createComponentModel("Kerberos principal attribute mapper", model.getId(), KerberosPrincipalAttributeMapperFactory.PROVIDER_ID, LDAPStorageMapper.class.getName());
|
||||||
|
realm.addComponentModel(mapperModel);
|
||||||
|
}
|
||||||
|
|
||||||
// In case that "Sync Registration" is ON and the LDAP v3 Password-modify extension is ON, we will create hardcoded mapper to create
|
// In case that "Sync Registration" is ON and the LDAP v3 Password-modify extension is ON, we will create hardcoded mapper to create
|
||||||
// random "userPassword" every time when creating user. Otherwise users won't be able to register and login
|
// random "userPassword" every time when creating user. Otherwise users won't be able to register and login
|
||||||
if (!activeDirectory && syncRegistrations && ldapConfig.useExtendedPasswordModifyOp()) {
|
if (!activeDirectory && syncRegistrations && ldapConfig.useExtendedPasswordModifyOp()) {
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 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.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 static org.keycloak.federation.kerberos.KerberosFederationProvider.KERBEROS_PRINCIPAL;
|
||||||
|
|
||||||
|
public class KerberosPrincipalAttributeMapper extends AbstractLDAPStorageMapper {
|
||||||
|
|
||||||
|
public KerberosPrincipalAttributeMapper(ComponentModel mapperModel, LDAPStorageProvider ldapProvider) {
|
||||||
|
super(mapperModel, ldapProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) {
|
||||||
|
String kerberosPrincipalAttribute = ldapProvider.getKerberosConfig().getKerberosPrincipalAttribute();
|
||||||
|
|
||||||
|
if (kerberosPrincipalAttribute != null) {
|
||||||
|
String localKerberosPrincipal = user.getFirstAttribute(KERBEROS_PRINCIPAL);
|
||||||
|
String ldapKerberosPrincipal = ldapUser.getAttributeAsString(kerberosPrincipalAttribute);
|
||||||
|
if (ldapKerberosPrincipal != null && localKerberosPrincipal != null) {
|
||||||
|
// update the Kerberos principal stored in DB as user's attribute if it doesn't match LDAP
|
||||||
|
if (!ldapKerberosPrincipal.equals(localKerberosPrincipal)) {
|
||||||
|
user.setSingleAttribute(KERBEROS_PRINCIPAL, ldapKerberosPrincipal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser, RealmModel realm) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserModel proxy(LDAPObject ldapUser, UserModel delegate, RealmModel realm) {
|
||||||
|
return delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeLDAPQuery(LDAPQuery query) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 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.storage.ldap.LDAPStorageProvider;
|
||||||
|
|
||||||
|
public class KerberosPrincipalAttributeMapperFactory extends AbstractLDAPStorageMapperFactory {
|
||||||
|
|
||||||
|
public static final String PROVIDER_ID = "kerberos-principal-attribute-mapper";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected KerberosPrincipalAttributeMapper createMapper(ComponentModel mapperModel, LDAPStorageProvider federationProvider) {
|
||||||
|
return new KerberosPrincipalAttributeMapper(mapperModel, federationProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return PROVIDER_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHelpText() {
|
||||||
|
return "This mapper will update Kerberos principal attribute in the DB when the attribute changes in LDAP.";
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ org.keycloak.storage.ldap.mappers.HardcodedLDAPGroupStorageMapperFactory
|
||||||
org.keycloak.storage.ldap.mappers.HardcodedAttributeMapperFactory
|
org.keycloak.storage.ldap.mappers.HardcodedAttributeMapperFactory
|
||||||
org.keycloak.storage.ldap.mappers.HardcodedLDAPAttributeMapperFactory
|
org.keycloak.storage.ldap.mappers.HardcodedLDAPAttributeMapperFactory
|
||||||
org.keycloak.storage.ldap.mappers.membership.group.GroupLDAPStorageMapperFactory
|
org.keycloak.storage.ldap.mappers.membership.group.GroupLDAPStorageMapperFactory
|
||||||
|
org.keycloak.storage.ldap.mappers.KerberosPrincipalAttributeMapperFactory
|
||||||
org.keycloak.storage.ldap.mappers.membership.role.RoleLDAPStorageMapperFactory
|
org.keycloak.storage.ldap.mappers.membership.role.RoleLDAPStorageMapperFactory
|
||||||
org.keycloak.storage.ldap.mappers.msad.MSADUserAccountControlStorageMapperFactory
|
org.keycloak.storage.ldap.mappers.msad.MSADUserAccountControlStorageMapperFactory
|
||||||
org.keycloak.storage.ldap.mappers.msadlds.MSADLDSUserAccountControlStorageMapperFactory
|
org.keycloak.storage.ldap.mappers.msadlds.MSADLDSUserAccountControlStorageMapperFactory
|
||||||
|
|
|
@ -193,6 +193,7 @@ public abstract class AbstractKerberosTest extends AbstractAuthTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected OAuthClient.AccessTokenResponse assertSuccessfulSpnegoLogin(String clientId, String loginUsername, String expectedUsername, String password) throws Exception {
|
protected OAuthClient.AccessTokenResponse assertSuccessfulSpnegoLogin(String clientId, String loginUsername, String expectedUsername, String password) throws Exception {
|
||||||
|
events.clear();
|
||||||
oauth.clientId(clientId);
|
oauth.clientId(clientId);
|
||||||
Response spnegoResponse = spnegoLogin(loginUsername, password);
|
Response spnegoResponse = spnegoLogin(loginUsername, password);
|
||||||
Assert.assertEquals(302, spnegoResponse.getStatus());
|
Assert.assertEquals(302, spnegoResponse.getStatus());
|
||||||
|
@ -264,7 +265,7 @@ public abstract class AbstractKerberosTest extends AbstractAuthTest {
|
||||||
if (client != null) {
|
if (client != null) {
|
||||||
cleanupApacheHttpClient();
|
cleanupApacheHttpClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
|
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
|
||||||
if (useSpnego) {
|
if (useSpnego) {
|
||||||
BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
|
BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
|
||||||
|
|
|
@ -26,16 +26,32 @@ import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.federation.kerberos.CommonKerberosConfig;
|
import org.keycloak.federation.kerberos.CommonKerberosConfig;
|
||||||
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.models.LDAPConstants;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.UserProvider;
|
||||||
import org.keycloak.representations.idm.ComponentRepresentation;
|
import org.keycloak.representations.idm.ComponentRepresentation;
|
||||||
import org.keycloak.representations.idm.UserRepresentation;
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
|
import org.keycloak.storage.UserStoragePrivateUtil;
|
||||||
import org.keycloak.storage.UserStorageProvider;
|
import org.keycloak.storage.UserStorageProvider;
|
||||||
import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
|
import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
|
||||||
|
import org.keycloak.storage.ldap.idm.model.LDAPObject;
|
||||||
import org.keycloak.storage.ldap.kerberos.LDAPProviderKerberosConfig;
|
import org.keycloak.storage.ldap.kerberos.LDAPProviderKerberosConfig;
|
||||||
|
import org.keycloak.storage.managers.UserStorageSyncManager;
|
||||||
|
import org.keycloak.storage.user.SynchronizationResult;
|
||||||
|
import org.keycloak.testsuite.federation.ldap.LDAPTestAsserts;
|
||||||
|
import org.keycloak.testsuite.federation.ldap.LDAPTestContext;
|
||||||
import org.keycloak.testsuite.util.AccountHelper;
|
import org.keycloak.testsuite.util.AccountHelper;
|
||||||
|
import org.keycloak.testsuite.util.ContainerAssume;
|
||||||
import org.keycloak.testsuite.util.KerberosRule;
|
import org.keycloak.testsuite.util.KerberosRule;
|
||||||
import org.keycloak.testsuite.KerberosEmbeddedServer;
|
import org.keycloak.testsuite.KerberosEmbeddedServer;
|
||||||
|
import org.keycloak.testsuite.util.LDAPTestUtils;
|
||||||
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
import org.keycloak.testsuite.util.TestAppHelper;
|
import org.keycloak.testsuite.util.TestAppHelper;
|
||||||
|
|
||||||
|
import static org.keycloak.common.constants.KerberosConstants.KERBEROS_PRINCIPAL;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test for the LDAPStorageProvider with kerberos enabled (kerberos with LDAP integration)
|
* Test for the LDAPStorageProvider with kerberos enabled (kerberos with LDAP integration)
|
||||||
*
|
*
|
||||||
|
@ -47,7 +63,6 @@ public class KerberosLdapTest extends AbstractKerberosSingleRealmTest {
|
||||||
@ClassRule
|
@ClassRule
|
||||||
public static KerberosRule kerberosRule = new KerberosRule(PROVIDER_CONFIG_LOCATION, KerberosEmbeddedServer.DEFAULT_KERBEROS_REALM);
|
public static KerberosRule kerberosRule = new KerberosRule(PROVIDER_CONFIG_LOCATION, KerberosEmbeddedServer.DEFAULT_KERBEROS_REALM);
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected KerberosRule getKerberosRule() {
|
protected KerberosRule getKerberosRule() {
|
||||||
return kerberosRule;
|
return kerberosRule;
|
||||||
|
@ -72,6 +87,88 @@ public class KerberosLdapTest extends AbstractKerberosSingleRealmTest {
|
||||||
assertUser("hnelson", "hnelson@keycloak.org", "Horatio", "Nelson", "hnelson@KEYCLOAK.ORG", false);
|
assertUser("hnelson", "hnelson@keycloak.org", "Horatio", "Nelson", "hnelson@KEYCLOAK.ORG", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void changeKerberosPrincipalWhenUserChangesInLDAPTest() throws Exception {
|
||||||
|
ContainerAssume.assumeNotAuthServerQuarkus();
|
||||||
|
|
||||||
|
try {
|
||||||
|
OAuthClient.AccessTokenResponse accessTokenResponse = assertSuccessfulSpnegoLogin("hnelson", "hnelson", "secret");
|
||||||
|
|
||||||
|
// Assert user was imported
|
||||||
|
assertUser("hnelson", "hnelson@keycloak.org", "Horatio", "Nelson", "hnelson@KEYCLOAK.ORG", false);
|
||||||
|
|
||||||
|
appPage.logout(accessTokenResponse.getIdToken());
|
||||||
|
|
||||||
|
testingClient.server().run(session -> {
|
||||||
|
LDAPTestContext ctx = LDAPTestContext.init(session);
|
||||||
|
RealmModel testRealm = ctx.getRealm();
|
||||||
|
|
||||||
|
ctx.getLdapModel().getConfig().putSingle(LDAPConstants.EDIT_MODE, UserStorageProvider.EditMode.WRITABLE.toString());
|
||||||
|
UserStorageSyncManager usersSyncManager = new UserStorageSyncManager();
|
||||||
|
|
||||||
|
renameUserInLDAP(ctx, testRealm, "hnelson", "hnelson2", "hnelson2@keycloak.org", "hnelson2@KEYCLOAK.ORG", "secret2");
|
||||||
|
|
||||||
|
// Assert still old users in local provider
|
||||||
|
LDAPTestAsserts.assertUserImported(UserStoragePrivateUtil.userLocalStorage(session), testRealm, "hnelson", "Horatio", "Nelson", "hnelson@keycloak.org", null);
|
||||||
|
|
||||||
|
// Trigger sync
|
||||||
|
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
|
||||||
|
SynchronizationResult syncResult = usersSyncManager.syncAllUsers(sessionFactory, testRealm.getId(), ctx.getLdapModel());
|
||||||
|
Assert.assertEquals(0, syncResult.getFailed());
|
||||||
|
});
|
||||||
|
|
||||||
|
testingClient.server().run(session -> {
|
||||||
|
LDAPTestContext ctx = LDAPTestContext.init(session);
|
||||||
|
RealmModel testRealm = ctx.getRealm();
|
||||||
|
UserProvider userProvider = UserStoragePrivateUtil.userLocalStorage(session);
|
||||||
|
// Assert users updated in local provider
|
||||||
|
LDAPTestAsserts.assertUserImported(session.users(), testRealm, "hnelson2", "Horatio", "Nelson", "hnelson2@keycloak.org", null);
|
||||||
|
UserModel updatedLocalUser = userProvider.getUserByUsername(testRealm, "hnelson2");
|
||||||
|
LDAPObject ldapUser = ctx.getLdapProvider().loadLDAPUserByUsername(testRealm, "hnelson2");
|
||||||
|
Assert.assertNull(userProvider.getUserByUsername(testRealm, "hnelson"));
|
||||||
|
// Assert UUID didn't change
|
||||||
|
Assert.assertEquals(updatedLocalUser.getAttributeStream(LDAPConstants.LDAP_ID).findFirst().get(), ldapUser.getUuid());
|
||||||
|
// Assert Kerberos principal was changed in Keycloak
|
||||||
|
Assert.assertEquals(updatedLocalUser.getAttributeStream(KERBEROS_PRINCIPAL).findFirst().get(), ldapUser.getAttributeAsString(ctx.getLdapProvider().getKerberosConfig().getKerberosPrincipalAttribute()));
|
||||||
|
});
|
||||||
|
|
||||||
|
// login not possible with old user
|
||||||
|
loginPage.open();
|
||||||
|
loginPage.login("hnelson", "secret2");
|
||||||
|
Assert.assertEquals("Invalid username or password.", loginPage.getInputError());
|
||||||
|
|
||||||
|
// login after update successful
|
||||||
|
assertSuccessfulSpnegoLogin("hnelson2", "hnelson2", "secret2");
|
||||||
|
} finally {
|
||||||
|
// revert changes in LDAP
|
||||||
|
testingClient.server().run(session -> {
|
||||||
|
LDAPTestContext ctx = LDAPTestContext.init(session);
|
||||||
|
RealmModel testRealm = ctx.getRealm();
|
||||||
|
|
||||||
|
renameUserInLDAP(ctx, testRealm, "hnelson2", "hnelson", "hnelson@keycloak.org", "hnelson@KEYCLOAK.ORG", "secret");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void renameUserInLDAP(LDAPTestContext ctx, RealmModel testRealm, String username, String newUsername, String newEmail, String newKr5Principal, String secret) {
|
||||||
|
// Update user in LDAP, change username, email, krb5Principal
|
||||||
|
LDAPObject ldapUser = ctx.getLdapProvider().loadLDAPUserByUsername(testRealm, username);
|
||||||
|
|
||||||
|
if (ldapUser != null) {
|
||||||
|
ldapUser.removeReadOnlyAttributeName("uid");
|
||||||
|
ldapUser.removeReadOnlyAttributeName("mail");
|
||||||
|
ldapUser.removeReadOnlyAttributeName(ctx.getLdapProvider().getKerberosConfig().getKerberosPrincipalAttribute());
|
||||||
|
String userNameLdapAttributeName = ctx.getLdapProvider().getLdapIdentityStore().getConfig().getUsernameLdapAttribute();
|
||||||
|
ldapUser.setSingleAttribute(userNameLdapAttributeName, newUsername);
|
||||||
|
ldapUser.setSingleAttribute(LDAPConstants.EMAIL, newEmail);
|
||||||
|
ldapUser.setSingleAttribute(ctx.getLdapProvider().getKerberosConfig().getKerberosPrincipalAttribute(), newKr5Principal);
|
||||||
|
ctx.getLdapProvider().getLdapIdentityStore().update(ldapUser);
|
||||||
|
|
||||||
|
// update also password in LDAP to force propagation into KDC
|
||||||
|
LDAPTestUtils.updateLDAPPassword(ctx.getLdapProvider(), ldapUser, secret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void validatePasswordPolicyTest() throws Exception{
|
public void validatePasswordPolicyTest() throws Exception{
|
||||||
updateProviderEditMode(UserStorageProvider.EditMode.WRITABLE);
|
updateProviderEditMode(UserStorageProvider.EditMode.WRITABLE);
|
||||||
|
|
Loading…
Reference in a new issue