Merge pull request #294 from usbharu/feature/activitypub-security

Feature/activitypub security
This commit is contained in:
usbharu 2024-04-03 14:37:01 +09:00 committed by GitHub
commit a3942a0330
25 changed files with 236 additions and 135 deletions

View File

@ -44,7 +44,7 @@ Ja15+ZWbOA4vJA9pOh3x4XM=
-----END PRIVATE KEY----- -----END PRIVATE KEY-----
', 1701398248417, ', 1701398248417,
'http://localhost/users/test-user#pubkey', 'http://localhost/users/test-user/following', 'http://localhost/users/test-user#pubkey', 'http://localhost/users/test-user/following',
'http://localhost/users/test-users/followers', null, false, 0, 0, 0, null); 'http://localhost/users/test-users/followers', 0, false, 0, 0, 0, null);
insert into user_details (actor_id, password, auto_accept_followee_follow_request) insert into user_details (actor_id, password, auto_accept_followee_follow_request)
values ( 1730415786666758144 values ( 1730415786666758144

View File

@ -7,11 +7,11 @@ VALUES (3733363, 'follow-test-user-1', 'example.com', 'follow-test-user-1-name',
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
'-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678,
'https://example.com/users/follow-test-user-1#pubkey', 'https://example.com/users/follow-test-user-1/following', 'https://example.com/users/follow-test-user-1#pubkey', 'https://example.com/users/follow-test-user-1/following',
'https://example.com/users/follow-test-user-1/followers', null, false, 0, 0, 0, null), 'https://example.com/users/follow-test-user-1/followers', 0, false, 0, 0, 0, null),
(37335363, 'follow-test-user-2', 'example.com', 'follow-test-user-2-name', '', (37335363, 'follow-test-user-2', 'example.com', 'follow-test-user-2-name', '',
'https://example.com/users/follow-test-user-2/inbox', 'https://example.com/users/follow-test-user-2/inbox',
'https://example.com/users/follow-test-user-2/outbox', 'https://example.com/users/follow-test-user-2', 'https://example.com/users/follow-test-user-2/outbox', 'https://example.com/users/follow-test-user-2',
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
'-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678,
'https://example.com/users/follow-test-user-2#pubkey', 'https://example.com/users/follow-test-user-2/following', 'https://example.com/users/follow-test-user-2#pubkey', 'https://example.com/users/follow-test-user-2/following',
'https://example.com/users/follow-test-user-2/followers', null, false, 0, 0, 0, null); 'https://example.com/users/follow-test-user-2/followers', 0, false, 0, 0, 0, null);

View File

@ -7,7 +7,7 @@ VALUES (8, 'test-user8', 'example.com', 'Im test-user8.', 'THis account is test-
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
'-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678,
'https://example.com/users/test-user8#pubkey', 'https://example.com/users/test-user8/following', 'https://example.com/users/test-user8#pubkey', 'https://example.com/users/test-user8/following',
'https://example.com/users/test-user8/followers', null, false, 0, 0, 0, null), 'https://example.com/users/test-user8/followers', 0, false, 0, 0, 0, null),
(9, 'test-user9', 'follower.example.com', 'Im test-user9.', 'THis account is test-user9.', (9, 'test-user9', 'follower.example.com', 'Im test-user9.', 'THis account is test-user9.',
'https://follower.example.com/users/test-user9/inbox', 'https://follower.example.com/users/test-user9/inbox',
'https://follower.example.com/users/test-user9/outbox', 'https://follower.example.com/users/test-user9', 'https://follower.example.com/users/test-user9/outbox', 'https://follower.example.com/users/test-user9',
@ -15,7 +15,7 @@ VALUES (8, 'test-user8', 'example.com', 'Im test-user8.', 'THis account is test-
null, 12345678, null, 12345678,
'https://follower.example.com/users/test-user9#pubkey', 'https://follower.example.com/users/test-user9#pubkey',
'https://follower.example.com/users/test-user9/following', 'https://follower.example.com/users/test-user9/following',
'https://follower.example.com/users/test-user9/followers', null, false, 0, 0, 0, null); 'https://follower.example.com/users/test-user9/followers', 0, false, 0, 0, 0, null);
insert into relationships (actor_id, target_actor_id, following, blocking, muting, follow_request, insert into relationships (actor_id, target_actor_id, following, blocking, muting, follow_request,
ignore_follow_request) ignore_follow_request)

View File

@ -7,7 +7,7 @@ VALUES (4, 'test-user4', 'example.com', 'Im test user4.', 'THis account is test
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
'-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678,
'https://example.com/users/test-user4#pubkey', 'https://example.com/users/test-user4/following', 'https://example.com/users/test-user4#pubkey', 'https://example.com/users/test-user4/following',
'https://example.com/users/test-user4/followers', null, false, 0, 0, 0, null), 'https://example.com/users/test-user4/followers', 0, false, 0, 0, 0, null),
(5, 'test-user5', 'follower.example.com', 'Im test user5.', 'THis account is test user5.', (5, 'test-user5', 'follower.example.com', 'Im test user5.', 'THis account is test user5.',
'https://follower.example.com/users/test-user5/inbox', 'https://follower.example.com/users/test-user5/inbox',
'https://follower.example.com/users/test-user5/outbox', 'https://follower.example.com/users/test-user5', 'https://follower.example.com/users/test-user5/outbox', 'https://follower.example.com/users/test-user5',
@ -15,7 +15,7 @@ VALUES (4, 'test-user4', 'example.com', 'Im test user4.', 'THis account is test
null, 12345678, null, 12345678,
'https://follower.example.com/users/test-user5#pubkey', 'https://follower.example.com/users/test-user5#pubkey',
'https://follower.example.com/users/test-user5/following', 'https://follower.example.com/users/test-user5/following',
'https://follower.example.com/users/test-user5/followers', null, false, 0, 0, 0, null); 'https://follower.example.com/users/test-user5/followers', 0, false, 0, 0, 0, null);
insert into relationships (actor_id, target_actor_id, following, blocking, muting, follow_request, insert into relationships (actor_id, target_actor_id, following, blocking, muting, follow_request,
ignore_follow_request) ignore_follow_request)

View File

@ -7,7 +7,7 @@ VALUES (6, 'test-user6', 'example.com', 'Im test-user6.', 'THis account is test-
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
'-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678,
'https://example.com/users/test-user6#pubkey', 'https://example.com/users/test-user6/following', 'https://example.com/users/test-user6#pubkey', 'https://example.com/users/test-user6/following',
'https://example.com/users/test-user6/followers', null, false, 0, 0, 0, null), 'https://example.com/users/test-user6/followers', 0, false, 0, 0, 0, null),
(7, 'test-user7', 'follower.example.com', 'Im test-user7.', 'THis account is test-user7.', (7, 'test-user7', 'follower.example.com', 'Im test-user7.', 'THis account is test-user7.',
'https://follower.example.com/users/test-user7/inbox', 'https://follower.example.com/users/test-user7/inbox',
'https://follower.example.com/users/test-user7/outbox', 'https://follower.example.com/users/test-user7', 'https://follower.example.com/users/test-user7/outbox', 'https://follower.example.com/users/test-user7',
@ -15,7 +15,7 @@ VALUES (6, 'test-user6', 'example.com', 'Im test-user6.', 'THis account is test-
null, 12345678, null, 12345678,
'https://follower.example.com/users/test-user7#pubkey', 'https://follower.example.com/users/test-user7#pubkey',
'https://follower.example.com/users/test-user7/following', 'https://follower.example.com/users/test-user7/following',
'https://follower.example.com/users/test-user7/followers', null, false, 0, 0, 0, null); 'https://follower.example.com/users/test-user7/followers', 0, false, 0, 0, 0, null);
insert into relationships (actor_id, target_actor_id, following, blocking, muting, follow_request, insert into relationships (actor_id, target_actor_id, following, blocking, muting, follow_request,
ignore_follow_request) ignore_follow_request)

View File

@ -7,7 +7,7 @@ VALUES (11, 'test-user11', 'example.com', 'Im test-user11.', 'THis account is te
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
'-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678,
'https://example.com/users/test-user11#pubkey', 'https://example.com/users/test-user11/following', 'https://example.com/users/test-user11#pubkey', 'https://example.com/users/test-user11/following',
'https://example.com/users/test-user11/followers', null, false, 0, 0, 0, null); 'https://example.com/users/test-user11/followers', 0, false, 0, 0, 0, null);
insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive, insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive,
ap_id, ap_id,

