Local user can't login when ldap error

Closes #43639

Signed-off-by: Martin Kanis <mkanis@redhat.com>
This commit is contained in:
Martin Kanis
2025-11-07 12:14:49 +01:00
committed by Pedro Igor
parent 36011008e8
commit c28cde359c
4 changed files with 151 additions and 3 deletions

View File

@@ -17,6 +17,11 @@
package org.keycloak.storage.ldap.idm.query.internal;
import org.keycloak.storage.StorageUnavailableException;
import javax.naming.CommunicationException;
import javax.naming.NameNotFoundException;
import javax.naming.AuthenticationException;
import org.jboss.logging.Logger;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.ModelDuplicateException;
@@ -181,6 +186,17 @@ public class LDAPQuery implements AutoCloseable {
result.add(ldapObject);
}
} catch (Exception e) {
// Check if this is an LDAP connectivity issue
Throwable current = e;
while (current != null) {
if (current instanceof NameNotFoundException || current instanceof CommunicationException ||
current instanceof AuthenticationException) {
throw new StorageUnavailableException("LDAP server is unavailable for provider [" +
ldapFedProvider.getModel().getName() + "]", e);
}
current = current.getCause();
}
throw new ModelException("Failed to fetch results from the LDAP [" + ldapFedProvider.getModel().getName() + "] provider", e);
}

View File

@@ -1146,9 +1146,18 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
}
private UserModel tryResolveFederatedUser(RealmModel realm, Function<UserLookupProvider, UserModel> loader) {
return mapEnabledStorageProvidersWithTimeout(realm, UserLookupProvider.class, loader)
.findFirst()
.orElse(null);
return mapEnabledStorageProvidersWithTimeout(realm, UserLookupProvider.class, provider -> {
try {
return loader.apply(provider);
} catch (StorageUnavailableException e) {
logger.warnf(e, "User storage provider %s is unavailable. " +
"Continuing with other providers for graceful degradation.",
provider.getClass().getSimpleName());
return null;
}
})
.findFirst()
.orElse(null);
}
private boolean isSyncSettingsUpdated(UserStorageProviderModel previous, UserStorageProviderModel actual) {

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2025 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;
/**
* Exception thrown by user storage providers to indicate that the external storage
* system is temporarily unavailable due to connectivity issues, server downtime,
* or other infrastructure problems.
*
* <p>This exception allows storage providers to signal graceful degradation scenarios
* where the UserStorageManager should skip the unavailable provider and continue
* with other available providers or local storage.</p>
*
*/
public class StorageUnavailableException extends RuntimeException {
public StorageUnavailableException() {
super();
}
public StorageUnavailableException(String message) {
super(message);
}
public StorageUnavailableException(String message, Throwable cause) {
super(message, cause);
}
public StorageUnavailableException(Throwable cause) {
super(cause != null ? cause.getMessage() : null, cause);
}
}

View File

@@ -21,18 +21,24 @@ import org.junit.Rule;
import org.junit.Test;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.testsuite.arquillian.annotation.ModelTest;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.util.LDAPRule;
import org.keycloak.testsuite.util.LDAPTestConfiguration;
import org.keycloak.testsuite.util.LDAPTestUtils;
import org.keycloak.testsuite.util.UserBuilder;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
/**
@@ -104,4 +110,74 @@ public class UserStorageGracefulDegradationLdapTest extends AbstractLDAPTest {
session.users().removeUser(realm, localUser);
}
}
@Test
public void testLoginWithEmbeddedLDAPFailure() {
// Get original URL first
String originalUrl = ldapRule.getConfig().get(LDAPConstants.CONNECTION_URL);
AtomicReference<String> userIdRef = new AtomicReference<>();
try {
// First create a dedicated LDAP user
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealm("test");
ComponentModel ldapModel = LDAPTestUtils.getLdapProviderModel(realm);
LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
// Create LDAP user for testing
LDAPObject testLdapUser = LDAPTestUtils.addLDAPUser(ldapProvider, realm, "testldapuser", "Test", "LdapUser", "testldap@example.com", null, "12345");
LDAPTestUtils.updateLDAPPassword(ldapProvider, testLdapUser, "TestPassword123!");
});
// Break LDAP connection and disable sync
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealm("test");
ComponentModel ldapModel = LDAPTestUtils.getLdapProviderModel(realm);
ldapModel.getConfig().putSingle("connectionUrl", "ldap://invalid-server:999");
ldapModel.getConfig().putSingle("syncRegistrations", "false");
ldapModel.getConfig().putSingle("importEnabled", "false");
realm.updateComponent(ldapModel);
});
// Create local user with @ in username
UserRepresentation localUser = UserBuilder.create()
.username("user@domain.com")
.password("password")
.enabled(true)
.build();
String userId = ApiUtil.getCreatedId(testRealm().users().create(localUser));
userIdRef.set(userId);
// Test that LDAP users fail to login when LDAP is down
loginPage.open();
loginPage.login("testldapuser", "TestPassword123!");
// Should stay on login page with error since LDAP user can't be authenticated
Assert.assertTrue("Should stay on login page when LDAP user login fails",
loginPage.isCurrent());
// Now try to login with the local user - this should work despite LDAP being down
loginPage.login("user@domain.com", "password");
// Should succeed despite LDAP failure
appPage.assertCurrent();
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
} finally {
// Cleanup
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealm("test");
ComponentModel ldapModel = LDAPTestUtils.getLdapProviderModel(realm);
ldapModel.getConfig().putSingle("connectionUrl", originalUrl);
ldapModel.getConfig().putSingle("syncRegistrations", "true");
ldapModel.getConfig().putSingle("importEnabled", "true");
realm.updateComponent(ldapModel);
LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
LDAPTestUtils.removeLDAPUserByUsername(ldapProvider, realm, ldapProvider.getLdapIdentityStore().getConfig(), "testldapuser");
});
testRealm().users().get(userIdRef.get()).remove();
}
}
}