Hello,
When using Shared User Accounts, how can I add a user created on the HOST to a specific tenant without them having to accept the invitation? I would also like to assign roles to them on the tenant in the same way.
I would like to implement this in the SaasDataSeedContributor.
We are currently carrying out a data migration and need to add around 20 users to all 300 tenants.
2 Answer(s)
-
0
- With Shared User Accounts (
TenantUserSharingStrategy.Shared), the supported/standard way to add a host-created (global) user into a tenant is the invitation flow. Joining a tenant is designed to happen by accepting an invitation (and roles can be pre-assigned at invitation time). - There is no documented/verified API/option in ABP 10.6 that “auto-accepts” invitations (i.e., adds the user to the tenant and assigns roles) without executing the accept flow.
To mass-associate users during migration (e.g., in
SaasDataSeedContributor), the verified approach is:- Create invitations programmatically for each tenant + user
- Assign roles on the invitation (so they get roles immediately after they join)
- If you don’t want users to deal with email, replace the email sender so invitations don’t require SMTP and you can distribute/handle the accept links internally (or log/store them)
This still uses the official invitation/accept mechanism, but removes the operational burden of SMTP.
Practical seeding pattern (SaasDataSeedContributor)
- Enumerate tenants (host side)
- For each tenant, create invitations for the target users (by email)
- Include role assignment in the invitation request (so tenant roles are set when the user joins)
I’m not including exact service/class names for “create invitation” from code because I couldn’t verify them from source in the available repositories here. The official docs confirm the behavior, but don’t expose the internal API surface in the snippet.
Optional: Remove SMTP requirement (so migration doesn’t block on email)
Replace
IIdentityEmailSenderto not send and instead log/persist the invite link. This is a verified approach from an ABP support ticket (used when SMTP is blocked). You can then have your migration/admin process consume those links (or forward them via internal tooling).If your goal is truly “no user interaction at all”, that would require calling the internal “accept invitation” logic directly (or manipulating invitation/tenant-user tables), but I don’t have a verified source for the required public API or the correct persistence model to recommend doing that safely.
(light) sources
- https://abp.io/docs/10.6/modules/account/shared-user-accounts (validated)
- https://abp.io/support/questions/10624 (validated)
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) - With Shared User Accounts (
-
0
Hi,
You can create the invitation and accept it in the same code. The user joins the tenant with the assigned roles right away. No email is sent and the user doesn't need to do anything.
Here is the template's
SaasDataSeedContributorwith this added:using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Volo.Abp.Data; using Volo.Abp.DependencyInjection; using Volo.Abp.Identity.UserInvitations; using Volo.Abp.Identity.UserSharing; using Volo.Abp.MultiTenancy; using Volo.Abp.Uow; using Volo.Saas.Editions; namespace YourProjectName.Saas; public class SaasDataSeedContributor : IDataSeedContributor, ITransientDependency { // Host user email -> role names to assign in each tenant private static readonly Dictionary<string, List<string>> SharedUsers = new() { { "user1@yourcompany.com", new List<string> { "admin", "Manager" } }, { "user2@yourcompany.com", new List<string> { "Manager" } } }; private readonly IEditionDataSeeder _editionDataSeeder; private readonly ICurrentTenant _currentTenant; private readonly UserSharingManager _userSharingManager; private readonly UserInvitationManager _userInvitationManager; public SaasDataSeedContributor( IEditionDataSeeder editionDataSeeder, ICurrentTenant currentTenant, UserSharingManager userSharingManager, UserInvitationManager userInvitationManager) { _editionDataSeeder = editionDataSeeder; _currentTenant = currentTenant; _userSharingManager = userSharingManager; _userInvitationManager = userInvitationManager; } [UnitOfWork] public virtual async Task SeedAsync(DataSeedContext context) { using (_currentTenant.Change(context?.TenantId)) { await _editionDataSeeder.CreateStandardEditionsAsync(); } if (context?.TenantId != null) { await AddSharedUsersToTenantAsync(context.TenantId.Value); } } private async Task AddSharedUsersToTenantAsync(Guid tenantId) { using (_currentTenant.Change(null)) { foreach (var (email, roles) in SharedUsers) { var hostUsers = await _userSharingManager.GetUsersByEmailFromHostAsync(email, includeLeaved: true); var hostUser = hostUsers.FirstOrDefault(u => u.TenantId == null); if (hostUser == null || hostUsers.Any(u => u.TenantId == tenantId)) { continue; } var invitation = await _userInvitationManager.CreateAsync(tenantId, email, roles); await _userSharingManager.AcceptInviteAsync(invitation.Invitation.Id, hostUser.UserName, null, hostUser.PasswordHash); } } } }The
DbMigratorruns this for each tenant. A user who has joined the tenant before is skipped, even if they left it later, so running the migrator again doesn't add anyone twice.- On MySQL you need 10.6.1 or later. 10.6.0 doesn't map the invitation roles column for MySQL.
- The roles must already exist in each tenant, otherwise the migration fails.
- Remove this code after the migration. The data seed also runs when a new tenant is created, but the roles don't exist yet at that point, so it only logs a
Role ... does not exist!error.
Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)