Skip to content

Commit

Permalink
Expose tenantId via EL with new annotation module (#255)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
timyates and sdelamo authored Nov 22, 2023
1 parent 0749ce4 commit af4c864
Show file tree
Hide file tree
Showing 30 changed files with 597 additions and 0 deletions.
9 changes: 9 additions & 0 deletions buildSrc/build.gradle
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
plugins {
id 'groovy-gradle-plugin'
}

repositories {
gradlePluginPortal()
mavenCentral()
}

dependencies {
implementation(libs.gradle.kotlin)
}
7 changes: 7 additions & 0 deletions buildSrc/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
dependencyResolutionManagement {
versionCatalogs {
libs {
from(files("../gradle/libs.versions.toml"))
}
}
}
13 changes: 13 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 10 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
16 changes: 16 additions & 0 deletions multitenancy-annotations/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.micronaut.multitenancy.expression.TenantEvaluationContextRegistrar
2 changes: 2 additions & 0 deletions multitenancy/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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
}
}
1 change: 1 addition & 0 deletions multitenancy/src/test/resources/logback.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
<root level="info">
<appender-ref ref="STDOUT" />
</root>
<logger name="io.micronaut.multitenancy" level="DEBUG" />
</configuration>
8 changes: 8 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
7 changes: 7 additions & 0 deletions src/main/docs/guide/multitenancy/tenantexpression.adoc
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions src/main/docs/guide/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions test-suite-groovy/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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<Test>().configureEach {
useJUnitPlatform()
}
Loading

0 comments on commit af4c864

Please sign in to comment.