Support for test of LDAP connection and authentication

This commit is contained in:
mposolda 2014-06-23 23:19:15 +02:00
parent 159a5e20bf
commit b1cfab34fd
14 changed files with 142 additions and 33 deletions

View file

@ -168,7 +168,7 @@ module.config([ '$routeProvider', function($routeProvider) {
return RealmLoader(); return RealmLoader();
} }
}, },
controller : 'RealmLdapSettingsCtrl' controller : 'RealmLDAPSettingsCtrl'
}) })
.when('/realms/:realm/audit', { .when('/realms/:realm/audit', {
templateUrl : 'partials/realm-audit.html', templateUrl : 'partials/realm-audit.html',

View file

@ -896,8 +896,8 @@ module.controller('RealmSMTPSettingsCtrl', function($scope, Current, Realm, real
} }
}); });
module.controller('RealmLdapSettingsCtrl', function($scope, $location, Notifications, Realm, realm) { module.controller('RealmLDAPSettingsCtrl', function($scope, $location, Notifications, Realm, realm, RealmLDAPConnectionTester) {
console.log('RealmLdapSettingsCtrl'); console.log('RealmLDAPSettingsCtrl');
$scope.ldapVendors = [ $scope.ldapVendors = [
{ "id": "ad", "name": "Active Directory" }, { "id": "ad", "name": "Active Directory" },
@ -943,6 +943,34 @@ module.controller('RealmLdapSettingsCtrl', function($scope, $location, Notificat
$scope.changed = false; $scope.changed = false;
$scope.lastVendor = $scope.realm.ldapServer.vendor; $scope.lastVendor = $scope.realm.ldapServer.vendor;
}; };
var initConnectionTest = function(testAction, ldapConfig) {
return {
action: testAction,
realm: $scope.realm.realm,
connectionUrl: ldapConfig.connectionUrl,
bindDn: ldapConfig.bindDn,
bindCredential: ldapConfig.bindCredential
};
};
$scope.testConnection = function() {
console.log('RealmLDAPSettingsCtrl: testConnection');
RealmLDAPConnectionTester.get(initConnectionTest("testConnection", $scope.realm.ldapServer), function() {
Notifications.success("LDAP connection successful.");
}, function() {
Notifications.error("Error when trying to connect to LDAP. See server.log for details.");
});
}
$scope.testAuthentication = function() {
console.log('RealmLDAPSettingsCtrl: testAuthentication');
RealmLDAPConnectionTester.get(initConnectionTest("testAuthentication", $scope.realm.ldapServer), function() {
Notifications.success("LDAP authentication successful.");
}, function() {
Notifications.error("LDAP authentication failed. See server.log for details");
});
}
}); });
module.controller('RealmAuthSettingsCtrl', function($scope, realm) { module.controller('RealmAuthSettingsCtrl', function($scope, realm) {

View file

@ -180,6 +180,10 @@ module.factory('RealmAuditEvents', function($resource) {
}); });
}); });
module.factory('RealmLDAPConnectionTester', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/testLDAPConnection');
});
module.factory('ServerInfo', function($resource) { module.factory('ServerInfo', function($resource) {
return $resource(authUrl + '/admin/serverinfo'); return $resource(authUrl + '/admin/serverinfo');
}); });

View file

@ -40,6 +40,9 @@
<div class="col-sm-4"> <div class="col-sm-4">
<input class="form-control" id="ldapConnectionUrl" type="text" ng-model="realm.ldapServer.connectionUrl" placeholder="LDAP connection URL" required> <input class="form-control" id="ldapConnectionUrl" type="text" ng-model="realm.ldapServer.connectionUrl" placeholder="LDAP connection URL" required>
</div> </div>
<div class="col-sm-4" data-ng-show="access.manageRealm">
<a class="btn btn-primary" data-ng-click="testConnection()">Test connection</a>
</div>
</div> </div>
<div class="form-group clearfix"> <div class="form-group clearfix">
<label class="col-sm-2 control-label" for="ldapBaseDn">Base DN <span class="required">*</span></label> <label class="col-sm-2 control-label" for="ldapBaseDn">Base DN <span class="required">*</span></label>
@ -64,6 +67,9 @@
<div class="col-sm-4"> <div class="col-sm-4">
<input class="form-control" id="ldapBindCredential" type="text" ng-model="realm.ldapServer.bindCredential" placeholder="LDAP Bind Credentials" required> <input class="form-control" id="ldapBindCredential" type="text" ng-model="realm.ldapServer.bindCredential" placeholder="LDAP Bind Credentials" required>
</div> </div>
<div class="col-sm-4" data-ng-show="access.manageRealm">
<a class="btn btn-primary" data-ng-click="testAuthentication()">Test authentication</a>
</div>
</div> </div>
</fieldset> </fieldset>

