{"record":{"id":"f1e21183d84a1786","repo":"elsa-workflows/elsa-core","slug":"cannot-overwrite-an-ai-proposal-that-belongs-to-another-user","errorCode":null,"errorMessage":"Cannot overwrite an AI proposal that belongs to another user.","messagePattern":"Cannot overwrite an AI proposal that belongs to another user\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/modules/Elsa.AI.Persistence.EFCore/Stores/EFCoreAIProposalStore.cs","lineNumber":126,"sourceCode":"\n        record.ReviewedBy = proposal.ReviewedBy;\n        record.ReviewedAt = proposal.ReviewedAt;\n        record.AppliedBy = proposal.AppliedBy;\n        record.AppliedAt = proposal.AppliedAt;\n    }\n\n    private static TEnum ParseEnum<TEnum>(string value, TEnum defaultValue) where TEnum : struct =>\n        Enum.TryParse<TEnum>(value, ignoreCase: true, out var result) ? result : defaultValue;\n\n    private static bool BelongsToTenant(string? storedTenantId, string? requestedTenantId) =>\n        string.Equals(NormalizeTenantId(storedTenantId), NormalizeTenantId(requestedTenantId), StringComparison.Ordinal);\n\n    private static string NormalizeTenantId(string? tenantId) => tenantId ?? \"\";\n\n    private static void ValidateUserOwnership(AIProposalRecord record, AIProposal proposal)\n    {\n        if (!string.IsNullOrWhiteSpace(record.CreatedBy) && !string.Equals(record.CreatedBy, proposal.CreatedBy, StringComparison.Ordinal))\n            throw new InvalidOperationException(\"Cannot overwrite an AI proposal that belongs to another user.\");\n    }\n\n    private static void Validate(AIProposal proposal)\n    {\n        if (string.IsNullOrWhiteSpace(proposal.Id))\n            throw new ArgumentException(\"A proposal ID is required.\", nameof(proposal));\n\n        if (string.IsNullOrWhiteSpace(proposal.ConversationId))\n            throw new ArgumentException(\"A proposal conversation ID is required.\", nameof(proposal));\n\n        if (string.IsNullOrWhiteSpace(proposal.CreatedBy))\n            throw new ArgumentException(\"A proposal creator is required.\", nameof(proposal));\n    }\n}\n","sourceCodeStart":108,"sourceCodeEnd":141,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.AI.Persistence.EFCore/Stores/EFCoreAIProposalStore.cs#L108-L141","documentation":"ValidateUserOwnership for proposals: when overwriting an existing proposal record that has a non-empty CreatedBy, the store requires the incoming proposal.CreatedBy to match exactly (ordinal). Mismatch throws InvalidOperationException to stop one creator overwriting another user's proposal.","triggerScenarios":"Calling SaveAsync (or the retry path) on an existing proposal where record.CreatedBy is non-whitespace and record.CreatedBy != proposal.CreatedBy.","commonSituations":"Reusing a proposal Id from another user's session; losing the authenticated user identity between create and update (CreatedBy empty or different); mapping bugs copying the wrong user field; importing proposals between users.","solutions":["Verify proposal.CreatedBy is populated with the same user who originally created the proposal.","Use fresh IDs for each user's proposals instead of shared/deterministic IDs.","Fetch the record first and only allow updates from the owning creator's context; otherwise create a new proposal.","For admin flows, delete and recreate rather than overwriting another creator's record."],"exampleFix":"// before\nproposal.CreatedBy = null; // identity lost in service layer\nawait proposalStore.SaveAsync(proposal);\n// after\nproposal.CreatedBy = originalCreatedBy ?? currentUser.Id;\nawait proposalStore.SaveAsync(proposal);","handlingStrategy":"validation","validationCode":"if (!string.IsNullOrWhiteSpace(proposal.Id) && string.IsNullOrWhiteSpace(proposal.CreatedBy)) throw new ArgumentException(\"CreatedBy is required and must match the record creator.\");","typeGuard":"static bool HasCreator(AIProposal p) => !string.IsNullOrWhiteSpace(p.CreatedBy);","tryCatchPattern":"try { await proposalStore.SaveAsync(proposal); }\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"belongs to another user\")) { /* reject update; require a fresh proposal for this creator */ }","preventionTips":["Always populate CreatedBy from the authenticated user.","Fetch-and-verify ownership before updating existing proposals.","Never share proposal IDs across creators.","Cover ownership rules in unit tests."],"tags":["ef-core","ai-proposals","ownership","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"}