This commit is contained in:
usbharu 2024-06-02 23:18:45 +09:00
parent d5cb840270
commit 71eeb47169
42 changed files with 33 additions and 2015 deletions

View File

@ -17,7 +17,6 @@
package mastodon.apps package mastodon.apps
import dev.usbharu.hideout.SpringApplication import dev.usbharu.hideout.SpringApplication
import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.RegisteredClient
import dev.usbharu.owl.producer.api.OwlProducer import dev.usbharu.owl.producer.api.OwlProducer
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
@ -30,6 +29,7 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType import org.springframework.http.MediaType
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient
import org.springframework.security.test.context.support.WithAnonymousUser import org.springframework.security.test.context.support.WithAnonymousUser
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers
import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.MockMvc

View File

@ -1,38 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.application.config
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.s3.S3Client
import java.net.URI
@Configuration
class AwsConfig {
@Bean
@ConditionalOnProperty("hideout.storage.type", havingValue = "s3")
fun s3Client(awsConfig: S3StorageConfig): S3Client {
return S3Client.builder()
.endpointOverride(URI.create(awsConfig.endpoint))
.region(Region.of(awsConfig.region))
.credentialsProvider { AwsBasicCredentials.create(awsConfig.accessKey, awsConfig.secretKey) }
.build()
}
}

View File

@ -1,9 +0,0 @@
package dev.usbharu.hideout.application.config
import org.springframework.boot.context.properties.ConfigurationProperties
@ConfigurationProperties("hideout.security")
data class CaptchaConfig(
val reCaptchaSiteKey: String?,
val reCaptchaSecretKey: String?
)

View File

@ -1,44 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.application.config
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.cache.*
import io.ktor.client.plugins.logging.*
import org.springframework.boot.info.BuildProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class HttpClientConfig {
@Bean
fun httpClient(buildProperties: BuildProperties, applicationConfig: ApplicationConfig): HttpClient =
HttpClient(CIO).config {
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.ALL
}
install(HttpCache) {
}
expectSuccess = true
install(UserAgent) {
agent = "Hideout/${buildProperties.version} (${applicationConfig.url})"
}
}
}

View File

@ -1,36 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.application.config
import dev.usbharu.httpsignature.sign.RsaSha256HttpSignatureSigner
import dev.usbharu.httpsignature.verify.DefaultSignatureHeaderParser
import dev.usbharu.httpsignature.verify.RsaSha256HttpSignatureVerifier
import dev.usbharu.httpsignature.verify.SignatureHeaderParser
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class HttpSignatureConfig {
@Bean
fun defaultSignatureHeaderParser(): DefaultSignatureHeaderParser = DefaultSignatureHeaderParser()
@Bean
fun rsaSha256HttpSignatureVerifier(
signatureHeaderParser: SignatureHeaderParser,
signatureSigner: RsaSha256HttpSignatureSigner
): RsaSha256HttpSignatureVerifier = RsaSha256HttpSignatureVerifier(signatureHeaderParser, signatureSigner)
}

View File

@ -1,45 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.application.config
import jakarta.servlet.Filter
import jakarta.servlet.FilterChain
import jakarta.servlet.ServletRequest
import jakarta.servlet.ServletResponse
import org.slf4j.MDC
import org.springframework.boot.autoconfigure.security.SecurityProperties
import org.springframework.core.annotation.Order
import org.springframework.stereotype.Component
import java.util.*
@Component
@Order(SecurityProperties.DEFAULT_FILTER_ORDER - 1)
class MdcXrequestIdFilter : Filter {
override fun doFilter(request: ServletRequest?, response: ServletResponse?, chain: FilterChain) {
val uuid = UUID.randomUUID()
try {
MDC.put(KEY, uuid.toString())
chain.doFilter(request, response)
} finally {
MDC.remove(KEY)
}
}
companion object {
private const val KEY = "x-request-id"
}
}

View File

@ -1,24 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.application.config
import org.springframework.boot.context.properties.ConfigurationProperties
@ConfigurationProperties("hideout.media")
data class MediaConfig(
val remoteMediaFileSizeLimit: Long = 3000000L
)

View File

@ -1,46 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.application.config
import dev.usbharu.hideout.generate.JsonOrFormModelMethodProcessor
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.converter.HttpMessageConverter
import org.springframework.web.method.support.HandlerMethodArgumentResolver
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor
import org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor
@Configuration
class MvcConfigurer(private val jsonOrFormModelMethodProcessor: JsonOrFormModelMethodProcessor) : WebMvcConfigurer {
override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {
resolvers.add(jsonOrFormModelMethodProcessor)
}
}
@Configuration
class JsonOrFormModelMethodProcessorConfig {
@Bean
fun jsonOrFormModelMethodProcessor(converter: List<HttpMessageConverter<*>>): JsonOrFormModelMethodProcessor {
return JsonOrFormModelMethodProcessor(
ServletModelAttributeMethodProcessor(true),
RequestResponseBodyMethodProcessor(
converter
)
)
}
}

View File

@ -1,89 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.application.config
import com.fasterxml.jackson.databind.ObjectMapper
import dev.usbharu.owl.broker.ModuleContext
import dev.usbharu.owl.common.property.*
import dev.usbharu.owl.common.retry.RetryPolicyFactory
import dev.usbharu.owl.producer.api.OWL
import dev.usbharu.owl.producer.api.OwlProducer
import dev.usbharu.owl.producer.defaultimpl.DEFAULT
import dev.usbharu.owl.producer.embedded.EMBEDDED
import dev.usbharu.owl.producer.embedded.EMBEDDED_GRPC
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.util.*
@Configuration
class OwlConfig(private val producerConfig: ProducerConfig) {
@Bean
fun producer(
@Autowired(required = false) retryPolicyFactory: RetryPolicyFactory? = null,
@Qualifier("activitypub") objectMapper: ObjectMapper,
): OwlProducer {
return when (producerConfig.mode) {
ProducerMode.EMBEDDED -> {
OWL(EMBEDDED) {
if (retryPolicyFactory != null) {
this.retryPolicyFactory = retryPolicyFactory
}
if (producerConfig.port != null) {
this.port = producerConfig.port.toString()
}
val moduleContext = ServiceLoader.load(ModuleContext::class.java).firstOrNull()
if (moduleContext != null) {
this.moduleContext = moduleContext
}
this.propertySerializerFactory = CustomPropertySerializerFactory(
setOf(
IntegerPropertySerializer(),
StringPropertyValueSerializer(),
DoublePropertySerializer(),
BooleanPropertySerializer(),
LongPropertySerializer(),
FloatPropertySerializer(),
ObjectPropertySerializer(objectMapper),
)
)
}
}
ProducerMode.GRPC -> {
OWL(EMBEDDED_GRPC) {
}
}
ProducerMode.EMBEDDED_GRPC -> {
OWL(DEFAULT) {
}
}
}
}
}
@ConfigurationProperties("hideout.owl.producer")
data class ProducerConfig(val mode: ProducerMode = ProducerMode.EMBEDDED, val port: Int? = null)
enum class ProducerMode {
GRPC,
EMBEDDED,
EMBEDDED_GRPC
}

View File

