Generate derived authentication tokens
All checks were successful
continuous-integration/drone/push Build is passing

Can be used to authorize download urls via query parameter since
the tokens can be used only once.
This commit is contained in:
Armin Friedl 2020-07-21 21:24:10 +02:00
parent c07a7866ce
commit a07379ebad
Signed by: armin
GPG key ID: 48C726EEE7FBCBC8
20 changed files with 483 additions and 189 deletions

View file

@ -47,6 +47,11 @@ Content-Type: application/json
:token :token
{"name": "Fling from querysheet with Auth and very long name", "expirationClicks": 12, "shared": true, "authCode": "abc"} {"name": "Fling from querysheet with Auth and very long name", "expirationClicks": 12, "shared": true, "authCode": "abc"}
# GET derived auth token
GET http://localhost:8080/api/auth/derive
Content-Type: application/json
:token
# #
:flingId = dfc208a3-5924-43b4-aa6a-c263541dca5e :flingId = dfc208a3-5924-43b4-aa6a-c263541dca5e

View file

@ -1,17 +1,23 @@
package net.friedl.fling.controller; package net.friedl.fling.controller;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.AccessDeniedException;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import net.friedl.fling.model.dto.AdminAuthDto; import net.friedl.fling.model.dto.AdminAuthDto;
import net.friedl.fling.model.dto.UserAuthDto; import net.friedl.fling.model.dto.UserAuthDto;
import net.friedl.fling.security.FlingWebSecurityConfigurer;
import net.friedl.fling.service.AuthenticationService; import net.friedl.fling.service.AuthenticationService;
@RestController @RestController
@ -50,4 +56,23 @@ public class AuthenticationController {
return authenticationService.authenticate(userAuthDto) return authenticationService.authenticate(userAuthDto)
.orElseThrow(() -> new AccessDeniedException("Wrong username or password")); .orElseThrow(() -> new AccessDeniedException("Wrong username or password"));
} }
//@formatter:off
/**
* Note that this endpoint is not protected. But the token will only get authority of the
* authenticated user.
* @see FlingWebSecurityConfigurer
* @see AuthenticationService
*/
@Operation(description = "Generate a derived token from the current authorization")
@ApiResponse(responseCode = "200", description = "Token impersonating the user")
@SecurityRequirement(name = "bearer")
@GetMapping("/derive")
public String deriveToken(
@Parameter(allowEmptyValue = true, description = "Token can only be used for authorizing one request. Defaults to true")
@RequestParam Optional<Boolean> singleUse)
{
return authenticationService.deriveToken(singleUse.orElse(true));
}
//@formatter:on
} }

View file

@ -0,0 +1,38 @@
package net.friedl.fling.persistence.entities;
import java.time.Instant;
import java.util.UUID;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Version;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "Token")
@Getter
@Setter
public class TokenEntity {
@Id
private UUID id; // Note that this is not generated to ensure randomness independent from the
// persistence provider
@Column(nullable = false)
private Boolean singleUse = true;
@Column(nullable = false)
private String token; // JWT token this token is derived from
@CreationTimestamp
private Instant creationTime;
@UpdateTimestamp
private Instant updateTime;
@Version
private Long version;
}

View file

@ -0,0 +1,8 @@
package net.friedl.fling.persistence.repositories;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import net.friedl.fling.persistence.entities.TokenEntity;
public interface TokenRepository extends JpaRepository<TokenEntity, UUID> {
}

View file

