package org.molgenis.data.meta;
import com.google.common.collect.TreeTraverser;
import org.molgenis.auth.GroupAuthority;
import org.molgenis.auth.UserAuthority;
import org.molgenis.data.*;
import org.molgenis.data.aggregation.AggregateQuery;
import org.molgenis.data.aggregation.AggregateResult;
import org.molgenis.data.meta.model.Attribute;
import org.molgenis.data.meta.model.EntityType;
import org.molgenis.data.meta.system.SystemEntityTypeRegistry;
import org.molgenis.data.support.QueryImpl;
import org.molgenis.security.core.MolgenisPermissionService;
import org.molgenis.security.core.Permission;
import org.molgenis.security.core.utils.SecurityUtils;
import javax.annotation.Nonnull;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static com.google.common.collect.Sets.difference;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.StreamSupport.stream;
import static org.molgenis.auth.AuthorityMetaData.ROLE;
import static org.molgenis.auth.GroupAuthorityMetaData.GROUP_AUTHORITY;
import static org.molgenis.auth.UserAuthorityMetaData.USER_AUTHORITY;
import static org.molgenis.data.meta.model.AttributeMetadata.ATTRIBUTE_META_DATA;
import static org.molgenis.security.core.Permission.COUNT;
import static org.molgenis.security.core.Permission.READ;
import static org.molgenis.security.core.utils.SecurityUtils.currentUserIsSu;
import static org.molgenis.security.core.utils.SecurityUtils.currentUserisSystem;
import static org.molgenis.util.SecurityDecoratorUtils.validatePermission;
/**
* Decorator for the entity meta data repository:
* - filters requested entities based on the permissions of the current user.
* - applies updates to the repository collection for entity meta data adds/deletes
* - adds and removes attribute columns to the repository collection for entity meta data updates
* <p>
* TODO replace permission based entity filtering with generic row-level security once available
*/
public class EntityTypeRepositoryDecorator extends AbstractRepositoryDecorator<EntityType>
{
private final Repository<EntityType> decoratedRepo;
private final DataService dataService;
private final SystemEntityTypeRegistry systemEntityTypeRegistry;
private final MolgenisPermissionService permissionService;
public EntityTypeRepositoryDecorator(Repository<EntityType> decoratedRepo, DataService dataService,
SystemEntityTypeRegistry systemEntityTypeRegistry, MolgenisPermissionService permissionService)
{
this.decoratedRepo = requireNonNull(decoratedRepo);
this.dataService = requireNonNull(dataService);
this.systemEntityTypeRegistry = requireNonNull(systemEntityTypeRegistry);
this.permissionService = requireNonNull(permissionService);
}
@Override
protected Repository<EntityType> delegate()
{
return decoratedRepo;
}
@Override
public long count()
{
if (currentUserIsSu() || currentUserisSystem())
{
return decoratedRepo.count();
}
else
{
Stream<EntityType> EntityTypes = StreamSupport.stream(decoratedRepo.spliterator(), false);
return filterCountPermission(EntityTypes).count();
}
}
@Override
public long count(Query<EntityType> q)
{
if (currentUserIsSu() || currentUserisSystem())
{
return decoratedRepo.count(q);
}
else
{
// ignore query offset and page size
Query<EntityType> qWithoutLimitOffset = new QueryImpl<>(q);
qWithoutLimitOffset.offset(0).pageSize(Integer.MAX_VALUE);
Stream<EntityType> EntityTypes = decoratedRepo.findAll(qWithoutLimitOffset);
return filterCountPermission(EntityTypes).count();
}
}
@Override
public Stream<EntityType> findAll(Query<EntityType> q)
{
if (currentUserIsSu() || currentUserisSystem())
{
return decoratedRepo.findAll(q);
}
else
{
Query<EntityType> qWithoutLimitOffset = new QueryImpl<>(q);
qWithoutLimitOffset.offset(0).pageSize(Integer.MAX_VALUE);
Stream<EntityType> EntityTypes = decoratedRepo.findAll(qWithoutLimitOffset);
Stream<EntityType> filteredEntityTypes = filterReadPermission(EntityTypes);
if (q.getOffset() > 0)
{
filteredEntityTypes = filteredEntityTypes.skip(q.getOffset());
}
if (q.getPageSize() > 0)
{
filteredEntityTypes = filteredEntityTypes.limit(q.getPageSize());
}
return filteredEntityTypes;
}
}
@Override
public Iterator<EntityType> iterator()
{
if (currentUserIsSu() || currentUserisSystem())
{
return decoratedRepo.iterator();
}
else
{
Stream<EntityType> EntityTypeStream = StreamSupport.stream(decoratedRepo.spliterator(), false);
return filterReadPermission(EntityTypeStream).iterator();
}
}
@Override
public void forEachBatched(Fetch fetch, Consumer<List<EntityType>> consumer, int batchSize)
{
if (currentUserIsSu() || currentUserisSystem())
{
decoratedRepo.forEachBatched(fetch, consumer, batchSize);
}
else
{
FilteredConsumer filteredConsumer = new FilteredConsumer(consumer, permissionService);
decoratedRepo.forEachBatched(fetch, filteredConsumer::filter, batchSize);
}
}
@Override
public EntityType findOne(Query<EntityType> q)
{
if (currentUserIsSu() || currentUserisSystem())
{
return decoratedRepo.findOne(q);
}
else
{
// ignore query offset and page size
return filterReadPermission(decoratedRepo.findOne(q));
}
}
@Override
public EntityType findOneById(Object id)
{
if (currentUserIsSu() || currentUserisSystem())
{
return decoratedRepo.findOneById(id);
}
else
{
return filterReadPermission(decoratedRepo.findOneById(id));
}
}
@Override
public EntityType findOneById(Object id, Fetch fetch)
{
if (currentUserIsSu() || currentUserisSystem())
{
return decoratedRepo.findOneById(id, fetch);
}
else
{
return filterReadPermission(decoratedRepo.findOneById(id, fetch));
}
}
@Override
public Stream<EntityType> findAll(Stream<Object> ids)
{
if (currentUserIsSu() || currentUserisSystem())
{
return decoratedRepo.findAll(ids);
}
else
{
return filterReadPermission(decoratedRepo.findAll(ids));
}
}
@Override
public Stream<EntityType> findAll(Stream<Object> ids, Fetch fetch)
{
if (currentUserIsSu() || currentUserisSystem())
{
return decoratedRepo.findAll(ids, fetch);
}
else
{
return filterReadPermission(decoratedRepo.findAll(ids, fetch));
}
}
@Override
public AggregateResult aggregate(AggregateQuery aggregateQuery)
{
if (currentUserIsSu() || currentUserisSystem())
{
return decoratedRepo.aggregate(aggregateQuery);
}
else
{
throw new MolgenisDataAccessException(format("Aggregation on entity [%s] not allowed", getName()));
}
}
@Override
public void update(EntityType entity)
{
updateEntity(entity);
}
@Override
public void update(Stream<EntityType> entities)
{
entities.forEach(this::updateEntity);
}
@Override
public void delete(EntityType entity)
{
deleteEntityType(entity);
}
@Override
public void delete(Stream<EntityType> entities)
{
entities.forEach(this::deleteEntityType);
}
@Override
public void deleteById(Object id)
{
EntityType entityType = findOneById(id);
if (entityType == null)
{
throw new UnknownEntityException(
format("Unknown entity meta data [%s] with id [%s]", getName(), id.toString()));
}
deleteEntityType(entityType);
}
@Override
public void deleteAll(Stream<Object> ids)
{
findAll(ids).forEach(this::deleteEntityType);
}
@Override
public void deleteAll()
{
iterator().forEachRemaining(this::deleteEntityType);
}
@Override
public void add(EntityType entity)
{
addEntityType(entity);
}
@Override
public Integer add(Stream<EntityType> entities)
{
AtomicInteger count = new AtomicInteger();
entities.filter(entity ->
{
count.incrementAndGet();
return true;
}).forEach(this::addEntityType);
return count.get();
}
private void addEntityType(EntityType entityType)
{
validatePermission(entityType.getName(), Permission.WRITEMETA);
// add row to entities table
decoratedRepo.add(entityType);
if (!entityType.isAbstract() && !dataService.getMeta().isMetaEntityType(entityType))
{
RepositoryCollection repoCollection = dataService.getMeta().getBackend(entityType);
if (repoCollection == null)
{
throw new MolgenisDataException(format("Unknown backend [%s]", entityType.getBackend()));
}
repoCollection.createRepository(entityType);
}
}
private void updateEntity(EntityType newEntityType)
{
validateUpdateAllowed(newEntityType);
addAndRemoveAttributesInBackend(newEntityType);
// update entity
decoratedRepo.update(newEntityType);
}
/**
* Add and remove entity attributes in the backend for an {@link EntityType}.
* If the {@link EntityType} is abstract, will update all concrete extending {@link EntityType}s.
* Attribute updates are handled by the {@link AttributeRepositoryDecorator}.
*
* @param entityType {@link EntityType} containing the desired situation.
*/
private void addAndRemoveAttributesInBackend(EntityType entityType)
{
EntityType existingEntityType = decoratedRepo.findOneById(entityType.getIdValue());
Map<String, Attribute> attrsMap = stream(entityType.getOwnAllAttributes().spliterator(), false)
.collect(toMap(Attribute::getName, Function.identity()));
Map<String, Attribute> existingAttrsMap = stream(existingEntityType.getOwnAllAttributes().spliterator(), false)
.collect(toMap(Attribute::getName, Function.identity()));
dataService.getMeta().getConcreteChildren(entityType).forEach(concreteEntityType ->
{
RepositoryCollection backend = dataService.getMeta().getBackend(concreteEntityType);
EntityType concreteExistingEntityType = decoratedRepo.findOneById(concreteEntityType.getIdValue());
// add added attributes in backend
difference(attrsMap.keySet(), existingAttrsMap.keySet()).stream().map(attrsMap::get)
.forEach(addedAttribute -> backend.addAttribute(concreteExistingEntityType, addedAttribute));
// remove removed attributes in backend
difference(existingAttrsMap.keySet(), attrsMap.keySet()).stream().map(existingAttrsMap::get)
.forEach(removedAttribute -> backend.deleteAttribute(concreteExistingEntityType, removedAttribute));
});
}
/**
* Updating entityType meta data is allowed for non-system entities. For system entities updating entityType meta data is
* only allowed if the meta data defined in Java differs from the meta data stored in the database (in other words
* the Java code was updated).
*
* @param entityType entity meta data
*/
private void validateUpdateAllowed(EntityType entityType)
{
String entityName = entityType.getName();
validatePermission(entityName, Permission.WRITEMETA);
SystemEntityType systemEntityType = systemEntityTypeRegistry.getSystemEntityType(entityName);
//FIXME: should only be possible to update system entities during bootstrap!
if (systemEntityType != null && !currentUserisSystem())
{
throw new MolgenisDataException(format("Updating system entity meta data [%s] is not allowed", entityName));
}
}
private void deleteEntityType(EntityType entityType)
{
validateDeleteAllowed(entityType);
// delete EntityType table
if (!entityType.isAbstract())
{
deleteEntityRepository(entityType);
}
// delete EntityType permissions
deleteEntityPermissions(entityType);
// delete rows from attributes table
deleteEntityAttributes(entityType);
// delete row from entities table
decoratedRepo.delete(entityType);
}
private void validateDeleteAllowed(EntityType entityType)
{
String entityName = entityType.getName();
validatePermission(entityName, Permission.WRITEMETA);
boolean isSystem = systemEntityTypeRegistry.hasSystemEntityType(entityName);
if (isSystem)
{
throw new MolgenisDataException(format("Deleting system entity meta data [%s] is not allowed", entityName));
}
}
private void deleteEntityAttributes(EntityType entityType)
{
Iterable<Attribute> rootAttrs = entityType.getOwnAttributes();
Stream<Attribute> allAttrs = StreamSupport.stream(rootAttrs.spliterator(), false).flatMap(
attrEntity -> StreamSupport
.stream(new AttributeTreeTraverser().preOrderTraversal(attrEntity).spliterator(), false));
dataService.delete(ATTRIBUTE_META_DATA, allAttrs);
}
private void deleteEntityRepository(EntityType entityType)
{
String backend = entityType.getBackend();
dataService.getMeta().getBackend(backend).deleteRepository(entityType);
}
private void deleteEntityPermissions(EntityType entityType)
{
String entityName = entityType.getName();
List<String> authorities = SecurityUtils.getEntityAuthorities(entityName);
// User permissions
List<UserAuthority> userPermissions = dataService.query(USER_AUTHORITY, UserAuthority.class)
.in(ROLE, authorities).findAll().collect(toList());
if (!userPermissions.isEmpty())
{
dataService.delete(USER_AUTHORITY, userPermissions.stream());
}
// Group permissions
List<GroupAuthority> groupPermissions = dataService.query(GROUP_AUTHORITY, GroupAuthority.class)
.in(ROLE, authorities).findAll().collect(toList());
if (!groupPermissions.isEmpty())
{
dataService.delete(GROUP_AUTHORITY, groupPermissions.stream());
}
}
private static class AttributeTreeTraverser extends TreeTraverser<Attribute>
{
@Override
public Iterable<Attribute> children(@Nonnull Attribute attr)
{
return attr.getChildren();
}
}
private EntityType filterReadPermission(EntityType entityType)
{
return entityType != null ? filterReadPermission(Stream.of(entityType)).findFirst().orElse(null) : null;
}
private Stream<EntityType> filterReadPermission(Stream<EntityType> EntityTypeStream)
{
return filterPermission(EntityTypeStream, READ);
}
private Stream<EntityType> filterCountPermission(Stream<EntityType> EntityTypeStream)
{
return filterPermission(EntityTypeStream, COUNT);
}
private Stream<EntityType> filterPermission(Stream<EntityType> EntityTypeStream, Permission permission)
{
return EntityTypeStream
.filter(entityType -> permissionService.hasPermissionOnEntity(entityType.getName(), permission));
}
private static class FilteredConsumer
{
private final Consumer<List<EntityType>> consumer;
private final MolgenisPermissionService permissionService;
FilteredConsumer(Consumer<List<EntityType>> consumer, MolgenisPermissionService permissionService)
{
this.consumer = requireNonNull(consumer);
this.permissionService = requireNonNull(permissionService);
}
public void filter(List<EntityType> entityTypes)
{
List<EntityType> filteredEntityTypes = entityTypes.stream()
.filter(entityType -> permissionService.hasPermissionOnEntity(entityType.getName(), READ))
.collect(toList());
consumer.accept(filteredEntityTypes);
}
}
}