Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Development: Theia integration test #9759

Open
wants to merge 69 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 67 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
a0497f3
Move Theia Button to Code Button
iyannsch Sep 23, 2024
2ab444e
Add functionality to pull build config on demand from server
iyannsch Sep 24, 2024
389f5fd
Merge branch 'develop' into feature/programming-exercises/provide-the…
iyannsch Sep 28, 2024
7ce3a49
Add Uri, Token, and landing-page URL to Theia Button
iyannsch Sep 28, 2024
5dec295
enable bearer authentication
janthoXO Oct 1, 2024
fc69fca
Add ProgrammingExercise by ProjectKey endpoint
janthoXO Oct 1, 2024
b447b28
Add re-key endpoint
janthoXO Oct 1, 2024
a3d0fbb
Implement rabbit feedback
janthoXO Oct 2, 2024
83ef1a5
check for access to exercise
janthoXO Oct 2, 2024
4160302
Add Form test and re-key endpoint
iyannsch Oct 3, 2024
eae2c89
Clean up initTheia method
iyannsch Oct 3, 2024
0f18104
Fix formatting
iyannsch Oct 3, 2024
486b2c5
Fix implicit type expressions
iyannsch Oct 4, 2024
069f2eb
Merge branch 'feature/bearer-support' of github.com:ls1intum/Artemis …
janthoXO Oct 4, 2024
39e4bad
make token theia specific
janthoXO Oct 4, 2024
d9bf634
Fix rekey endpoint to be reachable
iyannsch Oct 4, 2024
a643b42
Merge upstream key endpoint
iyannsch Oct 4, 2024
ddd8802
fix conversion from day to millis
janthoXO Oct 4, 2024
34a84d7
Merge branch 'feature/re-key' into feature/programming-exercises/prov…
iyannsch Oct 4, 2024
4f1a99b
Fix endpoint theia-token connection
iyannsch Oct 4, 2024
07afea2
only respond as cookie if asked for
janthoXO Oct 4, 2024
b59f8ad
fix class cast error for websockets
janthoXO Oct 5, 2024
896fd4c
let login return json with token
janthoXO Oct 7, 2024
4aa5171
Merge branch 'develop' into feature/programming-exercises/provide-the…
iyannsch Oct 8, 2024
7abb02c
Merge branch 'feature/re-key' into feature/programming-exercises/prov…
iyannsch Oct 8, 2024
b7ebea0
Merge upstream bearer branch
iyannsch Oct 8, 2024
685a483
Get Token unconditionally
iyannsch Oct 8, 2024
490692e
add @param to docs
janthoXO Oct 25, 2024
056fcc9
merge develop
janthoXO Oct 26, 2024
e8be59c
Merge branch 'develop' into feature/programming-exercises/provide-the…
iyannsch Oct 29, 2024
575379b
Fix failing correct data submission test
iyannsch Oct 29, 2024
2d2a5b7
Remove minus from appDef to align to TS configuration
iyannsch Oct 29, 2024
2e247db
add server test for bearer token
janthoXO Nov 6, 2024
6207ce8
Fix configuration values to align with new system
iyannsch Nov 7, 2024
e8bde67
Merge branch 'develop' into feature/bearer-support
iyannsch Nov 7, 2024
c36c6ec
Merge branch 'develop' into feature/re-key
iyannsch Nov 7, 2024
a6cc06e
Merge branch 'develop' into feature/programming-exercises/provide-the…
iyannsch Nov 7, 2024
f82d6fb
Merge branch 'feature/re-key' into feature/programming-exercises/prov…
iyannsch Nov 7, 2024
8743e73
Merge branch 'feature/bearer-support' of github.com:ls1intum/Artemis …
iyannsch Nov 7, 2024
beb3d48
Merge branch 'feature/re-key' of github.com:ls1intum/Artemis into fea…
iyannsch Nov 7, 2024
625216e
Rename endpoint to theia-token
iyannsch Nov 7, 2024
02e63b7
change theia token to general tool token endpoint
janthoXO Nov 8, 2024
44ca43c
Apply suggestions from code review
janthoXO Nov 8, 2024
6cab497
add tool token annotation
janthoXO Nov 9, 2024
e2bb2d0
Merge remote-tracking branch 'origin/feature/re-key' into feature/re-key
janthoXO Nov 9, 2024
6e61b26
move interceptor registration
janthoXO Nov 9, 2024
149a53a
add tool token option to login
janthoXO Nov 11, 2024
b0ed73d
change filter chain to only accept one auth method
janthoXO Nov 11, 2024
6b19a87
Merge remote-tracking branch 'origin/feature/bearer-support' into fea…
janthoXO Nov 11, 2024
8df1a6b
write filter a bit nicer
janthoXO Nov 11, 2024
7fe83a9
Merge branch 'feature/bearer-support' of github.com:ls1intum/Artemis …
janthoXO Nov 12, 2024
9f61018
annotate endpoints required by Scorpio
janthoXO Nov 12, 2024
7d3b311
Merge branch 'feature/re-key' of github.com:ls1intum/Artemis into fea…
janthoXO Nov 12, 2024
932ae63
only allow scorpio tokens
janthoXO Nov 12, 2024
5908f73
change bad request behaviour
janthoXO Nov 12, 2024
5c7ef80
Merge branch 'feature/re-key' of github.com:ls1intum/Artemis into fea…
janthoXO Nov 12, 2024
2946389
Merge branch 'feature/re-key' of github.com:ls1intum/Artemis into fea…
iyannsch Nov 12, 2024
c7c5322
Rename endpoint to tool-token
iyannsch Nov 12, 2024
9b0bcc3
Add ArtemisURL to theia query params
iyannsch Nov 12, 2024
ae35975
Merge branch 'feature/programming-exercises/provide-theia-clone-infor…
iyannsch Nov 12, 2024
0bc0474
Merge branch 'develop' of github.com:ls1intum/Artemis into chore/thei…
iyannsch Nov 13, 2024
d3817c8
Add gitUser and gitMail to Theia LP
iyannsch Nov 13, 2024
3c596ac
Merge branch 'feature/programming-exercises/provide-theia-clone-infor…
iyannsch Nov 13, 2024
590fde4
Fix ArtemisUrl
iyannsch Nov 13, 2024
ac87b07
Remove debug for artemisUrl
iyannsch Nov 13, 2024
10648b0
Merge branch 'develop' of github.com:ls1intum/Artemis into feature/pr…
iyannsch Nov 13, 2024
dbe23d9
Merge branch 'feature/programming-exercises/provide-theia-clone-infor…
iyannsch Nov 13, 2024
2941799
Merge branch 'develop' of github.com:ls1intum/Artemis into feature/pr…
iyannsch Nov 18, 2024
c76998c
Merge branch 'feature/programming-exercises/provide-theia-clone-infor…
iyannsch Nov 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,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;

