diff --git a/docs/changelog/136058.yaml b/docs/changelog/136058.yaml new file mode 100644 index 0000000000000..85c7460642af9 --- /dev/null +++ b/docs/changelog/136058.yaml @@ -0,0 +1,5 @@ +pr: 136058 +summary: "Fix + tests for #133542" +area: Security +type: bug +issues: [] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/saml/SamlRealmSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/saml/SamlRealmSettings.java index be655625523da..1c8afc234393e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/saml/SamlRealmSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/saml/SamlRealmSettings.java @@ -63,6 +63,20 @@ public class SamlRealmSettings { key -> Setting.boolSetting(key, false, Setting.Property.NodeScope) ); + public static final Function> IDP_METADATA_HTTP_CONNECT_TIMEOUT = (type) -> Setting + .affixKeySetting( + RealmSettings.realmSettingPrefix(type), + IDP_METADATA_SETTING_PREFIX + "http.connect_timeout", + key -> Setting.timeSetting(key, TimeValue.timeValueSeconds(5), Setting.Property.NodeScope) + ); + + public static final Function> IDP_METADATA_HTTP_READ_TIMEOUT = (type) -> Setting + .affixKeySetting( + RealmSettings.realmSettingPrefix(type), + IDP_METADATA_SETTING_PREFIX + "http.read_timeout", + key -> Setting.timeSetting(key, TimeValue.timeValueSeconds(10), Setting.Property.NodeScope) + ); + public static final Function> IDP_SINGLE_LOGOUT = (type) -> Setting.affixKeySetting( RealmSettings.realmSettingPrefix(type), "idp.use_single_logout", @@ -242,6 +256,8 @@ public static Set> getSettings(String type) { IDP_METADATA_HTTP_REFRESH.apply(type), IDP_METADATA_HTTP_MIN_REFRESH.apply(type), IDP_METADATA_HTTP_FAIL_ON_ERROR.apply(type), + IDP_METADATA_HTTP_CONNECT_TIMEOUT.apply(type), + IDP_METADATA_HTTP_READ_TIMEOUT.apply(type), IDP_SINGLE_LOGOUT.apply(type), NAMEID_FORMAT.apply(type), NAMEID_ALLOW_CREATE.apply(type), diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java index 2e834ab15ef80..5cc2fa5fa2f66 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java @@ -12,6 +12,7 @@ import net.shibboleth.utilities.java.support.xml.BasicParserPool; import org.apache.http.client.HttpClient; +import org.apache.http.client.config.RequestConfig; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.logging.log4j.LogManager; @@ -123,8 +124,10 @@ import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.FORCE_AUTHN; import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.GROUPS_ATTRIBUTE; import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_ENTITY_ID; +import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_METADATA_HTTP_CONNECT_TIMEOUT; import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_METADATA_HTTP_FAIL_ON_ERROR; import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_METADATA_HTTP_MIN_REFRESH; +import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_METADATA_HTTP_READ_TIMEOUT; import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_METADATA_HTTP_REFRESH; import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_METADATA_PATH; import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_SINGLE_LOGOUT; @@ -705,6 +708,14 @@ private static Tuple 0) { @@ -799,7 +810,13 @@ private static Tuple config = buildConfig("https://localhost:" + proxyServer.getPort(), builder -> { + builder.put( + SingleSpSamlRealmSettings.getFullSettingKey(REALM_NAME, SamlRealmSettings.IDP_METADATA_HTTP_CONNECT_TIMEOUT), + customConnectTimeout.getStringRep() + ); + builder.put( + SingleSpSamlRealmSettings.getFullSettingKey(REALM_NAME, SamlRealmSettings.IDP_METADATA_HTTP_READ_TIMEOUT), + customReadTimeout.getStringRep() + ); + }); + + // Verify settings are correctly configured + assertThat(config.v1().getSetting(SamlRealmSettings.IDP_METADATA_HTTP_CONNECT_TIMEOUT), equalTo(customConnectTimeout)); + assertThat(config.v1().getSetting(SamlRealmSettings.IDP_METADATA_HTTP_READ_TIMEOUT), equalTo(customReadTimeout)); + + final ResourceWatcherService watcherService = mock(ResourceWatcherService.class); + Tuple> tuple = SamlRealm.initializeResolver( + logger, + config.v1(), + config.v2(), + watcherService + ); + + try { + assertThat(proxyServer.requests().size(), greaterThanOrEqualTo(1)); + assertIdp1MetadataParsedCorrectly(tuple.v2().get()); + } finally { + tuple.v1().destroy(); + } + } + } + + public void testHttpMetadataWithDefaultTimeouts() throws Exception { + final Path path = getDataPath("idp1.xml"); + final String body = Files.readString(path); + TestsSSLService sslService = buildTestSslService(); + try (MockWebServer proxyServer = new MockWebServer(sslService.sslContext("xpack.security.http.ssl"), false)) { + proxyServer.start(); + proxyServer.enqueue(new MockResponse().setResponseCode(200).setBody(body).addHeader("Content-Type", "application/xml")); + + Tuple config = buildConfig("https://localhost:" + proxyServer.getPort()); + + // Verify default timeout values are used + assertThat(config.v1().getSetting(SamlRealmSettings.IDP_METADATA_HTTP_CONNECT_TIMEOUT), equalTo(TimeValue.timeValueSeconds(5))); + assertThat(config.v1().getSetting(SamlRealmSettings.IDP_METADATA_HTTP_READ_TIMEOUT), equalTo(TimeValue.timeValueSeconds(10))); + + final ResourceWatcherService watcherService = mock(ResourceWatcherService.class); + Tuple> tuple = SamlRealm.initializeResolver( + logger, + config.v1(), + config.v2(), + watcherService + ); + + try { + assertThat(proxyServer.requests().size(), greaterThanOrEqualTo(1)); + assertIdp1MetadataParsedCorrectly(tuple.v2().get()); + } finally { + tuple.v1().destroy(); + } + } + } + + public void testHttpMetadataConnectionTimeout() throws Exception { + // Use a non-routable IP address to simulate connection timeout + // 192.0.2.1 is reserved for documentation and will not be routable + final String unreachableUrl = "https://192.0.2.1:9999/metadata.xml"; + final TimeValue shortConnectTimeout = TimeValue.timeValueMillis(100); + + Tuple config = buildConfig(unreachableUrl, builder -> { + builder.put( + SingleSpSamlRealmSettings.getFullSettingKey(REALM_NAME, SamlRealmSettings.IDP_METADATA_HTTP_CONNECT_TIMEOUT), + shortConnectTimeout.getStringRep() + ); + builder.put(SingleSpSamlRealmSettings.getFullSettingKey(REALM_NAME, SamlRealmSettings.IDP_METADATA_HTTP_FAIL_ON_ERROR), false); + }); + + final ResourceWatcherService watcherService = mock(ResourceWatcherService.class); + + // initialization should complete even though the connection fails + Tuple> tuple = SamlRealm.initializeResolver( + logger, + config.v1(), + config.v2(), + watcherService + ); + + try { + EntityDescriptor descriptor = tuple.v2().get(); + assertThat(descriptor, instanceOf(UnresolvedEntity.class)); + } finally { + tuple.v1().destroy(); + } + } + private void assertIdp1MetadataParsedCorrectly(EntityDescriptor descriptor) { try { IDPSSODescriptor idpssoDescriptor = descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS);