diff --git a/build.gradle.kts b/build.gradle.kts index dd5c207..44be9b0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -70,6 +70,7 @@ dependencies { testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") testImplementation("org.springframework.security:spring-security-test") + runtimeOnly("com.h2database:h2") testRuntimeOnly("org.junit.platform:junit-platform-launcher") } diff --git a/src/main/kotlin/site/billilge/api/backend/domain/item/controller/AdminItemController.kt b/src/main/kotlin/site/billilge/api/backend/domain/item/controller/AdminItemController.kt index 183b6ab..2d19422 100644 --- a/src/main/kotlin/site/billilge/api/backend/domain/item/controller/AdminItemController.kt +++ b/src/main/kotlin/site/billilge/api/backend/domain/item/controller/AdminItemController.kt @@ -9,13 +9,14 @@ import site.billilge.api.backend.domain.item.dto.request.ItemRequest import site.billilge.api.backend.domain.item.dto.response.AdminItemFindAllResponse import site.billilge.api.backend.domain.item.dto.response.ItemDetail import site.billilge.api.backend.domain.item.facade.AdminItemFacade +import site.billilge.api.backend.domain.member.enums.Role import site.billilge.api.backend.global.annotation.OnlyAdmin import site.billilge.api.backend.global.dto.PageableCondition import site.billilge.api.backend.global.dto.SearchCondition @RestController @RequestMapping("/admin/items") -@OnlyAdmin +@OnlyAdmin(roles = [Role.ADMIN, Role.GA, Role.WORKER]) class AdminItemController( private val adminItemFacade: AdminItemFacade ) : AdminItemApi { @@ -32,6 +33,7 @@ class AdminItemController( ) } + @OnlyAdmin @PostMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) override fun addItem( @RequestPart image: MultipartFile, @@ -41,6 +43,7 @@ class AdminItemController( return ResponseEntity.status(HttpStatus.CREATED).build() } + @OnlyAdmin @PutMapping("/{itemId}", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) override fun updateItem( @PathVariable itemId: Long, @@ -58,6 +61,7 @@ class AdminItemController( return ResponseEntity.ok(adminItemFacade.getItemById(itemId)) } + @OnlyAdmin @DeleteMapping("/{itemId}") override fun deleteItem(@PathVariable itemId: Long): ResponseEntity { adminItemFacade.deleteItem(itemId) diff --git a/src/main/kotlin/site/billilge/api/backend/domain/member/controller/AdminMemberController.kt b/src/main/kotlin/site/billilge/api/backend/domain/member/controller/AdminMemberController.kt index b571fc2..4195d13 100644 --- a/src/main/kotlin/site/billilge/api/backend/domain/member/controller/AdminMemberController.kt +++ b/src/main/kotlin/site/billilge/api/backend/domain/member/controller/AdminMemberController.kt @@ -6,13 +6,14 @@ import site.billilge.api.backend.domain.member.dto.request.AdminRequest import site.billilge.api.backend.domain.member.dto.response.AdminFindAllResponse import site.billilge.api.backend.domain.member.dto.response.MemberFindAllResponse import site.billilge.api.backend.domain.member.facade.AdminMemberFacade +import site.billilge.api.backend.domain.member.enums.Role import site.billilge.api.backend.global.annotation.OnlyAdmin import site.billilge.api.backend.global.dto.PageableCondition import site.billilge.api.backend.global.dto.SearchCondition @RestController @RequestMapping("admin/members") -@OnlyAdmin +@OnlyAdmin(roles = [Role.ADMIN, Role.GA, Role.WORKER]) class AdminMemberController( private val adminMemberFacade: AdminMemberFacade ) : AdminMemberApi { @@ -32,12 +33,14 @@ class AdminMemberController( return ResponseEntity.ok(adminMemberFacade.getAdminList(pageableCondition, searchCondition)) } + @OnlyAdmin @PostMapping("/admins") override fun addAdmins(@RequestBody request: AdminRequest): ResponseEntity { adminMemberFacade.addAdmins(request) return ResponseEntity.ok().build() } + @OnlyAdmin @DeleteMapping("/admins") override fun deleteAdmins(@RequestBody request: AdminRequest): ResponseEntity { adminMemberFacade.deleteAdmins(request) diff --git a/src/main/kotlin/site/billilge/api/backend/domain/member/dto/request/AdminRequest.kt b/src/main/kotlin/site/billilge/api/backend/domain/member/dto/request/AdminRequest.kt index 3c14488..ec94a1d 100644 --- a/src/main/kotlin/site/billilge/api/backend/domain/member/dto/request/AdminRequest.kt +++ b/src/main/kotlin/site/billilge/api/backend/domain/member/dto/request/AdminRequest.kt @@ -1,9 +1,12 @@ package site.billilge.api.backend.domain.member.dto.request import io.swagger.v3.oas.annotations.media.Schema +import site.billilge.api.backend.domain.member.enums.Role @Schema data class AdminRequest( @field:Schema(description = "회원 ID 목록", example = "[1, 2, 3]") - val memberIds: List + val memberIds: List, + @field:Schema(description = "관리자 역할", example = "ADMIN") + val role: Role = Role.ADMIN, ) diff --git a/src/main/kotlin/site/billilge/api/backend/domain/member/enums/Role.kt b/src/main/kotlin/site/billilge/api/backend/domain/member/enums/Role.kt index 3ac009e..b96f8f4 100644 --- a/src/main/kotlin/site/billilge/api/backend/domain/member/enums/Role.kt +++ b/src/main/kotlin/site/billilge/api/backend/domain/member/enums/Role.kt @@ -5,5 +5,7 @@ enum class Role( val description: String, ) { USER("ROLE_USER", "사용자"), - ADMIN("ROLE_ADMIN", "관리자") + ADMIN("ROLE_ADMIN", "관리자"), + WORKER("ROLE_WORKER", "근무자"), + GA("ROLE_GA", "총무부"), } \ No newline at end of file diff --git a/src/main/kotlin/site/billilge/api/backend/domain/member/facade/AdminMemberFacade.kt b/src/main/kotlin/site/billilge/api/backend/domain/member/facade/AdminMemberFacade.kt index aa07eb7..ecba519 100644 --- a/src/main/kotlin/site/billilge/api/backend/domain/member/facade/AdminMemberFacade.kt +++ b/src/main/kotlin/site/billilge/api/backend/domain/member/facade/AdminMemberFacade.kt @@ -27,7 +27,7 @@ class AdminMemberFacade( } fun addAdmins(request: AdminRequest) { - memberService.addAdmins(request.memberIds) + memberService.addAdmins(request.memberIds, request.role) } fun deleteAdmins(request: AdminRequest) { diff --git a/src/main/kotlin/site/billilge/api/backend/domain/member/service/MemberService.kt b/src/main/kotlin/site/billilge/api/backend/domain/member/service/MemberService.kt index afca9af..265d0c7 100644 --- a/src/main/kotlin/site/billilge/api/backend/domain/member/service/MemberService.kt +++ b/src/main/kotlin/site/billilge/api/backend/domain/member/service/MemberService.kt @@ -69,10 +69,10 @@ class MemberService( } @Transactional - fun addAdmins(memberIds: List) { + fun addAdmins(memberIds: List, role: Role) { memberRepository.findAllByIds(memberIds) .forEach { member -> - member.updateRole(Role.ADMIN) + member.updateRole(role) } } @@ -125,7 +125,7 @@ class MemberService( if (password != adminPassword) throw ApiException(MemberErrorCode.ADMIN_PASSWORD_MISMATCH) - if (member.role != Role.ADMIN) + if (member.role !in listOf(Role.ADMIN, Role.GA, Role.WORKER)) throw ApiException(MemberErrorCode.FORBIDDEN) return tokenProvider.generateToken(member, Duration.ofDays(30)) diff --git a/src/main/kotlin/site/billilge/api/backend/domain/notification/controller/AdminNotificationController.kt b/src/main/kotlin/site/billilge/api/backend/domain/notification/controller/AdminNotificationController.kt index 4805352..1b52779 100644 --- a/src/main/kotlin/site/billilge/api/backend/domain/notification/controller/AdminNotificationController.kt +++ b/src/main/kotlin/site/billilge/api/backend/domain/notification/controller/AdminNotificationController.kt @@ -7,10 +7,11 @@ import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import site.billilge.api.backend.domain.notification.dto.response.NotificationFindAllResponse import site.billilge.api.backend.domain.notification.facade.AdminNotificationFacade +import site.billilge.api.backend.domain.member.enums.Role import site.billilge.api.backend.global.annotation.OnlyAdmin import site.billilge.api.backend.global.security.oauth2.UserAuthInfo -@OnlyAdmin +@OnlyAdmin(roles = [Role.ADMIN, Role.GA, Role.WORKER]) @RestController @RequestMapping("/admin/notifications") class AdminNotificationController( diff --git a/src/main/kotlin/site/billilge/api/backend/domain/payer/controller/AdminPayerController.kt b/src/main/kotlin/site/billilge/api/backend/domain/payer/controller/AdminPayerController.kt index cee72df..2c67053 100644 --- a/src/main/kotlin/site/billilge/api/backend/domain/payer/controller/AdminPayerController.kt +++ b/src/main/kotlin/site/billilge/api/backend/domain/payer/controller/AdminPayerController.kt @@ -10,6 +10,7 @@ import site.billilge.api.backend.domain.payer.dto.request.PayerDeleteRequest import site.billilge.api.backend.domain.payer.dto.request.PayerRequest import site.billilge.api.backend.domain.payer.dto.response.PayerFindAllResponse import site.billilge.api.backend.domain.payer.facade.AdminPayerFacade +import site.billilge.api.backend.domain.member.enums.Role import site.billilge.api.backend.global.annotation.OnlyAdmin import site.billilge.api.backend.global.dto.PageableCondition import site.billilge.api.backend.global.dto.SearchCondition @@ -18,7 +19,7 @@ import java.time.format.DateTimeFormatter @RestController @RequestMapping("/admin/members/payers") -@OnlyAdmin +@OnlyAdmin(roles = [Role.ADMIN, Role.GA]) class AdminPayerController( private val adminPayerFacade: AdminPayerFacade ) : AdminPayerApi { diff --git a/src/main/kotlin/site/billilge/api/backend/domain/rental/controller/AdminRentalController.kt b/src/main/kotlin/site/billilge/api/backend/domain/rental/controller/AdminRentalController.kt index d777ca7..e249eb4 100644 --- a/src/main/kotlin/site/billilge/api/backend/domain/rental/controller/AdminRentalController.kt +++ b/src/main/kotlin/site/billilge/api/backend/domain/rental/controller/AdminRentalController.kt @@ -9,6 +9,7 @@ import site.billilge.api.backend.domain.rental.dto.response.AdminRentalHistoryFi import site.billilge.api.backend.domain.rental.dto.response.DashboardResponse import site.billilge.api.backend.domain.rental.enums.RentalStatus import site.billilge.api.backend.domain.rental.facade.AdminRentalFacade +import site.billilge.api.backend.domain.member.enums.Role import site.billilge.api.backend.global.annotation.OnlyAdmin import site.billilge.api.backend.global.dto.PageableCondition import site.billilge.api.backend.global.dto.SearchCondition @@ -16,7 +17,7 @@ import site.billilge.api.backend.global.security.oauth2.UserAuthInfo @RestController @RequestMapping("/admin/rentals") -@OnlyAdmin +@OnlyAdmin(roles = [Role.ADMIN, Role.GA, Role.WORKER]) class AdminRentalController( private val adminRentalFacade: AdminRentalFacade ) : AdminRentalApi { @@ -53,6 +54,7 @@ class AdminRentalController( return ResponseEntity.ok().build() } + @OnlyAdmin @DeleteMapping("/{rentalHistoryId}") override fun deleteRentalHistory(@PathVariable rentalHistoryId: Long): ResponseEntity { adminRentalFacade.deleteRentalHistory(rentalHistoryId) diff --git a/src/main/kotlin/site/billilge/api/backend/domain/rental/controller/RentalController.kt b/src/main/kotlin/site/billilge/api/backend/domain/rental/controller/RentalController.kt index 0647dd0..3c802bb 100644 --- a/src/main/kotlin/site/billilge/api/backend/domain/rental/controller/RentalController.kt +++ b/src/main/kotlin/site/billilge/api/backend/domain/rental/controller/RentalController.kt @@ -9,6 +9,7 @@ import site.billilge.api.backend.domain.rental.dto.response.RentalHistoryFindAll import site.billilge.api.backend.domain.rental.dto.response.ReturnRequiredItemFindAllResponse import site.billilge.api.backend.domain.rental.enums.RentalStatus import site.billilge.api.backend.domain.rental.facade.RentalFacade +import site.billilge.api.backend.global.annotation.OnlyAdmin import site.billilge.api.backend.global.security.oauth2.UserAuthInfo @RestController @@ -27,6 +28,7 @@ class RentalController( return ResponseEntity.status(HttpStatus.CREATED).build() } + @OnlyAdmin @PostMapping("/dev") override fun createDevRental( @AuthenticationPrincipal userAuthInfo: UserAuthInfo, diff --git a/src/main/kotlin/site/billilge/api/backend/global/annotation/OnlyAdmin.kt b/src/main/kotlin/site/billilge/api/backend/global/annotation/OnlyAdmin.kt index c860f5d..8cbc1c3 100644 --- a/src/main/kotlin/site/billilge/api/backend/global/annotation/OnlyAdmin.kt +++ b/src/main/kotlin/site/billilge/api/backend/global/annotation/OnlyAdmin.kt @@ -1,6 +1,6 @@ package site.billilge.api.backend.global.annotation -import org.springframework.security.access.prepost.PreAuthorize +import site.billilge.api.backend.domain.member.enums.Role @Target( AnnotationTarget.FUNCTION, @@ -11,5 +11,6 @@ import org.springframework.security.access.prepost.PreAuthorize @Retention( AnnotationRetention.RUNTIME ) -@PreAuthorize("hasRole('ROLE_ADMIN')") -annotation class OnlyAdmin \ No newline at end of file +annotation class OnlyAdmin( + val roles: Array = [Role.ADMIN] +) diff --git a/src/main/kotlin/site/billilge/api/backend/global/annotation/OnlyAdminAspect.kt b/src/main/kotlin/site/billilge/api/backend/global/annotation/OnlyAdminAspect.kt new file mode 100644 index 0000000..b05d189 --- /dev/null +++ b/src/main/kotlin/site/billilge/api/backend/global/annotation/OnlyAdminAspect.kt @@ -0,0 +1,33 @@ +package site.billilge.api.backend.global.annotation + +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.security.authorization.AuthorizationDeniedException +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.stereotype.Component + +@Aspect +@Component +class OnlyAdminAspect { + + @Before("@within(site.billilge.api.backend.global.annotation.OnlyAdmin) || @annotation(site.billilge.api.backend.global.annotation.OnlyAdmin)") + fun checkRole(joinPoint: JoinPoint) { + val methodSignature = joinPoint.signature as MethodSignature + val method = methodSignature.method + + val annotation = method.getAnnotation(OnlyAdmin::class.java) + ?: joinPoint.target.javaClass.getAnnotation(OnlyAdmin::class.java) + ?: return + + val allowedRoles = annotation.roles.map { it.key }.toSet() + + val authentication = SecurityContextHolder.getContext().authentication + val authorities = authentication?.authorities?.map { it.authority }?.toSet() ?: emptySet() + + if (authorities.none { it in allowedRoles }) { + throw AuthorizationDeniedException("Access Denied") + } + } +}