Skip to content

fix(mongodb): preserve and resolve tenant-agnostic ("*") entities - #155

Merged
sfmskywalker merged 4 commits into
elsa-workflows:mainfrom
arledesma:fix/mongodb-agnostic-tenant-visibility
Sep 6, 2026
Merged

fix(mongodb): preserve and resolve tenant-agnostic ("*") entities#155
sfmskywalker merged 4 commits into
elsa-workflows:mainfrom
arledesma:fix/mongodb-agnostic-tenant-visibility

Conversation

@arledesma

@arledesma arledesma commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Summary

MongoDB multitenancy handling was inconsistent: tenant-agnostic (TenantId == "*") workflow definitions could be overwritten on write and excluded from tenant-scoped reads. This made CLR workflows unreachable under concrete tenants.

This change now:

  • preserves every explicit tenant ID, including "*", and stamps only documents whose tenant ID is null
  • includes tenant-agnostic documents in tenant-scoped reads
  • keeps tenant-scoped deletes strict, including the final delete filter, so a shared custom key cannot remove an agnostic or other-tenant document
  • documents direct-collection bypass and legacy null migration behavior
  • adds container-backed regression coverage for save, read, custom-key delete, ambient-null delete, and explicit agnostic management semantics

Migration note

Existing records whose intended agnostic marker was already clobbered to null are not changed automatically. Deployments should migrate affected records to TenantId = "*" or repopulate the workflow definitions after deployment. New writes preserve the marker.

Validation

  • Elsa.MongoDb.UnitTests on .NET 10: 8 passed
  • focused tenant-isolation tests: 4 passed
  • Elsa.Persistence.MongoDb Release build: .NET 8, 9, and 10 succeeded
  • git diff --check: clean

The build still reports repository-existing dependency/nullability/trimming warnings; this PR introduces no new package dependency.

Fixes #158

@arledesma

Copy link
Copy Markdown
Contributor Author

@dotnet-policy-service agree company="ContraForce"

@sfmskywalker

Copy link
Copy Markdown
Member

@greptile review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes MongoDB multitenancy behavior so tenant-agnostic ("*") entities are preserved on write and resolvable from within a specific tenant on read, aligning MongoDB persistence behavior with the EFCore provider and the documented tenant-agnostic contract.

Changes:

  • Update tenant-scoped query filtering to also include tenant-agnostic (TenantId == "*") documents.
  • Prevent ApplyTenantId from overwriting documents already marked as tenant-agnostic ("*"), preserving the marker during inserts/updates.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/modules/persistence/Elsa.Persistence.MongoDb/Common/MongoDbStore.cs Outdated
@greptile-apps

greptile-apps Bot commented Jun 3, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes MongoDB multi-tenancy handling to align with the EFCore provider's treatment of tenant-agnostic ("*") entities. The changes are confined to two private methods in MongoDbStore<TDocument>.

  • Write path (ApplyTenantId): entities already stamped with "*" are no longer overwritten with the ambient tenant ID, so CLR-registered global workflow definitions keep their agnostic marker.
  • Read path (GetQueryableCollection): tenant-scoped queries now include TenantId == "*" via a new includeTenantAgnostic parameter (defaulting to true), making global entities visible under any specific tenant.
  • Delete path (DeleteWhereAsync): both terminal delete overloads explicitly pass includeTenantAgnostic: false, ensuring a tenant-scoped delete predicate cannot match and destroy globally-shared entities. The second commit in this PR (362c5d1) directly addresses the concern raised in the prior review about deletes inadvertently removing "*" entities.

Confidence Score: 5/5

Safe to merge — the two private methods are changed consistently, all delete overloads protect global entities, and the fix matches documented EFCore provider behaviour.

The write fix, read fix, and delete guard are all internally consistent. The second commit in this PR directly addresses the delete-path concern raised in the prior review. Every DeleteWhereAsync call chain routes through one of the two patched terminal overloads, so no delete path regressed. Read and count operations correctly include '*' entities, keeping counts in sync with result sets.

No files require special attention.

Important Files Changed

Filename Overview
src/modules/persistence/Elsa.Persistence.MongoDb/Common/MongoDbStore.cs Three targeted fixes: ApplyTenantId now skips stamping entities already marked ""; GetQueryableCollection includes TenantId == "" in tenant-scoped reads (via new includeTenantAgnostic param, default true); both DeleteWhereAsync implementations explicitly pass includeTenantAgnostic: false so global entities cannot be destroyed by a tenant-scoped delete. All delete call chains route through the two patched overloads.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[GetQueryableCollection called] --> B{tenantAgnostic == true?}
    B -- Yes --> C[Return unfiltered queryable\nall tenants + all '*' entities]
    B -- No --> D{TDocument is Entity?}
    D -- No --> E[Return unfiltered queryable]
    D -- Yes --> F{includeTenantAgnostic?}
    F -- true\ndefault reads --> G["Filter: TenantId == tenantId OR TenantId == '*'"]
    F -- false\ndeletes --> H["Filter: TenantId == tenantId only"]

    subgraph Callers
        I[FindAsync / FindManyAsync / CountAsync / AnyAsync / ListAsync] --> |includeTenantAgnostic=true| F
        J[DeleteWhereAsync both overloads] --> |includeTenantAgnostic=false| F
    end

    subgraph Write path
        K[ApplyTenantId] --> L{entity.TenantId == '*'?}
        L -- Yes --> M[Skip: preserve agnostic marker]
        L -- No --> N[Stamp with tenantId.EmptyToNull]
    end
