{"record":{"id":"65108f49e664f03f","repo":"elsa-workflows/elsa-core","slug":"cannot-overwrite-an-ai-conversation-that-belongs-to-another-65108f","errorCode":null,"errorMessage":"Cannot overwrite an AI conversation that belongs to another user.","messagePattern":"Cannot overwrite an AI conversation that belongs to another user\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/modules/Elsa.AI.Persistence.EFCore/Stores/EFCoreAIConversationStore.cs","lineNumber":225,"sourceCode":"        var expiresAt = conversation.RetentionExpiresAt;\n        if (expiresAt == null)\n            return false;\n\n        return expiresAt <= DateTimeOffset.UtcNow;\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(AIConversationRecord record, AIConversation conversation)\n    {\n        if (!string.IsNullOrWhiteSpace(record.UserId) && !string.Equals(record.UserId, conversation.UserId, StringComparison.Ordinal))\n            throw new InvalidOperationException(\"Cannot overwrite an AI conversation that belongs to another user.\");\n    }\n\n    private static void Validate(AIConversation conversation)\n    {\n        if (string.IsNullOrWhiteSpace(conversation.Id))\n            throw new ArgumentException(\"A conversation ID is required.\", nameof(conversation));\n\n        if (string.IsNullOrWhiteSpace(conversation.UserId))\n            throw new ArgumentException(\"A conversation user ID is required.\", nameof(conversation));\n    }\n}\n","sourceCodeStart":207,"sourceCodeEnd":237,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.AI.Persistence.EFCore/Stores/EFCoreAIConversationStore.cs#L207-L237","documentation":"ValidateUserOwnership guards conversations that already have a UserId: when overwriting an existing record, if the stored record's UserId is non-empty and does not exactly match (ordinal) the UserId on the conversation being saved, the store throws InvalidOperationException. This prevents one user from overwriting another user's AI conversation with the same Id.","triggerScenarios":"Calling SaveAsync (directly or through RetryAsUpdateAsync) on an existing conversation record where record.UserId is non-whitespace and record.UserId != conversation.UserId (ordinal comparison).","commonSituations":"Reusing conversation IDs across user sessions; a deserialization or mapping bug dropping/altering the UserId before save; shared test fixtures reusing the same conversation Id for different users; an anonymous context (UserId empty on the incoming object) attempting to overwrite a user-owned conversation.","solutions":["Verify the AIConversation.UserId is correctly populated before SaveAsync; it must exactly match the record's stored UserId (ordinal).","Generate a fresh conversation Id for each user/session instead of reusing a fixed Id.","Query the existing record first and map updates from the actual owning user's context.","If ownership genuinely changed through an admin path, delete and recreate the conversation rather than overwriting."],"exampleFix":"// before\nconversation.UserId = HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier); // may be null in service context\nawait store.SaveAsync(conversation);\n// after\nif (conversation.UserId != storedUserId) conversation = storedConversation with updates; // keep original owner\nawait store.SaveAsync(conversation);","handlingStrategy":"validation","validationCode":"if (!string.IsNullOrWhiteSpace(conversation.Id) && conversation.UserId is null || string.IsNullOrWhiteSpace(conversation.UserId))\n    throw new ArgumentException(\"UserId is required and must match the record owner.\");","typeGuard":"static bool HasOwner(AIConversation c) => !string.IsNullOrWhiteSpace(c.UserId);","tryCatchPattern":"try { await store.SaveAsync(conversation); }\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"belongs to another user\")) { /* reject the update; do not overwrite the other user's conversation */ }","preventionTips":["Always set UserId from the authenticated principal before saving.","Never reuse conversation IDs across users.","Fetch the existing record and verify ownership in your service before updating.","Add integration tests covering second-user overwrite attempts."],"tags":["ef-core","ai-conversations","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"}