From 415687c60153350ed6f6ab680582faf56754dde4 Mon Sep 17 00:00:00 2001 From: Armin Friedl Date: Fri, 10 Jul 2020 06:55:39 +0200 Subject: [PATCH] Authorization and Authentication refactoring and tests --- .../net/friedl/fling/FlingConfiguration.java | 9 +- .../FlingSecurityConfiguration.java | 23 +-- .../fling/controller/ArtifactController.java | 7 +- .../controller/AuthenticationController.java | 53 ++++++ .../fling/controller/FlingController.java | 8 +- .../controller/OpenApiConfiguration.java | 2 +- .../friedl/fling/model/dto/AdminAuthDto.java | 21 +++ .../net/friedl/fling/model/dto/FlingDto.java | 1 - .../friedl/fling/model/dto/UserAuthDto.java | 21 +++ .../fling/model/json/PathDeserializer.java | 2 +- .../fling/model/json/PathSerializer.java | 3 +- .../fling/model/mapper/ArtifactMapper.java | 2 + .../fling/model/mapper/FlingMapper.java | 2 + .../persistence/entities/ArtifactEntity.java | 5 +- .../persistence/entities/FlingEntity.java | 3 +- .../fling/security/AuthorizationService.java | 71 -------- .../fling/security/FlingAuthorities.java | 32 ++++ .../friedl/fling/security/FlingAuthority.java | 5 - .../security/FlingWebSecurityConfigurer.java | 14 +- .../AuthenticationController.java | 33 ---- .../authentication/AuthenticationService.java | 101 ----------- .../authentication/FlingAdminAuthority.java | 15 ++ .../security/authentication/FlingToken.java | 25 ++- .../authentication/FlingUserAuthority.java | 25 +++ .../authentication/GrantedFlingAuthority.java | 33 ---- .../JwtAuthenticationFilter.java | 15 +- .../authentication/dto/OwnerAuthDto.java | 11 -- .../authentication/dto/UserAuthDto.java | 11 -- .../friedl/fling/service/ArtifactService.java | 4 +- .../fling/service/AuthenticationService.java | 116 +++++++++++++ .../fling/service/AuthorizationService.java | 68 ++++++++ .../friedl/fling/service/FlingService.java | 14 +- .../fling/service/archive/ArchiveService.java | 6 +- .../archive/impl/FileSystemArchive.java | 4 +- .../spring-configuration-metadata.json | 2 +- .../src/main/resources/application-local.yml | 8 +- .../src/main/resources/application-prod.yml | 16 +- .../controller/ArtifactControllerTest.java | 6 +- .../AuthenticationControllerTest.java | 87 ++++++++++ .../fling/controller/FlingControllerTest.java | 20 +++ .../net/friedl/fling/model/FlingDtoTest.java | 8 +- .../fling/service/ArtifactServiceTest.java | 25 +-- .../service/AuthenticationServiceTest.java | 159 ++++++++++++++++++ .../service/AuthorizationServiceTest.java | 116 +++++++++++++ .../fling/service/FlingServiceTest.java | 51 +++--- .../archive/FileSystemArchiveTest.java | 8 +- .../resources/application-test.properties | 3 + 47 files changed, 886 insertions(+), 388 deletions(-) rename service/fling/src/main/java/net/friedl/fling/{security => }/FlingSecurityConfiguration.java (72%) create mode 100644 service/fling/src/main/java/net/friedl/fling/controller/AuthenticationController.java create mode 100644 service/fling/src/main/java/net/friedl/fling/model/dto/AdminAuthDto.java create mode 100644 service/fling/src/main/java/net/friedl/fling/model/dto/UserAuthDto.java delete mode 100644 service/fling/src/main/java/net/friedl/fling/security/AuthorizationService.java create mode 100644 service/fling/src/main/java/net/friedl/fling/security/FlingAuthorities.java delete mode 100644 service/fling/src/main/java/net/friedl/fling/security/FlingAuthority.java delete mode 100644 service/fling/src/main/java/net/friedl/fling/security/authentication/AuthenticationController.java delete mode 100644 service/fling/src/main/java/net/friedl/fling/security/authentication/AuthenticationService.java create mode 100644 service/fling/src/main/java/net/friedl/fling/security/authentication/FlingAdminAuthority.java create mode 100644 service/fling/src/main/java/net/friedl/fling/security/authentication/FlingUserAuthority.java delete mode 100644 service/fling/src/main/java/net/friedl/fling/security/authentication/GrantedFlingAuthority.java delete mode 100644 service/fling/src/main/java/net/friedl/fling/security/authentication/dto/OwnerAuthDto.java delete mode 100644 service/fling/src/main/java/net/friedl/fling/security/authentication/dto/UserAuthDto.java create mode 100644 service/fling/src/main/java/net/friedl/fling/service/AuthenticationService.java create mode 100644 service/fling/src/main/java/net/friedl/fling/service/AuthorizationService.java create mode 100644 service/fling/src/test/java/net/friedl/fling/controller/AuthenticationControllerTest.java create mode 100644 service/fling/src/test/java/net/friedl/fling/service/AuthenticationServiceTest.java create mode 100644 service/fling/src/test/java/net/friedl/fling/service/AuthorizationServiceTest.java create mode 100644 service/fling/src/test/resources/application-test.properties diff --git a/service/fling/src/main/java/net/friedl/fling/FlingConfiguration.java b/service/fling/src/main/java/net/friedl/fling/FlingConfiguration.java index 7f97420..979d530 100644 --- a/service/fling/src/main/java/net/friedl/fling/FlingConfiguration.java +++ b/service/fling/src/main/java/net/friedl/fling/FlingConfiguration.java @@ -1,10 +1,10 @@ package net.friedl.fling; import java.nio.file.Path; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.argon2.Argon2PasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; @@ -16,9 +16,10 @@ import net.friedl.fling.model.json.PathSerializer; @Configuration public class FlingConfiguration { + @Bean - public MessageDigest keyHashDigest() throws NoSuchAlgorithmException { - return MessageDigest.getInstance("SHA-512"); + public PasswordEncoder passwordEncoder() { + return new Argon2PasswordEncoder(); } @Bean diff --git a/service/fling/src/main/java/net/friedl/fling/security/FlingSecurityConfiguration.java b/service/fling/src/main/java/net/friedl/fling/FlingSecurityConfiguration.java similarity index 72% rename from service/fling/src/main/java/net/friedl/fling/security/FlingSecurityConfiguration.java rename to service/fling/src/main/java/net/friedl/fling/FlingSecurityConfiguration.java index ac75054..43b437e 100644 --- a/service/fling/src/main/java/net/friedl/fling/security/FlingSecurityConfiguration.java +++ b/service/fling/src/main/java/net/friedl/fling/FlingSecurityConfiguration.java @@ -1,8 +1,7 @@ -package net.friedl.fling.security; +package net.friedl.fling; import java.nio.charset.StandardCharsets; import java.security.Key; -import java.util.List; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -15,26 +14,18 @@ import lombok.Data; @Configuration @ConfigurationProperties("fling.security") public class FlingSecurityConfiguration { - private List allowedOrigins; - - private String adminUser; - - private String adminPassword; - private String signingKey; - private Long jwtExpiration; + @Bean + public JwtParser jwtParser() { + return Jwts.parserBuilder() + .setSigningKey(jwtSigningKey()) + .build(); + } @Bean public Key jwtSigningKey() { byte[] key = signingKey.getBytes(StandardCharsets.UTF_8); return Keys.hmacShaKeyFor(key); } - - @Bean - public JwtParser jwtParser(Key jwtSignigKey) { - return Jwts.parserBuilder() - .setSigningKey(jwtSignigKey) - .build(); - } } diff --git a/service/fling/src/main/java/net/friedl/fling/controller/ArtifactController.java b/service/fling/src/main/java/net/friedl/fling/controller/ArtifactController.java index 15f1c4d..7dcde8e 100644 --- a/service/fling/src/main/java/net/friedl/fling/controller/ArtifactController.java +++ b/service/fling/src/main/java/net/friedl/fling/controller/ArtifactController.java @@ -9,6 +9,7 @@ import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -27,6 +28,7 @@ import net.friedl.fling.service.archive.ArchiveService; @RestController @RequestMapping("/api/artifacts") @Tag(name = "artifact", description = "Operations on /api/artifacts") +@Validated public class ArtifactController { private ArtifactService artifactService; @@ -52,8 +54,9 @@ public class ArtifactController { @RequestBody(content = @Content(schema = @Schema(type = "string", format = "binary"))) @PostMapping(path = "/{id}/data") - public void uploadArtifactData(@PathVariable UUID id, HttpServletRequest request) throws IOException { - archiveService.storeArtifact(id, request.getInputStream()); + public void uploadArtifactData(@PathVariable UUID id, HttpServletRequest request) + throws IOException { + archiveService.storeArtifact(id, request.getInputStream()); } @ApiResponse(responseCode = "200", diff --git a/service/fling/src/main/java/net/friedl/fling/controller/AuthenticationController.java b/service/fling/src/main/java/net/friedl/fling/controller/AuthenticationController.java new file mode 100644 index 0000000..ec896fe --- /dev/null +++ b/service/fling/src/main/java/net/friedl/fling/controller/AuthenticationController.java @@ -0,0 +1,53 @@ +package net.friedl.fling.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import net.friedl.fling.model.dto.AdminAuthDto; +import net.friedl.fling.model.dto.UserAuthDto; +import net.friedl.fling.service.AuthenticationService; + +@RestController +@RequestMapping("/api/auth") +@Tag(name = "auth", description = "Operations on /api/auth") +@Validated +public class AuthenticationController { + + private AuthenticationService authenticationService; + + @Autowired + public AuthenticationController(AuthenticationService authenticationService) { + this.authenticationService = authenticationService; + } + + @Operation(description = "Authenticates the fling admin by username and password") + @ApiResponse(responseCode = "200", + description = "JWT Token authenticating the admin of this fling instance") + @ApiResponse(responseCode = "403", + description = "Authentication failed, username or password are wrong") + @PostMapping(path = "/admin") + public String authenticateOwner(@RequestBody AdminAuthDto adminAuthDto) { + return authenticationService.authenticate(adminAuthDto) + .orElseThrow(() -> new AccessDeniedException("Wrong username or password")); + } + + @Operation(description = "Authenticates a fling user for a fling via code") + @ApiResponse(responseCode = "200", + description = "JWT Token authenticating the user for a fling") + @ApiResponse(responseCode = "403", + description = "Authentication failed, the provided code for the fling is wrong") + @ApiResponse(responseCode = "404", + description = "No fling for the given share id exists") + @PostMapping("/user") + public String authenticateUser(@RequestBody UserAuthDto userAuthDto) { + return authenticationService.authenticate(userAuthDto) + .orElseThrow(() -> new AccessDeniedException("Wrong username or password")); + } +} 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 bdd5163..43928e2 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 @@ -3,6 +3,7 @@ package net.friedl.fling.controller; import java.io.IOException; import java.util.List; import java.util.UUID; +import javax.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; @@ -51,12 +52,13 @@ public class FlingController { } @PostMapping - public FlingDto postFling(@RequestBody FlingDto flingDto) { + public FlingDto postFling(@RequestBody @Valid FlingDto flingDto) { return flingService.create(flingDto); } @PostMapping("/{id}/artifact") - public ArtifactDto postArtifact(@PathVariable UUID id, @RequestBody ArtifactDto artifactDto) { + public ArtifactDto postArtifact(@PathVariable UUID id, + @RequestBody @Valid ArtifactDto artifactDto) { return artifactService.create(id, artifactDto); } @@ -76,7 +78,7 @@ public class FlingController { } @Operation(responses = { - @ApiResponse(responseCode = "200", + @ApiResponse(responseCode = "200", content = @Content( mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE, schema = @Schema(type = "string", format = "binary"))) diff --git a/service/fling/src/main/java/net/friedl/fling/controller/OpenApiConfiguration.java b/service/fling/src/main/java/net/friedl/fling/controller/OpenApiConfiguration.java index 8bf33c9..bd57543 100644 --- a/service/fling/src/main/java/net/friedl/fling/controller/OpenApiConfiguration.java +++ b/service/fling/src/main/java/net/friedl/fling/controller/OpenApiConfiguration.java @@ -46,7 +46,7 @@ public class OpenApiConfiguration { .scheme("bearer") .bearerFormat("JWT"); } - + public Info apiInfo() { return new Info() .contact(new Contact() diff --git a/service/fling/src/main/java/net/friedl/fling/model/dto/AdminAuthDto.java b/service/fling/src/main/java/net/friedl/fling/model/dto/AdminAuthDto.java new file mode 100644 index 0000000..31e981e --- /dev/null +++ b/service/fling/src/main/java/net/friedl/fling/model/dto/AdminAuthDto.java @@ -0,0 +1,21 @@ +package net.friedl.fling.model.dto; + +import javax.validation.constraints.NotNull; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Data +@RequiredArgsConstructor +@AllArgsConstructor +@Builder +@Schema(name = "AdminAuth") +public class AdminAuthDto { + @NotNull + private String adminName; + + @NotNull + private String adminPassword; +} diff --git a/service/fling/src/main/java/net/friedl/fling/model/dto/FlingDto.java b/service/fling/src/main/java/net/friedl/fling/model/dto/FlingDto.java index 0403e0b..0d2da8f 100644 --- a/service/fling/src/main/java/net/friedl/fling/model/dto/FlingDto.java +++ b/service/fling/src/main/java/net/friedl/fling/model/dto/FlingDto.java @@ -31,7 +31,6 @@ public class FlingDto { private Instant creationTime = Instant.now(); @Schema(description = "Share id of the fling. Used in the share link.") - @NotNull private String shareId; @Schema(description = "Authentication code for password protecting a fling.") diff --git a/service/fling/src/main/java/net/friedl/fling/model/dto/UserAuthDto.java b/service/fling/src/main/java/net/friedl/fling/model/dto/UserAuthDto.java new file mode 100644 index 0000000..255b908 --- /dev/null +++ b/service/fling/src/main/java/net/friedl/fling/model/dto/UserAuthDto.java @@ -0,0 +1,21 @@ +package net.friedl.fling.model.dto; + +import javax.validation.constraints.NotNull; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Data +@RequiredArgsConstructor +@AllArgsConstructor +@Builder +@Schema(name = "UserAuth") +public class UserAuthDto { + @NotNull + String shareId; + + @NotNull + String authCode; +} diff --git a/service/fling/src/main/java/net/friedl/fling/model/json/PathDeserializer.java b/service/fling/src/main/java/net/friedl/fling/model/json/PathDeserializer.java index be33cfd..03406dc 100644 --- a/service/fling/src/main/java/net/friedl/fling/model/json/PathDeserializer.java +++ b/service/fling/src/main/java/net/friedl/fling/model/json/PathDeserializer.java @@ -27,7 +27,7 @@ public class PathDeserializer extends StdDeserializer { ObjectCodec codec = p.getCodec(); JsonNode node = codec.readTree(p); - + return Paths.get(node.textValue()); } diff --git a/service/fling/src/main/java/net/friedl/fling/model/json/PathSerializer.java b/service/fling/src/main/java/net/friedl/fling/model/json/PathSerializer.java index cc25022..5d3a227 100644 --- a/service/fling/src/main/java/net/friedl/fling/model/json/PathSerializer.java +++ b/service/fling/src/main/java/net/friedl/fling/model/json/PathSerializer.java @@ -19,7 +19,8 @@ public class PathSerializer extends StdSerializer { private static final long serialVersionUID = -1003917305429893614L; @Override - public void serialize(Path value, JsonGenerator gen, SerializerProvider provider) throws IOException { + public void serialize(Path value, JsonGenerator gen, SerializerProvider provider) + throws IOException { gen.writeString(value.toString()); } diff --git a/service/fling/src/main/java/net/friedl/fling/model/mapper/ArtifactMapper.java b/service/fling/src/main/java/net/friedl/fling/model/mapper/ArtifactMapper.java index b9091a2..aa917e3 100644 --- a/service/fling/src/main/java/net/friedl/fling/model/mapper/ArtifactMapper.java +++ b/service/fling/src/main/java/net/friedl/fling/model/mapper/ArtifactMapper.java @@ -8,8 +8,10 @@ import net.friedl.fling.persistence.entities.ArtifactEntity; @Mapper(componentModel = "spring") public interface ArtifactMapper { ArtifactDto map(ArtifactEntity artifactEntity); + ArtifactEntity map(ArtifactDto artifactDto); List mapEntities(List artifactEntities); + List mapDtos(List artifactDtos); } diff --git a/service/fling/src/main/java/net/friedl/fling/model/mapper/FlingMapper.java b/service/fling/src/main/java/net/friedl/fling/model/mapper/FlingMapper.java index 32b5184..7284ee3 100644 --- a/service/fling/src/main/java/net/friedl/fling/model/mapper/FlingMapper.java +++ b/service/fling/src/main/java/net/friedl/fling/model/mapper/FlingMapper.java @@ -8,8 +8,10 @@ import net.friedl.fling.persistence.entities.FlingEntity; @Mapper(componentModel = "spring") public interface FlingMapper { FlingDto map(FlingEntity flingEntity); + FlingEntity map(FlingDto flingDto); List mapEntities(List flingEntities); + List mapDtos(List flingDtos); } diff --git a/service/fling/src/main/java/net/friedl/fling/persistence/entities/ArtifactEntity.java b/service/fling/src/main/java/net/friedl/fling/persistence/entities/ArtifactEntity.java index 8cf5dca..1c3b6dc 100644 --- a/service/fling/src/main/java/net/friedl/fling/persistence/entities/ArtifactEntity.java +++ b/service/fling/src/main/java/net/friedl/fling/persistence/entities/ArtifactEntity.java @@ -14,7 +14,8 @@ import lombok.Setter; @Entity @Table(name = "Artifact") -@Getter @Setter +@Getter +@Setter public class ArtifactEntity { @Id @GeneratedValue @@ -25,7 +26,7 @@ public class ArtifactEntity { @Column(nullable = false) private Instant uploadTime = Instant.now(); - + @Column(unique = true, nullable = true) private String archiveId; diff --git a/service/fling/src/main/java/net/friedl/fling/persistence/entities/FlingEntity.java b/service/fling/src/main/java/net/friedl/fling/persistence/entities/FlingEntity.java index afcd94d..a3f46e5 100644 --- a/service/fling/src/main/java/net/friedl/fling/persistence/entities/FlingEntity.java +++ b/service/fling/src/main/java/net/friedl/fling/persistence/entities/FlingEntity.java @@ -15,7 +15,8 @@ import lombok.Setter; @Entity @Table(name = "Fling") -@Getter @Setter +@Getter +@Setter public class FlingEntity { @Id @GeneratedValue diff --git a/service/fling/src/main/java/net/friedl/fling/security/AuthorizationService.java b/service/fling/src/main/java/net/friedl/fling/security/AuthorizationService.java deleted file mode 100644 index 05cf99c..0000000 --- a/service/fling/src/main/java/net/friedl/fling/security/AuthorizationService.java +++ /dev/null @@ -1,71 +0,0 @@ -package net.friedl.fling.security; - -import java.util.UUID; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.authentication.AbstractAuthenticationToken; -import org.springframework.stereotype.Service; -import lombok.extern.slf4j.Slf4j; -import net.friedl.fling.security.authentication.FlingToken; -import net.friedl.fling.security.authentication.dto.UserAuthDto; -import net.friedl.fling.service.FlingService; - -@Slf4j -@Service -public class AuthorizationService { - private FlingService flingService; - - @Autowired - public AuthorizationService(FlingService flingService) { - this.flingService = flingService; - } - - public boolean allowUpload(UUID flingId, AbstractAuthenticationToken token) { - if (!(token instanceof FlingToken)) { - log.warn("Authorization attempt without fling token for {}. Authorization denied.", flingId); - return false; - } - - FlingToken flingToken = (FlingToken) token; - if (FlingAuthority.FLING_OWNER.name() - .equals(flingToken.getGrantedFlingAuthority().getAuthority())) { - log.debug("Owner authorized for upload [id = {}]", flingId); - return true; - } - - boolean uploadAllowed = flingService.getById(flingId).getAllowUpload(); - boolean authorized = uploadAllowed - && flingToken.getGrantedFlingAuthority().getFlingId().equals(flingId); - - log.debug("User {} authorized for upload [id = {}]", authorized ? "" : "not", flingId); - - return authorized; - } - - public boolean allowFlingAccess(UUID flingId, AbstractAuthenticationToken token) { - if (!(token instanceof FlingToken)) { - log.warn("Authorization attempt without fling token for {}. Authorization denied.", flingId); - return false; - } - - FlingToken flingToken = (FlingToken) token; - if (FlingAuthority.FLING_OWNER.name() - .equals(flingToken.getGrantedFlingAuthority().getAuthority())) { - log.debug("Owner authorized for fling access [id = {}]", flingId); - return true; - } - - boolean authorized = flingToken.getGrantedFlingAuthority().getFlingId().equals(flingId); - log.debug("User {} authorized for fling access [id = {}]", authorized ? "" : "not", flingId); - - return authorized; - } - - public boolean allowFlingAccess(UserAuthDto userAuth, String shareId) { - boolean authorized = userAuth.getShareId().equals(shareId); - log.debug("User {} authorized for fling access [shareId = {}]", authorized ? "" : "not", - shareId); - - return authorized; - } - -} diff --git a/service/fling/src/main/java/net/friedl/fling/security/FlingAuthorities.java b/service/fling/src/main/java/net/friedl/fling/security/FlingAuthorities.java new file mode 100644 index 0000000..3dd4698 --- /dev/null +++ b/service/fling/src/main/java/net/friedl/fling/security/FlingAuthorities.java @@ -0,0 +1,32 @@ +package net.friedl.fling.security; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +public enum FlingAuthorities { + FLING_ADMIN("admin"), FLING_USER("user"); + + String authority; + + FlingAuthorities(String authority) { + this.authority = authority; + } + + public boolean verify(String authority) { + return this.authority.equals(authority); + } + + public boolean verify(AbstractAuthenticationToken authenticationToken) { + return authenticationToken.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .anyMatch(this.authority::equals); + } + + public boolean verify(GrantedAuthority grantedAuthority) { + return this.authority.equals(grantedAuthority.getAuthority()); + } + + public String getAuthority() { + return authority; + } +} diff --git a/service/fling/src/main/java/net/friedl/fling/security/FlingAuthority.java b/service/fling/src/main/java/net/friedl/fling/security/FlingAuthority.java deleted file mode 100644 index 78b7f56..0000000 --- a/service/fling/src/main/java/net/friedl/fling/security/FlingAuthority.java +++ /dev/null @@ -1,5 +0,0 @@ -package net.friedl.fling.security; - -public enum FlingAuthority { - FLING_OWNER, FLING_USER -} 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 69580f4..e3d855c 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 @@ -3,6 +3,7 @@ package net.friedl.fling.security; import static org.springframework.security.config.Customizer.withDefaults; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; @@ -16,7 +17,9 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import net.friedl.fling.FlingSecurityConfiguration; import net.friedl.fling.security.authentication.JwtAuthenticationFilter; +import net.friedl.fling.service.AuthorizationService; @Slf4j @Configuration @@ -24,9 +27,11 @@ import net.friedl.fling.security.authentication.JwtAuthenticationFilter; @Getter @Setter public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter { + @Value("fling.security.allowedOrigins") + private List allowedOrigins; + private JwtAuthenticationFilter jwtAuthenticationFilter; private AuthorizationService authorizationService; - private FlingSecurityConfiguration securityConfiguration; @Autowired public FlingWebSecurityConfigurer(JwtAuthenticationFilter jwtAuthenticationFilter, @@ -35,7 +40,6 @@ public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter { this.jwtAuthenticationFilter = jwtAuthenticationFilter; this.authorizationService = authorizationService; - this.securityConfiguration = securityConfiguraiton; } @Override @@ -91,7 +95,7 @@ public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter { // And lastly, the owner is allowed everything .authorizeRequests() .antMatchers("/api/**") - .hasAuthority(FlingAuthority.FLING_OWNER.name()); + .hasAuthority(FlingAuthorities.FLING_ADMIN.getAuthority()); //@formatter:on } @@ -100,10 +104,10 @@ public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter { public CorsConfigurationSource corsConfigurationSource() { // see https://stackoverflow.com/a/43559266 - log.info("Allowed origins: {}", securityConfiguration.getAllowedOrigins()); + log.info("Allowed origins: {}", allowedOrigins); CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(securityConfiguration.getAllowedOrigins()); + configuration.setAllowedOrigins(allowedOrigins); configuration.setAllowedMethods(List.of("*")); // setAllowCredentials(true) is important, otherwise: diff --git a/service/fling/src/main/java/net/friedl/fling/security/authentication/AuthenticationController.java b/service/fling/src/main/java/net/friedl/fling/security/authentication/AuthenticationController.java deleted file mode 100644 index d9b1e30..0000000 --- a/service/fling/src/main/java/net/friedl/fling/security/authentication/AuthenticationController.java +++ /dev/null @@ -1,33 +0,0 @@ -package net.friedl.fling.security.authentication; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import io.swagger.v3.oas.annotations.tags.Tag; -import net.friedl.fling.security.authentication.dto.OwnerAuthDto; -import net.friedl.fling.security.authentication.dto.UserAuthDto; - -@RestController -@RequestMapping("/api/auth") -@Tag(name = "auth", description = "Operations on /api/auth") -public class AuthenticationController { - - private AuthenticationService authenticationService; - - @Autowired - public AuthenticationController(AuthenticationService authenticationService) { - this.authenticationService = authenticationService; - } - - @PostMapping(path = "/owner") - public String authenticateOwner(@RequestBody OwnerAuthDto ownerAuthDto) { - return authenticationService.authenticate(ownerAuthDto); - } - - @PostMapping("/user") - public String authenticateUser(@RequestBody UserAuthDto userAuthDto) { - return authenticationService.authenticate(userAuthDto); - } -} 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 deleted file mode 100644 index a8bc181..0000000 --- a/service/fling/src/main/java/net/friedl/fling/security/authentication/AuthenticationService.java +++ /dev/null @@ -1,101 +0,0 @@ -package net.friedl.fling.security.authentication; - -import java.security.Key; -import java.time.Instant; -import java.util.Date; -import java.util.UUID; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.core.Authentication; -import org.springframework.stereotype.Service; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.JwtBuilder; -import io.jsonwebtoken.JwtParser; -import io.jsonwebtoken.Jwts; -import net.friedl.fling.model.dto.FlingDto; -import net.friedl.fling.security.FlingAuthority; -import net.friedl.fling.security.FlingSecurityConfiguration; -import net.friedl.fling.security.authentication.dto.OwnerAuthDto; -import net.friedl.fling.security.authentication.dto.UserAuthDto; -import net.friedl.fling.service.FlingService; - -@Service -public class AuthenticationService { - private FlingService flingService; - private JwtParser jwtParser; - private Key signingKey; - private FlingSecurityConfiguration securityConfig; - - @Autowired - public AuthenticationService(JwtParser jwtParser, Key signingKey, FlingService flingService, - FlingSecurityConfiguration securityConfig) { - this.flingService = flingService; - this.jwtParser = jwtParser; - this.signingKey = signingKey; - this.securityConfig = securityConfig; - } - - public String authenticate(OwnerAuthDto ownerAuth) { - if (!securityConfig.getAdminUser().equals(ownerAuth.getUsername())) { - throw new AccessDeniedException("Wrong credentials"); - } - - if (!securityConfig.getAdminPassword().equals(ownerAuth.getPassword())) { - throw new AccessDeniedException("Wrong credentials"); - } - - return makeBaseBuilder() - .setSubject("owner") - .compact(); - } - - public String authenticate(UserAuthDto userAuth) { - FlingDto flingDto = flingService.getByShareId(userAuth.getShareId()); - String authCode = userAuth.getCode(); - - if (!flingService.validateAuthCode(flingDto.getId(), authCode)) { - throw new AccessDeniedException("Wrong fling code"); - } - - return makeBaseBuilder() - .setSubject("user") - .claim("sid", flingDto.getShareId()) - .compact(); - - } - - public Authentication parseAuthentication(String token) { - Claims claims = parseClaims(token); - - FlingAuthority authority; - UUID flingId; - - switch (claims.getSubject()) { - case "owner": - authority = FlingAuthority.FLING_OWNER; - flingId = null; - break; - case "user": - authority = FlingAuthority.FLING_USER; - String sid = claims.get("sid", String.class); - flingId = flingService.getByShareId(sid).getId(); - break; - default: - throw new BadCredentialsException("Invalid token"); - } - - return new FlingToken(new GrantedFlingAuthority(authority, flingId)); - } - - private JwtBuilder makeBaseBuilder() { - return Jwts.builder() - .setIssuedAt(Date.from(Instant.now())) - .setExpiration(Date.from(Instant.now().plusSeconds(securityConfig.getJwtExpiration()))) - .signWith(signingKey); - } - - private Claims parseClaims(String token) { - return jwtParser.parseClaimsJws(token).getBody(); - } -} diff --git a/service/fling/src/main/java/net/friedl/fling/security/authentication/FlingAdminAuthority.java b/service/fling/src/main/java/net/friedl/fling/security/authentication/FlingAdminAuthority.java new file mode 100644 index 0000000..1784d56 --- /dev/null +++ b/service/fling/src/main/java/net/friedl/fling/security/authentication/FlingAdminAuthority.java @@ -0,0 +1,15 @@ +package net.friedl.fling.security.authentication; + +import org.springframework.security.core.GrantedAuthority; +import net.friedl.fling.security.FlingAuthorities; + +public class FlingAdminAuthority implements GrantedAuthority { + + private static final long serialVersionUID = -4605768612393081070L; + + @Override + public String getAuthority() { + return FlingAuthorities.FLING_ADMIN.getAuthority(); + } + +} diff --git a/service/fling/src/main/java/net/friedl/fling/security/authentication/FlingToken.java b/service/fling/src/main/java/net/friedl/fling/security/authentication/FlingToken.java index ccff7cd..e731e35 100644 --- a/service/fling/src/main/java/net/friedl/fling/security/authentication/FlingToken.java +++ b/service/fling/src/main/java/net/friedl/fling/security/authentication/FlingToken.java @@ -1,25 +1,36 @@ package net.friedl.fling.security.authentication; import java.util.List; +import java.util.UUID; import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; public class FlingToken extends AbstractAuthenticationToken { private static final long serialVersionUID = -1112423505610346583L; - private GrantedFlingAuthority grantedFlingAuthority; + private String jwtToken; - public FlingToken(GrantedFlingAuthority authority) { + public FlingToken(GrantedAuthority authority, String jwtToken) { super(List.of(authority)); - this.grantedFlingAuthority = authority; + this.jwtToken = jwtToken; } - public GrantedFlingAuthority getGrantedFlingAuthority() { - return this.grantedFlingAuthority; + public boolean authorizedForFling(UUID id) { + for (GrantedAuthority grantedAuthority : getAuthorities()) { + if (grantedAuthority instanceof FlingAdminAuthority) return true; + + if (!(grantedAuthority instanceof FlingUserAuthority)) continue; + + UUID grantedFlingId = ((FlingUserAuthority) grantedAuthority).getFlingId(); + if (grantedFlingId.equals(id)) return true; + } + + return false; } @Override - public Object getCredentials() { - return null; + public String getCredentials() { + return this.jwtToken; } @Override diff --git a/service/fling/src/main/java/net/friedl/fling/security/authentication/FlingUserAuthority.java b/service/fling/src/main/java/net/friedl/fling/security/authentication/FlingUserAuthority.java new file mode 100644 index 0000000..566eaf1 --- /dev/null +++ b/service/fling/src/main/java/net/friedl/fling/security/authentication/FlingUserAuthority.java @@ -0,0 +1,25 @@ +package net.friedl.fling.security.authentication; + +import java.util.UUID; +import org.springframework.security.core.GrantedAuthority; +import net.friedl.fling.security.FlingAuthorities; + +public class FlingUserAuthority implements GrantedAuthority { + private static final long serialVersionUID = -1814514234042184275L; + + private UUID flingId; + + public FlingUserAuthority(UUID flingId) { + this.flingId = flingId; + } + + @Override + public String getAuthority() { + return FlingAuthorities.FLING_USER.getAuthority(); + } + + public UUID getFlingId() { + return flingId; + } + +} diff --git a/service/fling/src/main/java/net/friedl/fling/security/authentication/GrantedFlingAuthority.java b/service/fling/src/main/java/net/friedl/fling/security/authentication/GrantedFlingAuthority.java deleted file mode 100644 index a56462f..0000000 --- a/service/fling/src/main/java/net/friedl/fling/security/authentication/GrantedFlingAuthority.java +++ /dev/null @@ -1,33 +0,0 @@ -package net.friedl.fling.security.authentication; - -import java.util.UUID; -import org.springframework.security.core.GrantedAuthority; -import net.friedl.fling.security.FlingAuthority; - -/** - * Authority granting access to a fling - * - * @author Armin Friedl - */ -public class GrantedFlingAuthority implements GrantedAuthority { - - private static final long serialVersionUID = -1552301479158714777L; - - private FlingAuthority authority; - private UUID flingId; - - public GrantedFlingAuthority(FlingAuthority authority, UUID flingId) { - this.authority = authority; - this.flingId = flingId; - } - - public UUID getFlingId() { - return this.flingId; - } - - @Override - public String getAuthority() { - return authority.name(); - } - -} diff --git a/service/fling/src/main/java/net/friedl/fling/security/authentication/JwtAuthenticationFilter.java b/service/fling/src/main/java/net/friedl/fling/security/authentication/JwtAuthenticationFilter.java index 71427d6..2ad3c5b 100644 --- a/service/fling/src/main/java/net/friedl/fling/security/authentication/JwtAuthenticationFilter.java +++ b/service/fling/src/main/java/net/friedl/fling/security/authentication/JwtAuthenticationFilter.java @@ -1,17 +1,19 @@ package net.friedl.fling.security.authentication; import java.io.IOException; +import java.util.stream.Collectors; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import lombok.extern.slf4j.Slf4j; +import net.friedl.fling.service.AuthenticationService; @Slf4j @Component @@ -34,7 +36,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { String header = request.getHeader(HEADER_STRING); if (header == null || !header.startsWith(TOKEN_PREFIX)) { - log.warn("Could not find bearer token. No JWT authentication."); + log.info("Anonymous request for {} {}?{}", request.getMethod(), request.getRequestURL(), + request.getQueryString()); filterChain.doFilter(request, response); return; } @@ -44,8 +47,12 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { SecurityContext securityContext = SecurityContextHolder.getContext(); if (securityContext.getAuthentication() == null) { - Authentication authentication = authenticationService.parseAuthentication(authToken); - securityContext.setAuthentication(authentication); + log.info("Authenticating request for {} {}?{}", request.getMethod(), request.getRequestURL(), + request.getQueryString()); + FlingToken token = authenticationService.parseAuthentication(authToken); + log.info("Authenticated as {}", token.getAuthorities().stream() + .map(GrantedAuthority::getAuthority).collect(Collectors.joining(","))); + securityContext.setAuthentication(token); } filterChain.doFilter(request, response); diff --git a/service/fling/src/main/java/net/friedl/fling/security/authentication/dto/OwnerAuthDto.java b/service/fling/src/main/java/net/friedl/fling/security/authentication/dto/OwnerAuthDto.java deleted file mode 100644 index 606562f..0000000 --- a/service/fling/src/main/java/net/friedl/fling/security/authentication/dto/OwnerAuthDto.java +++ /dev/null @@ -1,11 +0,0 @@ -package net.friedl.fling.security.authentication.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -@Data -@Schema(name = "OwnerAuth") -public class OwnerAuthDto { - private String username; - private String password; -} diff --git a/service/fling/src/main/java/net/friedl/fling/security/authentication/dto/UserAuthDto.java b/service/fling/src/main/java/net/friedl/fling/security/authentication/dto/UserAuthDto.java deleted file mode 100644 index b4a8107..0000000 --- a/service/fling/src/main/java/net/friedl/fling/security/authentication/dto/UserAuthDto.java +++ /dev/null @@ -1,11 +0,0 @@ -package net.friedl.fling.security.authentication.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -@Data -@Schema(name = "UserAuth") -public class UserAuthDto { - String shareId; - String code; -} diff --git a/service/fling/src/main/java/net/friedl/fling/service/ArtifactService.java b/service/fling/src/main/java/net/friedl/fling/service/ArtifactService.java index 0eadaa2..1a848de 100644 --- a/service/fling/src/main/java/net/friedl/fling/service/ArtifactService.java +++ b/service/fling/src/main/java/net/friedl/fling/service/ArtifactService.java @@ -62,12 +62,12 @@ public class ArtifactService { artifactEntity = artifactRepository.save(artifactEntity); return artifactMapper.map(artifactEntity); } - + /** * Deletes an artifact identified by {@code id}. NOOP if the artifact cannot be found. * * @param id An {@link UUID} that identifies the artifact - * @throws IOException If the deletion failed + * @throws IOException If the deletion failed */ public void delete(UUID id) throws IOException { archiveService.deleteArtifact(id); diff --git a/service/fling/src/main/java/net/friedl/fling/service/AuthenticationService.java b/service/fling/src/main/java/net/friedl/fling/service/AuthenticationService.java new file mode 100644 index 0000000..ec35c5b --- /dev/null +++ b/service/fling/src/main/java/net/friedl/fling/service/AuthenticationService.java @@ -0,0 +1,116 @@ +package net.friedl.fling.service; + +import java.security.Key; +import java.time.Instant; +import java.util.Date; +import java.util.Optional; +import java.util.UUID; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import lombok.extern.slf4j.Slf4j; +import net.friedl.fling.model.dto.AdminAuthDto; +import net.friedl.fling.model.dto.UserAuthDto; +import net.friedl.fling.persistence.entities.FlingEntity; +import net.friedl.fling.persistence.repositories.FlingRepository; +import net.friedl.fling.security.authentication.FlingAdminAuthority; +import net.friedl.fling.security.authentication.FlingToken; +import net.friedl.fling.security.authentication.FlingUserAuthority; + +@Slf4j +@Service +public class AuthenticationService { + private JwtParser jwtParser; + private Key jwtSigningKey; + private FlingRepository flingRepository; + private PasswordEncoder passwordEncoder; + + @Value("${fling.security.admin-name}") + private String adminName; + @Value("${fling.security.admin-password}") + private String adminPassword; + @Value("${fling.security.jwt-expiration}") + private Long jwtExpiration; + + @Autowired + public AuthenticationService(JwtParser jwtParser, Key jwtSigningKey, + PasswordEncoder passwordEncoder, FlingRepository flingRepository) { + + this.jwtParser = jwtParser; + this.jwtSigningKey = jwtSigningKey; + this.passwordEncoder = passwordEncoder; + this.flingRepository = flingRepository; + } + + public Optional authenticate(AdminAuthDto adminAuth) { + log.info("Authenticating {}", adminAuth.getAdminName()); + if (!adminName.equals(adminAuth.getAdminName())) { + log.debug("Authentication failed for {}", adminAuth.getAdminName()); + return Optional.empty(); + } + + if (!adminPassword.equals(adminAuth.getAdminPassword())) { + log.debug("Authentication failed for {}", adminAuth.getAdminName()); + return Optional.empty(); + } + + log.debug("Authentication successful for {}", adminAuth.getAdminName()); + return Optional.of( + getJwtBuilder() + .setSubject("admin") + .compact()); + } + + public Optional authenticate(UserAuthDto userAuth) { + log.info("Authenticating for fling [.shareId={}]", userAuth.getShareId()); + FlingEntity flingEntity = flingRepository.findByShareId(userAuth.getShareId()); + String providedAuthCodeHash = passwordEncoder.encode(userAuth.getAuthCode()); + String actualAuthCodeHash = flingEntity.getAuthCode(); + + if (!actualAuthCodeHash.equals(providedAuthCodeHash)) { + log.debug("Authentication failed for fling [.shareId={}]", userAuth.getShareId()); + return Optional.empty(); + } + + log.debug("Authentication successful for fling [.shareId={}]", userAuth.getShareId()); + return Optional.of( + getJwtBuilder() + .setSubject("user") + .claim("id", flingEntity.getId()) + .compact()); + + } + + public FlingToken parseAuthentication(String token) { + Claims claims = jwtParser.parseClaimsJws(token).getBody(); + + switch (claims.getSubject()) { + case "owner": + return new FlingToken(new FlingAdminAuthority(), token); + case "user": + UUID grantedFlingId = UUID.fromString(claims.get("id", String.class)); + return new FlingToken(new FlingUserAuthority(grantedFlingId), token); + default: + throw new BadCredentialsException("Invalid token"); + } + } + + /** + * Creates a new JwtBuilder. A new builder must be constructed for each JWT creation, because the + * builder keeps its state. + * + * @return A new JwtBuilder with basic default configuration. + */ + private JwtBuilder getJwtBuilder() { + return Jwts.builder() + .setIssuedAt(Date.from(Instant.now())) + .setExpiration(Date.from(Instant.now().plusSeconds(jwtExpiration))) + .signWith(jwtSigningKey); + } +} diff --git a/service/fling/src/main/java/net/friedl/fling/service/AuthorizationService.java b/service/fling/src/main/java/net/friedl/fling/service/AuthorizationService.java new file mode 100644 index 0000000..55f6afb --- /dev/null +++ b/service/fling/src/main/java/net/friedl/fling/service/AuthorizationService.java @@ -0,0 +1,68 @@ +package net.friedl.fling.service; + +import java.util.UUID; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.stereotype.Service; +import lombok.extern.slf4j.Slf4j; +import net.friedl.fling.persistence.repositories.FlingRepository; +import net.friedl.fling.security.FlingAuthorities; +import net.friedl.fling.security.authentication.FlingToken; + +@Slf4j +@Service +public class AuthorizationService { + private FlingRepository flingRepository; + + @Autowired + public AuthorizationService(FlingRepository flingRepository) { + this.flingRepository = flingRepository; + } + + public boolean allowUpload(UUID flingId, AbstractAuthenticationToken token) { + if (!(token instanceof FlingToken)) { + log.debug("Token of type {} not allowed. Authentication denied.", token.getClass()); + return false; + } + + if (FlingAuthorities.FLING_ADMIN.verify(token)) { + log.debug("Owner authorized for upload fling[.id={}]", flingId); + return true; + } + + if (!flingRepository.getOne(flingId).getAllowUpload()) { + log.debug("Fling[.id={}] does not not allow uploads"); + return false; + } + + FlingToken flingToken = (FlingToken) token; + if (flingToken.authorizedForFling(flingId)) { + log.debug("User authorized for upload fling[.id={}]", flingId); + return true; + } + + log.info("User not authorized for upload fling[.id={}]", flingId); + return false; + } + + public boolean allowFlingAccess(UUID flingId, AbstractAuthenticationToken token) { + if (!(token instanceof FlingToken)) { + log.debug("Token of type {} not allowed. Authentication denied.", token.getClass()); + return false; + } + + if (FlingAuthorities.FLING_ADMIN.verify(token)) { + log.debug("Owner authorized for fling access [id = {}]", flingId); + return true; + } + + FlingToken flingToken = (FlingToken) token; + if (flingToken.authorizedForFling(flingId)) { + log.debug("User authorized for fling access [id = {}]"); + return true; + } + + log.info("User not authorized to access fling[.id={}]", flingId); + return false; + } +} 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 2e074f2..5320f51 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,14 +1,13 @@ package net.friedl.fling.service; import java.io.IOException; -import java.security.MessageDigest; import java.util.Base64; import java.util.List; import java.util.UUID; import javax.transaction.Transactional; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.codec.Hex; import org.springframework.security.crypto.keygen.KeyGenerators; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import lombok.extern.slf4j.Slf4j; @@ -26,17 +25,16 @@ public class FlingService { private FlingRepository flingRepository; private FlingMapper flingMapper; private ArchiveService archiveService; - private MessageDigest keyHashDigest; + private PasswordEncoder passwordEncoder; @Autowired public FlingService(FlingRepository flingRepository, FlingMapper flingMapper, - ArchiveService archiveService, - MessageDigest keyHashDigest) { + ArchiveService archiveService, PasswordEncoder passwordEcoder) { this.flingRepository = flingRepository; this.flingMapper = flingMapper; this.archiveService = archiveService; - this.keyHashDigest = keyHashDigest; + this.passwordEncoder = passwordEcoder; } /** @@ -96,7 +94,7 @@ public class FlingService { public boolean validateAuthCode(UUID id, String authCode) { FlingEntity flingEntity = flingRepository.getOne(id); - if(StringUtils.hasText(flingEntity.getAuthCode()) != StringUtils.hasText(authCode)) { + if (StringUtils.hasText(flingEntity.getAuthCode()) != StringUtils.hasText(authCode)) { return false; // only one of them is empty; implicit null safety check } @@ -106,7 +104,7 @@ public class FlingService { } private String hashAuthCode(String authCode) { - String hash = new String(Hex.encode(keyHashDigest.digest(authCode.getBytes()))); + String hash = passwordEncoder.encode(authCode); log.debug("Hashed authentication code to {}", hash); return hash; } diff --git a/service/fling/src/main/java/net/friedl/fling/service/archive/ArchiveService.java b/service/fling/src/main/java/net/friedl/fling/service/archive/ArchiveService.java index f1472c4..a2660bf 100644 --- a/service/fling/src/main/java/net/friedl/fling/service/archive/ArchiveService.java +++ b/service/fling/src/main/java/net/friedl/fling/service/archive/ArchiveService.java @@ -17,7 +17,7 @@ public interface ArchiveService { * @return An {@link InputStream} for reading the artifact */ InputStream getArtifact(UUID artifactId) throws IOException; - + /** * Retrieve a packaged fling from the archive * @@ -37,10 +37,10 @@ public interface ArchiveService { /** * Delete an artifact * - * @param id The unique artifact id + * @param id The unique artifact id */ void deleteArtifact(UUID artifactId) throws IOException; - + /** * Delete a fling * diff --git a/service/fling/src/main/java/net/friedl/fling/service/archive/impl/FileSystemArchive.java b/service/fling/src/main/java/net/friedl/fling/service/archive/impl/FileSystemArchive.java index ffbb18e..4189a55 100644 --- a/service/fling/src/main/java/net/friedl/fling/service/archive/impl/FileSystemArchive.java +++ b/service/fling/src/main/java/net/friedl/fling/service/archive/impl/FileSystemArchive.java @@ -130,7 +130,7 @@ public class FileSystemArchive implements ArchiveService { Path zipDiskPath = archivePath.resolve(flingId.toString() + ".zip"); log.debug("Deleting fling [.id={}] at {}", flingId, zipDiskPath); Files.delete(zipDiskPath); - + artifactRepository.findAllByFlingId(flingId).forEach(ar -> ar.setArchived(false)); } } @@ -210,7 +210,7 @@ public class FileSystemArchive implements ArchiveService { public void setArchivePath(String archivePath) { this.archivePath = Paths.get(archivePath); } - + public void setArchivePath(Path archivePath) { this.archivePath = archivePath; } diff --git a/service/fling/src/main/resources/META-INF/spring-configuration-metadata.json b/service/fling/src/main/resources/META-INF/spring-configuration-metadata.json index 187790c..fd20a97 100644 --- a/service/fling/src/main/resources/META-INF/spring-configuration-metadata.json +++ b/service/fling/src/main/resources/META-INF/spring-configuration-metadata.json @@ -48,7 +48,7 @@ "sourceType": "net.friedl.fling.security.FlingWebSecurityConfiguration" }, { - "name": "fling.security.admin-user", + "name": "fling.security.admin-name", "type": "java.lang.String", "description": "Username of the admin user/instance owner", "sourceType": "net.friedl.fling.security.FlingWebSecurityConfiguration" diff --git a/service/fling/src/main/resources/application-local.yml b/service/fling/src/main/resources/application-local.yml index c814456..84c1385 100644 --- a/service/fling/src/main/resources/application-local.yml +++ b/service/fling/src/main/resources/application-local.yml @@ -28,10 +28,10 @@ fling: - "http://localhost:3000" - "http://localhost:5000" - "http://10.0.2.2:5000" - admin-user: "${FLING_ADMIN_USER:admin}" - admin-password: "${FLING_ADMIN_PASSWORD:123}" - signing-key: "${FLING_SIGNING_KEY:changeitchangeitchangeitchangeit}" - jwt-expiration: "${FLING_JWT_EXPIRATION:180000}" + admin-name: "adminName" + admin-password: "adminPassword" + signing-key: "changeitchangeitchangeitchangeit" + jwt-expiration: "180000" api: version: "0" server-url: "http://localhost:8080" diff --git a/service/fling/src/main/resources/application-prod.yml b/service/fling/src/main/resources/application-prod.yml index edea956..9a19db4 100644 --- a/service/fling/src/main/resources/application-prod.yml +++ b/service/fling/src/main/resources/application-prod.yml @@ -19,9 +19,15 @@ fling: archive.filesystem.archive-path: "/var/fling/files" security: allowed-origins: - - "https://fling.friedl.net" + - "https://friedl.net" - "http://localhost:3000" - admin-user: "${FLING_ADMIN_USER:admin}" - admin-password: "${FLING_ADMIN_PASSWORD:123}" - signing-key: "${FLING_SIGNING_KEY:changeitchangeitchangeitchangeit}" - jwt-expiration: "${FLING_JWT_EXPIRATION:180000}" \ No newline at end of file + - "http://localhost:5000" + - "http://10.0.2.2:5000" + admin-name: "adminName" + admin-password: "adminPassword" + signing-key: "changeitchangeitchangeitchangeit" + jwt-expiration: "180000" + api: + version: "0" + server-url: "http://localhost:8080" + server-description: "API server for dev" \ No newline at end of file diff --git a/service/fling/src/test/java/net/friedl/fling/controller/ArtifactControllerTest.java b/service/fling/src/test/java/net/friedl/fling/controller/ArtifactControllerTest.java index c87264e..5f6ddd2 100644 --- a/service/fling/src/test/java/net/friedl/fling/controller/ArtifactControllerTest.java +++ b/service/fling/src/test/java/net/friedl/fling/controller/ArtifactControllerTest.java @@ -110,7 +110,8 @@ class ArtifactControllerTest { mvc.perform(get("/api/artifacts/{id}/data", ARTIFACT_ID)) .andExpect(header().doesNotExist(HttpHeaders.CONTENT_DISPOSITION)) - .andExpect(header().string(HttpHeaders.CONTENT_TYPE, not(equalTo(MediaType.APPLICATION_OCTET_STREAM_VALUE)))) + .andExpect(header().string(HttpHeaders.CONTENT_TYPE, + not(equalTo(MediaType.APPLICATION_OCTET_STREAM_VALUE)))) .andExpect(status().isNotFound()); } @@ -120,7 +121,8 @@ class ArtifactControllerTest { mvc.perform(get("/api/artifacts/{id}/data", ARTIFACT_ID)) .andExpect(header().doesNotExist(HttpHeaders.CONTENT_DISPOSITION)) - .andExpect(header().string(HttpHeaders.CONTENT_TYPE, not(equalTo(MediaType.APPLICATION_OCTET_STREAM_VALUE)))) + .andExpect(header().string(HttpHeaders.CONTENT_TYPE, + not(equalTo(MediaType.APPLICATION_OCTET_STREAM_VALUE)))) .andExpect(status().isInternalServerError()); } diff --git a/service/fling/src/test/java/net/friedl/fling/controller/AuthenticationControllerTest.java b/service/fling/src/test/java/net/friedl/fling/controller/AuthenticationControllerTest.java new file mode 100644 index 0000000..5576e9e --- /dev/null +++ b/service/fling/src/test/java/net/friedl/fling/controller/AuthenticationControllerTest.java @@ -0,0 +1,87 @@ +package net.friedl.fling.controller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import com.fasterxml.jackson.databind.ObjectMapper; +import net.friedl.fling.model.dto.AdminAuthDto; +import net.friedl.fling.model.dto.UserAuthDto; +import net.friedl.fling.service.AuthenticationService; +import net.friedl.fling.service.AuthorizationService; + +@WebMvcTest(controllers = AuthenticationController.class, + includeFilters = {@Filter(Configuration.class)}) +@ActiveProfiles("local") +public class AuthenticationControllerTest { + @Autowired + private MockMvc mvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private AuthenticationService authenticationService; + + @MockBean + private AuthorizationService authorizationService; + + @Test + public void authenticateOwner_noToken_403() throws Exception { + AdminAuthDto adminAuthDto = new AdminAuthDto("admin", "123"); + when(authenticationService.authenticate(any(AdminAuthDto.class))).thenReturn(Optional.empty()); + + mvc.perform(post("/api/auth/admin") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(adminAuthDto))) + .andExpect(status().is(403)); + } + + @Test + public void authenticateOwner_token_ok() throws Exception { + AdminAuthDto adminAuthDto = new AdminAuthDto("admin", "123"); + when(authenticationService.authenticate(any(AdminAuthDto.class))) + .thenReturn(Optional.of("token")); + + mvc.perform(post("/api/auth/admin") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(adminAuthDto))) + .andExpect(status().is(200)) + .andExpect(content().string("token")); + } + + @Test + public void authenticateUser_noToken_403() throws Exception { + UserAuthDto userAuthDto = new UserAuthDto("shareId", "authCode"); + when(authenticationService.authenticate(any(UserAuthDto.class))).thenReturn(Optional.empty()); + + mvc.perform(post("/api/auth/user") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(userAuthDto))) + .andExpect(status().is(403)); + } + + @Test + public void authenticateUser_token_ok() throws Exception { + UserAuthDto userAuthDto = new UserAuthDto("shareId", "authCode"); + when(authenticationService.authenticate(any(UserAuthDto.class))) + .thenReturn(Optional.of("token")); + + mvc.perform(post("/api/auth/user") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(userAuthDto))) + .andExpect(status().is(200)) + .andExpect(content().string("token")); + } +} diff --git a/service/fling/src/test/java/net/friedl/fling/controller/FlingControllerTest.java b/service/fling/src/test/java/net/friedl/fling/controller/FlingControllerTest.java index 3d0e80c..36edc34 100644 --- a/service/fling/src/test/java/net/friedl/fling/controller/FlingControllerTest.java +++ b/service/fling/src/test/java/net/friedl/fling/controller/FlingControllerTest.java @@ -96,6 +96,16 @@ public class FlingControllerTest { .andExpect(status().isOk()); } + @Test + public void postFling_validatesBody_notOk() throws Exception { + FlingDto invalidFlingDto = new FlingDto(); + + mockMvc.perform(post("/api/fling") + .content(mapper.writeValueAsString(invalidFlingDto)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + @Test public void postArtifact_ok() throws Exception { mockMvc.perform(post("/api/fling/{id}/artifact", flingId) @@ -104,6 +114,16 @@ public class FlingControllerTest { .andExpect(status().isOk()); } + @Test + public void postArtifact_validatesBody_notOk() throws Exception { + ArtifactDto invalidArtifactDto = new ArtifactDto(); + + mockMvc.perform(post("/api/fling/{id}/artifact", flingId) + .content(mapper.writeValueAsString(invalidArtifactDto)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + @Test public void getFling_noFlingWithId_notFound() throws Exception { doThrow(EntityNotFoundException.class).when(flingService).getById(flingId); diff --git a/service/fling/src/test/java/net/friedl/fling/model/FlingDtoTest.java b/service/fling/src/test/java/net/friedl/fling/model/FlingDtoTest.java index 3029211..6d6f2f5 100644 --- a/service/fling/src/test/java/net/friedl/fling/model/FlingDtoTest.java +++ b/service/fling/src/test/java/net/friedl/fling/model/FlingDtoTest.java @@ -75,8 +75,9 @@ public class FlingDtoTest { assertThat(violation.getMessage()).isEqualTo("must not be null"); } + @Test - void testSetShareId_null_validationFails() { + void testSetShareId_null_validationOk() { // must be nullable to support defaulting in service FlingDto flingDto = FlingDto.builder() .id(new UUID(0L, 0L)) .name("test") @@ -87,10 +88,7 @@ public class FlingDtoTest { Set> constraintViolations = validator.validate(flingDto); - assertThat(constraintViolations).hasSize(1); - ConstraintViolation violation = constraintViolations.iterator().next(); - assertThat(violation.getPropertyPath().toString()).isEqualTo("shareId"); - assertThat(violation.getMessage()).isEqualTo("must not be null"); + assertThat(constraintViolations).hasSize(0); } @Test diff --git a/service/fling/src/test/java/net/friedl/fling/service/ArtifactServiceTest.java b/service/fling/src/test/java/net/friedl/fling/service/ArtifactServiceTest.java index 95c49de..86eb932 100644 --- a/service/fling/src/test/java/net/friedl/fling/service/ArtifactServiceTest.java +++ b/service/fling/src/test/java/net/friedl/fling/service/ArtifactServiceTest.java @@ -62,7 +62,8 @@ public class ArtifactServiceTest { FlingRepository flingRepository, ArtifactMapper artifactMapper, ArchiveService archiveService) { - return new ArtifactService(artifactRepository, flingRepository, artifactMapper, archiveService); + return new ArtifactService(artifactRepository, flingRepository, artifactMapper, + archiveService); } } @@ -72,12 +73,12 @@ public class ArtifactServiceTest { artifactEntity1.setId(UUID.randomUUID()); artifactEntity1.setUploadTime(Instant.EPOCH); artifactEntity1.setPath(Path.of("artifact1")); - + this.artifactEntity2 = new ArtifactEntity(); artifactEntity2.setId(UUID.randomUUID()); artifactEntity2.setUploadTime(Instant.EPOCH.plus(12000, ChronoUnit.DAYS)); - artifactEntity2.setPath(Path.of("/","/sub","artifact2")); - + artifactEntity2.setPath(Path.of("/", "/sub", "artifact2")); + this.flingEntity = new FlingEntity(); flingEntity.setId(UUID.randomUUID()); flingEntity.setName("fling"); @@ -87,11 +88,11 @@ public class ArtifactServiceTest { @Override public FlingEntity answer(InvocationOnMock invocation) throws Throwable { FlingEntity flingEntity = invocation.getArgument(0); - if(flingEntity.getId() == null) flingEntity.setId(UUID.randomUUID()); + if (flingEntity.getId() == null) flingEntity.setId(UUID.randomUUID()); return flingEntity; } }); - + when(artifactRepository.save(any())).then(new Answer() { @Override public ArtifactEntity answer(InvocationOnMock invocation) throws Throwable { @@ -102,17 +103,17 @@ public class ArtifactServiceTest { }); } - + @Test public void getById_artifactExists_ok() { when(artifactRepository.getOne(artifactEntity1.getId())).thenReturn(artifactEntity1); - + ArtifactDto artifactDto = artifactService.getById(artifactEntity1.getId()); assertThat(artifactDto.getId(), equalTo(artifactEntity1.getId())); assertThat(artifactDto.getPath(), equalTo(artifactEntity1.getPath())); assertThat(artifactDto.getUploadTime(), equalTo(artifactEntity1.getUploadTime())); } - + @Test public void create_createsArtifact_ok() { ArtifactDto artifactToCreate = ArtifactDto.builder() @@ -121,15 +122,15 @@ public class ArtifactServiceTest { .build(); ArtifactDto createdArtifact = artifactService.create(flingEntity.getId(), artifactToCreate); - + assertThat(createdArtifact.getUploadTime(), equalTo(artifactToCreate.getUploadTime())); assertThat(createdArtifact.getPath(), equalTo(artifactToCreate.getPath())); } - + @Test public void delete_deletesArchiveAndArtifactEntry() throws IOException { artifactService.delete(artifactEntity1.getId()); - + verify(archiveService).deleteArtifact(artifactEntity1.getId()); verify(artifactRepository).deleteById(artifactEntity1.getId()); } diff --git a/service/fling/src/test/java/net/friedl/fling/service/AuthenticationServiceTest.java b/service/fling/src/test/java/net/friedl/fling/service/AuthenticationServiceTest.java new file mode 100644 index 0000000..646e675 --- /dev/null +++ b/service/fling/src/test/java/net/friedl/fling/service/AuthenticationServiceTest.java @@ -0,0 +1,159 @@ +package net.friedl.fling.service; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import java.security.Key; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Bean; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.impl.DefaultClaims; +import io.jsonwebtoken.impl.DefaultJws; +import io.jsonwebtoken.impl.DefaultJwsHeader; +import io.jsonwebtoken.security.Keys; +import net.friedl.fling.model.dto.AdminAuthDto; +import net.friedl.fling.model.dto.UserAuthDto; +import net.friedl.fling.persistence.entities.FlingEntity; +import net.friedl.fling.persistence.repositories.FlingRepository; +import net.friedl.fling.security.authentication.FlingAdminAuthority; +import net.friedl.fling.security.authentication.FlingToken; +import net.friedl.fling.security.authentication.FlingUserAuthority; + +@ExtendWith(SpringExtension.class) +@TestPropertySource("classpath:/application-test.properties") +@ActiveProfiles("test") +public class AuthenticationServiceTest { + @Autowired + public AuthenticationService authenticationService; + + @MockBean + private FlingRepository flingRepository; + + @MockBean + private JwtParser jwtParser; + + @MockBean + private PasswordEncoder passwordEncoder; + + @TestConfiguration + static class FlingServiceTestConfiguration { + private Key jwtSigningKey = Keys.hmacShaKeyFor(new byte[32]); + + @Bean + public AuthenticationService authenticationService(JwtParser jwtParser, + PasswordEncoder passwordEncoder, FlingRepository flingRepository) { + return new AuthenticationService(jwtParser, jwtSigningKey, passwordEncoder, flingRepository); + } + } + + @Test + public void authenticate_adminNameDiffers_empty() { + AdminAuthDto adminAuthDto = new AdminAuthDto("wrongadmin", "123"); + + assertThat(authenticationService.authenticate(adminAuthDto), equalTo(Optional.empty())); + } + + @Test + public void authenticate_passwordDiffers_empty() { + AdminAuthDto adminAuthDto = new AdminAuthDto("admin", "wrongpassword"); + + assertThat(authenticationService.authenticate(adminAuthDto), equalTo(Optional.empty())); + } + + @Test + public void authenticate_ok() { + AdminAuthDto adminAuthDto = new AdminAuthDto("admin", "123"); + + assertThat(authenticationService.authenticate(adminAuthDto), not(equalTo(Optional.empty()))); + } + + @Test + public void authenticate_authCodeDiffers_empty() { + FlingEntity flingEntity = new FlingEntity(); + flingEntity.setAuthCode("test"); + flingEntity.setId(UUID.randomUUID()); + + UserAuthDto userAuthDto = new UserAuthDto("shareId", "wrongCode"); + + when(flingRepository.findByShareId(any(String.class))).thenReturn(flingEntity); + when(passwordEncoder.encode(any(String.class))).thenReturn("wrongCode"); + + assertThat(authenticationService.authenticate(userAuthDto), equalTo(Optional.empty())); + } + + @Test + public void authenticate_authCodeEquals_ok() { + FlingEntity flingEntity = new FlingEntity(); + flingEntity.setAuthCode("authCodeHash"); + flingEntity.setId(UUID.randomUUID()); + + UserAuthDto userAuthDto = UserAuthDto.builder() + .authCode("authCode") + .shareId("shareId").build(); + + when(flingRepository.findByShareId(any(String.class))).thenReturn(flingEntity); + when(passwordEncoder.encode(any(String.class))).thenReturn("authCodeHash"); + + assertThat(authenticationService.authenticate(userAuthDto), not(equalTo(Optional.empty()))); + } + + @Test + public void parseAuthentication_owner_AdminAuthority() { + Jws jwsClaims = new DefaultJws<>(new DefaultJwsHeader(), + new DefaultClaims(Map.of("sub", "owner")), "signature"); + when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims); + + FlingToken flingToken = authenticationService.parseAuthentication("any"); + assertThat(flingToken.isAuthenticated(), equalTo(true)); + // authorized for any fling + assertThat(flingToken.authorizedForFling(UUID.randomUUID()), equalTo(true)); + assertThat(flingToken.getCredentials(), equalTo("any")); + assertThat(flingToken.getAuthorities(), + hasItem(org.hamcrest.Matchers.any(FlingAdminAuthority.class))); + } + + @Test + public void parseAuthentication_user_UserAuthorityForId() { + Jws jwsClaims = new DefaultJws<>(new DefaultJwsHeader(), + new DefaultClaims(Map.of("sub", "user", "id", new UUID(0, 0).toString())), "signature"); + when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims); + + FlingToken flingToken = authenticationService.parseAuthentication("any"); + assertThat(flingToken.isAuthenticated(), equalTo(true)); + // authorized for fling in token + assertThat(flingToken.authorizedForFling(new UUID(0, 0)), equalTo(true)); + // not authorized for fling other flings + assertThat(flingToken.authorizedForFling(new UUID(0, 1)), equalTo(false)); + assertThat(flingToken.getCredentials(), equalTo("any")); + assertThat(flingToken.getAuthorities(), + hasItem(org.hamcrest.Matchers.any(FlingUserAuthority.class))); + } + + @Test + public void parseAuthentication_unknownSubject_throws() { + Jws jwsClaims = new DefaultJws<>(new DefaultJwsHeader(), + new DefaultClaims(Map.of("sub", "unknownSubject")), "signature"); + when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims); + + assertThrows(BadCredentialsException.class, + () -> authenticationService.parseAuthentication("any")); + } +} diff --git a/service/fling/src/test/java/net/friedl/fling/service/AuthorizationServiceTest.java b/service/fling/src/test/java/net/friedl/fling/service/AuthorizationServiceTest.java new file mode 100644 index 0000000..5540941 --- /dev/null +++ b/service/fling/src/test/java/net/friedl/fling/service/AuthorizationServiceTest.java @@ -0,0 +1,116 @@ +package net.friedl.fling.service; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; +import java.util.List; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Bean; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import net.friedl.fling.persistence.entities.FlingEntity; +import net.friedl.fling.persistence.repositories.FlingRepository; +import net.friedl.fling.security.authentication.FlingAdminAuthority; +import net.friedl.fling.security.authentication.FlingToken; +import net.friedl.fling.security.authentication.FlingUserAuthority; + +@ExtendWith(SpringExtension.class) +public class AuthorizationServiceTest { + + @Autowired + private AuthorizationService authorizationService; + + @MockBean + private FlingRepository flingRepository; + + @TestConfiguration + static class FlingServiceTestConfiguration { + @Bean + public AuthorizationService authorizationService(FlingRepository flingRepository) { + return new AuthorizationService(flingRepository); + } + } + + @Test + public void allowUpload_unknownToken_false() { + var unkownToken = new AnonymousAuthenticationToken("key", "principal", + List.of(new SimpleGrantedAuthority("role"))); + + assertFalse(authorizationService.allowUpload(UUID.randomUUID(), unkownToken)); + } + + @Test + public void allowUpload_flingAdmin_true() { + FlingToken flingToken = new FlingToken(new FlingAdminAuthority(), "jwtToken"); + assertTrue(authorizationService.allowUpload(UUID.randomUUID(), flingToken)); + } + + @Test + public void allowUpload_noAdmin_uploadDisallowed_false() { + FlingEntity flingEntity = new FlingEntity(); + flingEntity.setAllowUpload(false); + + FlingToken flingToken = new FlingToken(new FlingUserAuthority(new UUID(0, 0)), "jwtToken"); + + when(flingRepository.getOne(new UUID(0, 0))).thenReturn(flingEntity); + + assertFalse(authorizationService.allowUpload(new UUID(0, 0), flingToken)); + } + + @Test + public void allowUpload_noAdmin_uploadAllowed_notAuthorized_false() { + FlingEntity flingEntity = new FlingEntity(); + flingEntity.setAllowUpload(true); + + FlingToken flingToken = new FlingToken(new FlingUserAuthority(new UUID(0, 0)), "jwtToken"); + + when(flingRepository.getOne(new UUID(1, 1))).thenReturn(flingEntity); + + // Token: UUID(0,0), Request: UUID(1,1) + assertFalse(authorizationService.allowUpload(new UUID(1, 1), flingToken)); + } + + @Test + public void allowUpload_noAdmin_uploadAllowed_authorized_true() { + FlingEntity flingEntity = new FlingEntity(); + flingEntity.setAllowUpload(true); + + FlingToken flingToken = new FlingToken(new FlingUserAuthority(new UUID(0, 0)), "jwtToken"); + + when(flingRepository.getOne(new UUID(0, 0))).thenReturn(flingEntity); + + // Token: UUID(0,0), Request: UUID(0,0) + assertTrue(authorizationService.allowUpload(new UUID(0, 0), flingToken)); + } + + @Test + public void allowFlingAccess_unknownToken_false() { + var unkownToken = new AnonymousAuthenticationToken("key", "principal", + List.of(new SimpleGrantedAuthority("role"))); + assertFalse(authorizationService.allowFlingAccess(UUID.randomUUID(), unkownToken)); + } + + @Test + public void allowFlingAcess_flingAdmin_true() { + FlingToken flingToken = new FlingToken(new FlingAdminAuthority(), "jwtToken"); + assertTrue(authorizationService.allowFlingAccess(UUID.randomUUID(), flingToken)); + } + + @Test + public void allowFlingAcess_flingUser_notAuthorizedForId_false() { + FlingToken flingToken = new FlingToken(new FlingUserAuthority(new UUID(0, 0)), "jwtToken"); + assertFalse(authorizationService.allowFlingAccess(new UUID(1, 1), flingToken)); + } + + @Test + public void allowFlingAcess_flingUser_authorizedForId_true() { + FlingToken flingToken = new FlingToken(new FlingUserAuthority(new UUID(0, 0)), "jwtToken"); + assertTrue(authorizationService.allowFlingAccess(new UUID(0, 0), flingToken)); + } +} diff --git a/service/fling/src/test/java/net/friedl/fling/service/FlingServiceTest.java b/service/fling/src/test/java/net/friedl/fling/service/FlingServiceTest.java index df1d1a7..524f8a2 100644 --- a/service/fling/src/test/java/net/friedl/fling/service/FlingServiceTest.java +++ b/service/fling/src/test/java/net/friedl/fling/service/FlingServiceTest.java @@ -11,8 +11,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.IOException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.time.Instant; import java.util.List; import java.util.UUID; @@ -25,7 +23,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.codec.Hex; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.test.context.junit.jupiter.SpringExtension; import net.friedl.fling.model.dto.FlingDto; import net.friedl.fling.model.mapper.FlingMapper; @@ -38,12 +36,12 @@ import net.friedl.fling.service.archive.ArchiveService; public class FlingServiceTest { @Autowired private FlingService flingService; - + @Autowired private FlingMapper flingMapper; - @Autowired - private MessageDigest keyHashDigest; + @MockBean + private PasswordEncoder passwordEncoder; @MockBean private FlingRepository flingRepository; @@ -61,16 +59,11 @@ public class FlingServiceTest { public FlingMapper flingMapper() { return new FlingMapperImpl(); } - - @Bean - public MessageDigest keyHashDigest() throws NoSuchAlgorithmException { - return MessageDigest.getInstance("SHA-512"); - } @Bean public FlingService flingService(FlingRepository flingRepository, FlingMapper flingMapper, - ArchiveService archiveService, MessageDigest keyHashDigest) { - return new FlingService(flingRepository, flingMapper, archiveService, keyHashDigest); + ArchiveService archiveService, PasswordEncoder passwordEncoder) { + return new FlingService(flingRepository, flingMapper, archiveService, passwordEncoder); } } @@ -79,7 +72,7 @@ public class FlingServiceTest { this.flingEntity1 = new FlingEntity(); flingEntity1.setId(UUID.randomUUID()); flingEntity1.setName("fling1"); - flingEntity1.setAuthCode(new String(Hex.encode(keyHashDigest.digest("authCode1".getBytes())))); + flingEntity1.setAuthCode("testhash"); flingEntity1.setCreationTime(Instant.now()); this.flingEntity2 = new FlingEntity(); @@ -94,7 +87,8 @@ public class FlingServiceTest { FlingEntity flingEntity = invocation.getArgument(0); flingEntity.setId(UUID.randomUUID()); return flingEntity; - }}); + } + }); } @Test @@ -131,11 +125,11 @@ public class FlingServiceTest { public void create_hasAuthCode_setAuthCode() { FlingDto flingDto = new FlingDto(); flingDto.setAuthCode("test"); - - String hashedAuthCode = new String(Hex.encode(keyHashDigest.digest(flingDto.getAuthCode().getBytes()))); - + + when(passwordEncoder.encode(any(String.class))).thenReturn("testhash"); + FlingDto createdFling = flingService.create(flingDto); - assertThat(createdFling.getAuthCode(), is(hashedAuthCode)); + assertThat(createdFling.getAuthCode(), is("testhash")); } @Test @@ -146,35 +140,36 @@ public class FlingServiceTest { FlingDto createdFling = flingService.create(flingDto); assertThat(createdFling.getShareId(), is("test")); } - + @Test public void getByShareId_flingDto() { when(flingRepository.findByShareId("shareId2")).thenReturn(flingEntity2); - + FlingDto foundFling = flingService.getByShareId("shareId2"); assertThat(foundFling.getShareId(), equalTo("shareId2")); } - + @Test public void delete_deletesFromArchiveAndDb() throws IOException { UUID testId = UUID.randomUUID(); flingService.delete(testId); - + verify(archiveService).deleteFling(testId); verify(flingRepository).deleteById(testId); } - + @Test public void validateAuthCode_codesMatch_true() { when(flingRepository.getOne(flingEntity1.getId())).thenReturn(flingEntity1); - + when(passwordEncoder.encode("authCode1")).thenReturn("testhash"); + assertThat(flingService.validateAuthCode(flingEntity1.getId(), "authCode1"), is(true)); } - + @Test public void validateAuthCode_codesDoNotMatch_false() { - when(flingRepository.getOne(flingEntity2.getId())).thenReturn(flingEntity2); - + when(flingRepository.getOne(flingEntity2.getId())).thenReturn(flingEntity2); + assertThat(flingService.validateAuthCode(flingEntity2.getId(), "authCode1"), is(false)); } diff --git a/service/fling/src/test/java/net/friedl/fling/service/archive/FileSystemArchiveTest.java b/service/fling/src/test/java/net/friedl/fling/service/archive/FileSystemArchiveTest.java index fc34e53..e5554b7 100644 --- a/service/fling/src/test/java/net/friedl/fling/service/archive/FileSystemArchiveTest.java +++ b/service/fling/src/test/java/net/friedl/fling/service/archive/FileSystemArchiveTest.java @@ -162,7 +162,8 @@ public class FileSystemArchiveTest { @Test public void storeArtifact_setsArchivedTrue() throws IOException, URISyntaxException { InputStream artifact2Stream = new FileInputStream( - new File(getClass().getClassLoader().getResource("filesystem/artifacts/artifact2").toURI())); + new File( + getClass().getClassLoader().getResource("filesystem/artifacts/artifact2").toURI())); when(artifactRepository.getOne(artifactEntity2.getId())).thenReturn(artifactEntity2); fileSystemArchive.storeArtifact(artifactEntity2.getId(), artifact2Stream); @@ -175,11 +176,12 @@ public class FileSystemArchiveTest { @Test public void storeArtifact_storesArtifactToFlingDisk() throws URISyntaxException, IOException { InputStream artifact2Stream = new FileInputStream( - new File(getClass().getClassLoader().getResource("filesystem/artifacts/artifact2").toURI())); + new File( + getClass().getClassLoader().getResource("filesystem/artifacts/artifact2").toURI())); when(artifactRepository.getOne(artifactEntity2.getId())).thenReturn(artifactEntity2); fileSystemArchive.storeArtifact(artifactEntity2.getId(), artifact2Stream); - + artifact2Stream.close(); InputStream flingStream = diff --git a/service/fling/src/test/resources/application-test.properties b/service/fling/src/test/resources/application-test.properties new file mode 100644 index 0000000..4a61042 --- /dev/null +++ b/service/fling/src/test/resources/application-test.properties @@ -0,0 +1,3 @@ +fling.security.jwt-expiration=18000 +fling.security.admin-name=admin +fling.security.admin-password=123 \ No newline at end of file