feat: QueryServiceの認可処理を追加して起動できるように

This commit is contained in:
usbharu 2024-08-11 12:33:37 +09:00
parent 540fe0eaa5
commit 146b907c69
Signed by: usbharu
GPG Key ID: 6556747BF94EEBC8
8 changed files with 110 additions and 51 deletions

View File

@ -17,12 +17,11 @@
package dev.usbharu.hideout.core.application.actor package dev.usbharu.hideout.core.application.actor
import dev.usbharu.hideout.core.application.exception.InternalServerException import dev.usbharu.hideout.core.application.exception.InternalServerException
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.actor.ActorRepository import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiRepository import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiRepository
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.model.userdetails.UserDetailId
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@ -34,10 +33,10 @@ class GetUserDetailApplicationService(
private val customEmojiRepository: CustomEmojiRepository, private val customEmojiRepository: CustomEmojiRepository,
transaction: Transaction, transaction: Transaction,
) : ) :
AbstractApplicationService<GetUserDetail, UserDetail>(transaction, Companion.logger) { LocalUserAbstractApplicationService<Unit, UserDetail>(transaction, Companion.logger) {
override suspend fun internalExecute(command: GetUserDetail, principal: Principal): UserDetail { override suspend fun internalExecute(command: Unit, principal: FromApi): UserDetail {
val userDetail = userDetailRepository.findById(UserDetailId(command.id)) val userDetail = userDetailRepository.findById(principal.userDetailId)
?: throw IllegalArgumentException("User ${command.id} does not exist") ?: throw IllegalArgumentException("User ${principal.userDetailId} does not exist")
val actor = actorRepository.findById(userDetail.actorId) val actor = actorRepository.findById(userDetail.actorId)
?: throw InternalServerException("Actor ${userDetail.actorId} not found") ?: throw InternalServerException("Actor ${userDetail.actorId} not found")
@ -47,6 +46,6 @@ class GetUserDetailApplicationService(
} }
companion object { companion object {
val logger = LoggerFactory.getLogger(GetUserDetailApplicationService::class.java) private val logger = LoggerFactory.getLogger(GetUserDetailApplicationService::class.java)
} }
} }

View File

@ -1,5 +1,6 @@
package dev.usbharu.hideout.core.application.shared package dev.usbharu.hideout.core.application.shared
import dev.usbharu.hideout.core.application.exception.PermissionDeniedException
import dev.usbharu.hideout.core.domain.model.support.principal.FromApi import dev.usbharu.hideout.core.domain.model.support.principal.FromApi
import dev.usbharu.hideout.core.domain.model.support.principal.Principal import dev.usbharu.hideout.core.domain.model.support.principal.Principal
import org.slf4j.Logger import org.slf4j.Logger
@ -7,7 +8,9 @@ import org.slf4j.Logger
abstract class LocalUserAbstractApplicationService<T : Any, R>(transaction: Transaction, logger: Logger) : abstract class LocalUserAbstractApplicationService<T : Any, R>(transaction: Transaction, logger: Logger) :
AbstractApplicationService<T, R>(transaction, logger) { AbstractApplicationService<T, R>(transaction, logger) {
override suspend fun internalExecute(command: T, principal: Principal): R { override suspend fun internalExecute(command: T, principal: Principal): R {
require(principal is FromApi) if (principal !is FromApi) {
throw PermissionDeniedException()
}
return internalExecute(command, principal) return internalExecute(command, principal)
} }

View File

@ -30,6 +30,7 @@ import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.createdAt
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.deleted import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.deleted
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.hide import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.hide
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.id import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.id
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.instanceId
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.moveTo import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.moveTo
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.overview import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.overview
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.replyId import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.replyId
@ -61,6 +62,7 @@ class ExposedPostRepository(
Posts.upsert { Posts.upsert {
it[id] = post.id.id it[id] = post.id.id
it[actorId] = post.actorId.id it[actorId] = post.actorId.id
it[instanceId] = post.instanceId.instanceId
it[overview] = post.overview?.overview it[overview] = post.overview?.overview
it[content] = post.content.content it[content] = post.content.content
it[text] = post.content.text it[text] = post.content.text
@ -106,6 +108,7 @@ class ExposedPostRepository(
Posts.batchUpsert(posts, id) { Posts.batchUpsert(posts, id) {
this[id] = it.id.id this[id] = it.id.id
this[actorId] = it.actorId.id this[actorId] = it.actorId.id
this[instanceId] = it.instanceId.instanceId
this[overview] = it.overview?.overview this[overview] = it.overview?.overview
this[content] = it.content.content this[content] = it.content.content
this[text] = it.content.text this[text] = it.content.text

View File

@ -1,7 +1,10 @@
package dev.usbharu.hideout.core.infrastructure.springframework.oauth2 package dev.usbharu.hideout.core.infrastructure.springframework.oauth2
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.support.acct.Acct 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.support.principal.FromApi
import dev.usbharu.hideout.core.domain.model.support.principal.Principal
import dev.usbharu.hideout.core.domain.model.support.principal.PrincipalContextHolder import dev.usbharu.hideout.core.domain.model.support.principal.PrincipalContextHolder
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId
import dev.usbharu.hideout.core.query.principal.PrincipalQueryService import dev.usbharu.hideout.core.query.principal.PrincipalQueryService
@ -10,18 +13,24 @@ import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
@Component @Component
class SpringSecurityOauth2PrincipalContextHolder(private val principalQueryService: PrincipalQueryService) : class SpringSecurityOauth2PrincipalContextHolder(
private val principalQueryService: PrincipalQueryService,
private val transaction: Transaction
) :
PrincipalContextHolder { PrincipalContextHolder {
override suspend fun getPrincipal(): FromApi { override suspend fun getPrincipal(): Principal {
val principal = SecurityContextHolder.getContext().authentication?.principal as Jwt val principal =
SecurityContextHolder.getContext().authentication?.principal as? Jwt ?: return Anonymous
val id = principal.getClaim<String>("uid").toLong() return transaction.transaction {
val userDetail = principalQueryService.findByUserDetailId(UserDetailId(id)) val id = principal.getClaim<String>("uid").toLong()
val userDetail = principalQueryService.findByUserDetailId(UserDetailId(id))
return FromApi( return@transaction FromApi(
userDetail.actorId, userDetail.actorId,
userDetail.userDetailId, userDetail.userDetailId,
Acct(userDetail.username, userDetail.host) Acct(userDetail.username, userDetail.host)
) )
}
} }
} }

View File

@ -71,6 +71,14 @@ create table if not exists actor_alsoknownas
constraint fk_actor_alsoknownas_actors__also_known_as foreign key ("also_known_as") references actors (id) on delete cascade on update cascade constraint fk_actor_alsoknownas_actors__also_known_as foreign key ("also_known_as") references actors (id) on delete cascade on update cascade
); );
create table timelines
(
id bigint primary key,
user_detail_id bigint not null,
name varchar(255) not null,
visibility varchar(100) not null,
is_system boolean not null default false
);
create table if not exists user_details create table if not exists user_details
( (
id bigserial primary key, id bigserial primary key,
@ -78,9 +86,14 @@ create table if not exists user_details
password varchar(255) not null, password varchar(255) not null,
auto_accept_followee_follow_request boolean not null, auto_accept_followee_follow_request boolean not null,
last_migration timestamp null default null, last_migration timestamp null default null,
constraint fk_user_details_actor_id__id foreign key (actor_id) references actors (id) on delete restrict on update restrict home_timeline_id bigint null default null,
constraint fk_user_details_actor_id__id foreign key (actor_id) references actors (id) on delete restrict on update restrict,
constraint fk_user_details_timelines_id__id foreign key (home_timeline_id) references timelines (id) on delete cascade on update cascade
); );
alter table timelines
add constraint fk_timelines_user_details__user_detail_id foreign key ("user_detail_id") references user_details (id) on delete cascade on update cascade;
create table if not exists media create table if not exists media
( (
id bigint primary key, id bigint primary key,
@ -104,6 +117,7 @@ create table if not exists posts
( (
id bigint primary key, id bigint primary key,
actor_id bigint not null, actor_id bigint not null,
instance_id bigint not null,
overview varchar(100) null, overview varchar(100) null,
content varchar(5000) not null, content varchar(5000) not null,
text varchar(3000) not null, text varchar(3000) not null,
@ -118,6 +132,8 @@ create table if not exists posts
hide boolean default false not null, hide boolean default false not null,
move_to bigint default null null move_to bigint default null null
); );
alter table posts
add constraint fk_posts_instance_id__id foreign key (instance_id) references instance (id) on delete cascade on update cascade;
alter table posts alter table posts
add constraint fk_posts_actor_id__id foreign key (actor_id) references actors (id) on delete restrict on update restrict; add constraint fk_posts_actor_id__id foreign key (actor_id) references actors (id) on delete restrict on update restrict;
alter table posts alter table posts

View File

@ -27,10 +27,7 @@ import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Status
import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Status.Visibility.* import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Status.Visibility.*
import dev.usbharu.hideout.mastodon.query.StatusQuery import dev.usbharu.hideout.mastodon.query.StatusQuery
import dev.usbharu.hideout.mastodon.query.StatusQueryService import dev.usbharu.hideout.mastodon.query.StatusQueryService
import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.andWhere
import org.jetbrains.exposed.sql.leftJoin
import org.jetbrains.exposed.sql.selectAll
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import java.net.URI import java.net.URI
import dev.usbharu.hideout.core.domain.model.media.Media as EntityMedia import dev.usbharu.hideout.core.domain.model.media.Media as EntityMedia
@ -39,6 +36,33 @@ import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.CustomEmoji a
@Suppress("IncompleteDestructuring") @Suppress("IncompleteDestructuring")
@Repository @Repository
class StatusQueryServiceImpl : StatusQueryService { class StatusQueryServiceImpl : StatusQueryService {
protected fun authorizedQuery(principal: Principal? = null): QueryAlias {
if (principal == null) {
return Posts
.selectAll()
.where {
Posts.visibility eq Visibility.PUBLIC.name or (Posts.visibility eq Visibility.UNLISTED.name)
}
.alias("authorized_table")
}
val relationshipsAlias = Relationships.alias("inverse_relationships")
return Posts
.leftJoin(PostsVisibleActors)
.leftJoin(Relationships, otherColumn = { actorId })
.leftJoin(relationshipsAlias, otherColumn = { relationshipsAlias[Relationships.actorId] })
.selectAll()
.where {
Posts.visibility eq Visibility.PUBLIC.name or
(Posts.visibility eq Visibility.UNLISTED.name) or
(Posts.visibility eq Visibility.DIRECT.name and (PostsVisibleActors.actorId eq principal.actorId.id)) or
(Posts.visibility eq Visibility.FOLLOWERS.name and (Relationships.blocking eq false and (relationshipsAlias[Relationships.following] eq true)))
}
.alias("authorized_table")
}
override suspend fun findByPostIds(ids: List<Long>): List<Status> = findByPostIdsWithMedia(ids) override suspend fun findByPostIds(ids: List<Long>): List<Status> = findByPostIdsWithMedia(ids)
override suspend fun findByPostIdsWithMediaIds(statusQueries: List<StatusQuery>): List<Status> { override suspend fun findByPostIdsWithMediaIds(statusQueries: List<StatusQuery>): List<Status> {
@ -50,10 +74,12 @@ class StatusQueryServiceImpl : StatusQueryService {
val emojiIdSet = mutableSetOf<Long>() val emojiIdSet = mutableSetOf<Long>()
emojiIdSet.addAll(statusQueries.flatMap { it.emojiIds }) emojiIdSet.addAll(statusQueries.flatMap { it.emojiIds })
val qa = authorizedQuery()
val postMap = Posts val postMap = Posts
.leftJoin(Actors) .leftJoin(Actors)
.selectAll().where { Posts.id inList postIdSet } .selectAll().where { Posts.id inList postIdSet }
.associate { it[Posts.id] to toStatus(it) } .associate { it[Posts.id] to toStatus(it, qa) }
val mediaMap = Media.selectAll().where { Media.id inList mediaIdSet } val mediaMap = Media.selectAll().where { Media.id inList mediaIdSet }
.associate { .associate {
it[Media.id] to it.toMedia().toMediaAttachments() it[Media.id] to it.toMedia().toMediaAttachments()
@ -82,7 +108,8 @@ class StatusQueryServiceImpl : StatusQueryService {
tagged: String?, tagged: String?,
includeFollowers: Boolean, includeFollowers: Boolean,
): List<Status> { ): List<Status> {
val query = Posts val qa = authorizedQuery()
val query = qa
.leftJoin(PostsMedia) .leftJoin(PostsMedia)
.leftJoin(Actors) .leftJoin(Actors)
.leftJoin(Media) .leftJoin(Media)
@ -107,7 +134,7 @@ class StatusQueryServiceImpl : StatusQueryService {
.groupBy { it[Posts.id] } .groupBy { it[Posts.id] }
.map { it.value } .map { it.value }
.map { .map {
toStatus(it.first()).copy( toStatus(it.first(), qa).copy(
mediaAttachments = it.mapNotNull { resultRow -> mediaAttachments = it.mapNotNull { resultRow ->
resultRow.toMediaOrNull()?.toMediaAttachments() resultRow.toMediaOrNull()?.toMediaAttachments()
} }
@ -119,21 +146,22 @@ class StatusQueryServiceImpl : StatusQueryService {
} }
override suspend fun findByPostId(id: Long, principal: Principal?): Status? { override suspend fun findByPostId(id: Long, principal: Principal?): Status? {
val map = Posts val aq = authorizedQuery(principal)
.leftJoin(PostsMedia) val map = aq
.leftJoin(Actors) .leftJoin(PostsMedia, { aq[Posts.id] }, { PostsMedia.postId })
.leftJoin(Media,{PostsMedia.mediaId},{Media.id}) .leftJoin(Actors, { aq[Posts.actorId] }, { Actors.id })
.leftJoin(Media, { PostsMedia.mediaId }, { Media.id })
.selectAll() .selectAll()
.where { Posts.id eq id } .where { aq[Posts.id] eq id }
.groupBy { it[Posts.id] } .groupBy { it[aq[Posts.id]] }
.map { it.value } .map { it.value }
.map { .map {
toStatus(it.first()).copy( toStatus(it.first(), aq).copy(
mediaAttachments = it.mapNotNull { resultRow -> mediaAttachments = it.mapNotNull { resultRow ->
resultRow.toMediaOrNull()?.toMediaAttachments() resultRow.toMediaOrNull()?.toMediaAttachments()
}, },
emojis = it.mapNotNull { resultRow -> resultRow.toCustomEmojiOrNull()?.toMastodonEmoji() } emojis = it.mapNotNull { resultRow -> resultRow.toCustomEmojiOrNull()?.toMastodonEmoji() }
) to it.first()[Posts.repostId] ) to it.first()[aq[Posts.repostId]]
} }
return resolveReplyAndRepost(map).singleOrNull() return resolveReplyAndRepost(map).singleOrNull()
} }
@ -160,6 +188,7 @@ class StatusQueryServiceImpl : StatusQueryService {
} }
private suspend fun findByPostIdsWithMedia(ids: List<Long>): List<Status> { private suspend fun findByPostIdsWithMedia(ids: List<Long>): List<Status> {
val qa = authorizedQuery()
val pairs = Posts val pairs = Posts
.leftJoin(PostsMedia) .leftJoin(PostsMedia)
.leftJoin(PostsEmojis) .leftJoin(PostsEmojis)
@ -170,7 +199,7 @@ class StatusQueryServiceImpl : StatusQueryService {
.groupBy { it[Posts.id] } .groupBy { it[Posts.id] }
.map { it.value } .map { it.value }
.map { .map {
toStatus(it.first()).copy( toStatus(it.first(), qa).copy(
mediaAttachments = it.mapNotNull { resultRow -> mediaAttachments = it.mapNotNull { resultRow ->
resultRow.toMediaOrNull()?.toMediaAttachments() resultRow.toMediaOrNull()?.toMediaAttachments()
}, },
@ -189,10 +218,10 @@ private fun CustomEmoji.toMastodonEmoji(): MastodonEmoji = MastodonEmoji(
category = this.category.orEmpty() category = this.category.orEmpty()
) )
private fun toStatus(it: ResultRow) = Status( private fun toStatus(it: ResultRow, queryAlias: QueryAlias) = Status(
id = it[Posts.id].toString(), id = it[queryAlias[Posts.id]].toString(),
uri = it[Posts.apId], uri = it[queryAlias[Posts.apId]],
createdAt = it[Posts.createdAt].toString(), createdAt = it[queryAlias[Posts.createdAt]].toString(),
account = Account( account = Account(
id = it[Actors.id].toString(), id = it[Actors.id].toString(),
username = it[Actors.name], username = it[Actors.name],
@ -220,15 +249,15 @@ private fun toStatus(it: ResultRow) = Status(
suspended = false, suspended = false,
limited = false limited = false
), ),
content = it[Posts.text], content = it[queryAlias[Posts.text]],
visibility = when (Visibility.valueOf(it[Posts.visibility])) { visibility = when (Visibility.valueOf(it[queryAlias[Posts.visibility]])) {
Visibility.PUBLIC -> public Visibility.PUBLIC -> public
Visibility.UNLISTED -> unlisted Visibility.UNLISTED -> unlisted
Visibility.FOLLOWERS -> private Visibility.FOLLOWERS -> private
Visibility.DIRECT -> direct Visibility.DIRECT -> direct
}, },
sensitive = it[Posts.sensitive], sensitive = it[queryAlias[Posts.sensitive]],
spoilerText = it[Posts.overview].orEmpty(), spoilerText = it[queryAlias[Posts.overview]].orEmpty(),
mediaAttachments = emptyList(), mediaAttachments = emptyList(),
mentions = emptyList(), mentions = emptyList(),
tags = emptyList(), tags = emptyList(),
@ -236,11 +265,11 @@ private fun toStatus(it: ResultRow) = Status(
reblogsCount = 0, reblogsCount = 0,
favouritesCount = 0, favouritesCount = 0,
repliesCount = 0, repliesCount = 0,
url = it[Posts.apId], url = it[queryAlias[Posts.apId]],
inReplyToId = it[Posts.replyId]?.toString(), inReplyToId = it[queryAlias[Posts.replyId]]?.toString(),
inReplyToAccountId = null, inReplyToAccountId = null,
language = null, language = null,
text = it[Posts.text], text = it[queryAlias[Posts.text]],
editedAt = null editedAt = null
) )

View File

@ -16,7 +16,6 @@
package dev.usbharu.hideout.mastodon.interfaces.api package dev.usbharu.hideout.mastodon.interfaces.api
import dev.usbharu.hideout.core.application.actor.GetUserDetail
import dev.usbharu.hideout.core.application.actor.GetUserDetailApplicationService import dev.usbharu.hideout.core.application.actor.GetUserDetailApplicationService
import dev.usbharu.hideout.core.application.relationship.acceptfollowrequest.AcceptFollowRequest import dev.usbharu.hideout.core.application.relationship.acceptfollowrequest.AcceptFollowRequest
import dev.usbharu.hideout.core.application.relationship.acceptfollowrequest.UserAcceptFollowRequestApplicationService import dev.usbharu.hideout.core.application.relationship.acceptfollowrequest.UserAcceptFollowRequestApplicationService
@ -160,7 +159,7 @@ class SpringAccountApi(
override suspend fun apiV1AccountsVerifyCredentialsGet(): ResponseEntity<CredentialAccount> { override suspend fun apiV1AccountsVerifyCredentialsGet(): ResponseEntity<CredentialAccount> {
val principal = principalContextHolder.getPrincipal() val principal = principalContextHolder.getPrincipal()
val localActor = val localActor =
getUserDetailApplicationService.execute(GetUserDetail(principal.userDetailId.id), principal) getUserDetailApplicationService.execute(Unit, principal)
return ResponseEntity.ok( return ResponseEntity.ok(
CredentialAccount( CredentialAccount(

View File

@ -54,6 +54,7 @@ class SpringStatusApi(
override suspend fun apiV1StatusesPost(statusesRequest: StatusesRequest): ResponseEntity<Status> { override suspend fun apiV1StatusesPost(statusesRequest: StatusesRequest): ResponseEntity<Status> {
val principal = principalContextHolder.getPrincipal()
val execute = registerLocalPostApplicationService.execute( val execute = registerLocalPostApplicationService.execute(
RegisterLocalPost( RegisterLocalPost(
content = statusesRequest.status.orEmpty(), content = statusesRequest.status.orEmpty(),
@ -69,12 +70,12 @@ class SpringStatusApi(
replyId = statusesRequest.inReplyToId?.toLong(), replyId = statusesRequest.inReplyToId?.toLong(),
sensitive = statusesRequest.sensitive == true, sensitive = statusesRequest.sensitive == true,
mediaIds = statusesRequest.mediaIds.orEmpty().map { it.toLong() } mediaIds = statusesRequest.mediaIds.orEmpty().map { it.toLong() }
), principalContextHolder.getPrincipal() ), principal
) )
val status = val status =
getStatusApplicationService.execute(GetStatus(execute.toString()), principalContextHolder.getPrincipal()) getStatusApplicationService.execute(GetStatus(execute.toString()), principal)
return ResponseEntity.ok( return ResponseEntity.ok(
status status
) )