Hi,
You're right, this is a problem. The invite-accept path didn't run the global username/email uniqueness check in shared mode, so two different invitees could each claim admin in their tenant. Host AbpUsers ended up with duplicate rows and login can only resolve the first one, which is why the second tenant's admin can't sign in.
Until the fix ships in 10.2.2 / 10.3.1 you can either give each tenant's admin a unique username (e.g. admin-tenant1 / admin-tenant2), or drop this UserSharingManager override into your Domain module to reject the duplicate right now:
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(UserSharingManager))]
public class MyUserSharingManager : UserSharingManager
{
public MyUserSharingManager(
IOptions<AbpMultiTenancyOptions> multiTenancyOptions,
IDataFilter dataFilter,
ITenantStore tenantStore,
IdentityProUserManager userManager,
IIdentityUserRepository userRepository,
IdentityRoleManager roleManager,
IIdentityRoleRepository roleRepository,
UserInvitationManager userInvitationManager,
IAbpDistributedLock distributedLock)
: base(multiTenancyOptions, dataFilter, tenantStore, userManager, userRepository, roleManager, roleRepository, userInvitationManager, distributedLock)
{
}
public override async Task AcceptInviteAsync(Guid invitationId, string username, string password, string passwordHash)
{
if (IsEnabled())
{
var invitation = await UserInvitationManager.GetAsync(invitationId);
if (invitation.Status == UserInvitationStatus.Pending)
{
var hostUsersWithUserName = await GetUsersByUserNameFromHostAsync(username, includeLeaved: true);
if (hostUsersWithUserName.Any(x => !string.Equals(x.Email, invitation.InviteeEmail, StringComparison.OrdinalIgnoreCase)))
{
throw new BusinessException(IdentityProErrorCodes.InviteUserUserNameAlreadyExists).WithData("UserName", username);
}
var hostUsersWithEmail = await GetUsersByEmailFromHostAsync(invitation.InviteeEmail, includeLeaved: true);
if (hostUsersWithEmail.Any(x => !string.Equals(x.UserName, username, StringComparison.OrdinalIgnoreCase)))
{
throw new BusinessException(IdentityProErrorCodes.InviteUserEmailAlreadyExists).WithData("Email", invitation.InviteeEmail);
}
}
}
await base.AcceptInviteAsync(invitationId, username, password, passwordHash);
}
}
If you already have duplicate rows in host AbpUsers from testing, delete one or rename its UserName so login can disambiguate.
Thanks
Hi,
Could you share a few things so we can reproduce this on our side?
HttpApi.Host, Web.Public(.Host) and Blazor.WebApp.Tieredhome CMS page was created (is home the slug, or did you set it as the default home page?)PagesFeature via GlobalFeatureManagerHttpApi.Host (port 44362 in your log) and the Web.Public(.Host) when the issue happens. Here is how to enable Debug logs: https://abp.io/support/questions/8622/How-to-enable-Debug-logs-for-troubleshoot-problemsIf possible, a minimal reproduction project would speed things up a lot.
liming.ma@volosoft.com
Thanks
Hi,
That's actually intentional. In shared mode, users are global across host and all tenants, so things like activate / deactivate / lock / 2FA / delete only make sense at the host level. That's why the tenant admin UI hides those buttons — it's not a permission issue on your side.
To remove someone from just your tenant, they can Leave from their own account menu. A full delete or deactivate needs a host admin to do it.
Thanks
Hi,
For per-tenant module control, you want Feature Management, not Permission Management. Permission = what a role is allowed to do; Feature = what a tenant/edition can use. Your case is the latter, and ABP has this OOTB.
Here's the full flow:
1. Define your features
public class MyAppFeatureDefinitionProvider : FeatureDefinitionProvider
{
public override void Define(IFeatureDefinitionContext context)
{
var group = context.AddGroup("MyApp", L("Features:MyApp"));
group.AddFeature(
"MyApp.HomeServices",
defaultValue: "false",
displayName: L("Features:HomeServices"),
valueType: new ToggleStringValueType());
group.AddFeature(
"MyApp.BusinessServices",
defaultValue: "false",
displayName: L("Features:BusinessServices"),
valueType: new ToggleStringValueType());
}
private static LocalizableString L(string name)
=> LocalizableString.Create<MyAppResource>(name);
}
2. Protect the application services
[RequiresFeature("MyApp.HomeServices")]
public class HomeServicesAppService : ApplicationService, IHomeServicesAppService
{
// only reachable when the feature is enabled for the current tenant
}
Or inline inside a method:
await FeatureChecker.CheckEnabledAsync("MyApp.HomeServices");
3. Hide the menu item for tenants that don't have it
context.Menu.AddItem(
new ApplicationMenuItem("HomeServices", l["Menu:HomeServices"], "/home-services")
.RequireFeatures("MyApp.HomeServices"));
4. Assign per tenant — no code needed
Login as host admin, go to Saas → Tenants → (the tenant) → Actions → Manage Features, toggle Home Services ON for Tenant A and leave Business Services OFF. Save. Tenant A now only sees/uses Home Services.
About the "all disabled" permission screen your tenant admin sees — that's unrelated to the module toggle. It just means the logged-in tenant admin user doesn't have PermissionManagement.ManagePermissions granted, so the grid is read-only. But for module-level per-tenant control, features are the right tool, not permissions.
Docs:
Thanks
Hi,
Confirmed, the dropdowns on the AuditLogs Clear button don't refresh because LeptonX wraps the <select> with a custom UI that doesn't yet react to programmatic value changes.
Until the next LeptonX patch is out, you can add this override on the page to reset the visible label manually:
$(function () {
$("#ClearFilterButton").on('click', function () {
$("#HttpMethod, #HttpStatusCode, #HasException").each(function () {
this.value = "";
var wrapper = this.closest(".custom-select-wrapper");
if (wrapper) {
var display = wrapper.querySelector(".custom-select-display span");
var selected = this.options[this.selectedIndex];
if (display && selected) {
display.textContent = selected.textContent;
}
}
});
});
});
This runs after the built-in AuditLogs/index.js (which already clears the text fields and reloads the table) and works regardless of the LeptonX version you're on. Once we release the Volo.Abp.AuditLogging fix and LeptonX ships the matching patch, upgrading both packages and re-running abp install-libs will remove the need for this override.
Thanks
Hi,
Both advisories are fixed in lodash 4.18.0+. @abp/lodash already allows any 4.x ("lodash": "^4.17.21"), so you can force the safe version in your own project right now:
package.json (use overrides for npm/pnpm):{
"resolutions": {
"lodash": "^4.18.1"
}
}
yarn install
abp install-libs
abp install-libs copies the physical files from node_modules into wwwroot/libs, so once lodash is bumped there, your wwwroot/libs/lodash/lodash.min.js will be the patched version and both advisories should clear.
We'll also raise the floor in @abp/lodash for future releases.
Thanks
Your current question has been refunded.
Thanks.
Hi,
Great to hear the workaround is working for you. The fix is queued for 10.2.2, so once you move to that version you can drop the two custom pages.
On managing tenant users from the host: right now the Users page is always scoped to the current tenant, so you have to be signed in as a tenant admin to edit / disable / delete users of that tenant.
Thanks again for the detailed logs and HAR on the 2FA issue — that helped a lot.
Thanks
Hi,
Thanks for the log and HAR — with the password correct this time, the flow tells the full story:
[INF] AuthenticationScheme: Identity.TwoFactorUserId signed in. ← login wrote the 2FA cookie
[INF] Executed handler method OnPostAsync, returned RedirectToPageResult
[INF] Redirecting to ./SendSecurityCode.
[INF] POST /Account/Login - 302
[INF] Executing handler method SendSecurityCodeModel.OnGetAsync
[INF] Executed handler method OnGetAsync, returned RedirectToPageResult ← bounced back
[INF] Redirecting to ./Login.
[INF] GET /Account/SendSecurityCode - 302
We've reproduced and confirmed this is a real bug in Account.Pro when Shared mode is combined with 2FA. Here's exactly what happens:
FindSharedUserByNameAsync locates the user across tenants. If the user row only lives under a tenant (no TenantId IS NULL row in AbpUsers), it returns the tenant user. The password check succeeds, 2FA is required, and ASP.NET Core Identity writes the .AspNetCore.Identity.TwoFactorUserId cookie. That cookie, by design of AspNetCore Identity, only carries the UserId — no TenantId.GET /Account/SendSecurityCode the user isn't authenticated yet, so AbpAccountTenantResolveContributor (Shared mode) short-circuits CurrentTenant to null.SendSecurityCode.OnGetAsync calls SignInManager.GetTwoFactorAuthenticationUserAsync(), which internally calls UserManager.FindByIdAsync(userId). That call goes through the default EF IMultiTenant filter (WHERE TenantId IS NULL), so the tenant-owned user row is filtered out, the method returns null, and the page redirects back to /Account/Login.Non-2FA accounts don't hit this because they complete the login in a single POST and get the full Identity.Application cookie (which does carry TenantId), so no tenant context is ever lost.
We'll fix this in 10.2.2. In the meantime, as a drop-in workaround on your side, override the two affected Razor Pages in your Blazor host project. The overrides disable the IMultiTenant filter while fetching the 2FA user, then switch CurrentTenant to the user's tenant for the rest of the handler. I compiled both files against the 10.2 Volo.Abp.Account.Pro.Public.Web assembly to make sure they build cleanly:
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Volo.Abp.Account;
using Volo.Abp.Account.Public.Web.Pages.Account;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Uow;
namespace YourProject.Web.Pages.Account;
[ExposeServices(typeof(SendSecurityCodeModel))]
public class CustomSendSecurityCodeModel : SendSecurityCodeModel
{
[UnitOfWork]
public override async Task<IActionResult> OnGetAsync()
{
var dataFilter = LazyServiceProvider.LazyGetRequiredService<IDataFilter>();
Volo.Abp.Identity.IdentityUser user;
using (dataFilter.Disable<IMultiTenant>())
{
user = await SignInManager.GetTwoFactorAuthenticationUserAsync();
}
if (user == null)
{
return RedirectToPage("./Login");
}
using (CurrentTenant.Change(user.TenantId))
{
Providers = (await AccountAppService.GetTwoFactorProvidersAsync(new GetTwoFactorProvidersInput
{
UserId = user.Id,
Token = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider,
nameof(Microsoft.AspNetCore.Identity.SignInResult.RequiresTwoFactor))
})).Select(userProvider => new SelectListItem
{
Text = userProvider,
Value = userProvider
}).ToList();
}
return Page();
}
[UnitOfWork]
public override async Task<IActionResult> OnPostAsync()
{
var dataFilter = LazyServiceProvider.LazyGetRequiredService<IDataFilter>();
Volo.Abp.Identity.IdentityUser user;
using (dataFilter.Disable<IMultiTenant>())
{
user = await SignInManager.GetTwoFactorAuthenticationUserAsync();
}
if (user == null)
{
return RedirectToAction("Login");
}
using (CurrentTenant.Change(user.TenantId))
{
return await base.OnPostAsync();
}
}
}
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp;
using Volo.Abp.Account.Public.Web.Pages.Account;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Security.Claims;
using Volo.Abp.Uow;
namespace YourProject.Web.Pages.Account;
[ExposeServices(typeof(VerifySecurityCodeModel))]
public class CustomVerifySecurityCodeModel : VerifySecurityCodeModel
{
public CustomVerifySecurityCodeModel(ICurrentPrincipalAccessor currentPrincipalAccessor)
: base(currentPrincipalAccessor)
{
}
[UnitOfWork]
public override async Task OnGetAsync()
{
var dataFilter = LazyServiceProvider.LazyGetRequiredService<IDataFilter>();
Volo.Abp.Identity.IdentityUser user;
using (dataFilter.Disable<IMultiTenant>())
{
user = await SignInManager.GetTwoFactorAuthenticationUserAsync();
}
if (user == null)
{
throw new UserFriendlyException(L["VerifySecurityCodeNotLoggedInErrorMessage"]);
}
using (CurrentTenant.Change(user.TenantId))
{
IsRememberBrowserEnabled = await IsRememberBrowserEnabledAsync();
}
}
[UnitOfWork]
public override async Task<IActionResult> OnPostAsync()
{
var dataFilter = LazyServiceProvider.LazyGetRequiredService<IDataFilter>();
Volo.Abp.Identity.IdentityUser user;
using (dataFilter.Disable<IMultiTenant>())
{
user = await SignInManager.GetTwoFactorAuthenticationUserAsync();
}
if (user == null)
{
return RedirectToPage("./Login");
}
using (CurrentTenant.Change(user.TenantId))
{
return await base.OnPostAsync();
}
}
}
Drop both files into your Blazor host project (any namespace is fine, the [ExposeServices] attribute tells ABP's DI to replace the built-in pages). No Startup / Module changes needed. Please give it a try and let me know if 2FA login works end-to-end for you.
Thanks