Tuesday, April 10, 2018

Building SaaS style multi-tenant web app with Spring Boot 2 and Spring Security 5 - Part 2

Continuing from part 1


In part 1 of this blog post, I shared how you can build a SaaS style multi-tenant web application and how it works.

In this part of the blog post, I will continue to show how to set up multi-tenant database access to the tenant databases using Spring JPA and Hibernate. Also how Spring Security is tied into all of this.

Multi-Tenancy data access and tenant database selection

In multi-tenant setup,  Hibernate requires a datasource to be fed so that it can establish a connection/session with the database defined by the datasource.

Data access

For a multi-tenancy setup, Hibernate provides two facilities -



Tenant identification and data source selection

In part 1 of the blog I had written that the tenant identifier is provided by the end user via the login form. The application code needs to parse this tenant field in the Spring Security layer and store it for tenant id resolution.

Spring provides a UsernamePasswordAuthenticationFilter which is extended by the example application to retrieve the tenant value from the login form. This tenant value is stored in a ThreadLocal variable so that it can be used by the CurrentTenantIdentifierResolver implementation.

Understanding the code used in the application

I will explain the significant pieces of code in this part of the blog post so that you can follow how it is all related and how it all works.

File: MultitenancyProperties.java

As shown in part 1, the application.yml defines the data sources for the tenant databases. These should be available to the application code. MultitenancyProperties class has the @ConfigurationProperties annotation and is asked to parse the properties under the multitenancy.app node from application.yml file.

package com.example.multitenancy;

import java.util.List;

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

/**
 * This class reads the <code>multitenancy.mtapp</code> node from
 * <code>application.yml</code> file and populates a list of
 * {@link org.springframework.boot.autoconfigure.jdbc.DataSourceProperties}
 * objects, with each instance containing the data source details about the
 * database like url, username, password etc
 * 
 * @author Sunit Katkar
 * @version 1.0 
 * @since 1.0 (April 2018)
 */
@Configuration
@ConfigurationProperties("multitenancy.mtapp")
public class MultitenancyProperties {

    private List<DataSourceProperties> dataSourcesProps;

    public List<DataSourceProperties> getDataSources() {
        return this.dataSourcesProps;
    }

    public void setDataSources(List<DataSourceProperties> dataSourcesProps) {
        this.dataSourcesProps = dataSourcesProps;
    }

    public static class DataSourceProperties 
        extends org.springframework.boot.autoconfigure.jdbc.DataSourceProperties {

        private String tenantId;

        public String getTenantId() {
            return tenantId;
        }

        public void setTenantId(String tenantId) {
            this.tenantId = tenantId;
        }
    }
}


File: MultiTenancyJpaConfiguration.java

Hibernate needs to be fed with the datasource that it should connect to. MultiTenancyJpaConfiguration reads the data sources from MultitenancyProperties and creates a map of the data sources. This map is then used for selecting the required tenant database based on the tenant identifier and Hibernate establishes a connection with that database.


package com.example.multitenancy;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;

import org.hibernate.MultiTenancyStrategy;
import org.hibernate.cfg.Environment;
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import com.example.model.Employee;
import com.example.multitenancy.MultitenancyProperties.DataSourceProperties;

/**
 * This class defines the data sources to be used for accessing the different
 * databases (one database per tenant). It generates the Hibernate session and
 * entity bean for database access via Spring JPA as well as the Transaction
 * manager to be used.
 * 
 * @author Sunit Katkar
 * @version 1.0
 * @since 1.0 (April 2018)
 */
@Configuration
@EnableConfigurationProperties({ MultitenancyProperties.class, JpaProperties.class })
@EnableTransactionManagement
public class MultiTenancyJpaConfiguration {

    @Autowired
    private JpaProperties jpaProperties;

    @Autowired
    private MultitenancyProperties multitenancyProperties;

