Fling packaging and authorization

Allow users with a claim to the fling to download a fling as zip archive.
Package all artifacts belonging to a fling as zip file.
This commit is contained in:
Armin Friedl 2020-05-24 19:52:14 +02:00
parent 0fe28d3db8
commit f502402cb6
Signed by: armin
GPG key ID: 48C726EEE7FBCBC8
5 changed files with 93 additions and 13 deletions

View file

@ -1,8 +1,13 @@
package net.friedl.fling.controller; package net.friedl.fling.controller;
import java.io.IOException;
import java.util.List; import java.util.List;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@ -15,6 +20,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import net.friedl.fling.model.dto.FlingDto; import net.friedl.fling.model.dto.FlingDto;
import net.friedl.fling.persistence.archive.ArchiveException;
import net.friedl.fling.service.FlingService; import net.friedl.fling.service.FlingService;
@RestController @RestController
@ -62,4 +68,19 @@ public class FlingController {
public void deleteFling(@PathVariable Long flingId) { public void deleteFling(@PathVariable Long flingId) {
flingService.deleteFlingById(flingId); flingService.deleteFlingById(flingId);
} }
@GetMapping(path = "/fling/{flingId}/download")
public ResponseEntity<Resource> downloadFling(@PathVariable Long flingId) throws ArchiveException, IOException {
var fling = flingService.findFlingById(flingId).orElseThrow();
var flingPackage = flingService.packageFling(flingId);
var stream = new InputStreamResource(flingPackage.getFirst());
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=\"" + fling.getName() + ".zip" + "\"")
.contentLength(flingPackage.getSecond())
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(stream);
}
} }

View file

@ -58,20 +58,21 @@ public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
.permitAll() .permitAll()
.and() .and()
.authorizeRequests() .authorizeRequests()
.antMatchers("/api/artifacts/{artifactId}/{downloadId}/download") .antMatchers(HttpMethod.GET, "/api/fling/{flingId}/download")
.permitAll() .hasAuthority(FlingAuthority.FLING_USER.name())
.and() .and()
.authorizeRequests() .authorizeRequests()
.antMatchers("/api/**") .antMatchers("/api/**")
.hasAuthority(FlingAuthority.FLING_OWNER.name()) .hasAuthority(FlingAuthority.FLING_OWNER.name())
.and() .and()
.authorizeRequests() .authorizeRequests()
.antMatchers(HttpMethod.POST, "/api/artifacts/{flingId}/**") // TODO: This is still insecure since URLs are not encrypted
.access("hasAuthority('"+FlingAuthority.FLING_USER.name()+"') and @authorizationService.allowUpload(#flingId)") .antMatchers("/api/artifacts/{artifactId}/{downloadId}/download")
.permitAll()
.and() .and()
.authorizeRequests() .authorizeRequests()
.antMatchers(HttpMethod.GET, "/api/artifacts/**") .antMatchers(HttpMethod.POST, "/api/artifacts/{flingId}/**")
.hasAuthority(FlingAuthority.FLING_USER.name()); .access("hasAuthority('"+FlingAuthority.FLING_USER.name()+"') and @authorizationService.allowUpload(#flingId)");
//@formatter:on //@formatter:on
} }

View file

@ -79,7 +79,9 @@ public class AuthenticationService {
break; break;
case "user": case "user":
authority = FlingAuthority.FLING_USER; authority = FlingAuthority.FLING_USER;
flingId = claims.get("fid", Long.class); var sid = claims.get("sid", String.class);
flingId = flingService.findFlingByShareId(sid).orElseThrow().getId();
break;
default: default:
throw new BadCredentialsException("Invalid token"); throw new BadCredentialsException("Invalid token");
} }

View file

