Skip to content

Commit

Permalink
Tenant Argument Binder
Browse files Browse the repository at this point in the history
This adds an api `Tenant`, which represents the abstract notion of a tenant.

Moreover, it adds a RequestFilter to resolve the tenant and set it as a request attribute.

This pull request adds an argument binder that uses the request attribute resolved in the filter. Because of this, a user can bind the tenant as a controller method parameter
  • Loading branch information
sdelamo committed Oct 30, 2024
1 parent c0769c8 commit ceabab7
Show file tree
Hide file tree
Showing 20 changed files with 663 additions and 0 deletions.
8 changes: 8 additions & 0 deletions multitenancy/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,12 @@ dependencies {
testImplementation(mn.micronaut.http.server.netty)
testImplementation(mnSession.micronaut.session)
testImplementation(mn.snakeyaml)

testAnnotationProcessor(mn.micronaut.inject.java)
testImplementation(mnTest.micronaut.test.junit5)
testRuntimeOnly(libs.junit.jupiter.engine)
}

tasks.withType<Test> {
useJUnitPlatform()
}
31 changes: 31 additions & 0 deletions multitenancy/src/main/java/io/micronaut/multitenancy/Tenant.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2017-2024 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;

/**
* This interface represents the abstract notion of a tenant.
* @author Sergio del Amo
* @since 5.5.0
*/

