jackrabbit-oak-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From tri...@apache.org
Subject svn commit: r1575468 - in /jackrabbit/oak/trunk: oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ oak-au...
Date Sat, 08 Mar 2014 01:18:49 GMT
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", "test@testuser.com")
+                .withProperty("email", "test@testuser.com")
                 .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.



Mime
View raw message