View File

@ -7,7 +7,7 @@ VALUES (10, 'test-user10', 'example.com', 'Im test-user10.', 'THis account is te
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
'-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678,
'https://example.com/users/test-user10#pubkey', 'https://example.com/users/test-user10/following', 'https://example.com/users/test-user10#pubkey', 'https://example.com/users/test-user10/following',
'https://example.com/users/test-user10/followers', null, false, 0, 0, 0, null); 'https://example.com/users/test-user10/followers', 0, false, 0, 0, 0, null);
insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive, insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive,
ap_id, ap_id,

View File

@ -7,7 +7,7 @@ VALUES (3, 'test-user3', 'example.com', 'Im test user3.', 'THis account is test
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
'-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678,
'https://example.com/users/test-user3#pubkey', 'https://example.com/users/test-user3/following', 'https://example.com/users/test-user3#pubkey', 'https://example.com/users/test-user3/following',
'https://example.com/users/test-user3/followers', null, false, 0, 0, 0, null); 'https://example.com/users/test-user3/followers', 0, false, 0, 0, 0, null);
insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive, insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive,
ap_id, ap_id,

View File

@ -7,7 +7,7 @@ VALUES (1, 'test-user', 'example.com', 'Im test user.', 'THis account is test us
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
'-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678,
'https://example.com/users/test-user#pubkey', 'https://example.com/users/test-user/following', 'https://example.com/users/test-user#pubkey', 'https://example.com/users/test-user/following',
'https://example.com/users/test-users/followers', null, false, 0, 0, 0, null); 'https://example.com/users/test-users/followers', 0, false, 0, 0, 0, null);
insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive, insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive,
ap_id, ap_id,

View File

