Author: tripod
Date: Sat Mar  8 01:18:49 2014
New Revision: 1575468

URL: http://svn.apache.org/r1575468
Log:
OAK-516 Create LdapLoginModule based on ExternalLoginModule (wip)

Added:
    
jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/LdapIdentityProperties.java
Modified:
    
jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalIdentityRef.java
    
jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncContext.java
    
jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncConfig.java
    
jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncHandler.java
    
jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModule.java
    
jackrabbit/oak/trunk/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalLoginModuleTestBase.java
    
jackrabbit/oak/trunk/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/TestIdentityProvider.java
    
jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/LdapIdentity.java
    
jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/external_login_module.md

Modified: 
jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalIdentityRef.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalIdentityRef.java?rev=1575468&r1=1575467&r2=1575468&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalIdentityRef.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalIdentityRef.java
 Sat Mar  8 01:18:49 2014
@@ -42,9 +42,10 @@ public class ExternalIdentityRef {
         this.providerName = providerName;
 
         StringBuilder b = new StringBuilder();
-        b.append(Text.escape(id));
+        escape(b, id);
         if (providerName != null && providerName.length() > 0) {
-            b.append('@').append(Text.escape(providerName));
+            b.append(';');
+            escape(b, providerName);
         }
         string =  b.toString();
     }
@@ -82,7 +83,7 @@ public class ExternalIdentityRef {
      * @return the reference
      */
     public static ExternalIdentityRef fromString(@Nonnull String str) {
-        int idx = str.indexOf('@');
+        int idx = str.indexOf(';');
         if (idx < 0) {
             return new ExternalIdentityRef(Text.unescape(str), null);
         } else {
@@ -93,13 +94,28 @@ public class ExternalIdentityRef {
         }
     }
 
+    /**
+     * Escapes the given string and appends it to the builder.
+     * @param builder the builder
+     * @param str the string
+     */
+    private void escape(StringBuilder builder, CharSequence str) {
+        final int len = str.length();
+        for (int i=0; i<len; i++) {
+            char c = str.charAt(i);
+            if (c == '%') {
+                builder.append("%25");
+            } else if (c == ';') {
+                builder.append("%3b");
+            } else {
+                builder.append(c);
+            }
+        }
+    }
+
     @Override
     public String toString() {
-        final StringBuilder sb = new StringBuilder("ExternalIdentityRef{");
-        sb.append("id='").append(id).append('\'');
-        sb.append(", providerName='").append(providerName).append('\'');
-        sb.append('}');
-        return sb.toString();
+        return "ExternalIdentityRef{" + "id='" + id + '\'' + ", 
providerName='" + providerName + '\'' + '}';
     }
 
     /**

Modified: 
jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncContext.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncContext.java?rev=1575468&r1=1575467&r2=1575468&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncContext.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncContext.java
 Sat Mar  8 01:18:49 2014
@@ -28,7 +28,7 @@ public interface SyncContext {
      * Synchronizes an external identity with the repository based on the 
respective configuration.
      *
      * @param identity the identity to sync.
-     * @return {@code true} if the given identity was synced; {@code false} if 
for no change.
+     * @return {@code true} if the given identity was synced; {@code false} 
for no change.
      * @throws SyncException if an error occurrs
      */
     boolean sync(@Nonnull ExternalIdentity identity) throws SyncException;

Modified: 
jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncConfig.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncConfig.java?rev=1575468&r1=1575467&r2=1575468&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncConfig.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncConfig.java
 Sat Mar  8 01:18:49 2014
@@ -18,6 +18,7 @@
 package org.apache.jackrabbit.oak.spi.security.authentication.external.impl;
 
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
@@ -255,7 +256,7 @@ public class DefaultSyncConfig {
          */
         @Nonnull
         public Set<String> getAutoMembership() {
-            return autoMembership;
+            return autoMembership == null ? Collections.<String>emptySet() : 
autoMembership;
         }
 
         /**
@@ -291,7 +292,7 @@ public class DefaultSyncConfig {
          */
         @Nonnull
         public Map<String, String> getPropertyMapping() {
-            return propertyMapping;
+            return propertyMapping == null ? Collections.<String, 
String>emptyMap() : propertyMapping;
         }
 
         /**
@@ -313,7 +314,7 @@ public class DefaultSyncConfig {
          */
         @Nonnull
         public String getPathPrefix() {
-            return pathPrefix;
+            return pathPrefix == null ? "" : pathPrefix;
         }
 
         /**
@@ -324,7 +325,7 @@ public class DefaultSyncConfig {
          */
         @Nonnull
         public Authorizable setPathPrefix(String pathPrefix) {
-            this.pathPrefix = pathPrefix == null ? "" : pathPrefix;
+            this.pathPrefix = pathPrefix;
             return this;
         }
     }

Modified: 
jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncHandler.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncHandler.java?rev=1575468&r1=1575467&r2=1575468&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncHandler.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncHandler.java
 Sat Mar  8 01:18:49 2014
@@ -16,19 +16,24 @@
  */
 package org.apache.jackrabbit.oak.spi.security.authentication.external.impl;
 
+import java.io.ByteArrayInputStream;
+import java.math.BigDecimal;
 import java.security.Principal;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
 import java.util.Collection;
+import java.util.Date;
 import java.util.List;
 import java.util.Map;
 
 import javax.annotation.CheckForNull;
 import javax.annotation.Nonnull;
-import javax.jcr.PropertyType;
+import javax.annotation.Nullable;
+import javax.jcr.Binary;
 import javax.jcr.RepositoryException;
 import javax.jcr.Value;
 import javax.jcr.ValueFactory;
-import javax.jcr.ValueFormatException;
 
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
@@ -74,6 +79,16 @@ public class DefaultSyncHandler implemen
     private static final Logger log = 
LoggerFactory.getLogger(DefaultSyncHandler.class);
 
     /**
+     * Name of the {@link ExternalIdentity#getExternalId()} property of a 
synchronized identity.
+     */
+    public static final String REP_EXTERNAL_ID = "rep:externalId";
+
+    /**
+     * Name of the property that stores the time when an identity was synced.
+     */
+    public static final String REP_LAST_SYNCED = "rep:lastSynced";
+
+    /**
      * internal configuration
      */
     private DefaultSyncConfig config;
@@ -107,7 +122,8 @@ public class DefaultSyncHandler implemen
 
     @Nonnull
     @Override
-    public SyncContext createContext(@Nonnull ExternalIdentityProvider idp, 
@Nonnull UserManager userManager, @Nonnull Root root) throws SyncException {
+    public SyncContext createContext(@Nonnull ExternalIdentityProvider idp, 
@Nonnull UserManager userManager, @Nonnull Root root)
+            throws SyncException {
         return new ContextImpl(idp, userManager, root);
     }
 
@@ -121,33 +137,48 @@ public class DefaultSyncHandler implemen
 
         private final ValueFactory valueFactory;
 
+        // we use the same wall clock for the entire context
+        private final long now;
+        private final Value nowValue;
+
         private ContextImpl(ExternalIdentityProvider idp, UserManager 
userManager, Root root) {
             this.idp = idp;
             this.userManager = userManager;
             this.root = root;
             valueFactory = new ValueFactoryImpl(root, NamePathMapper.DEFAULT);
+
+            // initialize 'now'
+            final Calendar nowCal = Calendar.getInstance();
+            this.nowValue = valueFactory.createValue(nowCal);
+            this.now = nowCal.getTimeInMillis();
         }
 
+        /**
+         * {@inheritDoc}
+         */
         @Override
         public void close() {
             // nothing to do
         }
 
+        /**
+         * {@inheritDoc}
+         */
         @Override
         public boolean sync(@Nonnull ExternalIdentity identity) throws 
SyncException {
             try {
                 if (identity instanceof ExternalUser) {
-                    User user = getUser(identity);
+                    User user = getAuthorizable(identity, User.class);
                     if (user == null) {
-                        createUser((ExternalUser) identity);
-                    } else {
-                        updateUser((ExternalUser) identity, user);
+                        user = createUser((ExternalUser) identity);
                     }
-                    return true;
+                    return syncUser((ExternalUser) identity, user);
                 } else if (identity instanceof ExternalGroup) {
-                    // TODO
-                    return false;
-
+                    Group group = getAuthorizable(identity, Group.class);
+                    if (group == null) {
+                        group = createGroup((ExternalGroup) identity);
+                    }
+                    return false;//syncGroup((ExternalGroup) identity, group);
                 } else {
                     throw new IllegalArgumentException("identity must be user 
or group but was: " + identity);
                 }
@@ -158,55 +189,152 @@ public class DefaultSyncHandler implemen
             }
         }
 
+        /**
+         * Retrieves the repository authorizable that corresponds to the given 
external identity
+         * @param external the external identity
+         * @param type the authorizable type
+         * @return the repository authorizable or {@code null} if not found.
+         * @throws RepositoryException if an error occurs.
+         * @throws SyncException if the repository contains a colliding 
authorizable with the same name.
+         */
         @CheckForNull
-        private User getUser(@Nonnull ExternalIdentity externalUser) throws 
RepositoryException {
-            Authorizable authorizable = 
userManager.getAuthorizable(externalUser.getId());
+        private <T extends Authorizable> T getAuthorizable(@Nonnull 
ExternalIdentity external, Class<T> type)
+                throws RepositoryException, SyncException {
+            Authorizable authorizable = 
userManager.getAuthorizable(external.getId());
             if (authorizable == null) {
-                authorizable = 
userManager.getAuthorizable(externalUser.getPrincipalName());
+                authorizable = 
userManager.getAuthorizable(external.getPrincipalName());
             }
             if (authorizable == null) {
                 return null;
-            } else if (authorizable instanceof User) {
-                return (User) authorizable;
+            } else if (type.isInstance(authorizable)) {
+                //noinspection unchecked
+                return (T) authorizable;
             } else {
-                // TODO: deal with colliding authorizable that is group.
-                log.warn("unexpected authorizable: {}", authorizable);
-                return null;
+                log.error("Unable to process external {}: {}. Colliding 
authorizable exists in repository.", type.getSimpleName(), external.getId());
+                throw new SyncException("Unexpected authorizable: " + 
authorizable);
             }
         }
 
+        /**
+         * Creates a new repository user for the given external one.
+         * Note that this method only creates the authorizable but does not 
perform any synchronization.
+         *
+         * @param externalUser the external user
+         * @return the repository user
+         * @throws RepositoryException if an error occurs
+         * @throws ExternalIdentityException if an error occurs
+         */
         @CheckForNull
-        private User createUser(ExternalUser externalUser)
-                throws RepositoryException, SyncException, 
ExternalIdentityException {
+        private User createUser(ExternalUser externalUser) throws 
RepositoryException, ExternalIdentityException {
             Principal principal = new 
PrincipalImpl(externalUser.getPrincipalName());
             User user = userManager.createUser(
                     externalUser.getId(),
                     null,
                     principal,
-                    concatPaths(config.user().getPathPrefix(), 
externalUser.getIntermediatePath())
+                    joinPaths(config.user().getPathPrefix(), 
externalUser.getIntermediatePath())
             );
-            syncAuthorizable(externalUser, user);
+            user.setProperty(REP_EXTERNAL_ID, 
valueFactory.createValue(externalUser.getExternalId().getString()));
             return user;
         }
 
+        /**
+         * Creates a new repository group for the given external one.
+         * Note that this method only creates the authorizable but does not 
perform any synchronization.
+         *
+         * @param externalGroup the external group
+         * @return the repository group
+         * @throws RepositoryException if an error occurs
+         * @throws ExternalIdentityException if an error occurs
+         */
         @CheckForNull
-        private Group createGroup(ExternalGroup externalGroup)
-                throws RepositoryException, SyncException, 
ExternalIdentityException {
+        private Group createGroup(ExternalGroup externalGroup) throws 
RepositoryException, ExternalIdentityException {
             Principal principal = new 
PrincipalImpl(externalGroup.getPrincipalName());
             Group group = userManager.createGroup(
                     externalGroup.getId(),
                     principal,
-                    concatPaths(config.user().getPathPrefix(), 
externalGroup.getIntermediatePath()));
-            syncAuthorizable(externalGroup, group);
+                    joinPaths(config.group().getPathPrefix(), 
externalGroup.getIntermediatePath())
+            );
+            group.setProperty(REP_EXTERNAL_ID, 
valueFactory.createValue(externalGroup.getExternalId().getString()));
             return group;
         }
 
-        private void updateUser(ExternalUser externalUser, User user)
-                throws RepositoryException, SyncException, 
ExternalIdentityException {
-            syncAuthorizable(externalUser, user);
+
+        private boolean syncUser(ExternalUser external, User user) throws 
RepositoryException {
+            // first check if user is expired
+            // todo: add "forceSync" property for potential background sync
+            if (!isExpired(user, config.user().getExpirationTime())) {
+                return false;
+            }
+
+            // synchronize the properties
+            syncProperties(external, user, config.user().getPropertyMapping());
+
+            // finally "touch" the sync property
+            user.setProperty(REP_LAST_SYNCED, nowValue);
+            return true;
+        }
+
+        /**
+         * Syncs the properties specified in the {@code mapping} from the 
external identity to the given authorizable.
+         * Note that this method does not check for value equality and just 
blindly copies or deletes the properties.
+         *
+         * @param ext external identity
+         * @param auth the authorizable
+         * @param mapping the property mapping
+         * @throws RepositoryException if an error occurs
+         */
+        private void syncProperties(ExternalIdentity ext, Authorizable auth, 
Map<String, String> mapping)
+                throws RepositoryException {
+            Map<String, ?> properties = ext.getProperties();
+            for (Map.Entry<String, String> entry: mapping.entrySet()) {
+                String relPath = entry.getKey();
+                String name = entry.getValue();
+                Object obj = properties.get(name);
+                if (obj == null) {
+                    auth.removeProperty(relPath);
+                } else {
+                    if (obj instanceof Collection) {
+                        auth.setProperty(relPath, createValues((Collection) 
obj));
+                    } else if (obj instanceof byte[] || obj instanceof char[]) 
{
+                        auth.setProperty(relPath, createValue(obj));
+                    } else if (obj instanceof Object[]) {
+                        auth.setProperty(relPath, 
createValues(Arrays.asList((Object[]) obj)));
+                    } else {
+                        auth.setProperty(relPath, createValue(obj));
+                    }
+                }
+            }
+        }
+
+        /**
+         * Checks if the given authorizable needs syncing based on the {@link 
#REP_LAST_SYNCED} property.
+         * @param auth the authorizable to check
+         * @param expirationTime the expiration time to compare to.
+         * @return {@code true} if the authorizable needs sync
+         */
+        private boolean isExpired(Authorizable auth, long expirationTime) 
throws RepositoryException {
+            Value[] values = auth.getProperty(REP_LAST_SYNCED);
+            if (values == null || values.length == 0) {
+                if (log.isDebugEnabled()) {
+                    log.debug("{} '{}' needs sync. " + REP_LAST_SYNCED + " not 
set.", auth.isGroup() ? "Group" : "User", auth.getID());
+                }
+                return true;
+            } else if (now - values[0].getLong() > expirationTime) {
+                if (log.isDebugEnabled()) {
+                    log.debug("{} '{}' needs sync. " + REP_LAST_SYNCED + " 
expired ({} > {})", new Object[]{
+                            auth.isGroup() ? "Group" : "User", auth.getID(), 
now - values[0].getLong(), expirationTime
+                    });
+                }
+                return true;
+            } else {
+                if (log.isDebugEnabled()) {
+                    log.debug("{} '{}' does not need sync.", auth.isGroup() ? 
"Group" : "User", auth.getID());
+                }
+                return false;
+            }
         }
 
-        private void syncAuthorizable(ExternalIdentity externalUser, 
Authorizable authorizable)
+        private boolean syncAuthorizable(ExternalIdentity externalUser, 
Authorizable authorizable)
                 throws RepositoryException, SyncException, 
ExternalIdentityException {
             for (ExternalIdentityRef externalGroupRef : 
externalUser.getDeclaredGroups()) {
                 ExternalIdentity id = idp.getIdentity(externalGroupRef);
@@ -244,20 +372,51 @@ public class DefaultSyncHandler implemen
                     }
                 }
             }
+            return true;
         }
 
+        /**
+         * Creates a new JCR value of the given object, checking the internal 
type.
+         * @param v the value
+         * @return the JCR value or null
+         * @throws RepositoryException if an error occurs
+         */
         @CheckForNull
-        private Value createValue(Object propValue) throws 
ValueFormatException {
-            int type = getType(propValue);
-            if (type == PropertyType.UNDEFINED) {
+        private Value createValue(@Nullable Object v) throws 
RepositoryException {
+            if (v == null) {
                 return null;
+            } else if (v instanceof Boolean) {
+                return valueFactory.createValue((Boolean) v);
+            } else if (v instanceof Byte || v instanceof Short || v instanceof 
Integer || v instanceof Long) {
+                return valueFactory.createValue(((Number) v).longValue());
+            } else if (v instanceof Float || v instanceof Double) {
+                return valueFactory.createValue(((Number) v).doubleValue());
+            } else if (v instanceof BigDecimal) {
+                return valueFactory.createValue((BigDecimal) v);
+            } else if (v instanceof Calendar) {
+                return valueFactory.createValue((Calendar) v);
+            } else if (v instanceof Date) {
+                Calendar cal = Calendar.getInstance();
+                cal.setTime((Date) v);
+                return valueFactory.createValue(cal);
+            } else if (v instanceof byte[]) {
+                Binary bin = valueFactory.createBinary(new 
ByteArrayInputStream((byte[])v));
+                return valueFactory.createValue(bin);
+            } else if (v instanceof char[]) {
+                return valueFactory.createValue(new String((char[]) v));
             } else {
-                return valueFactory.createValue(propValue.toString(), type);
+                return valueFactory.createValue(String.valueOf(v));
             }
         }
 
+        /**
+         * Creates an array of JCR values based on the type.
+         * @param propValues the given values
+         * @return and array of JCR values
+         * @throws RepositoryException if an error occurs
+         */
         @CheckForNull
-        private Value[] createValues(Collection<?> propValues) throws 
ValueFormatException {
+        private Value[] createValues(Collection<?> propValues) throws 
RepositoryException {
             List<Value> values = new ArrayList<Value>();
             for (Object obj : propValues) {
                 Value v = createValue(obj);
@@ -268,15 +427,6 @@ public class DefaultSyncHandler implemen
             return values.toArray(new Value[values.size()]);
         }
 
-        private int getType(Object propValue) {
-            // TODO: add proper type detection
-            if (propValue == null) {
-                return PropertyType.UNDEFINED;
-            } else {
-                return PropertyType.STRING;
-            }
-        }
-
     }
 
     /**
@@ -284,7 +434,7 @@ public class DefaultSyncHandler implemen
      * @param paths relative paths
      * @return the concatenated path
      */
-    private static String concatPaths(String ... paths) {
+    private static String joinPaths(String... paths) {
         StringBuilder result = new StringBuilder();
         for (String path: paths) {
             if (path != null && !path.isEmpty()) {

Modified: 
jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModule.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModule.java?rev=1575468&r1=1575467&r2=1575468&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModule.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModule.java
 Sat Mar  8 01:18:49 2014
@@ -165,9 +165,7 @@ public class ExternalLoginModule extends
         try {
             externalUser = idp.authenticate(credentials);
             if (externalUser != null) {
-                if (log.isDebugEnabled()) {
-                    log.debug("IDP {} returned valid user {}", idp.getName(), 
externalUser);
-                }
+                log.debug("IDP {} returned valid user {}", idp.getName(), 
externalUser);
 
                 //noinspection unchecked
                 sharedState.put(SHARED_KEY_CREDENTIALS, credentials);
@@ -175,6 +173,8 @@ public class ExternalLoginModule extends
                 //noinspection unchecked
                 sharedState.put(SHARED_KEY_LOGIN_NAME, externalUser.getId());
 
+                syncUser(externalUser);
+
                 return true;
             } else {
                 if (log.isDebugEnabled()) {
@@ -186,51 +186,67 @@ public class ExternalLoginModule extends
                 }
             }
         } catch (ExternalIdentityException e) {
-            log.error("Error while authenticating credentials {} with {}: {}", 
new Object[]{
-                    credentials, idp.getName(), e.toString()});
+            log.error("Error while authenticating credentials {} with {}", new 
Object[]{credentials, idp.getName(), e});
             return false;
         } catch (LoginException e) {
-            if (log.isDebugEnabled()) {
-                log.debug("IDP {} throws login exception for {}", 
idp.getName(), credentials);
-            }
+            log.debug("IDP {} throws login exception for {}", new Object[] 
{idp.getName(), credentials, e});
             throw e;
+        } catch (SyncException e) {
+            log.debug("SyncHandler {} throws sync exception for {}", new 
Object[] {idp.getName(), credentials, e});
+            LoginException le = new LoginException("Error while syncing 
user.");
+            le.initCause(e);
+            throw le;
         }
         return false;
     }
 
     @Override
     public boolean commit() throws LoginException {
-        if (externalUser == null || syncHandler == null) {
+        if (externalUser == null) {
             return false;
         }
+        Set<? extends Principal> principals = 
getPrincipals(externalUser.getId());
+        if (!principals.isEmpty()) {
+            if (!subject.isReadOnly()) {
+                subject.getPrincipals().addAll(principals);
+                subject.getPublicCredentials().add(credentials);
+                setAuthInfo(new AuthInfoImpl(externalUser.getId(), null, 
principals), subject);
+            } else {
+                log.debug("Could not add information to read only subject {}", 
subject);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean abort() throws LoginException {
+        clearState();
+        // do we need to remove the user again, in case we created it during 
login() ?
+        return true;
+    }
 
+    /**
+     * Initiates synchronization of the external user.
+     * @param user the external user
+     * @throws SyncException if an error occurs
+     */
+    private void syncUser(ExternalUser user) throws SyncException {
         SyncContext context = null;
         try {
             Root root = getRoot();
+            if (root == null) {
+                throw new SyncException("Cannot synchronize user. root == 
null");
+            }
             UserManager userManager = getUserManager();
-            if (root == null || userManager == null) {
-                throw new LoginException("Cannot synchronize user.");
+            if (userManager == null) {
+                throw new SyncException("Cannot synchronize user. userManager 
== null");
             }
             context = syncHandler.createContext(idp, userManager, root);
-            context.sync(externalUser);
+            context.sync(user);
             root.commit();
-
-            Set<? extends Principal> principals = 
getPrincipals(externalUser.getId());
-            if (!principals.isEmpty()) {
-                if (!subject.isReadOnly()) {
-                    subject.getPrincipals().addAll(principals);
-                    subject.getPublicCredentials().add(credentials);
-                    setAuthInfo(new AuthInfoImpl(externalUser.getId(), null, 
principals), subject);
-                } else {
-                    log.debug("Could not add information to read only subject 
{}", subject);
-                }
-                return true;
-            }
-            return false;
-        } catch (SyncException e) {
-            throw new LoginException("User synchronization failed: " + e);
         } catch (CommitFailedException e) {
-            throw new LoginException("User synchronization failed: " + e);
+            throw new SyncException("User synchronization failed during 
commit.", e);
         } finally {
             if (context != null) {
                 context.close();

Modified: 
jackrabbit/oak/trunk/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalLoginModuleTestBase.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalLoginModuleTestBase.java?rev=1575468&r1=1575467&r2=1575468&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalLoginModuleTestBase.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalLoginModuleTestBase.java
 Sat Mar  8 01:18:49 2014
@@ -21,6 +21,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.Map;
 import java.util.Set;
 
 import javax.security.auth.login.AppConfigurationEntry;
@@ -73,7 +74,14 @@ public abstract class ExternalLoginModul
         options.put(ExternalLoginModule.PARAM_IDP_NAME, idp.getName());
 
         // set default sync config
-        setSyncConfig(new DefaultSyncConfig());
+        DefaultSyncConfig cfg = new DefaultSyncConfig();
+        Map<String, String> mapping = new HashMap<String, String>();
+        mapping.put("name", "name");
+        mapping.put("email", "email");
+        mapping.put("profile/name", "profile/name");
+        mapping.put("profile/age", "profile/age");
+        cfg.user().setPropertyMapping(mapping);
+        setSyncConfig(cfg);
     }
 
     @After

Modified: 
jackrabbit/oak/trunk/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/TestIdentityProvider.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/TestIdentityProvider.java?rev=1575468&r1=1575467&r2=1575468&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/TestIdentityProvider.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/TestIdentityProvider.java
 Sat Mar  8 01:18:49 2014
@@ -41,7 +41,7 @@ public class TestIdentityProvider implem
                 .withProperty("name", "Test User")
                 .withProperty("profile/name", "Public Name")
                 .withProperty("profile/age", 72)
-                .withProperty("./email", "[email protected]")
+                .withProperty("email", "[email protected]")
                 .withGroups("a", "b", "c")
         );
     }

Modified: 
jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/LdapIdentity.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/LdapIdentity.java?rev=1575468&r1=1575467&r2=1575468&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/LdapIdentity.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/LdapIdentity.java
 Sat Mar  8 01:18:49 2014
@@ -16,13 +16,10 @@
  */
 package org.apache.jackrabbit.oak.security.authentication.ldap.impl;
 
-import java.util.Collections;
-import java.util.HashMap;
 import java.util.Map;
 
 import javax.annotation.Nonnull;
 
-import 
org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalGroup;
 import 
org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentity;
 import 
org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityException;
 import 
org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityRef;
@@ -42,7 +39,7 @@ public abstract class LdapIdentity imple
 
     private Map<String, ExternalIdentityRef> groups;
 
-    private final Map<String, Object> properties = new HashMap<String, 
Object>();
+    private final LdapIdentityProperties properties = new 
LdapIdentityProperties();
 
     protected LdapIdentity(LdapIdentityProvider provider, ExternalIdentityRef 
ref, String id, String path) {
         this.provider = provider;
@@ -110,10 +107,6 @@ public abstract class LdapIdentity imple
 
     @Override
     public String toString() {
-        final StringBuilder sb = new StringBuilder("LdapIdentity{");
-        sb.append("ref=").append(ref);
-        sb.append(", id='").append(id).append('\'');
-        sb.append('}');
-        return sb.toString();
+        return "LdapIdentity{" + "ref=" + ref + ", id='" + id + '\'' + '}';
     }
 }

Added: 
jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/LdapIdentityProperties.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/LdapIdentityProperties.java?rev=1575468&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/LdapIdentityProperties.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/LdapIdentityProperties.java
 Sat Mar  8 01:18:49 2014
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.oak.security.authentication.ldap.impl;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * {@code LdapIdentityProperties} implements a case insensitive hash map that 
preserves the case of the keys but
+ * ignores the case during lookup.
+ */
+public class LdapIdentityProperties extends HashMap<String, Object> {
+
+    private final Map<String, String> keyMapping = new HashMap<String, 
String>();
+
+    public LdapIdentityProperties(int initialCapacity, float loadFactor) {
+        super(initialCapacity, loadFactor);
+    }
+
+    public LdapIdentityProperties(int initialCapacity) {
+        super(initialCapacity);
+    }
+
+    public LdapIdentityProperties() {
+        super();
+    }
+
+    public LdapIdentityProperties(Map<? extends String, ?> m) {
+        super(m);
+        for (String key: m.keySet()) {
+            keyMapping.put(convert(key), key);
+        }
+    }
+
+    @Override
+    public Object put(String key, Object value) {
+        keyMapping.put(convert(key), key);
+        return super.put(key, value);
+    }
+
+    @Override
+    public Object remove(Object key) {
+        keyMapping.remove(convert(key));
+        return super.remove(key);
+    }
+
+    @Override
+    public Object get(Object key) {
+        String realKey = keyMapping.get(convert(key));
+        return realKey == null ? null : super.get(realKey);
+    }
+
+    @Override
+    public boolean containsKey(Object key) {
+        String realKey = keyMapping.get(convert(key));
+        return realKey != null && super.containsKey(realKey);
+    }
+
+    @Override
+    public void putAll(Map<? extends String, ?> m) {
+        super.putAll(m);
+        for (String key: m.keySet()) {
+            keyMapping.put(convert(key), key);
+        }
+    }
+
+    @Override
+    public void clear() {
+        super.clear();
+        keyMapping.clear();
+    }
+
+    private String convert(Object obj) {
+        String key = obj instanceof String ? (String) obj : 
String.valueOf(obj);
+        return key.toUpperCase(Locale.ENGLISH).toLowerCase(Locale.ENGLISH);
+    }
+}
\ No newline at end of file

Modified: 
jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/external_login_module.md
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/external_login_module.md?rev=1575468&r1=1575467&r2=1575468&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/external_login_module.md
 (original)
+++ 
jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/external_login_module.md
 Sat Mar  8 01:18:49 2014
@@ -36,6 +36,18 @@ what it does not:
 * provide a transparent oak principal provider.
 * offer services for background synchronization of users and groups
 
+### Structure
+The external identity and login handling is split into 3 parts:
+
+1. An external identity provider (IDP). This is a service implementing the 
`ExternalIdentityProvider` interface and is responsible to retrieve and 
authenticate identities towards an external system (e.g. LDAP).
+2. An synchronization handler. This is a service implementing the 
`SyncHandler` interface and is responsible to actually managing the external 
identities within the Oak user management. A very trivial implementation might 
just create users and groups for external ones on demand.
+3. The external login module (ExtLM). This is the connection between JAAS 
login mechanism, the external identity provider and the synchronization handler.
+
+This modularization allows to reuse the same external login module for 
different combinations of IDPs and synchronization handlers. Although in 
practice, systems usually have 1 of each. 
+
+An example where multiple such entities come into play would be the case to 
use several LDAP servers for authentication. Here we would configure 2 LDAP 
IDPs, 1 Sync handler and 2 ExtLMs.
+
+
 ### Types of login modules
 In order to understand how login modules work and how Oak can help providing 
extension points we need to look at how
 JAAS authentication works in general and discuss where the actual 
credential-verification is performed.


Reply via email to