Loading

Reviews (2): Last reviewed commit: "fix(mongodb): exclude tenant-agnostic ("..." | Re-trigger Greptile

@arledesma

Copy link
Copy Markdown
Contributor Author

Addressed the delete-path concern in 362c5d1.

GetQueryableCollection is shared by the read and delete paths, so the read-side || TenantId == "*" branch had also widened DeleteWhereAsync — a tenant-scoped delete could match and remove a shared "*" entity for every tenant.

Fix: added an includeTenantAgnostic flag (default true). Reads still include "*" rows, but both DeleteWhereAsync terminals now pass includeTenantAgnostic: false, so tenant-scoped deletes stay strict to the current tenant — matching the Dapper store. An explicit tenantAgnostic: true delete is unchanged, and the agnostic context (Tenant.Id == "*") can still manage "*" entities.

@sfmskywalker

Copy link
Copy Markdown
Member

@greptile review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

@arledesma

Copy link
Copy Markdown
Contributor Author

@sfmskywalker Just checking in to see if there are any further changes that are required for either this PR or #159

The MongoDb store overwrote the tenant-agnostic marker on write and
excluded agnostic rows on read, so tenant-agnostic entities (e.g. CLR
workflow definitions stamped by ClrWorkflowsProvider with "*") were
persisted with a tenant-specific id and became unreachable when resolved
under a specific tenant, throwing WorkflowDefinitionNotFoundException.

Bring MongoDbStore to parity with the EFCore provider:
- ApplyTenantId: don't overwrite TenantId when it is already "*"
- GetQueryableCollection: include TenantId == "*" in the tenant filter

Refs elsa-workflows/elsa-core#7691
…ed deletes

GetQueryableCollection is shared by the read and delete paths. The read fix
added an OR-branch for TenantId == "*", which also widened DeleteWhereAsync: a
tenant-scoped delete could match and remove a shared "*" entity for every
tenant (e.g. a CLR workflow definition deleted under one tenant).

Add an includeTenantAgnostic flag (default true). Reads keep including "*"
rows; both DeleteWhereAsync terminals pass includeTenantAgnostic: false so a
tenant-scoped delete stays strict to the current tenant, matching the Dapper
store. An explicit tenantAgnostic: true delete is unchanged, and the agnostic
context (Tenant.Id == "*") can still manage "*" entities.
Copilot AI review requested due to automatic review settings August 4, 2026 00:43
@arledesma
arledesma force-pushed the fix/mongodb-agnostic-tenant-visibility branch from 362c5d1 to 5b94ad1 Compare August 4, 2026 00:43
@greptile-apps

greptile-apps Bot commented Aug 4, 2026

Copy link
Copy Markdown

PR author is not in the allowed authors list.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/modules/persistence/Elsa.Persistence.MongoDb/Common/MongoDbStore.cs:448

  • When includeTenantAgnostic is false (delete paths), this filter excludes TenantId == "*" even when there is no current tenant (tenantId is null). Since reads in the same method include "*" when tenantId is null, this can make "*" entities visible but not deletable in tenant-less scenarios. Consider only excluding "*" when a concrete tenant id is present.
            // Deletes pass includeTenantAgnostic: false so a tenant-scoped delete cannot remove a
            // shared "*" entity (the Dapper store keeps tenant-scoped deletes strict).
            queryable = includeTenantAgnostic
                ? queryable.Where(x => (x as Entity)!.TenantId == tenantId || (x as Entity)!.TenantId == Tenant.AgnosticTenantId)
                : queryable.Where(x => (x as Entity)!.TenantId == tenantId);

@arledesma

Copy link
Copy Markdown
Contributor Author

@sfmskywalker rebased the branch. no other changes, but it looks like the PR workflow may require another review.

@sfmskywalker
sfmskywalker requested a balanced review from Copilot September 6, 2026 23:23

@sfmskywalker sfmskywalker left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed current head d489a75 against #158 and current main. Explicit and agnostic tenant IDs are preserved, only null IDs are stamped, tenant reads include shared rows, and both delete paths retain a final tenant predicate. Container-backed validation passes all 8 MongoDB unit tests; the persistence module builds for .NET 8, 9, and 10. No actionable root-review findings remain.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approved

The implementation matches the stated tenant semantics and includes focused MongoDB regression coverage.

Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@sfmskywalker
sfmskywalker merged commit b6632da into elsa-workflows:main Sep 6, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants