lti2 launch by link_id
Closes PLAT-2565 Test Plan: - Create an unsigned JWT with the following values in the body: - vendor_code - product_code - resource_type_code The values for each of these should be a valid code of an Lti::ProductFamily and Lti::ResourceHandler existing in Canvas - Navigate to `http://canvas.docker/<tool context>/:id/ lti/message_handler_link/<jwt>` - Verify that doing a launch in this way finds tools up the context's account chain. (i.e. doing this with the context in the URL set to a course should find a tool in the course, or any account up the chain). - Verify that the closest context to the current context is used. Change-Id: I397dbcadc5ed8bcdfd7cd7993c0ed997a0814bc9 Reviewed-on: https://gerrit.instructure.com/112171 Tested-by: Jenkins Reviewed-by: Brad Humphrey <brad@instructure.com> QA-Review: August Thornton <august@instructure.com> Product-Review: Brad Humphrey <brad@instructure.com> Reviewed-by: Cody Cutrer <cody@instructure.com>
This commit is contained in:
parent
0fbbecb5cd
commit
9acbad2a5c
|
@ -75,9 +75,41 @@ module Lti
|
|||
end
|
||||
private :reregistration_message
|
||||
|
||||
def message_handler_link
|
||||
link_id_hash = JSON::JWT.decode(params[:link_id]).with_indifferent_access
|
||||
message_handler = MessageHandler.by_resource_codes(vendor_code: link_id_hash[:vendor_code],
|
||||
product_code: link_id_hash[:product_code],
|
||||
resource_type_code: link_id_hash[:resource_type_code],
|
||||
context: @context)
|
||||
if message_handler.present?
|
||||
return lti2_basic_launch(message_handler)
|
||||
end
|
||||
not_found
|
||||
end
|
||||
|
||||
def basic_lti_launch_request
|
||||
if (message_handler = MessageHandler.find(params[:message_handler_id]))
|
||||
return lti2_basic_launch(message_handler)
|
||||
end
|
||||
not_found
|
||||
end
|
||||
|
||||
def registration_return
|
||||
@tool = ToolProxy.where(guid: params[:tool_proxy_guid]).first
|
||||
@data = {
|
||||
subject: 'lti.lti2Registration',
|
||||
status: params[:status],
|
||||
app_id: @tool.id,
|
||||
name: @tool.name,
|
||||
description: @tool.description,
|
||||
message: params[:lti_errormsg] || params[:lti_msg]
|
||||
}
|
||||
render layout: false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def lti2_basic_launch(message_handler)
|
||||
resource_handler = message_handler.resource_handler
|
||||
tool_proxy = resource_handler.tool_proxy
|
||||
# TODO: create scope for query
|
||||
|
@ -118,23 +150,6 @@ module Lti
|
|||
render Lti::AppUtil.display_template(display_override: params[:display]) and return
|
||||
end
|
||||
end
|
||||
not_found
|
||||
end
|
||||
|
||||
def registration_return
|
||||
@tool = ToolProxy.where(guid: params[:tool_proxy_guid]).first
|
||||
@data = {
|
||||
subject: 'lti.lti2Registration',
|
||||
status: params[:status],
|
||||
app_id: @tool.id,
|
||||
name: @tool.name,
|
||||
description: @tool.description,
|
||||
message: params[:lti_errormsg] || params[:lti_msg]
|
||||
}
|
||||
render layout: false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def enabled_parameters(tp, mh, variable_expander)
|
||||
tool_proxy = IMS::LTI::Models::ToolProxy.from_json(tp.raw_data)
|
||||
|
|
|
@ -75,5 +75,19 @@ module Lti
|
|||
end
|
||||
end
|
||||
|
||||
def self.by_resource_codes(vendor_code:, product_code:, resource_type_code:, context:, message_type: BASIC_LTI_LAUNCH_REQUEST)
|
||||
possible_handlers = ResourceHandler.by_resource_codes(vendor_code: vendor_code,
|
||||
product_code: product_code,
|
||||
resource_type_code: resource_type_code,
|
||||
context: context)
|
||||
resource_handler = nil
|
||||
search_contexts = context.account_chain.unshift(context)
|
||||
search_contexts.each do |search_context|
|
||||
break if resource_handler.present?
|
||||
resource_handler = possible_handlers.find { |rh| rh.tool_proxy.context == search_context }
|
||||
end
|
||||
resource_handler&.find_message_by_type(message_type)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
|
|
@ -27,22 +27,24 @@ module Lti
|
|||
|
||||
serialize :icon_info
|
||||
|
||||
validates_presence_of :resource_type_code, :name, :tool_proxy, :lookup_id
|
||||
before_validation :set_lookup_id
|
||||
validates :resource_type_code, :name, :tool_proxy, presence: true
|
||||
|
||||
def self.generate_lookup_id_for(resource_handler)
|
||||
tool_proxy = resource_handler.tool_proxy
|
||||
product_family = tool_proxy.product_family
|
||||
components = [product_family.product_code,
|
||||
product_family.vendor_code,
|
||||
resource_handler.resource_type_code].join('-')
|
||||
"#{components}-#{Canvas::Security.hmac_sha1(components)}"
|
||||
def find_message_by_type(message_type)
|
||||
message_handlers.by_message_types(message_type).first
|
||||
end
|
||||
|
||||
private
|
||||
def self.by_product_family(product_family, context)
|
||||
tool_proxies = ToolProxy.find_active_proxies_for_context(context)
|
||||
tool_proxies = tool_proxies.where(product_family: product_family)
|
||||
tool_proxies.map { |tp| tp.resources.flatten }.flatten
|
||||
end
|
||||
|
||||
def set_lookup_id
|
||||
self.lookup_id ||= self.class.generate_lookup_id_for(self)
|
||||
|
||||
def self.by_resource_codes(vendor_code:, product_code:, resource_type_code:, context:)
|
||||
product_family = ProductFamily.find_by(vendor_code: vendor_code,
|
||||
product_code: product_code)
|
||||
possible_handlers = ResourceHandler.by_product_family(product_family, context)
|
||||
possible_handlers.select { |rh| rh.resource_type_code == resource_type_code}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -300,6 +300,9 @@ CanvasRails::Application.routes.draw do
|
|||
end
|
||||
end
|
||||
|
||||
get 'lti/message_handler_link/:link_id', controller: 'lti/message',
|
||||
action: 'message_handler_link', as: :message_handler_link,
|
||||
constraints: {link_id: /\w+[.]\w+[.]/ }
|
||||
get 'lti/basic_lti_launch_request/:message_handler_id', controller: 'lti/message',
|
||||
action: 'basic_lti_launch_request', as: :basic_lti_launch_request
|
||||
get 'lti/tool_proxy_registration', controller: 'lti/message', action: 'registration', as: :tool_proxy_registration
|
||||
|
@ -610,6 +613,9 @@ CanvasRails::Application.routes.draw do
|
|||
end
|
||||
end
|
||||
|
||||
get 'lti/message_handler_link/:link_id', controller: 'lti/message',
|
||||
action: 'message_handler_link', as: :message_handler_link,
|
||||
constraints: {link_id: /\w+[.]\w+[.]/ }
|
||||
get 'lti/basic_lti_launch_request/:message_handler_id', controller: 'lti/message',
|
||||
action: 'basic_lti_launch_request', as: :basic_lti_launch_request
|
||||
get 'lti/tool_proxy_registration', controller: 'lti/message', action: 'registration', as: :tool_proxy_registration
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
#
|
||||
# Copyright (C) 2017 - 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 RemoveLookupIdFromLtiResourceHandler < ActiveRecord::Migration[4.2]
|
||||
tag :postdeploy
|
||||
|
||||
def change
|
||||
remove_column :lti_resource_handlers, :lookup_id
|
||||
end
|
||||
end
|
|
@ -17,6 +17,7 @@
|
|||
#
|
||||
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../../lti2_spec_helper')
|
||||
require_dependency "lti/message_controller"
|
||||
|
||||
module Lti
|
||||
|
@ -222,6 +223,68 @@ module Lti
|
|||
end
|
||||
end
|
||||
|
||||
describe "GET #message_handler_link" do
|
||||
include_context 'lti2_spec_helper'
|
||||
|
||||
let(:jwt_body) do
|
||||
{
|
||||
vendor_code: product_family.vendor_code,
|
||||
product_code: product_family.product_code,
|
||||
resource_type_code: resource_handler.resource_type_code
|
||||
}
|
||||
end
|
||||
let(:link_id) { JSON::JWT.new(jwt_body).to_s }
|
||||
|
||||
before do
|
||||
message_handler.update_attributes(message_type: MessageHandler::BASIC_LTI_LAUNCH_REQUEST)
|
||||
resource_handler.message_handlers = [message_handler]
|
||||
resource_handler.save!
|
||||
user_session(account_admin_user)
|
||||
end
|
||||
|
||||
it 'succeeds if tool is installed in the current account' do
|
||||
get 'message_handler_link', account_id: account.id, link_id: link_id
|
||||
expect(response).to be_ok
|
||||
end
|
||||
|
||||
it 'succeeds if the tool is installed in the current course' do
|
||||
tool_proxy.update_attributes(context: course)
|
||||
get 'message_handler_link', course_id: course.id, link_id: link_id
|
||||
expect(response).to be_ok
|
||||
end
|
||||
|
||||
it "succeeds if the tool is installed in the current course's account" do
|
||||
tool_proxy.update_attributes(context: account)
|
||||
get 'message_handler_link', course_id: course.id, link_id: link_id
|
||||
expect(response).to be_ok
|
||||
end
|
||||
|
||||
context 'search account chain' do
|
||||
let(:root_account) { Account.create! }
|
||||
|
||||
before { account.update_attributes(root_account: root_account) }
|
||||
|
||||
it "succeeds if the tool is installed in the current account's root account" do
|
||||
tool_proxy.update_attributes(context: root_account)
|
||||
get 'message_handler_link', account_id: account.id, link_id: link_id
|
||||
expect(response).to be_ok
|
||||
end
|
||||
|
||||
it "succeeds if the tool is installed in the current course's root account" do
|
||||
tool_proxy.update_attributes(context: root_account)
|
||||
get 'message_handler_link', course_id: course.id, link_id: link_id
|
||||
expect(response).to be_ok
|
||||
end
|
||||
end
|
||||
|
||||
it "renders 'not found' no message handler is found" do
|
||||
resource_handler.message_handlers = []
|
||||
resource_handler.save!
|
||||
get 'message_handler_link', account_id: account.id, link_id: link_id
|
||||
expect(response).to be_not_found
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET #basic_lti_launch_request" do
|
||||
before(:each) do
|
||||
course_with_student(account: account, active_all: true)
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 2017 - 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/>.
|
||||
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../../lti2_spec_helper')
|
||||
require 'db/migrate/20170508171328_change_lti_resouce_handler_lookup_id_not_null.rb'
|
||||
require 'spec_helper'
|
||||
|
||||
describe DataFixup::AddLookupToResourceHandlers do
|
||||
include_context 'lti2_spec_helper'
|
||||
|
||||
let(:mig_change_lookup_id) { ChangeLtiResouceHandlerLookupIdNotNull.new }
|
||||
|
||||
it 'sets the the lookup_id on existing resource handlers' do
|
||||
mig_change_lookup_id.migrate(:down)
|
||||
resource_handler.update_attribute(:lookup_id, nil)
|
||||
mig_change_lookup_id.migrate(:up)
|
||||
expected_id = Lti::ResourceHandler.generate_lookup_id_for(resource_handler)
|
||||
expected_id = expected_id.rpartition('-').first
|
||||
expect(resource_handler.reload.lookup_id.rpartition('-').first).to eq expected_id
|
||||
end
|
||||
end
|
|
@ -17,6 +17,8 @@
|
|||
#
|
||||
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../../lti2_spec_helper')
|
||||
|
||||
require_dependency "lti/message_handler"
|
||||
|
||||
module Lti
|
||||
|
@ -192,6 +194,71 @@ module Lti
|
|||
|
||||
end
|
||||
|
||||
describe '#self.by_resource_codes' do
|
||||
include_context 'lti2_spec_helper'
|
||||
|
||||
let(:jwt_body) do
|
||||
{
|
||||
vendor_code: product_family.vendor_code,
|
||||
product_code: product_family.product_code,
|
||||
resource_type_code: resource_handler.resource_type_code
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
message_handler.update_attributes(message_type: MessageHandler::BASIC_LTI_LAUNCH_REQUEST)
|
||||
end
|
||||
|
||||
it 'finds message handlers when tool is installed in current account' do
|
||||
tool_proxy.update_attributes(context: account)
|
||||
mh = MessageHandler.by_resource_codes(vendor_code: jwt_body[:vendor_code],
|
||||
product_code: jwt_body[:product_code],
|
||||
resource_type_code: jwt_body[:resource_type_code],
|
||||
context: tool_proxy.context)
|
||||
expect(mh).to eq message_handler
|
||||
end
|
||||
|
||||
it 'finds message handlers when tool is installed in current course' do
|
||||
tool_proxy.update_attributes(context: course)
|
||||
mh = MessageHandler.by_resource_codes(vendor_code: jwt_body[:vendor_code],
|
||||
product_code: jwt_body[:product_code],
|
||||
resource_type_code: jwt_body[:resource_type_code],
|
||||
context: tool_proxy.context)
|
||||
expect(mh).to eq message_handler
|
||||
end
|
||||
|
||||
it 'does not return message handlers with a different message_type' do
|
||||
message_handler.update_attributes(message_type: 'banana')
|
||||
mh = MessageHandler.by_resource_codes(vendor_code: jwt_body[:vendor_code],
|
||||
product_code: jwt_body[:product_code],
|
||||
resource_type_code: jwt_body[:resource_type_code],
|
||||
context: tool_proxy.context)
|
||||
expect(mh).to be_nil
|
||||
end
|
||||
|
||||
context 'account chain search' do
|
||||
it 'finds message handlers when tool is installed in course root account' do
|
||||
course.update_attributes(root_account: account)
|
||||
tool_proxy.update_attributes(context: account)
|
||||
mh = MessageHandler.by_resource_codes(vendor_code: jwt_body[:vendor_code],
|
||||
product_code: jwt_body[:product_code],
|
||||
resource_type_code: jwt_body[:resource_type_code],
|
||||
context: course)
|
||||
expect(mh).to eq message_handler
|
||||
end
|
||||
|
||||
it 'finds message handlers when tool is installed in account root account' do
|
||||
root_account = Account.create!
|
||||
account.update_attributes(root_account: root_account)
|
||||
mh = MessageHandler.by_resource_codes(vendor_code: jwt_body[:vendor_code],
|
||||
product_code: jwt_body[:product_code],
|
||||
resource_type_code: jwt_body[:resource_type_code],
|
||||
context: account)
|
||||
expect(mh).to eq message_handler
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def create_tool_proxy(opts = {})
|
||||
default_opts = {
|
||||
|
|
|
@ -41,41 +41,81 @@ module Lti
|
|||
resource_handler.save
|
||||
expect(resource_handler.errors.first).to eq [:tool_proxy, "can't be blank"]
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe 'set_lookup_id' do
|
||||
describe '#find_message_by_type' do
|
||||
let(:message_type) { 'custom-message-type' }
|
||||
|
||||
before do
|
||||
resource_handler.update_attributes(lookup_id: nil)
|
||||
message_handler.update_attributes(message_type: message_type)
|
||||
resource_handler.update_attributes(message_handlers: [message_handler])
|
||||
end
|
||||
|
||||
it 'sets the lookup_id if it is not set' do
|
||||
expect(resource_handler.lookup_id).to eq ResourceHandler.generate_lookup_id_for(resource_handler)
|
||||
it 'returns the message handler with the specified type' do
|
||||
expect(resource_handler.find_message_by_type(message_type)).to eq message_handler
|
||||
end
|
||||
|
||||
it "uses the 'product_code'" do
|
||||
pc = resource_handler.lookup_id.split('-').first
|
||||
expect(pc).to eq product_family.product_code
|
||||
it 'does not return messages with a different type' do
|
||||
message_handler.update_attributes(message_type: 'different-type')
|
||||
expect(resource_handler.find_message_by_type(message_type)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
it "uses the 'vendor_code'" do
|
||||
vc = resource_handler.lookup_id.split('-').second
|
||||
expect(vc).to eq product_family.vendor_code
|
||||
describe '#self.by_product_family' do
|
||||
before { resource_handler.update_attributes(tool_proxy: tool_proxy) }
|
||||
|
||||
it 'returns resource handlers with specified product family and context' do
|
||||
resource_handlers = ResourceHandler.by_product_family(product_family, tool_proxy.context)
|
||||
expect(resource_handlers).to include resource_handler
|
||||
end
|
||||
|
||||
it "uses the 'resource_type_code'" do
|
||||
rtc = resource_handler.lookup_id.split('-').third
|
||||
expect(rtc).to eq resource_handler.resource_type_code
|
||||
it 'does not return resource handlers with different product family' do
|
||||
pf = product_family.dup
|
||||
pf.update_attributes(product_code: SecureRandom.uuid)
|
||||
resource_handlers = ResourceHandler.by_product_family(pf, tool_proxy.context)
|
||||
expect(resource_handlers).not_to include resource_handler
|
||||
end
|
||||
|
||||
it "adds a signature to the lookup_id" do
|
||||
signature = resource_handler.lookup_id.split('-').last
|
||||
components = [product_family.product_code,
|
||||
product_family.vendor_code,
|
||||
resource_handler.resource_type_code].join('-')
|
||||
verified = Canvas::Security.verify_hmac_sha1(signature,
|
||||
components)
|
||||
expect(verified).to eq true
|
||||
it 'does not return resource handlers with different context' do
|
||||
a = Account.create!
|
||||
resource_handlers = ResourceHandler.by_product_family(product_family, a)
|
||||
expect(resource_handlers).not_to include resource_handler
|
||||
end
|
||||
end
|
||||
|
||||
describe '#self.by_resource_codes' do
|
||||
let(:jwt_body) do
|
||||
{
|
||||
vendor_code: product_family.vendor_code,
|
||||
product_code: product_family.product_code,
|
||||
resource_type_code: resource_handler.resource_type_code
|
||||
}
|
||||
end
|
||||
|
||||
it 'finds resource handlers specified in link id JWT' do
|
||||
resource_handlers = ResourceHandler.by_resource_codes(vendor_code: jwt_body[:vendor_code],
|
||||
product_code: jwt_body[:product_code],
|
||||
resource_type_code: jwt_body[:resource_type_code],
|
||||
context: tool_proxy.context)
|
||||
expect(resource_handlers).to match_array([resource_handler])
|
||||
end
|
||||
|
||||
it 'does not return resource handlers with the wrong resource type code' do
|
||||
jwt_body[:resource_type_code] = 'banana'
|
||||
resource_handlers = ResourceHandler.by_resource_codes(vendor_code: jwt_body[:vendor_code],
|
||||
product_code: jwt_body[:product_code],
|
||||
resource_type_code: jwt_body[:resource_type_code],
|
||||
context: tool_proxy.context)
|
||||
expect(resource_handlers).to be_blank
|
||||
end
|
||||
|
||||
it 'does not return resource handlers with different context' do
|
||||
a = Account.create!
|
||||
resource_handlers = ResourceHandler.by_resource_codes(vendor_code: jwt_body[:vendor_code],
|
||||
product_code: jwt_body[:product_code],
|
||||
resource_type_code: jwt_body[:resource_type_code],
|
||||
context: a)
|
||||
expect(resource_handlers).to be_blank
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in New Issue