Merge pull request #458 from mposolda/master
Support for Active Directory and RHDS
This commit is contained in:
commit
9a3abf6a6a
16 changed files with 226 additions and 70 deletions
|
@ -67,6 +67,10 @@ public class PicketlinkAuthenticationProvider implements AuthenticationProvider
|
||||||
try {
|
try {
|
||||||
User picketlinkUser = new User(username);
|
User picketlinkUser = new User(username);
|
||||||
identityManager.add(picketlinkUser);
|
identityManager.add(picketlinkUser);
|
||||||
|
|
||||||
|
// Hack needed due to ActiveDirectory bug in Picketlink TODO: Remove once https://issues.jboss.org/browse/PLINK-485 fixed and updated in keycloak master
|
||||||
|
picketlinkUser = BasicModel.getUser(identityManager, picketlinkUser.getLoginName());
|
||||||
|
|
||||||
return picketlinkUser.getId();
|
return picketlinkUser.getId();
|
||||||
} catch (IdentityManagementException ie) {
|
} catch (IdentityManagementException ie) {
|
||||||
throw convertIDMException(ie);
|
throw convertIDMException(ie);
|
||||||
|
|
|
@ -902,6 +902,12 @@ 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) {
|
||||||
console.log('RealmLdapSettingsCtrl');
|
console.log('RealmLdapSettingsCtrl');
|
||||||
|
|
||||||
|
$scope.ldapVendors = [
|
||||||
|
{ "id": "ad", "name": "Active Directory" },
|
||||||
|
{ "id": "rhds", "name": "Red Hat Directory Server" },
|
||||||
|
{ "id": "other", "name": "Other" }
|
||||||
|
];
|
||||||
|
|
||||||
$scope.realm = realm;
|
$scope.realm = realm;
|
||||||
|
|
||||||
var oldCopy = angular.copy($scope.realm);
|
var oldCopy = angular.copy($scope.realm);
|
||||||
|
|
|
@ -13,6 +13,17 @@
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend><span class="text">Required Settings</span></legend>
|
<legend><span class="text">Required Settings</span></legend>
|
||||||
|
<div class="form-group clearfix">
|
||||||
|
<label class="col-sm-2 control-label" for="vendor">Vendor <span class="required">*</span></label>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<div class="select-kc">
|
||||||
|
<select id="vendor"
|
||||||
|
ng-model="realm.ldapServer.vendor"
|
||||||
|
ng-options="vendor.id as vendor.name for vendor in ldapVendors">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group clearfix">
|
<div class="form-group clearfix">
|
||||||
<label class="col-sm-2 control-label" for="ldapConnectionUrl">Connection URL <span class="required">*</span></label>
|
<label class="col-sm-2 control-label" for="ldapConnectionUrl">Connection URL <span class="required">*</span></label>
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
|
|
|
@ -49,6 +49,7 @@ public class LDAPEmbeddedServer extends AbstractLDAPTest {
|
||||||
protected boolean startEmbeddedLdapLerver = true;
|
protected boolean startEmbeddedLdapLerver = true;
|
||||||
protected String bindDn = "uid=admin,ou=system";
|
protected String bindDn = "uid=admin,ou=system";
|
||||||
protected String bindCredential = "secret";
|
protected String bindCredential = "secret";
|
||||||
|
protected String vendor;
|
||||||
|
|
||||||
public static String IDM_TEST_LDAP_CONNECTION_URL = "idm.test.ldap.connection.url";
|
public static String IDM_TEST_LDAP_CONNECTION_URL = "idm.test.ldap.connection.url";
|
||||||
public static String IDM_TEST_LDAP_BASE_DN = "idm.test.ldap.base.dn";
|
public static String IDM_TEST_LDAP_BASE_DN = "idm.test.ldap.base.dn";
|
||||||
|
@ -59,6 +60,7 @@ public class LDAPEmbeddedServer extends AbstractLDAPTest {
|
||||||
public static String IDM_TEST_LDAP_START_EMBEDDED_LDAP_SERVER = "idm.test.ldap.start.embedded.ldap.server";
|
public static String IDM_TEST_LDAP_START_EMBEDDED_LDAP_SERVER = "idm.test.ldap.start.embedded.ldap.server";
|
||||||
public static String IDM_TEST_LDAP_BIND_DN = "idm.test.ldap.bind.dn";
|
public static String IDM_TEST_LDAP_BIND_DN = "idm.test.ldap.bind.dn";
|
||||||
public static String IDM_TEST_LDAP_BIND_CREDENTIAL = "idm.test.ldap.bind.credential";
|
public static String IDM_TEST_LDAP_BIND_CREDENTIAL = "idm.test.ldap.bind.credential";
|
||||||
|
public static String IDM_TEST_LDAP_VENDOR = "idm.test.ldap.vendor";
|
||||||
|
|
||||||
|
|
||||||
public LDAPEmbeddedServer() {
|
public LDAPEmbeddedServer() {
|
||||||
|
@ -85,6 +87,7 @@ public class LDAPEmbeddedServer extends AbstractLDAPTest {
|
||||||
startEmbeddedLdapLerver = Boolean.parseBoolean(p.getProperty(IDM_TEST_LDAP_START_EMBEDDED_LDAP_SERVER, "true"));
|
startEmbeddedLdapLerver = Boolean.parseBoolean(p.getProperty(IDM_TEST_LDAP_START_EMBEDDED_LDAP_SERVER, "true"));
|
||||||
bindDn = p.getProperty(IDM_TEST_LDAP_BIND_DN, bindDn);
|
bindDn = p.getProperty(IDM_TEST_LDAP_BIND_DN, bindDn);
|
||||||
bindCredential = p.getProperty(IDM_TEST_LDAP_BIND_CREDENTIAL, bindCredential);
|
bindCredential = p.getProperty(IDM_TEST_LDAP_BIND_CREDENTIAL, bindCredential);
|
||||||
|
vendor = p.getProperty(IDM_TEST_LDAP_VENDOR);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -132,6 +135,7 @@ public class LDAPEmbeddedServer extends AbstractLDAPTest {
|
||||||
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());
|
||||||
realm.setLdapServerConfig(ldapConfig);
|
realm.setLdapServerConfig(ldapConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,6 +206,10 @@ public class LDAPEmbeddedServer extends AbstractLDAPTest {
|
||||||
return bindCredential;
|
return bindCredential;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getVendor() {
|
||||||
|
return vendor;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void importLDIF(String fileName) throws Exception {
|
public void importLDIF(String fileName) throws Exception {
|
||||||
// import LDIF only in case we are running against embedded LDAP server
|
// import LDIF only in case we are running against embedded LDAP server
|
||||||
|
|
|
@ -75,23 +75,23 @@ public class AuthProvidersLDAPTest extends AbstractModelTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testLdapAuthentication() {
|
public void testLdapAuthentication() {
|
||||||
MultivaluedMap<String, String> formData = AuthProvidersExternalModelTest.createFormData("john", "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, "john", "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));
|
||||||
Assert.assertNull(realm.getUser("john"));
|
Assert.assertNull(realm.getUser("johnkeycloak"));
|
||||||
|
|
||||||
// Add ldap authenticationProvider
|
// Add ldap authenticationProvider
|
||||||
setupAuthenticationProviders();
|
setupAuthenticationProviders();
|
||||||
|
|
||||||
// Authenticate john and verify that now he exists in realm
|
// Authenticate john and verify that now he exists in realm
|
||||||
Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm, formData));
|
Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm, formData));
|
||||||
UserModel john = realm.getUser("john");
|
UserModel john = realm.getUser("johnkeycloak");
|
||||||
Assert.assertNotNull(john);
|
Assert.assertNotNull(john);
|
||||||
Assert.assertEquals("john", john.getLoginName());
|
Assert.assertEquals("johnkeycloak", john.getLoginName());
|
||||||
Assert.assertEquals("John", john.getFirstName());
|
Assert.assertEquals("John", john.getFirstName());
|
||||||
Assert.assertEquals("Doe", john.getLastName());
|
Assert.assertEquals("Doe", john.getLastName());
|
||||||
Assert.assertEquals("john@email.org", john.getEmail());
|
Assert.assertEquals("john@email.org", john.getEmail());
|
||||||
|
@ -119,7 +119,7 @@ public class AuthProvidersLDAPTest extends AbstractModelTest {
|
||||||
Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_USER, am.authenticateForm(null, realm, formData));
|
Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_USER, am.authenticateForm(null, realm, formData));
|
||||||
|
|
||||||
// User exists in ldap
|
// User exists in ldap
|
||||||
formData = AuthProvidersExternalModelTest.createFormData("john", "invalid");
|
formData = AuthProvidersExternalModelTest.createFormData("johnkeycloak", "invalid");
|
||||||
Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_CREDENTIALS, am.authenticateForm(null, realm, formData));
|
Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_CREDENTIALS, am.authenticateForm(null, realm, formData));
|
||||||
|
|
||||||
// User exists in realm
|
// User exists in realm
|
||||||
|
@ -142,23 +142,23 @@ public class AuthProvidersLDAPTest extends AbstractModelTest {
|
||||||
// Add ldap
|
// Add ldap
|
||||||
setupAuthenticationProviders();
|
setupAuthenticationProviders();
|
||||||
|
|
||||||
LdapTestUtils.setLdapPassword(providerSession, realm, "john", "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("john", "password");
|
MultivaluedMap<String, String> formData = AuthProvidersExternalModelTest.createFormData("johnkeycloak", "password");
|
||||||
Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm, formData));
|
Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm, formData));
|
||||||
|
|
||||||
// Change credential and validate that user can authenticate
|
// Change credential and validate that user can authenticate
|
||||||
AuthenticationProviderManager authProviderManager = AuthenticationProviderManager.getManager(realm, providerSession);
|
AuthenticationProviderManager authProviderManager = AuthenticationProviderManager.getManager(realm, providerSession);
|
||||||
|
|
||||||
UserModel john = realm.getUser("john");
|
UserModel john = realm.getUser("johnkeycloak");
|
||||||
try {
|
try {
|
||||||
Assert.assertTrue(authProviderManager.updatePassword(john, "password-updated"));
|
Assert.assertTrue(authProviderManager.updatePassword(john, "password-updated"));
|
||||||
} catch (AuthenticationProviderException ape) {
|
} catch (AuthenticationProviderException ape) {
|
||||||
ape.printStackTrace();
|
ape.printStackTrace();
|
||||||
Assert.fail("Error not expected");
|
Assert.fail("Error not expected");
|
||||||
}
|
}
|
||||||
formData = AuthProvidersExternalModelTest.createFormData("john", "password-updated");
|
formData = AuthProvidersExternalModelTest.createFormData("johnkeycloak", "password-updated");
|
||||||
Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm, formData));
|
Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm, formData));
|
||||||
|
|
||||||
// Password updated just in LDAP, so validating directly in realm should fail
|
// Password updated just in LDAP, so validating directly in realm should fail
|
||||||
|
@ -174,7 +174,7 @@ public class AuthProvidersLDAPTest extends AbstractModelTest {
|
||||||
ape.printStackTrace();
|
ape.printStackTrace();
|
||||||
Assert.fail("Error not expected");
|
Assert.fail("Error not expected");
|
||||||
}
|
}
|
||||||
formData = AuthProvidersExternalModelTest.createFormData("john", "password-updated2");
|
formData = AuthProvidersExternalModelTest.createFormData("johnkeycloak", "password-updated2");
|
||||||
Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_CREDENTIALS, am.authenticateForm(null, realm, formData));
|
Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_CREDENTIALS, am.authenticateForm(null, realm, formData));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -201,10 +201,11 @@ public class ImportTest extends AbstractModelTest {
|
||||||
|
|
||||||
// Test ldap config
|
// Test ldap config
|
||||||
Map<String, String> ldapConfig = realm.getLdapServerConfig();
|
Map<String, String> ldapConfig = realm.getLdapServerConfig();
|
||||||
Assert.assertTrue(ldapConfig.size() == 5);
|
Assert.assertTrue(ldapConfig.size() == 6);
|
||||||
Assert.assertEquals("ldap://localhost:10389", ldapConfig.get("connectionUrl"));
|
Assert.assertEquals("ldap://localhost:10389", ldapConfig.get("connectionUrl"));
|
||||||
Assert.assertEquals("dc=keycloak,dc=org", ldapConfig.get("baseDn"));
|
Assert.assertEquals("dc=keycloak,dc=org", ldapConfig.get("baseDn"));
|
||||||
Assert.assertEquals("ou=People,dc=keycloak,dc=org", ldapConfig.get("userDnSuffix"));
|
Assert.assertEquals("ou=People,dc=keycloak,dc=org", ldapConfig.get("userDnSuffix"));
|
||||||
|
Assert.assertEquals("other", ldapConfig.get("vendor"));
|
||||||
|
|
||||||
// Test authentication providers
|
// Test authentication providers
|
||||||
List<AuthenticationProviderModel> authProviderModels = realm.getAuthenticationProviders();
|
List<AuthenticationProviderModel> authProviderModels = realm.getAuthenticationProviders();
|
||||||
|
|
|
@ -9,12 +9,12 @@ objectclass: top
|
||||||
objectclass: organizationalUnit
|
objectclass: organizationalUnit
|
||||||
ou: People
|
ou: People
|
||||||
|
|
||||||
dn: uid=john,ou=People,dc=keycloak,dc=org
|
dn: uid=johnkeycloak,ou=People,dc=keycloak,dc=org
|
||||||
objectclass: top
|
objectclass: top
|
||||||
objectclass: uidObject
|
objectclass: uidObject
|
||||||
objectclass: person
|
objectclass: person
|
||||||
objectclass: inetOrgPerson
|
objectclass: inetOrgPerson
|
||||||
uid: john
|
uid: johnkeycloak
|
||||||
cn: John
|
cn: John
|
||||||
sn: Doe
|
sn: Doe
|
||||||
mail: john@email.org
|
mail: john@email.org
|
||||||
|
|
|
@ -17,7 +17,8 @@
|
||||||
"baseDn": "dc=keycloak,dc=org",
|
"baseDn": "dc=keycloak,dc=org",
|
||||||
"userDnSuffix": "ou=People,dc=keycloak,dc=org",
|
"userDnSuffix": "ou=People,dc=keycloak,dc=org",
|
||||||
"bindDn": "uid=admin,ou=system",
|
"bindDn": "uid=admin,ou=system",
|
||||||
"bindCredential": "secret"
|
"bindCredential": "secret",
|
||||||
|
"vendor": "other"
|
||||||
},
|
},
|
||||||
"socialProviders": {
|
"socialProviders": {
|
||||||
"google.key": "abc",
|
"google.key": "abc",
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
package org.keycloak.picketlink;
|
||||||
|
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.picketlink.idm.IdentityManager;
|
||||||
|
import org.picketlink.idm.PartitionManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-request IdentityManager caching . Not thread-safe
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public abstract class AbstractIdentityManagerProvider implements IdentityManagerProvider {
|
||||||
|
|
||||||
|
private IdentityManager identityManager;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IdentityManager getIdentityManager(RealmModel realm) {
|
||||||
|
if (identityManager == null) {
|
||||||
|
PartitionManager partitionManager = getPartitionManager(realm);
|
||||||
|
identityManager = partitionManager.createIdentityManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
return identityManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract PartitionManager getPartitionManager(RealmModel realm);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
identityManager = null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,27 +0,0 @@
|
||||||
package org.keycloak.picketlink.idm;
|
|
||||||
|
|
||||||
import org.picketlink.idm.IdentityManager;
|
|
||||||
import org.picketlink.idm.ldap.internal.LDAPPlainTextPasswordCredentialHandler;
|
|
||||||
import org.picketlink.idm.model.Account;
|
|
||||||
import org.picketlink.idm.model.basic.User;
|
|
||||||
import org.picketlink.idm.spi.IdentityContext;
|
|
||||||
|
|
||||||
import static org.picketlink.idm.IDMLog.CREDENTIAL_LOGGER;
|
|
||||||
import static org.picketlink.idm.model.basic.BasicModel.getUser;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
|
||||||
*/
|
|
||||||
public class LDAPAgentIgnoreCredentialHandler extends LDAPPlainTextPasswordCredentialHandler {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Account getAccount(IdentityContext context, String loginName) {
|
|
||||||
IdentityManager identityManager = getIdentityManager(context);
|
|
||||||
|
|
||||||
if (CREDENTIAL_LOGGER.isDebugEnabled()) {
|
|
||||||
CREDENTIAL_LOGGER.debugf("Trying to find account [%s] using default account type [%s]", loginName, User.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
return getUser(identityManager, loginName);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
package org.keycloak.picketlink.idm;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import javax.naming.directory.BasicAttribute;
|
||||||
|
import javax.naming.directory.DirContext;
|
||||||
|
import javax.naming.directory.ModificationItem;
|
||||||
|
|
||||||
|
import org.picketlink.idm.IdentityManager;
|
||||||
|
import org.picketlink.idm.credential.Password;
|
||||||
|
import org.picketlink.idm.ldap.internal.LDAPIdentityStore;
|
||||||
|
import org.picketlink.idm.ldap.internal.LDAPOperationManager;
|
||||||
|
import org.picketlink.idm.ldap.internal.LDAPPlainTextPasswordCredentialHandler;
|
||||||
|
import org.picketlink.idm.model.Account;
|
||||||
|
import org.picketlink.idm.model.AttributedType;
|
||||||
|
import org.picketlink.idm.model.basic.User;
|
||||||
|
import org.picketlink.idm.spi.IdentityContext;
|
||||||
|
|
||||||
|
import static org.picketlink.idm.IDMLog.CREDENTIAL_LOGGER;
|
||||||
|
import static org.picketlink.idm.model.basic.BasicModel.getUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class LDAPKeycloakCredentialHandler extends LDAPPlainTextPasswordCredentialHandler {
|
||||||
|
|
||||||
|
private static Method GET_BINDING_DN_METHOD;
|
||||||
|
private static Method GET_OPERATION_MANAGER_METHOD;
|
||||||
|
|
||||||
|
static {
|
||||||
|
GET_BINDING_DN_METHOD = getMethodOnLDAPStore("getBindingDN", AttributedType.class);
|
||||||
|
GET_OPERATION_MANAGER_METHOD = getMethodOnLDAPStore("getOperationManager");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used just in ActiveDirectory to put account into "enabled" state (aka userAccountControl=512, see http://support.microsoft.com/kb/305144/en ) after password update. If value is -1, it's ignored
|
||||||
|
private String userAccountControlAfterPasswordUpdate;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setup(LDAPIdentityStore store) {
|
||||||
|
if (store.getConfig().isActiveDirectory() || Boolean.getBoolean("keycloak.ldap.ad.skipUserAccountControlAfterPasswordUpdate")) {
|
||||||
|
String userAccountControlProp = System.getProperty("keycloak.ldap.ad.userAccountControlAfterPasswordUpdate");
|
||||||
|
this.userAccountControlAfterPasswordUpdate = userAccountControlProp!=null ? userAccountControlProp : "512";
|
||||||
|
CREDENTIAL_LOGGER.info("Will use userAccountControl=" + userAccountControlAfterPasswordUpdate + " after password update of user in Active Directory");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overridden as in Keycloak, we don't have Agents
|
||||||
|
@Override
|
||||||
|
protected Account getAccount(IdentityContext context, String loginName) {
|
||||||
|
IdentityManager identityManager = getIdentityManager(context);
|
||||||
|
|
||||||
|
if (CREDENTIAL_LOGGER.isDebugEnabled()) {
|
||||||
|
CREDENTIAL_LOGGER.debugf("Trying to find account [%s] using default account type [%s]", loginName, User.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getUser(identityManager, loginName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(IdentityContext context, Account account, Password password, LDAPIdentityStore store, Date effectiveDate, Date expiryDate) {
|
||||||
|
super.update(context, account, password, store, effectiveDate, expiryDate);
|
||||||
|
|
||||||
|
if (userAccountControlAfterPasswordUpdate != null) {
|
||||||
|
ModificationItem[] mods = new ModificationItem[1];
|
||||||
|
BasicAttribute mod0 = new BasicAttribute("userAccountControl", userAccountControlAfterPasswordUpdate);
|
||||||
|
mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, mod0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String bindingDN = (String) GET_BINDING_DN_METHOD.invoke(store, account);
|
||||||
|
LDAPOperationManager operationManager = (LDAPOperationManager) GET_OPERATION_MANAGER_METHOD.invoke(store);
|
||||||
|
operationManager.modifyAttribute(bindingDN, mod0);
|
||||||
|
} catch (IllegalAccessException iae) {
|
||||||
|
throw new RuntimeException(iae);
|
||||||
|
} catch (InvocationTargetException ite) {
|
||||||
|
Throwable cause = ite.getTargetException() != null ? ite.getTargetException() : ite;
|
||||||
|
throw new RuntimeException(cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hack as methods are protected on LDAPIdentityStore :/
|
||||||
|
private static Method getMethodOnLDAPStore(String methodName, Class... classes) {
|
||||||
|
try {
|
||||||
|
Method m = LDAPIdentityStore.class.getDeclaredMethod(methodName, classes);
|
||||||
|
m.setAccessible(true);
|
||||||
|
return m;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,11 @@ package org.keycloak.picketlink.idm;
|
||||||
*/
|
*/
|
||||||
public class LdapConstants {
|
public class LdapConstants {
|
||||||
|
|
||||||
|
public static final String VENDOR = "vendor";
|
||||||
|
public static final String VENDOR_RHDS = "rhds";
|
||||||
|
public static final String VENDOR_ACTIVE_DIRECTORY = "ad";
|
||||||
|
public static final String VENDOR_OTHER = "other";
|
||||||
|
|
||||||
public static final String CONNECTION_URL = "connectionUrl";
|
public static final String CONNECTION_URL = "connectionUrl";
|
||||||
public static final String BASE_DN = "baseDn";
|
public static final String BASE_DN = "baseDn";
|
||||||
public static final String USER_DN_SUFFIX = "userDnSuffix";
|
public static final String USER_DN_SUFFIX = "userDnSuffix";
|
||||||
|
|
|
@ -6,15 +6,15 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.picketlink.idm.LDAPAgentIgnoreCredentialHandler;
|
import org.keycloak.picketlink.idm.LDAPKeycloakCredentialHandler;
|
||||||
import org.keycloak.picketlink.idm.LdapConstants;
|
import org.keycloak.picketlink.idm.LdapConstants;
|
||||||
import org.picketlink.idm.PartitionManager;
|
import org.picketlink.idm.PartitionManager;
|
||||||
import org.picketlink.idm.config.IdentityConfigurationBuilder;
|
import org.picketlink.idm.config.IdentityConfigurationBuilder;
|
||||||
|
import org.picketlink.idm.config.LDAPIdentityStoreConfiguration;
|
||||||
import org.picketlink.idm.internal.DefaultPartitionManager;
|
import org.picketlink.idm.internal.DefaultPartitionManager;
|
||||||
import org.picketlink.idm.model.basic.User;
|
import org.picketlink.idm.model.basic.User;
|
||||||
|
|
||||||
import static org.picketlink.common.constants.LDAPConstants.CN;
|
import static org.picketlink.common.constants.LDAPConstants.CN;
|
||||||
import static org.picketlink.common.constants.LDAPConstants.CREATE_TIMESTAMP;
|
|
||||||
import static org.picketlink.common.constants.LDAPConstants.EMAIL;
|
import static org.picketlink.common.constants.LDAPConstants.EMAIL;
|
||||||
import static org.picketlink.common.constants.LDAPConstants.SN;
|
import static org.picketlink.common.constants.LDAPConstants.SN;
|
||||||
import static org.picketlink.common.constants.LDAPConstants.UID;
|
import static org.picketlink.common.constants.LDAPConstants.UID;
|
||||||
|
@ -39,8 +39,8 @@ 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", 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.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);
|
||||||
|
@ -66,26 +66,43 @@ 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);
|
||||||
|
|
||||||
|
// RHDS is using "nsuniqueid" as unique identifier instead of "entryUUID"
|
||||||
|
if (vendor != null && vendor.equals(LdapConstants.VENDOR_RHDS)) {
|
||||||
|
checkSystemProperty(LDAPIdentityStoreConfiguration.ENTRY_IDENTIFIER_ATTRIBUTE_NAME, "nsuniqueid");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean activeDirectory = vendor != null && vendor.equals(LdapConstants.VENDOR_ACTIVE_DIRECTORY);
|
||||||
|
|
||||||
|
// Try to compute properties based on LDAP server type, but still allow to override them through System properties TODO: Should allow better way than overriding from System properties. Perhaps init from XML?
|
||||||
|
String ldapLoginName = getNameOfLDAPAttribute("keycloak.ldap.idm.loginName", UID, CN, activeDirectory);
|
||||||
|
String ldapFirstName = getNameOfLDAPAttribute("keycloak.ldap.idm.firstName", CN, "givenName", activeDirectory);
|
||||||
|
String ldapLastName = getNameOfLDAPAttribute("keycloak.ldap.idm.lastName", SN, SN, activeDirectory);
|
||||||
|
String ldapEmail = getNameOfLDAPAttribute("keycloak.ldap.idm.email", EMAIL, EMAIL, activeDirectory);
|
||||||
|
|
||||||
|
logger.infof("LDAP Attributes mapping: loginName: %s, firstName: %s, lastName: %s, email: %s", ldapLoginName, ldapFirstName, ldapLastName, ldapEmail);
|
||||||
|
|
||||||
// Use same mapping for User and Agent for now
|
// Use same mapping for User and Agent for now
|
||||||
builder
|
builder
|
||||||
.named("SIMPLE_LDAP_STORE_CONFIG")
|
.named("SIMPLE_LDAP_STORE_CONFIG")
|
||||||
.stores()
|
.stores()
|
||||||
.ldap()
|
.ldap()
|
||||||
.connectionProperties(connectionProps)
|
.connectionProperties(connectionProps)
|
||||||
.addCredentialHandler(LDAPAgentIgnoreCredentialHandler.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)
|
||||||
.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", UID, true)
|
.attribute("loginName", ldapLoginName, true)
|
||||||
.attribute("firstName", CN)
|
.attribute("firstName", ldapFirstName)
|
||||||
.attribute("lastName", SN)
|
.attribute("lastName", ldapLastName)
|
||||||
.attribute("email", EMAIL)
|
.attribute("email", ldapEmail);
|
||||||
.readOnlyAttribute("createdDate", CREATE_TIMESTAMP);
|
|
||||||
|
|
||||||
return new DefaultPartitionManager(builder.buildAll());
|
return new DefaultPartitionManager(builder.buildAll());
|
||||||
}
|
}
|
||||||
|
@ -96,6 +113,16 @@ public class PartitionManagerRegistry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getNameOfLDAPAttribute(String systemPropertyName, String defaultAttrName, String defaultAttrNameInActiveDirectory, boolean activeDirectory) {
|
||||||
|
// System property has biggest priority if available
|
||||||
|
String sysProperty = System.getProperty(systemPropertyName);
|
||||||
|
if (sysProperty != null) {
|
||||||
|
return sysProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return activeDirectory ? defaultAttrNameInActiveDirectory : defaultAttrName;
|
||||||
|
}
|
||||||
|
|
||||||
private class PartitionManagerContext {
|
private class PartitionManagerContext {
|
||||||
|
|
||||||
private PartitionManagerContext(Map<String,String> config, PartitionManager manager) {
|
private PartitionManagerContext(Map<String,String> config, PartitionManager manager) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package org.keycloak.picketlink.realm;
|
package org.keycloak.picketlink.realm;
|
||||||
|
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.picketlink.AbstractIdentityManagerProvider;
|
||||||
import org.keycloak.picketlink.IdentityManagerProvider;
|
import org.keycloak.picketlink.IdentityManagerProvider;
|
||||||
import org.picketlink.idm.IdentityManager;
|
import org.picketlink.idm.IdentityManager;
|
||||||
import org.picketlink.idm.PartitionManager;
|
import org.picketlink.idm.PartitionManager;
|
||||||
|
@ -8,7 +9,7 @@ import org.picketlink.idm.PartitionManager;
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
public class RealmIdentityManagerProvider implements IdentityManagerProvider {
|
public class RealmIdentityManagerProvider extends AbstractIdentityManagerProvider {
|
||||||
|
|
||||||
private final PartitionManagerRegistry partitionManagerRegistry;
|
private final PartitionManagerRegistry partitionManagerRegistry;
|
||||||
|
|
||||||
|
@ -17,12 +18,7 @@ public class RealmIdentityManagerProvider implements IdentityManagerProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IdentityManager getIdentityManager(RealmModel realm) {
|
protected PartitionManager getPartitionManager(RealmModel realm) {
|
||||||
PartitionManager partitionManager = partitionManagerRegistry.getPartitionManager(realm);
|
return partitionManagerRegistry.getPartitionManager(realm);
|
||||||
return partitionManager.createIdentityManager();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,7 @@ public class AuthProvidersIntegrationTest {
|
||||||
|
|
||||||
// Configure LDAP
|
// Configure LDAP
|
||||||
ldapRule.getEmbeddedServer().setupLdapInRealm(appRealm);
|
ldapRule.getEmbeddedServer().setupLdapInRealm(appRealm);
|
||||||
LdapTestUtils.setLdapPassword(providerSession, appRealm, "john", "password");
|
LdapTestUtils.setLdapPassword(providerSession, appRealm, "johnkeycloak", "password");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -135,7 +135,7 @@ public class AuthProvidersIntegrationTest {
|
||||||
@Test
|
@Test
|
||||||
public void loginLdap() {
|
public void loginLdap() {
|
||||||
loginPage.open();
|
loginPage.open();
|
||||||
loginPage.login("john", "password");
|
loginPage.login("johnkeycloak", "password");
|
||||||
|
|
||||||
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||||
|
@ -186,20 +186,19 @@ public class AuthProvidersIntegrationTest {
|
||||||
@Test
|
@Test
|
||||||
public void passwordChangeLdap() throws Exception {
|
public void passwordChangeLdap() throws Exception {
|
||||||
changePasswordPage.open();
|
changePasswordPage.open();
|
||||||
loginPage.login("john", "password");
|
loginPage.login("johnkeycloak", "password");
|
||||||
changePasswordPage.changePassword("password", "new-password", "new-password");
|
changePasswordPage.changePassword("password", "new-password", "new-password");
|
||||||
|
|
||||||
Assert.assertEquals("Your password has been updated", profilePage.getSuccess());
|
Assert.assertEquals("Your password has been updated", profilePage.getSuccess());
|
||||||
|
|
||||||
changePasswordPage.logout();
|
changePasswordPage.logout();
|
||||||
|
|
||||||
// TODO: Disabled until https://issues.jboss.org/browse/PLINK-384 is released and updated
|
loginPage.open();
|
||||||
// loginPage.open();
|
loginPage.login("johnkeycloak", "password");
|
||||||
// loginPage.login("john", "password");
|
Assert.assertEquals("Invalid username or password.", loginPage.getError());
|
||||||
// Assert.assertEquals("Invalid username or password.", loginPage.getError());
|
|
||||||
|
|
||||||
loginPage.open();
|
loginPage.open();
|
||||||
loginPage.login("john", "new-password");
|
loginPage.login("johnkeycloak", "new-password");
|
||||||
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,12 +19,12 @@ objectclass: top
|
||||||
objectclass: organizationalUnit
|
objectclass: organizationalUnit
|
||||||
ou: Groups
|
ou: Groups
|
||||||
|
|
||||||
dn: uid=john,ou=People,dc=keycloak,dc=org
|
dn: uid=johnkeycloak,ou=People,dc=keycloak,dc=org
|
||||||
objectclass: top
|
objectclass: top
|
||||||
objectclass: uidObject
|
objectclass: uidObject
|
||||||
objectclass: person
|
objectclass: person
|
||||||
objectclass: inetOrgPerson
|
objectclass: inetOrgPerson
|
||||||
uid: john
|
uid: johnkeycloak
|
||||||
cn: John
|
cn: John
|
||||||
sn: Doe
|
sn: Doe
|
||||||
mail: john@email.org
|
mail: john@email.org
|
||||||
|
|
Loading…
Reference in a new issue