Commit 62cf100e authored by Matija Obreza's avatar Matija Obreza

Merge branch 'image-gallery-acl' into 'master'

Image gallery acl

See merge request !26
parents a627ed92 a6317a0d
...@@ -125,6 +125,13 @@ ...@@ -125,6 +125,13 @@
<version>${spring.security.version}</version> <version>${spring.security.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>${spring.security.version}</version>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.springframework</groupId> <groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId> <artifactId>spring-test</artifactId>
...@@ -132,6 +139,13 @@ ...@@ -132,6 +139,13 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.data</groupId> <groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId> <artifactId>spring-data-jpa</artifactId>
......
...@@ -19,9 +19,11 @@ import java.nio.file.Path; ...@@ -19,9 +19,11 @@ import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.List; import java.util.List;
import org.genesys.filerepository.model.ImageGallery;
import org.genesys.filerepository.model.QRepositoryFile; import org.genesys.filerepository.model.QRepositoryFile;
import org.genesys.filerepository.model.RepositoryFile; import org.genesys.filerepository.model.RepositoryFile;
import org.genesys.filerepository.model.RepositoryFolder; import org.genesys.filerepository.model.RepositoryFolder;
import org.genesys.filerepository.persistence.ImageGalleryPersistence;
import org.genesys.filerepository.persistence.RepositoryFilePersistence; import org.genesys.filerepository.persistence.RepositoryFilePersistence;
import org.genesys.filerepository.service.RepositoryService; import org.genesys.filerepository.service.RepositoryService;
import org.slf4j.Logger; import org.slf4j.Logger;
...@@ -56,6 +58,9 @@ public class RepositoryUpgrade20180920 { ...@@ -56,6 +58,9 @@ public class RepositoryUpgrade20180920 {
@Autowired @Autowired
private RepositoryService repositoryService; private RepositoryService repositoryService;
@Autowired
private ImageGalleryPersistence imagegalleryRepository;
@Autowired @Autowired
private JPAQueryFactory jpaQueryFactory; private JPAQueryFactory jpaQueryFactory;
...@@ -83,6 +88,18 @@ public class RepositoryUpgrade20180920 { ...@@ -83,6 +88,18 @@ public class RepositoryUpgrade20180920 {
} }
} }
}); });
tmpl.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
migrateImageGalleries();
} catch (Throwable e) {
LOG.error("Could not migrate 1.0 to 1.1 model: {}", e.getMessage(), e);
}
}
});
} }
/** /**
...@@ -141,4 +158,29 @@ public class RepositoryUpgrade20180920 { ...@@ -141,4 +158,29 @@ public class RepositoryUpgrade20180920 {
LOG.warn("\n\n\t** All repository files have been moved to proper folders **\n\t You can remove the {} bean.\n\n", this.getClass().getName()); LOG.warn("\n\n\t** All repository files have been moved to proper folders **\n\t You can remove the {} bean.\n\n", this.getClass().getName());
} }
} }
/**
* Do the migration.
*
* @throws Exception the exception
*/
void migrateImageGalleries() throws Exception {
long count = 0;
for (ImageGallery imageGallery : imagegalleryRepository.findAll()) {
if (imageGallery.getFolder() == null) {
RepositoryFolder repoFolder = repositoryService.getFolder(Paths.get(imageGallery.getPath()));
if (imageGallery.getFolder() == null || imageGallery.getFolder().getId() != repoFolder.getId()) {
LOG.warn("Upgrading image gallery {} to folder={}", imageGallery.getPath(), repoFolder.getPath());
imageGallery.setFolder(repoFolder);
imagegalleryRepository.save(imageGallery);
count++;
}
}
}
if (count == 0) {
LOG.warn("\n\n\t** All image galleries have been moved to proper folders **\n\t You can remove the {} bean.\n\n", this.getClass().getName());
}
}
} }
...@@ -16,27 +16,33 @@ ...@@ -16,27 +16,33 @@
package org.genesys.filerepository.model; package org.genesys.filerepository.model;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.persistence.CascadeType; import javax.persistence.CascadeType;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn; import javax.persistence.JoinColumn;
import javax.persistence.JoinTable; import javax.persistence.JoinTable;
import javax.persistence.Lob; import javax.persistence.Lob;
import javax.persistence.ManyToMany; import javax.persistence.ManyToMany;
import javax.persistence.OneToOne;
import javax.persistence.OrderColumn; import javax.persistence.OrderColumn;
import javax.persistence.Table; import javax.persistence.Table;
import javax.persistence.Transient;
import org.genesys.blocks.model.AuditedVersionedModel; import org.genesys.blocks.model.AuditedVersionedModel;
import org.genesys.blocks.model.Copyable; import org.genesys.blocks.model.Copyable;
import org.genesys.blocks.security.model.AclAwareModel; import org.genesys.blocks.security.model.AclAwareModel;
import org.hibernate.annotations.Type; import org.hibernate.annotations.Type;
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.JsonIdentityReference;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonProperty.Access;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
/** /**
* {@link ImageGallery} is a collection of ordered {@link RepositoryImage} * {@link ImageGallery} is a collection of ordered {@link RepositoryImage}
* instances. * instances.
...@@ -60,6 +66,7 @@ public class ImageGallery extends AuditedVersionedModel implements AclAwareModel ...@@ -60,6 +66,7 @@ public class ImageGallery extends AuditedVersionedModel implements AclAwareModel
* a unique path in the repository. * a unique path in the repository.
*/ */
@Column(nullable = false, unique = true) @Column(nullable = false, unique = true)
@JsonIgnore
private String path; private String path;
/** /**
...@@ -81,9 +88,40 @@ public class ImageGallery extends AuditedVersionedModel implements AclAwareModel ...@@ -81,9 +88,40 @@ public class ImageGallery extends AuditedVersionedModel implements AclAwareModel
@OrderColumn(name = "position") @OrderColumn(name = "position")
private List<RepositoryImage> images; private List<RepositoryImage> images;
@Transient /** The folder. */
private Path folderPath; @OneToOne(cascade = {}, fetch = FetchType.LAZY, optional = false)
@JoinColumn(unique = true)
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "path")
@JsonIdentityReference(alwaysAsId = true)
@JsonProperty(access = Access.READ_ONLY)
private RepositoryFolder folder;
@Override
public AclAwareModel aclParentObject() {
return folder;
}
/**
* Gets the folder.
*
* @return the folder
*/
public RepositoryFolder getFolder() {
return folder;
}
/**
* Sets the folder.
*
* @param folder the new folder
*/
public void setFolder(RepositoryFolder folder) {
this.folder = folder;
}
/** /**
* Gets the path. * Gets the path.
* *
...@@ -181,18 +219,4 @@ public class ImageGallery extends AuditedVersionedModel implements AclAwareModel ...@@ -181,18 +219,4 @@ public class ImageGallery extends AuditedVersionedModel implements AclAwareModel
copy.images = new ArrayList<>(this.images); copy.images = new ArrayList<>(this.images);
return copy; return copy;
} }
/**
* Gets the folder path.
*
* @return the folder path
*/
public Path getFolderPath() {
synchronized (this) {
if (folderPath == null) {
this.folderPath = Paths.get(this.path);
}
}
return this.folderPath;
}
} }
...@@ -17,18 +17,16 @@ ...@@ -17,18 +17,16 @@
package org.genesys.filerepository.persistence; package org.genesys.filerepository.persistence;
import org.genesys.filerepository.model.ImageGallery; import org.genesys.filerepository.model.ImageGallery;
import org.springframework.data.domain.Page; import org.genesys.filerepository.model.RepositoryFolder;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.querydsl.QueryDslPredicateExecutor;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
// TODO: Auto-generated Javadoc
/** /**
* The Interface ImageGalleryPersistence. * The Interface ImageGalleryPersistence.
*/ */
@Repository @Repository
public interface ImageGalleryPersistence extends JpaRepository<ImageGallery, Long> { public interface ImageGalleryPersistence extends JpaRepository<ImageGallery, Long>, QueryDslPredicateExecutor<ImageGallery> {
/** /**
* Find gallery for specified path. * Find gallery for specified path.
...@@ -36,16 +34,6 @@ public interface ImageGalleryPersistence extends JpaRepository<ImageGallery, Lon ...@@ -36,16 +34,6 @@ public interface ImageGalleryPersistence extends JpaRepository<ImageGallery, Lon
* @param path The path of image gallery in the repository * @param path The path of image gallery in the repository
* @return the Gallery! * @return the Gallery!
*/ */
ImageGallery findByPath(String path); ImageGallery findByFolder(RepositoryFolder folder);
/**
* Find galleries with repository path starting with <code>prefix</code>.
*
* @param prefix the prefix
* @param pageable the pageable
* @return the page
*/
@Query("select ig from org.genesys.filerepository.model.ImageGallery ig where ig.path like ?1%")
Page<ImageGallery> listByPath(String prefix, Pageable pageable);
} }
...@@ -52,8 +52,9 @@ public interface ImageGalleryService { ...@@ -52,8 +52,9 @@ public interface ImageGalleryService {
* @param title Image gallery title in English. * @param title Image gallery title in English.
* @param description Image gallery description in English. * @param description Image gallery description in English.
* @return the new ImageGallery or existing gallery at the specified path. * @return the new ImageGallery or existing gallery at the specified path.
* @throws InvalidRepositoryPathException
*/ */
ImageGallery createImageGallery(Path path, String title, String description); ImageGallery createImageGallery(Path path, String title, String description) throws InvalidRepositoryPathException;
/** /**
* Delete the image gallery, but don't remove the images at that path. * Delete the image gallery, but don't remove the images at that path.
...@@ -107,7 +108,8 @@ public interface ImageGalleryService { ...@@ -107,7 +108,8 @@ public interface ImageGalleryService {
* @param prefix the prefix * @param prefix the prefix
* @param pageable the pageable * @param pageable the pageable
* @return paginated image gallery data * @return paginated image gallery data
* @throws InvalidRepositoryPathException
*/ */
Page<ImageGallery> listImageGalleries(Path root, Pageable pageable); Page<ImageGallery> listImageGalleries(Path root, Pageable pageable) throws InvalidRepositoryPathException;
} }
...@@ -23,8 +23,11 @@ import java.util.ArrayList; ...@@ -23,8 +23,11 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import org.genesys.blocks.security.SecurityContextUtil;
import org.genesys.filerepository.InvalidRepositoryPathException; import org.genesys.filerepository.InvalidRepositoryPathException;
import org.genesys.filerepository.model.ImageGallery; import org.genesys.filerepository.model.ImageGallery;
import org.genesys.filerepository.model.QImageGallery;
import org.genesys.filerepository.model.RepositoryFolder;
import org.genesys.filerepository.model.RepositoryImage; import org.genesys.filerepository.model.RepositoryImage;
import org.genesys.filerepository.persistence.ImageGalleryPersistence; import org.genesys.filerepository.persistence.ImageGalleryPersistence;
import org.genesys.filerepository.service.BytesStorageService; import org.genesys.filerepository.service.BytesStorageService;
...@@ -35,14 +38,18 @@ import org.slf4j.Logger; ...@@ -35,14 +38,18 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
// TODO: Auto-generated Javadoc // TODO: Auto-generated Javadoc
/** /**
* The Class ImageGalleryServiceImpl. * The Class ImageGalleryServiceImpl.
...@@ -70,6 +77,10 @@ public class ImageGalleryServiceImpl implements ImageGalleryService { ...@@ -70,6 +77,10 @@ public class ImageGalleryServiceImpl implements ImageGalleryService {
@Autowired @Autowired
private ThumbnailGenerator thumbnailGenerator; private ThumbnailGenerator thumbnailGenerator;
/** The jpa query factory. */
@Autowired
private JPAQueryFactory jpaQueryFactory;
/* /*
* (non-Javadoc) * (non-Javadoc)
* @see * @see
...@@ -79,7 +90,7 @@ public class ImageGalleryServiceImpl implements ImageGalleryService { ...@@ -79,7 +90,7 @@ public class ImageGalleryServiceImpl implements ImageGalleryService {
@Override @Override
@PostAuthorize("returnObject == null or hasRole('ADMINISTRATOR') or hasPermission(returnObject, 'read')") @PostAuthorize("returnObject == null or hasRole('ADMINISTRATOR') or hasPermission(returnObject, 'read')")
public ImageGallery loadImageGallery(final Path path) { public ImageGallery loadImageGallery(final Path path) {
final ImageGallery imageGallery = imageGalleryPersistence.findByPath(path.normalize().toAbsolutePath().toString()); final ImageGallery imageGallery = imageGalleryPersistence.findByFolder(repositoryService.getFolder(path));
return deepLoad(imageGallery); return deepLoad(imageGallery);
} }
...@@ -106,17 +117,23 @@ public class ImageGalleryServiceImpl implements ImageGalleryService { ...@@ -106,17 +117,23 @@ public class ImageGalleryServiceImpl implements ImageGalleryService {
@Override @Override
@Transactional @Transactional
@PreAuthorize("isAuthenticated()") @PreAuthorize("isAuthenticated()")
public ImageGallery createImageGallery(final Path path, final String title, final String description) { public ImageGallery createImageGallery(final Path path, final String title, final String description) throws InvalidRepositoryPathException {
LOG.debug("Creating ImageGallery at path={}", path); LOG.debug("Creating ImageGallery at path={}", path);
ImageGallery imageGallery = imageGalleryPersistence.findByPath(path.normalize().toAbsolutePath().toString()); RepositoryFolder repositoryFolder = repositoryService.ensureFolder(path.normalize().toAbsolutePath());
ImageGallery imageGallery = imageGalleryPersistence.findByFolder(repositoryFolder);
if (imageGallery != null) { if (imageGallery != null) {
return imageGallery; return imageGallery;
} else {
LOG.info("Creating new image gallery at path={}", path);
imageGallery = new ImageGallery();
} }
if (!SecurityContextUtil.hasRole("ADMINISTRATOR") && !SecurityContextUtil.hasPermission(repositoryFolder, "WRITE")) {
throw new AccessDeniedException("No WRITE permission on " + repositoryFolder.getPath());
}
LOG.info("Creating new image gallery at path={}", path);
imageGallery = new ImageGallery();
imageGallery.setFolder(repositoryFolder);
imageGallery.setPath(path.normalize().toAbsolutePath().toString()); imageGallery.setPath(path.normalize().toAbsolutePath().toString());
imageGallery.setTitle(title); imageGallery.setTitle(title);
imageGallery.setDescription(description); imageGallery.setDescription(description);
...@@ -295,7 +312,7 @@ public class ImageGalleryServiceImpl implements ImageGalleryService { ...@@ -295,7 +312,7 @@ public class ImageGalleryServiceImpl implements ImageGalleryService {
* @return the page * @return the page
*/ */
@Override @Override
@PostFilter("hasRole('ADMINISTRATOR') or hasPermission(filterObject, 'read')") @PreAuthorize("hasRole('ADMINISTRATOR')")
public Page<ImageGallery> listImageGalleries(final Pageable pageable) { public Page<ImageGallery> listImageGalleries(final Pageable pageable) {
return imageGalleryPersistence.findAll(pageable); return imageGalleryPersistence.findAll(pageable);
} }
...@@ -307,8 +324,22 @@ public class ImageGalleryServiceImpl implements ImageGalleryService { ...@@ -307,8 +324,22 @@ public class ImageGalleryServiceImpl implements ImageGalleryService {
* java.lang.String, org.springframework.data.domain.Pageable) * java.lang.String, org.springframework.data.domain.Pageable)
*/ */
@Override @Override
@PostFilter("hasRole('ADMINISTRATOR') or hasPermission(filterObject, 'read')") public Page<ImageGallery> listImageGalleries(final Path root, final Pageable pageable) throws InvalidRepositoryPathException {
public Page<ImageGallery> listImageGalleries(final Path root, final Pageable pageable) { List<RepositoryFolder> folders = repositoryService.listPaths(root);
return imageGalleryPersistence.listByPath(root.normalize().toAbsolutePath().toString(), pageable); // Remove folders user can't read
folders.removeIf(folder -> !SecurityContextUtil.hasRole("ADMINISTRATOR") && !SecurityContextUtil.hasPermission(folder, "READ"));
JPAQuery<ImageGallery> query = jpaQueryFactory.selectFrom(QImageGallery.imageGallery).where(QImageGallery.imageGallery.folder.in(folders));
long total = query.fetchCount();
List<ImageGallery> content = query.join(QImageGallery.imageGallery.folder).fetchJoin()
// page
.offset(pageable.getOffset()).limit(pageable.getPageSize())
// fetch
.fetch();
// content.forEach(imageGallery -> imageGallery.getImages().size());
// content.forEach(imageGallery -> imageGallery.getFolder().getId());
return new PageImpl<>(content, pageable, total);
} }
} }
...@@ -33,6 +33,7 @@ import javax.imageio.ImageIO; ...@@ -33,6 +33,7 @@ import javax.imageio.ImageIO;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.apache.tika.Tika; import org.apache.tika.Tika;
import org.genesys.blocks.security.SecurityContextUtil;
import org.genesys.filerepository.FolderNotEmptyException; import org.genesys.filerepository.FolderNotEmptyException;
import org.genesys.filerepository.InvalidRepositoryFileDataException; import org.genesys.filerepository.InvalidRepositoryFileDataException;
import org.genesys.filerepository.InvalidRepositoryPathException; import org.genesys.filerepository.InvalidRepositoryPathException;
...@@ -51,6 +52,7 @@ import org.slf4j.Logger; ...@@ -51,6 +52,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
...@@ -88,7 +90,7 @@ public class RepositoryServiceImpl implements RepositoryService, InitializingBea ...@@ -88,7 +90,7 @@ public class RepositoryServiceImpl implements RepositoryService, InitializingBea
/** The bytes storage service. */ /** The bytes storage service. */
@Autowired @Autowired
private BytesStorageService bytesStorageService; private BytesStorageService bytesStorageService;
/* /*
* (non-Javadoc) * (non-Javadoc)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
...@@ -139,6 +141,8 @@ public class RepositoryServiceImpl implements RepositoryService, InitializingBea ...@@ -139,6 +141,8 @@ public class RepositoryServiceImpl implements RepositoryService, InitializingBea
PathValidator.checkValidPath(repositoryPath); PathValidator.checkValidPath(repositoryPath);
RepositoryFolder repositoryFolder = ensureFolder(repositoryPath);
contentType = updateContentTypeIfNecessary(contentType, bytes); contentType = updateContentTypeIfNecessary(contentType, bytes);
if ((originalFilename == null) || (contentType == null) || (bytes == null)) { if ((originalFilename == null) || (contentType == null) || (bytes == null)) {
...@@ -162,7 +166,6 @@ public class RepositoryServiceImpl implements RepositoryService, InitializingBea ...@@ -162,7 +166,6 @@ public class RepositoryServiceImpl implements RepositoryService, InitializingBea
repositoryFile.setMd5Sum(DigestUtils.md5Hex(bytes)); repositoryFile.setMd5Sum(DigestUtils.md5Hex(bytes));
repositoryFile.setSize(bytes.length); repositoryFile.setSize(bytes.length);
RepositoryFolder repositoryFolder = ensureFolder(repositoryPath);
repositoryFile.setFolder(repositoryFolder); repositoryFile.setFolder(repositoryFolder);