@ -1,15 +1,24 @@
package net.friedl.fling.service; package net.friedl.fling.service;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.Base64; import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.transaction.Transactional; import javax.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.util.Pair;
import org.springframework.security.crypto.codec.Hex; import org.springframework.security.crypto.codec.Hex;
import org.springframework.security.crypto.keygen.KeyGenerators; import org.springframework.security.crypto.keygen.KeyGenerators;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -18,6 +27,9 @@ import org.springframework.util.StringUtils;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.friedl.fling.model.dto.FlingDto; import net.friedl.fling.model.dto.FlingDto;
import net.friedl.fling.model.mapper.FlingMapper; import net.friedl.fling.model.mapper.FlingMapper;
import net.friedl.fling.persistence.archive.Archive;
import net.friedl.fling.persistence.archive.ArchiveException;
import net.friedl.fling.persistence.entities.ArtifactEntity;
import net.friedl.fling.persistence.entities.FlingEntity; import net.friedl.fling.persistence.entities.FlingEntity;
import net.friedl.fling.persistence.repositories.FlingRepository; import net.friedl.fling.persistence.repositories.FlingRepository;
@ -25,16 +37,17 @@ import net.friedl.fling.persistence.repositories.FlingRepository;
@Service @Service
@Transactional @Transactional
public class FlingService { public class FlingService {
private FlingRepository flingRepository; private FlingRepository flingRepository;
private FlingMapper flingMapper; private FlingMapper flingMapper;
private Archive archive;
private MessageDigest keyHashDigest; private MessageDigest keyHashDigest;
@Autowired @Autowired
public FlingService(FlingRepository flingRepository, FlingMapper flingMapper, MessageDigest keyHashDigest) { public FlingService(FlingRepository flingRepository, FlingMapper flingMapper, Archive archive, MessageDigest keyHashDigest) {
this.flingRepository = flingRepository; this.flingRepository = flingRepository;
this.flingMapper = flingMapper; this.flingMapper = flingMapper;
this.archive = archive;
this.keyHashDigest = keyHashDigest; this.keyHashDigest = keyHashDigest;
} }
@ -106,6 +119,41 @@ public class FlingService {
return flingRepository.countArtifactsById(flingId); return flingRepository.countArtifactsById(flingId);
} }
public Long getFlingSize(Long flingId) {
var fling = flingRepository.getOne(flingId);
return fling.getArtifacts().stream()
.map(ae -> ae.getSize())
.reduce(0L, (acc, as) -> acc+as);
}
public Pair<InputStream, Long> packageFling(Long flingId) throws IOException, ArchiveException {
var fling = flingRepository.getOne(flingId);
var tempFile = Files.createTempFile(Long.toString(flingId), ".zip");
try(var zipStream = new ZipOutputStream(new FileOutputStream(tempFile.toFile()))){
for(ArtifactEntity artifactEntity: fling.getArtifacts()) {
ZipEntry ze = new ZipEntry(artifactEntity.getName());
zipStream.putNextEntry(ze);
var artifactStream = archive.get(artifactEntity.getDoi());
try(var archiveEntryStream = new BufferedInputStream(artifactStream)) {
int b;
while( (b = archiveEntryStream.read()) != -1 ) {
zipStream.write(b);
}
} finally {
zipStream.closeEntry();
}
}
}
var archiveLength = tempFile.toFile().length();
var archiveStream = new FileInputStream(tempFile.toFile());
return Pair.of(archiveStream, archiveLength);
}
public String generateShareUrl() { public String generateShareUrl() {
var key = KeyGenerators var key = KeyGenerators
.secureRandom(16) .secureRandom(16)

View file

@ -1,10 +1,18 @@
import log from 'loglevel'; import log from 'loglevel';
import React, {useState} from 'react'; import React, {useState, useEffect} from 'react';
import {useParams, BrowserRouter} from 'react-router-dom'; import {useParams, BrowserRouter} from 'react-router-dom';
export default function FlingAdmin() { import {flingClient} from '../../util/flingclient';
let { fling } = useParams();
export default function FlingUser() {
let { shareId } = useParams();
let [fling, setFling] = useState({});
useEffect(() => {
flingClient.getFlingByShareId(shareId)
.then(f => setFling(f));
}, [shareId]);
return( return(
<div> <div>