mirror of
https://gitee.com/jmix/cuba.git
synced 2024-12-05 04:38:10 +08:00
PL-8551 Dynamic attribute access control (REST API supports update required/hidden/readonly attributes)
This commit is contained in:
parent
e1b302e3fa
commit
6149bf3771
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user