0.1 #1

Merged
armin merged 30 commits from 0.1 into master 2020-07-26 00:51:55 +00:00
47 changed files with 886 additions and 388 deletions
Showing only changes of commit 415687c601 - Show all commits

View file

@ -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

View file

@ -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();
}
} }

View file

@ -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());
} }

View file

@ -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"));
}
}

View file

@ -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);
} }

View file

@ -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;
}

View file

@ -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.")

View file

@ -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;
}

View file

@ -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());
} }

View file

@ -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);
} }

View file

@ -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);
} }

View file

@ -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

View file

@ -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

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -1,5 +0,0 @@
package net.friedl.fling.security;
public enum FlingAuthority {
FLING_OWNER, FLING_USER
}

View file

@ -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:

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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);

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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;
} }
/** /**
@ -96,7 +94,7 @@ public class FlingService {
public boolean validateAuthCode(UUID id, String authCode) { public boolean validateAuthCode(UUID id, String authCode) {
FlingEntity flingEntity = flingRepository.getOne(id); 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 return false; // only one of them is empty; implicit null safety check
} }
@ -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;
} }

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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());
} }

View file

@ -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"));
}
}

View file

@ -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);

View file

@ -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

View file

@ -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);
} }
} }
@ -76,7 +77,7 @@ public class ArtifactServiceTest {
this.artifactEntity2 = new ArtifactEntity(); this.artifactEntity2 = new ArtifactEntity();
artifactEntity2.setId(UUID.randomUUID()); artifactEntity2.setId(UUID.randomUUID());
artifactEntity2.setUploadTime(Instant.EPOCH.plus(12000, ChronoUnit.DAYS)); artifactEntity2.setUploadTime(Instant.EPOCH.plus(12000, ChronoUnit.DAYS));
artifactEntity2.setPath(Path.of("/","/sub","artifact2")); artifactEntity2.setPath(Path.of("/", "/sub", "artifact2"));
this.flingEntity = new FlingEntity(); this.flingEntity = new FlingEntity();
flingEntity.setId(UUID.randomUUID()); flingEntity.setId(UUID.randomUUID());
@ -87,7 +88,7 @@ public class ArtifactServiceTest {
@Override @Override
public FlingEntity answer(InvocationOnMock invocation) throws Throwable { public FlingEntity answer(InvocationOnMock invocation) throws Throwable {
FlingEntity flingEntity = invocation.getArgument(0); FlingEntity flingEntity = invocation.getArgument(0);
if(flingEntity.getId() == null) flingEntity.setId(UUID.randomUUID()); if (flingEntity.getId() == null) flingEntity.setId(UUID.randomUUID());
return flingEntity; return flingEntity;
} }
}); });

View file

@ -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"));
}
}

View file

@ -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));
}
}

View file

@ -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));
} }

View file

@ -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);

View file

@ -0,0 +1,3 @@
fling.security.jwt-expiration=18000
fling.security.admin-name=admin
fling.security.admin-password=123