{"record":{"id":"f44a22a879dca8c9","repo":"elsa-workflows/elsa-core","slug":"cannot-overwrite-an-ai-proposal-that-belongs-to-another","errorCode":null,"errorMessage":"Cannot overwrite an AI proposal that belongs to another tenant.","messagePattern":"Cannot overwrite an AI proposal that belongs to another tenant\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/modules/Elsa.AI.Persistence.EFCore/Stores/EFCoreAIProposalStore.cs","lineNumber":34,"sourceCode":"            x => x.Id == id && (x.TenantId ?? \"\") == normalizedTenantId,\n            cancellationToken);\n        return record == null ? null : Map(record);\n    }\n\n    public async ValueTask SaveAsync(AIProposal proposal, CancellationToken cancellationToken = default)\n    {\n        Validate(proposal);\n        var isNew = false;\n        var record = await dbContext.Proposals.FindAsync([proposal.Id], cancellationToken);\n        if (record == null)\n        {\n            record = new AIProposalRecord { Id = proposal.Id };\n            dbContext.Proposals.Add(record);\n            isNew = true;\n        }\n        else if (!BelongsToTenant(record.TenantId, proposal.TenantId))\n        {\n            throw new InvalidOperationException(\"Cannot overwrite an AI proposal that belongs to another tenant.\");\n        }\n        else\n        {\n            ValidateUserOwnership(record, proposal);\n        }\n\n        Map(proposal, record);\n\n        try\n        {\n            await dbContext.SaveChangesAsync(cancellationToken);\n        }\n        catch (DbUpdateException e) when (isNew)\n        {\n            await RetryAsUpdateAsync(proposal, e, cancellationToken);\n        }\n    }\n","sourceCodeStart":16,"sourceCodeEnd":52,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.AI.Persistence.EFCore/Stores/EFCoreAIProposalStore.cs#L16-L52","documentation":"EFCoreAIProposalStore.SaveAsync finds or creates a record for the proposal; when a record already exists, it verifies tenant ownership before overwriting. If the existing record's TenantId does not match proposal.TenantId (after NormalizeTenantId, null/empty = default \"\"), it throws InvalidOperationException to block cross-tenant overwrites of AI proposals.","triggerScenarios":"Calling SaveAsync with a proposal whose Id already exists in the Proposals table but is owned by a different tenant (record.TenantId != normalized proposal.TenantId).","commonSituations":"Deterministic proposal IDs (e.g. derived from conversation content) colliding across tenants; tenant context lost between creation and update so the update resolves to the default tenant; data copied between tenant databases with shared IDs.","solutions":["Use unique proposal IDs per tenant (Guids) so cross-tenant collisions are impossible.","Ensure tenant ID flows consistently into SaveAsync on both create and update (check tenancy middleware).","Inspect the conflicting record's owner tenant; delete or migrate it if created by a bug.","Handle InvalidOperationException in your service and surface a tenant-mismatch error to the caller."],"exampleFix":"// before\nawait proposalStore.SaveAsync(new AIProposal { Id = fixedId, ConversationId = cid, CreatedBy = user }); // TenantId null\n// after\nawait proposalStore.SaveAsync(new AIProposal { Id = Guid.NewGuid().ToString(), ConversationId = cid, CreatedBy = user, TenantId = tenantContext.Id });","handlingStrategy":"try-catch","validationCode":"var existing = await dbContext.Proposals.FindAsync([proposal.Id]);\nif (existing != null && !string.Equals(existing.TenantId ?? \"\", proposal.TenantId ?? \"\", StringComparison.Ordinal))\n    throw new InvalidOperationException(\"Cross-tenant proposal write blocked.\");","typeGuard":"bool TenantMatches(string? recordTenant, string? tenantId) => string.Equals(recordTenant ?? \"\", tenantId ?? \"\", StringComparison.Ordinal);","tryCatchPattern":"try { await proposalStore.SaveAsync(proposal); }\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"belongs to another tenant\")) { /* return 409/403-style conflict; do not retry blindly */ }","preventionTips":["Use per-tenant-unique proposal IDs.","Ensure tenant middleware supplies TenantId on every save.","Test tenant-scoping behavior (read and write paths) in CI.","Audit any data-copy/import jobs for tenant ID preservation."],"tags":["ef-core","multi-tenancy","ai-proposals","data-integrity"],"backgroundTag":"permission-denied","analyzedSha":"fe9217bdfa0e27f0e09e45006eb6898f616e513d","analyzedAt":"2026-09-13T20:32:34.702Z","contentChangedAt":"2026-09-13T20:32:34.702Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}