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

Feature/better testable code #83

Merged
merged 32 commits into from
Mar 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1701010
Added /api/users/{userId}
Aqua-sc Mar 4, 2024
63ea743
Updated msal config
usserwoutV2 Mar 4, 2024
5e84cd1
Started making role interceptor
usserwoutV2 Mar 4, 2024
a55bd1d
role test
usserwoutV2 Mar 4, 2024
a6bbdae
Fixed merge conflict
usserwoutV2 Mar 4, 2024
3b2ee90
Finished role interceptor
usserwoutV2 Mar 4, 2024
a0099db
Create a new user when new auth is detected
usserwoutV2 Mar 6, 2024
d45ef8b
Created filehandler
Aqua-sc Mar 6, 2024
391103d
Added route to save submission (not completed yet)
Aqua-sc Mar 6, 2024
4af54a6
Added persistent dir
Aqua-sc Mar 6, 2024
3c46daf
Finished role filter
usserwoutV2 Mar 6, 2024
5493611
Added db logic to saving submission
Aqua-sc Mar 6, 2024
8398c6e
Added route to fetch submission
Aqua-sc Mar 6, 2024
871ce4a
use Tika to check for zip file
Aqua-sc Mar 6, 2024
7131c80
submit:check to see if user is part of the project
Aqua-sc Mar 6, 2024
a9af18b
Merge pull request #39 from SELab-2/feature/savefiles
Matthias-VE Mar 7, 2024
45c0788
Merge remote-tracking branch 'origin/main' into feature/role-interceptor
usserwoutV2 Mar 7, 2024
80aec24
start of branch, not done yet
arnedierick Mar 7, 2024
2b0c01a
Added role dectorator de existing routes
usserwoutV2 Mar 7, 2024
8d4c190
Added timestamp when user is created
usserwoutV2 Mar 7, 2024
a2c57cd
Merge pull request #47 from SELab-2/feature/role-interceptor
usserwoutV2 Mar 7, 2024
ce1b297
added putmapping for project tests
arnedierick Mar 7, 2024
88e52f1
Merge branch 'main' into feature/project-tests
arnedierick Mar 8, 2024
d521620
code is opgekuist, pull request kan uitgevoerd worden mits goedkeuring
arnedierick Mar 8, 2024
7659d16
method for testpath
Aqua-sc Mar 8, 2024
3cd6a4a
updated dockerimage to string +fix db link
Aqua-sc Mar 8, 2024
5e1fdcd
re-enabled auth
Aqua-sc Mar 8, 2024
cd6c863
Merge remote-tracking branch 'origin/feature/test-suite' into feature…
usserwoutV2 Mar 8, 2024
8ce73b5
Added test example
usserwoutV2 Mar 8, 2024
a316e9f
Added easier way to make requests
usserwoutV2 Mar 9, 2024
f50ed9d
Updated test
usserwoutV2 Mar 9, 2024
6534d6c
added comment
usserwoutV2 Mar 9, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ out/

