package org.molgenis.data.importer.wizard;
import org.molgenis.auth.*;
import org.molgenis.data.*;
import org.molgenis.data.importer.*;
import org.molgenis.data.meta.NameValidator;
import org.molgenis.data.support.GenericImporterExtensions;
import org.molgenis.data.support.Href;
import org.molgenis.data.support.QueryImpl;
import org.molgenis.file.FileStore;
import org.molgenis.security.core.utils.SecurityUtils;
import org.molgenis.security.permission.Permission;
import org.molgenis.security.permission.Permissions;
import org.molgenis.security.user.UserAccountService;
import org.molgenis.ui.MolgenisPluginController;
import org.molgenis.ui.wizard.AbstractWizardController;
import org.molgenis.ui.wizard.Wizard;
import org.molgenis.util.FileExtensionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.Objects.requireNonNull;
import static org.apache.commons.io.FilenameUtils.getBaseName;
import static org.apache.commons.io.FilenameUtils.getExtension;
import static org.molgenis.auth.GroupAuthorityMetaData.GROUP_AUTHORITY;
import static org.molgenis.auth.GroupMetaData.GROUP;
import static org.molgenis.data.importer.wizard.ImportWizardController.URI;
import static org.molgenis.data.meta.DefaultPackage.PACKAGE_DEFAULT;
import static org.molgenis.security.core.Permission.*;
import static org.springframework.http.MediaType.TEXT_PLAIN;
@Controller
@RequestMapping(URI)
public class ImportWizardController extends AbstractWizardController
{
public static final String ID = "importwizard";
public static final String URI = MolgenisPluginController.PLUGIN_URI_PREFIX + ID;
private final UploadWizardPage uploadWizardPage;
private final OptionsWizardPage optionsWizardPage;
private final ValidationResultWizardPage validationResultWizardPage;
private final ImportResultsWizardPage importResultsWizardPage;
private final PackageWizardPage packageWizardPage;
private final GrantedAuthoritiesMapper grantedAuthoritiesMapper;
private final UserAccountService userAccountService;
private final GroupAuthorityFactory groupAuthorityFactory;
private ImportServiceFactory importServiceFactory;
private FileStore fileStore;
private FileRepositoryCollectionFactory fileRepositoryCollectionFactory;
private ImportRunService importRunService;
private ExecutorService asyncImportJobs;
private DataService dataService;
private static final Logger LOG = LoggerFactory.getLogger(ImportWizardController.class);
@Autowired
public ImportWizardController(UploadWizardPage uploadWizardPage, OptionsWizardPage optionsWizardPage,
PackageWizardPage packageWizardPage, ValidationResultWizardPage validationResultWizardPage,
ImportResultsWizardPage importResultsWizardPage, DataService dataService,
GrantedAuthoritiesMapper grantedAuthoritiesMapper, UserAccountService userAccountService,
ImportServiceFactory importServiceFactory, FileStore fileStore,
FileRepositoryCollectionFactory fileRepositoryCollectionFactory, ImportRunService importRunService,
GroupAuthorityFactory groupAuthorityFactory)
{
super(URI, "importWizard");
if (uploadWizardPage == null) throw new IllegalArgumentException("UploadWizardPage is null");
if (optionsWizardPage == null) throw new IllegalArgumentException("OptionsWizardPage is null");
if (validationResultWizardPage == null)
{
throw new IllegalArgumentException("ValidationResultWizardPage is null");
}
if (importResultsWizardPage == null) throw new IllegalArgumentException("ImportResultsWizardPage is null");
this.uploadWizardPage = uploadWizardPage;
this.optionsWizardPage = optionsWizardPage;
this.validationResultWizardPage = validationResultWizardPage;
this.importResultsWizardPage = importResultsWizardPage;
this.packageWizardPage = packageWizardPage;
this.userAccountService = userAccountService;
this.dataService = dataService;
this.grantedAuthoritiesMapper = grantedAuthoritiesMapper;
this.importServiceFactory = importServiceFactory;
this.fileStore = fileStore;
this.fileRepositoryCollectionFactory = fileRepositoryCollectionFactory;
this.importRunService = importRunService;
this.groupAuthorityFactory = requireNonNull(groupAuthorityFactory);
this.dataService = dataService;
this.asyncImportJobs = Executors.newSingleThreadExecutor();
}
public ImportWizardController(UploadWizardPage uploadWizardPage, OptionsWizardPage optionsWizardPage,
PackageWizardPage packageWizardPage, ValidationResultWizardPage validationResultWizardPage,
ImportResultsWizardPage importResultsWizardPage, DataService dataService,
GrantedAuthoritiesMapper grantedAuthoritiesMapper, UserAccountService userAccountService,
ImportServiceFactory importServiceFactory, FileStore fileStore,
FileRepositoryCollectionFactory fileRepositoryCollectionFactory, ImportRunService importRunService,
ExecutorService executorService, GroupAuthorityFactory groupAuthorityFactory)
{
super(URI, "importWizard");
if (uploadWizardPage == null) throw new IllegalArgumentException("UploadWizardPage is null");
if (optionsWizardPage == null) throw new IllegalArgumentException("OptionsWizardPage is null");
if (validationResultWizardPage == null)
throw new IllegalArgumentException("ValidationResultWizardPage is null");
if (importResultsWizardPage == null) throw new IllegalArgumentException("ImportResultsWizardPage is null");
this.uploadWizardPage = uploadWizardPage;
this.optionsWizardPage = optionsWizardPage;
this.validationResultWizardPage = validationResultWizardPage;
this.importResultsWizardPage = importResultsWizardPage;
this.packageWizardPage = packageWizardPage;
this.userAccountService = userAccountService;
this.dataService = dataService;
this.grantedAuthoritiesMapper = grantedAuthoritiesMapper;
this.importServiceFactory = importServiceFactory;
this.fileStore = fileStore;
this.fileRepositoryCollectionFactory = fileRepositoryCollectionFactory;
this.importRunService = importRunService;
this.dataService = dataService;
this.asyncImportJobs = executorService;
this.groupAuthorityFactory = groupAuthorityFactory;
}
@Override
protected Wizard createWizard()
{
Wizard wizard = new ImportWizard();
wizard.addPage(uploadWizardPage);
wizard.addPage(optionsWizardPage);
wizard.addPage(packageWizardPage);
wizard.addPage(validationResultWizardPage);
wizard.addPage(importResultsWizardPage);
return wizard;
}
@RequestMapping(value = "/entityclass/group/{groupId}", method = RequestMethod.GET)
@ResponseBody
public Permissions getGroupEntityClassPermissions(@PathVariable String groupId, WebRequest webRequest)
{
boolean allowed = false;
for (Group group : userAccountService.getCurrentUserGroups())
{
if (group.getId().equals(groupId))
{
allowed = true;
}
}
if (!allowed && !userAccountService.getCurrentUser().isSuperuser())
{
throw new RuntimeException("Current user does not belong to the requested group.");
}
String entitiesString = webRequest.getParameter("entityIds");
List<String> entities = Arrays.asList(entitiesString.split(","));
Group group = dataService.findOneById(GROUP, groupId, Group.class);
if (group == null) throw new RuntimeException("unknown group id [" + groupId + "]");
List<Authority> groupPermissions = getGroupPermissions(group);
Permissions permissions = createPermissions(groupPermissions, entities);
permissions.setGroupId(groupId);
return permissions;
}
@RequestMapping(value = "/add/entityclass/group", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.OK)
public void addGroupEntityClassPermissions(@RequestParam String groupId, WebRequest webRequest)
{
dataService.getEntityNames().forEach(entityClassId ->
{
GroupAuthority authority = getGroupAuthority(groupId, entityClassId);
boolean newGroupAuthority;
if(authority == null) {
newGroupAuthority = true;
authority = groupAuthorityFactory.create();
} else {
newGroupAuthority = false;
}
String param = "radio-" + entityClassId;
String value = webRequest.getParameter(param);
if (value != null && (SecurityUtils
.currentUserHasRole(SecurityUtils.AUTHORITY_ENTITY_WRITEMETA_PREFIX + entityClassId)
|| userAccountService.getCurrentUser().isSuperuser()))
{
if (value.equalsIgnoreCase(READ.toString()) || value.equalsIgnoreCase(COUNT.toString()) || value
.equalsIgnoreCase(WRITE.toString()) || value.equalsIgnoreCase(WRITEMETA.toString()))
{
authority.setGroup(dataService.findOneById(GROUP, groupId, Group.class));
authority.setRole(SecurityUtils.AUTHORITY_ENTITY_PREFIX + value.toUpperCase() + "_" + entityClassId);
if (newGroupAuthority)
{
authority.setId(UUID.randomUUID().toString());
dataService.add(GROUP_AUTHORITY, authority);
}
else
{
dataService.update(GROUP_AUTHORITY, authority);
}
}
else if (value.equalsIgnoreCase(NONE.toString()))
{
if (authority.getId() != null)
{
dataService.deleteById(GROUP_AUTHORITY, authority.getId());
}
}
else
{
throw new RuntimeException(
"Unknown value: " + value + " for permission on entity: " + entityClassId);
}
}
else
{
if (value != null) throw new MolgenisDataAccessException(
"Current user is not allowed to change the permissions for this entity: " + entityClassId);
}
});
}
private List<Authority> getGroupPermissions(Group group)
{
return dataService.findAll(GROUP_AUTHORITY,
new QueryImpl<GroupAuthority>().eq(GroupAuthorityMetaData.GROUP, group),
GroupAuthority.class).collect(Collectors.toList());
}
private Permissions createPermissions(List<? extends Authority> entityAuthorities, List<String> entityIds)
{
Permissions permissions = new Permissions();
if (entityIds != null)
{
Map<String, String> entityClassMap = new TreeMap<String, String>();
for (String entityClassId : entityIds)
{
entityClassMap.put(entityClassId, entityClassId);
}
permissions.setEntityIds(entityClassMap);
}
for (Authority authority : entityAuthorities)
{
// add permissions for authorities that match prefix
if (authority.getRole().startsWith(SecurityUtils.AUTHORITY_ENTITY_PREFIX))
{
Permission permission = new Permission();
String authorityType = getAuthorityType(authority.getRole());
String authorityPluginId = getAuthorityEntityId(authority.getRole());
permission.setType(authorityType);
if (authority instanceof GroupAuthority)
{
permission.setGroup(((GroupAuthority) authority).getGroup().getName());
permissions.addGroupPermission(authorityPluginId, permission);
}
}
// add permissions for inherited authorities from authority that match prefix
SimpleGrantedAuthority grantedAuthority = new SimpleGrantedAuthority(authority.getRole());
Collection<? extends GrantedAuthority> hierarchyAuthorities = grantedAuthoritiesMapper
.mapAuthorities(Collections.singletonList(grantedAuthority));
hierarchyAuthorities.remove(grantedAuthority);
for (GrantedAuthority hierarchyAuthority : hierarchyAuthorities)
{
if (hierarchyAuthority.getAuthority().startsWith(SecurityUtils.AUTHORITY_ENTITY_PREFIX))
{
String authorityPluginId = getAuthorityEntityId(hierarchyAuthority.getAuthority());
Permission hierarchyPermission = new Permission();
hierarchyPermission.setType(getAuthorityType(hierarchyAuthority.getAuthority()));
permissions.addHierarchyPermission(authorityPluginId, hierarchyPermission);
}
}
}
permissions.sort();
return permissions;
}
/**
* Returns a group authority based on group and entity class identifier.
*
* @param groupId group identifier
* @param entityClassId entity class identifier
* @return existing group authority or <code>null</code> if no matching group authority exists.
*/
private GroupAuthority getGroupAuthority(String groupId, String entityClassId)
{
Stream<GroupAuthority> stream = dataService.findAll(GROUP_AUTHORITY,
new QueryImpl<GroupAuthority>().eq(GroupAuthorityMetaData.GROUP, groupId),
GroupAuthority.class);
GroupAuthority existingGroupAuthority = null;
for (Iterator<GroupAuthority> it = stream.iterator(); it.hasNext(); )
{
GroupAuthority groupAuthority = it.next();
String entity = "";
if (groupAuthority.getRole().startsWith(SecurityUtils.AUTHORITY_ENTITY_COUNT_PREFIX) || groupAuthority
.getRole().startsWith(SecurityUtils.AUTHORITY_ENTITY_WRITE_PREFIX))
{
entity = groupAuthority.getRole().substring(SecurityUtils.AUTHORITY_ENTITY_COUNT_PREFIX.length());
}
else if (groupAuthority.getRole().startsWith(SecurityUtils.AUTHORITY_ENTITY_READ_PREFIX))
{
entity = groupAuthority.getRole().substring(SecurityUtils.AUTHORITY_ENTITY_READ_PREFIX.length());
}
else if (groupAuthority.getRole().startsWith(SecurityUtils.AUTHORITY_ENTITY_WRITEMETA_PREFIX))
{
entity = groupAuthority.getRole().substring(SecurityUtils.AUTHORITY_ENTITY_WRITEMETA_PREFIX.length());
}
if (entity.equals(entityClassId))
{
existingGroupAuthority = groupAuthority;
}
}
return existingGroupAuthority;
}
private String getAuthorityEntityId(String role)
{
role = role.substring(SecurityUtils.AUTHORITY_ENTITY_PREFIX.length());
return role.substring(role.indexOf('_') + 1).toLowerCase();
}
private String getAuthorityType(String role)
{
role = role.substring(SecurityUtils.AUTHORITY_ENTITY_PREFIX.length());
return role.substring(0, role.indexOf('_')).toLowerCase();
}
@RequestMapping(method = RequestMethod.POST, value = "/importByUrl")
@ResponseBody
public ResponseEntity<String> importFileByUrl(HttpServletRequest request, @RequestParam("url") String url,
@RequestParam(value = "entityName", required = false) String entityName,
@RequestParam(value = "action", required = false) String action,
@RequestParam(value = "notify", required = false) Boolean notify) throws IOException, URISyntaxException
{
ImportRun importRun;
try
{
File tmpFile = fileLocationToStoredRenamedFile(url, entityName);
importRun = importFile(request, tmpFile, action, notify);
}
catch (Exception e)
{
LOG.error(e.getMessage());
return ResponseEntity.badRequest().contentType(TEXT_PLAIN).body(e.getMessage());
}
return createCreatedResponseEntity(importRun);
}
@RequestMapping(method = RequestMethod.POST, value = "/importFile")
public ResponseEntity<String> importFile(HttpServletRequest request,
@RequestParam(value = "file", required = true) MultipartFile file,
@RequestParam(value = "entityName", required = false) String entityName,
@RequestParam(value = "action", required = false) String action,
@RequestParam(value = "notify", required = false) Boolean notify) throws IOException, URISyntaxException
{
ImportRun importRun;
String filename;
try
{
filename = getFilename(file.getOriginalFilename(), entityName);
File tmpFile = fileStore.store(file.getInputStream(), filename);
importRun = importFile(request, tmpFile, action, notify);
}
catch (Exception e)
{
LOG.error(e.getMessage());
return ResponseEntity.badRequest().contentType(TEXT_PLAIN).body(e.getMessage());
}
return createCreatedResponseEntity(importRun);
}
private ResponseEntity<String> createCreatedResponseEntity(ImportRun importRun) throws URISyntaxException
{
String href = Href.concatEntityHref("/api/v2", importRun.getEntityType().getName(), importRun.getIdValue());
return ResponseEntity.created(new java.net.URI(href)).contentType(TEXT_PLAIN).body(href);
}
private File fileLocationToStoredRenamedFile(String fileLocation, String entityName) throws IOException
{
Path path = Paths.get(fileLocation);
String filename = path.getFileName().toString();
URL url = new URL(fileLocation);
return fileStore.store(url.openStream(), getFilename(filename, entityName));
}
private String getFilename(String originalFileName, String entityName)
{
String filename;
String extension = FileExtensionUtils
.findExtensionFromPossibilities(originalFileName, GenericImporterExtensions.getAll());
if (entityName == null)
{
filename = originalFileName;
}
else
{
filename = entityName + "." + extension;
if (!extension.equals("vcf") && (!extension.equals("vcf.gz") && (!extension.equals("vcf.zip"))))
LOG.warn("Specifing a filename for a non-VCF file has no effect on entity names.");
}
return filename;
}
private ImportRun importFile(HttpServletRequest request, File file, String action, Boolean notify)
{
// no action specified? default is ADD just like the importerPlugin
ImportRun importRun;
String fileExtension = getExtension(file.getName());
DatabaseAction databaseAction = getDatabaseAction(file, action);
if (fileExtension.contains("vcf") && dataService.hasRepository(getBaseName(file.getName())))
{
throw new MolgenisDataException(
"A repository with name " + getBaseName(file.getName()) + " already exists");
}
ImportService importService = importServiceFactory.getImportService(file.getName());
RepositoryCollection repositoryCollection = fileRepositoryCollectionFactory
.createFileRepositoryCollection(file);
importRun = importRunService.addImportRun(SecurityUtils.getCurrentUsername(), Boolean.TRUE.equals(notify));
asyncImportJobs.execute(
new ImportJob(importService, SecurityContextHolder.getContext(), repositoryCollection, databaseAction,
importRun.getId(), importRunService, request.getSession(), PACKAGE_DEFAULT));
return importRun;
}
private DatabaseAction getDatabaseAction(File file, String action)
{
DatabaseAction databaseAction = DatabaseAction.ADD;
if (action != null)
{
try
{
databaseAction = DatabaseAction.valueOf(action.toUpperCase());
}
catch (IllegalArgumentException e)
{
throw new IllegalArgumentException(
"Invalid action:[" + action.toUpperCase() + "] valid values: " + (Arrays
.toString(DatabaseAction.values())));
}
}
String extension = FileExtensionUtils
.findExtensionFromPossibilities(file.getName(), GenericImporterExtensions.getAll());
if (extension.equals("vcf") || extension.equals("vcf.gz") || extension.equals("vcf.zip"))
{
NameValidator.validateName(file.getName().replace("." + extension, ""));
if (!DatabaseAction.ADD.equals(databaseAction))
{
throw new IllegalArgumentException(
"Update mode " + databaseAction + " is not supported, only ADD is supported for VCF");
}
}
return databaseAction;
}
}