From 326e886a41f16ff4c772cd2e36b778ab582887a7 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 13 Sep 2026 04:16:09 -0700 Subject: [PATCH] fix(labels): honor tenant isolation in InMemory label stores (#8102) * fix(labels): honor tenant isolation in InMemory label stores Stamp ambient TenantId on save and filter find/list/delete/replace through TenantVisibility so Memory Labels match EF SetTenantIdFilter and ApplyTenantId. Labels contracts have no TenantAgnostic flag. Closes #8089 Co-authored-by: Sipke Schoorstra * test(labels): account for * visibility when asserting tenant-b leftovers Tenant B correctly sees tenant-agnostic associations; assert those rows remain alongside the other tenant's data after delete/replace. Co-authored-by: Sipke Schoorstra * fix(labels): delete visible Memory rows in one locked step Find-then-Delete(id) could remove another tenant's same-ID replacement that landed between the visibility check and the remove. DeleteWhere under MemoryStore.Sync keeps the check and removal together. Co-authored-by: Sipke Schoorstra --------- Co-authored-by: Cursor Agent --- Elsa.sln | 16 ++ .../Services/InMemoryLabelStore.cs | 76 +++++-- .../InMemoryWorkflowDefinitionLabelStore.cs | 82 +++++--- .../Elsa.Labels.UnitTests.csproj | 12 ++ .../InMemoryLabelStoreTenantIsolationTests.cs | 188 ++++++++++++++++++ ...efinitionLabelStoreTenantIsolationTests.cs | 166 ++++++++++++++++ 6 files changed, 501 insertions(+), 39 deletions(-) create mode 100644 test/unit/Elsa.Labels.UnitTests/Elsa.Labels.UnitTests.csproj create mode 100644 test/unit/Elsa.Labels.UnitTests/Services/InMemoryLabelStoreTenantIsolationTests.cs create mode 100644 test/unit/Elsa.Labels.UnitTests/Services/InMemoryWorkflowDefinitionLabelStoreTenantIsolationTests.cs diff --git a/Elsa.sln b/Elsa.sln index 9e4b96eda..d5b8e012b 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -514,8 +514,11 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.UserTasks.Persistence.EFCore.UnitTests", "test\unit\Elsa.UserTasks.Persistence.EFCore.UnitTests\Elsa.UserTasks.Persistence.EFCore.UnitTests.csproj", "{A4F3CB08-759D-4FE6-B715-E8D91E3E3F3E}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.UserTasks.Persistence.ConformanceTests", "test\unit\Elsa.UserTasks.Persistence.ConformanceTests\Elsa.UserTasks.Persistence.ConformanceTests.csproj", "{0C16ED4D-2483-488D-9CA1-D269ED5BEB9F}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Hosts.SmokeTests", "test\integration\Elsa.Hosts.SmokeTests\Elsa.Hosts.SmokeTests.csproj", "{0BE4BC01-D02D-4185-A433-E33780584261}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Labels.UnitTests", "test\unit\Elsa.Labels.UnitTests\Elsa.Labels.UnitTests.csproj", "{ED8D7C78-154D-4124-8C0A-E63785A862E5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -2468,6 +2471,18 @@ Global {0BE4BC01-D02D-4185-A433-E33780584261}.Release|x64.Build.0 = Release|Any CPU {0BE4BC01-D02D-4185-A433-E33780584261}.Release|x86.ActiveCfg = Release|Any CPU {0BE4BC01-D02D-4185-A433-E33780584261}.Release|x86.Build.0 = Release|Any CPU + {ED8D7C78-154D-4124-8C0A-E63785A862E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED8D7C78-154D-4124-8C0A-E63785A862E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED8D7C78-154D-4124-8C0A-E63785A862E5}.Debug|x64.ActiveCfg = Debug|Any CPU + {ED8D7C78-154D-4124-8C0A-E63785A862E5}.Debug|x64.Build.0 = Debug|Any CPU + {ED8D7C78-154D-4124-8C0A-E63785A862E5}.Debug|x86.ActiveCfg = Debug|Any CPU + {ED8D7C78-154D-4124-8C0A-E63785A862E5}.Debug|x86.Build.0 = Debug|Any CPU + {ED8D7C78-154D-4124-8C0A-E63785A862E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED8D7C78-154D-4124-8C0A-E63785A862E5}.Release|Any CPU.Build.0 = Release|Any CPU + {ED8D7C78-154D-4124-8C0A-E63785A862E5}.Release|x64.ActiveCfg = Release|Any CPU + {ED8D7C78-154D-4124-8C0A-E63785A862E5}.Release|x64.Build.0 = Release|Any CPU + {ED8D7C78-154D-4124-8C0A-E63785A862E5}.Release|x86.ActiveCfg = Release|Any CPU + {ED8D7C78-154D-4124-8C0A-E63785A862E5}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2673,6 +2688,7 @@ Global {A4F3CB08-759D-4FE6-B715-E8D91E3E3F3E} = {AB07AAEB-2C7A-1088-880A-08DB82DA218D} {0C16ED4D-2483-488D-9CA1-D269ED5BEB9F} = {18453B51-25EB-4317-A4B3-B10518252E92} {0BE4BC01-D02D-4185-A433-E33780584261} = {90031D64-CA0F-46D0-9AF4-8DC023A5FFCD} + {ED8D7C78-154D-4124-8C0A-E63785A862E5} = {18453B51-25EB-4317-A4B3-B10518252E92} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E} diff --git a/src/modules/Elsa.Labels/Services/InMemoryLabelStore.cs b/src/modules/Elsa.Labels/Services/InMemoryLabelStore.cs index 7afee7270..a5e64795f 100644 --- a/src/modules/Elsa.Labels/Services/InMemoryLabelStore.cs +++ b/src/modules/Elsa.Labels/Services/InMemoryLabelStore.cs @@ -1,4 +1,6 @@ +using Elsa.Common.Entities; using Elsa.Common.Models; +using Elsa.Common.Multitenancy; using Elsa.Common.Services; using Elsa.Extensions; using Elsa.Labels.Contracts; @@ -9,62 +11,92 @@ namespace Elsa.Labels.Services; /// /// An in-memory store of labels. /// +/// +/// Ambient tenant is applied here rather than in callers. +/// EF owns that via SetTenantIdFilter / ApplyTenantId; Memory must compensate. +/// Labels contracts have no TenantAgnostic flag, so isolation always applies (EF query filter). +/// public class InMemoryLabelStore : ILabelStore { private readonly MemoryStore