Skip to content

Commit

Permalink
fix: 단건 조회 & 통합 검색 권한 체크 (#217)
Browse files Browse the repository at this point in the history
* fix: private 단건 조회 권한 체크

* Fix: Notice 비밀글 표시 안되도록 수정 (#216)

* Feat: Add FindByIdAndIsPrivateFalse

* Fix: Change totalSearchNotice to get isStaff and hide private when not a staff.

* Fix: Change searchTotalNotice and readNotice to consider isStaff.

* Fix: Change totalSearchNotice and readNotice to check authentication and get staff info.

* fix: newsTotalSearch 권한 체크

* style: ktlint

---------

Co-authored-by: 우혁준 (HyukJoon Woo) <whjoon0225@naver.com>
  • Loading branch information
leeeryboy and huGgW authored Mar 16, 2024
1 parent 43b96c9 commit 345b3ec
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 80 deletions.
16 changes: 16 additions & 0 deletions src/main/kotlin/com/wafflestudio/csereal/common/utils/Utils.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package com.wafflestudio.csereal.common.utils

import com.wafflestudio.csereal.common.CserealException
import com.wafflestudio.csereal.common.mockauth.CustomPrincipal
import org.jsoup.Jsoup
import org.jsoup.parser.Parser
import org.jsoup.safety.Safelist
import org.springframework.security.core.Authentication
import org.springframework.security.oauth2.core.oidc.user.OidcUser
import kotlin.math.ceil

fun cleanTextFromHtml(description: String): String {
Expand Down Expand Up @@ -40,3 +44,15 @@ fun exchangeValidPageNum(pageSize: Int, pageNum: Int, total: Long): Int {
else -> ceil(total.toDouble() / pageSize).toInt()
}
}

fun getUsername(authentication: Authentication?): String? {
val principal = authentication?.principal

return principal?.let {
when (principal) {
is OidcUser -> principal.idToken.getClaim("username")
is CustomPrincipal -> principal.userEntity.username
else -> throw CserealException.Csereal401("Unsupported principal type")
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.wafflestudio.csereal.core.news.api

import com.wafflestudio.csereal.common.CserealException
import com.wafflestudio.csereal.common.aop.AuthenticatedStaff
import com.wafflestudio.csereal.common.mockauth.CustomPrincipal
import com.wafflestudio.csereal.common.utils.getUsername
import com.wafflestudio.csereal.core.news.dto.NewsDto
import com.wafflestudio.csereal.core.news.dto.NewsSearchResponse
import com.wafflestudio.csereal.core.news.dto.NewsTotalSearchDto
import com.wafflestudio.csereal.core.news.service.NewsService
import com.wafflestudio.csereal.core.user.database.Role
import com.wafflestudio.csereal.core.user.database.UserRepository
Expand All @@ -16,7 +16,6 @@ import org.springframework.data.domain.PageRequest
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.security.core.Authentication
import org.springframework.security.oauth2.core.oidc.user.OidcUser
import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile

Expand All @@ -34,15 +33,9 @@ class NewsController(
@RequestParam(required = false, defaultValue = "10") pageSize: Int,
authentication: Authentication?
): ResponseEntity<NewsSearchResponse> {
val principal = authentication?.principal

val isStaff = principal?.let {
val username = when (principal) {
is OidcUser -> principal.idToken.getClaim("username")
is CustomPrincipal -> principal.userEntity.username
else -> throw CserealException.Csereal401("Unsupported principal type")
}
val user = userRepository.findByUsername(username)
val username = getUsername(authentication)
val isStaff = username?.let {
val user = userRepository.findByUsername(it)
user?.role == Role.ROLE_STAFF
} ?: false

Expand All @@ -59,16 +52,29 @@ class NewsController(
@NotBlank
keyword: String,
@RequestParam(required = true) @Positive number: Int,
@RequestParam(required = false, defaultValue = "200") @Positive stringLength: Int
) = ResponseEntity.ok(
newsService.searchTotalNews(keyword, number, stringLength)
)
@RequestParam(required = false, defaultValue = "200") @Positive stringLength: Int,
authentication: Authentication?
): NewsTotalSearchDto {
val username = getUsername(authentication)
val isStaff = username?.let {
val user = userRepository.findByUsername(it)
user?.role == Role.ROLE_STAFF
} ?: false

return newsService.searchTotalNews(keyword, number, stringLength, isStaff)
}

@GetMapping("/{newsId}")
fun readNews(
@PathVariable newsId: Long
@PathVariable newsId: Long,
authentication: Authentication?
): ResponseEntity<NewsDto> {
return ResponseEntity.ok(newsService.readNews(newsId))
val username = getUsername(authentication)
val isStaff = username?.let {
val user = userRepository.findByUsername(it)
user?.role == Role.ROLE_STAFF
} ?: false
return ResponseEntity.ok(newsService.readNews(newsId, isStaff))
}

@AuthenticatedStaff
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ interface CustomNewsRepository {
keyword: String,
number: Int,
amount: Int,
imageUrlCreator: (MainImageEntity?) -> String?
imageUrlCreator: (MainImageEntity?) -> String?,
isStaff: Boolean
): NewsTotalSearchDto

fun readAllSlides(pageNum: Long, pageSize: Int): AdminSlidesResponse
Expand Down Expand Up @@ -135,14 +136,17 @@ class NewsRepositoryImpl(
keyword: String,
number: Int,
amount: Int,
imageUrlCreator: (MainImageEntity?) -> String?
imageUrlCreator: (MainImageEntity?) -> String?,
isStaff: Boolean
): NewsTotalSearchDto {
val doubleTemplate = commonRepository.searchFullDoubleTextTemplate(
keyword,
newsEntity.title,
newsEntity.plainTextDescription
)

val privateBoolean = newsEntity.isPrivate.eq(false).takeUnless { isStaff }

val searchResult = queryFactory.select(
newsEntity.id,
newsEntity.title,
Expand All @@ -151,7 +155,7 @@ class NewsRepositoryImpl(
mainImageEntity
).from(newsEntity)
.leftJoin(mainImageEntity)
.where(doubleTemplate.gt(0.0))
.where(doubleTemplate.gt(0.0), privateBoolean)
.orderBy(newsEntity.date.desc())
.limit(number.toLong())
.fetch()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ interface NewsService {
isStaff: Boolean
): NewsSearchResponse

fun readNews(newsId: Long): NewsDto
fun readNews(newsId: Long, isStaff: Boolean): NewsDto
fun createNews(request: NewsDto, mainImage: MultipartFile?, attachments: List<MultipartFile>?): NewsDto
fun updateNews(
newsId: Long,
Expand All @@ -34,7 +34,7 @@ interface NewsService {

fun deleteNews(newsId: Long)
fun enrollTag(tagName: String)
fun searchTotalNews(keyword: String, number: Int, amount: Int): NewsTotalSearchDto
fun searchTotalNews(keyword: String, number: Int, amount: Int, isStaff: Boolean): NewsTotalSearchDto
fun readAllSlides(pageNum: Long, pageSize: Int): AdminSlidesResponse
fun unSlideManyNews(request: List<Long>)
}
Expand Down Expand Up @@ -62,21 +62,25 @@ class NewsServiceImpl(
override fun searchTotalNews(
keyword: String,
number: Int,
amount: Int
amount: Int,
isStaff: Boolean
) = newsRepository.searchTotalNews(
keyword,
number,
amount,
mainImageService::createImageURL
mainImageService::createImageURL,
isStaff
)

@Transactional(readOnly = true)
override fun readNews(newsId: Long): NewsDto {
override fun readNews(newsId: Long, isStaff: Boolean): NewsDto {
val news: NewsEntity = newsRepository.findByIdOrNull(newsId)
?: throw CserealException.Csereal404("존재하지 않는 새소식입니다.(newsId: $newsId)")

if (news.isDeleted) throw CserealException.Csereal404("삭제된 새소식입니다.(newsId: $newsId)")

if (news.isPrivate && !isStaff) throw CserealException.Csereal401("접근 권한이 없습니다.")

val imageURL = mainImageService.createImageURL(news.mainImage)
val attachmentResponses = attachmentService.createAttachmentResponses(news.attachments)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package com.wafflestudio.csereal.core.notice.api

import com.wafflestudio.csereal.common.CserealException
import com.wafflestudio.csereal.common.aop.AuthenticatedStaff
import com.wafflestudio.csereal.common.mockauth.CustomPrincipal
import com.wafflestudio.csereal.common.utils.getUsername
import com.wafflestudio.csereal.core.notice.dto.*
import com.wafflestudio.csereal.core.notice.service.NoticeService
import com.wafflestudio.csereal.core.user.database.Role
Expand All @@ -15,7 +14,6 @@ import org.springframework.data.domain.PageRequest
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.security.core.Authentication
import org.springframework.security.oauth2.core.oidc.user.OidcUser
import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile

Expand All @@ -33,15 +31,9 @@ class NoticeController(
@RequestParam(required = false, defaultValue = "20") pageSize: Int,
authentication: Authentication?
): ResponseEntity<NoticeSearchResponse> {
val principal = authentication?.principal

val isStaff = principal?.let {
val username = when (principal) {
is OidcUser -> principal.idToken.getClaim("username")
is CustomPrincipal -> principal.userEntity.username
else -> throw CserealException.Csereal401("Unsupported principal type")
}
val user = userRepository.findByUsername(username)
val username = getUsername(authentication)
val isStaff = username?.let {
val user = userRepository.findByUsername(it)
user?.role == Role.ROLE_STAFF
} ?: false

Expand All @@ -61,15 +53,9 @@ class NoticeController(
@RequestParam(required = false, defaultValue = "200") @Positive stringLength: Int,
authentication: Authentication?
): NoticeTotalSearchResponse {
val principal = authentication?.principal

val isStaff = principal?.let {
val username = when (principal) {
is OidcUser -> principal.idToken.getClaim("username")
is CustomPrincipal -> principal.userEntity.username
else -> throw CserealException.Csereal401("Unsupported principal type")
}
val user = userRepository.findByUsername(username)
val username = getUsername(authentication)
val isStaff = username?.let {
val user = userRepository.findByUsername(it)
user?.role == Role.ROLE_STAFF
} ?: false

Expand All @@ -80,20 +66,13 @@ class NoticeController(
fun readNotice(
@PathVariable noticeId: Long,
authentication: Authentication?
): NoticeDto {
val principal = authentication?.principal

val isStaff = principal?.let {
val username = when (principal) {
is OidcUser -> principal.idToken.getClaim("username")
is CustomPrincipal -> principal.userEntity.username
else -> throw CserealException.Csereal401("Unsupported principal type")
}
val user = userRepository.findByUsername(username)
): ResponseEntity<NoticeDto> {
val username = getUsername(authentication)
val isStaff = username?.let {
val user = userRepository.findByUsername(it)
user?.role == Role.ROLE_STAFF
} ?: false

return noticeService.readNotice(noticeId, isStaff)
return ResponseEntity.ok(noticeService.readNotice(noticeId, isStaff))
}

@AuthenticatedStaff
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,13 @@ class NoticeServiceImpl(

@Transactional(readOnly = true)
override fun readNotice(noticeId: Long, isStaff: Boolean): NoticeDto {
val notice = if (isStaff) {
noticeRepository.findByIdOrNull(noticeId)
} else {
noticeRepository.findByIdAndIsPrivateFalse(noticeId)
} ?: throw CserealException.Csereal404("존재하지 않는 공지사항입니다.(noticeId: $noticeId)")
val notice = noticeRepository.findByIdOrNull(noticeId)
?: throw CserealException.Csereal404("존재하지 않는 공지사항입니다.(noticeId: $noticeId)")

if (notice.isDeleted) throw CserealException.Csereal404("삭제된 공지사항입니다.(noticeId: $noticeId)")

if (notice.isPrivate && !isStaff) throw CserealException.Csereal401("접근 권한이 없습니다.")

val attachmentResponses = attachmentService.createAttachmentResponses(notice.attachments)

val prevNotice =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package com.wafflestudio.csereal.core.seminar.api

import com.wafflestudio.csereal.common.CserealException
import com.wafflestudio.csereal.common.aop.AuthenticatedStaff
import com.wafflestudio.csereal.common.mockauth.CustomPrincipal
import com.wafflestudio.csereal.common.utils.getUsername
import com.wafflestudio.csereal.core.seminar.dto.SeminarDto
import com.wafflestudio.csereal.core.seminar.dto.SeminarSearchResponse
import com.wafflestudio.csereal.core.seminar.service.SeminarService
Expand All @@ -12,7 +11,6 @@ import jakarta.validation.Valid
import org.springframework.data.domain.PageRequest
import org.springframework.http.ResponseEntity
import org.springframework.security.core.Authentication
import org.springframework.security.oauth2.core.oidc.user.OidcUser
import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile

Expand All @@ -29,15 +27,9 @@ class SeminarController(
@RequestParam(required = false, defaultValue = "10") pageSize: Int,
authentication: Authentication?
): ResponseEntity<SeminarSearchResponse> {
val principal = authentication?.principal

val isStaff = principal?.let {
val username = when (principal) {
is OidcUser -> principal.idToken.getClaim("username")
is CustomPrincipal -> principal.userEntity.username
else -> throw CserealException.Csereal401("Unsupported principal type")
}
val user = userRepository.findByUsername(username)
val username = getUsername(authentication)
val isStaff = username?.let {
val user = userRepository.findByUsername(it)
user?.role == Role.ROLE_STAFF
} ?: false

Expand All @@ -61,9 +53,15 @@ class SeminarController(

@GetMapping("/{seminarId}")
fun readSeminar(
@PathVariable seminarId: Long
@PathVariable seminarId: Long,
authentication: Authentication?
): ResponseEntity<SeminarDto> {
return ResponseEntity.ok(seminarService.readSeminar(seminarId))
val username = getUsername(authentication)
val isStaff = username?.let {
val user = userRepository.findByUsername(it)
user?.role == Role.ROLE_STAFF
} ?: false
return ResponseEntity.ok(seminarService.readSeminar(seminarId, isStaff))
}

@AuthenticatedStaff
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ interface SeminarService {
): SeminarSearchResponse

fun createSeminar(request: SeminarDto, mainImage: MultipartFile?, attachments: List<MultipartFile>?): SeminarDto
fun readSeminar(seminarId: Long): SeminarDto
fun readSeminar(seminarId: Long, isStaff: Boolean): SeminarDto
fun updateSeminar(
seminarId: Long,
request: SeminarDto,
Expand Down Expand Up @@ -73,12 +73,14 @@ class SeminarServiceImpl(
}

@Transactional(readOnly = true)
override fun readSeminar(seminarId: Long): SeminarDto {
override fun readSeminar(seminarId: Long, isStaff: Boolean): SeminarDto {
val seminar: SeminarEntity = seminarRepository.findByIdOrNull(seminarId)
?: throw CserealException.Csereal404("존재하지 않는 세미나입니다.(seminarId: $seminarId)")

if (seminar.isDeleted) throw CserealException.Csereal400("삭제된 세미나입니다. (seminarId: $seminarId)")

if (seminar.isPrivate && !isStaff) throw CserealException.Csereal401("접근 권한이 없습니다.")

val imageURL = mainImageService.createImageURL(seminar.mainImage)
val attachmentResponses = attachmentService.createAttachmentResponses(seminar.attachments)

Expand Down

0 comments on commit 345b3ec

Please sign in to comment.