# Multitenancy with Spring Observation: > All spring implementations are the same | Feature | Multi Databases | RLS | Schema | | -------------------- | ----------------------- | -------------- | --------------------------- | | DB Migrations | 1 migration for each db | 1 migration | 1 migration for each schema | | DB deployment | Create a new DB + migration | Create User | Create User + Schema + migration | | Security | High | Normal | Normal Plus | | Cost | High | Normal | Normal | | Performance | High | Normal | Normal | | Code Maintainability | Easy | Easy | Easy | | Testing | Test each config | Test each config | Test each config | | MS deployment | Lots of config | Lots of config | Lots of config | ## Pros/Cons Pros of all cases: 1. Maintainability of code i. Spring code is easy to mantain and do not affect current codebase 2. Improved security on data leaks between vendors Cons of all cases: 1. Micro-services deployment aspects i. All credentials should be mapped on helm chart, hence it will become very big ii. Also include mapping of vendors to credentials on application.yaml 2. Testing i. All tenants should be tested Multi Databases: 1. Security i. There is less chance of exposing all data at same time and data is very segregated. ii. Possible source of security incidents: wrong configuration of vendor per database on microsservice 2. Performance vs Cost: i. Most performant ii. Most expensive iii. There is data redundancy (on trained models and labels) 3. Maintainability of code ii. New tests to consider multiple database case 4. Database deployment and migrations i. Create a new database for each vendor ii. Migration multiple databases (might requer new scripts, no standard on spring) iii. Trained models and labels must be inserted on some/all databases, so we might need special ways of handling it Row Level Security: 1. Security i. Data is segregated on one database per multiple users ii. Possible source of security incidents: wrong configuration of vendor per database on microsservice, wrong configuration on database, wrong queries on tables without RLS (labels - trained models) iii. Care should be taken with admin user, as RLS does not apply to admin iiii. Technically, on our architecture, one vendor could create config to another vendor 2. Performance vs Cost: i. Performant ii. There is no data redundancy iii. Much cheaper and scallable 4. Database deployment and migrations i. Create a new user for each vendor iii. Activate RLS on new tables that depends on vendor_id ii. Migration on 1 database only Multi - Schema: 1. Security i. Data is segregated on one database per multiple users ii. Possible source of security incidents: wrong configuration of vendor per database on microsservice, wrong configuration on database, wrong queries on tables sharing schemas (if choosen to share schema) iii. Care should be taken with admin user, as RLS does not apply to admin 2. Performance vs Cost: i. Performant ii. There is no data redundancy if schema is shared, otherwise there is redundancy iii. Much cheaper and scallable 4. Database deployment and migrations i. Create a new user and schema for each vendor ii. Migration for each schema iii. handling shared schema (if enabled) might become complex ## 1 - Row Level Secury To setup the Row Level Security on the database, follow those steps: 1. Enable RLS on tables and include ``` sql ALTER TABLE configs ENABLE ROW LEVEL SECURITY; ``` 2. Create policy for filtering rows given the user ``` sql CREATE POLICY config_vendor_id ON configs USING (vendor_id = CAST (current_user AS uuid)); ``` 3. Create users for each vendor (the username of the user must be the vendor id) ``` sql CREATE USER "1a5f8705-f07c-4f20-bc67-f379c94b0a90" WITH PASSWORD '123'; GRANT ALL ON ALL TABLES IN SCHEMA public TO "1a5f8705-f07c-4f20-bc67-f379c94b0a90"; ``` Besides configuring the database, we also need to include the tenants on the application.yaml file: ``` yaml multitenancy: tenants: - vendorId: 1a5f8705-f07c-4f20-bc67-f379c94b0a90 url: ${POSTGRESQL_CONNECTION_STRING} username: 1a5f8705-f07c-4f20-bc67-f379c94b0a90 password: ${POSTGRESQL_PASSWORD_1} - vendorId: 13b246ee-fda3-477c-9a32-69bd6a67794f url: ${POSTGRESQL_CONNECTION_STRING} username: 13b246ee-fda3-477c-9a32-69bd6a67794f password: ${POSTGRESQL_PASSWORD_2} ``` That is enough ! **Warning** > For testing purpose, I wasn't able to replicate that with testcontainers, but a practical test worked though. > ## 2 - Multiple Databases Create multiple tenants on the application.yaml (one for each database you have) ``` yaml multitenancy: tenants: - vendorId: 1a5f8705-f07c-4f20-bc67-f379c94b0a90 url: ${POSTGRESQL_CONNECTION_1_STRING} username: username1 password: ${POSTGRESQL_PASSWORD_1} driverClassName: org.postgresql.Driver - vendorId: 1a5f8705-f07c-4f20-bc67-f379c94b0a90 url: ${POSTGRESQL_CONNECTION_2_STRING} username: username2 password: ${POSTGRESQL_PASSWORD_2} driverClassName: org.postgresql.Driver ``` ## 3 - Multiple schemas Multiples schemas works very like the multiple databases This SQL creates the new schema and gives access: ``` sql CREATE SCHEMA IF NOT EXISTS myschema GRANT USAGE ON SCHEMA myschema TO "1769e9ef-722b-4c19-b136-b3fc69ea15e8"; GRANT ALL ON ALL TABLES IN SCHEMA myschema TO "1769e9ef-722b-4c19-b136-b3fc69ea15e8"; ``` ``` yaml multitenancy: tenants: - vendorId: 1a5f8705-f07c-4f20-bc67-f379c94b0a90 url: ${POSTGRESQL_CONNECTION_STRING} username: username1 password: ${POSTGRESQL_PASSWORD_1} schema: schema1 - vendorId: 13b246ee-fda3-477c-9a32-69bd6a67794f url: ${POSTGRESQL_CONNECTION_STRING} username: username2 password: ${POSTGRESQL_PASSWORD_2} schema: schema2 ``` ## 0 - Java Setup In spring, there is a feature called AbstractRoutingDataSource, which allows us to "route" resources given some variable. This is a very generic methodology that handles all cases. In order make this work, I need to implement some classes: 1. Data class that has a ThreadLocal<String> variable with set/getter 2. Class that extends AbstractRoutingDataSource and provides the determineCurrentLookupKey 3. Bean that extends javax.servelet.Filter (and sets dataclass given the request) 4. Configuration class that provides the DataSource bean 5. Dataclasses to load properties from application.yaml ### Tenant Context This class holds the information for which tenant should use for each thread, that is why I use ThreadLocal, otherwise one thread would interfere in others threads. ``` java public class TenantContext { private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>(); public static String getCurrentTenant() { return CURRENT_TENANT.get(); } public static void setCurrentTenant(String tenant) { CURRENT_TENANT.set(tenant); } } ``` ### Routing Data Source This class connects our TenantContext with Spring's routing data source functionallities. Implementing this class is enough for Spring give each thread the correct connection with the database. ``` java public class MultitenantDataSource extends AbstractRoutingDataSource { @Override protected String determineCurrentLookupKey() { return TenantContext.getCurrentTenant(); } public void close(){ Map<Object, DataSource> resolvedDataSources = this.getResolvedDataSources(); resolvedDataSources.forEach( (o, dataSource) -> ((HikariDataSource) dataSource).close() ); } } ``` ### Servelet Filter One of this implementation's goals is to remove the logic of which tenant to use from the rest of our services business logic, so we don't incur in errors when querying the database for the wrong tenant. The best way to protect our services is to have a specific filter that acts before running our business logic, so we can create a filter that routes the correct database connection upon receiving a connection. ``` java @Component @AllArgsConstructor @Order(1) class TenantFilter implements Filter { JwtParser jwtParser; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; final String authorization = req.getHeader(HttpHeaders.AUTHORIZATION); String vendorId = jwtParser.parseVendorId(authorization); if (StringUtils.isBlank(vendorId)) { vendorId = req.getHeader(VENDOR_ID_HEADER); } TenantContext.setCurrentTenant(vendorId); try { chain.doFilter(request, response); } finally { TenantContext.setCurrentTenant(""); } } } ``` This class is able to get the tenant name from the header or from the token. ### Configuration ``` java @Configuration @EnableConfigurationProperties(MultitenancyConfigurationProperties.class) public class MultitenancyConfiguration { private final MultitenancyConfigurationProperties multitenancyProperties; public MultitenancyConfiguration(MultitenancyConfigurationProperties multitenancyProperties) { this.multitenancyProperties = multitenancyProperties; } @Primary @Bean(destroyMethod = "close") public DataSource dataSource() { Map<Object, Object> resolvedDataSources = new HashMap<>(); for (TenantConfigurationProperty properties : this.multitenancyProperties.getTenants()) { HikariConfig hikariConfig = new HikariConfig(); String tenantId = properties.getVendorId(); if (StringUtils.isBlank(properties.getDriverClassName())) { hikariConfig.setDriverClassName("org.postgresql.Driver"); } else { hikariConfig.setDriverClassName(properties.getDriverClassName()); } hikariConfig.setUsername(properties.getUsername()); hikariConfig.setPassword(properties.getPassword()); hikariConfig.setJdbcUrl(properties.getUrl()); resolvedDataSources.put(tenantId, new HikariDataSource(hikariConfig)); } MultitenantDataSource dataSource = new MultitenantDataSource(); dataSource.setDefaultTargetDataSource(resolvedDataSources.get("default")); dataSource.setTargetDataSources(resolvedDataSources); dataSource.afterPropertiesSet(); return dataSource; } } ``` ### Properties Those last classes are responsible for loading the properties from the application.yaml ``` java @Data @ToString @ConfigurationProperties(prefix = "multitenancy") @Validated public class MultitenancyConfigurationProperties { @NotEmpty private List<TenantConfigurationProperty> tenants; } ``` ``` java @Data @ToString @Validated public class TenantConfigurationProperty { @NotEmpty private String vendorId; @NotEmpty private String url; @NotEmpty private String password; @NotEmpty private String username; private String driverClassName; } ```