diff --git a/Gemfile.d/other_stuff.rb b/Gemfile.d/other_stuff.rb index fee043cf310..87710d69f65 100644 --- a/Gemfile.d/other_stuff.rb +++ b/Gemfile.d/other_stuff.rb @@ -46,7 +46,7 @@ gem 'hoe', '3.8.1' gem 'i18n', '0.6.9' gem 'i18nema', RUBY_VERSION >= '2.2' ? '0.0.8' : '0.0.7' gem 'icalendar', '1.5.4' -gem 'ims-lti', '2.0.0.beta.6' +gem 'ims-lti', '2.0.0.beta.7' gem 'jammit', '0.6.6' gem 'cssmin', '1.0.3' gem 'jsmin', '1.0.1' diff --git a/app/controllers/lti/message_controller.rb b/app/controllers/lti/message_controller.rb index 6987696460a..27b49f0ac3c 100644 --- a/app/controllers/lti/message_controller.rb +++ b/app/controllers/lti/message_controller.rb @@ -27,50 +27,49 @@ module Lti @lti_launch.params = message.post_params @lti_launch.link_text = I18n.t('lti2.register_tool', 'Register Tool') @lti_launch.launch_type = message.launch_presentation_document_target - + render template: 'lti/framed_launch' end def basic_lti_launch_request - if message_handler = MessageHandler.where(id: params[:lti_message_handler_id]).first - resource_handler = message_handler.resource + if message_handler = MessageHandler.find(params[:message_handler_id]) + resource_handler = message_handler.resource_handler tool_proxy = resource_handler.tool_proxy #TODO create scoped method for query - if ToolProxyBinding.where(tool_proxy_id: tool_proxy.id, context_id: @context.id, context_type: @context.class).count(:all) > 0 + if tool_proxy.workflow_state == 'active' message = IMS::LTI::Models::Messages::BasicLTILaunchRequest.new( launch_url: message_handler.launch_path, oauth_consumer_key: tool_proxy.guid, lti_version: IMS::LTI::Models::LTIModel::LTI_VERSION_2P0, - resource_link_id: Lti::Asset.opaque_identifier_for(@context), + resource_link_id: build_resource_link_id(tool_proxy), context_id: Lti::Asset.opaque_identifier_for(@context), tool_consumer_instance_guid: @context.root_account.lti_guid, launch_presentation_document_target: IMS::LTI::Models::Messages::Message::LAUNCH_TARGET_IFRAME ) - message.add_custom_params(custom_params(message_handler.parameters)) + message.add_custom_params(custom_params(message_handler.parameters, tool_proxy, message.resource_link_id)) @lti_launch = Launch.new @lti_launch.resource_url = message.launch_url @lti_launch.params = message.signed_post_params(tool_proxy.shared_secret) - @lti_launch.link_text = message_handler.resource.name + @lti_launch.link_text = resource_handler.name @lti_launch.launch_type = message.launch_presentation_document_target render template: 'lti/framed_launch' and return end end - not_found and return + not_found end private - def custom_params(parameters) - lookup_hash = common_variable_substitutions.inject({}) { |hash, (k,v)| hash[k.gsub(/\A\$/, '')] = v ; hash} + def custom_params(parameters, tool_proxy, resource_link_id) params = IMS::LTI::Models::Parameter.from_json(parameters || []) - IMS::LTI::Models::Parameter.process_params(params, lookup_hash) + IMS::LTI::Models::Parameter.process_params(params, lti2_variable_substitutions(parameters, tool_proxy, resource_link_id)) end def tool_consumer_profile_url - tp_id = SecureRandom.uuid + tp_id = "339b6700-e4cb-47c5-a54f-3ee0064921a9" #Hard coded until we start persisting the tcp case context when Course course_tool_consumer_profile_url(context, tp_id) @@ -92,5 +91,44 @@ module Lti end end + def find_binding(tool_proxy) + if @context.is_a?(Course) + binding = ToolProxyBinding.where(context_type: 'Course', context: @context.id, tool_proxy_id: tool_proxy.id) + return binding if binding + end + account_ids = @context.account_chain.map{ |a| a.id } + bindings = ToolProxyBinding.where(context_type: 'Account', context_id: account_ids, tool_proxy_id: tool_proxy.id) + binding_lookup = bindings.each_with_object({}) {|binding, hash| hash[binding.context_id] = binding } + sorted_bindings = account_ids.map { |account_id| binding_lookup[account_id] } + sorted_bindings.first + end + + def build_resource_link_id(message_handler, postfix = nil) + resource_link_id = "#{params[:tool_launch_context]}_#{message_handler.id}" + resource_link_id += "_#{params[:postfix_id]}" if params[:postfix_id] + Base64.urlsafe_encode64("#{resource_link_id}") + end + + def lti2_variable_substitutions(parameters, tool_proxy, resource_link_id) + substitutions = common_variable_substitutions.inject({}) { |hash, (k,v)| hash[k.gsub(/\A\$/, '')] = v ; hash} + substitutions.merge!(prep_tool_settings(parameters, tool_proxy, resource_link_id)) + substitutions + end + + def prep_tool_settings(parameters, tool_proxy, resource_link_id) + if parameters && (parameters.map {|p| p['variable']}.compact & (%w( LtiLink.custom.url ToolProxyBinding.custom.url ToolProxy.custom.url ))).any? + link = ToolSetting.first_or_create(tool_proxy: tool_proxy, context: @context, resource_link_id: resource_link_id) + binding = ToolSetting.first_or_create(tool_proxy: tool_proxy, context: @context, resource_link_id: nil) + proxy = ToolSetting.first_or_create(tool_proxy: tool_proxy, context: nil, resource_link_id: nil) + { + 'LtiLink.custom.url' => show_lti_tool_settings_url(link.id), + 'ToolProxyBinding.custom.url' => show_lti_tool_settings_url(binding.id), + 'ToolProxy.custom.url' => show_lti_tool_settings_url(proxy.id) + } + else + {} + end + end + end end \ No newline at end of file diff --git a/app/controllers/lti/tool_consumer_profile_controller.rb b/app/controllers/lti/tool_consumer_profile_controller.rb index beea2aadafd..1503183e3fc 100644 --- a/app/controllers/lti/tool_consumer_profile_controller.rb +++ b/app/controllers/lti/tool_consumer_profile_controller.rb @@ -24,23 +24,12 @@ module Lti def show uuid = "339b6700-e4cb-47c5-a54f-3ee0064921a9" #Hard coded until we start persisting the tcp - profile = Lti::ToolConsumerProfileCreator.new(@account, tool_consumer_profile_url(uuid), tool_proxy_url).create + profile = Lti::ToolConsumerProfileCreator.new(@account, tool_consumer_profile_url(uuid), request.domain, context.class.name.downcase).create render json: profile.to_json, :content_type => 'application/json' end private - def tool_proxy_url - case context - when Course - create_course_lti_tool_proxy_url(context) - when Account - create_account_lti_tool_proxy_url(context) - else - raise "Unsupported context" - end - end - def tool_consumer_profile_url(uuid) case context when Course diff --git a/app/controllers/lti/tool_proxy_controller.rb b/app/controllers/lti/tool_proxy_controller.rb index 0ed86375340..21631714d2b 100644 --- a/app/controllers/lti/tool_proxy_controller.rb +++ b/app/controllers/lti/tool_proxy_controller.rb @@ -17,13 +17,15 @@ module Lti class ToolProxyController < ApplicationController + include Lti::ApiServiceHelper + before_filter :require_context, :except => [:show] skip_before_filter :require_user, only: [:create, :show] skip_before_filter :load_user, only: [:create, :show] def show tool_proxy = ToolProxy.where(guid: params['tool_proxy_guid']).first - if tool_proxy && authorized_request?(tool_proxy.shared_secret) + if tool_proxy && oauth_authenticated_request?(tool_proxy.shared_secret) render json: tool_proxy.raw_data else render json: {error: 'unauthorized'}, status: :unauthorized @@ -32,7 +34,7 @@ module Lti def create secret = RegistrationRequestService.retrieve_registration_password(oauth_consumer_key) - if authorized_request?(secret) + if oauth_authenticated_request?(secret) tool_proxy = ToolProxyService.new.process_tool_proxy_json(request.body.read, context, oauth_consumer_key) json = { "@context" => "http://purl.imsglobal.org/ctx/lti/v2/ToolProxyId", @@ -47,14 +49,5 @@ module Lti end end - private - - def authorized_request?(secret) - OAuth::Signature.build(request, :consumer_secret => secret).verify() - end - - def oauth_consumer_key - @oauth_consumer_key ||= OAuth::Helper.parse_header(request.authorization)['oauth_consumer_key'] - end end end \ No newline at end of file diff --git a/app/controllers/lti/tool_setting_controller.rb b/app/controllers/lti/tool_setting_controller.rb new file mode 100644 index 00000000000..f46eff8274a --- /dev/null +++ b/app/controllers/lti/tool_setting_controller.rb @@ -0,0 +1,93 @@ +# Copyright (C) 2014 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 . +# + +module Lti + class ToolSettingController < ApplicationController + include Lti::ApiServiceHelper + + skip_before_filter :require_context + skip_before_filter :require_user + skip_before_filter :load_user + + def show + tool_setting = ToolSetting.includes(:tool_proxy).find(params[:tool_setting_id]) + if tool_setting && oauth_authenticated_request?(tool_setting.tool_proxy.shared_secret) + render json: tool_setting_json(tool_setting, value_to_boolean(params[:bubble])) + else + render json: {error: 'unauthorized'}, status: :unauthorized + end + end + + def update + tool_setting = ToolSetting.includes(:tool_proxy).find(params[:tool_setting_id]) + if tool_setting && oauth_authenticated_request?(tool_setting.tool_proxy.shared_secret) + tool_setting.update_attribute(:custom, custom_settings(tool_setting_type(tool_setting), JSON.parse(request.body.read))) + else + render json: {error: 'unauthorized'}, status: :unauthorized + end + end + + private + + def tool_setting_json(tool_setting, bubble) + if bubble + graph = [] + while tool_setting do + graph << collect_tool_settings(tool_setting) + case tool_setting_type(tool_setting) + when 'LtiLink' + tool_setting = ToolSetting.where(tool_proxy_id: tool_setting.tool_proxy_id, context_type: tool_setting.context_type, context_id: tool_setting.context_id, resource_link_id: nil).first + when 'ToolProxyBinding' + tool_setting = ToolSetting.where(tool_proxy_id: tool_setting.tool_proxy_id, context_type: nil, context_id: nil, resource_link_id: nil).first + when 'ToolProxy' + tool_setting = nil + end + end + IMS::LTI::Models::ToolSettingContainer.new(graph: graph) + else + tool_setting.custom + end + end + + def collect_tool_settings(tool_setting) + type = tool_setting_type(tool_setting) + url = show_lti_tool_settings_url(tool_setting.id) + custom = tool_setting.custom || {} + IMS::LTI::Models::ToolSetting.new(custom: custom, type: type, id: url) + end + + def custom_settings(type, json) + if request.content_type == 'application/vnd.ims.lti.v2.toolsettings+json' + setting = json['@graph'].find { |setting| setting['type'] == type } + setting['custom'] + else + json + end + end + + def tool_setting_type(tool_setting) + if tool_setting.resource_link_id.present? + 'LtiLink' + elsif tool_setting.context.present? + 'ToolProxyBinding' + else + 'ToolProxy' + end + end + + end +end \ No newline at end of file diff --git a/app/models/lti/message_handler.rb b/app/models/lti/message_handler.rb index 5a6e97ef188..f26f0715038 100644 --- a/app/models/lti/message_handler.rb +++ b/app/models/lti/message_handler.rb @@ -18,15 +18,15 @@ module Lti class MessageHandler< ActiveRecord::Base + attr_accessible :message_type, :launch_path, :capabilities, :parameters, :resource_handler, :links - attr_accessible :message_type, :launch_path, :capabilities, :parameters, :resource - - belongs_to :resource, class_name: "Lti::ResourceHandler", :foreign_key => :resource_handler_id + belongs_to :resource_handler, class_name: "Lti::ResourceHandler", :foreign_key => :resource_handler_id + has_many :links, :class_name => 'Lti::LtiLink' serialize :capabilities serialize :parameters - validates_presence_of :message_type, :resource, :launch_path + validates_presence_of :message_type, :resource_handler, :launch_path end end \ No newline at end of file diff --git a/app/models/lti/resource_handler.rb b/app/models/lti/resource_handler.rb index 246dcd55c1c..a1ba73a4422 100644 --- a/app/models/lti/resource_handler.rb +++ b/app/models/lti/resource_handler.rb @@ -24,7 +24,6 @@ module Lti belongs_to :tool_proxy, class_name: 'Lti::ToolProxy' has_many :message_handlers, class_name: 'Lti::MessageHandler', :foreign_key => :resource_handler_id has_many :placements, class_name: 'Lti::ResourcePlacement' - has_many :tool_links, :class_name => 'Lti::ToolLink' serialize :icon_info diff --git a/app/models/lti/tool_consumer_profile_creator.rb b/app/models/lti/tool_consumer_profile_creator.rb index c8f34f29161..7c74a357586 100644 --- a/app/models/lti/tool_consumer_profile_creator.rb +++ b/app/models/lti/tool_consumer_profile_creator.rb @@ -1,10 +1,11 @@ module Lti class ToolConsumerProfileCreator - def initialize(account, tcp_url, tp_registration_url) - @root_account = account.root_account + def initialize(account, tcp_url, domain, context_type) @tcp_url = tcp_url - @tp_registration_url = tp_registration_url + @context_type = context_type + @root_account = account.root_account + @domain = domain end def create @@ -12,7 +13,7 @@ module Lti profile.id = @tcp_url profile.lti_version = IMS::LTI::Models::ToolConsumerProfile::LTI_VERSION_2P0 profile.product_instance = create_product_instance - profile.service_offered = [create_tp_registration_service] + profile.service_offered = [ create_tp_registration_service, create_tp_item_service, create_tp_settings_service, create_binding_settings_service, create_link_settings_service ] profile.capability_offered = capabilities profile @@ -53,16 +54,55 @@ module Lti def create_tp_registration_service reg_srv = IMS::LTI::Models::RestService.new reg_srv.id = "#{@tcp_url}#ToolProxy.collection" - reg_srv.endpoint = @tp_registration_url + reg_srv.endpoint = "https://#{@domain}/api/lti/#{@context_type}s/{#{@context_type}_id}/tool_proxy" reg_srv.type = 'RestService' reg_srv.format = ['application/vnd.ims.lti.v2.toolproxy+json'] reg_srv.action = 'POST' reg_srv end - def capabilities + def create_tp_item_service + reg_srv = IMS::LTI::Models::RestService.new + reg_srv.id = "#{@tcp_url}#ToolProxy.item" + reg_srv.endpoint = "https://#{@domain}/api/lti/tool_settings/tool_proxy/{tool_proxy_id}" + reg_srv.type = 'RestService' + reg_srv.format = ["application/vnd.ims.lti.v2.toolproxy+json"] + reg_srv.action = ['GET'] + reg_srv + end - %w( basic-lti-launch-request Canvas.api.domain) + def create_tp_settings_service + reg_srv = IMS::LTI::Models::RestService.new + reg_srv.id = "#{@tcp_url}#ToolProxySettings" + reg_srv.endpoint = "https://#{@domain}/api/lti/tool_settings/tool_proxy/{tool_proxy_id}" + reg_srv.type = 'RestService' + reg_srv.format = ['application/vnd.ims.lti.v2.toolsettings+json', 'application/vnd.ims.lti.v2.toolsettings.simple+json'] + reg_srv.action = ['GET', 'PUT'] + reg_srv + end + + def create_binding_settings_service + reg_srv = IMS::LTI::Models::RestService.new + reg_srv.id = "#{@tcp_url}#ToolProxyBindingSettings" + reg_srv.endpoint = "https://#{@domain}/api/lti/tool_settings/bindings/{binding_id}" + reg_srv.type = 'RestService' + reg_srv.format = ['application/vnd.ims.lti.v2.toolsettings+json', 'application/vnd.ims.lti.v2.toolsettings.simple+json'] + reg_srv.action = ['GET', 'PUT'] + reg_srv + end + + def create_link_settings_service + reg_srv = IMS::LTI::Models::RestService.new + reg_srv.id = "#{@tcp_url}#LtiLinkSettings" + reg_srv.endpoint = "https://#{@domain}/api/lti/tool_settings/links/{tool_proxy_id}" + reg_srv.type = 'RestService' + reg_srv.format = ['application/vnd.ims.lti.v2.toolsettings+json', 'application/vnd.ims.lti.v2.toolsettings.simple+json'] + reg_srv.action = ['GET', 'PUT'] + reg_srv + end + + def capabilities + %w( basic-lti-launch-request Canvas.api.domain LtiLink.custom.url ToolProxyBinding.custom.url ToolProxy.custom.url) end end diff --git a/app/models/lti/tool_proxy.rb b/app/models/lti/tool_proxy.rb index 3a6a4f0cfba..bd8d4957ccc 100644 --- a/app/models/lti/tool_proxy.rb +++ b/app/models/lti/tool_proxy.rb @@ -27,13 +27,12 @@ module Lti belongs_to :context, :polymorphic => true belongs_to :product_family, class_name: 'Lti::ProductFamily' - has_one :tool_setting, :class_name => 'Lti::ToolSetting', as: :settable serialize :raw_data validates_presence_of :shared_secret, :guid, :product_version, :lti_version, :product_family_id, :workflow_state, :raw_data, :context validates_uniqueness_of :guid - validates_inclusion_of :context_type, :allow_nil => true, :in => ['Course', 'Account'] + validates_inclusion_of :workflow_state, in: ['active', 'deleted', 'disabled'] end end \ No newline at end of file diff --git a/app/models/lti/tool_proxy_binding.rb b/app/models/lti/tool_proxy_binding.rb index 6fa52f90923..d7c4a3aaee2 100644 --- a/app/models/lti/tool_proxy_binding.rb +++ b/app/models/lti/tool_proxy_binding.rb @@ -18,16 +18,15 @@ module Lti class ToolProxyBinding < ActiveRecord::Base - attr_accessible :context, :tool_proxy + attr_accessible :context, :tool_proxy, :enabled belongs_to :tool_proxy, class_name: 'Lti::ToolProxy' - validates_inclusion_of :context_type, :allow_nil => true, :in => ['Course', 'Account'] + belongs_to :context, :polymorphic => true - has_one :tool_setting, :class_name => 'Lti::ToolSetting', as: :settable + has_many :links, :class_name => 'Lti::LtiLink', foreign_key: 'tool_proxy_binding_id' - validates_presence_of :tool_proxy, :context - - after_save :touch_context + validates_presence_of :tool_proxy, :context, :enabled + validates_inclusion_of :context_type, :allow_nil => true, :in => ['Course', 'Account'] end end \ No newline at end of file diff --git a/app/models/lti/tool_proxy_service.rb b/app/models/lti/tool_proxy_service.rb index 26a5787e702..9d06b56c12f 100644 --- a/app/models/lti/tool_proxy_service.rb +++ b/app/models/lti/tool_proxy_service.rb @@ -73,7 +73,7 @@ module Lti message_handler.launch_path = "#{base_path}#{mh.path}" message_handler.capabilities = create_json(mh.enabled_capability) message_handler.parameters = create_json(mh.parameter.as_json) - message_handler.resource = resource + message_handler.resource_handler = resource message_handler.save! message_handler end diff --git a/app/models/lti/tool_setting.rb b/app/models/lti/tool_setting.rb index a5b387d050e..009fc22a56b 100644 --- a/app/models/lti/tool_setting.rb +++ b/app/models/lti/tool_setting.rb @@ -17,8 +17,22 @@ module Lti class ToolSetting < ActiveRecord::Base - belongs_to :settable, polymorphic: true + attr_accessible :tool_proxy, :context, :resource_link_id, :custom + + belongs_to :tool_proxy + belongs_to :context, polymorphic: true + + validates_presence_of :tool_proxy + validates_presence_of :context, if: :has_resource_link_id? + validates_inclusion_of :context_type, :allow_nil => true, :in => ['Course', 'Account'] serialize :custom + + + private + def has_resource_link_id? + resource_link_id.present? + end + end end \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 4c4449bb141..c3ef2bd8a7d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -285,7 +285,7 @@ CanvasRails::Application.routes.draw do end end - get 'lti/basic_lti_launch_request/:lti_message_handler_id', controller: 'lti/message', action: 'basic_lti_launch_request', as: :basic_lti_launch_request + 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 @@ -548,7 +548,7 @@ CanvasRails::Application.routes.draw do end - get 'lti/basic_lti_launch_request/:lti_message_handler_id', controller: 'lti/message', action: 'basic_lti_launch_request', as: :basic_lti_launch_request + 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 @@ -1595,7 +1595,13 @@ CanvasRails::Application.routes.draw do get "#{prefix}/tool_consumer_profile/:tool_consumer_profile_id", controller: 'lti/tool_consumer_profile', action: 'show', :as => "#{context}_tool_consumer_profile" post "#{prefix}/tool_proxy", :controller => 'lti/tool_proxy', :action => :create, :path_name => "create_#{context}_lti_tool_proxy" end + #Tool Setting Services + get "tool_settings/:tool_setting_id", controller: 'lti/tool_setting', action: :show, as: 'show_lti_tool_settings' + put "tool_settings/:tool_setting_id", controller: 'lti/tool_setting', action: :update, as: 'update_lti_tool_settings' + + #Tool Proxy Services get "tool_proxy/:tool_proxy_guid", :controller => 'lti/tool_proxy', :action => :show, :path_name => "show_lti_tool_proxy" + end match '/assets/:package.:extension' => 'jammit#package', :as => :jammit if defined?(Jammit) diff --git a/db/migrate/20140825200057_add_lti_link_binding_association.rb b/db/migrate/20140825200057_add_lti_link_binding_association.rb new file mode 100644 index 00000000000..b1deacb1242 --- /dev/null +++ b/db/migrate/20140825200057_add_lti_link_binding_association.rb @@ -0,0 +1,42 @@ +class AddLtiLinkBindingAssociation < ActiveRecord::Migration + tag :predeploy + + def self.up + drop_table :lti_tool_links + drop_table :lti_tool_settings + + add_column :lti_tool_proxy_bindings, :enabled, :boolean, null: false, default: true + + create_table :lti_tool_settings do |t| + t.integer :tool_proxy_id, limit:8, null: false + t.integer :context_id, limit: 8 + t.string :context_type + t.text :resource_link_id + t.text :custom + t.timestamps + end + + add_index :lti_tool_settings, [:resource_link_id, :context_type, :context_id, :tool_proxy_id],name: 'index_lti_tool_settings_on_link_context_and_tool_proxy', unique: true + + end + + def self.down + remove_column :lti_tool_proxy_bindings, :enabled + drop_table :lti_tool_settings + create_table :lti_tool_settings do |t| + t.integer :settable_id, limit: 8, null: false + t.string :settable_type, null: false + t.text :custom + end + + create_table :lti_tool_links do |t| + t.integer :resource_handler_id, limit: 8, null: false + t.string :uuid, null: false + end + + + add_index :lti_tool_settings, [:settable_id, :settable_type], unique: true + add_index :lti_tool_links, :uuid, unique: true + end + +end diff --git a/app/models/lti/tool_link.rb b/lib/lti/api_service_helper.rb similarity index 56% rename from app/models/lti/tool_link.rb rename to lib/lti/api_service_helper.rb index b4f84d3b712..eb6605997d6 100644 --- a/app/models/lti/tool_link.rb +++ b/lib/lti/api_service_helper.rb @@ -1,4 +1,5 @@ -# Copyright (C) 2014 Instructure, Inc. +# +# Copyright (C) 2011 - 2014 Instructure, Inc. # # This file is part of Canvas. # @@ -15,14 +16,19 @@ # with this program. If not, see . # +# Filters added to this controller apply to all controllers in the application. +# Likewise, all the methods added will be available for all controllers. + module Lti - class ToolLink < ActiveRecord::Base - belongs_to :resource_handler, class_name: 'Lti::ResourceHandler' - has_one :tool_setting, :class_name => 'Lti::ToolSetting', as: :settable + module ApiServiceHelper - attr_accessible :uuid + def oauth_authenticated_request?(secret) + !!OAuth::Signature.build(request, :consumer_secret => secret).verify() + end - after_initialize { self.uuid = SecureRandom::uuid} + def oauth_consumer_key + @oauth_consumer_key ||= OAuth::Helper.parse_header(request.authorization)['oauth_consumer_key'] + end end end \ No newline at end of file diff --git a/lib/lti/message_helper.rb b/lib/lti/message_helper.rb index fed02885164..c5129b8338f 100644 --- a/lib/lti/message_helper.rb +++ b/lib/lti/message_helper.rb @@ -45,14 +45,14 @@ module Lti if @context.is_a? Course substitutions.merge!( - { - '$Canvas.course.id' => @context.id, - '$Canvas.course.sisSourceId' => @context.sis_source_id, - '$Canvas.enrollment.enrollmentState' => -> { lti_helper.enrollment_state }, - '$Canvas.membership.roles' => -> { lti_helper.current_canvas_roles }, - #This is a list of IMS LIS roles should have a different key - '$Canvas.membership.concludedRoles' => -> { lti_helper.concluded_lis_roles }, - } + { + '$Canvas.course.id' => @context.id, + '$Canvas.course.sisSourceId' => @context.sis_source_id, + '$Canvas.enrollment.enrollmentState' => -> { lti_helper.enrollment_state }, + '$Canvas.membership.roles' => -> { lti_helper.current_canvas_roles }, + #This is a list of IMS LIS roles should have a different key + '$Canvas.membership.concludedRoles' => -> { lti_helper.concluded_lis_roles }, + } ) end diff --git a/spec/apis/lti/tool_setting_api_spec.rb b/spec/apis/lti/tool_setting_api_spec.rb new file mode 100644 index 00000000000..394d6eeaa54 --- /dev/null +++ b/spec/apis/lti/tool_setting_api_spec.rb @@ -0,0 +1,156 @@ +# +# Copyright (C) 2014 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 . +# + +require File.expand_path(File.dirname(__FILE__) + '/../api_spec_helper') + +module Lti + describe ToolSettingController, type: :request do + + let(:account) { Account.new } + let (:product_family) { ProductFamily.create(vendor_code: '123', product_code: 'abc', vendor_name: 'acme', root_account: account) } + let(:tool_proxy) do + ToolProxy.create!( + context: account, + guid: SecureRandom.uuid, + shared_secret: 'abc', + product_family: product_family, + root_account: account, + product_version: '1', + workflow_state: 'disabled', + raw_data: {'proxy' => 'value'}, + lti_version: '1' + ) + end + let(:resource_handler) { ResourceHandler.create!(resource_type_code: 'code', name: 'name', tool_proxy: tool_proxy) } + let(:message_handler) { MessageHandler.create(message_type: 'basic-lti-launch-request', launch_path: 'https://samplelaunch/blti', resource_handler: resource_handler) } + + before do + OAuth::Signature.stubs(:build).returns(mock(verify: true)) + @link_setting = ToolSetting.create(tool_proxy: tool_proxy, context: account, resource_link_id: 'abc', custom: {link: :setting}) + @binding_setting = ToolSetting.create(tool_proxy: tool_proxy, context: account, custom: {binding: :setting}) + @proxy_setting = ToolSetting.create(tool_proxy: tool_proxy, custom: {proxy: :setting}) + end + + describe '#lti_link_show', type: :request do + + it 'returns the lti link simple json' do + get "/api/lti/tool_settings/#{@link_setting.id}.json", tool_setting_id: @link_setting, bubble: false + JSON.parse(body).should == {"link" => "setting"} + end + + it 'returns the lti link tool settings json with bubble' do + get "/api/lti/tool_settings/#{@link_setting.id}.json", tool_setting_id: @link_setting, bubble: true + json = JSON.parse(body) + setting = json['@graph'].find { |setting| setting['@type'] == "LtiLink" } + setting['custom'].should == {"link" => "setting"} + end + + it 'creates a new lti link tool setting' do + tool_setting = ToolSetting.create(tool_proxy: tool_proxy, context: account, resource_link_id: 'resource_link') + params = {'link' => 'settings'} + put "/api/lti/tool_settings/#{tool_setting.id}.json", params.to_json, {'CONTENT_TYPE' => 'application/vnd.ims.lti.v2.toolsettings.simple+json', 'ACCEPT' => 'application/vnd.ims.lti.v2.toolsettings.simple+json'} + tool_setting.reload.custom.should == {'link' => 'settings'} + end + + end + + describe '#tool_proxy_binding_show' do + + it 'returns the lti link simple json' do + get "/api/lti/tool_settings/#{@binding_setting.id}.json", tool_setting_id: @binding_setting.id, bubble: false + JSON.parse(body).should == {"binding" => "setting"} + end + + it 'returns the lti link tool settings json with bubble' do + get "/api/lti/tool_settings/#{@binding_setting.id}.json", tool_setting_id: @binding_setting.id, bubble: true + json = JSON.parse(body) + setting = json['@graph'].find { |setting| setting['@type'] == "ToolProxyBinding" } + setting['custom'].should == {"binding" => "setting"} + end + + it 'creates a new binding tool setting' do + tool_setting = ToolSetting.create(tool_proxy: tool_proxy, context: account) + params = {'binding' => 'settings'} + put "/api/lti/tool_settings/#{tool_setting.id}.json", params.to_json, {'CONTENT_TYPE' => 'application/vnd.ims.lti.v2.toolsettings.simple+json', 'ACCEPT' => 'application/vnd.ims.lti.v2.toolsettings.simple+json'} + tool_setting.reload.custom.should == {'binding' => 'settings'} + end + + end + + describe '#tool_proxy_show' do + + it 'returns the lti link simple json' do + get "/api/lti/tool_settings/#{@proxy_setting.id}.json", link_id: @proxy_setting.id, bubble: false + JSON.parse(body).should == {"proxy" => "setting"} + end + + it 'returns the lti link tool settings json with bubble' do + get "/api/lti/tool_settings/#{@proxy_setting.id}.json", link_id: @proxy_setting.id, bubble: true + json = JSON.parse(body) + setting = json['@graph'].find { |setting| setting['@type'] == "ToolProxy" } + setting['custom'].should == {"proxy" => "setting"} + end + + it 'creates a new tool_proxy tool setting' do + tool_setting = ToolSetting.create(tool_proxy: tool_proxy) + params = {'tool_proxy' => 'settings'} + put "/api/lti/tool_settings/#{tool_setting.id}.json", params.to_json, {'CONTENT_TYPE' => 'application/vnd.ims.lti.v2.toolsettings.simple+json', 'ACCEPT' => 'application/vnd.ims.lti.v2.toolsettings.simple+json'} + tool_setting.reload.custom.should == {'tool_proxy' => 'settings'} + end + + context 'bubble' do + + it 'bubbles up all levels' do + get "/api/lti/tool_settings/#{@link_setting.id}.json", tool_setting_id: @link_setting.id, bubble: true + json = JSON.parse(body) + link_setting = json['@graph'].find { |setting| setting['@type'] == "LtiLink" } + link_setting['custom'].should == {"link" => "setting"} + binding_setting = json['@graph'].find { |setting| setting['@type'] == "ToolProxyBinding" } + binding_setting['custom'].should == {"binding" => "setting"} + proxy_setting = json['@graph'].find { |setting| setting['@type'] == "ToolProxy" } + proxy_setting['custom'].should == {"proxy" => "setting"} + end + + it 'bubbles up from binding' do + get "/api/lti/tool_settings/#{@binding_setting.id}.json", tool_setting_id: @binding_setting.id, bubble: true + json = JSON.parse(body) + link_setting = json['@graph'].find { |setting| setting['@type'] == "LtiLink" } + link_setting.should be_nil + binding_setting = json['@graph'].find { |setting| setting['@type'] == "ToolProxyBinding" } + binding_setting['custom'].should == {"binding" => "setting"} + proxy_setting = json['@graph'].find { |setting| setting['@type'] == "ToolProxy" } + proxy_setting['custom'].should == {"proxy" => "setting"} + end + + it 'bubbles up from tool proxy' do + get "/api/lti/tool_settings/#{@proxy_setting.id}.json", tool_setting_id: @proxy_setting.id, bubble: true + json = JSON.parse(body) + link_setting = json['@graph'].find { |setting| setting['@type'] == "LtiLink" } + link_setting.should be_nil + binding_setting = json['@graph'].find { |setting| setting['@type'] == "ToolProxyBinding" } + binding_setting.should be_nil + proxy_setting = json['@graph'].find { |setting| setting['@type'] == "ToolProxy" } + proxy_setting['custom'].should == {"proxy" => "setting"} + end + + end + + end + + end +end \ No newline at end of file diff --git a/spec/controllers/lti/message_controller_spec.rb b/spec/controllers/lti/message_controller_spec.rb index 869b7fb075b..8463b0e7695 100644 --- a/spec/controllers/lti/message_controller_spec.rb +++ b/spec/controllers/lti/message_controller_spec.rb @@ -64,7 +64,7 @@ module Lti let (:account) { Account.create } let (:product_family) { ProductFamily.create(vendor_code: '123', product_code: 'abc', vendor_name: 'acme', root_account: account) } let (:resource_handler) { ResourceHandler.create(resource_type_code: 'code', name: 'resource name', tool_proxy: tool_proxy) } - let (:message_handler) { MessageHandler.create(message_type: 'message_type', launch_path:'https://samplelaunch/blti', resource: resource_handler)} + let(:message_handler) { MessageHandler.create(message_type: 'basic-lti-launch-request', launch_path: 'https://samplelaunch/blti', resource_handler: resource_handler) } let (:tool_proxy) { ToolProxy.create( shared_secret: 'shared_secret', guid: 'guid', @@ -77,12 +77,13 @@ module Lti ) } context 'account' do - before :each do - @tool_proxy_binding = ToolProxyBinding.create(context: account, tool_proxy: tool_proxy) + + before do + ToolProxyBinding.create(context: account, tool_proxy: tool_proxy) end it 'returns the signed params' do - get 'basic_lti_launch_request', account_id: account.id, lti_message_handler_id: message_handler.id + get 'basic_lti_launch_request', account_id: account.id, message_handler_id: message_handler.id, params: {tool_launch_context: 'my_custom_context'} response.code.should == "200" lti_launch = assigns[:lti_launch] @@ -97,10 +98,30 @@ module Lti end it 'returns a 404 when when no handler is found' do - get 'basic_lti_launch_request', account_id: account.id, lti_message_handler_id: 0 + get 'basic_lti_launch_request', account_id: account.id, message_handler_id: 0 response.code.should == "404" end + it 'does custom variable expansion for tool settings' do + parameters = %w( LtiLink.custom.url ToolProxyBinding.custom.url ToolProxy.custom.url ).map do |key| + IMS::LTI::Models::Parameter.new(name: key.underscore, variable: key ) + end + message_handler.parameters = parameters.as_json + message_handler.save + + get 'basic_lti_launch_request', account_id: account.id, message_handler_id: message_handler.id, params: {tool_launch_context: 'my_custom_context'} + response.code.should == "200" + + params = assigns[:lti_launch].params.with_indifferent_access + params['custom_lti_link.custom.url'].should include('api/lti/tool_settings/') + params['custom_tool_proxy_binding.custom.url'].should include('api/lti/tool_settings/') + params['custom_tool_proxy.custom.url'].should include('api/lti/tool_settings/') + end + + it 'uses the correct binding' do + + end + end end end diff --git a/spec/models/lti/message_handler_spec.rb b/spec/models/lti/message_handler_spec.rb index c61aa043f6a..e380101a144 100644 --- a/spec/models/lti/message_handler_spec.rb +++ b/spec/models/lti/message_handler_spec.rb @@ -25,7 +25,7 @@ module Lti before(:each) do subject.message_type = 'message_type' subject.launch_path = 'launch_path' - subject.resource = ResourceHandler.new + subject.resource_handler = ResourceHandler.new end it 'requires the message type' do @@ -41,9 +41,9 @@ module Lti end it 'requires a resource_handler' do - subject.resource = nil + subject.resource_handler = nil subject.save - subject.errors.first.should == [:resource, "can't be blank"] + subject.errors.first.should == [:resource_handler, "can't be blank"] end end diff --git a/spec/models/lti/tool_consumer_profile_creator_spec.rb b/spec/models/lti/tool_consumer_profile_creator_spec.rb index 6a42c079f09..0226ea20175 100644 --- a/spec/models/lti/tool_consumer_profile_creator_spec.rb +++ b/spec/models/lti/tool_consumer_profile_creator_spec.rb @@ -6,9 +6,7 @@ module Lti let(:root_account) { mock('root account', lti_guid: 'my_guid') } let(:account) { mock('account', root_account: root_account) } let(:tcp_url) {'http://example.instructure.com/tcp/uuid'} - # let(:root_account) {mock('root account').stubs(:lti_guid).returns('my_guid')} - # let(:account) {mock('account').stubs(:root_account).returns(root_account)} - subject { ToolConsumerProfileCreator.new(account, tcp_url, 'http://tool-consumer.com/tp/reg') } + subject { ToolConsumerProfileCreator.new(account, tcp_url, 'example.instructure.com', 'account') } describe '#create' do @@ -51,7 +49,7 @@ module Lti profile = subject.create reg_srv = profile.service_offered.find {|srv| srv.id.include? 'ToolProxy.collection'} reg_srv.id.should == "#{tcp_url}#ToolProxy.collection" - reg_srv.endpoint.should == 'http://tool-consumer.com/tp/reg' + reg_srv.endpoint.should == 'https://example.instructure.com/api/lti/accounts/{account_id}/tool_proxy' reg_srv.type.should == 'RestService' reg_srv.format.should == ["application/vnd.ims.lti.v2.toolproxy+json"] reg_srv.action.should include 'POST' diff --git a/spec/models/lti/tool_link_spec.rb b/spec/models/lti/tool_link_spec.rb deleted file mode 100644 index 1bac87af9d1..00000000000 --- a/spec/models/lti/tool_link_spec.rb +++ /dev/null @@ -1,64 +0,0 @@ -# -# Copyright (C) 2014 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 . -# - -require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb') - -module Lti - describe ToolLink do - - let (:account) { Account.create } - let (:product_family) { ProductFamily.create(vendor_code: '123', product_code: 'abc', vendor_name: 'acme', root_account: account) } - let (:resource_handler) { ResourceHandler.create(resource_type_code: 'code', name: 'resource name', tool_proxy: tool_proxy) } - let (:message_handler) { MessageHandler.create(message_type: 'message_type', launch_path:'https://samplelaunch/blti', resource: resource_handler)} - let (:tool_proxy) { ToolProxy.create( - shared_secret: 'shared_secret', - guid: 'guid', - product_version: '1.0beta', - lti_version: 'LTI-2p0', - product_family: product_family, - context: account, - workflow_state: 'active', - raw_data: 'some raw data' - ) } - subject{resource_handler.tool_links.create} - - - describe '#create' do - - it 'sets the uuid by default' do - - link = resource_handler.tool_links.create - link.uuid.should_not == nil - - end - - - end - - context 'tool_settings' do - it 'can have a tool setting' do - subject.create_tool_setting(custom: {name: :foo}) - subject.tool_setting[:custom].should == {name: :foo} - - end - end - - - - end -end \ No newline at end of file diff --git a/spec/models/lti/tool_proxy_binding_spec.rb b/spec/models/lti/tool_proxy_binding_spec.rb index 4ca50d437d2..2fab823e946 100644 --- a/spec/models/lti/tool_proxy_binding_spec.rb +++ b/spec/models/lti/tool_proxy_binding_spec.rb @@ -43,31 +43,6 @@ describe ToolProxyBinding do subject.errors.first.should == [:tool_proxy, "can't be blank"] end - context 'tool_settings' do - let (:account) { Account.create } - let (:product_family) { ProductFamily.create(vendor_code: '123', product_code: 'abc', vendor_name: 'acme', root_account: account) } - let (:resource_handler) { ResourceHandler.create(resource_type_code: 'code', name: 'resource name', tool_proxy: tool_proxy) } - let (:message_handler) { MessageHandler.create(message_type: 'message_type', launch_path:'https://samplelaunch/blti', resource: resource_handler)} - let (:tool_proxy) { ToolProxy.create( - shared_secret: 'shared_secret', - guid: 'guid', - product_version: '1.0beta', - lti_version: 'LTI-2p0', - product_family: product_family, - context: account, - workflow_state: 'active', - raw_data: 'some raw data' - ) } - subject{ tool_proxy.bindings.create(context:account)} - - it 'can have a tool setting' do - subject.create_tool_setting(custom: {name: :foo}) - subject.tool_setting[:custom].should == {name: :foo} - - end - - end - end end diff --git a/spec/models/lti/tool_proxy_spec.rb b/spec/models/lti/tool_proxy_spec.rb index 67b0f87c1ad..a9c4e61dfce 100644 --- a/spec/models/lti/tool_proxy_spec.rb +++ b/spec/models/lti/tool_proxy_spec.rb @@ -102,24 +102,6 @@ module Lti subject.errors[:raw_data].should include("can't be blank") end - context 'tool_settings' do - subject { ToolProxy.create( - shared_secret: 'shared_secret', - guid: 'guid', - product_version: '1.0beta', - lti_version: 'LTI-2p0', - product_family: product_family, - context: account, - workflow_state: 'active', - raw_data: 'some raw data' - ) } - it 'can have a tool setting' do - subject.create_tool_setting(custom: {name: :foo}) - subject.tool_setting[:custom].should == {name: :foo} - - end - end - end end diff --git a/spec/models/lti/tool_setting_spec.rb b/spec/models/lti/tool_setting_spec.rb index 1f81175d56e..a08cbc2a9aa 100644 --- a/spec/models/lti/tool_setting_spec.rb +++ b/spec/models/lti/tool_setting_spec.rb @@ -20,6 +20,35 @@ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb') module Lti describe ToolSetting do + let (:account) { Account.create } + let (:product_family) { ProductFamily.create(vendor_code: '123', product_code: 'abc', vendor_name: 'acme', root_account: account) } + let (:resource_handler) { ResourceHandler.create(resource_type_code: 'code', name: 'resource name', tool_proxy: tool_proxy) } + let (:message_handler) { MessageHandler.create(message_type: 'message_type', launch_path: 'https://samplelaunch/blti', resource: resource_handler) } + let (:tool_proxy) { ToolProxy.create( + shared_secret: 'shared_secret', + guid: 'guid', + product_version: '1.0beta', + lti_version: 'LTI-2p0', + product_family: product_family, + context: account, + workflow_state: 'active', + raw_data: 'some raw data' + ) } + + it 'can be associated with a resource link' do + subject.tool_proxy = tool_proxy + subject.context = account + subject.resource_link_id = '123456' + subject.save.should == true + end + + it 'fails if there is a resource_link_id and no context' do + subject.tool_proxy = tool_proxy + subject.resource_link_id = '123456' + subject.save.should == false + subject.errors.first.should == [:context, "can't be blank"] + end + end end \ No newline at end of file