feat: ローカルユーザーを作成できるように

This commit is contained in:
usbharu 2024-06-06 23:26:20 +09:00
parent 9c271b8cc8
commit cf48ae651b
25 changed files with 674 additions and 240 deletions

View File

@ -28,6 +28,7 @@ import dev.usbharu.hideout.core.domain.service.userdetail.UserDetailDomainServic
import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService 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
import java.net.URI
@Service @Service
class RegisterLocalActorApplicationService( class RegisterLocalActorApplicationService(
@ -41,8 +42,8 @@ class RegisterLocalActorApplicationService(
private val userDetailRepository: UserDetailRepository, private val userDetailRepository: UserDetailRepository,
private val idGenerateService: IdGenerateService, private val idGenerateService: IdGenerateService,
) { ) {
suspend fun register(registerLocalActor: RegisterLocalActor) { suspend fun register(registerLocalActor: RegisterLocalActor): URI {
transaction.transaction { return transaction.transaction {
if (actorDomainService.usernameAlreadyUse(registerLocalActor.name)) { if (actorDomainService.usernameAlreadyUse(registerLocalActor.name)) {
// todo 適切な例外を考える // todo 適切な例外を考える
throw Exception("Username already exists") throw Exception("Username already exists")
@ -61,6 +62,7 @@ class RegisterLocalActorApplicationService(
password = userDetailDomainService.hashPassword(registerLocalActor.password), password = userDetailDomainService.hashPassword(registerLocalActor.password),
) )
userDetailRepository.save(userDetail) userDetailRepository.save(userDetail)
actor.url
} }
} }
} }

View File

@ -0,0 +1,26 @@
/*
* 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.application.application
import java.net.URI
data class RegisterApplication(
val name: String,
val redirectUris: Set<URI>,
val useRefreshToken: Boolean,
val scopes: Set<String>,
)

View File

@ -0,0 +1,93 @@
/*
* 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.application.application
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.application.Application
import dev.usbharu.hideout.core.domain.model.application.ApplicationId
import dev.usbharu.hideout.core.domain.model.application.ApplicationName
import dev.usbharu.hideout.core.domain.model.application.ApplicationRepository
import dev.usbharu.hideout.core.domain.service.userdetail.PasswordEncoder
import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService
import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.SecureTokenGenerator
import org.springframework.security.oauth2.core.AuthorizationGrantType
import org.springframework.security.oauth2.core.ClientAuthenticationMethod
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings
import org.springframework.stereotype.Service
import java.time.Duration
@Service
class RegisterApplicationApplicationService(
private val idGenerateService: IdGenerateService,
private val passwordEncoder: PasswordEncoder,
private val secureTokenGenerator: SecureTokenGenerator,
private val registeredClientRepository: RegisteredClientRepository,
private val transaction: Transaction,
private val applicationRepository: ApplicationRepository,
) {
suspend fun register(registerApplication: RegisterApplication): RegisteredApplication {
return transaction.transaction {
val id = idGenerateService.generateId()
val clientSecret = secureTokenGenerator.generate()
val registeredClient = RegisteredClient
.withId(id.toString())
.clientId(id.toString())
.clientSecret(passwordEncoder.encode(clientSecret))
.clientName(registerApplication.name)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.apply {
if (registerApplication.useRefreshToken) {
authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
tokenSettings(
TokenSettings
.builder()
.accessTokenTimeToLive(Duration.ofSeconds(31536000000))
.build()
)
}
}
.redirectUris { set ->
set.addAll(registerApplication.redirectUris.map { it.toString() })
}
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.scopes { it.addAll(registerApplication.scopes) }
.build()
registeredClientRepository.save(registeredClient)
val application = Application(ApplicationId(id), ApplicationName(registerApplication.name))
applicationRepository.save(application)
RegisteredApplication(
id = id,
name = registerApplication.name,
clientSecret = clientSecret,
clientId = id,
redirectUris = registerApplication.redirectUris
)
}
}
}

View File

@ -0,0 +1,27 @@
/*
* 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.application.application
import java.net.URI
data class RegisteredApplication(
val id: Long,
val name: String,
val redirectUris: Set<URI>,
val clientSecret: String,
val clientId: Long,
)

View File

@ -0,0 +1,59 @@
/*
* 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.application.instance
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.config.ApplicationConfig
import dev.usbharu.hideout.core.domain.model.instance.*
import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService
import org.springframework.boot.context.event.ApplicationReadyEvent
import org.springframework.boot.info.BuildProperties
import org.springframework.context.event.EventListener
import org.springframework.stereotype.Service
import java.time.Instant
@Service
class InitLocalInstanceApplicationService(
private val applicationConfig: ApplicationConfig,
private val instanceRepository: InstanceRepository,
private val idGenerateService: IdGenerateService,
private val buildProperties: BuildProperties,
private val transaction: Transaction,
) {
@EventListener(ApplicationReadyEvent::class)
suspend fun init() = transaction.transaction {
val findByUrl = instanceRepository.findByUrl(applicationConfig.url.toURI())
if (findByUrl == null) {
val instance = Instance(
InstanceId(idGenerateService.generateId()),
InstanceName(applicationConfig.url.host),
InstanceDescription(""),
applicationConfig.url.toURI(),
applicationConfig.url.toURI(),
null,
InstanceSoftware("hideout"),
InstanceVersion(buildProperties.version),
false,
false,
InstanceModerationNote(""),
Instant.now(),
)
instanceRepository.save(instance)
}
}
}

View File

@ -18,13 +18,124 @@ package dev.usbharu.hideout.core.config
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration 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.jdbc.core.JdbcOperations
import org.springframework.security.access.hierarchicalroles.RoleHierarchy
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl
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.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint
@Configuration @Configuration
@EnableWebSecurity(debug = false)
class SecurityConfig { class SecurityConfig {
@Bean @Bean
fun passwordEncoder(): PasswordEncoder { fun passwordEncoder(): PasswordEncoder {
return BCryptPasswordEncoder() return BCryptPasswordEncoder()
} }
@Bean
@Order(1)
fun oauth2Provider(http: HttpSecurity): SecurityFilterChain {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http)
http {
exceptionHandling {
authenticationEntryPoint = LoginUrlAuthenticationEntryPoint("/login")
}
}
return http.build()
}
@Bean
@Order(3)
fun httpSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize("/error", permitAll)
authorize("/login", permitAll)
authorize(GET, "/.well-known/**", permitAll)
authorize(GET, "/nodeinfo/2.0", permitAll)
authorize(GET, "/auth/sign_up", hasRole("ANONYMOUS"))
authorize(POST, "/auth/sign_up", permitAll)
authorize(anyRequest, authenticated)
}
formLogin {
}
}
return http.build()
}
@Bean
fun registeredClientRepository(jdbcOperations: JdbcOperations): RegisteredClientRepository {
return JdbcRegisteredClientRepository(jdbcOperations)
}
@Bean
fun roleHierarchy(): RoleHierarchy {
val roleHierarchyImpl = RoleHierarchyImpl.fromHierarchy(
"""
SCOPE_read > SCOPE_read:accounts
SCOPE_read > SCOPE_read:accounts
SCOPE_read > SCOPE_read:blocks
SCOPE_read > SCOPE_read:bookmarks
SCOPE_read > SCOPE_read:favourites
SCOPE_read > SCOPE_read:filters
SCOPE_read > SCOPE_read:follows
SCOPE_read > SCOPE_read:lists
SCOPE_read > SCOPE_read:mutes
SCOPE_read > SCOPE_read:notifications
SCOPE_read > SCOPE_read:search
SCOPE_read > SCOPE_read:statuses
SCOPE_write > SCOPE_write:accounts
SCOPE_write > SCOPE_write:blocks
SCOPE_write > SCOPE_write:bookmarks
SCOPE_write > SCOPE_write:conversations
SCOPE_write > SCOPE_write:favourites
SCOPE_write > SCOPE_write:filters
SCOPE_write > SCOPE_write:follows
SCOPE_write > SCOPE_write:lists
SCOPE_write > SCOPE_write:media
SCOPE_write > SCOPE_write:mutes
SCOPE_write > SCOPE_write:notifications
SCOPE_write > SCOPE_write:reports
SCOPE_write > SCOPE_write:statuses
SCOPE_follow > SCOPE_write:blocks
SCOPE_follow > SCOPE_write:follows
SCOPE_follow > SCOPE_write:mutes
SCOPE_follow > SCOPE_read:blocks
SCOPE_follow > SCOPE_read:follows
SCOPE_follow > SCOPE_read:mutes
SCOPE_admin > SCOPE_admin:read
SCOPE_admin > SCOPE_admin:write
SCOPE_admin:read > SCOPE_admin:read:accounts
SCOPE_admin:read > SCOPE_admin:read:reports
SCOPE_admin:read > SCOPE_admin:read:domain_allows
SCOPE_admin:read > SCOPE_admin:read:domain_blocks
SCOPE_admin:read > SCOPE_admin:read:ip_blocks
SCOPE_admin:read > SCOPE_admin:read:email_domain_blocks
SCOPE_admin:read > SCOPE_admin:read:canonical_email_blocks
SCOPE_admin:write > SCOPE_admin:write:accounts
SCOPE_admin:write > SCOPE_admin:write:reports
SCOPE_admin:write > SCOPE_admin:write:domain_allows
SCOPE_admin:write > SCOPE_admin:write:domain_blocks
SCOPE_admin:write > SCOPE_admin:write:ip_blocks
SCOPE_admin:write > SCOPE_admin:write:email_domain_blocks
SCOPE_admin:write > SCOPE_admin:write:canonical_email_blocks
""".trimIndent()
)
return roleHierarchyImpl
}
} }

View File

@ -0,0 +1,46 @@
/*
* 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.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

@ -0,0 +1,22 @@
/*
* 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.domain.model.application
class Application(
val applicationId: ApplicationId,
val name: ApplicationName,
)

View File

@ -0,0 +1,20 @@
/*
* 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.domain.model.application
@JvmInline
value class ApplicationId(val id: Long)

View File

@ -0,0 +1,20 @@
/*
* 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.domain.model.application
@JvmInline
value class ApplicationName(val name: String)

View File

@ -0,0 +1,22 @@
/*
* 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.domain.model.application
interface ApplicationRepository {
suspend fun save(application: Application): Application
suspend fun delete(application: Application)
}

View File

@ -29,7 +29,7 @@ class LocalActorDomainServiceImpl(
private val applicationConfig: ApplicationConfig, private val applicationConfig: ApplicationConfig,
) : LocalActorDomainService { ) : LocalActorDomainService {
override suspend fun usernameAlreadyUse(name: String): Boolean = override suspend fun usernameAlreadyUse(name: String): Boolean =
actorRepository.findByNameAndDomain(name, applicationConfig.url.host) == null actorRepository.findByNameAndDomain(name, applicationConfig.url.host) != null
override suspend fun generateKeyPair(): Pair<ActorPublicKey, ActorPrivateKey> { override suspend fun generateKeyPair(): Pair<ActorPublicKey, ActorPrivateKey> {
val keyPairGenerator = KeyPairGenerator.getInstance("RSA") val keyPairGenerator = KeyPairGenerator.getInstance("RSA")

View File

@ -135,10 +135,10 @@ object Actors : Table("actors") {
} }
} }
object ActorsAlsoKnownAs : Table("actor_alsoknwonas") { object ActorsAlsoKnownAs : Table("actor_alsoknownas") {
val actorId = val actorId =
long("actor_id").references(Actors.id, onDelete = ReferenceOption.CASCADE, onUpdate = ReferenceOption.CASCADE) long("actor_id").references(Actors.id, onDelete = ReferenceOption.CASCADE, onUpdate = ReferenceOption.CASCADE)
val alsoKnownAs = long("alsoKnownAs").references(Actors.id, ReferenceOption.CASCADE, ReferenceOption.CASCADE) val alsoKnownAs = long("also_known_as").references(Actors.id, ReferenceOption.CASCADE, ReferenceOption.CASCADE)
override val primaryKey: PrimaryKey = PrimaryKey(actorId, alsoKnownAs) override val primaryKey: PrimaryKey = PrimaryKey(actorId, alsoKnownAs)
} }

View File

@ -0,0 +1,57 @@
/*
* 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.exposedrepository
import dev.usbharu.hideout.core.domain.model.application.Application
import dev.usbharu.hideout.core.domain.model.application.ApplicationRepository
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.upsert
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Repository
@Repository
class ExposedApplicationRepository : ApplicationRepository, AbstractRepository() {
override suspend fun save(application: Application) = query {
Applications.upsert {
it[id] = application.applicationId.id
it[name] = application.name.name
}
application
}
override suspend fun delete(application: Application): Unit = query {
Applications.deleteWhere { id eq application.applicationId.id }
}
override val logger: Logger
get() = Companion.logger
companion object {
private val logger = LoggerFactory.getLogger(ExposedApplicationRepository::class.java)
}
}
object Applications : Table("applications") {
val id = long("id")
val name = varchar("name", 500)
override val primaryKey: PrimaryKey = PrimaryKey(id)
}

View File

@ -45,7 +45,7 @@ class ActorFactoryImpl(
description = ActorDescription(""), description = ActorDescription(""),
inbox = URI.create("$userUrl/inbox"), inbox = URI.create("$userUrl/inbox"),
outbox = URI.create("$userUrl/outbox"), outbox = URI.create("$userUrl/outbox"),
url = applicationConfig.url.toURI(), url = URI.create(userUrl),
publicKey = keyPair.first, publicKey = keyPair.first,
privateKey = keyPair.second, privateKey = keyPair.second,
createdAt = Instant.now(), createdAt = Instant.now(),

View File

@ -0,0 +1,39 @@
/*
* 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 org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
class HideoutUserDetails(
private val authorities: MutableList<out GrantedAuthority>,
private val password: String,
private val username: String,
val userDetailsId: Long,
) : UserDetails {
override fun getAuthorities(): MutableCollection<out GrantedAuthority> {
return authorities
}
override fun getPassword(): String {
return password
}
override fun getUsername(): String {
return username
}
}

View File

@ -0,0 +1,54 @@
/*
* 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 dev.usbharu.hideout.core.config.ApplicationConfig
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository
import kotlinx.coroutines.runBlocking
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.stereotype.Component
@Component
class UserDetailsServiceImpl(
private val actorRepository: ActorRepository,
private val userDetailRepository: UserDetailRepository,
private val applicationConfig: ApplicationConfig,
private val transaction: Transaction,
) : UserDetailsService {
override fun loadUserByUsername(username: String?): UserDetails = runBlocking {
if (username == null) {
throw UsernameNotFoundException("Username not found")
}
transaction.transaction {
val actor = actorRepository.findByNameAndDomain(username, applicationConfig.url.host)
?: throw UsernameNotFoundException("$username not found")
val userDetail = userDetailRepository.findByActorId(actor.id.id)
?: throw UsernameNotFoundException("${actor.id.id} not found")
HideoutUserDetails(
authorities = mutableListOf(),
password = userDetail.password.password,
actor.name.name,
userDetailsId = userDetail.id.id
)
}
}
}

View File

@ -16,16 +16,27 @@
package dev.usbharu.hideout.core.interfaces.api.auth package dev.usbharu.hideout.core.interfaces.api.auth
import org.springframework.ui.Model import dev.usbharu.hideout.core.application.actor.RegisterLocalActor
import dev.usbharu.hideout.core.application.actor.RegisterLocalActorApplicationService
import jakarta.servlet.http.HttpServletRequest
import org.springframework.stereotype.Controller
import org.springframework.validation.annotation.Validated import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.ModelAttribute import org.springframework.web.bind.annotation.ModelAttribute
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
interface AuthController { @Controller
class AuthController(private val registerLocalActorApplicationService: RegisterLocalActorApplicationService) {
@GetMapping("/auth/sign_up") @GetMapping("/auth/sign_up")
fun signUp(model: Model): String fun signUp(): String {
return "sign_up"
}
@PostMapping("/auth/sign_up") @PostMapping("/auth/sign_up")
suspend fun signUp(@Validated @ModelAttribute signUpForm: SignUpForm): String suspend fun signUp(@Validated @ModelAttribute signUpForm: SignUpForm, request: HttpServletRequest): String {
val registerLocalActor = RegisterLocalActor(signUpForm.username, signUpForm.password)
val uri = registerLocalActorApplicationService.register(registerLocalActor)
request.login(signUpForm.username, signUpForm.password)
return "redirect:$uri"
}
} }

View File

@ -3,5 +3,5 @@ package dev.usbharu.hideout.core.interfaces.api.auth
data class SignUpForm( data class SignUpForm(
val username: String, val username: String,
val password: String, val password: String,
val recaptchaResponse: String // val recaptchaResponse: String
) )

View File

@ -1,18 +0,0 @@
create table if not exists filters
(
id bigint primary key not null,
user_id bigint not null,
name varchar(255) not null,
context varchar(500) not null,
action varchar(255) not null,
constraint fk_filters_user_id__id foreign key (user_id) references actors (id) on delete cascade on update cascade
);
create table if not exists filter_keywords
(
id bigint primary key not null,
filter_id bigint not null,
keyword varchar(1000) not null,
mode varchar(100) not null,
constraint fk_filter_keywords_filter_id__id foreign key (filter_id) references filters (id) on delete cascade on update cascade
);

View File

@ -55,17 +55,27 @@ create table if not exists actors
suspend boolean not null, suspend boolean not null,
move_to bigint null default null, move_to bigint null default null,
emojis varchar(3000) not null default '', emojis varchar(3000) not null default '',
deleted boolean not null default false,
unique ("name", "domain"), unique ("name", "domain"),
constraint fk_actors_instance__id foreign key ("instance") references instance (id) on delete restrict on update restrict, constraint fk_actors_instance__id foreign key ("instance") references instance (id) on delete restrict on update restrict,
constraint fk_actors_actors__move_to foreign key ("move_to") references actors (id) on delete restrict on update restrict constraint fk_actors_actors__move_to foreign key ("move_to") references actors (id) on delete restrict on update restrict
); );
create table if not exists actor_alsoknownas
(
actor_id bigint not null,
also_known_as bigint not null,
constraint fk_actor_alsoknownas_actors__actor_id foreign key ("actor_id") references actors (id) on delete cascade on update cascade,
constraint fk_actor_alsoknownas_actors__also_known_as foreign key ("also_known_as") references actors (id) on delete cascade on update cascade
);
create table if not exists user_details create table if not exists user_details
( (
id bigserial primary key, id bigserial primary key,
actor_id bigint not null unique, actor_id bigint not null unique,
password varchar(255) not null, password varchar(255) not null,
auto_accept_followee_follow_request boolean not null, auto_accept_followee_follow_request boolean not null,
last_migration timestamp null default null,
constraint fk_user_details_actor_id__id foreign key (actor_id) references actors (id) on delete restrict on update restrict constraint fk_user_details_actor_id__id foreign key (actor_id) references actors (id) on delete restrict on update restrict
); );
@ -81,14 +91,6 @@ create table if not exists media
mime_type varchar(255) not null, mime_type varchar(255) not null,
description varchar(4000) null description varchar(4000) null
); );
create table if not exists meta_info
(
id bigint primary key,
version varchar(1000) not null,
kid varchar(1000) not null,
jwt_private_key varchar(100000) not null,
jwt_public_key varchar(100000) not null
);
create table if not exists posts create table if not exists posts
( (
id bigint primary key, id bigint primary key,
@ -134,100 +136,6 @@ alter table posts_emojis
alter table posts_emojis alter table posts_emojis
add constraint fk_posts_emojis_emoji_id__id foreign key (emoji_id) references emojis (id) on delete cascade on update cascade; add constraint fk_posts_emojis_emoji_id__id foreign key (emoji_id) references emojis (id) on delete cascade on update cascade;
create table if not exists reactions
(
id bigint primary key,
unicode_emoji varchar(255) null default null,
custom_emoji_id bigint null default null,
post_id bigint not null,
actor_id bigint not null,
unique (post_id, actor_id)
);
alter table reactions
add constraint fk_reactions_post_id__id foreign key (post_id) references posts (id) on delete restrict on update restrict;
alter table reactions
add constraint fk_reactions_actor_id__id foreign key (actor_id) references actors (id) on delete restrict on update restrict;
alter table reactions
add constraint fk_reactions_custom_emoji_id__id foreign key (custom_emoji_id) references emojis (id) on delete cascade on update cascade;
create table if not exists timelines
(
id bigint primary key,
user_id bigint not null,
timeline_id bigint not null,
post_id bigint not null,
post_actor_id bigint not null,
created_at bigint not null,
reply_id bigint null,
repost_id bigint null,
visibility int not null,
"sensitive" boolean not null,
is_local boolean not null,
is_pure_repost boolean not null,
media_ids varchar(255) not null,
emoji_ids varchar(255) not null
);
create table if not exists application_authorization
(
id varchar(255) primary key,
registered_client_id varchar(255) not null,
principal_name varchar(255) not null,
authorization_grant_type varchar(255) not null,
authorized_scopes varchar(1000) default null null,
"attributes" varchar(4000) default null null,
"state" varchar(500) default null null,
authorization_code_value varchar(4000) default null null,
authorization_code_issued_at timestamp default null null,
authorization_code_expires_at timestamp default null null,
authorization_code_metadata varchar(2000) default null null,
access_token_value varchar(4000) default null null,
access_token_issued_at timestamp default null null,
access_token_expires_at timestamp default null null,
access_token_metadata varchar(2000) default null null,
access_token_type varchar(255) default null null,
access_token_scopes varchar(1000) default null null,
refresh_token_value varchar(4000) default null null,
refresh_token_issued_at timestamp default null null,
refresh_token_expires_at timestamp default null null,
refresh_token_metadata varchar(2000) default null null,
oidc_id_token_value varchar(4000) default null null,
oidc_id_token_issued_at timestamp default null null,
oidc_id_token_expires_at timestamp default null null,
oidc_id_token_metadata varchar(2000) default null null,
oidc_id_token_claims varchar(2000) default null null,
user_code_value varchar(4000) default null null,
user_code_issued_at timestamp default null null,
user_code_expires_at timestamp default null null,
user_code_metadata varchar(2000) default null null,
device_code_value varchar(4000) default null null,
device_code_issued_at timestamp default null null,
device_code_expires_at timestamp default null null,
device_code_metadata varchar(2000) default null null
);
create table if not exists oauth2_authorization_consent
(
registered_client_id varchar(100),
principal_name varchar(200),
authorities varchar(1000) not null,
constraint pk_oauth2_authorization_consent primary key (registered_client_id, principal_name)
);
create table if not exists registered_client
(
id varchar(100) primary key,
client_id varchar(100) not null,
client_id_issued_at timestamp default current_timestamp not null,
client_secret varchar(200) default null null,
client_secret_expires_at timestamp default null null,
client_name varchar(200) not null,
client_authentication_methods varchar(1000) not null,
authorization_grant_types varchar(1000) not null,
redirect_uris varchar(1000) default null null,
post_logout_redirect_uris varchar(1000) default null null,
scopes varchar(1000) not null,
client_settings varchar(2000) not null,
token_settings varchar(2000) not null
);
create table if not exists relationships create table if not exists relationships
( (
@ -254,40 +162,26 @@ insert into actors (id, name, domain, screen_name, description, inbox, outbox, u
values (0, '', '', '', '', '', '', '', '', null, current_timestamp, '', null, null, 0, true, null, null, 0, null, values (0, '', '', '', '', '', '', '', '', null, current_timestamp, '', null, null, 0, true, null, null, 0, null,
current_timestamp, false, null, ''); current_timestamp, false, null, '');
create table if not exists deleted_actors create table if not exists applications
( (
id bigint primary key, id bigint primary key,
"name" varchar(300) not null, name varchar(500) not null
domain varchar(255) not null,
public_key varchar(10000) not null,
deleted_at timestamp not null,
unique ("name", domain)
); );
create table if not exists notifications create table if not exists oauth2_registered_client
( (
id bigint primary key, id varchar(100) NOT NULL,
type varchar(100) not null, client_id varchar(100) NOT NULL,
user_id bigint not null, client_id_issued_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
source_actor_id bigint null, client_secret varchar(200) DEFAULT NULL,
post_id bigint null, client_secret_expires_at timestamp DEFAULT NULL,
text varchar(3000) null, client_name varchar(200) NOT NULL,
reaction_id bigint null, client_authentication_methods varchar(1000) NOT NULL,
created_at timestamp not null, authorization_grant_types varchar(1000) NOT NULL,
constraint fk_notifications_user_id__id foreign key (user_id) references actors (id) on delete cascade on update cascade, redirect_uris varchar(1000) DEFAULT NULL,
constraint fk_notifications_source_actor__id foreign key (source_actor_id) references actors (id) on delete cascade on update cascade, post_logout_redirect_uris varchar(1000) DEFAULT NULL,
constraint fk_notifications_post_id__id foreign key (post_id) references posts (id) on delete cascade on update cascade, scopes varchar(1000) NOT NULL,
constraint fk_notifications_reaction_id__id foreign key (reaction_id) references reactions (id) on delete cascade on update cascade client_settings varchar(2000) NOT NULL,
token_settings varchar(2000) NOT NULL,
PRIMARY KEY (id)
); );
create table if not exists mastodon_notifications
(
id bigint primary key,
user_id bigint not null,
type varchar(100) not null,
created_at timestamp not null,
account_id bigint not null,
status_id bigint null,
report_id bigint null,
relationship_serverance_event_id bigint null
)

View File

@ -6,9 +6,10 @@
</Console> </Console>
</Appenders> </Appenders>
<Loggers> <Loggers>
<Root level="INFO"> <Root level="DEBUG">
<AppenderRef ref="Console"/> <AppenderRef ref="Console"/>
</Root> </Root>
<Logger name="dev.usbharu.owl.broker.service.QueuedTaskAssignerImpl" level="TRACE"/> <Logger name="dev.usbharu.owl.broker.service.QueuedTaskAssignerImpl" level="TRACE"/>
<Logger name="org.mongodb.driver.cluster" level="WARN"/>
</Loggers> </Loggers>
</Configuration> </Configuration>

View File

@ -3,23 +3,12 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>SignUp</title> <title>SignUp</title>
<script th:src="https://www.google.com/recaptcha/api.js?render=${siteKey}"></script>
<script th:inline="javascript">
grecaptcha.ready(function () {
grecaptcha.execute( /*[[${siteKey}]]*/ '', {action: 'homepage'}).then(function (token) {
var recaptchaResponse = document.getElementById('recaptchaResponse');
recaptchaResponse.value = token;
});
});
</script>
</head> </head>
<body> <body>
<form method='post' th:action="@{/dev/usbharu/hideout/core/service/auth/sign_up}" <form method='post' th:action="@{/auth/sign_up}">
th:disabled="${applicationConfig.private}">
<input name='username' type='text' value=''> <input name='username' type='text' value=''>
<input name='password' type='password'> <input name='password' type='password'>
<input type="hidden" name="recaptchaResponse" id="recaptchaResponse">
<input type="submit"> <input type="submit">
</form> </form>
</body> </body>

