feat: Postのアクセス制御を追加

This commit is contained in:
usbharu 2024-08-07 11:41:05 +09:00
parent 0825be76b3
commit 4fd97b6182
Signed by: usbharu
GPG Key ID: 6556747BF94EEBC8
9 changed files with 359 additions and 17 deletions

View File

@ -16,10 +16,10 @@
package dev.usbharu.hideout.core.application.media package dev.usbharu.hideout.core.application.media
import dev.usbharu.hideout.core.application.shared.AbstractApplicationService import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService
import dev.usbharu.hideout.core.application.shared.Transaction import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.media.* import dev.usbharu.hideout.core.domain.model.media.*
import dev.usbharu.hideout.core.domain.model.support.principal.Principal import dev.usbharu.hideout.core.domain.model.support.principal.FromApi
import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService
import dev.usbharu.hideout.core.external.media.MediaProcessor import dev.usbharu.hideout.core.external.media.MediaProcessor
import dev.usbharu.hideout.core.external.mediastore.MediaStore import dev.usbharu.hideout.core.external.mediastore.MediaStore
@ -35,11 +35,11 @@ class UploadMediaApplicationService(
private val mediaRepository: MediaRepository, private val mediaRepository: MediaRepository,
private val idGenerateService: IdGenerateService, private val idGenerateService: IdGenerateService,
transaction: Transaction transaction: Transaction
) : AbstractApplicationService<UploadMedia, Media>( ) : LocalUserAbstractApplicationService<UploadMedia, Media>(
transaction, transaction,
logger logger
) { ) {
override suspend fun internalExecute(command: UploadMedia, principal: Principal): Media { override suspend fun internalExecute(command: UploadMedia, principal: FromApi): Media {
val process = mediaProcessor.process(command.path, command.name, null) val process = mediaProcessor.process(command.path, command.name, null)
val id = idGenerateService.generateId() val id = idGenerateService.generateId()
val thumbnailUri = if (process.thumbnailPath != null) { val thumbnailUri = if (process.thumbnailPath != null) {

View File

@ -0,0 +1,3 @@
package dev.usbharu.hideout.core.application.post
data class DeleteLocalPost(val postId: Long)

View File

@ -16,24 +16,33 @@
package dev.usbharu.hideout.core.application.post package dev.usbharu.hideout.core.application.post
import dev.usbharu.hideout.core.application.exception.PermissionDeniedException
import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.domain.model.post.PostId import dev.usbharu.hideout.core.domain.model.post.PostId
import dev.usbharu.hideout.core.domain.model.post.PostRepository import dev.usbharu.hideout.core.domain.model.post.PostRepository
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId import dev.usbharu.hideout.core.domain.model.support.principal.FromApi
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@Service @Service
class DeleteLocalPostApplicationService( class DeleteLocalPostApplicationService(
private val postRepository: PostRepository, private val postRepository: PostRepository,
private val userDetailRepository: UserDetailRepository, private val actorRepository: ActorRepository, transaction: Transaction,
private val actorRepository: ActorRepository, ) : LocalUserAbstractApplicationService<DeleteLocalPost, Unit>(transaction, logger) {
) {
suspend fun delete(postId: Long, userDetailId: Long) { override suspend fun internalExecute(command: DeleteLocalPost, principal: FromApi) {
val findById = postRepository.findById(PostId(postId))!! val findById = postRepository.findById(PostId(command.postId))!!
val user = userDetailRepository.findById(UserDetailId(userDetailId))!! if (findById.actorId != principal.actorId) {
val actor = actorRepository.findById(user.actorId)!! throw PermissionDeniedException()
}
val actor = actorRepository.findById(principal.actorId)!!
findById.delete(actor) findById.delete(actor)
postRepository.save(findById) postRepository.save(findById)
} }
companion object {
private val logger = LoggerFactory.getLogger(DeleteLocalPostApplicationService::class.java)
}
} }

View File

@ -16,21 +16,29 @@
package dev.usbharu.hideout.core.application.post package dev.usbharu.hideout.core.application.post
import dev.usbharu.hideout.core.application.exception.PermissionDeniedException
import dev.usbharu.hideout.core.application.shared.AbstractApplicationService import dev.usbharu.hideout.core.application.shared.AbstractApplicationService
import dev.usbharu.hideout.core.application.shared.Transaction import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.post.PostId import dev.usbharu.hideout.core.domain.model.post.PostId
import dev.usbharu.hideout.core.domain.model.post.PostRepository import dev.usbharu.hideout.core.domain.model.post.PostRepository
import dev.usbharu.hideout.core.domain.model.support.principal.Principal import dev.usbharu.hideout.core.domain.model.support.principal.Principal
import dev.usbharu.hideout.core.domain.service.post.IPostReadAccessControl
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@Service @Service
class GetPostApplicationService(private val postRepository: PostRepository, transaction: Transaction) : class GetPostApplicationService(
private val postRepository: PostRepository,
private val iPostReadAccessControl: IPostReadAccessControl,
transaction: Transaction
) :
AbstractApplicationService<GetPost, Post>(transaction, logger) { AbstractApplicationService<GetPost, Post>(transaction, logger) {
override suspend fun internalExecute(command: GetPost, principal: Principal): Post { override suspend fun internalExecute(command: GetPost, principal: Principal): Post {
val post = postRepository.findById(PostId(command.postId)) ?: throw Exception("Post not found") val post = postRepository.findById(PostId(command.postId)) ?: throw IllegalArgumentException("Post not found")
if (iPostReadAccessControl.isAllow(post, principal).not()) {
throw PermissionDeniedException()
}
return Post.of(post) return Post.of(post)
} }

View File

@ -0,0 +1,54 @@
package dev.usbharu.hideout.core.domain.service.post
import dev.usbharu.hideout.core.domain.model.post.Post
import dev.usbharu.hideout.core.domain.model.post.Visibility
import dev.usbharu.hideout.core.domain.model.relationship.Relationship
import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository
import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous
import dev.usbharu.hideout.core.domain.model.support.principal.Principal
import org.springframework.stereotype.Component
interface IPostReadAccessControl {
suspend fun isAllow(post: Post, principal: Principal): Boolean
}
@Component
class DefaultPostReadAccessControl(private val relationshipRepository: RelationshipRepository) :
IPostReadAccessControl {
override suspend fun isAllow(post: Post, principal: Principal): Boolean {
val relationship = (relationshipRepository.findByActorIdAndTargetId(post.actorId, principal.actorId)
?: Relationship.default(post.actorId, principal.actorId))
//ブロックされてたら見れない
if (relationship.blocking) {
return false
}
//PublicかUnlistedなら見れる
if (post.visibility == Visibility.PUBLIC || post.visibility == Visibility.UNLISTED) {
return true
}
//principalがAnonymousなら見れない
if (principal is Anonymous) {
return false
}
//DirectでvisibleActorsに含まれていたら見れる
if (post.visibility == Visibility.DIRECT && post.visibleActors.contains(principal.actorId)) {
return true
}
//Followersでフォロワーなら見れる
if (post.visibility == Visibility.FOLLOWERS) {
val inverseRelationship =
relationshipRepository.findByActorIdAndTargetId(principal.actorId, post.actorId) ?: return false
return inverseRelationship.following
}
//その他の場合は見れない
return false
}
}

View File

@ -14,8 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
package dev.usbharu.hideout.core.domain.service.post package dev.usbharu.hideout.core.infrastructure.other
import dev.usbharu.hideout.core.domain.service.post.FormattedPostContent
import dev.usbharu.hideout.core.domain.service.post.PostContentFormatter
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element

View File

@ -0,0 +1,44 @@
package dev.usbharu.hideout.core.application.post
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.domain.model.actor.TestActorFactory
import dev.usbharu.hideout.core.domain.model.post.PostId
import dev.usbharu.hideout.core.domain.model.post.PostRepository
import dev.usbharu.hideout.core.domain.model.post.TestPostFactory
import dev.usbharu.hideout.core.domain.model.support.acct.Acct
import dev.usbharu.hideout.core.domain.model.support.principal.FromApi
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Spy
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.whenever
import utils.TestTransaction
@ExtendWith(MockitoExtension::class)
class DeleteLocalPostApplicationServiceTest {
@InjectMocks
lateinit var service: DeleteLocalPostApplicationService
@Mock
lateinit var postRepository: PostRepository
@Mock
lateinit var actorRepository: ActorRepository
@Spy
val transaction = TestTransaction
@Test
fun Post主はローカルPostを削除できる() = runTest {
whenever(postRepository.findById(PostId(1))).doReturn(TestPostFactory.create(actorId = 2))
whenever(actorRepository.findById(ActorId(2))).doReturn(TestActorFactory.create(id = 2))
service.execute(DeleteLocalPost(1), FromApi(ActorId(2), UserDetailId(2), Acct("test", "example.com")))
}
}

View File

@ -0,0 +1,64 @@
package dev.usbharu.hideout.core.application.post
import dev.usbharu.hideout.core.application.exception.PermissionDeniedException
import dev.usbharu.hideout.core.domain.model.post.PostId
import dev.usbharu.hideout.core.domain.model.post.PostRepository
import dev.usbharu.hideout.core.domain.model.post.TestPostFactory
import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous
import dev.usbharu.hideout.core.domain.service.post.IPostReadAccessControl
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Spy
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.whenever
import utils.TestTransaction
@ExtendWith(MockitoExtension::class)
class GetPostApplicationServiceTest {
@InjectMocks
lateinit var service: GetPostApplicationService
@Mock
lateinit var postRepository: PostRepository
@Mock
lateinit var iPostReadAccessControl: IPostReadAccessControl
@Spy
val transaction = TestTransaction
@Test
fun postReadAccessControlがtrueを返したらPostが返ってくる() = runTest {
val post = TestPostFactory.create(id = 1)
whenever(postRepository.findById(PostId(1))).doReturn(post)
whenever(iPostReadAccessControl.isAllow(any(), any())).doReturn(true)
val actual = service.execute(GetPost(1), Anonymous)
assertEquals(Post.of(post), actual)
}
@Test
fun postが見つからない場合失敗() = runTest {
assertThrows<IllegalArgumentException> {
service.execute(GetPost(2), Anonymous)
}
}
@Test
fun postReadAccessControlがfalseを返したら失敗() = runTest {
val post = TestPostFactory.create(id = 1)
whenever(postRepository.findById(PostId(1))).doReturn(post)
whenever(iPostReadAccessControl.isAllow(any(), any())).doReturn(false)
assertThrows<PermissionDeniedException> {
service.execute(GetPost(1), Anonymous)
}
}
}

View File

@ -0,0 +1,158 @@
package dev.usbharu.hideout.core.domain.service.post
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.post.TestPostFactory
import dev.usbharu.hideout.core.domain.model.post.Visibility
import dev.usbharu.hideout.core.domain.model.relationship.Relationship
import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository
import dev.usbharu.hideout.core.domain.model.support.acct.Acct
import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous
import dev.usbharu.hideout.core.domain.model.support.principal.FromApi
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.whenever
@ExtendWith(MockitoExtension::class)
class DefaultPostReadAccessControlTest {
@InjectMocks
lateinit var service: DefaultPostReadAccessControl
@Mock
lateinit var relationshipRepository: RelationshipRepository
@Test
fun ブロックされてたら見れない() = runTest {
whenever(relationshipRepository.findByActorIdAndTargetId(ActorId(1), ActorId(2))).doReturn(
Relationship(
actorId = ActorId(1),
targetActorId = ActorId(2),
following = false,
blocking = true,
muting = false,
followRequesting = false,
mutingFollowRequest = false,
)
)
val actual = service.isAllow(
TestPostFactory.create(actorId = 1),
FromApi(ActorId(2), UserDetailId(2), Acct("test", "example.com"))
)
assertFalse(actual)
}
@Test
fun PublicかUnlistedなら見れる() = runTest {
val actual = service.isAllow(TestPostFactory.create(visibility = Visibility.PUBLIC), Anonymous)
assertTrue(actual)
val actual2 = service.isAllow(TestPostFactory.create(visibility = Visibility.UNLISTED), Anonymous)
assertTrue(actual2)
}
@Test
fun FollowersかDirecのときAnonymousなら見れない() = runTest {
val actual = service.isAllow(TestPostFactory.create(visibility = Visibility.FOLLOWERS), Anonymous)
assertFalse(actual)
val actual2 = service.isAllow(TestPostFactory.create(visibility = Visibility.DIRECT), Anonymous)
assertFalse(actual2)
}
@Test
fun DirectでvisibleActorsに含まれていたら見れる() = runTest {
val actual = service.isAllow(
TestPostFactory.create(actorId = 1, visibility = Visibility.DIRECT, visibleActors = listOf(2)),
FromApi(ActorId(2), UserDetailId(2), Acct("test", "example.com"))
)
assertTrue(actual)
}
@Test
fun DirectでvisibleActorsに含まれていなかったら見れない() = runTest {
val actual = service.isAllow(
TestPostFactory.create(actorId = 1, visibility = Visibility.DIRECT, visibleActors = listOf(3)),
FromApi(ActorId(2), UserDetailId(2), Acct("test", "example.com"))
)
assertFalse(actual)
}
@Test
fun Followersでフォロワーなら見れる() = runTest {
whenever(relationshipRepository.findByActorIdAndTargetId(ActorId(1), ActorId(2))).doReturn(
Relationship.default(
actorId = ActorId(1),
targetActorId = ActorId(2)
)
)
whenever(relationshipRepository.findByActorIdAndTargetId(ActorId(2), ActorId(1))).doReturn(
Relationship(
actorId = ActorId(2),
targetActorId = ActorId(1),
following = true,
blocking = false,
muting = false,
followRequesting = false,
mutingFollowRequest = false
)
)
val actual = service.isAllow(
TestPostFactory.create(actorId = 1, visibility = Visibility.FOLLOWERS),
FromApi(ActorId(2), UserDetailId(2), Acct("test", "example.com"))
)
assertTrue(actual)
}
@Test
fun relationshipが見つからない場合見れない() = runTest {
val actual = service.isAllow(
TestPostFactory.create(actorId = 1, visibility = Visibility.FOLLOWERS),
FromApi(ActorId(2), UserDetailId(2), Acct("test", "example.com"))
)
assertFalse(actual)
}
@Test
fun フォロワーじゃない場合は見れない() = runTest {
whenever(relationshipRepository.findByActorIdAndTargetId(ActorId(1), ActorId(2))).doReturn(
Relationship.default(
actorId = ActorId(1),
targetActorId = ActorId(2)
)
)
whenever(relationshipRepository.findByActorIdAndTargetId(ActorId(2), ActorId(1))).doReturn(
Relationship(
actorId = ActorId(2),
targetActorId = ActorId(1),
following = false,
blocking = false,
muting = false,
followRequesting = false,
mutingFollowRequest = false
)
)
val actual = service.isAllow(
TestPostFactory.create(actorId = 1, visibility = Visibility.FOLLOWERS),
FromApi(ActorId(2), UserDetailId(2), Acct("test", "example.com"))
)
assertFalse(actual)
}
}