diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 97c9c7e3b0d..aa6e964665f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches-ignore: - main - dependabot/** + - issue* pull_request: workflow_dispatch: @@ -573,7 +574,7 @@ jobs: name: Store IT needs: build runs-on: ubuntu-latest - timeout-minutes: 75 + timeout-minutes: 90 strategy: matrix: db: [postgres, mysql, oracle, mssql, mariadb, tidb] @@ -871,7 +872,7 @@ jobs: runs-on: ubuntu-latest needs: - build - timeout-minutes: 45 + timeout-minutes: 60 steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 8a6b52dfb41..6386e1e2f9b 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -6,6 +6,7 @@ on: - main - dependabot/** - quarkus-next + - issue* pull_request: workflow_dispatch: diff --git a/.github/workflows/js-ci.yml b/.github/workflows/js-ci.yml index 67b95b10099..b5371009476 100644 --- a/.github/workflows/js-ci.yml +++ b/.github/workflows/js-ci.yml @@ -6,6 +6,7 @@ on: - main - dependabot/** - quarkus-next + - issue* pull_request: workflow_dispatch: diff --git a/.github/workflows/operator-ci.yml b/.github/workflows/operator-ci.yml index eea2cde3739..a7193280f93 100644 --- a/.github/workflows/operator-ci.yml +++ b/.github/workflows/operator-ci.yml @@ -5,6 +5,7 @@ on: branches-ignore: - main - dependabot/** + - issue* pull_request: workflow_dispatch: diff --git a/docs/documentation/release_notes/index.adoc b/docs/documentation/release_notes/index.adoc index 7c5a58ebddf..aed9f95bf6c 100644 --- a/docs/documentation/release_notes/index.adoc +++ b/docs/documentation/release_notes/index.adoc @@ -13,6 +13,9 @@ include::topics/templates/document-attributes.adoc[] :release_header_latest_link: {releasenotes_link_latest} include::topics/templates/release-header.adoc[] +== {project_name_full} 26.4.6 +include::topics/26_4_6.adoc[leveloffset=2] + == {project_name_full} 26.4.0 include::topics/26_4_0.adoc[leveloffset=2] diff --git a/docs/documentation/release_notes/topics/26_4_6.adoc b/docs/documentation/release_notes/topics/26_4_6.adoc new file mode 100644 index 00000000000..3e0862189d6 --- /dev/null +++ b/docs/documentation/release_notes/topics/26_4_6.adoc @@ -0,0 +1,9 @@ +// Release notes should contain only headline-worthy new features, +// assuming that people who migrate will read the upgrading guide anyway. + +This release adds filtering of LDAP referrals by default. +This change enhances security and aligns with best practices for LDAP configurations. + +If you can not upgrade to this release yet, we recommend disabling LDAP referrals in all LDAP providers in all of your realms. + +For detailed upgrade instructions, https://www.keycloak.org/docs/latest/upgrading/index.html[review the upgrading guide]. diff --git a/docs/documentation/upgrading/topics/changes/changes-26_4_6.adoc b/docs/documentation/upgrading/topics/changes/changes-26_4_6.adoc new file mode 100644 index 00000000000..574074fccc7 --- /dev/null +++ b/docs/documentation/upgrading/topics/changes/changes-26_4_6.adoc @@ -0,0 +1,21 @@ +// ------------------------ Breaking changes ------------------------ // +== Notable changes + +Notable changes may include internal behavior changes that prevent common misconfigurations, bugs that are fixed, or changes to simplify running {project_name}. + +=== LDAP referrals filtered to allow only LDAP referrals + +LDAP referrals now by default are only allowed to include LDAP URLs. +This change enhances security and aligns with best practices for LDAP configurations. + +This also prevents other JDNI references from being used in case you have written custom extensions. +To restore the original behavior, set the option `spi-storage--ldap--secure-referral` to `false`. +When doing this, we recommend to disable LDAP referrals in all LDAP providers. + +== Deprecated features + +The following sections provide details on deprecated features. + +=== Disabling filtering of LDAP referrals + +The option `spi-storage--ldap--secure-referral` to disable filtering referrals is deprecated. It will be removed in a future release and filtering will then be enforced. diff --git a/docs/documentation/upgrading/topics/changes/changes.adoc b/docs/documentation/upgrading/topics/changes/changes.adoc index 7a8b6969288..092d07a9411 100644 --- a/docs/documentation/upgrading/topics/changes/changes.adoc +++ b/docs/documentation/upgrading/topics/changes/changes.adoc @@ -1,6 +1,10 @@ [[migration-changes]] == Migration Changes +=== Migrating to 26.4.6 + +include::changes-26_4_6.adoc[leveloffset=2] + === Migrating to 26.4.3 include::changes-26_4_3.adoc[leveloffset=2] diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java index a6dacb20d2d..152ddf8f7c1 100755 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java @@ -73,6 +73,9 @@ import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; +import javax.naming.NamingException; +import javax.naming.spi.NamingManager; + /** * @author Marek Posolda * @author Bill Burke @@ -84,6 +87,8 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory getConfigMetadata() { + + ProviderConfigurationBuilder builder = ProviderConfigurationBuilder.create(); + + builder.property() + .name(SECURE_REFERRAL) + .type("boolean") + .helpText("Allow only secure LDAP referrals (deprecated)") + .defaultValue(SECURE_REFERRAL_DEFAULT) + .add(); + + return builder.build(); + + } + @Override public void close() { this.ldapStoreRegistry = null; @@ -727,4 +755,15 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactoryA {@link javax.naming.spi.ObjectFactoryBuilder} implementation to filter out referral references if they do not + * point to an LDAP URL. + * + *

When the LDAP provider encounters a referral, it tries to create an {@link ObjectFactory} from this builder. + * If the referral reference contains an LDAP URL, a {@link DirContextObjectFactory} is created to handle the referral. + * Otherwise, a {@link CommunicationException} is thrown to indicate that the referral cannot be processed. + */ +final class ObjectFactoryBuilder implements javax.naming.spi.ObjectFactoryBuilder, ObjectFactory { + + private static final Logger logger = Logger.getLogger(ObjectFactoryBuilder.class); + private static final String IS_KC_OBJECT_FACTORY_BUILDER = "kc.jndi.object.factory.builder"; + + static boolean isSet() { + Hashtable env = new Hashtable<>(); + + env.put(ObjectFactoryBuilder.IS_KC_OBJECT_FACTORY_BUILDER, Boolean.TRUE); + + try { + Object instance = NamingManager.getObjectInstance(null, null, null, env); + + if (instance != null && instance.getClass().getName().equals(ObjectFactoryBuilder.class.getName())) { + return true; + } + } catch (Exception e) { + throw new RuntimeException("Failed to determine if ObjectFactoryBuilder is set", e); + } + + return false; + } + + @Override + public ObjectFactory createObjectFactory(Object obj, Hashtable environment) throws NamingException { + if (logger.isTraceEnabled()) { + logger.tracef("Creating ObjectFactory for object: %s", obj); + } + + if (obj instanceof Reference ref) { + String factoryClassName = ref.getFactoryClassName(); + + if (factoryClassName != null) { + logger.warnf("Referral refence contains an object factory %s but it will be ignored", factoryClassName); + } + + String ldapUrl = getLdapUrl(ref); + + if (ldapUrl != null) { + return new DirContextObjectFactory(ldapUrl); + } + } else { + logger.debugf("Unsupported reference object of type %s: ", obj); + return this; + } + + throw new CommunicationException("Referral reference does not contain an LDAP URL: " + obj); + } + + @Override + public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable env) { + if (env != null && env.containsKey(IS_KC_OBJECT_FACTORY_BUILDER)) { + return this; + } + return obj; + } + + private String getLdapUrl(Reference ref) { + for (int i = 0; i < ref.size(); i++) { + RefAddr addr = ref.get(i); + String addrType = addr.getType(); + + if ("URL".equalsIgnoreCase(addrType)) { + Object content = addr.getContent(); + + if (content == null) { + return null; + } + + String rawUrl = content.toString(); + + for (String url : List.of(rawUrl.split(" "))) { + if (!url.toLowerCase().startsWith("ldap")) { + logger.warnf("Unsupported scheme from reference URL %s. Ignoring reference.", url); + return null; + } + } + + return rawUrl; + } else { + logger.warnf("Ignoring address of type '%s' from referral reference", addrType); + } + } + + return null; + } + + private record DirContextObjectFactory(String ldapUrl) implements ObjectFactory { + + @Override + public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable env) throws Exception { + @SuppressWarnings("unchecked") + Hashtable newEnv = (Hashtable) env.clone(); + newEnv.put(LdapContext.PROVIDER_URL, ldapUrl); + return new SessionBoundInitialLdapContext(KeycloakSessionUtil.getKeycloakSession(), newEnv, null); + } + } +} diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ImportDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ImportDistTest.java index 7699dd7ae0c..874afb29b2b 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ImportDistTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ImportDistTest.java @@ -85,7 +85,7 @@ public class ImportDistTest { ExecutorService ex = Executors.newFixedThreadPool(1); Future result = ex.submit(() -> dist.run("import", "--dir=" + dir.getAbsolutePath())); try { - cliResult = result.get(20, TimeUnit.SECONDS); + cliResult = result.get(40, TimeUnit.SECONDS); cliResult.assertMessage("Realm 'master' imported"); cliResult.assertMessage("Import finished successfully"); cliResult.assertMessage("master-users-0.json");