@ -7,7 +7,7 @@ VALUES (2, 'test-user2', 'example.com', 'Im test user2.', 'THis account is test
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
'-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678,
'https://example.com/users/test-user2#pubkey', 'https://example.com/users/test-user2/following', 'https://example.com/users/test-user2#pubkey', 'https://example.com/users/test-user2/following',
'https://example.com/users/test-user2/followers', null, false, 0, 0, 0, null); 'https://example.com/users/test-user2/followers', 0, false, 0, 0, 0, null);
insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive, insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive,
ap_id, ap_id,

View File

@ -7,4 +7,4 @@ VALUES (1, 'test-user', 'example.com', 'Im test user.', 'THis account is test us
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
'-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678,
'https://example.com/users/test-user#pubkey', 'https://example.com/users/test-user/following', 'https://example.com/users/test-user#pubkey', 'https://example.com/users/test-user/following',
'https://example.com/users/test-users/followers', null, false, 0, 0, 0, null); 'https://example.com/users/test-users/followers', 0, false, 0, 0, 0, null);

View File

@ -7,4 +7,4 @@ VALUES (2, 'test-user2', 'example.com', 'Im test user.', 'THis account is test u
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
'-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678,
'https://example.com/users/test-user2#pubkey', 'https://example.com/users/test-user2/following', 'https://example.com/users/test-user2#pubkey', 'https://example.com/users/test-user2/following',
'https://example.com/users/test-user2s/followers', null, false, 0, 0, 0, null); 'https://example.com/users/test-user2s/followers', 0, false, 0, 0, 0, null);

View File

