mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-30 11:29:57 -06:00
Local user can't login when ldap error
Closes #43639 Signed-off-by: Martin Kanis <mkanis@redhat.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user