    /**
     * Builds a map of all data sources defined the application.yml file
     * 
     * @return
     */
    @Primary
    @Bean(name = "dataSourcesMtApp")
    public Map<String, DataSource> dataSourcesMtApp() {
        Map<String, DataSource> result = new HashMap<>();
        for (DataSourceProperties dsProperties : this.multitenancyProperties.getDataSources()) {
            
            DataSourceBuilder factory = DataSourceBuilder.create()
                    .url(dsProperties.getUrl())
                    .username(dsProperties.getUsername())
                    .password(dsProperties.getPassword())
                    .driverClassName(dsProperties.getDriverClassName());
            
            result.put(dsProperties.getTenantId(), factory.build());
        }
        return result;
    }

    /**
     * Autowires the data sources so that they can be used by the Spring JPA to
     * access the database
     * 
     * @return
     */
    @Bean
    public MultiTenantConnectionProvider multiTenantConnectionProvider() {
        // Autowires dataSourcesMtApp
        return new DataSourceBasedMultiTenantConnectionProviderImpl();
    }

    /**
     * Since this is a multi-tenant application, Hibernate requires that the current
     * tenant identifier is resolved for use with
     * {@link org.hibernate.context.spi.CurrentSessionContext} and
     * {@link org.hibernate.SessionFactory#getCurrentSession()}
     * 
     * @return
     */
    @Bean
    public CurrentTenantIdentifierResolver currentTenantIdentifierResolver() {
        return new CurrentTenantIdentifierResolverImpl();
    }

    /**
     * org.springframework.beans.factory.FactoryBean that creates a JPA
     * {@link javax.persistence.EntityManagerFactory} according to JPA's standard
     * container bootstrap contract. This is the most powerful way to set up a
     * shared JPA EntityManagerFactory in a Spring application context; the
     * EntityManagerFactory can then be passed to JPA-based DAOs via dependency
     * injection. Note that switching to a JNDI lookup or to a
     * {@link org.springframework.orm.jpa.LocalEntityManagerFactoryBean} definition
     * is just a matter of configuration!
     * 
     * @param multiTenantConnectionProvider
     * @param currentTenantIdentifierResolver
     * @return
     */
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(
            MultiTenantConnectionProvider multiTenantConnectionProvider,
            CurrentTenantIdentifierResolver currentTenantIdentifierResolver) {

        Map<String, Object> hibernateProps = new LinkedHashMap<>();
        hibernateProps.putAll(this.jpaProperties.getProperties());
        hibernateProps.put(Environment.MULTI_TENANT, MultiTenancyStrategy.DATABASE);
        hibernateProps.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider);
        hibernateProps.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, currentTenantIdentifierResolver);

        // No dataSource is set to resulting entityManagerFactoryBean
        LocalContainerEntityManagerFactoryBean result = new LocalContainerEntityManagerFactoryBean();
        result.setPackagesToScan(new String[] { Employee.class.getPackage().getName() });
        result.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        result.setJpaPropertyMap(hibernateProps);

        return result;
    }

    /**
     * Interface used to interact with the entity manager factory for the
     * persistence unit.
     * 
     * @param entityManagerFactoryBean
     * @return
     */
    @Bean
    public EntityManagerFactory entityManagerFactory(LocalContainerEntityManagerFactoryBean 
            entityManagerFactoryBean) {
        return entityManagerFactoryBean.getObject();
    }

    /**
     * Creates a new
     * {@link org.springframework.orm.jpa.JpaTransactionManager#JpaTransactionManager(EntityManagerFactory emf)}
     * instance.
     * 
     * {@link org.springframework.transaction.PlatformTransactionManager} is the
     * central interface in Spring's transaction infrastructure. Applications can
     * use this directly, but it is not primarily meant as API: Typically,
     * applications will work with either TransactionTemplate or declarative
     * transaction demarcation through AOP.
     * 
     * @param entityManagerFactory
     * @return
     */
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory);
    }
}

File: DataSourceBasedMultiTenantConnectionProviderImpl.java

