mirror of https://github.com/usbharu/Hideout.git
Merge pull request #92 from usbharu/feature/ap-posts-endpoint
Feature: 投稿のURLにアクセスすると投稿を取得できるように
This commit is contained in:
commit
16f67dc040
|
@ -26,6 +26,7 @@ import org.springframework.context.annotation.Configuration
|
||||||
import org.springframework.context.annotation.Primary
|
import org.springframework.context.annotation.Primary
|
||||||
import org.springframework.core.annotation.Order
|
import org.springframework.core.annotation.Order
|
||||||
import org.springframework.http.HttpMethod
|
import org.springframework.http.HttpMethod
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder
|
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder
|
||||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
|
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
|
||||||
import org.springframework.security.authentication.AccountStatusUserDetailsChecker
|
import org.springframework.security.authentication.AccountStatusUserDetailsChecker
|
||||||
|
@ -45,9 +46,13 @@ import org.springframework.security.oauth2.server.authorization.settings.Authori
|
||||||
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext
|
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext
|
||||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer
|
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer
|
||||||
import org.springframework.security.web.SecurityFilterChain
|
import org.springframework.security.web.SecurityFilterChain
|
||||||
|
import org.springframework.security.web.access.ExceptionTranslationFilter
|
||||||
|
import org.springframework.security.web.authentication.AuthenticationEntryPointFailureHandler
|
||||||
|
import org.springframework.security.web.authentication.HttpStatusEntryPoint
|
||||||
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint
|
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint
|
||||||
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider
|
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider
|
||||||
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher
|
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher
|
||||||
|
import org.springframework.security.web.util.matcher.AnyRequestMatcher
|
||||||
import org.springframework.web.servlet.handler.HandlerMappingIntrospector
|
import org.springframework.web.servlet.handler.HandlerMappingIntrospector
|
||||||
import java.security.KeyPairGenerator
|
import java.security.KeyPairGenerator
|
||||||
import java.security.interfaces.RSAPrivateKey
|
import java.security.interfaces.RSAPrivateKey
|
||||||
|
@ -69,17 +74,30 @@ class SecurityConfig {
|
||||||
@Bean
|
@Bean
|
||||||
@Order(1)
|
@Order(1)
|
||||||
fun httpSignatureFilterChain(http: HttpSecurity, httpSignatureFilter: HttpSignatureFilter): SecurityFilterChain {
|
fun httpSignatureFilterChain(http: HttpSecurity, httpSignatureFilter: HttpSignatureFilter): SecurityFilterChain {
|
||||||
http.securityMatcher("/inbox", "/outbox", "/users/*/inbox", "/users/*/outbox")
|
http
|
||||||
|
.securityMatcher("/inbox", "/outbox", "/users/*/inbox", "/users/*/outbox", "/users/*/posts/*")
|
||||||
.addFilter(httpSignatureFilter)
|
.addFilter(httpSignatureFilter)
|
||||||
|
.addFilterBefore(
|
||||||
|
ExceptionTranslationFilter(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)),
|
||||||
|
HttpSignatureFilter::class.java
|
||||||
|
)
|
||||||
.authorizeHttpRequests {
|
.authorizeHttpRequests {
|
||||||
it.anyRequest().permitAll()
|
it.anyRequest().permitAll()
|
||||||
}
|
}
|
||||||
.csrf {
|
.csrf {
|
||||||
it.disable()
|
it.disable()
|
||||||
}
|
}
|
||||||
|
.exceptionHandling {
|
||||||
|
it.authenticationEntryPoint(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
|
||||||
|
it.defaultAuthenticationEntryPointFor(
|
||||||
|
HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
|
||||||
|
AnyRequestMatcher.INSTANCE
|
||||||
|
)
|
||||||
|
}
|
||||||
.sessionManagement {
|
.sessionManagement {
|
||||||
it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
return http.build()
|
return http.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,6 +105,12 @@ class SecurityConfig {
|
||||||
fun getHttpSignatureFilter(authenticationManager: AuthenticationManager): HttpSignatureFilter {
|
fun getHttpSignatureFilter(authenticationManager: AuthenticationManager): HttpSignatureFilter {
|
||||||
val httpSignatureFilter = HttpSignatureFilter(DefaultSignatureHeaderParser())
|
val httpSignatureFilter = HttpSignatureFilter(DefaultSignatureHeaderParser())
|
||||||
httpSignatureFilter.setAuthenticationManager(authenticationManager)
|
httpSignatureFilter.setAuthenticationManager(authenticationManager)
|
||||||
|
httpSignatureFilter.setContinueFilterChainOnUnsuccessfulAuthentication(false)
|
||||||
|
val authenticationEntryPointFailureHandler =
|
||||||
|
AuthenticationEntryPointFailureHandler(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
|
||||||
|
authenticationEntryPointFailureHandler.setRethrowAuthenticationServiceException(false)
|
||||||
|
httpSignatureFilter.setAuthenticationFailureHandler(authenticationEntryPointFailureHandler)
|
||||||
|
|
||||||
return httpSignatureFilter
|
return httpSignatureFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,8 +123,7 @@ class SecurityConfig {
|
||||||
HttpSignatureVerifierComposite(
|
HttpSignatureVerifierComposite(
|
||||||
mapOf(
|
mapOf(
|
||||||
"rsa-sha256" to RsaSha256HttpSignatureVerifier(
|
"rsa-sha256" to RsaSha256HttpSignatureVerifier(
|
||||||
DefaultSignatureHeaderParser(),
|
DefaultSignatureHeaderParser(), RsaSha256HttpSignatureSigner()
|
||||||
RsaSha256HttpSignatureSigner()
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
DefaultSignatureHeaderParser()
|
DefaultSignatureHeaderParser()
|
||||||
|
@ -118,15 +141,13 @@ class SecurityConfig {
|
||||||
val builder = MvcRequestMatcher.Builder(introspector)
|
val builder = MvcRequestMatcher.Builder(introspector)
|
||||||
|
|
||||||
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http)
|
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http)
|
||||||
http
|
http.exceptionHandling {
|
||||||
.exceptionHandling {
|
it.authenticationEntryPoint(
|
||||||
it.authenticationEntryPoint(
|
LoginUrlAuthenticationEntryPoint("/login")
|
||||||
LoginUrlAuthenticationEntryPoint("/login")
|
)
|
||||||
)
|
}.oauth2ResourceServer {
|
||||||
}
|
it.jwt(Customizer.withDefaults())
|
||||||
.oauth2ResourceServer {
|
}
|
||||||
it.jwt(Customizer.withDefaults())
|
|
||||||
}
|
|
||||||
return http.build()
|
return http.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,43 +156,37 @@ class SecurityConfig {
|
||||||
fun defaultSecurityFilterChain(http: HttpSecurity, introspector: HandlerMappingIntrospector): SecurityFilterChain {
|
fun defaultSecurityFilterChain(http: HttpSecurity, introspector: HandlerMappingIntrospector): SecurityFilterChain {
|
||||||
val builder = MvcRequestMatcher.Builder(introspector)
|
val builder = MvcRequestMatcher.Builder(introspector)
|
||||||
|
|
||||||
http
|
http.authorizeHttpRequests {
|
||||||
.authorizeHttpRequests {
|
it.requestMatchers(PathRequest.toH2Console()).permitAll()
|
||||||
it.requestMatchers(PathRequest.toH2Console()).permitAll()
|
it.requestMatchers(
|
||||||
it.requestMatchers(
|
builder.pattern("/inbox"),
|
||||||
builder.pattern("/inbox"),
|
builder.pattern("/users/*/inbox"),
|
||||||
builder.pattern("/users/*/inbox"),
|
builder.pattern("/api/v1/apps"),
|
||||||
builder.pattern("/api/v1/apps"),
|
builder.pattern("/api/v1/instance/**"),
|
||||||
builder.pattern("/api/v1/instance/**"),
|
builder.pattern("/.well-known/**"),
|
||||||
builder.pattern("/.well-known/**"),
|
builder.pattern("/error"),
|
||||||
builder.pattern("/error"),
|
builder.pattern("/nodeinfo/2.0")
|
||||||
builder.pattern("/nodeinfo/2.0")
|
).permitAll()
|
||||||
).permitAll()
|
it.requestMatchers(
|
||||||
it.requestMatchers(
|
builder.pattern("/auth/**")
|
||||||
builder.pattern("/auth/**")
|
).anonymous()
|
||||||
).anonymous()
|
it.requestMatchers(builder.pattern("/change-password")).authenticated()
|
||||||
it.requestMatchers(builder.pattern("/change-password")).authenticated()
|
it.requestMatchers(builder.pattern("/api/v1/accounts/verify_credentials"))
|
||||||
it.requestMatchers(builder.pattern("/api/v1/accounts/verify_credentials"))
|
.hasAnyAuthority("SCOPE_read", "SCOPE_read:accounts")
|
||||||
.hasAnyAuthority("SCOPE_read", "SCOPE_read:accounts")
|
it.anyRequest().permitAll()
|
||||||
it.anyRequest().permitAll()
|
}
|
||||||
}
|
http.oauth2ResourceServer {
|
||||||
http
|
it.jwt(Customizer.withDefaults())
|
||||||
.oauth2ResourceServer {
|
}.passwordManagement { }.formLogin(Customizer.withDefaults()).csrf {
|
||||||
it.jwt(Customizer.withDefaults())
|
it.ignoringRequestMatchers(builder.pattern("/users/*/inbox"))
|
||||||
}
|
it.ignoringRequestMatchers(builder.pattern(HttpMethod.POST, "/api/v1/apps"))
|
||||||
.passwordManagement { }
|
it.ignoringRequestMatchers(builder.pattern("/inbox"))
|
||||||
.formLogin(Customizer.withDefaults())
|
it.ignoringRequestMatchers(PathRequest.toH2Console())
|
||||||
.csrf {
|
}.headers {
|
||||||
it.ignoringRequestMatchers(builder.pattern("/users/*/inbox"))
|
it.frameOptions {
|
||||||
it.ignoringRequestMatchers(builder.pattern(HttpMethod.POST, "/api/v1/apps"))
|
it.sameOrigin()
|
||||||
it.ignoringRequestMatchers(builder.pattern("/inbox"))
|
|
||||||
it.ignoringRequestMatchers(PathRequest.toH2Console())
|
|
||||||
}
|
|
||||||
.headers {
|
|
||||||
it.frameOptions {
|
|
||||||
it.sameOrigin()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return http.build()
|
return http.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,11 +201,7 @@ class SecurityConfig {
|
||||||
val generateKeyPair = keyPairGenerator.generateKeyPair()
|
val generateKeyPair = keyPairGenerator.generateKeyPair()
|
||||||
val rsaPublicKey = generateKeyPair.public as RSAPublicKey
|
val rsaPublicKey = generateKeyPair.public as RSAPublicKey
|
||||||
val rsaPrivateKey = generateKeyPair.private as RSAPrivateKey
|
val rsaPrivateKey = generateKeyPair.private as RSAPrivateKey
|
||||||
val rsaKey = RSAKey
|
val rsaKey = RSAKey.Builder(rsaPublicKey).privateKey(rsaPrivateKey).keyID(UUID.randomUUID().toString()).build()
|
||||||
.Builder(rsaPublicKey)
|
|
||||||
.privateKey(rsaPrivateKey)
|
|
||||||
.keyID(UUID.randomUUID().toString())
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val jwkSet = JWKSet(rsaKey)
|
val jwkSet = JWKSet(rsaKey)
|
||||||
return ImmutableJWKSet(jwkSet)
|
return ImmutableJWKSet(jwkSet)
|
||||||
|
@ -200,9 +211,7 @@ class SecurityConfig {
|
||||||
@ConditionalOnProperty(name = ["hideout.security.jwt.generate"], havingValue = "")
|
@ConditionalOnProperty(name = ["hideout.security.jwt.generate"], havingValue = "")
|
||||||
fun loadJwkSource(jwkConfig: JwkConfig): JWKSource<SecurityContext> {
|
fun loadJwkSource(jwkConfig: JwkConfig): JWKSource<SecurityContext> {
|
||||||
val rsaKey = RSAKey.Builder(RsaUtil.decodeRsaPublicKey(jwkConfig.publicKey))
|
val rsaKey = RSAKey.Builder(RsaUtil.decodeRsaPublicKey(jwkConfig.publicKey))
|
||||||
.privateKey(RsaUtil.decodeRsaPrivateKey(jwkConfig.privateKey))
|
.privateKey(RsaUtil.decodeRsaPrivateKey(jwkConfig.privateKey)).keyID(jwkConfig.keyId).build()
|
||||||
.keyID(jwkConfig.keyId)
|
|
||||||
.build()
|
|
||||||
return ImmutableJWKSet(JWKSet(rsaKey))
|
return ImmutableJWKSet(JWKSet(rsaKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,11 +221,8 @@ class SecurityConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun authorizationServerSettings(): AuthorizationServerSettings {
|
fun authorizationServerSettings(): AuthorizationServerSettings {
|
||||||
return AuthorizationServerSettings.builder()
|
return AuthorizationServerSettings.builder().authorizationEndpoint("/oauth/authorize")
|
||||||
.authorizationEndpoint("/oauth/authorize")
|
.tokenEndpoint("/oauth/token").tokenRevocationEndpoint("/oauth/revoke").build()
|
||||||
.tokenEndpoint("/oauth/token")
|
|
||||||
.tokenRevocationEndpoint("/oauth/revoke")
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
@ -239,8 +245,7 @@ class SecurityConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun mappingJackson2HttpMessageConverter(): MappingJackson2HttpMessageConverter {
|
fun mappingJackson2HttpMessageConverter(): MappingJackson2HttpMessageConverter {
|
||||||
val builder = Jackson2ObjectMapperBuilder()
|
val builder = Jackson2ObjectMapperBuilder().serializationInclusion(JsonInclude.Include.NON_NULL)
|
||||||
.serializationInclusion(JsonInclude.Include.NON_NULL)
|
|
||||||
return MappingJackson2HttpMessageConverter(builder.build())
|
return MappingJackson2HttpMessageConverter(builder.build())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package dev.usbharu.hideout.controller
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.model.ap.Note
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.security.core.annotation.CurrentSecurityContext
|
||||||
|
import org.springframework.security.core.context.SecurityContext
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
|
|
||||||
|
interface NoteApController {
|
||||||
|
@GetMapping("/users/*/posts/{postId}")
|
||||||
|
suspend fun postsAp(
|
||||||
|
@PathVariable("postId") postId: Long,
|
||||||
|
@CurrentSecurityContext context: SecurityContext
|
||||||
|
): ResponseEntity<Note>
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package dev.usbharu.hideout.controller
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.model.ap.Note
|
||||||
|
import dev.usbharu.hideout.service.api.NoteApApiService
|
||||||
|
import dev.usbharu.hideout.service.signature.HttpSignatureUser
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.security.core.annotation.CurrentSecurityContext
|
||||||
|
import org.springframework.security.core.context.SecurityContext
|
||||||
|
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
class NoteApControllerImpl(private val noteApApiService: NoteApApiService) : NoteApController {
|
||||||
|
override suspend fun postsAp(
|
||||||
|
@PathVariable(value = "postId") postId: Long,
|
||||||
|
@CurrentSecurityContext context: SecurityContext
|
||||||
|
): ResponseEntity<Note> {
|
||||||
|
val userId =
|
||||||
|
if (context.authentication is PreAuthenticatedAuthenticationToken && context.authentication.details is HttpSignatureUser) {
|
||||||
|
(context.authentication.details as HttpSignatureUser).id
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val note = noteApApiService.getNote(postId, userId)
|
||||||
|
if (note != null) {
|
||||||
|
return ResponseEntity.ok(note)
|
||||||
|
}
|
||||||
|
return ResponseEntity.notFound().build()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,11 @@
|
||||||
package dev.usbharu.hideout.exception
|
package dev.usbharu.hideout.exception
|
||||||
|
|
||||||
import java.io.Serial
|
import java.io.Serial
|
||||||
|
import javax.naming.AuthenticationException
|
||||||
|
|
||||||
class HttpSignatureVerifyException : IllegalArgumentException {
|
class HttpSignatureVerifyException : AuthenticationException {
|
||||||
constructor() : super()
|
constructor() : super()
|
||||||
constructor(s: String?) : super(s)
|
constructor(s: String?) : super(s)
|
||||||
constructor(message: String?, cause: Throwable?) : super(message, cause)
|
|
||||||
constructor(cause: Throwable?) : super(cause)
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@Serial
|
@Serial
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
package dev.usbharu.hideout.query
|
package dev.usbharu.hideout.query
|
||||||
|
|
||||||
import dev.usbharu.hideout.domain.model.hideout.entity.User
|
import dev.usbharu.hideout.domain.model.hideout.entity.User
|
||||||
import org.springframework.stereotype.Repository
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
interface FollowerQueryService {
|
interface FollowerQueryService {
|
||||||
suspend fun findFollowersById(id: Long): List<User>
|
suspend fun findFollowersById(id: Long): List<User>
|
||||||
suspend fun findFollowersByNameAndDomain(name: String, domain: String): List<User>
|
suspend fun findFollowersByNameAndDomain(name: String, domain: String): List<User>
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package dev.usbharu.hideout.query.activitypub
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.model.ap.Note
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.entity.Post
|
||||||
|
|
||||||
|
interface NoteQueryService {
|
||||||
|
suspend fun findById(id: Long): Pair<Note, Post>
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package dev.usbharu.hideout.query.activitypub
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.model.ap.Note
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.entity.Post
|
||||||
|
import dev.usbharu.hideout.exception.FailedToGetResourcesException
|
||||||
|
import dev.usbharu.hideout.repository.Posts
|
||||||
|
import dev.usbharu.hideout.repository.Users
|
||||||
|
import dev.usbharu.hideout.repository.toPost
|
||||||
|
import dev.usbharu.hideout.util.singleOr
|
||||||
|
import org.jetbrains.exposed.sql.ResultRow
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
class NoteQueryServiceImpl : NoteQueryService {
|
||||||
|
override suspend fun findById(id: Long): Pair<Note, Post> {
|
||||||
|
return Posts
|
||||||
|
.leftJoin(Users)
|
||||||
|
.select { Posts.id eq id }
|
||||||
|
.singleOr { FailedToGetResourcesException("id $id is duplicate or does not exist.") }
|
||||||
|
.let { it.toNote() to it.toPost() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ResultRow.toNote(): Note {
|
||||||
|
return Note(
|
||||||
|
name = "Post",
|
||||||
|
id = this[Posts.apId],
|
||||||
|
attributedTo = this[Users.url],
|
||||||
|
content = this[Posts.text],
|
||||||
|
published = Instant.ofEpochMilli(this[Posts.createdAt]).toString(),
|
||||||
|
to = listOf(),
|
||||||
|
cc = listOf(),
|
||||||
|
inReplyTo = null,
|
||||||
|
sensitive = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package dev.usbharu.hideout.service.api
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.model.ap.Note
|
||||||
|
|
||||||
|
interface NoteApApiService {
|
||||||
|
suspend fun getNote(postId: Long, userId: Long?): Note?
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package dev.usbharu.hideout.service.api
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.model.ap.Note
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
|
||||||
|
import dev.usbharu.hideout.query.FollowerQueryService
|
||||||
|
import dev.usbharu.hideout.query.activitypub.NoteQueryService
|
||||||
|
import dev.usbharu.hideout.service.core.Transaction
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class NoteApApiServiceImpl(
|
||||||
|
private val noteQueryService: NoteQueryService,
|
||||||
|
private val followerQueryService: FollowerQueryService,
|
||||||
|
private val transaction: Transaction
|
||||||
|
) : NoteApApiService {
|
||||||
|
override suspend fun getNote(postId: Long, userId: Long?): Note? = transaction.transaction {
|
||||||
|
val findById = noteQueryService.findById(postId)
|
||||||
|
when (findById.second.visibility) {
|
||||||
|
Visibility.PUBLIC, Visibility.UNLISTED -> {
|
||||||
|
return@transaction findById.first
|
||||||
|
}
|
||||||
|
|
||||||
|
Visibility.FOLLOWERS -> {
|
||||||
|
if (userId == null) {
|
||||||
|
return@transaction null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (followerQueryService.alreadyFollow(findById.second.userId, userId).not()) {
|
||||||
|
return@transaction null
|
||||||
|
}
|
||||||
|
return@transaction findById.first
|
||||||
|
}
|
||||||
|
|
||||||
|
Visibility.DIRECT -> return@transaction null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,13 +10,19 @@ import java.net.URL
|
||||||
|
|
||||||
class HttpSignatureFilter(private val httpSignatureHeaderParser: SignatureHeaderParser) :
|
class HttpSignatureFilter(private val httpSignatureHeaderParser: SignatureHeaderParser) :
|
||||||
AbstractPreAuthenticatedProcessingFilter() {
|
AbstractPreAuthenticatedProcessingFilter() {
|
||||||
override fun getPreAuthenticatedPrincipal(request: HttpServletRequest?): Any {
|
override fun getPreAuthenticatedPrincipal(request: HttpServletRequest?): Any? {
|
||||||
val headersList = request?.headerNames?.toList().orEmpty()
|
val headersList = request?.headerNames?.toList().orEmpty()
|
||||||
|
|
||||||
val headers =
|
val headers =
|
||||||
headersList.associateWith { header -> request?.getHeaders(header)?.toList().orEmpty() }
|
headersList.associateWith { header -> request?.getHeaders(header)?.toList().orEmpty() }
|
||||||
|
|
||||||
val signature = httpSignatureHeaderParser.parse(HttpHeaders(headers))
|
val signature = try {
|
||||||
|
httpSignatureHeaderParser.parse(HttpHeaders(headers))
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
return null
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
return signature.keyId
|
return signature.keyId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import java.io.Serial
|
||||||
class HttpSignatureUser(
|
class HttpSignatureUser(
|
||||||
username: String,
|
username: String,
|
||||||
val domain: String,
|
val domain: String,
|
||||||
|
val id: Long,
|
||||||
credentialsNonExpired: Boolean,
|
credentialsNonExpired: Boolean,
|
||||||
accountNonLocked: Boolean,
|
accountNonLocked: Boolean,
|
||||||
authorities: MutableCollection<out GrantedAuthority>?
|
authorities: MutableCollection<out GrantedAuthority>?
|
||||||
|
@ -19,6 +20,25 @@ class HttpSignatureUser(
|
||||||
accountNonLocked,
|
accountNonLocked,
|
||||||
authorities
|
authorities
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is HttpSignatureUser) return false
|
||||||
|
if (!super.equals(other)) return false
|
||||||
|
|
||||||
|
if (domain != other.domain) return false
|
||||||
|
if (id != other.id) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = super.hashCode()
|
||||||
|
result = 31 * result + domain.hashCode()
|
||||||
|
result = 31 * result + id.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@Serial
|
@Serial
|
||||||
private const val serialVersionUID: Long = -3330552099960982997L
|
private const val serialVersionUID: Long = -3330552099960982997L
|
||||||
|
|
|
@ -10,6 +10,8 @@ import dev.usbharu.httpsignature.common.PublicKey
|
||||||
import dev.usbharu.httpsignature.verify.FailedVerification
|
import dev.usbharu.httpsignature.verify.FailedVerification
|
||||||
import dev.usbharu.httpsignature.verify.HttpSignatureVerifier
|
import dev.usbharu.httpsignature.verify.HttpSignatureVerifier
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.security.authentication.BadCredentialsException
|
||||||
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService
|
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService
|
||||||
import org.springframework.security.core.userdetails.UserDetails
|
import org.springframework.security.core.userdetails.UserDetails
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
||||||
|
@ -22,37 +24,47 @@ class HttpSignatureUserDetailsService(
|
||||||
) :
|
) :
|
||||||
AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {
|
AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {
|
||||||
override fun loadUserDetails(token: PreAuthenticatedAuthenticationToken): UserDetails = runBlocking {
|
override fun loadUserDetails(token: PreAuthenticatedAuthenticationToken): UserDetails = runBlocking {
|
||||||
transaction.transaction {
|
if (token.principal !is String) {
|
||||||
if (token.principal !is String) {
|
throw IllegalStateException("Token is not String")
|
||||||
throw IllegalStateException("Token is not String")
|
}
|
||||||
}
|
if (token.credentials !is HttpRequest) {
|
||||||
if (token.credentials !is HttpRequest) {
|
throw IllegalStateException("Credentials is not HttpRequest")
|
||||||
throw IllegalStateException("Credentials is not HttpRequest")
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val keyId = token.principal as String
|
val keyId = token.principal as String
|
||||||
val findByKeyId = try {
|
val findByKeyId = transaction.transaction {
|
||||||
|
try {
|
||||||
userQueryService.findByKeyId(keyId)
|
userQueryService.findByKeyId(keyId)
|
||||||
} catch (e: FailedToGetResourcesException) {
|
} catch (e: FailedToGetResourcesException) {
|
||||||
throw UsernameNotFoundException("User not found", e)
|
throw UsernameNotFoundException("User not found", e)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val verify = httpSignatureVerifier.verify(
|
val verify = try {
|
||||||
|
httpSignatureVerifier.verify(
|
||||||
token.credentials as HttpRequest,
|
token.credentials as HttpRequest,
|
||||||
PublicKey(RsaUtil.decodeRsaPublicKeyPem(findByKeyId.publicKey), keyId)
|
PublicKey(RsaUtil.decodeRsaPublicKeyPem(findByKeyId.publicKey), keyId)
|
||||||
)
|
)
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
if (verify is FailedVerification) {
|
throw BadCredentialsException("", e)
|
||||||
throw HttpSignatureVerifyException(verify.reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
HttpSignatureUser(
|
|
||||||
username = findByKeyId.name,
|
|
||||||
domain = findByKeyId.domain,
|
|
||||||
credentialsNonExpired = true,
|
|
||||||
accountNonLocked = true,
|
|
||||||
authorities = mutableListOf()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (verify is FailedVerification) {
|
||||||
|
logger.warn("FAILED Verify HTTP Signature reason: {}", verify.reason)
|
||||||
|
throw HttpSignatureVerifyException(verify.reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpSignatureUser(
|
||||||
|
username = findByKeyId.name,
|
||||||
|
domain = findByKeyId.domain,
|
||||||
|
id = findByKeyId.id,
|
||||||
|
credentialsNonExpired = true,
|
||||||
|
accountNonLocked = true,
|
||||||
|
authorities = mutableListOf()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger(HttpSignatureUserDetailsService::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue