Revert "Revert "Accept signed JWT tokens as the sourcedid for basic outcomes""

This reverts commit 50e604a8a9.

Change-Id: Ib9b403abe9dbfa5ba1468bd86317d210687b33c0
Reviewed-on: https://gerrit.instructure.com/133324
QA-Review: August Thornton <august@instructure.com>
Reviewed-by: Andrew Butterfield <abutterfield@instructure.com>
Tested-by: Jenkins
Product-Review: Weston Dransfield <wdransfield@instructure.com>
This commit is contained in:
Weston Dransfield 2017-11-20 17:07:38 +00:00
parent 90f2530420
commit 278682f152
21 changed files with 466 additions and 70 deletions

View File

@ -113,10 +113,14 @@ module Lti
# ensures that only this launch of the tool can modify the score.
def encode_source_id(assignment)
@tool.shard.activate do
if @root_account.feature_enabled?(:encrypted_sourcedids)
BasicLTI::Sourcedid.new(@tool, @context, assignment, @user).to_s
else
payload = [@tool.id, @context.id, assignment.id, @user.id].join('-')
"#{payload}-#{Canvas::Security.hmac_sha1(payload)}"
end
end
end
private
def default_launch_url(resource_type = nil)

View File

@ -33,35 +33,13 @@ module BasicLTI
end
end
class InvalidSourceId < StandardError
end
SOURCE_ID_REGEX = %r{^(\d+)-(\d+)-(\d+)-(\d+)-(\w+)$}
def self.decode_source_id(tool, sourceid)
tool.shard.activate do
raise InvalidSourceId, 'Invalid sourcedid' if sourceid.blank?
md = sourceid.match(SOURCE_ID_REGEX)
raise InvalidSourceId, 'Invalid sourcedid' unless md
new_encoding = [md[1], md[2], md[3], md[4]].join('-')
raise InvalidSourceId, 'Invalid signature' unless Canvas::Security.
verify_hmac_sha1(md[5], new_encoding, key: tool.shard.settings[:encryption_key])
raise InvalidSourceId, 'Tool is invalid' unless tool.id == md[1].to_i
course = Course.active.where(id: md[2]).first
raise InvalidSourceId, 'Course is invalid' unless course
user = course.student_enrollments.active.where(user_id: md[4]).first.try(:user)
raise InvalidSourceId, 'User is no longer in course' unless user
assignment = course.assignments.active.where(id: md[3]).first
raise InvalidSourceId, 'Assignment is invalid' unless assignment
tag = assignment.external_tool_tag if assignment
raise InvalidSourceId, 'Assignment is no longer associated with this tool' unless tag and tool.
matches_url?(tag.url, false) and tool.workflow_state != 'deleted'
return course, assignment, user
sourcedid = BasicLTI::Sourcedid.load!(sourceid)
raise BasicLTI::Errors::InvalidSourceId, 'Tool is invalid' unless tool == sourcedid.tool
return sourcedid.course, sourcedid.assignment, sourcedid.user
end
end
@ -183,7 +161,7 @@ module BasicLTI
begin
course, assignment, user = BasicLTI::BasicOutcomes.decode_source_id(tool, source_id)
rescue InvalidSourceId => e
rescue Errors::InvalidSourceId => e
self.code_major = 'failure'
self.description = e.to_s
self.body = "<#{operation_ref_identifier}Response />"

24
lib/basic_lti/errors.rb Normal file
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/>.
#
module BasicLTI
module Errors
class InvalidSourceId < StandardError
end
end
end

110
lib/basic_lti/sourcedid.rb Normal file
View File