Hibernate provides AbstractDataSourceBasedMultiTenantConnectionProviderImpl which provides support for connections in a multi-tenant setup like the example application.

DataSourceBasedMultiTenantConnectionProviderImpl extends the abstract class and implements the selectDataSource() and selectAnyDataSource() methods.  This class basically does the job of selecting the correct database based on the tenant id found by the tenant id resolver, which I will show next.


package com.example.multitenancy;

import java.util.Map;

import javax.sql.DataSource;

import org.hibernate.engine.jdbc.connections.spi.AbstractDataSourceBasedMultiTenantConnectionProviderImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * This class does the job of selecting the correct database based on the tenant
 * id found by the {@link CurrentTenantIdentifierResolverImpl}
 * 
 * @author Sunit Katkar
 * @version 1.0
 * @since 1.0 (April 2018)
 */
@Component
public class DataSourceBasedMultiTenantConnectionProviderImpl
        extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl {

    private static final long serialVersionUID = 1L;

    @Autowired
    private Map<String, DataSource> dataSourcesMtApp;

    /*
     * (non-Javadoc)
     * 
     * @see org.hibernate.engine.jdbc.connections.spi.
     * AbstractDataSourceBasedMultiTenantConnectionProviderImpl#selectAnyDataSource(
     * )
     */
    @Override
    protected DataSource selectAnyDataSource() {
        return this.dataSourcesMtApp.values().iterator().next();
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.hibernate.engine.jdbc.connections.spi.
     * AbstractDataSourceBasedMultiTenantConnectionProviderImpl#selectDataSource(
     * java.lang.String)
     */
    @Override
    protected DataSource selectDataSource(String tenantIdentifier) {
        return this.dataSourcesMtApp.get(tenantIdentifier);
    }
}



File:  CurrentTenantIdentifierResolverImpl.java

This class does the job of identifying the tenant. In the example application the tenant id is stored in a ThreadLocal variable which is accessed in this class and provided to Hibernate


package com.example.multitenancy;

import org.apache.commons.lang3.StringUtils;
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
import org.springframework.stereotype.Component;

import com.example.util.TenantContextHolder;

/**
 * Hibernate needs to know which database to use i.e. which tenant to connect
 * to. This class provides a mechanism to provide the correct datasource at run
 * time.
 * 
 * @see {@link com.example.util.TenantContextHolder}
 * @see {@link com.example.security.CustomAuthenticationFilter}
 * 
 * @author Sunit Katkar
 * @version 1.0
 * @since 1.0 (April 2018)
 */
