keycloak-memoizeit

Details

diff --git a/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/idm/KeycloakLDAPIdentityStore.java b/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/idm/KeycloakLDAPIdentityStore.java
new file mode 100644
index 0000000..8296854
--- /dev/null
+++ b/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/idm/KeycloakLDAPIdentityStore.java
@@ -0,0 +1,94 @@
+package org.keycloak.picketlink.idm;
+
+import java.lang.reflect.Method;
+
+import javax.naming.directory.BasicAttributes;
+
+import org.keycloak.models.utils.reflection.Reflections;
+import org.picketlink.idm.config.LDAPMappingConfiguration;
+import org.picketlink.idm.ldap.internal.LDAPIdentityStore;
+import org.picketlink.idm.ldap.internal.LDAPOperationManager;
+import org.picketlink.idm.model.AttributedType;
+import org.picketlink.idm.model.basic.User;
+import org.picketlink.idm.query.IdentityQuery;
+import org.picketlink.idm.spi.IdentityContext;
+
+import static org.picketlink.common.constants.LDAPConstants.CN;
+import static org.picketlink.common.constants.LDAPConstants.COMMA;
+import static org.picketlink.common.constants.LDAPConstants.EQUAL;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class KeycloakLDAPIdentityStore extends LDAPIdentityStore {
+
+    public static Method GET_BINDING_DN_METHOD;
+    public static Method GET_OPERATION_MANAGER_METHOD;
+    public static Method CREATE_SEARCH_FILTER_METHOD;
+    public static Method EXTRACT_ATTRIBUTES_METHOD;
+    public static Method GET_ENTRY_IDENTIFIER_METHOD;
+
+    public static final String SAM_ACCOUNT_NAME = "sAMAccountName";
+
+    static {
+        GET_BINDING_DN_METHOD = getMethodOnLDAPStore("getBindingDN", AttributedType.class);
+        GET_OPERATION_MANAGER_METHOD = getMethodOnLDAPStore("getOperationManager");
+        CREATE_SEARCH_FILTER_METHOD = getMethodOnLDAPStore("createIdentityTypeSearchFilter", IdentityQuery.class, LDAPMappingConfiguration.class);
+        EXTRACT_ATTRIBUTES_METHOD = getMethodOnLDAPStore("extractAttributes", AttributedType.class, boolean.class);
+        GET_ENTRY_IDENTIFIER_METHOD = getMethodOnLDAPStore("getEntryIdentifier", AttributedType.class);
+    }
+
+    @Override
+    public void addAttributedType(IdentityContext context, AttributedType attributedType) {
+        LDAPMappingConfiguration userMappingConfig = getConfig().getMappingConfig(attributedType.getClass());
+        String ldapUsernameAttrName = userMappingConfig.getMappedProperties().get(userMappingConfig.getIdProperty().getName());
+
+        if (getConfig().isActiveDirectory() && SAM_ACCOUNT_NAME.equals(ldapUsernameAttrName)) {
+            // TODO: pain, but everything in picketlink is private... Improve if possible...
+            LDAPOperationManager operationManager = Reflections.invokeMethod(false, GET_OPERATION_MANAGER_METHOD, LDAPOperationManager.class, this);
+
+            String cn = getCommonName(attributedType);
+            String bindingDN = CN + EQUAL + cn + COMMA + userMappingConfig.getBaseDN();
+
+            BasicAttributes ldapAttributes = Reflections.invokeMethod(false, EXTRACT_ATTRIBUTES_METHOD, BasicAttributes.class, this, attributedType, true);
+            ldapAttributes.put(CN, cn);
+
+            operationManager.createSubContext(bindingDN, ldapAttributes);
+
+            String ldapId = Reflections.invokeMethod(false, GET_ENTRY_IDENTIFIER_METHOD, String.class, this, attributedType);
+            attributedType.setId(ldapId);
+        } else {
+            super.addAttributedType(context, attributedType);
+        }
+    }
+
+    // Hack as methods are protected on LDAPIdentityStore :/
+    public 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);
+        }
+    }
+
+    protected String getCommonName(AttributedType aType) {
+        User user = (User)aType;
+        String fullName;
+        if (user.getFirstName() != null && user.getLastName() != null) {
+            fullName = user.getFirstName() + " " + user.getLastName();
+        } else if (user.getFirstName() != null && user.getFirstName().trim().length() > 0) {
+            fullName = user.getFirstName();
+        } else {
+            fullName = user.getLastName();
+        }
+
+        // Fallback to loginName
+        if (fullName == null || fullName.trim().length() == 0) {
+            fullName = user.getLoginName();
+        }
+
+        return fullName;
+    }
+}
diff --git a/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/idm/LDAPKeycloakCredentialHandler.java b/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/idm/LDAPKeycloakCredentialHandler.java
index 62c64f3..38827e7 100644
--- a/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/idm/LDAPKeycloakCredentialHandler.java
+++ b/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/idm/LDAPKeycloakCredentialHandler.java
@@ -1,21 +1,27 @@
 package org.keycloak.picketlink.idm;
 
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
+import java.io.UnsupportedEncodingException;
 import java.util.Date;
