From f95f65b9aa21d027599b0383dd4a3e317e9d3438 Mon Sep 17 00:00:00 2001 From: Leetaehoon <66353672+xogns1514@users.noreply.github.com> Date: Tue, 15 Oct 2024 16:03:07 +0900 Subject: [PATCH] =?UTF-8?q?deploy-be:=20cruru=20version=201.1.2=20?= =?UTF-8?q?=EC=9A=B4=EC=98=81=20=EC=84=9C=EB=B2=84=20=EB=B0=B0=ED=8F=AC=20?= =?UTF-8?q?(v24.10.14)=20(#812)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: HyungHoKim00 Co-authored-by: Do Yeop Kim <113661364+Dobby-Kim@users.noreply.github.com> Co-authored-by: Kwoun Ki Ho --- .github/workflows/be-cd_dev-docker.yml | 1 + .github/workflows/be-cd_prod-docker.yml | 1 + .github/workflows/be-cd_test-docker.yml | 1 + backend/docker-compose.dev.yml | 1 + backend/docker-compose.prod.yml | 1 + backend/docker-compose.test.yml | 1 + backend/promtail-config.yml | 63 +----- .../src/main/java/com/cruru/DataLoader.java | 4 - .../cruru/advice/CruruCustomException.java | 4 - .../com/cruru/advice/ExceptionCallback.java | 7 + .../cruru/advice/GlobalExceptionHandler.java | 120 ++++++---- .../advice/UncatchedExceptionHandler.java | 36 --- .../controller/ApplicantController.java | 41 ++-- .../controller/EvaluationController.java | 20 +- .../request/ApplicantMoveRequest.java | 4 + .../request/ApplicantsRejectRequest.java | 4 + .../repository/ApplicantRepository.java | 2 +- .../repository/EvaluationRepository.java | 2 +- .../applicant/service/ApplicantService.java | 5 - .../applicant/service/EvaluationService.java | 5 - .../controller/ApplyFormController.java | 8 +- .../repository/ApplyFormRepository.java | 2 +- .../applyform/service/ApplyFormService.java | 5 - ...RequireAuthCheck.java => RequireAuth.java} | 6 +- .../com/cruru/auth/annotation/ValidAuth.java | 12 + .../cruru/auth/aspect/AuthCheckAspect.java | 111 --------- .../auth/aspect/AuthValidationAspect.java | 159 +++++++++++++ .../domain/repository/ClubRepository.java | 2 +- .../com/cruru/club/service/ClubService.java | 5 - .../controller/DashboardController.java | 19 +- .../repository/DashboardRepository.java | 2 +- .../dashboard/service/DashboardService.java | 5 - .../email/controller/EmailController.java | 8 +- .../email/controller/dto/EmailRequest.java | 5 + .../cruru/global/LoginArgumentResolver.java | 4 +- .../cruru/global/util/ExceptionLogger.java | 71 ------ .../java/com/cruru/global/util/MdcUtils.java | 48 ++++ .../process/controller/ProcessController.java | 34 +-- .../domain/repository/ProcessRepository.java | 2 +- .../cruru/process/service/ProcessService.java | 5 - .../controller/QuestionController.java | 10 +- .../domain/repository/AnswerRepository.java | 2 +- .../domain/repository/ChoiceRepository.java | 2 +- .../domain/repository/QuestionRepository.java | 2 +- backend/src/main/resources/application.yml | 9 + backend/src/main/resources/logback.xml | 28 ++- .../controller/ApplicantControllerTest.java | 7 +- .../controller/ApplyFormControllerTest.java | 1 + .../auth/aspect/AuthValidationAspectTest.java | 210 ++++++++++++++++++ .../auth/controller/AuthTestController.java | 57 +++++ .../cruru/auth/controller/AuthTestDto.java | 21 ++ .../controller/DashboardControllerTest.java | 75 ++++--- .../controller/QuestionControllerTest.java | 2 +- 53 files changed, 794 insertions(+), 468 deletions(-) create mode 100644 backend/src/main/java/com/cruru/advice/ExceptionCallback.java delete mode 100644 backend/src/main/java/com/cruru/advice/UncatchedExceptionHandler.java rename backend/src/main/java/com/cruru/auth/annotation/{RequireAuthCheck.java => RequireAuth.java} (67%) create mode 100644 backend/src/main/java/com/cruru/auth/annotation/ValidAuth.java delete mode 100644 backend/src/main/java/com/cruru/auth/aspect/AuthCheckAspect.java create mode 100644 backend/src/main/java/com/cruru/auth/aspect/AuthValidationAspect.java delete mode 100644 backend/src/main/java/com/cruru/global/util/ExceptionLogger.java create mode 100644 backend/src/main/java/com/cruru/global/util/MdcUtils.java create mode 100644 backend/src/test/java/com/cruru/auth/aspect/AuthValidationAspectTest.java create mode 100644 backend/src/test/java/com/cruru/auth/controller/AuthTestController.java create mode 100644 backend/src/test/java/com/cruru/auth/controller/AuthTestDto.java diff --git a/.github/workflows/be-cd_dev-docker.yml b/.github/workflows/be-cd_dev-docker.yml index 7a970c1fa..bef7adaea 100644 --- a/.github/workflows/be-cd_dev-docker.yml +++ b/.github/workflows/be-cd_dev-docker.yml @@ -105,6 +105,7 @@ jobs: MONITORING_BINDING_PORT=${{ secrets.MONITORING_BINDING_PORT }} MONITORING_PORT=${{ secrets.MONITORING_PORT }} MONITORING_BASE_PATH=${{ secrets.MONITORING_BASE_PATH }} + MONITORING_PROFILE=${{ secrets.MONITORING_PROFILE }} # Apply-form post URL generating format APPLY_POST_BASE_URL=${{ secrets.APPLY_POST_BASE_URL }} diff --git a/.github/workflows/be-cd_prod-docker.yml b/.github/workflows/be-cd_prod-docker.yml index c826655a2..c9e118a11 100644 --- a/.github/workflows/be-cd_prod-docker.yml +++ b/.github/workflows/be-cd_prod-docker.yml @@ -109,6 +109,7 @@ jobs: MONITORING_BINDING_PORT=${{ secrets.MONITORING_BINDING_PORT }} MONITORING_PORT=${{ secrets.MONITORING_PORT }} MONITORING_BASE_PATH=${{ secrets.MONITORING_BASE_PATH }} + MONITORING_PROFILE=${{ secrets.MONITORING_PROFILE }} # Apply configuration server info from Github secrets APPLY_POST_BASE_URL=${{ secrets.APPLY_POST_BASE_URL }} diff --git a/.github/workflows/be-cd_test-docker.yml b/.github/workflows/be-cd_test-docker.yml index 26b6a28a4..2f21acebb 100644 --- a/.github/workflows/be-cd_test-docker.yml +++ b/.github/workflows/be-cd_test-docker.yml @@ -100,6 +100,7 @@ jobs: MONITORING_BINDING_PORT=${{ secrets.MONITORING_BINDING_PORT }} MONITORING_PORT=${{ secrets.MONITORING_PORT }} MONITORING_BASE_PATH=${{ secrets.MONITORING_BASE_PATH }} + MONITORING_PROFILE=${{ secrets.MONITORING_PROFILE }} # Apply configuration server info from Github secrets APPLY_POST_BASE_URL=${{ secrets.APPLY_POST_BASE_URL }} diff --git a/backend/docker-compose.dev.yml b/backend/docker-compose.dev.yml index 380227fa1..270764bbd 100644 --- a/backend/docker-compose.dev.yml +++ b/backend/docker-compose.dev.yml @@ -43,6 +43,7 @@ services: environment: TZ: Asia/Seoul MONITORING_INSTANCE_ADDR_LOKI_PORT: ${MONITORING_INSTANCE_ADDR_LOKI_PORT} + MONITORING_PROFILE: ${MONITORING_PROFILE} container_name: promtail image: grafana/promtail:latest volumes: diff --git a/backend/docker-compose.prod.yml b/backend/docker-compose.prod.yml index 363390e87..59206e011 100644 --- a/backend/docker-compose.prod.yml +++ b/backend/docker-compose.prod.yml @@ -24,6 +24,7 @@ services: environment: TZ: Asia/Seoul MONITORING_INSTANCE_ADDR_LOKI_PORT: ${MONITORING_INSTANCE_ADDR_LOKI_PORT} + MONITORING_PROFILE: ${MONITORING_PROFILE} container_name: promtail image: grafana/promtail:latest volumes: diff --git a/backend/docker-compose.test.yml b/backend/docker-compose.test.yml index 97d469964..5a68ef540 100644 --- a/backend/docker-compose.test.yml +++ b/backend/docker-compose.test.yml @@ -24,6 +24,7 @@ services: environment: TZ: Asia/Seoul MONITORING_INSTANCE_ADDR_LOKI_PORT: ${MONITORING_INSTANCE_ADDR_LOKI_PORT} + MONITORING_PROFILE: ${MONITORING_PROFILE} container_name: promtail image: grafana/promtail:latest volumes: diff --git a/backend/promtail-config.yml b/backend/promtail-config.yml index 49870f4df..eb05d2437 100644 --- a/backend/promtail-config.yml +++ b/backend/promtail-config.yml @@ -14,7 +14,7 @@ scrape_configs: - targets: - localhost labels: - job: error_logs + job: ${MONITORING_PROFILE}_error_logs __path__: /log/error/*.log pipeline_stages: - json: @@ -22,39 +22,29 @@ scrape_configs: timestamp: timestamp level: level logger: logger - httpMethod: httpMethod + requestId: requestId requestUri: requestUri + requestMethod: requestMethod statusCode: statusCode sourceClass: sourceClass sourceMethod: sourceMethod - exceptionClass: exceptionClass - exceptionMessage: exceptionMessage - module: module + msg: message environment: environment - labels: level: - logger: - httpMethod: requestUri: - statusCode: - sourceClass: - sourceMethod: - exceptionClass: - exceptionMessage: - module: environment: - timestamp: source: timestamp format: 2006-01-02T15:04:05.000 location: "Asia/Seoul" - - job_name: info_logs static_configs: - targets: - localhost labels: - job: info_logs + job: ${MONITORING_PROFILE}_info_logs __path__: /log/info/*.log pipeline_stages: - json: @@ -62,26 +52,17 @@ scrape_configs: timestamp: timestamp level: level logger: logger - httpMethod: httpMethod + requestId: requestId requestUri: requestUri + requestMethod: requestMethod statusCode: statusCode sourceClass: sourceClass sourceMethod: sourceMethod - exceptionClass: exceptionClass - exceptionMessage: exceptionMessage - module: module + msg: message environment: environment - labels: level: - logger: - httpMethod: requestUri: - statusCode: - sourceClass: - sourceMethod: - exceptionClass: - exceptionMessage: - module: environment: - timestamp: source: timestamp @@ -94,7 +75,7 @@ scrape_configs: - targets: - localhost labels: - job: warn_logs + job: ${MONITORING_PROFILE}_warn_logs __path__: /log/warn/*.log pipeline_stages: - json: @@ -102,39 +83,19 @@ scrape_configs: timestamp: timestamp level: level logger: logger - httpMethod: httpMethod + requestId: requestId requestUri: requestUri + requestMethod: requestMethod statusCode: statusCode sourceClass: sourceClass sourceMethod: sourceMethod - exceptionClass: exceptionClass - exceptionMessage: exceptionMessage - module: module + msg: message environment: environment - labels: level: - logger: - httpMethod: requestUri: - statusCode: - sourceClass: - sourceMethod: - exceptionClass: - exceptionMessage: - module: environment: - timestamp: source: timestamp format: 2006-01-02T15:04:05.000 location: "Asia/Seoul" - - - job_name: http_logs - static_configs: - - targets: - - localhost - labels: - job: http_logs - __path__: /var/log/nginx/*.log - pipeline_stages: - - regex: - expression: '^(?P[^\s]+) - - \[(?P[^\]]+)\] "(?P[A-Z]+) (?P[^ ]+) HTTP/[^"]+" (?P\d+) (?P\d+)' diff --git a/backend/src/main/java/com/cruru/DataLoader.java b/backend/src/main/java/com/cruru/DataLoader.java index b886b6d13..8afc36496 100644 --- a/backend/src/main/java/com/cruru/DataLoader.java +++ b/backend/src/main/java/com/cruru/DataLoader.java @@ -2,7 +2,6 @@ import static com.cruru.question.domain.QuestionType.LONG_ANSWER; import static com.cruru.question.domain.QuestionType.MULTIPLE_CHOICE; -import static com.cruru.question.domain.QuestionType.SHORT_ANSWER; import static com.cruru.question.domain.QuestionType.SINGLE_CHOICE; import com.cruru.applicant.domain.Applicant; @@ -109,9 +108,6 @@ private void runDataLoader() { List applicants = List.of(lurgi, dobby, arrr, chocochip, myungoh, rush, nyangin, redpanda); applicantRepository.saveAll(applicants); - Question essayQuestion = questionRepository.save( - new Question(SHORT_ANSWER, "좋아하는 숫자가 무엇인가요?", 1, false, applyForm)); - Question question1 = questionRepository.save( new Question(LONG_ANSWER, "효과적인 학습 방식과 경험", 1, false, applyForm) ); diff --git a/backend/src/main/java/com/cruru/advice/CruruCustomException.java b/backend/src/main/java/com/cruru/advice/CruruCustomException.java index f983645fd..c89362d6e 100644 --- a/backend/src/main/java/com/cruru/advice/CruruCustomException.java +++ b/backend/src/main/java/com/cruru/advice/CruruCustomException.java @@ -12,8 +12,4 @@ public CruruCustomException(String message, HttpStatus status) { super(message); this.status = status; } - - public String statusCode() { - return status.toString(); - } } diff --git a/backend/src/main/java/com/cruru/advice/ExceptionCallback.java b/backend/src/main/java/com/cruru/advice/ExceptionCallback.java new file mode 100644 index 000000000..a47906b03 --- /dev/null +++ b/backend/src/main/java/com/cruru/advice/ExceptionCallback.java @@ -0,0 +1,7 @@ +package com.cruru.advice; + +@FunctionalInterface +public interface ExceptionCallback { + + void handleException(Exception e); +} diff --git a/backend/src/main/java/com/cruru/advice/GlobalExceptionHandler.java b/backend/src/main/java/com/cruru/advice/GlobalExceptionHandler.java index b9c0c8edf..89a6bede0 100644 --- a/backend/src/main/java/com/cruru/advice/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/cruru/advice/GlobalExceptionHandler.java @@ -1,76 +1,100 @@ package com.cruru.advice; -import com.cruru.global.util.ExceptionLogger; -import jakarta.servlet.http.HttpServletRequest; +import com.cruru.global.util.MdcUtils; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; -import java.util.Objects; -import lombok.RequiredArgsConstructor; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.http.ProblemDetail; import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +@Slf4j @RestControllerAdvice -@RequiredArgsConstructor -public class GlobalExceptionHandler { +public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { - private final UncatchedExceptionHandler handler; + private static final int STACK_TRACE_LIMIT = 5; @ExceptionHandler(CruruCustomException.class) - public ResponseEntity handleCustomException(CruruCustomException e) { - HttpServletRequest request = getCurrentHttpRequest(); - ExceptionLogger.info(request, e); - + public ResponseEntity handleCustomException(CruruCustomException e) { ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(e.getStatus(), e.getMessage()); - return ResponseEntity.of(problemDetail).build(); + + return handleExceptionWithMdc(e, problemDetail, + ex -> log.info("CRURU_EXCEPTION [errorMessage = {}]", ex.getMessage()) + ); } @ExceptionHandler(Exception.class) - public ResponseEntity handleUnexpectedException(Exception e, WebRequest request) { - ProblemDetail problemDetail = handleException(e, request); - HttpStatus statusCode = HttpStatus.valueOf(problemDetail.getStatus()); - if (statusCode.is5xxServerError()) { - ExceptionLogger.error(problemDetail); - } else { - ExceptionLogger.warn(problemDetail); - } - ProblemDetail detailsToSend = ProblemDetail.forStatus(statusCode); - return ResponseEntity.of(detailsToSend).build(); + public ResponseEntity handleUnexpectedException(Exception e) { + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류입니다."); + + return handleExceptionWithMdc(e, problemDetail, + ex -> log.error("SERVER_EXCEPTION [errorMessage = {}, stackTrace = {}]", + ex.getMessage(), + getLimitedStackTrace(ex)) + ); } - private ProblemDetail handleException(Exception e, WebRequest request) { - try { - ProblemDetail problemDetail = (ProblemDetail) Objects.requireNonNull(handler.handleException(e, request)) - .getBody(); - problemDetail.setProperties(setDetails(getCurrentHttpRequest(), e, HttpStatus.INTERNAL_SERVER_ERROR)); - return problemDetail; - } catch (Exception ex) { - return ProblemDetail.forStatusAndDetail( - HttpStatus.INTERNAL_SERVER_ERROR, - "예기치 못한 오류가 발생하였습니다." - ); + private String getLimitedStackTrace(Exception e) { + StackTraceElement[] stackTraceElements = e.getStackTrace(); + + return Arrays.stream(stackTraceElements) + .limit(STACK_TRACE_LIMIT) + .map(StackTraceElement::toString) + .collect(Collectors.joining()); + } + + @Override + protected ResponseEntity handleMethodArgumentNotValid( + MethodArgumentNotValidException e, + HttpHeaders headers, + HttpStatusCode status, + WebRequest request + ) { + Map validation = new HashMap<>(); + for (FieldError fieldError : e.getFieldErrors()) { + validation.put(fieldError.getField(), fieldError.getDefaultMessage()); } + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, validation.toString()); + + return handleExceptionWithMdc(e, problemDetail, + ex -> log.warn("METHOD_ARGUMENT_EXCEPTION [errorMessage = {}]", validation) + ); } - private HttpServletRequest getCurrentHttpRequest() { - return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); + @Override + protected ResponseEntity handleExceptionInternal( + Exception e, + Object body, + HttpHeaders headers, + HttpStatusCode statusCode, + WebRequest request + ) { + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(statusCode, "오류가 발생했습니다. 다시 시도해 주세요."); + + return handleExceptionWithMdc(e, problemDetail, + ex -> log.warn("SPRING_BASIC_EXCEPTION [errorMessage = {}]", ex.getMessage()) + ); } - private static Map setDetails(HttpServletRequest request, Exception exception, HttpStatus status) { - StackTraceElement origin = exception.getStackTrace()[0]; - Map map = new HashMap<>(); - map.put("httpMethod", request.getMethod()); - map.put("requestUri", request.getRequestURI()); - map.put("statusCode", status.toString()); - map.put("sourceClass", origin.getClassName()); - map.put("sourceMethod", origin.getMethodName()); - map.put("exceptionClass", exception.getClass().getSimpleName()); - map.put("exceptionMessage", exception.getMessage()); - return map; + private ResponseEntity handleExceptionWithMdc( + Exception e, + ProblemDetail problemDetail, + ExceptionCallback callback + ) { + MdcUtils.setMdcForException(e, problemDetail); + callback.handleException(e); + MDC.clear(); + return ResponseEntity.of(problemDetail).build(); } } diff --git a/backend/src/main/java/com/cruru/advice/UncatchedExceptionHandler.java b/backend/src/main/java/com/cruru/advice/UncatchedExceptionHandler.java deleted file mode 100644 index a667f5206..000000000 --- a/backend/src/main/java/com/cruru/advice/UncatchedExceptionHandler.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.cruru.advice; - -import java.util.HashMap; -import java.util.Map; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.HttpStatusCode; -import org.springframework.http.ProblemDetail; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Component; -import org.springframework.validation.FieldError; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.context.request.WebRequest; -import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; - -@Component -public class UncatchedExceptionHandler extends ResponseEntityExceptionHandler { - - @Override - protected ResponseEntity handleMethodArgumentNotValid( - MethodArgumentNotValidException e, - HttpHeaders headers, - HttpStatusCode status, - WebRequest request - ) { - Map validation = new HashMap<>(); - for (FieldError fieldError : e.getFieldErrors()) { - validation.put(fieldError.getField(), fieldError.getDefaultMessage()); - } - ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail( - HttpStatus.BAD_REQUEST, - validation.values().toString() - ); - return ResponseEntity.of(problemDetail).build(); - } -} diff --git a/backend/src/main/java/com/cruru/applicant/controller/ApplicantController.java b/backend/src/main/java/com/cruru/applicant/controller/ApplicantController.java index 98e6602af..9815620ab 100644 --- a/backend/src/main/java/com/cruru/applicant/controller/ApplicantController.java +++ b/backend/src/main/java/com/cruru/applicant/controller/ApplicantController.java @@ -7,7 +7,8 @@ import com.cruru.applicant.controller.response.ApplicantBasicResponse; import com.cruru.applicant.domain.Applicant; import com.cruru.applicant.facade.ApplicantFacade; -import com.cruru.auth.annotation.RequireAuthCheck; +import com.cruru.auth.annotation.RequireAuth; +import com.cruru.auth.annotation.ValidAuth; import com.cruru.global.LoginProfile; import com.cruru.process.domain.Process; import jakarta.validation.Valid; @@ -28,27 +29,30 @@ public class ApplicantController { private final ApplicantFacade applicantFacade; - @RequireAuthCheck(targetId = "applicantId", targetDomain = Applicant.class) @GetMapping("/{applicantId}") - public ResponseEntity read(@PathVariable Long applicantId, LoginProfile loginProfile) { + @ValidAuth + public ResponseEntity read( + @RequireAuth(targetDomain = Applicant.class) @PathVariable Long applicantId, + LoginProfile loginProfile + ) { ApplicantBasicResponse applicantResponse = applicantFacade.readBasicById(applicantId); return ResponseEntity.ok().body(applicantResponse); } - @RequireAuthCheck(targetId = "applicantId", targetDomain = Applicant.class) @GetMapping("/{applicantId}/detail") + @ValidAuth public ResponseEntity readDetail( - @PathVariable Long applicantId, + @RequireAuth(targetDomain = Applicant.class) @PathVariable Long applicantId, LoginProfile loginProfile ) { ApplicantAnswerResponses applicantAnswerResponses = applicantFacade.readDetailById(applicantId); return ResponseEntity.ok().body(applicantAnswerResponses); } - @RequireAuthCheck(targetId = "applicantId", targetDomain = Applicant.class) @PatchMapping("/{applicantId}") + @ValidAuth public ResponseEntity updateInformation( - @PathVariable Long applicantId, + @RequireAuth(targetDomain = Applicant.class) @PathVariable Long applicantId, @RequestBody @Valid ApplicantUpdateRequest request, LoginProfile loginProfile ) { @@ -56,10 +60,10 @@ public ResponseEntity updateInformation( return ResponseEntity.ok().build(); } - @RequireAuthCheck(targetId = "processId", targetDomain = Process.class) @PutMapping("/move-process/{processId}") + @ValidAuth public ResponseEntity updateProcess( - @PathVariable Long processId, + @RequireAuth(targetDomain = Process.class) @PathVariable Long processId, @RequestBody @Valid ApplicantMoveRequest moveRequest, LoginProfile loginProfile ) { @@ -67,30 +71,37 @@ public ResponseEntity updateProcess( return ResponseEntity.ok().build(); } - @RequireAuthCheck(targetId = "applicantId", targetDomain = Applicant.class) @PatchMapping("/{applicantId}/reject") - public ResponseEntity reject(@PathVariable Long applicantId, LoginProfile loginProfile) { + @ValidAuth + public ResponseEntity reject( + @RequireAuth(targetDomain = Applicant.class) @PathVariable Long applicantId, + LoginProfile loginProfile + ) { applicantFacade.reject(applicantId); return ResponseEntity.ok().build(); } - @RequireAuthCheck(targetId = "applicantId", targetDomain = Applicant.class) @PatchMapping("/{applicantId}/unreject") - public ResponseEntity unreject(@PathVariable Long applicantId, LoginProfile loginProfile) { + @ValidAuth + public ResponseEntity unreject( + @RequireAuth(targetDomain = Applicant.class) @PathVariable Long applicantId, + LoginProfile loginProfile + ) { applicantFacade.unreject(applicantId); return ResponseEntity.ok().build(); } @PatchMapping("/reject") + @ValidAuth public ResponseEntity reject(@RequestBody @Valid ApplicantsRejectRequest request, LoginProfile loginProfile) { applicantFacade.reject(request); return ResponseEntity.ok().build(); } @PatchMapping("/unreject") + @ValidAuth public ResponseEntity unreject( - @RequestBody @Valid ApplicantsRejectRequest request, LoginProfile loginProfile - ) { + @RequestBody @Valid ApplicantsRejectRequest request, LoginProfile loginProfile) { applicantFacade.unreject(request); return ResponseEntity.ok().build(); } diff --git a/backend/src/main/java/com/cruru/applicant/controller/EvaluationController.java b/backend/src/main/java/com/cruru/applicant/controller/EvaluationController.java index 91ead6d62..59b4c87a6 100644 --- a/backend/src/main/java/com/cruru/applicant/controller/EvaluationController.java +++ b/backend/src/main/java/com/cruru/applicant/controller/EvaluationController.java @@ -3,9 +3,11 @@ import com.cruru.applicant.controller.request.EvaluationCreateRequest; import com.cruru.applicant.controller.request.EvaluationUpdateRequest; import com.cruru.applicant.controller.response.EvaluationResponses; +import com.cruru.applicant.domain.Applicant; import com.cruru.applicant.domain.Evaluation; import com.cruru.applicant.facade.EvaluationFacade; -import com.cruru.auth.annotation.RequireAuthCheck; +import com.cruru.auth.annotation.RequireAuth; +import com.cruru.auth.annotation.ValidAuth; import com.cruru.global.LoginProfile; import com.cruru.process.domain.Process; import jakarta.validation.Valid; @@ -28,12 +30,12 @@ public class EvaluationController { private final EvaluationFacade evaluationFacade; - @RequireAuthCheck(targetId = "processId", targetDomain = Process.class) @PostMapping + @ValidAuth public ResponseEntity create( @RequestBody @Valid EvaluationCreateRequest request, - @RequestParam(name = "processId") Long processId, - @RequestParam(name = "applicantId") Long applicantId, + @RequireAuth(targetDomain = Process.class) @RequestParam(name = "processId") Long processId, + @RequireAuth(targetDomain = Applicant.class) @RequestParam(name = "applicantId") Long applicantId, LoginProfile loginProfile ) { evaluationFacade.create(request, processId, applicantId); @@ -41,22 +43,22 @@ public ResponseEntity create( return ResponseEntity.created(URI.create(url)).build(); } - @RequireAuthCheck(targetId = "processId", targetDomain = Process.class) @GetMapping + @ValidAuth public ResponseEntity read( - @RequestParam(name = "processId") Long processId, - @RequestParam(name = "applicantId") Long applicantId, + @RequireAuth(targetDomain = Process.class) @RequestParam(name = "processId") Long processId, + @RequireAuth(targetDomain = Applicant.class) @RequestParam(name = "applicantId") Long applicantId, LoginProfile loginProfile ) { EvaluationResponses response = evaluationFacade.readEvaluationsOfApplicantInProcess(processId, applicantId); return ResponseEntity.ok(response); } - @RequireAuthCheck(targetId = "evaluationId", targetDomain = Evaluation.class) @PatchMapping("/{evaluationId}") + @ValidAuth public ResponseEntity update( @RequestBody @Valid EvaluationUpdateRequest request, - @PathVariable Long evaluationId, + @RequireAuth(targetDomain = Evaluation.class) @PathVariable Long evaluationId, LoginProfile loginProfile ) { evaluationFacade.updateSingleEvaluation(request, evaluationId); diff --git a/backend/src/main/java/com/cruru/applicant/controller/request/ApplicantMoveRequest.java b/backend/src/main/java/com/cruru/applicant/controller/request/ApplicantMoveRequest.java index 98cc53154..b11358209 100644 --- a/backend/src/main/java/com/cruru/applicant/controller/request/ApplicantMoveRequest.java +++ b/backend/src/main/java/com/cruru/applicant/controller/request/ApplicantMoveRequest.java @@ -1,9 +1,13 @@ package com.cruru.applicant.controller.request; +import com.cruru.applicant.domain.Applicant; +import com.cruru.auth.annotation.RequireAuth; import jakarta.validation.constraints.NotNull; import java.util.List; public record ApplicantMoveRequest( + + @RequireAuth(targetDomain = Applicant.class) @NotNull(message = "지원자 목록은 필수 값입니다.") List applicantIds ) { diff --git a/backend/src/main/java/com/cruru/applicant/controller/request/ApplicantsRejectRequest.java b/backend/src/main/java/com/cruru/applicant/controller/request/ApplicantsRejectRequest.java index ab9028384..6a5c6ecad 100644 --- a/backend/src/main/java/com/cruru/applicant/controller/request/ApplicantsRejectRequest.java +++ b/backend/src/main/java/com/cruru/applicant/controller/request/ApplicantsRejectRequest.java @@ -1,9 +1,13 @@ package com.cruru.applicant.controller.request; +import com.cruru.applicant.domain.Applicant; +import com.cruru.auth.annotation.RequireAuth; import jakarta.validation.constraints.NotNull; import java.util.List; public record ApplicantsRejectRequest( + + @RequireAuth(targetDomain = Applicant.class) @NotNull(message = "지원자 목록은 필수 값입니다.") List applicantIds ) { diff --git a/backend/src/main/java/com/cruru/applicant/domain/repository/ApplicantRepository.java b/backend/src/main/java/com/cruru/applicant/domain/repository/ApplicantRepository.java index 917aae6b9..7545cf1c1 100644 --- a/backend/src/main/java/com/cruru/applicant/domain/repository/ApplicantRepository.java +++ b/backend/src/main/java/com/cruru/applicant/domain/repository/ApplicantRepository.java @@ -19,7 +19,7 @@ public interface ApplicantRepository extends JpaRepository { @EntityGraph(attributePaths = {"process.dashboard.club.member"}) @Query("SELECT a FROM Applicant a WHERE a.id = :id") - Optional findByIdFetchingMember(long id); + Optional findByIdFetchingMember(Long id); long countByProcess(Process process); diff --git a/backend/src/main/java/com/cruru/applicant/domain/repository/EvaluationRepository.java b/backend/src/main/java/com/cruru/applicant/domain/repository/EvaluationRepository.java index a30aa0d4e..9c12d0d34 100644 --- a/backend/src/main/java/com/cruru/applicant/domain/repository/EvaluationRepository.java +++ b/backend/src/main/java/com/cruru/applicant/domain/repository/EvaluationRepository.java @@ -20,7 +20,7 @@ public interface EvaluationRepository extends JpaRepository { @EntityGraph(attributePaths = {"process.dashboard.club.member"}) @Query("SELECT e FROM Evaluation e WHERE e.id = :id") - Optional findByIdFetchingMember(long id); + Optional findByIdFetchingMember(Long id); @Modifying(clearAutomatically = true, flushAutomatically = true) @Transactional diff --git a/backend/src/main/java/com/cruru/applicant/service/ApplicantService.java b/backend/src/main/java/com/cruru/applicant/service/ApplicantService.java index 827eacd57..6a97e412a 100644 --- a/backend/src/main/java/com/cruru/applicant/service/ApplicantService.java +++ b/backend/src/main/java/com/cruru/applicant/service/ApplicantService.java @@ -47,11 +47,6 @@ public Applicant findById(Long applicantId) { .orElseThrow(ApplicantNotFoundException::new); } - public Applicant findByIdFetchingMember(Long applicantId) { - return applicantRepository.findByIdFetchingMember(applicantId) - .orElseThrow(ApplicantNotFoundException::new); - } - private boolean changeExists(ApplicantUpdateRequest request, Applicant applicant) { return !(applicant.getName().equals(request.name()) && applicant.getEmail().equals(request.email()) diff --git a/backend/src/main/java/com/cruru/applicant/service/EvaluationService.java b/backend/src/main/java/com/cruru/applicant/service/EvaluationService.java index f6b138824..ef4b43600 100644 --- a/backend/src/main/java/com/cruru/applicant/service/EvaluationService.java +++ b/backend/src/main/java/com/cruru/applicant/service/EvaluationService.java @@ -24,11 +24,6 @@ public Evaluation findById(Long evaluationId) { .orElseThrow(EvaluationNotFoundException::new); } - public Evaluation findByIdFetchingMember(Long evaluationId) { - return evaluationRepository.findByIdFetchingMember(evaluationId) - .orElseThrow(EvaluationNotFoundException::new); - } - @Transactional public void create(EvaluationCreateRequest request, Process process, Applicant applicant) { evaluationRepository.save(new Evaluation(request.score(), request.content(), process, applicant)); diff --git a/backend/src/main/java/com/cruru/applyform/controller/ApplyFormController.java b/backend/src/main/java/com/cruru/applyform/controller/ApplyFormController.java index d9769ab86..4731fa767 100644 --- a/backend/src/main/java/com/cruru/applyform/controller/ApplyFormController.java +++ b/backend/src/main/java/com/cruru/applyform/controller/ApplyFormController.java @@ -3,7 +3,11 @@ import com.cruru.applyform.controller.request.ApplyFormSubmitRequest; import com.cruru.applyform.controller.request.ApplyFormWriteRequest; import com.cruru.applyform.controller.response.ApplyFormResponse; +import com.cruru.applyform.domain.ApplyForm; import com.cruru.applyform.facade.ApplyFormFacade; +import com.cruru.auth.annotation.RequireAuth; +import com.cruru.auth.annotation.ValidAuth; +import com.cruru.global.LoginProfile; import jakarta.validation.Valid; import java.net.URI; import lombok.RequiredArgsConstructor; @@ -39,9 +43,11 @@ public ResponseEntity read(@PathVariable("applyformId") long } @PatchMapping("/{applyformId}") + @ValidAuth public ResponseEntity update( @RequestBody @Valid ApplyFormWriteRequest request, - @PathVariable("applyformId") Long applyFormId + @RequireAuth(targetDomain = ApplyForm.class) @PathVariable("applyformId") Long applyFormId, + LoginProfile loginProfile ) { applyFormFacade.update(request, applyFormId); return ResponseEntity.ok().build(); diff --git a/backend/src/main/java/com/cruru/applyform/domain/repository/ApplyFormRepository.java b/backend/src/main/java/com/cruru/applyform/domain/repository/ApplyFormRepository.java index 633791ee9..81ba9e1b9 100644 --- a/backend/src/main/java/com/cruru/applyform/domain/repository/ApplyFormRepository.java +++ b/backend/src/main/java/com/cruru/applyform/domain/repository/ApplyFormRepository.java @@ -14,7 +14,7 @@ public interface ApplyFormRepository extends JpaRepository { @EntityGraph(attributePaths = {"dashboard.club.member"}) @Query("SELECT a FROM ApplyForm a WHERE a.id = :id") - Optional findByIdFetchingMember(long id); + Optional findByIdFetchingMember(Long id); Optional findByDashboard(Dashboard dashboard); diff --git a/backend/src/main/java/com/cruru/applyform/service/ApplyFormService.java b/backend/src/main/java/com/cruru/applyform/service/ApplyFormService.java index cfaab5ee8..c81936a3a 100644 --- a/backend/src/main/java/com/cruru/applyform/service/ApplyFormService.java +++ b/backend/src/main/java/com/cruru/applyform/service/ApplyFormService.java @@ -77,11 +77,6 @@ public ApplyForm findById(Long applyFormId) { .orElseThrow(ApplyFormNotFoundException::new); } - public ApplyForm findByIdFetchingMember(Long applyFormId) { - return applyFormRepository.findByIdFetchingMember(applyFormId) - .orElseThrow(ApplyFormNotFoundException::new); - } - public ApplyForm findByDashboardId(Long dashboardId) { return applyFormRepository.findByDashboardId(dashboardId) .orElseThrow(ApplyFormNotFoundException::new); diff --git a/backend/src/main/java/com/cruru/auth/annotation/RequireAuthCheck.java b/backend/src/main/java/com/cruru/auth/annotation/RequireAuth.java similarity index 67% rename from backend/src/main/java/com/cruru/auth/annotation/RequireAuthCheck.java rename to backend/src/main/java/com/cruru/auth/annotation/RequireAuth.java index 7a948df13..cdc3894c4 100644 --- a/backend/src/main/java/com/cruru/auth/annotation/RequireAuthCheck.java +++ b/backend/src/main/java/com/cruru/auth/annotation/RequireAuth.java @@ -6,11 +6,9 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -@Target(ElementType.METHOD) +@Target({ElementType.FIELD,ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) -public @interface RequireAuthCheck { - - String targetId(); // 권한을 확인할 대상 리소스 (예: "clubId", "dashboardId") +public @interface RequireAuth { Class targetDomain(); } diff --git a/backend/src/main/java/com/cruru/auth/annotation/ValidAuth.java b/backend/src/main/java/com/cruru/auth/annotation/ValidAuth.java new file mode 100644 index 000000000..5111638fb --- /dev/null +++ b/backend/src/main/java/com/cruru/auth/annotation/ValidAuth.java @@ -0,0 +1,12 @@ +package com.cruru.auth.annotation; + +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 ValidAuth { + +} diff --git a/backend/src/main/java/com/cruru/auth/aspect/AuthCheckAspect.java b/backend/src/main/java/com/cruru/auth/aspect/AuthCheckAspect.java deleted file mode 100644 index 65e2b89cd..000000000 --- a/backend/src/main/java/com/cruru/auth/aspect/AuthCheckAspect.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.cruru.auth.aspect; - -import com.cruru.auth.annotation.RequireAuthCheck; -import com.cruru.auth.util.AuthChecker; -import com.cruru.auth.util.SecureResource; -import com.cruru.global.LoginProfile; -import com.cruru.member.domain.Member; -import com.cruru.member.service.MemberService; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.Optional; -import java.util.stream.IntStream; -import lombok.RequiredArgsConstructor; -import org.aspectj.lang.JoinPoint; -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.Before; -import org.aspectj.lang.reflect.MethodSignature; -import org.hibernate.Hibernate; -import org.springframework.context.ApplicationContext; -import org.springframework.stereotype.Component; - -@Aspect -@Component -@RequiredArgsConstructor -public class AuthCheckAspect { - - private static final String SERVICE_IDENTIFIER = "Service"; - - private final ApplicationContext applicationContext; // 서비스 빈을 동적으로 가져오기 위해 ApplicationContext 사용 - private final MemberService memberService; - - @Before("@annotation(com.cruru.auth.annotation.RequireAuthCheck)") - public void checkAuthorization(JoinPoint joinPoint) throws Throwable { - MethodSignature signature = (MethodSignature) joinPoint.getSignature(); - Method method = signature.getMethod(); - RequireAuthCheck authCheck = method.getAnnotation(RequireAuthCheck.class); - - Object[] args = joinPoint.getArgs(); // 메서드의 실제 인자 값 - String[] parameterNames = signature.getParameterNames(); // 메서드 파라미터 이름들 - - LoginProfile loginProfile = extractLoginProfile(parameterNames, args); - Long targetId = extractTargetId(parameterNames, args, authCheck.targetId()); - - Class domainClass = authCheck.targetDomain(); - authorize(domainClass, targetId, loginProfile); - } - - private LoginProfile extractLoginProfile(String[] parameterNames, Object[] args) { - return findParameterByName(parameterNames, args, "loginProfile", LoginProfile.class) - .orElseThrow(() -> new IllegalArgumentException("loginProfile가 존재하지 않습니다.")); - } - - private Long extractTargetId(String[] parameterNames, Object[] args, String targetIdParamName) { - return findParameterByName(parameterNames, args, targetIdParamName, Long.class) - .orElseThrow(() -> new IllegalArgumentException("targetId가 존재하지 않습니다.")); - } - - // 리소스에 대한 권한 검사 로직 분리 - private void authorize( - Class domainClass, - Long targetId, - LoginProfile loginProfile - ) throws Throwable { - try { - checkAuthorizationForTarget(domainClass, targetId, loginProfile); - } catch (InvocationTargetException e) { - throw e.getCause(); - } catch (ReflectiveOperationException e) { - throw new IllegalArgumentException(domainClass + ": Service 또는 findById Method가 존재하지 않습니다."); - } - } - - // 파라미터 이름과 값을 기반으로 원하는 타입의 파라미터 추출 - private Optional findParameterByName( - String[] parameterNames, - Object[] args, - String targetParamName, - Class type - ) { - return IntStream.range(0, parameterNames.length) - .filter(i -> parameterNames[i].equals(targetParamName) && type.isInstance(args[i])) - .mapToObj(i -> type.cast(args[i])) - .findFirst(); - } - - // targetDomain에 따른 권한 검사 수행 - private void checkAuthorizationForTarget( - Class targetDomain, - Long targetId, - LoginProfile loginProfile - ) throws Exception { - Member member = memberService.findByEmail(loginProfile.email()); - - // 도메인 이름을 기반으로 서비스 클래스의 이름을 동적으로 생성 - String targetDomainName = targetDomain.getSimpleName(); - String serviceName = - Character.toLowerCase(targetDomainName.charAt(0)) + targetDomainName.substring(1) + SERVICE_IDENTIFIER; - - // ApplicationContext를 통해 해당 서비스 빈을 동적으로 가져옴 - Object service = applicationContext.getBean(serviceName); - - // findById 메서드를 호출하여 해당 도메인 객체(SecureResource)를 가져옴 - Method findByIdMethod = service.getClass().getMethod("findByIdFetchingMember", Long.class); - SecureResource secureResource = (SecureResource) findByIdMethod.invoke(service, targetId); - - // Lazy Loading된 연관 엔티티를 강제 로딩함 - Hibernate.initialize(secureResource); - - AuthChecker.checkAuthority(secureResource, member); - } -} diff --git a/backend/src/main/java/com/cruru/auth/aspect/AuthValidationAspect.java b/backend/src/main/java/com/cruru/auth/aspect/AuthValidationAspect.java new file mode 100644 index 000000000..206160e1e --- /dev/null +++ b/backend/src/main/java/com/cruru/auth/aspect/AuthValidationAspect.java @@ -0,0 +1,159 @@ +package com.cruru.auth.aspect; + +import com.cruru.advice.NotFoundException; +import com.cruru.auth.annotation.RequireAuth; +import com.cruru.auth.util.AuthChecker; +import com.cruru.auth.util.SecureResource; +import com.cruru.global.LoginProfile; +import com.cruru.member.domain.Member; +import com.cruru.member.service.MemberService; +import java.lang.reflect.Field; +import java.lang.reflect.InaccessibleObjectException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Collection; +import java.util.Deque; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import lombok.RequiredArgsConstructor; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Aspect +@Component +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AuthValidationAspect { + + private static final String REPOSITORY_SUFFIX = "Repository"; + private static final String REPOSITORY_METHOD_NAME = "findByIdFetchingMember"; + private static final Map, Field[]> FIELD_CACHE = new ConcurrentHashMap<>(); + + private final ApplicationContext applicationContext; + private final MemberService memberService; + + @Before("@annotation(com.cruru.auth.annotation.ValidAuth) && within(com.cruru..controller..*)") + public void checkAuthorization(JoinPoint joinPoint) { + Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); + Object[] controllerArgs = joinPoint.getArgs(); + LoginProfile loginProfile = extractLoginProfile(controllerArgs); + + for (int i = 0; i < controllerArgs.length; i++) { + if (controllerArgs[i] == null) { + continue; + } + + Parameter parameter = method.getParameters()[i]; + Object arg = controllerArgs[i]; + + if (parameter.isAnnotationPresent(RequireAuth.class) && arg instanceof Long targetId) { + authorize(loginProfile, parameter.getAnnotation(RequireAuth.class).targetDomain(), targetId); + } else if (!(arg instanceof Long)) { + processDtoFields(loginProfile, arg); + } + } + } + + private LoginProfile extractLoginProfile(Object[] args) { + return Arrays.stream(args) + .filter(LoginProfile.class::isInstance) + .map(LoginProfile.class::cast) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("LoginProfile이 존재하지 않습니다.")); + } + + private void processDtoFields(LoginProfile loginProfile, Object dto) { + Deque dtoStack = new ArrayDeque<>(); + dtoStack.push(dto); + + while (!dtoStack.isEmpty()) { + Object currentDto = dtoStack.pop(); + for (Field field : retrieveFields(currentDto.getClass())) { + Object fieldValue = safelyGetFieldValue(field, currentDto); + if (fieldValue == null) { + continue; + } + processField(loginProfile, field, fieldValue); + } + } + } + + private Field[] retrieveFields(Class clazz) { + return FIELD_CACHE.computeIfAbsent(clazz, c -> { + Field[] declaredFields = c.getDeclaredFields(); + Arrays.stream(declaredFields) + .forEach(field -> { + try { + field.setAccessible(true); + } catch (InaccessibleObjectException ignored) { + // Java 기본 클래스 접근시 발생 예외 무시 + } + }); + return declaredFields; + }); + } + + private Object safelyGetFieldValue(Field field, Object obj) { + try { + return field.get(obj); + } catch (IllegalAccessException ignored) { + return null; + } + } + + private void processField(LoginProfile loginProfile, Field field, Object fieldValue) { + if (field.isAnnotationPresent(RequireAuth.class)) { + RequireAuth requireAuth = field.getAnnotation(RequireAuth.class); + authorizeField(loginProfile, requireAuth, fieldValue); + } + } + + private void authorizeField(LoginProfile loginProfile, RequireAuth requireAuth, Object fieldValue) { + if (fieldValue instanceof Long targetId) { + authorize(loginProfile, requireAuth.targetDomain(), targetId); + return; + } + if (fieldValue instanceof Collection collection) { + collection.stream() + .filter(Long.class::isInstance) + .map(Long.class::cast) + .forEach(id -> authorize(loginProfile, requireAuth.targetDomain(), id)); + } + } + + private void authorize(LoginProfile loginProfile, Class domainClass, Long targetId) { + try { + Member member = memberService.findByEmail(loginProfile.email()); + String domainName = domainClass.getSimpleName(); + String repositoryName = getServiceName(domainName); + Object repository = applicationContext.getBean(repositoryName); + Method findByIdFetchingMember = repository.getClass().getMethod(REPOSITORY_METHOD_NAME, Long.class); + Optional resourceOpt = (Optional) findByIdFetchingMember.invoke( + repository, + targetId + ); + + resourceOpt.ifPresentOrElse( + resource -> AuthChecker.checkAuthority(resource, member), + () -> { + throw new NotFoundException(domainClass.getSimpleName()); + } + ); + } catch (ReflectiveOperationException e) { + throw new IllegalArgumentException( + domainClass.getSimpleName() + ": Repository 또는 findByIdFetchingMember Method가 존재하지 않습니다."); + } + } + + private String getServiceName(String domainName) { + return Character.toLowerCase(domainName.charAt(0)) + domainName.substring(1) + REPOSITORY_SUFFIX; + } +} diff --git a/backend/src/main/java/com/cruru/club/domain/repository/ClubRepository.java b/backend/src/main/java/com/cruru/club/domain/repository/ClubRepository.java index 812475372..cded1ea1c 100644 --- a/backend/src/main/java/com/cruru/club/domain/repository/ClubRepository.java +++ b/backend/src/main/java/com/cruru/club/domain/repository/ClubRepository.java @@ -13,5 +13,5 @@ public interface ClubRepository extends JpaRepository { @EntityGraph(attributePaths = "member") @Query("SELECT c FROM Club c WHERE c.id = :id") - Optional findByIdFetchingMember(long id); + Optional findByIdFetchingMember(Long id); } diff --git a/backend/src/main/java/com/cruru/club/service/ClubService.java b/backend/src/main/java/com/cruru/club/service/ClubService.java index 0037c0043..56f950fbe 100644 --- a/backend/src/main/java/com/cruru/club/service/ClubService.java +++ b/backend/src/main/java/com/cruru/club/service/ClubService.java @@ -35,9 +35,4 @@ public Club findByMember(Member member) { return clubRepository.findByMember(member) .orElseThrow(ClubNotFoundException::new); } - - public Club findByIdFetchingMember(Long id) { - return clubRepository.findByIdFetchingMember(id) - .orElseThrow(ClubNotFoundException::new); - } } diff --git a/backend/src/main/java/com/cruru/dashboard/controller/DashboardController.java b/backend/src/main/java/com/cruru/dashboard/controller/DashboardController.java index 593d07020..f84d88b7e 100644 --- a/backend/src/main/java/com/cruru/dashboard/controller/DashboardController.java +++ b/backend/src/main/java/com/cruru/dashboard/controller/DashboardController.java @@ -1,6 +1,7 @@ package com.cruru.dashboard.controller; -import com.cruru.auth.annotation.RequireAuthCheck; +import com.cruru.auth.annotation.RequireAuth; +import com.cruru.auth.annotation.ValidAuth; import com.cruru.club.domain.Club; import com.cruru.dashboard.controller.request.DashboardCreateRequest; import com.cruru.dashboard.controller.response.DashboardCreateResponse; @@ -29,22 +30,21 @@ public class DashboardController { private final DashboardFacade dashboardFacade; @PostMapping - @RequireAuthCheck(targetId = "clubId", targetDomain = Club.class) + @ValidAuth public ResponseEntity create( - @RequestParam(name = "clubId") Long clubId, + @RequireAuth(targetDomain = Club.class) @RequestParam(name = "clubId") Long clubId, @RequestBody @Valid DashboardCreateRequest request, LoginProfile loginProfile ) { - DashboardCreateResponse dashboardCreateResponse = dashboardFacade.create(clubId, request); return ResponseEntity.created(URI.create("/v1/dashboards/" + dashboardCreateResponse.dashboardId())) .body(dashboardCreateResponse); } @GetMapping - @RequireAuthCheck(targetId = "clubId", targetDomain = Club.class) + @ValidAuth public ResponseEntity readDashboards( - @RequestParam(name = "clubId") Long clubId, + @RequireAuth(targetDomain = Club.class) @RequestParam(name = "clubId") Long clubId, LoginProfile loginProfile ) { DashboardsOfClubResponse dashboards = dashboardFacade.findAllDashboardsByClubId(clubId); @@ -52,8 +52,11 @@ public ResponseEntity readDashboards( } @DeleteMapping("/{dashboardId}") - @RequireAuthCheck(targetId = "dashboardId", targetDomain = Dashboard.class) - public ResponseEntity delete(@PathVariable Long dashboardId, LoginProfile loginProfile) { + @ValidAuth + public ResponseEntity delete( + @RequireAuth(targetDomain = Dashboard.class) @PathVariable Long dashboardId, + LoginProfile loginProfile + ) { dashboardFacade.delete(dashboardId); return ResponseEntity.noContent().build(); } diff --git a/backend/src/main/java/com/cruru/dashboard/domain/repository/DashboardRepository.java b/backend/src/main/java/com/cruru/dashboard/domain/repository/DashboardRepository.java index 11531d63f..46d46ae53 100644 --- a/backend/src/main/java/com/cruru/dashboard/domain/repository/DashboardRepository.java +++ b/backend/src/main/java/com/cruru/dashboard/domain/repository/DashboardRepository.java @@ -10,5 +10,5 @@ public interface DashboardRepository extends JpaRepository { @EntityGraph(attributePaths = {"club.member"}) @Query("SELECT d FROM Dashboard d WHERE d.id = :id") - Optional findByIdFetchingMember(long id); + Optional findByIdFetchingMember(Long id); } diff --git a/backend/src/main/java/com/cruru/dashboard/service/DashboardService.java b/backend/src/main/java/com/cruru/dashboard/service/DashboardService.java index 91922e9ac..5af332de4 100644 --- a/backend/src/main/java/com/cruru/dashboard/service/DashboardService.java +++ b/backend/src/main/java/com/cruru/dashboard/service/DashboardService.java @@ -41,11 +41,6 @@ public Dashboard findById(Long id) { .orElseThrow(DashboardNotFoundException::new); } - public Dashboard findByIdFetchingMember(Long id) { - return dashboardRepository.findByIdFetchingMember(id) - .orElseThrow(DashboardNotFoundException::new); - } - public List findAllByClub(long clubId) { return applyFormRepository.findAllByClub(clubId); } diff --git a/backend/src/main/java/com/cruru/email/controller/EmailController.java b/backend/src/main/java/com/cruru/email/controller/EmailController.java index 181f0931b..8f624b8f8 100644 --- a/backend/src/main/java/com/cruru/email/controller/EmailController.java +++ b/backend/src/main/java/com/cruru/email/controller/EmailController.java @@ -1,7 +1,9 @@ package com.cruru.email.controller; +import com.cruru.auth.annotation.ValidAuth; import com.cruru.email.controller.dto.EmailRequest; import com.cruru.email.facade.EmailFacade; +import com.cruru.global.LoginProfile; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -18,7 +20,11 @@ public class EmailController { private final EmailFacade emailFacade; @PostMapping("/send") - public ResponseEntity send(@Valid @ModelAttribute EmailRequest request) { + @ValidAuth + public ResponseEntity send( + @Valid @ModelAttribute EmailRequest request, + LoginProfile loginProfile + ) { emailFacade.send(request); return ResponseEntity.ok().build(); } diff --git a/backend/src/main/java/com/cruru/email/controller/dto/EmailRequest.java b/backend/src/main/java/com/cruru/email/controller/dto/EmailRequest.java index 6d82b6563..946b29d09 100644 --- a/backend/src/main/java/com/cruru/email/controller/dto/EmailRequest.java +++ b/backend/src/main/java/com/cruru/email/controller/dto/EmailRequest.java @@ -1,5 +1,8 @@ package com.cruru.email.controller.dto; +import com.cruru.applicant.domain.Applicant; +import com.cruru.auth.annotation.RequireAuth; +import com.cruru.club.domain.Club; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @@ -9,10 +12,12 @@ public record EmailRequest( @NotNull(message = "발신자는 필수 값입니다.") + @RequireAuth(targetDomain = Club.class) Long clubId, @NotEmpty(message = "수신자는 필수 값입니다.") @Valid + @RequireAuth(targetDomain = Applicant.class) List<@NotNull(message = "수신자는 필수 값입니다.") Long> applicantIds, @NotNull(message = "이메일 제목은 필수 값입니다.") diff --git a/backend/src/main/java/com/cruru/global/LoginArgumentResolver.java b/backend/src/main/java/com/cruru/global/LoginArgumentResolver.java index 5a7e7980d..ceca58810 100644 --- a/backend/src/main/java/com/cruru/global/LoginArgumentResolver.java +++ b/backend/src/main/java/com/cruru/global/LoginArgumentResolver.java @@ -1,7 +1,7 @@ package com.cruru.global; import com.cruru.advice.UnauthorizedException; -import com.cruru.auth.annotation.RequireAuthCheck; +import com.cruru.auth.annotation.ValidAuth; import com.cruru.auth.service.AuthService; import com.cruru.global.util.CookieManager; import com.cruru.member.domain.MemberRole; @@ -22,7 +22,7 @@ public class LoginArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { - return Objects.requireNonNull(parameter.getMethod()).isAnnotationPresent(RequireAuthCheck.class); + return Objects.requireNonNull(parameter.getMethod()).isAnnotationPresent(ValidAuth.class); } @Override diff --git a/backend/src/main/java/com/cruru/global/util/ExceptionLogger.java b/backend/src/main/java/com/cruru/global/util/ExceptionLogger.java deleted file mode 100644 index faae85b48..000000000 --- a/backend/src/main/java/com/cruru/global/util/ExceptionLogger.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.cruru.global.util; - -import com.cruru.advice.CruruCustomException; -import jakarta.servlet.http.HttpServletRequest; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.slf4j.MDC; -import org.springframework.http.ProblemDetail; - -@Slf4j -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class ExceptionLogger { - - public static void info(HttpServletRequest request, CruruCustomException exception) { - setMDC(request, exception); - log.info("handle info level exception"); - clearMDC(); - } - - // MDC에 메타데이터 설정 - private static void setMDC(HttpServletRequest request, CruruCustomException exception) { - StackTraceElement origin = exception.getStackTrace()[0]; - - MDC.put("httpMethod", request.getMethod()); - MDC.put("requestUri", request.getRequestURI()); - MDC.put("statusCode", exception.statusCode()); - MDC.put("sourceClass", origin.getClassName()); - MDC.put("sourceMethod", origin.getMethodName()); - MDC.put("exceptionClass", exception.getClass().getSimpleName()); - MDC.put("exceptionMessage", exception.getMessage()); - } - - private static void clearMDC() { - MDC.clear(); - } - - // MDC 초기화 - public static void info(ProblemDetail problemDetail) { - setMDC(problemDetail); - log.info("handle info level exception"); - clearMDC(); - } - - private static void setMDC(ProblemDetail problemDetail) { - Map details = problemDetail.getProperties(); - if (details == null || details.isEmpty()) { - return; - } - Map map = new HashMap<>(); - for (Entry stringObjectEntry : details.entrySet()) { - map.put(stringObjectEntry.getKey(), java.lang.String.valueOf(stringObjectEntry.getValue())); - } - MDC.setContextMap(map); - } - - public static void warn(ProblemDetail problemDetail) { - setMDC(problemDetail); - log.warn("handle warn level exception"); - clearMDC(); - } - - public static void error(ProblemDetail problemDetail) { - setMDC(problemDetail); - log.error("handle error level exception"); - clearMDC(); - } -} diff --git a/backend/src/main/java/com/cruru/global/util/MdcUtils.java b/backend/src/main/java/com/cruru/global/util/MdcUtils.java new file mode 100644 index 000000000..150e78853 --- /dev/null +++ b/backend/src/main/java/com/cruru/global/util/MdcUtils.java @@ -0,0 +1,48 @@ +package com.cruru.global.util; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.Objects; +import java.util.UUID; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.slf4j.MDC; +import org.springframework.http.ProblemDetail; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MdcUtils { + + public static void setMdcForException(Exception e, ProblemDetail problemDetail) { + setRequestId(); + setRequest(); + setStatusCode(problemDetail); + setSource(e); + } + + private static void setRequestId() { + String requestId = UUID.randomUUID().toString(); + MDC.put("requestId", requestId); + } + + private static void setRequest() { + HttpServletRequest request = getCurrentHttpRequest(); + MDC.put("requestUri", request.getRequestURI()); + MDC.put("requestMethod", request.getMethod()); + } + + private static void setStatusCode(ProblemDetail problemDetail) { + MDC.put("statusCode", String.valueOf(problemDetail.getStatus())); + } + + private static void setSource(Exception e) { + StackTraceElement source = e.getStackTrace()[0]; + MDC.put("sourceClass", source.getClassName()); + MDC.put("sourceMethod", source.getMethodName()); + } + + private static HttpServletRequest getCurrentHttpRequest() { + return ((ServletRequestAttributes) Objects.requireNonNull( + RequestContextHolder.getRequestAttributes())).getRequest(); + } +} diff --git a/backend/src/main/java/com/cruru/process/controller/ProcessController.java b/backend/src/main/java/com/cruru/process/controller/ProcessController.java index 3aa79c61e..1d29db47c 100644 --- a/backend/src/main/java/com/cruru/process/controller/ProcessController.java +++ b/backend/src/main/java/com/cruru/process/controller/ProcessController.java @@ -1,6 +1,7 @@ package com.cruru.process.controller; -import com.cruru.auth.annotation.RequireAuthCheck; +import com.cruru.auth.annotation.RequireAuth; +import com.cruru.auth.annotation.ValidAuth; import com.cruru.dashboard.domain.Dashboard; import com.cruru.global.LoginProfile; import com.cruru.process.controller.request.ProcessCreateRequest; @@ -30,16 +31,16 @@ public class ProcessController { private final ProcessFacade processFacade; - @RequireAuthCheck(targetId = "dashboardId", targetDomain = Dashboard.class) @GetMapping + @ValidAuth public ResponseEntity read( - LoginProfile loginProfile, - @RequestParam(name = "dashboardId") Long dashboardId, + @RequireAuth(targetDomain = Dashboard.class) @RequestParam(name = "dashboardId") Long dashboardId, @RequestParam(name = "minScore", required = false, defaultValue = "0.00") Double minScore, @RequestParam(name = "maxScore", required = false, defaultValue = "5.00") Double maxScore, @RequestParam(name = "evaluationStatus", required = false, defaultValue = "ALL") String evaluationStatus, @RequestParam(name = "sortByCreatedAt", required = false, defaultValue = "DESC") String sortByCreatedAt, - @RequestParam(name = "sortByScore", required = false, defaultValue = "DESC") String sortByScore + @RequestParam(name = "sortByScore", required = false, defaultValue = "DESC") String sortByScore, + LoginProfile loginProfile ) { ProcessResponses processes = processFacade.readAllByDashboardId( dashboardId, minScore, maxScore, evaluationStatus, sortByCreatedAt, sortByScore @@ -47,31 +48,34 @@ public ResponseEntity read( return ResponseEntity.ok().body(processes); } - @RequireAuthCheck(targetId = "dashboardId", targetDomain = Dashboard.class) @PostMapping + @ValidAuth public ResponseEntity create( - LoginProfile loginProfile, - @RequestParam(name = "dashboardId") Long dashboardId, - @RequestBody @Valid ProcessCreateRequest request + @RequireAuth(targetDomain = Dashboard.class) @RequestParam(name = "dashboardId") Long dashboardId, + @RequestBody @Valid ProcessCreateRequest request, + LoginProfile loginProfile ) { processFacade.create(request, dashboardId); return ResponseEntity.status(HttpStatus.CREATED).build(); } - @RequireAuthCheck(targetId = "processId", targetDomain = Process.class) @PatchMapping("/{processId}") + @ValidAuth public ResponseEntity update( - LoginProfile loginProfile, - @PathVariable Long processId, - @RequestBody @Valid ProcessUpdateRequest request + @RequireAuth(targetDomain = Process.class) @PathVariable Long processId, + @RequestBody @Valid ProcessUpdateRequest request, + LoginProfile loginProfile ) { ProcessResponse response = processFacade.update(request, processId); return ResponseEntity.ok().body(response); } - @RequireAuthCheck(targetId = "processId", targetDomain = Process.class) @DeleteMapping("/{processId}") - public ResponseEntity delete(LoginProfile loginProfile, @PathVariable Long processId) { + @ValidAuth + public ResponseEntity delete( + @RequireAuth(targetDomain = Process.class) @PathVariable Long processId, + LoginProfile loginProfile + ) { processFacade.delete(processId); return ResponseEntity.noContent().build(); } diff --git a/backend/src/main/java/com/cruru/process/domain/repository/ProcessRepository.java b/backend/src/main/java/com/cruru/process/domain/repository/ProcessRepository.java index d3044e46a..4f2ab89c4 100644 --- a/backend/src/main/java/com/cruru/process/domain/repository/ProcessRepository.java +++ b/backend/src/main/java/com/cruru/process/domain/repository/ProcessRepository.java @@ -18,5 +18,5 @@ public interface ProcessRepository extends JpaRepository { @EntityGraph(attributePaths = {"dashboard.club.member"}) @Query("SELECT p FROM Process p WHERE p.id = :id") - Optional findByIdFetchingMember(long id); + Optional findByIdFetchingMember(Long id); } diff --git a/backend/src/main/java/com/cruru/process/service/ProcessService.java b/backend/src/main/java/com/cruru/process/service/ProcessService.java index 461768bfd..42d60f9c7 100644 --- a/backend/src/main/java/com/cruru/process/service/ProcessService.java +++ b/backend/src/main/java/com/cruru/process/service/ProcessService.java @@ -93,11 +93,6 @@ public Process findById(Long processId) { .orElseThrow(ProcessNotFoundException::new); } - public Process findByIdFetchingMember(Long processId) { - return processRepository.findByIdFetchingMember(processId) - .orElseThrow(ProcessNotFoundException::new); - } - private boolean changeExists(ProcessUpdateRequest request, Process process) { return !(request.name().equals(process.getName()) && request.description().equals(process.getDescription())); } diff --git a/backend/src/main/java/com/cruru/question/controller/QuestionController.java b/backend/src/main/java/com/cruru/question/controller/QuestionController.java index bf84b1076..6a06b392f 100644 --- a/backend/src/main/java/com/cruru/question/controller/QuestionController.java +++ b/backend/src/main/java/com/cruru/question/controller/QuestionController.java @@ -1,5 +1,9 @@ package com.cruru.question.controller; +import com.cruru.applyform.domain.ApplyForm; +import com.cruru.auth.annotation.RequireAuth; +import com.cruru.auth.annotation.ValidAuth; +import com.cruru.global.LoginProfile; import com.cruru.question.controller.request.QuestionUpdateRequests; import com.cruru.question.facade.QuestionFacade; import jakarta.validation.Valid; @@ -19,9 +23,11 @@ public class QuestionController { private final QuestionFacade questionFacade; @PatchMapping + @ValidAuth public ResponseEntity update( - @RequestParam(name = "applyformId") Long applyFormId, - @RequestBody @Valid QuestionUpdateRequests request + @RequireAuth(targetDomain = ApplyForm.class) @RequestParam(name = "applyformId") Long applyFormId, + @RequestBody @Valid QuestionUpdateRequests request, + LoginProfile loginProfile ) { questionFacade.update(request, applyFormId); return ResponseEntity.ok().build(); diff --git a/backend/src/main/java/com/cruru/question/domain/repository/AnswerRepository.java b/backend/src/main/java/com/cruru/question/domain/repository/AnswerRepository.java index daba7b648..8cba54c8a 100644 --- a/backend/src/main/java/com/cruru/question/domain/repository/AnswerRepository.java +++ b/backend/src/main/java/com/cruru/question/domain/repository/AnswerRepository.java @@ -15,7 +15,7 @@ public interface AnswerRepository extends JpaRepository { @EntityGraph(attributePaths = {"applicant.process.dashboard.club.member"}) @Query("SELECT a FROM Answer a WHERE a.id = :id") - Optional findByIdFetchingMember(long id); + Optional findByIdFetchingMember(Long id); @Query("SELECT a FROM Answer a JOIN FETCH a.question WHERE a.applicant = :applicant") List findAllByApplicantWithQuestions(@Param("applicant") Applicant applicant); diff --git a/backend/src/main/java/com/cruru/question/domain/repository/ChoiceRepository.java b/backend/src/main/java/com/cruru/question/domain/repository/ChoiceRepository.java index d2499f90a..3c117618a 100644 --- a/backend/src/main/java/com/cruru/question/domain/repository/ChoiceRepository.java +++ b/backend/src/main/java/com/cruru/question/domain/repository/ChoiceRepository.java @@ -12,7 +12,7 @@ public interface ChoiceRepository extends JpaRepository { @EntityGraph(attributePaths = {"question.applyForm.dashboard.club.member"}) @Query("SELECT c FROM Choice c WHERE c.id = :id") - Optional findByIdFetchingMember(long id); + Optional findByIdFetchingMember(Long id); List findAllByQuestion(Question question); diff --git a/backend/src/main/java/com/cruru/question/domain/repository/QuestionRepository.java b/backend/src/main/java/com/cruru/question/domain/repository/QuestionRepository.java index e0121fbdf..9c1b1eefa 100644 --- a/backend/src/main/java/com/cruru/question/domain/repository/QuestionRepository.java +++ b/backend/src/main/java/com/cruru/question/domain/repository/QuestionRepository.java @@ -12,7 +12,7 @@ public interface QuestionRepository extends JpaRepository { @EntityGraph(attributePaths = {"applyForm.dashboard.club.member"}) @Query("SELECT q FROM Question q WHERE q.id = :id") - Optional findByIdFetchingMember(long id); + Optional findByIdFetchingMember(Long id); List findAllByApplyForm(ApplyForm applyForm); } diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 9f96efa48..6752c8733 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -135,6 +135,9 @@ dataloader: enable: true server: port: ${SERVER_PORT} + tomcat: + mbeanregistry: + enabled: true management: server: @@ -221,6 +224,9 @@ dataloader: enable: false server: port: ${SERVER_PORT} + tomcat: + mbeanregistry: + enabled: true management: server: @@ -306,6 +312,9 @@ dataloader: enable: false server: port: ${SERVER_PORT} + tomcat: + mbeanregistry: + enabled: true management: server: diff --git a/backend/src/main/resources/logback.xml b/backend/src/main/resources/logback.xml index afb563c46..28165268c 100644 --- a/backend/src/main/resources/logback.xml +++ b/backend/src/main/resources/logback.xml @@ -3,11 +3,25 @@ - + - - + + @@ -71,16 +85,13 @@ - - - - + @@ -90,7 +101,6 @@ - @@ -100,7 +110,7 @@ - + diff --git a/backend/src/test/java/com/cruru/applicant/controller/ApplicantControllerTest.java b/backend/src/test/java/com/cruru/applicant/controller/ApplicantControllerTest.java index 5d41224c8..15bcb9a43 100644 --- a/backend/src/test/java/com/cruru/applicant/controller/ApplicantControllerTest.java +++ b/backend/src/test/java/com/cruru/applicant/controller/ApplicantControllerTest.java @@ -94,7 +94,8 @@ void updateProcess() { @Test void updateApplicantProcess_processNotFound() { // given - Process now = processRepository.save(ProcessFixture.applyType()); + Dashboard dashboard = dashboardRepository.save(DashboardFixture.backend(defaultClub)); + Process now = processRepository.save(ProcessFixture.applyType(dashboard)); Applicant applicant = ApplicantFixture.pendingDobby(now); applicantRepository.save(applicant); Long invalidProcessId = -1L; @@ -166,7 +167,7 @@ void read_applicantNotFound() { @Test void readDetail() { // given - Dashboard dashboard = dashboardRepository.save(DashboardFixture.backend()); + Dashboard dashboard = dashboardRepository.save(DashboardFixture.backend(defaultClub)); Process process = processRepository.save(ProcessFixture.applyType(dashboard)); Applicant applicant = applicantRepository.save(ApplicantFixture.pendingDobby(process)); ApplyForm applyForm = applyFormRepository.save(ApplyFormFixture.backend(dashboard)); @@ -228,7 +229,7 @@ void reject() { @Test void reject_applicantNotFound() { // given - long invalidApplicantId = 1; + long invalidApplicantId = -1; // when&then RestAssured.given(spec).log().all() diff --git a/backend/src/test/java/com/cruru/applyform/controller/ApplyFormControllerTest.java b/backend/src/test/java/com/cruru/applyform/controller/ApplyFormControllerTest.java index 08fd8574c..a5fe595b8 100644 --- a/backend/src/test/java/com/cruru/applyform/controller/ApplyFormControllerTest.java +++ b/backend/src/test/java/com/cruru/applyform/controller/ApplyFormControllerTest.java @@ -391,6 +391,7 @@ void read() { // when&then RestAssured.given(spec).log().all() + .cookie("token", token) .contentType(ContentType.JSON) .filter(document("applicant/read-applyform", pathParameters(parameterWithName("applyFormId").description("지원폼의 id")), diff --git a/backend/src/test/java/com/cruru/auth/aspect/AuthValidationAspectTest.java b/backend/src/test/java/com/cruru/auth/aspect/AuthValidationAspectTest.java new file mode 100644 index 000000000..d0aab993c --- /dev/null +++ b/backend/src/test/java/com/cruru/auth/aspect/AuthValidationAspectTest.java @@ -0,0 +1,210 @@ +package com.cruru.auth.aspect; + +import com.cruru.applicant.domain.Applicant; +import com.cruru.applicant.domain.Evaluation; +import com.cruru.applicant.domain.repository.ApplicantRepository; +import com.cruru.applicant.domain.repository.EvaluationRepository; +import com.cruru.applyform.domain.ApplyForm; +import com.cruru.applyform.domain.repository.ApplyFormRepository; +import com.cruru.auth.controller.AuthTestDto; +import com.cruru.auth.service.AuthService; +import com.cruru.club.domain.repository.ClubRepository; +import com.cruru.dashboard.domain.Dashboard; +import com.cruru.dashboard.domain.repository.DashboardRepository; +import com.cruru.member.domain.Member; +import com.cruru.member.domain.repository.MemberRepository; +import com.cruru.process.domain.Process; +import com.cruru.process.domain.repository.ProcessRepository; +import com.cruru.util.ControllerTest; +import com.cruru.util.fixture.ApplicantFixture; +import com.cruru.util.fixture.ApplyFormFixture; +import com.cruru.util.fixture.ClubFixture; +import com.cruru.util.fixture.DashboardFixture; +import com.cruru.util.fixture.EvaluationFixture; +import com.cruru.util.fixture.MemberFixture; +import com.cruru.util.fixture.ProcessFixture; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +@DisplayName("인가 AOP 테스트") +class AuthValidationAspectTest extends ControllerTest { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private AuthService authService; + + @Autowired + private ClubRepository clubRepository; + + @Autowired + private ApplyFormRepository applyFormRepository; + + @Autowired + private DashboardRepository dashboardRepository; + + @Autowired + private ProcessRepository processRepository; + + @Autowired + private ApplicantRepository applicantRepository; + + @Autowired + private EvaluationRepository evaluationRepository; + + private ApplyForm applyForm; + private Dashboard dashboard; + private Process process; + private Applicant applicant; + private List evaluations; + private AuthTestDto authTestDto; + private String unauthorizedToken; + + @BeforeEach + void setUp() { + Member unauthorizedMember = memberRepository.save(MemberFixture.RUSH); + unauthorizedToken = authService.createToken(unauthorizedMember); + clubRepository.save(ClubFixture.create(unauthorizedMember)); + dashboard = dashboardRepository.save(DashboardFixture.backend(defaultClub)); + applyForm = applyFormRepository.save(ApplyFormFixture.backend(dashboard)); + process = processRepository.save(ProcessFixture.applyType(dashboard)); + applicant = applicantRepository.save(ApplicantFixture.pendingDobby(process)); + evaluations = evaluationRepository.saveAll(List.of( + EvaluationFixture.fivePoints(process, applicant), + EvaluationFixture.fourPoints(process, applicant), + EvaluationFixture.fourPoints(process, applicant) + )); + authTestDto = new AuthTestDto( + process.getId(), + applicant.getId(), + evaluations.stream() + .map(Evaluation::getId) + .toList() + ); + } + + @DisplayName("권한이 있는 사용자가 applyformId를 요청하면 성공한다.") + @Test + void testReadByRequestParam_Success() { + RestAssured.given().log().all() + .cookie("token", token) + .contentType(ContentType.JSON) + .when().get("/auth-test/test1?applyformId=" + applyForm.getId()) + .then().log().all().statusCode(200); + } + + @DisplayName("권한이 없는 사용자가 applyformId를 요청하면 403 오류가 발생한다.") + @Test + void testReadByRequestParam_Forbidden() { + + RestAssured.given().log().all() + .cookie("token", unauthorizedToken) + .contentType(ContentType.JSON) + .when().get("/auth-test/test1?applyformId=" + applyForm.getId()) + .then().log().all().statusCode(403); + } + + @DisplayName("권한이 있는 사용자가 PathVariable로 요청하면 성공한다.") + @Test + void testReadByPathVariable_Success() { + RestAssured.given().log().all() + .cookie("token", token) + .contentType(ContentType.JSON) + .when().get("/auth-test/test2/" + applyForm.getId()) + .then().log().all().statusCode(200); + } + + @DisplayName("권한이 없는 사용자가 PathVariable로 요청하면 403 오류가 발생한다.") + @Test + void testReadByPathVariable_Forbidden() { + RestAssured.given().log().all() + .cookie("token", unauthorizedToken) + .contentType(ContentType.JSON) + .when().get("/auth-test/test2/" + applyForm.getId()) + .then().log().all().statusCode(403); + } + + @DisplayName("권한이 있는 사용자가 RequestBody로 요청하면 성공한다.") + @Test + void testReadByRequestBody_Success() { + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .cookie("token", token) + .body(authTestDto) + .when().get("/auth-test/test3") + .then().log().all().statusCode(200); + } + + @DisplayName("권한이 없는 사용자가 RequestBody로 요청하면 403 오류가 발생한다.") + @Test + void testReadByRequestBody_Forbidden() { + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .cookie("token", unauthorizedToken) + .body(authTestDto) + .when().get("/auth-test/test3") + .then().log().all().statusCode(403); + } + + @DisplayName("권한이 있는 사용자가 모든 Request 타입으로 요청하면 성공한다.") + @Test + void testReadByAllRequestType_Success() { + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .cookie("token", token) + .body(authTestDto) + .when().get("/auth-test/test4/" + dashboard.getId() + "?applyformId=" + applyForm.getId()) + .then().log().all().statusCode(200); + } + + @DisplayName("권한이 없는 사용자가 모든 Request 타입으로 요청하면 403 오류가 발생한다.") + @Test + void testReadByAllRequestType_Forbidden() { + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .cookie("token", unauthorizedToken) + .body(authTestDto) + .when().get("/auth-test/test4/" + dashboard.getId() + "?applyformId=" + applyForm.getId()) + .then().log().all().statusCode(403); + } + + @DisplayName("권한이 있는 대상과 없는 대상을 섞어서 요청하면 403 오류가 발생한다..") + @Test + void readByAllRequestType_SomeOfForbidden() { + Dashboard unauthorizedDashboard = dashboardRepository.save(DashboardFixture.backend(defaultClub)); + applyFormRepository.save(ApplyFormFixture.backend(unauthorizedDashboard)); + Process unauthorizedProcess = processRepository.save(ProcessFixture.applyType(unauthorizedDashboard)); + Applicant unauthorizedApplicant = applicantRepository.save(ApplicantFixture.pendingDobby(unauthorizedProcess)); + Evaluation unauthorizedEvaluation = evaluationRepository.save(EvaluationFixture.fivePoints( + unauthorizedProcess, + unauthorizedApplicant + )); + List authorizedEvaluationIds = new ArrayList<>(evaluations.stream() + .map(Evaluation::getId) + .toList()); + authorizedEvaluationIds.add(unauthorizedEvaluation.getId()); + AuthTestDto unauthorizedAuthTestDto = new AuthTestDto( + process.getId(), + applicant.getId(), + authorizedEvaluationIds + ); + + evaluationRepository.saveAll(List.of(EvaluationFixture.fivePoints(process, applicant))); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .cookie("token", unauthorizedToken) + .param("applyformId", applyForm.getId()) + .body(unauthorizedAuthTestDto) + .when().get("/auth-test/test4/" + dashboard.getId() + "?applyformId=" + applyForm.getId()) + .then().log().all().statusCode(403); + } +} diff --git a/backend/src/test/java/com/cruru/auth/controller/AuthTestController.java b/backend/src/test/java/com/cruru/auth/controller/AuthTestController.java new file mode 100644 index 000000000..8d047cff2 --- /dev/null +++ b/backend/src/test/java/com/cruru/auth/controller/AuthTestController.java @@ -0,0 +1,57 @@ +package com.cruru.auth.controller; + +import com.cruru.applyform.domain.ApplyForm; +import com.cruru.auth.annotation.RequireAuth; +import com.cruru.auth.annotation.ValidAuth; +import com.cruru.dashboard.domain.Dashboard; +import com.cruru.global.LoginProfile; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/auth-test") +public class AuthTestController { + + @GetMapping("/test1") + @ValidAuth + public ResponseEntity readByRequestParam( + @RequireAuth(targetDomain = ApplyForm.class) @RequestParam(name = "applyformId") Long applyFormId, + LoginProfile loginProfile + ) { + return ResponseEntity.ok().build(); + } + + @GetMapping("/test2/{targetId}") + @ValidAuth + public ResponseEntity readByPathVariable( + @RequireAuth(targetDomain = ApplyForm.class) @PathVariable(name = "targetId") Long applyFormId, + LoginProfile loginProfile + ) { + return ResponseEntity.ok().build(); + } + + @GetMapping("/test3") + @ValidAuth + public ResponseEntity readByRequestBody( + @RequestBody AuthTestDto authTestDto, + LoginProfile loginProfile + ) { + return ResponseEntity.ok().build(); + } + + @GetMapping("/test4/{targetId}") + @ValidAuth + public ResponseEntity readByAllRequestType( + @RequireAuth(targetDomain = ApplyForm.class) @RequestParam(name = "applyformId") Long applyFormId, + @RequireAuth(targetDomain = Dashboard.class) @PathVariable(name = "targetId") Long dashBoardId, + @RequestBody AuthTestDto authTestDto, + LoginProfile loginProfile + ) { + return ResponseEntity.ok().build(); + } +} diff --git a/backend/src/test/java/com/cruru/auth/controller/AuthTestDto.java b/backend/src/test/java/com/cruru/auth/controller/AuthTestDto.java new file mode 100644 index 000000000..8a5d6ea85 --- /dev/null +++ b/backend/src/test/java/com/cruru/auth/controller/AuthTestDto.java @@ -0,0 +1,21 @@ +package com.cruru.auth.controller; + +import com.cruru.applicant.domain.Applicant; +import com.cruru.applicant.domain.Evaluation; +import com.cruru.auth.annotation.RequireAuth; +import com.cruru.process.domain.Process; +import java.util.List; + +public record AuthTestDto( + + @RequireAuth(targetDomain = Process.class) + Long processId, + + @RequireAuth(targetDomain = Applicant.class) + Long applicantId, + + @RequireAuth(targetDomain = Evaluation.class) + List evaluationIds +) { + +} diff --git a/backend/src/test/java/com/cruru/dashboard/controller/DashboardControllerTest.java b/backend/src/test/java/com/cruru/dashboard/controller/DashboardControllerTest.java index 91280cedf..d56a5fd9e 100644 --- a/backend/src/test/java/com/cruru/dashboard/controller/DashboardControllerTest.java +++ b/backend/src/test/java/com/cruru/dashboard/controller/DashboardControllerTest.java @@ -15,7 +15,6 @@ import com.cruru.applicant.domain.repository.EvaluationRepository; import com.cruru.applyform.domain.ApplyForm; import com.cruru.applyform.domain.repository.ApplyFormRepository; -import com.cruru.club.domain.Club; import com.cruru.club.domain.repository.ClubRepository; import com.cruru.dashboard.controller.request.DashboardCreateRequest; import com.cruru.dashboard.domain.Dashboard; @@ -34,7 +33,6 @@ import com.cruru.util.fixture.ApplicantFixture; import com.cruru.util.fixture.ApplyFormFixture; import com.cruru.util.fixture.ChoiceFixture; -import com.cruru.util.fixture.ClubFixture; import com.cruru.util.fixture.DashboardFixture; import com.cruru.util.fixture.EmailFixture; import com.cruru.util.fixture.EvaluationFixture; @@ -45,7 +43,6 @@ import io.restassured.http.ContentType; import java.util.List; import java.util.stream.Stream; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -111,8 +108,6 @@ class DashboardControllerTest extends ControllerTest { @Autowired private EvaluationRepository evaluationRepository; - private Club club; - private static Stream InvalidQuestionCreateRequest() { String validChoice = "선택지1"; int validOrderIndex = 0; @@ -123,35 +118,41 @@ private static Stream InvalidQuestionCreateRequest() { boolean validRequired = false; return Stream.of( new QuestionCreateRequest(null, validQuestion, - validChoiceCreateRequests, validOrderIndex, validRequired), + validChoiceCreateRequests, validOrderIndex, validRequired + ), new QuestionCreateRequest("", validQuestion, - validChoiceCreateRequests, validOrderIndex, validRequired), + validChoiceCreateRequests, validOrderIndex, validRequired + ), new QuestionCreateRequest(validType, null, - validChoiceCreateRequests, validOrderIndex, validRequired), + validChoiceCreateRequests, validOrderIndex, validRequired + ), new QuestionCreateRequest(validType, "", - validChoiceCreateRequests, validOrderIndex, validRequired), + validChoiceCreateRequests, validOrderIndex, validRequired + ), new QuestionCreateRequest(validType, validQuestion, - List.of(new ChoiceCreateRequest(null, validOrderIndex)), validOrderIndex, validRequired), + List.of(new ChoiceCreateRequest(null, validOrderIndex)), validOrderIndex, validRequired + ), new QuestionCreateRequest(validType, validQuestion, - List.of(new ChoiceCreateRequest("", validOrderIndex)), validOrderIndex, validRequired), + List.of(new ChoiceCreateRequest("", validOrderIndex)), validOrderIndex, validRequired + ), new QuestionCreateRequest(validType, validQuestion, - List.of(new ChoiceCreateRequest(validChoice, null)), validOrderIndex, validRequired), + List.of(new ChoiceCreateRequest(validChoice, null)), validOrderIndex, validRequired + ), new QuestionCreateRequest(validType, validQuestion, - List.of(new ChoiceCreateRequest(validChoice, -1)), validOrderIndex, validRequired), + List.of(new ChoiceCreateRequest(validChoice, -1)), validOrderIndex, validRequired + ), new QuestionCreateRequest(validType, validQuestion, - validChoiceCreateRequests, null, validRequired), + validChoiceCreateRequests, null, validRequired + ), new QuestionCreateRequest(validType, validQuestion, - validChoiceCreateRequests, -1, validRequired), + validChoiceCreateRequests, -1, validRequired + ), new QuestionCreateRequest(validType, validQuestion, - validChoiceCreateRequests, validOrderIndex, null) + validChoiceCreateRequests, validOrderIndex, null + ) ); } - @BeforeEach - void setUp() { - club = clubRepository.save(ClubFixture.create(defaultMember)); - } - @DisplayName("대시보드 생성 성공 시, 201을 응답한다.") @Test void create() { @@ -166,14 +167,15 @@ void create() { LocalDateFixture.oneDayLater(), LocalDateFixture.oneWeekLater() ); - String url = String.format("/v1/dashboards?clubId=%d", club.getId()); + String url = String.format("/v1/dashboards?clubId=%d", defaultClub.getId()); // when&then RestAssured.given(spec).log().all() .contentType(ContentType.JSON) .cookie("token", token) .body(request) - .filter(document("dashboard/create", + .filter(document( + "dashboard/create", requestCookies(cookieWithName("token").description("사용자 토큰")), queryParameters(parameterWithName("clubId").description("동아리의 id")), requestFields( @@ -206,14 +208,15 @@ void create_invalidQuestionCreateRequest(QuestionCreateRequest invalidQuestionCr LocalDateFixture.oneDayLater(), LocalDateFixture.oneWeekLater() ); - String url = String.format("/v1/dashboards?clubId=%d", club.getId()); + String url = String.format("/v1/dashboards?clubId=%d", defaultClub.getId()); // when&then RestAssured.given(spec).log().all() .contentType(ContentType.JSON) .cookie("token", token) .body(request) - .filter(document("dashboard/create-fail/invalid-question", + .filter(document( + "dashboard/create-fail/invalid-question", requestCookies(cookieWithName("token").description("사용자 토큰")), queryParameters(parameterWithName("clubId").description("동아리의 id")), requestFields( @@ -251,7 +254,8 @@ void create_invalidClub() { .contentType(ContentType.JSON) .cookie("token", token) .body(request) - .filter(document("dashboard/create-fail/club-not-found", + .filter(document( + "dashboard/create-fail/club-not-found", requestCookies(cookieWithName("token").description("사용자 토큰")), queryParameters(parameterWithName("clubId").description("존재하지 않는 동아리 id")), requestFields( @@ -271,14 +275,15 @@ void create_invalidClub() { @Test void readDashboards_success() { // given - Dashboard dashboard = dashboardRepository.save(DashboardFixture.backend(club)); + Dashboard dashboard = dashboardRepository.save(DashboardFixture.backend(defaultClub)); applyFormRepository.save(ApplyFormFixture.backend(dashboard)); - String url = String.format("/v1/dashboards?clubId=%d", club.getId()); + String url = String.format("/v1/dashboards?clubId=%d", defaultClub.getId()); // when&then RestAssured.given(spec).log().all() .cookie("token", token) - .filter(document("dashboard/read", + .filter(document( + "dashboard/read", requestCookies(cookieWithName("token").description("사용자 토큰")), queryParameters(parameterWithName("clubId").description("동아리의 id")), responseFields( @@ -294,20 +299,21 @@ void readDashboards_success() { @Test void delete() { // given - Dashboard dashboard = dashboardRepository.save(DashboardFixture.backend(club)); + Dashboard dashboard = dashboardRepository.save(DashboardFixture.backend(defaultClub)); ApplyForm applyForm = applyFormRepository.save(ApplyFormFixture.backend(dashboard)); Question question = questionRepository.save(QuestionFixture.singleChoiceType(applyForm)); choiceRepository.save(ChoiceFixture.first(question)); Process process = processRepository.save(ProcessFixture.applyType(dashboard)); Applicant applicant = applicantRepository.save(ApplicantFixture.pendingDobby(process)); answerRepository.save(AnswerFixture.first(question, applicant)); - emailRepository.save(EmailFixture.rejectEmail(club, applicant)); + emailRepository.save(EmailFixture.rejectEmail(defaultClub, applicant)); evaluationRepository.save(EvaluationFixture.fivePoints(process, applicant)); // when&then RestAssured.given(spec).log().all() .cookie("token", token) - .filter(document("dashboard/delete", + .filter(document( + "dashboard/delete", requestCookies(cookieWithName("token").description("사용자 토큰")), pathParameters(parameterWithName("dashboardId").description("삭제할 대시보드의 id")) )) @@ -319,12 +325,13 @@ void delete() { @Test void delete_notFound() { // given - long invalidId = -1; + Long invalidId = -1L; // when&then RestAssured.given(spec).log().all() .cookie("token", token) - .filter(document("dashboard/delete/not-found", + .filter(document( + "dashboard/delete/not-found", requestCookies(cookieWithName("token").description("사용자 토큰")), pathParameters(parameterWithName("dashboardId").description("존재하지 않는 대시보드의 id")) )) diff --git a/backend/src/test/java/com/cruru/question/controller/QuestionControllerTest.java b/backend/src/test/java/com/cruru/question/controller/QuestionControllerTest.java index 1b81f9296..12ba3980c 100644 --- a/backend/src/test/java/com/cruru/question/controller/QuestionControllerTest.java +++ b/backend/src/test/java/com/cruru/question/controller/QuestionControllerTest.java @@ -86,7 +86,7 @@ void update() { @Test void update_applyFormNotFound() { // given - long invalidApplyFormId = -1; + Long invalidApplyFormId = -1L; ApplyForm applyForm = applyFormRepository.save(ApplyFormFixture.notStarted()); questionRepository.save(QuestionFixture.multipleChoiceType(applyForm)); QuestionUpdateRequests questionUpdateRequests = new QuestionUpdateRequests(