@Component
public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver {

    private static final String DEFAULT_TENANT_ID = "tenant_1";

    /*
     * (non-Javadoc)
     * 
     * @see org.hibernate.context.spi.CurrentTenantIdentifierResolver#
     * resolveCurrentTenantIdentifier()
     */
    @Override
    public String resolveCurrentTenantIdentifier() {
        // The tenant is stored in a ThreadLocal before the end user's login information
        // is submitted for spring security authentication mechanism. Refer to
        // CustomAuthenticationFilter
        String tenant = TenantContextHolder.getTenant();
        return StringUtils.isNotBlank(tenant) ? tenant : DEFAULT_TENANT_ID;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.hibernate.context.spi.CurrentTenantIdentifierResolver#
     * validateExistingCurrentSessions()
     */
    @Override
    public boolean validateExistingCurrentSessions() {
        return true;
    }

}


Please note the yellow highlighted line in the code above which retrieves the tenant identifier. I will explain how it was stored and retrieved in the next file description.

File: TenantContextHolder.java

When Spring Security parses the login form which has the extra 'tenant' field, the field value is important. This value is the tenant identifier which will Hibernate which data source to use to connect to the desired tenant database.

ThreadLocal is a good solution to storing this tenant identifier. Please refer to the javadocs for a detailed explanation and some example code to understand the concept if you are new to it.

The following diagram shows how the tenant id is retrieved from the login form and stored in a ThreadLocal variable and finally retrieved from this variable to identify the tenant database.

ThreadLocal storage to store and retrieve tenant identifier
ThreadLocal storage to store and retrieve tenant identifier


The example application uses the following code for a ThreadLocal variable


package com.example.util;


/**
 * When the end user submits the login form, the tenant id is required to
 * determine which database to connect to. This needs to be captured in the
 * spring security authentication mechanism, specifically in the
 * {@link UsernamePasswordAuthenticationFilter} implemented by
 * {@link CustomAuthenticationFilter}. This tenant id is then required by the
 * {@link CurrentTenantIdentifierResolver} implemeted by the
 * {@link CurrentTenantIdentifierResolverImpl}
 * 
 * <br/>
 * <br/>
 * <b>Explanation:</b> Thread Local can be considered as a scope of access, like
 * a request scope or session scope. It’s a thread scope. You can set any object
 * in Thread Local and this object will be global and local to the specific
 * thread which is accessing this object. Global and local at the same time? :
 * 
 * <ul>
 * <li>Values stored in Thread Local are global to the thread, meaning that they
 * can be accessed from anywhere inside that thread. If a thread calls methods
 * from several classes, then all the methods can see the Thread Local variable
 * set by other methods (because they are executing in same thread). The value
 * need not be passed explicitly. It’s like how you use global variables.</li>
 * <li>Values stored in Thread Local are local to the thread, meaning that each
 * thread will have it’s own Thread Local variable. One thread can not
 * access/modify other thread’s Thread Local variables.</li>
 * </ul>
 * 
 * @see https://dzone.com/articles/painless-introduction-javas-threadlocal-storage
 * @author Sunit Katkar
 * @version 1.0
 * @since 1.0 (April 2018)
 */
public class TenantContextHolder {

    private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();

    public static void setTenantId(String tenant) {
        CONTEXT.set(tenant);
    }

    public static String getTenant() {
        return CONTEXT.get();
    }

    public static void clear() {
        CONTEXT.remove();
    }
}

These are the significant files for the application. The application described in this blog post is checked into GitHub where you can see other code not explained here.


Modifying the Spring Boot main application class

Typically, the Spring Boot main application class is nothing special as it has a main method which starts the application. But there is a lot of Spring Boot magic going on behind the scenes. A lot of defaults are assumed, a lot of configurations are done automatically by detecting artifacts in the classpath, application properties file and pom.xml analysis.

If you are familiar with Spring Boot and JPA or even JDBC, then you know that any mention of spring jpa properties in the application properties file will cause Spring Boot to automatically configure the datasource. In this web application, we dont want Spring Boot to configure the data sources because we want to define multiple data sources and there is no default data source available when the application starts. Only when a user logs in with tenant information, the correct datasource needs to be used to connect to the tenant database.

So, using a simple exclude attribute to exclude the DataSourceAutoConfiguration class in the main @SpringBootApplication annotation, we can tell Spring Boot to leave data source configuration aside.

Since we have asked Spring Boot to not do some auto configuration related to data sources, it is a good idea to specifically ask Spring Boot to enable JPA Repositories using the @EnableJpaRepositories annotation.


package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

/**
 * @author Sunit Katkar
 * @version 1.0
 * @since 1.0 (April 2018)
 */
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
@EnableJpaRepositories("com.example.repository")
public class MultitenancyMySqlApplication {

    public static void main(String[] args) {
        SpringApplication.run(MultitenancyMySqlApplication.class, args);
    }
}

Sample SQL to create a tenant database on MySQL 

Had this example been set up as a single database application and the property spring.jpa.hibernate.ddl-auto set to create or update, then the following three tables would be generated automatically by Hibernate based on the JPA entity definitions. Read more about it here.


Table: User


CREATE TABLE `user` (
 `user_id` INT(11) NOT NULL,
 `password` VARCHAR(255) NOT NULL,
 `username` VARCHAR(255) NOT NULL,
 `active` BIT(1) NULL DEFAULT NULL,
 `tenant` VARCHAR(255) NULL DEFAULT NULL,
 PRIMARY KEY (`user_id`)
)
COLLATE='latin1_swedish_ci'
ENGINE=MyISAM
;

Table: Role


CREATE TABLE `role` (
 `role_id` INT(11) NOT NULL,
 `role` VARCHAR(255) NULL DEFAULT NULL,
 PRIMARY KEY (`role_id`)
)
COLLATE='latin1_swedish_ci'
ENGINE=MyISAM
;


Table: User_Role

This is the mapping table where users and roles are recordeed

CREATE TABLE `user_role` (
 `user_id` INT(11) NOT NULL,
 `role_id` INT(11) NOT NULL,
 PRIMARY KEY (`user_id`, `role_id`),
 INDEX `FKa68196081fvovjhkek5m97n3y` (`role_id`)
)
COLLATE='latin1_swedish_ci'
ENGINE=MyISAM
;

Resources 

The complete source code is checked into GitHub. Its a standard Maven project which you can import into your IDE. 


Conclusion

I have explained multi-tenancy and shown you with example code how you can build a SaaS style 'tenant per database' multi-tenant web application with Spring Boot; and how to secure it with Spring Security. I will write about adapting this code to use Microsoft SQL Server in a future blog post as there were some challenges and how I had to change some code to overcome them.

Please do let me know your feedback by adding it in the comments section below. I am sure I will learn something new.

Happy coding :)

