Skip to content

Commit

Permalink
Merge branch 'develop' into chore/posting-header-footer-client-migration
Browse files Browse the repository at this point in the history
  • Loading branch information
sachmii authored Dec 11, 2024
2 parents e6dab24 + da468e2 commit 4134f1f
Show file tree
Hide file tree
Showing 119 changed files with 2,578 additions and 428 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ spotless {
}
}
importOrderFile "artemis-spotless.importorder"
eclipse("4.28").configFile "artemis-spotless-style.xml"
eclipse("4.33").configFile "artemis-spotless-style.xml"

removeUnusedImports()
trimTrailingWhitespace()
Expand Down
47 changes: 34 additions & 13 deletions docs/dev/playwright.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ Set up Playwright locally
To run the tests locally, developers need to set up Playwright on their machines.
End-to-end tests test entire workflows; therefore, they require the whole Artemis setup - database, client, and server to be running.
Playwright tests rely on the Playwright Node.js library, browser binaries, and some helper packages.
To run playwright tests locally, you need to start the Artemis server and client, have the correct users set up and install and run playwright.
This setup should be used for debugging, and creating new tests for your code, but needs intellij to work, and relies on fully setting up your local Artemis instance
following :ref:`the server setup guide<dev_setup>`.


For a quick test setup with only three steps, you can use the scripts provided in `supportingScripts/playwright`.
The README explains what you need to do.
It sets up Artemis inside a dockerized environment, creates users and directly starts playwright. The main drawback with this setup is, that you cannot
easily change the version of Artemis itself.


If you want to manually install playwright, you can follow these steps:

1. Install dependencies:

Expand All @@ -29,40 +41,49 @@ Playwright tests rely on the Playwright Node.js library, browser binaries, and s

.. code-block:: text
PLAYWRIGHT_USERNAME_TEMPLATE=artemis_test_user_USERID
PLAYWRIGHT_PASSWORD_TEMPLATE=artemis_test_user_USERID
PLAYWRIGHT_USERNAME_TEMPLATE=artemis_test_user_
PLAYWRIGHT_PASSWORD_TEMPLATE=artemis_test_user_
ADMIN_USERNAME=artemis_admin
ADMIN_PASSWORD=artemis_admin
ALLOW_GROUP_CUSTOMIZATION=true
STUDENT_GROUP_NAME=students
TUTOR_GROUP_NAME=tutors
EDITOR_GROUP_NAME=editors
INSTRUCTOR_GROUP_NAME=instructors
CREATE_USERS=true
BASE_URL=http://localhost:9000
EXERCISE_REPO_DIRECTORY=test-exercise-repos
FAST_TEST_TIMEOUT_SECONDS=45
SLOW_TEST_TIMEOUT_SECONDS=180
Make sure ``BASE_URL`` matches your Artemis client URL and ``ADMIN_USERNAME`` and
``ADMIN_PASSWORD`` match your Artemis admin user credentials.

3. Configure test users

Playwright tests require users with different roles to simulate concurrent user interactions. You can configure
user IDs and check their corresponding user roles in the ``src/test/playwright/support/users.ts`` file. Usernames
are defined automatically by replacing the ``USERID`` part in ``PLAYWRIGHT_USERNAME_TEMPLATE`` with the
corresponding user ID. If users with such usernames do not exist, set ``CREATE_USERS`` to ``true`` on the
``playwright.env`` file for users to be created during the setup stage. If users with the same usernames but
different user roles already exist, change the user IDs to different values to ensure that new users are created
with roles defined in the configuration.
Playwright tests require users with different roles to simulate concurrent user interactions. If you already
have generated test users, you can skip this step. Generate users with the help of the user creation scripts under the
`supportingScripts/playwright` folder:

.. code-block:: bash
setupUsers.sh
You can configure user IDs and check their corresponding user roles in the ``src/test/playwright/support/users.ts`` file.
Usernames are defined automatically by appending the userId to the ``PLAYWRIGHT_USERNAME_TEMPLATE``.
At the moment it is discouraged to change the template string, as the user creation script does not support other names yet.