+import java.util.List;
 
+import javax.naming.NamingException;
 import javax.naming.directory.BasicAttribute;
 import javax.naming.directory.DirContext;
 import javax.naming.directory.ModificationItem;
+import javax.naming.directory.SearchResult;
 
+import org.keycloak.models.utils.reflection.Reflections;
 import org.picketlink.idm.IdentityManager;
+import org.picketlink.idm.config.LDAPMappingConfiguration;
+import org.picketlink.idm.credential.Credentials;
 import org.picketlink.idm.credential.Password;
+import org.picketlink.idm.credential.UsernamePasswordCredentials;
 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.query.IdentityQuery;
 import org.picketlink.idm.spi.IdentityContext;
 
 import static org.picketlink.idm.IDMLog.CREDENTIAL_LOGGER;
@@ -26,19 +32,12 @@ import static org.picketlink.idm.model.basic.BasicModel.getUser;
  */
 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) {
+        // TODO: Don't setup it here once PLIDM-508 is fixed
         if (store.getConfig().isActiveDirectory() || Boolean.getBoolean("keycloak.ldap.ad.skipUserAccountControlAfterPasswordUpdate")) {
             String userAccountControlProp = System.getProperty("keycloak.ldap.ad.userAccountControlAfterPasswordUpdate");
             this.userAccountControlAfterPasswordUpdate = userAccountControlProp!=null ? userAccountControlProp : "512";
@@ -48,7 +47,7 @@ public class LDAPKeycloakCredentialHandler extends LDAPPlainTextPasswordCredenti
 
     // Overridden as in Keycloak, we don't have Agents
     @Override
-    protected Account getAccount(IdentityContext context, String loginName) {
+    protected User getAccount(IdentityContext context, String loginName) {
         IdentityManager identityManager = getIdentityManager(context);
 
         if (CREDENTIAL_LOGGER.isDebugEnabled()) {
@@ -60,34 +59,111 @@ public class LDAPKeycloakCredentialHandler extends LDAPPlainTextPasswordCredenti
 
     @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);
+        if (!store.getConfig().isActiveDirectory()) {
+            super.update(context, account, password, store, effectiveDate, expiryDate);
+        } else {
+            User user = (User)account;
+            LDAPOperationManager operationManager = Reflections.invokeMethod(false, KeycloakLDAPIdentityStore.GET_OPERATION_MANAGER_METHOD, LDAPOperationManager.class, store);
+            IdentityManager identityManager = getIdentityManager(context);
+            String userDN = getDNOfUser(user, identityManager, store, operationManager);
+
+            updateADPassword(userDN, new String(password.getValue()), operationManager);
+
+            if (userAccountControlAfterPasswordUpdate != null) {
+                ModificationItem[] mods = new ModificationItem[1];
+                BasicAttribute mod0 = new BasicAttribute("userAccountControl", userAccountControlAfterPasswordUpdate);
+                mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, mod0);
+                operationManager.modifyAttribute(userDN, mod0);
             }
         }
     }
 
-    // Hack as methods are protected on LDAPIdentityStore :/
-    private static Method getMethodOnLDAPStore(String methodName, Class... classes) {
+    protected void updateADPassword(String userDN, String password, LDAPOperationManager operationManager) {
         try {
-            Method m = LDAPIdentityStore.class.getDeclaredMethod(methodName, classes);
-            m.setAccessible(true);
-            return m;
-        } catch (Exception e) {
+            // Replace the "unicdodePwd" attribute with a new value
+            // Password must be both Unicode and a quoted string
+            String newQuotedPassword = "\"" + password + "\"";
+            byte[] newUnicodePassword = newQuotedPassword.getBytes("UTF-16LE");
+            BasicAttribute unicodePwd = new BasicAttribute("unicodePwd", newUnicodePassword);
+            operationManager.modifyAttribute(userDN, unicodePwd);
+        } catch (UnsupportedEncodingException e) {
             throw new RuntimeException(e);
         }
     }
+
+    @Override
+    public void validate(IdentityContext context, UsernamePasswordCredentials credentials,
+                         LDAPIdentityStore store) {
+        credentials.setStatus(Credentials.Status.INVALID);
+        credentials.setValidatedAccount(null);
+
+        if (CREDENTIAL_LOGGER.isDebugEnabled()) {
+            CREDENTIAL_LOGGER.debugf("Validating credentials [%s][%s] using identity store [%s] and credential handler [%s].", credentials.getClass(), credentials, store, this);
+        }
+
+        User account = getAccount(context, credentials.getUsername());
+
+        // If the user for the provided username cannot be found we fail validation
+        if (account != null) {
+            if (CREDENTIAL_LOGGER.isDebugEnabled()) {
+                CREDENTIAL_LOGGER.debugf("Found account [%s] from credentials [%s].", account, credentials);
+            }
+
+            if (account.isEnabled()) {
+                if (CREDENTIAL_LOGGER.isDebugEnabled()) {
+                    CREDENTIAL_LOGGER.debugf("Account [%s] is ENABLED.", account, credentials);
+                }
+
+                char[] password = credentials.getPassword().getValue();
+
+                // String bindingDN = store.getBindingDN(account);
+
+                LDAPOperationManager operationManager = Reflections.invokeMethod(false, KeycloakLDAPIdentityStore.GET_OPERATION_MANAGER_METHOD, LDAPOperationManager.class, store);
+                String bindingDN = getDNOfUser(account, getIdentityManager(context), store, operationManager);
+
+                if (operationManager.authenticate(bindingDN, new String(password))) {
+                    credentials.setValidatedAccount(account);
+                    credentials.setStatus(Credentials.Status.VALID);
+                }
+            } else {
+                if (CREDENTIAL_LOGGER.isDebugEnabled()) {
+                    CREDENTIAL_LOGGER.debugf("Account [%s] is DISABLED.", account, credentials);
+                }
+                credentials.setStatus(Credentials.Status.ACCOUNT_DISABLED);
+            }
+        } else {
+            if (CREDENTIAL_LOGGER.isDebugEnabled()) {
+                CREDENTIAL_LOGGER.debugf("Account NOT FOUND for credentials [%s][%s].", credentials.getClass(), credentials);
+            }
+        }
+
+        if (CREDENTIAL_LOGGER.isDebugEnabled()) {
+            CREDENTIAL_LOGGER.debugf("Credential [%s][%s] validated using identity store [%s] and credential handler [%s]. Status [%s]. Validated Account [%s]",
+                    credentials.getClass(), credentials, store, this, credentials.getStatus(), credentials.getValidatedAccount());
+        }
+    }
+
+    // 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'
+    // but DN like: cn=John Doe,OU=foo,DC=bar
+    protected String getDNOfUser(User user, IdentityManager identityManager, LDAPIdentityStore ldapStore, LDAPOperationManager operationManager) {
+
+        LDAPMappingConfiguration ldapEntryConfig = ldapStore.getConfig().getMappingConfig(User.class);
+        IdentityQuery<User> identityQuery = identityManager.createIdentityQuery(User.class)
+                .setParameter(User.LOGIN_NAME, user.getLoginName());
+        StringBuilder filter = Reflections.invokeMethod(false, KeycloakLDAPIdentityStore.CREATE_SEARCH_FILTER_METHOD, StringBuilder.class, ldapStore, identityQuery, ldapEntryConfig);
+
+        List<SearchResult> search = null;
+        try {
+            search = operationManager.search(ldapEntryConfig.getBaseDN(), filter.toString(), ldapEntryConfig);
+        } catch (NamingException ne) {
+            throw new RuntimeException(ne);
+        }
+
+        if (search.size() > 0) {
+            SearchResult sr1 = search.get(0);
+            return sr1.getNameInNamespace();
+        } else {
+            return null;
+        }
+    }
 }
diff --git a/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/realm/PartitionManagerRegistry.java b/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/realm/PartitionManagerRegistry.java
index 6204aa7..985c22a 100644
--- a/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/realm/PartitionManagerRegistry.java
+++ b/picketlink/keycloak-picketlink-realm/src/main/java/org/keycloak/picketlink/realm/PartitionManagerRegistry.java
@@ -1,15 +1,20 @@
 package org.keycloak.picketlink.realm;
 
+import java.util.List;
 import java.util.Map;
 import java.util.Properties;
 import java.util.concurrent.ConcurrentHashMap;
 
 import org.jboss.logging.Logger;
 import org.keycloak.models.RealmModel;
+import org.keycloak.picketlink.idm.KeycloakLDAPIdentityStore;
 import org.keycloak.picketlink.idm.LDAPKeycloakCredentialHandler;
 import org.keycloak.picketlink.idm.LdapConstants;
 import org.picketlink.idm.PartitionManager;
+import org.picketlink.idm.config.AbstractIdentityStoreConfiguration;
+import org.picketlink.idm.config.IdentityConfiguration;
 import org.picketlink.idm.config.IdentityConfigurationBuilder;
+import org.picketlink.idm.config.IdentityStoreConfiguration;
 import org.picketlink.idm.config.LDAPIdentityStoreConfiguration;
 import org.picketlink.idm.internal.DefaultPartitionManager;
 import org.picketlink.idm.model.basic.User;
@@ -104,7 +109,12 @@ public class PartitionManagerRegistry {
                             .attribute("lastName", ldapLastName)
                             .attribute("email", ldapEmail);
 
-        return new DefaultPartitionManager(builder.buildAll());
+        // Workaround to override the LDAPIdentityStore with our own :/
+        List<IdentityConfiguration> identityConfigs = builder.buildAll();
+        IdentityStoreConfiguration identityStoreConfig = identityConfigs.get(0).getStoreConfiguration().get(0);
+        ((AbstractIdentityStoreConfiguration)identityStoreConfig).setIdentityStoreType(KeycloakLDAPIdentityStore.class);
+
+        return new DefaultPartitionManager(identityConfigs);
     }
 
     private void checkSystemProperty(String name, String defaultValue) {