GithubHelp home page GithubHelp logo

Comments (61)

jonagoldman avatar jonagoldman commented on May 18, 2024 8

Ok so I found a solution that works for my specific use case.

  1. Set the $connection property of the User model to the central database connection:
class User extends Authenticatable
{
    protected $connection = 'mysql';
}
  1. Make cookies shared in subdomains:
// config/session.php
'domain' => env('SESSION_DOMAIN', null),
// .env
SESSION_DOMAIN=.mysite.test // notice the "." before the domain

Now Auth::user() will return the user in all subdomains.

!!!: Of course we now must check if the user have permissions to access the tenant!

This is very useful if users have access to multiple tenants as they can switch tenants without the need to login again.

from tenancy.

viezel avatar viezel commented on May 18, 2024 5

@stancl I can maybe add to this thread that we have run a SaaS Platform for about 6 years now with multi db setup. I would say that im quite happy with the global_user_id relation as it serves as a great unique identifier in various use cases:

  • Clear cache on user level on all tenants
  • When you build out your SaaS platform you might introduce various microservices that handles user related processes
  • great for logging and exception handling to be able to locale which user it is
  • SSO, LDAP etc

I would not live without it. Just my two cent on this topic.

from tenancy.

stancl avatar stancl commented on May 18, 2024 4

I haven't tested this yet (will likely require a few tweaks) but I've come up with what I think is a good base design for this:

Jobs/SyncResource.php

class SyncResource implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /** @var Model */
    protected $syncedModel;

    /** The tenant from whom the data comes from. */
    protected $except;

    public function __construct(Model $model, $except)
    {
        $this->syncedModel = $model;
        $this->except = $except;
    }

    public function handle()
    {
        tenancy()->all()->filter(function (Tenant $tenant) {
            return $tenant->id !== $this->except;
        })->each->run(function () {
            $globalIdentifier = $this->syncedModel->getGlobalIdentifier();
            $model = $this->syncedModel->newQuery()->where($globalIdentifier, $this->syncedModel->$globalIdentifier)->first();
            $model->update($this->syncedModel->attributesToArray());
        });
    }
}

SharedBetweenTenants.php

trait SharedBetweenTenants
{
    public static function bootSharedBetweenTenants()
    {
        static::saved([static::class, 'syncResourceBetweenTenants']);
    }

    protected function syncResourceBetweenTenants(Model $model)
    {
        if (config('tenancy.queue_syncing')) {
            SyncResource::dispatch($model, tenant('id'));
        } else {
            SyncResource::dispatchNow($model, tenant('id'));
        }
    }

    abstract public function getGlobalIdentifier(): string;
    abstract public function getSyncedColumns(): array;
}

User.php

class User extends Authenticatable
{
    use Notifiable, SharedBetweenTenants;

    // ...

    public function getGlobalIdentifier(): string
    {
        return 'global_user_id';
    }

