When attempting to delete an OU from the backend, the system does not perform proper validation to check whether any users or roles are still associated with it. As a result, the OU can be deleted even when dependencies exist, potentially causing data inconsistency and orphaned references.
Environment Details: ABP Version: ABP 10.0.1 Pro Reproducibility: Issue is reproducible via Swagger (not via UI) Tenant Resolution Strategy: Subdomain-based tenant resolution , different tenants have different URL and login credentials. Testing Context: Tenant context
Preconditions: A tenant exists and is in Active state An Organization Unit (OU) exists One or more Users and/or Roles are associated with the OU in different
Steps to Reproduce: Log in as a Tenant Admin Ensure the tenant is in Active state Open Swagger UI for the tenant Identify an OU that has: Assigned Users and/or Roles Invoke the API: DELETE /api/identity/organization-units/{id} Provide the ID of the OU with existing associations Execute the request
Expected Result: The system should validate whether the OU has dependent entities If users or roles are associated: Deletion should be blocked A validation error should be returned, such as: “Cannot delete OU with assigned users or roles” Alternatively, system should enforce safe handling (e.g., require reassignment or explicit cleanup)
Actual Result: The OU is deleted successfully No validation or warning is triggered Associated users/roles lose their linkage, resulting in orphaned references
Severity: High – Affects data integrity and authorization structure
2 Answer(s)
-
0
In ABP 10.0.1,
DELETE /api/identity/organization-units/{id}deletes the OrganizationUnit aggregate itself. The framework does not automatically block deletion when there are still rows in the link tables (OU–User and OU–Role relations). Those relations are not modeled as required FK constraints to the OU entity in a way that would cause a database “restrict delete” error, so deletion can succeed and the relation rows may become orphaned depending on your DB configuration.The UI typically prevents/avoids this scenario by guiding you to remove members/roles first, but calling the endpoint directly from Swagger bypasses any UI guardrails—so what you observe is expected with the default backend behavior.
To enforce “cannot delete if it has users/roles”, you should implement an application-layer validation before delete, then throw a user-friendly/business exception.
A safe, ABP-style implementation (override the app service):
using System; using System.Threading.Tasks; using Volo.Abp; using Volo.Abp.Application.Services; using Volo.Abp.Domain.Repositories; using Volo.Abp.Identity; public class MyOrganizationUnitAppService : OrganizationUnitAppService { private readonly IRepository<IdentityUserOrganizationUnit> _userOuRepo; private readonly IRepository<IdentityRoleOrganizationUnit> _roleOuRepo; public MyOrganizationUnitAppService( OrganizationUnitManager organizationUnitManager, IOrganizationUnitRepository organizationUnitRepository, IdentityUserManager userManager, IdentityRoleManager roleManager, IRepository<IdentityUserOrganizationUnit> userOuRepo, IRepository<IdentityRoleOrganizationUnit> roleOuRepo) : base(organizationUnitManager, organizationUnitRepository, userManager, roleManager) { _userOuRepo = userOuRepo; _roleOuRepo = roleOuRepo; } public override async Task DeleteAsync(Guid id) { // Users? var hasUsers = await _userOuRepo.AnyAsync(x => x.OrganizationUnitId == id); // Roles? var hasRoles = await _roleOuRepo.AnyAsync(x => x.OrganizationUnitId == id); if (hasUsers || hasRoles) { throw new UserFriendlyException( "Cannot delete organization unit because it has assigned users and/or roles."); } await base.DeleteAsync(id); } }Wire it up by replacing the default service:
using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Identity; public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.Replace( ServiceDescriptor.Transient<IOrganizationUnitAppService, MyOrganizationUnitAppService>() ); }Alternative (if you prefer automatic cleanup instead of blocking)
- Before calling
base.DeleteAsync(id), explicitly remove OU memberships and OU role links (using the relevant managers/repositories), then delete the OU. This avoids orphan links but changes behavior, so only do it if that matches your business rules.
Sources (validated):
- https://abp.io/docs/latest/modules/identity-pro
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) -
0
Hi,
One clarification first: the framework does not leave orphaned references behind. Looking at
OrganizationUnitAppService.DeleteAsync:await UserManager.UpdateOrganizationAsync(id, null); // unlinks all users from this OU await OrganizationUnitManager.DeleteAsync(id); // removes members + roles + child OUs, then the OU itselfThe first call runs an
ExecuteDeleteoverIdentityUserOrganizationUnitfor the OU. The manager then recursively removes child OU memberships/roles before deleting the OU. So the link tables are actively cleaned up — no dangling rows.The intentional design is "delete = auto-cleanup". Our own MVC and Blazor delete dialogs already wrap the API with this safeguard:
- Load the OU and its
UserCountfirst. - If
UserCount > 0, force the operator to choose: Unassign the users (default), or move them to another OU they pick from a list. - Submit
MoveAllUsersAsync(id, targetOuOrNull)followed byDeleteAsync(id).
You can see this in
Volo.Abp.Identity.Pro.Web/Pages/Identity/OrganizationUnits/DeleteModal.cshtml(.cs)and the equivalent Blazor component. CallingDELETEdirectly from Swagger skips that prompt but lands at the same final state as picking "Unassign" in the UI — there's no orphaned data.If your business rule is "API calls must explicitly clean up first, otherwise reject", that's a policy on top of the framework. The cleanest approach is to follow the same pattern as our UI from your client (check membership, decide, then call), or enforce it server-side by overriding the Pro app service:
[ExposeServices(typeof(IOrganizationUnitAppService))] [Dependency(ReplaceServices = true)] public class MyOrganizationUnitAppService : OrganizationUnitAppService { public MyOrganizationUnitAppService( OrganizationUnitManager organizationUnitManager, IdentityUserManager userManager, IOrganizationUnitRepository organizationUnitRepository, IIdentityUserRepository identityUserRepository, IIdentityRoleRepository identityRoleRepository, IdentityProTwoFactorManager identityProTwoFactorManager) : base(organizationUnitManager, userManager, organizationUnitRepository, identityUserRepository, identityRoleRepository, identityProTwoFactorManager) { } public override async Task DeleteAsync(Guid id) { var ou = await OrganizationUnitRepository.GetAsync(id, includeDetails: true); var memberCount = await OrganizationUnitRepository.GetMembersCountAsync(ou, includeChildren: true); if (memberCount > 0) { throw new UserFriendlyException( $"Cannot delete organization unit '{ou.DisplayName}' because it has {memberCount} assigned user(s). Please unassign or move them first."); } if (ou.Roles.Any()) { throw new UserFriendlyException( $"Cannot delete organization unit '{ou.DisplayName}' because it has assigned role(s). Please remove them first."); } await base.DeleteAsync(id); } }GetMembersCountAsync(..., includeChildren: true)covers users attached to any descendant OU as well, so deletion is blocked even if only a child OU has members.ou.Rolescomes from the aggregate (an embedded collection onOrganizationUnit), soincludeDetails: trueis required when loading.Thanks.
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) - Load the OU and its