Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ tasks.register('unitTest', Test) {
includeTags 'unit'
}
}
tasks.register('check rule', Test) {
tasks.register('check_rule', Test) {
useJUnitPlatform {
includeTags 'rule'
}
Expand Down
2 changes: 1 addition & 1 deletion git.environment-variables
23 changes: 23 additions & 0 deletions src/docs/asciidoc/api/auth/auth.adoc
Original file line number Diff line number Diff line change
@@ -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[]
5 changes: 5 additions & 0 deletions src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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

'''
Expand Down
32 changes: 32 additions & 0 deletions src/main/java/app/bottlenote/user/controller/AuthV2Controller.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
29 changes: 29 additions & 0 deletions src/main/java/app/bottlenote/user/domain/RootAdmin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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(name = "root_admins")
@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;
}
Original file line number Diff line number Diff line change
@@ -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<RootAdmin, Long> {
boolean existsByUserId(Long userId);
}
20 changes: 20 additions & 0 deletions src/main/java/app/bottlenote/user/service/AuthService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package app.bottlenote.user.service;

import app.bottlenote.user.repository.RootAdminRepository;
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 {

private final RootAdminRepository rootAdminRepository;

@Transactional(readOnly = true)
public boolean checkAdminStatus(Long userId) {
return rootAdminRepository.existsByUserId(userId);
}
}
80 changes: 80 additions & 0 deletions src/test/java/app/docs/user/RestAuthV2ControllerTest.java
Original file line number Diff line number Diff line change
@@ -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<SecurityContextUtil> 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("서버 응답 시간")
)
)
);
}
}
1 change: 1 addition & 0 deletions src/test/java/app/rule/api/ControllerLayerRules.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading