Index: oak-core/src/main/java/org/apache/jackrabbit/oak/security/authorization/permission/PermissionConsistency.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- oak-core/src/main/java/org/apache/jackrabbit/oak/security/authorization/permission/PermissionConsistency.java	(revision )
+++ oak-core/src/main/java/org/apache/jackrabbit/oak/security/authorization/permission/PermissionConsistency.java	(revision )
@@ -0,0 +1,245 @@
+/*
+ * 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.authorization.permission;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import javax.annotation.Nonnull;
+import javax.management.openmbean.CompositeData;
+
+import com.google.common.collect.Lists;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.core.ImmutableRoot;
+import org.apache.jackrabbit.oak.management.ConsistencyMBean;
+import org.apache.jackrabbit.oak.management.ManagementOperation;
+import org.apache.jackrabbit.oak.plugins.nodetype.TypePredicate;
+import org.apache.jackrabbit.oak.plugins.tree.ImmutableTree;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.security.authorization.AuthorizationConfiguration;
+import org.apache.jackrabbit.oak.spi.security.authorization.accesscontrol.AccessControlConstants;
+import org.apache.jackrabbit.oak.spi.security.authorization.restriction.RestrictionProvider;
+import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeBitsProvider;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.apache.jackrabbit.oak.util.TreeUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * MBean to find and fix inconsistencies between access control content and the
+ * permission store.
+ */
+public class PermissionConsistency implements ConsistencyMBean, AccessControlConstants {
+
+    private static final Logger log = LoggerFactory.getLogger(PermissionConsistency.class);
+
+    private static final long DEFAULT_LIFETIME = TimeUnit.HOURS.toMillis(1);
+
+    private static final String NAME = "PermissionConsistency";
+
+    private final NodeStore store;
+    private final String workspaceName;
+    private final AuthorizationConfiguration config;
+    private final Executor executor;
+
+    private ManagementOperation consistencyOp = ManagementOperation.done(NAME, 0);
+
+    private PrivilegeBitsProvider bitsProvider;
+    private RestrictionProvider restrictionProvider;
+
+    private NodeBuilder permissionRoot;
+    private PermissionStore permissionStore;
+
+    private TypePredicate isACE;
+    private TypePredicate isGrantACE;
+
+    private List<String> extraEntries;
+    private List<String> missingEntries;
+
+    /**
+     * @param store  store to back up from or restore to
+     * @param executor  executor for running the back up or restore operation
+     */
+    public PermissionConsistency(@Nonnull NodeStore store,
+                                 @Nonnull String workspaceName,
+                                 @Nonnull AuthorizationConfiguration config,
+                                 @Nonnull Executor executor) {
+        this.store = store;
+        this.workspaceName = workspaceName;
+        this.config = config;
+        this.executor = executor;
+    }
+
+
+    @Override
+    public String getName() {
+        return NAME;
+    }
+
+    @Nonnull
+    @Override
+    public CompositeData analyseConsistency() {
+        if (consistencyOp.isDone()) {
+            consistencyOp = new ManagementOperation("PermissionConsistencyAnalysis", new Callable<Long>() {
+                @Override
+                public Long call() throws Exception {
+                    long t0 = System.nanoTime();
+                    checkConsistency(false);
+                    return System.nanoTime() - t0;
+                }
+            });
+            executor.execute(consistencyOp);
+        }
+        return getConsistencyStatus();
+    }
+
+    @Nonnull
+    @Override
+    public CompositeData fixConsistency() {
+        if (consistencyOp.isDone()) {
+            consistencyOp = new ManagementOperation("PermissionConsistencyFix", new Callable<Long>() {
+                @Override
+                public Long call() throws Exception {
+                    long t0 = System.nanoTime();
+                    checkConsistency(true);
+                    return System.nanoTime() - t0;
+                }
+            });
+            executor.execute(consistencyOp);
+        }
+        return getConsistencyStatus();
+    }
+
+    @Nonnull
+    @Override
+    public CompositeData getConsistencyStatus() {
+        // TODO include inconsistency information
+        return consistencyOp.getStatus().toCompositeData();
+    }
+
+    //------------------------------------------------------------< private >---
+
+    public void checkConsistency(boolean doFix) throws CommitFailedException {
+        long s = System.currentTimeMillis();
+
+        // create a new checkpoint with the current state
+        String checkpoint = store.checkpoint(DEFAULT_LIFETIME);
+        NodeState rootState = store.retrieve(checkpoint);
+        if (rootState == null) {
+            // unable to retrieve the checkpoint; use root state instead
+            rootState = store.getRoot();
+        }
+
+        ImmutableRoot root = new ImmutableRoot(rootState);
+
+        bitsProvider = new PrivilegeBitsProvider(root);
+        restrictionProvider = config.getRestrictionProvider();
+
+        permissionRoot = ((ImmutableTree) PermissionUtil.getPermissionsRoot(root, workspaceName)).getNodeState().builder();
+        permissionStore = new PermissionStoreImpl(root, workspaceName, restrictionProvider);
+
+        isACE = new TypePredicate(rootState, NT_REP_ACE);
+        isGrantACE = new TypePredicate(rootState, NT_REP_GRANT_ACE);
+
+        extraEntries = Lists.newArrayList();
+        missingEntries = Lists.newArrayList();
+
+        traverse(root.getTree("/"), permissionStore, restrictionProvider, doFix);
+        if (doFix && !extraEntries.isEmpty()) {
+            store.merge(permissionRoot, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        }
+
+        log.debug("Permission consistency check finished in {} ms.", System.currentTimeMillis() - s);
+    }
+
+    private void traverse(@Nonnull ImmutableTree tree, @Nonnull PermissionStore permissionStore,
+                          @Nonnull RestrictionProvider restrictionProvider, boolean doFix) {
+        if (entering(tree, permissionStore, restrictionProvider, doFix)) {
+            for (Tree child : tree.getChildren()) {
+                traverse((ImmutableTree) child, permissionStore, restrictionProvider, doFix);
+            }
+        }
+    }
+
+    private boolean entering(@Nonnull ImmutableTree tree, @Nonnull PermissionStore permissionStore,
+                             @Nonnull RestrictionProvider restrictionProvider, boolean doFix) {
+        if (config.getContext().definesContextRoot(tree)) {
+            return false;
+        }
+
+        if (tree.isRoot() && tree.hasChild(AccessControlConstants.REP_REPO_POLICY)) {
+            ImmutableTree policy = tree.getChild(AccessControlConstants.REP_REPO_POLICY);
+            verify("", policy, doFix);
+        }
+        if (tree.hasChild(AccessControlConstants.REP_POLICY)) {
+            ImmutableTree policy = tree.getChild(AccessControlConstants.REP_POLICY);
+            verify(tree.getPath(), policy, doFix);
+
+        }
+        return true;
+    }
+
+    private void verify(@Nonnull String accessControlledPath, @Nonnull ImmutableTree policyTree, boolean doFix) {
+        PermissionStoreEditor editor = new PermissionStoreEditor(accessControlledPath, policyTree.getName(), policyTree.getNodeState(), permissionRoot, isACE, isGrantACE, bitsProvider, restrictionProvider);
+
+        for (Map.Entry<String, List<PermissionStoreEditor.AcEntry>> mapEntry : editor.entries.entrySet()) {
+            String principalName = mapEntry.getKey();
+            Collection<PermissionEntry> storeEntries = permissionStore.load(null, principalName, accessControlledPath);
+            Iterator<PermissionStoreEditor.AcEntry> acEntries = mapEntry.getValue().iterator();
+
+            while (acEntries.hasNext()) {
+                PermissionStoreEditor.AcEntry acEntry = acEntries.next();
+                if (!storeEntries.remove(acEntry.asPermissionEntry())) {
+                    log.warn("Missing permission entry detected for ACE " + acEntry + ".");
+                    missingEntries.add(acEntry.toString());
+                } else {
+                    acEntries.remove();
+                }
+            }
+
+            // remove all remaining permission entries that have been obtained
+            // from the permission store as they have no correspondence in the ac content
+            for (PermissionEntry pe : storeEntries) {
+                log.warn("Inconsistent or superfluous permission entry detected in the permission store " + pe + ".");
+                if (doFix) {
+                    editor.removePermissionEntry(principalName, pe);
+                }
+                // TODO add proper string representation of the pe
+                extraEntries.add(pe.toString());
+            }
+        }
+
+        // finally add all ac-entries for the various principals that are not
+        // contained in the permission store
+        if (doFix) {
+            editor.updatePermissionEntries();
+        }
+    }
+
+    private static boolean isAccessControlEntry(@Nonnull Tree tree) {
+        String ntName = TreeUtil.getPrimaryTypeName(tree);
+        return AccessControlConstants.NT_REP_GRANT_ACE.equals(ntName) || AccessControlConstants.NT_REP_DENY_ACE.equals(ntName);
+    }
+}
\ No newline at end of file
Index: oak-core/src/test/java/org/apache/jackrabbit/oak/security/authorization/permission/PermissionConsistencyTest.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- oak-core/src/test/java/org/apache/jackrabbit/oak/security/authorization/permission/PermissionConsistencyTest.java	(revision )
+++ oak-core/src/test/java/org/apache/jackrabbit/oak/security/authorization/permission/PermissionConsistencyTest.java	(revision )
@@ -0,0 +1,49 @@
+/*
+ * 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.authorization.permission;
+
+import org.apache.jackrabbit.oak.AbstractSecurityTest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * PermissionConsistencyTest... TODO
+ */
+public class PermissionConsistencyTest extends AbstractSecurityTest {
+
+    /**
+     * logger instance
+     */
+    private static final Logger log = LoggerFactory.getLogger(PermissionConsistencyTest.class);
+
+
+    public void testMissingPermissionEntry() {
+        // TODO
+    }
+
+    public void testInconsistentPermissionEntry() {
+        // TODO
+    }
+
+    public void testExtraPermissionEntry() {
+        // TODO
+    }
+
+    public void testConsistentPermissionEntries() {
+        // TODO
+    }
+}
\ No newline at end of file
Index: oak-core/src/main/java/org/apache/jackrabbit/oak/management/ConsistencyMBean.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- oak-core/src/main/java/org/apache/jackrabbit/oak/management/ConsistencyMBean.java	(revision )
+++ oak-core/src/main/java/org/apache/jackrabbit/oak/management/ConsistencyMBean.java	(revision )
@@ -0,0 +1,60 @@
+/*
+ * 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.management;
+
+import javax.annotation.Nonnull;
+import javax.management.openmbean.CompositeData;
+
+/**
+ * MBean for verifying and restoring consistency of a {@code NodeStore}.
+ */
+public interface ConsistencyMBean {
+
+    String TYPE = "Consistency";
+
+    /**
+     * The name of this consistency mbean implementation.
+     *
+     * @return the name.
+     */
+    String getName();
+
+    /**
+     * Initiate the consistency analysis operation.
+     *
+     * @return the status of the operation right after it was initiated
+     */
+    @Nonnull
+    CompositeData analyseConsistency();
+
+    /**
+     * Initiate the consistency fix operation.
+     *
+     * @return the status of the operation right after it was initiated
+     */
+    @Nonnull
+    CompositeData fixConsistency();
+
+    /**
+     * The status of the ongoing consistency check operation.
+     *
+     * @return  the status of the ongoing operation or if none the terminal
+     * status of the last operation or <em>Status not available</em> if none.
+     */
+    @Nonnull
+    CompositeData getConsistencyStatus();
+}
\ No newline at end of file
Index: oak-core/src/main/java/org/apache/jackrabbit/oak/api/jmx/RepositoryManagementMBean.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- oak-core/src/main/java/org/apache/jackrabbit/oak/api/jmx/RepositoryManagementMBean.java	(revision 1585963)
+++ oak-core/src/main/java/org/apache/jackrabbit/oak/api/jmx/RepositoryManagementMBean.java	(revision )
@@ -73,6 +73,7 @@
  * status to invocation. {@code -1} is returned when not available.
  */
 public interface RepositoryManagementMBean {
+
     String TYPE = "RepositoryManagement";
 
     /**
@@ -174,6 +175,39 @@
      */
     @CheckForNull
     String checkpoint(long lifetime);
+
+    /**
+     * Start the consistency analysis for the {@link org.apache.jackrabbit.oak.management.ConsistencyMBean}
+     * implementation with the specified {@code name}.
+     *
+     * @param name The name of the {@link org.apache.jackrabbit.oak.management.ConsistencyMBean}.
+     * @return the status of the operation right after it was initiated
+     * @see {@link org.apache.jackrabbit.oak.management.ConsistencyMBean#getName()}
+     */
+    @Nonnull
+    CompositeData analyseConsistency(@Nonnull String name);
+
+    /**
+     * Start the consistency fix for the {@link org.apache.jackrabbit.oak.management.ConsistencyMBean}
+     * implementation with the specified {@code name}.
+     *
+     * @param name The name of the {@link org.apache.jackrabbit.oak.management.ConsistencyMBean}.
+     * @return the status of the operation right after it was initiated
+     * @see {@link org.apache.jackrabbit.oak.management.ConsistencyMBean#getName()}
+     */
+    @Nonnull
+    CompositeData fixConsistency(@Nonnull String name);
+
+    /**
+     * Status of the consistency analisys.
+     *
+     * @param name The name of the {@link org.apache.jackrabbit.oak.management.ConsistencyMBean}.
+     * @return  the status of the ongoing operation or if none the terminal
+     * status of the last operation or <em>Status not available</em> if none.
+     * @see {@link org.apache.jackrabbit.oak.management.ConsistencyMBean#getName()}
+     */
+    @Nonnull
+    CompositeData getConsistencyStatus(@Nonnull String name);
 
     /**
      * Initiate a reindex operation for the property indexes marked for
Index: oak-core/src/main/java/org/apache/jackrabbit/oak/management/RepositoryManager.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- oak-core/src/main/java/org/apache/jackrabbit/oak/management/RepositoryManager.java	(revision 1585963)
+++ oak-core/src/main/java/org/apache/jackrabbit/oak/management/RepositoryManager.java	(revision )
@@ -19,19 +19,12 @@
 
 package org.apache.jackrabbit.oak.management;
 
-import static com.google.common.base.Preconditions.checkNotNull;
-import static org.apache.jackrabbit.oak.management.ManagementOperation.Status;
-import static org.apache.jackrabbit.oak.management.ManagementOperation.Status.failed;
-import static org.apache.jackrabbit.oak.management.ManagementOperation.Status.fromCompositeData;
-import static org.apache.jackrabbit.oak.management.ManagementOperation.Status.succeeded;
-import static org.apache.jackrabbit.oak.management.ManagementOperation.Status.unavailable;
-
 import java.util.List;
-
 import javax.annotation.Nonnull;
 import javax.management.openmbean.CompositeData;
 
 import com.google.common.base.Function;
+import com.google.common.collect.Lists;
 import org.apache.jackrabbit.oak.api.jmx.RepositoryManagementMBean;
 import org.apache.jackrabbit.oak.plugins.backup.FileStoreBackupRestoreMBean;
 import org.apache.jackrabbit.oak.plugins.blob.BlobGCMBean;
@@ -40,6 +33,13 @@
 import org.apache.jackrabbit.oak.spi.whiteboard.Tracker;
 import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.apache.jackrabbit.oak.management.ManagementOperation.Status;
+import static org.apache.jackrabbit.oak.management.ManagementOperation.Status.failed;
+import static org.apache.jackrabbit.oak.management.ManagementOperation.Status.fromCompositeData;
+import static org.apache.jackrabbit.oak.management.ManagementOperation.Status.succeeded;
+import static org.apache.jackrabbit.oak.management.ManagementOperation.Status.unavailable;
+
 /**
  * Default implementation of the {@link RepositoryManagementMBean} based
  * on a {@link Whiteboard} instance, which is used to look up individual
@@ -78,6 +78,27 @@
         }
     }
 
+    @Nonnull
+    private Status executeConsistencyMBean(@Nonnull String name, @Nonnull Function<ConsistencyMBean, Status> operation) {
+        Tracker<? extends ConsistencyMBean> tracker = whiteboard.track(ConsistencyMBean.class);
+        try {
+            List<ConsistencyMBean> mbeans = Lists.newArrayList();
+            for (ConsistencyMBean service : tracker.getServices()) {
+                if (name.equals(service.getName())) {
+                    mbeans.add(service);
+                }
+            }
+
+            switch (mbeans.size()) {
+                case 0 : return unavailable("Cannot perform operation: no ConsistencyMBean service of type " + name + " found.");
+                case 1 : return operation.apply(mbeans.get(0));
+                default : return failed("Cannot perform operation: multiple ConsistencyMBean services of type " + name + " found.");
+            }
+        } finally {
+            tracker.stop();
+        }
+    }
+
     @Override
     public CompositeData startBackup() {
         return execute(FileStoreBackupRestoreMBean.class, new Function<FileStoreBackupRestoreMBean, Status>() {
@@ -204,8 +225,44 @@
                     public Status apply(PropertyIndexAsyncReindexMBean reindexer) {
                         return fromCompositeData(reindexer
                                 .getPropertyIndexAsyncReindexStatus());
+                    }
+                }).toCompositeData();
+    }
+
+    @Nonnull
+    @Override
+    public CompositeData analyseConsistency(@Nonnull String name) {
+        return executeConsistencyMBean(name, new Function<ConsistencyMBean, Status>() {
+            @Nonnull
+            @Override
+            public Status apply(ConsistencyMBean consistencyMBean) {
+                return fromCompositeData(consistencyMBean.analyseConsistency());
+            }
+        }).toCompositeData();
+    }
+
+    @Nonnull
+    @Override
+    public CompositeData fixConsistency(@Nonnull String name) {
+        return executeConsistencyMBean(name, new Function<ConsistencyMBean, Status>() {
+            @Nonnull
+            @Override
+            public Status apply(ConsistencyMBean consistencyMBean) {
+                return fromCompositeData(consistencyMBean.fixConsistency());
+            }
+        }).toCompositeData();
+    }
+
+    @Nonnull
+    @Override
+    public CompositeData getConsistencyStatus(@Nonnull String name) {
+        return executeConsistencyMBean(name, new Function<ConsistencyMBean, Status>() {
+            @Nonnull
+            @Override
+            public Status apply(ConsistencyMBean consistencyMBean) {
+                return fromCompositeData(consistencyMBean.getConsistencyStatus());
-                    }
-                }).toCompositeData();
+            }
+        }).toCompositeData();
     }
 
 }