@ -0,0 +1,110 @@
#
# 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/>.
#
module BasicLTI
class Sourcedid
SOURCE_ID_REGEX = %r{^(\d+)-(\d+)-(\d+)-(\d+)-(\w+)$}
attr_reader :tool, :course, :assignment, :user
def initialize(tool, course, assignment, user)
@tool, @course, @assignment, @user = tool, course, assignment, user
end
def to_s
crypted_token = Canvas::Security.create_encrypted_jwt(
jwt_payload,
self.class.signing_secret,
self.class.encryption_secret
)
Canvas::Security.base64_encode(crypted_token)
end
def jwt_payload
{
iss: "Canvas",
aud: ["Instructure"],
iat: Time.zone.now.to_i,
tool_id: tool.id,
course_id: course.id,
assignment_id: assignment.id,
user_id: user.id,
}
end
private :jwt_payload
def validate!
raise Errors::InvalidSourceId, 'Course is invalid' unless course
raise Errors::InvalidSourceId, 'User is no longer in course' unless user
raise Errors::InvalidSourceId, 'Assignment is invalid' unless assignment
tag = assignment.external_tool_tag
raise Errors::InvalidSourceId, 'Assignment is no longer associated with this tool' unless tag && tool.
matches_url?(tag.url, false) && tool.workflow_state != 'deleted'
end
def self.load!(sourcedid_string)
raise Errors::InvalidSourceId, 'Invalid sourcedid' if sourcedid_string.blank?
token = load_from_legacy_sourcedid!(sourcedid_string) ||
token_from_sourcedid!(sourcedid_string)
tool = ContextExternalTool.find_by(id: token[:tool_id])
course = Course.active.find_by(id: token[:course_id])
if course
user = course.student_enrollments.active.find_by(user_id: token[:user_id])&.user
assignment = course.assignments.active.find_by(id: token[:assignment_id])
end
sourcedid = self.new(tool, course, assignment, user)
sourcedid.validate!
sourcedid
end
def self.load_from_legacy_sourcedid!(sourcedid)
token = nil
md = sourcedid.match(SOURCE_ID_REGEX)
if md
tool = ContextExternalTool.find_by(id: md[1])
raise Errors::InvalidSourceId, 'Tool is invalid' unless tool
new_encoding = [md[1], md[2], md[3], md[4]].join('-')
raise Errors::InvalidSourceId, 'Invalid signature' unless Canvas::Security.
verify_hmac_sha1(md[5], new_encoding, key: tool.shard.settings[:encryption_key])
token = { tool_id: md[1].to_i, course_id: md[2], assignment_id: md[3], user_id: md[4] }
end
token
end
def self.token_from_sourcedid!(sourcedid)
Canvas::Security.decrypt_services_jwt(
Canvas::Security.base64_decode(sourcedid),
signing_secret,
encryption_secret
)
rescue JSON::JWT::InvalidFormat
raise Errors::InvalidSourceId, 'Invalid sourcedid'
end
def self.signing_secret
Canvas::DynamicSettings.find()["lti-signing-secret"]
end
def self.encryption_secret
Canvas::DynamicSettings.find()["lti-encryption-secret"]
end
end
end

View File

@ -557,6 +557,12 @@ END
state: "allowed",
root_opt_in: true
},
'encrypted_sourcedids' => {
display_name: -> { I18n.t('Encrypted Sourcedids for Basic Outcomes') },
description: -> { I18n.t('If enabled, Sourcedids used by Canvas for Basic Outcomes will be encrypted.') },
applies_to: 'RootAccount',
state: 'allowed'
}
)
def self.definitions

View File

@ -39,6 +39,11 @@ describe LtiApiController, type: :request do
}
end
before do
allow(BasicLTI::Sourcedid).to receive(:encryption_secret) {'encryption-secret-5T14NjaTbcYjc4'}
allow(BasicLTI::Sourcedid).to receive(:signing_secret) {'signing-secret-vp04BNqApwdwUYPUI'}
end
def lti_api_call(method, path, body = nil)
consumer = OAuth::Consumer.new(tool.consumer_key, tool.shared_secret, :site => "https://www.example.com", :signature_method => "HMAC-SHA1")

View File