@ -1,319 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.application.config
import com.nimbusds.jose.jwk.JWKSet
import com.nimbusds.jose.jwk.RSAKey
import com.nimbusds.jose.jwk.source.ImmutableJWKSet
import com.nimbusds.jose.jwk.source.JWKSource
import com.nimbusds.jose.proc.SecurityContext
import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.UserDetailsImpl
import dev.usbharu.hideout.util.RsaUtil
import jakarta.annotation.PostConstruct
import jakarta.servlet.*
import org.springframework.beans.factory.support.BeanDefinitionRegistry
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.annotation.Order
import org.springframework.http.HttpMethod.GET
import org.springframework.http.HttpMethod.POST
import org.springframework.http.HttpStatus
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.dao.DaoAuthenticationProvider
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolderStrategy
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.oauth2.core.AuthorizationGrantType
import org.springframework.security.oauth2.jwt.JwtDecoder
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer
import org.springframework.security.web.FilterChainProxy
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.HttpStatusEntryPoint
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer
import org.springframework.security.web.debug.DebugFilter
import org.springframework.security.web.firewall.HttpFirewall
import org.springframework.security.web.firewall.RequestRejectedHandler
import org.springframework.security.web.util.matcher.AnyRequestMatcher
import org.springframework.web.filter.CompositeFilter
import java.io.IOException
import java.security.KeyPairGenerator
import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey
import java.util.*
@EnableWebSecurity(debug = false)
@Configuration
@Suppress("FunctionMaxLength", "TooManyFunctions", "LongMethod")
class SecurityConfig {
@Bean
fun authenticationManager(authenticationConfiguration: AuthenticationConfiguration): AuthenticationManager? =
authenticationConfiguration.authenticationManager
@Bean
@Order(1)
fun httpSignatureFilterChain(
http: HttpSecurity,
): SecurityFilterChain {
http {
securityMatcher("/users/*/posts/*")
authorizeHttpRequests {
authorize(anyRequest, permitAll)
}
exceptionHandling {
authenticationEntryPoint = HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)
defaultAuthenticationEntryPointFor(
HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
AnyRequestMatcher.INSTANCE
)
}
sessionManagement {
sessionCreationPolicy = SessionCreationPolicy.STATELESS
}
}
return http.build()
}
@Bean
@Order(2)
fun oauth2SecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http)
http {
exceptionHandling {
authenticationEntryPoint = LoginUrlAuthenticationEntryPoint("/login")
}
oauth2ResourceServer {
jwt {
}
}
}
return http.build()
}
@Bean
@Order(5)
fun defaultSecurityFilterChain(
http: HttpSecurity,
): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize("/error", permitAll)
authorize("/login", permitAll)
authorize(GET, "/.well-known/**", permitAll)
authorize(GET, "/nodeinfo/2.0", permitAll)
authorize(POST, "/inbox", permitAll)
authorize(POST, "/users/*/inbox", permitAll)
authorize(GET, "/users/*", permitAll)
authorize(GET, "/users/*/posts/*", permitAll)
authorize("/dev/usbharu/hideout/core/service/auth/sign_up", hasRole("ANONYMOUS"))
authorize(GET, "/files/*", permitAll)
authorize(GET, "/users/*/icon.jpg", permitAll)
authorize(GET, "/users/*/header.jpg", permitAll)
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt { }
}
formLogin {
}
csrf {
ignoringRequestMatchers("/users/*/inbox", "/inbox", "/api/v1/apps")
}
headers {
frameOptions {
sameOrigin = true
}
}
}
return http.build()
}
@Bean
fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()
@Bean
@ConditionalOnProperty(name = ["hideout.security.jwt.generate"], havingValue = "false", matchIfMissing = true)
fun genJwkSource(): JWKSource<SecurityContext> {
val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
keyPairGenerator.initialize(2048)
val generateKeyPair = keyPairGenerator.generateKeyPair()
val rsaPublicKey = generateKeyPair.public as RSAPublicKey
val rsaPrivateKey = generateKeyPair.private as RSAPrivateKey
val rsaKey = RSAKey.Builder(rsaPublicKey).privateKey(rsaPrivateKey).keyID(UUID.randomUUID().toString()).build()
val jwkSet = JWKSet(rsaKey)
return ImmutableJWKSet(jwkSet)
}
@Bean
@ConditionalOnProperty(name = ["hideout.security.jwt.generate"], havingValue = "")
fun loadJwkSource(jwkConfig: JwkConfig): JWKSource<SecurityContext> {
val rsaKey = RSAKey.Builder(RsaUtil.decodeRsaPublicKey(jwkConfig.publicKey))
.privateKey(RsaUtil.decodeRsaPrivateKey(jwkConfig.privateKey)).keyID(jwkConfig.keyId).build()
return ImmutableJWKSet(JWKSet(rsaKey))
}
@Bean
fun jwtDecoder(jwkSource: JWKSource<SecurityContext>): JwtDecoder =
OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource)
@Bean
fun authorizationServerSettings(): AuthorizationServerSettings {
return AuthorizationServerSettings.builder().authorizationEndpoint("/oauth/authorize")
.tokenEndpoint("/oauth/token").tokenRevocationEndpoint("/oauth/revoke").build()
}
@Bean
fun jwtTokenCustomizer(): OAuth2TokenCustomizer<JwtEncodingContext> {
return OAuth2TokenCustomizer { context: JwtEncodingContext ->
if (OAuth2TokenType.ACCESS_TOKEN == context.tokenType &&
context.authorization?.authorizationGrantType == AuthorizationGrantType.AUTHORIZATION_CODE
) {
val userDetailsImpl = context.getPrincipal<Authentication>().principal as UserDetailsImpl
context.claims.claim("uid", userDetailsImpl.id.toString())
}
}
}
// Spring Security 3.2.1 に存在する EnableWebSecurity(debug = true)にすると発生するエラーに対処するためのコード
// trueにしないときはコメントアウト
// @Bean
fun beanDefinitionRegistryPostProcessor(): BeanDefinitionRegistryPostProcessor {
return BeanDefinitionRegistryPostProcessor { registry: BeanDefinitionRegistry ->
registry.getBeanDefinition(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME).beanClassName =
CompositeFilterChainProxy::class.java.name
}
}
@Suppress("ExpressionBodySyntax")
internal class CompositeFilterChainProxy(filters: List<Filter?>) : FilterChainProxy() {
private val doFilterDelegate: Filter
private val springSecurityFilterChain: FilterChainProxy
init {
this.doFilterDelegate = createDoFilterDelegate(filters)
this.springSecurityFilterChain = findFilterChainProxy(filters)
}
override fun afterPropertiesSet() {
springSecurityFilterChain.afterPropertiesSet()
}
@Throws(IOException::class, ServletException::class)
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
doFilterDelegate.doFilter(request, response, chain)
}
override fun getFilters(url: String): List<Filter> {
return springSecurityFilterChain.getFilters(url)
}
override fun getFilterChains(): List<SecurityFilterChain> {
return springSecurityFilterChain.filterChains
}
override fun setSecurityContextHolderStrategy(securityContextHolderStrategy: SecurityContextHolderStrategy) {
springSecurityFilterChain.setSecurityContextHolderStrategy(securityContextHolderStrategy)
}
override fun setFilterChainValidator(filterChainValidator: FilterChainValidator) {
springSecurityFilterChain.setFilterChainValidator(filterChainValidator)
}
override fun setFilterChainDecorator(filterChainDecorator: FilterChainDecorator) {
springSecurityFilterChain.setFilterChainDecorator(filterChainDecorator)
}
override fun setFirewall(firewall: HttpFirewall) {
springSecurityFilterChain.setFirewall(firewall)
}
override fun setRequestRejectedHandler(requestRejectedHandler: RequestRejectedHandler) {
springSecurityFilterChain.setRequestRejectedHandler(requestRejectedHandler)
}
companion object {
private fun createDoFilterDelegate(filters: List<Filter?>): Filter {
val delegate: CompositeFilter = CompositeFilter()
delegate.setFilters(filters)
return delegate
}
private fun findFilterChainProxy(filters: List<Filter?>): FilterChainProxy {
for (filter in filters) {
if (filter is FilterChainProxy) {
return filter
}
if (filter is DebugFilter) {
return filter.filterChainProxy
}
}
throw IllegalStateException("Couldn't find FilterChainProxy in $filters")
}
}
}
}
@ConfigurationProperties("hideout.security.jwt")
@ConditionalOnProperty(name = ["hideout.security.jwt.generate"], havingValue = "")
data class JwkConfig(
val keyId: String,
val publicKey: String,
val privateKey: String,
)
@Configuration
class PostSecurityConfig(
val auth: AuthenticationManagerBuilder,
val daoAuthenticationProvider: DaoAuthenticationProvider,
val httpSignatureAuthenticationProvider: PreAuthenticatedAuthenticationProvider,
) {
@PostConstruct
fun config() {
auth.authenticationProvider(daoAuthenticationProvider)
auth.authenticationProvider(httpSignatureAuthenticationProvider)
}
}

View File

@ -1,105 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.application.config
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.filter.CommonsRequestLoggingFilter
import java.net.URL
@Configuration
class SpringConfig {
@Autowired
lateinit var config: ApplicationConfig
@Bean
fun requestLoggingFilter(): CommonsRequestLoggingFilter {
val loggingFilter = CommonsRequestLoggingFilter()
loggingFilter.setIncludeHeaders(true)
loggingFilter.setIncludeClientInfo(true)
loggingFilter.setIncludeQueryString(true)
loggingFilter.setIncludePayload(true)
loggingFilter.setMaxPayloadLength(64000)
return loggingFilter
}
}
@ConfigurationProperties("hideout")
data class ApplicationConfig(
val url: URL,
val private: Boolean = true,
)
@ConfigurationProperties("hideout.storage.s3")
@ConditionalOnProperty("hideout.storage.type", havingValue = "s3")
data class S3StorageConfig(
val endpoint: String,
val publicUrl: String,
val bucket: String,
val region: String,
val accessKey: String,
val secretKey: String
)
/**
* メディアの保存にローカルファイルシステムを使用する際のコンフィグ
*
* @property path フォゾンする場所へのパス /から始めると絶対パスとなります
* @property publicUrl 公開用URL 省略可能 指定するとHideoutがファイルを配信しなくなります
*/
@ConfigurationProperties("hideout.storage.local")
@ConditionalOnProperty("hideout.storage.type", havingValue = "local", matchIfMissing = true)
data class LocalStorageConfig(
val path: String = "files",
val publicUrl: String?
)
@ConfigurationProperties("hideout.character-limit")
data class CharacterLimit(
val general: General = General(),
val post: Post = Post(),
val account: Account = Account(),
val instance: Instance = Instance()
) {
data class General(
val url: Int = 1000,
val domain: Int = 1000,
val publicKey: Int = 10000,
val privateKey: Int = 10000
)
data class Post(
val text: Int = 3000,
val overview: Int = 3000
)
data class Account(
val id: Int = 300,
val name: Int = 300,
val description: Int = 10000
)
data class Instance(
val name: Int = 600,
val description: Int = 10000
)
}

