canvas-lms/lib/permissions_helper.rb

227 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.key?(permission)
enrollments ||= participating_enrollments
ActiveRecord::Associations.preload(enrollments, :course)
ActiveRecord::Associations.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_grades => 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, :shard.to_proc) 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")
if unpublished.any?
all_applicable_enrollments += enrollment_scope.where(course_id: unpublished)
.where(type: %w[TeacherEnrollment TaEnrollment DesignerEnrollment StudentViewEnrollment]).to_a
end
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? do |e|
enabled_for_enrollment(e.role_id, e.type, e.date_based_state_in_db.to_sym, perm_hash, permission)
end))
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.squish
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:,
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