Skip to content

Commit 49442ce

Browse files
ankit--sethielasticsearchmachine
andauthored
Configurable HTTP read and connect timeouts for url based SAML metadata resolution (#136058) (#136831)
* Fix + tests for #133542 * [CI] Auto commit changes from spotless * Update docs/changelog/136058.yaml * update summary --------- Co-authored-by: elasticsearchmachine <[email protected]>
1 parent 79dc393 commit 49442ce

File tree

4 files changed

+145
-1
lines changed

4 files changed

+145
-1
lines changed

docs/changelog/136058.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 136058
2+
summary: "Configurable HTTP read and connect timeouts for url based SAML metadata resolution"
3+
area: Security
4+
type: bug
5+
issues: []

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/saml/SamlRealmSettings.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,20 @@ public class SamlRealmSettings {
6363
key -> Setting.boolSetting(key, false, Setting.Property.NodeScope)
6464
);
6565

66+
public static final Function<String, Setting.AffixSetting<TimeValue>> IDP_METADATA_HTTP_CONNECT_TIMEOUT = (type) -> Setting
67+
.affixKeySetting(
68+
RealmSettings.realmSettingPrefix(type),
69+
IDP_METADATA_SETTING_PREFIX + "http.connect_timeout",
70+
key -> Setting.timeSetting(key, TimeValue.timeValueSeconds(5), Setting.Property.NodeScope)
71+
);
72+
73+
public static final Function<String, Setting.AffixSetting<TimeValue>> IDP_METADATA_HTTP_READ_TIMEOUT = (type) -> Setting
74+
.affixKeySetting(
75+
RealmSettings.realmSettingPrefix(type),
76+
IDP_METADATA_SETTING_PREFIX + "http.read_timeout",
77+
key -> Setting.timeSetting(key, TimeValue.timeValueSeconds(10), Setting.Property.NodeScope)
78+
);
79+
6680
public static final Function<String, Setting.AffixSetting<Boolean>> IDP_SINGLE_LOGOUT = (type) -> Setting.affixKeySetting(
6781
RealmSettings.realmSettingPrefix(type),
6882
"idp.use_single_logout",
@@ -242,6 +256,8 @@ public static Set<Setting.AffixSetting<?>> getSettings(String type) {
242256
IDP_METADATA_HTTP_REFRESH.apply(type),
243257
IDP_METADATA_HTTP_MIN_REFRESH.apply(type),
244258
IDP_METADATA_HTTP_FAIL_ON_ERROR.apply(type),
259+
IDP_METADATA_HTTP_CONNECT_TIMEOUT.apply(type),
260+
IDP_METADATA_HTTP_READ_TIMEOUT.apply(type),
245261
IDP_SINGLE_LOGOUT.apply(type),
246262
NAMEID_FORMAT.apply(type),
247263
NAMEID_ALLOW_CREATE.apply(type),

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import net.shibboleth.utilities.java.support.xml.BasicParserPool;
1313

1414
import org.apache.http.client.HttpClient;
15+
import org.apache.http.client.config.RequestConfig;
1516
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
1617
import org.apache.http.impl.client.HttpClientBuilder;
1718
import org.apache.logging.log4j.LogManager;
@@ -123,8 +124,10 @@
123124
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.FORCE_AUTHN;
124125
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.GROUPS_ATTRIBUTE;
125126
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_ENTITY_ID;
127+
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_METADATA_HTTP_CONNECT_TIMEOUT;
126128
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_METADATA_HTTP_FAIL_ON_ERROR;
127129
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_METADATA_HTTP_MIN_REFRESH;
130+
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_METADATA_HTTP_READ_TIMEOUT;
128131
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_METADATA_HTTP_REFRESH;
129132
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_METADATA_PATH;
130133
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_SINGLE_LOGOUT;
@@ -705,6 +708,14 @@ private static Tuple<AbstractReloadingMetadataResolver, Supplier<EntityDescripto
705708
final SSLConnectionSocketFactory factory = sslProfile.connectionSocketFactory();
706709
builder.setSSLSocketFactory(factory);
707710

711+
TimeValue connectTimeout = config.getSetting(IDP_METADATA_HTTP_CONNECT_TIMEOUT);
712+
TimeValue readTimeout = config.getSetting(IDP_METADATA_HTTP_READ_TIMEOUT);
713+
RequestConfig requestConfig = RequestConfig.custom()
714+
.setConnectTimeout(Math.toIntExact(connectTimeout.millis()))
715+
.setSocketTimeout(Math.toIntExact(readTimeout.millis()))
716+
.build();
717+
builder.setDefaultRequestConfig(requestConfig);
718+
708719
TimeValue maxRefresh = config.getSetting(IDP_METADATA_HTTP_REFRESH);
709720
TimeValue minRefresh = config.getSetting(IDP_METADATA_HTTP_MIN_REFRESH);
710721
if (minRefresh.compareTo(maxRefresh) > 0) {
@@ -799,7 +810,13 @@ private static Tuple<AbstractReloadingMetadataResolver, Supplier<EntityDescripto
799810
final Path path = config.env().configDir().resolve(metadataPath);
800811
final FilesystemMetadataResolver resolver = new SamlFilesystemMetadataResolver(path.toFile());
801812

802-
for (var httpSetting : List.of(IDP_METADATA_HTTP_REFRESH, IDP_METADATA_HTTP_MIN_REFRESH, IDP_METADATA_HTTP_FAIL_ON_ERROR)) {
813+
for (var httpSetting : List.of(
814+
IDP_METADATA_HTTP_REFRESH,
815+
IDP_METADATA_HTTP_MIN_REFRESH,
816+
IDP_METADATA_HTTP_FAIL_ON_ERROR,
817+
IDP_METADATA_HTTP_CONNECT_TIMEOUT,
818+
IDP_METADATA_HTTP_READ_TIMEOUT
819+
)) {
803820
if (config.hasSetting(httpSetting.apply(config.type()))) {
804821
logger.info(
805822
"Ignoring setting [{}] because the IdP metadata is being loaded from a file",

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlRealmTests.java

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1576,6 +1576,112 @@ private TestsSSLService buildTestSslService() {
15761576
return new TestsSSLService(TestEnvironment.newEnvironment(settings));
15771577
}
15781578

1579+
public void testHttpMetadataWithCustomTimeouts() throws Exception {
1580+
final Path path = getDataPath("idp1.xml");
1581+
final String body = Files.readString(path);
1582+
TestsSSLService sslService = buildTestSslService();
1583+
try (MockWebServer proxyServer = new MockWebServer(sslService.sslContext("xpack.security.http.ssl"), false)) {
1584+
proxyServer.start();
1585+
proxyServer.enqueue(new MockResponse().setResponseCode(200).setBody(body).addHeader("Content-Type", "application/xml"));
1586+
1587+
final TimeValue customConnectTimeout = TimeValue.timeValueSeconds(3);
1588+
final TimeValue customReadTimeout = TimeValue.timeValueSeconds(15);
1589+
1590+
Tuple<RealmConfig, SSLService> config = buildConfig("https://localhost:" + proxyServer.getPort(), builder -> {
1591+
builder.put(
1592+
SingleSpSamlRealmSettings.getFullSettingKey(REALM_NAME, SamlRealmSettings.IDP_METADATA_HTTP_CONNECT_TIMEOUT),
1593+
customConnectTimeout.getStringRep()
1594+
);
1595+
builder.put(
1596+
SingleSpSamlRealmSettings.getFullSettingKey(REALM_NAME, SamlRealmSettings.IDP_METADATA_HTTP_READ_TIMEOUT),
1597+
customReadTimeout.getStringRep()
1598+
);
1599+
});
1600+
1601+
// Verify settings are correctly configured
1602+
assertThat(config.v1().getSetting(SamlRealmSettings.IDP_METADATA_HTTP_CONNECT_TIMEOUT), equalTo(customConnectTimeout));
1603+
assertThat(config.v1().getSetting(SamlRealmSettings.IDP_METADATA_HTTP_READ_TIMEOUT), equalTo(customReadTimeout));
1604+
1605+
final ResourceWatcherService watcherService = mock(ResourceWatcherService.class);
1606+
Tuple<AbstractReloadingMetadataResolver, Supplier<EntityDescriptor>> tuple = SamlRealm.initializeResolver(
1607+
logger,
1608+
config.v1(),
1609+
config.v2(),
1610+
watcherService
1611+
);
1612+
1613+
try {
1614+
assertThat(proxyServer.requests().size(), greaterThanOrEqualTo(1));
1615+
assertIdp1MetadataParsedCorrectly(tuple.v2().get());
1616+
} finally {
1617+
tuple.v1().destroy();
1618+
}
1619+
}
1620+
}
1621+
1622+
public void testHttpMetadataWithDefaultTimeouts() throws Exception {
1623+
final Path path = getDataPath("idp1.xml");
1624+
final String body = Files.readString(path);
1625+
TestsSSLService sslService = buildTestSslService();
1626+
try (MockWebServer proxyServer = new MockWebServer(sslService.sslContext("xpack.security.http.ssl"), false)) {
1627+
proxyServer.start();
1628+
proxyServer.enqueue(new MockResponse().setResponseCode(200).setBody(body).addHeader("Content-Type", "application/xml"));
1629+
1630+
Tuple<RealmConfig, SSLService> config = buildConfig("https://localhost:" + proxyServer.getPort());
1631+
1632+
// Verify default timeout values are used
1633+
assertThat(config.v1().getSetting(SamlRealmSettings.IDP_METADATA_HTTP_CONNECT_TIMEOUT), equalTo(TimeValue.timeValueSeconds(5)));
1634+
assertThat(config.v1().getSetting(SamlRealmSettings.IDP_METADATA_HTTP_READ_TIMEOUT), equalTo(TimeValue.timeValueSeconds(10)));
1635+
1636+
final ResourceWatcherService watcherService = mock(ResourceWatcherService.class);
1637+
Tuple<AbstractReloadingMetadataResolver, Supplier<EntityDescriptor>> tuple = SamlRealm.initializeResolver(
1638+
logger,
1639+
config.v1(),
1640+
config.v2(),
1641+
watcherService
1642+
);
1643+
1644+
try {
1645+
assertThat(proxyServer.requests().size(), greaterThanOrEqualTo(1));
1646+
assertIdp1MetadataParsedCorrectly(tuple.v2().get());
1647+
} finally {
1648+
tuple.v1().destroy();
1649+
}
1650+
}
1651+
}
1652+
1653+
public void testHttpMetadataConnectionTimeout() throws Exception {
1654+
// Use a non-routable IP address to simulate connection timeout
1655+
// 192.0.2.1 is reserved for documentation and will not be routable
1656+
final String unreachableUrl = "https://192.0.2.1:9999/metadata.xml";
1657+
final TimeValue shortConnectTimeout = TimeValue.timeValueMillis(100);
1658+
1659+
Tuple<RealmConfig, SSLService> config = buildConfig(unreachableUrl, builder -> {
1660+
builder.put(
1661+
SingleSpSamlRealmSettings.getFullSettingKey(REALM_NAME, SamlRealmSettings.IDP_METADATA_HTTP_CONNECT_TIMEOUT),
1662+
shortConnectTimeout.getStringRep()
1663+
);
1664+
builder.put(SingleSpSamlRealmSettings.getFullSettingKey(REALM_NAME, SamlRealmSettings.IDP_METADATA_HTTP_FAIL_ON_ERROR), false);
1665+
});
1666+
1667+
final ResourceWatcherService watcherService = mock(ResourceWatcherService.class);
1668+
1669+
// initialization should complete even though the connection fails
1670+
Tuple<AbstractReloadingMetadataResolver, Supplier<EntityDescriptor>> tuple = SamlRealm.initializeResolver(
1671+
logger,
1672+
config.v1(),
1673+
config.v2(),
1674+
watcherService
1675+
);
1676+
1677+
try {
1678+
EntityDescriptor descriptor = tuple.v2().get();
1679+
assertThat(descriptor, instanceOf(UnresolvedEntity.class));
1680+
} finally {
1681+
tuple.v1().destroy();
1682+
}
1683+
}
1684+
15791685
private void assertIdp1MetadataParsedCorrectly(EntityDescriptor descriptor) {
15801686
try {
15811687
IDPSSODescriptor idpssoDescriptor = descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS);

0 commit comments

Comments
 (0)