From f502402cb6e39c8766c860d9854e3f8c7550d458 Mon Sep 17 00:00:00 2001 From: Armin Friedl Date: Sun, 24 May 2020 19:52:14 +0200 Subject: [PATCH] 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. --- .../fling/controller/FlingController.java | 21 ++++++++ .../security/FlingWebSecurityConfigurer.java | 13 ++--- .../authentication/AuthenticationService.java | 4 +- .../friedl/fling/service/FlingService.java | 54 +++++++++++++++++-- web/fling/src/components/user/FlingUser.jsx | 14 +++-- 5 files changed, 93 insertions(+), 13 deletions(-) diff --git a/service/fling/src/main/java/net/friedl/fling/controller/FlingController.java b/service/fling/src/main/java/net/friedl/fling/controller/FlingController.java index 5001ac8..1e85ce9 100644 --- a/service/fling/src/main/java/net/friedl/fling/controller/FlingController.java +++ b/service/fling/src/main/java/net/friedl/fling/controller/FlingController.java @@ -1,8 +1,13 @@ package net.friedl.fling.controller; +import java.io.IOException; import java.util.List; 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.web.bind.annotation.DeleteMapping; 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 net.friedl.fling.model.dto.FlingDto; +import net.friedl.fling.persistence.archive.ArchiveException; import net.friedl.fling.service.FlingService; @RestController @@ -62,4 +68,19 @@ public class FlingController { public void deleteFling(@PathVariable Long flingId) { flingService.deleteFlingById(flingId); } + + + @GetMapping(path = "/fling/{flingId}/download") + public ResponseEntity 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); + } + } diff --git a/service/fling/src/main/java/net/friedl/fling/security/FlingWebSecurityConfigurer.java b/service/fling/src/main/java/net/friedl/fling/security/FlingWebSecurityConfigurer.java index a8e6118..0495071 100644 --- a/service/fling/src/main/java/net/friedl/fling/security/FlingWebSecurityConfigurer.java +++ b/service/fling/src/main/java/net/friedl/fling/security/FlingWebSecurityConfigurer.java @@ -58,20 +58,21 @@ public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter { .permitAll() .and() .authorizeRequests() - .antMatchers("/api/artifacts/{artifactId}/{downloadId}/download") - .permitAll() + .antMatchers(HttpMethod.GET, "/api/fling/{flingId}/download") + .hasAuthority(FlingAuthority.FLING_USER.name()) .and() .authorizeRequests() .antMatchers("/api/**") .hasAuthority(FlingAuthority.FLING_OWNER.name()) .and() .authorizeRequests() - .antMatchers(HttpMethod.POST, "/api/artifacts/{flingId}/**") - .access("hasAuthority('"+FlingAuthority.FLING_USER.name()+"') and @authorizationService.allowUpload(#flingId)") + // TODO: This is still insecure since URLs are not encrypted + .antMatchers("/api/artifacts/{artifactId}/{downloadId}/download") + .permitAll() .and() .authorizeRequests() - .antMatchers(HttpMethod.GET, "/api/artifacts/**") - .hasAuthority(FlingAuthority.FLING_USER.name()); + .antMatchers(HttpMethod.POST, "/api/artifacts/{flingId}/**") + .access("hasAuthority('"+FlingAuthority.FLING_USER.name()+"') and @authorizationService.allowUpload(#flingId)"); //@formatter:on } diff --git a/service/fling/src/main/java/net/friedl/fling/security/authentication/AuthenticationService.java b/service/fling/src/main/java/net/friedl/fling/security/authentication/AuthenticationService.java index 62ab908..5d71c8c 100644 --- a/service/fling/src/main/java/net/friedl/fling/security/authentication/AuthenticationService.java +++ b/service/fling/src/main/java/net/friedl/fling/security/authentication/AuthenticationService.java @@ -79,7 +79,9 @@ public class AuthenticationService { break; case "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: throw new BadCredentialsException("Invalid token"); } diff --git a/service/fling/src/main/java/net/friedl/fling/service/FlingService.java b/service/fling/src/main/java/net/friedl/fling/service/FlingService.java index 12b9542..b336d02 100644 --- a/service/fling/src/main/java/net/friedl/fling/service/FlingService.java +++ b/service/fling/src/main/java/net/friedl/fling/service/FlingService.java @@ -1,15 +1,24 @@ 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.util.Base64; import java.util.List; import java.util.Optional; import java.util.function.Consumer; import java.util.function.Supplier; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; import javax.transaction.Transactional; 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.keygen.KeyGenerators; import org.springframework.stereotype.Service; @@ -18,6 +27,9 @@ import org.springframework.util.StringUtils; import lombok.extern.slf4j.Slf4j; import net.friedl.fling.model.dto.FlingDto; 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.repositories.FlingRepository; @@ -25,16 +37,17 @@ import net.friedl.fling.persistence.repositories.FlingRepository; @Service @Transactional public class FlingService { + private FlingRepository flingRepository; - private FlingMapper flingMapper; - + private Archive archive; private MessageDigest keyHashDigest; @Autowired - public FlingService(FlingRepository flingRepository, FlingMapper flingMapper, MessageDigest keyHashDigest) { + public FlingService(FlingRepository flingRepository, FlingMapper flingMapper, Archive archive, MessageDigest keyHashDigest) { this.flingRepository = flingRepository; this.flingMapper = flingMapper; + this.archive = archive; this.keyHashDigest = keyHashDigest; } @@ -106,6 +119,41 @@ public class FlingService { 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 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() { var key = KeyGenerators .secureRandom(16) diff --git a/web/fling/src/components/user/FlingUser.jsx b/web/fling/src/components/user/FlingUser.jsx index 1532a57..8c64bec 100644 --- a/web/fling/src/components/user/FlingUser.jsx +++ b/web/fling/src/components/user/FlingUser.jsx @@ -1,10 +1,18 @@ import log from 'loglevel'; -import React, {useState} from 'react'; +import React, {useState, useEffect} from 'react'; import {useParams, BrowserRouter} from 'react-router-dom'; -export default function FlingAdmin() { - let { fling } = useParams(); +import {flingClient} from '../../util/flingclient'; + +export default function FlingUser() { + let { shareId } = useParams(); + let [fling, setFling] = useState({}); + + useEffect(() => { + flingClient.getFlingByShareId(shareId) + .then(f => setFling(f)); + }, [shareId]); return(