@ -118,6 +118,11 @@ describe ExternalToolsController, type: :request do
end
context 'assessment launch' do
before do
allow(BasicLTI::Sourcedid).to receive(:encryption_secret) {'encryption-secret-5T14NjaTbcYjc4'}
allow(BasicLTI::Sourcedid).to receive(:signing_secret) {'signing-secret-vp04BNqApwdwUYPUI'}
end
it 'returns a bad request response if there is no assignment_id' do
params = {id: tool.id.to_s, launch_type: 'assessment'}
code = get_raw_sessionless_launch_url(@course, 'course', params)

View File

@ -439,6 +439,8 @@ describe AssignmentsController do
Setting.set('enable_page_views', 'db')
@old_thread_context = Thread.current[:context]
Thread.current[:context] = { request_id: SecureRandom.uuid }
allow(BasicLTI::Sourcedid).to receive(:encryption_secret) {'encryption-secret-5T14NjaTbcYjc4'}
allow(BasicLTI::Sourcedid).to receive(:signing_secret) {'signing-secret-vp04BNqApwdwUYPUI'}
end
after do

View File

@ -1227,6 +1227,11 @@ describe ExternalToolsController do
end
describe "'GET 'generate_sessionless_launch'" do
before do
allow(BasicLTI::Sourcedid).to receive(:encryption_secret) {'encryption-secret-5T14NjaTbcYjc4'}
allow(BasicLTI::Sourcedid).to receive(:signing_secret) {'signing-secret-vp04BNqApwdwUYPUI'}
end
it "generates a sessionless launch" do
@tool = new_valid_tool(@course)
user_session(@user)

View File

@ -32,6 +32,11 @@ describe LtiApiController, type: :request do
tag.save!
end
before do
allow(BasicLTI::Sourcedid).to receive(:encryption_secret) {'encryption-secret-5T14NjaTbcYjc4'}
allow(BasicLTI::Sourcedid).to receive(:signing_secret) {'signing-secret-vp04BNqApwdwUYPUI'}
end
def check_error_response(message, check_generated_sig=true)
expect(response.body.strip).to_not be_empty, "Should not have an empty response body"
@ -266,8 +271,6 @@ XML
desc = xml.at_css('imsx_description').content.match(/(?<description>.+)\n\[EID_(?<error_report>[^\]]+)\]/)
expect(desc[:description]).to eq error_message if error_message
expect(desc[:error_report]).to_not be_empty
end
def check_success

View File

@ -0,0 +1,29 @@
#
# 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/>.
#
module Factories
def external_tool_model(context: nil)
context ||= course_model
context.context_external_tools.create(
:name => "a",
:url => "http://google.com",
:consumer_key => '12345',
:shared_secret => 'secret'
)
end
end

View File

@ -23,6 +23,8 @@ require 'nokogiri'
describe "External Tools" do
describe "Assignments" do
before do
allow(BasicLTI::Sourcedid).to receive(:encryption_secret) {'encryption-secret-5T14NjaTbcYjc4'}
allow(BasicLTI::Sourcedid).to receive(:signing_secret) {'signing-secret-vp04BNqApwdwUYPUI'}
course_factory(active_all: true)
assignment_model(:course => @course, :submission_types => "external_tool", :points_possible => 25)
@tool = @course.context_external_tools.create!(:shared_secret => 'test_secret', :consumer_key => 'test_key', :name => 'my grade passback test tool', :domain => 'example.com')
@ -50,16 +52,17 @@ describe "External Tools" do
end
it "should include outcome service params when viewing as student" do
allow_any_instance_of(Account).to receive(:feature_enabled?) { false }
allow_any_instance_of(Account).to receive(:feature_enabled?).with(:encrypted_sourcedids).and_return(true)
allow(Canvas::Security).to receive(:create_encrypted_jwt) { 'an.encrypted.jwt' }
student_in_course(:course => @course, :active_all => true)
user_session(@user)
allow(Canvas::Security).to receive(:hmac_sha1).and_return('some_sha')
payload = [@tool.id, @course.id, @assignment.id, @user.id].join('-')
get "/courses/#{@course.id}/assignments/#{@assignment.id}"
expect(response).to be_success
doc = Nokogiri::HTML.parse(response.body)
expect(doc.at_css('form#tool_form input#lis_result_sourcedid')['value']).to eq "#{payload}-some_sha"
expect(doc.at_css('form#tool_form input#lis_result_sourcedid')['value']).to eq BasicLTI::Sourcedid.new(@tool, @course, @assignment, @user).to_s
expect(doc.at_css('form#tool_form input#lis_outcome_service_url')['value']).to eq lti_grade_passback_api_url(@tool)
expect(doc.at_css('form#tool_form input#ext_ims_lis_basic_outcome_url')['value']).to eq blti_legacy_grade_passback_api_url(@tool)
end

