Spring security – multitenant


In this post I will demonstrate how to configure Spring security with a per tenant user database.

In order to make it work we will need:

  • Custom UserDetailsService
  • Rewrite the UserPasswordEncoderListener
  • Initialize per tenant beans

First lets create the application and add security.

# Open a terminal, cd into a desired base directory
grails create-app ssc-multitenant-test1 --profile web

Import the new project into IDEA.

Edit build.gradle:

dependencies {
.. add ssc
compile 'org.grails.plugins:spring-security-core:4.0.0'
..
}

Initialize spring security core from the terminal:

cd ssc-multitenant-test1
grails s2-quickstart no.prpr.security User Role

Make all domains MultiTenant, by adding MultiTenant<domain>. Below you’ll find the updated first line of the domain classes.

class Role implements Serializable, MultiTenant<Role> {
class User implements Serializable, MultiTenant<User> {
class UserRole implements Serializable, MultiTenant<UserRole> {

Configure application for multitenancy

This application will be set up with one datasource for one tenant. No default datasource will be used.

Edit application.yml and add multiTenancy. For this purpose we will use the SystemPropertyTenantResolver:

grails:
    gorm:
        multiTenancy:
            mode: DATABASE
            tenantResolverClass: org.grails.datastore.mapping.multitenancy.resolvers.SystemPropertyTenantResolver

Configure datasources. Remove all the default datasource and replace with:

dataSources:
    abc:
        dbCreate: create-drop
        driverClassName: org.h2.Driver
        username: sa
        password: ''
        url: jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE

Create a custom UserDetailsService

Create src/main/groovy/no.prpr.security.PerTenantUserDetailsService.groovy

package no.prpr.security

import grails.gorm.multitenancy.CurrentTenant
import grails.gorm.transactions.Transactional
import grails.plugin.springsecurity.SpringSecurityUtils
import grails.plugin.springsecurity.userdetails.GrailsUser
import grails.plugin.springsecurity.userdetails.GrailsUserDetailsService
import grails.plugin.springsecurity.userdetails.NoStackUsernameNotFoundException
import groovy.util.logging.Slf4j
import org.springframework.dao.DataAccessException
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UsernameNotFoundException

@Slf4j
@CurrentTenant
class PerTenantUserDetailsService implements GrailsUserDetailsService {

    static final List NO_ROLES = [new SimpleGrantedAuthority(SpringSecurityUtils.NO_ROLE)]

    @Override
    UserDetails loadUserByUsername(String username, boolean loadRoles) throws UsernameNotFoundException, DataAccessException {
        loadUserByUsername(username)
    }

    @Transactional(readOnly = true, noRollbackFor = [IllegalArgumentException, UsernameNotFoundException])
    @Override
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.debug("username: ${username}")
        User user = User.findByUsername(username)
        if (!user) throw new NoStackUsernameNotFoundException()
        def roles = user.getAuthorities()
        def authorities = roles.collect {
            new SimpleGrantedAuthority(it.authority)
        }
        new GrailsUser(user.username, user.password, user.enabled,
                !user.accountExpired, !user.passwordExpired,
                !user.accountLocked, authorities ?: NO_ROLES, user.id)
    }

}

Update resources.groovy – remove the UserPasswordEncoder and add the PerTenantUserDetailsService. It will be added during BootStrap.

import no.prpr.security.PerTenantUserDetailsService

// Place your Spring DSL code here
beans = {
    userDetailsService(PerTenantUserDetailsService)
}

Rewrite src/main/groovy/no.prpr.security/UserPasswordEncoderListener.groovy:

package no.prpr.security

import grails.core.GrailsApplication
import grails.plugin.springsecurity.SpringSecurityService
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import org.grails.datastore.mapping.core.Datastore
import org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent
import org.grails.datastore.mapping.engine.event.AbstractPersistenceEventListener
import org.grails.datastore.mapping.engine.event.PreInsertEvent
import org.grails.datastore.mapping.engine.event.PreUpdateEvent
import org.springframework.context.ApplicationEvent

@CompileStatic
@Slf4j
class UserPasswordEncoderListener extends AbstractPersistenceEventListener {

    SpringSecurityService springSecurityService

    protected UserPasswordEncoderListener(Datastore datastore) {
        super(datastore)
    }

