Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Empty file added .env.example
Empty file.
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ COPY src ./src
RUN mvn clean package -DskipTests
FROM amazoncorretto:21
WORKDIR /app
COPY .env* .
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
4 changes: 4 additions & 0 deletions src/main/java/org/pkwmtt/examCalendar/ExamController.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.pkwmtt.examCalendar;

import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Positive;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -29,6 +30,7 @@ public class ExamController {
* @return 201 created with URI to GET method which returns created resource
*/
@PostMapping("")
@SecurityRequirement(name = "bearerAuth")
public ResponseEntity<Void> addExam(@RequestBody @Valid ExamDto examDto){
int id = examService.addExam(examDto);
URI uri = ServletUriComponentsBuilder
Expand All @@ -45,6 +47,7 @@ public ResponseEntity<Void> addExam(@RequestBody @Valid ExamDto examDto){
* @return 204 no content
*/
@PutMapping("/{id}")
@SecurityRequirement(name = "bearerAuth")
public ResponseEntity<Void> modifyExam(@PathVariable @Positive int id, @RequestBody @Valid ExamDto examDto) {
examService.modifyExam(examDto, id);
return ResponseEntity.noContent().build();
Expand All @@ -55,6 +58,7 @@ public ResponseEntity<Void> modifyExam(@PathVariable @Positive int id, @RequestB
* @return 204 no content
*/
@DeleteMapping("/{id}")
@SecurityRequirement(name = "bearerAuth")
public ResponseEntity<Void> deleteExam(@PathVariable int id) {
examService.deleteExam(id);
return ResponseEntity.noContent().build();
Expand Down
60 changes: 59 additions & 1 deletion src/main/java/org/pkwmtt/examCalendar/ExamService.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,15 @@
import org.pkwmtt.examCalendar.repository.ExamTypeRepository;
import org.pkwmtt.examCalendar.repository.GroupRepository;
import org.pkwmtt.exceptions.*;
import org.pkwmtt.security.token.JwtAuthenticationToken;
import org.pkwmtt.timetable.TimetableService;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Service
Expand All @@ -34,6 +39,8 @@ public class ExamService {
*/
public int addExam(ExamDto examDto) {

verifyGroupPermissionsForNewResource(examDto.getGeneralGroups());

Set<StudentGroup> groups = verifyAndUpdateExamGroups(examDto);

ExamType examType = examTypeRepository.findByName(examDto.getExamType())
Expand All @@ -55,6 +62,8 @@ public void modifyExam(ExamDto examDto, int id) {

examRepository.findById(id).orElseThrow(() -> new NoSuchElementWithProvidedIdException(id));

verifyGroupPermissionsForModifiedResource(examDto.getGeneralGroups(), id);

Set<StudentGroup> groups = verifyAndUpdateExamGroups(examDto);

ExamType examType = examTypeRepository.findByName(examDto.getExamType())
Expand All @@ -68,6 +77,7 @@ public void modifyExam(ExamDto examDto, int id) {
*/
public void deleteExam(int id) {
examRepository.findById(id).orElseThrow(() -> new NoSuchElementWithProvidedIdException(id));
verifyGroupPermissionsForExistingResource(id);
examRepository.deleteById(id);
}

Expand Down Expand Up @@ -210,6 +220,8 @@ private static String trimLastDigit(String generalGroup) {
* @throws InvalidGroupIdentifierException when not all provided groups belong to the same year of study
*/
private static String trimLastDigit(Set<String> superiorGroups) throws InvalidGroupIdentifierException {
if(superiorGroups == null || superiorGroups.isEmpty())
throw new InvalidGroupIdentifierException("general group is missing");
Set<String> trimmedGroups = superiorGroups.stream()
.map(ExamService::trimLastDigit)
.collect(Collectors.toSet());
Expand Down Expand Up @@ -261,4 +273,50 @@ private static void verifySubgroupsFormat(Set<String> subgroups) throws Specifie
throw new SpecifiedSubGroupDoesntExistsException(group);
});
}

/**
* verifies if user has authorities to add new resource
* @param newGroups set of provided groups
*/
private void verifyGroupPermissionsForNewResource(Set<String> newGroups){
String userGroup = getUserGroup();
if(!trimLastDigit(newGroups).equals(userGroup))
throw new AccessDeniedException("You don't have permission to access this group");
}

/**
* verifies if user has authorities to modify existing resource
* @param examId id of existing resource
*/
private void verifyGroupPermissionsForExistingResource(Integer examId){
String userGroup = getUserGroup();
Set<String> generalGroupsOfExam = examRepository.findGroupsByExamId(examId)
.stream()
.filter(group -> group.matches("^\\d.*"))
.collect(Collectors.toSet());
if(!trimLastDigit(generalGroupsOfExam).equals(userGroup))
throw new AccessDeniedException("You don't have permission to access this group");
}

/**
* verifies if user had authorities to replace existing resource with new one
* @param newGroups set of groups of new resource
* @param examId id of existing resource
*/
private void verifyGroupPermissionsForModifiedResource(Set<String> newGroups, Integer examId){
verifyGroupPermissionsForNewResource(newGroups);
verifyGroupPermissionsForExistingResource(examId);
}

/**
* @return superior group identifier (e.g. 12K) of currently authenticated user
* @throws AccessDeniedException when user doesn't have assigned group
*/
private String getUserGroup() throws AccessDeniedException {
JwtAuthenticationToken authentication = (JwtAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
String group = authentication.getExamGroup();
if(group == null)
throw new AccessDeniedException("You doesn't have access to any group");
return group;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public interface ExamRepository extends JpaRepository<Exam, Integer> {

Set<Exam> findAllByTitle(String title);

@Query("SELECT g.name FROM Exam e LEFT JOIN e.groups g WHERE e.examId = :id")
Set<String> findGroupsByExamId(@Param("id") Integer examId);

/**
* @param groups set of generalGroups
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
package org.pkwmtt.examCalendar.repository;

import jakarta.transaction.Transactional;
import org.pkwmtt.examCalendar.entity.GeneralGroup;
import org.pkwmtt.examCalendar.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Integer> {
Optional<User> findByEmail (String email);

Optional<User> findByGeneralGroup (GeneralGroup generalGroup);

@Query("SELECT g.name FROM User u LEFT JOIN u.generalGroup g where u.email = :email")
Optional<String> findGroupByUserEmail (@Param("email") String userEmail);

@Transactional
void deleteUserByEmail (String email);
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,16 @@ public GroupedOpenApi publicEndpointCustomizer () {
.pathsToMatch(apiPrefix + "/**", "/admin/**").addOpenApiCustomizer(openApi -> {
Paths paths = openApi.getPaths();

paths.forEach((path, pathItem) -> pathItem.readOperations().forEach(operation -> {
paths.forEach((path, pathItem) -> pathItem.readOperationsMap().forEach(((httpMethod, operation) -> {
if (path.startsWith("/admin")) {
addHeaderIfMissing(
operation,
"X-ADMIN-KEY",
"Admin API key",
"Admin-only endpoint",
"Requires X-ADMIN-KEY header",
"admin"
"admin",
true
);
} else if (path.startsWith(apiPrefix)) {
addHeaderIfMissing(
Expand All @@ -57,21 +58,26 @@ public GroupedOpenApi publicEndpointCustomizer () {
"Your API key",
"Public API endpoint",
"Requires X-API-KEY header",
"public"
"public",
true
);
}
}));
// if (path.contains("exams") && (httpMethod.equals(PathItem.HttpMethod.POST) || httpMethod.equals(
// PathItem.HttpMethod.PUT) || httpMethod.equals(PathItem.HttpMethod.DELETE))) {
// operation.addSecurityItem(new SecurityRequirement().addList("bearerAuth"));
// }
})));
}).build();
}

private void addHeaderIfMissing (Operation operation, String headerName, String headerDescription, String summary, String description, String tag) {
private void addHeaderIfMissing (Operation operation, String headerName, String headerDescription, String summary, String description, String tag, boolean required) {
operation.setSummary(summary);
operation.setDescription(description);
operation.addTagsItem(tag);
operation.addParametersItem(new Parameter()
.name(headerName)
.in("header")
.required(true)
.required(required)
.description(headerDescription)
.schema(new StringSchema()));
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/pkwmtt/otp/OTPController.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public ResponseEntity<String> authenticate (@RequestParam(name = "c") String cod

@PostMapping("/codes/generate")
public ResponseEntity<Void> generateCodes (@RequestBody List<OTPRequest> request)
throws MailCouldNotBeSendException, WrongArgumentException, SpecifiedGeneralGroupDoesntExistsException {
throws MailCouldNotBeSendException, WrongArgumentException, SpecifiedGeneralGroupDoesntExistsException, IllegalArgumentException {
service.sendOTPCodesForManyGroups(request);
return ResponseEntity.ok().build();
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/pkwmtt/otp/OTPExceptionHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

@RestControllerAdvice(assignableTypes = {OTPController.class})
public class OTPExceptionHandler {
@ExceptionHandler({OTPCodeNotFoundException.class, WrongOTPFormatException.class, UserNotFoundException.class, WrongArgumentException.class, SpecifiedGeneralGroupDoesntExistsException.class})
@ExceptionHandler({OTPCodeNotFoundException.class, WrongOTPFormatException.class, UserNotFoundException.class, WrongArgumentException.class, SpecifiedGeneralGroupDoesntExistsException.class, IllegalArgumentException.class})
public ResponseEntity<ErrorResponseDTO> handleBadRequests (Exception e) {
return new ResponseEntity<>(new ErrorResponseDTO(e.getMessage()), HttpStatus.BAD_REQUEST);
}
Expand Down
23 changes: 14 additions & 9 deletions src/main/java/org/pkwmtt/otp/OTPService.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,34 +54,35 @@ public String generateTokenForRepresentative (String code)
}

public void sendOTPCodesForManyGroups (List<OTPRequest> requests)
throws MailCouldNotBeSendException, WrongArgumentException, SpecifiedSubGroupDoesntExistsException {
throws MailCouldNotBeSendException, WrongArgumentException, SpecifiedSubGroupDoesntExistsException, IllegalArgumentException {
requests.forEach(request -> {
var code = generateNewCode();
var mail = createMail(request, code);
var groupName = request.getGeneralGroupName();
var groupNameLength = groupName.length();

if (groupNameLength > 3 && Character.isDigit(groupName.charAt(groupNameLength - 1))) {
if (groupNameLength > 3 && Character.isDigit(groupName.charAt(groupNameLength - 1))) { //Check general group name
throw new WrongArgumentException(
"Wrong general group provided. Make sure you are not providing subgroup. (f.e 12K1 -> wrong, 12K -> good)");
}

if (!generalGroupExists(groupName)) {
if (!generalGroupExists(groupName)) { // Check if general group with provided name exists
throw new SpecifiedGeneralGroupDoesntExistsException();
}

var generalGroup = generalGroupRepository.findByName(groupName);

if (generalGroup.isPresent()) {
if (otpRepository.existsOTPCodeByGeneralGroup(generalGroup.get())) {
throw new RuntimeException("");
if (generalGroup.isPresent()) { //Check if general group is already saved in database
if (otpRepository.existsOTPCodeByGeneralGroup(generalGroup.get())) { //Check if provided general group has assigned code
otpRepository.deleteByGeneralGroup(generalGroup.get()); // Delete existing code
}
} else {
//Save general group to database
generalGroup = Optional.of(generalGroupRepository.save(new GeneralGroup(null, groupName)));
}

try {
emailService.send(mail);
emailService.send(mail); //Send email
} catch (MessagingException e) {
throw new MailCouldNotBeSendException("Couldn't send mail for group: " + groupName);
}
Expand All @@ -94,13 +95,17 @@ public void sendOTPCodesForManyGroups (List<OTPRequest> requests)
.isActive(true)
.build();

userRepository.save(user);
userRepository
.findByGeneralGroup(generalGroup.get())
.ifPresent(value -> userRepository.deleteUserByEmail(value.getEmail()));

userRepository.save(user);
otpRepository.save(new OTPCode(code, generalGroup.get()));
});
}

private GeneralGroup getGeneralGroupAssignedToCode (String code) throws OTPCodeNotFoundException, WrongOTPFormatException {
private GeneralGroup getGeneralGroupAssignedToCode (String code)
throws OTPCodeNotFoundException, WrongOTPFormatException {
this.validateCode(code);

Optional<OTPCode> result = otpRepository.findByCode(code);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ public interface OTPCodeRepository extends JpaRepository<OTPCode, Integer> {
boolean existsOTPCodeByGeneralGroup (GeneralGroup generalGroup);

boolean existsOTPCodeByCode (String code);

@Transactional
void deleteByGeneralGroup (GeneralGroup generalGroup);
}
18 changes: 16 additions & 2 deletions src/main/java/org/pkwmtt/security/config/SpringSecurity.java
Original file line number Diff line number Diff line change
@@ -1,29 +1,43 @@
package org.pkwmtt.security.config;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.pkwmtt.security.token.filter.JwtFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import static org.springframework.security.config.Customizer.withDefaults;
import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS;

@EnableWebSecurity
@Slf4j
@Configuration
@RequiredArgsConstructor
public class SpringSecurity {

private final JwtFilter jwtFilter;

@Bean
public SecurityFilterChain filterChain (HttpSecurity http) throws Exception {
log.info("Configuring Security Filter Chain...");
http
.cors(withDefaults())
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth.requestMatchers("/**").permitAll().anyRequest().authenticated())
.sessionManagement(session -> session.sessionCreationPolicy(STATELESS));
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.POST , "/pkwmtt/api/v1/exams").authenticated()
.requestMatchers(HttpMethod.PUT , "/pkwmtt/api/v1/exams").authenticated()
.requestMatchers(HttpMethod.DELETE , "/pkwmtt/api/v1/exams").authenticated()
.requestMatchers("/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session.sessionCreationPolicy(STATELESS))
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
log.info("Configuring Success...");
return http.build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.pkwmtt.security.token;

import lombok.Getter;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

public class JwtAuthenticationToken extends UsernamePasswordAuthenticationToken {

@Getter
private String examGroup;


public JwtAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(principal, null, authorities);
}

public JwtAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities, String group) {
super(principal, null, authorities);
this.examGroup = group;
}

}
Loading