You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by an...@apache.org on 2015/08/11 09:41:07 UTC

svn commit: r1695223 [1/2] - in /jackrabbit/oak/trunk: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/ oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/ oak-core/src/main/resources/org/apache/jackrabbit/oak/plugins/node...

Author: angela
Date: Tue Aug 11 07:41:07 2015
New Revision: 1695223

URL: http://svn.apache.org/r1695223
Log:
OAK-3003 : Improve login performance with huge group membership

Added:
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/CacheConstants.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/CacheValidatorProvider.java
    jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/CacheValidatorProviderTest.java
    jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserPrincipalProviderWithCacheTest.java
    jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/principal/cache.md
    jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/principal/principalprovider.md
    jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/UserImportCacheTest.java
      - copied, changed from r1692462, jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/UserImportHistoryTest.java
Modified:
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/TypePredicate.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/package-info.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/AbstractGroupPrincipal.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserImporter.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserPrincipalProvider.java
    jackrabbit/oak/trunk/oak-core/src/main/resources/org/apache/jackrabbit/oak/plugins/nodetype/write/builtin_nodetypes.cnd
    jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImplTest.java
    jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/principal.md
    jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/user.md
    jackrabbit/oak/trunk/oak-run/run_concurrent_login.sh
    jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/AbstractLoginTest.java
    jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/BenchmarkRunner.java
    jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/LoginWithMembersTest.java
    jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/LoginWithMembershipTest.java

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/TypePredicate.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/TypePredicate.java?rev=1695223&r1=1695222&r2=1695223&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/TypePredicate.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/TypePredicate.java Tue Aug 11 07:41:07 2015
@@ -25,8 +25,10 @@ import javax.annotation.Nullable;
 import com.google.common.base.Predicate;
 import com.google.common.collect.Iterables;
 
+import org.apache.jackrabbit.oak.api.Tree;
 import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.util.TreeUtil;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Predicates.in;
@@ -165,6 +167,21 @@ public class TypePredicate implements Pr
         }
         return false;
     }
+
+    public boolean apply(@Nullable Tree input) {
+        if (input != null) {
+            init();
+            if (primaryTypes != null
+                    && primaryTypes.contains(TreeUtil.getPrimaryTypeName(input))) {
+                return true;
+            }
+            if (mixinTypes != null
+                    && any(TreeUtil.getNames(input, JCR_MIXINTYPES), in(mixinTypes))) {
+                return true;
+            }
+        }
+        return false;
+    }
 
     //---------------------------------------------------------< Predicate >--
 

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/package-info.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/package-info.java?rev=1695223&r1=1695222&r2=1695223&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/package-info.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/package-info.java Tue Aug 11 07:41:07 2015
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-@Version("1.0")
+@Version("1.1.0")
 @Export(optional = "provide:=true")
 package org.apache.jackrabbit.oak.plugins.nodetype;
 

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/AbstractGroupPrincipal.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/AbstractGroupPrincipal.java?rev=1695223&r1=1695222&r2=1695223&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/AbstractGroupPrincipal.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/AbstractGroupPrincipal.java Tue Aug 11 07:41:07 2015
@@ -44,6 +44,10 @@ abstract class AbstractGroupPrincipal ex
         super(principalName, groupTree, namePathMapper);
     }
 
+    AbstractGroupPrincipal(@Nonnull String principalName, @Nonnull String groupPath, @Nonnull NamePathMapper namePathMapper) {
+        super(principalName, groupPath, namePathMapper);
+    }
+
     abstract UserManager getUserManager();
 
     abstract boolean isEveryone() throws RepositoryException;

Added: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/CacheConstants.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/CacheConstants.java?rev=1695223&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/CacheConstants.java (added)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/CacheConstants.java Tue Aug 11 07:41:07 2015
@@ -0,0 +1,32 @@
+/*
+ * 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.user;
+
+/**
+ * Constants for persisted user management related caches. Currently this only
+ * includes a basic cache for group principals names that is used to populate
+ * the set of {@link java.security.Principal}s as present on the
+ * {@link javax.security.auth.Subject} in the commit phase of the authentication.
+ */
+interface CacheConstants {
+
+    String NT_REP_CACHE = "rep:Cache";
+    String REP_CACHE = "rep:cache";
+    String REP_EXPIRATION = "rep:expiration";
+    String REP_GROUP_PRINCIPAL_NAMES = "rep:groupPrincipalNames";
+
+}
\ No newline at end of file

Added: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/CacheValidatorProvider.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/CacheValidatorProvider.java?rev=1695223&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/CacheValidatorProvider.java (added)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/CacheValidatorProvider.java Tue Aug 11 07:41:07 2015
@@ -0,0 +1,153 @@
+/*
+ * 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.user;
+
+import java.security.Principal;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.plugins.nodetype.TypePredicate;
+import org.apache.jackrabbit.oak.plugins.tree.TreeFactory;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.DefaultValidator;
+import org.apache.jackrabbit.oak.spi.commit.Validator;
+import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider;
+import org.apache.jackrabbit.oak.spi.commit.VisibleValidator;
+import org.apache.jackrabbit.oak.spi.security.principal.SystemPrincipal;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Validator provider to ensure that the principal-cache stored with a given
+ * user is only maintained by the {@link org.apache.jackrabbit.oak.security.user.UserPrincipalProvider}
+ * associated with a internal system session.
+ */
+class CacheValidatorProvider extends ValidatorProvider implements CacheConstants {
+
+    private final boolean isSystem;
+
+    CacheValidatorProvider(@Nonnull Set<Principal> principals) {
+        super();
+        isSystem = principals.contains(SystemPrincipal.INSTANCE);
+    }
+
+    @CheckForNull
+    @Override
+    protected Validator getRootValidator(NodeState before, NodeState after, CommitInfo info) {
+        TypePredicate cachePredicate = new TypePredicate(after, NT_REP_CACHE);
+        boolean isValidCommitInfo = CommitMarker.isValidCommitInfo(info);
+        return new CacheValidator(TreeFactory.createReadOnlyTree(before), TreeFactory.createReadOnlyTree(after), cachePredicate, isValidCommitInfo);
+    }
+
+    //--------------------------------------------------------------------------
+
+    static Map<String, Object> asCommitAttributes() {
+        return Collections.<String, Object>singletonMap(CommitMarker.KEY, CommitMarker.INSTANCE);
+    }
+
+    private static final class CommitMarker {
+
+        private static final String KEY = CommitMarker.class.getName();
+
+        private static final CommitMarker INSTANCE = new CommitMarker();
+
+        private static boolean isValidCommitInfo(@Nonnull CommitInfo commitInfo) {
+            return CommitMarker.INSTANCE == commitInfo.getInfo().get(CommitMarker.KEY);
+        }
+
+        private CommitMarker() {}
+    }
+
+    private static CommitFailedException constraintViolation(int code, @Nonnull String message) {
+        return new CommitFailedException(CommitFailedException.CONSTRAINT, code, message);
+    }
+
+    //-----------------------------------------------------< CacheValidator >---
+    private final class CacheValidator extends DefaultValidator {
+
+        private final Tree parentBefore;
+        private final Tree parentAfter;
+
+        private final TypePredicate cachePredicate;
+        private final boolean isValidCommitInfo;
+
+        private final boolean isCache;
+
+        private CacheValidator(@Nullable Tree parentBefore, @Nonnull Tree parentAfter, TypePredicate cachePredicate, boolean isValidCommitInfo) {
+            this.parentBefore = parentBefore;
+            this.parentAfter = parentAfter;
+
+            this.cachePredicate = cachePredicate;
+            this.isValidCommitInfo = isValidCommitInfo;
+
+            isCache = isCache(parentAfter);
+        }
+
+        @Override
+        public void propertyAdded(PropertyState after) throws CommitFailedException {
+            if (isCache) {
+                checkValidCommit();
+            }
+        }
+
+        @Override
+        public void propertyChanged(PropertyState before, PropertyState after) throws CommitFailedException {
+            if (isCache) {
+                checkValidCommit();
+            }
+        }
+
+        @Override
+        public Validator childNodeChanged(String name, NodeState before, NodeState after) throws CommitFailedException {
+            Tree beforeTree = (parentBefore == null) ? null : parentBefore.getChild(name);
+            Tree afterTree = parentAfter.getChild(name);
+
+            if (isCache || isCache(beforeTree) || isCache(afterTree)) {
+                checkValidCommit();
+            }
+
+            return new VisibleValidator(new CacheValidator(beforeTree, afterTree, cachePredicate, isValidCommitInfo), true, true);
+        }
+
+        @Override
+        public Validator childNodeAdded(String name, NodeState after) throws CommitFailedException {
+            Tree tree = checkNotNull(parentAfter.getChild(name));
+            if (isCache || isCache(tree)) {
+                checkValidCommit();
+            }
+            return new VisibleValidator(new CacheValidator(null, tree, cachePredicate, isValidCommitInfo), true, true);
+        }
+
+        private boolean isCache(@CheckForNull Tree tree) {
+            return tree != null && (REP_CACHE.equals(tree.getName()) || cachePredicate.apply(tree));
+        }
+
+        private void checkValidCommit() throws CommitFailedException {
+            if (!(isSystem && isValidCommitInfo)) {
+                throw constraintViolation(34, "Attempt to create or change the system maintained cache.");
+            }
+        }
+    }
+}
\ No newline at end of file

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java?rev=1695223&r1=1695222&r2=1695223&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java Tue Aug 11 07:41:07 2015
@@ -25,6 +25,7 @@ import java.util.Set;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import com.google.common.collect.ImmutableList;
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.Properties;
@@ -109,7 +110,13 @@ import org.apache.jackrabbit.oak.spi.xml
         @Property(name = UserConstants.PARAM_PASSWORD_HISTORY_SIZE,
                 label = "Maximum Password History Size",
                 description = "Maximum number of passwords recorded for a user after changing her password (NOTE: upper limit is 1000). When changing the password the new password must not be present in the password history. A value of 0 indicates no password history is recorded.",
-                intValue = UserConstants.PASSWORD_HISTORY_DISABLED_SIZE)
+                intValue = UserConstants.PASSWORD_HISTORY_DISABLED_SIZE),
+        @Property(name = UserPrincipalProvider.PARAM_CACHE_EXPIRATION,
+                label = "Principal Cache Expiration",
+                description = "Optional configuration defining the number of milliseconds " +
+                        "until the principal cache expires (NOTE: currently only respected for principal resolution with the internal system session such as used for login). " +
+                        "If not set or equal/lower than zero no caches are created/evaluated.",
+                longValue = UserPrincipalProvider.EXPIRATION_NO_CACHE)
 })
 public class UserConfigurationImpl extends ConfigurationBase implements UserConfiguration, SecurityConfiguration {
 
@@ -162,7 +169,7 @@ public class UserConfigurationImpl exten
     @Nonnull
     @Override
     public List<? extends ValidatorProvider> getValidators(@Nonnull String workspaceName, @Nonnull Set<Principal> principals, @Nonnull MoveTracker moveTracker) {
-        return Collections.singletonList(new UserValidatorProvider(getParameters()));
+        return ImmutableList.of(new UserValidatorProvider(getParameters()), new CacheValidatorProvider(principals));
     }
 
     @Nonnull

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserImporter.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserImporter.java?rev=1695223&r1=1695222&r2=1695223&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserImporter.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserImporter.java Tue Aug 11 07:41:07 2015
@@ -327,31 +327,36 @@ class UserImporter implements ProtectedP
 
     @Override
     public void propertiesCompleted(@Nonnull Tree protectedParent) throws RepositoryException {
-        Authorizable a = userManager.getAuthorizable(protectedParent);
-        if (a == null) {
-            // not an authorizable
-            return;
-        }
-
-        // make sure the authorizable ID property is always set even if the
-        // authorizable defined by the imported XML didn't provide rep:authorizableID
-        if (!protectedParent.hasProperty(REP_AUTHORIZABLE_ID)) {
-            protectedParent.setProperty(REP_AUTHORIZABLE_ID, a.getID(), Type.STRING);
-        }
-
-        /*
-        Execute authorizable actions for a NEW user at this point after
-        having set the password and the principal name (all protected properties
-        have been processed now).
-        */
-        if (protectedParent.getStatus() == Tree.Status.NEW) {
-            if (a.isGroup()) {
-                userManager.onCreate((Group) a);
-            } else {
-                userManager.onCreate((User) a, currentPw);
+        if (isCacheNode(protectedParent)) {
+            // remove the cache if present
+            protectedParent.remove();
+        } else {
+            Authorizable a = userManager.getAuthorizable(protectedParent);
+            if (a == null) {
+                // not an authorizable
+                return;
+            }
+
+            // make sure the authorizable ID property is always set even if the
+            // authorizable defined by the imported XML didn't provide rep:authorizableID
+            if (!protectedParent.hasProperty(REP_AUTHORIZABLE_ID)) {
+                protectedParent.setProperty(REP_AUTHORIZABLE_ID, a.getID(), Type.STRING);
+            }
+
+            /*
+            Execute authorizable actions for a NEW user at this point after
+            having set the password and the principal name (all protected properties
+            have been processed now).
+            */
+            if (protectedParent.getStatus() == Tree.Status.NEW) {
+                if (a.isGroup()) {
+                    userManager.onCreate((Group) a);
+                } else {
+                    userManager.onCreate((User) a, currentPw);
+                }
             }
+            currentPw = null;
         }
-        currentPw = null;
     }
 
     @Override
@@ -518,6 +523,10 @@ class UserImporter implements ProtectedP
         return true;
     }
 
+    private static boolean isCacheNode(@Nonnull Tree tree) {
+        return tree.exists() && CacheConstants.REP_CACHE.equals(tree.getName()) && CacheConstants.NT_REP_CACHE.equals(TreeUtil.getPrimaryTypeName(tree));
+    }
+
     /**
      * Handling the import behavior
      *

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserPrincipalProvider.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserPrincipalProvider.java?rev=1695223&r1=1695222&r2=1695223&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserPrincipalProvider.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserPrincipalProvider.java Tue Aug 11 07:41:07 2015
@@ -20,35 +20,44 @@ import java.security.Principal;
 import java.security.acl.Group;
 import java.text.ParseException;
 import java.util.Collections;
+import java.util.Date;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Set;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.jcr.AccessDeniedException;
 import javax.jcr.RepositoryException;
 
 import com.google.common.base.Function;
+import com.google.common.base.Joiner;
 import com.google.common.base.Predicate;
 import com.google.common.base.Predicates;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Iterators;
 import org.apache.jackrabbit.api.security.principal.PrincipalManager;
 import org.apache.jackrabbit.api.security.user.Authorizable;
 import org.apache.jackrabbit.api.security.user.UserManager;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
 import org.apache.jackrabbit.oak.api.PropertyState;
 import org.apache.jackrabbit.oak.api.Result;
 import org.apache.jackrabbit.oak.api.ResultRow;
 import org.apache.jackrabbit.oak.api.Root;
 import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.commons.LongUtils;
 import org.apache.jackrabbit.oak.namepath.NamePathMapper;
 import org.apache.jackrabbit.oak.security.user.query.QueryUtil;
 import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal;
 import org.apache.jackrabbit.oak.spi.security.principal.PrincipalImpl;
 import org.apache.jackrabbit.oak.spi.security.principal.PrincipalProvider;
+import org.apache.jackrabbit.oak.spi.security.principal.SystemPrincipal;
 import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType;
 import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration;
 import org.apache.jackrabbit.oak.spi.security.user.UserConstants;
 import org.apache.jackrabbit.oak.spi.security.user.util.UserUtil;
+import org.apache.jackrabbit.oak.util.NodeUtil;
+import org.apache.jackrabbit.util.Text;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -64,6 +73,11 @@ class UserPrincipalProvider implements P
 
     private static final Logger log = LoggerFactory.getLogger(UserPrincipalProvider.class);
 
+    static final String PARAM_CACHE_EXPIRATION = "cacheExpiration";
+    static final long EXPIRATION_NO_CACHE = 0;
+
+    private static final long MEMBERSHIP_THRESHOLD = 0;
+
     private final Root root;
     private final UserConfiguration config;
     private final NamePathMapper namePathMapper;
@@ -71,6 +85,9 @@ class UserPrincipalProvider implements P
     private final UserProvider userProvider;
     private final MembershipProvider membershipProvider;
 
+    private final long expiration;
+    private final boolean cacheEnabled;
+
     UserPrincipalProvider(@Nonnull Root root,
                           @Nonnull UserConfiguration userConfiguration,
                           @Nonnull NamePathMapper namePathMapper) {
@@ -80,6 +97,9 @@ class UserPrincipalProvider implements P
 
         this.userProvider = new UserProvider(root, config.getParameters());
         this.membershipProvider = new MembershipProvider(root, config.getParameters());
+
+        expiration = config.getParameters().getConfigValue(PARAM_CACHE_EXPIRATION, EXPIRATION_NO_CACHE);
+        cacheEnabled = (expiration > EXPIRATION_NO_CACHE && root.getContentSession().getAuthInfo().getPrincipals().contains(SystemPrincipal.INSTANCE));
     }
 
     //--------------------------------------------------< PrincipalProvider >---
@@ -219,23 +239,108 @@ class UserPrincipalProvider implements P
 
     @Nonnull
     private Set<Group> getGroupMembership(@Nonnull Tree authorizableTree) {
-        Set<Group> groupPrincipals = new HashSet<Group>();
-        Iterator<String> groupPaths = membershipProvider.getMembership(authorizableTree, true);
-        while (groupPaths.hasNext()) {
-            Tree groupTree = userProvider.getAuthorizableByPath(groupPaths.next());
-            if (groupTree != null && UserUtil.isType(groupTree, AuthorizableType.GROUP)) {
-                Group gr = createGroupPrincipal(groupTree);
-                if (gr != null) {
-                    groupPrincipals.add(createGroupPrincipal(groupTree));
+        Set<Group> groupPrincipals = null;
+        NodeUtil authorizableNode = new NodeUtil(authorizableTree);
+        boolean doCache = cacheEnabled && UserUtil.isType(authorizableTree, AuthorizableType.USER);
+        if (doCache) {
+            groupPrincipals = readGroupsFromCache(authorizableNode);
+        }
+
+        // caching not configured or cache expired: use the membershipProvider to calculate
+        if (groupPrincipals == null) {
+            groupPrincipals = new HashSet<Group>();
+            Iterator<String> groupPaths = membershipProvider.getMembership(authorizableTree, true);
+            while (groupPaths.hasNext()) {
+                Tree groupTree = userProvider.getAuthorizableByPath(groupPaths.next());
+                if (groupTree != null && UserUtil.isType(groupTree, AuthorizableType.GROUP)) {
+                    Group gr = createGroupPrincipal(groupTree);
+                    if (gr != null) {
+                        groupPrincipals.add(createGroupPrincipal(groupTree));
+                    }
                 }
             }
+
+            // remember the regular groups in case caching is enabled
+            if (doCache) {
+                cacheGroups(authorizableNode, groupPrincipals);
+            }
         }
+
         // add the dynamic everyone principal group which is not included in
         // the 'getMembership' call.
         groupPrincipals.add(EveryonePrincipal.getInstance());
         return groupPrincipals;
     }
 
+    private void cacheGroups(@Nonnull NodeUtil authorizableNode, @Nonnull Set<Group> groupPrincipals) {
+        try {
+            root.refresh();
+            NodeUtil cache = authorizableNode.getChild(CacheConstants.REP_CACHE);
+            if (cache == null) {
+                if (groupPrincipals.size() <= MEMBERSHIP_THRESHOLD) {
+                    log.debug("Omit cache creation for user without group membership at " + authorizableNode.getTree().getPath());
+                    return;
+                } else {
+                    log.debug("Create new group membership cache at " + authorizableNode.getTree().getPath());
+                    cache = authorizableNode.addChild(CacheConstants.REP_CACHE, CacheConstants.NT_REP_CACHE);
+                }
+            }
+
+            cache.setLong(CacheConstants.REP_EXPIRATION, LongUtils.calculateExpirationTime(expiration));
+            String value = (groupPrincipals.isEmpty()) ? "" : Joiner.on(",").join(Iterables.transform(groupPrincipals, new Function<Group, String>() {
+                @Override
+                public String apply(Group input) {
+                    return Text.escape(input.getName());
+                }
+            }));
+            cache.setString(CacheConstants.REP_GROUP_PRINCIPAL_NAMES, value);
+
+            root.commit(CacheValidatorProvider.asCommitAttributes());
+            log.debug("Cached group membership at " + authorizableNode.getTree().getPath());
+
+        } catch (AccessDeniedException e) {
+            log.debug("Failed to cache group membership", e.getMessage());
+        } catch (CommitFailedException e) {
+            log.debug("Failed to cache group membership", e.getMessage(), e);
+        } finally {
+            root.refresh();
+        }
+    }
+
+    @CheckForNull
+    private Set<Group> readGroupsFromCache(@Nonnull NodeUtil authorizableNode) {
+        NodeUtil principalCache = authorizableNode.getChild(CacheConstants.REP_CACHE);
+        if (principalCache == null) {
+            log.debug("No group cache at " + authorizableNode.getTree().getPath());
+            return null;
+        }
+
+        if (isValidCache(principalCache)) {
+            log.debug("Reading group membership at " + authorizableNode.getTree().getPath());
+
+            String str = principalCache.getString(CacheConstants.REP_GROUP_PRINCIPAL_NAMES, null);
+            if (str == null || str.isEmpty()) {
+                return new HashSet<Group>(1);
+            }
+
+            Set<Group> groups = new HashSet<Group>();
+            for (String s : Text.explode(str, ',')) {
+                final String name = Text.unescape(s);
+                groups.add(new CachedGroupPrincipal(name));
+            }
+            return groups;
+        } else {
+            log.debug("Expired group cache for " + authorizableNode.getTree().getPath());
+            return null;
+        }
+    }
+
+    private static boolean isValidCache(NodeUtil principalCache)  {
+        long expirationTime = principalCache.getLong(CacheConstants.REP_EXPIRATION, EXPIRATION_NO_CACHE);
+        long now = new Date().getTime();
+        return expirationTime > EXPIRATION_NO_CACHE && now < expirationTime;
+    }
+
     private static String buildSearchPattern(String nameHint) {
         if (nameHint == null) {
             return "%";
@@ -246,7 +351,6 @@ class UserPrincipalProvider implements P
             sb.append('%');
             return sb.toString();
         }
-
     }
 
     private static boolean matchesEveryone(String nameHint, int searchType) {
@@ -288,19 +392,22 @@ class UserPrincipalProvider implements P
         }
     }
 
-    /**
-     * Implementation of {@link AbstractGroupPrincipal} that reads the underlying
-     * authorizable group lazily in case the group membership must be retrieved.
-     */
-    private final class GroupPrincipal extends AbstractGroupPrincipal {
+    //--------------------------------------------------------------------------
+    // Group Principal implementations that retrieve member information on demand
+    //--------------------------------------------------------------------------
+
+    private abstract class BaseGroupPrincipal extends AbstractGroupPrincipal {
 
         private UserManager userManager;
-        private org.apache.jackrabbit.api.security.user.Group group;
 
-        GroupPrincipal(@Nonnull String principalName, @Nonnull Tree groupTree) {
+        BaseGroupPrincipal(@Nonnull String principalName, @Nonnull Tree groupTree) {
             super(principalName, groupTree, namePathMapper);
         }
 
+        BaseGroupPrincipal(@Nonnull String principalName, @Nonnull String groupPath) {
+            super(principalName, groupPath, namePathMapper);
+        }
+
         @Override
         UserManager getUserManager() {
             if (userManager == null) {
@@ -327,7 +434,25 @@ class UserPrincipalProvider implements P
             return (g == null) ? Iterators.<Authorizable>emptyIterator() : g.getMembers();
         }
 
-        private org.apache.jackrabbit.api.security.user.Group getGroup() throws RepositoryException {
+        @CheckForNull
+        abstract org.apache.jackrabbit.api.security.user.Group getGroup()throws RepositoryException;
+    }
+
+    /**
+     * Implementation of {@link AbstractGroupPrincipal} that reads the underlying
+     * authorizable group lazily in case the group membership must be retrieved.
+     */
+    private final class GroupPrincipal extends BaseGroupPrincipal {
+
+        private org.apache.jackrabbit.api.security.user.Group group;
+
+        GroupPrincipal(@Nonnull String principalName, @Nonnull Tree groupTree) {
+            super(principalName, groupTree);
+        }
+
+        @Override
+        @CheckForNull
+        org.apache.jackrabbit.api.security.user.Group getGroup() throws RepositoryException {
             if (group == null) {
                 Authorizable authorizable = getUserManager().getAuthorizable(this);
                 if (authorizable != null && authorizable.isGroup()) {
@@ -337,4 +462,44 @@ class UserPrincipalProvider implements P
             return group;
         }
     }
+
+    private final class CachedGroupPrincipal extends BaseGroupPrincipal {
+
+        private org.apache.jackrabbit.api.security.user.Group group;
+
+        CachedGroupPrincipal(@Nonnull String principalName) {
+            super(principalName, "");
+        }
+
+        @Override
+        String getOakPath() {
+            String groupPath = getPath();
+            return (groupPath == null) ? null : namePathMapper.getOakPath(getPath());
+        }
+
+        @Override
+        public String getPath() {
+            try {
+                org.apache.jackrabbit.api.security.user.Group gr = getGroup();
+                return (gr == null) ? null : gr.getPath();
+            } catch (RepositoryException e) {
+                log.error("Failed to retrieve path from group principal", e.getMessage());
+                return null;
+            }
+        }
+
+        @Override
+        @CheckForNull
+        org.apache.jackrabbit.api.security.user.Group getGroup() throws RepositoryException {
+            if (group == null) {
+                Authorizable authorizable = getUserManager().getAuthorizable(new PrincipalImpl(getName()));
+                if (authorizable != null && authorizable.isGroup()) {
+                    group = (org.apache.jackrabbit.api.security.user.Group) authorizable;
+                }
+            }
+            return group;
+        }
+    }
 }
+
+

Modified: jackrabbit/oak/trunk/oak-core/src/main/resources/org/apache/jackrabbit/oak/plugins/nodetype/write/builtin_nodetypes.cnd
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/resources/org/apache/jackrabbit/oak/plugins/nodetype/write/builtin_nodetypes.cnd?rev=1695223&r1=1695222&r2=1695223&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/resources/org/apache/jackrabbit/oak/plugins/nodetype/write/builtin_nodetypes.cnd (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/resources/org/apache/jackrabbit/oak/plugins/nodetype/write/builtin_nodetypes.cnd Tue Aug 11 07:41:07 2015
@@ -287,6 +287,17 @@
  - * (UNDEFINED) IGNORE
  + * (nt:base) = rep:Unstructured IGNORE
 
+/**
+ * Unstructured base node type for protected repository internal information
+ * that is protected and must not be copied to the version store OPV
+ *
+ * @since oak 1.4
+ */
+[rep:UnstructuredProtected] abstract
+- * (UNDEFINED) protected multiple IGNORE
+- * (UNDEFINED) protected IGNORE
++ * (rep:UnstructuredProtected) protected IGNORE
+
 //------------------------------------------------------------------------------
 // R E F E R E N C E A B L E
 //------------------------------------------------------------------------------
@@ -754,6 +765,12 @@
 [rep:MemberReferencesList]
   + * (rep:MemberReferences) = rep:MemberReferences protected COPY
 
+/**
+ * @since oak 1.4
+ */
+[rep:Cache] > rep:UnstructuredProtected
+  - rep:expiration (LONG) protected IGNORE
+
 // -----------------------------------------------------------------------------
 // Privilege Management
 // -----------------------------------------------------------------------------

Added: jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/CacheValidatorProviderTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/CacheValidatorProviderTest.java?rev=1695223&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/CacheValidatorProviderTest.java (added)
+++ jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/CacheValidatorProviderTest.java Tue Aug 11 07:41:07 2015
@@ -0,0 +1,280 @@
+/*
+ * 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.user;
+
+import java.security.PrivilegedExceptionAction;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nonnull;
+import javax.jcr.NoSuchWorkspaceException;
+import javax.jcr.RepositoryException;
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginException;
+
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.oak.AbstractSecurityTest;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.api.ContentSession;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.api.Root;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.plugins.memory.PropertyStates;
+import org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.security.authentication.SystemSubject;
+import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal;
+import org.apache.jackrabbit.oak.util.NodeUtil;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class CacheValidatorProviderTest extends AbstractSecurityTest {
+
+    private Group testGroup;
+    private Authorizable[] authorizables;
+
+    @Override
+    public void before() throws Exception {
+        super.before();
+
+        testGroup = getUserManager(root).createGroup("testGroup_" + UUID.randomUUID());
+        root.commit();
+
+        authorizables = new Authorizable[] {getTestUser(), testGroup};
+    }
+
+    @Override
+    public void after() throws Exception {
+        try {
+            if (testGroup != null) {
+                testGroup.remove();
+                root.commit();
+            }
+        } finally {
+            super.after();
+        }
+    }
+
+    private Tree getAuthorizableTree(@Nonnull Authorizable authorizable) throws RepositoryException {
+        return root.getTree(authorizable.getPath());
+    }
+
+    private Tree getCache(@Nonnull Authorizable authorizable) throws Exception {
+        ContentSession cs = Subject.doAs(SystemSubject.INSTANCE, new PrivilegedExceptionAction<ContentSession>() {
+            @Override
+            public ContentSession run() throws LoginException, NoSuchWorkspaceException {
+                return login(null);
+
+            }
+        });
+        try {
+            Root r = cs.getLatestRoot();
+            NodeUtil n = new NodeUtil(r.getTree(authorizable.getPath()));
+            NodeUtil c = n.getOrAddChild(CacheConstants.REP_CACHE, CacheConstants.NT_REP_CACHE);
+            c.setLong(CacheConstants.REP_EXPIRATION, 1);
+            r.commit(CacheValidatorProvider.asCommitAttributes());
+        } finally {
+            cs.close();
+        }
+
+        root.refresh();
+        return root.getTree(authorizable.getPath()).getChild(CacheConstants.REP_CACHE);
+    }
+
+    @Test
+    public void testCreateCacheByName() throws RepositoryException {
+        for (Authorizable a : authorizables) {
+            try {
+                NodeUtil node = new NodeUtil(getAuthorizableTree(a));
+                node.addChild(CacheConstants.REP_CACHE, JcrConstants.NT_UNSTRUCTURED);
+                root.commit();
+                fail("Creating rep:cache node below a user or group must fail.");
+            } catch (CommitFailedException e) {
+                assertTrue(e.isConstraintViolation());
+                assertEquals(34, e.getCode());
+            } finally {
+                root.refresh();
+            }
+        }
+    }
+
+    @Test
+    public void testCreateCacheByNodeType() throws RepositoryException {
+        for (Authorizable a : authorizables) {
+            try {
+                NodeUtil node = new NodeUtil(getAuthorizableTree(a));
+                NodeUtil cache = node.addChild("childNode", CacheConstants.NT_REP_CACHE);
+                cache.setLong(CacheConstants.REP_EXPIRATION, 1);
+                root.commit();
+                fail("Creating node with nt rep:Cache below a user or group must fail.");
+            } catch (CommitFailedException e) {
+                assertTrue(e.isConstraintViolation());
+                assertEquals(34, e.getCode());
+            } finally {
+                root.refresh();
+            }
+        }
+    }
+
+    @Test
+    public void testChangePrimaryType() throws RepositoryException {
+        for (Authorizable a : authorizables) {
+            try {
+                NodeUtil node = new NodeUtil(getAuthorizableTree(a));
+                NodeUtil cache = node.addChild("childNode", JcrConstants.NT_UNSTRUCTURED);
+                root.commit();
+
+                cache.setName(JcrConstants.JCR_PRIMARYTYPE, CacheConstants.NT_REP_CACHE);
+                cache.setLong(CacheConstants.REP_EXPIRATION, 1);
+                root.commit();
+                fail("Changing primary type of residual node below an user/group to rep:Cache must fail.");
+            } catch (CommitFailedException e) {
+                assertTrue(e.isConstraintViolation());
+                assertEquals(34, e.getCode());
+            } finally {
+                root.refresh();
+            }
+        }
+    }
+
+    @Test
+    public void testCreateCacheWithCommitInfo() throws RepositoryException {
+        for (Authorizable a : authorizables) {
+            try {
+                NodeUtil node = new NodeUtil(getAuthorizableTree(a));
+                NodeUtil cache = node.addChild(CacheConstants.REP_CACHE, CacheConstants.NT_REP_CACHE);
+                cache.setLong(CacheConstants.REP_EXPIRATION, 1);
+                root.commit(CacheValidatorProvider.asCommitAttributes());
+                fail("Creating rep:cache node below a user or group must fail.");
+            } catch (CommitFailedException e) {
+                assertTrue(e.isConstraintViolation());
+                assertEquals(34, e.getCode());
+            } finally {
+                root.refresh();
+            }
+        }
+    }
+
+    @Test
+    public void testCreateCacheBelowProfile() throws Exception {
+        try {
+            NodeUtil node = new NodeUtil(getAuthorizableTree(getTestUser()));
+            NodeUtil child = node.addChild("profile", NodeTypeConstants.NT_OAK_UNSTRUCTURED);
+            child.addChild(CacheConstants.REP_CACHE, CacheConstants.NT_REP_CACHE).setLong(CacheConstants.REP_EXPIRATION, 23);
+            root.commit(CacheValidatorProvider.asCommitAttributes());
+            fail("Creating rep:cache node below a user or group must fail.");
+        } catch (CommitFailedException e) {
+            assertTrue(e.isConstraintViolation());
+            assertEquals(34, e.getCode());
+        } finally {
+            root.refresh();
+        }
+    }
+
+    @Test
+    public void testCreateCacheBelowPersistedProfile() throws Exception {
+        try {
+            NodeUtil node = new NodeUtil(getAuthorizableTree(getTestUser()));
+            NodeUtil child = node.addChild("profile", NodeTypeConstants.NT_OAK_UNSTRUCTURED);
+            root.commit();
+
+            child.addChild(CacheConstants.REP_CACHE, CacheConstants.NT_REP_CACHE).setLong(CacheConstants.REP_EXPIRATION, 23);
+            root.commit(CacheValidatorProvider.asCommitAttributes());
+            fail("Creating rep:cache node below a user or group must fail.");
+        } catch (CommitFailedException e) {
+            assertTrue(e.isConstraintViolation());
+            assertEquals(34, e.getCode());
+        } finally {
+            root.refresh();
+        }
+    }
+
+    @Test
+    public void testModifyCache() throws Exception {
+        List<PropertyState> props = new ArrayList();
+        props.add(PropertyStates.createProperty(CacheConstants.REP_EXPIRATION, 25));
+        props.add(PropertyStates.createProperty(CacheConstants.REP_GROUP_PRINCIPAL_NAMES, EveryonePrincipal.NAME));
+        props.add(PropertyStates.createProperty(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_UNSTRUCTURED));
+        props.add(PropertyStates.createProperty("residualProp", "anyvalue"));
+
+        Tree cache = getCache(getTestUser());
+        for (PropertyState prop : props) {
+            try {
+                cache.setProperty(prop);
+                root.commit(CacheValidatorProvider.asCommitAttributes());
+
+                fail("Modifying rep:cache node below a user or group must fail.");
+            } catch (CommitFailedException e) {
+                assertTrue(e.isConstraintViolation());
+                assertEquals(34, e.getCode());
+            } finally {
+                root.refresh();
+            }
+        }
+    }
+
+    @Test
+    public void testNestedCache() throws Exception {
+        NodeUtil cache = new NodeUtil(getCache(getTestUser()));
+        try {
+            NodeUtil c = cache.getOrAddChild(CacheConstants.REP_CACHE, CacheConstants.NT_REP_CACHE);
+            c.setLong(CacheConstants.REP_EXPIRATION, 223);
+            root.commit(CacheValidatorProvider.asCommitAttributes());
+
+            fail("Creating nested cache must fail.");
+        } catch (CommitFailedException e) {
+            assertTrue(e.isConstraintViolation());
+            assertEquals(34, e.getCode());
+        } finally {
+            root.refresh();
+        }
+    }
+
+    @Test
+    public void testRemoveCache() throws Exception {
+        Tree cache = getCache(getTestUser());
+        cache.remove();
+        root.commit();
+    }
+
+    @Test
+    public void testCreateCacheOutsideOfAuthorizable() throws Exception {
+        NodeUtil n = new NodeUtil(root.getTree("/"));
+        try {
+            NodeUtil child = n.addChild(CacheConstants.REP_CACHE, CacheConstants.NT_REP_CACHE);
+            child.setLong(CacheConstants.REP_EXPIRATION, 1);
+            root.commit();
+            fail("Using rep:cache/rep:Cache outside a user or group must fail.");
+        } catch (CommitFailedException e) {
+            assertTrue(e.isConstraintViolation());
+            assertEquals(34, e.getCode());
+        } finally {
+            root.refresh();
+            Tree c = n.getTree().getChild(CacheConstants.REP_CACHE);
+            if (c.exists()) {
+                c.remove();
+                root.commit();
+            }
+        }
+
+    }
+}
\ No newline at end of file

Modified: jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImplTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImplTest.java?rev=1695223&r1=1695222&r2=1695223&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImplTest.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImplTest.java Tue Aug 11 07:41:07 2015
@@ -16,9 +16,15 @@
  */
 package org.apache.jackrabbit.oak.security.user;
 
+import java.security.Principal;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 
+import com.google.common.collect.Lists;
 import org.apache.jackrabbit.oak.AbstractSecurityTest;
+import org.apache.jackrabbit.oak.spi.commit.MoveTracker;
+import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider;
 import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
 import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration;
 import org.apache.jackrabbit.oak.spi.security.user.UserConstants;
@@ -28,6 +34,7 @@ import org.apache.jackrabbit.oak.spi.xml
 import org.junit.Test;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 public class UserConfigurationImplTest extends AbstractSecurityTest {
 
@@ -49,6 +56,23 @@ public class UserConfigurationImplTest e
     }
 
     @Test
+    public void testValidators() {
+        UserConfigurationImpl configuration = new UserConfigurationImpl(getSecurityProvider());
+        List<? extends ValidatorProvider> validators = configuration.getValidators(adminSession.getWorkspaceName(), Collections.<Principal>emptySet(), new MoveTracker());
+        assertEquals(2, validators.size());
+
+        List<String> clNames = Lists.newArrayList(
+                UserValidatorProvider.class.getName(),
+                CacheValidatorProvider.class.getName());
+
+        for (ValidatorProvider vp : validators) {
+            clNames.remove(vp.getClass().getName());
+        }
+
+        assertTrue(clNames.isEmpty());
+    }
+
+    @Test
     public void testUserConfigurationWithConstructor() throws Exception {
         UserConfigurationImpl userConfiguration = new UserConfigurationImpl(getSecurityProvider());
         testConfigurationParameters(userConfiguration.getParameters());

Added: jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserPrincipalProviderWithCacheTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserPrincipalProviderWithCacheTest.java?rev=1695223&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserPrincipalProviderWithCacheTest.java (added)
+++ jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserPrincipalProviderWithCacheTest.java Tue Aug 11 07:41:07 2015
@@ -0,0 +1,638 @@
+/*
+ * 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.user;
+
+import java.security.Principal;
+import java.security.PrivilegedExceptionAction;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import javax.annotation.Nullable;
+import javax.jcr.NoSuchWorkspaceException;
+import javax.jcr.SimpleCredentials;
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginException;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.api.security.principal.PrincipalIterator;
+import org.apache.jackrabbit.api.security.principal.PrincipalManager;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.api.ContentSession;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.api.Root;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.plugins.memory.PropertyStates;
+import org.apache.jackrabbit.oak.spi.security.ConfigurationBase;
+import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
+import org.apache.jackrabbit.oak.spi.security.authentication.SystemSubject;
+import org.apache.jackrabbit.oak.spi.security.principal.AbstractPrincipalProviderTest;
+import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal;
+import org.apache.jackrabbit.oak.spi.security.principal.PrincipalImpl;
+import org.apache.jackrabbit.oak.spi.security.principal.PrincipalProvider;
+import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration;
+import org.apache.jackrabbit.oak.util.NodeUtil;
+import org.apache.jackrabbit.oak.util.TreeUtil;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Testing the optional caching with the {@link org.apache.jackrabbit.oak.security.user.UserPrincipalProvider}.
+ */
+public class UserPrincipalProviderWithCacheTest extends AbstractPrincipalProviderTest {
+
+    private String userId;
+    private String groupId;
+    private Group testGroup;
+
+    private String groupId2;
+    private Group testGroup2;
+
+    private ContentSession systemSession;
+    private Root systemRoot;
+
+    @Override
+    public void before() throws Exception {
+        super.before();
+
+        userId = getTestUser().getID();
+
+        groupId = "testGroup" + UUID.randomUUID();
+        testGroup = getUserManager(root).createGroup(groupId);
+        testGroup.addMember(getTestUser());
+
+        groupId2 = "testGroup2" + UUID.randomUUID();
+        testGroup2 = getUserManager(root).createGroup(groupId2);
+        testGroup.addMember(testGroup2);
+
+        root.commit();
+
+        systemSession = getSystemSession();
+        systemRoot = systemSession.getLatestRoot();
+    }
+
+    @Override
+    public void after() throws Exception {
+        try {
+            if (systemSession != null) {
+                systemSession.close();
+            }
+
+            root.refresh();
+            Group gr = getUserManager(root).getAuthorizable(groupId, Group.class);
+            if (gr != null) {
+                gr.remove();
+                root.commit();
+            }
+
+            gr = getUserManager(root).getAuthorizable(groupId2, Group.class);
+            if (gr != null) {
+                gr.remove();
+                root.commit();
+            }
+        } finally {
+            super.after();
+        }
+    }
+
+    @Override
+    protected ConfigurationParameters getSecurityConfigParameters() {
+        return ConfigurationParameters.of(
+                UserConfiguration.NAME,
+                ConfigurationParameters.of(UserPrincipalProvider.PARAM_CACHE_EXPIRATION, 3600 * 1000)
+        );
+    }
+
+    @Override
+    protected PrincipalProvider createPrincipalProvider() {
+        return createPrincipalProvider(root);
+    }
+
+    private PrincipalProvider createPrincipalProvider(Root root) {
+        return new UserPrincipalProvider(root, getUserConfiguration(), namePathMapper);
+    }
+
+    private ContentSession getSystemSession() throws Exception {
+        if (systemSession == null) {
+            systemSession = Subject.doAs(SystemSubject.INSTANCE, new PrivilegedExceptionAction<ContentSession>() {
+                @Override
+                public ContentSession run() throws LoginException, NoSuchWorkspaceException {
+                    return login(null);
+
+                }
+            });
+        }
+        return systemSession;
+    }
+
+    private UserConfiguration changeUserConfiguration(ConfigurationParameters params) {
+        UserConfiguration userConfig = getUserConfiguration();
+        ((ConfigurationBase) userConfig).setParameters(params);
+        return userConfig;
+    }
+
+    private Tree getCacheTree(Root root) throws Exception {
+        return getCacheTree(root, getTestUser().getPath());
+    }
+
+    private Tree getCacheTree(Root root, String authorizablePath) throws Exception {
+        return root.getTree(authorizablePath + '/' + CacheConstants.REP_CACHE);
+    }
+
+    private static void assertPrincipals(Set<? extends Principal> principals, Principal... expectedPrincipals) {
+        assertEquals(expectedPrincipals.length, principals.size());
+        for (Principal principal : expectedPrincipals) {
+            assertTrue(principals.contains(principal));
+        }
+    }
+
+    @Test
+    public void testGetPrincipalsPopulatesCache() throws Exception {
+        PrincipalProvider pp = createPrincipalProvider(systemRoot);
+
+        Set<? extends Principal> principals = pp.getPrincipals(userId);
+        assertPrincipals(principals, EveryonePrincipal.getInstance(), testGroup.getPrincipal(), getTestUser().getPrincipal());
+
+        root.refresh();
+
+        Tree principalCache = getCacheTree(root);
+        assertTrue(principalCache.exists());
+        assertEquals(CacheConstants.NT_REP_CACHE, TreeUtil.getPrimaryTypeName(principalCache));
+
+        assertNotNull(principalCache.getProperty(CacheConstants.REP_EXPIRATION));
+
+        PropertyState ps = principalCache.getProperty(CacheConstants.REP_GROUP_PRINCIPAL_NAMES);
+        assertNotNull(ps);
+
+        String val = ps.getValue(Type.STRING);
+        assertEquals(testGroup.getPrincipal().getName(), val);
+    }
+
+    @Test
+    public void testGetGroupMembershipPopulatesCache() throws Exception {
+        PrincipalProvider pp = createPrincipalProvider(systemRoot);
+
+        Set<? extends Principal> principals = pp.getGroupMembership(getTestUser().getPrincipal());
+        assertPrincipals(principals, EveryonePrincipal.getInstance(), testGroup.getPrincipal());
+
+        root.refresh();
+
+        Tree principalCache = getCacheTree(root);
+        assertTrue(principalCache.exists());
+        assertEquals(CacheConstants.NT_REP_CACHE, TreeUtil.getPrimaryTypeName(principalCache));
+
+        assertNotNull(principalCache.getProperty(CacheConstants.REP_EXPIRATION));
+
+        PropertyState ps = principalCache.getProperty(CacheConstants.REP_GROUP_PRINCIPAL_NAMES);
+        assertNotNull(ps);
+
+        String val = ps.getValue(Type.STRING);
+        assertEquals(testGroup.getPrincipal().getName(), val);
+    }
+
+    @Test
+    public void testPrincipalManagerGetGroupMembershipPopulatesCache() throws Exception {
+        PrincipalManager principalManager = getPrincipalManager(systemRoot);
+
+        PrincipalIterator principalIterator = principalManager.getGroupMembership(getTestUser().getPrincipal());
+        assertPrincipals(ImmutableSet.copyOf(principalIterator), EveryonePrincipal.getInstance(), testGroup.getPrincipal());
+
+        root.refresh();
+
+        Tree principalCache = getCacheTree(root);
+        assertTrue(principalCache.exists());
+        assertEquals(CacheConstants.NT_REP_CACHE, TreeUtil.getPrimaryTypeName(principalCache));
+
+        assertNotNull(principalCache.getProperty(CacheConstants.REP_EXPIRATION));
+
+        PropertyState ps = principalCache.getProperty(CacheConstants.REP_GROUP_PRINCIPAL_NAMES);
+        assertNotNull(ps);
+
+        String val = ps.getValue(Type.STRING);
+        assertEquals(testGroup.getPrincipal().getName(), val);
+    }
+
+    @Test
+    public void testGetPrincipalsForGroups() throws Exception {
+        PrincipalProvider pp = createPrincipalProvider(systemRoot);
+
+        Set<? extends Principal> principals = pp.getPrincipals(testGroup.getID());
+        assertTrue(principals.isEmpty());
+
+        principals = pp.getPrincipals(testGroup2.getID());
+        assertTrue(principals.isEmpty());
+
+        root.refresh();
+
+        Tree principalCache = getCacheTree(root, testGroup.getPath());
+        assertFalse(principalCache.exists());
+
+        principalCache = getCacheTree(root, testGroup2.getPath());
+        assertFalse(principalCache.exists());
+    }
+
+    @Test
+    public void testGetGroupMembershipForGroups() throws Exception {
+        PrincipalProvider pp = createPrincipalProvider(systemRoot);
+
+        Set<? extends Principal> principals = pp.getGroupMembership(testGroup.getPrincipal());
+        assertPrincipals(principals, EveryonePrincipal.getInstance());
+
+        principals = pp.getGroupMembership(testGroup2.getPrincipal());
+        assertPrincipals(principals, EveryonePrincipal.getInstance(), testGroup.getPrincipal());
+
+        root.refresh();
+
+        Tree principalCache = getCacheTree(root, testGroup.getPath());
+        assertFalse(principalCache.exists());
+
+        principalCache = getCacheTree(root, testGroup2.getPath());
+        assertFalse(principalCache.exists());
+    }
+
+    @Test
+    public void testExtractPrincipalsFromCache() throws Exception {
+        // a) force the cache to be created
+        PrincipalProvider pp = createPrincipalProvider(systemRoot);
+
+        // set of principals that read from user + membership-provider.
+        Set<? extends Principal> principals = pp.getPrincipals(userId);
+        assertPrincipals(principals, EveryonePrincipal.getInstance(), testGroup.getPrincipal(), getTestUser().getPrincipal());
+
+        // b) retrieve principals again (this time from the cache)
+        Set<? extends Principal> principalsAgain = pp.getPrincipals(userId);
+
+        // make sure both sets are equal
+        assertEquals(principals, principalsAgain);
+    }
+
+    @Test
+    public void testGroupPrincipals() throws Exception {
+        // a) force the cache to be created
+        PrincipalProvider pp = createPrincipalProvider(systemRoot);
+        Iterable<? extends Principal> principals = Iterables.filter(pp.getPrincipals(userId), new GroupPredicate());
+
+        for (Principal p : principals) {
+            String className = p.getClass().getName();
+            assertEquals("org.apache.jackrabbit.oak.security.user.UserPrincipalProvider$GroupPrincipal", className);
+        }
+
+        Principal testPrincipal = getTestUser().getPrincipal();
+
+        // b) retrieve principals again (this time from the cache)
+        // -> verify that they are a different implementation
+        Iterable<? extends Principal> principalsAgain = Iterables.filter(pp.getPrincipals(userId), new GroupPredicate());
+        for (Principal p : principalsAgain) {
+            String className = p.getClass().getName();
+            assertEquals("org.apache.jackrabbit.oak.security.user.UserPrincipalProvider$CachedGroupPrincipal", className);
+
+            assertTrue(p instanceof TreeBasedPrincipal);
+            assertEquals(testGroup.getPath(), ((TreeBasedPrincipal) p).getPath());
+
+            java.security.acl.Group principalGroup = (java.security.acl.Group) p;
+            assertTrue(principalGroup.isMember(testPrincipal));
+
+            Enumeration<? extends Principal> members = principalGroup.members();
+            assertTrue(members.hasMoreElements());
+            assertEquals(testPrincipal, members.nextElement());
+            assertEquals(testGroup2.getPrincipal(), members.nextElement());
+            assertFalse(members.hasMoreElements());
+        }
+    }
+
+    @Test
+    public void testCachedPrincipalsGroupRemoved() throws Exception {
+        // a) force the cache to be created
+        PrincipalProvider pp = createPrincipalProvider(systemRoot);
+        Iterable<? extends Principal> principals = Iterables.filter(pp.getPrincipals(userId), new GroupPredicate());
+
+        for (Principal p : principals) {
+            String className = p.getClass().getName();
+            assertEquals("org.apache.jackrabbit.oak.security.user.UserPrincipalProvider$GroupPrincipal", className);
+        }
+
+        testGroup.remove();
+        root.commit();
+
+        systemRoot.refresh();
+
+        // b) retrieve principals again (this time from the cache)
+        //    principal for 'testGroup' is no longer backed by an user mgt group
+        //    verify that this doesn't lead to runtime exceptions
+        Iterable<? extends Principal> principalsAgain = Iterables.filter(pp.getPrincipals(userId), new GroupPredicate());
+        for (Principal p : principalsAgain) {
+            String className = p.getClass().getName();
+            assertEquals("org.apache.jackrabbit.oak.security.user.UserPrincipalProvider$CachedGroupPrincipal", className);
+
+            assertTrue(p instanceof TreeBasedPrincipal);
+            assertNull(((TreeBasedPrincipal) p).getPath());
+
+            java.security.acl.Group principalGroup = (java.security.acl.Group) p;
+            assertFalse(principalGroup.isMember(getTestUser().getPrincipal()));
+
+            Enumeration<? extends Principal> members = principalGroup.members();
+            assertFalse(members.hasMoreElements());
+        }
+    }
+
+    @Test
+    public void testGroupPrincipalNameEscape() throws Exception {
+        String gId = null;
+        try {
+            Principal groupPrincipal = new PrincipalImpl(groupId + ",,%,%%");
+            Group gr = getUserManager(root).createGroup(groupPrincipal);
+            gId = gr.getID();
+            gr.addMember(getTestUser());
+            root.commit();
+            systemRoot.refresh();
+
+            PrincipalProvider pp = createPrincipalProvider(systemRoot);
+            Set<? extends Principal> principals = pp.getPrincipals(userId);
+            assertTrue(principals.contains(groupPrincipal));
+
+            principals = pp.getPrincipals(userId);
+            assertTrue(principals.contains(groupPrincipal));
+        } finally {
+            root.refresh();
+            if (gId != null) {
+                getUserManager(root).getAuthorizable(gId).remove();
+                root.commit();
+            }
+        }
+    }
+
+    @Test
+    public void testMembershipChange() throws Exception {
+        PrincipalProvider pp = createPrincipalProvider(systemRoot);
+
+        // set of principals that read from user + membership-provider.
+        Set<? extends Principal> principals = pp.getPrincipals(userId);
+
+        // change group membership with a different root
+        UserManager uMgr = getUserManager(root);
+        Group gr = uMgr.getAuthorizable(groupId, Group.class);
+        assertTrue(gr.removeMember(uMgr.getAuthorizable(userId)));
+        root.commit();
+        systemRoot.refresh();
+
+        // system-principal provider must still see the principals from the cache (not the changed onces)
+        Set<? extends Principal> principalsAgain = pp.getPrincipals(userId);
+        assertEquals(principals, principalsAgain);
+
+        // disable the cache again
+        changeUserConfiguration(ConfigurationParameters.EMPTY);
+        pp = createPrincipalProvider(systemRoot);
+
+        // now group principals must no longer be retrieved from the cache
+        assertPrincipals(pp.getPrincipals(userId), EveryonePrincipal.getInstance(), getTestUser().getPrincipal());
+    }
+
+    @Test
+    public void testCacheUpdate() throws Exception {
+        PrincipalProvider pp = createPrincipalProvider(systemRoot);
+
+        // set of principals that read from user + membership-provider -> cache being filled
+        Set<? extends Principal> principals = pp.getPrincipals(userId);
+        assertTrue(getCacheTree(systemRoot).exists());
+
+        // change the group membership of the test user
+        UserManager uMgr = getUserConfiguration().getUserManager(systemRoot, namePathMapper);
+        Group gr = uMgr.getAuthorizable(groupId, Group.class);
+        assertTrue(gr.removeMember(uMgr.getAuthorizable(userId)));
+        systemRoot.commit();
+
+        // force cache expiration by manually setting the expiration time
+        Tree cache = getCacheTree(systemRoot);
+        cache.setProperty(CacheConstants.REP_EXPIRATION, 2);
+        systemRoot.commit(CacheValidatorProvider.asCommitAttributes());
+
+        // retrieve principals again to have cache updated
+        pp = createPrincipalProvider(systemRoot);
+        Set<? extends Principal> principalsAgain = pp.getPrincipals(userId);
+        assertFalse(principals.equals(principalsAgain));
+        assertPrincipals(principalsAgain, EveryonePrincipal.getInstance(), getTestUser().getPrincipal());
+
+        // verify that the cache has really been updated
+        cache = getCacheTree(systemRoot);
+        assertNotSame(2, new NodeUtil(cache).getLong(CacheConstants.REP_EXPIRATION, 2));
+        assertEquals("", TreeUtil.getString(cache, CacheConstants.REP_GROUP_PRINCIPAL_NAMES));
+    }
+
+    @Test
+    public void testMissingExpiration() throws Exception {
+        PrincipalProvider pp = createPrincipalProvider(systemRoot);
+
+        // set of principals that read from user + membership-provider -> cache being filled
+        Set<? extends Principal> principals = pp.getPrincipals(userId);
+        assertTrue(getCacheTree(systemRoot).exists());
+
+        // manually remove rep:expiration property to verify this doesn't cause NPE
+        Tree cache = getCacheTree(systemRoot);
+        cache.removeProperty(CacheConstants.REP_EXPIRATION);
+        systemRoot.commit(CacheValidatorProvider.asCommitAttributes());
+
+        assertFalse(getCacheTree(systemRoot).hasProperty(CacheConstants.REP_EXPIRATION));
+
+        // retrieve principals again: the cache must be treated as expired and
+        // not causing NPE although the property is missing
+        pp = createPrincipalProvider(systemRoot);
+        Set<? extends Principal> principalsAgain = pp.getPrincipals(userId);
+        assertTrue(principals.equals(principalsAgain));
+
+        // verify that the cache has really been updated
+        cache = getCacheTree(systemRoot);
+        assertTrue(cache.hasProperty(CacheConstants.REP_EXPIRATION));
+    }
+
+    @Test
+    public void testOnlySystemCreatesCache() throws Exception {
+        Set<? extends Principal> principals = principalProvider.getPrincipals(getTestUser().getID());
+        assertPrincipals(principals, EveryonePrincipal.getInstance(), testGroup.getPrincipal(), getTestUser().getPrincipal());
+
+        root.refresh();
+        Tree userTree = root.getTree(getTestUser().getPath());
+
+        assertFalse(userTree.hasChild(CacheConstants.REP_CACHE));
+    }
+
+    @Test
+    public void testOnlySystemReadsFromCache() throws Exception {
+        String userId = getTestUser().getID();
+
+        PrincipalProvider systemPP = createPrincipalProvider(systemRoot);
+        Set<? extends Principal> principals = systemPP.getPrincipals(userId);
+        assertPrincipals(principals, EveryonePrincipal.getInstance(), testGroup.getPrincipal(), getTestUser().getPrincipal());
+
+        root.refresh();
+        assertPrincipals(principalProvider.getPrincipals(userId), EveryonePrincipal.getInstance(), testGroup.getPrincipal(), getTestUser().getPrincipal());
+
+        testGroup.removeMember(getTestUser());
+        root.commit();
+
+        assertPrincipals(principalProvider.getPrincipals(userId), EveryonePrincipal.getInstance(), getTestUser().getPrincipal());
+        assertPrincipals(systemPP.getPrincipals(userId), EveryonePrincipal.getInstance(), testGroup.getPrincipal(), getTestUser().getPrincipal());
+    }
+
+    @Test
+    public void testInvalidExpiry() throws Exception {
+        long[] noCache = new long[] {0, -1, Long.MIN_VALUE};
+        for (long exp : noCache) {
+
+            changeUserConfiguration(ConfigurationParameters.of(UserPrincipalProvider.PARAM_CACHE_EXPIRATION, exp));
+
+            PrincipalProvider pp = createPrincipalProvider(systemRoot);
+            pp.getPrincipals(userId);
+
+            root.refresh();
+            Tree userTree = root.getTree(getTestUser().getPath());
+            assertFalse(userTree.hasChild(CacheConstants.REP_CACHE));
+        }
+    }
+
+    @Test
+    public void testLongOverflow() throws Exception {
+        long[] maxCache = new long[] {Long.MAX_VALUE, Long.MAX_VALUE-1, Long.MAX_VALUE-10000};
+
+        Root systemRoot = getSystemSession().getLatestRoot();
+        for (long exp : maxCache) {
+            changeUserConfiguration(ConfigurationParameters.of(UserPrincipalProvider.PARAM_CACHE_EXPIRATION, exp));
+
+            PrincipalProvider pp = createPrincipalProvider(systemRoot);
+            pp.getPrincipals(userId);
+
+            Tree userTree = systemRoot.getTree(getTestUser().getPath());
+
+            Tree cache = userTree.getChild(CacheConstants.REP_CACHE);
+            assertTrue(cache.exists());
+
+            PropertyState propertyState = cache.getProperty(CacheConstants.REP_EXPIRATION);
+            assertNotNull(propertyState);
+            assertEquals(Long.MAX_VALUE, propertyState.getValue(Type.LONG).longValue());
+
+            cache.remove();
+            systemRoot.commit();
+        }
+    }
+
+    @Test
+    public void testChangeCache() throws Exception {
+        PrincipalProvider pp = createPrincipalProvider(systemRoot);
+        pp.getPrincipals(userId);
+
+        root.refresh();
+
+        List<PropertyState> props = new ArrayList();
+        props.add(PropertyStates.createProperty(CacheConstants.REP_EXPIRATION, 25));
+        props.add(PropertyStates.createProperty(CacheConstants.REP_GROUP_PRINCIPAL_NAMES, EveryonePrincipal.NAME));
+        props.add(PropertyStates.createProperty(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_UNSTRUCTURED));
+        props.add(PropertyStates.createProperty("residualProp", "anyvalue"));
+
+        // changing cache with (normally) sufficiently privileged session must not succeed
+        for (PropertyState ps : props) {
+            try {
+                Tree cache = getCacheTree(root);
+                cache.setProperty(ps);
+                root.commit();
+                fail("Attempt to modify the cache tree must fail.");
+            } catch (CommitFailedException e) {
+                // success
+            } finally {
+                root.refresh();
+            }
+        }
+
+        // changing cache with system session must not succeed either
+        for (PropertyState ps : props) {
+            try {
+                Tree cache = getCacheTree(systemRoot);
+                cache.setProperty(ps);
+                systemRoot.commit();
+                fail("Attempt to modify the cache tree must fail.");
+            } catch (CommitFailedException e) {
+                // success
+            } finally {
+                systemRoot.refresh();
+            }
+        }
+    }
+
+    @Test
+    public void testRemoveCache() throws Exception {
+        PrincipalProvider pp = createPrincipalProvider(systemRoot);
+        pp.getPrincipals(userId);
+
+        // removing cache with sufficiently privileged session must succeed
+        root.refresh();
+        Tree cache = getCacheTree(root);
+        cache.remove();
+        root.commit();
+    }
+
+    @Test
+    public void testConcurrentLoginWithCacheRemoval() throws Exception {
+        changeUserConfiguration(ConfigurationParameters.of(UserPrincipalProvider.PARAM_CACHE_EXPIRATION, 1));
+
+        final List<Exception> exceptions = new ArrayList<Exception>();
+        List<Thread> threads = new ArrayList<Thread>();
+        for (int i = 0; i < 100; i++) {
+            threads.add(new Thread(new Runnable() {
+                public void run() {
+                    try {
+                        login(new SimpleCredentials(userId, userId.toCharArray())).close();
+                    } catch (Exception e) {
+                        exceptions.add(e);
+                    }
+                }
+            }));
+        }
+        for (Thread t : threads) {
+            t.start();
+        }
+        for (Thread t : threads) {
+            t.join();
+        }
+        for (Exception e : exceptions) {
+            e.printStackTrace();
+        }
+        if (!exceptions.isEmpty()) {
+            fail();
+        }
+    }
+
+    //--------------------------------------------------------------------------
+
+    private static final class GroupPredicate implements Predicate<Principal> {
+        @Override
+        public boolean apply(@Nullable Principal input) {
+            return (input instanceof java.security.acl.Group) && !EveryonePrincipal.getInstance().equals(input);
+        }
+    }
+}
\ No newline at end of file

Modified: jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/principal.md
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/principal.md?rev=1695223&r1=1695222&r2=1695223&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/principal.md (original)
+++ jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/principal.md Tue Aug 11 07:41:07 2015
@@ -41,6 +41,9 @@ different sources.
 - [CompositePrincipalProvider]: Implementation that combines different principals
 from different source providers.
 
+See section [Implementations of the PrincipalProvider Interface](principal/principalprovider.html)
+for details.
+
 ##### Special Principals
 - [AdminPrincipal]: Marker interface to identify the principal associated with administrative user(s).
 - [EveryonePrincipal]: built-in group principal implementation that has every other valid principal as member.

Added: jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/principal/cache.md
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/principal/cache.md?rev=1695223&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/principal/cache.md (added)
+++ jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/principal/cache.md Tue Aug 11 07:41:07 2015
@@ -0,0 +1,148 @@
+<!--
+   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.
+-->
+
+Caching Results of Principal Resolution
+--------------------------------------------------------------------------------
+
+### General
+
+Since Oak 1.3.4 this `UserPrincipalProvider` optionally allows for temporary
+caching of the principal resolution mainly to optimize login performance (OAK-3003).
+
+This cache contains the result of the group principal resolution as performed by
+`PrincipalProvider.getPrincipals(String userId)`and `PrincipalProvider.getGroupMembership(Principal)`
+and will read from the cache upon subsequent calls for the configured expiration
+time.
+
+### Configuration
+
+An administrator may enable the group principal caching via the
+_org.apache.jackrabbit.oak.security.user.UserConfigurationImpl_
+OSGi configuration. By default caching is disabled.
+
+The following configuration option is supported:
+
+- Cache Expiration (`cacheExpiration`): Specifying a long greater 0 enables the
+  caching.
+
+NOTE: It is important that the configured expiration time balances between login
+performance and cache invalidation to reflect changes made to the group membership.
+An application that makes use of this cache, must be able to live with shot term
+diverging of principal resolution and user management upon repository login.
+
+It is expected that the cache is used in scenarios where subsequent repository
+login calls can (or even should) result in the creation of a `javax.security.auth.Subject`
+with equal principal set irrespective of group membership changes.
+See section Invalidation below for further details.
+
+
+### How it works
+
+#### Caching Principal Names
+
+If the feature is enabled, evaluating `UserPrincipalProvider.getPrincipals(String userId)`
+and `PrincipalProvider.getGroupMembership(Principal)` as well as the corresponding
+calls on `PrincipalManager` will trigger the group principal names to be remembered
+in a cache if the following conditions are met:
+
+- a valid expiration time is configured (i.e. > 0),
+- the `PrincipalProvider` has been obtained for a system session (see below),
+- the tree to hold the cache belongs to a user (i.e. tree with primary type
+  `rep:User` (i.e. no caches are created for groups)
+
+The cache itself consists of a tree named `rep:cache` with the built-in node type
+`rep:Cache`, which defines a mandatory, protected `rep:expiration` property and
+may have additional protected, residual properties.
+
+Subsequent calls will read the names of the group principals from the cache until
+the cache expires. Once expired the default resolution will be performed again in
+order to update the cache.
+
+##### Limitation to System Calls
+
+The creation and maintenance of this caches as well as the shortcut upon reading
+is limited to system internal sessions for security reasons: The cache must always
+be filled with the comprehensive list of group principals (as required upon login)
+as must any subsequent call never expose principal information that might not
+be accessible in the non-cache scenario where access to principals is protected
+by regular permission evalution.
+
+<a name="validation"/>
+##### Validation
+
+The cache is system maintained, protected repository content that can only
+be created and updated by the implementation. Any attempt to manipulate these
+caches using JCR or Oak API calls will fail. Also the cache can only be created
+or updated using the internal system subject.
+
+Also this validation is always enforce irrespective on whether the caching
+feature is enabled or not, to prevent unintended manipulation.
+
+These constraints and the consistency of the cache structure is asserted by a
+dedicated `CacheValidator`. The corresponding errors are all of type `Constraint`
+with the following codes:
+
+| Code              | Message                                                  |
+|-------------------|----------------------------------------------------------|
+| 0034              | Attempt to create or change the system maintained cache. |
+
+Note however, that the cache tree might be removed by any session that has
+sufficient privileges to remove it.
+
+
+##### Cache Invalidation
+
+The caches hold with the different user trees get invalidated once the expiration
+time is reached. There is no explicit, forced invalidation if group membership
+as reflected by the user management implementation is being changed.
+
+Consequently, system sessions which might read principal information from the cache
+(if enabled) can be provided with a set of principals (as stored in the cache)
+that might have diverged from the group membership stored in the repository
+for the time until the cache expires.
+
+Applications that rely on principal resolution being _always_ in sync with the
+revision associated with the system session that perform the repository login,
+must not enable the cache.
+
+Similarly, applications that have due to their design have an extremely high
+turnover wrt group membership might not be able to profit from this cache in
+the expected way.
+
+
+#### Interaction With User Management
+
+The cache is created and maintained by the `PrincipalProvider` implementation as
+exposed by the optional `UserConfiguration.getUserPrincipalProvider` call and
+will therefore only effect the results provided by the principal management API.
+
+Regular Jackrabbit user management API calls are not affected by this cache and
+vice versa; i.e. changes made using the user management API have no immediate
+effect on the cache and will not trigger it's invalidation.
+
+In other words user management API calls will always read from the revision of the
+content repository that is associated with the give JCR `Session` (and Oak
+`ContentSession`). The same is true for principal management API calls of all
+non-system sessions.
+
+See the introduction and section Invalidation above for the expected behavior
+for system sessions.
+
+##### XML Import
+
+When users are imported via JCR XML import, the protected cache structure will
+be ignored (i.e. will not be imported).