| /* |
| * 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.exercise.security.authorization.permission; |
| |
| import java.security.Principal; |
| import java.util.Map; |
| import javax.jcr.AccessDeniedException; |
| import javax.jcr.Node; |
| import javax.jcr.Property; |
| import javax.jcr.RepositoryException; |
| import javax.jcr.Session; |
| import javax.jcr.Value; |
| import javax.jcr.security.AccessControlManager; |
| import javax.jcr.security.Privilege; |
| |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Maps; |
| import org.apache.jackrabbit.JcrConstants; |
| import org.apache.jackrabbit.api.JackrabbitSession; |
| import org.apache.jackrabbit.api.security.user.Authorizable; |
| import org.apache.jackrabbit.api.security.user.Group; |
| import org.apache.jackrabbit.api.security.user.User; |
| import org.apache.jackrabbit.api.security.user.UserManager; |
| import org.apache.jackrabbit.commons.jackrabbit.authorization.AccessControlUtils; |
| import org.apache.jackrabbit.oak.exercise.security.privilege.L5_PrivilegeContentTest; |
| import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants; |
| import org.apache.jackrabbit.oak.exercise.ExerciseUtility; |
| import org.apache.jackrabbit.oak.spi.security.authorization.permission.Permissions;import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConstants; |
| import org.apache.jackrabbit.test.AbstractJCRTest; |
| import org.apache.jackrabbit.test.api.util.Text; |
| |
| /** |
| * <pre> |
| * Module: Authorization (Permission Evaluation) |
| * ============================================================================= |
| * |
| * Title: Privileges and Permissions |
| * ----------------------------------------------------------------------------- |
| * |
| * Goal: |
| * The aim of this is test is to make you familiar with the subtle differences |
| * between privileges (that are always granted on an existing node) and effective |
| * permissions on the individual items (nodes, properties or even non-existing |
| * items). |
| * Having completed this exercise you should also be familiar with the oddities |
| * of those privileges that allow modify the parent collection, while the effective |
| * permission is evaluated the target item. |
| * |
| * Exercises: |
| * |
| * - {@link #testAddNodes()} |
| * This test aims to practise the subtle difference between granting 'jcr:addChildNode' |
| * privilege at a given parent node and {@link Session#hasPermission(String, String)} |
| * using {@link Session#ACTION_ADD_NODE}, which effectively tests if a given |
| * new node could be created. |
| * Fill in the expected values for the permission discovery with the given |
| * permission setup. Subsequently, list the expected values for |
| * {@link javax.jcr.security.AccessControlManager#getPrivileges(String)}. |
| * Explain the difference given the mini-explanation in the test introduction. |
| * |
| * - {@link #testAddProperties()} |
| * This test grants the test user privilege to add properties at 'childPath'. |
| * Fill in the expected result of {@link javax.jcr.Session#hasPermission(String, String)} |
| * both for the regular JCR action {@link Session#ACTION_SET_PROPERTY} and |
| * for the string value of {@link Permissions#ADD_PROPERTY}. Compare and explain |
| * the differences. |
| * |
| * - {@link #testRemoveNodes()} ()} |
| * This test illustrates what kind of privileges are required in order to |
| * remove a given node. Fix the test-case by setting the correct permission |
| * setup. |
| * |
| * - {@link #testRemoveProperties()} ()} |
| * In this test the test-user is only granted rep:removeProperties privilege |
| * and lists which properties must be removable. Nevertheless the test-case is |
| * broken... can you identify the root cause without modifying the result map |
| * nor the permission setup? |
| * |
| * - {@link #testRemoveNonExistingItems()} |
| * Look at the contract of {@link Session#hasPermission(String, String)} and |
| * the implementation present in Oak to understand what happens if you test |
| * permissions for removal of non-existing items. |
| * |
| * - {@link #testModifyProperties()} |
| * Now the test-user is just allowed to modify properties. Complete the test |
| * by modifying the result-map: add for each path whether accessing and setting |
| * the properties is expected to succeed. Explain for each path why it passes |
| * or fails. In case of doubt debug the test to understand what is going on. |
| * |
| * - {@link #testSetProperty()} |
| * Properties cannot only be modified by calling {@link Property#setValue} |
| * but also by calling {@link Node#setProperty} each for different types and |
| * single vs multivalued properties. While they are mostly equivalent there |
| * are subtle differences that have an impact on the permission evaluation. |
| * Can you find suitable value(s) for each of these paths such that the |
| * test passes? |
| * Hint: passing a 'null' value is defined to be equivalent to removal. |
| * Discuss your findings. |
| * |
| * Question: Can you explain the required values if the test user was allowed |
| * to remove properties instead? -> Modify the test to verify your expectations. |
| * |
| * - {@link #testChangingPrimaryAndMixinTypes()} |
| * In order to change the primary (or mixin) type(s) of a given existing node |
| * the editing session must have {@link Privilege#JCR_NODE_TYPE_MANAGEMENT} |
| * privilege granted. |
| * For consistency with this requirement, also {@link Node#addNode(String, String)}, |
| * which explicitly specifies the primary type requires this privilege as |
| * the effective operation is equivalent to the combination of: |
| * 1. {@link Node#addNode(String)} + |
| * 2. {@link Node#setPrimaryType(String)} |
| * Use this test case to become familiar with setting or changing the primary |
| * type and modifying the set of mixin types. |
| * |
| * |
| * Additional Exercises: |
| * ----------------------------------------------------------------------------- |
| * |
| * - Modifying Nodes |
| * Discuss why there is no dedicated privilege (and test case) for "modify nodes". |
| * Explain how {@link Privilege#JCR_NODE_TYPE_MANAGEMENT, {@link Privilege#JCR_ADD_CHILD_NODES}, |
| * {@link Privilege#JCR_REMOVE_CHILD_NODES} (and maybe others) are linked to |
| * node modification. |
| * |
| * |
| * Advanced Exercises: |
| * ----------------------------------------------------------------------------- |
| * |
| * If you wished to become more familiar with the interals of the Oak permission |
| * evaluation, it is indispensible to understand the internal representation of |
| * registered privileges (as {@link org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeBits} |
| * and their mapping to {@link org.apache.jackrabbit.oak.spi.security.authorization.permission.Permissions}. |
| * |
| * Precondition for these advanced exercises is the following test: |
| * |
| * - {@link L5_PrivilegeContentTest } |
| * |
| * The following exercises aim to provide you with some insight and allow you |
| * to understand the internal structure of the permission store. |
| * |
| * - PrivilegeBits and the corresponding provider class |
| * Take a look at the methods exposed by |
| * {@link org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeBitsProvider} |
| * and the {@link org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeBits} |
| * class itself. Using these classes Oak generates the repository internal |
| * representation of the privileges that are used for |
| * - calculation of effective privileges |
| * - calculation of final permission from effective privileges granted (or denied) |
| * |
| * - Mapping Privileges to Permissions |
| * As you could see in the above exercises there is no 1:1 mapping between |
| * privileges and permissions as a few privileges (like jcr:addChildNodes and |
| * jcr:removeChildNodes) are granted for the parent node. This needs to be |
| * handled specially when evaluating if a given node can be created or removed. |
| * Look at {@link org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeBits#calculatePermissions()} |
| * and compare the code with the results obtained from the test-cases above. |
| * |
| * - Mapping of JCR Actions to Permissions |
| * The exercises also illustrated the subtle differences between 'actions' |
| * as defined on {@link javax.jcr.Session} and the effective permissions |
| * being evaluated. |
| * Take a closer look at {@link org.apache.jackrabbit.oak.spi.security.authorization.permission.Permissions#getPermissions(String, org.apache.jackrabbit.oak.plugins.tree.TreeLocation, boolean)} |
| * to improve your understanding on how the mapping is being implemented |
| * and discuss the consequences for {@link Session#hasPermission(String, String)}. |
| * Compare the code with the test-results. |
| * |
| * |
| * Related Exercises |
| * ----------------------------------------------------------------------------- |
| * |
| * - {@link L5_SpecialPermissionsTest } |
| * - {@link L7_PermissionContentTest } |
| * |
| * </pre> |
| */ |
| public class L4_PrivilegesAndPermissionsTest extends AbstractJCRTest { |
| |
| private User testUser; |
| private Principal testPrincipal; |
| private Principal testGroupPrincipal; |
| private Session testSession; |
| |
| private String childPath; |
| private String grandChildPath; |
| private String propertyPath; |
| private String childPropertyPath; |
| |
| @Override |
| protected void setUp() throws Exception { |
| super.setUp(); |
| |
| Property p = testRootNode.setProperty(propertyName1, "val"); |
| propertyPath = p.getPath(); |
| |
| Node child = testRootNode.addNode(nodeName1); |
| childPath = child.getPath(); |
| |
| p = child.setProperty(propertyName2, "val"); |
| childPropertyPath = p.getPath(); |
| |
| Node grandChild = child.addNode(nodeName2); |
| grandChildPath = grandChild.getPath(); |
| |
| testUser = ExerciseUtility.createTestUser(((JackrabbitSession) superuser).getUserManager()); |
| Group testGroup = ExerciseUtility.createTestGroup(((JackrabbitSession) superuser).getUserManager()); |
| testGroup.addMember(testUser); |
| superuser.save(); |
| |
| testPrincipal = testUser.getPrincipal(); |
| testGroupPrincipal = testGroup.getPrincipal(); |
| } |
| |
| @Override |
| protected void tearDown() throws Exception { |
| try { |
| if (testSession != null && testSession.isLive()) { |
| testSession.logout(); |
| } |
| UserManager uMgr = ((JackrabbitSession) superuser).getUserManager(); |
| if (testUser != null) { |
| testUser.remove(); |
| } |
| Authorizable testGroup = uMgr.getAuthorizable(testGroupPrincipal); |
| if (testGroup != null) { |
| testGroup.remove(); |
| } |
| superuser.save(); |
| } finally { |
| super.tearDown(); |
| } |
| } |
| |
| private Session createTestSession() throws RepositoryException { |
| if (testSession == null) { |
| testSession = superuser.getRepository().login(ExerciseUtility.getTestCredentials(testUser.getID())); |
| } |
| return testSession; |
| } |
| |
| public void testAddNodes() throws Exception { |
| // grant the test principal jcr:addChildNode privilege at 'childPath' |
| AccessControlUtils.addAccessControlEntry(superuser, childPath, testPrincipal, new String[] {Privilege.JCR_ADD_CHILD_NODES}, true); |
| superuser.save(); |
| |
| Session userSession = createTestSession(); |
| |
| // EXERCISE: fill in the expected return values for Session.hasPermission as performed below |
| // EXERCISE: verify that the test passes and explain the individual results |
| Map<String, Boolean> pathHasPermissionMap = ImmutableMap.of( |
| testRootNode.getPath(), null, |
| childPath, null, |
| childPath + "/toCreate", null, |
| grandChildPath + "/nextGeneration", null, |
| propertyPath, null |
| ); |
| |
| for (String path : pathHasPermissionMap.keySet()) { |
| boolean expectedHasPermission = pathHasPermissionMap.get(path); |
| assertEquals(expectedHasPermission, userSession.hasPermission(path, Session.ACTION_ADD_NODE)); |
| } |
| |
| // EXERCISE: fill in the expected return values for AccessControlManager#getPrivileges as performed below |
| // EXERCISE: verify that the test passes and compare the results with your findings from the permission-discovery |
| Map<String, Privilege[]> pathPrivilegesMap = ImmutableMap.of( |
| testRootNode.getPath(), null, |
| childPath, null, |
| childPath + "/toCreate", null, |
| grandChildPath + "/nextGeneration", null |
| ); |
| |
| for (String path : pathPrivilegesMap.keySet()) { |
| Privilege[] expectedPrivileges = pathPrivilegesMap.get(path); |
| assertEquals(ImmutableSet.of(expectedPrivileges), ImmutableSet.of(userSession.getAccessControlManager().getPrivileges(path))); |
| } |
| |
| // EXERCISE: optionally add nodes at the expected allowed path(s) |
| // EXERCISE: using 'userSession' to verify that it actually works and |
| // EXERCISE: save the changes to trigger the evaluation |
| } |
| |
| public void testAddProperties() throws Exception { |
| // grant the test principal rep:addProperties privilege at 'childPath' |
| // EXERCISE: explain the difference between rep:addProperites and jcr:modifyProperties privilege! |
| AccessControlUtils.addAccessControlEntry(superuser, childPath, testPrincipal, new String[] {PrivilegeConstants.REP_ADD_PROPERTIES}, true); |
| superuser.save(); |
| |
| // EXERCISE: fill in the expected return values for Session.hasPermission as performed below |
| // EXERCISE: verify that the test passes and explain the individual results |
| Map<String, Boolean[]> pathHasPermissionMap = ImmutableMap.of( |
| propertyPath, new Boolean[]{null, null}, |
| childPath + "/newProp", new Boolean[]{null, null}, |
| childPropertyPath, new Boolean[]{null, null}, |
| grandChildPath + "/" + JcrConstants.JCR_PRIMARYTYPE, new Boolean[]{null, null} |
| ); |
| |
| Session userSession = createTestSession(); |
| for (String path : pathHasPermissionMap.keySet()) { |
| Boolean[] result = pathHasPermissionMap.get(path); |
| boolean setPropertyAction = result[0]; |
| boolean addPropertiesPermission = result[1]; |
| assertEquals(setPropertyAction, userSession.hasPermission(path, Session.ACTION_SET_PROPERTY)); |
| assertEquals(addPropertiesPermission, userSession.hasPermission(path, Permissions.getString(Permissions.ADD_PROPERTY))); |
| } |
| } |
| |
| public void testRemoveNodes() throws Exception { |
| // EXERCISE: setup the correct set of privileges such that the test passes |
| superuser.save(); |
| |
| Map<String, Boolean> pathHasPermissionMap = ImmutableMap.of( |
| testRootNode.getPath(), false, |
| childPath, false, |
| grandChildPath, true |
| ); |
| |
| Session userSession = createTestSession(); |
| for (String path : pathHasPermissionMap.keySet()) { |
| boolean expectedHasPermission = pathHasPermissionMap.get(path); |
| assertEquals(expectedHasPermission, userSession.hasPermission(path, Session.ACTION_REMOVE)); |
| } |
| |
| AccessControlManager acMgr = userSession.getAccessControlManager(); |
| assertFalse(acMgr.hasPrivileges(childPath, new Privilege[]{acMgr.privilegeFromName(Privilege.JCR_REMOVE_NODE)})); |
| |
| userSession.getNode(grandChildPath).remove(); |
| userSession.save(); |
| } |
| |
| public void testRemoveProperties() throws Exception { |
| // EXERCISE: fix the test case without changing the result-map :-) |
| |
| // grant the test principal rep:removeProperties privilege at 'childPath' |
| AccessControlUtils.addAccessControlEntry(superuser, childPath, testPrincipal, new String[] {PrivilegeConstants.REP_REMOVE_PROPERTIES}, true); |
| superuser.save(); |
| |
| Map<String, Boolean> pathCanRemoveMap = ImmutableMap.of( |
| propertyPath, false, |
| childPropertyPath, true, |
| grandChildPath + "/" + JcrConstants.JCR_PRIMARYTYPE, false, |
| grandChildPath + "/" + propertyName2, false |
| ); |
| |
| Session userSession = createTestSession(); |
| for (String path : pathCanRemoveMap.keySet()) { |
| boolean canRemove = pathCanRemoveMap.get(path); |
| |
| try { |
| userSession.getProperty(path).remove(); |
| if (!canRemove) { |
| fail("property at " + path + " should not be removable."); |
| } |
| } catch (RepositoryException e) { |
| if (canRemove) { |
| fail("property at " + path + " should be removable."); |
| } |
| } finally { |
| userSession.refresh(false); |
| } |
| } |
| } |
| |
| public void testRemoveNonExistingItems() throws Exception { |
| AccessControlUtils.addAccessControlEntry(superuser, childPath, testPrincipal, new String[] { |
| Privilege.JCR_REMOVE_NODE, |
| Privilege.JCR_REMOVE_CHILD_NODES, |
| PrivilegeConstants.REP_REMOVE_PROPERTIES}, true); |
| superuser.save(); |
| |
| // EXERCISE: fill in the expected values for Session.hasPermission(path, Session.ACTION_REMOVE) and explain |
| Map<String, Boolean> pathHasPermission = ImmutableMap.of( |
| childPath + "_non_existing_sibling", null, |
| childPath + "/_non_existing_childitem", null, |
| grandChildPath + "/_non_existing_child_item", null |
| ); |
| |
| Session userSession = createTestSession(); |
| for (String nonExistingItemPath : pathHasPermission.keySet()) { |
| boolean hasPermission = pathHasPermission.get(nonExistingItemPath).booleanValue(); |
| assertEquals(hasPermission, userSession.hasPermission(nonExistingItemPath, Session.ACTION_REMOVE)); |
| } |
| |
| // Additional exercise: |
| // EXERCISE: change the set of privileges granted initially and observe the result of Session.hasPermission. Explain the diff |
| } |
| |
| public void testModifyProperties() throws Exception { |
| // grant the test principal rep:alterProperties privilege at 'childPath' |
| AccessControlUtils.addAccessControlEntry(superuser, childPath, testPrincipal, new String[] {PrivilegeConstants.REP_ALTER_PROPERTIES}, true); |
| superuser.save(); |
| |
| // EXERCISE: Fill if setting the property at the path(s) is expected to pass or not |
| Map<String, Boolean> pathCanModify = ImmutableMap.of( |
| propertyPath, null, |
| childPropertyPath, null, |
| grandChildPath + "/" + JcrConstants.JCR_PRIMARYTYPE, null, |
| grandChildPath + "/" + propertyName2, null |
| ); |
| |
| Session userSession = createTestSession(); |
| for (String path : pathCanModify.keySet()) { |
| boolean canModify = pathCanModify.get(path); |
| try { |
| userSession.getProperty(path).setValue("newVal"); |
| userSession.save(); |
| if (!canModify) { |
| fail("setting property at " + path + " should fail."); |
| } |
| } catch (RepositoryException e) { |
| if (canModify) { |
| fail("setting property at " + path + " should not fail."); |
| } |
| } finally { |
| userSession.refresh(false); |
| } |
| } |
| } |
| |
| public void testSetProperty() throws RepositoryException { |
| // grant the test principal rep:removeProperties privilege at 'childPath' |
| AccessControlUtils.addAccessControlEntry(superuser, childPath, testPrincipal, new String[] {PrivilegeConstants.REP_ALTER_PROPERTIES}, true); |
| superuser.save(); |
| |
| // EXERCISE: Fill if new properties values such that the test-cases succeeds |
| // EXERCISE: Discuss your findings and explain each value. |
| |
| Map<String, Value> pathResultMap = Maps.newHashMap(); |
| pathResultMap.put(childPropertyPath, (Value) null); |
| pathResultMap.put(grandChildPath + "/nonexisting", (Value) null); |
| |
| Session userSession = createTestSession(); |
| for (String path : pathResultMap.keySet()) { |
| try { |
| Value val = pathResultMap.get(path); |
| |
| Node parent = userSession.getNode(Text.getRelativeParent(path, 1)); |
| parent.setProperty(Text.getName(path), val); |
| |
| userSession.save(); |
| } finally { |
| userSession.refresh(false); |
| } |
| } |
| } |
| |
| public void testChangingPrimaryAndMixinTypes() throws RepositoryException { |
| // 1 - grant privilege to change node type information |
| // EXERCISE: fill in the required privilege name such that the test passes |
| String privilegeName = null; |
| AccessControlUtils.addAccessControlEntry(superuser, childPath, testPrincipal, new String[] {privilegeName}, true); |
| superuser.save(); |
| |
| Session userSession = createTestSession(); |
| Node n = userSession.getNode(childPath); |
| n.setPrimaryType(NodeTypeConstants.NT_OAK_UNSTRUCTURED); |
| n.addMixin(JcrConstants.MIX_REFERENCEABLE); |
| userSession.save(); |
| |
| // 2 - additionally grant privilege to add a child node |
| // EXERCISE: extend the set of privileges such that the adding a child node succeeds as well |
| privilegeName = null; |
| AccessControlUtils.addAccessControlEntry(superuser, childPath, testPrincipal, new String[] {privilegeName}, true); |
| superuser.save(); |
| |
| userSession.refresh(false); |
| userSession.getNode(childPath).addNode(nodeName4, NodeTypeConstants.NT_OAK_UNSTRUCTURED); |
| userSession.save(); |
| |
| // 3 - revoke privilege to change node type information |
| // EXERCISE: change the permission setup again such that the rest of the test passes |
| superuser.save(); |
| |
| userSession.refresh(false); |
| n = userSession.getNode(childPath); |
| n.addNode(nodeName3); |
| userSession.save(); |
| |
| try { |
| n.addNode(nodeName1, NodeTypeConstants.NT_OAK_UNSTRUCTURED); |
| userSession.save(); |
| fail("Adding node with explicitly the primary type should fail"); |
| } catch (AccessDeniedException e) { |
| // success |
| } finally { |
| userSession.refresh(false); |
| } |
| |
| |
| AccessControlUtils.addAccessControlEntry(superuser, childPath, testPrincipal, new String[] {privilegeName}, true); |
| |
| } |
| } |