From 00cf918720de4267e8dd56192d42f6858a14c27f Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Fri, 16 Feb 2024 14:32:34 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20RoleHierarchy=E3=82=92=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E3=81=99=E3=82=8B=E3=82=B9=E3=82=B3=E3=83=BC=E3=83=97?= =?UTF-8?q?=E6=8C=87=E5=AE=9A=E3=81=AE=E3=83=A6=E3=83=BC=E3=83=86=E3=82=A3?= =?UTF-8?q?=E3=83=AA=E3=83=86=E3=82=A3=E3=83=BC=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RoleHierarchyAuthorizationManagerFactory.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/main/kotlin/dev/usbharu/hideout/application/infrastructure/springframework/RoleHierarchyAuthorizationManagerFactory.kt diff --git a/src/main/kotlin/dev/usbharu/hideout/application/infrastructure/springframework/RoleHierarchyAuthorizationManagerFactory.kt b/src/main/kotlin/dev/usbharu/hideout/application/infrastructure/springframework/RoleHierarchyAuthorizationManagerFactory.kt new file mode 100644 index 00000000..42a249ea --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/application/infrastructure/springframework/RoleHierarchyAuthorizationManagerFactory.kt @@ -0,0 +1,16 @@ +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 { + val hasAuthority = AuthorityAuthorizationManager.hasAuthority("SCOPE_$role") + hasAuthority.setRoleHierarchy(roleHierarchy) + return hasAuthority + } +} \ No newline at end of file From 011e79b5264cf2c3b2a885bc421ca21a2fdb9cbe Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Fri, 16 Feb 2024 14:34:42 +0900 Subject: [PATCH 2/4] =?UTF-8?q?chore:=20e2e=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E7=94=A8=E3=81=AEDB=E3=82=92=E3=82=A4=E3=83=B3=E3=83=A1?= =?UTF-8?q?=E3=83=A2=E3=83=AA=E3=83=A2=E3=83=BC=E3=83=89=E3=81=A7=E8=B5=B7?= =?UTF-8?q?=E5=8B=95=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/e2eTest/resources/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/e2eTest/resources/application.yml b/src/e2eTest/resources/application.yml index dcd84955..73e011d0 100644 --- a/src/e2eTest/resources/application.yml +++ b/src/e2eTest/resources/application.yml @@ -19,7 +19,7 @@ spring: clean-disabled: false datasource: driver-class-name: org.h2.Driver - url: "jdbc:h2:./e2e-test;MODE=POSTGRESQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;CASE_INSENSITIVE_IDENTIFIERS=true;TRACE_LEVEL_FILE=4" + url: "jdbc:h2:mem:e2e-test;MODE=POSTGRESQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;CASE_INSENSITIVE_IDENTIFIERS=true;TRACE_LEVEL_FILE=4" username: "" password: data: From 4a7152b771cbf57d708f95b3a6df4f9cadb3b11d Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Fri, 16 Feb 2024 14:36:38 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=E5=BF=85=E8=A6=81=E3=82=B9?= =?UTF-8?q?=E3=82=B3=E3=83=BC=E3=83=97=E3=81=AE=E6=8C=87=E5=AE=9A=E3=81=AB?= =?UTF-8?q?RoleHierarchy=E3=82=92=E4=BD=BF=E7=94=A8=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/config/SecurityConfig.kt | 140 +++++++++++++----- .../util/SpringSecurityKotlinDslExtension.kt | 12 -- 2 files changed, 105 insertions(+), 47 deletions(-) delete mode 100644 src/main/kotlin/dev/usbharu/hideout/util/SpringSecurityKotlinDslExtension.kt diff --git a/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt b/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt index 3bca0089..30956595 100644 --- a/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt @@ -7,6 +7,7 @@ import com.nimbusds.jose.jwk.source.ImmutableJWKSet import com.nimbusds.jose.jwk.source.JWKSource import com.nimbusds.jose.proc.SecurityContext import dev.usbharu.hideout.application.external.Transaction +import dev.usbharu.hideout.application.infrastructure.springframework.RoleHierarchyAuthorizationManagerFactory import dev.usbharu.hideout.core.domain.model.actor.ActorRepository import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureFilter import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureUserDetailsService @@ -14,7 +15,6 @@ import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.Htt import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.UserDetailsImpl import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.UserDetailsServiceImpl import dev.usbharu.hideout.util.RsaUtil -import dev.usbharu.hideout.util.hasAnyScope import dev.usbharu.httpsignature.sign.RsaSha256HttpSignatureSigner import dev.usbharu.httpsignature.verify.DefaultSignatureHeaderParser import dev.usbharu.httpsignature.verify.RsaSha256HttpSignatureVerifier @@ -30,6 +30,8 @@ import org.springframework.http.HttpMethod.* import org.springframework.http.HttpStatus import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter +import org.springframework.security.access.hierarchicalroles.RoleHierarchy +import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl import org.springframework.security.authentication.AccountStatusUserDetailsChecker import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.authentication.dao.DaoAuthenticationProvider @@ -62,14 +64,16 @@ import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey import java.util.* + @EnableWebSecurity(debug = false) @Configuration @Suppress("FunctionMaxLength", "TooManyFunctions") class SecurityConfig { @Bean - fun authenticationManager(authenticationConfiguration: AuthenticationConfiguration): AuthenticationManager? = - authenticationConfiguration.authenticationManager + fun authenticationManager(authenticationConfiguration: AuthenticationConfiguration): AuthenticationManager? { + return authenticationConfiguration.authenticationManager + } @Bean @Order(1) @@ -169,7 +173,10 @@ class SecurityConfig { @Bean @Order(4) - fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain { + fun defaultSecurityFilterChain( + http: HttpSecurity, + rf: RoleHierarchyAuthorizationManagerFactory + ): SecurityFilterChain { http { authorizeHttpRequests { authorize("/error", permitAll) @@ -191,54 +198,57 @@ class SecurityConfig { authorize(GET, "/users/*/icon.jpg", permitAll) authorize(GET, "/users/*/header.jpg", permitAll) - authorize(GET, "/api/v1/accounts/verify_credentials", hasAnyScope("read", "read:accounts")) - authorize(GET, "/api/v1/accounts/relationships", hasAnyScope("read", "read:follows")) + authorize(GET, "/api/v1/accounts/verify_credentials", rf.hasScope("read:accounts")) + authorize(GET, "/api/v1/accounts/relationships", rf.hasScope("read:follows")) authorize(GET, "/api/v1/accounts/*", permitAll) authorize(GET, "/api/v1/accounts/*/statuses", permitAll) - authorize(POST, "/api/v1/accounts/*/follow", hasAnyScope("write", "write:follows")) - authorize(POST, "/api/v1/accounts/*/unfollow", hasAnyScope("write", "write:follows")) - authorize(POST, "/api/v1/accounts/*/block", hasAnyScope("write", "write:blocks")) - authorize(POST, "/api/v1/accounts/*/unblock", hasAnyScope("write", "write:blocks")) - authorize(POST, "/api/v1/accounts/*/mute", hasAnyScope("write", "write:mutes")) - authorize(POST, "/api/v1/accounts/*/unmute", hasAnyScope("write", "write:mutes")) - authorize(GET, "/api/v1/mutes", hasAnyScope("read", "read:mutes")) + authorize(POST, "/api/v1/accounts/*/follow", rf.hasScope("write:follows")) + authorize(POST, "/api/v1/accounts/*/unfollow", rf.hasScope("write:follows")) + authorize(POST, "/api/v1/accounts/*/block", rf.hasScope("write:blocks")) + authorize(POST, "/api/v1/accounts/*/unblock", rf.hasScope("write:blocks")) + authorize(POST, "/api/v1/accounts/*/mute", rf.hasScope("write:mutes")) + authorize(POST, "/api/v1/accounts/*/unmute", rf.hasScope("write:mutes")) + authorize(GET, "/api/v1/mutes", rf.hasScope("read:mutes")) - authorize(POST, "/api/v1/media", hasAnyScope("write", "write:media")) - authorize(POST, "/api/v1/statuses", hasAnyScope("write", "write:statuses")) + authorize(POST, "/api/v1/media", rf.hasScope("write:media")) + authorize(POST, "/api/v1/statuses", rf.hasScope("write:statuses")) authorize(GET, "/api/v1/timelines/public", permitAll) - authorize(GET, "/api/v1/timelines/home", hasAnyScope("read", "read:statuses")) + authorize(GET, "/api/v1/timelines/home", rf.hasScope("read:statuses")) - authorize(GET, "/api/v2/filters", hasAnyScope("read", "read:filters")) - authorize(POST, "/api/v2/filters", hasAnyScope("write", "write:filters")) + authorize(GET, "/api/v2/filters", rf.hasScope("read:filters")) + authorize(POST, "/api/v2/filters", rf.hasScope("write:filters")) - authorize(GET, "/api/v2/filters/*", hasAnyScope("read", "read:filters")) - authorize(PUT, "/api/v2/filters/*", hasAnyScope("write", "write:filters")) - authorize(DELETE, "/api/v2/filters/*", hasAnyScope("write", "write:filters")) + authorize(GET, "/api/v2/filters/*", rf.hasScope("read:filters")) + authorize(PUT, "/api/v2/filters/*", rf.hasScope("write:filters")) + authorize(DELETE, "/api/v2/filters/*", rf.hasScope("write:filters")) - authorize(GET, "/api/v2/filters/*/keywords", hasAnyScope("read", "read:filters")) - authorize(POST, "/api/v2/filters/*/keywords", hasAnyScope("write", "write:filters")) + authorize(GET, "/api/v2/filters/*/keywords", rf.hasScope("read:filters")) + authorize(POST, "/api/v2/filters/*/keywords", rf.hasScope("write:filters")) - authorize(GET, "/api/v2/filters/keywords/*", hasAnyScope("read", "read:filters")) - authorize(PUT, "/api/v2/filters/keywords/*", hasAnyScope("write", "write:filters")) - authorize(DELETE, "/api/v2/filters/keywords/*", hasAnyScope("write", "write:filters")) + authorize(GET, "/api/v2/filters/keywords/*", rf.hasScope("read:filters")) + authorize(PUT, "/api/v2/filters/keywords/*", rf.hasScope("write:filters")) + authorize(DELETE, "/api/v2/filters/keywords/*", rf.hasScope("write:filters")) - authorize(GET, "/api/v2/filters/*/statuses", hasAnyScope("read", "read:filters")) - authorize(POST, "/api/v2/filters/*/statuses", hasAnyScope("write", "write:filters")) + authorize(GET, "/api/v2/filters/*/statuses", rf.hasScope("read:filters")) + authorize(POST, "/api/v2/filters/*/statuses", rf.hasScope("write:filters")) - authorize(GET, "/api/v2/filters/statuses/*", hasAnyScope("read", "read:filters")) - authorize(DELETE, "/api/v2/filters/statuses/*", hasAnyScope("write", "write:filters")) + authorize(GET, "/api/v2/filters/statuses/*", rf.hasScope("read:filters")) + authorize(DELETE, "/api/v2/filters/statuses/*", rf.hasScope("write:filters")) - authorize(GET, "/api/v1/filters", hasAnyScope("read", "read:filters")) - authorize(POST, "/api/v1/filters", hasAnyScope("write", "write:filters")) + authorize(GET, "/api/v1/filters", rf.hasScope("read:filters")) + authorize(POST, "/api/v1/filters", rf.hasScope("write:filters")) - authorize(GET, "/api/v1/filters/*", hasAnyScope("read", "read:filters")) - authorize(POST, "/api/v1/filters/*", hasAnyScope("write", "write:filters")) - authorize(DELETE, "/api/v1/filters/*", hasAnyScope("write", "write:filters")) + authorize(GET, "/api/v1/filters/*", rf.hasScope("read:filters")) + authorize(POST, "/api/v1/filters/*", rf.hasScope("write:filters")) + authorize(DELETE, "/api/v1/filters/*", rf.hasScope("write:filters")) authorize(anyRequest, authenticated) } + + + oauth2ResourceServer { jwt { } } @@ -320,8 +330,68 @@ class SecurityConfig { val builder = Jackson2ObjectMapperBuilder().serializationInclusion(JsonInclude.Include.NON_NULL) return MappingJackson2HttpMessageConverter(builder.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 + } } + @ConfigurationProperties("hideout.security.jwt") @ConditionalOnProperty(name = ["hideout.security.jwt.generate"], havingValue = "") data class JwkConfig( diff --git a/src/main/kotlin/dev/usbharu/hideout/util/SpringSecurityKotlinDslExtension.kt b/src/main/kotlin/dev/usbharu/hideout/util/SpringSecurityKotlinDslExtension.kt deleted file mode 100644 index 52a2f486..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/util/SpringSecurityKotlinDslExtension.kt +++ /dev/null @@ -1,12 +0,0 @@ -package dev.usbharu.hideout.util - -import org.springframework.security.authorization.AuthorizationManager -import org.springframework.security.config.annotation.web.AuthorizeHttpRequestsDsl -import org.springframework.security.web.access.intercept.RequestAuthorizationContext - -fun AuthorizeHttpRequestsDsl.hasScope(scope: String): AuthorizationManager = - hasAuthority("SCOPE_$scope") - -@Suppress("SpreadOperator") -fun AuthorizeHttpRequestsDsl.hasAnyScope(vararg scopes: String): AuthorizationManager = - hasAnyAuthority(*scopes.map { "SCOPE_$it" }.toTypedArray()) From 8d4785b002bfbfea81bffc6d4a14fce72187b13f Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Fri, 16 Feb 2024 17:19:57 +0900 Subject: [PATCH 4/4] style: fix lint --- .../usbharu/hideout/application/config/SecurityConfig.kt | 9 ++------- .../hideout/application/infrastructure/exposed/Page.kt | 1 + .../RoleHierarchyAuthorizationManagerFactory.kt | 2 +- .../interfaces/api/filter/MastodonFilterApiController.kt | 5 ++--- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt b/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt index 30956595..aa3c2aaf 100644 --- a/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt @@ -64,10 +64,9 @@ import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey import java.util.* - @EnableWebSecurity(debug = false) @Configuration -@Suppress("FunctionMaxLength", "TooManyFunctions") +@Suppress("FunctionMaxLength", "TooManyFunctions", "LongMethod") class SecurityConfig { @Bean @@ -246,9 +245,6 @@ class SecurityConfig { authorize(anyRequest, authenticated) } - - - oauth2ResourceServer { jwt { } } @@ -384,14 +380,13 @@ class SecurityConfig { 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() + """.trimIndent() ) return roleHierarchyImpl } } - @ConfigurationProperties("hideout.security.jwt") @ConditionalOnProperty(name = ["hideout.security.jwt.generate"], havingValue = "") data class JwkConfig( diff --git a/src/main/kotlin/dev/usbharu/hideout/application/infrastructure/exposed/Page.kt b/src/main/kotlin/dev/usbharu/hideout/application/infrastructure/exposed/Page.kt index 71df7898..537143d1 100644 --- a/src/main/kotlin/dev/usbharu/hideout/application/infrastructure/exposed/Page.kt +++ b/src/main/kotlin/dev/usbharu/hideout/application/infrastructure/exposed/Page.kt @@ -23,6 +23,7 @@ sealed class Page { } companion object { + @Suppress("FunctionMinLength") fun of( maxId: Long? = null, sinceId: Long? = null, diff --git a/src/main/kotlin/dev/usbharu/hideout/application/infrastructure/springframework/RoleHierarchyAuthorizationManagerFactory.kt b/src/main/kotlin/dev/usbharu/hideout/application/infrastructure/springframework/RoleHierarchyAuthorizationManagerFactory.kt index 42a249ea..758f131a 100644 --- a/src/main/kotlin/dev/usbharu/hideout/application/infrastructure/springframework/RoleHierarchyAuthorizationManagerFactory.kt +++ b/src/main/kotlin/dev/usbharu/hideout/application/infrastructure/springframework/RoleHierarchyAuthorizationManagerFactory.kt @@ -13,4 +13,4 @@ class RoleHierarchyAuthorizationManagerFactory(private val roleHierarchy: RoleHi hasAuthority.setRoleHierarchy(roleHierarchy) return hasAuthority } -} \ No newline at end of file +} diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/filter/MastodonFilterApiController.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/filter/MastodonFilterApiController.kt index 05fc998c..6fcd3a58 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/filter/MastodonFilterApiController.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/filter/MastodonFilterApiController.kt @@ -92,9 +92,8 @@ class MastodonFilterApiController( ) } - override fun apiV2FiltersGet(): ResponseEntity> { - return ResponseEntity.ok(mastodonFilterApiService.filters(loginUserContextHolder.getLoginUserId())) - } + override fun apiV2FiltersGet(): ResponseEntity> = + ResponseEntity.ok(mastodonFilterApiService.filters(loginUserContextHolder.getLoginUserId())) override suspend fun apiV2FiltersIdDelete(id: String): ResponseEntity { mastodonFilterApiService.deleteById(loginUserContextHolder.getLoginUserId(), id.toLong())