KEYCLOAK-4175 Provide a way to set the connect and read timeout for ldap connections

This commit is contained in:
mposolda 2017-01-09 21:29:06 +01:00
parent be4f2aff63
commit c32620b718
13 changed files with 85 additions and 14 deletions

View file

@ -109,6 +109,14 @@ public class LDAPConfig {
return config.getFirst(LDAPConstants.CONNECTION_POOLING); return config.getFirst(LDAPConstants.CONNECTION_POOLING);
} }
public String getConnectionTimeout() {
return config.getFirst(LDAPConstants.CONNECTION_TIMEOUT);
}
public String getReadTimeout() {
return config.getFirst(LDAPConstants.READ_TIMEOUT);
}
public Properties getAdditionalConnectionProperties() { public Properties getAdditionalConnectionProperties() {
// not supported for now // not supported for now
return null; return null;

View file

@ -145,6 +145,12 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LD
.type(ProviderConfigProperty.BOOLEAN_TYPE) .type(ProviderConfigProperty.BOOLEAN_TYPE)
.defaultValue("true") .defaultValue("true")
.add() .add()
.property().name(LDAPConstants.CONNECTION_TIMEOUT)
.type(ProviderConfigProperty.STRING_TYPE)
.add()
.property().name(LDAPConstants.READ_TIMEOUT)
.type(ProviderConfigProperty.STRING_TYPE)
.add()
.property().name(LDAPConstants.PAGINATION) .property().name(LDAPConstants.PAGINATION)
.type(ProviderConfigProperty.BOOLEAN_TYPE) .type(ProviderConfigProperty.BOOLEAN_TYPE)
.defaultValue("true") .defaultValue("true")
@ -212,6 +218,25 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LD
LDAPConfig cfg = new LDAPConfig(config.getConfig()); LDAPConfig cfg = new LDAPConfig(config.getConfig());
String customFilter = cfg.getCustomUserSearchFilter(); String customFilter = cfg.getCustomUserSearchFilter();
LDAPUtils.validateCustomLdapFilter(customFilter); LDAPUtils.validateCustomLdapFilter(customFilter);
String connectionTimeout = cfg.getConnectionTimeout();
if (connectionTimeout != null && !connectionTimeout.isEmpty()) {
try {
Long.parseLong(connectionTimeout);
} catch (NumberFormatException nfe) {
throw new ComponentValidationException("ldapErrorConnectionTimeoutNotNumber");
}
}
String readTimeout = cfg.getReadTimeout();
if (readTimeout != null && !readTimeout.isEmpty()) {
try {
Long.parseLong(readTimeout);
} catch (NumberFormatException nfe) {
throw new ComponentValidationException("ldapErrorReadTimeoutNotNumber");
}
}
} }
@Override @Override

View file

@ -507,6 +507,16 @@ public class LDAPOperationManager {
env.put("com.sun.jndi.ldap.connect.pool", connectionPooling); env.put("com.sun.jndi.ldap.connect.pool", connectionPooling);
} }
String connectionTimeout = config.getConnectionTimeout();
if (connectionTimeout != null && !connectionTimeout.isEmpty()) {
env.put("com.sun.jndi.ldap.connect.timeout", connectionTimeout);
}
String readTimeout = config.getReadTimeout();
if (readTimeout != null && !readTimeout.isEmpty()) {
env.put("com.sun.jndi.ldap.read.timeout", readTimeout);
}
// Just dump the additional properties // Just dump the additional properties
Properties additionalProperties = this.config.getAdditionalConnectionProperties(); Properties additionalProperties = this.config.getAdditionalConnectionProperties();
if (additionalProperties != null) { if (additionalProperties != null) {

View file

@ -177,7 +177,7 @@ public interface RealmResource {
@NoCache @NoCache
Response testLDAPConnection(@QueryParam("action") String action, @QueryParam("connectionUrl") String connectionUrl, Response testLDAPConnection(@QueryParam("action") String action, @QueryParam("connectionUrl") String connectionUrl,
@QueryParam("bindDn") String bindDn, @QueryParam("bindCredential") String bindCredential, @QueryParam("bindDn") String bindDn, @QueryParam("bindCredential") String bindCredential,
@QueryParam("useTruststoreSpi") String useTruststoreSpi); @QueryParam("useTruststoreSpi") String useTruststoreSpi, @QueryParam("connectionTimeout") String connectionTimeout);
@Path("clear-realm-cache") @Path("clear-realm-cache")
@POST @POST

View file

@ -57,6 +57,8 @@ public class LDAPConstants {
public static final String SEARCH_SCOPE = "searchScope"; public static final String SEARCH_SCOPE = "searchScope";
public static final String CONNECTION_POOLING = "connectionPooling"; public static final String CONNECTION_POOLING = "connectionPooling";
public static final String CONNECTION_TIMEOUT = "connectionTimeout";
public static final String READ_TIMEOUT = "readTimeout";
public static final String PAGINATION = "pagination"; public static final String PAGINATION = "pagination";
public static final String EDIT_MODE = "editMode"; public static final String EDIT_MODE = "editMode";

View file

@ -35,7 +35,7 @@ public class LDAPConnectionTestManager {
public static final String TEST_CONNECTION = "testConnection"; public static final String TEST_CONNECTION = "testConnection";
public static final String TEST_AUTHENTICATION = "testAuthentication"; public static final String TEST_AUTHENTICATION = "testAuthentication";
public boolean testLDAP(String action, String connectionUrl, String bindDn, String bindCredential, String useTruststoreSpi) { public boolean testLDAP(String action, String connectionUrl, String bindDn, String bindCredential, String useTruststoreSpi, String connectionTimeout) {
if (!TEST_CONNECTION.equals(action) && !TEST_AUTHENTICATION.equals(action)) { if (!TEST_CONNECTION.equals(action) && !TEST_AUTHENTICATION.equals(action)) {
ServicesLogger.LOGGER.unknownAction(action); ServicesLogger.LOGGER.unknownAction(action);
return false; return false;
@ -70,6 +70,10 @@ public class LDAPConnectionTestManager {
LDAPConstants.setTruststoreSpiIfNeeded(useTruststoreSpi, connectionUrl, env); LDAPConstants.setTruststoreSpiIfNeeded(useTruststoreSpi, connectionUrl, env);
if (connectionTimeout != null && !connectionTimeout.isEmpty()) {
env.put("com.sun.jndi.ldap.connect.timeout", connectionTimeout);
}
ldapContext = new InitialLdapContext(env, null); ldapContext = new InitialLdapContext(env, null);
return true; return true;
} catch (Exception ne) { } catch (Exception ne) {

View file

@ -750,14 +750,15 @@ public class RealmAdminResource {
@NoCache @NoCache
public Response testLDAPConnection(@QueryParam("action") String action, @QueryParam("connectionUrl") String connectionUrl, public Response testLDAPConnection(@QueryParam("action") String action, @QueryParam("connectionUrl") String connectionUrl,
@QueryParam("bindDn") String bindDn, @QueryParam("bindCredential") String bindCredential, @QueryParam("bindDn") String bindDn, @QueryParam("bindCredential") String bindCredential,
@QueryParam("useTruststoreSpi") String useTruststoreSpi, @QueryParam("componentId") String componentId) { @QueryParam("useTruststoreSpi") String useTruststoreSpi, @QueryParam("connectionTimeout") String connectionTimeout,
@QueryParam("componentId") String componentId) {
auth.init(RealmAuth.Resource.REALM).requireManage(); auth.init(RealmAuth.Resource.REALM).requireManage();
if (componentId != null && bindCredential.equals(ComponentRepresentation.SECRET_VALUE)) { if (componentId != null && bindCredential.equals(ComponentRepresentation.SECRET_VALUE)) {
bindCredential = realm.getComponent(componentId).getConfig().getFirst(LDAPConstants.BIND_CREDENTIAL); bindCredential = realm.getComponent(componentId).getConfig().getFirst(LDAPConstants.BIND_CREDENTIAL);
} }
boolean result = new LDAPConnectionTestManager().testLDAP(action, connectionUrl, bindDn, bindCredential, useTruststoreSpi); boolean result = new LDAPConnectionTestManager().testLDAP(action, connectionUrl, bindDn, bindCredential, useTruststoreSpi, connectionTimeout);
return result ? Response.noContent().build() : ErrorResponse.error("LDAP test error", Response.Status.BAD_REQUEST); return result ? Response.noContent().build() : ErrorResponse.error("LDAP test error", Response.Status.BAD_REQUEST);
} }

View file

@ -271,7 +271,7 @@ public class PermissionsTest extends AbstractKeycloakTest {
invoke(new InvocationWithResponse() { invoke(new InvocationWithResponse() {
public void invoke(RealmResource realm, AtomicReference<Response> response) { public void invoke(RealmResource realm, AtomicReference<Response> response) {
response.set(realm.testLDAPConnection("nosuch", "nosuch", "nosuch", "nosuch", "nosuch")); response.set(realm.testLDAPConnection("nosuch", "nosuch", "nosuch", "nosuch", "nosuch", "nosuch"));
} }
}, Resource.REALM, true); }, Resource.REALM, true);

View file

@ -36,23 +36,23 @@ public class UserFederationLdapConnectionTest extends AbstractAdminTest {
@Test @Test
public void testLdapConnections1() { public void testLdapConnections1() {
// Unknown action // Unknown action
Response response = realm.testLDAPConnection("unknown", "ldap://localhost:10389", "foo", "bar", "false"); Response response = realm.testLDAPConnection("unknown", "ldap://localhost:10389", "foo", "bar", "false", null);
assertStatus(response, 400); assertStatus(response, 400);
// Bad host // Bad host
response = realm.testLDAPConnection(LDAPConnectionTestManager.TEST_CONNECTION, "ldap://localhostt:10389", "foo", "bar", "false"); response = realm.testLDAPConnection(LDAPConnectionTestManager.TEST_CONNECTION, "ldap://localhostt:10389", "foo", "bar", "false", null);
assertStatus(response, 400); assertStatus(response, 400);
// Connection success // Connection success
response = realm.testLDAPConnection(LDAPConnectionTestManager.TEST_CONNECTION, "ldap://localhost:10389", "foo", "bar", "false"); response = realm.testLDAPConnection(LDAPConnectionTestManager.TEST_CONNECTION, "ldap://localhost:10389", "foo", "bar", "false", null);
assertStatus(response, 204); assertStatus(response, 204);
// Bad authentication // Bad authentication
response = realm.testLDAPConnection(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldap://localhost:10389", "foo", "bar", "false"); response = realm.testLDAPConnection(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldap://localhost:10389", "foo", "bar", "false", "10000");
assertStatus(response, 400); assertStatus(response, 400);
// Authentication success // Authentication success
response = realm.testLDAPConnection(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldap://localhost:10389", "uid=admin,ou=system", "secret", "false"); response = realm.testLDAPConnection(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldap://localhost:10389", "uid=admin,ou=system", "secret", "false", null);
assertStatus(response, 204); assertStatus(response, 204);
} }
@ -60,16 +60,16 @@ public class UserFederationLdapConnectionTest extends AbstractAdminTest {
@Test @Test
public void testLdapConnectionsSsl() { public void testLdapConnectionsSsl() {
Response response = realm.testLDAPConnection(LDAPConnectionTestManager.TEST_CONNECTION, "ldaps://localhost:10636", "foo", "bar", "false"); Response response = realm.testLDAPConnection(LDAPConnectionTestManager.TEST_CONNECTION, "ldaps://localhost:10636", "foo", "bar", "false", null);
assertStatus(response, 204); assertStatus(response, 204);
response = realm.testLDAPConnection(LDAPConnectionTestManager.TEST_CONNECTION, "ldaps://localhostt:10636", "foo", "bar", "false"); response = realm.testLDAPConnection(LDAPConnectionTestManager.TEST_CONNECTION, "ldaps://localhostt:10636", "foo", "bar", "false", null);
assertStatus(response, 400); assertStatus(response, 400);
response = realm.testLDAPConnection(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldaps://localhost:10636", "foo", "bar", "false"); response = realm.testLDAPConnection(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldaps://localhost:10636", "foo", "bar", "false", null);
assertStatus(response, 400); assertStatus(response, 400);
response = realm.testLDAPConnection(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldaps://localhost:10636", "uid=admin,ou=system", "secret", "true"); response = realm.testLDAPConnection(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldaps://localhost:10636", "uid=admin,ou=system", "secret", "true", null);
assertStatus(response, 204); assertStatus(response, 204);
} }

View file

@ -744,6 +744,10 @@ ldap.search-scope.tooltip=For one level, we search for users just in DNs specifi
use-truststore-spi=Use Truststore SPI use-truststore-spi=Use Truststore SPI
ldap.use-truststore-spi.tooltip=Specifies whether LDAP connection will use the truststore SPI with the truststore configured in standalone.xml/domain.xml. 'Always' means that it will always use it. 'Never' means that it won't use it. 'Only for ldaps' means that it will use if your connection URL use ldaps. Note even if standalone.xml/domain.xml is not configured, the default Java cacerts or certificate specified by 'javax.net.ssl.trustStore' property will be used. ldap.use-truststore-spi.tooltip=Specifies whether LDAP connection will use the truststore SPI with the truststore configured in standalone.xml/domain.xml. 'Always' means that it will always use it. 'Never' means that it won't use it. 'Only for ldaps' means that it will use if your connection URL use ldaps. Note even if standalone.xml/domain.xml is not configured, the default Java cacerts or certificate specified by 'javax.net.ssl.trustStore' property will be used.
connection-pooling=Connection Pooling connection-pooling=Connection Pooling
ldap-connection-timeout=Connection Timeout
ldap.connection-timeout.tooltip=LDAP Connection Timeout in milliseconds
ldap-read-timeout=Read Timeout
ldap.read-timeout.tooltip=LDAP Read Timeout in milliseconds. This timeout applies for LDAP read operations
ldap.connection-pooling.tooltip=Does Keycloak should use connection pooling for accessing LDAP server ldap.connection-pooling.tooltip=Does Keycloak should use connection pooling for accessing LDAP server
ldap.pagination.tooltip=Does the LDAP server support pagination. ldap.pagination.tooltip=Does the LDAP server support pagination.
kerberos-integration=Kerberos Integration kerberos-integration=Kerberos Integration

View file

@ -8,6 +8,8 @@ invalidPasswordRegexPatternMessage=Invalid password: fails to match regex patter
invalidPasswordHistoryMessage=Invalid password: must not be equal to any of last {0} passwords. invalidPasswordHistoryMessage=Invalid password: must not be equal to any of last {0} passwords.
ldapErrorInvalidCustomFilter=Custom configured LDAP filter does not start with "(" or does not end with ")". ldapErrorInvalidCustomFilter=Custom configured LDAP filter does not start with "(" or does not end with ")".
ldapErrorConnectionTimeoutNotNumber=Connection Timeout must be a number
ldapErrorReadTimeoutNotNumber=Read Timeout must be a number
ldapErrorMissingClientId=Client ID needs to be provided in config when Realm Roles Mapping is not used. ldapErrorMissingClientId=Client ID needs to be provided in config when Realm Roles Mapping is not used.
ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType=Not possible to preserve group inheritance and use UID membership type together. ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType=Not possible to preserve group inheritance and use UID membership type together.
ldapErrorCantWriteOnlyForReadOnlyLdap=Can't set write only when LDAP provider mode is not WRITABLE ldapErrorCantWriteOnlyForReadOnlyLdap=Can't set write only when LDAP provider mode is not WRITABLE

View file

@ -1262,6 +1262,7 @@ module.controller('LDAPUserStorageCtrl', function($scope, $location, Notificatio
bindDn: ldapConfig.bindDn, bindDn: ldapConfig.bindDn,
bindCredential: ldapConfig.bindCredential, bindCredential: ldapConfig.bindCredential,
useTruststoreSpi: ldapConfig.useTruststoreSpi, useTruststoreSpi: ldapConfig.useTruststoreSpi,
connectionTimeout: ldapConfig.connectionTimeout,
componentId: instance.id componentId: instance.id
}; };
}; };

View file

@ -186,6 +186,20 @@
</div> </div>
<kc-tooltip>{{:: 'ldap.connection-pooling.tooltip' | translate}}</kc-tooltip> <kc-tooltip>{{:: 'ldap.connection-pooling.tooltip' | translate}}</kc-tooltip>
</div> </div>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="connectionTimeout">{{:: 'ldap-connection-timeout' | translate}}</label>
<div class="col-md-6">
<input class="form-control" id="connectionTimeout" type="text" ng-model="instance.config['connectionTimeout'][0]" placeholder="{{:: 'ldap-connection-timeout' | translate}}">
</div>
<kc-tooltip>{{:: 'ldap.connection-timeout.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="readTimeout">{{:: 'ldap-read-timeout' | translate}}</label>
<div class="col-md-6">
<input class="form-control" id="readTimeout" type="text" ng-model="instance.config['readTimeout'][0]" placeholder="{{:: 'ldap-read-timeout' | translate}}">
</div>
<kc-tooltip>{{:: 'ldap.read-timeout.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix"> <div class="form-group clearfix">
<label class="col-md-2 control-label" for="pagination">{{:: 'pagination' | translate}}</label> <label class="col-md-2 control-label" for="pagination">{{:: 'pagination' | translate}}</label>
<div class="col-md-6"> <div class="col-md-6">