Check attribute permissions on Middleware. #PL-6092

This commit is contained in:
Konstantin Krivopustov 2015-10-02 08:21:00 +00:00
parent 73f5ee4db5
commit c56f50a10c
13 changed files with 400 additions and 28 deletions

View File

@ -0,0 +1,216 @@
/*
* Copyright (c) 2008-2015 Haulmont. All rights reserved.
* Use is subject to license terms, see http://www.cuba-platform.com/license for details.
*/
package com.haulmont.cuba.core.app;
import com.haulmont.bali.util.Preconditions;
import com.haulmont.chile.core.model.MetaClass;
import com.haulmont.chile.core.model.MetaProperty;
import com.haulmont.cuba.core.PersistenceSecurity;
import com.haulmont.cuba.core.entity.BaseGenericIdEntity;
import com.haulmont.cuba.core.entity.Entity;
import com.haulmont.cuba.core.global.*;
import com.haulmont.cuba.core.sys.persistence.CubaEntityFetchGroup;
import org.eclipse.persistence.queries.FetchGroup;
import org.eclipse.persistence.queries.FetchGroupTracker;
import org.springframework.stereotype.Component;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
/**
* Supports enforcing entity attribute permissions on Middleware.
*
* @author Konstantin Krivopustov
* @version $Id$
*/
@Component(AttributeSecuritySupport.NAME)
public class AttributeSecuritySupport {
public static final String NAME = "cuba_AttributeSecuritySupport";
@Inject
protected Metadata metadata;
@Inject
protected MetadataTools metadataTools;
@Inject
protected PersistenceSecurity security;
@Inject
protected ServerConfig config;
/**
* Removes restricted attributes from a view.
*
* @param view source view
* @return restricted view
*/
public View createRestrictedView(View view) {
if (!config.getEntityAttributePermissionChecking()) {
return view;
}
Preconditions.checkNotNullArgument(view, "view is null");
View restrictedView = new View(view.getEntityClass());
copyViewConsideringPermissions(view, restrictedView);
return restrictedView;
}
private void copyViewConsideringPermissions(View srcView, View dstView) {
MetaClass metaClass = metadata.getClassNN(srcView.getEntityClass());
for (ViewProperty property : srcView.getProperties()) {
if (security.isEntityAttrReadPermitted(metaClass, property.getName())) {
View viewCopy = null;
if (property.getView() != null) {
viewCopy = new View(property.getView().getEntityClass(), property.getView().getName() + "(restricted)");
copyViewConsideringPermissions(property.getView(), viewCopy);
}
dstView.addProperty(property.getName(), viewCopy, property.getFetchMode());
}
}
}
/**
* Should be called after loading an entity from the database.
*
* @param entity just loaded detached entity
*/
public void afterLoad(Entity entity) {
if (!config.getEntityAttributePermissionChecking()) {
return;
}
if (entity != null) {
metadataTools.traverseAttributes(entity, new FillingInaccessibleAttributesVisitor());
}
}
/**
* Should be called after loading a list of entities from the database.
*
* @param entities list of just loaded detached entities
*/
public void afterLoad(Collection<? extends Entity> entities) {
if (!config.getEntityAttributePermissionChecking()) {
return;
}
Preconditions.checkNotNullArgument(entities, "entities list is null");
for (Entity entity : entities) {
afterLoad(entity);
}
}
/**
* Should be called before persisting a new entity.
*
* @param entity new entity
*/
public void beforePersist(Entity entity) {
if (!config.getEntityAttributePermissionChecking()) {
return;
}
metadata.getTools().traverseAttributes(entity, new ClearReadOnlyAttributesVisitor());
}
/**
* Should be called before merging an entity.
*
* @param entity detached entity
*/
public void beforeMerge(Entity entity) {
if (!config.getEntityAttributePermissionChecking()) {
return;
}
MetaClass metaClass = metadata.getClassNN(entity.getClass());
FetchGroup fetchGroup = ((FetchGroupTracker) entity)._persistence_getFetchGroup();
if (fetchGroup != null) {
List<String> attributesToRemove = new ArrayList<>();
for (String attrName : fetchGroup.getAttributeNames()) {
String[] parts = attrName.split("\\.");
MetaClass tmpMetaClass = metaClass;
for (String part : parts) {
if (!security.isEntityAttrUpdatePermitted(tmpMetaClass, part)) {
attributesToRemove.add(attrName);
break;
}
MetaProperty metaProperty = tmpMetaClass.getPropertyNN(part);
if (metaProperty.getRange().isClass()) {
tmpMetaClass = metaProperty.getRange().asClass();
}
}
}
if (!attributesToRemove.isEmpty()) {
List<String> attributeNames = new ArrayList<>(fetchGroup.getAttributeNames());
attributeNames.removeAll(attributesToRemove);
((FetchGroupTracker) entity)._persistence_setFetchGroup(new CubaEntityFetchGroup(attributeNames));
}
} else {
List<String> attributeNames = new ArrayList<>();
for (MetaProperty metaProperty : metaClass.getProperties()) {
if (security.isEntityAttrUpdatePermitted(metaClass, metaProperty.getName())) {
attributeNames.add(metaProperty.getName());
}
}
((FetchGroupTracker) entity)._persistence_setFetchGroup(new CubaEntityFetchGroup(attributeNames));
}
}
/**
* Should be called after merging an entity and transaction commit.
*
* @param entity detached entity
*/
public void afterMerge(Entity entity, View view) {
if (!config.getEntityAttributePermissionChecking()) {
return;
}
if (entity != null) {
metadataTools.traverseAttributesByView(view, entity, new ClearInaccessibleAttributesVisitor());
}
}
private void addInaccessibleAttribute(BaseGenericIdEntity entity, String property) {
String[] attributes = entity.__inaccessibleAttributes();
attributes = attributes == null ? new String[1] : Arrays.copyOf(attributes, attributes.length + 1);
attributes[attributes.length - 1] = property;
entity.__inaccessibleAttributes(attributes);
}
private class FillingInaccessibleAttributesVisitor implements EntityAttributeVisitor {
@Override
public void visit(Entity entity, MetaProperty property) {
MetaClass metaClass = metadata.getClassNN(entity.getClass());
if (!security.isEntityAttrReadPermitted(metaClass, property.getName())) {
addInaccessibleAttribute((BaseGenericIdEntity) entity, property.getName());
}
}
}
private class ClearReadOnlyAttributesVisitor implements EntityAttributeVisitor {
@Override
public void visit(Entity entity, MetaProperty property) {
MetaClass metaClass = metadata.getClassNN(entity.getClass());
if (!security.isEntityAttrUpdatePermitted(metaClass, property.getName())) {
entity.setValue(property.getName(), null);
}
}
}
private class ClearInaccessibleAttributesVisitor implements EntityAttributeVisitor {
@Override
public void visit(Entity entity, MetaProperty property) {
MetaClass metaClass = metadata.getClassNN(entity.getClass());
if (!security.isEntityAttrReadPermitted(metaClass, property.getName())) {
addInaccessibleAttribute((BaseGenericIdEntity) entity, property.getName());
entity.setValue(property.getName(), null);
}
}
}
}