View File

@ -109,63 +109,56 @@ describe BasicLTI::BasicOutcomes do
end
describe ".decode_source_id" do
it 'successfully decodes a source_id' do
expect(described_class.decode_source_id(tool, source_id)).to eq [@course, assignment, @user]
end
it 'throws Invalid sourcedid if sourcedid is nil' do
expect{described_class.decode_source_id(tool, nil)}.
to raise_error(BasicLTI::BasicOutcomes::InvalidSourceId, 'Invalid sourcedid')
to raise_error(BasicLTI::Errors::InvalidSourceId, 'Invalid sourcedid')
end
it 'throws Invalid sourcedid if sourcedid is empty' do
expect{described_class.decode_source_id(tool, "")}.
to raise_error(BasicLTI::BasicOutcomes::InvalidSourceId, 'Invalid sourcedid')
end
it 'throws Invalid sourcedid if no signature' do
missing_signature = source_id.split('-')[0..3].join('-')
expect{described_class.decode_source_id(tool, missing_signature)}.
to raise_error(BasicLTI::BasicOutcomes::InvalidSourceId, 'Invalid sourcedid')
to raise_error(BasicLTI::Errors::InvalidSourceId, 'Invalid sourcedid')
end
it 'throws Invalid signature if the signature is invalid' do
bad_signature = source_id.split('-')[0..3].join('-') + '-asb9dksld9k3'
expect{described_class.decode_source_id(tool, bad_signature)}.
to raise_error(BasicLTI::BasicOutcomes::InvalidSourceId, 'Invalid signature')
to raise_error(BasicLTI::Errors::InvalidSourceId, 'Invalid signature')
end
it "throws Tool is invalid if the tool doesn't match" do
it "throws 'Tool is invalid' if the tool doesn't match" do
t = @course.context_external_tools.
create(:name => "b", :url => "http://google.com", :consumer_key => '12345', :shared_secret => 'secret')
expect{described_class.decode_source_id(tool, gen_source_id(t: t))}.
to raise_error(BasicLTI::BasicOutcomes::InvalidSourceId, 'Tool is invalid')
to raise_error(BasicLTI::Errors::InvalidSourceId, 'Tool is invalid')
end
it "throws Course is invalid if the course doesn't match" do
@course.workflow_state = 'deleted'
@course.save!
expect{described_class.decode_source_id(tool, source_id)}.
to raise_error(BasicLTI::BasicOutcomes::InvalidSourceId, 'Course is invalid')
to raise_error(BasicLTI::Errors::InvalidSourceId, 'Course is invalid')
end
it "throws User is no longer in course isuser enrollment is missing" do
@user.enrollments.destroy_all
expect{described_class.decode_source_id(tool, source_id)}.
to raise_error(BasicLTI::BasicOutcomes::InvalidSourceId, 'User is no longer in course')
to raise_error(BasicLTI::Errors::InvalidSourceId, 'User is no longer in course')
end
it "throws Assignment is invalid if the Addignment doesn't match" do
assignment.destroy
expect{described_class.decode_source_id(tool, source_id)}.
to raise_error(BasicLTI::BasicOutcomes::InvalidSourceId, 'Assignment is invalid')
to raise_error(BasicLTI::Errors::InvalidSourceId, 'Assignment is invalid')
end
it "throws Assignment is no longer associated with this tool if tool is deleted" do
tool.destroy
expect{described_class.decode_source_id(tool, source_id)}.
to raise_error(BasicLTI::BasicOutcomes::InvalidSourceId, 'Assignment is no longer associated with this tool')
to raise_error(BasicLTI::Errors::InvalidSourceId, 'Assignment is no longer associated with this tool')
end
it "throws Assignment is no longer associated with this tool if tool doesn't match the url" do
@ -173,15 +166,39 @@ describe BasicLTI::BasicOutcomes do
tag.url = 'example.com'
tag.save!
expect{described_class.decode_source_id(tool, source_id)}.
to raise_error(BasicLTI::BasicOutcomes::InvalidSourceId, 'Assignment is no longer associated with this tool')
to raise_error(BasicLTI::Errors::InvalidSourceId, 'Assignment is no longer associated with this tool')
end
it "throws Assignment is no longer associated with this tool if tag is missing" do
assignment.external_tool_tag.delete
expect{described_class.decode_source_id(tool, source_id)}.
to raise_error(BasicLTI::BasicOutcomes::InvalidSourceId, 'Assignment is no longer associated with this tool')
to raise_error(BasicLTI::Errors::InvalidSourceId, 'Assignment is no longer associated with this tool')
end
context "jwt sourcedid" do
before do
dynamic_settings = {
"lti-signing-secret" => 'signing-secret-vp04BNqApwdwUYPUI',
"lti-encryption-secret" => 'encryption-secret-5T14NjaTbcYjc4'
}
allow(Canvas::DynamicSettings).to receive(:find) { dynamic_settings }
end
let(:jwt_source_id) do
BasicLTI::Sourcedid.new(tool, @course, assignment, @user).to_s
end
it "decodes a jwt signed sourcedid" do
expect(described_class.decode_source_id(tool, jwt_source_id)).to eq [@course, assignment, @user]
end
it 'throws invalid JWT if token is unrecognized' do
missing_signature = source_id.split('-')[0..3].join('-')
expect{described_class.decode_source_id(tool, missing_signature)}.
to raise_error(BasicLTI::Errors::InvalidSourceId, 'Invalid sourcedid')
end
end
end
describe "#handle_request" do

