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:
wdransfield 2017-05-17 12:52:00 -06:00 committed by Weston Dransfield
parent 0fbbecb5cd
commit 9acbad2a5c
9 changed files with 304 additions and 108 deletions

View File

@ -75,48 +75,21 @@ 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]))
resource_handler = message_handler.resource_handler
tool_proxy = resource_handler.tool_proxy
# TODO: create scope for query
if tool_proxy.workflow_state == 'active'
launch_params = {
launch_url: message_handler.launch_path,
oauth_consumer_key: tool_proxy.guid,
lti_version: IMS::LTI::Models::LTIModel::LTI_VERSION_2P0,
resource_link_id: build_resource_link_id(message_handler),
}
if params[:secure_params].present?
secure_params = Canvas::Security.decode_jwt(params[:secure_params])
launch_params.merge!({ext_lti_assignment_id: secure_params[:lti_assignment_id]}) if secure_params[:lti_assignment_id].present?
end
@lti_launch = Launch.new
tag = find_tag
custom_param_opts = prep_tool_settings(message_handler.parameters, tool_proxy, launch_params[:resource_link_id])
custom_param_opts[:content_tag] = tag if tag
variable_expander = create_variable_expander(custom_param_opts.merge(tool: tool_proxy))
launch_params.merge! enabled_parameters(tool_proxy, message_handler, variable_expander)
message = IMS::LTI::Models::Messages::BasicLTILaunchRequest.new(launch_params)
message.user_id = Lti::Asset.opaque_identifier_for(@current_user)
@active_tab = message_handler.asset_string
@lti_launch.resource_url = message.launch_url
@lti_launch.link_text = resource_handler.name
@lti_launch.launch_type = message.launch_presentation_document_target
module_sequence(tag) if tag
message.add_custom_params(custom_params(message_handler.parameters, variable_expander))
message.add_custom_params(ToolSetting.custom_settings(tool_proxy.id, @context, message.resource_link_id))
@lti_launch.params = message.signed_post_params(tool_proxy.shared_secret)
render Lti::AppUtil.display_template(display_override: params[:display]) and return
end
return lti2_basic_launch(message_handler)
end
not_found
end
@ -136,6 +109,48 @@ module Lti
private
def lti2_basic_launch(message_handler)
resource_handler = message_handler.resource_handler
tool_proxy = resource_handler.tool_proxy
# TODO: create scope for query
if tool_proxy.workflow_state == 'active'
launch_params = {
launch_url: message_handler.launch_path,
oauth_consumer_key: tool_proxy.guid,
lti_version: IMS::LTI::Models::LTIModel::LTI_VERSION_2P0,
resource_link_id: build_resource_link_id(message_handler),
}
if params[:secure_params].present?
secure_params = Canvas::Security.decode_jwt(params[:secure_params])
launch_params.merge!({ext_lti_assignment_id: secure_params[:lti_assignment_id]}) if secure_params[:lti_assignment_id].present?
end
@lti_launch = Launch.new
tag = find_tag
custom_param_opts = prep_tool_settings(message_handler.parameters, tool_proxy, launch_params[:resource_link_id])
custom_param_opts[:content_tag] = tag if tag
variable_expander = create_variable_expander(custom_param_opts.merge(tool: tool_proxy))
launch_params.merge! enabled_parameters(tool_proxy, message_handler, variable_expander)
message = IMS::LTI::Models::Messages::BasicLTILaunchRequest.new(launch_params)
message.user_id = Lti::Asset.opaque_identifier_for(@current_user)
@active_tab = message_handler.asset_string
@lti_launch.resource_url = message.launch_url
@lti_launch.link_text = resource_handler.name
@lti_launch.launch_type = message.launch_presentation_document_target
module_sequence(tag) if tag
message.add_custom_params(custom_params(message_handler.parameters, variable_expander))
message.add_custom_params(ToolSetting.custom_settings(tool_proxy.id, @context, message.resource_link_id))
@lti_launch.params = message.signed_post_params(tool_proxy.shared_secret)
render Lti::AppUtil.display_template(display_override: params[:display]) and return
end
end
def enabled_parameters(tp, mh, variable_expander)
tool_proxy = IMS::LTI::Models::ToolProxy.from_json(tp.raw_data)
enabled_capability = tool_proxy.enabled_capabilities

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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 = {

View File

@ -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
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 'vendor_code'" do
vc = resource_handler.lookup_id.split('-').second
expect(vc).to eq product_family.vendor_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 "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 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 "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 '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