instructure/canvas-lms · error
cannot read sis ids
Error message
cannot read sis ids
What it means
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.
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)
Example fix
// before UserListV2.new(params[:user_list], search: :sis_user_id, can_read_sis: false) // after search = current_user.can_read_sis?(course) ? :sis_user_id : :unique_id UserListV2.new(params[:user_list], search: search, can_read_sis: current_user.can_read_sis?(course))
Defensive patterns
Strategy: validation
Validate before calling
raise 'need sis permission' if search_type == :sis_user_id && !acting_user.can_read_sis?(context)
Type guard
def can_resolve_sis?(user, context) = user&.can_read_sis?(context) == true
Try / catch
begin UserListV2.new(list, search: :sis_user_id, can_read_sis: can_read) rescue RuntimeError => e raise unless e.message == 'cannot read sis ids' UserListV2.new(list, search: :unique_id, can_read_sis: false) end
Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- A cross-listing referenced a non-existent section #
- A deleted cross-listing failed: #
- A new_id, '# ', referenced an existing # and the # with #…
- A new_integration_id, '#
- A student referenced a non-existent user #
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/757753fd278941ff.
Report an issue: GitHub.
Appendix: source
Thrown at lib/user_list_v2.rb:58
@all_results = []
@resolved_results = []
@duplicate_results = []
@missing_results = []
@root_account = root_account
@current_user = current_user
@can_read_sis = can_read_sis
unless SEARCH_TYPES.include?(search_type)
raise ParameterError, "search_type must be one of #{SEARCH_TYPES}"
end
parse_list(list_in)
case search_type
when "unique_id"
resolve_by_unique_id
when "sis_user_id"
raise "cannot read sis ids" unless @can_read_sis
resolve_by_sis_user_id
when "cc_path"
resolve_by_cc_path
end
resolve_duplicates_and_missing
end
attr_reader :errors, :addresses, :resolved_results, :duplicate_results, :missing_results
include UserList::Parsing
def as_json(*)
{
users: @resolved_results,
duplicates: @duplicate_results,
missing: @missing_results,
errors: @errorsView on GitHub (pinned to 1c9f0bb801)