abpframework / abp

Open-source web application framework for ASP.NET Core! Offers an opinionated architecture to build enterprise software solutions with best practices on top of the .NET. Provides the fundamental infrastructure, cross-cutting-concern implementations, startup templates, application modules, UI themes, tooling and documentation.
https://abp.io
GNU Lesser General Public License v3.0
12.8k stars 3.41k forks source link

What's the best way to implement function for one user joined to many tenants? #2611

Closed hidojerry closed 1 year ago

hidojerry commented 4 years ago

In many saas platform production, a user can be invited to join many teams/companies(aka tenants), like the airtable etc. Do you have any plan to handle this situation as built in functionality? In current tenant implementation, if a user belongs to many tenant, he/she needs to register many accout.

hikalkan commented 4 years ago

We don't think to implement this because it has problems with some use cases, especially when tenants have their own separate databases.

I accept that for some applications may need such a functionality. In AspNet Zero we had solved it with "linked account" concept. Probably we will implement a similar feature with the ABP framework.

hidojerry commented 4 years ago

Thanks for the reply.

I implement it by introduce a table "TenantUser", but it need to modify many codes in abp framework modues and the identity module. So it make to upgrade abp modules harder.

I will try to provide an option (maybe named TenantMode(Single, Multiple)), and provide spicific services to overide related functions. If the TenantMode is Mutiple, can only use one database for all tenants. I don't know wether it can achived it, but i will give a try.

It would be great if you made it a built-in feature.

fileman commented 3 years ago

I have the same requirement that some users, with limited rights need to access to all tenants. I read that "commercial" has linking account, that can be a solution with few tenants, but if I have hundreds of tenants, I'have to link an account for everyone of those.

@hidojerry have you tried to using "host" users in a specific role with only required permissions?

willignicolas commented 3 years ago

Hi,

Exact same need here, with users that need access many tenant with one login. Link user in commercial seems promising for that purpose but with user for many tenant it's not perfect to ask user to create multiple account for each tenant. Maybe create user and user Link automatically and sync password beetween accounts? Have you any workaround to use Link user in that case without ask user to create multiple account ?

hikalkan commented 3 years ago

That was a tradeoff and we decided to not share the same account between tenants. When a user is created, you may register to the creation event (see) and create the same user in other tenants. This is what comes in my mind without thinking much :)

willignicolas commented 3 years ago

Hello @hikalkan

Thanks for the Event Bus idea. It's the way we are going to implement this behavior.

bluprints-todd commented 3 years ago

Not sure if this helps or not, but there is an IdentityLinkUser that seems to handle this for you. See - https://github.com/abpframework/abp/blob/dev/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityLinkUser.cs

I am currently using it and works well. You can link a user with multiple tenants. When multi-tenancy is on, users are unique to tenants, so with this, you can link them to a master user record for example.

The below example shows:

IdentityLinkUser is a host table concept. So when a user logs in, you would query this table to find all linked tenants for the user, and allow them to choose one, and once they choose you would impersonate (or CurrentUser.change()) tenant user for the tenant they select or within.

