PL-8551 Dynamic attribute access control (REST API supports update required/hidden/readonly attributes)

This commit is contained in:
Andrey Subbotin 2017-10-18 11:25:01 +04:00
parent e1b302e3fa
commit 6149bf3771
8 changed files with 131 additions and 62 deletions

View File

@ -78,11 +78,25 @@ public interface PersistenceSecurity extends Security {
boolean filterByConstraints(Entity entity);
/**
* Reads security token and restores
* filtered data and readOnly, hidden, required attributes
* @param resultEntity -
* Reads security token and restores security state
* @param entity - entity to restore security state
*/
void restoreSecurityData(BaseGenericIdEntity<?> resultEntity);
void restoreSecurityState(Entity entity);
/**
* Restores filtered data from security token
* @param entity - entity to restore filtered data
*/
void restoreFilteredData(Entity entity);
/**
* Reads security token and restores security state and filtered data
* @param entity - entity to restore
*/
default void restoreSecurityStateAndFilteredData(Entity entity) {
restoreSecurityState(entity);
restoreFilteredData(entity);
}
/**
* Calculate filtered data

View File

@ -234,7 +234,7 @@ public class AttributeSecuritySupport {
if (securityState != null && !securityState.getRequiredAttributes().isEmpty()) {
for (MetaProperty metaProperty : entity.getMetaClass().getProperties()) {
String propertyName = metaProperty.getName();
if (isRequired(securityState, propertyName) && entity.getValue(propertyName) == null) {
if (BaseEntityInternalAccess.isRequired(securityState, propertyName) && entity.getValue(propertyName) == null) {
throw new RowLevelSecurityException(format("Attribute [%s] is required for entity %s", propertyName, entity),
entity.getMetaClass().getName());
}
@ -254,7 +254,7 @@ public class AttributeSecuritySupport {
List<String> attributesToRemove = new ArrayList<>();
for (String attrName : fetchGroup.getAttributeNames()) {
String[] parts = attrName.split("\\.");
if (parts.length > 0 && isHiddenOrReadOnly(securityState, parts[0])) {
if (parts.length > 0 && BaseEntityInternalAccess.isHiddenOrReadOnly(securityState, parts[0])) {
attributesToRemove.add(attrName);
} else {
MetaClass currentMetaClass = metaClass;
@ -283,7 +283,7 @@ public class AttributeSecuritySupport {
attributeNames.add(propertyName);
}
if (security.isEntityAttrUpdatePermitted(metaClass, propertyName) &&
!isHiddenOrReadOnly(securityState, propertyName)) {
!BaseEntityInternalAccess.isHiddenOrReadOnly(securityState, propertyName)) {
attributeNames.add(metaProperty.getName());
}
}
@ -291,21 +291,6 @@ public class AttributeSecuritySupport {
}
}
protected boolean isHiddenOrReadOnly(SecurityState securityState, String attributeName) {
if (securityState == null) {
return false;
}
return securityState.getHiddenAttributes().contains(attributeName)
|| securityState.getReadonlyAttributes().contains(attributeName);
}
protected boolean isRequired(SecurityState securityState, String attributeName) {
if (securityState == null) {
return false;
}
return securityState.getRequiredAttributes().contains(attributeName);
}
private void addInaccessibleAttribute(BaseGenericIdEntity entity, String property) {
SecurityState securityState = getOrCreateSecurityState(entity);
String[] attributes = getInaccessibleAttributes(securityState);

View File

@ -347,7 +347,7 @@ public class RdbmsStore implements DataStore {
// merge the rest - instances can be detached or not
for (Entity entity : context.getCommitInstances()) {
if (!PersistenceHelper.isNew(entity)) {
security.restoreSecurityData((BaseGenericIdEntity) entity);
security.restoreSecurityStateAndFilteredData((BaseGenericIdEntity) entity);
attributeSecurity.beforeMerge(entity);
Entity merged = em.merge(entity);
@ -372,7 +372,7 @@ public class RdbmsStore implements DataStore {
// remove
for (Entity entity : context.getRemoveInstances()) {
security.restoreSecurityData((BaseGenericIdEntity) entity);
security.restoreSecurityStateAndFilteredData((BaseGenericIdEntity) entity);
Entity e;
if (entity instanceof SoftDelete) {

View File

@ -33,6 +33,7 @@ import com.haulmont.cuba.core.app.serialization.EntitySerializationAPI;
import com.haulmont.cuba.core.app.serialization.EntitySerializationOption;
import com.haulmont.cuba.core.entity.*;
import com.haulmont.cuba.core.global.*;
import com.haulmont.cuba.core.global.validation.CustomValidationException;
import com.haulmont.cuba.core.global.validation.EntityValidationException;
import com.haulmont.cuba.core.global.validation.groups.RestApiChecks;
import org.apache.commons.compress.archivers.ArchiveEntry;
@ -55,6 +56,8 @@ import java.util.*;
import java.util.stream.Collectors;
import java.util.zip.CRC32;
import static java.lang.String.format;
@Component(EntityImportExportAPI.NAME)
public class EntityImportExport implements EntityImportExportAPI {
@ -275,9 +278,26 @@ public class EntityImportExport implements EntityImportExportAPI {
//we must specify a view here because otherwise we may get UnfetchedAttributeException during merge
commitContext.addInstanceToCommit(dstEntity, regularView);
SecurityState securityState = null;
if (srcEntity instanceof BaseGenericIdEntity) {
String storeName = metadata.getTools().getStoreName(srcEntity.getMetaClass());
DataStore dataStore = storeFactory.get(storeName);
//row-level security works only for entities from RdbmsStore
if (dataStore instanceof RdbmsStore) {
persistenceSecurity.restoreSecurityState(srcEntity);
securityState = BaseEntityInternalAccess.getSecurityState(srcEntity);
}
}
for (EntityImportViewProperty importViewProperty : importView.getProperties()) {
String propertyName = importViewProperty.getName();
MetaProperty metaProperty = metaClass.getPropertyNN(propertyName);
if (BaseEntityInternalAccess.isHiddenOrReadOnly(securityState, propertyName)) {
continue;
}
if (BaseEntityInternalAccess.isRequired(securityState, propertyName) && srcEntity.getValue(propertyName) == null) {
throw new CustomValidationException(format("Attribute [%s] is required for entity %s", propertyName, srcEntity));
}
if ((metaProperty.getRange().isDatatype() && !"version".equals(metaProperty.getName())) || metaProperty.getRange().isEnum()) {
dstEntity.setValue(propertyName, srcEntity.getValue(propertyName));
} else if (metaProperty.getRange().isClass()) {
@ -345,20 +365,9 @@ public class EntityImportExport implements EntityImportExportAPI {
if (srcEntity instanceof BaseGenericIdEntity) {
String storeName = metadata.getTools().getStoreName(srcEntity.getMetaClass());
DataStore dataStore = storeFactory.get(storeName);
BaseGenericIdEntity srcGenericIdEntity = (BaseGenericIdEntity) srcEntity;
//row-level security works only for entities from RdbmsStore
if (dataStore instanceof RdbmsStore) {
//create an entity copy here, because filtered items must not be reloaded in the srcEntity for now,
//we only need a collection of filtered properties
try (Transaction tx = persistence.getTransaction()) {
BaseGenericIdEntity srcGenericIdEntityCopy = metadata.getTools().deepCopy(srcGenericIdEntity);
byte[] securityToken = BaseEntityInternalAccess.getSecurityToken(srcGenericIdEntity);
SecurityState securityStateCopy = BaseEntityInternalAccess.getOrCreateSecurityState(srcGenericIdEntity);
BaseEntityInternalAccess.setSecurityToken(securityStateCopy, securityToken);
persistenceSecurity.restoreSecurityData(srcGenericIdEntityCopy);
filteredItems = BaseEntityInternalAccess.getFilteredData(srcGenericIdEntityCopy);
tx.commit();
}
filteredItems = BaseEntityInternalAccess.getFilteredData(srcEntity);
}
}
@ -476,8 +485,25 @@ public class EntityImportExport implements EntityImportExportAPI {
dstEmbeddedEntity = metadata.create(embeddedAttrMetaClass);
}
SecurityState securityState = null;
if (srcEntity instanceof BaseGenericIdEntity) {
String storeName = metadata.getTools().getStoreName(srcEntity.getMetaClass());
DataStore dataStore = storeFactory.get(storeName);
//row-level security works only for entities from RdbmsStore
if (dataStore instanceof RdbmsStore) {
persistenceSecurity.restoreSecurityState(srcEmbeddedEntity);
securityState = BaseEntityInternalAccess.getSecurityState(srcEmbeddedEntity);
}
}
for (EntityImportViewProperty vp : importViewProperty.getView().getProperties()) {
MetaProperty mp = embeddedAttrMetaClass.getPropertyNN(vp.getName());
if (BaseEntityInternalAccess.isHiddenOrReadOnly(securityState, mp.getName())) {
continue;
}
if (BaseEntityInternalAccess.isRequired(securityState, mp.getName()) && srcEmbeddedEntity.getValue(mp.getName()) == null) {
throw new CustomValidationException(format("Attribute [%s] is required for entity %s", mp.getName(), srcEmbeddedEntity));
}
if ((mp.getRange().isDatatype() && !"version".equals(mp.getName())) || mp.getRange().isEnum()) {
dstEmbeddedEntity.setValue(vp.getName(), srcEmbeddedEntity.getValue(vp.getName()));
} else if (mp.getRange().isClass()) {
@ -563,7 +589,7 @@ public class EntityImportExport implements EntityImportExportAPI {
if (dataStore instanceof RdbmsStore) {
//restore filtered data, otherwise they will be lost
try (Transaction tx = persistence.getTransaction()) {
persistenceSecurity.restoreSecurityData((BaseGenericIdEntity<?>) entity);
persistenceSecurity.restoreSecurityStateAndFilteredData((BaseGenericIdEntity<?>) entity);
tx.commit();
}
}

View File

@ -24,7 +24,9 @@ import com.haulmont.cuba.core.EntityManager;
import com.haulmont.cuba.core.Persistence;
import com.haulmont.cuba.core.PersistenceSecurity;
import com.haulmont.cuba.core.Query;
import com.haulmont.cuba.core.entity.*;
import com.haulmont.cuba.core.entity.BaseEntityInternalAccess;
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.jpql.JpqlSyntaxException;
import com.haulmont.cuba.security.entity.ConstraintOperationType;
@ -157,26 +159,21 @@ public class PersistenceSecurityImpl extends SecurityImpl implements Persistence
}
@Override
@SuppressWarnings("unchecked")
public void restoreSecurityData(BaseGenericIdEntity<?> resultEntity) {
MetaClass metaClass = metadata.getClassNN(resultEntity.getClass());
public void restoreSecurityState(Entity entity) {
checkSecurityToken(entity);
securityTokenManager.readSecurityToken(entity);
}
@Override
@SuppressWarnings("unchecked")
public void restoreFilteredData(Entity entity) {
checkSecurityToken(entity);
MetaClass metaClass = metadata.getClassNN(entity.getClass());
String storeName = metadataTools.getStoreName(metaClass);
EntityManager entityManager = persistence.getEntityManager(storeName);
securityTokenManager.readSecurityToken(resultEntity);
if (BaseEntityInternalAccess.getSecurityToken(resultEntity) == null) {
List<ConstraintData> existingConstraints = getConstraints(metaClass,
constraint -> constraint.getCheckType().memory());
if (CollectionUtils.isNotEmpty(existingConstraints)) {
throw new RowLevelSecurityException(format("Could not read security token from entity %s, " +
"even though there are active constraints for the entity.", resultEntity),
resultEntity.getMetaClass().getName());
}
}
Multimap<String, Object> filtered = BaseEntityInternalAccess.getFilteredData(resultEntity);
Multimap<String, Object> filtered = BaseEntityInternalAccess.getFilteredData(entity);
if (filtered == null) {
return;
}
@ -189,7 +186,7 @@ public class PersistenceSecurityImpl extends SecurityImpl implements Persistence
Class entityClass = property.getRange().asClass().getJavaClass();
Class propertyClass = property.getJavaType();
if (Collection.class.isAssignableFrom(propertyClass)) {
Collection currentCollection = resultEntity.getValue(property.getName());
Collection currentCollection = entity.getValue(property.getName());
if (currentCollection == null) {
throw new RowLevelSecurityException(
format("Could not restore an object to currentValue because it is null [%s]. Entity [%s].",
@ -205,7 +202,24 @@ public class PersistenceSecurityImpl extends SecurityImpl implements Persistence
Object entityId = filteredIds.iterator().next();
Entity reference = entityManager.getReference((Class<Entity>) entityClass, entityId);
//we ignore the situation when the field is read-only
resultEntity.setValue(property.getName(), reference);
entity.setValue(property.getName(), reference);
}
}
}
}
protected void checkSecurityToken(Entity entity) {
if (BaseEntityInternalAccess.getSecurityToken(entity) == null) {
MetaClass metaClass = metadata.getClassNN(entity.getClass());
for (MetaProperty metaProperty : metaClass.getProperties()) {
if (metaProperty.getRange().isClass()) {
List<ConstraintData> existingConstraints = getConstraints(metaProperty.getRange().asClass(),
constraint -> constraint.getCheckType().memory());
if (CollectionUtils.isNotEmpty(existingConstraints)) {
throw new RowLevelSecurityException(format("Could not read security token from entity %s, " +
"even though there are active constraints for the entity.", entity),
entity.getMetaClass().getName());
}
}
}
}

View File

@ -232,8 +232,8 @@ public class EntitySerialization implements EntitySerializationAPI {
writeFields(entity, jsonObject, view, cyclicReferences);
}
if (entity instanceof BaseGenericIdEntity) {
SecurityState securityState = getSecurityState((BaseGenericIdEntity) entity);
if (entity instanceof BaseGenericIdEntity || entity instanceof EmbeddableEntity) {
SecurityState securityState = getSecurityState(entity);
if (securityState != null) {
byte[] securityToken = getSecurityToken(securityState);
if (securityToken != null) {
@ -464,7 +464,7 @@ public class EntitySerialization implements EntitySerializationAPI {
JsonPrimitive securityTokenJonPrimitive = jsonObject.getAsJsonPrimitive("__securityToken");
if (securityTokenJonPrimitive != null) {
byte[] securityToken = Base64.getDecoder().decode(securityTokenJonPrimitive.getAsString());
setSecurityToken(getOrCreateSecurityState((BaseGenericIdEntity)entity), securityToken);
setSecurityToken(getOrCreateSecurityState(entity), securityToken);
}
}
@ -558,6 +558,13 @@ public class EntitySerialization implements EntitySerializationAPI {
Entity entity = metadata.create(metaClass);
clearFields(entity);
readFields(jsonObject, entity);
if (entity instanceof EmbeddableEntity) {
JsonPrimitive securityTokenJonPrimitive = jsonObject.getAsJsonPrimitive("__securityToken");
if (securityTokenJonPrimitive != null) {
byte[] securityToken = Base64.getDecoder().decode(securityTokenJonPrimitive.getAsString());
setSecurityToken(getOrCreateSecurityState(entity), securityToken);
}
}
return entity;
}

View File

@ -224,7 +224,22 @@ public final class BaseEntityInternalAccess {
return securityState;
}
public static void setValue(BaseGenericIdEntity entity, String attribute, @Nullable Object value) {
public static boolean isHiddenOrReadOnly(SecurityState securityState, String attributeName) {
if (securityState == null) {
return false;
}
return securityState.getHiddenAttributes().contains(attributeName)
|| securityState.getReadonlyAttributes().contains(attributeName);
}
public static boolean isRequired(SecurityState securityState, String attributeName) {
if (securityState == null) {
return false;
}
return securityState.getRequiredAttributes().contains(attributeName);
}
public static void setValue(Entity entity, String attribute, @Nullable Object value) {
Preconditions.checkNotNullArgument(entity, "entity is null");
Field field = FieldUtils.getField(entity.getClass(), attribute, true);
if (field == null)
@ -236,7 +251,7 @@ public final class BaseEntityInternalAccess {
}
}
public static Object getValue(BaseGenericIdEntity entity, String attribute) {
public static Object getValue(Entity entity, String attribute) {
Preconditions.checkNotNullArgument(entity, "entity is null");
Field field = FieldUtils.getField(entity.getClass(), attribute, true);
if (field == null)

View File

@ -114,7 +114,15 @@ public class RestControllerUtils {
if (!metadataTools.isSystem(property) && !property.isReadOnly()) {
// Using reflective access to field because the attribute can be unfetched if loading not partial entities,
// which is the case when in-memory constraints exist
BaseEntityInternalAccess.setValue((BaseGenericIdEntity) entity, property.getName(), null);
BaseEntityInternalAccess.setValue(entity, property.getName(), null);
}
}
SecurityState securityState = BaseEntityInternalAccess.getSecurityState(entity);
if (securityState != null && securityState.getHiddenAttributes().contains(property.getName())) {
if (!metadataTools.isSystem(property)) {
// Using reflective access to field because the attribute can be unfetched if loading not partial entities,
// which is the case when in-memory constraints exist
BaseEntityInternalAccess.setValue(entity, property.getName(), null);
}
}
}