View file

@ -1,9 +1,9 @@
package org.keycloak.picketlink.idm; package org.keycloak.models;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public class LdapConstants { public class LDAPConstants {
public static final String VENDOR = "vendor"; public static final String VENDOR = "vendor";
public static final String VENDOR_RHDS = "rhds"; public static final String VENDOR_RHDS = "rhds";

View file

@ -17,8 +17,8 @@ import javax.naming.NamingException;
import javax.naming.directory.DirContext; import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext; import javax.naming.directory.InitialDirContext;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.picketlink.idm.LdapConstants;
import org.picketbox.test.ldap.AbstractLDAPTest; import org.picketbox.test.ldap.AbstractLDAPTest;
/** /**
@ -130,12 +130,12 @@ public class LDAPEmbeddedServer extends AbstractLDAPTest {
public void setupLdapInRealm(RealmModel realm) { public void setupLdapInRealm(RealmModel realm) {
Map<String,String> ldapConfig = new HashMap<String,String>(); Map<String,String> ldapConfig = new HashMap<String,String>();
ldapConfig.put(LdapConstants.CONNECTION_URL, getConnectionUrl()); ldapConfig.put(LDAPConstants.CONNECTION_URL, getConnectionUrl());
ldapConfig.put(LdapConstants.BASE_DN, getBaseDn()); ldapConfig.put(LDAPConstants.BASE_DN, getBaseDn());
ldapConfig.put(LdapConstants.BIND_DN, getBindDn()); ldapConfig.put(LDAPConstants.BIND_DN, getBindDn());
ldapConfig.put(LdapConstants.BIND_CREDENTIAL, getBindCredential()); ldapConfig.put(LDAPConstants.BIND_CREDENTIAL, getBindCredential());
ldapConfig.put(LdapConstants.USER_DN_SUFFIX, getUserDnSuffix()); ldapConfig.put(LDAPConstants.USER_DN_SUFFIX, getUserDnSuffix());
ldapConfig.put(LdapConstants.VENDOR, getVendor()); ldapConfig.put(LDAPConstants.VENDOR, getVendor());
realm.setLdapServerConfig(ldapConfig); realm.setLdapServerConfig(ldapConfig);
} }

View file

@ -12,7 +12,7 @@ import org.picketlink.idm.model.basic.User;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public class LdapTestUtils { public class LDAPTestUtils {
public static void setLdapPassword(ProviderSession providerSession, RealmModel realm, String username, String password) { public static void setLdapPassword(ProviderSession providerSession, RealmModel realm, String username, String password) {
// Update password directly in ldap. It's workaround, but LDIF import doesn't seem to work on windows for ApacheDS // Update password directly in ldap. It's workaround, but LDIF import doesn't seem to work on windows for ApacheDS

View file

@ -5,8 +5,6 @@ import java.util.Collections;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.junit.After;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
@ -78,7 +76,7 @@ public class AuthProvidersLDAPTest extends AbstractModelTest {
MultivaluedMap<String, String> formData = AuthProvidersExternalModelTest.createFormData("johnkeycloak", "password"); MultivaluedMap<String, String> formData = AuthProvidersExternalModelTest.createFormData("johnkeycloak", "password");
// Set password of user in LDAP // Set password of user in LDAP
LdapTestUtils.setLdapPassword(providerSession, realm, "johnkeycloak", "password"); LDAPTestUtils.setLdapPassword(providerSession, realm, "johnkeycloak", "password");
// Verify that user doesn't exists in realm2 and can't authenticate here // Verify that user doesn't exists in realm2 and can't authenticate here
Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_USER, am.authenticateForm(null, realm, formData)); Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_USER, am.authenticateForm(null, realm, formData));
@ -142,7 +140,7 @@ public class AuthProvidersLDAPTest extends AbstractModelTest {
// Add ldap // Add ldap
setupAuthenticationProviders(); setupAuthenticationProviders();
LdapTestUtils.setLdapPassword(providerSession, realm, "johnkeycloak", "password"); LDAPTestUtils.setLdapPassword(providerSession, realm, "johnkeycloak", "password");
// First authenticate successfully to sync john into realm // First authenticate successfully to sync john into realm
MultivaluedMap<String, String> formData = AuthProvidersExternalModelTest.createFormData("johnkeycloak", "password"); MultivaluedMap<String, String> formData = AuthProvidersExternalModelTest.createFormData("johnkeycloak", "password");

View file

@ -22,7 +22,6 @@ import static org.picketlink.common.constants.LDAPConstants.EQUAL;
*/ */
public class KeycloakLDAPIdentityStore extends LDAPIdentityStore { public class KeycloakLDAPIdentityStore extends LDAPIdentityStore {
public static Method GET_BINDING_DN_METHOD;
public static Method GET_OPERATION_MANAGER_METHOD; public static Method GET_OPERATION_MANAGER_METHOD;
public static Method CREATE_SEARCH_FILTER_METHOD; public static Method CREATE_SEARCH_FILTER_METHOD;
public static Method EXTRACT_ATTRIBUTES_METHOD; public static Method EXTRACT_ATTRIBUTES_METHOD;
@ -31,7 +30,6 @@ public class KeycloakLDAPIdentityStore extends LDAPIdentityStore {
public static final String SAM_ACCOUNT_NAME = "sAMAccountName"; public static final String SAM_ACCOUNT_NAME = "sAMAccountName";
static { static {
GET_BINDING_DN_METHOD = getMethodOnLDAPStore("getBindingDN", AttributedType.class);
GET_OPERATION_MANAGER_METHOD = getMethodOnLDAPStore("getOperationManager"); GET_OPERATION_MANAGER_METHOD = getMethodOnLDAPStore("getOperationManager");
CREATE_SEARCH_FILTER_METHOD = getMethodOnLDAPStore("createIdentityTypeSearchFilter", IdentityQuery.class, LDAPMappingConfiguration.class); CREATE_SEARCH_FILTER_METHOD = getMethodOnLDAPStore("createIdentityTypeSearchFilter", IdentityQuery.class, LDAPMappingConfiguration.class);
EXTRACT_ATTRIBUTES_METHOD = getMethodOnLDAPStore("extractAttributes", AttributedType.class, boolean.class); EXTRACT_ATTRIBUTES_METHOD = getMethodOnLDAPStore("extractAttributes", AttributedType.class, boolean.class);

View file

@ -143,7 +143,7 @@ public class LDAPKeycloakCredentialHandler extends LDAPPlainTextPasswordCredenti
} }
} }
// TODO: remove later... It's needed just because LDAPIdentityStore.getBindingName, which always uses idProperty as first part of DN, but in AD it doesn't work as we may have idProperty 'sAMAccountName' // TODO: remove later... It's needed just because LDAPIdentityStore.getBindingDN, which always uses idProperty as first part of DN, but in AD it doesn't work as we may have idProperty 'sAMAccountName'
// but DN like: cn=John Doe,OU=foo,DC=bar // but DN like: cn=John Doe,OU=foo,DC=bar
protected String getDNOfUser(User user, IdentityManager identityManager, LDAPIdentityStore ldapStore, LDAPOperationManager operationManager) { protected String getDNOfUser(User user, IdentityManager identityManager, LDAPIdentityStore ldapStore, LDAPOperationManager operationManager) {

View file

@ -6,10 +6,10 @@ import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.picketlink.idm.KeycloakLDAPIdentityStore; import org.keycloak.picketlink.idm.KeycloakLDAPIdentityStore;
import org.keycloak.picketlink.idm.LDAPKeycloakCredentialHandler; import org.keycloak.picketlink.idm.LDAPKeycloakCredentialHandler;
import org.keycloak.picketlink.idm.LdapConstants;
import org.picketlink.idm.PartitionManager; import org.picketlink.idm.PartitionManager;
import org.picketlink.idm.config.AbstractIdentityStoreConfiguration; import org.picketlink.idm.config.AbstractIdentityStoreConfiguration;
import org.picketlink.idm.config.IdentityConfiguration; import org.picketlink.idm.config.IdentityConfiguration;
@ -45,7 +45,7 @@ public class PartitionManagerRegistry {
// Ldap config might have changed for the realm. In this case, we must re-initialize // Ldap config might have changed for the realm. In this case, we must re-initialize
if (context == null || !ldapConfig.equals(context.config)) { if (context == null || !ldapConfig.equals(context.config)) {
logger.infof("Creating new partition manager for the realm: %s, LDAP Connection URL: %s, LDAP Base DN: %s, LDAP Vendor: %s", realm.getId(), logger.infof("Creating new partition manager for the realm: %s, LDAP Connection URL: %s, LDAP Base DN: %s, LDAP Vendor: %s", realm.getId(),
ldapConfig.get(LdapConstants.CONNECTION_URL), ldapConfig.get(LdapConstants.BASE_DN), ldapConfig.get(LdapConstants.VENDOR)); ldapConfig.get(LDAPConstants.CONNECTION_URL), ldapConfig.get(LDAPConstants.BASE_DN), ldapConfig.get(LDAPConstants.VENDOR));
PartitionManager manager = createPartitionManager(ldapConfig); PartitionManager manager = createPartitionManager(ldapConfig);
context = new PartitionManagerContext(ldapConfig, manager); context = new PartitionManagerContext(ldapConfig, manager);
partitionManagers.put(realm.getId(), context); partitionManagers.put(realm.getId(), context);
@ -71,16 +71,16 @@ public class PartitionManagerRegistry {
checkSystemProperty("com.sun.jndi.ldap.connect.pool.protocol", "plain"); checkSystemProperty("com.sun.jndi.ldap.connect.pool.protocol", "plain");
checkSystemProperty("com.sun.jndi.ldap.connect.pool.debug", "off"); checkSystemProperty("com.sun.jndi.ldap.connect.pool.debug", "off");
String vendor = ldapConfig.get(LdapConstants.VENDOR); String vendor = ldapConfig.get(LDAPConstants.VENDOR);
// RHDS is using "nsuniqueid" as unique identifier instead of "entryUUID" // RHDS is using "nsuniqueid" as unique identifier instead of "entryUUID"
if (vendor != null && vendor.equals(LdapConstants.VENDOR_RHDS)) { if (vendor != null && vendor.equals(LDAPConstants.VENDOR_RHDS)) {
checkSystemProperty(LDAPIdentityStoreConfiguration.ENTRY_IDENTIFIER_ATTRIBUTE_NAME, "nsuniqueid"); checkSystemProperty(LDAPIdentityStoreConfiguration.ENTRY_IDENTIFIER_ATTRIBUTE_NAME, "nsuniqueid");
} }
boolean activeDirectory = vendor != null && vendor.equals(LdapConstants.VENDOR_ACTIVE_DIRECTORY); boolean activeDirectory = vendor != null && vendor.equals(LDAPConstants.VENDOR_ACTIVE_DIRECTORY);
String ldapLoginNameMapping = ldapConfig.get(LdapConstants.USERNAME_LDAP_ATTRIBUTE); String ldapLoginNameMapping = ldapConfig.get(LDAPConstants.USERNAME_LDAP_ATTRIBUTE);
if (ldapLoginNameMapping == null) { if (ldapLoginNameMapping == null) {
ldapLoginNameMapping = activeDirectory ? CN : UID; ldapLoginNameMapping = activeDirectory ? CN : UID;
} }
@ -100,14 +100,14 @@ public class PartitionManagerRegistry {
.ldap() .ldap()
.connectionProperties(connectionProps) .connectionProperties(connectionProps)
.addCredentialHandler(LDAPKeycloakCredentialHandler.class) .addCredentialHandler(LDAPKeycloakCredentialHandler.class)
.baseDN(ldapConfig.get(LdapConstants.BASE_DN)) .baseDN(ldapConfig.get(LDAPConstants.BASE_DN))
.bindDN(ldapConfig.get(LdapConstants.BIND_DN)) .bindDN(ldapConfig.get(LDAPConstants.BIND_DN))
.bindCredential(ldapConfig.get(LdapConstants.BIND_CREDENTIAL)) .bindCredential(ldapConfig.get(LDAPConstants.BIND_CREDENTIAL))
.url(ldapConfig.get(LdapConstants.CONNECTION_URL)) .url(ldapConfig.get(LDAPConstants.CONNECTION_URL))
.activeDirectory(activeDirectory) .activeDirectory(activeDirectory)
.supportAllFeatures() .supportAllFeatures()
.mapping(User.class) .mapping(User.class)
.baseDN(ldapConfig.get(LdapConstants.USER_DN_SUFFIX)) .baseDN(ldapConfig.get(LDAPConstants.USER_DN_SUFFIX))
.objectClasses("inetOrgPerson", "organizationalPerson") .objectClasses("inetOrgPerson", "organizationalPerson")
.attribute("loginName", ldapLoginNameMapping, true) .attribute("loginName", ldapLoginNameMapping, true)
.attribute("firstName", ldapFirstNameMapping) .attribute("firstName", ldapFirstNameMapping)

View file

@ -0,0 +1,64 @@
package org.keycloak.services.managers;
import java.util.Hashtable;
import java.util.Map;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.ldap.InitialLdapContext;
import org.jboss.logging.Logger;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LDAPConnectionTestManager {
protected static final Logger logger = Logger.getLogger(LDAPConnectionTestManager.class);
public static final String TEST_CONNECTION = "testConnection";
public static final String TEST_AUTHENTICATION = "testAuthentication";
public boolean testLDAP(String action, String connectionUrl, String bindDn, String bindCredential) {
if (!TEST_CONNECTION.equals(action) && !TEST_AUTHENTICATION.equals(action)) {
logger.error("Unknown action: " + action);
return false;
}
Context ldapContext = null;
try {
Hashtable<String, Object> env = new Hashtable<String, Object>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.PROVIDER_URL, connectionUrl);
if (TEST_AUTHENTICATION.equals(action)) {
env.put(Context.SECURITY_PRINCIPAL, bindDn);
char[] bindCredentialChar = null;
if (bindCredential != null) {
bindCredentialChar = bindCredential.toCharArray();
}
env.put(Context.SECURITY_CREDENTIALS, bindCredentialChar);
}
ldapContext = new InitialLdapContext(env, null);
return true;
} catch (NamingException ne) {
String errorMessage = (TEST_AUTHENTICATION.equals(action)) ? "Error when authenticating to LDAP: " : "Error when connecting to LDAP: ";
logger.error(errorMessage + ne.getMessage(), ne);
return false;
} finally {
if (ldapContext != null) {
try {
ldapContext.close();
} catch (NamingException ne) {
logger.warn("Error when closing LDAP connection", ne);
}
}
}
}
}

View file

@ -17,6 +17,7 @@ import org.keycloak.provider.ProviderSession;
import org.keycloak.representations.adapters.action.SessionStats; import org.keycloak.representations.adapters.action.SessionStats;
import org.keycloak.representations.idm.RealmAuditRepresentation; import org.keycloak.representations.idm.RealmAuditRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.managers.LDAPConnectionTestManager;
import org.keycloak.services.managers.ModelToRepresentation; import org.keycloak.services.managers.ModelToRepresentation;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.ResourceAdminManager; import org.keycloak.services.managers.ResourceAdminManager;
@ -359,4 +360,14 @@ public class RealmAdminResource {
AuditProvider audit = providers.getProvider(AuditProvider.class); AuditProvider audit = providers.getProvider(AuditProvider.class);
audit.clear(realm.getId()); audit.clear(realm.getId());
} }
@Path("testLDAPConnection")
@GET
public Response testLDAPConnection(@QueryParam("action") String action, @QueryParam("connectionUrl") String connectionUrl,
@QueryParam("bindDn") String bindDn, @QueryParam("bindCredential") String bindCredential) {
auth.init(RealmAuth.Resource.REALM).requireManage();
boolean result = new LDAPConnectionTestManager().testLDAP(action, connectionUrl, bindDn, bindCredential);
return result ? Response.noContent().build() : Flows.errors().error("LDAP test error", Response.Status.BAD_REQUEST);
}
} }

View file

@ -9,7 +9,7 @@ import org.junit.rules.RuleChain;
import org.junit.rules.TestRule; import org.junit.rules.TestRule;
import org.junit.runners.MethodSorters; import org.junit.runners.MethodSorters;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.model.test.LdapTestUtils; import org.keycloak.model.test.LDAPTestUtils;
import org.keycloak.models.AuthenticationProviderModel; import org.keycloak.models.AuthenticationProviderModel;
import org.keycloak.models.PasswordPolicy; import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -62,7 +62,7 @@ public class AuthProvidersIntegrationTest {
// Configure LDAP // Configure LDAP
ldapRule.getEmbeddedServer().setupLdapInRealm(appRealm); ldapRule.getEmbeddedServer().setupLdapInRealm(appRealm);
LdapTestUtils.setLdapPassword(providerSession, appRealm, "johnkeycloak", "password"); LDAPTestUtils.setLdapPassword(providerSession, appRealm, "johnkeycloak", "password");
} }
}); });