@ -4,7 +4,7 @@ import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
public enum FlingAuthorities { public enum FlingAuthorities {
FLING_ADMIN("admin"), FLING_USER("user"); FLING_ADMIN("admin"), FLING_USER("user"), FLING_TOKEN("token");
String authority; String authority;

View file

@ -1,6 +1,7 @@
package net.friedl.fling.security; package net.friedl.fling.security;
import static net.friedl.fling.security.FlingAuthorities.FLING_ADMIN; import static net.friedl.fling.security.FlingAuthorities.FLING_ADMIN;
import static net.friedl.fling.security.FlingAuthorities.FLING_USER;
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;
@ -18,8 +19,8 @@ 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.filter.BearerAuthenticationFilter;
import net.friedl.fling.security.authentication.JwtAuthenticationFilter; import net.friedl.fling.security.authentication.filter.TokenAuthenticationFilter;
import net.friedl.fling.service.AuthorizationService; import net.friedl.fling.service.AuthorizationService;
@Slf4j @Slf4j
@ -30,15 +31,18 @@ import net.friedl.fling.service.AuthorizationService;
public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter { public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
private List<String> allowedOrigins; private List<String> allowedOrigins;
private JwtAuthenticationFilter jwtAuthenticationFilter; private TokenAuthenticationFilter tokenAuthenticationFilter;
private BearerAuthenticationFilter bearerAuthenticationFilter;
private AuthorizationService authorizationService; private AuthorizationService authorizationService;
@Autowired @Autowired
public FlingWebSecurityConfigurer(JwtAuthenticationFilter jwtAuthenticationFilter, public FlingWebSecurityConfigurer(
AuthorizationService authorizationService, TokenAuthenticationFilter tokenAuthenticationFilter,
FlingSecurityConfiguration securityConfiguraiton) { BearerAuthenticationFilter bearerAuthenticationFilter,
AuthorizationService authorizationService) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter; this.tokenAuthenticationFilter = tokenAuthenticationFilter;
this.bearerAuthenticationFilter = bearerAuthenticationFilter;
this.authorizationService = authorizationService; this.authorizationService = authorizationService;
} }
@ -52,7 +56,8 @@ public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
/**********************************************/ /**********************************************/
/** Authentication Interceptor Configuration **/ /** Authentication Interceptor Configuration **/
/**********************************************/ /**********************************************/
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(bearerAuthenticationFilter, TokenAuthenticationFilter.class)
// Do not keep authorization token in session. This would interfere with bearer authentication // Do not keep authorization token in session. This would interfere with bearer authentication
// in that it is possible to authenticate without a bearer token if the session is kept. // in that it is possible to authenticate without a bearer token if the session is kept.
// Turn off this confusing and non-obvious behavior. // Turn off this confusing and non-obvious behavior.
@ -68,6 +73,10 @@ public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
/**********************************/ /**********************************/
/** Authorization for: /api/auth **/ /** Authorization for: /api/auth **/
/**********************************/ /**********************************/
.authorizeRequests()
.antMatchers("/api/auth/derive")
.hasAnyAuthority(FLING_ADMIN.getAuthority(), FLING_USER.getAuthority())
.and()
.authorizeRequests() .authorizeRequests()
.antMatchers("/api/auth/**") .antMatchers("/api/auth/**")
.permitAll() .permitAll()

View file

@ -4,25 +4,31 @@ import java.util.List;
import java.util.UUID; import java.util.UUID;
import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import net.friedl.fling.security.authentication.authorities.FlingAdminAuthority;
import net.friedl.fling.security.authentication.authorities.FlingUserAuthority;
public class FlingToken extends AbstractAuthenticationToken { public class FlingToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = -1112423505610346583L; private static final long serialVersionUID = -1112423505610346583L;
private String jwtToken; private String token;
public FlingToken(GrantedAuthority authority, String jwtToken) { public FlingToken(List<GrantedAuthority> authorities, String token) {
super(List.of(authority)); super(authorities);
this.jwtToken = jwtToken; this.token = token;
} }
public boolean authorizedForFling(UUID id) { public boolean authorizedForFling(UUID id) {
for (GrantedAuthority grantedAuthority : getAuthorities()) { for (GrantedAuthority grantedAuthority : getAuthorities()) {
if (grantedAuthority instanceof FlingAdminAuthority) return true; if (grantedAuthority instanceof FlingAdminAuthority) {
return true;
}
if (!(grantedAuthority instanceof FlingUserAuthority)) continue; if (grantedAuthority instanceof FlingUserAuthority) {
UUID grantedFlingId = ((FlingUserAuthority) grantedAuthority).getFlingId();
UUID grantedFlingId = ((FlingUserAuthority) grantedAuthority).getFlingId(); if (grantedFlingId.equals(id)) {
if (grantedFlingId.equals(id)) return true; return true;
}
}
} }
return false; return false;
@ -30,7 +36,7 @@ public class FlingToken extends AbstractAuthenticationToken {
@Override @Override
public String getCredentials() { public String getCredentials() {
return this.jwtToken; return this.token;
} }
@Override @Override

View file

@ -1,4 +1,4 @@
package net.friedl.fling.security.authentication; package net.friedl.fling.security.authentication.authorities;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import net.friedl.fling.security.FlingAuthorities; import net.friedl.fling.security.FlingAuthorities;

View file

@ -1,4 +1,4 @@
package net.friedl.fling.security.authentication; package net.friedl.fling.security.authentication.authorities;
import java.util.UUID; import java.util.UUID;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;

View file

@ -1,4 +1,4 @@
package net.friedl.fling.security.authentication; package net.friedl.fling.security.authentication.filter;
import java.io.IOException; import java.io.IOException;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -13,18 +13,19 @@ 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.security.authentication.FlingToken;
import net.friedl.fling.service.AuthenticationService; import net.friedl.fling.service.AuthenticationService;
@Slf4j @Slf4j
@Component @Component
public class JwtAuthenticationFilter extends OncePerRequestFilter { public class BearerAuthenticationFilter extends OncePerRequestFilter {
private static final String TOKEN_PREFIX = "Bearer "; private static final String TOKEN_PREFIX = "Bearer ";
private static final String HEADER_STRING = "Authorization"; private static final String HEADER_STRING = "Authorization";
private AuthenticationService authenticationService; private AuthenticationService authenticationService;
@Autowired @Autowired
public JwtAuthenticationFilter(AuthenticationService authenticationService) { public BearerAuthenticationFilter(AuthenticationService authenticationService) {
this.authenticationService = authenticationService; this.authenticationService = authenticationService;
} }
@ -37,7 +38,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
if (header == null || !header.startsWith(TOKEN_PREFIX)) { if (header == null || !header.startsWith(TOKEN_PREFIX)) {
log.info("Anonymous request for {} {}{}", request.getMethod(), request.getRequestURL(), log.info("Anonymous request for {} {}{}", request.getMethod(), request.getRequestURL(),
request.getQueryString() != null ? "?"+request.getQueryString(): ""); request.getQueryString() != null ? "?" + request.getQueryString() : "");
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
return; return;
} }
@ -48,8 +49,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
if (securityContext.getAuthentication() == null) { if (securityContext.getAuthentication() == null) {
log.info("Authenticating request for {} {}{}", request.getMethod(), request.getRequestURL(), log.info("Authenticating request for {} {}{}", request.getMethod(), request.getRequestURL(),
request.getQueryString() != null ? "?"+request.getQueryString(): ""); request.getQueryString() != null ? "?" + request.getQueryString() : "");
FlingToken token = authenticationService.parseAuthentication(authToken); FlingToken token = authenticationService.parseJwtAuthentication(authToken);
log.info("Authenticated as {}", token.getAuthorities().stream() log.info("Authenticated as {}", token.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.joining(","))); .map(GrantedAuthority::getAuthority).collect(Collectors.joining(",")));
securityContext.setAuthentication(token); securityContext.setAuthentication(token);

View file

@ -0,0 +1,59 @@
package net.friedl.fling.security.authentication.filter;
import java.io.IOException;
import java.util.stream.Collectors;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import lombok.extern.slf4j.Slf4j;
import net.friedl.fling.security.authentication.FlingToken;
import net.friedl.fling.service.AuthenticationService;
@Slf4j
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private AuthenticationService authenticationService;
@Autowired
public TokenAuthenticationFilter(AuthenticationService authenticationService) {
this.authenticationService = authenticationService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String derivedToken = request.getParameter("derivedtoken");
if (derivedToken == null) {
log.info("No derived token in request for {} {}{}", request.getMethod(),
request.getRequestURL(),
request.getQueryString() != null ? "?" + request.getQueryString() : "");
filterChain.doFilter(request, response);
return;
}
SecurityContext securityContext = SecurityContextHolder.getContext();
if (securityContext.getAuthentication() == null) {
log.info("Authenticating request for {} {}{}", request.getMethod(), request.getRequestURL(),
request.getQueryString() != null ? "?" + request.getQueryString() : "");
FlingToken token = authenticationService.parseDerivedToken(derivedToken);
log.info("Authenticated as {}", token.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.joining(",")));
securityContext.setAuthentication(token);
}
filterChain.doFilter(request, response);
}
}

View file

@ -56,7 +56,7 @@ public class ArtifactService {
*/ */
public ArtifactDto create(UUID flingId, ArtifactDto artifactDto) { public ArtifactDto create(UUID flingId, ArtifactDto artifactDto) {
FlingEntity flingEntity = flingRepository.getOne(flingId); FlingEntity flingEntity = flingRepository.getOne(flingId);
log.debug("Creating new ArtifactEntity for ArtifactDto[.path={}]", artifactDto.getPath()); log.debug("Creating new ArtifactEntity for ArtifactDto[.path={}]", artifactDto.getPath());
ArtifactEntity artifactEntity = artifactMapper.map(artifactDto); ArtifactEntity artifactEntity = artifactMapper.map(artifactDto);
artifactEntity.setFling(flingEntity); artifactEntity.setFling(flingEntity);

View file

@ -3,12 +3,16 @@ package net.friedl.fling.service;
import java.security.Key; import java.security.Key;
import java.time.Instant; import java.time.Instant;
import java.util.Date; import java.util.Date;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import javax.persistence.EntityNotFoundException; import javax.persistence.EntityNotFoundException;
import javax.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
@ -19,10 +23,12 @@ import lombok.extern.slf4j.Slf4j;
import net.friedl.fling.model.dto.AdminAuthDto; import net.friedl.fling.model.dto.AdminAuthDto;
import net.friedl.fling.model.dto.UserAuthDto; import net.friedl.fling.model.dto.UserAuthDto;
import net.friedl.fling.persistence.entities.FlingEntity; import net.friedl.fling.persistence.entities.FlingEntity;
import net.friedl.fling.persistence.entities.TokenEntity;
import net.friedl.fling.persistence.repositories.FlingRepository; import net.friedl.fling.persistence.repositories.FlingRepository;
import net.friedl.fling.security.authentication.FlingAdminAuthority; import net.friedl.fling.persistence.repositories.TokenRepository;
import net.friedl.fling.security.authentication.FlingToken; import net.friedl.fling.security.authentication.FlingToken;
import net.friedl.fling.security.authentication.FlingUserAuthority; import net.friedl.fling.security.authentication.authorities.FlingAdminAuthority;
import net.friedl.fling.security.authentication.authorities.FlingUserAuthority;
@Slf4j @Slf4j
@Service @Service
@ -30,6 +36,7 @@ public class AuthenticationService {
private JwtParser jwtParser; private JwtParser jwtParser;
private Key jwtSigningKey; private Key jwtSigningKey;
private FlingRepository flingRepository; private FlingRepository flingRepository;
private TokenRepository tokenRepository;
private PasswordEncoder passwordEncoder; private PasswordEncoder passwordEncoder;
@Value("${fling.security.admin-name}") @Value("${fling.security.admin-name}")
@ -41,12 +48,14 @@ public class AuthenticationService {
@Autowired @Autowired
public AuthenticationService(JwtParser jwtParser, Key jwtSigningKey, public AuthenticationService(JwtParser jwtParser, Key jwtSigningKey,
PasswordEncoder passwordEncoder, FlingRepository flingRepository) { PasswordEncoder passwordEncoder, FlingRepository flingRepository,
TokenRepository tokenRepository) {
this.jwtParser = jwtParser; this.jwtParser = jwtParser;
this.jwtSigningKey = jwtSigningKey; this.jwtSigningKey = jwtSigningKey;
this.passwordEncoder = passwordEncoder; this.passwordEncoder = passwordEncoder;
this.flingRepository = flingRepository; this.flingRepository = flingRepository;
this.tokenRepository = tokenRepository;
} }
public Optional<String> authenticate(AdminAuthDto adminAuth) { public Optional<String> authenticate(AdminAuthDto adminAuth) {
@ -71,7 +80,9 @@ public class AuthenticationService {
public Optional<String> authenticate(UserAuthDto userAuth) { public Optional<String> authenticate(UserAuthDto userAuth) {
log.info("Authenticating for fling [.shareId={}]", userAuth.getShareId()); log.info("Authenticating for fling [.shareId={}]", userAuth.getShareId());
FlingEntity flingEntity = flingRepository.findByShareId(userAuth.getShareId()); FlingEntity flingEntity = flingRepository.findByShareId(userAuth.getShareId());
if(flingEntity == null) { throw new EntityNotFoundException("No entity for shareId="+userAuth.getShareId()); } if (flingEntity == null) {
throw new EntityNotFoundException("No entity for shareId=" + userAuth.getShareId());
}
String providedAuthCodeHash = passwordEncoder.encode(userAuth.getAuthCode()); String providedAuthCodeHash = passwordEncoder.encode(userAuth.getAuthCode());
String actualAuthCodeHash = flingEntity.getAuthCode(); String actualAuthCodeHash = flingEntity.getAuthCode();
@ -90,15 +101,15 @@ public class AuthenticationService {
} }
public FlingToken parseAuthentication(String token) { public FlingToken parseJwtAuthentication(String token) {
Claims claims = jwtParser.parseClaimsJws(token).getBody(); Claims claims = jwtParser.parseClaimsJws(token).getBody();
switch (claims.getSubject()) { switch (claims.getSubject()) {
case "admin": case "admin":
return new FlingToken(new FlingAdminAuthority(), token); return new FlingToken(List.of(new FlingAdminAuthority()), token);
case "user": case "user":
UUID grantedFlingId = UUID.fromString(claims.get("id", String.class)); UUID grantedFlingId = UUID.fromString(claims.get("id", String.class));
return new FlingToken(new FlingUserAuthority(grantedFlingId), token); return new FlingToken(List.of(new FlingUserAuthority(grantedFlingId)), token);
default: default:
throw new BadCredentialsException("Invalid token"); throw new BadCredentialsException("Invalid token");
} }
@ -116,4 +127,47 @@ public class AuthenticationService {
.setExpiration(Date.from(Instant.now().plusSeconds(jwtExpiration))) .setExpiration(Date.from(Instant.now().plusSeconds(jwtExpiration)))
.signWith(jwtSigningKey); .signWith(jwtSigningKey);
} }
/**
* Creates a derived token with the given settings. Note that the returned string is opaque and
* should not not be interpreted in any way but only used as is.
*
* @param singleUse Whether this token should be deleted after a single use
* @return An opaque string representing the token
*/
@Transactional
public String deriveToken(Boolean singleUse) {
UUID id = UUID.randomUUID();
TokenEntity tokenEntity = new TokenEntity();
tokenEntity.setId(id);
if (singleUse != null) {
tokenEntity.setSingleUse(singleUse);
}
SecurityContext securityContext = SecurityContextHolder.getContext();
if (securityContext.getAuthentication() instanceof FlingToken) {
FlingToken flingToken = (FlingToken) securityContext.getAuthentication();
tokenEntity.setToken(flingToken.getCredentials());
} else {
// This should be prevented in FlingWebSecurityConfigurer
throw new IllegalStateException("Cannot derive token from current authentication");
}
tokenRepository.save(tokenEntity);
return id.toString();
}
@Transactional
public FlingToken parseDerivedToken(String derivedToken) {
TokenEntity tokenEntity = tokenRepository.getOne(UUID.fromString(derivedToken));
FlingToken flingToken = parseJwtAuthentication(tokenEntity.getToken());
if (tokenEntity.getSingleUse()) {
tokenRepository.delete(tokenEntity);
}
return flingToken;
}
} }

View file

@ -75,7 +75,9 @@ public class AuthorizationService {
} }
FlingEntity flingEntity = flingRepository.findByShareId(shareId); FlingEntity flingEntity = flingRepository.findByShareId(shareId);
if(flingEntity == null) { throw new EntityNotFoundException("No entity for shareId="+shareId); } if (flingEntity == null) {
throw new EntityNotFoundException("No entity for shareId=" + shareId);
}
return allowFlingAccess(flingEntity.getId(), token); return allowFlingAccess(flingEntity.getId(), token);
} }

View file

@ -90,7 +90,9 @@ public class FlingService {
public FlingDto getByShareId(String shareId) { public FlingDto getByShareId(String shareId) {
FlingEntity flingEntity = flingRepository.findByShareId(shareId); FlingEntity flingEntity = flingRepository.findByShareId(shareId);
if(flingEntity == null) { throw new EntityNotFoundException("No entity for shareId="+shareId); } if (flingEntity == null) {
throw new EntityNotFoundException("No entity for shareId=" + shareId);
}
return flingMapper.map(flingEntity); return flingMapper.map(flingEntity);
} }

View file

@ -84,4 +84,5 @@ public class AuthenticationControllerTest {
.andExpect(status().is(200)) .andExpect(status().is(200))
.andExpect(content().string("token")); .andExpect(content().string("token"));
} }
} }

View file

@ -1,24 +1,33 @@
package net.friedl.fling.service; package net.friedl.fling.service;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.emptyOrNullString;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import java.security.Key; import java.security.Key;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import javax.persistence.EntityNotFoundException; import javax.persistence.EntityNotFoundException;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired; 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.authentication.BadCredentialsException; import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
@ -33,10 +42,13 @@ import io.jsonwebtoken.security.Keys;
import net.friedl.fling.model.dto.AdminAuthDto; import net.friedl.fling.model.dto.AdminAuthDto;
import net.friedl.fling.model.dto.UserAuthDto; import net.friedl.fling.model.dto.UserAuthDto;
import net.friedl.fling.persistence.entities.FlingEntity; import net.friedl.fling.persistence.entities.FlingEntity;
import net.friedl.fling.persistence.entities.TokenEntity;
import net.friedl.fling.persistence.repositories.FlingRepository; import net.friedl.fling.persistence.repositories.FlingRepository;
import net.friedl.fling.security.authentication.FlingAdminAuthority; import net.friedl.fling.persistence.repositories.TokenRepository;
import net.friedl.fling.security.FlingAuthorities;
import net.friedl.fling.security.authentication.FlingToken; import net.friedl.fling.security.authentication.FlingToken;
import net.friedl.fling.security.authentication.FlingUserAuthority; import net.friedl.fling.security.authentication.authorities.FlingAdminAuthority;
import net.friedl.fling.security.authentication.authorities.FlingUserAuthority;
@ExtendWith(SpringExtension.class) @ExtendWith(SpringExtension.class)
@TestPropertySource("classpath:/application-test.properties") @TestPropertySource("classpath:/application-test.properties")
@ -48,6 +60,9 @@ public class AuthenticationServiceTest {
@MockBean @MockBean
private FlingRepository flingRepository; private FlingRepository flingRepository;
@MockBean
private TokenRepository tokenRepository;
@MockBean @MockBean
private JwtParser jwtParser; private JwtParser jwtParser;
@ -60,8 +75,11 @@ public class AuthenticationServiceTest {
@Bean @Bean
public AuthenticationService authenticationService(JwtParser jwtParser, public AuthenticationService authenticationService(JwtParser jwtParser,
PasswordEncoder passwordEncoder, FlingRepository flingRepository) { PasswordEncoder passwordEncoder, FlingRepository flingRepository,
return new AuthenticationService(jwtParser, jwtSigningKey, passwordEncoder, flingRepository); TokenRepository tokenRepository) {
return new AuthenticationService(jwtParser, jwtSigningKey, passwordEncoder, flingRepository,
tokenRepository);
} }
} }
@ -118,23 +136,24 @@ public class AuthenticationServiceTest {
@Test @Test
public void authenticate_noFlingForShareId_throws() { public void authenticate_noFlingForShareId_throws() {
UserAuthDto userAuthDto = UserAuthDto.builder() UserAuthDto userAuthDto = UserAuthDto.builder()
.authCode("authCode") .authCode("authCode")
.shareId("doesNotExist").build(); .shareId("doesNotExist").build();
when(flingRepository.findByShareId(any(String.class))).thenReturn(null); when(flingRepository.findByShareId(any(String.class))).thenReturn(null);
when(passwordEncoder.encode(any(String.class))).thenReturn("authCodeHash"); when(passwordEncoder.encode(any(String.class))).thenReturn("authCodeHash");
assertThrows(EntityNotFoundException.class, () -> authenticationService.authenticate(userAuthDto)); assertThrows(EntityNotFoundException.class,
() -> authenticationService.authenticate(userAuthDto));
} }
@Test @Test
public void parseAuthentication_owner_AdminAuthority() { public void parseJwtAuthentication_owner_AdminAuthority() {
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(), Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
new DefaultClaims(Map.of("sub", "admin")), "signature"); new DefaultClaims(Map.of("sub", "admin")), "signature");
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims); when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
FlingToken flingToken = authenticationService.parseAuthentication("any"); FlingToken flingToken = authenticationService.parseJwtAuthentication("any");
assertThat(flingToken.isAuthenticated(), equalTo(true)); assertThat(flingToken.isAuthenticated(), equalTo(true));
// authorized for any fling // authorized for any fling
assertThat(flingToken.authorizedForFling(UUID.randomUUID()), equalTo(true)); assertThat(flingToken.authorizedForFling(UUID.randomUUID()), equalTo(true));
@ -144,12 +163,12 @@ public class AuthenticationServiceTest {
} }
@Test @Test
public void parseAuthentication_user_UserAuthorityForId() { public void parseJwtAuthentication_user_UserAuthorityForId() {
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(), Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
new DefaultClaims(Map.of("sub", "user", "id", new UUID(0, 0).toString())), "signature"); new DefaultClaims(Map.of("sub", "user", "id", new UUID(0, 0).toString())), "signature");
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims); when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
FlingToken flingToken = authenticationService.parseAuthentication("any"); FlingToken flingToken = authenticationService.parseJwtAuthentication("any");
assertThat(flingToken.isAuthenticated(), equalTo(true)); assertThat(flingToken.isAuthenticated(), equalTo(true));
// authorized for fling in token // authorized for fling in token
assertThat(flingToken.authorizedForFling(new UUID(0, 0)), equalTo(true)); assertThat(flingToken.authorizedForFling(new UUID(0, 0)), equalTo(true));
@ -161,12 +180,110 @@ public class AuthenticationServiceTest {
} }
@Test @Test
public void parseAuthentication_unknownSubject_throws() { public void parseJwtAuthentication_unknownSubject_throws() {
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(), Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
new DefaultClaims(Map.of("sub", "unknownSubject")), "signature"); new DefaultClaims(Map.of("sub", "unknownSubject")), "signature");
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims); when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
assertThrows(BadCredentialsException.class, assertThrows(BadCredentialsException.class,
() -> authenticationService.parseAuthentication("any")); () -> authenticationService.parseJwtAuthentication("any"));
}
@Test
public void deriveToken_noAuthenticationInSecurityContext_throws() {
assertThrows(IllegalStateException.class,
() -> authenticationService.deriveToken(false));
}
@Test
public void deriveToken_authenticationInSecurityContext_ok() {
FlingToken flingToken = new FlingToken(List.of(new FlingAdminAuthority()), "token");
SecurityContextHolder.setContext(new SecurityContextImpl(flingToken));
String derivedToken = authenticationService.deriveToken(null);
assertThat(derivedToken, is(not(emptyOrNullString())));
SecurityContextHolder.clearContext();
}
@Test
public void deriveToken_singleUseNotSet_singleUseIsTrue() {
FlingToken flingToken = new FlingToken(List.of(new FlingAdminAuthority()), "token");
SecurityContextHolder.setContext(new SecurityContextImpl(flingToken));
ArgumentCaptor<TokenEntity> tokenEntityCaptor = ArgumentCaptor.forClass(TokenEntity.class);
authenticationService.deriveToken(null);
verify(tokenRepository).save(tokenEntityCaptor.capture());
assertThat(tokenEntityCaptor.getValue().getSingleUse(), is(true));
}
@Test
public void deriveToken_singleUseFalse_singleUseIsFalse() {
FlingToken flingToken = new FlingToken(List.of(new FlingAdminAuthority()), "token");
SecurityContextHolder.setContext(new SecurityContextImpl(flingToken));
ArgumentCaptor<TokenEntity> tokenEntityCaptor = ArgumentCaptor.forClass(TokenEntity.class);
authenticationService.deriveToken(false);
verify(tokenRepository).save(tokenEntityCaptor.capture());
assertThat(tokenEntityCaptor.getValue().getSingleUse(), is(false));
}
@Test
public void parseDerivedToken_singleUse_deletesToken() {
String token = UUID.randomUUID().toString();
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
new DefaultClaims(Map.of("sub", "admin")), "signature");
TokenEntity tokenEntity = new TokenEntity();
tokenEntity.setId(UUID.fromString(token));
tokenEntity.setSingleUse(true);
tokenEntity.setToken("jwtToken");
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
when(tokenRepository.getOne(any(UUID.class))).thenReturn(tokenEntity);
authenticationService.parseDerivedToken(token);
verify(tokenRepository).delete(tokenEntity);
}
@Test
public void parseDerivedToken_singleUseFalse_doesNotDeleteToken() {
String token = UUID.randomUUID().toString();
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
new DefaultClaims(Map.of("sub", "admin")), "signature");
TokenEntity tokenEntity = new TokenEntity();
tokenEntity.setId(UUID.fromString(token));
tokenEntity.setSingleUse(false);
tokenEntity.setToken("jwtToken");
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
when(tokenRepository.getOne(any(UUID.class))).thenReturn(tokenEntity);
authenticationService.parseDerivedToken(token);
verify(tokenRepository, never()).delete(tokenEntity);
}
@Test
public void parseDerivedToken_returnsParentAuthentication() {
String token = UUID.randomUUID().toString();
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
new DefaultClaims(Map.of("sub", "admin")), "signature");
TokenEntity tokenEntity = new TokenEntity();
tokenEntity.setId(UUID.fromString(token));
tokenEntity.setSingleUse(true);
tokenEntity.setToken("jwtToken");
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
when(tokenRepository.getOne(any(UUID.class))).thenReturn(tokenEntity);
FlingToken flingToken = authenticationService.parseDerivedToken(token);
assertEquals(flingToken.getAuthorities().stream().findFirst().get().getAuthority(),
FlingAuthorities.FLING_ADMIN.getAuthority());
} }
} }

