canvas-lms/lib/permissions_helper.rb

223 lines
11 KiB
Ruby

# frozen_string_literal: true
#
# Copyright (C) 2018 - 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 PermissionsHelper
def manageable_enrollments_by_permission(permission, enrollments=nil)
permission = permission.to_sym
raise "invalid permission" unless RoleOverride.permissions.keys.include?(permission)
enrollments ||= participating_enrollments
ActiveRecord::Associations::Preloader.new.preload(enrollments, :course)
ActiveRecord::Associations::Preloader.new.preload(enrollments, :enrollment_state)
allowed_ens = []
Shard.partition_by_shard(enrollments) do |sharded_enrollments|
perms_hash = get_permissions_info_by_account(sharded_enrollments.map(&:course), sharded_enrollments, [permission])
allowed_ens += sharded_enrollments.select do |e|
perm_hash = perms_hash[e.course.account_id]
perm_hash && (enabled_for_account_admin(perm_hash, permission) ||
enabled_for_enrollment(e.role_id, e.type, e.state_based_on_date, perm_hash, permission))
end
end
allowed_ens
end
# will return a hash linking global course ids with precalculated permissions
# e.g. {10000000000001 => {:manage_calendar => true, :manage_assignments => false}}
def precalculate_permissions_for_courses(courses, permissions, loaded_root_accounts=[])
courses = courses.reject(&:deleted?) # just in case
permissions = permissions.map(&:to_sym)
nonexistent_permissions = permissions - RoleOverride.permissions.keys
raise "invalid permissions - #{nonexistent_permissions}" if nonexistent_permissions.any?
precalculated_map = {}
Shard.partition_by_shard(courses, lambda(&:shard)) do |sharded_courses|
unpublished, published = sharded_courses.partition(&:unpublished?)
all_applicable_enrollments = []
enrollment_scope = Enrollment.not_inactive_by_date.for_user(self).select("enrollments.*, enrollment_states.state AS date_based_state_in_db")
all_applicable_enrollments += enrollment_scope.where(:course_id => unpublished).
where(:type => ['TeacherEnrollment', 'TaEnrollment', 'DesignerEnrollment', 'StudentViewEnrollment']).to_a if unpublished.any?
all_applicable_enrollments += enrollment_scope.where(:course_id => published).to_a if published.any?
grouped_enrollments = all_applicable_enrollments.group_by(&:course_id)
sharded_courses.each do |course|
grouped_enrollments[course.id] ||= []
grouped_enrollments[course.id].each{|e| e.course = course}
end
root_account_ids = sharded_courses.map(&:root_account_id).uniq
unloaded_ra_ids = root_account_ids - loaded_root_accounts.map(&:id)
root_accounts = loaded_root_accounts + (unloaded_ra_ids.any? ? Account.where(:id => unloaded_ra_ids).to_a : [])
roles = root_accounts.map{|ra| self.roles(ra)}.flatten.uniq
return nil if roles.include?('consortium_admin') # cross-shard precalculation doesn't work - just fallback to the usual calculations
is_account_admin = roles.include?('admin')
account_roles = is_account_admin ? AccountUser.where(user: self).active.preload(:role).to_a : []
all_permissions_data = get_permissions_info_by_account(sharded_courses, all_applicable_enrollments, permissions, account_roles)
sharded_courses.each do |course|
course_permissions = {}
permissions.each do |permission|
perm_hash = all_permissions_data[course.account_id]
course_permissions[permission] = !!(perm_hash &&
(enabled_for_account_admin(perm_hash, permission) || grouped_enrollments[course.id].any?{|e|
enabled_for_enrollment(e.role_id, e.type, e.date_based_state_in_db.to_sym, perm_hash, permission)}))
end
# load some other permissions that we can possibly skip calculating - we can't say for sure they're false but we can mark them true
active_ens = grouped_enrollments[course.id].select{|e| e.date_based_state_in_db.to_sym == :active}
course_permissions[:read] = true if active_ens.any?
if active_ens.any?(&:student?)
course_permissions[:read_grades] = true
course_permissions[:participate_as_student] = true
end
if grouped_enrollments[course.id].any?(&:admin?)
course_permissions[:read] = true
course_permissions[:read_as_admin] = true
elsif !is_account_admin
course_permissions[:read_as_admin] = false # wait a second i can totally mark this one as false if they don't have any account users
end
precalculated_map[course.global_id] = course_permissions
end
end
precalculated_map
end
def enabled_for_account_admin(perm_hash, permission)
# enabled by account role
permission_details = RoleOverride.permissions[permission]
true_for_roles = permission_details[:true_for]
available_to_roles = permission_details[:available_to]
perm_hash[:admin_roles].any? do |role|
if available_to_roles.include?(role.base_role_type)
role_on = perm_hash.dig(:role_overrides, [role.id, permission], :enabled) && perm_hash.dig(:role_overrides, [role.id, permission], :self)
role_on.nil? ? true_for_roles.include?(role.base_role_type) : role_on
end
end
end
def enabled_for_enrollment(role_id, role_type, enrollment_state, perm_hash, permission)
role_type = "StudentEnrollment" if role_type == "StudentViewEnrollment"
permission_details = RoleOverride.permissions[permission]
true_for_roles = permission_details[:true_for]
available_to_roles = permission_details[:available_to]
# enabled for enrollment role
if enrollment_state == :completed
concluded_roles = permission_details[:applies_to_concluded]
return false unless concluded_roles
return false if concluded_roles.is_a?(Array) && !concluded_roles.include?(role_type)
elsif enrollment_state != :active # future
return false if permission_details[:restrict_future_enrollments]
end
if available_to_roles.include?(role_type)
role_on = perm_hash.dig(:role_overrides, [role_id, permission], :enabled) && perm_hash.dig(:role_overrides, [role_id, permission], :self)
role_on.nil? ? true_for_roles.include?(role_type) : role_on
else
false
end
end
# examines permissions for accounts related to enrollments and returns a map from account_id to a hash containing
# sub_accounts: set of subaccount ids
# role_overrides: map from role id to hash containing :enabled, :locked, :self, :children
# (these are calculated for the specific account, taking inheritance and locking into account)
# admin_roles: set of Roles the user has active account memberships for in this account
def get_permissions_info_by_account(courses, enrollments, permissions, account_roles=nil)
account_roles ||= AccountUser.where(user: self).active.preload(:role).to_a
role_ids = (enrollments.map(&:role_id) + account_roles.map(&:role_id)).uniq
root_account_ids = courses.map(&:root_account_id).uniq
query = <<~SQL
WITH RECURSIVE t(id, name, parent_account_id, role_id, enabled, locked, self, children, permission) AS (
SELECT accounts.id, name, parent_account_id, ro.role_id, ro.enabled, ro.locked,
ro.applies_to_self, ro.applies_to_descendants, ro.permission
FROM #{Account.quoted_table_name}
LEFT JOIN #{RoleOverride.quoted_table_name} AS ro ON ro.context_id = accounts.id
AND ro.context_type = 'Account'
AND ro.permission IN (:permissions)
AND ro.role_id IN (:role_ids)
WHERE accounts.id IN (:account_ids)
UNION
SELECT accounts.id, accounts.name, accounts.parent_account_id, ro.role_id, ro.enabled,
ro.locked, ro.applies_to_self, ro.applies_to_descendants, ro.permission
FROM #{Account.quoted_table_name}
INNER JOIN t ON accounts.id = t.parent_account_id
LEFT JOIN #{RoleOverride.quoted_table_name} AS ro ON ro.context_id = accounts.id
AND ro.context_type = 'Account'
AND ro.permission IN (:permissions)
AND ro.role_id IN (:role_ids)
WHERE accounts.workflow_state = 'active'
)
SELECT *
FROM t
SQL
params = {
account_ids: courses.map(&:account_id),
permissions: permissions,
role_ids: role_ids
}
rows = User.connection.execute(sanitize_sql([query, params]))
hash_permissions(rows, root_account_ids, account_roles)
end
private
def hash_permissions(rows, root_account_ids, account_roles)
perms_hash = {}
new_perm = {sub_accounts: Set.new, role_overrides: {}, admin_roles: Set.new}
root_account_ids.each{|ri| perms_hash[ri] = new_perm.deep_dup}
rows.each do |row|
account_id = row['id']
parent_id = row['parent_account_id']
role_id = row['role_id']
permission = row['permission']
perms_hash[account_id] ||= new_perm.deep_dup
if role_id && permission
override = {enabled: row['enabled'], locked: row['locked'], self: row['self'], children: row['children']}
perms_hash[account_id][:role_overrides][[role_id, permission.to_sym]] = override
end
perms_hash[account_id][:admin_roles] += account_roles.select{|au| au.account_id == account_id}.map(&:role)
if parent_id
perms_hash[parent_id] ||= new_perm.deep_dup
perms_hash[parent_id][:sub_accounts] << row['id']
end
end
root_account_ids.each do |rai|
fill_permissions_recursive(perms_hash, rai, perms_hash[rai])
end
perms_hash
end
def fill_permissions_recursive(perms_hash, id, parent_hash)
perm_hash = perms_hash[id]
unless parent_hash == perm_hash
parent_hash[:role_overrides].each_pair do |ro, parent_values|
next if parent_values[:children] == false && !parent_values[:locked]
perm_hash[:role_overrides][ro] = parent_values.slice(:locked, :enabled, :children) if perm_hash[:role_overrides][ro].nil? || parent_values[:locked]
perm_hash[:role_overrides][ro][:self] = parent_values[:children] if perm_hash[:role_overrides][ro][:self].nil?
end
perm_hash[:admin_roles] += parent_hash[:admin_roles]
end
perm_hash[:sub_accounts].each{|sa| fill_permissions_recursive(perms_hash, sa, perm_hash)}
end
end