View File

@ -0,0 +1,137 @@
#
# 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__) + '/../../spec_helper.rb')
describe BasicLTI::Sourcedid do
subject(:sourcedid) {described_class.new(tool, course, assignment, user)}
let(:tool) { external_tool_model(context: course) }
let(:course) { course_model }
let(:assignment) do
course.assignments.create!(
{
title: "value for title",
description: "value for description",
due_at: Time.zone.now,
points_possible: "1.5",
submission_types: 'external_tool',
external_tool_tag_attributes: {url: tool.url}
}
)
end
let(:user) { course_with_student(course: course).user }
before do
fake_secrets = {
'lti-signing-secret' => 'signing-secret-vp04BNqApwdwUYPUI',
'lti-encryption-secret' => 'encryption-secret-5T14NjaTbcYjc4',
}
allow(Canvas::DynamicSettings).to receive(:find).and_return(fake_secrets)
end
it 'creates a signed and encrypted sourcedid' do
timestamp = Time.zone.now
allow(Time.zone).to receive(:now).and_return(timestamp)
token = BasicLTI::Sourcedid.token_from_sourcedid!(sourcedid.to_s)
expect(token[:iss]).to eq 'Canvas'
expect(token[:aud]).to eq ['Instructure']
expect(token[:iat]).to eq timestamp.to_i
expect(token[:tool_id]).to eq tool.id
expect(token[:course_id]).to eq course.id
expect(token[:assignment_id]).to eq assignment.id
expect(token[:user_id]).to eq user.id
end
describe ".load!" do
it 'raises an exception for an invalid sourcedid' do
expect{ described_class.load!('invalid-sourcedid') }.to raise_error(
BasicLTI::Errors::InvalidSourceId, 'Invalid sourcedid'
)
end
it 'raises an exception for a nil sourcedid' do
expect{ described_class.load!(nil) }.to raise_error(
BasicLTI::Errors::InvalidSourceId, 'Invalid sourcedid'
)
end
context "legacy sourcedid" do
it 'raises an exception when improperly signed' do
sourcedid = "#{tool.id}-#{course.id}-#{assignment.id}-#{user.id}-badsignature"
expect{ described_class.load!(sourcedid) }.to raise_error(
BasicLTI::Errors::InvalidSourceId, 'Invalid signature'
)
end
it 'raises an exception when the tool id is invalid' do
sourcedid = "9876543210-#{course.id}-#{assignment.id}-#{user.id}-badsignature"
expect{ described_class.load!(sourcedid) }.to raise_error(
BasicLTI::Errors::InvalidSourceId, 'Tool is invalid'
)
end
it 'builds a sourcedid' do
payload = [tool.id, course.id, assignment.id, user.id].join('-')
legacy_sourcedid = "#{payload}-#{Canvas::Security.hmac_sha1(payload)}"
sourcedid = described_class.load!(legacy_sourcedid)
expect(sourcedid.tool).to eq tool
expect(sourcedid.course).to eq course
expect(sourcedid.assignment).to eq assignment
expect(sourcedid.user).to eq user
end
end
it 'raises an exception when the course is invalid' do
course.destroy!
expect{ described_class.load!(sourcedid.to_s) }.to raise_error(
BasicLTI::Errors::InvalidSourceId, 'Course is invalid'
)
end
it 'raises an exception when the user is not in the course' do
user.enrollments.find_by(course_id: course.id).destroy!
expect{ described_class.load!(sourcedid.to_s) }.to raise_error(
BasicLTI::Errors::InvalidSourceId, 'User is no longer in course'
)
end
it 'raises an exception when the assignment is not valid' do
assignment.destroy!
expect{ described_class.load!(sourcedid.to_s) }.to raise_error(
BasicLTI::Errors::InvalidSourceId, 'Assignment is invalid'
)
end
it 'raises an exception when the assignment is not associated with the tool' do
assignment.external_tool_tag.update(url: 'http://invalidurl.com')
expect{ described_class.load!(sourcedid.to_s) }.to raise_error(
BasicLTI::Errors::InvalidSourceId, 'Assignment is no longer associated with this tool'
)
end
end
end