View file

@ -19,9 +19,9 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.junit.jupiter.SpringExtension;
import net.friedl.fling.persistence.entities.FlingEntity; import net.friedl.fling.persistence.entities.FlingEntity;
import net.friedl.fling.persistence.repositories.FlingRepository; import net.friedl.fling.persistence.repositories.FlingRepository;
import net.friedl.fling.security.authentication.FlingAdminAuthority;
import net.friedl.fling.security.authentication.FlingToken; import net.friedl.fling.security.authentication.FlingToken;
import net.friedl.fling.security.authentication.FlingUserAuthority; import net.friedl.fling.security.authentication.authorities.FlingAdminAuthority;
import net.friedl.fling.security.authentication.authorities.FlingUserAuthority;
@ExtendWith(SpringExtension.class) @ExtendWith(SpringExtension.class)
public class AuthorizationServiceTest { public class AuthorizationServiceTest {
@ -50,7 +50,7 @@ public class AuthorizationServiceTest {
@Test @Test
public void allowUpload_flingAdmin_true() { public void allowUpload_flingAdmin_true() {
FlingToken flingToken = new FlingToken(new FlingAdminAuthority(), "jwtToken"); FlingToken flingToken = new FlingToken(List.of(new FlingAdminAuthority()), "jwtToken");
assertTrue(authorizationService.allowUpload(UUID.randomUUID(), flingToken)); assertTrue(authorizationService.allowUpload(UUID.randomUUID(), flingToken));
} }
@ -59,7 +59,8 @@ public class AuthorizationServiceTest {
FlingEntity flingEntity = new FlingEntity(); FlingEntity flingEntity = new FlingEntity();
flingEntity.setAllowUpload(false); flingEntity.setAllowUpload(false);
FlingToken flingToken = new FlingToken(new FlingUserAuthority(new UUID(0, 0)), "jwtToken"); FlingToken flingToken =
new FlingToken(List.of(new FlingUserAuthority(new UUID(0, 0))), "jwtToken");
when(flingRepository.getOne(new UUID(0, 0))).thenReturn(flingEntity); when(flingRepository.getOne(new UUID(0, 0))).thenReturn(flingEntity);
@ -71,7 +72,8 @@ public class AuthorizationServiceTest {
FlingEntity flingEntity = new FlingEntity(); FlingEntity flingEntity = new FlingEntity();
flingEntity.setAllowUpload(true); flingEntity.setAllowUpload(true);
FlingToken flingToken = new FlingToken(new FlingUserAuthority(new UUID(0, 0)), "jwtToken"); FlingToken flingToken =
new FlingToken(List.of(new FlingUserAuthority(new UUID(0, 0))), "jwtToken");
when(flingRepository.getOne(new UUID(1, 1))).thenReturn(flingEntity); when(flingRepository.getOne(new UUID(1, 1))).thenReturn(flingEntity);
@ -84,7 +86,8 @@ public class AuthorizationServiceTest {
FlingEntity flingEntity = new FlingEntity(); FlingEntity flingEntity = new FlingEntity();
flingEntity.setAllowUpload(true); flingEntity.setAllowUpload(true);
FlingToken flingToken = new FlingToken(new FlingUserAuthority(new UUID(0, 0)), "jwtToken"); FlingToken flingToken =
new FlingToken(List.of(new FlingUserAuthority(new UUID(0, 0))), "jwtToken");
when(flingRepository.getOne(new UUID(0, 0))).thenReturn(flingEntity); when(flingRepository.getOne(new UUID(0, 0))).thenReturn(flingEntity);
@ -101,25 +104,28 @@ public class AuthorizationServiceTest {
@Test @Test
public void allowFlingAcess_flingAdmin_true() { public void allowFlingAcess_flingAdmin_true() {
FlingToken flingToken = new FlingToken(new FlingAdminAuthority(), "jwtToken"); FlingToken flingToken = new FlingToken(List.of(new FlingAdminAuthority()), "jwtToken");
assertTrue(authorizationService.allowFlingAccess(UUID.randomUUID(), flingToken)); assertTrue(authorizationService.allowFlingAccess(UUID.randomUUID(), flingToken));
} }
@Test @Test
public void allowFlingAcess_flingUser_notAuthorizedForId_false() { public void allowFlingAcess_flingUser_notAuthorizedForId_false() {
FlingToken flingToken = new FlingToken(new FlingUserAuthority(new UUID(0, 0)), "jwtToken"); FlingToken flingToken =
new FlingToken(List.of(new FlingUserAuthority(new UUID(0, 0))), "jwtToken");
assertFalse(authorizationService.allowFlingAccess(new UUID(1, 1), flingToken)); assertFalse(authorizationService.allowFlingAccess(new UUID(1, 1), flingToken));
} }
@Test @Test
public void allowFlingAcess_flingUser_authorizedForId_true() { public void allowFlingAcess_flingUser_authorizedForId_true() {
FlingToken flingToken = new FlingToken(new FlingUserAuthority(new UUID(0, 0)), "jwtToken"); FlingToken flingToken =
new FlingToken(List.of(new FlingUserAuthority(new UUID(0, 0))), "jwtToken");
assertTrue(authorizationService.allowFlingAccess(new UUID(0, 0), flingToken)); assertTrue(authorizationService.allowFlingAccess(new UUID(0, 0), flingToken));
} }
@Test @Test
public void allowFlingAccessByShareId_noFlingForShareId_throw() { public void allowFlingAccessByShareId_noFlingForShareId_throw() {
FlingToken flingToken = new FlingToken(new FlingUserAuthority(new UUID(0, 0)), "jwtToken"); FlingToken flingToken =
new FlingToken(List.of(new FlingUserAuthority(new UUID(0, 0))), "jwtToken");
when(flingRepository.findByShareId(any(String.class))).thenReturn(null); when(flingRepository.findByShareId(any(String.class))).thenReturn(null);
assertThrows(EntityNotFoundException.class, assertThrows(EntityNotFoundException.class,

View file

@ -1,114 +1,114 @@
import log from 'loglevel'; import log from 'loglevel';
import React, {useState, useEffect, useRef} from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { useSelector } from 'react-redux';
import {artifactClient} from '../../util/flingclient'; import { ArtifactClient, FlingClient } from '../../util/fc';
import { prettifyTimestamp } from '../../util/fn';
function FlingArtifactControl(props) { function FlingArtifactControl(props) {
let iframeContainer = useRef(null); let iframeContainer = useRef(null);
const artifactClient = new ArtifactClient();
function handleDelete(ev) { function handleDelete(ev) {
artifactClient.deleteArtifact(props.artifact.id) artifactClient.deleteArtifact(props.artifact.id)
.then(() => props.reloadArtifactsFn()); .then(() => props.reloadArtifactsFn());
} }
function handleDownload(ev) { function handleDownload(ev) {
artifactClient.downloadArtifact(props.artifact.id) artifactClient.downloadArtifactWithHttpInfo(props.artifact.id)
.then(url => { .then(response => {
// We need this iframe hack because with a regular href, while log.info(response.headers);
// the browser downloads the file fine, it also reloads the page, hence var blob = new Blob([response.data], {type: response.type});
// loosing all logs and state if(window.navigator.msSaveOrOpenBlob) {
let frame = document.createElement("iframe"); window.navigator.msSaveBlob(blob, response.name);
frame.src = url; }
iframeContainer.current.appendChild(frame); else{
}); var elem = window.document.createElement('a');
} elem.href = window.URL.createObjectURL(blob);
elem.download = response.name;
document.body.appendChild(elem);
elem.click();
document.body.removeChild(elem);
}
});
}
return( return (
<div className={`btn-group ${props.hidden ? "d-invisible": "d-visible"}`}> <div className={`btn-group ${props.hidden ? "d-invisible" : "d-visible"}`}>
<button className="btn btn-sm" onClick={handleDelete}><i className="icon icon-delete"/></button> <button className="btn btn-sm" onClick={handleDelete}><i className="icon icon-delete" /></button>
<button className="btn btn-sm"><i className="icon icon-edit"/></button> <button className="btn btn-sm"><i className="icon icon-edit" /></button>
<button className="btn btn-sm" onClick={handleDownload}><i className="icon icon-download"/></button> <button className="btn btn-sm" onClick={handleDownload}><i className="icon icon-download" /></button>
<div className="d-hide" ref={iframeContainer}/> <div className="d-hide" ref={iframeContainer} />
</div> </div>
); );
} }
function FlingArtifactRow(props) { function FlingArtifactRow(props) {
let [hovered, setHovered] = useState(false); let [hovered, setHovered] = useState(false);
function readableBytes(bytes) {
if(bytes <= 0) return "0 KB";
var i = Math.floor(Math.log(bytes) / Math.log(1024)), return (
sizes = ['Byte', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; <tr key={props.artifact.id} className="artifact-row" onMouseOver={() => setHovered(true)} onMouseOut={() => setHovered(false)}>
<td>{props.artifact.path}</td>
return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + sizes[i]; <td>{prettifyTimestamp(props.artifact.creationTime, true)}</td>
} <td></td>
<td><FlingArtifactControl artifact={props.artifact} reloadArtifactsFn={props.reloadArtifactsFn} hidden={!hovered} /></td>
function localizedUploadDate() { </tr>
let d = new Date(props.artifact.uploadTime); );
return d.toLocaleDateString();
}
return(
<tr key={props.artifact.id} className="artifact-row" onMouseOver={() => setHovered(true)} onMouseOut={() => setHovered(false)}>
<td>{props.artifact.name}</td>
<td>{localizedUploadDate()}</td>
<td>{readableBytes(props.artifact.size)}</td>
<td><FlingArtifactControl artifact={props.artifact} reloadArtifactsFn={props.reloadArtifactsFn} hidden={!hovered} /></td>
</tr>
);
} }
function FlingInfo(props) { function FlingInfo(props) {
return( return (
<div className="m-2"> <div className="m-2">
{ /* Add some infos about the fling */ } { /* Add some infos about the fling */}
</div> </div>
); );
} }
export default function FlingArtifacts(props) { export default function FlingArtifacts() {
const [artifacts, setArtifacts] = useState([]); const flingClient = new FlingClient();
useEffect(getArtifacts, [props.activeFling]); const activeFling = useSelector(state => state.flings.activeFling);
const [artifacts, setArtifacts] = useState([]);
return ( useEffect(getArtifacts, [activeFling]);
<div>
<FlingInfo />
<table className="table"> return (
<thead> <div>
<tr> <FlingInfo />
<th>Name</th>
<th>Uploaded</th>
<th>Size</th>
<th/>
</tr>
</thead>
<tbody>
{artifacts}
</tbody>
</table>
</div>
);
function getArtifacts() { <table className="table">
if (!props.activeFling) { <thead>
log.debug("No fling active. Not getting artifacts."); <tr>
return; <th>Name</th>
<th>Uploaded</th>
<th>Size</th>
<th />
</tr>
</thead>
<tbody>
{artifacts}
</tbody>
</table>
</div>
);
function getArtifacts() {
if (!activeFling) {
log.debug("No fling active. Not getting artifacts.");
return;
}
log.debug(`Fling ${activeFling} active. Getting artifacts.`);
let artifacts = [];
flingClient.getArtifacts(activeFling.id)
.then(result => {
log.debug(`Got ${result.length} artifacts`);
for (let artifact of result) {
artifacts.push(<FlingArtifactRow key={artifact.id} artifact={artifact} reloadArtifactsFn={getArtifacts} />);
} }
log.debug(`Fling ${props.activeFling} active. Getting artifacts.`); setArtifacts(artifacts);
let artifacts = []; });
}
artifactClient.getArtifacts(props.activeFling)
.then(result => {
log.debug(`Got ${result.length} artifacts`);
for(let artifact of result) {
artifacts.push(<FlingArtifactRow key={artifact.id} artifact={artifact} reloadArtifactsFn={getArtifacts} />);
}
setArtifacts(artifacts);
});
}
} }

View file

@ -1,39 +0,0 @@
import log from "loglevel";
import produce from "immer";
import { SET_FLINGS, SET_ACTIVE_FLING, ADD_FLING } from "../actionTypes";
const initialState = {
// type [fc.Artifact]
aritfacts: []
}
export default produce((draft, action) => {
switch (action.type) {
case SET_FLINGS:
draft.flings = action.payload;
break;
case ADD_FLING:
// Check storage again here, otherwise there could be a race
// condition due to async calls of SET_FLINGS and ADD_FLING
let foundFlingIdx = draft.flings.findIndex(fling =>
fling.id === action.payload.id);
if (foundFlingIdx === -1) {
log.debug(`Adding new fling with id ${action.payload.id}`)
draft.flings.push(action.payload);
} else {
log.debug(`Fling already exists. ` +
`Updating fling with id ${action.payload.id}`)
draft.flings[foundFlingIdx] = action.payload
}
break;
case SET_ACTIVE_FLING:
draft.activeFling = action.payload;
break;
default:
break;
}
return draft;
}, initialState);