/*
* 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.security.principal.ItemBasedPrincipal;
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.commons.iterator.RangeIteratorAdapter;
import org.apache.jackrabbit.core.NodeImpl;
import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.nodetype.NodeTypeImpl;
import org.apache.jackrabbit.core.security.principal.EveryonePrincipal;
import org.apache.jackrabbit.core.security.principal.PrincipalImpl;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.util.Text;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.Value;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.nodetype.PropertyDefinition;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/**
* AuthorizableImpl
*/
abstract class AuthorizableImpl implements Authorizable, UserConstants {
static final Logger log = LoggerFactory.getLogger(AuthorizableImpl.class);
final UserManagerImpl userManager;
private final NodeImpl node;
private int hashCode;
/**
* @param node The node this Authorizable is persisted to.
* @param userManager UserManager that created this Authorizable.
* @throws IllegalArgumentException if the given node isn't of node type
* {@link #NT_REP_AUTHORIZABLE}.
*/
protected AuthorizableImpl(NodeImpl node, UserManagerImpl userManager) {
this.node = node;
this.userManager = userManager;
}
//-------------------------------------------------------< Authorizable >---
/**
* Returns the unescaped name of the node that defines this <code>Authorizable</code>.
*
* @return the unescaped name of the node that defines this <code>Authorizable</code>.
* @see Authorizable#getID()
*/
public String getID() throws RepositoryException {
return Text.unescapeIllegalJcrChars(getNode().getName());
}
/**
* @see Authorizable#declaredMemberOf()
*/
public Iterator<Group> declaredMemberOf() throws RepositoryException {
return collectMembership(false);
}
/**
* @see Authorizable#memberOf()
*/
public Iterator<Group> memberOf() throws RepositoryException {
return collectMembership(true);
}
/**
* @see Authorizable#getPropertyNames()
*/
public Iterator<String> getPropertyNames() throws RepositoryException {
List<String> l = new ArrayList<String>();
for (PropertyIterator it = node.getProperties(); it.hasNext();) {
Property prop = it.nextProperty();
if (isAuthorizableProperty(prop, false)) {
l.add(prop.getName());
}
}
return l.iterator();
}
/**
* @see Authorizable#getPropertyNames(String)
*/
public Iterator<String> getPropertyNames(String relPath) throws RepositoryException {
Node n = node.getNode(relPath);
if (n.isSame(node)) {
// same as #getPropertyNames()
return getPropertyNames();
} else if (Text.isDescendant(node.getPath(), n.getPath())) {
List<String> l = new ArrayList<String>();
for (PropertyIterator it = n.getProperties(); it.hasNext();) {
Property prop = it.nextProperty();
if (isAuthorizableProperty(prop, false)) {
l.add(prop.getName());
}
}
return l.iterator();
} else {
throw new IllegalArgumentException("Relative path " + relPath + " refers to items outside of scope of authorizable " + getID());
}
}
/**
* @see #getProperty(String)
*/
public boolean hasProperty(String relPath) throws RepositoryException {
return node.hasProperty(relPath) && isAuthorizableProperty(node.getProperty(relPath), true);
}
/**
* @see #hasProperty(String)
* @see Authorizable#getProperty(String)
*/
public Value[] getProperty(String relPath) throws RepositoryException {
if (node.hasProperty(relPath)) {
Property prop = node.getProperty(relPath);
if (isAuthorizableProperty(prop, true)) {
if (prop.isMultiple()) {
return prop.getValues();
} else {
return new Value[]{prop.getValue()};
}
}
}
return null;
}
/**
* Sets the Value for the given name. If a value existed, it is replaced,
* if not it is created.
*
* @param relPath The relative path to the property or the property name.
* @param value The property value.
* @throws RepositoryException If the specified name defines a property
* that needs to be modified by this user API or setting the corresponding
* JCR property fails.
* @see Authorizable#setProperty(String, Value)
*/
public synchronized void setProperty(String relPath, Value value) throws RepositoryException {
String name = Text.getName(relPath);
String intermediate = (relPath.equals(name)) ? null : Text.getRelativeParent(relPath, 1);
checkProtectedProperty(name);
try {
Node n = getOrCreateTargetNode(intermediate);
// check if the property has already been created as multi valued
// property before -> in this case remove in order to avoid
// ValueFormatException.
if (n.hasProperty(name)) {
Property p = n.getProperty(name);
if (p.isMultiple()) {
p.remove();
}
}
n.setProperty(name, value);
if (userManager.isAutoSave()) {
node.save();
}
} catch (RepositoryException e) {
log.debug("Failed to set Property " + name + " for " + this, e);
node.refresh(false);
throw e;
}
}
/**
* Sets the Value[] for the given name. If a value existed, it is replaced,
* if not it is created.
*
* @param relPath The relative path to the property or the property name.
* @param values The property values.
* @throws RepositoryException If the specified name defines a property
* that needs to be modified by this user API or setting the corresponding
* JCR property fails.
* @see Authorizable#setProperty(String, Value[])
*/
public synchronized void setProperty(String relPath, Value[] values) throws RepositoryException {
String name = Text.getName(relPath);
String intermediate = (relPath.equals(name)) ? null : Text.getRelativeParent(relPath, 1);
checkProtectedProperty(name);
try {
Node n = getOrCreateTargetNode(intermediate);
// check if the property has already been created as single valued
// property before -> in this case remove in order to avoid
// ValueFormatException.
if (n.hasProperty(name)) {
Property p = n.getProperty(name);
if (!p.isMultiple()) {
p.remove();
}
}
n.setProperty(name, values);
if (userManager.isAutoSave()) {
node.save();
}
} catch (RepositoryException e) {
log.debug("Failed to set Property " + name + " for " + this, e);
node.refresh(false);
throw e;
}
}
/**
* @see Authorizable#removeProperty(String)
*/
public synchronized boolean removeProperty(String relPath) throws RepositoryException {
String name = Text.getName(relPath);
checkProtectedProperty(name);
try {
if (node.hasProperty(relPath)) {
Property p = node.getProperty(relPath);
if (isAuthorizableProperty(p, true)) {
p.remove();
if (userManager.isAutoSave()) {
node.save();
}
return true;
}
}
// no such property or wasn't a property of this authorizable.
return false;
} catch (RepositoryException e) {
log.debug("Failed to remove Property " + relPath + " from " + this, e);
node.refresh(false);
throw e;
}
}
/**
* @see Authorizable#remove()
*/
public synchronized void remove() throws RepositoryException {
// don't allow for removal of the administrator even if the executing
// session has all permissions.
if (!isGroup() && ((User) this).isAdmin()) {
throw new RepositoryException("The administrator cannot be removed.");
}
Session s = getSession();
userManager.onRemove(this);
node.remove();
if (userManager.isAutoSave()) {
s.save();
}
}
/**
* @see Authorizable#getPath()
*/
public String getPath() throws UnsupportedRepositoryOperationException, RepositoryException {
return userManager.getPath(node);
}
//-------------------------------------------------------------< Object >---
@Override
public int hashCode() {
if (hashCode == 0) {
try {
StringBuilder sb = new StringBuilder();
sb.append(isGroup() ? "group:" : "user:");
sb.append(getSession().getWorkspace().getName());
sb.append(":");
sb.append(node.getIdentifier());
hashCode = sb.toString().hashCode();
} catch (RepositoryException e) {
}
}
return hashCode;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof AuthorizableImpl) {
AuthorizableImpl otherAuth = (AuthorizableImpl) obj;
try {
return isGroup() == otherAuth.isGroup() && node.isSame(otherAuth.node);
} catch (RepositoryException e) {
// should not occur -> return false in this case.
}
}
return false;
}
@Override
public String toString() {
try {
String typeStr = (isGroup()) ? "Group '" : "User '";
return new StringBuilder().append(typeStr).append(getID()).append("'").toString();
} catch (RepositoryException e) {
return super.toString();
}
}
//--------------------------------------------------------------------------
/**
* @return node The underlying <code>Node</code> object.
*/
NodeImpl getNode() {
return node;
}
SessionImpl getSession() throws RepositoryException {
return (SessionImpl) node.getSession();
}
String getPrincipalName() throws RepositoryException {
// principal name is mandatory property -> no check required.
return node.getProperty(P_PRINCIPAL_NAME).getString();
}
boolean isEveryone() throws RepositoryException {
return isGroup() && EveryonePrincipal.NAME.equals(getPrincipalName());
}
private Iterator<Group> collectMembership(boolean includeIndirect) throws RepositoryException {
Collection<String> groupNodeIds;
MembershipCache cache = userManager.getMembershipCache();
String nid = node.getIdentifier();
final long t0 = System.nanoTime();
boolean collect = false;
if (node.getSession().hasPendingChanges()) {
collect = true;
// avoid retrieving outdated cache entries or filling the cache with
// invalid data due to group-membership changes pending on the
// current session.
// this is mainly for backwards compatibility reasons (no cache present)
// where transient changes (in non-autosave mode) were reflected to the
// editing session (see JCR-2713)
Session session = node.getSession();
groupNodeIds = (includeIndirect) ? cache.collectMembership(nid, session) : cache.collectDeclaredMembership(nid, session);
} else {
// retrieve cached membership. there are no pending changes.
groupNodeIds = (includeIndirect) ? cache.getMemberOf(nid) : cache.getDeclaredMemberOf(nid);
}
final long t1 = System.nanoTime();
Set<Group> groups = new HashSet<Group>(groupNodeIds.size());
for (String identifier : groupNodeIds) {
try {
NodeImpl n = (NodeImpl) getSession().getNodeByIdentifier(identifier);
Group group = userManager.createGroup(n);
groups.add(group);
} catch (RepositoryException e) {
// group node doesn't exist or cannot be read -> ignore.
}
}
final long t2 = System.nanoTime();
if (log.isDebugEnabled()) {
log.debug("Collected {} {} group ids for [{}] in {}us, loaded {} groups in {}us (collect={}, cachesize={})", new Object[]{
groupNodeIds.size(),
includeIndirect ? "all" : "declared",
getID(),
(t1-t0) / 1000,
groups.size(),
(t2-t1) / 1000,
collect,
cache.getSize()
});
}
return new RangeIteratorAdapter(groups.iterator(), groups.size());
}
/**
* Returns true if the given property of the authorizable node is one of the
* non-protected properties defined by the rep:Authorizable node type or a
* some other descendant of the authorizable node.
*
* @param prop Property to be tested.
* @param verifyAncestor If true the property is tested to be a descendant
* of the node of this authorizable; otherwise it is expected that this
* test has been executed by the caller.
* @return <code>true</code> if the given property is defined
* by the rep:authorizable node type or one of it's sub-node types;
* <code>false</code> otherwise.
* @throws RepositoryException If the property definition cannot be retrieved.
*/
private boolean isAuthorizableProperty(Property prop, boolean verifyAncestor) throws RepositoryException {
if (verifyAncestor && !Text.isDescendant(node.getPath(), prop.getPath())) {
log.debug("Attempt to access property outside of authorizable scope.");
return false;
}
PropertyDefinition def = prop.getDefinition();
if (def.isProtected()) {
return false;
} else if (node.isSame(prop.getParent())) {
NodeTypeImpl declaringNt = (NodeTypeImpl) prop.getDefinition().getDeclaringNodeType();
return declaringNt.isNodeType(UserConstants.NT_REP_AUTHORIZABLE);
} else {
// another non-protected property somewhere in the subtree of this
// authorizable node -> is a property that can be set using #setProperty.
return true;
}
}
/**
* Test if the JCR property to be modified/removed is one of the
* following that has a special meaning and must be altered using this
* user API:
* <ul>
* <li>rep:principalName</li>
* <li>rep:members</li>
* <li>rep:impersonators</li>
* <li>rep:password</li>
* </ul>
* Those properties are 'protected' in their property definition. This
* method is a simple utility in order to save the extra effort to modify
* the props just to find out later that they are in fact protected.
*
* @param propertyName Name of the property.
* @return true if the property with the given name represents a protected
* user/group property that needs to be changed through the API.
* @throws RepositoryException If the specified name is not valid.
*/
private boolean isProtectedProperty(String propertyName) throws RepositoryException {
Name pName = getSession().getQName(propertyName);
return P_PRINCIPAL_NAME.equals(pName)
|| P_MEMBERS.equals(pName)
|| P_IMPERSONATORS.equals(pName)
|| P_DISABLED.equals(pName)
|| P_PASSWORD.equals(pName);
}
/**
* Throws ConstraintViolationException if {@link #isProtectedProperty(String)}
* returns <code>true</code>.
*
* @param propertyName Name of the property.
* @throws ConstraintViolationException If the property is protected according
* to {@link #isProtectedProperty(String)}.
* @throws RepositoryException If another error occurs.
*/
private void checkProtectedProperty(String propertyName) throws ConstraintViolationException, RepositoryException {
if (isProtectedProperty(propertyName)) {
throw new ConstraintViolationException("Attempt to modify protected property " + propertyName + " of " + this);
}
}
/**
*
* @param relPath A relative path.
* @return The corresponding node.
* @throws RepositoryException If an error occurs.
*/
private Node getOrCreateTargetNode(String relPath) throws RepositoryException {
Node n;
if (relPath != null) {
if (node.hasNode(relPath)) {
n = node.getNode(relPath);
} else {
n = node;
for (String segment : Text.explode(relPath, '/')) {
if (n.hasNode(segment)) {
n = n.getNode(segment);
} else {
n = n.addNode(segment);
}
}
}
if (!Text.isDescendantOrEqual(node.getPath(), n.getPath())) {
node.refresh(false);
throw new RepositoryException("Relative path " + relPath + " outside of scope of " + this);
}
} else {
n = node;
}
return n;
}
//--------------------------------------------------------------------------
/**
*
*/
class NodeBasedPrincipal extends PrincipalImpl implements ItemBasedPrincipal {
/**
* @param name for the principal
*/
NodeBasedPrincipal(String name) {
super(name);
}
NodeId getNodeId() {
return node.getNodeId();
}
//---------------------------------------------< ItemBasedPrincipal >---
/**
* Method revealing the path to the Node that represents the
* Authorizable this principal is created for.
*
* @return The path of the underlying node.
* @see ItemBasedPrincipal#getPath()
*/
public String getPath() throws RepositoryException {
return node.getPath();
}
}
}