24 comments:

Unknown said...

Thanks for your idea and source code. If there is a way to dynamic create datebase when adding a new register tenant?

Sunit said...

@Xu


For simplicity, you will need to create a new tenant database and necessary tables inside it, define the connection details in application.yml and restart server.

For a dynamic behavior, you could write a SQL script which creates this new database for the new tenant in your system. I would not fire this script from your application but would leave this as a job for the DevOps team or a separate task.

You can load this new database (tenant) in your application without restarting the server. For this you will need to enhance my code example with a dynamic datasource instead of the one where all datasources are defined in the application.yml file.

When I get time, I will update my blog with how to dynamically reload the tenants without server shut down. I am planning to use this technique at work.

Thank you for visiting my blog.

Unknown said...

@Sunit Thanks for your reply and advice. Hope to ready your new post:)

Sunit said...

@Xu Niu

I have posted a blog post which answers your question.
https://sunitkatkar.blogspot.com/2018/05/adding-tenants-without-application.html

Unknown said...

Good Morning,

I live in Brazil.

Is it difficult to adapt this code for Spring MVC?

Thank you.

Sunit said...

@Unknown
I am sure you can adapt this application to a regular Spring MVC application. If you look at the app, its basically all about configuring a Master database connection and then a tenant database connection. I am sure you can do this in Spring MVC. Sorry, that I am busy with work and cannot help in creating a regular Spring MVC version of this app.

Anonymous said...

Hi Sunit,

I am new to this springboot and want to implement this multitanents. This is how my user table looks like.

mysql> select * from user;
+---------+----------+----------+--------+---------+
| user_id | password | username | active | tenant |
+---------+----------+----------+--------+---------+
| 1 | root | root | NULL | tenant1 |

How does this login works here. Because when I deploy the application I get this below message.


2018-07-23 10:35:35.857 WARN 15316 --- [nio-8080-exec-6] o.s.s.c.bcrypt.BCryptPasswordEncoder : Empty encoded password

Niket Agrawal said...

Nice post. I wanted to understand how to publish the API over web to expose it to different clients.

Sunit said...

@Niket Agarwal
Thanks for reading my blog post. Please note that my blog post is about showing how multi-tenancy works. The API in the example like getting all Users, etc can be exposed either as a regular Spring MVC response when you annotate the controller method with @Controller. Or you can use the @RestController annotation to ensure that the response is sent in the response body in a JSON format. In the regular @Controller annotated methods you can achieve a REST like JSON response by further adding the @ResponseBody annotation to the return.

As for securing your API, you can use Spring Security like I have used. To make your API purely stateless you might find using JWT tokens much more convenient than just Spring Security.

Sunit said...

@Unknown
My blog post is about adding multi-tenancy to a Spring secured web application.

Since you are new to Spring and Spring Boot, I recommend you first read up on Spring Security and then follow an example of HTML Form based Spring Security examples. Take a look at this official Spring documentation example: https://docs.spring.io/spring-security/site/docs/current/guides/html5/form-javaconfig.html

There you will learn how to use a password encryption scheme like BCrypt to securely hash your password in one way. That will help you understand how a user login works for a Spring secured application.

Once you understand that, you will realize that you need to create a username and an encrypted/hashed password for this user and store it in the database so that user login works.

Unknown said...

Very Good explanation.
It clarified many features.
My problem now is that I am using spring security oauth, and I wonder Is it possible to use this approach or not

Sunit said...

@amir choubani,

Spring Security is ultimately a session based token exchange security. In simplest terms, a http session is created with a token on the server side and a token sent in the http communication (cookies) with the browser. So you understand how it works.

Now OAuth2 is not exactly the same as session based security. Here, there is a token which is passed in the headers (typically as a Bearer token). Now in spring session security, you have various schemes like comparing the token with the user retrieved from a database table, etc. In case of OAuth2, the basic idea is to let the token be verified by the authorization server.

So to answer your question, yes you can do multi-tenancy using OAuth2 approach. You need to build the mechanism where you can identify the user and then identify the user's tenant id and then allow access to the resources meant for that user. i.e. access the schema meant for the user based on the user's tenant id.

Have I done it? No. Sorry, I cannot help you here as I am working on the approach I published in my follow up post here for the project at my work place.

Unknown said...

Hi Sunit,
Is Multi tanancy possible for tables in a databse and is it possible to fetch result with sql queries which included joins.

Sunit said...

@Unknown
Please understand that multi-tenancy can be implemented in any of the following ways
1) Same database with discriminator column in each table to distinguish tenant
2) Same database with separate schema for each tenant
3) Separate databases with one database for each tenant.

I have shown the 3rd approach. So you have to decide on which approach you want for multi-tenancy.

Felipe Marques Batista said...

Hello, if you need an example, I made it available in my repository in github:
https://github.com/felipefmb/springboot_multitenancy

Feel free to contribute! ;)

Luisina said...

Hello, my name is Luisina. Very interesting your blog. Would you like to know if you could create a blog showing the option to dynamically create the database? Thanks.

kartik said...

hey sunit katkar please provide your contact information to me its very urgent its the matter of my life and death please provide info at kartiktamta@gmail.com

Sunit said...

@kartik
What is it that its your life and death situation? I have posted three blog posts regarding multi-tenancy. Please read through each of them and more importantly all the comments from readers and responses. I am sure you will find the answer you need.

Atieq ur Rehman said...

Great article, helped alot to understand and setup multi tenancy model with multiple databases.

Irfan Nasim said...

Dear @Sunit

loved your code and fully understand, but I have a query as to how do I setup OAuth 2.0 along with this architecture that you have shared. i want to get token base authentication. user and roles are stored in master database and i want to shift the database on that token.

please help.

Unknown said...

Hi Sunit, very nice article. How about handling password rotation on these tenant databases. did you get a chance to explore that? Appreciate any guidance on this.

Sunit said...

I never explored password rotation for the tenant databases as the purpose of this article was to explore multi-tenancy. I would not tie password maintenance with this multitenancy project. It is a separate DEVOPS task to manage password rotation in sync with the development team.

zarahvalera said...

Casinos Near Casino, Laughlin, NV - MapYRO
Find Casinos Near Casino, Laughlin, 아산 출장안마 NV, 안양 출장안마 United States and reviews of 사천 출장마사지 casinos and places to stay closest 의정부 출장안마 to 시흥 출장안마 Casino, Las Vegas, NV.

Unknown said...

Hi Sumit, can you provide a course for this