{"record":{"id":"3f33f5d197c1b04a","repo":"instructure/canvas-lms","slug":"conversation-ids-needs-to-be-scoped-to-a-user","errorCode":null,"errorMessage":"conversation_ids needs to be scoped to a user","messagePattern":"conversation_ids needs to be scoped to a user","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/models/conversation_participant.rb","lineNumber":556,"sourceCode":"    @last_message ||= messages.human.first if last_message_at\n  end\n\n  attr_writer :last_authored_message\n\n  def last_authored_message\n    @last_authored_message ||= conversation.shard.activate { messages.human.by_user(user_id).first } if visible_last_authored_at\n  end\n\n  def self.preload_latest_messages(conversations, author)\n    # preload last_message\n    ConversationMessage.preload_latest conversations.select(&:last_message_at)\n    # preload last_authored_message\n    ConversationMessage.preload_latest conversations.select(&:visible_last_authored_at), author\n  end\n\n  def self.conversation_ids\n    where_predicates = all.where_clause.instance_variable_get(:@predicates)\n    raise \"conversation_ids needs to be scoped to a user\" unless where_predicates.any? do |v|\n      if v.is_a?(Arel::Nodes::Binary) && v.left.is_a?(Arel::Attributes::Attribute)\n        v.left.name == \"user_id\"\n      else\n        v =~ /user_id (?:= |IN \\()\\d+/\n      end\n    end\n\n    order = \"last_message_at DESC\" unless all.order_values.present?\n    self.order(order).pluck(:conversation_id)\n  end\n\n  def self.users_by_conversation_shard(user_ids)\n    { Shard.current => user_ids }\n  end\n\n  def update_one(update_params)\n    case update_params[:event]\n","sourceCodeStart":538,"sourceCodeEnd":574,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/models/conversation_participant.rb#L538-L574","documentation":"ConversationParticipant.conversation_ids is designed as a scope-chaining helper that requires the relation be scoped to a specific user_id. It introspects the relation's WHERE predicates and raises if none constrain user_id, because running it unscoped would leak conversation IDs across users.","triggerScenarios":"Calling ConversationParticipant.conversation_ids on a relation without a where(user_id: ...) clause, e.g. ConversationParticipant.conversation_ids directly, or after chaining only non-user predicates (workflow_state, etc.).","commonSituations":"Refactoring code that dropped the user scope; building ad-hoc console/admin queries without scoping; composing scopes where the user_id condition is applied later than this call.","solutions":["Scope the relation to a user first: user.conversation_participants.conversation_ids or ConversationParticipant.where(user_id: user).conversation_ids","Reorder scope chaining so where(user_id:) is applied before calling conversation_ids","In the regex-compatible path, a raw SQL string predicate like 'user_id = 5' also satisfies the check"],"exampleFix":"// before\nConversationParticipant.conversation_ids\n// after\nConversationParticipant.where(user_id: user).conversation_ids","handlingStrategy":"validation","validationCode":"raise 'scope to a user first' unless relation.where_clause Predicates inspect — simply always build as ConversationParticipant.where(user_id: user).conversation_ids","typeGuard":"null","tryCatchPattern":"null","preventionTips":["Never call class-level ConversationParticipant.conversation_ids directly","Chain user scoping before any relation-consuming helper","Prefer user.conversation_participants for user-scoped queries"],"tags":["ruby","rails","activerecord","scoping"],"backgroundTag":"missing-required-argument","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}