0.1 #1
47 changed files with 886 additions and 388 deletions
|
@ -1,10 +1,10 @@
|
||||||
package net.friedl.fling;
|
package net.friedl.fling;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
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.annotation.JsonInclude.Include;
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
@ -16,9 +16,10 @@ import net.friedl.fling.model.json.PathSerializer;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class FlingConfiguration {
|
public class FlingConfiguration {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public MessageDigest keyHashDigest() throws NoSuchAlgorithmException {
|
public PasswordEncoder passwordEncoder() {
|
||||||
return MessageDigest.getInstance("SHA-512");
|
return new Argon2PasswordEncoder();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
package net.friedl.fling.security;
|
package net.friedl.fling;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
import java.util.List;
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
@ -15,26 +14,18 @@ import lombok.Data;
|
||||||
@Configuration
|
@Configuration
|
||||||
@ConfigurationProperties("fling.security")
|
@ConfigurationProperties("fling.security")
|
||||||
public class FlingSecurityConfiguration {
|
public class FlingSecurityConfiguration {
|
||||||
private List<String> allowedOrigins;
|
|
||||||
|
|
||||||
private String adminUser;
|
|
||||||
|
|
||||||
private String adminPassword;
|
|
||||||
|
|
||||||
private String signingKey;
|
private String signingKey;
|
||||||
|
|
||||||
private Long jwtExpiration;
|
@Bean
|
||||||
|
public JwtParser jwtParser() {
|
||||||
|
return Jwts.parserBuilder()
|
||||||
|
.setSigningKey(jwtSigningKey())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public Key jwtSigningKey() {
|
public Key jwtSigningKey() {
|
||||||
byte[] key = signingKey.getBytes(StandardCharsets.UTF_8);
|
byte[] key = signingKey.getBytes(StandardCharsets.UTF_8);
|
||||||
return Keys.hmacShaKeyFor(key);
|
return Keys.hmacShaKeyFor(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
public JwtParser jwtParser(Key jwtSignigKey) {
|
|
||||||
return Jwts.parserBuilder()
|
|
||||||
.setSigningKey(jwtSignigKey)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -9,6 +9,7 @@ import org.springframework.core.io.Resource;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
@ -27,6 +28,7 @@ import net.friedl.fling.service.archive.ArchiveService;
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/artifacts")
|
@RequestMapping("/api/artifacts")
|
||||||
@Tag(name = "artifact", description = "Operations on /api/artifacts")
|
@Tag(name = "artifact", description = "Operations on /api/artifacts")
|
||||||
|
@Validated
|
||||||
public class ArtifactController {
|
public class ArtifactController {
|
||||||
|
|
||||||
private ArtifactService artifactService;
|
private ArtifactService artifactService;
|
||||||
|
@ -52,7 +54,8 @@ public class ArtifactController {
|
||||||
|
|
||||||
@RequestBody(content = @Content(schema = @Schema(type = "string", format = "binary")))
|
@RequestBody(content = @Content(schema = @Schema(type = "string", format = "binary")))
|
||||||
@PostMapping(path = "/{id}/data")
|
@PostMapping(path = "/{id}/data")
|
||||||
public void uploadArtifactData(@PathVariable UUID id, HttpServletRequest request) throws IOException {
|
public void uploadArtifactData(@PathVariable UUID id, HttpServletRequest request)
|
||||||
|
throws IOException {
|
||||||
archiveService.storeArtifact(id, request.getInputStream());
|
archiveService.storeArtifact(id, request.getInputStream());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ package net.friedl.fling.controller;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import javax.validation.Valid;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.core.io.InputStreamResource;
|
import org.springframework.core.io.InputStreamResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
|
@ -51,12 +52,13 @@ public class FlingController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public FlingDto postFling(@RequestBody FlingDto flingDto) {
|
public FlingDto postFling(@RequestBody @Valid FlingDto flingDto) {
|
||||||
return flingService.create(flingDto);
|
return flingService.create(flingDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/{id}/artifact")
|
@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);
|
return artifactService.create(id, artifactDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -31,7 +31,6 @@ public class FlingDto {
|
||||||
private Instant creationTime = Instant.now();
|
private Instant creationTime = Instant.now();
|
||||||
|
|
||||||
@Schema(description = "Share id of the fling. Used in the share link.")
|
@Schema(description = "Share id of the fling. Used in the share link.")
|
||||||
@NotNull
|
|
||||||
private String shareId;
|
private String shareId;
|
||||||
|
|
||||||
@Schema(description = "Authentication code for password protecting a fling.")
|
@Schema(description = "Authentication code for password protecting a fling.")
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -19,7 +19,8 @@ public class PathSerializer extends StdSerializer<Path> {
|
||||||
private static final long serialVersionUID = -1003917305429893614L;
|
private static final long serialVersionUID = -1003917305429893614L;
|
||||||
|
|
||||||
@Override
|
@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());
|
gen.writeString(value.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,10 @@ import net.friedl.fling.persistence.entities.ArtifactEntity;
|
||||||
@Mapper(componentModel = "spring")
|
@Mapper(componentModel = "spring")
|
||||||
public interface ArtifactMapper {
|
public interface ArtifactMapper {
|
||||||
ArtifactDto map(ArtifactEntity artifactEntity);
|
ArtifactDto map(ArtifactEntity artifactEntity);
|
||||||
|
|
||||||
ArtifactEntity map(ArtifactDto artifactDto);
|
ArtifactEntity map(ArtifactDto artifactDto);
|
||||||
|
|
||||||
List<ArtifactDto> mapEntities(List<ArtifactEntity> artifactEntities);
|
List<ArtifactDto> mapEntities(List<ArtifactEntity> artifactEntities);
|
||||||
|
|
||||||
List<ArtifactEntity> mapDtos(List<ArtifactDto> artifactDtos);
|
List<ArtifactEntity> mapDtos(List<ArtifactDto> artifactDtos);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,10 @@ import net.friedl.fling.persistence.entities.FlingEntity;
|
||||||
@Mapper(componentModel = "spring")
|
@Mapper(componentModel = "spring")
|
||||||
public interface FlingMapper {
|
public interface FlingMapper {
|
||||||
FlingDto map(FlingEntity flingEntity);
|
FlingDto map(FlingEntity flingEntity);
|
||||||
|
|
||||||
FlingEntity map(FlingDto flingDto);
|
FlingEntity map(FlingDto flingDto);
|
||||||
|
|
||||||
List<FlingDto> mapEntities(List<FlingEntity> flingEntities);
|
List<FlingDto> mapEntities(List<FlingEntity> flingEntities);
|
||||||
|
|
||||||
List<FlingEntity> mapDtos(List<FlingDto> flingDtos);
|
List<FlingEntity> mapDtos(List<FlingDto> flingDtos);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,8 @@ import lombok.Setter;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "Artifact")
|
@Table(name = "Artifact")
|
||||||
@Getter @Setter
|
@Getter
|
||||||
|
@Setter
|
||||||
public class ArtifactEntity {
|
public class ArtifactEntity {
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue
|
@GeneratedValue
|
||||||
|
|
|
@ -15,7 +15,8 @@ import lombok.Setter;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "Fling")
|
@Table(name = "Fling")
|
||||||
@Getter @Setter
|
@Getter
|
||||||
|
@Setter
|
||||||
public class FlingEntity {
|
public class FlingEntity {
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue
|
@GeneratedValue
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +0,0 @@
|
||||||
package net.friedl.fling.security;
|
|
||||||
|
|
||||||
public enum FlingAuthority {
|
|
||||||
FLING_OWNER, FLING_USER
|
|
||||||
}
|
|
|
@ -3,6 +3,7 @@ package net.friedl.fling.security;
|
||||||
import static org.springframework.security.config.Customizer.withDefaults;
|
import static org.springframework.security.config.Customizer.withDefaults;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
|
@ -16,7 +17,9 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.friedl.fling.FlingSecurityConfiguration;
|
||||||
import net.friedl.fling.security.authentication.JwtAuthenticationFilter;
|
import net.friedl.fling.security.authentication.JwtAuthenticationFilter;
|
||||||
|
import net.friedl.fling.service.AuthorizationService;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Configuration
|
@Configuration
|
||||||
|
@ -24,9 +27,11 @@ import net.friedl.fling.security.authentication.JwtAuthenticationFilter;
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
|
public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
|
||||||
|
@Value("fling.security.allowedOrigins")
|
||||||
|
private List<String> allowedOrigins;
|
||||||
|
|
||||||
private JwtAuthenticationFilter jwtAuthenticationFilter;
|
private JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||||
private AuthorizationService authorizationService;
|
private AuthorizationService authorizationService;
|
||||||
private FlingSecurityConfiguration securityConfiguration;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public FlingWebSecurityConfigurer(JwtAuthenticationFilter jwtAuthenticationFilter,
|
public FlingWebSecurityConfigurer(JwtAuthenticationFilter jwtAuthenticationFilter,
|
||||||
|
@ -35,7 +40,6 @@ public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
|
||||||
|
|
||||||
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
|
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
|
||||||
this.authorizationService = authorizationService;
|
this.authorizationService = authorizationService;
|
||||||
this.securityConfiguration = securityConfiguraiton;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -91,7 +95,7 @@ public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
|
||||||
// And lastly, the owner is allowed everything
|
// And lastly, the owner is allowed everything
|
||||||
.authorizeRequests()
|
.authorizeRequests()
|
||||||
.antMatchers("/api/**")
|
.antMatchers("/api/**")
|
||||||
.hasAuthority(FlingAuthority.FLING_OWNER.name());
|
.hasAuthority(FlingAuthorities.FLING_ADMIN.getAuthority());
|
||||||
|
|
||||||
//@formatter:on
|
//@formatter:on
|
||||||
}
|
}
|
||||||
|
@ -100,10 +104,10 @@ public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
|
||||||
public CorsConfigurationSource corsConfigurationSource() {
|
public CorsConfigurationSource corsConfigurationSource() {
|
||||||
// see https://stackoverflow.com/a/43559266
|
// see https://stackoverflow.com/a/43559266
|
||||||
|
|
||||||
log.info("Allowed origins: {}", securityConfiguration.getAllowedOrigins());
|
log.info("Allowed origins: {}", allowedOrigins);
|
||||||
|
|
||||||
CorsConfiguration configuration = new CorsConfiguration();
|
CorsConfiguration configuration = new CorsConfiguration();
|
||||||
configuration.setAllowedOrigins(securityConfiguration.getAllowedOrigins());
|
configuration.setAllowedOrigins(allowedOrigins);
|
||||||
configuration.setAllowedMethods(List.of("*"));
|
configuration.setAllowedMethods(List.of("*"));
|
||||||
|
|
||||||
// setAllowCredentials(true) is important, otherwise:
|
// setAllowCredentials(true) is important, otherwise:
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,25 +1,36 @@
|
||||||
package net.friedl.fling.security.authentication;
|
package net.friedl.fling.security.authentication;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
|
||||||
public class FlingToken extends AbstractAuthenticationToken {
|
public class FlingToken extends AbstractAuthenticationToken {
|
||||||
|
|
||||||
private static final long serialVersionUID = -1112423505610346583L;
|
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));
|
super(List.of(authority));
|
||||||
this.grantedFlingAuthority = authority;
|
this.jwtToken = jwtToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
public GrantedFlingAuthority getGrantedFlingAuthority() {
|
public boolean authorizedForFling(UUID id) {
|
||||||
return this.grantedFlingAuthority;
|
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
|
@Override
|
||||||
public Object getCredentials() {
|
public String getCredentials() {
|
||||||
return null;
|
return this.jwtToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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 <dev@friedl.net>
|
|
||||||
*/
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,17 +1,19 @@
|
||||||
package net.friedl.fling.security.authentication;
|
package net.friedl.fling.security.authentication;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import javax.servlet.FilterChain;
|
import javax.servlet.FilterChain;
|
||||||
import javax.servlet.ServletException;
|
import javax.servlet.ServletException;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
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.SecurityContext;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.friedl.fling.service.AuthenticationService;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
|
@ -34,7 +36,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
String header = request.getHeader(HEADER_STRING);
|
String header = request.getHeader(HEADER_STRING);
|
||||||
|
|
||||||
if (header == null || !header.startsWith(TOKEN_PREFIX)) {
|
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);
|
filterChain.doFilter(request, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -44,8 +47,12 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
SecurityContext securityContext = SecurityContextHolder.getContext();
|
SecurityContext securityContext = SecurityContextHolder.getContext();
|
||||||
|
|
||||||
if (securityContext.getAuthentication() == null) {
|
if (securityContext.getAuthentication() == null) {
|
||||||
Authentication authentication = authenticationService.parseAuthentication(authToken);
|
log.info("Authenticating request for {} {}?{}", request.getMethod(), request.getRequestURL(),
|
||||||
securityContext.setAuthentication(authentication);
|
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);
|
filterChain.doFilter(request, response);
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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<String> 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<String> 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,13 @@
|
||||||
package net.friedl.fling.service;
|
package net.friedl.fling.service;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import javax.transaction.Transactional;
|
import javax.transaction.Transactional;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.crypto.codec.Hex;
|
|
||||||
import org.springframework.security.crypto.keygen.KeyGenerators;
|
import org.springframework.security.crypto.keygen.KeyGenerators;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
@ -26,17 +25,16 @@ public class FlingService {
|
||||||
private FlingRepository flingRepository;
|
private FlingRepository flingRepository;
|
||||||
private FlingMapper flingMapper;
|
private FlingMapper flingMapper;
|
||||||
private ArchiveService archiveService;
|
private ArchiveService archiveService;
|
||||||
private MessageDigest keyHashDigest;
|
private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public FlingService(FlingRepository flingRepository, FlingMapper flingMapper,
|
public FlingService(FlingRepository flingRepository, FlingMapper flingMapper,
|
||||||
ArchiveService archiveService,
|
ArchiveService archiveService, PasswordEncoder passwordEcoder) {
|
||||||
MessageDigest keyHashDigest) {
|
|
||||||
|
|
||||||
this.flingRepository = flingRepository;
|
this.flingRepository = flingRepository;
|
||||||
this.flingMapper = flingMapper;
|
this.flingMapper = flingMapper;
|
||||||
this.archiveService = archiveService;
|
this.archiveService = archiveService;
|
||||||
this.keyHashDigest = keyHashDigest;
|
this.passwordEncoder = passwordEcoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -106,7 +104,7 @@ public class FlingService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private String hashAuthCode(String authCode) {
|
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);
|
log.debug("Hashed authentication code to {}", hash);
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,7 @@
|
||||||
"sourceType": "net.friedl.fling.security.FlingWebSecurityConfiguration"
|
"sourceType": "net.friedl.fling.security.FlingWebSecurityConfiguration"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "fling.security.admin-user",
|
"name": "fling.security.admin-name",
|
||||||
"type": "java.lang.String",
|
"type": "java.lang.String",
|
||||||
"description": "Username of the admin user/instance owner",
|
"description": "Username of the admin user/instance owner",
|
||||||
"sourceType": "net.friedl.fling.security.FlingWebSecurityConfiguration"
|
"sourceType": "net.friedl.fling.security.FlingWebSecurityConfiguration"
|
||||||
|
|
|
@ -28,10 +28,10 @@ fling:
|
||||||
- "http://localhost:3000"
|
- "http://localhost:3000"
|
||||||
- "http://localhost:5000"
|
- "http://localhost:5000"
|
||||||
- "http://10.0.2.2:5000"
|
- "http://10.0.2.2:5000"
|
||||||
admin-user: "${FLING_ADMIN_USER:admin}"
|
admin-name: "adminName"
|
||||||
admin-password: "${FLING_ADMIN_PASSWORD:123}"
|
admin-password: "adminPassword"
|
||||||
signing-key: "${FLING_SIGNING_KEY:changeitchangeitchangeitchangeit}"
|
signing-key: "changeitchangeitchangeitchangeit"
|
||||||
jwt-expiration: "${FLING_JWT_EXPIRATION:180000}"
|
jwt-expiration: "180000"
|
||||||
api:
|
api:
|
||||||
version: "0"
|
version: "0"
|
||||||
server-url: "http://localhost:8080"
|
server-url: "http://localhost:8080"
|
||||||
|
|
|
@ -19,9 +19,15 @@ fling:
|
||||||
archive.filesystem.archive-path: "/var/fling/files"
|
archive.filesystem.archive-path: "/var/fling/files"
|
||||||
security:
|
security:
|
||||||
allowed-origins:
|
allowed-origins:
|
||||||
- "https://fling.friedl.net"
|
- "https://friedl.net"
|
||||||
- "http://localhost:3000"
|
- "http://localhost:3000"
|
||||||
admin-user: "${FLING_ADMIN_USER:admin}"
|
- "http://localhost:5000"
|
||||||
admin-password: "${FLING_ADMIN_PASSWORD:123}"
|
- "http://10.0.2.2:5000"
|
||||||
signing-key: "${FLING_SIGNING_KEY:changeitchangeitchangeitchangeit}"
|
admin-name: "adminName"
|
||||||
jwt-expiration: "${FLING_JWT_EXPIRATION:180000}"
|
admin-password: "adminPassword"
|
||||||
|
signing-key: "changeitchangeitchangeitchangeit"
|
||||||
|
jwt-expiration: "180000"
|
||||||
|
api:
|
||||||
|
version: "0"
|
||||||
|
server-url: "http://localhost:8080"
|
||||||
|
server-description: "API server for dev"
|
|
@ -110,7 +110,8 @@ class ArtifactControllerTest {
|
||||||
|
|
||||||
mvc.perform(get("/api/artifacts/{id}/data", ARTIFACT_ID))
|
mvc.perform(get("/api/artifacts/{id}/data", ARTIFACT_ID))
|
||||||
.andExpect(header().doesNotExist(HttpHeaders.CONTENT_DISPOSITION))
|
.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());
|
.andExpect(status().isNotFound());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,7 +121,8 @@ class ArtifactControllerTest {
|
||||||
|
|
||||||
mvc.perform(get("/api/artifacts/{id}/data", ARTIFACT_ID))
|
mvc.perform(get("/api/artifacts/{id}/data", ARTIFACT_ID))
|
||||||
.andExpect(header().doesNotExist(HttpHeaders.CONTENT_DISPOSITION))
|
.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());
|
.andExpect(status().isInternalServerError());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -96,6 +96,16 @@ public class FlingControllerTest {
|
||||||
.andExpect(status().isOk());
|
.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
|
@Test
|
||||||
public void postArtifact_ok() throws Exception {
|
public void postArtifact_ok() throws Exception {
|
||||||
mockMvc.perform(post("/api/fling/{id}/artifact", flingId)
|
mockMvc.perform(post("/api/fling/{id}/artifact", flingId)
|
||||||
|
@ -104,6 +114,16 @@ public class FlingControllerTest {
|
||||||
.andExpect(status().isOk());
|
.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
|
@Test
|
||||||
public void getFling_noFlingWithId_notFound() throws Exception {
|
public void getFling_noFlingWithId_notFound() throws Exception {
|
||||||
doThrow(EntityNotFoundException.class).when(flingService).getById(flingId);
|
doThrow(EntityNotFoundException.class).when(flingService).getById(flingId);
|
||||||
|
|
|
@ -75,8 +75,9 @@ public class FlingDtoTest {
|
||||||
assertThat(violation.getMessage()).isEqualTo("must not be null");
|
assertThat(violation.getMessage()).isEqualTo("must not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testSetShareId_null_validationFails() {
|
void testSetShareId_null_validationOk() { // must be nullable to support defaulting in service
|
||||||
FlingDto flingDto = FlingDto.builder()
|
FlingDto flingDto = FlingDto.builder()
|
||||||
.id(new UUID(0L, 0L))
|
.id(new UUID(0L, 0L))
|
||||||
.name("test")
|
.name("test")
|
||||||
|
@ -87,10 +88,7 @@ public class FlingDtoTest {
|
||||||
|
|
||||||
Set<ConstraintViolation<FlingDto>> constraintViolations = validator.validate(flingDto);
|
Set<ConstraintViolation<FlingDto>> constraintViolations = validator.validate(flingDto);
|
||||||
|
|
||||||
assertThat(constraintViolations).hasSize(1);
|
assertThat(constraintViolations).hasSize(0);
|
||||||
ConstraintViolation<FlingDto> violation = constraintViolations.iterator().next();
|
|
||||||
assertThat(violation.getPropertyPath().toString()).isEqualTo("shareId");
|
|
||||||
assertThat(violation.getMessage()).isEqualTo("must not be null");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -62,7 +62,8 @@ public class ArtifactServiceTest {
|
||||||
FlingRepository flingRepository, ArtifactMapper artifactMapper,
|
FlingRepository flingRepository, ArtifactMapper artifactMapper,
|
||||||
ArchiveService archiveService) {
|
ArchiveService archiveService) {
|
||||||
|
|
||||||
return new ArtifactService(artifactRepository, flingRepository, artifactMapper, archiveService);
|
return new ArtifactService(artifactRepository, flingRepository, artifactMapper,
|
||||||
|
archiveService);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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<Claims> 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<Claims> 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<Claims> 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"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,8 +11,6 @@ import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
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.context.TestConfiguration;
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
import org.springframework.context.annotation.Bean;
|
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 org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||||
import net.friedl.fling.model.dto.FlingDto;
|
import net.friedl.fling.model.dto.FlingDto;
|
||||||
import net.friedl.fling.model.mapper.FlingMapper;
|
import net.friedl.fling.model.mapper.FlingMapper;
|
||||||
|
@ -42,8 +40,8 @@ public class FlingServiceTest {
|
||||||
@Autowired
|
@Autowired
|
||||||
private FlingMapper flingMapper;
|
private FlingMapper flingMapper;
|
||||||
|
|
||||||
@Autowired
|
@MockBean
|
||||||
private MessageDigest keyHashDigest;
|
private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
@MockBean
|
@MockBean
|
||||||
private FlingRepository flingRepository;
|
private FlingRepository flingRepository;
|
||||||
|
@ -62,15 +60,10 @@ public class FlingServiceTest {
|
||||||
return new FlingMapperImpl();
|
return new FlingMapperImpl();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
public MessageDigest keyHashDigest() throws NoSuchAlgorithmException {
|
|
||||||
return MessageDigest.getInstance("SHA-512");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public FlingService flingService(FlingRepository flingRepository, FlingMapper flingMapper,
|
public FlingService flingService(FlingRepository flingRepository, FlingMapper flingMapper,
|
||||||
ArchiveService archiveService, MessageDigest keyHashDigest) {
|
ArchiveService archiveService, PasswordEncoder passwordEncoder) {
|
||||||
return new FlingService(flingRepository, flingMapper, archiveService, keyHashDigest);
|
return new FlingService(flingRepository, flingMapper, archiveService, passwordEncoder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +72,7 @@ public class FlingServiceTest {
|
||||||
this.flingEntity1 = new FlingEntity();
|
this.flingEntity1 = new FlingEntity();
|
||||||
flingEntity1.setId(UUID.randomUUID());
|
flingEntity1.setId(UUID.randomUUID());
|
||||||
flingEntity1.setName("fling1");
|
flingEntity1.setName("fling1");
|
||||||
flingEntity1.setAuthCode(new String(Hex.encode(keyHashDigest.digest("authCode1".getBytes()))));
|
flingEntity1.setAuthCode("testhash");
|
||||||
flingEntity1.setCreationTime(Instant.now());
|
flingEntity1.setCreationTime(Instant.now());
|
||||||
|
|
||||||
this.flingEntity2 = new FlingEntity();
|
this.flingEntity2 = new FlingEntity();
|
||||||
|
@ -94,7 +87,8 @@ public class FlingServiceTest {
|
||||||
FlingEntity flingEntity = invocation.getArgument(0);
|
FlingEntity flingEntity = invocation.getArgument(0);
|
||||||
flingEntity.setId(UUID.randomUUID());
|
flingEntity.setId(UUID.randomUUID());
|
||||||
return flingEntity;
|
return flingEntity;
|
||||||
}});
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -132,10 +126,10 @@ public class FlingServiceTest {
|
||||||
FlingDto flingDto = new FlingDto();
|
FlingDto flingDto = new FlingDto();
|
||||||
flingDto.setAuthCode("test");
|
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);
|
FlingDto createdFling = flingService.create(flingDto);
|
||||||
assertThat(createdFling.getAuthCode(), is(hashedAuthCode));
|
assertThat(createdFling.getAuthCode(), is("testhash"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -167,6 +161,7 @@ public class FlingServiceTest {
|
||||||
@Test
|
@Test
|
||||||
public void validateAuthCode_codesMatch_true() {
|
public void validateAuthCode_codesMatch_true() {
|
||||||
when(flingRepository.getOne(flingEntity1.getId())).thenReturn(flingEntity1);
|
when(flingRepository.getOne(flingEntity1.getId())).thenReturn(flingEntity1);
|
||||||
|
when(passwordEncoder.encode("authCode1")).thenReturn("testhash");
|
||||||
|
|
||||||
assertThat(flingService.validateAuthCode(flingEntity1.getId(), "authCode1"), is(true));
|
assertThat(flingService.validateAuthCode(flingEntity1.getId(), "authCode1"), is(true));
|
||||||
}
|
}
|
||||||
|
|
|
@ -162,7 +162,8 @@ public class FileSystemArchiveTest {
|
||||||
@Test
|
@Test
|
||||||
public void storeArtifact_setsArchivedTrue() throws IOException, URISyntaxException {
|
public void storeArtifact_setsArchivedTrue() throws IOException, URISyntaxException {
|
||||||
InputStream artifact2Stream = new FileInputStream(
|
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);
|
when(artifactRepository.getOne(artifactEntity2.getId())).thenReturn(artifactEntity2);
|
||||||
|
|
||||||
fileSystemArchive.storeArtifact(artifactEntity2.getId(), artifact2Stream);
|
fileSystemArchive.storeArtifact(artifactEntity2.getId(), artifact2Stream);
|
||||||
|
@ -175,7 +176,8 @@ public class FileSystemArchiveTest {
|
||||||
@Test
|
@Test
|
||||||
public void storeArtifact_storesArtifactToFlingDisk() throws URISyntaxException, IOException {
|
public void storeArtifact_storesArtifactToFlingDisk() throws URISyntaxException, IOException {
|
||||||
InputStream artifact2Stream = new FileInputStream(
|
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);
|
when(artifactRepository.getOne(artifactEntity2.getId())).thenReturn(artifactEntity2);
|
||||||
|
|
||||||
fileSystemArchive.storeArtifact(artifactEntity2.getId(), artifact2Stream);
|
fileSystemArchive.storeArtifact(artifactEntity2.getId(), artifact2Stream);
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
fling.security.jwt-expiration=18000
|
||||||
|
fling.security.admin-name=admin
|
||||||
|
fling.security.admin-password=123
|
Loading…
Reference in a new issue