package org.molgenis.data.meta; 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.AttributeMetadata; 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.util.EntityUtils; import java.util.Iterator; import java.util.List; import java.util.function.Consumer; import java.util.stream.Stream; import java.util.stream.StreamSupport; import static java.lang.String.format; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.toList; 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; /** * Decorator for the attribute repository: * - filters requested entities based on the entity permissions of the current user. * - applies attribute metadata updates to the backend * <p> * TODO replace permission based entity filtering with generic row-level security once available */ public class AttributeRepositoryDecorator extends AbstractRepositoryDecorator<Attribute> { private final Repository<Attribute> decoratedRepo; private final SystemEntityTypeRegistry systemEntityTypeRegistry; private final DataService dataService; private final MolgenisPermissionService permissionService; public AttributeRepositoryDecorator(Repository<Attribute> decoratedRepo, SystemEntityTypeRegistry systemEntityTypeRegistry, DataService dataService, MolgenisPermissionService permissionService) { this.decoratedRepo = requireNonNull(decoratedRepo); this.systemEntityTypeRegistry = requireNonNull(systemEntityTypeRegistry); this.dataService = requireNonNull(dataService); this.permissionService = requireNonNull(permissionService); } @Override protected Repository<Attribute> delegate() { return decoratedRepo; } @Override public long count() { if (currentUserIsSu() || currentUserisSystem()) { return decoratedRepo.count(); } else { Stream<Attribute> attrs = StreamSupport.stream(decoratedRepo.spliterator(), false); return filterCountPermission(attrs).count(); } } @Override public long count(Query<Attribute> q) { if (currentUserIsSu() || currentUserisSystem()) { return decoratedRepo.count(q); } else { // ignore query offset and page size Query<Attribute> qWithoutLimitOffset = new QueryImpl<>(q); qWithoutLimitOffset.offset(0).pageSize(Integer.MAX_VALUE); Stream<Attribute> attrs = decoratedRepo.findAll(qWithoutLimitOffset); return filterCountPermission(attrs).count(); } } @Override public Stream<Attribute> findAll(Query<Attribute> q) { if (currentUserIsSu() || currentUserisSystem()) { return decoratedRepo.findAll(q); } else { Query<Attribute> qWithoutLimitOffset = new QueryImpl<>(q); qWithoutLimitOffset.offset(0).pageSize(Integer.MAX_VALUE); Stream<Attribute> attrs = decoratedRepo.findAll(qWithoutLimitOffset); Stream<Attribute> filteredAttrs = filterReadPermission(attrs); if (q.getOffset() > 0) { filteredAttrs = filteredAttrs.skip(q.getOffset()); } if (q.getPageSize() > 0) { filteredAttrs = filteredAttrs.limit(q.getPageSize()); } return filteredAttrs; } } @Override public Iterator<Attribute> iterator() { if (currentUserIsSu() || currentUserisSystem()) { return decoratedRepo.iterator(); } else { Stream<Attribute> attrs = StreamSupport.stream(decoratedRepo.spliterator(), false); return filterReadPermission(attrs).iterator(); } } @Override public void forEachBatched(Fetch fetch, Consumer<List<Attribute>> consumer, int batchSize) { if (currentUserIsSu() || currentUserisSystem()) { decoratedRepo.forEachBatched(fetch, consumer, batchSize); } else { FilteredConsumer filteredConsumer = new FilteredConsumer(consumer); decoratedRepo.forEachBatched(fetch, filteredConsumer::filter, batchSize); } } @Override public Attribute findOne(Query<Attribute> q) { if (currentUserIsSu() || currentUserisSystem()) { return decoratedRepo.findOne(q); } else { // ignore query offset and page size return filterReadPermission(decoratedRepo.findOne(q)); } } @Override public Attribute findOneById(Object id) { if (currentUserIsSu() || currentUserisSystem()) { return decoratedRepo.findOneById(id); } else { return filterReadPermission(decoratedRepo.findOneById(id)); } } @Override public Attribute findOneById(Object id, Fetch fetch) { if (currentUserIsSu() || currentUserisSystem()) { return decoratedRepo.findOneById(id, fetch); } else { return filterReadPermission(decoratedRepo.findOneById(id, fetch)); } } @Override public Stream<Attribute> findAll(Stream<Object> ids) { if (currentUserIsSu() || currentUserisSystem()) { return decoratedRepo.findAll(ids); } else { return filterReadPermission(decoratedRepo.findAll(ids)); } } @Override public Stream<Attribute> 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(Attribute attr) { validateUpdateAllowedAndUpdate(attr); decoratedRepo.update(attr); } @Override public void update(Stream<Attribute> attrs) { decoratedRepo.update(attrs.filter(attr -> { validateUpdateAllowedAndUpdate(attr); return true; })); } @Override public void delete(Attribute attr) { validateDeleteAllowed(attr); // If compound attribute is deleted then change the parent of children to null // This will change the children attributes into regular attributes. if (AttributeType.COMPOUND.equals(attr.getDataType())) { attr.getChildren().forEach(e -> { if (null != e.getParent()) { dataService.getMeta().getRepository(AttributeMetadata.ATTRIBUTE_META_DATA) .update(e.setParent(null)); } }); } // remove this attribute decoratedRepo.delete(attr); } @Override public void delete(Stream<Attribute> attrs) { // The validateDeleteAllowed check if querying the table in which we are deleting. Since the decorated repo only // guarantees that the attributes are deleted after the operation completes we have to delete the attributes one // by one attrs.forEach(this::delete); } @Override public void deleteById(Object id) { Attribute attr = findOneById(id); delete(attr); } @Override public void deleteAll(Stream<Object> ids) { delete(findAll(ids)); } @Override public void deleteAll() { delete(this.query().findAll()); } @Override public void add(Attribute attr) { decoratedRepo.add(attr); } @Override public Integer add(Stream<Attribute> attrs) { return decoratedRepo.add(attrs); } /** * Updating attribute meta data is allowed for non-system attributes. For system attributes updating attribute 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 attr attribute */ private void validateUpdateAllowed(Attribute attr) { String attrIdentifier = attr.getIdentifier(); Attribute systemAttr = systemEntityTypeRegistry.getSystemAttribute(attrIdentifier); if (systemAttr != null && !EntityUtils.equals(attr, systemAttr)) { throw new MolgenisDataException( format("Updating system entity attribute [%s] is not allowed", attr.getName())); } } /** * Deleting attribute meta data is allowed for non-system attributes. * * @param attr attribute */ private void validateDeleteAllowed(Attribute attr) { String attrIdentifier = attr.getIdentifier(); if (systemEntityTypeRegistry.hasSystemAttribute(attrIdentifier)) { throw new MolgenisDataException( format("Deleting system entity attribute [%s] is not allowed", attr.getName())); } } /** * Updates an attribute's representation in the backend for each concrete {@link EntityType} that * has the {@link Attribute}. * * @param attr current version of the attribute * @param updatedAttr new version of the attribute */ private void updateAttributeInBackend(Attribute attr, Attribute updatedAttr) { MetaDataService meta = dataService.getMeta(); meta.getConcreteChildren(attr.getEntity()) .forEach(entityType -> meta.getBackend(entityType).updateAttribute(entityType, attr, updatedAttr)); } private void validateUpdateAllowedAndUpdate(Attribute attr) { validateUpdateAllowed(attr); Attribute currentAttr = findOneById(attr.getIdentifier()); updateAttributeInBackend(currentAttr, attr); } private Stream<Attribute> filterCountPermission(Stream<Attribute> attrs) { return filterPermission(attrs, COUNT); } private Attribute filterReadPermission(Attribute attr) { return attr != null ? filterReadPermission(Stream.of(attr)).findFirst().orElse(null) : null; } private Stream<Attribute> filterReadPermission(Stream<Attribute> attrs) { return filterPermission(attrs, READ); } private Stream<Attribute> filterPermission(Stream<Attribute> attrs, Permission permission) { return attrs.filter(attr -> permissionService.hasPermissionOnEntity(attr.getEntity().getName(), permission)); } private class FilteredConsumer { private final Consumer<List<Attribute>> consumer; FilteredConsumer(Consumer<List<Attribute>> consumer) { this.consumer = requireNonNull(consumer); } public void filter(List<Attribute> attrs) { Stream<Attribute> filteredAttrs = filterPermission(attrs.stream(), READ); consumer.accept(filteredAttrs.collect(toList())); } } }