View File

@ -20,6 +20,11 @@ require File.expand_path(File.dirname(__FILE__) + '/turnitin_spec_helper')
require 'turnitin_api'
module Turnitin
describe OutcomeResponseProcessor do
before do
allow(BasicLTI::Sourcedid).to receive(:encryption_secret) {'encryption-secret-5T14NjaTbcYjc4'}
allow(BasicLTI::Sourcedid).to receive(:signing_secret) {'signing-secret-vp04BNqApwdwUYPUI'}
end
include_context "shared_tii_lti"
subject { described_class.new(tool, lti_assignment, lti_student, outcome_response_json) }

View File

@ -18,6 +18,11 @@
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
RSpec.shared_context "shared_tii_lti", :shared_context => :metadata do
before do
allow(BasicLTI::Sourcedid).to receive(:encryption_secret) {'encryption-secret-5T14NjaTbcYjc4'}
allow(BasicLTI::Sourcedid).to receive(:signing_secret) {'signing-secret-vp04BNqApwdwUYPUI'}
end
let(:lti_student) { user_model }
let(:lti_course) { course_with_student({user: lti_student}).course }
let(:tool) do

View File

@ -392,6 +392,11 @@ describe "LTI integration tests" do
end
context "outcome launch" do
before do
allow(BasicLTI::Sourcedid).to receive(:encryption_secret) {'encryption-secret-5T14NjaTbcYjc4'}
allow(BasicLTI::Sourcedid).to receive(:signing_secret) {'signing-secret-vp04BNqApwdwUYPUI'}
end
def tool_setup(for_student=true)
if for_student
course_with_student(:active_all => true)
@ -409,11 +414,12 @@ describe "LTI integration tests" do
end
it "should include assignment outcome service params for student" do
allow(Canvas::Security).to receive(:hmac_sha1).and_return('some_sha')
hash = tool_setup
allow(Canvas::Security).to receive(:create_encrypted_jwt) { 'an.encrypted.jwt' }
allow_any_instance_of(Account).to receive(:feature_enabled?) { false }
allow_any_instance_of(Account).to receive(:feature_enabled?).with(:encrypted_sourcedids).and_return(true)
payload = [@tool.id, @course.id, @assignment.id, @user.id].join('-')
expect(hash['lis_result_sourcedid']).to eq "#{payload}-some_sha"
hash = tool_setup
expect(hash['lis_result_sourcedid']).to eq BasicLTI::Sourcedid.new(@tool, @course, @assignment, @user).to_s
expect(hash['lis_outcome_service_url']).to eq "/my/test/url"
expect(hash['ext_ims_lis_basic_outcome_url']).to eq "/my/other/test/url"
expect(hash['ext_outcome_data_values_accepted']).to eq 'url,text'

