hi
What is your abp package version?
I will share some code with you. Thanks.
Hi,
I've analyzed your admin server logs. Your AdministrationService has 4,233 permissions registered from multiple microservices. This is expected in the microservice template with Dynamic Permission Store enabled.
From the logs:
05:48:03)05:44:45)We suspect the bottleneck is the Redis cache operations with this large number of permissions. To confirm, could you temporarily disable Redis caching and use in-memory cache instead to test the same API call?
You can do this by removing the AbpCachingStackExchangeRedisModule dependency and the related Redis configuration from your AdministrationService, then compare the response time.
This will help us confirm the root cause before providing a solution.
Thanks
hi
The recommended approach is to extend the built-in OpenLdapManager and LdapExternalLoginProvider to add tenant validation after LDAP authentication succeeds.
Step 1: Extend OpenLdapManager to query the user's organizational unit from LDAP
Following the same pattern as the existing GetUserEmailAsync method:
using System.Linq;
using System.Threading.Tasks;
using LdapForNet;
using Microsoft.Extensions.Logging;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Identity.ExternalLoginProviders.Ldap;
using Volo.Abp.Ldap;
namespace YourProject;
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(OpenLdapManager), typeof(ILdapManager), typeof(LdapManager))]
public class CustomOpenLdapManager : OpenLdapManager
{
public CustomOpenLdapManager(ILdapSettingProvider ldapSettingProvider)
: base(ldapSettingProvider)
{
}
public virtual async Task<string> GetUserOrganizationalUnitAsync(string userName)
{
using (var conn = await CreateLdapConnectionAsync())
{
await AuthenticateLdapConnectionAsync(
conn,
await NormalizeUserNameAsync(await LdapSettingProvider.GetUserNameAsync()),
await LdapSettingProvider.GetPasswordAsync());
var searchResults = await conn.SearchAsync(
await GetBaseDnAsync(),
await GetUserFilterAsync(userName));
try
{
var userEntry = searchResults.First();
return GetUserOrganizationalUnit(userEntry);
}
catch (LdapException e)
{
Logger.LogException(e);
}
return null;
}
}
protected virtual string GetUserOrganizationalUnit(LdapEntry ldapEntry)
{
// Option 1: Read "ou" or "department" attribute directly from the LDAP entry
var ou = ldapEntry.ToDirectoryEntry().GetAttribute("ou")?.GetValue<string>();
if (!ou.IsNullOrWhiteSpace())
{
return ou;
}
// Option 2: Parse OU from the user's DN
// e.g. "uid=john,ou=Unit1,dc=company,dc=com" -> "Unit1"
var dn = ldapEntry.Dn;
var ouPart = dn?.Split(',')
.FirstOrDefault(p => p.Trim().StartsWith("ou=", System.StringComparison.OrdinalIgnoreCase));
return ouPart?.Split('=').LastOrDefault()?.Trim();
}
}
Step 2: Create a tenant-restricted LDAP login provider
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Volo.Abp.Features;
using Volo.Abp.Guids;
using Volo.Abp.Identity;
using Volo.Abp.Identity.ExternalLoginProviders.Ldap;
using Volo.Abp.Ldap;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Settings;
namespace YourProject;
public class TenantRestrictedLdapExternalLoginProvider : LdapExternalLoginProvider
{
protected ITenantStore TenantStore { get; }
public TenantRestrictedLdapExternalLoginProvider(
IGuidGenerator guidGenerator,
ICurrentTenant currentTenant,
IdentityUserManager userManager,
IIdentityUserRepository identityUserRepository,
OpenLdapManager ldapManager,
ILdapSettingProvider ldapSettingProvider,
IFeatureChecker featureChecker,
ISettingProvider settingProvider,
IOptions<IdentityOptions> identityOptions,
ITenantStore tenantStore)
: base(guidGenerator, currentTenant, userManager, identityUserRepository,
ldapManager, ldapSettingProvider, featureChecker, settingProvider, identityOptions)
{
TenantStore = tenantStore;
}
public override async Task<bool> TryAuthenticateAsync(string userName, string plainPassword)
{
// First, perform standard LDAP authentication
var isAuthenticated = await base.TryAuthenticateAsync(userName, plainPassword);
if (!isAuthenticated)
{
return false;
}
// Skip tenant validation for host users
if (CurrentTenant.Id == null)
{
return true;
}
// Query LDAP for user's organizational unit
var customLdapManager = (CustomOpenLdapManager)LdapManager;
var userOu = await customLdapManager.GetUserOrganizationalUnitAsync(userName);
if (userOu.IsNullOrWhiteSpace())
{
Logger.LogWarning($"Could not determine organizational unit for LDAP user: {userName}");
return false;
}
// Get current tenant info
var tenant = await TenantStore.FindAsync(CurrentTenant.Id.Value);
if (tenant == null)
{
return false;
}
// Compare user's OU with the current tenant
if (!IsUserAllowedForTenant(userOu, tenant.Name))
{
Logger.LogWarning(
$"LDAP user '{userName}' belongs to OU '{userOu}' and is not allowed for tenant '{tenant.Name}'");
return false;
}
return true;
}
protected virtual bool IsUserAllowedForTenant(string userOu, string tenantName)
{
// Simple approach: tenant name matches OU name (case-insensitive)
// Customize this mapping based on your business requirements
return string.Equals(userOu, tenantName, System.StringComparison.OrdinalIgnoreCase);
}
}
Step 3: Register the custom provider in your module
public override void ConfigureServices(ServiceConfigurationContext context)
{
// Replace the default LdapExternalLoginProvider with our tenant-restricted version
Configure<AbpIdentityOptions>(options =>
{
options.ExternalLoginProviders.Remove(LdapExternalLoginProvider.Name);
options.ExternalLoginProviders.Add<TenantRestrictedLdapExternalLoginProvider>(LdapExternalLoginProvider.Name);
});
}
How it works:
AbpSignInManager.PasswordSignInAsync iterates external login providers and calls TryAuthenticateAsync.base.TryAuthenticateAsync).CurrentTenant.Id == null), it allows login directly.CustomOpenLdapManager.GetUserOrganizationalUnitAsync.IsUserAllowedForTenant.false if there's a mismatch, which blocks the sign-in.Notes:
CustomOpenLdapManager is automatically injected into TenantRestrictedLdapExternalLoginProvider because OpenLdapManager is replaced in the DI container via the [Dependency(ReplaceServices = true)] attribute.GetUserOrganizationalUnit method provides two strategies for determining the user's OU: reading a direct LDAP attribute (ou) or parsing the OU from the user's DN. You should adjust this based on your LDAP directory structure.IsUserAllowedForTenant method uses a simple name comparison. If your tenant names don't match OU names directly, you can implement a custom mapping (e.g., from a configuration file or database table).Thanks
hi
The UserInvitation is a new entity, so add it to your DbContext is correct.
We will check your angular issues.
Thanks.
Hi,
Yes, you can reuse Blazor WebAssembly pages in a MAUI Blazor Hybrid application with ABP. ABP provides a MAUI Blazor application template specifically designed for this purpose.
How reuse works at different levels:
ABP built-in module pages (Identity, Settings, Audit Logging, Language Management, OpenIddict, etc.) — These are already shared. Both MAUI Blazor and Blazor WebAssembly reference the same base Blazor UI packages (e.g., Volo.Abp.Identity.Pro.Blazor). The WebAssembly variant just adds a .WebAssembly suffix package for WASM-specific service registration, but the actual Razor pages/components come from the same base package. So you get all the built-in module pages working in MAUI Blazor without any extra work.
Template-generated pages (like Index.razor, HostDashboard.razor) — In our current templates, these pages are duplicated between MAUI Blazor and Blazor WebApp Client projects. The markup is almost identical, but there are small differences. For example, the Login button on Index.razor uses <a href="/Account/Login"> in MAUI Blazor vs <a @onclick="Login"> with NavigationManager in WebApp Client. The HostDashboard.razor is essentially the same in both templates.
Your custom pages — If you want to share your own custom pages between both apps, you can create a Razor Class Library (RCL) project and reference it from both the MAUI Blazor and Blazor WebAssembly projects. Keep in mind that you may need to handle some platform-specific differences (like navigation and authentication) through abstractions or conditional logic.
HttpApi.Client — Both MAUI Blazor and Blazor WebAssembly reference the same HttpApi.Client project, so all your API client proxy code is fully shared.
What requires separate implementation:
Authentication: MAUI Blazor uses OIDC via Duende.IdentityModel.OidcClient with a platform-specific MauiAuthenticationBrowser (supporting both authorization code flow and password flow). Blazor WebAssembly uses standard ASP.NET Core authentication (AddBlazorWebAppServices()). ABP provides Volo.Abp.Account.Pro.Public.MauiBlazor package that handles MAUI authentication for you.
Module configuration: MAUI Blazor module (MyProjectNameMauiBlazorModule) has its own setup for Blazorise, routing, bundling, and authentication that differs from the WebAssembly module.
Navigation menus: Each template has its own MenuContributor. For example, the MAUI Blazor user menu links to external URLs (Account management pages on the auth server) with target: "_blank", while the WebAssembly version uses OperatingSystem.IsBrowser() to differentiate behavior.
Platform-specific code: MAUI Blazor includes platform handlers for Android, iOS, MacCatalyst, and Windows (e.g., WebAuthenticator callbacks, SSL certificate handling in debug mode).
Getting started:
You can create a new ABP solution with the MAUI Blazor option using ABP Studio. The generated template comes pre-configured with everything needed. Then, to maximize code reuse with your existing Blazor WebAssembly app, extract shared custom components into a Razor Class Library.
Thanks
hi
Can you try to share full /api/abp/application-configuration HTTP request debug logs?
liming.ma@volosoft.com
https://abp.io/support/questions/8622/How-to-enable-Debug-logs-for-troubleshoot-problems
Have you used Redis? If so. Can you disable it and try again?
Thanks.
hi
Is your problem solved?
Thanks.
Get the LeptonXTheme source code.
dotnet tool list -g
Package Id Version Commands
------------------------------------------------------------------
volo.abp.studio.cli 2.2.4 abp
abp get-source Volo.Abp.LeptonXTheme
[09:46:59 INF] Downloading source code of Volo.Abp.LeptonXTheme (Latest)
[09:46:59 INF] Output folder: /Downloads/test
[09:47:14 INF] Volo.Abp.LeptonXTheme.SourceCode (v: 5.1.1) package downloaded from https://nuget.abp.io/xxx/v3/index.json and saved to (/Users/maliming/.nuget/packages/)
[09:47:15 INF] 'Volo.Abp.LeptonXTheme' has been successfully downloaded to '/Downloads/test'