/* * 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.core.security.user; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Set; import javax.jcr.AccessDeniedException; import javax.jcr.ItemNotFoundException; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.Property; import javax.jcr.PropertyIterator; import javax.jcr.PropertyType; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.Value; import javax.jcr.observation.Event; import javax.jcr.observation.EventIterator; import org.apache.jackrabbit.core.NodeImpl; import org.apache.jackrabbit.core.PropertyImpl; import org.apache.jackrabbit.core.SessionImpl; import org.apache.jackrabbit.core.SessionListener; import org.apache.jackrabbit.core.cache.ConcurrentCache; import org.apache.jackrabbit.core.nodetype.NodeTypeImpl; import org.apache.jackrabbit.core.observation.SynchronousEventListener; import org.apache.jackrabbit.spi.Name; import org.apache.jackrabbit.util.Text; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * <code>MembershipCache</code>... */ public class MembershipCache implements UserConstants, SynchronousEventListener, SessionListener { /** * logger instance */ private static final Logger log = LoggerFactory.getLogger(MembershipCache.class); /** * The maximum size of this cache */ private static final int MAX_CACHE_SIZE = Integer.getInteger("org.apache.jackrabbit.MembershipCache", 5000); private final SessionImpl systemSession; private final String groupsPath; private final boolean useMembersNode; private final String pMembers; private final ConcurrentCache<String, Collection<String>> cache; MembershipCache(SessionImpl systemSession, String groupsPath, boolean useMembersNode) throws RepositoryException { this.systemSession = systemSession; this.groupsPath = (groupsPath == null) ? UserConstants.GROUPS_PATH : groupsPath; this.useMembersNode = useMembersNode; pMembers = systemSession.getJCRName(UserManagerImpl.P_MEMBERS); cache = new ConcurrentCache<String, Collection<String>>("MembershipCache", 16); cache.setMaxMemorySize(MAX_CACHE_SIZE); String[] ntNames = new String[] { systemSession.getJCRName(UserConstants.NT_REP_GROUP), systemSession.getJCRName(UserConstants.NT_REP_MEMBERS) }; // register event listener to be informed about membership changes. systemSession.getWorkspace().getObservationManager().addEventListener(this, Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED | Event.PROPERTY_REMOVED, groupsPath, true, null, ntNames, false); // make sure the membership cache is informed if the system session is // logged out in order to stop listening to events. systemSession.addListener(this); log.debug("Membership cache initialized. Max Size = {}", MAX_CACHE_SIZE); } //------------------------------------------------------< EventListener >--- /** * @see javax.jcr.observation.EventListener#onEvent(javax.jcr.observation.EventIterator) */ public void onEvent(EventIterator eventIterator) { // evaluate if the membership cache needs to be cleared; boolean clear = false; while (eventIterator.hasNext() && !clear) { Event ev = eventIterator.nextEvent(); try { if (pMembers.equals(Text.getName(ev.getPath()))) { // simple case: a rep:members property that is affected clear = true; } else if (useMembersNode) { // test if it affects a property defined by rep:Members node type. int type = ev.getType(); if (type == Event.PROPERTY_ADDED || type == Event.PROPERTY_CHANGED) { Property p = systemSession.getProperty(ev.getPath()); Name declNtName = ((NodeTypeImpl) p.getDefinition().getDeclaringNodeType()).getQName(); clear = NT_REP_MEMBERS.equals(declNtName); } else { // PROPERTY_REMOVED // test if the primary node type of the parent node is rep:Members // this could potentially by some other property as well as the // rep:Members node are not protected and could changed by // adding a mixin type. // ignoring this and simply clear the cache String parentId = ev.getIdentifier(); Node n = systemSession.getNodeByIdentifier(parentId); Name ntName = ((NodeTypeImpl) n.getPrimaryNodeType()).getQName(); clear = (UserConstants.NT_REP_MEMBERS.equals(ntName)); } } } catch (RepositoryException e) { log.warn(e.getMessage()); // exception while processing the event -> clear the cache to // be sure it isn't outdated. clear = true; } } if (clear) { cache.clear(); log.debug("Membership cache cleared because of observation event."); } } //----------------------------------------------------< SessionListener >--- /** * @see SessionListener#loggingOut(org.apache.jackrabbit.core.SessionImpl) */ public void loggingOut(SessionImpl session) { try { systemSession.getWorkspace().getObservationManager().removeEventListener(this); } catch (RepositoryException e) { log.error("Unexpected error: Failed to stop event listening of MembershipCache.", e); } } /** * @see SessionListener#loggedOut(org.apache.jackrabbit.core.SessionImpl) */ public void loggedOut(SessionImpl session) { // nothing to do } //-------------------------------------------------------------------------- /** * @param authorizableNodeIdentifier The identifier of the node representing * the authorizable to retrieve the declared membership for. * @return A collection of node identifiers of those group nodes the * authorizable in question is declared member of. * @throws RepositoryException If an error occurs. */ Collection<String> getDeclaredMemberOf(String authorizableNodeIdentifier) throws RepositoryException { return declaredMemberOf(authorizableNodeIdentifier); } /** * @param authorizableNodeIdentifier The identifier of the node representing * the authorizable to retrieve the membership for. * @return A collection of node identifiers of those group nodes the * authorizable in question is a direct or indirect member of. * @throws RepositoryException If an error occurs. */ Collection<String> getMemberOf(String authorizableNodeIdentifier) throws RepositoryException { Set<String> groupNodeIds = new HashSet<String>(); memberOf(authorizableNodeIdentifier, groupNodeIds); return Collections.unmodifiableCollection(groupNodeIds); } /** * Returns the size of the membership cache * @return the size */ int getSize() { return (int) cache.getElementCount(); } /** * For testing purposes only. */ void clear() { cache.clear(); } /** * Collects the declared memberships for the specified identifier of an * authorizable using the specified session. * * @param authorizableNodeIdentifier The identifier of the node representing * the authorizable to retrieve the membership for. * @param session The session to be used to read the membership information. * @return @return A collection of node identifiers of those group nodes the * authorizable in question is a direct member of. * @throws RepositoryException If an error occurs. */ Collection<String> collectDeclaredMembership(String authorizableNodeIdentifier, Session session) throws RepositoryException { final long t0 = System.nanoTime(); Collection<String> groupNodeIds = collectDeclaredMembershipFromReferences(authorizableNodeIdentifier, session); final long t1 = System.nanoTime(); if (log.isDebugEnabled()) { log.debug(" collected {} groups for {} via references in {}us", new Object[]{ groupNodeIds == null ? -1 : groupNodeIds.size(), authorizableNodeIdentifier, (t1-t0) / 1000 }); } if (groupNodeIds == null) { groupNodeIds = collectDeclaredMembershipFromTraversal(authorizableNodeIdentifier, session); final long t2 = System.nanoTime(); if (log.isDebugEnabled()) { log.debug(" collected {} groups for {} via traversal in {}us", new Object[]{ groupNodeIds == null ? -1 : groupNodeIds.size(), authorizableNodeIdentifier, (t2-t1) / 1000 }); } } return groupNodeIds; } /** * Collects the complete memberships for the specified identifier of an * authorizable using the specified session. * * @param authorizableNodeIdentifier The identifier of the node representing * the authorizable to retrieve the membership for. * @param session The session to be used to read the membership information. * @return A collection of node identifiers of those group nodes the * authorizable in question is a direct or indirect member of. * @throws RepositoryException If an error occurs. */ Collection<String> collectMembership(String authorizableNodeIdentifier, Session session) throws RepositoryException { Set<String> groupNodeIds = new HashSet<String>(); memberOf(authorizableNodeIdentifier, groupNodeIds, session); return groupNodeIds; } //------------------------------------------------------------< private >--- /** * Collects the groups where the given authorizable is a declared member of. If the information is not cached, it * is collected from the repository. * * @param authorizableNodeIdentifier Identifier of the authorizable node * @return the collection of groups where the authorizable is a declared member of * @throws RepositoryException if an error occurs */ private Collection<String> declaredMemberOf(String authorizableNodeIdentifier) throws RepositoryException { final long t0 = System.nanoTime(); Collection<String> groupNodeIds = cache.get(authorizableNodeIdentifier); boolean wasCached = true; if (groupNodeIds == null) { wasCached = false; // retrieve a new session with system-subject in order to avoid // concurrent read operations using the system session of this workspace. Session session = getSession(); try { groupNodeIds = collectDeclaredMembership(authorizableNodeIdentifier, session); cache.put(authorizableNodeIdentifier, Collections.unmodifiableCollection(groupNodeIds), 1); } finally { // release session if it isn't the original system session if (session != systemSession) { session.logout(); } } } if (log.isDebugEnabled()) { final long t1 = System.nanoTime(); log.debug("Membership cache {} {} declared memberships of {} in {}us. cache size = {}", new Object[]{ wasCached ? "returns" : "collected", groupNodeIds.size(), authorizableNodeIdentifier, (t1-t0) / 1000, cache.getElementCount() }); } return groupNodeIds; } /** * Collects the groups where the given authorizable is a member of by recursively fetching the declared memberships * via {@link #declaredMemberOf(String)} (cached). * * @param authorizableNodeIdentifier Identifier of the authorizable node * @param groupNodeIds Map to receive the node ids of the groups * @throws RepositoryException if an error occurs */ private void memberOf(String authorizableNodeIdentifier, Collection<String> groupNodeIds) throws RepositoryException { Collection<String> declared = declaredMemberOf(authorizableNodeIdentifier); for (String identifier : declared) { if (groupNodeIds.add(identifier)) { memberOf(identifier, groupNodeIds); } } } /** * Collects the groups where the given authorizable is a member of by recursively fetching the declared memberships * by reading the relations from the repository (uncached!). * * @param authorizableNodeIdentifier Identifier of the authorizable node * @param groupNodeIds Map to receive the node ids of the groups * @param session the session to read from * @throws RepositoryException if an error occurs */ private void memberOf(String authorizableNodeIdentifier, Collection<String> groupNodeIds, Session session) throws RepositoryException { Collection<String> declared = collectDeclaredMembership(authorizableNodeIdentifier, session); for (String identifier : declared) { if (groupNodeIds.add(identifier)) { memberOf(identifier, groupNodeIds, session); } } } /** * Collects the declared memberships for the given authorizable by resolving the week references to the authorizable. * If the lookup fails, <code>null</code> is returned. This most likely the case if the authorizable does not exit (yet) * in the session that is used for the lookup. * * @param authorizableNodeIdentifier Identifier of the authorizable node * @param session the session to read from * @return a collection of group node ids or <code>null</code> if the lookup failed. * @throws RepositoryException if an error occurs */ private Collection<String> collectDeclaredMembershipFromReferences(String authorizableNodeIdentifier, Session session) throws RepositoryException { Set<String> pIds = new HashSet<String>(); Set<String> nIds = new HashSet<String>(); // Try to get membership information from references PropertyIterator refs = getMembershipReferences(authorizableNodeIdentifier, session); if (refs == null) { return null; } while (refs.hasNext()) { try { PropertyImpl pMember = (PropertyImpl) refs.nextProperty(); NodeImpl nGroup = (NodeImpl) pMember.getParent(); Set<String> groupNodeIdentifiers; if (P_MEMBERS.equals(pMember.getQName())) { // Found membership information in members property groupNodeIdentifiers = pIds; } else { // Found membership information in members node groupNodeIdentifiers = nIds; while (nGroup.isNodeType(NT_REP_MEMBERS)) { nGroup = (NodeImpl) nGroup.getParent(); } } if (nGroup.isNodeType(NT_REP_GROUP)) { groupNodeIdentifiers.add(nGroup.getIdentifier()); } else { // weak-ref property 'rep:members' that doesn't reside under an // group node -> doesn't represent a valid group member. log.debug("Invalid member reference to '{}' -> Not included in membership set.", this); } } catch (ItemNotFoundException e) { // group node doesn't exist -> -> ignore exception // and skip this reference from membership list. } catch (AccessDeniedException e) { // not allowed to see the group node -> ignore exception // and skip this reference from membership list. } } // Based on the user's setting return either of the found membership information return select(pIds, nIds); } /** * Collects the declared memberships for the given authorizable by traversing the groups structure. * * @param authorizableNodeIdentifier Identifier of the authorizable node * @param session the session to read from * @return a collection of group node ids. * @throws RepositoryException if an error occurs */ private Collection<String> collectDeclaredMembershipFromTraversal( final String authorizableNodeIdentifier, Session session) throws RepositoryException { final Set<String> pIds = new HashSet<String>(); final Set<String> nIds = new HashSet<String>(); // workaround for failure of Node#getWeakReferences // traverse the tree below groups-path and collect membership manually. log.info("Traversing groups tree to collect membership."); if (session.nodeExists(groupsPath)) { Node groupsNode = session.getNode(groupsPath); traverseAndCollect(authorizableNodeIdentifier, pIds, nIds, (NodeImpl) groupsNode); } // else: no groups exist -> nothing to do. // Based on the user's setting return either of the found membership information return select(pIds, nIds); } /** * traverses the groups structure to find the groups of which the given authorizable is member of. * * @param authorizableNodeIdentifier Identifier of the authorizable node * @param pIds output set to update of group node ids that were found via the property memberships * @param nIds output set to update of group node ids that were found via the node memberships * @param node the node to traverse * @throws RepositoryException if an error occurs */ private void traverseAndCollect(String authorizableNodeIdentifier, Set<String> pIds, Set<String> nIds, NodeImpl node) throws RepositoryException { if (node.isNodeType(NT_REP_GROUP)) { String groupId = node.getIdentifier(); if (node.hasProperty(P_MEMBERS)) { for (Value value : node.getProperty(P_MEMBERS).getValues()) { String v = value.getString(); if (v.equals(authorizableNodeIdentifier)) { pIds.add(groupId); } } } NodeIterator iter = node.getNodes(); while (iter.hasNext()) { NodeImpl child = (NodeImpl) iter.nextNode(); if (child.isNodeType(NT_REP_MEMBERS)) { isMemberOfNodeBaseMembershipGroup(authorizableNodeIdentifier, groupId, nIds, child); } } } else { NodeIterator iter = node.getNodes(); while (iter.hasNext()) { NodeImpl child = (NodeImpl) iter.nextNode(); traverseAndCollect(authorizableNodeIdentifier, pIds, nIds, child); } } } /** * traverses the group structure of a node-based group to check if the given authorizable is member of this group. * * @param authorizableNodeIdentifier Identifier of the authorizable node * @param groupId if of the group * @param nIds output set to update of group node ids that were found via the node memberships * @param node the node to traverse * @throws RepositoryException if an error occurs */ private void isMemberOfNodeBaseMembershipGroup(String authorizableNodeIdentifier, String groupId, Set<String> nIds, NodeImpl node) throws RepositoryException { PropertyIterator pIter = node.getProperties(); while (pIter.hasNext()) { PropertyImpl p = (PropertyImpl) pIter.nextProperty(); if (p.getType() == PropertyType.WEAKREFERENCE) { Value[] values = p.isMultiple() ? p.getValues() : new Value[]{p.getValue()}; for (Value v: values) { if (v.getString().equals(authorizableNodeIdentifier)) { nIds.add(groupId); return; } } } } NodeIterator iter = node.getNodes(); while (iter.hasNext()) { NodeImpl child = (NodeImpl) iter.nextNode(); if (child.isNodeType(NT_REP_MEMBERS)) { isMemberOfNodeBaseMembershipGroup(authorizableNodeIdentifier, groupId, nIds, child); } } } /** * Return either of both sets depending on the users setting whether * to use the members property or the members node to record membership * information. If both sets are non empty, the one configured in the * settings will take precedence and an warning is logged. * * @param pIds the set of group node ids retrieved through membership properties * @param nIds the set of group node ids retrieved through membership nodes * @return the selected set. */ private Set<String> select(Set<String> pIds, Set<String> nIds) { Set<String> result; if (useMembersNode) { if (!nIds.isEmpty() || pIds.isEmpty()) { result = nIds; } else { result = pIds; } } else { if (!pIds.isEmpty() || nIds.isEmpty()) { result = pIds; } else { result = nIds; } } if (!pIds.isEmpty() && !nIds.isEmpty()) { log.warn("Found members node and members property. Ignoring {} members", useMembersNode ? "property" : "node"); } return result; } /** * @return a new Session that needs to be properly released after usage. */ private SessionImpl getSession() { try { return (SessionImpl) systemSession.createSession(systemSession.getWorkspace().getName()); } catch (RepositoryException e) { // fallback return systemSession; } } /** * Returns the membership references for the given authorizable. * @param authorizableNodeIdentifier Identifier of the authorizable node * @param session session to read from * @return the property iterator or <code>null</code> */ private static PropertyIterator getMembershipReferences(String authorizableNodeIdentifier, Session session) { PropertyIterator refs = null; try { refs = session.getNodeByIdentifier(authorizableNodeIdentifier).getWeakReferences(null); } catch (RepositoryException e) { log.error("Failed to retrieve membership references of " + authorizableNodeIdentifier + ".", e); } return refs; } }