### VS Code ###
.vscode/
backend/app/data/*
2 changes: 2 additions & 0 deletions backend/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ dependencies {
implementation 'com.auth0:jwks-rsa:0.18.0'
implementation 'javax.servlet:javax.servlet-api:4.0.1'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.apache.tika:tika-core:1.27'
runtimeOnly 'org.postgresql:postgresql'


implementation "org.springframework.boot:spring-boot-devtools"
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.ugent.pidgeon.config;
package com.ugent.pidgeon.auth;

import com.auth0.jwk.Jwk;
import com.auth0.jwk.JwkException;
Expand All @@ -10,6 +10,10 @@
import com.auth0.jwt.interfaces.DecodedJWT;
import com.ugent.pidgeon.model.Auth;
import com.ugent.pidgeon.model.User;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
Expand All @@ -22,13 +26,20 @@
import java.util.ArrayList;
import java.util.List;

/**
* This class extends OncePerRequestFilter to provide a filter that decodes and verifies JWT tokens.
* It uses JwkProvider to fetch the public key for verification.
*/
public class JwtAuthenticationFilter extends OncePerRequestFilter {
// JwkProvider instance to fetch the public key for JWT verification
private JwkProvider provider;



public JwtAuthenticationFilter(String tenantId)
{
/**
* Constructor for JwtAuthenticationFilter.
* It initializes the JwkProvider with the URL of the public key.
* @param tenantId the tenantId used to construct the URL of the public key
*/
public JwtAuthenticationFilter(String tenantId) {
try {
logger.info("tenantId: " + tenantId);
provider = new UrlJwkProvider(new URL("https://login.microsoftonline.com/"+tenantId+"/discovery/v2.0/keys"));
Expand All @@ -38,17 +49,27 @@ public JwtAuthenticationFilter(String tenantId)
}


/**
* This method is called for every request to filter requests based on JWT token.
* It decodes the JWT token from the Authorization header, verifies it, and sets the authentication in the SecurityContext.
* If the JWT token is not present or invalid, it sets the response status to UNAUTHORIZED.
* @param request HttpServletRequest that is being processed
* @param response HttpServletResponse that is being created
* @param filterChain FilterChain for calling the next filter
* @throws jakarta.servlet.ServletException in case of errors
* @throws IOException in case of I/O errors
*/
@Override
protected void doFilterInternal(jakarta.servlet.http.HttpServletRequest request, jakarta.servlet.http.HttpServletResponse response, jakarta.servlet.FilterChain filterChain) throws jakarta.servlet.ServletException, IOException {
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
logger.info(request.getRequestURL().toString());

String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
String token = bearerToken.substring(7);

DecodedJWT jwt = JWT.decode(token);
Jwk jwk =null;
Algorithm algorithm=null;
Jwk jwk;
Algorithm algorithm;

try {
jwk = provider.get(jwt.getKeyId());
Expand All @@ -60,14 +81,13 @@ protected void doFilterInternal(jakarta.servlet.http.HttpServletRequest request,
String firstName = jwt.getClaim("given_name").asString();
String lastName = jwt.getClaim("family_name").asString();
String email = jwt.getClaim("unique_name").asString();
List<String> groups = jwt.getClaim("groups").asList(String.class);
String oid = jwt.getClaim("oid").asString();

// print full object
//logger.info(jwt.getClaims());
// logger.info(jwt.getClaims());


User user = new User(displayName, firstName,lastName, email, groups, oid);
User user = new User(displayName, firstName,lastName, email, oid);

Auth authUser = new Auth(user, new ArrayList<>());
SecurityContextHolder.getContext().setAuthentication(authUser);
Expand Down
14 changes: 14 additions & 0 deletions backend/app/src/main/java/com/ugent/pidgeon/auth/Roles.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.ugent.pidgeon.auth;

import com.ugent.pidgeon.postgre.models.types.UserRole;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Roles {
UserRole[] value();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.ugent.pidgeon.auth;


import com.ugent.pidgeon.model.Auth;
import com.ugent.pidgeon.postgre.models.UserEntity;
import com.ugent.pidgeon.postgre.models.types.UserRole;
import com.ugent.pidgeon.postgre.repository.UserRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import java.sql.Timestamp;
import java.util.Arrays;
import java.util.List;


/**
* This class is a Spring component that implements the HandlerInterceptor interface.
* It is used to intercept HTTP requests and perform role-based access control.
*/
@Component
public class RolesInterceptor implements HandlerInterceptor {

// UserRepository instance for interacting with the user data in the database
private final UserRepository userRepository;

/**
* Constructor for RolesInterceptor.
* @param userRepository UserRepository instance for interacting with the user data in the database
*/
@Autowired
public RolesInterceptor(UserRepository userRepository) {
this.userRepository = userRepository;
}

/**
* This method is called before the actual handler is executed.
* It checks if the handler is a HandlerMethod and if it has a Roles annotation.
* If the Roles annotation is present, it checks if the authenticated user has the required role.
* If the user does not exist, it creates a new user with the role of 'student'.
* If the user does not have the required role, it sends an HTTP 403 error and returns false.
* @param request HttpServletRequest that is being processed
* @param response HttpServletResponse that is being created
* @param handler chosen handler to execute, for type and/or instance evaluation
* @return true if the execution chain should proceed with the next interceptor or the handler itself
* @throws Exception in case of errors
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod handlerMethod) {
Roles rolesAnnotation = handlerMethod.getMethodAnnotation(Roles.class);
if (rolesAnnotation != null) {
List<UserRole> requiredRoles = Arrays.asList(rolesAnnotation.value());
Auth auth = (Auth) SecurityContextHolder.getContext().getAuthentication();
UserEntity userEntity = userRepository.findUserByAzureId(auth.getOid());

if(userEntity == null) {
System.out.println("User does not exist, creating new one. user_id: " + auth.getOid());
userEntity = new UserEntity(auth.getUser().firstName,auth.getUser().lastName, auth.getEmail(), UserRole.student, auth.getOid());
Timestamp now = new Timestamp(System.currentTimeMillis());
userEntity.setCreatedAt(now);
userRepository.save(userEntity);
System.out.println("User created with id: " + userEntity.getId());

}
auth.setUserEntity(userEntity);

if (!requiredRoles.contains(userEntity.getRole()) || userEntity.getRole() == UserRole.admin) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "User does not have required role");
return false;
}
}
}
return true;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.ugent.pidgeon.config;

import com.ugent.pidgeon.auth.JwtAuthenticationFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
Expand All @@ -22,7 +23,7 @@ public FilterRegistrationBean<JwtAuthenticationFilter> filterRegistrationBean()

FilterRegistrationBean<JwtAuthenticationFilter> filter = new FilterRegistrationBean<>();
filter.setFilter(new JwtAuthenticationFilter(tenantId));
filter.addUrlPatterns("/api/ietswatiknietwiltesten");
filter.addUrlPatterns("/api/*");
return filter;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
package com.ugent.pidgeon.config;

import com.ugent.pidgeon.auth.RolesInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {


private final RolesInterceptor rolesInterceptor;

@Autowired
public WebConfig(RolesInterceptor rolesInterceptor) {
this.rolesInterceptor = rolesInterceptor;
}

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedMethods("*")
.allowedOrigins("*")
.allowedHeaders("*");
}


@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(rolesInterceptor);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.ugent.pidgeon.controllers;

// TODO: Change this to an enum
public final class ApiRoutes {
public static final String USER_BASE_PATH = "/api/user/";
public static final String COURSE_BASE_PATH = "/api/course/";
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.ugent.pidgeon.controllers;
import com.ugent.pidgeon.auth.Roles;
import com.ugent.pidgeon.model.Auth;
import com.ugent.pidgeon.model.User;
import com.ugent.pidgeon.postgre.models.types.UserRole;
import com.ugent.pidgeon.postgre.repository.UserRepository;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -13,7 +15,9 @@ public class AuthTestController {
@Autowired
private UserRepository userRepository;


@GetMapping("/api/test")
@Roles({UserRole.student, UserRole.teacher})
public User testApi(HttpServletRequest request, Auth auth) {
return auth.getUser();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package com.ugent.pidgeon.controllers;

import com.ugent.pidgeon.auth.Roles;
import com.ugent.pidgeon.model.Auth;
import com.ugent.pidgeon.postgre.models.FileEntity;
import com.ugent.pidgeon.postgre.models.SubmissionEntity;
import com.ugent.pidgeon.postgre.models.types.UserRole;
import com.ugent.pidgeon.postgre.repository.FileRepository;
import com.ugent.pidgeon.postgre.repository.GroupRepository;
import com.ugent.pidgeon.postgre.repository.ProjectRepository;
import com.ugent.pidgeon.postgre.repository.SubmissionRepository;
import com.ugent.pidgeon.util.Filehandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.nio.file.Path;
import java.sql.Timestamp;

@RestController
public class FilesubmissiontestController {

@Autowired
private GroupRepository groupRepository;
@Autowired
private FileRepository fileRepository;
@Autowired
private SubmissionRepository submissionRepository;
@Autowired
private ProjectRepository projectRepository;

@PostMapping("/project/{projectid}/submit") //Route to submit a file, it accepts a multiform with the file and submissionTime
@Roles({UserRole.teacher, UserRole.student})
public ResponseEntity<String> submitFile(@RequestParam("file") MultipartFile file, @RequestParam("submissionTime") Timestamp time, @PathVariable("projectid") long projectid,Auth auth) {
long userId = auth.getUserEntity().getId();
Long groupId = groupRepository.groupIdByProjectAndUser(projectid, userId);

if (!projectRepository.userPartOfProject(projectid, userId)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("You aren't part of this project");
}
//TODO: executes the tests onces these are implemented
try {
//Save the file entry in the database to get the id
FileEntity fileEntity = new FileEntity("", "", userId);
long fileid = fileRepository.save(fileEntity).getId();

//Save the submission in the database TODO: update the accepted parameter
SubmissionEntity submissionEntity = new SubmissionEntity(projectid, groupId, fileid, time, false);
SubmissionEntity submission = submissionRepository.save(submissionEntity);

//Save the file on the server
Path path = Filehandler.getSubmissionPath(projectid, groupId, submission.getId());
String filename = Filehandler.saveSubmission(path, file);

//Update name and path for the file entry
fileEntity.setName(filename);
fileEntity.setPath(path.toString());
fileRepository.save(fileEntity);

return ResponseEntity.ok("File saved");
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error while saving file: " + e.getMessage());
}

}

@GetMapping("submissions/{submissionid}")
@Roles({UserRole.teacher, UserRole.student})
public ResponseEntity<Resource> getSubmission(@PathVariable("submissionid") long submissionid, Auth auth) {
long userId = auth.getUserEntity().getId();
// Get the submission entry from the database
SubmissionEntity submission = submissionRepository.findById(submissionid).orElse(null);
if (submission == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}

if (!groupRepository.userInGroup(submission.getGroupId(), userId)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(null);
}
// Get the file entry from the database
FileEntity file = fileRepository.findById(submission.getFileId()).orElse(null);
if (file == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}


// Get the file from the server
try {
Resource zipFile = Filehandler.getSubmissionAsResource(Path.of(file.getPath(), file.getName()));

// Set headers for the response
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + file.getName());
headers.add(HttpHeaders.CONTENT_TYPE, "application/zip");

return ResponseEntity.ok()
.headers(headers)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(zipFile);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
}
}
}
Loading