    private void encodePasswordForEvent(AbstractPersistenceEvent event) {
        log.debug('encodePasswordForEvent')
        if (event.entityObject instanceof User) {
            User u = event.entityObject as User
            if (u.password && ((event instanceof  PreInsertEvent) || (event instanceof PreUpdateEvent && u.isDirty('password')))) {
                event.getEntityAccess().setProperty('password', encodePassword(u.password))
            }
        }
    }

    private String encodePassword(String password) {
        manuallyWireServices()
        String result = springSecurityService?.passwordEncoder ? springSecurityService.encodePassword(password) : password
        log.debug("encodePassword: ${password} -> ${result}")
        result
    }

    @Override
    protected void onPersistenceEvent(AbstractPersistenceEvent event) {
        log.debug("Source: ${event.source}. Datastore: ${this.datastore}. Event: ${event}")
        encodePasswordForEvent(event)
    }

    @Override
    boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
        boolean supportsEvent = eventType.isAssignableFrom(PreInsertEvent) ||
                eventType.isAssignableFrom(PreUpdateEvent)
        log.debug("${eventType}: ${supportsEvent}")
        return supportsEvent
    }

    void manuallyWireServices() {
        if (springSecurityService) {
            return
        }
        GrailsApplication grailsApplication = grails.util.Holders.grailsApplication
        if (!springSecurityService) {
            springSecurityService = (SpringSecurityService) grailsApplication.mainContext.getBean('springSecurityService')
            log.info "getBean authUserService: ${springSecurityService}"
        }
    }

}

Make the log more verbose, edit logback.groovy:

// at the end - replace root(ERROR with root(INFO
root(INFO, ['STDOUT'])
// add more logging for our own code.
logger('no.prpr', DEBUG, ['STDOUT'], false)

Add initialization of listener, users and roles to BootStrap:

package ssc.multitenant.test1

import grails.core.GrailsApplication
import groovy.util.logging.Slf4j
import no.prpr.security.Role
import no.prpr.security.User
import no.prpr.security.UserPasswordEncoderListener
import no.prpr.security.UserRole
import org.grails.datastore.mapping.multitenancy.resolvers.SystemPropertyTenantResolver
import org.grails.orm.hibernate.HibernateDatastore

import static grails.gorm.multitenancy.Tenants.withId

@Slf4j
class BootStrap {

    GrailsApplication grailsApplication
    HibernateDatastore hibernateDatastore

    def init = { servletContext ->
        def ctx = grailsApplication.mainContext
        ['abc'].each { String tenantName ->
            // create a listener for each tenant
            HibernateDatastore ds = hibernateDatastore.getDatastoreForConnection(tenantName)
            UserPasswordEncoderListener listener = new UserPasswordEncoderListener(ds)
            ctx.addApplicationListener(listener)
            // add users and roles for each tenant
            withId(tenantName) {
                def adminRole = Role.findByAuthority('ROLE_ADMIN')
                if (!adminRole) {
                    log.info("Adding role ROLE_ADMIN")
                    Role.withNewTransaction {
                        adminRole = new Role(authority: 'ROLE_ADMIN')
                        adminRole.save()
                    }
                }
                def adminUser = User.findByUsername('admin')
                if (!adminUser) {
                    log.info("Adding user admin")
                    User.withNewTransaction {
                        adminUser = new User(username: 'admin', password: 'password')
                        adminUser.save()
                    }
                }
                def adminUserRole = UserRole.get(adminUser.id, adminRole.id)
                if (!adminUserRole) {
                    log.info("adding userrole")
                    UserRole.withNewTransaction {
                        adminUserRole = new UserRole(user: adminUser, role: adminRole)
                        adminUserRole.save()
                    }
                }
            }
        }
        System.setProperty(SystemPropertyTenantResolver.PROPERTY_NAME, 'abc')
    }
    def destroy = {
    }
}

You should now be able to log in using the ‘abc’ tenant datasource.

Add User UI with:

grails generate-all no.prpr.security.User

Make the LogoutController work by appending to application.groovy:

....
grails.plugin.springsecurity.logout.postOnly = false

And then add @CurrentTenant and @Secured on UserController:

@Secured('ROLE_ADMIN')
@CurrentTenant
class UserController {
...

And modify UserService for multitenancy:

@CurrentTenant
@Service(User)
interface UserService {

Here’s the application: https://johnny.prpr.no/wp-content/uploads/2020/05/ssc-multitenant-test1.zip