Skip to content

Commit 7607cb3

Browse files
authored
Backport/saml timeout settings (#136977)
* Configurable HTTP read and connect timeouts for url based SAML metadata resolution (#136058) * Fix + tests for #133542 * [CI] Auto commit changes from spotless * Update docs/changelog/136058.yaml * update summary --------- Co-authored-by: elasticsearchmachine <[email protected]> (cherry picked from commit ddf9b68) * fix merge * fix merge
1 parent 587acce commit 7607cb3

File tree

4 files changed

+143
-1
lines changed

4 files changed

+143
-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: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,18 @@ public class SamlRealmSettings {
6363
key -> Setting.boolSetting(key, false, Setting.Property.NodeScope)
6464
);
6565

66+
public static final Setting.AffixSetting<TimeValue> IDP_METADATA_HTTP_CONNECT_TIMEOUT = Setting.affixKeySetting(
67+
RealmSettings.realmSettingPrefix(TYPE),
68+
IDP_METADATA_SETTING_PREFIX + "http.connect_timeout",
69+
key -> Setting.timeSetting(key, TimeValue.timeValueSeconds(5), Setting.Property.NodeScope)
70+
);
71+
72+
public static final Setting.AffixSetting<TimeValue> IDP_METADATA_HTTP_READ_TIMEOUT = Setting.affixKeySetting(
73+
RealmSettings.realmSettingPrefix(TYPE),
74+
IDP_METADATA_SETTING_PREFIX + "http.read_timeout",
75+
key -> Setting.timeSetting(key, TimeValue.timeValueSeconds(10), Setting.Property.NodeScope)
76+
);
77+
6678
public static final Setting.AffixSetting<Boolean> IDP_SINGLE_LOGOUT = Setting.affixKeySetting(
6779
RealmSettings.realmSettingPrefix(TYPE),
6880
"idp.use_single_logout",
@@ -200,6 +212,8 @@ public static Set<Setting.AffixSetting<?>> getSettings() {
200212
IDP_METADATA_HTTP_REFRESH,
201213
IDP_METADATA_HTTP_MIN_REFRESH,
202214
IDP_METADATA_HTTP_FAIL_ON_ERROR,
215+
IDP_METADATA_HTTP_CONNECT_TIMEOUT,
216+
IDP_METADATA_HTTP_READ_TIMEOUT,
203217
IDP_SINGLE_LOGOUT,
204218
SP_ENTITY_ID,
205219
SP_ACS,

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;
@@ -120,8 +121,10 @@
120121
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.FORCE_AUTHN;
121122
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.GROUPS_ATTRIBUTE;
122123
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_ENTITY_ID;
124+
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_METADATA_HTTP_CONNECT_TIMEOUT;
123125
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_METADATA_HTTP_FAIL_ON_ERROR;
124126
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_METADATA_HTTP_MIN_REFRESH;
127+
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_METADATA_HTTP_READ_TIMEOUT;
125128
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_METADATA_HTTP_REFRESH;
126129
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_METADATA_PATH;
127130
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_SINGLE_LOGOUT;
@@ -701,6 +704,14 @@ private static Tuple<AbstractReloadingMetadataResolver, Supplier<EntityDescripto
701704
SSLConnectionSocketFactory factory = new SSLConnectionSocketFactory(sslService.sslSocketFactory(sslConfiguration), verifier);
702705
builder.setSSLSocketFactory(factory);
703706

707+
TimeValue connectTimeout = config.getSetting(IDP_METADATA_HTTP_CONNECT_TIMEOUT);
708+
TimeValue readTimeout = config.getSetting(IDP_METADATA_HTTP_READ_TIMEOUT);
709+
RequestConfig requestConfig = RequestConfig.custom()
710+
.setConnectTimeout(Math.toIntExact(connectTimeout.millis()))
711+
.setSocketTimeout(Math.toIntExact(readTimeout.millis()))
712+
.build();
713+
builder.setDefaultRequestConfig(requestConfig);
714+
704715
TimeValue maxRefresh = config.getSetting(IDP_METADATA_HTTP_REFRESH);
705716
TimeValue minRefresh = config.getSetting(IDP_METADATA_HTTP_MIN_REFRESH);
706717
if (minRefresh.compareTo(maxRefresh) > 0) {
@@ -795,7 +806,13 @@ private static Tuple<AbstractReloadingMetadataResolver, Supplier<EntityDescripto
795806
final Path path = config.env().configDir().resolve(metadataPath);
796807
final FilesystemMetadataResolver resolver = new SamlFilesystemMetadataResolver(path.toFile());
797808

798-
for (var httpSetting : List.of(IDP_METADATA_HTTP_REFRESH, IDP_METADATA_HTTP_MIN_REFRESH, IDP_METADATA_HTTP_FAIL_ON_ERROR)) {
809+
for (var httpSetting : List.of(
810+
IDP_METADATA_HTTP_REFRESH,
811+
IDP_METADATA_HTTP_MIN_REFRESH,
812+
IDP_METADATA_HTTP_FAIL_ON_ERROR,
813+
IDP_METADATA_HTTP_CONNECT_TIMEOUT,
814+
IDP_METADATA_HTTP_READ_TIMEOUT
815+
)) {
799816
if (config.hasSetting(httpSetting)) {
800817
logger.info(
801818
"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
@@ -1372,6 +1372,112 @@ private TestsSSLService buildTestSslService() {
13721372
return new TestsSSLService(TestEnvironment.newEnvironment(settings));
13731373
}
13741374

1375+
public void testHttpMetadataWithCustomTimeouts() throws Exception {
1376+
final Path path = getDataPath("idp1.xml");
1377+
final String body = Files.readString(path);
1378+
TestsSSLService sslService = buildTestSslService();
1379+
try (MockWebServer proxyServer = new MockWebServer(sslService.sslContext("xpack.security.http.ssl"), false)) {
1380+
proxyServer.start();
1381+
proxyServer.enqueue(new MockResponse().setResponseCode(200).setBody(body).addHeader("Content-Type", "application/xml"));
1382+
1383+
final TimeValue customConnectTimeout = TimeValue.timeValueSeconds(3);
1384+
final TimeValue customReadTimeout = TimeValue.timeValueSeconds(15);
1385+
1386+
Tuple<RealmConfig, SSLService> config = buildConfig("https://localhost:" + proxyServer.getPort(), builder -> {
1387+
builder.put(
1388+
getFullSettingKey(REALM_NAME, SamlRealmSettings.IDP_METADATA_HTTP_CONNECT_TIMEOUT),
1389+
customConnectTimeout.getStringRep()
1390+
);
1391+
builder.put(
1392+
getFullSettingKey(REALM_NAME, SamlRealmSettings.IDP_METADATA_HTTP_READ_TIMEOUT),
1393+
customReadTimeout.getStringRep()
1394+
);
1395+
});
1396+
1397+
// Verify settings are correctly configured
1398+
assertThat(config.v1().getSetting(SamlRealmSettings.IDP_METADATA_HTTP_CONNECT_TIMEOUT), equalTo(customConnectTimeout));
1399+
assertThat(config.v1().getSetting(SamlRealmSettings.IDP_METADATA_HTTP_READ_TIMEOUT), equalTo(customReadTimeout));
1400+
1401+
final ResourceWatcherService watcherService = mock(ResourceWatcherService.class);
1402+
Tuple<AbstractReloadingMetadataResolver, Supplier<EntityDescriptor>> tuple = SamlRealm.initializeResolver(
1403+
logger,
1404+
config.v1(),
1405+
config.v2(),
1406+
watcherService
1407+
);
1408+
1409+
try {
1410+
assertThat(proxyServer.requests().size(), greaterThanOrEqualTo(1));
1411+
assertIdp1MetadataParsedCorrectly(tuple.v2().get());
1412+
} finally {
1413+
tuple.v1().destroy();
1414+
}
1415+
}
1416+
}
1417+
1418+
public void testHttpMetadataWithDefaultTimeouts() throws Exception {
1419+
final Path path = getDataPath("idp1.xml");
1420+
final String body = Files.readString(path);
1421+
TestsSSLService sslService = buildTestSslService();
1422+
try (MockWebServer proxyServer = new MockWebServer(sslService.sslContext("xpack.security.http.ssl"), false)) {
1423+
proxyServer.start();
1424+
proxyServer.enqueue(new MockResponse().setResponseCode(200).setBody(body).addHeader("Content-Type", "application/xml"));
1425+
1426+
Tuple<RealmConfig, SSLService> config = buildConfig("https://localhost:" + proxyServer.getPort());
1427+
1428+
// Verify default timeout values are used
1429+
assertThat(config.v1().getSetting(SamlRealmSettings.IDP_METADATA_HTTP_CONNECT_TIMEOUT), equalTo(TimeValue.timeValueSeconds(5)));
1430+
assertThat(config.v1().getSetting(SamlRealmSettings.IDP_METADATA_HTTP_READ_TIMEOUT), equalTo(TimeValue.timeValueSeconds(10)));
1431+
1432+
final ResourceWatcherService watcherService = mock(ResourceWatcherService.class);
1433+
Tuple<AbstractReloadingMetadataResolver, Supplier<EntityDescriptor>> tuple = SamlRealm.initializeResolver(
1434+
logger,
1435+
config.v1(),
1436+
config.v2(),
1437+
watcherService
1438+
);
1439+
1440+
try {
1441+
assertThat(proxyServer.requests().size(), greaterThanOrEqualTo(1));
1442+
assertIdp1MetadataParsedCorrectly(tuple.v2().get());
1443+
} finally {
1444+
tuple.v1().destroy();
1445+
}
1446+
}
1447+
}
1448+
1449+
public void testHttpMetadataConnectionTimeout() throws Exception {
1450+
// Use a non-routable IP address to simulate connection timeout
1451+
// 192.0.2.1 is reserved for documentation and will not be routable
1452+
final String unreachableUrl = "https://192.0.2.1:9999/metadata.xml";
1453+
final TimeValue shortConnectTimeout = TimeValue.timeValueMillis(100);
1454+
1455+
Tuple<RealmConfig, SSLService> config = buildConfig(unreachableUrl, builder -> {
1456+
builder.put(
1457+
getFullSettingKey(REALM_NAME, SamlRealmSettings.IDP_METADATA_HTTP_CONNECT_TIMEOUT),
1458+
shortConnectTimeout.getStringRep()
1459+
);
1460+
builder.put(getFullSettingKey(REALM_NAME, SamlRealmSettings.IDP_METADATA_HTTP_FAIL_ON_ERROR), false);
1461+
});
1462+
1463+
final ResourceWatcherService watcherService = mock(ResourceWatcherService.class);
1464+
1465+
// initialization should complete even though the connection fails
1466+
Tuple<AbstractReloadingMetadataResolver, Supplier<EntityDescriptor>> tuple = SamlRealm.initializeResolver(
1467+
logger,
1468+
config.v1(),
1469+
config.v2(),
1470+
watcherService
1471+
);
1472+
1473+
try {
1474+
EntityDescriptor descriptor = tuple.v2().get();
1475+
assertThat(descriptor, instanceOf(UnresolvedEntity.class));
1476+
} finally {
1477+
tuple.v1().destroy();
1478+
}
1479+
}
1480+
13751481
private void assertIdp1MetadataParsedCorrectly(EntityDescriptor descriptor) {
13761482
try {
13771483
IDPSSODescriptor idpssoDescriptor = descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS);

0 commit comments

Comments
 (0)