4. Setup Playwright package and its browser binaries:

Install Playwright browser binaries, set up the environment to ensure Playwright can locate these binaries, and
create test users (if creating users is enabled in the configuration) with the following command:
Install Playwright browser binaries, set up the environment to ensure Playwright can locate these binaries.
On some operating systems this might not work, and playwright needs to be manually installed via a package manager.

.. code-block:: bash
npm run playwright:setup
npm run playwright:setup-local
npm run playwright:init
5. Open Playwright UI

Expand Down
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ module.exports = {
},
],
},
modulePathIgnorePatterns: ['<rootDir>/src/main/resources/templates/'],
modulePathIgnorePatterns: ['<rootDir>/src/main/resources/templates/', '<rootDir>/build/'],
testTimeout: 3000,
testMatch: [
'<rootDir>/src/test/javascript/spec/component/**/*.spec.ts',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
import de.tum.cit.aet.artemis.atlas.repository.CompetencyRepository;
import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException;
import de.tum.cit.aet.artemis.core.repository.UserRepository;
import de.tum.cit.aet.artemis.iris.service.session.IrisCourseChatSessionService;
import de.tum.cit.aet.artemis.iris.service.pyris.PyrisEventService;
import de.tum.cit.aet.artemis.iris.service.pyris.event.CompetencyJolSetEvent;

/**
* Service Implementation for managing CompetencyJol.
Expand All @@ -44,15 +45,15 @@ public class CompetencyJolService {

private final UserRepository userRepository;

private final Optional<IrisCourseChatSessionService> irisCourseChatSessionService;
private final Optional<PyrisEventService> pyrisEventService;

public CompetencyJolService(CompetencyJolRepository competencyJolRepository, CompetencyRepository competencyRepository,
CompetencyProgressRepository competencyProgressRepository, UserRepository userRepository, Optional<IrisCourseChatSessionService> irisCourseChatSessionService) {
CompetencyProgressRepository competencyProgressRepository, UserRepository userRepository, Optional<PyrisEventService> pyrisEventService) {
this.competencyJolRepository = competencyJolRepository;
this.competencyRepository = competencyRepository;
this.competencyProgressRepository = competencyProgressRepository;
this.userRepository = userRepository;
this.irisCourseChatSessionService = irisCourseChatSessionService;
this.pyrisEventService = pyrisEventService;
}

/**
Expand Down Expand Up @@ -83,10 +84,10 @@ public void setJudgementOfLearning(long competencyId, long userId, short jolValu
final var jol = createCompetencyJol(competencyId, userId, jolValue, ZonedDateTime.now(), competencyProgress);
competencyJolRepository.save(jol);

irisCourseChatSessionService.ifPresent(service -> {
pyrisEventService.ifPresent(service -> {
// Inform Iris so it can send a message to the user
try {
service.onJudgementOfLearningSet(jol);
service.trigger(new CompetencyJolSetEvent(jol));
}
catch (Exception e) {
log.warn("Something went wrong while sending the judgement of learning to Iris", e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,16 +342,11 @@ private void createScriptFile(String buildJobContainerId) {
private void addAndPrepareDirectoryAndReplaceContent(String containerId, Path repositoryPath, String newDirectoryName) {
copyToContainer(repositoryPath.toString(), containerId);
addDirectory(containerId, newDirectoryName, true);
removeDirectoryAndFiles(containerId, newDirectoryName);
renameDirectoryOrFile(containerId, LOCALCI_WORKING_DIRECTORY + "/" + repositoryPath.getFileName().toString(), newDirectoryName);
insertRepositoryFiles(containerId, LOCALCI_WORKING_DIRECTORY + "/" + repositoryPath.getFileName().toString(), newDirectoryName);
}

private void removeDirectoryAndFiles(String containerId, String newName) {
executeDockerCommand(containerId, null, false, false, true, "rm", "-rf", newName);
}

private void renameDirectoryOrFile(String containerId, String oldName, String newName) {
executeDockerCommand(containerId, null, false, false, true, "mv", oldName, newName);
private void insertRepositoryFiles(String containerId, String oldName, String newName) {
executeDockerCommand(containerId, null, false, false, true, "cp", "-r", oldName + (oldName.endsWith("/") ? "." : "/."), newName);
}

private void addDirectory(String containerId, String directoryName, boolean createParentsIfNecessary) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ public final class Constants {

public static final String DELETE_EXAM = "DELETE_EXAM";

public static final String UPDATE_EXAM = "UPDATE_EXAM";

public static final String ADD_USER_TO_EXAM = "ADD_USER_TO_EXAM";

public static final String REMOVE_USER_FROM_EXAM = "REMOVE_USER_FROM_EXAM";
Expand Down
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
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 @@ -22,6 +24,10 @@ public class JWTFilter extends GenericFilterBean {

public static final String JWT_COOKIE_NAME = "jwt";

private static final String AUTHORIZATION_HEADER = "Authorization";

private static final String BEARER_PREFIX = "Bearer ";

private final TokenProvider tokenProvider;

public JWTFilter(TokenProvider tokenProvider) {
Expand All @@ -31,26 +37,89 @@ 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;
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_HEADER);

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("Multiple authentication methods detected: Both JWT cookie and Bearer token are present");
}

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

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

return jwtToken;
}

/**
* 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_PREFIX)) {
return null;
}
String jwt = jwtCookie.getValue();
return StringUtils.hasText(jwt) && tokenProvider.validateTokenForAuthority(jwt);

String token = jwtBearer.substring(BEARER_PREFIX.length()).trim();
return StringUtils.hasText(token) ? token : null;
}

/**
* 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ private Claims parseClaims(String authToken) {
return Jwts.parser().verifyWith(key).build().parseSignedClaims(authToken).getPayload();
}

public <T> T getClaim(String token, String claimName, Class<T> claimType) {
Claims claims = parseClaims(token);
return claims.get(claimName, claimType);
}

public Date getExpirationDate(String authToken) {
return parseClaims(authToken).getExpiration();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE;

import java.util.Map;
import java.util.Optional;

import jakarta.servlet.ServletException;
Expand Down Expand Up @@ -69,7 +70,7 @@ public PublicUserJwtResource(JWTCookieService jwtCookieService, AuthenticationMa
*/
@PostMapping("authenticate")
@EnforceNothing
public ResponseEntity<Void> authorize(@Valid @RequestBody LoginVM loginVM, @RequestHeader("User-Agent") String userAgent, HttpServletResponse response) {
public ResponseEntity<Map<String, String>> authorize(@Valid @RequestBody LoginVM loginVM, @RequestHeader("User-Agent") String userAgent, HttpServletResponse response) {

var username = loginVM.getUsername();
var password = loginVM.getPassword();
Expand All @@ -86,7 +87,7 @@ public ResponseEntity<Void> authorize(@Valid @RequestBody LoginVM loginVM, @Requ
ResponseCookie responseCookie = jwtCookieService.buildLoginCookie(rememberMe);
response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString());

return ResponseEntity.ok().build();
return ResponseEntity.ok(Map.of("access_token", responseCookie.getValue()));
}
catch (BadCredentialsException ex) {
log.warn("Wrong credentials during login for user {}", loginVM.getUsername());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,10 @@ public ResponseEntity<Exam> updateExam(@PathVariable Long courseId, @RequestBody

Exam savedExam = examRepository.save(updatedExam);

User instructor = userRepository.getUser();
final var auditEvent = new AuditEvent(instructor.getLogin(), Constants.UPDATE_EXAM, "exam=" + savedExam.getId());
auditEventRepository.add(auditEvent);

// NOTE: We have to get exercises and groups as we need them for re-scheduling
Exam examWithExercises = examService.findByIdWithExerciseGroupsAndExercisesElseThrow(savedExam.getId(), false);

Expand Down
Loading

0 comments on commit 4134f1f

Please sign in to comment.