bouncer icon indicating copy to clipboard operation
bouncer copied to clipboard

Multi-database tenancy

Open norotico opened this issue 3 years ago • 5 comments

I have a multi-database multi-tenancy app where the auth User model uses the system database while the Bouncer models use the tenant databases.

Tenant DB's have a TenantUser model which belongsTo User in the system database.

By doing Bouncer::useUserModel(TenantUser::class) everything works fine, except for checking abilities with the Bouncer façade or directly from Laravel's gate, because there's nothing telling Bouncer or the Gate that they need to use the associated TenantUser instead of auth()->user(). I can of course do $tenantUser->can($ability), though.

How would I go about telling Bouncer (and Laravel's Gate) to authorize the associated TenantUser instead of the global User?

norotico avatar Mar 14 '22 10:03 norotico

Thanks, but yes my custom Bouncer models do use tenant connection.

The issue is only the User model -- when checking authorization with Bouncer::can($ability), Bouncer resolves the User using Laravel's auth system, which in my app is the global User (system connection) model. But the model that should be checked in this scenario is the TenantUser model (which belongs to User and is not a Laravel auth User).

My TenantUser model is properly setup, using the Authorizable trait and doing Bouncer::useUserModel(TenantUser::class). But that's not enough in my use case since my TenantUser is not the auth()->user().

norotico avatar Mar 14 '22 18:03 norotico

I set the bouncer user model on the AppServiceProvider. I'm not using a package, but DB connection is not really the issue.

When calling Bouncer::can($ability), Bouncer defers to the gate:

    public function can($ability, $arguments = [])
    {
        return $this->gate()->allows($ability, $arguments);
    }

Tracing this leads you to:

    $this->userResolver = function ($guard = null) {
        return $this->guard($guard)->user();
    };

So the userResolver will always be the auth User. There's no way Bouncer would know that it needs to check abilities on the related TenantUser model without me telling Bouncer to do it, somehow. What I'm looking for is the cleanest way to do that without messing up the auth system.

For instance, this is what Bouncer/Gate would have to do in my use case after resolving the auth User:

TenantUser::whereUserId($this->userResolver()->id)->first()

norotico avatar Mar 14 '22 20:03 norotico

As you've already discovered, this is not a question unique to Bouncer. It's a general question about Laravel's Gate.

You can register your own Gate in the Container, overriding the default one:

Container::getInstance()->singleton(GateContract::class, function ($container) {
    return new Gate($container, userResolver: function () use ($app) {
        $user = ; // Resolve the user however you want...

        return $user;
    });
});

Bouncer uses whichever Gate has been registered in the container, so if you've wired it up correctly, Bouncer will use your Gate.

JosephSilber avatar Mar 16 '22 17:03 JosephSilber

Thanks, @JosephSilber

Do I still need to Bouncer::useUserModel(...) if I register my own Gate?

norotico avatar Mar 16 '22 17:03 norotico

If you register your own gate before Bouncer is instantiated, then no.

JosephSilber avatar Mar 21 '22 04:03 JosephSilber