-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat/admin authorization] - 사용자 권한에 따른 시큐리티 설정 #103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
29df488
6a21b62
04c9d79
aec6b40
4061857
fcc4dd7
897b138
f8fd195
c6e126e
621c07a
3af71a4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package org.runimo.runimo.auth.exceptions; | ||
|
|
||
| import org.runimo.runimo.auth.filters.UserErrorCode; | ||
| import org.runimo.runimo.exceptions.BusinessException; | ||
|
|
||
| public class UnauthorizedAccessException extends BusinessException { | ||
|
|
||
| public UnauthorizedAccessException(String message) { | ||
| super(UserErrorCode.INSUFFICIENT_PERMISSIONS, message); | ||
| } | ||
|
|
||
| public UnauthorizedAccessException(Throwable cause) { | ||
| super(UserErrorCode.INSUFFICIENT_PERMISSIONS, cause.getMessage()); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package org.runimo.runimo.config; | ||
|
|
||
| import lombok.AccessLevel; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @NoArgsConstructor(access = AccessLevel.PRIVATE) | ||
| final class SecurityConstants { | ||
|
|
||
|
|
||
| static final String[] COMMON_PUBLIC_ENDPOINTS = { | ||
| "/api/v1/auth/**", | ||
| "/checker/**", | ||
| "/actuator/**", | ||
| "/error" | ||
| }; | ||
|
|
||
| static final String[] DEV_PUBLIC_ENDPOINTS = { | ||
| "/swagger-ui/**", | ||
| "/swagger-ui.html", | ||
| "/v3/api-docs/**" | ||
| }; | ||
|
|
||
| static final String ADMIN_ENDPOINT_PATTERN = "/api/v1/admin/**"; | ||
|
|
||
| static final String USER_ENDPOINT_PATTERN = "/api/v1/users/**"; | ||
|
|
||
| static final String USER_ROLE = "USER"; | ||
| static final String ADMIN_ROLE = "ADMIN"; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package org.runimo.runimo.exceptions; | ||
|
|
||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import jakarta.servlet.http.HttpServletRequest; | ||
| import jakarta.servlet.http.HttpServletResponse; | ||
| import java.io.IOException; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.runimo.runimo.auth.filters.UserErrorCode; | ||
| import org.runimo.runimo.common.response.ErrorResponse; | ||
| import org.springframework.http.MediaType; | ||
| import org.springframework.security.access.AccessDeniedException; | ||
| import org.springframework.security.core.context.SecurityContextHolder; | ||
| import org.springframework.security.web.access.AccessDeniedHandler; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| @Slf4j | ||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class CustomAccessDeniedHandler implements AccessDeniedHandler { | ||
|
|
||
| private final ObjectMapper objectMapper; | ||
|
|
||
| @Override | ||
| public void handle(HttpServletRequest request, HttpServletResponse response, | ||
| AccessDeniedException accessDeniedException) throws IOException { | ||
|
|
||
| log.warn("[ERROR]접근 거부됨 - URI: {}, 사용자: {}", request.getRequestURI(), | ||
| SecurityContextHolder.getContext().getAuthentication() != null | ||
| ? SecurityContextHolder.getContext().getAuthentication().getName() : "Unknown"); | ||
|
|
||
| response.setStatus(HttpServletResponse.SC_FORBIDDEN); | ||
| response.setContentType(MediaType.APPLICATION_JSON_VALUE); | ||
| response.setCharacterEncoding("UTF-8"); | ||
|
|
||
| ErrorResponse errorResponse = ErrorResponse.of(UserErrorCode.INSUFFICIENT_PERMISSIONS); | ||
| response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,7 +4,7 @@ | |||||||||||||||||||||||||||||
| import java.util.NoSuchElementException; | ||||||||||||||||||||||||||||||
| import lombok.extern.slf4j.Slf4j; | ||||||||||||||||||||||||||||||
| import org.runimo.runimo.auth.exceptions.SignUpException; | ||||||||||||||||||||||||||||||
| import org.runimo.runimo.auth.exceptions.UnRegisteredUserException; | ||||||||||||||||||||||||||||||
| import org.runimo.runimo.auth.exceptions.UnauthorizedAccessException; | ||||||||||||||||||||||||||||||
| import org.runimo.runimo.auth.exceptions.UserJwtException; | ||||||||||||||||||||||||||||||
| import org.runimo.runimo.common.response.ErrorResponse; | ||||||||||||||||||||||||||||||
| import org.runimo.runimo.external.ExternalServiceException; | ||||||||||||||||||||||||||||||
|
|
@@ -19,6 +19,7 @@ | |||||||||||||||||||||||||||||
| import org.springframework.web.bind.MissingServletRequestParameterException; | ||||||||||||||||||||||||||||||
| import org.springframework.web.bind.annotation.ExceptionHandler; | ||||||||||||||||||||||||||||||
| import org.springframework.web.bind.annotation.RestControllerAdvice; | ||||||||||||||||||||||||||||||
| import org.springframework.web.servlet.resource.NoResourceFoundException; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| @Slf4j | ||||||||||||||||||||||||||||||
| @RestControllerAdvice | ||||||||||||||||||||||||||||||
|
|
@@ -27,6 +28,22 @@ public class GlobalExceptionHandler { | |||||||||||||||||||||||||||||
| private static final String ERROR_LOG_HEADER = "ERROR: "; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| @ExceptionHandler(NoResourceFoundException.class) | ||||||||||||||||||||||||||||||
| public ResponseEntity<ErrorResponse> handleNoResourceFoundException( | ||||||||||||||||||||||||||||||
| NoResourceFoundException e) { | ||||||||||||||||||||||||||||||
| log.warn("{} {}", ERROR_LOG_HEADER, e.getMessage(), e); | ||||||||||||||||||||||||||||||
| return ResponseEntity.status(HttpStatus.NOT_FOUND) | ||||||||||||||||||||||||||||||
| .body(ErrorResponse.of("요청한 리소스를 찾을 수 없습니다.", e.getMessage())); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| @ExceptionHandler(UnauthorizedAccessException.class) | ||||||||||||||||||||||||||||||
| public ResponseEntity<ErrorResponse> handleUnauthorizedAccessException( | ||||||||||||||||||||||||||||||
| UnauthorizedAccessException e) { | ||||||||||||||||||||||||||||||
| log.debug("{} {}", ERROR_LOG_HEADER, e.getMessage(), e); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
| log.debug("{} {}", ERROR_LOG_HEADER, e.getMessage(), e); | |
| log.warn("{} {}", ERROR_LOG_HEADER, e.getMessage(), e); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jeeheaG 권한 없는 사용자에 대한 로그를 warn으로 두는게 좋을까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Increase log level for security violations.
Unauthorized access attempts should be logged at a higher level than debug for security monitoring. Consider using log.warn() or log.info() for better visibility in production logs.
- log.debug("{} {}", ERROR_LOG_HEADER, e.getMessage(), e);
+ log.warn("{} {}", ERROR_LOG_HEADER, e.getMessage(), e);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @ExceptionHandler(UnauthorizedAccessException.class) | |
| public ResponseEntity<ErrorResponse> handleUnauthorizedAccessException( | |
| UnauthorizedAccessException e) { | |
| log.debug("{} {}", ERROR_LOG_HEADER, e.getMessage(), e); | |
| return ResponseEntity.status(HttpStatus.FORBIDDEN) | |
| .body(ErrorResponse.of(e.getErrorCode())); | |
| } | |
| @ExceptionHandler(UnauthorizedAccessException.class) | |
| public ResponseEntity<ErrorResponse> handleUnauthorizedAccessException( | |
| UnauthorizedAccessException e) { | |
| log.warn("{} {}", ERROR_LOG_HEADER, e.getMessage(), e); | |
| return ResponseEntity.status(HttpStatus.FORBIDDEN) | |
| .body(ErrorResponse.of(e.getErrorCode())); | |
| } |
🤖 Prompt for AI Agents
In src/main/java/org/runimo/runimo/exceptions/GlobalExceptionHandler.java around
lines 39 to 45, the log level for unauthorized access exceptions is set to
debug, which is too low for security-related events. Change the log method from
log.debug() to log.warn() or log.info() to increase visibility of these security
violations in production logs.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| package org.runimo.runimo.security; | ||
|
|
||
| import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; | ||
| import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | ||
|
|
||
| import org.junit.jupiter.api.DisplayName; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.springframework.beans.factory.annotation.Autowired; | ||
| import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; | ||
| import org.springframework.boot.test.context.SpringBootTest; | ||
| import org.springframework.test.context.ActiveProfiles; | ||
| import org.springframework.test.web.servlet.MockMvc; | ||
|
|
||
| @SpringBootTest | ||
| @AutoConfigureMockMvc | ||
| @ActiveProfiles("dev") | ||
| class SecurityConfigDevTest { | ||
|
|
||
| @Autowired | ||
| private MockMvc mockMvc; | ||
|
|
||
| @Test | ||
| @DisplayName("개발 환경에서 Swagger 접근 가능") | ||
| void swaggerAccessibleInDev() throws Exception { | ||
| mockMvc.perform(get("/swagger-ui.html")) | ||
| .andExpect(status().isFound()); // 302 redirect to swagger-ui/index.html | ||
|
|
||
| mockMvc.perform(get("/v3/api-docs")) | ||
| .andExpect(status().isOk()); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("개발 환경에서도 기본 보안 규칙 적용") | ||
| void basicSecurityRulesStillApply() throws Exception { | ||
| mockMvc.perform(get("/api/v1/users/me")) | ||
| .andExpect(status().isUnauthorized()); | ||
|
|
||
| mockMvc.perform(get("/api/v1/admin/users")) | ||
| .andExpect(status().isUnauthorized()); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.