From af4c8640d7edce33d6e13fe5f583869f585652ff Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Wed, 22 Nov 2023 13:14:40 +0000 Subject: [PATCH] Expose tenantId via EL with new annotation module (#255) * Expose tenantId via EL with new annotation module This PR adds a new ExpressionEvaluationContext which allows us to use tenantId in EL expressions. Added test-suite in all 3 languages to allow documenting it's usage and validating the docs source * Unused dependency * Appease sonar's nonsense * Catch the exception when there's no tenant header/cookie/etc and resolve to null * Remove unrequired dependency * use properties configuration * use StringUtils constant * remove junit jupiter engine * more javadoc * use HttpClient as method parameter * Update src/main/docs/guide/multitenancy/tenantexpression.adoc * Update src/main/docs/guide/multitenancy/tenantexpression.adoc --------- Co-authored-by: Sergio del Amo --- buildSrc/build.gradle | 9 +++ buildSrc/settings.gradle | 7 ++ gradle.properties | 13 ++++ gradle/libs.versions.toml | 10 +++ multitenancy-annotations/build.gradle.kts | 16 ++++ .../TenantEvaluationContextRegistrar.java | 32 ++++++++ ...icronaut.inject.visitor.TypeElementVisitor | 1 + multitenancy/build.gradle.kts | 2 + .../expression/TenantEvaluationContext.java | 76 ++++++++++++++++++ .../multitenancy/expression/package-info.java | 22 ++++++ .../TenantEvaluationContextSpec.groovy | 78 +++++++++++++++++++ multitenancy/src/test/resources/logback.xml | 1 + settings.gradle | 8 ++ .../guide/multitenancy/tenantexpression.adoc | 7 ++ src/main/docs/guide/toc.yml | 1 + test-suite-groovy/build.gradle.kts | 30 +++++++ .../TenantCheckingSecuredController.groovy | 20 +++++ .../io/micronaut/multitenancy/Tests.groovy | 36 +++++++++ .../src/test/resources/application.properties | 2 + .../src/test/resources/logback.xml | 15 ++++ test-suite-kotlin/build.gradle.kts | 41 ++++++++++ .../TenantCheckingSecuredController.kt | 18 +++++ .../kotlin/io/micronaut/multitenancy/Tests.kt | 33 ++++++++ .../src/test/resources/application.properties | 2 + .../src/test/resources/logback.xml | 15 ++++ test-suite/build.gradle.kts | 33 ++++++++ .../TenantCheckingSecuredController.java | 20 +++++ .../java/io/micronaut/multitenancy/Tests.java | 32 ++++++++ .../src/test/resources/application.properties | 2 + test-suite/src/test/resources/logback.xml | 15 ++++ 30 files changed, 597 insertions(+) create mode 100644 buildSrc/settings.gradle create mode 100644 multitenancy-annotations/build.gradle.kts create mode 100644 multitenancy-annotations/src/main/java/io/micronaut/multitenancy/expression/TenantEvaluationContextRegistrar.java create mode 100644 multitenancy-annotations/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor create mode 100644 multitenancy/src/main/java/io/micronaut/multitenancy/expression/TenantEvaluationContext.java create mode 100644 multitenancy/src/main/java/io/micronaut/multitenancy/expression/package-info.java create mode 100644 multitenancy/src/test/groovy/io/micronaut/multitenancy/expression/TenantEvaluationContextSpec.groovy create mode 100644 src/main/docs/guide/multitenancy/tenantexpression.adoc create mode 100644 test-suite-groovy/build.gradle.kts create mode 100644 test-suite-groovy/src/test/groovy/io/micronaut/multitenancy/TenantCheckingSecuredController.groovy create mode 100644 test-suite-groovy/src/test/groovy/io/micronaut/multitenancy/Tests.groovy create mode 100644 test-suite-groovy/src/test/resources/application.properties create mode 100644 test-suite-groovy/src/test/resources/logback.xml create mode 100644 test-suite-kotlin/build.gradle.kts create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/multitenancy/TenantCheckingSecuredController.kt create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/multitenancy/Tests.kt create mode 100644 test-suite-kotlin/src/test/resources/application.properties create mode 100644 test-suite-kotlin/src/test/resources/logback.xml create mode 100644 test-suite/build.gradle.kts create mode 100644 test-suite/src/test/java/io/micronaut/multitenancy/TenantCheckingSecuredController.java create mode 100644 test-suite/src/test/java/io/micronaut/multitenancy/Tests.java create mode 100644 test-suite/src/test/resources/application.properties create mode 100644 test-suite/src/test/resources/logback.xml diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 67840524..308a74cc 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -1,3 +1,12 @@ plugins { id 'groovy-gradle-plugin' } + +repositories { + gradlePluginPortal() + mavenCentral() +} + +dependencies { + implementation(libs.gradle.kotlin) +} diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle new file mode 100644 index 00000000..6f31e6ef --- /dev/null +++ b/buildSrc/settings.gradle @@ -0,0 +1,7 @@ +dependencyResolutionManagement { + versionCatalogs { + libs { + from(files("../gradle/libs.versions.toml")) + } + } +} diff --git a/gradle.properties b/gradle.properties index a0fea10c..0a0d429c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,3 +12,16 @@ org.gradle.caching=true org.gradle.jvmargs=-Xmx1g testsmultitenancy=multitenancy/src/test/groovy/io/micronaut/docs + +# No matter which Java toolchain we use, the Kotlin Daemon is always invoked by the current JDK. +# Therefor to fix Kapt errors when running tests under Java 21, we need to open up some modules for the Kotlin Daemon. +kotlin.daemon.jvmargs=--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED\ + --add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \ + --add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c547d0db..7cd0a879 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,19 +7,29 @@ guava = "32.1.3-jre" managed-publicsuffixlist = "2.2.0" spock = "2.3-groovy-4.0" +micronaut-logging = "1.1.2" micronaut-reactor = "3.1.0" +micronaut-security = "4.4.0" micronaut-serde = "2.4.0" micronaut-session = "4.1.0" micronaut-validation = "4.2.0" +kotlin-gradle-plugin = "1.9.20" + [libraries] guava = { module = 'com.google.guava:guava', version.ref = 'guava' } managed-publicsuffixlist = { module = 'de.malkusch.whois-server-list:public-suffix-list', version.ref = 'managed-publicsuffixlist' } +junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine" } # Core micronaut-core = { module = 'io.micronaut:micronaut-core-bom', version.ref = 'micronaut' } +micronaut-security = { module = "io.micronaut.security:micronaut-security-bom", version.ref = "micronaut-security" } micronaut-session = { module = "io.micronaut.session:micronaut-session-bom", version.ref = "micronaut-session" } micronaut-reactor = { module = "io.micronaut.reactor:micronaut-reactor-bom", version.ref = "micronaut-reactor" } micronaut-serde = { module = "io.micronaut.serde:micronaut-serde-bom", version.ref = "micronaut-serde" } micronaut-validation = { module = "io.micronaut.validation:micronaut-validation-bom", version.ref = "micronaut-validation" } + +# PLUGINS + +gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin-gradle-plugin" } diff --git a/multitenancy-annotations/build.gradle.kts b/multitenancy-annotations/build.gradle.kts new file mode 100644 index 00000000..f4b65c94 --- /dev/null +++ b/multitenancy-annotations/build.gradle.kts @@ -0,0 +1,16 @@ + +plugins { + id("io.micronaut.build.internal.multitenancy-module") +} + +description = "Expression language support for multi-tenancy" + +dependencies { + compileOnly(mn.micronaut.core.processor) +} + +micronautBuild { + binaryCompatibility { + enabled = false + } +} diff --git a/multitenancy-annotations/src/main/java/io/micronaut/multitenancy/expression/TenantEvaluationContextRegistrar.java b/multitenancy-annotations/src/main/java/io/micronaut/multitenancy/expression/TenantEvaluationContextRegistrar.java new file mode 100644 index 00000000..d53aba4e --- /dev/null +++ b/multitenancy-annotations/src/main/java/io/micronaut/multitenancy/expression/TenantEvaluationContextRegistrar.java @@ -0,0 +1,32 @@ +/* + * Copyright 2017-2023 original authors + * + * 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 + * + * https://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 io.micronaut.multitenancy.expression; + +import io.micronaut.expressions.context.ExpressionEvaluationContextRegistrar; + +/** + * Registers the `TenantEvaluationContext` from the main module as an Evaluation context. + * + * @since 5.2.0 + * @author Tim Yates + */ +public class TenantEvaluationContextRegistrar implements ExpressionEvaluationContextRegistrar { + + @Override + public String getContextClassName() { + return "io.micronaut.multitenancy.expression.TenantEvaluationContext"; + } +} diff --git a/multitenancy-annotations/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/multitenancy-annotations/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor new file mode 100644 index 00000000..674bf67a --- /dev/null +++ b/multitenancy-annotations/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -0,0 +1 @@ +io.micronaut.multitenancy.expression.TenantEvaluationContextRegistrar diff --git a/multitenancy/build.gradle.kts b/multitenancy/build.gradle.kts index 184678b8..4650feac 100644 --- a/multitenancy/build.gradle.kts +++ b/multitenancy/build.gradle.kts @@ -9,6 +9,8 @@ dependencies { compileOnly(mnSession.micronaut.session) compileOnly(libs.managed.publicsuffixlist) compileOnly(libs.guava) + + testImplementation(projects.micronautMultitenancyAnnotations) testImplementation(libs.managed.publicsuffixlist) testImplementation(libs.guava) testImplementation(mnSerde.micronaut.serde.api) diff --git a/multitenancy/src/main/java/io/micronaut/multitenancy/expression/TenantEvaluationContext.java b/multitenancy/src/main/java/io/micronaut/multitenancy/expression/TenantEvaluationContext.java new file mode 100644 index 00000000..f89de2f3 --- /dev/null +++ b/multitenancy/src/main/java/io/micronaut/multitenancy/expression/TenantEvaluationContext.java @@ -0,0 +1,76 @@ +/* + * Copyright 2017-2023 original authors + * + * 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 + * + * https://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 io.micronaut.multitenancy.expression; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.multitenancy.exceptions.TenantNotFoundException; +import io.micronaut.multitenancy.tenantresolver.TenantResolver; +import jakarta.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serializable; + +/** + * Context for supporting annotation expressions with tenant id. + * + * @since 5.2.0 + * @author Tim Yates + */ +@Internal +@Singleton +@Requires(beans = {TenantResolver.class}) +public final class TenantEvaluationContext { + + private static final Logger LOG = LoggerFactory.getLogger(TenantEvaluationContext.class); + + private final TenantResolver tenantResolver; + + /** + * @param tenantResolver The enabled and configured TenantResolver + */ + public TenantEvaluationContext(TenantResolver tenantResolver) { + this.tenantResolver = tenantResolver; + } + + /** + * Resolves and returns the Tenant ID with {@link TenantResolver#resolveTenantIdentifier()}. + * @return the tenant id or {@literal null}. + */ + @Nullable + public String getTenantId() { + try { + Serializable tenant = tenantResolver.resolveTenantIdentifier(); + if (tenant instanceof CharSequence charSequenceTenant) { + if (LOG.isDebugEnabled()) { + LOG.debug("Resolved tenant: {}", tenant); + } + return charSequenceTenant.toString(); + } + if (LOG.isDebugEnabled()) { + LOG.debug("Tenant not resolvable to a String: {}", tenant); + } + return null; + } catch (TenantNotFoundException ex) { + if (LOG.isDebugEnabled()) { + LOG.debug("Tenant not found: {}", ex.getMessage()); + } + return null; + } + } +} diff --git a/multitenancy/src/main/java/io/micronaut/multitenancy/expression/package-info.java b/multitenancy/src/main/java/io/micronaut/multitenancy/expression/package-info.java new file mode 100644 index 00000000..be783b9b --- /dev/null +++ b/multitenancy/src/main/java/io/micronaut/multitenancy/expression/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2017-2023 original authors + * + * 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 + * + * https://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. + */ +/** + * Expression Language support for multi-tenancy. + * + * @since 5.2.0 + * @author Tim Yates + */ +package io.micronaut.multitenancy.expression; diff --git a/multitenancy/src/test/groovy/io/micronaut/multitenancy/expression/TenantEvaluationContextSpec.groovy b/multitenancy/src/test/groovy/io/micronaut/multitenancy/expression/TenantEvaluationContextSpec.groovy new file mode 100644 index 00000000..9c6e73e3 --- /dev/null +++ b/multitenancy/src/test/groovy/io/micronaut/multitenancy/expression/TenantEvaluationContextSpec.groovy @@ -0,0 +1,78 @@ +package io.micronaut.multitenancy.expression + +import io.micronaut.context.BeanContext +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Property +import io.micronaut.context.annotation.Requires +import io.micronaut.context.annotation.Value +import io.micronaut.core.annotation.Nullable +import io.micronaut.core.util.StringUtils +import io.micronaut.http.HttpRequest +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Produces +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.multitenancy.tenantresolver.HttpHeaderTenantResolverConfiguration +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import spock.lang.Specification + +@MicronautTest +@Property(name = "spec.name", value = "TenantEvaluationContextSpec") +@Property(name = "micronaut.multitenancy.tenantresolver.httpheader.enabled", value = StringUtils.TRUE) +class TenantEvaluationContextSpec extends Specification { + + @Inject + @Client("/") + HttpClient client + + void "test tenant evaluation context"() { + when: + def request = HttpRequest.GET("/tenant/expression").header(HttpHeaderTenantResolverConfiguration.DEFAULT_HEADER_NAME, "allowed") + def response = client.toBlocking().retrieve(request) + + then: + response == "allowed" + + when: + request = HttpRequest.GET("/tenant/expression") + response = client.toBlocking().retrieve(request) + + then: + response == "none" + } + + @Controller("/tenant") + @Requires(property = "spec.name", value = "TenantEvaluationContextSpec") + static class TestTenantResolver { + + final BeanContext beanContext + + TestTenantResolver(BeanContext beanContext) { + this.beanContext = beanContext + } + + @Get("/expression") + @Produces(MediaType.TEXT_PLAIN) + String expression() { + beanContext.createBean(Holder).value + } + } + + @Factory + @Requires(property = "spec.name", value = "TenantEvaluationContextSpec") + static class TestFactory { + + @Bean + Holder holder(@Value("#{ tenantId }") @Nullable String tenantId) { + new Holder(value: tenantId ?: 'none') + } + } + + static class Holder { + String value + } +} diff --git a/multitenancy/src/test/resources/logback.xml b/multitenancy/src/test/resources/logback.xml index 65335218..aae064e6 100644 --- a/multitenancy/src/test/resources/logback.xml +++ b/multitenancy/src/test/resources/logback.xml @@ -10,4 +10,5 @@ + diff --git a/settings.gradle b/settings.gradle index f651a393..64fd8441 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,16 +9,24 @@ plugins { id("io.micronaut.build.shared.settings") version "6.6.1" } +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + rootProject.name = 'multitenancy-parent' +include 'multitenancy-annotations' include 'multitenancy-bom' include 'multitenancy' +include "test-suite" +include "test-suite-groovy" +include "test-suite-kotlin" + micronautBuild { useStandardizedProjectNames = true importMicronautCatalog() importMicronautCatalog("micronaut-serde") importMicronautCatalog("micronaut-reactor") + importMicronautCatalog("micronaut-security") importMicronautCatalog("micronaut-session") importMicronautCatalog("micronaut-validation") } diff --git a/src/main/docs/guide/multitenancy/tenantexpression.adoc b/src/main/docs/guide/multitenancy/tenantexpression.adoc new file mode 100644 index 00000000..0613e324 --- /dev/null +++ b/src/main/docs/guide/multitenancy/tenantexpression.adoc @@ -0,0 +1,7 @@ +To allow using the current request tenant Id in the https://docs.micronaut.io/latest/guide/#evaluatedExpressions[Micronaut Expression Language], you need to include the multitenancy-annotation as an annotation processor dependency. + +dependency:io.micronaut.multitenancy:micronaut-multitenancy-annotation[scope="annotationProcessor"] + +snippet::io.micronaut.multitenancy.TenantCheckingSecuredController[tags="clazz"] + +<1> If `tenantId` (provided by the configured TenantResolver) is equal to `'allowed'` then allow the request. diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index c5ef9da9..7d41fb1b 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -3,6 +3,7 @@ multitenancy: title: Introduction installation: Installation tenantresolvers: Built-In Tenant Resolvers + tenantexpression: Tenant Expression Language subdomainresolver: title: Subdomain Tenant Resolver publicsuffixlist: Public Suffix List Subdomain Tenant Resolver diff --git a/test-suite-groovy/build.gradle.kts b/test-suite-groovy/build.gradle.kts new file mode 100644 index 00000000..9fa6c8b6 --- /dev/null +++ b/test-suite-groovy/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + id("groovy") +} + +description = "Test Suite for testing and documenting the Multitenancy features" + +repositories { + mavenCentral() +} + +dependencies { + testImplementation(projects.micronautMultitenancyAnnotations) + testImplementation(projects.micronautMultitenancy) + + testImplementation(mn.micronaut.inject.groovy) + testImplementation(mnSecurity.micronaut.security.annotations) + + testImplementation(mn.micronaut.http.client) + testImplementation(mn.micronaut.http.server.netty) + testImplementation(mn.micronaut.jackson.databind) + + testImplementation(mnSecurity.micronaut.security) + testImplementation(mnTest.micronaut.test.spock) + + testRuntimeOnly(mnLogging.logback.classic) +} + +tasks.withType().configureEach { + useJUnitPlatform() +} diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/multitenancy/TenantCheckingSecuredController.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/multitenancy/TenantCheckingSecuredController.groovy new file mode 100644 index 00000000..adec15f3 --- /dev/null +++ b/test-suite-groovy/src/test/groovy/io/micronaut/multitenancy/TenantCheckingSecuredController.groovy @@ -0,0 +1,20 @@ +package io.micronaut.multitenancy + +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Produces +import io.micronaut.security.annotation.Secured + +// tag::clazz[] +@Controller +class TenantCheckingSecuredController { + + @Get + @Produces(MediaType.TEXT_PLAIN) + @Secured("#{ tenantId == 'allowed' }") // <1> + String index() { + "Hello World" + } +} +// end::clazz[] diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/multitenancy/Tests.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/multitenancy/Tests.groovy new file mode 100644 index 00000000..1912e640 --- /dev/null +++ b/test-suite-groovy/src/test/groovy/io/micronaut/multitenancy/Tests.groovy @@ -0,0 +1,36 @@ +package io.micronaut.multitenancy; + +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.test.extensions.spock.annotation.MicronautTest; +import jakarta.inject.Inject +import spock.lang.Specification; + +@MicronautTest +class Tests extends Specification { + + @Inject + @Client("/") + HttpClient client; + + void testSecuredAnnotationCanSeeTenantIds() { + when: + MutableHttpRequest sergio = HttpRequest.GET("/").header("X-Tenant", "sergio") + client.toBlocking().exchange(sergio) + + then: + def exception = thrown(HttpClientResponseException) + exception.status == HttpStatus.UNAUTHORIZED + + when: + // But the tim request has tenant as tim so secured annotation evaluates to false + MutableHttpRequest tim = HttpRequest.GET("/").header("X-Tenant", "allowed") + + then: + client.toBlocking().retrieve(tim) == "Hello World" + } +} diff --git a/test-suite-groovy/src/test/resources/application.properties b/test-suite-groovy/src/test/resources/application.properties new file mode 100644 index 00000000..fc07ec43 --- /dev/null +++ b/test-suite-groovy/src/test/resources/application.properties @@ -0,0 +1,2 @@ +micronaut.multitenancy.tenantresolver.httpheader.enabled=true +micronaut.multitenancy.tenantresolver.httpheader.header-name=X-Tenant diff --git a/test-suite-groovy/src/test/resources/logback.xml b/test-suite-groovy/src/test/resources/logback.xml new file mode 100644 index 00000000..8cff1e50 --- /dev/null +++ b/test-suite-groovy/src/test/resources/logback.xml @@ -0,0 +1,15 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + diff --git a/test-suite-kotlin/build.gradle.kts b/test-suite-kotlin/build.gradle.kts new file mode 100644 index 00000000..4280918c --- /dev/null +++ b/test-suite-kotlin/build.gradle.kts @@ -0,0 +1,41 @@ +plugins { + id("org.jetbrains.kotlin.jvm") + id("org.jetbrains.kotlin.kapt") +} + +description = "Test Suite for testing and documenting the Multitenancy features" + +repositories { + mavenCentral() +} + +dependencies { + kaptTest(mn.micronaut.inject.kotlin) + kaptTest(projects.micronautMultitenancyAnnotations) + kaptTest(mnSecurity.micronaut.security.annotations) + + testImplementation(projects.micronautMultitenancy) + + testImplementation(mn.micronaut.http.client) + testImplementation(mn.micronaut.inject.java) + testImplementation(mn.micronaut.http.server.netty) + testImplementation(mn.micronaut.jackson.databind) + + testImplementation(mnSecurity.micronaut.security) + testImplementation(mnSecurity.micronaut.security.annotations) + + testImplementation(mnTest.micronaut.test.junit5) + + testRuntimeOnly(mnLogging.logback.classic) + testRuntimeOnly(libs.junit.jupiter.engine) +} + +tasks.withType().configureEach { + useJUnitPlatform() +} + +kotlin { + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/multitenancy/TenantCheckingSecuredController.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/multitenancy/TenantCheckingSecuredController.kt new file mode 100644 index 00000000..8857933e --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/multitenancy/TenantCheckingSecuredController.kt @@ -0,0 +1,18 @@ +package io.micronaut.multitenancy + +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Produces +import io.micronaut.security.annotation.Secured + +// tag::clazz[] +@Controller +class TenantCheckingSecuredController { + + @Get + @Produces(MediaType.TEXT_PLAIN) + @Secured("#{ tenantId == 'allowed' }") // <1> + fun index() = "Hello World" +} +// end::clazz[] diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/multitenancy/Tests.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/multitenancy/Tests.kt new file mode 100644 index 00000000..8cabff7b --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/multitenancy/Tests.kt @@ -0,0 +1,33 @@ +package io.micronaut.multitenancy + +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import jakarta.inject.Inject +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +@MicronautTest +class Tests { + + @Inject + @field:Client("/") + lateinit var client: HttpClient + + @Test + fun testSecuredAnnotationCanSeeTenantIds() { + // Sergio requires authentication as the tenant is not 'allowed' + val sergio = HttpRequest.GET("/").header("X-Tenant", "sergio") + val thrown = Assertions.assertThrows(HttpClientResponseException::class.java) { + client.toBlocking().exchange(sergio) + } + Assertions.assertEquals(HttpStatus.UNAUTHORIZED, thrown.status) + + // But this request has tenant as 'allowed' so secured annotation evaluates to true and the request is allowed + val tim = HttpRequest.GET("/").header("X-Tenant", "allowed") + Assertions.assertEquals("Hello World", client.toBlocking().retrieve(tim)) + } +} diff --git a/test-suite-kotlin/src/test/resources/application.properties b/test-suite-kotlin/src/test/resources/application.properties new file mode 100644 index 00000000..fc07ec43 --- /dev/null +++ b/test-suite-kotlin/src/test/resources/application.properties @@ -0,0 +1,2 @@ +micronaut.multitenancy.tenantresolver.httpheader.enabled=true +micronaut.multitenancy.tenantresolver.httpheader.header-name=X-Tenant diff --git a/test-suite-kotlin/src/test/resources/logback.xml b/test-suite-kotlin/src/test/resources/logback.xml new file mode 100644 index 00000000..8cff1e50 --- /dev/null +++ b/test-suite-kotlin/src/test/resources/logback.xml @@ -0,0 +1,15 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + diff --git a/test-suite/build.gradle.kts b/test-suite/build.gradle.kts new file mode 100644 index 00000000..addd1474 --- /dev/null +++ b/test-suite/build.gradle.kts @@ -0,0 +1,33 @@ +plugins { + id("java-library") +} + +description = "Test Suite for testing and documenting the Multitenancy features" + +repositories { + mavenCentral() +} + +dependencies { + testAnnotationProcessor(mn.micronaut.inject.java) + testAnnotationProcessor(projects.micronautMultitenancyAnnotations) + testAnnotationProcessor(mnSecurity.micronaut.security.annotations) + + testImplementation(projects.micronautMultitenancy) + + testImplementation(mn.micronaut.http.client) + testImplementation(mn.micronaut.http.server.netty) + testImplementation(mn.micronaut.jackson.databind) + + testImplementation(mnSecurity.micronaut.security) + testImplementation(mnSecurity.micronaut.security.annotations) + + testImplementation(mnTest.micronaut.test.junit5) + + testRuntimeOnly(mnLogging.logback.classic) + testRuntimeOnly(libs.junit.jupiter.engine) +} + +tasks.withType().configureEach { + useJUnitPlatform() +} diff --git a/test-suite/src/test/java/io/micronaut/multitenancy/TenantCheckingSecuredController.java b/test-suite/src/test/java/io/micronaut/multitenancy/TenantCheckingSecuredController.java new file mode 100644 index 00000000..0b521c73 --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/multitenancy/TenantCheckingSecuredController.java @@ -0,0 +1,20 @@ +package io.micronaut.multitenancy; + +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Produces; +import io.micronaut.security.annotation.Secured; + +// tag::clazz[] +@Controller +public class TenantCheckingSecuredController { + + @Get + @Produces(MediaType.TEXT_PLAIN) + @Secured("#{ tenantId == 'allowed' }") // <1> + public String index() { + return "Hello World"; + } +} +// end::clazz[] diff --git a/test-suite/src/test/java/io/micronaut/multitenancy/Tests.java b/test-suite/src/test/java/io/micronaut/multitenancy/Tests.java new file mode 100644 index 00000000..bedad58d --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/multitenancy/Tests.java @@ -0,0 +1,32 @@ +package io.micronaut.multitenancy; + +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.client.BlockingHttpClient; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.http.client.exceptions.HttpClientResponseException; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@MicronautTest +class Tests { + + @Test + void testSecuredAnnotationCanSeeTenantIds(@Client("/") HttpClient client) { + BlockingHttpClient blockingClient = client.toBlocking(); + + // Sergio requires authentication as the tenant is not 'allowed' + MutableHttpRequest sergio = HttpRequest.GET("/").header("X-Tenant", "sergio"); + HttpClientResponseException thrown = assertThrows(HttpClientResponseException.class, () -> blockingClient.exchange(sergio)); + assertEquals(HttpStatus.UNAUTHORIZED, thrown.getStatus()); + + // But the allowed request has tenant as allowed so secured annotation evaluates to true and the request is allowed + MutableHttpRequest tim = HttpRequest.GET("/").header("X-Tenant", "allowed"); + assertEquals("Hello World", blockingClient.retrieve(tim)); + } +} diff --git a/test-suite/src/test/resources/application.properties b/test-suite/src/test/resources/application.properties new file mode 100644 index 00000000..fc07ec43 --- /dev/null +++ b/test-suite/src/test/resources/application.properties @@ -0,0 +1,2 @@ +micronaut.multitenancy.tenantresolver.httpheader.enabled=true +micronaut.multitenancy.tenantresolver.httpheader.header-name=X-Tenant diff --git a/test-suite/src/test/resources/logback.xml b/test-suite/src/test/resources/logback.xml new file mode 100644 index 00000000..8cff1e50 --- /dev/null +++ b/test-suite/src/test/resources/logback.xml @@ -0,0 +1,15 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + +