/*
* 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 org.apache.jackrabbit.api.JackrabbitWorkspace;
import org.apache.jackrabbit.api.security.principal.ItemBasedPrincipal;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.api.security.user.UserManager;
import org.apache.jackrabbit.core.ItemImpl;
import org.apache.jackrabbit.core.NodeImpl;
import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.id.ItemId;
import org.apache.jackrabbit.core.nodetype.NodeTypeImpl;
import org.apache.jackrabbit.core.observation.SynchronousEventListener;
import org.apache.jackrabbit.core.security.AnonymousPrincipal;
import org.apache.jackrabbit.core.security.SecurityConstants;
import org.apache.jackrabbit.core.security.authorization.AbstractAccessControlProvider;
import org.apache.jackrabbit.core.security.authorization.AbstractCompiledPermissions;
import org.apache.jackrabbit.core.security.authorization.AccessControlEditor;
import org.apache.jackrabbit.core.security.authorization.CompiledPermissions;
import org.apache.jackrabbit.core.security.authorization.NamedAccessControlPolicyImpl;
import org.apache.jackrabbit.core.security.authorization.Permission;
import org.apache.jackrabbit.core.security.authorization.PrivilegeBits;
import org.apache.jackrabbit.core.security.authorization.PrivilegeManagerImpl;
import org.apache.jackrabbit.core.security.authorization.PrivilegeRegistry;
import org.apache.jackrabbit.core.security.principal.PrincipalImpl;
import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.util.Text;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.ItemNotFoundException;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.observation.Event;
import javax.jcr.observation.EventIterator;
import javax.jcr.security.AccessControlPolicy;
import javax.jcr.security.Privilege;
import java.security.Principal;
import java.security.acl.Group;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
/**
* Implementation of the <code>AccessControlProvider</code> interface that
* is used to protected the 'security workspace' containing the user and
* group data. It applies special care to make sure that modifying user data
* (e.g. password), group membership and impersonation is properly controlled.
* <p/>
* This provider creates upon initialization the following 2 groups:
* <ul>
* <li>User administrator</li>
* <li>Group administrator</li>
* </ul>
*
* The default access control policy defined by this provider has the following
* characteristics:
* <ul>
* <li>All authenticated users have READ permission to all items. If {link #PARAM_ANONYMOUS_ACCESS}
* is configured to be <code>true</code> this also applies to the anonymous user.</li>
*
* <li>every known user is allowed to modify it's own properties except for
* her/his group membership,</li>
*
* <li>members of the 'User administrator' group are allowed to create, modify
* and remove users,</li>
*
* <li>members of the 'Group administrator' group are allowed to create, modify
* and remove groups,</li>
*
* <li>group membership can only be edited by members of the 'Group administrator'
* and the 'User administrator' group.</li>
* </ul>
*/
public class UserAccessControlProvider extends AbstractAccessControlProvider
implements UserConstants {
private static Logger log = LoggerFactory.getLogger(UserAccessControlProvider.class);
/**
* Constant for the name of the configuration option "anonymousId".
* The option is a flag indicating the name of the anonymous user id.
*/
public static final String PARAM_ANONYMOUS_ID = "anonymousId";
/**
* Constant for the name of the configuration option "anonymousAccess".
*/
public static final String PARAM_ANONYMOUS_ACCESS = "anonymousAccess";
private final AccessControlPolicy policy;
private String groupsPath;
private String usersPath;
private Principal userAdminGroup;
private Principal groupAdminGroup;
private String userAdminGroupPath;
private String groupAdminGroupPath;
private String administratorsGroupPath;
private boolean membersInProperty;
private String anonymousId;
private boolean anonymousAccess;
/**
*
*/
public UserAccessControlProvider() {
policy = new NamedAccessControlPolicyImpl("userPolicy");
}
//-------------------------------------------------< AccessControlUtils >---
/**
* Always returns false, since this ac provider does not use content stored
* in items to evaluate AC information.
*
* @see org.apache.jackrabbit.core.security.authorization.AccessControlUtils#isAcItem(Path)
*/
@Override
public boolean isAcItem(Path absPath) throws RepositoryException {
return false;
}
/**
* Always returns false, since this ac provider does not use content stored
* in items to evaluate AC information.
*
* @see org.apache.jackrabbit.core.security.authorization.AccessControlUtils#isAcItem(ItemImpl)
*/
@Override
public boolean isAcItem(ItemImpl item) throws RepositoryException {
return false;
}
//----------------------------------------------< AccessControlProvider >---
/**
* @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#init(Session, Map)
*/
@Override
public void init(Session systemSession, Map configuration) throws RepositoryException {
super.init(systemSession, configuration);
if (systemSession instanceof SessionImpl) {
SessionImpl sImpl = (SessionImpl) systemSession;
String userAdminName = (configuration.containsKey(USER_ADMIN_GROUP_NAME)) ? configuration.get(USER_ADMIN_GROUP_NAME).toString() : USER_ADMIN_GROUP_NAME;
String groupAdminName = (configuration.containsKey(GROUP_ADMIN_GROUP_NAME)) ? configuration.get(GROUP_ADMIN_GROUP_NAME).toString() : GROUP_ADMIN_GROUP_NAME;
// make sure the groups exist (and possibly create them).
UserManager uMgr = sImpl.getUserManager();
userAdminGroup = initGroup(uMgr, userAdminName);
if (userAdminGroup != null && userAdminGroup instanceof ItemBasedPrincipal) {
userAdminGroupPath = ((ItemBasedPrincipal) userAdminGroup).getPath();
}
groupAdminGroup = initGroup(uMgr, groupAdminName);
if (groupAdminGroup != null && groupAdminGroup instanceof ItemBasedPrincipal) {
groupAdminGroupPath = ((ItemBasedPrincipal) groupAdminGroup).getPath();
}
Principal administrators = initGroup(uMgr, SecurityConstants.ADMINISTRATORS_NAME);
if (administrators != null && administrators instanceof ItemBasedPrincipal) {
administratorsGroupPath = ((ItemBasedPrincipal) administrators).getPath();
}
usersPath = (uMgr instanceof UserManagerImpl) ? ((UserManagerImpl) uMgr).getUsersPath() : UserConstants.USERS_PATH;
groupsPath = (uMgr instanceof UserManagerImpl) ? ((UserManagerImpl) uMgr).getGroupsPath() : UserConstants.GROUPS_PATH;
membersInProperty = (!(uMgr instanceof UserManagerImpl)) || ((UserManagerImpl) uMgr).getGroupMembershipSplitSize() <= 0;
if (configuration.containsKey(PARAM_ANONYMOUS_ID)) {
anonymousId = (String) configuration.get(PARAM_ANONYMOUS_ID);
} else {
anonymousId = SecurityConstants.ANONYMOUS_ID;
}
if (configuration.containsKey(PARAM_ANONYMOUS_ACCESS)) {
anonymousAccess = Boolean.parseBoolean((String) configuration.get(PARAM_ANONYMOUS_ACCESS));
} else {
anonymousAccess = true;
}
} else {
throw new RepositoryException("SessionImpl (system session) expected.");
}
}
/**
* @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#getEffectivePolicies(org.apache.jackrabbit.spi.Path,org.apache.jackrabbit.core.security.authorization.CompiledPermissions)
*/
public AccessControlPolicy[] getEffectivePolicies(Path absPath, CompiledPermissions permissions) throws ItemNotFoundException, RepositoryException {
checkInitialized();
return new AccessControlPolicy[] {policy};
}
/**
* @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#getEffectivePolicies(java.util.Set, CompiledPermissions)
*/
public AccessControlPolicy[] getEffectivePolicies(Set<Principal> principals, CompiledPermissions permission) throws ItemNotFoundException, RepositoryException {
checkInitialized();
return new AccessControlPolicy[] {policy};
}
/**
* Always returns <code>null</code>.
*
* @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#getEditor(Session)
*/
public AccessControlEditor getEditor(Session session) {
checkInitialized();
// not editable at all: policy is always the default and cannot be
// changed using the JCR API.
return null;
}
/**
* @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#compilePermissions(Set)
*/
public CompiledPermissions compilePermissions(Set<Principal> principals) throws RepositoryException {
checkInitialized();
if (isAdminOrSystem(principals)) {
return getAdminPermissions();
} else {
if (!anonymousAccess && isAnonymous(principals)) {
return CompiledPermissions.NO_PERMISSION;
}
// determined the 'user' present in the given set of principals.
ItemBasedPrincipal userPrincipal = getUserPrincipal(principals);
NodeImpl userNode = getUserNode(userPrincipal);
if (userNode == null) {
// no 'user' within set of principals -> no permissions in the
// security workspace.
return CompiledPermissions.NO_PERMISSION;
} else {
return new CompiledPermissionsImpl(principals, userNode.getPath());
}
}
}
/**
* @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#canAccessRoot(Set)
*/
public boolean canAccessRoot(Set<Principal> principals) throws RepositoryException {
checkInitialized();
if (!anonymousAccess && isAnonymous(principals)) {
return false;
}
return true;
}
//------------------------------------------------------------< private >---
private ItemBasedPrincipal getUserPrincipal(Set<Principal> principals) {
try {
UserManager uMgr = session.getUserManager();
for (Principal p : principals) {
if (!(p instanceof Group) && p instanceof ItemBasedPrincipal
&& uMgr.getAuthorizable(p) != null) {
return (ItemBasedPrincipal) p;
}
}
} catch (RepositoryException e) {
// should never get here
log.error("Internal error while retrieving user principal", e.getMessage());
}
// none of the principals in the set is assigned to a User.
return null;
}
private NodeImpl getUserNode(ItemBasedPrincipal principal) {
NodeImpl userNode = null;
if (principal != null) {
try {
String path = principal.getPath();
userNode = (NodeImpl) session.getNode(path);
} catch (RepositoryException e) {
log.warn("Error while retrieving user node.", e.getMessage());
}
}
return userNode;
}
private Node getExistingNode(Path path) throws RepositoryException {
String absPath = session.getJCRPath(path.getNormalizedPath());
if (session.nodeExists(absPath)) {
return session.getNode(absPath);
} else if (session.propertyExists(absPath)) {
return session.getProperty(absPath).getParent();
} else {
String pPath = Text.getRelativeParent(absPath, 1);
while (!"/".equals(pPath)) {
if (session.nodeExists(pPath)) {
return session.getNode(pPath);
} else {
pPath = Text.getRelativeParent(pPath, 1);
}
}
throw new ItemNotFoundException("Unable to determine permissions: No item and no existing parent for target path " + absPath);
}
}
private static boolean containsGroup(Set<Principal> principals, Principal group) {
for (Iterator<Principal> it = principals.iterator(); it.hasNext() && group != null;) {
Principal p = it.next();
if (p.getName().equals(group.getName())) {
return true;
}
}
return false;
}
private static Principal initGroup(UserManager uMgr, String principalName) {
Principal prnc = new PrincipalImpl(principalName);
try {
Authorizable auth = uMgr.getAuthorizable(prnc);
if (auth == null) {
auth = uMgr.createGroup(prnc);
} else {
if (!auth.isGroup()) {
log.warn("Cannot create group '" + principalName + "'; User with that principal already exists.");
auth = null;
}
}
if (auth != null) {
return auth.getPrincipal();
}
} catch (RepositoryException e) {
// should never get here
log.error("Error while initializing user/group administrators", e.getMessage());
}
return null;
}
private boolean isAnonymous(Set<Principal> principals) {
for (Principal p : principals) {
if (p instanceof AnonymousPrincipal) {
return true;
} else if (p.getName().equals(anonymousId)) {
return true;
}
}
return false;
}
//--------------------------------------------------------< inner class >---
/**
*
*/
private class CompiledPermissionsImpl extends AbstractCompiledPermissions
implements SynchronousEventListener {
private final String userNodePath;
private final Set<Principal> principals;
protected CompiledPermissionsImpl(Set<Principal> principals, String userNodePath) throws RepositoryException {
this.userNodePath = userNodePath;
this.principals = principals;
int events = Event.PROPERTY_CHANGED | Event.PROPERTY_ADDED | Event.PROPERTY_REMOVED;
observationMgr.addEventListener(this, events, groupsPath, true, null, null, false);
}
private PrivilegeBits getPrivilegeBits(String... privNames) throws RepositoryException {
PrivilegeManagerImpl impl = getPrivilegeManagerImpl();
Privilege[] privs = new Privilege[privNames.length];
for (int i = 0; i < privNames.length; i++) {
privs[i] = impl.getPrivilege(privNames[i]);
}
return impl.getBits(privs);
}
//------------------------------------< AbstractCompiledPermissions >---
/**
* @see AbstractCompiledPermissions#buildResult(Path)
*/
@Override
protected Result buildResult(Path path) throws RepositoryException {
NodeImpl userNode = null;
try {
if (session.nodeExists(userNodePath)) {
userNode = (NodeImpl) session.getNode(userNodePath);
}
} catch (RepositoryException e) {
// ignore
}
if (userNode == null) {
// no Node corresponding to user for which the permissions are
// calculated -> no permissions/privileges.
log.debug("No node at " + userNodePath);
return Result.EMPTY;
}
// no explicit denied permissions:
int denies = Permission.NONE;
// default allow permission and default privileges
int allows = Permission.READ;
PrivilegeBits privs;
// Determine if for path, the set of privileges must be calculated:
// Generally, privileges can only be determined for existing nodes.
String jcrPath = session.getJCRPath(path.getNormalizedPath());
boolean calcPrivs = session.nodeExists(jcrPath);
if (calcPrivs) {
privs = getPrivilegeBits(Privilege.JCR_READ);
} else {
privs = PrivilegeBits.EMPTY;
}
if (Text.isDescendant(usersPath, jcrPath)) {
boolean isUserAdmin = containsGroup(principals, userAdminGroup);
/*
below the user-tree
- determine position of target relative to the editing user
- target may not be below an existing user but only below an
authorizable folder.
- determine if the editing user is user-admin
*/
NodeImpl node = (NodeImpl) getExistingNode(path);
if (node.isNodeType(NT_REP_AUTHORIZABLE_FOLDER)) {
// an authorizable folder -> must be user admin in order
// to have permission to write.
if (isUserAdmin) {
allows |= (Permission.ADD_NODE | Permission.REMOVE_NODE | Permission.SET_PROPERTY | Permission.REMOVE_PROPERTY | Permission.NODE_TYPE_MNGMT);
if (calcPrivs) {
// grant WRITE privilege
// note: ac-read/modification is not included
privs.add(getPrivilegeBits(PrivilegeRegistry.REP_WRITE));
}
}
} else {
// rep:User node or some other custom node below an existing user.
// as the authorizable folder doesn't allow other residual
// child nodes.
boolean editingOwnUser = node.isSame(userNode);
if (editingOwnUser) {
// user can only read && write his own props
allows |= (Permission.SET_PROPERTY | Permission.REMOVE_PROPERTY);
if (calcPrivs) {
privs.add(getPrivilegeBits(Privilege.JCR_MODIFY_PROPERTIES));
}
} else if (isUserAdmin) {
allows |= (Permission.ADD_NODE | Permission.REMOVE_NODE | Permission.SET_PROPERTY | Permission.REMOVE_PROPERTY | Permission.NODE_TYPE_MNGMT);
if (calcPrivs) {
// grant WRITE privilege
// note: ac-read/modification is not included
privs.add(getPrivilegeBits(PrivilegeRegistry.REP_WRITE));
}
} // else: normal user that isn't allowed to modify another user.
}
} else if (Text.isDescendant(groupsPath, jcrPath)) {
boolean isGroupAdmin = containsGroup(principals, groupAdminGroup);
/*
below group-tree:
- test if the user is group-administrator.
- make sure group-admin cannot modify user-admin or administrators
- ... and cannot remove itself.
*/
if (isGroupAdmin) {
if (!jcrPath.startsWith(administratorsGroupPath) &&
!jcrPath.startsWith(userAdminGroupPath)) {
if (jcrPath.equals(groupAdminGroupPath)) {
// no remove perm on group-admin node
allows |= (Permission.ADD_NODE | Permission.SET_PROPERTY | Permission.REMOVE_PROPERTY | Permission.NODE_TYPE_MNGMT);
if (calcPrivs) {
privs.add(getPrivilegeBits(Privilege.JCR_ADD_CHILD_NODES, Privilege.JCR_MODIFY_PROPERTIES, Privilege.JCR_NODE_TYPE_MANAGEMENT));
}
} else {
// complete write
allows |= (Permission.ADD_NODE | Permission.REMOVE_NODE | Permission.SET_PROPERTY | Permission.REMOVE_PROPERTY | Permission.NODE_TYPE_MNGMT);
if (calcPrivs) {
privs.add(getPrivilegeBits(PrivilegeRegistry.REP_WRITE));
}
}
}
}
} // else outside of user/group tree -> read only.
return new Result(allows, denies, privs, PrivilegeBits.EMPTY);
}
@Override
protected PrivilegeManagerImpl getPrivilegeManagerImpl() throws RepositoryException {
return (PrivilegeManagerImpl) ((JackrabbitWorkspace) session.getWorkspace()).getPrivilegeManager();
}
//--------------------------------------------< CompiledPermissions >---
/**
* @see CompiledPermissions#close()
*/
@Override
public void close() {
try {
observationMgr.removeEventListener(this);
} catch (RepositoryException e) {
log.error("Internal error: ", e.getMessage());
}
super.close();
}
/**
* @see CompiledPermissions#grants(Path, int)
*/
@Override
public boolean grants(Path absPath, int permissions) throws RepositoryException {
if (permissions == Permission.READ) {
return canReadAll();
}
// otherwise: retrieve from cache (or build)
return super.grants(absPath, permissions);
}
/**
* @see CompiledPermissions#canReadAll()
*/
@Override
public boolean canReadAll() throws RepositoryException {
// for consistency with 'grants(Path, int) this method only returns
// true if there exists a node for 'userNodePath'
return session.nodeExists(userNodePath);
}
/**
* @see CompiledPermissions#canRead(Path, ItemId)
*/
public boolean canRead(Path path, ItemId itemId) throws RepositoryException {
return canReadAll();
}
//--------------------------------------------------< EventListener >---
/**
* Event listener is only interested in changes of group-membership
* that effect the permission-evaluation.
*
* @see javax.jcr.observation.EventListener#onEvent(EventIterator)
*/
public void onEvent(EventIterator events) {
while (events.hasNext()) {
Event ev = events.nextEvent();
try {
String evPath = ev.getPath();
String repMembers = session.getJCRName(UserConstants.P_MEMBERS);
if (repMembers.equals(Text.getName(evPath))) {
// invalidate the cached results
clearCache();
// only need to clear the cache once. stop processing
break;
} else if (!membersInProperty) {
/* the affected property is not rep:Members and members are
stored in a tree structure (user manager configuration.
test if the parent node is of type rep:Members in order
to determine if any membership modification occurred.*/
Node parent = session.getNodeByIdentifier(ev.getIdentifier());
if (UserConstants.NT_REP_MEMBERS.equals(((NodeTypeImpl) parent.getPrimaryNodeType()).getQName())) {
clearCache();
}
} // else: not interested.
} catch (RepositoryException e) {
// should never get here
log.warn("Internal error ", e.getMessage());
clearCache();
}
}
}
}
}