{"record":{"id":"757753fd278941ff","repo":"instructure/canvas-lms","slug":"cannot-read-sis-ids","errorCode":null,"errorMessage":"cannot read sis ids","messagePattern":"cannot read sis ids","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"lib/user_list_v2.rb","lineNumber":58,"sourceCode":"    @all_results = []\n    @resolved_results = []\n    @duplicate_results = []\n    @missing_results = []\n\n    @root_account = root_account\n    @current_user = current_user\n    @can_read_sis = can_read_sis\n    unless SEARCH_TYPES.include?(search_type)\n      raise ParameterError, \"search_type must be one of #{SEARCH_TYPES}\"\n    end\n\n    parse_list(list_in)\n\n    case search_type\n    when \"unique_id\"\n      resolve_by_unique_id\n    when \"sis_user_id\"\n      raise \"cannot read sis ids\" unless @can_read_sis\n\n      resolve_by_sis_user_id\n    when \"cc_path\"\n      resolve_by_cc_path\n    end\n    resolve_duplicates_and_missing\n  end\n\n  attr_reader :errors, :addresses, :resolved_results, :duplicate_results, :missing_results\n\n  include UserList::Parsing\n\n  def as_json(*)\n    {\n      users: @resolved_results,\n      duplicates: @duplicate_results,\n      missing: @missing_results,\n      errors: @errors","sourceCodeStart":40,"sourceCodeEnd":76,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/lib/user_list_v2.rb#L40-L76","documentation":"UserListV2#initialize parses the address list and dispatches on search_type. When search_type is 'sis_user_id' but the caller lacks SIS read permission (@can_read_sis is false), it raises 'cannot read sis ids' before attempting resolution, to prevent leaking SIS identifiers to unauthorized users.","triggerScenarios":"Instantiating UserList.new(list, search: :sis_user_id, can_read_sis: false) — e.g. from the course people page, MessageableUser API, or a controller context where the current user lacks :read_sis permission on the account/course — with a list containing SIS IDs.","commonSituations":"Developers calling UserListV2 in background jobs without passing the permission context; non-admin users entering SIS IDs in the add-people dialog; recently tightened SIS permission checks surfacing in older integrations.","solutions":["Pass can_read_sis: true only when the acting user truly has :read_sis permission — and then grant the user that permission","Fall back to search_type 'unique_id' when the caller cannot read SIS IDs","Strip SIS IDs from the input list when permissions are absent","Verify the permission object passed to UserListV2 (it should be the enrollment/admin context, not nil)"],"exampleFix":"// before\nUserListV2.new(params[:user_list], search: :sis_user_id, can_read_sis: false)\n// after\nsearch = current_user.can_read_sis?(course) ? :sis_user_id : :unique_id\nUserListV2.new(params[:user_list], search: search, can_read_sis: current_user.can_read_sis?(course))","handlingStrategy":"validation","validationCode":"raise 'need sis permission' if search_type == :sis_user_id && !acting_user.can_read_sis?(context)","typeGuard":"def can_resolve_sis?(user, context) = user&.can_read_sis?(context) == true","tryCatchPattern":"begin\n  UserListV2.new(list, search: :sis_user_id, can_read_sis: can_read)\nrescue RuntimeError => e\n  raise unless e.message == 'cannot read sis ids'\n  UserListV2.new(list, search: :unique_id, can_read_sis: false)\nend","preventionTips":["Check :read_sis permission before choosing the search strategy","Never hardcode can_read_sis: true in shared code","Strip SIS IDs from inputs when permission is absent"],"tags":["permissions","sis","user-lookup"],"backgroundTag":"permission-denied","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"}