/** * Copyright (C) 2010-2017 Structr GmbH * * This file is part of Structr <http://structr.org>. * * Structr is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * Structr is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Structr. If not, see <http://www.gnu.org/licenses/>. */ package org.structr.core.entity; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; import org.apache.chemistry.opencmis.commons.data.Ace; import org.apache.chemistry.opencmis.commons.data.AllowableActions; import org.apache.chemistry.opencmis.commons.enums.BaseTypeId; import org.apache.chemistry.opencmis.commons.enums.PropertyType; import org.apache.commons.codec.digest.DigestUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.structr.api.Predicate; import org.structr.api.config.Settings; import org.structr.api.graph.Direction; import org.structr.api.graph.Node; import org.structr.api.graph.PropertyContainer; import org.structr.api.graph.Relationship; import org.structr.api.graph.RelationshipType; import org.structr.api.index.Index; import org.structr.api.util.FixedSizeCache; import org.structr.api.util.Iterables; import org.structr.cmis.CMISInfo; import org.structr.cmis.common.CMISExtensionsData; import org.structr.cmis.common.StructrItemActions; import org.structr.cmis.info.CMISDocumentInfo; import org.structr.cmis.info.CMISFolderInfo; import org.structr.cmis.info.CMISItemInfo; import org.structr.cmis.info.CMISPolicyInfo; import org.structr.cmis.info.CMISRelationshipInfo; import org.structr.cmis.info.CMISSecondaryInfo; import org.structr.common.AccessControllable; import org.structr.common.GraphObjectComparator; import org.structr.common.IdSorter; import org.structr.common.Permission; import org.structr.common.PermissionPropagation; import org.structr.common.PermissionResolutionMask; import org.structr.common.PropertyView; import org.structr.common.SecurityContext; import org.structr.common.ValidationHelper; import org.structr.common.View; import org.structr.common.error.ErrorBuffer; import org.structr.common.error.FrameworkException; import org.structr.common.error.InternalSystemPropertyToken; import org.structr.common.error.NullArgumentToken; import org.structr.common.error.ReadOnlyPropertyToken; import org.structr.core.GraphObject; import org.structr.core.IterableAdapter; import org.structr.core.Services; import org.structr.core.app.App; import org.structr.core.app.StructrApp; import org.structr.core.converter.PropertyConverter; import org.structr.core.entity.relationship.Ownership; import org.structr.core.entity.relationship.PrincipalOwnsNode; import org.structr.core.graph.ModificationQueue; import org.structr.core.graph.NodeInterface; import org.structr.core.graph.NodeRelationshipStatisticsCommand; import org.structr.core.graph.NodeService; import org.structr.core.graph.RelationshipFactory; import org.structr.core.graph.RelationshipInterface; import org.structr.core.property.FunctionProperty; import org.structr.core.property.PropertyKey; import org.structr.core.property.PropertyMap; import org.structr.core.script.Scripting; import org.structr.schema.action.ActionContext; import org.structr.schema.action.Function; //~--- classes ---------------------------------------------------------------- /** * Abstract base class for all node entities in structr. * * * */ public abstract class AbstractNode implements NodeInterface, AccessControllable, CMISInfo, CMISItemInfo { private static final int permissionResolutionMaxLevel = Settings.ResolutionDepth.getValue(); private static final Logger logger = LoggerFactory.getLogger(AbstractNode.class.getName()); private static final FixedSizeCache<String, Object> relationshipTemplateInstanceCache = new FixedSizeCache<>(1000); private static final Map<Long, Map<Long, PermissionResolutionResult>> globalPermissionResolutionCache = new HashMap<>(); public static final View defaultView = new View(AbstractNode.class, PropertyView.Public, id, type); public static final View uiView = new View(AbstractNode.class, PropertyView.Ui, id, name, owner, type, createdBy, deleted, hidden, createdDate, lastModifiedDate, visibleToPublicUsers, visibleToAuthenticatedUsers, visibilityStartDate, visibilityEndDate ); public boolean internalSystemPropertiesUnlocked = false; private Relationship rawPathSegment = null; private boolean readOnlyPropertiesUnlocked = false; private boolean isCreation = false; protected String cachedUuid = null; protected SecurityContext securityContext = null; protected Principal cachedOwnerNode = null; protected Class entityType = null; protected Node dbNode = null; public AbstractNode() { } public AbstractNode(SecurityContext securityContext, final Node dbNode, final Class entityType) { init(securityContext, dbNode, entityType, false); } @Override public void onNodeCreation() { } @Override public void onNodeInstantiation(final boolean isCreation) { this.cachedUuid = getProperty(GraphObject.id); } @Override public void onNodeDeletion() { } @Override public final void init(final SecurityContext securityContext, final Node dbNode, final Class entityType, final boolean isCreation) { this.isCreation = isCreation; this.dbNode = dbNode; this.entityType = entityType; this.securityContext = securityContext; } @Override public final void setSecurityContext(SecurityContext securityContext) { this.securityContext = securityContext; } @Override public final SecurityContext getSecurityContext() { return securityContext; } @Override public boolean equals(final Object o) { if (o == null) { return false; } if (!(o instanceof AbstractNode)) { return false; } return (Integer.valueOf(this.hashCode()).equals(o.hashCode())); } @Override public int hashCode() { if (this.dbNode == null) { return (super.hashCode()); } return Long.valueOf(dbNode.getId()).hashCode(); } @Override public int compareTo(final Object other) { if (other instanceof AbstractNode) { final AbstractNode node = (AbstractNode)other; final String _name = getName(); if (_name == null) { return -1; } final String nodeName = node.getName(); if (nodeName == null) { return -1; } return _name.compareTo(nodeName); } if (other instanceof String) { return getUuid().compareTo((String)other); } if (other == null) { throw new NullPointerException(); } throw new IllegalStateException("Cannot compare " + this + " to " + other); } /** * Implement standard toString() method */ @Override public String toString() { return getUuid(); } /** * Can be used to permit the setting of a read-only property once. The * lock will be restored automatically after the next setProperty * operation. This method exists to prevent automatic set methods from * setting a read-only property while allowing a manual set method to * override this default behaviour. */ @Override public final void unlockReadOnlyPropertiesOnce() { this.readOnlyPropertiesUnlocked = true; } /** * Can be used to permit the setting of a system property once. The * lock will be restored automatically after the next setProperty * operation. This method exists to prevent automatic set methods from * setting a system property while allowing a manual set method to * override this default behaviour. */ @Override public final void unlockSystemPropertiesOnce() { this.internalSystemPropertiesUnlocked = true; unlockReadOnlyPropertiesOnce(); } @Override public final void removeProperty(final PropertyKey key) throws FrameworkException { if (!isGranted(Permission.write, securityContext)) { throw new FrameworkException(403, "Modification not permitted."); } if (this.dbNode != null) { if (key == null) { logger.error("Tried to set property with null key (action was denied)"); return; } // check for read-only properties if (key.isReadOnly()) { // allow super user to set read-only properties if (readOnlyPropertiesUnlocked || securityContext.isSuperUser()) { // permit write operation once and // lock read-only properties again internalSystemPropertiesUnlocked = false; } else { throw new FrameworkException(404, "Property " + key.jsonName() + " is read-only", new ReadOnlyPropertyToken(getType(), key)); } } // check for system properties - cannot be overriden with super-user rights if (key.isSystemInternal()) { // allow super user to set read-only properties if (internalSystemPropertiesUnlocked) { // permit write operation once and // lock read-only properties again internalSystemPropertiesUnlocked = false; } else { throw new FrameworkException(404, "Property " + key.jsonName() + " is read-only", new InternalSystemPropertyToken(getType(), key)); } } dbNode.removeProperty(key.dbName()); // remove from index removeFromIndex(key); } } @Override public final PropertyKey getDefaultSortKey() { return AbstractNode.name; } @Override public final String getDefaultSortOrder() { return GraphObjectComparator.ASCENDING; } @Override public final String getType() { return getProperty(AbstractNode.type); } @Override public final PropertyContainer getPropertyContainer() { return dbNode; } /** * Get name from underlying db node * * If name is null, return node id as fallback */ @Override public final String getName() { String name = getProperty(AbstractNode.name); if (name == null) { name = getNodeId().toString(); } return name; } /** * Get id from underlying db */ @Override public final long getId() { if (dbNode == null) { return -1; } return dbNode.getId(); } @Override public final String getUuid() { if (cachedUuid == null) { cachedUuid = getProperty(GraphObject.id); } return cachedUuid; } public final Long getNodeId() { return getId(); } public final String getIdString() { return Long.toString(getId()); } /** * Indicates whether this node is visible to public users. * * @return whether this node is visible to public users */ public final boolean getVisibleToPublicUsers() { return getProperty(visibleToPublicUsers); } /** * Indicates whether this node is visible to authenticated users. * * @return whether this node is visible to authenticated users */ public final boolean getVisibleToAuthenticatedUsers() { return getProperty(visibleToPublicUsers); } /** * Indicates whether this node is hidden. * * @return whether this node is hidden */ public final boolean getHidden() { return getProperty(hidden); } /** * Indicates whether this node is deleted. * * @return whether this node is deleted */ public final boolean getDeleted() { return getProperty(deleted); } /** * Returns the property set for the given view as an Iterable. * * @param propertyView * @return the property set for the given view */ @Override public Iterable<PropertyKey> getPropertyKeys(final String propertyView) { // check for custom view in content-type field if (securityContext != null && securityContext.hasCustomView()) { final Set<PropertyKey> keys = new LinkedHashSet<>(StructrApp.getConfiguration().getPropertySet(entityType, propertyView)); final Set<String> customView = securityContext.getCustomView(); for (Iterator<PropertyKey> it = keys.iterator(); it.hasNext();) { if (!customView.contains(it.next().jsonName())) { it.remove(); } } return keys; } // this is the default if no application/json; properties=[...] content-type header is present on the request return StructrApp.getConfiguration().getPropertySet(entityType, propertyView); } /** * Returns the (converted, validated, transformed, etc.) property for * the given property key. * * @param <T> * @param key the property key to retrieve the value for * @return the converted, validated, transformed property value */ @Override public <T> T getProperty(final PropertyKey<T> key) { return getProperty(key, null); } @Override public <T> T getProperty(final PropertyKey<T> key, final Predicate<GraphObject> predicate) { return getProperty(key, true, predicate); } private <T> T getProperty(final PropertyKey<T> key, boolean applyConverter, final Predicate<GraphObject> predicate) { // early null check, this should not happen... if (key == null || key.dbName() == null) { return null; } return key.getProperty(securityContext, this, applyConverter, predicate); } public final String getPropertyMD5(final PropertyKey key) { Object value = getProperty(key); if (value instanceof String) { return DigestUtils.md5Hex((String) value); } else if (value instanceof byte[]) { return DigestUtils.md5Hex((byte[]) value); } logger.warn("Could not create MD5 hex out of value {}", value); return null; } /** * Returns the property value for the given key as a Comparable * * @param key the property key to retrieve the value for * @return the property value for the given key as a Comparable */ @Override public final <T> Comparable getComparableProperty(final PropertyKey<T> key) { if (key != null) { final T propertyValue = getProperty(key); // check property converter PropertyConverter<T, ?> converter = key.databaseConverter(securityContext, this); if (converter != null) { try { return converter.convertForSorting(propertyValue); } catch (Throwable t) { logger.warn("Unable to convert property {} of type {}: {}", new Object[]{ key.dbName(), getClass().getSimpleName(), t.getMessage() }); logger.warn("", t); } } // conversion failed, may the property value itself is comparable if (propertyValue instanceof Comparable) { return (Comparable) propertyValue; } // last try: convertFromInput to String to make comparable if (propertyValue != null) { return propertyValue.toString(); } } return null; } /** * Returns the property value for the given key as a Iterable * * @param propertyKey the property key to retrieve the value for * @return the property value for the given key as a Iterable */ public final Iterable getIterableProperty(final PropertyKey<? extends Iterable> propertyKey) { return (Iterable) getProperty(propertyKey); } /** * Returns a list of related nodes for which a modification propagation * is configured via the relationship. Override this method to return a * set of nodes that should receive propagated modifications. * * @return a set of nodes to which modifications should be propagated */ public Set<AbstractNode> getNodesForModificationPropagation() { return null; } /** * Returns database node. * * @return the database node */ @Override public final Node getNode() { return dbNode; } @Override public final <R extends AbstractRelationship> Iterable<R> getRelationships() { return new IterableAdapter<>(dbNode.getRelationships(), new RelationshipFactory<R>(securityContext)); } @Override public final <A extends NodeInterface, B extends NodeInterface, S extends Source, T extends Target, R extends Relation<A, B, S, T>> Iterable<R> getRelationships(final Class<R> type) { final RelationshipFactory<R> factory = new RelationshipFactory<>(securityContext); final R template = getRelationshipForType(type); final Direction direction = template.getDirectionForType(entityType); final RelationshipType relType = template; return new IterableAdapter<>(dbNode.getRelationships(direction, relType), factory); } @Override public final <A extends NodeInterface, B extends NodeInterface, T extends Target, R extends Relation<A, B, OneStartpoint<A>, T>> R getIncomingRelationship(final Class<R> type) { final RelationshipFactory<R> factory = new RelationshipFactory<>(securityContext); final R template = getRelationshipForType(type); final Relationship relationship = template.getSource().getRawSource(securityContext, dbNode, null); if (relationship != null) { return factory.adapt(relationship); } return null; } @Override public final <A extends NodeInterface, B extends NodeInterface, T extends Target, R extends Relation<A, B, ManyStartpoint<A>, T>> Iterable<R> getIncomingRelationships(final Class<R> type) { final RelationshipFactory<R> factory = new RelationshipFactory<>(securityContext); final R template = getRelationshipForType(type); return new IterableAdapter<>(new IdSorter<>(template.getSource().getRawSource(securityContext, dbNode, null)), factory); } @Override public final <A extends NodeInterface, B extends NodeInterface, S extends Source, R extends Relation<A, B, S, OneEndpoint<B>>> R getOutgoingRelationship(final Class<R> type) { final RelationshipFactory<R> factory = new RelationshipFactory<>(securityContext); final R template = getRelationshipForType(type); final Relationship relationship = template.getTarget().getRawSource(securityContext, dbNode, null); if (relationship != null) { return factory.adapt(relationship); } return null; } @Override public final <A extends NodeInterface, B extends NodeInterface, S extends Source, R extends Relation<A, B, S, ManyEndpoint<B>>> Iterable<R> getOutgoingRelationships(final Class<R> type) { final RelationshipFactory<R> factory = new RelationshipFactory<>(securityContext); final R template = getRelationshipForType(type); return new IterableAdapter<>(new IdSorter<>(template.getTarget().getRawSource(securityContext, dbNode, null)), factory); } @Override public final <R extends AbstractRelationship> Iterable<R> getIncomingRelationships() { return new IterableAdapter<>(new IdSorter<>(dbNode.getRelationships(Direction.INCOMING)), new RelationshipFactory<R>(securityContext)); } @Override public final <R extends AbstractRelationship> Iterable<R> getOutgoingRelationships() { return new IterableAdapter<>(new IdSorter<>(dbNode.getRelationships(Direction.OUTGOING)), new RelationshipFactory<R>(securityContext)); } @Override public final <R extends AbstractRelationship> Iterable<R> getRelationshipsAsSuperUser() { return new IterableAdapter<>(dbNode.getRelationships(), new RelationshipFactory<R>(SecurityContext.getSuperUserInstance())); } protected final <A extends NodeInterface, B extends NodeInterface, T extends Target, R extends Relation<A, B, ManyStartpoint<A>, T>> Iterable<R> getIncomingRelationshipsAsSuperUser(final Class<R> type) { final RelationshipFactory<R> factory = new RelationshipFactory<>(SecurityContext.getSuperUserInstance()); final R template = getRelationshipForType(type); return new IterableAdapter<>(template.getSource().getRawSource(SecurityContext.getSuperUserInstance(), dbNode, null), factory); } protected final <A extends NodeInterface, B extends NodeInterface, T extends Target, R extends Relation<A, B, ManyStartpoint<A>, T>> R getOutgoingRelationshipAsSuperUser(final Class<R> type) { final RelationshipFactory<R> factory = new RelationshipFactory<>(SecurityContext.getSuperUserInstance()); final R template = getRelationshipForType(type); final Relationship relationship = template.getSource().getRawTarget(SecurityContext.getSuperUserInstance(), dbNode, null); if (relationship != null) { return factory.adapt(relationship); } return null; } protected final <A extends NodeInterface, B extends NodeInterface, S extends Source, T extends Target, R extends Relation<A, B, S, T>> Iterable<R> getRelationshipsAsSuperUser(final Class<R> type) { final RelationshipFactory<R> factory = new RelationshipFactory<>(SecurityContext.getSuperUserInstance()); final R template = getRelationshipForType(type); final Direction direction = template.getDirectionForType(entityType); final RelationshipType relType = template; return new IterableAdapter<>(dbNode.getRelationships(direction, relType), factory); } /** * Return statistical information on all relationships of this node * * @param dir * @return number of relationships */ public final Map<String, Long> getRelationshipInfo(final Direction dir) throws FrameworkException { return StructrApp.getInstance(securityContext).command(NodeRelationshipStatisticsCommand.class).execute(this, dir); } /** * Returns the owner node of this node, following an INCOMING OWNS * relationship. * * @return the owner node of this node */ @Override public final Principal getOwnerNode() { if (cachedOwnerNode == null) { final Ownership ownership = getIncomingRelationshipAsSuperUser(PrincipalOwnsNode.class); if (ownership != null) { Principal principal = ownership.getSourceNode(); cachedOwnerNode = (Principal) principal; } } return cachedOwnerNode; } /** * Returns the database ID of the owner node of this node. * * @return the database ID of the owner node of this node */ public final Long getOwnerId() { return getOwnerNode().getId(); } protected <A extends NodeInterface, B extends NodeInterface, T extends Target, R extends Relation<A, B, OneStartpoint<A>, T>> R getIncomingRelationshipAsSuperUser(final Class<R> type) { final RelationshipFactory<R> factory = new RelationshipFactory<>(SecurityContext.getSuperUserInstance()); final R template = getRelationshipForType(type); final Relationship relationship = template.getSource().getRawSource(SecurityContext.getSuperUserInstance(), dbNode, null); if (relationship != null) { return factory.adapt(relationship); } return null; } /** * Return true if this node has a relationship of given type and * direction. * * @param <A> * @param <B> * @param <S> * @param <T> * @param type * @return relationships */ public final <A extends NodeInterface, B extends NodeInterface, S extends Source, T extends Target> boolean hasRelationship(final Class<? extends Relation<A, B, S, T>> type) { return this.getRelationships(type).iterator().hasNext(); } public final <A extends NodeInterface, B extends NodeInterface, S extends Source, T extends Target, R extends Relation<A, B, S, T>> boolean hasIncomingRelationships(final Class<R> type) { return getRelationshipForType(type).getSource().hasElements(securityContext, dbNode, null); } public final <A extends NodeInterface, B extends NodeInterface, S extends Source, T extends Target, R extends Relation<A, B, S, T>> boolean hasOutgoingRelationships(final Class<R> type) { return getRelationshipForType(type).getTarget().hasElements(securityContext, dbNode, null); } // ----- interface AccessControllable ----- @Override public final boolean isGranted(final Permission permission, final SecurityContext context) { // super user can do everything if (context != null && context.isSuperUser()) { return true; } Principal accessingUser = null; if (context != null) { accessingUser = context.getUser(false); } final boolean doLog = securityContext.hasParameter("logPermissionResolution"); return isGranted(permission, accessingUser, new PermissionResolutionMask(), 0, new AlreadyTraversed(), true, doLog); } private boolean isGranted(final Permission permission, final Principal accessingUser, final PermissionResolutionMask mask, final int level, final AlreadyTraversed alreadyTraversed, final boolean resolvePermissions, final boolean doLog) { if (level > 100) { logger.warn("Aborting recursive permission resolution because of recursion level > 100, this is quite likely an infinite loop."); return false; } // use quick checks for maximum performance if (isCreation && (accessingUser == null || accessingUser.equals(this) || accessingUser.equals(getOwnerNode()) ) ) { return true; } // this includes SuperUser if (accessingUser != null && accessingUser.isAdmin()) { return true; } // allow accessingUser to access itself, but not parents etc. if (this.equals(accessingUser) && (level == 0 || (permission.equals(Permission.read) && level > 0))) { return true; } // check owner final Principal _owner = getOwnerNode(); final boolean hasOwner = (_owner != null); // allow full access for nodes without owner // (covered by ResourceAccess objects) if (!hasOwner && Services.getPermissionsForOwnerlessNodes().contains(permission)) { if (accessingUser != null && isVisibleToAuthenticatedUsers()) { return true; } if (accessingUser == null && isVisibleToPublicUsers()) { return true; } } // node has an owner, deny anonymous access if (hasOwner && accessingUser == null) { return false; } if (accessingUser != null) { // owner is always allowed to do anything with its nodes if (hasOwner && accessingUser.equals(_owner)) { return true; } final Security security = getSecurityRelationship(accessingUser); if (security != null && security.isAllowed(permission)) { return true; } // Check permissions from domain relationships if (resolvePermissions) { final Queue<BFSInfo> bfsNodes = new LinkedList<>(); final BFSInfo root = new BFSInfo(null, this); // add initial element bfsNodes.add(root); do { final BFSInfo info = bfsNodes.poll(); if (info != null && info.level < permissionResolutionMaxLevel) { final Boolean value = info.node.getPermissionResolutionResult(accessingUser.getId(), permission); if (value != null) { // returning immediately if (Boolean.TRUE.equals(value)) { // do backtracking backtrack(info, accessingUser.getId(), permission, true, 0, doLog); return true; } } else { if (info.node.hasEffectivePermissions(info, accessingUser, permission, mask, level, alreadyTraversed, bfsNodes, doLog)) { // do backtracking backtrack(info, accessingUser.getId(), permission, true, 0, doLog); return true; } } } } while (!bfsNodes.isEmpty()); // do backtracking backtrack(root, accessingUser.getId(), permission, false, 0, doLog); } // Last: recursively check possible parent principals for (Principal parent : accessingUser.getParents()) { if (isGranted(permission, parent, mask, level+1, alreadyTraversed, false, doLog)) { return true; } } } return false; } private void backtrack(final BFSInfo info, final long principalId, final Permission permission, final boolean value, final int level, final boolean doLog) { if (doLog) { if (level == 0) { if (value) { System.out.print("granted: "); } else { System.out.print("denied: "); } } System.out.print(info.node.getType() + " (" + info.node.getUuid() + ") --> "); } info.node.storePermissionResolutionResult(principalId, permission, value); // go to parent(s) if (info.parent != null) { backtrack(info.parent, principalId, permission, value, level+1, doLog); } if (doLog && level == 0) { System.out.println(); } } private boolean hasEffectivePermissions(final BFSInfo parent, final Principal principal, final Permission permission, final PermissionResolutionMask mask, final int level, final AlreadyTraversed alreadyTraversed, final Queue<BFSInfo> bfsNodes, final boolean doLog) { // check nodes here to avoid circles in permission-propagating relationships if (alreadyTraversed.contains("Node", dbNode.getId())) { return false; } for (final Class<Relation> propagatingType : SchemaRelationshipNode.getPropagatingRelationshipTypes()) { final Relation template = getRelationshipForType(propagatingType); final Direction direction = template.getDirectionForType(entityType); // skip relationship type if it is not applicable for the current node type if (Direction.BOTH.equals(direction)) { continue; } // iterate over list of relationships final Iterable<Relation> iterable = getRelationshipsAsSuperUser(propagatingType); for (final Relation source : iterable) { if (source instanceof PermissionPropagation) { final PermissionPropagation perm = (PermissionPropagation)source; final RelationshipInterface rel = (RelationshipInterface)source; // check propagation direction vs. evaluation direction if (propagationAllowed(this, rel, perm.getPropagationDirection(), doLog)) { applyCurrentStep(perm, mask); if (mask.allowsPermission(permission)) { final AbstractNode otherNode = (AbstractNode)rel.getOtherNode(this); if (otherNode.isGranted(permission, principal, mask, level+1, alreadyTraversed, false, doLog)) { otherNode.storePermissionResolutionResult(principal.getId(), permission, true); // break early return true; } else { // add node to BFS queue bfsNodes.add(new BFSInfo(parent, otherNode)); } } } } } } return false; } /** * Determines whether propagation of permissions is allowed along the given relationship. * * CAUTION: this is a complex situation. * * - we need to determine the EVALUATION DIRECTION, which can be either WITH or AGAINST the RELATIONSHIP DIRECTION * - if we are looking at the START NODE of the relationship, we are evaluating WITH the relationship direction * - if we are looking at the END NODE of the relationship, we are evaluating AGAINST the relationship direction * - the result obtained by the above check must be compared to the PROPAGATION DIRECTION which can be either * SOURCE_TO_TARGET or TARGET_TO_SOURCE * - a propagation direction of SOURCE_TO_TARGET implies that the permissions of the SOURCE NODE can be applied * to the TARGET NODE, so if we are evaluating AGAINST the relationship direction, we are good to go * - a propagation direction of TARGET_TO_SOURCE implies that the permissions of that TARGET NODE can be applied * to the SOURCE NODE, so if we are evaluating WITH the relationship direction, we are good to go * * @param thisNode * @param rel * @param propagationDirection * * @return whether permission resolution can continue along this relationship */ private boolean propagationAllowed(final AbstractNode thisNode, final RelationshipInterface rel, final SchemaRelationshipNode.Direction propagationDirection, final boolean doLog) { // early exit if (propagationDirection.equals(SchemaRelationshipNode.Direction.Both)) { return true; } // early exit if (propagationDirection.equals(SchemaRelationshipNode.Direction.None)) { return false; } final long sourceNodeId = rel.getSourceNode().getId(); final long thisNodeId = thisNode.getId(); if (sourceNodeId == thisNodeId) { // evaluation WITH the relationship direction switch (propagationDirection) { case Out: return false; case In: return true; } } else { // evaluation AGAINST the relationship direction switch (propagationDirection) { case Out: return true; case In: return false; } } return false; } private void applyCurrentStep(final PermissionPropagation rel, PermissionResolutionMask mask) { switch (rel.getReadPropagation()) { case Add: case Keep: mask.addRead(); break; case Remove: mask.removeRead(); break; default: break; } switch (rel.getWritePropagation()) { case Add: case Keep: mask.addWrite(); break; case Remove: mask.removeWrite(); break; default: break; } switch (rel.getDeletePropagation()) { case Add: case Keep: mask.addDelete(); break; case Remove: mask.removeDelete(); break; default: break; } switch (rel.getAccessControlPropagation()) { case Add: case Keep: mask.addAccessControl(); break; case Remove: mask.removeAccessControl(); break; default: break; } // handle delta properties mask.handleProperties(rel.getDeltaProperties()); } private Boolean getPermissionResolutionResult(final long principalId, final Permission permission) { Map<Long, PermissionResolutionResult> permissionResolutionCache = globalPermissionResolutionCache.get(getId()); if (permissionResolutionCache == null) { permissionResolutionCache = new HashMap<>(); globalPermissionResolutionCache.put(getId(), permissionResolutionCache); } PermissionResolutionResult result = permissionResolutionCache.get(principalId); if (result != null) { if (permission.equals(Permission.read)) { return result.read; } if (permission.equals(Permission.write)) { return result.write; } if (permission.equals(Permission.delete)) { return result.delete; } if (permission.equals(Permission.accessControl)) { return result.accessControl; } } return null; } private void storePermissionResolutionResult(final long principalId, final Permission permission, final boolean value) { Map<Long, PermissionResolutionResult> permissionResolutionCache = globalPermissionResolutionCache.get(getId()); if (permissionResolutionCache == null) { permissionResolutionCache = new HashMap<>(); globalPermissionResolutionCache.put(getId(), permissionResolutionCache); } PermissionResolutionResult result = permissionResolutionCache.get(principalId); if (result == null) { result = new PermissionResolutionResult(); permissionResolutionCache.put(principalId, result); } if (permission.equals(Permission.read) && (result.read == null || result.read == false)) { result.read = value; } if (permission.equals(Permission.write) && (result.write == null || result.write == false)) { result.write = value; } if (permission.equals(Permission.delete) && (result.delete == null || result.delete == false)) { result.delete = value; } if (permission.equals(Permission.accessControl) && (result.accessControl == null || result.accessControl == false)) { result.accessControl = value; } } /** * Return the (cached) incoming relationship between this node and the * given principal which holds the security information. * * @param p * @return incoming security relationship */ @Override public final Security getSecurityRelationship(final Principal p) { if (p == null) { return null; } for (final Security r : getIncomingRelationshipsAsSuperUser(Security.class)) { if (r != null) { if (p.equals(r.getSourceNode())) { return r; } } } return null; } @Override public boolean onCreation(SecurityContext securityContext, ErrorBuffer errorBuffer) throws FrameworkException { return true; } @Override public boolean onModification(SecurityContext securityContext, ErrorBuffer errorBuffer, final ModificationQueue modificationQueue) throws FrameworkException { clearPermissionResolutionCache(); return true; } @Override public boolean onDeletion(SecurityContext securityContext, ErrorBuffer errorBuffer, PropertyMap properties) throws FrameworkException { clearPermissionResolutionCache(); return true; } @Override public void afterCreation(SecurityContext securityContext) { } @Override public void afterModification(SecurityContext securityContext) { } @Override public void afterDeletion(SecurityContext securityContext, PropertyMap properties) { } @Override public void ownerModified(SecurityContext securityContext) { clearPermissionResolutionCache(); } @Override public void securityModified(SecurityContext securityContext) { clearPermissionResolutionCache(); } @Override public void locationModified(SecurityContext securityContext) { clearPermissionResolutionCache(); } @Override public void propagatedModification(SecurityContext securityContext) { clearPermissionResolutionCache(); } @Override public boolean isValid(ErrorBuffer errorBuffer) { boolean valid = true; valid &= ValidationHelper.isValidStringNotBlank(this, id, errorBuffer); valid &= ValidationHelper.isValidGloballyUniqueProperty(this, id, errorBuffer); valid &= ValidationHelper.isValidStringMatchingRegex(this, id, "[a-fA-F0-9]{32}", errorBuffer); valid &= ValidationHelper.isValidStringNotBlank(this, type, errorBuffer); return valid; } @Override public final boolean isVisibleToPublicUsers() { return getVisibleToPublicUsers(); } @Override public final boolean isVisibleToAuthenticatedUsers() { return getProperty(visibleToAuthenticatedUsers); } @Override public final boolean isNotHidden() { return !getHidden(); } @Override public final boolean isHidden() { return getHidden(); } @Override public final Date getVisibilityStartDate() { return getProperty(visibilityStartDate); } @Override public final Date getVisibilityEndDate() { return getProperty(visibilityEndDate); } @Override public final Date getCreatedDate() { return getProperty(createdDate); } @Override public final Date getLastModifiedDate() { return getProperty(lastModifiedDate); } // ----- end interface AccessControllable ----- public final boolean isNotDeleted() { return !getDeleted(); } @Override public final boolean isDeleted() { return getDeleted(); } /** * Return true if node is the root node * * @return isRootNode */ public final boolean isRootNode() { return getId() == 0; } public final boolean isVisible() { return securityContext.isVisible(this); } /** * Set a property in database backend. This method needs to be wrappend * into a StructrTransaction, otherwise Neo4j will throw a * NotInTransactionException! Set property only if value has changed. * * @param <T> * @param key * @throws org.structr.common.error.FrameworkException */ @Override public <T> Object setProperty(final PropertyKey<T> key, final T value) throws FrameworkException { // allow setting of ID without permissions if (!key.equals(GraphObject.id)) { if (!isGranted(Permission.write, securityContext)) { internalSystemPropertiesUnlocked = false; readOnlyPropertiesUnlocked = false; throw new FrameworkException(403, "Modification not permitted."); } } T oldValue = getProperty(key); // no old value exists OR old value exists and is NOT equal => set property if ( ((oldValue == null) && (value != null)) || ((oldValue != null) && (!oldValue.equals(value)) || (key instanceof FunctionProperty)) ) { return setPropertyInternal(key, value); } internalSystemPropertiesUnlocked = false; readOnlyPropertiesUnlocked = false; return null; } @Override public void setProperties(final SecurityContext securityContext, final PropertyMap properties) throws FrameworkException { if (!isGranted(Permission.write, securityContext)) { internalSystemPropertiesUnlocked = false; readOnlyPropertiesUnlocked = false; throw new FrameworkException(403, "Modification not permitted."); } for (final PropertyKey key : properties.keySet()) { if (!key.equals(GraphObject.id)) { if (dbNode != null && dbNode.hasProperty(key.dbName())) { // check for system properties if (key.isSystemInternal() && !internalSystemPropertiesUnlocked) { throw new FrameworkException(422, "Property " + key.jsonName() + " is an internal system property", new InternalSystemPropertyToken(getClass().getSimpleName(), key)); } // check for read-only properties if ((key.isReadOnly() || key.isWriteOnce()) && !readOnlyPropertiesUnlocked && !securityContext.isSuperUser()) { throw new FrameworkException(422, "Property " + key.jsonName() + " is read-only", new ReadOnlyPropertyToken(getClass().getSimpleName(), key)); } } } } NodeInterface.super.setProperties(securityContext, properties); } private <T> Object setPropertyInternal(final PropertyKey<T> key, final T value) throws FrameworkException { if (key == null) { logger.error("Tried to set property with null key (action was denied)"); throw new FrameworkException(422, "Tried to set property with null key (action was denied)", new NullArgumentToken(getClass().getSimpleName(), base)); } try { if (dbNode != null && dbNode.hasProperty(key.dbName())) { // check for system properties if (key.isSystemInternal() && !internalSystemPropertiesUnlocked) { throw new FrameworkException(422, "Property " + key.jsonName() + " is an internal system property", new InternalSystemPropertyToken(getClass().getSimpleName(), key)); } // check for read-only properties if ((key.isReadOnly() || key.isWriteOnce()) && !readOnlyPropertiesUnlocked && !securityContext.isSuperUser()) { throw new FrameworkException(422, "Property " + key.jsonName() + " is read-only", new ReadOnlyPropertyToken(getClass().getSimpleName(), key)); } } return key.setProperty(securityContext, this, value); } finally { // unconditionally lock read-only properties after every write (attempt) to avoid security problems // since we made "unlock_readonly_properties_once" available through scripting internalSystemPropertiesUnlocked = false; readOnlyPropertiesUnlocked = false; } } @Override public final void addToIndex() { for (PropertyKey key : StructrApp.getConfiguration().getPropertySet(entityType, PropertyView.All)) { if (key.isIndexed()) { key.index(this, this.getProperty(key)); } } } @Override public final void updateInIndex() { removeFromIndex(); addToIndex(); } @Override public final void removeFromIndex() { final Index<Node> index = Services.getInstance().getService(NodeService.class).getNodeIndex(); index.remove(dbNode); } public final void removeFromIndex(PropertyKey key) { final Index<Node> index = Services.getInstance().getService(NodeService.class).getNodeIndex(); index.remove(dbNode, key.dbName()); } @Override public final void indexPassiveProperties() { for (PropertyKey key : StructrApp.getConfiguration().getPropertySet(entityType, PropertyView.All)) { if (key.isPassivelyIndexed()) { key.index(this, this.getProperty(key)); } } } public static void clearRelationshipTemplateInstanceCache() { relationshipTemplateInstanceCache.clear(); } public static void clearPermissionResolutionCache() { globalPermissionResolutionCache.clear(); } public static <A extends NodeInterface, B extends NodeInterface, R extends Relation<A, B, ?, ?>> R getRelationshipForType(final Class<R> type) { R instance = (R) relationshipTemplateInstanceCache.get(type.getName()); if (instance == null) { try { instance = type.newInstance(); relationshipTemplateInstanceCache.put(type.getName(), instance); } catch (Throwable t) { // TODO: throw meaningful exception here, // should be a RuntimeException that indicates // wrong use of Relationships etc. logger.warn("", t); } } return instance; } @Override public final String getPropertyWithVariableReplacement(ActionContext renderContext, PropertyKey<String> key) throws FrameworkException { final Object value = getProperty(key); String result = null; try { result = Scripting.replaceVariables(renderContext, this, value); } catch (Throwable t) { logger.warn("Scripting error in {} {}:\n{}", key.dbName(), getUuid(), value, t); } return result; } @Override public final Object evaluate(final SecurityContext securityContext, final String key, final String defaultValue) throws FrameworkException { switch (key) { case "owner": return getOwnerNode(); case "_path": if (rawPathSegment != null) { return new RelationshipFactory<>(securityContext).adapt(rawPathSegment); } else { return null; } default: // evaluate object value or return default Object value = getProperty(StructrApp.getConfiguration().getPropertyKeyForJSONName(entityType, key)); if (value != null) { return value; } value = invokeMethod(key, Collections.EMPTY_MAP, false); if (value != null) { return value; } return Function.numberOrString(defaultValue); } } @Override public final Object invokeMethod(final String methodName, final Map<String, Object> propertySet, final boolean throwExceptionForUnknownMethods) throws FrameworkException { final Method method = StructrApp.getConfiguration().getExportedMethodsForType(entityType).get(methodName); if (method != null) { try { // First, try if single parameter is a map, then directly invoke method if (method.getParameterTypes().length == 1 && method.getParameterTypes()[0].equals(Map.class)) { return method.invoke(this, propertySet); } // second try: extracted parameter list return method.invoke(this, extractParameters(propertySet, method.getParameterTypes())); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException t) { if (t instanceof FrameworkException) { throw (FrameworkException) t; } else if (t.getCause() instanceof FrameworkException) { throw (FrameworkException) t.getCause(); } else { logger.debug("Unable to invoke method {}: {}", new Object[]{methodName, t.getMessage()}); logger.warn("", t); } } } // in the case of REST access we want to know if the method exists or not if (throwExceptionForUnknownMethods) { throw new FrameworkException(400, "Method " + methodName + " not found in type " + getType()); } return null; } private Object[] extractParameters(Map<String, Object> properties, Class[] parameterTypes) { final List<Object> values = new ArrayList<>(properties.values()); final List<Object> parameters = new ArrayList<>(); int index = 0; // only try to convert when both lists have equal size if (values.size() == parameterTypes.length) { for (final Class parameterType : parameterTypes) { final Object value = convert(values.get(index++), parameterType); if (value != null) { parameters.add(value); } } } return parameters.toArray(new Object[0]); } /* * Tries to convert the given value into an object * of the given type, using an intermediate type * of String for the conversion. */ private Object convert(Object value, Class type) { Object convertedObject = null; if (type.equals(String.class)) { // strings can be returned immediately return value.toString(); } else if (value instanceof Number) { Number number = (Number) value; if (type.equals(Integer.class) || type.equals(Integer.TYPE)) { return number.intValue(); } else if (type.equals(Long.class) || type.equals(Long.TYPE)) { return number.longValue(); } else if (type.equals(Double.class) || type.equals(Double.TYPE)) { return number.doubleValue(); } else if (type.equals(Float.class) || type.equals(Float.TYPE)) { return number.floatValue(); } else if (type.equals(Short.class) || type.equals(Integer.TYPE)) { return number.shortValue(); } else if (type.equals(Byte.class) || type.equals(Byte.TYPE)) { return number.byteValue(); } } else if (value instanceof List) { return value; } else if (value instanceof Map) { return value; } // fallback try { Method valueOf = type.getMethod("valueOf", String.class); if (valueOf != null) { convertedObject = valueOf.invoke(null, value.toString()); } else { logger.warn("Unable to find static valueOf method for type {}", type); } } catch (Throwable t) { logger.warn("Unable to deserialize value {} of type {}, Class has no static valueOf method.", new Object[]{value, type}); } return convertedObject; } @Override public final void grant(Permission permission, Principal principal) throws FrameworkException { if (!isGranted(Permission.accessControl, securityContext)) { throw new FrameworkException(403, "Access control not permitted"); } Security secRel = getSecurityRelationship(principal); if (secRel == null) { try { secRel = StructrApp.getInstance().create(principal, (NodeInterface)this, Security.class); } catch (FrameworkException ex) { logger.error("Could not create security relationship!", ex); } } secRel.addPermission(permission); } @Override public final void revoke(Permission permission, Principal principal) throws FrameworkException { if (!isGranted(Permission.accessControl, securityContext)) { throw new FrameworkException(403, "Access control not permitted"); } Security secRel = getSecurityRelationship(principal); if (secRel == null) { logger.error("Could not create revoke permission, no security relationship exists!"); } else { secRel.removePermission(permission); } } @Override public final void setRawPathSegment(final Relationship rawPathSegment) { this.rawPathSegment = rawPathSegment; } @Override public final Relationship getRawPathSegment() { return rawPathSegment; } public final void revokeAll() throws FrameworkException { if (!isGranted(Permission.accessControl, securityContext)) { throw new FrameworkException(403, "Access control not permitted"); } final App app = StructrApp.getInstance(); for (final Security security : getIncomingRelationshipsAsSuperUser(Security.class)) { app.delete(security); } } public List<Security> getSecurityRelationships() { final List<Security> grants = Iterables.toList(getIncomingRelationshipsAsSuperUser(Security.class)); // sort list by principal name (important for diff'able export) Collections.sort(grants, new Comparator<Security>() { @Override public int compare(final Security o1, final Security o2) { final Principal p1 = o1.getSourceNode(); final Principal p2 = o2.getSourceNode(); final String n1 = p1 != null ? p1.getProperty(AbstractNode.name) : "empty"; final String n2 = p2 != null ? p2.getProperty(AbstractNode.name) : "empty"; if (n1 != null && n2 != null) { return n1.compareTo(n2); } else if (n1 != null) { return 1; } else if (n2 != null) { return -1; } return 0; } }); return grants; } // ----- Cloud synchronization and replication ----- @Override public List<GraphObject> getSyncData() throws FrameworkException { return new ArrayList<>(); // provide a basis for super.getSyncData() calls } @Override public final boolean isNode() { return true; } @Override public final boolean isRelationship() { return false; } @Override public final NodeInterface getSyncNode() { return this; } @Override public final RelationshipInterface getSyncRelationship() { throw new ClassCastException(this.getClass() + " cannot be cast to org.structr.core.graph.RelationshipInterface"); } // ----- CMIS support methods ----- @Override public CMISInfo getCMISInfo() { return this; } @Override public BaseTypeId getBaseTypeId() { return BaseTypeId.CMIS_ITEM; } @Override public CMISFolderInfo getFolderInfo() { return null; } @Override public CMISDocumentInfo getDocumentInfo() { return null; } @Override public CMISItemInfo geItemInfo() { return this; } @Override public CMISRelationshipInfo getRelationshipInfo() { return null; } @Override public CMISPolicyInfo getPolicyInfo() { return null; } @Override public CMISSecondaryInfo getSecondaryInfo() { return null; } @Override public String getCreatedBy() { return getProperty(AbstractNode.createdBy); } @Override public String getLastModifiedBy() { return getProperty(AbstractNode.lastModifiedBy); } @Override public GregorianCalendar getLastModificationDate() { final Date creationDate = getProperty(AbstractNode.lastModifiedDate); if (creationDate != null) { final GregorianCalendar calendar = new GregorianCalendar(); calendar.setTime(creationDate); return calendar; } return null; } @Override public GregorianCalendar getCreationDate() { final Date creationDate = getProperty(AbstractNode.createdDate); if (creationDate != null) { final GregorianCalendar calendar = new GregorianCalendar(); calendar.setTime(creationDate); return calendar; } return null; } @Override public PropertyMap getDynamicProperties() { final PropertyMap propertyMap = new PropertyMap(); final Class type = getClass(); for (final PropertyKey key : StructrApp.getConfiguration().getPropertySet(type, PropertyView.All)) { // include all dynamic keys in definition if (key.isDynamic() || key.isCMISProperty()) { // only include primitives here final PropertyType dataType = key.getDataType(); if (dataType != null) { propertyMap.put(key, getProperty(key)); } } } return propertyMap; } @Override public AllowableActions getAllowableActions() { return new StructrItemActions(); } @Override public List<Ace> getAccessControlEntries() { final List<Ace> entries = new LinkedList<>(); for (final Security security : getIncomingRelationshipsAsSuperUser(Security.class)) { if (security != null) { entries.add(new AceEntry(security)); } } return entries; } // ----- nested classes ----- private static class AceEntry extends CMISExtensionsData implements Ace, org.apache.chemistry.opencmis.commons.data.Principal { private final List<String> permissions = new LinkedList<>(); private String principalId = null; /** * Construct a new AceEntry from the given Security relationship. This * method assumes that is is called in a transaction. * * @param security */ public AceEntry(final Security security) { final Principal principal = security.getSourceNode(); if (principal != null) { this.principalId = principal.getProperty(Principal.name); } permissions.addAll(security.getPermissions()); } @Override public org.apache.chemistry.opencmis.commons.data.Principal getPrincipal() { return this; } @Override public String getPrincipalId() { return principalId; } @Override public List<String> getPermissions() { return permissions; } @Override public boolean isDirect() { return true; } // ----- interface Principal ----- @Override public String getId() { return getPrincipalId(); } } private static class AlreadyTraversed { private Map<String, Set<Long>> sets = new LinkedHashMap<>(); public boolean contains(final String key, final Long id) { Set<Long> set = sets.get(key); if (set == null) { set = new HashSet<>(); sets.put(key, set); } return !set.add(id); } public int size(final String key) { final Set<Long> set = sets.get(key); if (set != null) { return set.size(); } return 0; } } private static class BFSInfo { public AbstractNode node = null; public BFSInfo parent = null; public int level = 0; public BFSInfo(final BFSInfo parent, final AbstractNode node) { this.parent = parent; this.node = node; if (parent != null) { this.level = parent.level+1; } } } private static class PermissionResolutionResult { Boolean read = false; Boolean write = false; Boolean delete = false; Boolean accessControl = false; } }