` var tenantName = "test_tenant"; var tenantCs = string.Empty; //$"Server=localhost;Database={tenantName};Trusted_Connection=True"; var roleName = "Tenant Role"; var userName = "test_user"; var userEmail = "test@test.com"; using (CurrentTenant.Change(null)) { try { var tenant = await _tenantRepository.FindByNameAsync(tenantName); if (tenant == null) { tenant = await _tenantManager.CreateAsync(tenantName); if (!tenantCs.IsNullOrEmpty()) tenant.SetDefaultConnectionString(tenantCs); tenant = await _tenantRepository.InsertAsync(tenant, true);

                    //create new tenant DB if we have a specified connection string
                    if (!tenantCs.IsNullOrEmpty())
                    {
                        var migrator = ServiceProvider.GetRequiredService<PatientwingDbMigrationService>();
                        await migrator.MigrateAsync();
                    }
                }

                Guid tenantUserId;
                Guid hostUserId;

                //second, create user at host level
                using (CurrentTenant.Change(null))
                {
                    var hostUser = await _userManager.FindByEmailAsync(userEmail);
                    if (hostUser == null)
                    {
                        hostUser = new IdentityUser(GuidGenerator.Create(), userName, userEmail);
                        (await _userManager.CreateAsync(hostUser)).CheckErrors();
                        await CurrentUnitOfWork.SaveChangesAsync();
                    }

                    hostUserId = hostUser.Id;
                }

                //third, add user to tenant
                using (CurrentTenant.Change(tenant.Id, tenant.Name))
                {
                    var tenantUser = await _userManager.FindByEmailAsync(userEmail);
                    if (tenantUser == null)
                    {
                        tenantUser = new IdentityUser(GuidGenerator.Create(), userName, userEmail, tenant.Id);
                        (await _userManager.CreateAsync(tenantUser)).CheckErrors();

                        var role = await _roleManager.FindByNameAsync(roleName);
                        if (role == null)
                        {
                            role = new IdentityRole(GuidGenerator.Create(), roleName, CurrentTenant.Id)
                            {
                                IsStatic = false,
                                IsDefault = true,
                                IsPublic = true
                            };
                            (await _roleManager.CreateAsync(role)).CheckErrors();
                        }

                        await _userRepo.EnsureCollectionLoadedAsync(tenantUser, x => x.Roles);
                        if (!tenantUser.IsInRole(role.Id))
                        {
                            tenantUser.AddRole(role.Id);
                        }

                        await CurrentUnitOfWork.SaveChangesAsync();
                    }

                    tenantUserId = tenantUser.Id;
                }

                //fourth, link host user to tenant user
                using (CurrentTenant.Change(null))
                {
                    await _identityLinkUserManager.LinkAsync(new IdentityLinkUserInfo(hostUserId, null),
                        new IdentityLinkUserInfo(tenantUserId, tenant.Id));
                    await CurrentUnitOfWork.SaveChangesAsync();
                }

                using (CurrentTenant.Change(tenant.Id, tenant.Name))
                {
                    var user = await _userManager.FindByIdAsync(tenantUserId.ToString());
                    if (user != null)
                    {
                        await _userRepo.EnsureCollectionLoadedAsync(user, x => x.Roles);
                        var tenantRoles = await _userManager.GetRolesAsync(user);
                        //assert new tenant role exists
                    }
                }
            }
            catch (Exception ex)
            {
                return Problem(ex.ToString());
            }
        }

`

Hopefully, this makes sense and helped me in a similar situation.

Thanks

tvddev commented 3 years ago

Any thoughts on when this might be included in the framework? We have this situation where a tenant is an advisory company. The advisors users then provide advice to other companies. Other companies (also tenants) need to give one or more users in the advisory company access to their system.

0andy commented 3 years ago

Will abp vnext commercial implemente host user access mulit-tenants data? When will be done? Approximate timeline? Thx Saas platform need this feature.....

Y2zz commented 3 years ago

Can I understand that the current tenant is an OEM identification code. In fact, according to the understanding of SAAS products in China, the function of team isolation should be realized by itself, and the tenant should not be relied on

GMCfourX4 commented 2 years ago

We have this situation as well - our tenants may hire the same companies to advise/consult (IT staff, accountants, business consultants, etc). We would like to be able to tie the same email address and password to multiple tenants.

tvddev commented 2 years ago

It would be great if the host side login, and selection of which tenant to impersonate, could be extended to allow tenants, that have permission from another, to also impersonate.

Can you also fix the issue where impersonating a tenant does not work with WASM Blazor?

tvddev commented 2 years ago

@hikalkan - Any update on this issue and the Blazor WASM impersonation?

maliming commented 2 years ago

hi @tvddev

It's not implemented yet, and we don't have an end date for this feature. I'll leave you a message when it is done.

steves-bits commented 2 years ago

When it is done could you leave me a message too, please?

antonGritsenko commented 2 years ago

Can someone please explain in details what actually the linking doing and what benefits it gives? Is it just more quick tenant switch?

maliming commented 1 year ago

hi

https://docs.abp.io/en/commercial/latest/modules/account/linkedaccounts