Expand All @@ -36,17 +39,20 @@
*/
@Profile(PROFILE_CORE)
@Configuration
public class WebConfigurer implements ServletContextInitializer, WebServerFactoryCustomizer<WebServerFactory> {
public class WebConfigurer implements ServletContextInitializer, WebServerFactoryCustomizer<WebServerFactory>, WebMvcConfigurer {

private static final Logger log = LoggerFactory.getLogger(WebConfigurer.class);

private final Environment env;

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
Expand Down Expand Up @@ -125,4 +131,9 @@ public CorsFilter corsFilter() {
}
return new CorsFilter(source);
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(toolsInterceptor);
}
janthoXO marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import java.util.regex.Pattern;

import jakarta.annotation.Nullable;
import jakarta.servlet.http.Cookie;
import jakarta.validation.constraints.NotNull;

import org.slf4j.Logger;
Expand All @@ -28,6 +27,7 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
Expand All @@ -52,7 +52,6 @@
import org.springframework.web.socket.server.HandshakeInterceptor;
import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
import org.springframework.web.socket.sockjs.transport.handler.WebSocketTransportHandler;
import org.springframework.web.util.WebUtils;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Iterators;
Expand Down Expand Up @@ -201,9 +200,14 @@ public HandshakeInterceptor httpSessionHandshakeInterceptor() {
public boolean beforeHandshake(@NotNull ServerHttpRequest request, @NotNull ServerHttpResponse response, @NotNull WebSocketHandler wsHandler,
@NotNull Map<String, Object> attributes) {
if (request instanceof ServletServerHttpRequest servletRequest) {
attributes.put(IP_ADDRESS, servletRequest.getRemoteAddress());
Cookie jwtCookie = WebUtils.getCookie(servletRequest.getServletRequest(), JWTFilter.JWT_COOKIE_NAME);
return JWTFilter.isJwtCookieValid(tokenProvider, jwtCookie);
try {
attributes.put(IP_ADDRESS, servletRequest.getRemoteAddress());
return JWTFilter.extractValidJwt(servletRequest.getServletRequest(), tokenProvider) != null;
}
catch (IllegalArgumentException e) {
response.setStatusCode(HttpStatusCode.valueOf(400));
return false;
}
}
return false;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
janthoXO marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package de.tum.cit.aet.artemis.core.security.allowedTools;

public enum ToolTokenType {
SCORPIO
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package de.tum.cit.aet.artemis.core.security.allowedTools;

import java.lang.reflect.Method;
import java.util.Arrays;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

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;

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

// 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()))) {
janthoXO marked this conversation as resolved.
Show resolved Hide resolved
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access denied due to 'tools' claim.");
return false;
}
}
}
return true;
}
janthoXO marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

