From 0069416af71601ba88b9c88e8b04ab733d03c352 Mon Sep 17 00:00:00 2001 From: hgkim Date: Wed, 18 Jun 2025 01:03:24 +0900 Subject: [PATCH 1/4] feat: implement AuthV2Controller and AuthService for admin status check Signed-off-by: hgkim --- git.environment-variables | 2 +- .../user/controller/AuthV2Controller.java | 32 ++++++++++++++++ .../app/bottlenote/user/domain/RootAdmin.java | 38 +++++++++++++++++++ .../bottlenote/user/service/AuthService.java | 18 +++++++++ 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 src/main/java/app/bottlenote/user/controller/AuthV2Controller.java create mode 100644 src/main/java/app/bottlenote/user/domain/RootAdmin.java create mode 100644 src/main/java/app/bottlenote/user/service/AuthService.java diff --git a/git.environment-variables b/git.environment-variables index cd96d787a..cbda8dc1e 160000 --- a/git.environment-variables +++ b/git.environment-variables @@ -1 +1 @@ -Subproject commit cd96d787a9bb9e9554ae209da068ab3060344bdd +Subproject commit cbda8dc1ef34507f4803ad7c778d854fa5c9cec2 diff --git a/src/main/java/app/bottlenote/user/controller/AuthV2Controller.java b/src/main/java/app/bottlenote/user/controller/AuthV2Controller.java new file mode 100644 index 000000000..cf20e64f4 --- /dev/null +++ b/src/main/java/app/bottlenote/user/controller/AuthV2Controller.java @@ -0,0 +1,32 @@ +package app.bottlenote.user.controller; + +import app.bottlenote.global.data.response.GlobalResponse; +import app.bottlenote.global.security.SecurityContextUtil; +import app.bottlenote.user.exception.UserException; +import app.bottlenote.user.service.AuthService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static app.bottlenote.user.exception.UserExceptionCode.REQUIRED_USER_ID; + + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v2/auth") +public class AuthV2Controller { + private final AuthService authService; + + @GetMapping("/admin/permissions") + public ResponseEntity checkAdminStatus() { + Long currentUserId = SecurityContextUtil.getUserIdByContext(). + orElseThrow(() -> new UserException(REQUIRED_USER_ID)); + + boolean is = authService.checkAdminStatus(currentUserId); + return GlobalResponse.ok(is); + } +} diff --git a/src/main/java/app/bottlenote/user/domain/RootAdmin.java b/src/main/java/app/bottlenote/user/domain/RootAdmin.java new file mode 100644 index 000000000..6dfcebaeb --- /dev/null +++ b/src/main/java/app/bottlenote/user/domain/RootAdmin.java @@ -0,0 +1,38 @@ +package app.bottlenote.user.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.MapsId; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "root_admins") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class RootAdmin { + + @Id + @Column(name = "user_id") + private Long userId; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + @MapsId + private User user; + + public RootAdmin(Long userId) { + this.userId = userId; + } + + public RootAdmin(User user) { + this.userId = user.getId(); + this.user = user; + } +} diff --git a/src/main/java/app/bottlenote/user/service/AuthService.java b/src/main/java/app/bottlenote/user/service/AuthService.java new file mode 100644 index 000000000..0c981c3f2 --- /dev/null +++ b/src/main/java/app/bottlenote/user/service/AuthService.java @@ -0,0 +1,18 @@ +package app.bottlenote.user.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AuthService { + + @Transactional(readOnly = true) + public boolean checkAdminStatus(Long userId) { + + return false; + } +} From 111bddca92ee208e10a619a3baed792c2451862a Mon Sep 17 00:00:00 2001 From: hgkim Date: Wed, 18 Jun 2025 01:05:20 +0900 Subject: [PATCH 2/4] feat: add RootAdminRepository and update AuthService to check admin status Signed-off-by: hgkim --- src/main/java/app/bottlenote/user/domain/RootAdmin.java | 9 --------- .../bottlenote/user/repository/RootAdminRepository.java | 8 ++++++++ .../java/app/bottlenote/user/service/AuthService.java | 6 ++++-- 3 files changed, 12 insertions(+), 11 deletions(-) create mode 100644 src/main/java/app/bottlenote/user/repository/RootAdminRepository.java diff --git a/src/main/java/app/bottlenote/user/domain/RootAdmin.java b/src/main/java/app/bottlenote/user/domain/RootAdmin.java index 6dfcebaeb..917fdf7ae 100644 --- a/src/main/java/app/bottlenote/user/domain/RootAdmin.java +++ b/src/main/java/app/bottlenote/user/domain/RootAdmin.java @@ -26,13 +26,4 @@ public class RootAdmin { @JoinColumn(name = "user_id") @MapsId private User user; - - public RootAdmin(Long userId) { - this.userId = userId; - } - - public RootAdmin(User user) { - this.userId = user.getId(); - this.user = user; - } } diff --git a/src/main/java/app/bottlenote/user/repository/RootAdminRepository.java b/src/main/java/app/bottlenote/user/repository/RootAdminRepository.java new file mode 100644 index 000000000..7e3a5d6c1 --- /dev/null +++ b/src/main/java/app/bottlenote/user/repository/RootAdminRepository.java @@ -0,0 +1,8 @@ +package app.bottlenote.user.repository; + +import app.bottlenote.user.domain.RootAdmin; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RootAdminRepository extends JpaRepository { + boolean existsByUserId(Long userId); +} diff --git a/src/main/java/app/bottlenote/user/service/AuthService.java b/src/main/java/app/bottlenote/user/service/AuthService.java index 0c981c3f2..6c7260d93 100644 --- a/src/main/java/app/bottlenote/user/service/AuthService.java +++ b/src/main/java/app/bottlenote/user/service/AuthService.java @@ -1,5 +1,6 @@ package app.bottlenote.user.service; +import app.bottlenote.user.repository.RootAdminRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -10,9 +11,10 @@ @RequiredArgsConstructor public class AuthService { + private final RootAdminRepository rootAdminRepository; + @Transactional(readOnly = true) public boolean checkAdminStatus(Long userId) { - - return false; + return rootAdminRepository.existsByUserId(userId); } } From 96905d4f53245124b34b7e4b7cb9d4163efd6615 Mon Sep 17 00:00:00 2001 From: hgkim Date: Wed, 18 Jun 2025 21:28:01 +0900 Subject: [PATCH 3/4] feat: add AuthV2ControllerTest and update ControllerLayerRules with new allowed prefix Signed-off-by: hgkim --- build.gradle | 2 +- .../app/bottlenote/user/controller/AuthV2ControllerTest.java | 5 +++++ src/test/java/app/rule/api/ControllerLayerRules.java | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 src/test/java/app/bottlenote/user/controller/AuthV2ControllerTest.java diff --git a/build.gradle b/build.gradle index 55fb2b284..99fac61f6 100644 --- a/build.gradle +++ b/build.gradle @@ -216,7 +216,7 @@ tasks.register('unitTest', Test) { includeTags 'unit' } } -tasks.register('check rule', Test) { +tasks.register('check_rule', Test) { useJUnitPlatform { includeTags 'rule' } diff --git a/src/test/java/app/bottlenote/user/controller/AuthV2ControllerTest.java b/src/test/java/app/bottlenote/user/controller/AuthV2ControllerTest.java new file mode 100644 index 000000000..579ea963b --- /dev/null +++ b/src/test/java/app/bottlenote/user/controller/AuthV2ControllerTest.java @@ -0,0 +1,5 @@ +package app.bottlenote.user.controller; + +class AuthV2ControllerTest { + +} diff --git a/src/test/java/app/rule/api/ControllerLayerRules.java b/src/test/java/app/rule/api/ControllerLayerRules.java index 90944632f..c04677741 100644 --- a/src/test/java/app/rule/api/ControllerLayerRules.java +++ b/src/test/java/app/rule/api/ControllerLayerRules.java @@ -341,6 +341,7 @@ public void check(JavaMethod method, ConditionEvents events) { allowedPrefixesMap.put("restore", "복원하다"); allowedPrefixesMap.put("reissue", "재발급하다"); allowedPrefixesMap.put("signup", "가입하다"); + allowedPrefixesMap.put("check", "확인하다"); boolean startsWithVerb = allowedPrefixesMap.keySet().stream() .anyMatch(methodName::startsWith); From a08c743750a2401fa71442ff6f37b40ba977ce2e Mon Sep 17 00:00:00 2001 From: hgkim Date: Wed, 18 Jun 2025 21:41:06 +0900 Subject: [PATCH 4/4] feat: add documentation for root admin permission validation and update test class structure Signed-off-by: hgkim --- src/docs/asciidoc/api/auth/auth.adoc | 23 ++++++ src/docs/asciidoc/index.adoc | 5 ++ .../app/bottlenote/user/domain/RootAdmin.java | 2 +- .../user/controller/AuthV2ControllerTest.java | 5 -- .../docs/user/RestAuthV2ControllerTest.java | 80 +++++++++++++++++++ 5 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 src/docs/asciidoc/api/auth/auth.adoc delete mode 100644 src/test/java/app/bottlenote/user/controller/AuthV2ControllerTest.java create mode 100644 src/test/java/app/docs/user/RestAuthV2ControllerTest.java diff --git a/src/docs/asciidoc/api/auth/auth.adoc b/src/docs/asciidoc/api/auth/auth.adoc new file mode 100644 index 000000000..fcb8c81cf --- /dev/null +++ b/src/docs/asciidoc/api/auth/auth.adoc @@ -0,0 +1,23 @@ +=== 루트 관리자 권한 검증 === + +- 현재 로그인한 사용자가 루트 관리자 권한을 가지고 있는지 검증합니다. +- 개발 / 운영 환경이 변경 가능한지 검증하는데 활용 할 수 있습니다. + +[source] +---- +api/v2/auth/admin/permissions +---- + +[discrete] +==== 요청 파라미터 ==== + +[discrete] +include::{snippets}/auth/admin/root-permissions/curl-request.adoc[] +include::{snippets}/auth/admin/root-permissions/http-request.adoc[] + +[discrete] +==== 응답 파라미터 ==== + +[discrete] +include::{snippets}/auth/admin/root-permissions/response-fields.adoc[] +include::{snippets}/auth/admin/root-permissions/http-response.adoc[] diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 48b8b30bc..b206b06c9 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -84,6 +84,11 @@ include::api/overview/global-auth.adoc[] ''' include::api/file/image/upload/presign-url.adoc[] +== 권한 (auth) 관련 API + +''' +include::api/auth/auth.adoc[] + == 회원 (user) 관련 API ''' diff --git a/src/main/java/app/bottlenote/user/domain/RootAdmin.java b/src/main/java/app/bottlenote/user/domain/RootAdmin.java index 917fdf7ae..413201702 100644 --- a/src/main/java/app/bottlenote/user/domain/RootAdmin.java +++ b/src/main/java/app/bottlenote/user/domain/RootAdmin.java @@ -13,7 +13,7 @@ import lombok.NoArgsConstructor; @Getter -@Entity +@Entity(name = "root_admins") @Table(name = "root_admins") @NoArgsConstructor(access = AccessLevel.PROTECTED) public class RootAdmin { diff --git a/src/test/java/app/bottlenote/user/controller/AuthV2ControllerTest.java b/src/test/java/app/bottlenote/user/controller/AuthV2ControllerTest.java deleted file mode 100644 index 579ea963b..000000000 --- a/src/test/java/app/bottlenote/user/controller/AuthV2ControllerTest.java +++ /dev/null @@ -1,5 +0,0 @@ -package app.bottlenote.user.controller; - -class AuthV2ControllerTest { - -} diff --git a/src/test/java/app/docs/user/RestAuthV2ControllerTest.java b/src/test/java/app/docs/user/RestAuthV2ControllerTest.java new file mode 100644 index 000000000..0124a9918 --- /dev/null +++ b/src/test/java/app/docs/user/RestAuthV2ControllerTest.java @@ -0,0 +1,80 @@ +package app.docs.user; + +import app.bottlenote.global.security.SecurityContextUtil; +import app.bottlenote.user.controller.AuthV2Controller; +import app.bottlenote.user.service.AuthService; +import app.docs.AbstractRestDocs; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.springframework.http.MediaType; + +import java.util.Optional; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Tag("document") +@DisplayName("유저 Auth 컨트롤러 V2x RestDocs 테스트") +class RestAuthV2ControllerTest extends AbstractRestDocs { + private final AuthService authService = mock(AuthService.class); + private MockedStatic mockedSecurityUtil; + + @Override + protected Object initController() { + return new AuthV2Controller(authService); + } + + @BeforeEach + void setup() { + mockedSecurityUtil = mockStatic(SecurityContextUtil.class); + } + + + @AfterEach + void tearDown() { + mockedSecurityUtil.close(); + } + + @Test + @DisplayName("루트 어드민 검증을 수행합니다.") + void login_test() throws Exception { + + //given + final long userId = 1L; + when(SecurityContextUtil.getUserIdByContext()).thenReturn(Optional.of(userId)); + + //when + when(authService.checkAdminStatus(userId)).thenReturn(true); + + //then + mockMvc.perform(get("/api/v2/auth/admin/permissions") + .contentType(MediaType.APPLICATION_JSON) + .with(csrf())) + .andExpect(status().isOk()) + .andDo( + document("auth/admin/root-permissions", + responseFields( + fieldWithPath("success").description("응답 성공 여부"), + fieldWithPath("code").description("응답 코드(http status code)"), + fieldWithPath("data").description("검증 결과"), + fieldWithPath("errors").description("응답 성공 여부가 false일 경우 에러 메시지(없을 경우 null)"), + fieldWithPath("meta.serverEncoding").description("서버 인코딩 정도"), + fieldWithPath("meta.serverVersion").description("서버 버전"), + fieldWithPath("meta.serverPathVersion").description("서버 경로 버전"), + fieldWithPath("meta.serverResponseTime").description("서버 응답 시간") + ) + ) + ); + } +}