/*
* (C) Copyright 2017 Netcentric AG.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package biz.netcentric.cq.tools.actool.aceinstaller;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.MockitoAnnotations.initMocks;
import java.io.InputStream;
import java.math.BigDecimal;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import javax.jcr.Binary;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.jcr.ValueFormatException;
import javax.jcr.security.AccessControlManager;
import javax.jcr.security.Privilege;
import org.apache.jackrabbit.api.security.JackrabbitAccessControlEntry;
import org.apache.jackrabbit.api.security.JackrabbitAccessControlList;
import org.apache.jackrabbit.api.security.JackrabbitAccessControlManager;
import org.apache.jackrabbit.oak.spi.security.principal.PrincipalImpl;
import org.apache.sling.jcr.api.SlingRepository;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import biz.netcentric.cq.tools.actool.configmodel.AceBean;
import biz.netcentric.cq.tools.actool.configmodel.Restriction;
import biz.netcentric.cq.tools.actool.configreader.YamlConfigReader;
import biz.netcentric.cq.tools.actool.history.AcInstallationLog;
public class AceBeanInstallerIncrementalTest {
private static final String FAKE_PRINCIPAL_ID = "author";
String testPath = "/content/testpath";
String testPrincipal1 = "testPrincipal1";
String testPrincipal2 = "testPrincipal2";
String testPrincipal3 = "testPrincipal3";
AceBean bean1 = createTestBean(testPath, testPrincipal1, true, "jcr:read", "");
AceBean bean2 = createTestBean(testPath, testPrincipal2, true, "jcr:read,jcr:lockManagement,jcr:versionManagement,rep:write", "");
AceBean bean2Content = createTestBean(testPath, testPrincipal2, true,
"jcr:addChildNodes,jcr:nodeTypeManagement,jcr:removeChildNodes,jcr:removeNode", "",
new Restriction(AceBean.RESTRICTION_NAME_GLOB, "*/jcr:content*"));
AceBean bean3 = createTestBean(testPath, testPrincipal3, true, "rep:write", "");
AceBean beanWithAction1 = createTestBean(testPath, testPrincipal1, true, "", "read");
AceBean beanWithAction2 = createTestBean(testPath, testPrincipal2, true, "", "read,create,modify,delete");
@Spy
@InjectMocks
AceBeanInstallerIncremental aceBeanInstallerIncremental;
@Spy
AcInstallationLog installLog;
@Mock
JackrabbitAccessControlList jackrabbitAccessControlList;
@Mock
Session session;
@Mock
JackrabbitAccessControlManager accessControlManager;
@Mock
SlingRepository slingRepository;
@Before
public void setup() throws RepositoryException {
initMocks(this);
doReturn(accessControlManager).when(session).getAccessControlManager();
// empty by default
doReturn(new JackrabbitAccessControlEntry[0]).when(jackrabbitAccessControlList).getAccessControlEntries();
doReturn(testPath).when(jackrabbitAccessControlList).getPath();
doReturn(jackrabbitAccessControlList).when(aceBeanInstallerIncremental).getAccessControlList(eq(accessControlManager), anyString());
doReturn(true).when(aceBeanInstallerIncremental).installPrivileges(any(AceBean.class), any(Principal.class),
eq(jackrabbitAccessControlList), eq(session), eq(accessControlManager));
// default privilege is a simple privilege with the given string name
doAnswer(new Answer<Privilege>() {
public Privilege answer(InvocationOnMock invocation) {
Object[] args = invocation.getArguments();
return new TestPrivilege(args[0].toString());
}
}).when(accessControlManager).privilegeFromName(anyString());
// to test aggregates
doReturn(new TestPrivilege("jcr:read", new String[] { "jcr:readNodes", "jcr:readProperties" })).when(accessControlManager)
.privilegeFromName("jcr:read");
// easier to mock than the static SessionUtil.clone()
doReturn(session).when(slingRepository).loginService(anyString(), anyString());
doReturn(true).when(aceBeanInstallerIncremental).definesContent(testPath, session);
doReturn(new PrincipalImpl(FAKE_PRINCIPAL_ID)).when(aceBeanInstallerIncremental).applyCqActions(any(AceBean.class), eq(session),
anyString());
}
@Test
public void testPrivilegesToComparableSet() throws RepositoryException {
// ensure mocking is correct
assertFalse(accessControlManager.privilegeFromName("jcr:removeNode").isAggregate()); // default mocking
assertFalse(accessControlManager.privilegeFromName("jcr:lockManagement").isAggregate()); // default mocking
assertTrue(accessControlManager.privilegeFromName("jcr:read").isAggregate()); // aggragate test mocking
assertEquals("simple non-aggregate must equal",
"[jcr:lockManagement]",
createComparablePrivSet("jcr:lockManagement"));
assertEquals("simple aggregate must be resolved to non-aggregates",
"[jcr:readNodes, jcr:readProperties]",
createComparablePrivSet("jcr:read"));
assertEquals("non-aggregate order is sorted (test order un-changed)",
"[jcr:lockManagement, jcr:removeNode]",
createComparablePrivSet("jcr:lockManagement, jcr:removeNode"));
assertEquals("non-aggregate order is sorted (test order changed)",
"[jcr:lockManagement, jcr:removeNode]",
createComparablePrivSet("jcr:removeNode, jcr:lockManagement"));
assertEquals("privilege order not important even for mix of aggregate and non-aggregate privs (must be still equal)",
"[jcr:lockManagement, jcr:readNodes, jcr:readProperties, jcr:removeNode]",
createComparablePrivSet("jcr:removeNode, jcr:read, jcr:lockManagement"));
}
@Test
public void testSimplePrivilegesAcesAdditive() throws Exception {
aceBeanInstallerIncremental.installAcl(
asSet(bean1, bean2, bean3), testPath,
asSet(testPrincipal1, testPrincipal2, testPrincipal3), session, installLog);
verify(jackrabbitAccessControlList, never()).removeAccessControlEntry(any(JackrabbitAccessControlEntry.class));
verify(aceBeanInstallerIncremental).installPrivileges(eq(bean1), eq(new PrincipalImpl(testPrincipal1)),
eq(jackrabbitAccessControlList), eq(session), eq(accessControlManager));
verify(aceBeanInstallerIncremental).installPrivileges(eq(bean2), eq(new PrincipalImpl(testPrincipal2)),
eq(jackrabbitAccessControlList), eq(session), eq(accessControlManager));
verify(aceBeanInstallerIncremental).installPrivileges(eq(bean3), eq(new PrincipalImpl(testPrincipal3)),
eq(jackrabbitAccessControlList), eq(session), eq(accessControlManager));
}
@Test
public void testSimplePrivilegesAcesUnchanged() throws Exception {
// make bean1 and bean
doReturn(new JackrabbitAccessControlEntry[] {
aceBeanToAce(bean1), aceBeanToAce(bean2), aceBeanToAce(bean3)
}).when(jackrabbitAccessControlList).getAccessControlEntries();
aceBeanInstallerIncremental.installAcl(
asSet(bean1, bean2, bean3), testPath,
asSet(testPrincipal1, testPrincipal2, testPrincipal3), session, installLog);
verify(jackrabbitAccessControlList, never()).removeAccessControlEntry(any(JackrabbitAccessControlEntry.class));
verify(aceBeanInstallerIncremental, never()).installPrivileges(any(AceBean.class), any(Principal.class),
any(JackrabbitAccessControlList.class), any(Session.class), any(AccessControlManager.class));
}
@Test
public void testSimplePrivilegesAcesRemoved() throws Exception {
// make bean1 and bea
JackrabbitAccessControlEntry ace1 = aceBeanToAce(bean1);
JackrabbitAccessControlEntry ace2 = aceBeanToAce(bean2);
JackrabbitAccessControlEntry ace3 = aceBeanToAce(bean3);
doReturn(new JackrabbitAccessControlEntry[] { ace1, ace2, ace3 }).when(jackrabbitAccessControlList).getAccessControlEntries();
aceBeanInstallerIncremental.installAcl(
Collections.<AceBean> emptySet(), testPath,
asSet(testPrincipal1, testPrincipal2, testPrincipal3), session, installLog);
verify(jackrabbitAccessControlList).removeAccessControlEntry(ace1);
verify(jackrabbitAccessControlList).removeAccessControlEntry(ace2);
verify(jackrabbitAccessControlList).removeAccessControlEntry(ace3);
verify(aceBeanInstallerIncremental, never()).installPrivileges(any(AceBean.class), any(Principal.class),
any(JackrabbitAccessControlList.class), any(Session.class), any(AccessControlManager.class));
}
@Test
public void testGetPrincipalAceBeansForActionAceBeanIsCalledToResolveActions() throws Exception {
// read maps to one simple bean
doReturn(asSet(bean1)).when(aceBeanInstallerIncremental).getPrincipalAceBeansForActionAceBean(beanWithAction1, session);
// read,create,modify,delete maps to two beans
doReturn(asSet(bean2, bean2Content)).when(aceBeanInstallerIncremental).getPrincipalAceBeansForActionAceBean(beanWithAction2,
session);
aceBeanInstallerIncremental.installAcl(
asSet(beanWithAction1, beanWithAction2), testPath,
asSet(testPrincipal1, testPrincipal2), session, installLog);
verify(jackrabbitAccessControlList, never()).removeAccessControlEntry(any(JackrabbitAccessControlEntry.class));
verify(aceBeanInstallerIncremental).installPrivileges(eq(bean1), eq(new PrincipalImpl(testPrincipal1)),
eq(jackrabbitAccessControlList), eq(session), eq(accessControlManager));
verify(aceBeanInstallerIncremental).installPrivileges(eq(bean2), eq(new PrincipalImpl(testPrincipal2)),
eq(jackrabbitAccessControlList), eq(session), eq(accessControlManager));
verify(aceBeanInstallerIncremental).installPrivileges(eq(bean2Content), eq(new PrincipalImpl(testPrincipal2)),
eq(jackrabbitAccessControlList), eq(session), eq(accessControlManager));
}
@Test
public void testGetPrincipalAceBeansForActionRead() throws Exception {
AceBean bean1Clone = bean1.clone();
bean1Clone.setPrincipal(FAKE_PRINCIPAL_ID);
// test simple read bean
doReturn(new JackrabbitAccessControlEntry[] {
aceBeanToAce(bean1Clone)
}).when(jackrabbitAccessControlList).getAccessControlEntries();
Set<AceBean> resultAceBeans = aceBeanInstallerIncremental.getPrincipalAceBeansForActionAceBean(beanWithAction1,
session);
assertEquals(1, resultAceBeans.size());
Iterator<AceBean> resultAceBeansIt = resultAceBeans.iterator();
AceBean firstResult = resultAceBeansIt.next();
assertEquals(bean1.getPrincipalName(), firstResult.getPrincipalName());
assertEquals(bean1.getPermission(), firstResult.getPermission());
assertEquals(bean1.getJcrPath(), firstResult.getJcrPath());
assertArrayEquals(bean1.getPrivileges(), firstResult.getPrivileges());
assertArrayEquals(null, firstResult.getActions());
assertTrue(firstResult.getRestrictions().isEmpty());
}
@Test
public void testGetPrincipalAceBeansForActionReadCreateModifyDelete() throws Exception {
// test read,create,modify.delete
AceBean bean2Clone = bean2.clone();
bean2Clone.setPrincipal(FAKE_PRINCIPAL_ID);
AceBean bean2ContentClone = bean2Content.clone();
bean2ContentClone.setPrincipal(FAKE_PRINCIPAL_ID);
doReturn(new JackrabbitAccessControlEntry[] {
aceBeanToAce(bean2Clone), aceBeanToAce(bean2ContentClone)
}).when(jackrabbitAccessControlList).getAccessControlEntries();
Set<AceBean> resultAceBeans = aceBeanInstallerIncremental.getPrincipalAceBeansForActionAceBean(beanWithAction2,
session);
assertEquals(2, resultAceBeans.size());
Iterator<AceBean> resultAceBeansIt = resultAceBeans.iterator();
AceBean firstResult = resultAceBeansIt.next();
assertEquals(bean2.getPrincipalName(), firstResult.getPrincipalName());
assertEquals(bean2.getPermission(), firstResult.getPermission());
assertEquals(bean2.getJcrPath(), firstResult.getJcrPath());
assertArrayEquals(bean2.getPrivileges(), firstResult.getPrivileges());
assertArrayEquals(null, firstResult.getActions());
assertTrue(firstResult.getRestrictions().isEmpty());
AceBean secondResult = resultAceBeansIt.next();
assertEquals(bean2Content.getPrincipalName(), secondResult.getPrincipalName());
assertEquals(bean2Content.getPermission(), secondResult.getPermission());
assertEquals(bean2Content.getJcrPath(), secondResult.getJcrPath());
assertArrayEquals(bean2Content.getPrivileges(), secondResult.getPrivileges());
assertArrayEquals(null, secondResult.getActions());
assertEquals(1, secondResult.getRestrictions().size());
assertEquals(AceBean.RESTRICTION_NAME_GLOB, secondResult.getRestrictions().get(0).getName());
assertEquals("*/jcr:content*", secondResult.getRestrictions().get(0).getValue());
}
public static <T> Set<T> asSet(T... objects) {
return new LinkedHashSet<T>(Arrays.asList(objects));
}
public static AceBean createTestBean(String path, String principalName, boolean isAllow, String privileges, String actions,
Restriction... restrictions) {
AceBean testBean = new AceBean();
testBean.setJcrPath(path);
testBean.setPrincipal(principalName);
testBean.setPermission(isAllow ? "allow" : "deny");
testBean.setPrivilegesString(privileges);
testBean.setActions(YamlConfigReader.parseActionsString(actions));
testBean.setRestrictions(Arrays.asList(restrictions));
return testBean;
}
private String createComparablePrivSet(String privsIn) throws RepositoryException {
return aceBeanInstallerIncremental.privilegesToComparableSet(privsIn.split(" *, *"), accessControlManager);
}
public static JackrabbitAccessControlEntry aceBeanToAce(final AceBean bean) {
return new JackrabbitAccessControlEntry() {
@Override
public Principal getPrincipal() {
return new PrincipalImpl(bean.getPrincipalName());
}
@Override
public Privilege[] getPrivileges() {
List<Privilege> privileges = new ArrayList<Privilege>();
for (final String priv : bean.getPrivileges()) {
privileges.add(new TestPrivilege(priv));
}
return privileges.toArray(new Privilege[privileges.size()]);
}
@Override
public boolean isAllow() {
return bean.isAllow();
}
@Override
public String[] getRestrictionNames() throws RepositoryException {
List<String> names = new ArrayList<String>();
for (Restriction restriction : bean.getRestrictions()) {
names.add(restriction.getName());
}
return names.toArray(new String[names.size()]);
}
@Override
public Value getRestriction(String name) throws ValueFormatException, RepositoryException {
for (final Restriction restriction : bean.getRestrictions()) {
if (restriction.getName().equals(name)) {
return new TestValue(restriction.getValue());
}
}
return null;
}
@Override
public Value[] getRestrictions(String name) throws RepositoryException {
List<Value> values = new ArrayList<Value>();
for (final Restriction restriction : bean.getRestrictions()) {
if (restriction.getName().equals(name)) {
values.add(new TestValue(restriction.getValue()));
}
}
return values.toArray(new Value[values.size()]);
}
};
}
private static final class TestPrivilege implements Privilege {
private final String priv;
private final Privilege[] aggregatePrivileges;
private TestPrivilege(String priv) {
this.priv = priv;
this.aggregatePrivileges = new Privilege[0];
}
private TestPrivilege(String priv, String[] aggregatePrivilegesStrArr) {
this.priv = priv;
aggregatePrivileges = new Privilege[aggregatePrivilegesStrArr.length];
for (int i = 0; i < aggregatePrivilegesStrArr.length; i++) {
aggregatePrivileges[i] = new TestPrivilege(aggregatePrivilegesStrArr[i]);
}
}
@Override
public String getName() {
return priv;
}
@Override
public boolean isAbstract() {
return false;
}
@Override
public boolean isAggregate() {
return aggregatePrivileges.length > 0;
}
@Override
public Privilege[] getDeclaredAggregatePrivileges() {
return aggregatePrivileges;
}
@Override
public Privilege[] getAggregatePrivileges() {
return aggregatePrivileges;
}
@Override
public String toString() {
return "[TestPrivilege " + priv + " aggregate of " + Arrays.toString(aggregatePrivileges) + "]";
}
}
static final class TestValue implements Value {
private final String val;
TestValue(String val) {
this.val = val;
}
@Override
public int getType() {
return PropertyType.STRING;
}
@Override
public String getString() throws ValueFormatException, IllegalStateException, RepositoryException {
return val;
}
@Override
public InputStream getStream() throws RepositoryException {
throw new UnsupportedOperationException();
}
@Override
public long getLong() throws ValueFormatException, RepositoryException {
throw new UnsupportedOperationException();
}
@Override
public double getDouble() throws ValueFormatException, RepositoryException {
throw new UnsupportedOperationException();
}
@Override
public BigDecimal getDecimal() throws ValueFormatException, RepositoryException {
throw new UnsupportedOperationException();
}
@Override
public Calendar getDate() throws ValueFormatException, RepositoryException {
throw new UnsupportedOperationException();
}
@Override
public boolean getBoolean() throws ValueFormatException, RepositoryException {
throw new UnsupportedOperationException();
}
@Override
public Binary getBinary() throws RepositoryException {
throw new UnsupportedOperationException();
}
}
}