blob: 099fbb36ea7bade73d04c3086e6a851a0ca24c42 [file] [log] [blame]
/*
* 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 com.google.common.collect.ImmutableSet;
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.plugins.tree.TreeUtil;
import org.apache.jackrabbit.oak.security.principal.AbstractPrincipalProviderTest;
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.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.jetbrains.annotations.NotNull;
import org.junit.Test;
import javax.jcr.SimpleCredentials;
import javax.security.auth.Subject;
import java.security.Principal;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
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 ContentSession systemSession;
private Root systemRoot;
@Override
public void before() throws Exception {
super.before();
userId = getTestUser().getID();
systemSession = getSystemSession();
systemRoot = systemSession.getLatestRoot();
}
@Override
public void after() throws Exception {
try {
if (systemSession != null) {
systemSession.close();
}
} finally {
super.after();
}
}
@Override
protected ConfigurationParameters getSecurityConfigParameters() {
return ConfigurationParameters.of(
UserConfiguration.NAME,
ConfigurationParameters.of(UserPrincipalProvider.PARAM_CACHE_EXPIRATION, 3600 * 1000)
);
}
@NotNull
@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, (PrivilegedExceptionAction<ContentSession>) () -> login(null));
}
return systemSession;
}
private void changeUserConfiguration(ConfigurationParameters params) {
UserConfiguration userConfig = getUserConfiguration();
((ConfigurationBase) userConfig).setParameters(params);
}
private Tree getCacheTree(Root root) throws Exception {
return getCacheTree(root, getTestUser().getPath());
}
private Tree getCacheTree(Root root, String authorizablePath) {
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.getMembershipPrincipals(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.getMembershipPrincipals(testGroup.getPrincipal());
assertPrincipals(principals, EveryonePrincipal.getInstance());
principals = pp.getMembershipPrincipals(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 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);
assertNotEquals(principals, principalsAgain);
assertPrincipals(principalsAgain, EveryonePrincipal.getInstance(), getTestUser().getPrincipal());
// verify that the cache has really been updated
cache = getCacheTree(systemRoot);
assertNotSame(2, TreeUtil.getLong(cache, CacheConstants.REP_EXPIRATION, 2));
assertEquals("", TreeUtil.getString(cache, CacheConstants.REP_GROUP_PRINCIPAL_NAMES));
// check that an cached empty membership set doesn't break the retrieval (OAK-8306)
principalsAgain = pp.getPrincipals(userId);
assertNotEquals(principals, principalsAgain);
assertPrincipals(principalsAgain, EveryonePrincipal.getInstance(), getTestUser().getPrincipal());
}
@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);
assertEquals(principals, 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 and with system-session must not succeed
for (Root r : new Root[] {root, systemRoot}) {
for (PropertyState ps : props) {
try {
Tree cache = getCacheTree(r);
cache.setProperty(ps);
r.commit();
fail("Attempt to modify the cache tree must fail.");
} catch (CommitFailedException e) {
// success
} finally {
r.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<>();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 100; i++) {
threads.add(new Thread(() -> {
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();
}
}
}