346 lines
13 KiB
Ruby
346 lines
13 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
#
|
|
# Copyright (C) 2011 - 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/>.
|
|
#
|
|
|
|
class Role < ActiveRecord::Base
|
|
NULL_ROLE_TYPE = "NoPermissions"
|
|
|
|
ENROLLMENT_TYPES = %w[StudentEnrollment TeacherEnrollment TaEnrollment DesignerEnrollment ObserverEnrollment].freeze
|
|
|
|
DEFAULT_ACCOUNT_TYPE = "AccountMembership"
|
|
ACCOUNT_TYPES = ["AccountAdmin", "AccountMembership"].freeze
|
|
|
|
BASE_TYPES = (ACCOUNT_TYPES + ENROLLMENT_TYPES + [NULL_ROLE_TYPE]).freeze
|
|
KNOWN_TYPES = (BASE_TYPES +
|
|
%w[StudentViewEnrollment
|
|
NilEnrollment
|
|
teacher
|
|
ta
|
|
designer
|
|
student
|
|
observer]).freeze
|
|
|
|
module AssociationHelper
|
|
# this is an override to take advantage of built-in role caching since those are by far the most common
|
|
def role
|
|
return super if association(:role).loaded?
|
|
|
|
self.role = shard.activate do
|
|
# Use `default_canvas_role` even though `default_role` sounds better since default_role is a rails method in rails >= 6.1
|
|
Role.get_role_by_id(read_attribute(:role_id)) || (respond_to?(:default_canvas_role) ? default_canvas_role : nil)
|
|
end
|
|
end
|
|
|
|
def self.included(klass)
|
|
klass.before_save(:resolve_cross_account_role)
|
|
end
|
|
|
|
def resolve_cross_account_role
|
|
if will_save_change_to_role_id? && respond_to?(:root_account_id) && root_account_id && role.root_account_id != root_account_id
|
|
self.role = role.role_for_root_account_id(root_account_id)
|
|
end
|
|
end
|
|
end
|
|
|
|
belongs_to :account
|
|
belongs_to :root_account, class_name: "Account"
|
|
has_many :role_overrides
|
|
|
|
before_validation :infer_root_account_id, if: :belongs_to_account?
|
|
|
|
validate :ensure_unique_name_for_account, if: :belongs_to_account?
|
|
validates :name, :workflow_state, presence: true
|
|
validates :account_id, presence: { if: :belongs_to_account? }
|
|
|
|
validates :base_role_type, inclusion: { in: BASE_TYPES, message: -> { t("is invalid") } }
|
|
validates :name, exclusion: { in: KNOWN_TYPES, unless: :built_in?, message: -> { t("is reserved") } }
|
|
validate :ensure_non_built_in_name
|
|
|
|
def role_for_root_account_id(target_root_account_id)
|
|
if built_in? &&
|
|
root_account_id != target_root_account_id &&
|
|
(target_role = Role.get_built_in_role(name, root_account_id: target_root_account_id))
|
|
target_role
|
|
else
|
|
self
|
|
end
|
|
end
|
|
|
|
def ensure_unique_name_for_account
|
|
if active?
|
|
scope = Role.where("name = ? AND account_id = ? AND workflow_state = ?", name, account_id, "active")
|
|
if new_record? ? scope.exists? : scope.where.not(id:).exists?
|
|
errors.add(:label, t(:duplicate_role, "A role with this name already exists"))
|
|
false
|
|
end
|
|
end
|
|
end
|
|
|
|
def ensure_non_built_in_name
|
|
if !built_in? && Role.built_in_roles(root_account_id:).map(&:label).include?(name)
|
|
errors.add(:label, t(:duplicate_role, "A role with this name already exists"))
|
|
false
|
|
end
|
|
end
|
|
|
|
def infer_root_account_id
|
|
unless account
|
|
errors.add(:account_id)
|
|
throw :abort
|
|
end
|
|
self.root_account_id = account.resolved_root_account_id
|
|
end
|
|
|
|
include Workflow
|
|
workflow do
|
|
state :active do
|
|
event :deactivate, transitions_to: :inactive
|
|
end
|
|
state :inactive do
|
|
event :activate, transitions_to: :active
|
|
end
|
|
state :built_in # for previously built-in roles
|
|
state :deleted
|
|
end
|
|
|
|
def belongs_to_account?
|
|
!built_in? && !deleted?
|
|
end
|
|
|
|
def self.built_in_roles(root_account_id:)
|
|
raise "root_account_id required" unless root_account_id
|
|
|
|
# giving up on in-process built-in role caching because it's probably not really worth it anymore
|
|
RequestCache.cache("built_in_roles", root_account_id) do
|
|
local_id, shard = Shard.local_id_for(root_account_id)
|
|
(shard || Shard.current).activate do
|
|
Role.where(workflow_state: "built_in", root_account_id: local_id).order(:id).to_a
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.built_in_course_roles(root_account_id:)
|
|
built_in_roles(root_account_id:).select(&:course_role?)
|
|
end
|
|
|
|
def self.visible_built_in_roles(root_account_id:)
|
|
built_in_roles(root_account_id:).select(&:visible?)
|
|
end
|
|
|
|
def self.get_role_by_id(id)
|
|
return nil unless id
|
|
return nil if id.is_a?(String) && id !~ Api::ID_REGEX
|
|
|
|
Role.where(id:).take # giving up on built-in role caching because it's silly now and we should just preload more
|
|
end
|
|
|
|
def self.get_built_in_role(name, root_account_id:)
|
|
built_in_roles(root_account_id:).detect { |role| role.name == name }
|
|
end
|
|
|
|
def ==(other_role)
|
|
if other_role.is_a?(Role) && built_in? && other_role.built_in?
|
|
name == other_role.name # be equivalent even if they're on different shards/root_accounts
|
|
else
|
|
super
|
|
end
|
|
end
|
|
|
|
def visible?
|
|
active? || (built_in? && !["AccountMembership", "NoPermissions"].include?(name))
|
|
end
|
|
|
|
def account_role?
|
|
ACCOUNT_TYPES.include?(base_role_type)
|
|
end
|
|
|
|
def course_role?
|
|
ENROLLMENT_TYPES.include?(base_role_type)
|
|
end
|
|
|
|
def label
|
|
if built_in?
|
|
if course_role?
|
|
RoleOverride.enrollment_type_labels.detect { |label| label[:name] == name }[:label].call
|
|
elsif name == "AccountAdmin"
|
|
RoleOverride::ACCOUNT_ADMIN_LABEL.call
|
|
else
|
|
name
|
|
end
|
|
else
|
|
name
|
|
end
|
|
end
|
|
|
|
# Should order course roles so we get "StudentEnrollment", custom student roles, "Teacher Enrollment", custom teacher roles, etc
|
|
# then sort alphabetically within groups
|
|
def display_sort_index
|
|
group_order = if course_role?
|
|
(ENROLLMENT_TYPES.index(base_role_type) * 2) + (built_in? ? 0 : 1)
|
|
else
|
|
built_in? ? 0 : 1
|
|
end
|
|
[group_order, Canvas::ICU.collation_key(label)]
|
|
end
|
|
|
|
alias_method :destroy_permanently!, :destroy
|
|
def destroy
|
|
self.workflow_state = "deleted"
|
|
self.deleted_at = Time.now.utc
|
|
save!
|
|
end
|
|
|
|
scope :not_deleted, -> { where("roles.workflow_state IN ('active', 'inactive')") }
|
|
scope :deleted, -> { where(workflow_state: "deleted") }
|
|
scope :active, -> { where(workflow_state: "active") }
|
|
scope :inactive, -> { where(workflow_state: "inactive") }
|
|
scope :for_courses, -> { where(base_role_type: ENROLLMENT_TYPES) }
|
|
scope :for_accounts, -> { where(base_role_type: ACCOUNT_TYPES) }
|
|
scope :full_account_admin, -> { where(base_role_type: "AccountAdmin") }
|
|
scope :custom_account_admin_with_permission, lambda { |permission|
|
|
where(base_role_type: "AccountMembership")
|
|
.where("EXISTS (
|
|
SELECT 1
|
|
FROM #{RoleOverride.quoted_table_name}
|
|
WHERE role_overrides.role_id = roles.id
|
|
AND role_overrides.permission = ?
|
|
AND role_overrides.enabled = ?
|
|
)",
|
|
permission,
|
|
true)
|
|
}
|
|
|
|
# Returns a list of hashes for each base enrollment type, and each will have a
|
|
# custom_roles key, each will look like:
|
|
# [{:base_role_name => "StudentEnrollment",
|
|
# :name => "StudentEnrollment",
|
|
# :label => "Student",
|
|
# :plural_label => "Students",
|
|
# :custom_roles =>
|
|
# [{:base_role_name => "StudentEnrollment",
|
|
# :name => "weirdstudent",
|
|
# :asset_string => "role_4"
|
|
# :label => "weirdstudent"}]},
|
|
# ]
|
|
def self.all_enrollment_roles_for_account(account, include_inactive = false)
|
|
custom_roles = account.available_custom_course_roles(include_inactive)
|
|
RoleOverride.enrollment_type_labels.map do |br|
|
|
new = br.clone
|
|
new[:id] = Role.get_built_in_role(br[:name], root_account_id: account.resolved_root_account_id).id
|
|
new[:label] = br[:label].call
|
|
new[:plural_label] = br[:plural_label].call
|
|
new[:custom_roles] = custom_roles.select { |cr| cr.base_role_type == new[:base_role_name] }.map do |cr|
|
|
{ id: cr.id, base_role_name: cr.base_role_type, name: cr.name, label: cr.name, asset_string: cr.asset_string, workflow_state: cr.workflow_state }
|
|
end
|
|
new
|
|
end
|
|
end
|
|
|
|
# returns same hash as all_enrollment_roles_for_account but adds enrollment
|
|
# counts for the given course to each item
|
|
def self.custom_roles_and_counts_for_course(course, user, include_inactive = false)
|
|
users_scope = course.users_visible_to(user)
|
|
built_in_role_ids = Role.built_in_course_roles(root_account_id: course.root_account_id).map(&:id)
|
|
base_counts = users_scope.where(enrollments: { role_id: built_in_role_ids })
|
|
.group("enrollments.type").select("users.id").distinct.count
|
|
role_counts = users_scope.where.not(enrollments: { role_id: built_in_role_ids })
|
|
.group("enrollments.role_id").select("users.id").distinct.count
|
|
|
|
@enrollment_types = Role.all_enrollment_roles_for_account(course.account, include_inactive)
|
|
@enrollment_types.each do |base_type|
|
|
base_type[:count] = base_counts[base_type[:name]] || 0
|
|
base_type[:custom_roles].each do |custom_role|
|
|
id = custom_role[:id]
|
|
custom_role[:count] = role_counts[id] || 0
|
|
end
|
|
end
|
|
|
|
@enrollment_types
|
|
end
|
|
|
|
def self.manageable_roles_by_user(user, context)
|
|
is_blueprint = context.is_a?(Course) && MasterCourses::MasterTemplate.is_master_course?(context)
|
|
manageable = []
|
|
if context.grants_right?(user, :manage_students) && !is_blueprint
|
|
manageable += %w[StudentEnrollment ObserverEnrollment]
|
|
end
|
|
if context.grants_right?(user, :manage_admin_users)
|
|
manageable += %w[TeacherEnrollment TaEnrollment DesignerEnrollment]
|
|
manageable << "ObserverEnrollment" unless is_blueprint
|
|
end
|
|
manageable.uniq.sort
|
|
end
|
|
|
|
def self.add_delete_roles_by_user(user, context)
|
|
is_blueprint = context.is_a?(Course) && MasterCourses::MasterTemplate.is_master_course?(context)
|
|
addable = []
|
|
deleteable = []
|
|
addable += ["TeacherEnrollment"] if context.grants_right?(user, :add_teacher_to_course)
|
|
deleteable += ["TeacherEnrollment"] if context.grants_right?(user, :remove_teacher_from_course)
|
|
addable += ["TaEnrollment"] if context.grants_right?(user, :add_ta_to_course)
|
|
deleteable += ["TaEnrollment"] if context.grants_right?(user, :remove_ta_from_course)
|
|
addable += ["DesignerEnrollment"] if context.grants_right?(user, :add_designer_to_course)
|
|
deleteable += ["DesignerEnrollment"] if context.grants_right?(user, :remove_designer_from_course)
|
|
addable += ["StudentEnrollment"] if context.grants_right?(user, :add_student_to_course) && !is_blueprint
|
|
deleteable += ["StudentEnrollment"] if context.grants_right?(user, :remove_student_from_course)
|
|
addable += ["ObserverEnrollment"] if context.grants_right?(user, :add_observer_to_course) && !is_blueprint
|
|
deleteable += ["ObserverEnrollment"] if context.grants_right?(user, :remove_observer_from_course)
|
|
|
|
[addable, deleteable]
|
|
end
|
|
|
|
def self.compile_manageable_roles(role_data, user, context)
|
|
# for use with the old sad enrollment dialog
|
|
granular_admin = context.root_account.feature_enabled?(:granular_permissions_manage_users)
|
|
manageable = manageable_roles_by_user(user, context) unless granular_admin
|
|
addable, deleteable = add_delete_roles_by_user(user, context) if granular_admin
|
|
role_data.each_with_object([]) do |role, roles|
|
|
is_manageable = manageable.include?(role[:base_role_name]) unless granular_admin
|
|
is_addable = addable.include?(role[:base_role_name]) if granular_admin
|
|
is_deleteable = deleteable.include?(role[:base_role_name]) if granular_admin
|
|
role[:manageable_by_user] = is_manageable unless granular_admin
|
|
if granular_admin
|
|
role[:addable_by_user] = is_addable
|
|
role[:deleteable_by_user] = is_deleteable
|
|
end
|
|
custom_roles = role.delete(:custom_roles)
|
|
roles << role
|
|
|
|
custom_roles.each do |custom_role|
|
|
custom_role[:manageable_by_user] = is_manageable unless granular_admin
|
|
if granular_admin
|
|
custom_role[:addable_by_user] = is_addable
|
|
custom_role[:deleteable_by_user] = is_deleteable
|
|
end
|
|
roles << custom_role
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.role_data(course, user, include_inactive = false)
|
|
role_data = custom_roles_and_counts_for_course(course, user, include_inactive)
|
|
compile_manageable_roles(role_data, user, course)
|
|
end
|
|
|
|
def self.course_role_data_for_account(account, user)
|
|
role_data = all_enrollment_roles_for_account(account)
|
|
compile_manageable_roles(role_data, user, account)
|
|
end
|
|
end
|