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.utils.StreamsUtil;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</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.HardcodedLDAPAttributeMapper;
|
||||
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.LDAPMappersComparator;
|
||||
import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
|
||||
|
@ -451,6 +452,11 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LD
|
|||
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
|
||||
// random "userPassword" every time when creating user. Otherwise users won't be able to register and login
|
||||
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.HardcodedLDAPAttributeMapperFactory
|
||||
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.msad.MSADUserAccountControlStorageMapperFactory
|
||||
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 {
|
||||
events.clear();
|
||||
oauth.clientId(clientId);
|
||||
Response spnegoResponse = spnegoLogin(loginUsername, password);
|
||||
Assert.assertEquals(302, spnegoResponse.getStatus());
|
||||
|
@ -264,7 +265,7 @@ public abstract class AbstractKerberosTest extends AbstractAuthTest {
|
|||
if (client != null) {
|
||||
cleanupApacheHttpClient();
|
||||
}
|
||||
|
||||
|
||||
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
|
||||
if (useSpnego) {
|
||||
BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
|
||||
|
|
|
@ -26,16 +26,32 @@ import org.junit.ClassRule;
|
|||
import org.junit.Test;
|
||||
import org.keycloak.events.Details;
|
||||
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.UserRepresentation;
|
||||
import org.keycloak.storage.UserStoragePrivateUtil;
|
||||
import org.keycloak.storage.UserStorageProvider;
|
||||
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.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.ContainerAssume;
|
||||
import org.keycloak.testsuite.util.KerberosRule;
|
||||
import org.keycloak.testsuite.KerberosEmbeddedServer;
|
||||
import org.keycloak.testsuite.util.LDAPTestUtils;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
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)
|
||||
*
|
||||
|
@ -47,7 +63,6 @@ public class KerberosLdapTest extends AbstractKerberosSingleRealmTest {
|
|||
@ClassRule
|
||||
public static KerberosRule kerberosRule = new KerberosRule(PROVIDER_CONFIG_LOCATION, KerberosEmbeddedServer.DEFAULT_KERBEROS_REALM);
|
||||
|
||||
|
||||
@Override
|
||||
protected KerberosRule getKerberosRule() {
|
||||
return kerberosRule;
|
||||
|
@ -72,6 +87,88 @@ public class KerberosLdapTest extends AbstractKerberosSingleRealmTest {
|
|||
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
|
||||
public void validatePasswordPolicyTest() throws Exception{
|
||||
updateProviderEditMode(UserStorageProvider.EditMode.WRITABLE);
|
||||
|
|
Loading…
Reference in a new issue