Website : rimsha.abasa.com
backdoor
Home
Console
Upload
information
Create File
Create Folder
About
Tools
:
/
var
/
canvas
/
lib
/
services
/
Filename :
address_book.rb
back
Copy
# frozen_string_literal: true # # Copyright (C) 2016 - present Instructure, Inc. # # This file is part of Canvas. # # Canvas is free software: you can redistribute it and/or modify it under # the terms of the GNU Affero General Public License as published by the Free # Software Foundation, version 3 of the License. # # Canvas is distributed in the hope that it will be useful, but WITHOUT ANY # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # A PARTICULAR PURPOSE. See the GNU Affero General Public License for more # details. # # You should have received a copy of the GNU Affero General Public License along # with this program. If not, see <http://www.gnu.org/licenses/>. module Services class AddressBook # regarding these methods' parameters or options, generally: # # * `sender` is a User or a user ID # * `context` is an asset string ('course_123', or optionally subscoped # such as 'course_123_teachers') or a Course, CourseSection, or Group # * `users` is a list of Users or user IDs # * `search` is a string # * `exclude` is a list of Users or of user IDs # * `weak_checks` is a truthy/falsey value # # which of the users does the sender know, and what contexts do they and # the sender have in common? def self.common_contexts(sender, users, ignore_result = false) recipients(sender:, user_ids: users, ignore_result:).common_contexts end # which of the users have roles in the context and what are those roles? def self.roles_in_context(context, users, ignore_result = false) context = context.course if context.is_a?(CourseSection) recipients(context:, user_ids: users, ignore_result:).common_contexts end # which users: # # - does the sender know in the context and what are their roles in that # context? (sender present) # # --OR-- # # - have roles in the context and what are those roles? (sender absent; # admin view) # def self.known_in_context(sender, context, user_ids = nil, ignore_result = false) params = { sender:, context:, ignore_result: } params[:user_ids] = user_ids if user_ids response = recipients(params) [response.user_ids, response.common_contexts] end # how many users does the sender know in each of the contexts? def self.count_in_contexts(sender, contexts, ignore_result = false) counts = count_recipients(sender:, contexts:, ignore_result:) # map back from normalized to argument contexts.each do |ctx| serialized = serialize_context(ctx) if serialized != ctx counts[ctx] = counts.delete(serialized) end end counts end # of the users who are not in `exclude_ids` and whose name matches the # `search` term, if any, which: # # - does the sender know, and what are their common contexts with the # sender? (no context provided, sender must be) # # - does the sender know in the context and what are their roles in that # context? (context provided with sender) # # --OR-- # # - have roles in the context and what are those roles? (context provided # without sender; admin view) # def self.search_users(sender, options, service_options, ignore_result = false) params = options.slice(:search, :context, :exclude_ids, :weak_checks) params[:ignore_result] = ignore_result params[:sender] = sender # interpret pagination as specified in service_options params[:per_page] = service_options[:per_page] if service_options[:per_page] params[:cursor] = service_options[:cursor] if service_options[:cursor] # call out to service response = recipients(params) # interpret response [response.user_ids, response.common_contexts, response.cursors] end def self.recipients(params) Response.new(fetch("/recipients", query_params(params))) end def self.count_recipients(params) return {} if params[:contexts].blank? fetch("/recipients/counts", query_params(params))["counts"] || {} end def self.jwt # public only for testing, should not be used directly Canvas::Security.create_jwt({ iat: Time.now.to_i }, nil, jwt_secret, :HS512) rescue => e Canvas::Errors.capture_exception(:address_book, e) nil end class << self private def setting(key) DynamicSettings.find("address-book", default_ttl: 5.minutes)[key, failsafe: nil] end def app_host setting("app-host") end def jwt_secret Canvas::Security.base64_decode(setting("secret")) end # generic retrieve, parse def fetch(path, params = {}) url = app_host + path url += "?" + params.to_query unless params.empty? fallback = { "records" => [] } timeout_service_name = if params[:ignore_result] == 1 "address_book_performance_tap" else "address_book" end Canvas.timeout_protection(timeout_service_name) do response = CanvasHttp.get(url, { "Authorization" => "Bearer #{jwt}" }) if ![200, 202].include?(response.code.to_i) err = CanvasHttp::InvalidResponseCodeError.new(response.code.to_i) data = { extra: { url:, response: response.body }, tags: { type: "address_book_fault" } } Canvas::Errors.capture(err, data, :warn) return fallback elsif params[:ignore_result] == 1 return fallback else return JSON.parse(response.body) end end || fallback end # serialize logical params into query string values def query_params(params = {}) query_params = {} query_params[:cursor] = params[:cursor] if params[:cursor] query_params[:per_page] = params[:per_page] if params[:per_page] query_params[:search] = params[:search] if params[:search] if params[:sender] sender = params[:sender] sender = User.find(sender) unless sender.is_a?(User) visible_accounts = sender.associated_accounts.select { |account| account.grants_right?(sender, :read_roster) } restricted_courses = sender.all_courses.reject { |course| course.grants_right?(sender, :send_messages) } query_params[:for_sender] = serialize_item(sender) query_params[:visible_account_ids] = serialize_list(visible_accounts) unless visible_accounts.empty? query_params[:restricted_course_ids] = serialize_list(restricted_courses) unless restricted_courses.empty? end query_params[:in_context] = serialize_context(params[:context]) if params[:context] if params[:contexts] contexts = params[:contexts].map { |ctx| serialize_context(ctx) } query_params[:in_contexts] = contexts.join(",") end query_params[:user_ids] = serialize_list(params[:user_ids]) if params[:user_ids] query_params[:exclude_ids] = serialize_list(params[:exclude_ids]) if params[:exclude_ids] query_params[:weak_checks] = 1 if params[:weak_checks] query_params[:ignore_result] = 1 if params[:ignore_result] query_params end def serialize_item(item) Shard.global_id_for(item) end def serialize_list(list) # can be either IDs or objects (e.g. User) list.map { |item| serialize_item(item) }.join(",") end def serialize_context(context) if context.respond_to?(:global_asset_string) context.global_asset_string else context_type, context_id, scope = context.split("_", 3) global_context_id = serialize_item(context_id) asset_string = "#{context_type}_#{global_context_id}" asset_string += "_#{scope}" if scope asset_string end end end # /recipients returns data in the (JSON) shape: # # { # records: [ # { # 'user_id': '10000000000002', # 'contexts': [ # { 'context_type': 'course', 'context_id': '10000000000001', 'roles': ['TeacherEnrollment'] } # ], # cursor: ... # }, # { # 'user_id': '10000000000005', # 'contexts': [ # { 'context_type': 'course', 'context_id': '10000000000002', 'roles': ['StudentEnrollment'] }, # { 'context_type': 'group', 'context_id': '10000000000001', 'roles': ['Member'] } # ], # cursor: ... # } # ], # ... # } # # where `user_id` is a string representation of the recipient's global # user ID, `contexts` is a list of contexts they have in common with # the sender, and `cursor` is the cursor to pass to start at the next # record. each context states the type, id (again as a string # representation of the global ID), and roles the recipient has in that # context (to the knowledge of the sender). # # this class facilitates separating those pieces class Response def initialize(response) @response = response end # extract just the user IDs from the response, as an ordered list def user_ids @response["records"].map { |record| record["user_id"].to_i } end # reshape the records into a ruby hash with integers instead of strings # for IDs (but still global), user_ids promoted to keys, and context # types collated. e.g. for the above example, the transformed data would # have the (ruby) shape: # # { # 10000000000002 => { # courses: { 10000000000001 => ['TeacherEnrollment'] }, # groups: {} # }, # 10000000000005 => { # courses: { 10000000000002 => ['StudentEnrollment'] }, # groups: { 10000000000001 => ['Member'] } # } # } # def common_contexts common_contexts = {} @response["records"].each do |recipient| global_user_id = recipient["user_id"].to_i contexts = recipient["contexts"] common_contexts[global_user_id] ||= { courses: {}, groups: {} } contexts.each do |context| context_type = context["context_type"].pluralize.to_sym next unless common_contexts[global_user_id].key?(context_type) global_context_id = context["context_id"].to_i common_contexts[global_user_id][context_type][global_context_id] = context["roles"] end end common_contexts end # extract the next page cursor from the response def cursors @response["records"].pluck("cursor") end end end end