@ -18,37 +18,55 @@ package dev.usbharu.hideout.core.domain.model.actor
import dev.usbharu.hideout.application.config.ApplicationConfig import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.application.config.CharacterLimit import dev.usbharu.hideout.application.config.CharacterLimit
import jakarta.validation.Validator
import jakarta.validation.constraints.*
import org.hibernate.validator.constraints.URL
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import java.time.Instant import java.time.Instant
import kotlin.math.max import kotlin.math.max
data class Actor private constructor( data class Actor private constructor(
@get:NotNull
@get:Positive
val id: Long, val id: Long,
@get:Pattern(regexp = "^[a-zA-Z0-9_-]{1,300}\$")
@get:Size(min = 1)
val name: String, val name: String,
val domain: String, val domain: String,
val screenName: String, val screenName: String,
val description: String, val description: String,
@get:URL
val inbox: String, val inbox: String,
@get:URL
val outbox: String, val outbox: String,
@get:URL
val url: String, val url: String,
@get:NotBlank
val publicKey: String, val publicKey: String,
val privateKey: String? = null, val privateKey: String? = null,
@get:PastOrPresent
val createdAt: Instant, val createdAt: Instant,
@get:NotBlank
val keyId: String, val keyId: String,
val followers: String? = null, val followers: String? = null,
val following: String? = null, val following: String? = null,
val instance: Long? = null, @get:PositiveOrZero
val instance: Long,
val locked: Boolean, val locked: Boolean,
val followersCount: Int = 0, val followersCount: Int = 0,
val followingCount: Int = 0, val followingCount: Int = 0,
val postsCount: Int = 0, val postsCount: Int = 0,
val lastPostDate: Instant? = null, val lastPostDate: Instant? = null,
val emojis: List<Long> = emptyList() val emojis: List<Long> = emptyList(),
) { ) {
@Component @Component
class UserBuilder(private val characterLimit: CharacterLimit, private val applicationConfig: ApplicationConfig) { class UserBuilder(
private val characterLimit: CharacterLimit,
private val applicationConfig: ApplicationConfig,
private val validator: Validator,
) {
private val logger = LoggerFactory.getLogger(UserBuilder::class.java) private val logger = LoggerFactory.getLogger(UserBuilder::class.java)
@ -68,13 +86,13 @@ data class Actor private constructor(
keyId: String, keyId: String,
following: String? = null, following: String? = null,
followers: String? = null, followers: String? = null,
instance: Long? = null, instance: Long,
locked: Boolean, locked: Boolean,
followersCount: Int = 0, followersCount: Int = 0,
followingCount: Int = 0, followingCount: Int = 0,
postsCount: Int = 0, postsCount: Int = 0,
lastPostDate: Instant? = null, lastPostDate: Instant? = null,
emojis: List<Long> = emptyList() emojis: List<Long> = emptyList(),
): Actor { ): Actor {
if (id == 0L) { if (id == 0L) {
return Actor( return Actor(
@ -176,7 +194,7 @@ data class Actor private constructor(
"keyId must contain non-blank characters." "keyId must contain non-blank characters."
} }
return Actor( val actor = Actor(
id = id, id = id,
name = limitedName, name = limitedName,
domain = domain, domain = domain,
@ -199,6 +217,13 @@ data class Actor private constructor(
lastPostDate = lastPostDate, lastPostDate = lastPostDate,
emojis = emojis emojis = emojis
) )
val validate = validator.validate(actor)
for (constraintViolation in validate) {
throw IllegalArgumentException("${constraintViolation.propertyPath} : ${constraintViolation.message}")
}
return actor
} }
} }
@ -217,27 +242,27 @@ data class Actor private constructor(
fun withLastPostAt(lastPostDate: Instant): Actor = this.copy(lastPostDate = lastPostDate) fun withLastPostAt(lastPostDate: Instant): Actor = this.copy(lastPostDate = lastPostDate)
override fun toString(): String { override fun toString(): String {
return "Actor(" + return "Actor(" +
"id=$id, " + "id=$id, " +
"name='$name', " + "name='$name', " +
"domain='$domain', " + "domain='$domain', " +
"screenName='$screenName', " + "screenName='$screenName', " +
"description='$description', " + "description='$description', " +
"inbox='$inbox', " + "inbox='$inbox', " +
"outbox='$outbox', " + "outbox='$outbox', " +
"url='$url', " + "url='$url', " +
"publicKey='$publicKey', " + "publicKey='$publicKey', " +
"privateKey=$privateKey, " + "privateKey=$privateKey, " +
"createdAt=$createdAt, " + "createdAt=$createdAt, " +
"keyId='$keyId', " + "keyId='$keyId', " +
"followers=$followers, " + "followers=$followers, " +
"following=$following, " + "following=$following, " +
"instance=$instance, " + "instance=$instance, " +
"locked=$locked, " + "locked=$locked, " +
"followersCount=$followersCount, " + "followersCount=$followersCount, " +
"followingCount=$followingCount, " + "followingCount=$followingCount, " +
"postsCount=$postsCount, " + "postsCount=$postsCount, " +
"lastPostDate=$lastPostDate, " + "lastPostDate=$lastPostDate, " +
"emojis=$emojis" + "emojis=$emojis" +
")" ")"
} }
} }

View File

@ -18,31 +18,40 @@ package dev.usbharu.hideout.core.domain.model.post
import dev.usbharu.hideout.application.config.CharacterLimit import dev.usbharu.hideout.application.config.CharacterLimit
import dev.usbharu.hideout.core.service.post.PostContentFormatter import dev.usbharu.hideout.core.service.post.PostContentFormatter
import jakarta.validation.Validator
import jakarta.validation.constraints.Positive
import org.hibernate.validator.constraints.URL
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import java.time.Instant import java.time.Instant
data class Post private constructor( data class Post private constructor(
@get:Positive
val id: Long, val id: Long,
@get:Positive
val actorId: Long, val actorId: Long,
val overview: String? = null, val overview: String? = null,
val content: String, val content: String,
val text: String, val text: String,
@get:Positive
val createdAt: Long, val createdAt: Long,
val visibility: Visibility, val visibility: Visibility,
@get:URL
val url: String, val url: String,
val repostId: Long? = null, val repostId: Long? = null,
val replyId: Long? = null, val replyId: Long? = null,
val sensitive: Boolean = false, val sensitive: Boolean = false,
@get:URL
val apId: String = url, val apId: String = url,
val mediaIds: List<Long> = emptyList(), val mediaIds: List<Long> = emptyList(),
val delted: Boolean = false, val delted: Boolean = false,
val emojiIds: List<Long> = emptyList() val emojiIds: List<Long> = emptyList(),
) { ) {
@Component @Component
class PostBuilder( class PostBuilder(
private val characterLimit: CharacterLimit, private val characterLimit: CharacterLimit,
private val postContentFormatter: PostContentFormatter private val postContentFormatter: PostContentFormatter,
private val validator: Validator,
) { ) {
@Suppress("FunctionMinLength", "LongParameterList") @Suppress("FunctionMinLength", "LongParameterList")
fun of( fun of(
@ -86,7 +95,7 @@ data class Post private constructor(
require((repostId ?: 0) >= 0) { "repostId must be greater then or equal to 0." } require((repostId ?: 0) >= 0) { "repostId must be greater then or equal to 0." }
require((replyId ?: 0) >= 0) { "replyId must be greater then or equal to 0." } require((replyId ?: 0) >= 0) { "replyId must be greater then or equal to 0." }
return Post( val post = Post(
id = id, id = id,
actorId = actorId, actorId = actorId,
overview = limitedOverview, overview = limitedOverview,
@ -103,6 +112,14 @@ data class Post private constructor(
delted = false, delted = false,
emojiIds = emojiIds emojiIds = emojiIds
) )
val validate = validator.validate(post)
for (constraintViolation in validate) {
throw IllegalArgumentException("${constraintViolation.propertyPath} : ${constraintViolation.message}")
}
return post
} }
@Suppress("LongParameterList") @Suppress("LongParameterList")
@ -126,7 +143,7 @@ data class Post private constructor(
require(actorId >= 0) { "actorId must be greater than or equal to 0." } require(actorId >= 0) { "actorId must be greater than or equal to 0." }
return Post( val post = Post(
id = id, id = id,
actorId = actorId, actorId = actorId,
overview = null, overview = null,
@ -143,6 +160,14 @@ data class Post private constructor(
delted = false, delted = false,
emojiIds = emptyList() emojiIds = emptyList()
) )
val validate = validator.validate(post)
for (constraintViolation in validate) {
throw IllegalArgumentException("${constraintViolation.propertyPath} : ${constraintViolation.message}")
}
return post
} }
@Suppress("LongParameterList") @Suppress("LongParameterList")
@ -193,7 +218,7 @@ data class Post private constructor(
require((replyId ?: 0) >= 0) { "replyId must be greater then or equal to 0." } require((replyId ?: 0) >= 0) { "replyId must be greater then or equal to 0." }
return Post( val post = Post(
id = id, id = id,
actorId = actorId, actorId = actorId,
overview = limitedOverview, overview = limitedOverview,
@ -210,6 +235,14 @@ data class Post private constructor(
delted = false, delted = false,
emojiIds = emojiIds emojiIds = emojiIds
) )
val validate = validator.validate(post)
for (constraintViolation in validate) {
throw IllegalArgumentException("${constraintViolation.propertyPath} : ${constraintViolation.message}")
}
return post
} }
@Suppress("LongParameterList") @Suppress("LongParameterList")

View File

@ -167,7 +167,7 @@ object Actors : Table("actors") {
val keyId = varchar("key_id", length = 1000) val keyId = varchar("key_id", length = 1000)
val following = varchar("following", length = 1000).nullable() val following = varchar("following", length = 1000).nullable()
val followers = varchar("followers", length = 1000).nullable() val followers = varchar("followers", length = 1000).nullable()
val instance = long("instance").references(Instance.id).nullable() val instance = long("instance").references(Instance.id)
val locked = bool("locked") val locked = bool("locked")
val followingCount = integer("following_count") val followingCount = integer("following_count")
val followersCount = integer("followers_count") val followersCount = integer("followers_count")

View File

@ -37,7 +37,7 @@ interface InstanceService {
class InstanceServiceImpl( class InstanceServiceImpl(
private val instanceRepository: InstanceRepository, private val instanceRepository: InstanceRepository,
private val resourceResolveService: ResourceResolveService, private val resourceResolveService: ResourceResolveService,
@Qualifier("activitypub") private val objectMapper: ObjectMapper @Qualifier("activitypub") private val objectMapper: ObjectMapper,
) : InstanceService { ) : InstanceService {
override suspend fun fetchInstance(url: String, sharedInbox: String?): Instance { override suspend fun fetchInstance(url: String, sharedInbox: String?): Instance {
val u = URL(url) val u = URL(url)
@ -50,57 +50,53 @@ class InstanceServiceImpl(
} }
logger.info("Instance not found. try fetch instance info. url: {}", resolveInstanceUrl) logger.info("Instance not found. try fetch instance info. url: {}", resolveInstanceUrl)
@Suppress("TooGenericExceptionCaught")
try {
val nodeinfoJson = resourceResolveService.resolve("$resolveInstanceUrl/.well-known/nodeinfo").bodyAsText()
val nodeinfo = objectMapper.readValue(nodeinfoJson, Nodeinfo::class.java)
val nodeinfoPathMap = nodeinfo.links.associate { it.rel to it.href }
val nodeinfoJson = resourceResolveService.resolve("$resolveInstanceUrl/.well-known/nodeinfo").bodyAsText() for ((key, value) in nodeinfoPathMap) {
val nodeinfo = objectMapper.readValue(nodeinfoJson, Nodeinfo::class.java) when (key) {
val nodeinfoPathMap = nodeinfo.links.associate { it.rel to it.href } "http://nodeinfo.diaspora.software/ns/schema/2.0",
"http://nodeinfo.diaspora.software/ns/schema/2.1",
-> {
val nodeinfo20 = objectMapper.readValue(
resourceResolveService.resolve(value!!).bodyAsText(),
Nodeinfo2_0::class.java
)
for ((key, value) in nodeinfoPathMap) { val instanceCreateDto = InstanceCreateDto(
when (key) { name = nodeinfo20.metadata?.nodeName,
"http://nodeinfo.diaspora.software/ns/schema/2.0" -> { description = nodeinfo20.metadata?.nodeDescription,
val nodeinfo20 = objectMapper.readValue( url = resolveInstanceUrl,
resourceResolveService.resolve(value!!).bodyAsText(), iconUrl = "$resolveInstanceUrl/favicon.ico",
Nodeinfo2_0::class.java sharedInbox = sharedInbox,
) software = nodeinfo20.software?.name,
version = nodeinfo20.software?.version
)
return createNewInstance(instanceCreateDto)
}
val instanceCreateDto = InstanceCreateDto( else -> {
name = nodeinfo20.metadata?.nodeName, throw IllegalStateException("Unknown nodeinfo versions: $key url: $value")
description = nodeinfo20.metadata?.nodeDescription, }
url = resolveInstanceUrl,
iconUrl = "$resolveInstanceUrl/favicon.ico",
sharedInbox = sharedInbox,
software = nodeinfo20.software?.name,
version = nodeinfo20.software?.version
)
return createNewInstance(instanceCreateDto)
}
// TODO: 多分2.0と2.1で互換性有るのでそのまま使うけどなおす
"http://nodeinfo.diaspora.software/ns/schema/2.1" -> {
val nodeinfo20 = objectMapper.readValue(
resourceResolveService.resolve(value!!).bodyAsText(),
Nodeinfo2_0::class.java
)
val instanceCreateDto = InstanceCreateDto(
name = nodeinfo20.metadata?.nodeName,
description = nodeinfo20.metadata?.nodeDescription,
url = resolveInstanceUrl,
iconUrl = "$resolveInstanceUrl/favicon.ico",
sharedInbox = sharedInbox,
software = nodeinfo20.software?.name,
version = nodeinfo20.software?.version
)
return createNewInstance(instanceCreateDto)
}
else -> {
throw IllegalStateException("Unknown nodeinfo versions: $key url: $value")
} }
} }
} catch (e: Exception) {
logger.warn("FAILED Fetch Instance", e)
} }
return createNewInstance(
throw IllegalStateException("Nodeinfo aren't found.") InstanceCreateDto(
name = null,
description = null,
url = resolveInstanceUrl,
iconUrl = "$resolveInstanceUrl/favicon.ico",
sharedInbox = null,
software = null,
version = null
)
)
} }
override suspend fun createNewInstance(instanceCreateDto: InstanceCreateDto): Instance { override suspend fun createNewInstance(instanceCreateDto: InstanceCreateDto): Instance {

View File

@ -78,7 +78,8 @@ class UserServiceImpl(
following = "$userUrl/following", following = "$userUrl/following",
followers = "$userUrl/followers", followers = "$userUrl/followers",
keyId = "$userUrl#pubkey", keyId = "$userUrl#pubkey",
locked = false locked = false,
instance = 0
) )
val save = actorRepository.save(userEntity) val save = actorRepository.save(userEntity)
userDetailRepository.save(UserDetail(nextId, hashedPassword, true)) userDetailRepository.save(UserDetail(nextId, hashedPassword, true))
@ -95,13 +96,7 @@ class UserServiceImpl(
throw IllegalStateException("Cannot create Deleted actor.") throw IllegalStateException("Cannot create Deleted actor.")
} }
@Suppress("TooGenericExceptionCaught") val instance = instanceService.fetchInstance(user.url, user.sharedInbox)
val instance = try {
instanceService.fetchInstance(user.url, user.sharedInbox)
} catch (e: Exception) {
logger.warn("FAILED to fetch instance. url: {}", user.url, e)
null
}
val nextId = actorRepository.nextId() val nextId = actorRepository.nextId()
val userEntity = actorBuilder.of( val userEntity = actorBuilder.of(
@ -118,7 +113,7 @@ class UserServiceImpl(
followers = user.followers, followers = user.followers,
following = user.following, following = user.following,
keyId = user.keyId, keyId = user.keyId,
instance = instance?.id, instance = instance.id,
locked = user.locked ?: false locked = user.locked ?: false
) )
return try { return try {

View File

@ -31,6 +31,7 @@ import org.springframework.security.web.SecurityFilterChain
class MastodonApiSecurityConfig { class MastodonApiSecurityConfig {
@Bean @Bean
@Order(4) @Order(4)
@Suppress("LongMethod")
fun mastodonApiSecurityFilterChain( fun mastodonApiSecurityFilterChain(
http: HttpSecurity, http: HttpSecurity,
rf: RoleHierarchyAuthorizationManagerFactory, rf: RoleHierarchyAuthorizationManagerFactory,

View File

@ -45,7 +45,7 @@ create table if not exists actors
key_id varchar(1000) not null, key_id varchar(1000) not null,
"following" varchar(1000) null, "following" varchar(1000) null,
followers varchar(1000) null, followers varchar(1000) null,
"instance" bigint null, "instance" bigint not null,
locked boolean not null, locked boolean not null,
following_count int not null, following_count int not null,
followers_count int not null, followers_count int not null,
@ -240,10 +240,14 @@ create table if not exists relationships
unique (actor_id, target_actor_id) unique (actor_id, target_actor_id)
); );
insert into instance (id, name, description, url, icon_url, shared_inbox, software, version, is_blocked, is_muted,
moderation_note, created_at)
values (0, 'system', '', '', '', null, '', '', false, false, '', current_timestamp);
insert into actors (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key, created_at, insert into actors (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key, created_at,
key_id, following, followers, instance, locked, following_count, followers_count, posts_count, key_id, following, followers, instance, locked, following_count, followers_count, posts_count,
last_post_at) last_post_at)
values (0, 'ghost', '', '', '', '', '', '', '', null, 0, '', '', '', null, true, 0, 0, 0, null); values (0, 'ghost', '', '', '', '', '', '', '', null, 0, '', '', '', 0, true, 0, 0, 0, null);
create table if not exists deleted_actors create table if not exists deleted_actors
( (

View File

@ -45,6 +45,7 @@ import io.ktor.http.*
import io.ktor.http.content.* import io.ktor.http.content.*
import io.ktor.util.* import io.ktor.util.*
import io.ktor.util.date.* import io.ktor.util.date.*
import jakarta.validation.Validation
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -60,7 +61,10 @@ import java.time.Instant
class APNoteServiceImplTest { class APNoteServiceImplTest {
val postBuilder = Post.PostBuilder(CharacterLimit(), DefaultPostContentFormatter(HtmlSanitizeConfig().policy())) val postBuilder = Post.PostBuilder(
CharacterLimit(), DefaultPostContentFormatter(HtmlSanitizeConfig().policy()),
Validation.buildDefaultValidatorFactory().validator
)
@Test @Test
fun `fetchNote(String,String) ートが既に存在する場合はDBから取得したものを返す`() = runTest { fun `fetchNote(String,String) ートが既に存在する場合はDBから取得したものを返す`() = runTest {
@ -89,10 +93,7 @@ class APNoteServiceImplTest {
apUserService = mock(), apUserService = mock(),
postService = mock(), postService = mock(),
apResourceResolveService = mock(), apResourceResolveService = mock(),
postBuilder = Post.PostBuilder( postBuilder = postBuilder,
CharacterLimit(),
DefaultPostContentFormatter(HtmlSanitizeConfig().policy())
),
noteQueryService = noteQueryService, noteQueryService = noteQueryService,
mock(), mock(),
mock(), mock(),
@ -163,10 +164,7 @@ class APNoteServiceImplTest {
apUserService = apUserService, apUserService = apUserService,
postService = mock(), postService = mock(),
apResourceResolveService = apResourceResolveService, apResourceResolveService = apResourceResolveService,
postBuilder = Post.PostBuilder( postBuilder = postBuilder,
CharacterLimit(),
DefaultPostContentFormatter(HtmlSanitizeConfig().policy())
),
noteQueryService = noteQueryService, noteQueryService = noteQueryService,
mock(), mock(),
mock { }, mock { },
@ -216,14 +214,11 @@ class APNoteServiceImplTest {
apUserService = mock(), apUserService = mock(),
postService = mock(), postService = mock(),
apResourceResolveService = apResourceResolveService, apResourceResolveService = apResourceResolveService,
postBuilder = Post.PostBuilder( postBuilder = postBuilder,
CharacterLimit(),
DefaultPostContentFormatter(HtmlSanitizeConfig().policy())
),
noteQueryService = noteQueryService, noteQueryService = noteQueryService,
mock(), mock(),
mock(), mock(),
mock { } mock { }
) )
assertThrows<FailedToGetActivityPubResourceException> { apNoteServiceImpl.fetchNote(url) } assertThrows<FailedToGetActivityPubResourceException> { apNoteServiceImpl.fetchNote(url) }

View File

@ -0,0 +1,13 @@
package dev.usbharu.hideout.core.domain.model.actor
import org.junit.jupiter.api.Test
import utils.UserBuilder
class ActorTest {
@Test
fun validator() {
org.junit.jupiter.api.assertThrows<IllegalArgumentException> {
UserBuilder.localUserOf(name = "うんこ")
}
}
}

View File

@ -26,6 +26,7 @@ import dev.usbharu.hideout.core.domain.model.post.Post
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.reaction.ReactionRepository import dev.usbharu.hideout.core.domain.model.reaction.ReactionRepository
import dev.usbharu.hideout.core.service.timeline.TimelineService import dev.usbharu.hideout.core.service.timeline.TimelineService
import jakarta.validation.Validation
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@ -56,7 +57,7 @@ class PostServiceImplTest {
private var postBuilder: Post.PostBuilder = Post.PostBuilder( private var postBuilder: Post.PostBuilder = Post.PostBuilder(
CharacterLimit(), DefaultPostContentFormatter( CharacterLimit(), DefaultPostContentFormatter(
HtmlSanitizeConfig().policy() HtmlSanitizeConfig().policy()
) ), Validation.buildDefaultValidatorFactory().validator
) )
@Mock @Mock

View File

@ -20,11 +20,11 @@ package dev.usbharu.hideout.core.service.user
import dev.usbharu.hideout.application.config.ApplicationConfig import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.application.config.CharacterLimit import dev.usbharu.hideout.application.config.CharacterLimit
import dev.usbharu.hideout.application.config.HtmlSanitizeConfig
import dev.usbharu.hideout.core.domain.model.actor.Actor import dev.usbharu.hideout.core.domain.model.actor.Actor
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.Post import dev.usbharu.hideout.core.domain.model.instance.Instance
import dev.usbharu.hideout.core.service.post.DefaultPostContentFormatter import dev.usbharu.hideout.core.service.instance.InstanceService
import jakarta.validation.Validation
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@ -33,12 +33,17 @@ import org.mockito.kotlin.*
import utils.TestApplicationConfig.testApplicationConfig import utils.TestApplicationConfig.testApplicationConfig
import java.net.URL import java.net.URL
import java.security.KeyPairGenerator import java.security.KeyPairGenerator
import java.time.Instant
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNull import kotlin.test.assertNull
class ActorServiceTest { class ActorServiceTest {
val actorBuilder = Actor.UserBuilder(CharacterLimit(), ApplicationConfig(URL("https://example.com"))) val actorBuilder = Actor.UserBuilder(
val postBuilder = Post.PostBuilder(CharacterLimit(), DefaultPostContentFormatter(HtmlSanitizeConfig().policy())) CharacterLimit(),
ApplicationConfig(URL("https://example.com")),
Validation.buildDefaultValidatorFactory().validator
)
@Test @Test
fun `createLocalUser ローカルユーザーを作成できる`() = runTest { fun `createLocalUser ローカルユーザーを作成できる`() = runTest {
@ -89,13 +94,34 @@ class ActorServiceTest {
onBlocking { nextId() } doReturn 113345L onBlocking { nextId() } doReturn 113345L
} }
val instanceService = mock<InstanceService> {
onBlocking {
fetchInstance(
eq("https://remote.example.com"),
isNull()
)
} doReturn Instance(
12345L,
"",
"",
"https://remote.example.com",
"https://remote.example.com/favicon.ico",
null,
"unknown",
"",
false,
false,
"",
Instant.now()
)
}
val userService = val userService =
UserServiceImpl( UserServiceImpl(
actorRepository = actorRepository, actorRepository = actorRepository,
userAuthService = mock(), userAuthService = mock(),
actorBuilder = actorBuilder, actorBuilder = actorBuilder,
applicationConfig = testApplicationConfig, applicationConfig = testApplicationConfig,
instanceService = mock(), instanceService = instanceService,
userDetailRepository = mock(), userDetailRepository = mock(),
deletedActorRepository = mock(), deletedActorRepository = mock(),
reactionRepository = mock(), reactionRepository = mock(),

View File

@ -22,13 +22,18 @@ import dev.usbharu.hideout.application.service.id.TwitterSnowflakeIdGenerateServ
import dev.usbharu.hideout.core.domain.model.post.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.post.Visibility
import dev.usbharu.hideout.core.service.post.DefaultPostContentFormatter import dev.usbharu.hideout.core.service.post.DefaultPostContentFormatter
import jakarta.validation.Validation
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.time.Instant import java.time.Instant
object PostBuilder { object PostBuilder {
private val postBuilder = private val postBuilder =
Post.PostBuilder(CharacterLimit(), DefaultPostContentFormatter(HtmlSanitizeConfig().policy())) Post.PostBuilder(
CharacterLimit(),
DefaultPostContentFormatter(HtmlSanitizeConfig().policy()),
Validation.buildDefaultValidatorFactory().validator
)
private val idGenerator = TwitterSnowflakeIdGenerateService private val idGenerator = TwitterSnowflakeIdGenerateService
@ -39,7 +44,7 @@ object PostBuilder {
text: String = "Hello World", text: String = "Hello World",
createdAt: Long = Instant.now().toEpochMilli(), createdAt: Long = Instant.now().toEpochMilli(),
visibility: Visibility = Visibility.PUBLIC, visibility: Visibility = Visibility.PUBLIC,
url: String = "https://example.com/users/$userId/posts/$id" url: String = "https://example.com/users/$userId/posts/$id",
): Post { ): Post {
return postBuilder.of( return postBuilder.of(
id = id, id = id,

View File

@ -20,12 +20,16 @@ import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.application.config.CharacterLimit import dev.usbharu.hideout.application.config.CharacterLimit
import dev.usbharu.hideout.application.service.id.TwitterSnowflakeIdGenerateService import dev.usbharu.hideout.application.service.id.TwitterSnowflakeIdGenerateService
import dev.usbharu.hideout.core.domain.model.actor.Actor import dev.usbharu.hideout.core.domain.model.actor.Actor
import jakarta.validation.Validation
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.net.URL import java.net.URL
import java.time.Instant import java.time.Instant
object UserBuilder { object UserBuilder {
private val actorBuilder = Actor.UserBuilder(CharacterLimit(), ApplicationConfig(URL("https://example.com"))) private val actorBuilder = Actor.UserBuilder(
CharacterLimit(), ApplicationConfig(URL("https://example.com")),
Validation.buildDefaultValidatorFactory().validator
)
private val idGenerator = TwitterSnowflakeIdGenerateService private val idGenerator = TwitterSnowflakeIdGenerateService
@ -43,7 +47,7 @@ object UserBuilder {
createdAt: Instant = Instant.now(), createdAt: Instant = Instant.now(),
keyId: String = "https://$domain/users/$id#pubkey", keyId: String = "https://$domain/users/$id#pubkey",
followers: String = "https://$domain/users/$id/followers", followers: String = "https://$domain/users/$id/followers",
following: String = "https://$domain/users/$id/following" following: String = "https://$domain/users/$id/following",
): Actor { ): Actor {
return actorBuilder.of( return actorBuilder.of(
id = id, id = id,
@ -60,7 +64,8 @@ object UserBuilder {
keyId = keyId, keyId = keyId,
followers = followers, followers = followers,
following = following, following = following,
locked = false locked = false,
instance = 0
) )
} }
@ -77,7 +82,8 @@ object UserBuilder {
createdAt: Instant = Instant.now(), createdAt: Instant = Instant.now(),
keyId: String = "https://$domain/$id#pubkey", keyId: String = "https://$domain/$id#pubkey",
followers: String = "https://$domain/$id/followers", followers: String = "https://$domain/$id/followers",
following: String = "https://$domain/$id/following" following: String = "https://$domain/$id/following",
instanceId: Long = generateId(),
): Actor { ): Actor {
return actorBuilder.of( return actorBuilder.of(
id = id, id = id,
@ -94,7 +100,8 @@ object UserBuilder {
keyId = keyId, keyId = keyId,
followers = followers, followers = followers,
following = following, following = following,
locked = false locked = false,
instance = instanceId
) )
} }