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:
Martin Kanis 2024-08-30 10:14:40 +02:00 committed by Pedro Igor
parent 2f1307a162
commit 0ebf862b63
7 changed files with 213 additions and 3 deletions

View file

@ -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>

View file

@ -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()) {

View file

@ -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) {
}
}

View file

@ -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.";
}
}

View file

@ -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

View file

@ -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();

View file

@ -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);