diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java b/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java index ed6bc5ce12d3..05a913cc8714 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java @@ -43,6 +43,8 @@ import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; import de.tum.cit.aet.artemis.core.repository.UserRepository; import de.tum.cit.aet.artemis.core.security.Role; +import de.tum.cit.aet.artemis.core.security.allowedTools.AllowedTools; +import de.tum.cit.aet.artemis.core.security.allowedTools.ToolTokenType; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastInstructor; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastTutor; @@ -166,6 +168,7 @@ public ResponseEntity getResult(@PathVariable Long participationId, @Pat */ @GetMapping("participations/{participationId}/results/{resultId}/details") @EnforceAtLeastStudent + @AllowedTools(ToolTokenType.SCORPIO) public ResponseEntity> getResultDetails(@PathVariable Long participationId, @PathVariable Long resultId) { log.debug("REST request to get details of Result : {}", resultId); Result result = resultRepository.findByIdWithEagerFeedbacksElseThrow(resultId); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/WebConfigurer.java b/src/main/java/de/tum/cit/aet/artemis/core/config/WebConfigurer.java index e79b1de3939c..1bfc57acafbe 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/WebConfigurer.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/WebConfigurer.java @@ -28,7 +28,10 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import de.tum.cit.aet.artemis.core.security.allowedTools.ToolsInterceptor; import de.tum.cit.aet.artemis.core.security.filter.CachingHttpHeadersFilter; import tech.jhipster.config.JHipsterProperties; @@ -37,7 +40,7 @@ */ @Profile(PROFILE_CORE) @Configuration -public class WebConfigurer implements ServletContextInitializer, WebServerFactoryCustomizer { +public class WebConfigurer implements ServletContextInitializer, WebServerFactoryCustomizer, WebMvcConfigurer { private static final Logger log = LoggerFactory.getLogger(WebConfigurer.class); @@ -45,9 +48,12 @@ public class WebConfigurer implements ServletContextInitializer, WebServerFactor private final JHipsterProperties jHipsterProperties; - public WebConfigurer(Environment env, JHipsterProperties jHipsterProperties) { + private final ToolsInterceptor toolsInterceptor; + + public WebConfigurer(Environment env, JHipsterProperties jHipsterProperties, ToolsInterceptor toolsInterceptor) { this.env = env; this.jHipsterProperties = jHipsterProperties; + this.toolsInterceptor = toolsInterceptor; } @Override @@ -126,4 +132,9 @@ public CorsFilter corsFilter() { } return new CorsFilter(source); } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(toolsInterceptor); + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/AllowedTools.java b/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/AllowedTools.java new file mode 100644 index 000000000000..d183d60b2607 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/AllowedTools.java @@ -0,0 +1,13 @@ +package de.tum.cit.aet.artemis.core.security.allowedTools; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface AllowedTools { + + ToolTokenType[] value(); +} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/ToolTokenType.java b/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/ToolTokenType.java new file mode 100644 index 000000000000..4159656d94bf --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/ToolTokenType.java @@ -0,0 +1,5 @@ +package de.tum.cit.aet.artemis.core.security.allowedTools; + +public enum ToolTokenType { + SCORPIO +} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/ToolsInterceptor.java b/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/ToolsInterceptor.java new file mode 100644 index 000000000000..d9c3288e4955 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/ToolsInterceptor.java @@ -0,0 +1,72 @@ +package de.tum.cit.aet.artemis.core.security.allowedTools; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + +import java.lang.reflect.Method; +import java.util.Arrays; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import de.tum.cit.aet.artemis.core.security.jwt.JWTFilter; +import de.tum.cit.aet.artemis.core.security.jwt.TokenProvider; + +@Profile(PROFILE_CORE) +@Component +public class ToolsInterceptor implements HandlerInterceptor { + + private final TokenProvider tokenProvider; + + public ToolsInterceptor(TokenProvider tokenProvider) { + this.tokenProvider = tokenProvider; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + String jwtToken; + try { + jwtToken = JWTFilter.extractValidJwt(request, tokenProvider); + } + catch (IllegalArgumentException e) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return false; + } + + if (handler instanceof HandlerMethod && jwtToken != null) { + HandlerMethod handlerMethod = (HandlerMethod) handler; + Method method = handlerMethod.getMethod(); + + // Check if the method or its class has the @AllowedTools annotation + AllowedTools allowedToolsAnnotation = method.getAnnotation(AllowedTools.class); + if (allowedToolsAnnotation == null) { + allowedToolsAnnotation = method.getDeclaringClass().getAnnotation(AllowedTools.class); + } + + // Extract the "tools" claim from the JWT token + String toolsClaim = tokenProvider.getClaim(jwtToken, "tools", String.class); + + // If no @AllowedTools annotation is present and the token is a tool token, reject the request + if (allowedToolsAnnotation == null && toolsClaim != null) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access denied due to 'tools' claim."); + return false; + } + + // If @AllowedTools is present, check if the toolsClaim is among the allowed values + if (allowedToolsAnnotation != null && toolsClaim != null) { + ToolTokenType[] allowedTools = allowedToolsAnnotation.value(); + // no match between allowed tools and tools claim + var toolsClaimList = toolsClaim.split(","); + if (Arrays.stream(allowedTools).noneMatch(tool -> Arrays.asList(toolsClaimList).contains(tool.toString()))) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access denied due to 'tools' claim."); + return false; + } + } + } + return true; + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTCookieService.java b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTCookieService.java index 73320dee2813..88d8d6405d51 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTCookieService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTCookieService.java @@ -14,6 +14,8 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; +import de.tum.cit.aet.artemis.core.security.allowedTools.ToolTokenType; + @Profile(PROFILE_CORE) @Service public class JWTCookieService { @@ -36,9 +38,30 @@ public JWTCookieService(TokenProvider tokenProvider, Environment environment) { * @return the login ResponseCookie containing the JWT */ public ResponseCookie buildLoginCookie(boolean rememberMe) { - String jwt = tokenProvider.createToken(SecurityContextHolder.getContext().getAuthentication(), rememberMe); - Duration duration = Duration.of(tokenProvider.getTokenValidity(rememberMe), ChronoUnit.MILLIS); - return buildJWTCookie(jwt, duration); + return buildLoginCookie(rememberMe, null); + } + + /** + * Builds the cookie containing the jwt for a login + * + * @param rememberMe boolean used to determine the duration of the jwt. + * @param tool the tool claim in the jwt + * @return the login ResponseCookie containing the JWT + */ + public ResponseCookie buildLoginCookie(boolean rememberMe, ToolTokenType tool) { + return buildLoginCookie(tokenProvider.getTokenValidity(rememberMe), tool); + } + + /** + * Builds a cookie with the tool claim in the jwt + * + * @param duration the duration of the cookie in milli seconds and the jwt + * @param tool the tool claim in the jwt + * @return the login ResponseCookie containing the JWT + */ + public ResponseCookie buildLoginCookie(long duration, ToolTokenType tool) { + String jwt = tokenProvider.createToken(SecurityContextHolder.getContext().getAuthentication(), duration, tool); + return buildJWTCookie(jwt, Duration.of(duration, ChronoUnit.MILLIS)); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java index 044d897d12c7..9463e850322c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.stream.Collectors; +import jakarta.annotation.Nullable; import jakarta.annotation.PostConstruct; import javax.crypto.SecretKey; @@ -24,6 +25,7 @@ import org.springframework.util.StringUtils; import de.tum.cit.aet.artemis.core.management.SecurityMetersService; +import de.tum.cit.aet.artemis.core.security.allowedTools.ToolTokenType; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; @@ -95,11 +97,27 @@ public long getTokenValidity(boolean rememberMe) { * @return JWT Token */ public String createToken(Authentication authentication, boolean rememberMe) { + return createToken(authentication, getTokenValidity(rememberMe), null); + } + + /** + * Create JWT Token a fully populated Authentication object. + * + * @param authentication Authentication Object + * @param duration the Token lifetime in milli seconds + * @param tool tool this token is used for. If null, it's a general access token + * @return JWT Token + */ + public String createToken(Authentication authentication, long duration, @Nullable ToolTokenType tool) { String authorities = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(",")); - long now = (new Date()).getTime(); - Date validity = new Date(now + getTokenValidity(rememberMe)); - return Jwts.builder().subject(authentication.getName()).claim(AUTHORITIES_KEY, authorities).signWith(key, Jwts.SIG.HS512).expiration(validity).compact(); + var validity = System.currentTimeMillis() + duration; + var jwtBuilder = Jwts.builder().subject(authentication.getName()).claim(AUTHORITIES_KEY, authorities); + if (tool != null) { + jwtBuilder.claim("tools", tool); + } + + return jwtBuilder.signWith(key, Jwts.SIG.HS512).expiration(new Date(validity)).compact(); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java index 16c94629047c..fae3822f08cf 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java @@ -34,6 +34,8 @@ import de.tum.cit.aet.artemis.core.exception.EmailAlreadyUsedException; import de.tum.cit.aet.artemis.core.exception.PasswordViolatesRequirementsException; import de.tum.cit.aet.artemis.core.repository.UserRepository; +import de.tum.cit.aet.artemis.core.security.allowedTools.AllowedTools; +import de.tum.cit.aet.artemis.core.security.allowedTools.ToolTokenType; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; import de.tum.cit.aet.artemis.core.service.AccountService; import de.tum.cit.aet.artemis.core.service.FilePathService; @@ -172,6 +174,7 @@ public ResponseEntity deleteVcsAccessToken() { */ @GetMapping("account/participation-vcs-access-token") @EnforceAtLeastStudent + @AllowedTools(ToolTokenType.SCORPIO) public ResponseEntity getVcsAccessToken(@RequestParam("participationId") Long participationId) { User user = userRepository.getUser(); @@ -188,6 +191,7 @@ public ResponseEntity getVcsAccessToken(@RequestParam("participationId") */ @PutMapping("account/participation-vcs-access-token") @EnforceAtLeastStudent + @AllowedTools(ToolTokenType.SCORPIO) public ResponseEntity createVcsAccessToken(@RequestParam("participationId") Long participationId) { User user = userRepository.getUser(); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java index 2545a6ffb192..e0e54bc68ef9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java @@ -93,6 +93,8 @@ import de.tum.cit.aet.artemis.core.repository.CourseRepository; import de.tum.cit.aet.artemis.core.repository.UserRepository; import de.tum.cit.aet.artemis.core.security.Role; +import de.tum.cit.aet.artemis.core.security.allowedTools.AllowedTools; +import de.tum.cit.aet.artemis.core.security.allowedTools.ToolTokenType; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastEditor; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastInstructor; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; @@ -584,6 +586,7 @@ public ResponseEntity> getCoursesForEnrollment() { // TODO: we should rename this into courses/{courseId}/details @GetMapping("courses/{courseId}/for-dashboard") @EnforceAtLeastStudent + @AllowedTools(ToolTokenType.SCORPIO) public ResponseEntity getCourseForDashboard(@PathVariable long courseId) { long timeNanoStart = System.nanoTime(); log.debug("REST request to get one course {} with exams, lectures, exercises, participations, submissions and results, etc.", courseId); @@ -648,6 +651,7 @@ public record CourseDropdownDTO(Long id, String title, String courseIcon) { */ @GetMapping("courses/for-dashboard") @EnforceAtLeastStudent + @AllowedTools(ToolTokenType.SCORPIO) public ResponseEntity getCoursesForDashboard() { long timeNanoStart = System.nanoTime(); User user = userRepository.getUserWithGroupsAndAuthorities(); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/TokenResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/TokenResource.java new file mode 100644 index 000000000000..54b3ad20a07c --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/TokenResource.java @@ -0,0 +1,71 @@ +package de.tum.cit.aet.artemis.core.web; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + +import java.time.Duration; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import de.tum.cit.aet.artemis.core.security.allowedTools.ToolTokenType; +import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; +import de.tum.cit.aet.artemis.core.security.jwt.JWTCookieService; +import de.tum.cit.aet.artemis.core.security.jwt.JWTFilter; +import de.tum.cit.aet.artemis.core.security.jwt.TokenProvider; + +@Profile(PROFILE_CORE) +@RestController +@RequestMapping("api/") +public class TokenResource { + + private final JWTCookieService jwtCookieService; + + private final TokenProvider tokenProvider; + + public TokenResource(JWTCookieService jwtCookieService, TokenProvider tokenProvider) { + this.jwtCookieService = jwtCookieService; + this.tokenProvider = tokenProvider; + } + + /** + * Sends a tool token back as cookie or bearer token + * + * @param tool the tool for which the token is requested + * @param asCookie if true the token is sent back as a cookie + * @param request HTTP request + * @param response HTTP response + * @return the ResponseEntity with status 200 (ok), 401 (unauthorized) and depending on the asCookie flag a bearer token in the body + */ + @PostMapping("tool-token") + @EnforceAtLeastStudent + public ResponseEntity convertCookieToToolToken(@RequestParam(name = "tool", required = true) ToolTokenType tool, + @RequestParam(name = "as-cookie", defaultValue = "false") boolean asCookie, HttpServletRequest request, HttpServletResponse response) { + // remaining time in milliseconds + var jwtToken = JWTFilter.extractValidJwt(request, tokenProvider); + if (jwtToken == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + + // get validity of the token + long tokenRemainingTime = tokenProvider.getExpirationDate(jwtToken).getTime() - System.currentTimeMillis(); + + // 1 day validity + long maxDuration = Duration.ofDays(1).toMillis(); + ResponseCookie responseCookie = jwtCookieService.buildLoginCookie(Math.min(tokenRemainingTime, maxDuration), tool); + + if (asCookie) { + response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString()); + } + return ResponseEntity.ok(responseCookie.getValue()); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java index 90020572f571..ce61f2998317 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java @@ -28,12 +28,15 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import de.tum.cit.aet.artemis.core.dto.vm.LoginVM; import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException; import de.tum.cit.aet.artemis.core.security.SecurityUtils; import de.tum.cit.aet.artemis.core.security.UserNotActivatedException; +import de.tum.cit.aet.artemis.core.security.allowedTools.AllowedTools; +import de.tum.cit.aet.artemis.core.security.allowedTools.ToolTokenType; import de.tum.cit.aet.artemis.core.security.annotations.EnforceNothing; import de.tum.cit.aet.artemis.core.security.jwt.JWTCookieService; import de.tum.cit.aet.artemis.core.service.connectors.SAML2Service; @@ -70,7 +73,9 @@ public PublicUserJwtResource(JWTCookieService jwtCookieService, AuthenticationMa */ @PostMapping("authenticate") @EnforceNothing - public ResponseEntity> authorize(@Valid @RequestBody LoginVM loginVM, @RequestHeader("User-Agent") String userAgent, HttpServletResponse response) { + @AllowedTools(ToolTokenType.SCORPIO) + public ResponseEntity> authorize(@Valid @RequestBody LoginVM loginVM, @RequestHeader("User-Agent") String userAgent, + @RequestParam(name = "tool", required = false) ToolTokenType tool, HttpServletResponse response) { var username = loginVM.getUsername(); var password = loginVM.getPassword(); @@ -84,7 +89,7 @@ public ResponseEntity> authorize(@Valid @RequestBody LoginVM SecurityContextHolder.getContext().setAuthentication(authentication); boolean rememberMe = loginVM.isRememberMe() != null && loginVM.isRememberMe(); - ResponseCookie responseCookie = jwtCookieService.buildLoginCookie(rememberMe); + ResponseCookie responseCookie = jwtCookieService.buildLoginCookie(rememberMe, tool); response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString()); return ResponseEntity.ok(Map.of("access_token", responseCookie.getValue())); @@ -127,7 +132,7 @@ public ResponseEntity authorizeSAML2(@RequestBody final String body, HttpS } final boolean rememberMe = Boolean.parseBoolean(body); - ResponseCookie responseCookie = jwtCookieService.buildLoginCookie(rememberMe); + ResponseCookie responseCookie = jwtCookieService.buildLoginCookie(rememberMe, null); response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString()); return ResponseEntity.ok().build(); @@ -143,6 +148,7 @@ public ResponseEntity authorizeSAML2(@RequestBody final String body, HttpS */ @PostMapping("logout") @EnforceNothing + @AllowedTools(ToolTokenType.SCORPIO) public ResponseEntity logout(HttpServletRequest request, HttpServletResponse response) throws ServletException { request.logout(); // Logout needs to build the same cookie (secure, httpOnly and sameSite='Lax') or browsers will ignore the header and not unset the cookie diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java b/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java index 3a8c0b6368b9..14ab3e8da46d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java @@ -55,6 +55,8 @@ import de.tum.cit.aet.artemis.core.repository.CourseRepository; import de.tum.cit.aet.artemis.core.repository.UserRepository; import de.tum.cit.aet.artemis.core.security.Role; +import de.tum.cit.aet.artemis.core.security.allowedTools.AllowedTools; +import de.tum.cit.aet.artemis.core.security.allowedTools.ToolTokenType; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastInstructor; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastTutor; @@ -218,6 +220,7 @@ public ParticipationResource(ParticipationService participationService, Programm */ @PostMapping("exercises/{exerciseId}/participations") @EnforceAtLeastStudentInExercise + @AllowedTools(ToolTokenType.SCORPIO) public ResponseEntity startParticipation(@PathVariable Long exerciseId) throws URISyntaxException { log.debug("REST request to start Exercise : {}", exerciseId); Exercise exercise = exerciseRepository.findByIdElseThrow(exerciseId); @@ -777,6 +780,7 @@ public ResponseEntity getParticipationBuildArtifact(@PathVariable Long p */ @GetMapping("exercises/{exerciseId}/participation") @EnforceAtLeastStudent + @AllowedTools(ToolTokenType.SCORPIO) public ResponseEntity getParticipationForCurrentUser(@PathVariable Long exerciseId, Principal principal) { log.debug("REST request to get Participation for Exercise : {}", exerciseId); Exercise exercise = exerciseRepository.findByIdElseThrow(exerciseId); diff --git a/src/test/java/de/tum/cit/aet/artemis/core/authentication/AuthenticationIntegrationTestHelper.java b/src/test/java/de/tum/cit/aet/artemis/core/authentication/AuthenticationIntegrationTestHelper.java index bc10fb0d9dce..c757a43b8fa8 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/authentication/AuthenticationIntegrationTestHelper.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/authentication/AuthenticationIntegrationTestHelper.java @@ -4,6 +4,9 @@ import jakarta.servlet.http.Cookie; +import de.tum.cit.aet.artemis.core.security.allowedTools.ToolTokenType; +import de.tum.cit.aet.artemis.core.security.jwt.TokenProvider; + public class AuthenticationIntegrationTestHelper { public static void authenticationCookieAssertions(Cookie cookie, boolean logoutCookie) { @@ -21,4 +24,19 @@ public static void authenticationCookieAssertions(Cookie cookie, boolean logoutC assertThat(cookie.getValue()).isNotEmpty(); } } + + public static void toolTokenAssertions(TokenProvider tokenProvider, String token, long initialLifetime, ToolTokenType... tools) { + assertThat(token).isNotNull(); + + String[] toolClaims = tokenProvider.getClaim(token, "tools", String.class).split(","); + assertThat(toolClaims).isNotEmpty(); + for (ToolTokenType tool : tools) { + assertThat(toolClaims).contains(tool.toString()); + } + + var lifetime = tokenProvider.getExpirationDate(token).getTime() - System.currentTimeMillis(); + // assert that the token has a lifetime of less than a day + assertThat(lifetime).isLessThan(24 * 60 * 60 * 1000); + assertThat(lifetime).isLessThan(initialLifetime); + } } diff --git a/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java index 99e424d35022..8cab79b93311 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java @@ -6,6 +6,8 @@ import static de.tum.cit.aet.artemis.core.domain.Authority.USER_AUTHORITY; import static de.tum.cit.aet.artemis.core.user.util.UserFactory.USER_PASSWORD; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.time.ZonedDateTime; import java.util.HashSet; @@ -14,6 +16,7 @@ import java.util.Optional; import java.util.Set; +import jakarta.servlet.http.Cookie; import jakarta.validation.constraints.NotNull; import org.junit.jupiter.api.AfterEach; @@ -23,9 +26,11 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.test.context.support.WithAnonymousUser; import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.util.LinkedMultiValueMap; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -39,6 +44,8 @@ import de.tum.cit.aet.artemis.core.repository.AuthorityRepository; import de.tum.cit.aet.artemis.core.security.Role; import de.tum.cit.aet.artemis.core.security.SecurityUtils; +import de.tum.cit.aet.artemis.core.security.allowedTools.ToolTokenType; +import de.tum.cit.aet.artemis.core.security.jwt.JWTCookieService; import de.tum.cit.aet.artemis.core.security.jwt.TokenProvider; import de.tum.cit.aet.artemis.core.service.user.PasswordService; import de.tum.cit.aet.artemis.core.util.CourseFactory; @@ -58,6 +65,9 @@ class InternalAuthenticationIntegrationTest extends AbstractSpringIntegrationJen @Autowired private TokenProvider tokenProvider; + @Autowired + private JWTCookieService jwtCookieService; + @Autowired private ProgrammingExerciseTestRepository programmingExerciseRepository; @@ -237,6 +247,25 @@ void testJWTAuthentication() throws Exception { assertThat(tokenProvider.validateTokenForAuthority(responseBody.get("access_token").toString())).isTrue(); } + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testScorpioTokenGeneration() throws Exception { + ResponseCookie responseCookie = jwtCookieService.buildLoginCookie(true); + + Cookie cookie = new Cookie(responseCookie.getName(), responseCookie.getValue()); + cookie.setMaxAge((int) responseCookie.getMaxAge().toMillis()); + + var initialLifetime = tokenProvider.getExpirationDate(cookie.getValue()).getTime() - System.currentTimeMillis(); + + LinkedMultiValueMap params = new LinkedMultiValueMap<>(); + params.add("tool", ToolTokenType.SCORPIO.toString()); + + var responseBody = request.performMvcRequest(post("/api/tool-token").cookie(cookie).params(params)).andExpect(status().isOk()).andReturn().getResponse() + .getContentAsString(); + + AuthenticationIntegrationTestHelper.toolTokenAssertions(tokenProvider, responseBody, initialLifetime, ToolTokenType.SCORPIO); + } + @Test @WithAnonymousUser void testJWTAuthenticationLogoutAnonymous() throws Exception { diff --git a/src/test/java/de/tum/cit/aet/artemis/core/security/allowedTools/AllowedToolsResource.java b/src/test/java/de/tum/cit/aet/artemis/core/security/allowedTools/AllowedToolsResource.java new file mode 100644 index 000000000000..2f8e63ed11a6 --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/core/security/allowedTools/AllowedToolsResource.java @@ -0,0 +1,30 @@ +package de.tum.cit.aet.artemis.core.security.allowedTools; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + +import org.springframework.context.annotation.Profile; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; + +@Profile(PROFILE_CORE) +@RestController +@RequestMapping("api/test/") +public class AllowedToolsResource { + + @GetMapping("testAllowedToolTokenScorpio") + @EnforceAtLeastStudent + @AllowedTools(ToolTokenType.SCORPIO) + public ResponseEntity testAllowedToolTokenScorpio() { + return ResponseEntity.ok().build(); + } + + @GetMapping("testNoAllowedToolToken") + @EnforceAtLeastStudent + public ResponseEntity testNoAllowedToolToken() { + return ResponseEntity.ok().build(); + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/core/security/allowedTools/AllowedToolsTest.java b/src/test/java/de/tum/cit/aet/artemis/core/security/allowedTools/AllowedToolsTest.java new file mode 100644 index 000000000000..c9a988cf336a --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/core/security/allowedTools/AllowedToolsTest.java @@ -0,0 +1,69 @@ +package de.tum.cit.aet.artemis.core.security.allowedTools; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Collections; + +import jakarta.servlet.http.Cookie; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.test.context.support.WithMockUser; + +import de.tum.cit.aet.artemis.core.security.Role; +import de.tum.cit.aet.artemis.core.security.jwt.JWTFilter; +import de.tum.cit.aet.artemis.core.security.jwt.TokenProvider; +import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; + +class AllowedToolsTest extends AbstractSpringIntegrationIndependentTest { + + private static final String TEST_PREFIX = "allowedtools"; + + @Autowired + private TokenProvider tokenProvider; + + @BeforeEach + void initTestCase() { + userUtilService.addUsers(TEST_PREFIX, 1, 0, 0, 0); + } + + @Test + void testAllowedToolsRouteWithToolToken() throws Exception { + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken("test-user", "test-password", + Collections.singletonList(new SimpleGrantedAuthority(Role.STUDENT.getAuthority()))); + + String jwt = tokenProvider.createToken(authentication, 24 * 60 * 60 * 1000, ToolTokenType.SCORPIO); + Cookie cookie = new Cookie(JWTFilter.JWT_COOKIE_NAME, jwt); + + request.performMvcRequest(get("/api/test/testAllowedToolTokenScorpio").cookie(cookie)).andExpect(status().isOk()); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testAllowedToolsRouteWithGeneralToken() throws Exception { + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken("test-user", "test-password", + Collections.singletonList(new SimpleGrantedAuthority(Role.STUDENT.getAuthority()))); + + String jwt = tokenProvider.createToken(authentication, 24 * 60 * 60 * 1000, null); + Cookie cookie = new Cookie(JWTFilter.JWT_COOKIE_NAME, jwt); + + request.performMvcRequest(get("/api/test/testAllowedToolTokenScorpio").cookie(cookie)).andExpect(status().isOk()); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testAllowedToolsRouteWithDifferentToolToken() throws Exception { + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken("test-user", "test-password", + Collections.singletonList(new SimpleGrantedAuthority(Role.STUDENT.getAuthority()))); + + String jwt = tokenProvider.createToken(authentication, 24 * 60 * 60 * 1000, ToolTokenType.SCORPIO); + Cookie cookie = new Cookie(JWTFilter.JWT_COOKIE_NAME, jwt); + + request.performMvcRequest(get("/api/test/testNoAllowedToolToken").cookie(cookie)).andExpect(status().isForbidden()); + } + +}