View File

@ -26,8 +26,8 @@ import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.Nullable;
import javax.inject.Inject;
import java.util.*;
@ -55,6 +55,9 @@ public class DataManagerBean implements DataManager {
@Inject
protected PersistenceSecurity security;
@Inject
protected AttributeSecuritySupport attributeSecurity;
@Inject
protected Persistence persistence;
@ -95,7 +98,7 @@ public class DataManagerBean implements DataManager {
com.haulmont.cuba.core.Query query = createQuery(em, context);
//noinspection unchecked
List<E> resultList = query.getResultList();
List<E> resultList = executeQuery(query);
if (!resultList.isEmpty())
result = resultList.get(0);
@ -108,6 +111,8 @@ public class DataManagerBean implements DataManager {
tx.end();
}
attributeSecurity.afterLoad(result);
return result;
}
@ -161,6 +166,8 @@ public class DataManagerBean implements DataManager {
tx.end();
}
attributeSecurity.afterLoad(resultList);
return resultList;
}
@ -225,6 +232,7 @@ public class DataManagerBean implements DataManager {
// persist new
for (Entity entity : context.getCommitInstances()) {
if (PersistenceHelper.isNew(entity)) {
attributeSecurity.beforePersist(entity);
em.persist(entity);
res.add(entity);
persisted.add(entity);
@ -238,8 +246,14 @@ public class DataManagerBean implements DataManager {
// merge detached
for (Entity entity : context.getCommitInstances()) {
if (PersistenceHelper.isDetached(entity)) {
attributeSecurity.beforeMerge(entity);
View view = context.getViews().get(entity);
Entity merged = em.merge(entity, view);
if (view == null) {
view = viewRepository.getView(entity.getClass(), View.LOCAL);
}
View restrictedView = attributeSecurity.createRestrictedView(view);
Entity merged = em.merge(entity, restrictedView);
res.add(merged);
if (entityHasDynamicAttributes(entity)) {
BaseGenericIdEntity originalBaseGenericIdEntity = (BaseGenericIdEntity) entity;
@ -280,6 +294,16 @@ public class DataManagerBean implements DataManager {
tx.end();
}
for (Entity entity : res) {
if (!persisted.contains(entity)) {
View view = context.getViews().get(entity);
if (view == null) {
view = viewRepository.getView(entity.getClass(), View.LOCAL);
}
attributeSecurity.afterMerge(entity, view);
}
}
updateReferences(persisted, res);
return res;
@ -415,10 +439,13 @@ public class DataManagerBean implements DataManager {
}
if (entityLoadInfoBuilder.contains(newInstances, entity)) {
attributeSecurity.beforePersist(entity);
em.persist(entity);
result.add(entity);
} else {
Entity e = em.merge(entity);
attributeSecurity.beforeMerge(entity);
View view = context.getViews().get(entity);
Entity e = em.merge(entity, view);
result.add(e);
}
}
@ -486,16 +513,16 @@ public class DataManagerBean implements DataManager {
query.setMaxResults(contextQuery.getMaxResults());
}
if (context.getView() != null) {
query.setView(context.getView());
}
View view = context.getView() != null ? context.getView() :
viewRepository.getView(metadata.getClassNN(context.getMetaClass()), View.LOCAL);
View restrictedView = attributeSecurity.createRestrictedView(view);
query.setView(restrictedView);
return query;
}
protected <E extends Entity> List<E> getResultList(LoadContext<E> context, Query query, boolean ensureDistinct) {
//noinspection unchecked
List<E> list = query.getResultList();
List<E> list = executeQuery(query);
if (!ensureDistinct || list.size() == 0)
return list;
@ -553,6 +580,23 @@ public class DataManagerBean implements DataManager {
return result;
}
protected <E extends Entity> List<E> executeQuery(Query query) {
List<E> list;
try {
//noinspection unchecked
list = query.getResultList();
} catch (javax.persistence.PersistenceException e) {
if (e.getCause() instanceof org.eclipse.persistence.exceptions.QueryException
&& e.getMessage() != null
&& e.getMessage().contains("Fetch group cannot be set on report query")) {
throw new DevelopmentException("DataManager cannot execute query for single attributes");
} else {
throw e;
}
}
return list;
}
protected void checkPermissions(CommitContext context) {
Set<MetaClass> checkedCreateRights = new HashSet<>();
Set<MetaClass> checkedUpdateRights = new HashSet<>();

View File

@ -132,4 +132,12 @@ public interface ServerConfig extends Config {
@Property("cuba.prettyTimeProperties")
@DefaultString("")
String getPrettyTimeProperties();
/**
* If set to false, attribute permissions are not enforced on Middleware. This is appropriate if only server-side
* clients are used.
*/
@Property("cuba.entityAttributePermissionChecking")
@DefaultBoolean(true)
boolean getEntityAttributePermissionChecking();
}

View File

@ -268,7 +268,10 @@ public class EntityManagerImpl implements EntityManager {
return delegate.unwrap(Connection.class);
}
private void deepCopyIgnoringNulls(Entity source, Entity dest) {
/**
* Copies all property values from source to dest excluding null values.
*/
protected void deepCopyIgnoringNulls(Entity source, Entity dest) {
for (MetaProperty srcProperty : source.getMetaClass().getProperties()) {
String name = srcProperty.getName();

View File

@ -95,8 +95,12 @@ public class DataManagerTest extends CubaTestCase {
loadContext.setQueryString("select u.group from sec$User u where u.id = :userId")
.setParameter("userId", UUID.fromString("60885987-1b61-4247-94c7-dff348347f93"));
List<User> list = dataManager.loadList(loadContext);
assertTrue(list.size() == 1);
try {
dataManager.loadList(loadContext);
fail();
} catch (DevelopmentException e) {
assertEquals("DataManager cannot execute query for single attributes", e.getMessage());
}
}
public void testLoadListCaseInsensitive() {

View File

@ -6,10 +6,13 @@
package com.haulmont.cuba.testsupport;
import com.haulmont.bali.db.QueryRunner;
import com.haulmont.chile.core.model.MetaClass;
import com.haulmont.cuba.core.EntityManager;
import com.haulmont.cuba.core.Persistence;
import com.haulmont.cuba.core.entity.Entity;
import com.haulmont.cuba.core.global.AppBeans;
import com.haulmont.cuba.core.global.Metadata;
import com.haulmont.cuba.core.global.MetadataTools;
import com.haulmont.cuba.core.sys.AbstractAppContextLoader;
import com.haulmont.cuba.core.sys.AppContext;
import com.haulmont.cuba.core.sys.AppContextLoader;
@ -130,12 +133,12 @@ public class TestContainer extends ExternalResource {
return AppBeans.get(Metadata.class);
}
public void deleteRecord(String table, UUID... ids) {
public void deleteRecord(String table, Object... ids) {
deleteRecord(table, "ID", ids);
}
public void deleteRecord(String table, String primaryKeyCol, UUID... ids) {
for (UUID id : ids) {
public void deleteRecord(String table, String primaryKeyCol, Object... ids) {
for (Object id : ids) {
String sql = "delete from " + table + " where " + primaryKeyCol + " = '" + id.toString() + "'";
QueryRunner runner = new QueryRunner(persistence().getDataSource());
try {
@ -146,6 +149,20 @@ public class TestContainer extends ExternalResource {
}
}
public void deleteRecord(Entity... entities) {
for (Entity entity : entities) {
MetadataTools metadataTools = metadata().getTools();
MetaClass metaClass = metadata().getClassNN(entity.getClass());
String table = metadataTools.getDatabaseTable(metaClass);
String primaryKey = metadataTools.getPrimaryKeyName(metaClass);
if (table == null || primaryKey == null)
throw new RuntimeException("Unable to determine table or primary key name for " + entity);
deleteRecord(table, primaryKey, entity.getId());
}
}
public List<String> getAppPropertiesFiles() {
return appPropertiesFiles;
}

View File

@ -54,15 +54,18 @@ public abstract class BaseGenericIdEntity<T> extends AbstractInstance implements
@Transient
protected boolean __removed;
@Transient
protected String[] __inaccessibleAttributes;
@Transient
protected Map<String, CategoryAttributeValue> dynamicAttributes = null;
@Column(name = "CREATE_TS")
protected Date createTs;
@Column(name = "CREATED_BY", length = LOGIN_FIELD_LEN)
protected String createdBy;
@Transient
protected Map<String, CategoryAttributeValue> dynamicAttributes = null;
public abstract void setId(T id);
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
@ -111,6 +114,16 @@ public abstract class BaseGenericIdEntity<T> extends AbstractInstance implements
this.__removed = removed;
}
/** INTERNAL */
public String[] __inaccessibleAttributes() {
return __inaccessibleAttributes;
}
/** INTERNAL */
public void __inaccessibleAttributes(String[] __inaccessibleAttributes) {
this.__inaccessibleAttributes = __inaccessibleAttributes;
}
@Override
public MetaClass getMetaClass() {
Metadata metadata = AppBeans.get(Metadata.NAME);

View File

@ -41,6 +41,11 @@ public class StandardEntity extends BaseUuidEntity implements Versioned, Updatab
return version;
}
@Override
public void setVersion(Integer version) {
this.version = version;
}
@Override
public Date getUpdateTs() {
return updateTs;

View File

@ -12,7 +12,11 @@ package com.haulmont.cuba.core.entity;
*/
public interface Versioned {
String[] PROPERTIES = {"version"};
Integer getVersion();
/**
* Do not set version if you are not sure - it must be null for a new entity or loaded from the database
* for a persistent one.
*/
void setVersion(Integer version);
}

View File

@ -4,6 +4,9 @@
*/
package com.haulmont.cuba.core.global;
import com.haulmont.cuba.core.entity.Entity;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
@ -13,6 +16,18 @@ public class NotDetachedCommitContext extends CommitContext {
protected Set<String> newInstanceIds = new HashSet<>();
public NotDetachedCommitContext(Entity... commitInstances) {
super(commitInstances);
}
public NotDetachedCommitContext(Collection commitInstances) {
super(commitInstances);
}
public NotDetachedCommitContext(Collection commitInstances, Collection removeInstances) {
super(commitInstances, removeInstances);
}
public Set<String> getNewInstanceIds() {
return newInstanceIds;
}

View File

@ -118,6 +118,13 @@ public class PersistenceHelper {
* @return true if loaded
*/
public static boolean isLoaded(Object entity, String property) {
if (entity instanceof BaseGenericIdEntity
&& ((BaseGenericIdEntity) entity).__inaccessibleAttributes() != null) {
for (String inaccessibleAttr : ((BaseGenericIdEntity) entity).__inaccessibleAttributes()) {
if (inaccessibleAttr.equals(property))
return false;
}
}
if (entity instanceof FetchGroupTracker) {
FetchGroup fetchGroup = ((FetchGroupTracker) entity)._persistence_getFetchGroup();
if (fetchGroup != null)

View File

@ -5,12 +5,15 @@
package com.haulmont.cuba.core.sys.persistence;
import com.haulmont.cuba.core.entity.BaseGenericIdEntity;
import com.haulmont.cuba.core.global.IllegalEntityStateException;
import org.eclipse.persistence.internal.localization.ExceptionLocalization;
import org.eclipse.persistence.internal.queries.EntityFetchGroup;
import org.eclipse.persistence.queries.FetchGroup;
import org.eclipse.persistence.queries.FetchGroupTracker;
import java.util.Collection;
/**
* @author krivopustov
* @version $Id$
@ -21,8 +24,20 @@ public class CubaEntityFetchGroup extends EntityFetchGroup {
super(fetchGroup);
}
public CubaEntityFetchGroup(Collection<String> attributeNames) {
super(attributeNames);
}
@Override
public String onUnfetchedAttribute(FetchGroupTracker entity, String attributeName) {
String[] inaccessible = ((BaseGenericIdEntity) entity).__inaccessibleAttributes();
if (inaccessible != null) {
for (String inaccessibleAttribute : inaccessible) {
if (attributeName.equals(inaccessibleAttribute))
return null;
}
}
if (attributeName == null && entity._persistence_getSession() != null) { // occurs on merge
return super.onUnfetchedAttribute(entity, null);
}

View File

@ -44,6 +44,9 @@ public class UserSession implements Serializable {
protected Map<String, Serializable> attributes;
/**
* INTERNAL
*/
public UserSession(UUID id, User user, Collection<Role> roles, Locale locale, boolean system) {
this.id = id;
this.user = user;
@ -69,12 +72,18 @@ public class UserSession implements Serializable {
attributes = new ConcurrentHashMap<>();
}
/**
* INTERNAL
*/
public UserSession(UserSession src, User user, Collection<Role> roles, Locale locale) {
this(src.id, user, roles, locale, src.system);
this.user = src.user;
this.substitutedUser = this.user.equals(user) ? null : user;
}
/**
* INTERNAL
*/
public UserSession(UserSession src) {
id = src.id;
user = src.user;
@ -104,7 +113,7 @@ public class UserSession implements Serializable {
}
/**
* Don't do it
* INTERNAL
*/
public void setUser(User user) {
this.user = user;
@ -118,7 +127,7 @@ public class UserSession implements Serializable {
}
/**
* Don't do it
* INTERNAL
*/
public void setSubstitutedUser(User substitutedUser) {
this.substitutedUser = substitutedUser;
@ -145,6 +154,9 @@ public class UserSession implements Serializable {
return locale;
}
/**
* INTERNAL
*/
public void setLocale(Locale locale) {
this.locale = locale;
}
@ -157,6 +169,9 @@ public class UserSession implements Serializable {
return timeZone;
}
/**
* INTERNAL
*/
public void setTimeZone(TimeZone timeZone) {
this.timeZone = timeZone;
}
@ -168,6 +183,9 @@ public class UserSession implements Serializable {
return address;
}
/**
* INTERNAL
*/
public void setAddress(String address) {
this.address = address;
}
@ -179,14 +197,17 @@ public class UserSession implements Serializable {
return clientInfo;
}
/**
* INTERNAL
*/
public void setClientInfo(String clientInfo) {
this.clientInfo = clientInfo;
}
/**
* This method is used by security subsystem
* INTERNAL
*/
public void addPermission(PermissionType type, String target, String extTarget, int value) {
public void addPermission(PermissionType type, String target, @Nullable String extTarget, int value) {
Integer currentValue = permissions[type.ordinal()].get(target);
if (currentValue == null || currentValue < value) {
permissions[type.ordinal()].put(target, value);
@ -196,14 +217,14 @@ public class UserSession implements Serializable {
}
/**
* This method is used by security subsystem
* INTERNAL
*/
public void removePermission(PermissionType type, String target) {
permissions[type.ordinal()].remove(target);
}
/**
* This method is used by security subsystem
* INTERNAL
*/
public Integer getPermissionValue(PermissionType type, String target) {
return permissions[type.ordinal()].get(target);
@ -286,7 +307,7 @@ public class UserSession implements Serializable {
}
/**
* This method is used by security subsystem
* INTERNAL
*/
public void addConstraint(String entityName, String joinClause, String whereClause) {
List<String[]> list = constraints.get(entityName);
@ -298,7 +319,7 @@ public class UserSession implements Serializable {
}
/**
* This method is used by security subsystem
* INTERNAL
*/
public List<String[]> getConstraints(String entityName) {
List<String[]> list = constraints.get(entityName);