Skip to content

Commit

Permalink
feat: Tenant details enhanced config and validation
Browse files Browse the repository at this point in the history
Signed-off-by: Thomas Vitale <[email protected]>
  • Loading branch information
ThomasVitale committed Mar 30, 2024
1 parent c2c6e56 commit ec1101a
Show file tree
Hide file tree
Showing 22 changed files with 345 additions and 165 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
* A {@link TenantEventListener} that sets the tenant identifier from the current context
* on an existing {@link Observation}.
*/
public class ObservationTenantContextEventListener implements TenantEventListener {
public final class ObservationTenantContextEventListener implements TenantEventListener {

private static final Logger logger = LoggerFactory.getLogger(ObservationTenantContextEventListener.class);

protected static final Cardinality DEFAULT_CARDINALITY = Cardinality.HIGH;
static final Cardinality DEFAULT_CARDINALITY = Cardinality.HIGH;

protected static final String DEFAULT_TENANT_IDENTIFIER_KEY = "tenant.id";
static final String DEFAULT_TENANT_IDENTIFIER_KEY = "tenant.id";

private final Cardinality cardinality;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.arconia.core.multitenancy.context.events;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.arconia.core.multitenancy.events.TenantEvent;
import io.arconia.core.multitenancy.events.TenantEventListener;
import io.arconia.core.multitenancy.exceptions.TenantResolutionException;
import io.arconia.core.multitenancy.tenantdetails.TenantDetailsService;

/**
* A {@link TenantEventListener} that validates the tenant for the current context.
*/
public final class ValidatingTenantContextEventListener implements TenantEventListener {

private static final Logger logger = LoggerFactory.getLogger(ValidatingTenantContextEventListener.class);

private final TenantDetailsService tenantDetailsService;

public ValidatingTenantContextEventListener(TenantDetailsService tenantDetailsService) {
this.tenantDetailsService = tenantDetailsService;
}

@Override
public void onApplicationEvent(TenantEvent tenantEvent) {
if (tenantEvent instanceof TenantContextAttachedEvent event) {
logger.trace("Validating tenant {}", event.getTenantIdentifier());
var tenant = tenantDetailsService.loadTenantByIdentifier(event.getTenantIdentifier());
if (tenant == null || !tenant.isEnabled()) {
throw new TenantResolutionException("The resolved tenant is invalid or disabled");
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class Tenant implements TenantDetails {

private final Map<String, Object> attributes;

public Tenant(String identifier, boolean enabled, @Nullable Map<String, Object> attributes) {
protected Tenant(String identifier, boolean enabled, @Nullable Map<String, Object> attributes) {
Assert.hasText(identifier, "identifier cannot be null or empty");

this.identifier = identifier;
Expand Down Expand Up @@ -49,7 +49,7 @@ public static class Builder {

private String identifier;

private boolean enabled;
private boolean enabled = true;

private Map<String, Object> attributes = new HashMap<>();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package io.arconia.core.multitenancy.context.events;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import io.arconia.core.multitenancy.exceptions.TenantResolutionException;
import io.arconia.core.multitenancy.tenantdetails.Tenant;
import io.arconia.core.multitenancy.tenantdetails.TenantDetailsService;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class ValidatingTenantContextEventListenerTests {

@Mock
TenantDetailsService tenantDetailsService;

@InjectMocks
ValidatingTenantContextEventListener validatingTenantContextEventListener;

@Test
void whenTenantExistsThenOk() {
var tenantIdentifier = "acme";
when(tenantDetailsService.loadTenantByIdentifier(anyString()))
.thenReturn(Tenant.create().identifier(tenantIdentifier).build());

validatingTenantContextEventListener.onApplicationEvent(new TenantContextAttachedEvent(tenantIdentifier, this));
}

@Test
void whenTenantDoesNotExistThenThrow() {
var tenantIdentifier = "acme";
when(tenantDetailsService.loadTenantByIdentifier(anyString())).thenReturn(null);

assertThatThrownBy(() -> validatingTenantContextEventListener
.onApplicationEvent(new TenantContextAttachedEvent(tenantIdentifier, this)))
.isInstanceOf(TenantResolutionException.class)
.hasMessageContaining("The resolved tenant is invalid or disabled");
}

@Test
void whenTenantDisabledThenThrow() {
var tenantIdentifier = "acme";
when(tenantDetailsService.loadTenantByIdentifier(anyString()))
.thenReturn(Tenant.create().identifier(tenantIdentifier).enabled(false).build());

assertThatThrownBy(() -> validatingTenantContextEventListener
.onApplicationEvent(new TenantContextAttachedEvent(tenantIdentifier, this)))
.isInstanceOf(TenantResolutionException.class)
.hasMessageContaining("The resolved tenant is invalid or disabled");
}

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package io.arconia.core.multitenancy.events;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.ApplicationEventPublisher;

import io.arconia.core.multitenancy.context.events.TenantContextAttachedEvent;
Expand All @@ -11,8 +15,15 @@
/**
* Unit tests for {@link DefaultTenantEventPublisher}.
*/
@ExtendWith(MockitoExtension.class)
class DefaultTenantEventPublisherTests {

@Mock
ApplicationEventPublisher applicationEventPublisher;

@InjectMocks
DefaultTenantEventPublisher tenantEventPublisher;

@Test
void whenNullApplicationEventPublisherThenThrow() {
assertThatThrownBy(() -> new DefaultTenantEventPublisher(null)).isInstanceOf(IllegalArgumentException.class)
Expand All @@ -21,21 +32,17 @@ void whenNullApplicationEventPublisherThenThrow() {

@Test
void whenTenantEventThenPublish() {
var applicationEventPublisher = Mockito.mock(ApplicationEventPublisher.class);
var publisher = new DefaultTenantEventPublisher(applicationEventPublisher);
var event = new TenantContextAttachedEvent("tenant", this);

publisher.publishTenantEvent(event);
tenantEventPublisher.publishTenantEvent(event);

Mockito.verify(applicationEventPublisher).publishEvent(event);
}

@Test
void whenNullTenantEventThenThrow() {
var applicationEventPublisher = Mockito.mock(ApplicationEventPublisher.class);
var publisher = new DefaultTenantEventPublisher(applicationEventPublisher);

assertThatThrownBy(() -> publisher.publishTenantEvent(null)).isInstanceOf(IllegalArgumentException.class)
assertThatThrownBy(() -> tenantEventPublisher.publishTenantEvent(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("tenantEvent cannot be null");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ void whenIdentifierIsEmptyThenThrow() {
}

@Test
void whenAttributesIsNullThenUseEmptymap() {
void whenAttributesIsNullThenUseEmptyMap() {
var tenant = Tenant.create().identifier("acme").attributes(null).build();
assertThat(tenant.getAttributes()).isNotNull();
assertThat(tenant.getAttributes()).isEmpty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;

import io.arconia.autoconfigure.multitenancy.core.tenantdetails.PropertiesTenantDetailsService;
import io.arconia.autoconfigure.multitenancy.core.tenantdetails.TenantDetailsProperties;
import io.arconia.autoconfigure.multitenancy.core.tenantdetails.TenantDetailsConfiguration;
import io.arconia.core.multitenancy.cache.DefaultTenantKeyGenerator;
import io.arconia.core.multitenancy.cache.TenantKeyGenerator;
import io.arconia.core.multitenancy.context.events.HolderTenantContextEventListener;
Expand All @@ -17,14 +17,13 @@
import io.arconia.core.multitenancy.context.resolvers.FixedTenantResolver;
import io.arconia.core.multitenancy.events.DefaultTenantEventPublisher;
import io.arconia.core.multitenancy.events.TenantEventPublisher;
import io.arconia.core.multitenancy.tenantdetails.TenantDetailsService;

/**
* Auto-configuration for core multitenancy.
*/
@AutoConfiguration
@EnableConfigurationProperties({ FixedTenantResolutionProperties.class, TenantDetailsProperties.class,
TenantManagementProperties.class })
@EnableConfigurationProperties({ FixedTenantResolutionProperties.class, TenantManagementProperties.class })
@Import(TenantDetailsConfiguration.class)
public class MultitenancyCoreAutoConfiguration {

@Bean
Expand All @@ -34,7 +33,6 @@ TenantKeyGenerator tenantKeyGenerator() {
}

@Bean
@ConditionalOnMissingBean
HolderTenantContextEventListener holderTenantContextEventListener() {
return new HolderTenantContextEventListener();
}
Expand Down Expand Up @@ -71,12 +69,4 @@ TenantEventPublisher tenantEventPublisher(ApplicationEventPublisher applicationE
return new DefaultTenantEventPublisher(applicationEventPublisher);
}

@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = TenantDetailsProperties.CONFIG_PREFIX, name = "source", havingValue = "properties",
matchIfMissing = true)
TenantDetailsService tenantDetailsService(TenantDetailsProperties tenantDetailsProperties) {
return new PropertiesTenantDetailsService(tenantDetailsProperties);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

import io.arconia.core.multitenancy.tenantdetails.Tenant;
import io.arconia.core.multitenancy.tenantdetails.TenantDetails;
import io.arconia.core.multitenancy.tenantdetails.TenantDetailsService;

Expand All @@ -22,7 +23,7 @@ public PropertiesTenantDetailsService(TenantDetailsProperties tenantDetailsPrope

@Override
public List<? extends TenantDetails> loadAllTenants() {
return tenantDetailsProperties.getTenants();
return tenantDetailsProperties.getTenants().stream().map(this::toTenant).toList();
}

@Nullable
Expand All @@ -31,9 +32,18 @@ public TenantDetails loadTenantByIdentifier(String identifier) {
Assert.hasText(identifier, "identifier cannot be null or empty");
return tenantDetailsProperties.getTenants()
.stream()
.map(this::toTenant)
.filter(tenant -> tenant.getIdentifier().equals(identifier))
.findFirst()
.orElse(null);
}

private Tenant toTenant(TenantDetailsProperties.TenantConfig tenantConfig) {
return Tenant.create()
.identifier(tenantConfig.getIdentifier())
.enabled(tenantConfig.isEnabled())
.attributes(tenantConfig.getAttributes())
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.arconia.autoconfigure.multitenancy.core.tenantdetails;

import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import io.arconia.core.multitenancy.context.events.ValidatingTenantContextEventListener;
import io.arconia.core.multitenancy.tenantdetails.TenantDetailsService;

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(TenantDetailsProperties.class)
public class TenantDetailsConfiguration {

@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = TenantDetailsProperties.CONFIG_PREFIX, name = "source", havingValue = "properties")
TenantDetailsService tenantDetailsService(TenantDetailsProperties tenantDetailsProperties) {
return new PropertiesTenantDetailsService(tenantDetailsProperties);
}

@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(TenantDetailsService.class)
ValidatingTenantContextEventListener validatingTenantContextEventListener(
TenantDetailsService tenantDetailsService) {
return new ValidatingTenantContextEventListener(tenantDetailsService);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.springframework.boot.context.properties.ConfigurationProperties;

import io.arconia.core.multitenancy.tenantdetails.Tenant;
import org.springframework.util.Assert;

/**
* Configuration properties for tenant details.
Expand All @@ -18,12 +18,12 @@ public class TenantDetailsProperties {
/**
* The source of tenant details.
*/
private Source source = Source.PROPERTIES;
private Source source = Source.NONE;

/**
* List of tenant details.
*/
private List<Tenant> tenants = new ArrayList<>();
private List<TenantConfig> tenants = new ArrayList<>();

public Source getSource() {
return source;
Expand All @@ -33,17 +33,53 @@ public void setSource(Source source) {
this.source = source;
}

public List<Tenant> getTenants() {
public List<TenantConfig> getTenants() {
return tenants;
}

public void setTenants(List<Tenant> tenants) {
public void setTenants(List<TenantConfig> tenants) {
this.tenants = tenants;
}

public enum Source {

HTTP, JDBC, PROPERTIES
NONE, PROPERTIES

}

public static class TenantConfig {

private String identifier;

private boolean enabled = true;

private Map<String, Object> attributes = Map.of();

public String getIdentifier() {
return identifier;
}

public void setIdentifier(String identifier) {
Assert.hasText(identifier, "identifier cannot be null or empty");
this.identifier = identifier;
}

public boolean isEnabled() {
return enabled;
}

public void setEnabled(boolean enabled) {
this.enabled = enabled;
}

public Map<String, Object> getAttributes() {
return attributes;
}

public void setAttributes(Map<String, Object> attributes) {
Assert.notNull(attributes, "attributes cannot be null");
this.attributes = attributes;
}

}

Expand Down
Loading

0 comments on commit ec1101a

Please sign in to comment.