Index: oak-core/src/main/java/org/apache/jackrabbit/oak/management/NamedOperation.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/management/NamedOperation.java (revision ) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/management/NamedOperation.java (revision ) @@ -0,0 +1,36 @@ +/* + * 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; + +/** + * Interface implemented by management operations that + * can be executed by name. + * + * @see RepositoryManager#execute(Class, String, com.google.common.base.Function) + */ +interface NamedOperation { + + /** + * The name of this operation + * + * @return the name. + */ + String getName(); +} 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,55 @@ +/* + * 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 extends NamedOperation { + + String TYPE = "Consistency"; + + /** + * 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 Status not available if none. + */ + @Nonnull + CompositeData getConsistencyStatus(); +} 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,247 @@ +/* + * 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 extraEntries; + private List 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() { + @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() { + @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> mapEntry : editor.entries.entrySet()) { + String principalName = mapEntry.getKey(); + Collection storeEntries = permissionStore.load(null, principalName, accessControlledPath); + Iterator 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); + } +} 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 d9af020286cb8db1804551f10ac18576bda0d01b) +++ 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 Status not available 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/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,51 @@ +/* + * 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 + } +} 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 d9af020286cb8db1804551f10ac18576bda0d01b) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/management/RepositoryManager.java (revision ) @@ -59,9 +59,20 @@ } private Status execute(Class serviceType, Function operation) { + return execute(serviceType, null, operation); + } + + private Status execute(Class serviceType, String name, Function operation) { Tracker tracker = whiteboard.track(serviceType); try { List services = tracker.getServices(); + for (T service : tracker.getServices()) { + if (name == null + || (service instanceof NamedOperation) && name.equals(((NamedOperation) service).getName())) { + services.add(service); + } + } + if (services.size() == 1) { return operation.apply(services.get(0)); } else if (services.isEmpty()) { @@ -204,8 +215,44 @@ public Status apply(PropertyIndexAsyncReindexMBean reindexer) { return fromCompositeData(reindexer .getPropertyIndexAsyncReindexStatus()); + } + }).toCompositeData(); + } + + @Nonnull + @Override + public CompositeData analyseConsistency(@Nonnull String name) { + return execute(ConsistencyMBean.class, name, new Function() { + @Nonnull + @Override + public Status apply(ConsistencyMBean consistencyMBean) { + return fromCompositeData(consistencyMBean.analyseConsistency()); + } + }).toCompositeData(); + } + + @Nonnull + @Override + public CompositeData fixConsistency(@Nonnull String name) { + return execute(ConsistencyMBean.class, name, new Function() { + @Nonnull + @Override + public Status apply(ConsistencyMBean consistencyMBean) { + return fromCompositeData(consistencyMBean.fixConsistency()); + } + }).toCompositeData(); + } + + @Nonnull + @Override + public CompositeData getConsistencyStatus(@Nonnull String name) { + return execute(ConsistencyMBean.class, name, new Function() { + @Nonnull + @Override + public Status apply(ConsistencyMBean consistencyMBean) { + return fromCompositeData(consistencyMBean.getConsistencyStatus()); - } - }).toCompositeData(); + } + }).toCompositeData(); } }