import java.io.IOException;

import jakarta.annotation.Nullable;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
Expand All @@ -31,26 +33,88 @@ public JWTFilter(TokenProvider tokenProvider) {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
Cookie jwtCookie = WebUtils.getCookie(httpServletRequest, JWT_COOKIE_NAME);
if (isJwtCookieValid(this.tokenProvider, jwtCookie)) {
Authentication authentication = this.tokenProvider.getAuthentication(jwtCookie.getValue());
HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
janthoXO marked this conversation as resolved.
Show resolved Hide resolved
String jwtToken;
try {
jwtToken = extractValidJwt(httpServletRequest, this.tokenProvider);
}
catch (IllegalArgumentException e) {
httpServletResponse.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}

if (jwtToken != null) {
Authentication authentication = this.tokenProvider.getAuthentication(jwtToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
}

filterChain.doFilter(servletRequest, servletResponse);
}

/**
* Checks if the cookie containing the jwt is valid
* Extracts the valid jwt found in the cookie or the Authorization header
*
* @param tokenProvider the artemis token provider used to generate and validate jwt's
* @param jwtCookie the cookie containing the jwt
* @return true if the jwt is valid, false if missing or invalid
* @param httpServletRequest the http request
* @param tokenProvider the Artemis token provider used to generate and validate jwt's
* @return the valid jwt or null if not found or invalid
*/
public static boolean isJwtCookieValid(TokenProvider tokenProvider, Cookie jwtCookie) {
public static @Nullable String extractValidJwt(HttpServletRequest httpServletRequest, TokenProvider tokenProvider) {
var cookie = WebUtils.getCookie(httpServletRequest, JWT_COOKIE_NAME);
var authHeader = httpServletRequest.getHeader("Authorization");

if (cookie == null && authHeader == null) {
return null;
}

if (cookie != null && authHeader != null) {
// Single Method Enforcement: Only one method of authentication is allowed
throw new IllegalArgumentException("Only one method of authentication is allowed");
}

String jwtToken = cookie != null ? getJwtFromCookie(cookie) : getJwtFromBearer(authHeader);

if (!isJwtValid(tokenProvider, jwtToken)) {
return null;
}

return jwtToken;
}
janthoXO marked this conversation as resolved.
Show resolved Hide resolved

/**
* Extracts the jwt from the cookie
*
* @param jwtCookie the cookie with Key "jwt"
* @return the jwt or null if not found
*/
private static @Nullable String getJwtFromCookie(@Nullable Cookie jwtCookie) {
if (jwtCookie == null) {
return false;
return null;
}
return jwtCookie.getValue();
}

/**
* Extracts the jwt from the Authorization header
*
* @param jwtBearer the content of the Authorization header
* @return the jwt or null if not found
*/
private static @Nullable String getJwtFromBearer(@Nullable String jwtBearer) {
if (!StringUtils.hasText(jwtBearer) || !jwtBearer.startsWith("Bearer ")) {
return null;
}
String jwt = jwtCookie.getValue();
return StringUtils.hasText(jwt) && tokenProvider.validateTokenForAuthority(jwt);

return jwtBearer.substring(7).trim();
}

/**
* Checks if the jwt is valid
*
* @param tokenProvider the Artemis token provider used to generate and validate jwt's
* @param jwtToken the jwt
* @return true if the jwt is valid, false if missing or invalid
*/
private static boolean isJwtValid(TokenProvider tokenProvider, @Nullable String jwtToken) {
return StringUtils.hasText(jwtToken) && tokenProvider.validateTokenForAuthority(jwtToken);
}
janthoXO marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -95,11 +97,28 @@ 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 <code>Authentication</code> 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();
Date validity = new Date(now + 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(validity).compact();
}

/**
Expand Down Expand Up @@ -170,6 +189,11 @@ private Claims parseClaims(String authToken) {
return Jwts.parser().verifyWith(key).build().parseSignedClaims(authToken).getPayload();
}

public String getClaim(String token, String claimName) {
Claims claims = parseClaims(token);
return claims.get(claimName, String.class);
}
janthoXO marked this conversation as resolved.
Show resolved Hide resolved

public Date getExpirationDate(String authToken) {
return parseClaims(authToken).getExpiration();
}
Expand Down
Loading
Loading