View File

@ -238,6 +238,8 @@ describe Lti::LtiOutboundAdapter do
before(:each) do
allow(LtiOutbound::ToolLaunch).to receive(:new).and_return(tool_launch)
allow(BasicLTI::Sourcedid).to receive(:encryption_secret) {'encryption-secret-5T14NjaTbcYjc4'}
allow(BasicLTI::Sourcedid).to receive(:signing_secret) {'signing-secret-vp04BNqApwdwUYPUI'}
end
it "includes the 'ext_lti_assignment_id' parameter" do
@ -256,20 +258,6 @@ describe Lti::LtiOutboundAdapter do
adapter.generate_post_payload_for_assignment(assignment, outcome_service_url, legacy_outcome_service_url, lti_turnitin_outcomes_placement_url)
end
it "generates the correct source_id for the assignment" do
generated_sha = 'generated_sha'
allow(Canvas::Security).to receive(:hmac_sha1).and_return(generated_sha)
source_id = "tool_id-course_id-assignment_id-#{user.id}-#{generated_sha}"
allow(tool_launch).to receive(:for_assignment!)
assignment_creator = double
allow(assignment_creator).to receive(:convert).and_return(tool_launch)
adapter.prepare_tool_launch(return_url, variable_expander)
expect(Lti::LtiAssignmentCreator).to receive(:new).with(assignment, source_id).and_return(assignment_creator)
adapter.generate_post_payload_for_assignment(assignment, outcome_service_url, legacy_outcome_service_url, lti_turnitin_outcomes_placement_url)
end
it "raises a not prepared error if the tool launch has not been prepared" do
expect {
adapter.generate_post_payload_for_assignment(assignment, outcome_service_url, legacy_outcome_service_url, lti_turnitin_outcomes_placement_url)
@ -315,4 +303,62 @@ describe Lti::LtiOutboundAdapter do
expect(Lti::LtiOutboundAdapter.consumer_instance_class).to eq LtiOutbound::LTIConsumerInstance
end
end
describe '#encode_source_id' do
let(:user) do
student_in_course
@student
end
let(:assignment) { assignment_model(course: @course) }
let(:course) { assignment.course }
let(:tool) { external_tool_model(context: course) }
let(:adapter) { Lti::LtiOutboundAdapter.new(tool, user, course) }
let(:enrollment) { StudentEnrollment.create!(user: user, course: course, workflow_state: 'active') }
before do
allow(BasicLTI::Sourcedid).to receive(:encryption_secret) {'encryption-secret-5T14NjaTbcYjc4'}
allow(BasicLTI::Sourcedid).to receive(:signing_secret) {'signing-secret-vp04BNqApwdwUYPUI'}
assignment.update_attributes!(
external_tool_tag: ContentTag.create!(
context: assignment,
content: tool,
title: 'test',
url: tool.url
)
)
end
it 'builds the expected encrypted JWT with the correct course data' do
allow_any_instance_of(Account).to receive(:feature_enabled?).with(:encrypted_sourcedids).and_return(true)
sourced_id = adapter.encode_source_id(assignment)
parsed_sourced_id = BasicLTI::Sourcedid.load! sourced_id
expect(parsed_sourced_id.course).to eq course
end
it 'builds the expected encrypted JWT with the correct assignment data' do
allow_any_instance_of(Account).to receive(:feature_enabled?).with(:encrypted_sourcedids).and_return(true)
sourced_id = adapter.encode_source_id(assignment)
parsed_sourced_id = BasicLTI::Sourcedid.load! sourced_id
expect(parsed_sourced_id.assignment).to eq assignment
end
it 'builds the expected encrypted JWT with the correct user data' do
allow_any_instance_of(Account).to receive(:feature_enabled?).with(:encrypted_sourcedids).and_return(true)
sourced_id = adapter.encode_source_id(assignment)
parsed_sourced_id = BasicLTI::Sourcedid.load! sourced_id
expect(parsed_sourced_id.user).to eq user
end
it 'uses the new sourcedids if the "encrypted_sourcedids" FF is enabled' do
allow_any_instance_of(Account).to receive(:feature_enabled?).with(:encrypted_sourcedids).and_return(true)
sourced_id = adapter.encode_source_id(assignment)
expect(sourced_id).not_to match(BasicLTI::Sourcedid::SOURCE_ID_REGEX)
end
it 'uses legacy sourcedids if the "encrypted_sourcedids" FF is disabled' do
allow_any_instance_of(Account).to receive(:feature_enabled?).with(:encrypted_sourcedids).and_return(false)
sourced_id = adapter.encode_source_id(assignment)
expect(sourced_id).to match(BasicLTI::Sourcedid::SOURCE_ID_REGEX)
end
end
end

View File

@ -105,6 +105,8 @@ describe "external tool assignments" do
end
it "should show module sequence even without module_item_id param" do
allow(BasicLTI::Sourcedid).to receive(:encryption_secret) {'encryption-secret-5T14NjaTbcYjc4'}
allow(BasicLTI::Sourcedid).to receive(:signing_secret) {'signing-secret-vp04BNqApwdwUYPUI'}
a = assignment_model(:course => @course, :title => "test2", :submission_types => 'external_tool')
a.create_external_tool_tag(:url => @t1.url)
a.external_tool_tag.update_attribute(:content_type, 'ContextExternalTool')

View File

@ -492,6 +492,8 @@ describe "context modules" do
end
it "shows Mark as Done button for assignments with external tool submission", priority: "2", test_id: 3340306 do
allow(BasicLTI::Sourcedid).to receive(:encryption_secret) {'encryption-secret-5T14NjaTbcYjc4'}
allow(BasicLTI::Sourcedid).to receive(:signing_secret) {'signing-secret-vp04BNqApwdwUYPUI'}
tool = @course.context_external_tools.create!(name: "a",
url: "example.com",
consumer_key: '12345',

View File

@ -87,6 +87,8 @@ describe "assignments" do
end
it "should validate an assignment created with the type of external tool", priority: "1", test_id: 2624905 do
allow(BasicLTI::Sourcedid).to receive(:encryption_secret) {'encryption-secret-5T14NjaTbcYjc4'}
allow(BasicLTI::Sourcedid).to receive(:signing_secret) {'signing-secret-vp04BNqApwdwUYPUI'}
t1 = factory_with_protected_attributes(@course.context_external_tools, :url => "http://www.example.com/", :shared_secret => 'test123', :consumer_key => 'test123', :name => 'tool 1')
external_tool_assignment = assignment_model(:course => @course, :title => "test2", :submission_types => 'external_tool')
external_tool_assignment.create_external_tool_tag(:url => t1.url)