Comments (61)
Ok so I found a solution that works for my specific use case.
- Set the
$connection
property of theUser
model to the central database connection:
class User extends Authenticatable
{
protected $connection = 'mysql';
}
- 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.
@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.
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.
@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.
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.
@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.
Implemented in v3.
from tenancy.
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.
@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.
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.
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.
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.
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.
@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.
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.
@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.
Thats fine too. UUID is quite practical. But indexing and performance is not quite as good.
from tenancy.
@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.
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.
Yes please. Once that is in place, then a user impersonation can by done.
from tenancy.
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.
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.
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.
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.
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.
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.
@viezel Can you elaborate on that setup? Sounds interesting.
from tenancy.
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.
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.
@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.
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.
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.
Makes sense. I'll think about both approaches.
from tenancy.
@viezel How do you assign global user ids when creating users? You need to ensure the uniqueness of those keys.
from tenancy.
@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.
So you always create users in the central DB and then let them sync into tenant DBs?
from tenancy.
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.
@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.
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.
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.
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.
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.
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.
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.
oh that was not the intention. Im just quite focused on decoupling tenant and central, so they can function individually.
from tenancy.
Then to have (some) shared users, using uuids seems like the only option.
from tenancy.
@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.
@stancl yes i do.
from tenancy.
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.
@stancl totally agree.
Maybe just built the flexibility into the package, so people can choose what to do themselves.
from tenancy.
btw, this implementation #57 (comment) will do just fine. People can choose to use that trait or built their own.
from tenancy.
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.
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.
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.
@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.
@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.
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.
@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.
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.
@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.
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)
- Does Tenancy support Laravel 11? HOT 1
- Single-database tenancy: make table index
- Add --force option to MigrateFresh HOT 1
- After upgrading from Laravel 10x to Laravel 11x error in TenancyServiceProvider.php HOT 2
- please show , how to use laravel in backend and next.js for forntend HOT 2
- Table migrated in central db instead of tenant db HOT 2
- Ability to use a custom column in PathTenantResolver instead of only id HOT 3
- Setting all parts of connection for a tenant goes wrong HOT 3
- how to configure tenancy for Laravel on Laravel 11 version HOT 1
- Laravel 11 cannot access subdomain on multi-database setup HOT 1
- Issue with Laravel Nova Image not compatible with subdomain HOT 1
- Reset Password for Tenant not working HOT 1
- Preventing Timestamps from Being Auto-inserted into the data Column HOT 1
- Unable to seed spatie/laravel permission across multiple tenancy databases HOT 5
- Undefined constant RedisCluster::OPT_PREFIX HOT 5
- With laravel breeze and react ,Inertia.js not working after installing the package HOT 1
- Wrong domain url when on queue HOT 2
- Jestream Laravel Tenacy HOT 5
- InitializeTenancyByPath with livewire within tenant context throws exception HOT 8
- Make runForMultiple yield the callbacks return HOT 5
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from tenancy.