View File

@ -21,8 +21,6 @@ import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
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.security.access.hierarchicalroles.RoleHierarchy
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl
import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.invoke import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.SecurityFilterChain
@ -30,7 +28,7 @@ import org.springframework.security.web.SecurityFilterChain
@Configuration @Configuration
class MastodonSecurityConfig { class MastodonSecurityConfig {
@Bean @Bean
@Order(4) @Order(2)
@Suppress("LongMethod") @Suppress("LongMethod")
fun mastodonApiSecurityFilterChain( fun mastodonApiSecurityFilterChain(
http: HttpSecurity, http: HttpSecurity,
@ -111,63 +109,4 @@ class MastodonSecurityConfig {
return http.build() return http.build()
} }
@Bean
fun roleHierarchy(): RoleHierarchy {
val roleHierarchyImpl = RoleHierarchyImpl()
roleHierarchyImpl.setHierarchy(
"""
SCOPE_read > SCOPE_read:accounts
SCOPE_read > SCOPE_read:accounts
SCOPE_read > SCOPE_read:blocks
SCOPE_read > SCOPE_read:bookmarks
SCOPE_read > SCOPE_read:favourites
SCOPE_read > SCOPE_read:filters
SCOPE_read > SCOPE_read:follows
SCOPE_read > SCOPE_read:lists
SCOPE_read > SCOPE_read:mutes
SCOPE_read > SCOPE_read:notifications
SCOPE_read > SCOPE_read:search
SCOPE_read > SCOPE_read:statuses
SCOPE_write > SCOPE_write:accounts
SCOPE_write > SCOPE_write:blocks
SCOPE_write > SCOPE_write:bookmarks
SCOPE_write > SCOPE_write:conversations
SCOPE_write > SCOPE_write:favourites
SCOPE_write > SCOPE_write:filters
SCOPE_write > SCOPE_write:follows
SCOPE_write > SCOPE_write:lists
SCOPE_write > SCOPE_write:media
SCOPE_write > SCOPE_write:mutes
SCOPE_write > SCOPE_write:notifications
SCOPE_write > SCOPE_write:reports
SCOPE_write > SCOPE_write:statuses
SCOPE_follow > SCOPE_write:blocks
SCOPE_follow > SCOPE_write:follows
SCOPE_follow > SCOPE_write:mutes
SCOPE_follow > SCOPE_read:blocks
SCOPE_follow > SCOPE_read:follows
SCOPE_follow > SCOPE_read:mutes
SCOPE_admin > SCOPE_admin:read
SCOPE_admin > SCOPE_admin:write
SCOPE_admin:read > SCOPE_admin:read:accounts
SCOPE_admin:read > SCOPE_admin:read:reports
SCOPE_admin:read > SCOPE_admin:read:domain_allows
SCOPE_admin:read > SCOPE_admin:read:domain_blocks
SCOPE_admin:read > SCOPE_admin:read:ip_blocks
SCOPE_admin:read > SCOPE_admin:read:email_domain_blocks
SCOPE_admin:read > SCOPE_admin:read:canonical_email_blocks
SCOPE_admin:write > SCOPE_admin:write:accounts
SCOPE_admin:write > SCOPE_admin:write:reports
SCOPE_admin:write > SCOPE_admin:write:domain_allows
SCOPE_admin:write > SCOPE_admin:write:domain_blocks
SCOPE_admin:write > SCOPE_admin:write:ip_blocks
SCOPE_admin:write > SCOPE_admin:write:email_domain_blocks
SCOPE_admin:write > SCOPE_admin:write:canonical_email_blocks
""".trimIndent()
)
return roleHierarchyImpl
}
} }

View File

@ -16,15 +16,35 @@
package dev.usbharu.hideout.mastodon.interfaces.api package dev.usbharu.hideout.mastodon.interfaces.api
import dev.usbharu.hideout.core.application.application.RegisterApplication
import dev.usbharu.hideout.core.application.application.RegisterApplicationApplicationService
import dev.usbharu.hideout.mastodon.interfaces.api.generated.AppApi import dev.usbharu.hideout.mastodon.interfaces.api.generated.AppApi
import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Application import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Application
import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.AppsRequest import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.AppsRequest
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller import org.springframework.stereotype.Controller
import java.net.URI
@Controller @Controller
class SpringAppApi : AppApi { class SpringAppApi(private val registerApplicationApplicationService: RegisterApplicationApplicationService) : AppApi {
override suspend fun apiV1AppsPost(appsRequest: AppsRequest): ResponseEntity<Application> { override suspend fun apiV1AppsPost(appsRequest: AppsRequest): ResponseEntity<Application> {
return super.apiV1AppsPost(appsRequest)
val registerApplication = RegisterApplication(
appsRequest.clientName,
setOf(URI.create(appsRequest.redirectUris)),
false,
appsRequest.scopes?.split(" ").orEmpty().toSet()
)
val registeredApplication = registerApplicationApplicationService.register(registerApplication)
return ResponseEntity.ok(
Application(
registeredApplication.name,
"invalid-vapid-key",
null,
registeredApplication.clientId.toString(),
registeredApplication.clientSecret,
appsRequest.redirectUris
)
)
} }
} }