View File

@ -1,42 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.application.external
import dev.usbharu.owl.common.task.TaskDefinition
import dev.usbharu.owl.producer.api.OwlProducer
import kotlinx.coroutines.runBlocking
import org.springframework.beans.factory.DisposableBean
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
import org.springframework.stereotype.Component
@Component
class OwlProducerRunner(private val owlProducer: OwlProducer, private val taskDefinitions: List<TaskDefinition<*>>) :
ApplicationRunner, DisposableBean {
override fun run(args: ApplicationArguments?) {
runBlocking {
owlProducer.start()
taskDefinitions.forEach { taskDefinition -> owlProducer.registerTask(taskDefinition) }
}
}
override fun destroy() {
runBlocking {
owlProducer.stop()
}
}
}

View File

@ -1,35 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.application.infrastructure.exposed
import org.jetbrains.exposed.sql.*
fun <S : Any> Query.withPagination(page: Page, exp: ExpressionWithColumnType<S>): PaginationList<ResultRow, S> {
page.limit?.let { limit(it) }
val resultRows = if (page.minId != null) {
page.maxId?.let { it: Long -> andWhere { exp.less(it) } }
andWhere { exp.greater(page.minId!!) }
reversed()
} else {
page.maxId?.let { andWhere { exp.less(it) } }
page.sinceId?.let { andWhere { exp.greater(it) } }
orderBy(exp, SortOrder.DESC)
toList()
}
return PaginationList(resultRows, resultRows.firstOrNull()?.getOrNull(exp), resultRows.lastOrNull()?.getOrNull(exp))
}

View File

@ -1,63 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.application.infrastructure.exposed
sealed class Page {
abstract val maxId: Long?
abstract val sinceId: Long?
abstract val minId: Long?
abstract val limit: Int?
data class PageByMaxId(
override val maxId: Long?,
override val sinceId: Long?,
override val limit: Int?
) : Page() {
override val minId: Long? = null
}
data class PageByMinId(
override val maxId: Long?,
override val minId: Long?,
override val limit: Int?
) : Page() {
override val sinceId: Long? = null
}
companion object {
@Suppress("FunctionMinLength")
fun of(
maxId: Long? = null,
sinceId: Long? = null,
minId: Long? = null,
limit: Int? = null
): Page =
if (minId != null) {
PageByMinId(
maxId,
minId,
limit
)
} else {
PageByMaxId(
maxId,
sinceId,
limit
)
}
}
}

View File

@ -1,38 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.application.infrastructure.exposed
class PaginationList<T, ID>(list: List<T>, val next: ID?, val prev: ID?) : List<T> by list
fun <T, ID> PaginationList<T, ID>.toHttpHeader(
nextBlock: (string: String) -> String,
prevBlock: (string: String) -> String
): String? {
val mutableListOf = mutableListOf<String>()
if (next != null) {
mutableListOf.add("<${nextBlock(this.next.toString())}>; rel=\"next\"")
}
if (prev != null) {
mutableListOf.add("<${prevBlock(this.prev.toString())}>; rel=\"prev\"")
}
if (mutableListOf.isEmpty()) {
return null
}
return mutableListOf.joinToString(", ")
}

View File

@ -1,32 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.application.infrastructure.springframework
import org.springframework.security.access.hierarchicalroles.RoleHierarchy
import org.springframework.security.authorization.AuthorityAuthorizationManager
import org.springframework.security.authorization.AuthorizationManager
import org.springframework.security.web.access.intercept.RequestAuthorizationContext
import org.springframework.stereotype.Component
@Component
class RoleHierarchyAuthorizationManagerFactory(private val roleHierarchy: RoleHierarchy) {
fun hasScope(role: String): AuthorizationManager<RequestAuthorizationContext> {
val hasAuthority = AuthorityAuthorizationManager.hasAuthority<RequestAuthorizationContext>("SCOPE_$role")
hasAuthority.setRoleHierarchy(roleHierarchy)
return hasAuthority
}
}

View File

@ -17,7 +17,6 @@
package dev.usbharu.hideout.core.application.actor package dev.usbharu.hideout.core.application.actor
import dev.usbharu.hideout.application.config.ApplicationConfig import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.application.service.id.IdGenerateService
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.instance.InstanceRepository import dev.usbharu.hideout.core.domain.model.instance.InstanceRepository
@ -26,6 +25,7 @@ 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 dev.usbharu.hideout.core.domain.service.actor.local.LocalActorDomainService import dev.usbharu.hideout.core.domain.service.actor.local.LocalActorDomainService
import dev.usbharu.hideout.core.domain.service.userdetail.UserDetailDomainService import dev.usbharu.hideout.core.domain.service.userdetail.UserDetailDomainService
import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService
import dev.usbharu.hideout.core.infrastructure.factory.ActorFactoryImpl import dev.usbharu.hideout.core.infrastructure.factory.ActorFactoryImpl
import org.springframework.stereotype.Service import org.springframework.stereotype.Service

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package dev.usbharu.hideout.application.config package dev.usbharu.hideout.core.config
import org.owasp.html.HtmlPolicyBuilder import org.owasp.html.HtmlPolicyBuilder
import org.owasp.html.PolicyFactory import org.owasp.html.PolicyFactory

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package dev.usbharu.hideout.core.service.post package dev.usbharu.hideout.core.domain.service.post
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
@ -24,11 +24,6 @@ import org.jsoup.select.Elements
import org.owasp.html.PolicyFactory import org.owasp.html.PolicyFactory
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
interface PostContentFormatter {
fun format(content: String): FormattedPostContent
}
@Service @Service
class DefaultPostContentFormatter(private val policyFactory: PolicyFactory) : PostContentFormatter { class DefaultPostContentFormatter(private val policyFactory: PolicyFactory) : PostContentFormatter {
override fun format(content: String): FormattedPostContent { override fun format(content: String): FormattedPostContent {
@ -102,8 +97,3 @@ class DefaultPostContentFormatter(private val policyFactory: PolicyFactory) : Po
} }
} }
} }
data class FormattedPostContent(
val html: String,
val content: String,
)

View File

@ -14,10 +14,14 @@
* limitations under the License. * limitations under the License.
*/ */
package dev.usbharu.hideout.core.infrastructure.springframework.security package dev.usbharu.hideout.core.domain.service.post
interface LoginUserContextHolder {
fun getLoginUserId(): Long
fun getLoginUserIdOrNull(): Long? interface PostContentFormatter {
fun format(content: String): FormattedPostContent
} }
data class FormattedPostContent(
val html: String,
val content: String,
)

View File

@ -14,11 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
package dev.usbharu.hideout.application.service.id package dev.usbharu.hideout.core.domain.shared.id
import org.springframework.stereotype.Service
@Service
interface IdGenerateService { interface IdGenerateService {
suspend fun generateId(): Long suspend fun generateId(): Long
} }

View File

@ -16,8 +16,6 @@
package dev.usbharu.hideout.core.infrastructure.exposed package dev.usbharu.hideout.core.infrastructure.exposed
import dev.usbharu.hideout.application.infrastructure.exposed.QueryMapper
import dev.usbharu.hideout.application.infrastructure.exposed.ResultRowMapper
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.ActorId import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Actors import dev.usbharu.hideout.core.infrastructure.exposedrepository.Actors

View File

@ -16,7 +16,6 @@
package dev.usbharu.hideout.core.infrastructure.exposed package dev.usbharu.hideout.core.infrastructure.exposed
import dev.usbharu.hideout.application.infrastructure.exposed.ResultRowMapper
import dev.usbharu.hideout.core.domain.model.actor.* import dev.usbharu.hideout.core.domain.model.actor.*
import dev.usbharu.hideout.core.domain.model.emoji.EmojiId import dev.usbharu.hideout.core.domain.model.emoji.EmojiId
import dev.usbharu.hideout.core.domain.model.instance.InstanceId import dev.usbharu.hideout.core.domain.model.instance.InstanceId

View File