    public function getSyncedColumns(): array
    {
        return ['name', 'email', 'password'];
    }

I probably need to make this store the user in the central database as well, though. Or that could be configurable.

from tenancy.

jonagoldman avatar jonagoldman commented on May 18, 2024 3

@rrpadilla Path identification might be a nice addition, but path identification and authentication are two separate things. Each application should be able to choose how implement each, and not be forced to use a specific authentication method because of the identification method.

Also, If you use path identification then shared sessions are irrelevant because all tenants are on the same domain, so they automatically share sessions.

For my application I do want to share sessions between subdomains, so I can have a single users table in my central database so the user can login once and access multiple domains with the same credentials without needing to login again for each domain.

Other applications might want only admins to login to the central database and have separate users tables for each domain. Again, it depends on the requirements of each SaaS.

from tenancy.

viezel avatar viezel commented on May 18, 2024 3

To show a simple example of a user that is part of 2 tenants.

Central:
- user_id: 1
- name: Foo Bar
- email: [email protected]
- password: *****
- current_tenant_id: 'UUID4'
- created_at: timestamp
- updated_at: timestamp
Tenant A:
- user_id: 22
- global_user_id: 1
- name: Foo Bar
- email: [email protected]
- password: *****
- address: xyz
- phone: 123
- role: "the boss"
Tenant B:
- user_id: 43
- global_user_id: 1
- name: Foo Bar
- email: [email protected]
- password: *****
- address: xyz
- phone: 432
- role: "something else"

from tenancy.

viezel avatar viezel commented on May 18, 2024 3

@zeidanbm you wrote as they all are going through the central db and that is the entire point here. This is not always the use case.
The tenant app is self contained. Meaning, whatever user registration/invite/bulkimport/etc flow it got, it will only know of the tenant app - not the central. This means that a user can register into the tenant and get back the user model. Secondly, a queued job if dispatched to ensure that this user is also created in central db.

The concept is two way based.

from tenancy.

stancl avatar stancl commented on May 18, 2024 3

Implemented in v3.

from tenancy.

stancl avatar stancl commented on May 18, 2024 2

This could be solved by using the DB storage driver, storing users in a table on the central database and making the session Auth guard use that DB connection. Though relationships could be problematic.

from tenancy.

jonagoldman avatar jonagoldman commented on May 18, 2024 2

@drbyte I see, so domains are for flexibility in case its needed.

@stancl I was thinking about a simpler solution (for my use case). All users are in the central table (admins and tenant specific users) and they authenticate against the central database. So I think my question is how to give permission to a specific user to a tenant.

My thinking is, a regular user logs in in the central domain (saas.com) and is redirected to tenant (tenant1.saas.com). An admin user also logs in in the central domain, but he is presented with a dashboard and all the tenants he admins, then he can also go to tenant1.saas.com if he wants.

Also possible to show the login form directly in the tenant domain but always authenticate against the central users table.

from tenancy.

viezel avatar viezel commented on May 18, 2024 2

we use Events like this:

User Controller on Tenant side:

public function update(UpdateUserRequest $request, int $id): JsonResponse
{
    // some logic here

    event(new TenantUserWasUpdatedEvent(
        tenancy()->current(),
        $user->getAttribute('global_user_id'),
        $request->get('first_name', $user->getAttribute('first_name')),
        $request->get('last_name', $user->getAttribute('last_name')),
        $request->get('email', $user->getAttribute('email')),
        $request->get('password')
    ));

    return $this->respond($this->transformer->transformOutput($user));
}

Broadcast updates out to all tenants from Central:

protected function broadcastUpdate(TenantUserWasUpdatedEvent $event, User $globalUser)
{
    $globalUser->tenants()->each($team, function () use ($event, $globalUser) {
        $user = $this->usersRepository->findByAttributes(['global_user_id' => $globalUser->getKey()]);
        $user->setAttribute('email', $event->getEmail());
        $user->setAttribute('first_name', $event->getFirstname());
        $user->setAttribute('last_name', $event->getLastname());
        if (false === empty($event->getPassword())) {
            $user->setAttribute('hashed_password', $globalUser->getAttribute('password'));
        }

        $this->usersRepository->save($user);
    });

}

from tenancy.

stancl avatar stancl commented on May 18, 2024 2

I don't use global user id and instead use a user id that is forced in the tenant user table

So in a way it's a composite primary key - consisting of the user id and the tenant id.

Laravel is not built for that very much so there might be some ugliness included in going with that solution. So for now I'll go with global ids.

from tenancy.

stancl avatar stancl commented on May 18, 2024 2

This feature comes with a ton of complexity. Hopefully I can make an official solution but I'm starting to feel like there are so many ways to implement this that I should let users do this.

For example:

  • no syncing, just central auth guard
  • optional tenant_users mapping
  • creating users in either central/tenant DB with everything syncing, including the tenant_users mapping
  • queueing the sync job, in a way that would sync all 1000 imported users at the same time instead of creating 1000 separate jobs

I'll focus on some smaller tasks now (e.g. Nova tenant CRUD, Passport integration) and do this after that. Need to think about all these edge cases.

from tenancy.

jonagoldman avatar jonagoldman commented on May 18, 2024 1

If i'm OK without having relations between users in the central database and the tenant databases, but I want relations between the users table and the tenants/domains tables, whats your suggested implementation? user-tenant or user-domain ? Is a user the same as a tenant? Can you give a basic example?

from tenancy.

jonagoldman avatar jonagoldman commented on May 18, 2024 1

@viezel is there a specific reason you need the users table in each tenant? In my case I have all users in Central and I authenticate using the central connection and for everything else I use the Tenant connection. The downside is that you can't have a user foregin key in tenant that points to central but you already have global_user_id that can't have a foreign key to central so is there a specific reason to have multiple users tables?

from tenancy.

stancl avatar stancl commented on May 18, 2024 1

I think the saving Eloquent event is ideal for this.

I hope to make the boilerplate needed for this part of the package in 2.3.0.

from tenancy.

viezel avatar viezel commented on May 18, 2024 1

@zeidanbm Remember that Sync users is a two way street.
Both an SaaS admin can update the user, as well as the user itself in one of the tenants.
Therefore we need to abstract the functionality to Actions/queuejobs/events etc that is not directly coupled to the implementation.

from tenancy.

viezel avatar viezel commented on May 18, 2024 1

Thats fine too. UUID is quite practical. But indexing and performance is not quite as good.

from tenancy.

jonagoldman avatar jonagoldman commented on May 18, 2024 1

@viezel yes, performance at this level can be improved saving the UUID as a time sorted binary using UUID_TO_BIN(). This will improve indexing speed and storage space.

@stancl yes, I use one single UUID column named id and I use that for syncing. I don't use auto increments at all, all the id columns are UUIDs.

This is only in case you need each tenant to have a separate users table. Generally I prefer to have only one users table in the central database and authenticate against that database, this way you don't have duplicated users and there is no need to sync anything or any additional logic. You already need the central database connection to check the tenants table, so while I do that I also check the users table using the same connection, so performance is the same.

from tenancy.

stancl avatar stancl commented on May 18, 2024 1

Hi @neovive. I think you're looking for this: #194 (comment)

You create the "user" as a record in the tenants table, and then create a users record with the same data in the tenant's database.

from tenancy.

viezel avatar viezel commented on May 18, 2024

Yes please. Once that is in place, then a user impersonation can by done.

from tenancy.

drbyte avatar drbyte commented on May 18, 2024

but I want relations between the users table and the tenants/domains tables

Like what? What do you wish to use that relation for?

from tenancy.

jonagoldman avatar jonagoldman commented on May 18, 2024

For example in a hotel management saas where a user can have one or more hotels.
In that case each hotel is a tenant or a domain? Or each user is a tenant and each hotel is a domain? I guess the first one is correct so why each tenant need multiple domains?

from tenancy.

stancl avatar stancl commented on May 18, 2024

I've been thinking about this feature for some time and with multi-database tenancy, the best thing I could think of was having like a sync service between tenants. You'd have the central database that holds the global information about the user:

Central:
- user_id: 1
- name: Foo
- email: [email protected]

And then tenant databases could have a more complex schema:

Tenant:
- user_id: 1
- name: Foo
- email: [email protected]
- address: xyz
- phone: 123

When you'd update this model in any database, the keys held in the central database would get synced to all databases.

This comes with a lot of complexity and is basically at the bottom of my priority list when it comes to this package's features. I'm not even sure if it's in scope of the package, or whether it should be a separate package/up to the user to implement.

The alternative to this approach is cross-DB queries, which aren't a first-class feature in Laravel, and are problematic if you use multiple hosts for your DBs (this is a common motivation for the multi-database model of tenancy).

I think the 2.x codebase might be flexible enough to let me add support for single-database tenancy, which makes shared users a non-problem.

from tenancy.

drbyte avatar drbyte commented on May 18, 2024

why each tenant need multiple domains?

If "each hotel is a tenant" (makes sense), then you would probably be thinking that they need only 1 domain, ie: myfancyhotel.com.
But in early stages of onboarding to your examplesaas.com platform you might want to let them use fancy.examplesaas.com until they're ready to "go live".
And maybe they've previously had fancylicious.com as a sister brand, and they want that brand to be reachable in the same account. In this case you'd have 3 domains tied to the same tenant: myfancyhotel.com, fancy.examplesaas.com, and fancylicious.com.

But if you have all your admin users of that tenant "inside" a database within the tenant's tables, and not in the "central" (onboarding/support/admin) examplesaas.com site, then you might be able to skip relating users to tables outside the tenant space.

from tenancy.

rrpadilla avatar rrpadilla commented on May 18, 2024

I think this is a good case only when you are using path identification because you do not want to share sessions between subdomains. Your users should access the system from:

https://yourapp.com/login

then they will be able to pick the tenant they want to access to in case they have access to more than one and get redirected to:

https://yourapp.com/app/TENANT-ID-SLUG-DOMAIN

If you are using domain identification you should have access to the tenant database only. Then you access the tenant app from:

https://subdomain.yourappcom/login

I think this feature (shared users between tenants) SHOULD NOT be added to this package though. It's a business decision you have to handle from your app.

That's why opened a ticket to Support Path Identification. See: #173

from tenancy.

stancl avatar stancl commented on May 18, 2024

Right, using the central connection on your User model solves a part of this problem, but if you need to use Eloquent relationships or make a JOIN query with some tables from within the tenant database, it won't work. Depends on how much functionality you need with the shared users.

from tenancy.

stancl avatar stancl commented on May 18, 2024

@viezel Can you elaborate on that setup? Sounds interesting.

from tenancy.

viezel avatar viezel commented on May 18, 2024

Sure. So our master dB contains a user table and an tenants_users table with the relationship. A user can belong to multiple tenants.
If a user is updated in one of the tenants it’s synced back to master db and populated down to the rest of tenants databases that this user belongs too.

Now each user has a unique global id. This means stuff like SSO in multiple tenants is possible due to this global user id.

Anything specific you want me to elaborate on?

from tenancy.

viezel avatar viezel commented on May 18, 2024

It all depends on the app you are building. A have a couple of SaaS where it’s needed due to the user can be part of different tenants and have different properties And functionality like role, permissions, job title etc..

Secondly, we use the tenant user to limit the amount of context switching, meaning it’s not very performant if you need to switch to central database each time you need the user info. Instead reusing the existing database connection to the current tenant is far better

from tenancy.

metadeck avatar metadeck commented on May 18, 2024

@viezel Thanks for the example schema. How do you implement the syncing on tenant user name updates for example?

So if I am logged into Tenant A and I update my name from Foo Bar to Foo Baz, how would that propogate through to the other tenants? TIA

from tenancy.

zeidanbm avatar zeidanbm commented on May 18, 2024

I use the Saved event defined by Laravel on models that @stancl mentioned above. So, on the controller I save to everything to central database and then pass down to tenant what is needed. I don't pass down the password as I don't really need that. I authenticate from central database and use passport on central database as well.

public function saved(User $user)
{
     // sync user down to tenant db
     tenancy()->find($user->tenant_id)->run(function ($tenant) use ($user) {
          $user = User::withoutEvents(function () use ($user) {
              $model = User::firstOrNew($user->id);
              $model->name = $user->name;
              $model->email = $user->email;
              $model->role_id = $user->role_id;
              $model->tenant_id = $user->tenant_id;
              $model->save();
          });
     });
}

from tenancy.

zeidanbm avatar zeidanbm commented on May 18, 2024

Nice and clean way to use a trait for this and becomes re-usable for other models if needed. I will probably test it out in the next few days.

But I'll adjust it as I don't use global user id and instead use a user id that is forced in the tenant user table (i.e not automatically set like auto increment = false). This way each user will have unique id across all databases not sure if it's better this way or not!

from tenancy.

stancl avatar stancl commented on May 18, 2024

Makes sense. I'll think about both approaches.

from tenancy.

stancl avatar stancl commented on May 18, 2024

@viezel How do you assign global user ids when creating users? You need to ensure the uniqueness of those keys.

from tenancy.

viezel avatar viezel commented on May 18, 2024

@stancl the global_user_id is just the id incremental field in the users table of the central database. So mySQL or other databases supporting auto_increment will make the ID unique.

from tenancy.

stancl avatar stancl commented on May 18, 2024

So you always create users in the central DB and then let them sync into tenant DBs?

from tenancy.

jonagoldman avatar jonagoldman commented on May 18, 2024

I use UUIDs, so I can create the user with the same id in both databases, no need for a global_user_id column. If you use MySQL there is even native support for creating time ordered UUIDs. Also supported in Ramsey/UUID package.

from tenancy.

stancl avatar stancl commented on May 18, 2024

@jonagoldman So you use id as the global identifier and uuid as the "local" id? Or do you use only a single uuid column and sync that with all DBs?

from tenancy.

stancl avatar stancl commented on May 18, 2024

It would definitely be better to store the users in the central database, but that would not let you have relationships to tables in the tenant databases (technically you could do that, since most RDBMS support cross-database queries, but it's quite hacky, doesn't let you use foreign keys, and isn't natively supported by Eloquent).

So even though it takes much more space, syncing the users between databases is the only way to have relationships in a nice way.

The uuid seems like a great solution. Time-based, to prevent race conditions (hopefully that's enough and multiple requests coming in the same second won't be an issue, I'll have to check that), and optionally stored as binary for faster indexing and space usage.

And then an optional tenant_users table (if you don't want to sync all users with all tenants) that says to which tenants' databases the users should be synced.

from tenancy.

jonagoldman avatar jonagoldman commented on May 18, 2024

Yes you loose the users relationships with a single users table, so I guess it will depend on each usecase/preference, I think both are valid options.

Regarding UUIDs, the collision probability is very very low. Even if the requests come in the same microsecond the UUIDs will be different as there is additional randomness added and also an internal counter implemented for extreme cases.

from tenancy.

stancl avatar stancl commented on May 18, 2024

I guess it will depend on each usecase

To authenticate users against the central database, do you just make the auth guard use that DB? Would be nice if we supported both approaches in 2.3. Both synced users & auth against central DB.

additional randomness added and also an internal counter

Sounds great. I'll look into using uuids and hopefully I can push a branch with this functionality in the next few days.

Thanks for the feedback, it's really appreciated.

from tenancy.

viezel avatar viezel commented on May 18, 2024

I still don’t understand what the issue with the same user I master db and tenant db.
That user serves different purposes. In master db it could be used to count seats for pricing. It will have relation to tenant(s), subscription, do impersonation etc.
On tenant db it’s used for the main application. Here you use it like a regular Laravel app.

from tenancy.

zeidanbm avatar zeidanbm commented on May 18, 2024

I know this might sound silly but i can't wrap my mind around why we need to use uuid or two ids one global and one local. The way I see this, is that everyone creating a user whether through a tenant or at the central level they must create the user first at the central db users table and then the created user will be synced with a queue job (on save) down to the Tenant table and the same user id from the central table will be synced as the id on the tenant users table.

There is no way two users can have same id as they all are going through the central db(one table which has auto increment on) and we are using that same id on the tenants user table.

Central DB:
user 1 -- id: 1, name: jane doe...
user 2 -- id: 2, name: test two...
user 3 -- id: 3, name: john doe...

Tenant A
user 1 -- id: 1, name: jane doe...
user 2 -- id: 2, name: test two...

Tenant B
user 1 -- id: 1, name: jane doe...
user 3 -- id: 3, name: john doe...

from tenancy.

stancl avatar stancl commented on May 18, 2024

@viezel

This means that a user can register into the tenant and get back the user model. Secondly, a queued job if dispatched to ensure that this user is also created in central db.

This is why I asked you about getting the global id. You said:

the global_user_id is just the id incremental field in the users table of the central database. So mySQL or other databases supporting auto_increment will make the ID unique.

which implies that you always create users in the central database first.

from tenancy.

viezel avatar viezel commented on May 18, 2024

oh that was not the intention. Im just quite focused on decoupling tenant and central, so they can function individually.

from tenancy.

stancl avatar stancl commented on May 18, 2024

Then to have (some) shared users, using uuids seems like the only option.

from tenancy.

stancl avatar stancl commented on May 18, 2024

@viezel Do you use a tenant_users table to map which users belong to which tenant? Because that requires making a query to the central database when creating a user.

from tenancy.

viezel avatar viezel commented on May 18, 2024

@stancl yes i do.

from tenancy.

viezel avatar viezel commented on May 18, 2024

But it’s queued since the tenant could import 1000 users. Then it’s handy that those extra central db queries happens async

from tenancy.

viezel avatar viezel commented on May 18, 2024

@stancl totally agree.
Maybe just built the flexibility into the package, so people can choose what to do themselves.

from tenancy.

viezel avatar viezel commented on May 18, 2024

btw, this implementation #57 (comment) will do just fine. People can choose to use that trait or built their own.

from tenancy.

stancl avatar stancl commented on May 18, 2024

I don't think there's anything that needs to be changed in the package to let you implement this yourself.

The implementation in that comment had some issues that I've resolved, but I still need to take care of many edge cases before opening a pull request.

from tenancy.

richeklein avatar richeklein commented on May 18, 2024

Thanks for such a great package! I was reading through the redirect docs and will be using a redirect to redirect the customer to their domain after signup. Is there a preferred way to add their account info to the tenant database? Should I add a custom handler for the tenant.created event? I want the user that created the new tenant to be the site admin, but also be part of the tenant users table (outside of the central database). This seems similar to this shared users issue.

I'm considering storing tenant admins in a central users table and then creating a second account for the user in the tenant db users table.

from tenancy.

metadeck avatar metadeck commented on May 18, 2024

Has anyone considered using/ has successfully used Laravel Passport on the central app to solve this issue? Just wondering if it would be possible before I go down the rabbit hole. TIA

from tenancy.

viezel avatar viezel commented on May 18, 2024

@metadeck I would not do that if I were you. Remember that tenant id is in the core of what you do.
Laravel Passport is "only" auth. You can use that now, later you refactor to a external SSO services, later support AD, LDAP etc.
so I would not rely my business logic on an auth provider.

from tenancy.

metadeck avatar metadeck commented on May 18, 2024

@viezel Thanks for your input across all of this thread. I think we'll go with the global user id and sync approach.

from tenancy.

cyrillkalita avatar cyrillkalita commented on May 18, 2024

Ok so I found a solution that works for my specific use case.

I see how that works. How did you solve RBAC @jonagoldman ?

from tenancy.

jonagoldman avatar jonagoldman commented on May 18, 2024

@cyrillkalita I have the roles and permissions tables also in the central database, together with the users and tenants. I use an additional 'tenant_id' foreign key in the roles table so a user can have different roles for different tenants. It can be tricky but it works for me.

from tenancy.

peter-brennan avatar peter-brennan commented on May 18, 2024

Implemented in v3.

How would I go about using this in v3? I am currently developing a sass application with v3 and this is exactly what I am needing. I can't find any documentation on how to use shared users between master and tenants. Thanks in advanced.

from tenancy.

cyrillkalita avatar cyrillkalita commented on May 18, 2024

@peter-brennan imagine a standard many-to-many: what would you do? Attach one model to another? Ok.
Now, docs cover the case of "Shared Resource" - which is what you need.

So when you make a central user, it is attached to a tenant.

And when you invite/ register the user with the same email in another tenant, find that central model and attach it to a new tenant.

When you need to delete a user, after you are done on a tenant level, detach central user from your tenant. You can also verify if the central use doesn't have any other tenants attached - and remove it from central users table.

Does that make sense?

from tenancy.

peter-brennan avatar peter-brennan commented on May 18, 2024

Yes perfect sense thank you @cyrillkalita. If I run into any troubles I will be sure to shoot you guys a line. Thank you.

from tenancy.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.