@FunctionalInterface
public interface Tenant {
/**
* A Unique identifier for the tenant.
* @return A Unique identifier for the tenant.
*/
String id();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Copyright 2017-2024 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.filter;

import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.order.Ordered;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.annotation.RequestFilter;
import io.micronaut.http.annotation.ServerFilter;
import io.micronaut.http.filter.FilterPatternStyle;
import io.micronaut.http.filter.ServerFilterPhase;
import io.micronaut.multitenancy.exceptions.TenantNotFoundException;
import io.micronaut.multitenancy.tenantresolver.HttpRequestTenantResolver;
import io.micronaut.multitenancy.tenantresolver.TenantResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Serializable;
import java.util.Optional;

/**
* Adds a tenant identifier if resolved as a request attribute.
* @author Sergio del Amo
* @since 5.5.0
*/
@ServerFilter(patternStyle = FilterPatternStyle.REGEX,
value = "${" + TenantResolverFilterConfigurationProperties.PREFIX + ".regex-pattern:" + TenantResolverFilterConfigurationProperties.DEFAULT_REGEX_PATTERN + "}")
@Internal
final class TenantResolverFilter implements Ordered {
/**
* Request attribute for tenant Identifier.
*/
public static final String ATTRIBUTE_TENANT = "tenantIdentifier";

private static final Logger LOG = LoggerFactory.getLogger(TenantResolverFilter.class);

@Nullable
private final HttpRequestTenantResolver httpRequestTenantResolver;

@Nullable
private final TenantResolver tenantResolver;

/**
*
* @param httpRequestTenantResolver HTTP Request Tenant Resolver
* @param tenantResolver Tenant Resolver
*/
TenantResolverFilter(@Nullable HttpRequestTenantResolver httpRequestTenantResolver,
@Nullable TenantResolver tenantResolver) {
this.httpRequestTenantResolver = httpRequestTenantResolver;
this.tenantResolver = tenantResolver;
}

@Override
public int getOrder() {
return ServerFilterPhase.SECURITY.before();
}

@RequestFilter
void filter(HttpRequest<?> request) {
Optional<Serializable> tenantOptional = resolveTenant(request);
if (tenantOptional.isEmpty()) {
tenantOptional = resolveTenant();
}
tenantOptional.ifPresent(tenant -> request.setAttribute(ATTRIBUTE_TENANT, tenant));
}

private Optional<Serializable> resolveTenant(HttpRequest<?> request) {
if (httpRequestTenantResolver == null) {
return Optional.empty();
}
try {
return Optional.of(httpRequestTenantResolver.resolveTenantIdentifier(request));
} catch (TenantNotFoundException ex) {
if (LOG.isTraceEnabled()) {
LOG.trace("Tenant could not be resolved");
}
}
return Optional.empty();
}


private Optional<Serializable> resolveTenant() {
if (tenantResolver == null) {
return Optional.empty();
}
try {
return Optional.of(tenantResolver.resolveTenantIdentifier());
} catch (TenantNotFoundException ex) {
if (LOG.isTraceEnabled()) {
LOG.trace("Tenant could not be resolved");
}
}
return Optional.empty();
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2017-2024 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.filter;

import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.util.Toggleable;

/**
* Configuration for the {@link TenantResolverFilter}.
* @author Sergio del Amo
* @since 5.5.0
*/
public interface TenantResolverFilterConfiguration extends Toggleable {
/**
*
* @return Regular expression pattern. Filter will only process requests whose path matches this pattern.
*/
@NonNull
String getRegexPattern();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright 2017-2024 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.filter;

import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.core.annotation.Internal;
import io.micronaut.multitenancy.MultitenancyConfiguration;

/**
* {@link ConfigurationProperties} implementation of {@link TenantResolverFilterConfiguration}.
*/
@ConfigurationProperties(TenantResolverFilterConfigurationProperties.PREFIX)
@Internal
final class TenantResolverFilterConfigurationProperties implements TenantResolverFilterConfiguration {
public static final String PREFIX = MultitenancyConfiguration.PREFIX + ".filter";
/**
* The default regex pattern.
*/
@SuppressWarnings("WeakerAccess")
public static final String DEFAULT_REGEX_PATTERN = "^.*$";

/**
* The default enable value.
*/
@SuppressWarnings("WeakerAccess")
public static final boolean DEFAULT_ENABLED = true;

private boolean enabled = DEFAULT_ENABLED;
private String regexPattern = DEFAULT_REGEX_PATTERN;

@Override
public boolean isEnabled() {
return enabled;
}

/**
* Whether {@link io.micronaut.multitenancy.filter.TenantResolverFilter} should be enabled. Default value ({@value #DEFAULT_ENABLED}).
* @param enabled enabled flag
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}

@Override
public String getRegexPattern() {
return regexPattern;
}

/**
* Tenant Resolver filter processes only request paths matching this regular expression. Default Value: {@value #DEFAULT_REGEX_PATTERN}
* @param regexPattern Regular expression pattern for the filter.
*/
public void setRegexPattern(String regexPattern) {
this.regexPattern = regexPattern;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2017-2024 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.filter;

import io.micronaut.core.annotation.Internal;
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.type.Argument;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.bind.binders.TypedRequestArgumentBinder;
import io.micronaut.multitenancy.Tenant;
import jakarta.inject.Singleton;

import java.util.Optional;

@Internal
@Singleton
final class TenantTypedRequestArgumentBinder implements TypedRequestArgumentBinder<Tenant> {
@Override
public Argument<Tenant> argumentType() {
return Argument.of(Tenant.class);
}

@Override
public BindingResult<Tenant> bind(ArgumentConversionContext<Tenant> context, HttpRequest<?> source) {
if (!source.getAttributes().contains(TenantResolverFilter.ATTRIBUTE_TENANT)) {
return BindingResult.UNSATISFIED;
}
Optional<BindingResult<Tenant>> bindingResult = source.getAttribute(TenantResolverFilter.ATTRIBUTE_TENANT, String.class)
.map(tenantId -> (Tenant) () -> tenantId)
.map(tenant -> () -> Optional.of(tenant));
return bindingResult.isEmpty()
? BindingResult.EMPTY
: bindingResult.get();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2017-2024 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.
*/
/**
* Classes related to Tenant Resolution Filter.
* @author Sergio del Amo
* @since 5.5.0
*/
@Requires(property = TenantResolverFilterConfigurationProperties.PREFIX + ".enabled",
notEquals = StringUtils.FALSE,
defaultValue = StringUtils.TRUE)
@Configuration
package io.micronaut.multitenancy.filter;

import io.micronaut.context.annotation.Configuration;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.util.StringUtils;
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.micronaut.multitenancy.filter;

import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertTrue;

@MicronautTest(startApplication = false)
class TenantResolverFilterConfigurationTest {

@Test
void defaultEnabled(TenantResolverFilterConfiguration tenantResolverFilterConfiguration) {
assertTrue(tenantResolverFilterConfiguration.isEnabled());
}
}
Loading

0 comments on commit ceabab7

Please sign in to comment.