Render simplified launch params into NRPS v2 responses
- Output a smaller version of a LTI 1.3 launch payload for each NRPS v2 member when the NRPS request includes a `rlid` query parameter. - Most claims and custom params from the launch payload are excluded either because: - They describe the context and would thus be redundant, or - They pose performance problems (N+1 queries, usually), or - They are absent from the spec example, e.g. `https://purl.imsglobal.org/spec/lti/claim/version`, or - They require additional development and thus need to be handled as a separate task. - See `memberships_provider.rb` ~line 68 for list of custom params supported in this commit. (More coming later.) - Vast majority of the patch has to do with tests against `JwtMessage`, which was modified to allow claims to be turned on/off via a new white/blacklist mechanism in `AppUtil`. - Custom param white/blacklisting is handled directly in `VariableExpander` to satisfy the LTI rule that unsupported params should just be echoed as-is. This (instead of keeping all the white/blacklist support in `JwtMessage` ensures consistent behavior w/r/t `VariableExpander`'s more sophisticated features, specifically its suport for expanding variables embedded into larger strings. Closes LTIA-40 Test Plan - Configure a LTI 1.3/Advantage tool with the supported set of custom params listed in `memberships_provider.rb` starting ~line 68. If using the POST `/api/lti/accounts/:account_id/developer_keys/tool_configuration` API, this is done by setting `tool_configuration.settings.custom_fields` to a JSON object where keys are the param name to be rendered into LTI payloads and values are the $-prefixed custom param names themselves. Include several nonsense entries as well as unsupported entries e.g.: ``` // ... snip ... "tool_configuration": { "settings": { // ... snip ... "custom_fields": { "person_name_full": "$Person.name.full", "person_name_display": "$Person.name.display", "person_name_family": "$Person.name.family", "person_name_given": "$Person.name.given", "canvas_user_isrootaccountadmin": "$Canvas.user.isRootAccountAdmin" "unsupported_param_1": "$unsupported.param.1", "unsupported_param_2": "$unsupported.param.2" } // ... snip ... } // ... snip ... } // ... snip ... ``` - Place this tool into a course, ensure the course has several active members. - Launch the tool in order to observe the course context's LTI identifier. Use that identifier as the value of the NRPS `rlid` parameter, e.g. a GET to: `/api/lti/courses/1/names_and_roles?rlid=4dde05e8ca1973bcca9bffc13e1548820eee93a3` - Each `members` array element in the response should have a `message` array with a single element being the simplified representation of a LTI 1.3 launch payload, were that user to launch the context referenced by `rlid`. - The `message` entry should have two top level claims: - `"https://purl.imsglobal.org/spec/lti/claim/message_type": "LtiResourceLinkRequest"` - `"https://purl.imsglobal.org/spec/lti/claim/custom": <object>` - The `custom` claim should include an entry for each `custom_fields` key/value pair configured above, with supported entries being correctly expanded and nonsense and unsupported entries being echoed as-is. - Repeat for a group context in the same course (still using the course's LTI ID as the `rlid` value). Results should be the same, though scoped to group membership. Change-Id: If2591c62c494756d65774e3115abeca19935c988 Reviewed-on: https://gerrit.instructure.com/169090 Tested-by: Jenkins Product-Review: Karl Lloyd <karl@instructure.com> Reviewed-by: Weston Dransfield <wdransfield@instructure.com> Reviewed-by: Marc Phillips <mphillips@instructure.com> QA-Review: Bill Smith <bsmith@instructure.com>
This commit is contained in:
parent
2f2ca82c36
commit
75fdd13da6
|
@ -107,6 +107,10 @@ module Lti::Ims::Providers
|
|||
@user_factory = user_factory
|
||||
end
|
||||
|
||||
def unwrap
|
||||
enrollments
|
||||
end
|
||||
|
||||
def user
|
||||
@_user ||= @user_factory.user(enrollments.first.user)
|
||||
end
|
||||
|
@ -125,6 +129,10 @@ module Lti::Ims::Providers
|
|||
end
|
||||
|
||||
class CourseContextDecorator < SimpleDelegator
|
||||
def unwrap
|
||||
__getobj__
|
||||
end
|
||||
|
||||
def context_label
|
||||
course_code
|
||||
end
|
||||
|
|
|
@ -77,6 +77,10 @@ module Lti::Ims::Providers
|
|||
@user_factory = user_factory
|
||||
end
|
||||
|
||||
def unwrap
|
||||
__getobj__
|
||||
end
|
||||
|
||||
def context
|
||||
@_context ||= GroupContextDecorator.new(super)
|
||||
end
|
||||
|
@ -105,6 +109,10 @@ module Lti::Ims::Providers
|
|||
end
|
||||
|
||||
class GroupContextDecorator < SimpleDelegator
|
||||
def unwrap
|
||||
__getobj__
|
||||
end
|
||||
|
||||
def context_label
|
||||
nil
|
||||
end
|
||||
|
|
|
@ -37,7 +37,15 @@ module Lti::Ims::Providers
|
|||
{
|
||||
memberships: memberships,
|
||||
context: context,
|
||||
api_metadata: api_metadata
|
||||
assignment: assignment,
|
||||
api_metadata: api_metadata,
|
||||
controller: controller,
|
||||
tool: tool,
|
||||
opts: {
|
||||
rlid: rlid,
|
||||
role: role,
|
||||
limit: limit
|
||||
}.compact
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -45,15 +53,32 @@ module Lti::Ims::Providers
|
|||
UserDecorator.new(
|
||||
user,
|
||||
tool,
|
||||
Lti::VariableExpander.new(
|
||||
course.root_account,
|
||||
course,
|
||||
controller,
|
||||
{
|
||||
current_user: user,
|
||||
tool: tool
|
||||
}
|
||||
)
|
||||
user_variable_expander(user)
|
||||
)
|
||||
end
|
||||
|
||||
def user_variable_expander(user)
|
||||
Lti::VariableExpander.new(
|
||||
course.root_account,
|
||||
course,
|
||||
controller,
|
||||
{
|
||||
current_user: user,
|
||||
tool: tool,
|
||||
variable_whitelist: %w(
|
||||
Person.name.full
|
||||
Person.name.display
|
||||
Person.name.family
|
||||
Person.name.given
|
||||
User.image
|
||||
User.id
|
||||
Canvas.user.id
|
||||
vnd.instructure.User.uuid
|
||||
Canvas.user.globalId
|
||||
Canvas.user.sisSourceId
|
||||
Person.sourcedId
|
||||
)
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -179,6 +204,10 @@ module Lti::Ims::Providers
|
|||
enrollments
|
||||
end
|
||||
|
||||
def limit
|
||||
controller.params[:limit].to_i
|
||||
end
|
||||
|
||||
def paginate(scope)
|
||||
Api.jsonapi_paginate(scope, controller, base_url, pagination_args)
|
||||
end
|
||||
|
@ -186,7 +215,7 @@ module Lti::Ims::Providers
|
|||
def pagination_args
|
||||
# Treat LTI's `limit` param as override of std `per_page` API pagination param. Is no LTI override for `page`.
|
||||
pagination_args = {}
|
||||
if (limit = controller.params[:limit].to_i) > 0
|
||||
if limit > 0
|
||||
pagination_args[:per_page] = [limit, Api.max_per_page].min
|
||||
# Ensure page size reset isn't accidentally clobbered by other pagination API params
|
||||
clear_request_param :per_page
|
||||
|
@ -207,7 +236,7 @@ module Lti::Ims::Providers
|
|||
# has to change if more custom param support is added in the future. But that day may never come, so err on the
|
||||
# side of making it very hard to leak user attributes.
|
||||
class UserDecorator
|
||||
attr_reader :user # Intentional backdoor. See Lti::Ims::NamesAndRolesSerializer for use case/s.
|
||||
attr_reader :user, :expander, :tool
|
||||
|
||||
def initialize(user, tool, expander)
|
||||
@user = user
|
||||
|
@ -215,6 +244,10 @@ module Lti::Ims::Providers
|
|||
@expander = expander
|
||||
end
|
||||
|
||||
def unwrap
|
||||
user
|
||||
end
|
||||
|
||||
def id
|
||||
user.id
|
||||
end
|
||||
|
|
|
@ -33,7 +33,7 @@ module Lti::Ims
|
|||
|
||||
def serialize_context
|
||||
{
|
||||
id: lti_id_for(page[:context]),
|
||||
id: lti_id(page[:context]),
|
||||
label: page[:context].context_label,
|
||||
title: page[:context].context_title,
|
||||
}.compact
|
||||
|
@ -46,6 +46,10 @@ module Lti::Ims
|
|||
def serialize_membership(enrollment)
|
||||
# Inbound model is either an ActiveRecord Enrollment or GroupMembership, with delegations in place
|
||||
# to make them behave more or less the same for our purposes
|
||||
member(enrollment).merge!(message(enrollment)).compact
|
||||
end
|
||||
|
||||
def member(enrollment)
|
||||
{
|
||||
status: 'Active',
|
||||
name: enrollment.user.name,
|
||||
|
@ -56,13 +60,37 @@ module Lti::Ims
|
|||
lis_person_sourcedid: enrollment.user.sourced_id,
|
||||
# enrollment.user often wrapped for privacy policy reasons, but calculating the LTI ID really needs
|
||||
# access to underlying AR model.
|
||||
user_id: lti_id_for(enrollment.user.respond_to?(:user) ? enrollment.user.user : enrollment.user),
|
||||
user_id: lti_id(enrollment.user),
|
||||
roles: enrollment.lti_roles
|
||||
}.compact
|
||||
}
|
||||
end
|
||||
|
||||
def lti_id_for(entity)
|
||||
Lti::Asset.opaque_identifier_for(entity)
|
||||
def message(enrollment)
|
||||
return {} if page[:opts].blank? || page[:opts][:rlid].blank?
|
||||
launch = Lti::Messages::ResourceLinkRequest.new(
|
||||
tool: page[:tool],
|
||||
context: unwrap(page[:context]),
|
||||
user: enrollment.user,
|
||||
expander: enrollment.user.expander,
|
||||
return_url: nil,
|
||||
opts: {
|
||||
# See Lti::Ims::Providers::MembershipsProvider for additional constraints on custom param expansion
|
||||
# already baked into `enrollment.user.expander`
|
||||
claim_group_whitelist: [ :custom_params ]
|
||||
}
|
||||
).generate_post_payload_message
|
||||
|
||||
# One straggler field we can't readily control via white/blacklists
|
||||
launch_hash = launch.to_h.except!("#{LtiAdvantage::Serializers::JwtMessageSerializer::IMS_CLAIM_PREFIX}version")
|
||||
{ message: [ launch_hash ] }
|
||||
end
|
||||
|
||||
def lti_id(entity)
|
||||
Lti::Asset.opaque_identifier_for(unwrap(entity))
|
||||
end
|
||||
|
||||
def unwrap(wrapped)
|
||||
wrapped&.respond_to?(:unwrap) ? wrapped.unwrap : wrapped
|
||||
end
|
||||
|
||||
attr_reader :page
|
||||
|
|
|
@ -25,6 +25,7 @@ module Lti
|
|||
'default' => {template: 'lti/framed_launch'}.freeze,
|
||||
'full_width_in_context' => {template: 'lti/full_width_in_context'}.freeze,
|
||||
}.freeze
|
||||
BLACKLIST_WILDCARD = '*'.freeze # to set up 'deny all' rules
|
||||
|
||||
def self.display_template(display_type=nil, display_override:nil)
|
||||
unless TOOL_DISPLAY_TEMPLATES.key?(display_type)
|
||||
|
@ -48,5 +49,11 @@ module Lti
|
|||
end
|
||||
end
|
||||
|
||||
def self.allowed?(candidate, whitelist, blacklist)
|
||||
return true if whitelist.blank? && blacklist.blank?
|
||||
return false if blacklist.present? && (blacklist.include?(candidate) || blacklist.include?(BLACKLIST_WILDCARD))
|
||||
whitelist.blank? || whitelist.include?(candidate)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,17 +31,26 @@ module Lti::Messages
|
|||
@message = LtiAdvantage::Messages::JwtMessage.new
|
||||
end
|
||||
|
||||
def generate_post_payload
|
||||
add_security_claims!
|
||||
add_public_claims! if @tool.public?
|
||||
add_include_email_claims! if @tool.include_email?
|
||||
add_include_name_claims! if @tool.include_name?
|
||||
add_resource_claims!
|
||||
add_private_claims!
|
||||
def generate_post_payload_message
|
||||
add_security_claims! if include_claims?(:security)
|
||||
add_public_claims! if @tool.public? && include_claims?(:public)
|
||||
add_include_email_claims! if @tool.include_email? && include_claims?(:email)
|
||||
add_include_name_claims! if @tool.include_name? && include_claims?(:name)
|
||||
add_resource_claims! if include_claims?(:resource)
|
||||
add_context_claims! if include_claims?(:context)
|
||||
add_tool_platform_claims! if include_claims?(:tool_platform)
|
||||
add_launch_presentation_claims! if include_claims?(:launch_presentation)
|
||||
add_i18n_claims! if include_claims?(:i18n)
|
||||
add_roles_claims! if include_claims?(:roles)
|
||||
add_custom_params_claims! if include_claims?(:custom_params)
|
||||
add_names_and_roles_service_claims! if include_names_and_roles_service_claims?
|
||||
|
||||
@expander.expand_variables!(@message.extensions)
|
||||
{ id_token: @message.to_jws(Lti::KeyStorage.present_key) }
|
||||
@message
|
||||
end
|
||||
|
||||
def generate_post_payload
|
||||
{ id_token: generate_post_payload_message.to_jws(Lti::KeyStorage.present_key) }
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -56,27 +65,44 @@ module Lti::Messages
|
|||
@message.sub = Lti::Asset.opaque_identifier_for(@user)
|
||||
end
|
||||
|
||||
def add_private_claims!
|
||||
@message.locale = I18n.locale || I18n.default_locale.to_s
|
||||
@message.roles = expand_variable('$com.Instructure.membership.roles').split ','
|
||||
def add_context_claims!
|
||||
@message.context.id = Lti::Asset.opaque_identifier_for(@context)
|
||||
@message.context.label = @context.course_code if @context.respond_to?(:course_code)
|
||||
@message.context.title = @context.name
|
||||
@message.context.type = [Lti::SubstitutionsHelper::LIS_V2_ROLE_MAP[@context.class] || @context.class.to_s]
|
||||
end
|
||||
|
||||
def add_tool_platform_claims!
|
||||
@message.tool_platform.guid = @context.root_account.lti_guid
|
||||
@message.tool_platform.name = @context.root_account.name
|
||||
@message.tool_platform.version = 'cloud'
|
||||
@message.tool_platform.product_family_code = 'canvas'
|
||||
end
|
||||
|
||||
def add_launch_presentation_claims!
|
||||
@message.launch_presentation.document_target = 'iframe'
|
||||
@message.launch_presentation.height = @tool.extension_setting(@opts[:resource_type], :selection_height)
|
||||
@message.launch_presentation.width = @tool.extension_setting(@opts[:resource_type], :selection_width)
|
||||
@message.launch_presentation.return_url = @return_url
|
||||
@message.launch_presentation.locale = I18n.locale || I18n.default_locale.to_s
|
||||
@message.custom = custom_parameters
|
||||
end
|
||||
|
||||
def add_i18n_claims!
|
||||
# Repeated as @message.launch_presentation.locale above. Separated b/c often want one or the other but not both,
|
||||
# e.g. NRPS v2 only wants this one and none of the launch_presention fields.
|
||||
@message.locale = I18n.locale || I18n.default_locale.to_s
|
||||
end
|
||||
|
||||
def add_roles_claims!
|
||||
@message.roles = expand_variable('$com.Instructure.membership.roles').split ','
|
||||
add_extension('roles', '$Canvas.xuser.allRoles')
|
||||
add_extension('canvas_enrollment_state', '$Canvas.enrollment.enrollmentState')
|
||||
end
|
||||
|
||||
def add_custom_params_claims!
|
||||
@message.custom = custom_parameters
|
||||
end
|
||||
|
||||
def add_include_name_claims!
|
||||
@message.name = @user.name
|
||||
@message.given_name = @user.first_name
|
||||
|
@ -131,7 +157,9 @@ module Lti::Messages
|
|||
end
|
||||
|
||||
def include_names_and_roles_service_claims?
|
||||
(@context.is_a?(Course) || @context.is_a?(Group)) && @tool.lti_1_3_enabled?
|
||||
include_claims?(:names_and_roles_service) &&
|
||||
(@context.is_a?(Course) || @context.is_a?(Group)) &&
|
||||
@tool.lti_1_3_enabled?
|
||||
end
|
||||
|
||||
def add_names_and_roles_service_claims!
|
||||
|
@ -163,9 +191,18 @@ module Lti::Messages
|
|||
@expander.expand_variables!(custom_params_hash)
|
||||
end
|
||||
|
||||
def include_claims?(claim_group)
|
||||
Lti::AppUtil.allowed?(claim_group, @opts[:claim_group_whitelist], @opts[:claim_group_blacklist])
|
||||
end
|
||||
|
||||
def include_extension?(extension_name)
|
||||
Lti::AppUtil.allowed?(extension_name, @opts[:extension_whitelist], @opts[:extension_blacklist])
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def add_extension(key, value)
|
||||
return unless include_extension?(key.to_sym)
|
||||
@message.extensions["#{JwtMessage::EXTENSION_PREFIX}#{key}"] = value
|
||||
end
|
||||
end
|
||||
|
|
|
@ -22,8 +22,8 @@ module Lti::Messages
|
|||
@message = LtiAdvantage::Messages::ResourceLinkRequest.new
|
||||
end
|
||||
|
||||
def generate_post_payload
|
||||
add_resource_link_request_claims!
|
||||
def generate_post_payload_message
|
||||
add_resource_link_request_claims! if include_claims?(:rlid)
|
||||
super
|
||||
end
|
||||
|
||||
|
@ -60,4 +60,4 @@ module Lti::Messages
|
|||
add_extension('canvas_assignment_points_possible', '$Canvas.assignment.pointsPossible')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -29,11 +29,16 @@ module Lti
|
|||
|
||||
attr_accessor :current_pseudonym, :content_tag, :assignment,
|
||||
:tool_setting_link_id, :tool_setting_binding_id, :tool_setting_proxy_id, :tool, :attachment,
|
||||
:collaboration
|
||||
:collaboration, :variable_whitelist, :variable_blacklist
|
||||
|
||||
def self.register_expansion(name, permission_groups, expansion_proc, *guards)
|
||||
@expansions ||= {}
|
||||
@expansions["$#{name}".to_sym] = VariableExpansion.new(name, permission_groups, expansion_proc, *guards)
|
||||
@expansions["$#{name}".to_sym] = VariableExpansion.new(
|
||||
name,
|
||||
permission_groups,
|
||||
expansion_proc,
|
||||
*([-> { Lti::AppUtil.allowed?(name, @variable_whitelist, @variable_blacklist) }] + guards)
|
||||
)
|
||||
end
|
||||
|
||||
def self.expansions
|
||||
|
|
|
@ -159,20 +159,83 @@ describe Lti::Ims::NamesAndRolesController do
|
|||
end
|
||||
end
|
||||
|
||||
# rubocop:disable RSpec/LetSetup
|
||||
shared_examples 'rlid check' do
|
||||
let(:rlid_param) { raise 'Override in example' }
|
||||
let(:rlid_param) { expected_lti_id(course) }
|
||||
let(:params_overrides) { super().merge(rlid: rlid_param) }
|
||||
let(:user_full_name) { 'Marta Perkins' }
|
||||
|
||||
context 'when the rlid param specifies the course context LTI ID' do
|
||||
let(:rlid_param) { expected_lti_id(course) }
|
||||
|
||||
it 'behaves just like a \'normal\' NRPS course membership lookup' do
|
||||
send_request
|
||||
expect_single_member(enrollment)
|
||||
end
|
||||
end
|
||||
|
||||
# Always an error at this writing b/c we don't yet support rlid=assignment.lti_context_id
|
||||
context 'and the tool is configured with a mix of supported and unsupported custom parameters' do
|
||||
let!(:tool) do
|
||||
tool = super()
|
||||
tool.settings[:custom_fields] = {
|
||||
person_name_full: '$Person.name.full',
|
||||
person_name_display: '$Person.name.display',
|
||||
person_name_family: '$Person.name.family',
|
||||
person_name_given: '$Person.name.given',
|
||||
user_image: '$User.image',
|
||||
user_id: '$User.id',
|
||||
canvas_user_id: '$Canvas.user.id',
|
||||
vns_instructure_user_uuid: '$vnd.instructure.User.uuid',
|
||||
canvas_user_globalid: '$Canvas.user.globalId',
|
||||
canvas_user_sissourceid: '$Canvas.user.sisSourceId',
|
||||
person_sourced_id: '$Person.sourcedId',
|
||||
unsupported_param_1: '$unsupported.param.1',
|
||||
unsupported_param_2: '$unsupported.param.2'
|
||||
}
|
||||
tool.save!
|
||||
tool
|
||||
end
|
||||
let!(:user) do
|
||||
user = enrollment.user
|
||||
user.email = 'marta.perkins@school.edu'
|
||||
user.avatar_image_url = 'http://school.edu/image/url.png'
|
||||
user.save!
|
||||
user.pseudonyms.create!({
|
||||
account: course.account,
|
||||
unique_id: 'user1@example.com',
|
||||
password: 'asdfasdf',
|
||||
password_confirmation: 'asdfasdf',
|
||||
workflow_state: 'active',
|
||||
sis_user_id: 'user-1-sis-user-id-1'
|
||||
})
|
||||
user
|
||||
end
|
||||
|
||||
it 'expands the supported parameters and echoes the rest' do
|
||||
send_request
|
||||
expect(json[:members][0]).to match_enrollment_for_rlid(
|
||||
{
|
||||
'https://purl.imsglobal.org/spec/lti/claim/custom' => {
|
||||
'person_name_full' => 'Marta Perkins',
|
||||
'person_name_display' => 'Marta Perkins',
|
||||
'person_name_family' => 'Perkins',
|
||||
'person_name_given' => 'Marta',
|
||||
'user_image' => 'http://school.edu/image/url.png',
|
||||
'user_id' => user.id,
|
||||
'canvas_user_id' => user.id,
|
||||
'vns_instructure_user_uuid' => user.uuid,
|
||||
'canvas_user_globalid' => user.global_id,
|
||||
'canvas_user_sissourceid' => 'user-1-sis-user-id-1',
|
||||
'person_sourced_id' => 'user-1-sis-user-id-1',
|
||||
'unsupported_param_1' => '$unsupported.param.1',
|
||||
'unsupported_param_2' => '$unsupported.param.2'
|
||||
}
|
||||
},
|
||||
enrollment
|
||||
)
|
||||
expect_member_count(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
# rubocop:enable RSpec/LetSetup
|
||||
|
||||
context 'when the rlid param does not specify the course context LTI ID' do
|
||||
let(:rlid_param) { "nonsense-#{expected_lti_id(course)}" }
|
||||
|
||||
|
@ -332,15 +395,13 @@ describe Lti::Ims::NamesAndRolesController do
|
|||
end
|
||||
end
|
||||
|
||||
# rubocop:disable RSpec/LetSetup
|
||||
context 'when the rlid param is specified' do
|
||||
# rubocop:disable RSpec/LetSetup
|
||||
# rubocop:disable RSpec/EmptyLineAfterFinalLet
|
||||
let!(:enrollment) { teacher_in_course(course: course, active_all: true) }
|
||||
# rubocop:enable RSpec/EmptyLineAfterFinalLet
|
||||
# rubocop:enable RSpec/LetSetup
|
||||
let!(:enrollment) { teacher_in_course(course: course, active_all: true, name: user_full_name) }
|
||||
|
||||
it_behaves_like 'rlid check'
|
||||
end
|
||||
# rubocop:enable RSpec/LetSetup
|
||||
|
||||
context 'when a course has multiple enrollments' do
|
||||
let!(:teacher_enrollment) { teacher_in_course(course: course, active_all: true) }
|
||||
|
@ -782,17 +843,15 @@ describe Lti::Ims::NamesAndRolesController do
|
|||
end
|
||||
end
|
||||
|
||||
# rubocop:disable RSpec/LetSetup
|
||||
context 'when the rlid param is specified' do
|
||||
let(:group_record) { group_with_user(active_all: true, context: course).group }
|
||||
let(:group_record) { group_with_user(active_all: true, context: course, name: user_full_name).group }
|
||||
let(:group_member) { group_record.group_memberships.first }
|
||||
# rubocop:disable RSpec/LetSetup
|
||||
# rubocop:disable RSpec/EmptyLineAfterFinalLet
|
||||
let!(:enrollment) { group_member }
|
||||
# rubocop:enable RSpec/EmptyLineAfterFinalLet
|
||||
# rubocop:enable RSpec/LetSetup
|
||||
|
||||
it_behaves_like 'rlid check'
|
||||
end
|
||||
# rubocop:enable RSpec/LetSetup
|
||||
|
||||
context 'when a group has multiple memberships' do
|
||||
let!(:group_leadership) do
|
||||
|
@ -1156,7 +1215,18 @@ describe Lti::Ims::NamesAndRolesController do
|
|||
end
|
||||
|
||||
def match_enrollment(*enrollment)
|
||||
enrollment.first.is_a?(Enrollment) ? be_lti_course_membership(*enrollment) : be_lti_group_membership(*enrollment)
|
||||
if self.respond_to?(:rlid_param) && rlid_param.present?
|
||||
match_enrollment_for_rlid({}, *enrollment)
|
||||
else
|
||||
enrollment.first.is_a?(Enrollment) ? be_lti_course_membership(*enrollment) : be_lti_group_membership(*enrollment)
|
||||
end
|
||||
end
|
||||
|
||||
def match_enrollment_for_rlid(message_matcher, *enrollment)
|
||||
if enrollment.first.is_a?(Enrollment)
|
||||
be_lti_course_membership_for_rlid(message_matcher, *enrollment)
|
||||
else be_lti_group_membership_for_rlid(message_matcher, *enrollment)
|
||||
end
|
||||
end
|
||||
|
||||
def expect_enrollment_response_page
|
||||
|
@ -1230,6 +1300,6 @@ describe Lti::Ims::NamesAndRolesController do
|
|||
end
|
||||
|
||||
def users_from(*users_or_user_containers)
|
||||
users_or_user_containers.map { |uoe| uoe.respond_to?(:user) ? uoe.user : uoe }
|
||||
users_or_user_containers.map { |uc| uc.respond_to?(:user) ? uc.user : uc }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -67,4 +67,50 @@ describe Lti::AppUtil do
|
|||
expect(Lti::AppUtil.display_template("default", display_override: "full_width")).to eq(Lti::AppUtil::TOOL_DISPLAY_TEMPLATES["full_width"])
|
||||
end
|
||||
end
|
||||
|
||||
describe '.allowed?' do
|
||||
it 'allows candidate if white- and blacklists are nil' do
|
||||
expect(Lti::AppUtil).to be_allowed('foo', nil, nil)
|
||||
end
|
||||
|
||||
it 'allows candidate if white- and blacklists are empty' do
|
||||
expect(Lti::AppUtil).to be_allowed('foo', [], [])
|
||||
end
|
||||
|
||||
it 'allows candidate if present in whitelist and not in blacklist' do
|
||||
expect(Lti::AppUtil).to be_allowed('foo', ['foo'], ['bar'])
|
||||
end
|
||||
|
||||
it 'disallows candidate if present in blacklist and not in whitelist' do
|
||||
expect(Lti::AppUtil).to_not be_allowed('foo', ['bar'], ['foo'])
|
||||
end
|
||||
|
||||
it 'disallows candidate if present in white- and blacklist' do
|
||||
expect(Lti::AppUtil).to_not be_allowed('foo', ['foo'], ['foo'])
|
||||
end
|
||||
|
||||
it 'disallows candidate if whitelist empty and blacklist wildcarded' do
|
||||
expect(Lti::AppUtil).to_not be_allowed('foo', [], ['*'])
|
||||
end
|
||||
|
||||
it 'disallows candidate if whitelist empty and is present blacklist' do
|
||||
expect(Lti::AppUtil).to_not be_allowed('foo', [], ['foo'])
|
||||
end
|
||||
|
||||
it 'disallows candidate if absent from both white- and blacklists' do
|
||||
expect(Lti::AppUtil).to_not be_allowed('foo', ['bar'], ['baz'])
|
||||
end
|
||||
|
||||
it 'disallows candidate if absent from whitelist and blacklist is empty' do
|
||||
expect(Lti::AppUtil).to_not be_allowed('foo', ['bar'], [])
|
||||
end
|
||||
|
||||
it 'allows candidate if present in multi-valued whitelist and not present in multi-valued blacklist' do
|
||||
expect(Lti::AppUtil).to be_allowed('foo', ['bar', 'foo', 'baz'], ['bap','bam','ban'])
|
||||
end
|
||||
|
||||
it 'disallows candidate if present in multi-valued blacklist and not present in multi-valued whitelist' do
|
||||
expect(Lti::AppUtil).to_not be_allowed('foo', ['bap','bam','ban'], ['bar', 'foo', 'baz'])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -122,6 +122,14 @@ describe Lti::Messages::ResourceLinkRequest do
|
|||
it 'sets the resource link id' do
|
||||
expect(jws.dig('https://purl.imsglobal.org/spec/lti/claim/resource_link', 'id')).to eq course.lti_context_id
|
||||
end
|
||||
|
||||
context 'when rlid claim group disabled' do
|
||||
let(:opts) { super().merge({claim_group_blacklist: [:rlid]}) }
|
||||
|
||||
it 'does not set the resource link id' do
|
||||
expect(jws).not_to include('https://purl.imsglobal.org/spec/lti/claim/resource_link')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#generate_post_payload_for_assignment' do
|
||||
|
|
|
@ -144,6 +144,44 @@ module Lti
|
|||
expect(expanded[:some_name]).to eq "my variable is buried in here ${tests_expan} can you find it?"
|
||||
end
|
||||
|
||||
it 'echoes registered variable if blacklisted' do
|
||||
VariableExpander.register_expansion('test_expan', ['a'], -> { @context })
|
||||
VariableExpander.register_expansion('variable1', ['a'], -> { 1 })
|
||||
variable_expander.variable_blacklist = ['test_expan']
|
||||
expanded1 = variable_expander.expand_variables!({some_name: '$test_expan'})
|
||||
expanded2 = variable_expander.expand_variables!({some_name: '$variable1'})
|
||||
expect(expanded1.count).to eq 1
|
||||
expect(expanded1[:some_name]).to eq '$test_expan'
|
||||
expect(expanded2.count).to eq 1
|
||||
expect(expanded2[:some_name]).to eq 1
|
||||
end
|
||||
|
||||
it 'echoes substring variable if blacklisted' do
|
||||
allow(account).to receive(:id).and_return(42)
|
||||
VariableExpander.register_expansion('test_expan', ['a'], -> { @context.id })
|
||||
VariableExpander.register_expansion('variable1', ['a'], -> { 1 })
|
||||
variable_expander.variable_blacklist = ['test_expan']
|
||||
expanded1 = variable_expander.expand_variables!({some_name: 'my variable is buried in here ${test_expan} can you find it?'})
|
||||
expanded2 = variable_expander.expand_variables!({some_name: 'my variable is buried in here ${variable1} can you find it?'})
|
||||
expect(expanded1.count).to eq 1
|
||||
expect(expanded1[:some_name]).to eq 'my variable is buried in here $test_expan can you find it?'
|
||||
expect(expanded2.count).to eq 1
|
||||
expect(expanded2[:some_name]).to eq 'my variable is buried in here 1 can you find it?'
|
||||
end
|
||||
|
||||
it 'echoes multiple substring variables if blacklisted' do
|
||||
allow(account).to receive(:id).and_return(42)
|
||||
VariableExpander.register_expansion('test_expan', ['a'], -> { @context.id })
|
||||
VariableExpander.register_expansion('variable1', ['a'], -> { 1 })
|
||||
VariableExpander.register_expansion('other_variable', ['a'], -> { 2 })
|
||||
variable_expander.variable_blacklist = ['test_expan','variable1']
|
||||
expanded = variable_expander.expand_variables!(
|
||||
{some_name: 'my variables ${variable1} is buried ${other_variable} in here ${test_expan} can you find them?'}
|
||||
)
|
||||
expect(expanded.count).to eq 1
|
||||
expect(expanded[:some_name]).to eq 'my variables $variable1 is buried 2 in here $test_expan can you find them?'
|
||||
end
|
||||
|
||||
describe '#self.expansion_keys' do
|
||||
let(:expected_keys) do
|
||||
VariableExpander.expansions.keys.map { |c| c.to_s[1..-1] }
|
||||
|
|
|
@ -38,6 +38,7 @@ describe Lti::Ims::NamesAndRolesSerializer do
|
|||
workflow_state: privacy_level
|
||||
)
|
||||
end
|
||||
let(:message_matcher) { {} }
|
||||
|
||||
def serialize
|
||||
subject.as_json.with_indifferent_access
|
||||
|
@ -48,7 +49,12 @@ describe Lti::Ims::NamesAndRolesSerializer do
|
|||
be_lti_group_membership_context(decorated_group)
|
||||
end
|
||||
|
||||
def be_lti_membership
|
||||
def be_lti_membership(for_rlid=false)
|
||||
if for_rlid
|
||||
return be_lti_course_membership_for_rlid(message_matcher, decorated_enrollment) if context_type == :course
|
||||
return be_lti_group_membership_for_rlid(message_matcher, decorated_group_member)
|
||||
end
|
||||
|
||||
return be_lti_course_membership(decorated_enrollment) if context_type == :course
|
||||
be_lti_group_membership(decorated_group_member)
|
||||
end
|
||||
|
@ -64,15 +70,44 @@ describe Lti::Ims::NamesAndRolesSerializer do
|
|||
})
|
||||
end
|
||||
|
||||
shared_examples 'enrollment serialization' do
|
||||
shared_examples 'enrollment serialization' do |for_rlid=false|
|
||||
it 'properly formats NRPS json' do
|
||||
json = serialize
|
||||
expect(json[:id]).to eq url
|
||||
expect(json[:context]).to be_lti_membership_context
|
||||
expect(json[:members][0]).to be_lti_membership
|
||||
expect(json[:members][0]).to be_lti_membership(for_rlid)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'serializes message array if rlid param present' do
|
||||
let(:tool) do
|
||||
tool = super()
|
||||
tool.settings[:custom_fields] = {
|
||||
user_id: '$User.id',
|
||||
canvas_user_id: '$Canvas.user.id',
|
||||
unsupported_param_1: '$unsupported.param.1',
|
||||
unsupported_param_2: '$unsupported.param.2'
|
||||
}
|
||||
tool.save!
|
||||
tool
|
||||
end
|
||||
let(:page) do
|
||||
super().merge(opts: { rlid: 'rlid-value' })
|
||||
end
|
||||
let(:message_matcher) do
|
||||
{
|
||||
'https://purl.imsglobal.org/spec/lti/claim/custom' => {
|
||||
'user_id' => user.id,
|
||||
'canvas_user_id' => user.id,
|
||||
'unsupported_param_1' => '$unsupported.param.1',
|
||||
'unsupported_param_2' => '$unsupported.param.2'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it_behaves_like 'enrollment serialization', true
|
||||
end
|
||||
|
||||
# Technically all these '...privacy policy' examples are redundant w/r/t be_lti_*_membership(). But those matchers
|
||||
# know nothing about privacy policies... they just know that if a model provides a value, it should appear in the
|
||||
# resulting json/hash. So you could have an incorrectly implemented privacy policy, but all the serialization tests
|
||||
|
@ -128,6 +163,7 @@ describe Lti::Ims::NamesAndRolesSerializer do
|
|||
create_pseudonym!(user)
|
||||
enrollment
|
||||
end
|
||||
let(:user) { enrollment.user }
|
||||
let(:decorated_user_factory) do
|
||||
Lti::Ims::Providers::CourseMembershipsProvider.new(course, nil, tool)
|
||||
end
|
||||
|
@ -141,9 +177,14 @@ describe Lti::Ims::NamesAndRolesSerializer do
|
|||
let(:decorated_course) { Lti::Ims::Providers::CourseMembershipsProvider::CourseContextDecorator.new(course) }
|
||||
let(:page) do
|
||||
{
|
||||
memberships: [decorated_enrollment],
|
||||
url: url,
|
||||
context: decorated_course
|
||||
memberships: [decorated_enrollment],
|
||||
context: decorated_course,
|
||||
assignment: nil,
|
||||
api_metadata: nil,
|
||||
controller: nil,
|
||||
tool: tool,
|
||||
opts: {}
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -155,7 +196,7 @@ describe Lti::Ims::NamesAndRolesSerializer do
|
|||
context 'and an anonymous tool' do
|
||||
let(:privacy_level) { 'anonymous' }
|
||||
|
||||
it_behaves_like 'enrollment serialization'
|
||||
it_behaves_like 'enrollment serialization'
|
||||
it_behaves_like 'anonymous privacy policy'
|
||||
end
|
||||
|
||||
|
@ -172,6 +213,8 @@ describe Lti::Ims::NamesAndRolesSerializer do
|
|||
it_behaves_like 'enrollment serialization'
|
||||
it_behaves_like 'email_only privacy policy'
|
||||
end
|
||||
|
||||
it_behaves_like 'serializes message array if rlid param present'
|
||||
end
|
||||
|
||||
context 'with a group' do
|
||||
|
@ -186,6 +229,7 @@ describe Lti::Ims::NamesAndRolesSerializer do
|
|||
create_pseudonym!(user)
|
||||
enrollment
|
||||
end
|
||||
let(:user) { group_member.user }
|
||||
let(:decorated_user_factory) do
|
||||
Lti::Ims::Providers::GroupMembershipsProvider.new(group_record, nil, tool)
|
||||
end
|
||||
|
@ -199,9 +243,14 @@ describe Lti::Ims::NamesAndRolesSerializer do
|
|||
let(:decorated_group) { Lti::Ims::Providers::GroupMembershipsProvider::GroupContextDecorator.new(group_record) }
|
||||
let(:page) do
|
||||
{
|
||||
memberships: [decorated_group_member],
|
||||
url: url,
|
||||
context: decorated_group
|
||||
memberships: [decorated_group_member],
|
||||
context: decorated_group,
|
||||
assignment: nil,
|
||||
api_metadata: nil,
|
||||
controller: nil,
|
||||
tool: tool,
|
||||
opts: {}
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -230,6 +279,8 @@ describe Lti::Ims::NamesAndRolesSerializer do
|
|||
it_behaves_like 'enrollment serialization'
|
||||
it_behaves_like 'email_only privacy policy'
|
||||
end
|
||||
|
||||
it_behaves_like 'serializes message array if rlid param present'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -96,16 +96,37 @@ module Lti::Ims::NamesAndRolesMatchers
|
|||
}.compact
|
||||
end
|
||||
|
||||
def expected_message_array(message_matcher)
|
||||
[
|
||||
{
|
||||
'https://purl.imsglobal.org/spec/lti/claim/message_type' => 'LtiResourceLinkRequest',
|
||||
'https://purl.imsglobal.org/spec/lti/claim/custom' => {}
|
||||
}.merge!(message_matcher.presence || {})
|
||||
]
|
||||
end
|
||||
|
||||
def expected_course_membership(user, *enrollments)
|
||||
expected_base_membership(user).merge!('roles' => match_array(expected_course_lti_roles(*enrollments))).compact
|
||||
end
|
||||
|
||||
def expected_course_membership_for_rlid(message_matcher, user, *enrollments)
|
||||
expected_course_membership(user, *enrollments).
|
||||
merge('message' => match_array(expected_message_array(message_matcher))).
|
||||
compact
|
||||
end
|
||||
|
||||
def expected_group_membership(user, membership)
|
||||
expected_base_membership(user).merge!('roles' => match_array(expected_group_lti_roles(membership))).compact
|
||||
end
|
||||
|
||||
def expected_group_membership_for_rlid(message_matcher, user, membership)
|
||||
expected_group_membership(user, membership).
|
||||
merge('message' => match_array(expected_message_array(message_matcher))).
|
||||
compact
|
||||
end
|
||||
|
||||
def unwrap_user(user)
|
||||
user.respond_to?(:user) ? user.user : user
|
||||
user.respond_to?(:unwrap) ? user.unwrap : user
|
||||
end
|
||||
|
||||
RSpec::Matchers.define :be_lti_course_membership_context do |expected|
|
||||
|
@ -144,6 +165,18 @@ module Lti::Ims::NamesAndRolesMatchers
|
|||
attr_reader :actual, :expected
|
||||
end
|
||||
|
||||
RSpec::Matchers.define :be_lti_course_membership_for_rlid do |message_matcher, *expected|
|
||||
match do |actual|
|
||||
@expected = expected_course_membership_for_rlid(message_matcher, expected.first.user, *expected)
|
||||
values_match? @expected, actual
|
||||
end
|
||||
|
||||
diffable
|
||||
|
||||
# Make sure a failure diffs the two JSON structs (w/o this will compare 'actual' JSON to 'expected' AR model)
|
||||
attr_reader :actual, :expected
|
||||
end
|
||||
|
||||
RSpec::Matchers.define :be_lti_group_membership do |expected|
|
||||
match do |actual|
|
||||
@expected = expected_group_membership(expected.user, expected)
|
||||
|
@ -156,6 +189,18 @@ module Lti::Ims::NamesAndRolesMatchers
|
|||
attr_reader :actual, :expected
|
||||
end
|
||||
|
||||
RSpec::Matchers.define :be_lti_group_membership_for_rlid do |message_matcher, expected|
|
||||
match do |actual|
|
||||
@expected = expected_group_membership_for_rlid(message_matcher, expected.user, expected)
|
||||
values_match? @expected, actual
|
||||
end
|
||||
|
||||
diffable
|
||||
|
||||
# Make sure a failure diffs the two JSON structs (w/o this will compare 'actual' JSON to 'expected' AR model)
|
||||
attr_reader :actual, :expected
|
||||
end
|
||||
|
||||
RSpec::Matchers.define :be_lti_advantage_error_response_body do |expected_type, expected_message|
|
||||
match do |actual|
|
||||
@expected = {
|
||||
|
|
Loading…
Reference in New Issue