@ -14,28 +14,27 @@
* limitations under the License. * limitations under the License.
*/ */
package dev.usbharu.hideout.application.infrastructure.exposed package dev.usbharu.hideout.core.infrastructure.exposed
import dev.usbharu.hideout.core.application.shared.Transaction import dev.usbharu.hideout.core.application.shared.Transaction
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.slf4j.MDCContext import kotlinx.coroutines.slf4j.MDCContext
import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger
import org.jetbrains.exposed.sql.addLogger import org.jetbrains.exposed.sql.addLogger
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import org.jetbrains.exposed.sql.transactions.transaction import org.springframework.stereotype.Component
import org.springframework.stereotype.Service
import java.sql.Connection import java.sql.Connection
@Service @Component
class ExposedTransaction : Transaction { class ExposedTransaction : Transaction {
override suspend fun <T> transaction(block: suspend () -> T): T { override suspend fun <T> transaction(block: suspend () -> T): T {
return transaction(transactionIsolation = Connection.TRANSACTION_READ_COMMITTED) { return newSuspendedTransaction(
transactionIsolation = Connection.TRANSACTION_READ_COMMITTED,
context = MDCContext()
) {
debug = true debug = true
warnLongQueriesDuration = 1000 warnLongQueriesDuration = 1000
addLogger(Slf4jSqlDebugLogger) addLogger(Slf4jSqlDebugLogger)
runBlocking(MDCContext()) { block()
block()
}
} }
} }

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package dev.usbharu.hideout.application.infrastructure.exposed package dev.usbharu.hideout.core.infrastructure.exposed
import org.jetbrains.exposed.sql.Query import org.jetbrains.exposed.sql.Query

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package dev.usbharu.hideout.application.infrastructure.exposed package dev.usbharu.hideout.core.infrastructure.exposed
import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.ResultRow

View File

@ -1,10 +1,10 @@
package dev.usbharu.hideout.core.infrastructure.exposedrepository package dev.usbharu.hideout.core.infrastructure.exposedrepository
import dev.usbharu.hideout.application.infrastructure.exposed.QueryMapper
import dev.usbharu.hideout.core.domain.model.actor.* import dev.usbharu.hideout.core.domain.model.actor.*
import dev.usbharu.hideout.core.domain.model.shared.Domain import dev.usbharu.hideout.core.domain.model.shared.Domain
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventPublisher import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventPublisher
import dev.usbharu.hideout.core.domain.shared.repository.DomainEventPublishableRepository import dev.usbharu.hideout.core.domain.shared.repository.DomainEventPublishableRepository
import dev.usbharu.hideout.core.infrastructure.exposed.QueryMapper
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.javatime.timestamp import org.jetbrains.exposed.sql.javatime.timestamp

View File

@ -17,10 +17,10 @@
package dev.usbharu.hideout.core.infrastructure.factory package dev.usbharu.hideout.core.infrastructure.factory
import dev.usbharu.hideout.application.config.ApplicationConfig import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.application.service.id.IdGenerateService
import dev.usbharu.hideout.core.domain.model.actor.* import dev.usbharu.hideout.core.domain.model.actor.*
import dev.usbharu.hideout.core.domain.model.instance.InstanceId import dev.usbharu.hideout.core.domain.model.instance.InstanceId
import dev.usbharu.hideout.core.domain.model.shared.Domain import dev.usbharu.hideout.core.domain.model.shared.Domain
import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import java.net.URI import java.net.URI
import java.time.Instant import java.time.Instant

View File

@ -17,7 +17,7 @@
package dev.usbharu.hideout.core.infrastructure.factory package dev.usbharu.hideout.core.infrastructure.factory
import dev.usbharu.hideout.core.domain.model.post.PostContent import dev.usbharu.hideout.core.domain.model.post.PostContent
import dev.usbharu.hideout.core.service.post.PostContentFormatter import dev.usbharu.hideout.core.domain.service.post.PostContentFormatter
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
@Component @Component

View File

@ -17,7 +17,6 @@
package dev.usbharu.hideout.core.infrastructure.factory package dev.usbharu.hideout.core.infrastructure.factory
import dev.usbharu.hideout.application.config.ApplicationConfig import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.application.service.id.IdGenerateService
import dev.usbharu.hideout.core.domain.model.actor.ActorId import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.actor.ActorName import dev.usbharu.hideout.core.domain.model.actor.ActorName
import dev.usbharu.hideout.core.domain.model.media.MediaId import dev.usbharu.hideout.core.domain.model.media.MediaId
@ -25,6 +24,7 @@ import dev.usbharu.hideout.core.domain.model.post.Post
import dev.usbharu.hideout.core.domain.model.post.PostId import dev.usbharu.hideout.core.domain.model.post.PostId
import dev.usbharu.hideout.core.domain.model.post.PostOverview import dev.usbharu.hideout.core.domain.model.post.PostOverview
import dev.usbharu.hideout.core.domain.model.post.Visibility import dev.usbharu.hideout.core.domain.model.post.Visibility
import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import java.net.URI import java.net.URI
import java.time.Instant import java.time.Instant

View File

@ -1,45 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.infrastructure.httpsignature
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import dev.usbharu.httpsignature.common.HttpHeaders
import dev.usbharu.httpsignature.common.HttpMethod
import dev.usbharu.httpsignature.common.HttpRequest
import java.net.URL
@JsonDeserialize(using = HttpRequestDeserializer::class)
@JsonSubTypes
@Suppress("UnnecessaryAbstractClass")
abstract class HttpRequestMixIn
class HttpRequestDeserializer : JsonDeserializer<HttpRequest>() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): HttpRequest {
val readTree: JsonNode = p.codec.readTree(p)
return HttpRequest(
URL(readTree["url"].textValue()),
HttpHeaders(emptyMap()),
HttpMethod.valueOf(readTree["method"].textValue())
)
}
}

View File

@ -14,15 +14,16 @@
* limitations under the License. * limitations under the License.
*/ */
package dev.usbharu.hideout.application.service.id package dev.usbharu.hideout.core.infrastructure.other
import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import java.time.Instant import java.time.Instant
@Suppress("MagicNumber") @Suppress("MagicNumber")
class SnowflakeIdGenerateService(private val baseTime: Long) : IdGenerateService { open class SnowflakeIdGenerateService(private val baseTime: Long) : IdGenerateService {
var lastTimeStamp: Long = -1 var lastTimeStamp: Long = -1
var sequenceId: Int = 0 var sequenceId: Int = 0
val mutex = Mutex() val mutex = Mutex()

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package dev.usbharu.hideout.application.service.id package dev.usbharu.hideout.core.infrastructure.other
import org.springframework.context.annotation.Primary import org.springframework.context.annotation.Primary
import org.springframework.stereotype.Service import org.springframework.stereotype.Service

View File

@ -1,97 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.infrastructure.springframework.oauth2
import dev.usbharu.hideout.core.application.shared.Transaction
import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository
import org.springframework.stereotype.Service
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent as AuthorizationConsent
@Service
class ExposedOAuth2AuthorizationConsentService(
private val registeredClientRepository: RegisteredClientRepository,
private val transaction: Transaction,
) :
OAuth2AuthorizationConsentService {
override fun save(authorizationConsent: AuthorizationConsent?): Unit = runBlocking {
requireNotNull(authorizationConsent)
transaction.transaction {
val singleOrNull =
OAuth2AuthorizationConsent.selectAll().where {
OAuth2AuthorizationConsent.registeredClientId
.eq(authorizationConsent.registeredClientId)
.and(OAuth2AuthorizationConsent.principalName.eq(authorizationConsent.principalName))
}
.singleOrNull()
if (singleOrNull == null) {
OAuth2AuthorizationConsent.insert {
it[registeredClientId] = authorizationConsent.registeredClientId
it[principalName] = authorizationConsent.principalName
it[authorities] = authorizationConsent.authorities.joinToString(",")
}
}
}
}
override fun remove(authorizationConsent: AuthorizationConsent?) {
if (authorizationConsent == null) {
return
}
OAuth2AuthorizationConsent.deleteWhere {
registeredClientId eq authorizationConsent.registeredClientId and (principalName eq principalName)
}
}
override fun findById(registeredClientId: String?, principalName: String?): AuthorizationConsent? = runBlocking {
requireNotNull(registeredClientId)
requireNotNull(principalName)
transaction.transaction {
OAuth2AuthorizationConsent.selectAll().where {
(OAuth2AuthorizationConsent.registeredClientId eq registeredClientId)
.and(OAuth2AuthorizationConsent.principalName eq principalName)
}
.singleOrNull()?.toAuthorizationConsent()
}
}
fun ResultRow.toAuthorizationConsent(): AuthorizationConsent {
val registeredClientId = this[OAuth2AuthorizationConsent.registeredClientId]
registeredClientRepository.findById(registeredClientId)
val principalName = this[OAuth2AuthorizationConsent.principalName]
val builder = AuthorizationConsent.withId(registeredClientId, principalName)
this[OAuth2AuthorizationConsent.authorities].split(",").forEach {
builder.authority(SimpleGrantedAuthority(it))
}
return builder.build()
}
}
object OAuth2AuthorizationConsent : Table("oauth2_authorization_consent") {
val registeredClientId: Column<String> = varchar("registered_client_id", 100)
val principalName: Column<String> = varchar("principal_name", 200)
val authorities: Column<String> = varchar("authorities", 1000)
override val primaryKey: PrimaryKey = PrimaryKey(registeredClientId, principalName)
}

View File

@ -1,393 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.infrastructure.springframework.oauth2
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.core.application.shared.Transaction
import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.javatime.timestamp
import org.springframework.security.jackson2.CoreJackson2Module
import org.springframework.security.jackson2.SecurityJackson2Modules
import org.springframework.security.oauth2.core.*
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames
import org.springframework.security.oauth2.core.oidc.OidcIdToken
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository
import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module
import org.springframework.stereotype.Service
import java.time.Instant
@Service
class ExposedOAuth2AuthorizationService(
private val registeredClientRepository: RegisteredClientRepository,
private val transaction: Transaction,
) :
OAuth2AuthorizationService {
@Suppress("LongMethod", "CyclomaticComplexMethod")
override fun save(authorization: OAuth2Authorization?): Unit = runBlocking {
requireNotNull(authorization)
transaction.transaction {
val singleOrNull = Authorization.selectAll().where { Authorization.id eq authorization.id }.singleOrNull()
if (singleOrNull == null) {
val authorizationCodeToken = authorization.getToken(OAuth2AuthorizationCode::class.java)
val accessToken = authorization.getToken(OAuth2AccessToken::class.java)
val refreshToken = authorization.getToken(OAuth2RefreshToken::class.java)
val oidcIdToken = authorization.getToken(OidcIdToken::class.java)
val userCode = authorization.getToken(OAuth2UserCode::class.java)
val deviceCode = authorization.getToken(OAuth2DeviceCode::class.java)
Authorization.insert {
it[id] = authorization.id
it[registeredClientId] = authorization.registeredClientId
it[principalName] = authorization.principalName
it[authorizationGrantType] = authorization.authorizationGrantType.value
it[authorizedScopes] =
authorization.authorizedScopes.joinToString(",").takeIf { s -> s.isNotEmpty() }
it[attributes] = mapToJson(authorization.attributes)
it[state] = authorization.getAttribute(OAuth2ParameterNames.STATE)
it[authorizationCodeValue] = authorizationCodeToken?.token?.tokenValue
it[authorizationCodeIssuedAt] = authorizationCodeToken?.token?.issuedAt
it[authorizationCodeExpiresAt] = authorizationCodeToken?.token?.expiresAt
it[authorizationCodeMetadata] =
authorizationCodeToken?.metadata?.let { it1 -> mapToJson(it1) }
it[accessTokenValue] = accessToken?.token?.tokenValue
it[accessTokenIssuedAt] = accessToken?.token?.issuedAt
it[accessTokenExpiresAt] = accessToken?.token?.expiresAt
it[accessTokenMetadata] = accessToken?.metadata?.let { it1 -> mapToJson(it1) }
it[accessTokenType] = accessToken?.token?.tokenType?.value
it[accessTokenScopes] =
accessToken?.run { token.scopes.joinToString(",").takeIf { s -> s.isNotEmpty() } }
it[refreshTokenValue] = refreshToken?.token?.tokenValue
it[refreshTokenIssuedAt] = refreshToken?.token?.issuedAt
it[refreshTokenExpiresAt] = refreshToken?.token?.expiresAt
it[refreshTokenMetadata] = refreshToken?.metadata?.let { it1 -> mapToJson(it1) }
it[oidcIdTokenValue] = oidcIdToken?.token?.tokenValue
it[oidcIdTokenIssuedAt] = oidcIdToken?.token?.issuedAt
it[oidcIdTokenExpiresAt] = oidcIdToken?.token?.expiresAt
it[oidcIdTokenMetadata] = oidcIdToken?.metadata?.let { it1 -> mapToJson(it1) }
it[userCodeValue] = userCode?.token?.tokenValue
it[userCodeIssuedAt] = userCode?.token?.issuedAt
it[userCodeExpiresAt] = userCode?.token?.expiresAt
it[userCodeMetadata] = userCode?.metadata?.let { it1 -> mapToJson(it1) }
it[deviceCodeValue] = deviceCode?.token?.tokenValue
it[deviceCodeIssuedAt] = deviceCode?.token?.issuedAt
it[deviceCodeExpiresAt] = deviceCode?.token?.expiresAt
it[deviceCodeMetadata] = deviceCode?.metadata?.let { it1 -> mapToJson(it1) }
}
} else {
val authorizationCodeToken = authorization.getToken(OAuth2AuthorizationCode::class.java)
val accessToken = authorization.getToken(OAuth2AccessToken::class.java)
val refreshToken = authorization.getToken(OAuth2RefreshToken::class.java)
val oidcIdToken = authorization.getToken(OidcIdToken::class.java)
val userCode = authorization.getToken(OAuth2UserCode::class.java)
val deviceCode = authorization.getToken(OAuth2DeviceCode::class.java)
Authorization.update({ Authorization.id eq authorization.id }) {
it[registeredClientId] = authorization.registeredClientId
it[principalName] = authorization.principalName
it[authorizationGrantType] = authorization.authorizationGrantType.value
it[authorizedScopes] =
authorization.authorizedScopes.joinToString(",").takeIf { s -> s.isNotEmpty() }
it[attributes] = mapToJson(authorization.attributes)
it[state] = authorization.getAttribute(OAuth2ParameterNames.STATE)
it[authorizationCodeValue] = authorizationCodeToken?.token?.tokenValue
it[authorizationCodeIssuedAt] = authorizationCodeToken?.token?.issuedAt
it[authorizationCodeExpiresAt] = authorizationCodeToken?.token?.expiresAt
it[authorizationCodeMetadata] =
authorizationCodeToken?.metadata?.let { it1 -> mapToJson(it1) }
it[accessTokenValue] = accessToken?.token?.tokenValue
it[accessTokenIssuedAt] = accessToken?.token?.issuedAt
it[accessTokenExpiresAt] = accessToken?.token?.expiresAt
it[accessTokenMetadata] = accessToken?.metadata?.let { it1 -> mapToJson(it1) }
it[accessTokenType] = accessToken?.run { token.tokenType.value }
it[accessTokenScopes] =
accessToken?.run { token.scopes.joinToString(",").takeIf { s -> s.isNotEmpty() } }
it[refreshTokenValue] = refreshToken?.token?.tokenValue
it[refreshTokenIssuedAt] = refreshToken?.token?.issuedAt
it[refreshTokenExpiresAt] = refreshToken?.token?.expiresAt
it[refreshTokenMetadata] = refreshToken?.metadata?.let { it1 -> mapToJson(it1) }
it[oidcIdTokenValue] = oidcIdToken?.token?.tokenValue
it[oidcIdTokenIssuedAt] = oidcIdToken?.token?.issuedAt
it[oidcIdTokenExpiresAt] = oidcIdToken?.token?.expiresAt
it[oidcIdTokenMetadata] = oidcIdToken?.metadata?.let { it1 -> mapToJson(it1) }
it[userCodeValue] = userCode?.token?.tokenValue
it[userCodeIssuedAt] = userCode?.token?.issuedAt
it[userCodeExpiresAt] = userCode?.token?.expiresAt
it[userCodeMetadata] = userCode?.metadata?.let { it1 -> mapToJson(it1) }
it[deviceCodeValue] = deviceCode?.token?.tokenValue
it[deviceCodeIssuedAt] = deviceCode?.token?.issuedAt
it[deviceCodeExpiresAt] = deviceCode?.token?.expiresAt
it[deviceCodeMetadata] = deviceCode?.metadata?.let { it1 -> mapToJson(it1) }
}
}
}
}
override fun remove(authorization: OAuth2Authorization?) {
if (authorization == null) {
return
}
Authorization.deleteWhere { id eq authorization.id }
}
override fun findById(id: String?): OAuth2Authorization? {
if (id == null) {
return null
}
return Authorization.selectAll().where { Authorization.id eq id }.singleOrNull()?.toAuthorization()
}
override fun findByToken(token: String?, tokenType: OAuth2TokenType?): OAuth2Authorization? = runBlocking {
requireNotNull(token)
transaction.transaction {
when (tokenType?.value) {
null -> {
Authorization.selectAll().where { Authorization.authorizationCodeValue eq token }.orWhere {
Authorization.accessTokenValue eq token
}.orWhere {
Authorization.oidcIdTokenValue eq token
}.orWhere {
Authorization.refreshTokenValue eq token
}.orWhere {
Authorization.userCodeValue eq token
}.orWhere {
Authorization.deviceCodeValue eq token
}
}
OAuth2ParameterNames.STATE -> {
Authorization.selectAll().where { Authorization.state eq token }
}
OAuth2ParameterNames.CODE -> {
Authorization.selectAll().where { Authorization.authorizationCodeValue eq token }
}
OAuth2ParameterNames.ACCESS_TOKEN -> {
Authorization.selectAll().where { Authorization.accessTokenValue eq token }
}
OidcParameterNames.ID_TOKEN -> {
Authorization.selectAll().where { Authorization.oidcIdTokenValue eq token }
}
OAuth2ParameterNames.REFRESH_TOKEN -> {
Authorization.selectAll().where { Authorization.refreshTokenValue eq token }
}
OAuth2ParameterNames.USER_CODE -> {
Authorization.selectAll().where { Authorization.userCodeValue eq token }
}
OAuth2ParameterNames.DEVICE_CODE -> {
Authorization.selectAll().where { Authorization.deviceCodeValue eq token }
}
else -> {
null
}
}?.singleOrNull()?.toAuthorization()
}
}
@Suppress("LongMethod", "CyclomaticComplexMethod", "CastToNullableType", "UNCHECKED_CAST")
fun ResultRow.toAuthorization(): OAuth2Authorization {
val registeredClientId = this[Authorization.registeredClientId]
val registeredClient = registeredClientRepository.findById(registeredClientId)
val builder = OAuth2Authorization.withRegisteredClient(registeredClient)
val id = this[Authorization.id]
val principalName = this[Authorization.principalName]
val authorizationGrantType = this[Authorization.authorizationGrantType]
val authorizedScopes = this[Authorization.authorizedScopes]?.split(",").orEmpty().toSet()
val attributes = this[Authorization.attributes]?.let { jsonToMap<String, Any>(it) }.orEmpty()
builder.id(id).principalName(principalName)
.authorizationGrantType(AuthorizationGrantType(authorizationGrantType)).authorizedScopes(authorizedScopes)
.attributes { it.putAll(attributes) }
val state = this[Authorization.state].orEmpty()
if (state.isNotBlank()) {
builder.attribute(OAuth2ParameterNames.STATE, state)
}
val authorizationCodeValue = this[Authorization.authorizationCodeValue].orEmpty()
if (authorizationCodeValue.isNotBlank()) {
val authorizationCodeIssuedAt = this[Authorization.authorizationCodeIssuedAt]
val authorizationCodeExpiresAt = this[Authorization.authorizationCodeExpiresAt]
val authorizationCodeMetadata = this[Authorization.authorizationCodeMetadata]?.let {
jsonToMap<String, Any>(
it
)
}.orEmpty()
val oAuth2AuthorizationCode =
OAuth2AuthorizationCode(authorizationCodeValue, authorizationCodeIssuedAt, authorizationCodeExpiresAt)
builder.token(oAuth2AuthorizationCode) {
it.putAll(authorizationCodeMetadata)
}
}
val accessTokenValue = this[Authorization.accessTokenValue].orEmpty()
if (accessTokenValue.isNotBlank()) {
val accessTokenIssuedAt = this[Authorization.accessTokenIssuedAt]
val accessTokenExpiresAt = this[Authorization.accessTokenExpiresAt]
val accessTokenMetadata =
this[Authorization.accessTokenMetadata]?.let { jsonToMap<String, Any>(it) }.orEmpty()
val accessTokenType =
if (this[Authorization.accessTokenType].equals(OAuth2AccessToken.TokenType.BEARER.value, true)) {
OAuth2AccessToken.TokenType.BEARER
} else {
null
}
val accessTokenScope = this[Authorization.accessTokenScopes]?.split(",").orEmpty().toSet()
val oAuth2AccessToken = OAuth2AccessToken(
accessTokenType,
accessTokenValue,
accessTokenIssuedAt,
accessTokenExpiresAt,
accessTokenScope
)
builder.token(oAuth2AccessToken) { it.putAll(accessTokenMetadata) }
}
val oidcIdTokenValue = this[Authorization.oidcIdTokenValue].orEmpty()
if (oidcIdTokenValue.isNotBlank()) {
val oidcTokenIssuedAt = this[Authorization.oidcIdTokenIssuedAt]
val oidcTokenExpiresAt = this[Authorization.oidcIdTokenExpiresAt]
val oidcTokenMetadata =
this[Authorization.oidcIdTokenMetadata]?.let { jsonToMap<String, Any>(it) }.orEmpty()
val oidcIdToken = OidcIdToken(
oidcIdTokenValue,
oidcTokenIssuedAt,
oidcTokenExpiresAt,
oidcTokenMetadata.getValue(OAuth2Authorization.Token.CLAIMS_METADATA_NAME)
as MutableMap<String, Any>?
)
builder.token(oidcIdToken) { it.putAll(oidcTokenMetadata) }
}
val refreshTokenValue = this[Authorization.refreshTokenValue].orEmpty()
if (refreshTokenValue.isNotBlank()) {
val refreshTokenIssuedAt = this[Authorization.refreshTokenIssuedAt]
val refreshTokenExpiresAt = this[Authorization.refreshTokenExpiresAt]
val refreshTokenMetadata =
this[Authorization.refreshTokenMetadata]?.let { jsonToMap<String, Any>(it) }.orEmpty()
val oAuth2RefreshToken = OAuth2RefreshToken(refreshTokenValue, refreshTokenIssuedAt, refreshTokenExpiresAt)
builder.token(oAuth2RefreshToken) { it.putAll(refreshTokenMetadata) }
}
val userCodeValue = this[Authorization.userCodeValue].orEmpty()
if (userCodeValue.isNotBlank()) {
val userCodeIssuedAt = this[Authorization.userCodeIssuedAt]
val userCodeExpiresAt = this[Authorization.userCodeExpiresAt]
val userCodeMetadata =
this[Authorization.userCodeMetadata]?.let { jsonToMap<String, Any>(it) }.orEmpty()
val oAuth2UserCode = OAuth2UserCode(userCodeValue, userCodeIssuedAt, userCodeExpiresAt)
builder.token(oAuth2UserCode) { it.putAll(userCodeMetadata) }
}
val deviceCodeValue = this[Authorization.deviceCodeValue].orEmpty()
if (deviceCodeValue.isNotBlank()) {
val deviceCodeIssuedAt = this[Authorization.deviceCodeIssuedAt]
val deviceCodeExpiresAt = this[Authorization.deviceCodeExpiresAt]
val deviceCodeMetadata =
this[Authorization.deviceCodeMetadata]?.let { jsonToMap<String, Any>(it) }.orEmpty()
val oAuth2DeviceCode = OAuth2DeviceCode(deviceCodeValue, deviceCodeIssuedAt, deviceCodeExpiresAt)
builder.token(oAuth2DeviceCode) { it.putAll(deviceCodeMetadata) }
}
return builder.build()
}
private fun mapToJson(map: Map<*, *>): String = objectMapper.writeValueAsString(map)
private fun <T, U> jsonToMap(json: String): Map<T, U> = objectMapper.readValue(json)
companion object {
val objectMapper: ObjectMapper = ObjectMapper()
init {
val classLoader = ExposedOAuth2AuthorizationService::class.java.classLoader
val modules = SecurityJackson2Modules.getModules(classLoader)
objectMapper.registerModules(JavaTimeModule())
objectMapper.registerModules(modules)
objectMapper.registerModules(OAuth2AuthorizationServerJackson2Module())
objectMapper.registerModules(CoreJackson2Module())
objectMapper.addMixIn(UserDetailsImpl::class.java, UserDetailsMixin::class.java)
}
}
}
object Authorization : Table("application_authorization") {
val id: Column<String> = varchar("id", 255)
val registeredClientId: Column<String> = varchar("registered_client_id", 255)
val principalName: Column<String> = varchar("principal_name", 255)
val authorizationGrantType: Column<String> = varchar("authorization_grant_type", 255)
val authorizedScopes: Column<String?> = varchar("authorized_scopes", 1000).nullable().default(null)
val attributes: Column<String?> = varchar("attributes", 4000).nullable().default(null)
val state: Column<String?> = varchar("state", 500).nullable().default(null)
val authorizationCodeValue: Column<String?> = varchar("authorization_code_value", 4000).nullable().default(null)
val authorizationCodeIssuedAt: Column<Instant?> = timestamp("authorization_code_issued_at").nullable().default(null)
val authorizationCodeExpiresAt: Column<Instant?> = timestamp("authorization_code_expires_at").nullable().default(
null
)
val authorizationCodeMetadata: Column<String?> = varchar("authorization_code_metadata", 2000).nullable().default(
null
)
val accessTokenValue: Column<String?> = varchar("access_token_value", 4000).nullable().default(null)
val accessTokenIssuedAt: Column<Instant?> = timestamp("access_token_issued_at").nullable().default(null)
val accessTokenExpiresAt: Column<Instant?> = timestamp("access_token_expires_at").nullable().default(null)
val accessTokenMetadata: Column<String?> = varchar("access_token_metadata", 2000).nullable().default(null)
val accessTokenType: Column<String?> = varchar("access_token_type", 255).nullable().default(null)
val accessTokenScopes: Column<String?> = varchar("access_token_scopes", 1000).nullable().default(null)
val refreshTokenValue: Column<String?> = varchar("refresh_token_value", 4000).nullable().default(null)
val refreshTokenIssuedAt: Column<Instant?> = timestamp("refresh_token_issued_at").nullable().default(null)
val refreshTokenExpiresAt: Column<Instant?> = timestamp("refresh_token_expires_at").nullable().default(null)
val refreshTokenMetadata: Column<String?> = varchar("refresh_token_metadata", 2000).nullable().default(null)
val oidcIdTokenValue: Column<String?> = varchar("oidc_id_token_value", 4000).nullable().default(null)
val oidcIdTokenIssuedAt: Column<Instant?> = timestamp("oidc_id_token_issued_at").nullable().default(null)
val oidcIdTokenExpiresAt: Column<Instant?> = timestamp("oidc_id_token_expires_at").nullable().default(null)
val oidcIdTokenMetadata: Column<String?> = varchar("oidc_id_token_metadata", 2000).nullable().default(null)
val oidcIdTokenClaims: Column<String?> = varchar("oidc_id_token_claims", 2000).nullable().default(null)
val userCodeValue: Column<String?> = varchar("user_code_value", 4000).nullable().default(null)
val userCodeIssuedAt: Column<Instant?> = timestamp("user_code_issued_at").nullable().default(null)
val userCodeExpiresAt: Column<Instant?> = timestamp("user_code_expires_at").nullable().default(null)
val userCodeMetadata: Column<String?> = varchar("user_code_metadata", 2000).nullable().default(null)
val deviceCodeValue: Column<String?> = varchar("device_code_value", 4000).nullable().default(null)
val deviceCodeIssuedAt: Column<Instant?> = timestamp("device_code_issued_at").nullable().default(null)
val deviceCodeExpiresAt: Column<Instant?> = timestamp("device_code_expires_at").nullable().default(null)
val deviceCodeMetadata: Column<String?> = varchar("device_code_metadata", 2000).nullable().default(null)
override val primaryKey: PrimaryKey = PrimaryKey(id)
}

View File

@ -1,206 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.infrastructure.springframework.oauth2
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.RegisteredClient.clientId
import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.RegisteredClient.clientSettings
import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.RegisteredClient.tokenSettings
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.javatime.CurrentTimestamp
import org.jetbrains.exposed.sql.javatime.timestamp
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.security.jackson2.SecurityJackson2Modules
import org.springframework.security.oauth2.core.AuthorizationGrantType
import org.springframework.security.oauth2.core.ClientAuthenticationMethod
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository
import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings
import org.springframework.security.oauth2.server.authorization.settings.ConfigurationSettingNames
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings
import org.springframework.stereotype.Repository
import org.springframework.transaction.annotation.Transactional
import java.time.Instant
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient as SpringRegisteredClient
@Repository
class RegisteredClientRepositoryImpl : RegisteredClientRepository {
override fun save(registeredClient: SpringRegisteredClient?) {
requireNotNull(registeredClient)
val singleOrNull =
RegisteredClient.selectAll().where { RegisteredClient.id eq registeredClient.id }.singleOrNull()
if (singleOrNull == null) {
RegisteredClient.insert {
it[id] = registeredClient.id
it[clientId] = registeredClient.clientId
it[clientIdIssuedAt] = registeredClient.clientIdIssuedAt ?: Instant.now()
it[clientSecret] = registeredClient.clientSecret
it[clientSecretExpiresAt] = registeredClient.clientSecretExpiresAt
it[clientName] = registeredClient.clientName
it[clientAuthenticationMethods] =
registeredClient.clientAuthenticationMethods.joinToString(",") { method -> method.value }
it[authorizationGrantTypes] =
registeredClient.authorizationGrantTypes.joinToString(",") { type -> type.value }
it[redirectUris] = registeredClient.redirectUris.joinToString(",")
it[postLogoutRedirectUris] = registeredClient.postLogoutRedirectUris.joinToString(",")
it[scopes] = registeredClient.scopes.joinToString(",")
it[clientSettings] = mapToJson(registeredClient.clientSettings.settings)
it[tokenSettings] = mapToJson(registeredClient.tokenSettings.settings)
}
} else {
RegisteredClient.update({ RegisteredClient.id eq registeredClient.id }) {
it[clientId] = registeredClient.clientId
it[clientIdIssuedAt] = registeredClient.clientIdIssuedAt ?: Instant.now()
it[clientSecret] = registeredClient.clientSecret
it[clientSecretExpiresAt] = registeredClient.clientSecretExpiresAt
it[clientName] = registeredClient.clientName
it[clientAuthenticationMethods] = registeredClient.clientAuthenticationMethods.joinToString(",")
it[authorizationGrantTypes] = registeredClient.authorizationGrantTypes.joinToString(",")
it[redirectUris] = registeredClient.redirectUris.joinToString(",")
it[postLogoutRedirectUris] = registeredClient.postLogoutRedirectUris.joinToString(",")
it[scopes] = registeredClient.scopes.joinToString(",")
it[clientSettings] = mapToJson(registeredClient.clientSettings.settings)
it[tokenSettings] = mapToJson(registeredClient.tokenSettings.settings)
}
}
}
override fun findById(id: String?): SpringRegisteredClient? {
if (id == null) {
return null
}
return RegisteredClient.selectAll().where { RegisteredClient.id eq id }.singleOrNull()?.toRegisteredClient()
}
@Transactional
override fun findByClientId(clientId: String?): SpringRegisteredClient? {
if (clientId == null) {
return null
}
val toRegisteredClient =
RegisteredClient.selectAll().where { RegisteredClient.clientId eq clientId }.singleOrNull()
?.toRegisteredClient()
LOGGER.trace("findByClientId: {}", toRegisteredClient)
return toRegisteredClient
}
private fun mapToJson(map: Map<*, *>): String = objectMapper.writeValueAsString(map)
private fun <T, U> jsonToMap(json: String): Map<T, U> = objectMapper.readValue(json)
@Suppress("CyclomaticComplexMethod")
fun ResultRow.toRegisteredClient(): SpringRegisteredClient {
fun resolveClientAuthenticationMethods(string: String): ClientAuthenticationMethod {
return when (string) {
ClientAuthenticationMethod.CLIENT_SECRET_BASIC.value -> ClientAuthenticationMethod.CLIENT_SECRET_BASIC
ClientAuthenticationMethod.CLIENT_SECRET_JWT.value -> ClientAuthenticationMethod.CLIENT_SECRET_JWT
ClientAuthenticationMethod.CLIENT_SECRET_POST.value -> ClientAuthenticationMethod.CLIENT_SECRET_POST
ClientAuthenticationMethod.NONE.value -> ClientAuthenticationMethod.NONE
else -> {
ClientAuthenticationMethod(string)
}
}
}
fun resolveAuthorizationGrantType(string: String): AuthorizationGrantType {
return when (string) {
AuthorizationGrantType.AUTHORIZATION_CODE.value -> AuthorizationGrantType.AUTHORIZATION_CODE
AuthorizationGrantType.CLIENT_CREDENTIALS.value -> AuthorizationGrantType.CLIENT_CREDENTIALS
AuthorizationGrantType.REFRESH_TOKEN.value -> AuthorizationGrantType.REFRESH_TOKEN
else -> {
AuthorizationGrantType(string)
}
}
}
val clientAuthenticationMethods = this[RegisteredClient.clientAuthenticationMethods].split(",").toSet()
val authorizationGrantTypes = this[RegisteredClient.authorizationGrantTypes].split(",").toSet()
val redirectUris = this[RegisteredClient.redirectUris]?.split(",").orEmpty().toSet()
val postLogoutRedirectUris = this[RegisteredClient.postLogoutRedirectUris]?.split(",").orEmpty().toSet()
val clientScopes = this[RegisteredClient.scopes].split(",").toSet()
val builder = SpringRegisteredClient
.withId(this[RegisteredClient.id])
.clientId(this[clientId])
.clientIdIssuedAt(this[RegisteredClient.clientIdIssuedAt])
.clientSecret(this[RegisteredClient.clientSecret])
.clientSecretExpiresAt(this[RegisteredClient.clientSecretExpiresAt])
.clientName(this[RegisteredClient.clientName])
.clientAuthenticationMethods {
clientAuthenticationMethods.forEach { s ->
it.add(resolveClientAuthenticationMethods(s))
}
}
.authorizationGrantTypes {
authorizationGrantTypes.forEach { s ->
it.add(resolveAuthorizationGrantType(s))
}
}
.redirectUris { it.addAll(redirectUris) }
.postLogoutRedirectUris { it.addAll(postLogoutRedirectUris) }
.scopes { it.addAll(clientScopes) }
.clientSettings(ClientSettings.withSettings(jsonToMap(this[clientSettings])).build())
val tokenSettingsMap = jsonToMap<String, Any>(this[tokenSettings])
val withSettings = TokenSettings.withSettings(tokenSettingsMap)
if (tokenSettingsMap.containsKey(ConfigurationSettingNames.Token.ACCESS_TOKEN_FORMAT)) {
withSettings.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
}
builder.tokenSettings(withSettings.build())
return builder.build()
}
companion object {
val objectMapper: ObjectMapper = ObjectMapper()
val LOGGER: Logger = LoggerFactory.getLogger(RegisteredClientRepositoryImpl::class.java)
init {
val classLoader = ExposedOAuth2AuthorizationService::class.java.classLoader
val modules = SecurityJackson2Modules.getModules(classLoader)
objectMapper.registerModules(JavaTimeModule())
objectMapper.registerModules(modules)
objectMapper.registerModules(OAuth2AuthorizationServerJackson2Module())
}
}
}
// org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql
object RegisteredClient : Table("registered_client") {
val id: Column<String> = varchar("id", 100)
val clientId: Column<String> = varchar("client_id", 100)
val clientIdIssuedAt: Column<Instant> = timestamp("client_id_issued_at").defaultExpression(CurrentTimestamp)
val clientSecret: Column<String?> = varchar("client_secret", 200).nullable().default(null)
val clientSecretExpiresAt: Column<Instant?> = timestamp("client_secret_expires_at").nullable().default(null)
val clientName: Column<String> = varchar("client_name", 200)
val clientAuthenticationMethods: Column<String> = varchar("client_authentication_methods", 1000)
val authorizationGrantTypes: Column<String> = varchar("authorization_grant_types", 1000)
val redirectUris: Column<String?> = varchar("redirect_uris", 1000).nullable().default(null)
val postLogoutRedirectUris: Column<String?> = varchar("post_logout_redirect_uris", 1000).nullable().default(null)
val scopes: Column<String> = varchar("scopes", 1000)
val clientSettings: Column<String> = varchar("client_settings", 2000)
val tokenSettings: Column<String> = varchar("token_settings", 2000)
override val primaryKey: PrimaryKey = PrimaryKey(id)
}

View File

@ -1,120 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.infrastructure.springframework.oauth2
import com.fasterxml.jackson.annotation.JsonAutoDetect
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.User
import java.io.Serial
class UserDetailsImpl(
val id: Long,
username: String?,
password: String?,
enabled: Boolean,
accountNonExpired: Boolean,
credentialsNonExpired: Boolean,
accountNonLocked: Boolean,
authorities: MutableCollection<out GrantedAuthority>?
) : User(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities) {
override fun toString(): String {
return "UserDetailsImpl(" +
"id=$id" +
")" +
" ${super.toString()}"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as UserDetailsImpl
return id == other.id
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + id.hashCode()
return result
}
companion object {
@Serial
private const val serialVersionUID: Long = -899168205656607781L
}
}
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
@JsonDeserialize(using = UserDetailsDeserializer::class)
@JsonAutoDetect(
fieldVisibility = JsonAutoDetect.Visibility.ANY,
getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE,
creatorVisibility = JsonAutoDetect.Visibility.NONE
)
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonSubTypes
@Suppress("UnnecessaryAbstractClass")
abstract class UserDetailsMixin
class UserDetailsDeserializer : JsonDeserializer<UserDetailsImpl>() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): UserDetailsImpl {
val mapper = p.codec as ObjectMapper
val jsonNode: JsonNode = mapper.readTree(p)
val authorities: Set<GrantedAuthority> = mapper.convertValue(
jsonNode["authorities"],
SIMPLE_GRANTED_AUTHORITY_SET
)
val password = jsonNode.readText("password")
return UserDetailsImpl(
id = jsonNode["id"].longValue(),
username = jsonNode.readText("username"),
password = password,
enabled = true,
accountNonExpired = true,
credentialsNonExpired = true,
accountNonLocked = true,
authorities = authorities.toMutableList(),
)
}
fun JsonNode.readText(field: String, defaultValue: String = ""): String {
return when {
has(field) -> get(field).asText(defaultValue)
else -> defaultValue
}
}
companion object {
private val SIMPLE_GRANTED_AUTHORITY_SET = object : TypeReference<Set<SimpleGrantedAuthority>>() {}
}
}

View File

@ -1,39 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.infrastructure.springframework.security
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.stereotype.Component
@Component
class OAuth2JwtLoginUserContextHolder : LoginUserContextHolder {
override fun getLoginUserId(): Long {
val principal = SecurityContextHolder.getContext().authentication.principal as Jwt
return principal.getClaim<String>("uid").toLong()
}
override fun getLoginUserIdOrNull(): Long? {
val principal = SecurityContextHolder.getContext()?.authentication?.principal
if (principal !is Jwt) {
return null
}
return principal.getClaim<String>("uid").toLongOrNull()
}
}

View File

@ -1,22 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.generate
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class JsonOrFormBind

View File

@ -1,78 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.generate
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.core.MethodParameter
import org.springframework.validation.BindException
import org.springframework.web.bind.support.WebDataBinderFactory
import org.springframework.web.context.request.NativeWebRequest
import org.springframework.web.method.annotation.ModelAttributeMethodProcessor
import org.springframework.web.method.support.HandlerMethodArgumentResolver
import org.springframework.web.method.support.ModelAndViewContainer
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor
@Suppress("TooGenericExceptionCaught")
class JsonOrFormModelMethodProcessor(
private val modelAttributeMethodProcessor: ModelAttributeMethodProcessor,
private val requestResponseBodyMethodProcessor: RequestResponseBodyMethodProcessor
) : HandlerMethodArgumentResolver {
private val isJsonRegex = Regex("application/((\\w*)\\+)?json")
override fun supportsParameter(parameter: MethodParameter): Boolean =
parameter.hasParameterAnnotation(JsonOrFormBind::class.java)
override fun resolveArgument(
parameter: MethodParameter,
mavContainer: ModelAndViewContainer?,
webRequest: NativeWebRequest,
binderFactory: WebDataBinderFactory?
): Any? {
val contentType = webRequest.getHeader("Content-Type").orEmpty()
logger.trace("ContentType is {}", contentType)
if (contentType.contains(isJsonRegex)) {
logger.trace("Determine content type as json.")
return requestResponseBodyMethodProcessor.resolveArgument(
parameter,
mavContainer,
webRequest,
binderFactory
)
}
return try {
modelAttributeMethodProcessor.resolveArgument(parameter, mavContainer, webRequest, binderFactory)
} catch (e: BindException) {
throw e
} catch (exception: Exception) {
try {
requestResponseBodyMethodProcessor.resolveArgument(parameter, mavContainer, webRequest, binderFactory)
} catch (e: BindException) {
throw e
} catch (e: Exception) {
logger.warn("Failed to bind request (1)", exception)
logger.warn("Failed to bind request (2)", e)
throw IllegalArgumentException("Failed to bind request.")
}
}
}
companion object {
val logger: Logger = LoggerFactory.getLogger(JsonOrFormModelMethodProcessor::class.java)
}
}

View File

@ -1,9 +1,9 @@
package dev.usbharu.hideout.core.domain.model.actor package dev.usbharu.hideout.core.domain.model.actor
import dev.usbharu.hideout.application.service.id.TwitterSnowflakeIdGenerateService
import dev.usbharu.hideout.core.domain.model.emoji.EmojiId import dev.usbharu.hideout.core.domain.model.emoji.EmojiId
import dev.usbharu.hideout.core.domain.model.instance.InstanceId import dev.usbharu.hideout.core.domain.model.instance.InstanceId
import dev.usbharu.hideout.core.domain.model.shared.Domain import dev.usbharu.hideout.core.domain.model.shared.Domain
import dev.usbharu.hideout.core.infrastructure.other.TwitterSnowflakeIdGenerateService
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.net.URI import java.net.URI
import java.time.Instant import java.time.Instant

View File

@ -1,5 +0,0 @@
package dev.usbharu
fun main() {
println("Hello World!")
}