Add DataServices create action

closes PLAT-4757

Test Plan:
 - create a subscription using the lti service, note that it works

Change-Id: Ia7cb10e4f2c1fd1e6d4a13be2f3d25b2f05e9bc7
Reviewed-on: https://gerrit.instructure.com/206291
Tested-by: Jenkins
Reviewed-by: Weston Dransfield <wdransfield@instructure.com>
QA-Review: Marc Phillips <mphillips@instructure.com>
Product-Review: Marc Phillips <mphillips@instructure.com>
This commit is contained in:
Marc Phillips 2019-08-22 10:31:23 -06:00
parent e46919ae2c
commit d2e26c9295
5 changed files with 144 additions and 31 deletions

View File

@ -17,15 +17,51 @@
module Lti
class DataServicesController < ApplicationController
include Ims::Concerns::LtiServices
include Ims::Concerns::AdvantageServices
MIME_TYPE = 'application/vnd.canvas.dataservices+json'.freeze
ACTION_SCOPE_MATCHERS = {
create: all_of(TokenScopes::LTI_CREATE_DATA_SERVICE_SUBSCRIPTION_SCOPE)
}.freeze.with_indifferent_access
rescue_from Lti::SubscriptionsValidator::InvalidContextType do
render json: {error: 'Invalid context type for subscription'}, status: :bad_request
end
rescue_from Lti::SubscriptionsValidator::ContextNotFound do
render json: {error: 'Invalid context for subscription - context not found.'}, status: :bad_request
end
before_action :verify_service_configured
# @API Create a Webhook Subscription
# Creates a webook subscription for the specified event type and
# context.
#
# @argument subscription[ContextId] [Required, String]
# The id of the context for the subscription.
#
# @argument subscription[ContextType] [Required, String]
# The type of context for the subscription. Must be 'assignment',
# 'account', or 'course'.
#
# @argument subscription[EventTypes] [Required, Array]
# Array of strings representing the event types for
# the subscription.
#
# @argument subscription[Format] [Required, String]
# Format to deliver the live events. Must be 'live-event' or 'caliper'.
#
# @argument subscription[TransportMetadata] [Required, Object]
# An object with a single key: 'Url'. Example: { "Url": "sqs.example" }
#
# @argument subscription[TransportType] [Required, String]
# Must be either 'sqs' or 'https'.
def create
render json: {info: 'Not yet Implemented'}, content_type: MIME_TYPE
sub = params.require(:subscription)
SubscriptionsValidator.validate_subscription_context!(sub)
response = Services::LiveEventsSubscriptionService.create(jwt_body, sub.to_unsafe_h)
forward_service_response(response)
end
private
@ -33,5 +69,28 @@ module Lti
def scopes_matcher
ACTION_SCOPE_MATCHERS.fetch(action_name, self.class.none)
end
def verify_service_configured
unless Services::LiveEventsSubscriptionService.available?
render json: {error: 'Subscription service not configured'}, status: :internal_server_error
end
end
def forward_service_response(service_response)
render json: service_response.body, status: service_response.code, content_type: MIME_TYPE
end
def jwt_body
{
sub: SecureRandom.uuid,
DeveloperKey: developer_key.global_id.to_s,
RootAccountId: context.global_id,
RootAccountUUID: context.uuid
}
end
def context
Account.active.find(params[:account_id])
end
end
end

View File

@ -23,6 +23,8 @@ module Lti
end
class ToolNotInContext < StandardError
end
class ContextNotFound < StandardError
end
CONTEXT_WHITELIST = {
'root_account' => Account,
@ -59,19 +61,30 @@ module Lti
check_tool_context!
end
def self.validate_subscription_context!(subscription)
raise ContextNotFound unless retrieve_context(subscription).present?
true
end
def self.retrieve_context(subscription)
model = CONTEXT_WHITELIST[subscription[:ContextType]]
raise InvalidContextType unless model
case subscription[:ContextType]
when "root_account"
model.find_by(uuid: subscription[:ContextId])
else
model.find(subscription[:ContextId])
end
rescue ActiveRecord::RecordNotFound
raise ContextNotFound
end
private
def subscription_context
@_subscription_context ||= begin
model = CONTEXT_WHITELIST[subscription[:ContextType]]
raise InvalidContextType unless model
case subscription[:ContextType]
when "root_account"
model.find_by(uuid: subscription[:ContextId])
else
model.find(subscription[:ContextId])
end
SubscriptionsValidator.retrieve_context(subscription)
end
end
end

View File

@ -23,13 +23,11 @@ module Services
end
def tool_proxy_subscription(tool_proxy, subscription_id)
options = { headers: headers(tool_proxy_jwt_body(tool_proxy)) }
request(:get, "/api/subscriptions/#{subscription_id}", options)
show(tool_proxy_jwt_body(tool_proxy), subscription_id)
end
def tool_proxy_subscriptions(tool_proxy, optional_headers = {})
options = { headers: headers(tool_proxy_jwt_body(tool_proxy), optional_headers) }
request(:get, '/api/subscriptions', options)
index(tool_proxy_jwt_body(tool_proxy), optional_headers)
end
def create_tool_proxy_subscription(tool_proxy, subscription)
@ -37,19 +35,11 @@ module Services
"in: LiveEventsSubscriptionService::create_tool_proxy_subscription, "\
"tool_proxy_id: #{tool_proxy.id}, subscription: #{subscription}"
end
options = {
headers: headers(tool_proxy_jwt_body(tool_proxy), { 'Content-Type' => 'application/json' }),
body: subscription.to_json
}
request(:post, '/api/subscriptions', options)
create(tool_proxy_jwt_body(tool_proxy), subscription)
end
def update_tool_proxy_subscription(tool_proxy, subscription_id, subscription)
options = {
headers: headers(tool_proxy_jwt_body(tool_proxy), { 'Content-Type' => 'application/json' }),
body: subscription.to_json
}
request(:put, "/api/subscriptions/#{subscription_id}", options)
def update_tool_proxy_subscription(tool_proxy, _subscription_id, subscription)
update(tool_proxy_jwt_body(tool_proxy), subscription)
end
def destroy_tool_proxy_subscription(tool_proxy, subscription_id)
@ -57,8 +47,7 @@ module Services
"in: LiveEventsSubscriptionService::destroy_tool_proxy_subscription, "\
"tool_proxy_id: #{tool_proxy.id}, subscription_id: #{subscription_id}"
end
options = { headers: headers(tool_proxy_jwt_body(tool_proxy)) }
request(:delete, "/api/subscriptions/#{subscription_id}", options)
destroy(tool_proxy_jwt_body(tool_proxy), subscription_id)
end
def destroy_all_tool_proxy_subscriptions(tool_proxy)
@ -66,7 +55,39 @@ module Services
request(:delete, "/api/subscriptions", options)
end
def create(jwt_body, subscription)
options = {
headers: headers(jwt_body, { 'Content-Type' => 'application/json' }),
body: subscription.to_json
}
request(:post, '/api/subscriptions', options)
end
def show(jwt_body, subscription_id)
options = { headers: headers(jwt_body) }
request(:get, "/api/subscriptions/#{subscription_id}", options)
end
def update(jwt_body, subscription)
options = {
headers: headers(jwt_body, { 'Content-Type' => 'application/json' }),
body: subscription.to_json
}
request(:put, "/api/subscriptions/#{subscription['Id']}", options)
end
def destroy(jwt_body, subscription_id)
options = { headers: headers(jwt_body) }
request(:delete, "/api/subscriptions/#{subscription_id}", options)
end
def index(jwt_body, opts)
options = { headers: headers(jwt_body, opts) }
request(:get, '/api/subscriptions', options)
end
private
def request(method, endpoint, options = {})
Canvas.timeout_protection("live-events-subscription-service-session", raise_on_timeout: true) do
HTTParty.send(method, "#{settings['app-host']}#{endpoint}", options.merge(timeout: 10))

View File

@ -23,13 +23,33 @@ require_dependency "lti/public_jwk_controller"
describe Lti::DataServicesController do
describe '#create' do
include WebMock::API
include_context 'advantage services context'
let(:subscription) do
{
ContextId: root_account.uuid,
ContextType: 'root_account',
EventTypes: ['discussion_topic_created'],
Format: 'live-event',
TransportMetadata: { Url: 'sqs.example' },
TransportType: 'sqs'
}
end
before do
allow(Canvas::Security::ServicesJwt).to receive(:encryption_secret).and_return('setecastronomy92' * 2)
allow(Canvas::Security::ServicesJwt).to receive(:signing_secret).and_return('donttell' * 10)
allow(HTTParty).to receive(:send).and_return(double(body: subscription, code: 200))
end
it_behaves_like 'lti services' do
let(:action) { :create }
let(:expected_mime_type) { described_class::MIME_TYPE }
let(:scope_to_remove) { "https://canvas.instructure.com/lti/data_services/scope/create"}
let(:params_overrides) do
{ developer_key: { public_jwk: {} }, account_id: root_account.id }
{ subscription: subscription, account_id: root_account.id }
end
end
end

View File

@ -192,11 +192,11 @@ module Services
it 'makes the expected request' do
allow(tool_proxy).to receive(:context).and_return(root_account_context)
allow(root_account_context).to receive(:root_account).and_return(root_account_object)
subscription = { 'my' => 'subscription' }
subscription = { 'my' => 'subscription', 'Id' => '1234' }
expect(HTTParty).to receive(:send) do |method, endpoint, options|
expect(method).to eq(:put)
expect(endpoint).to eq('http://example.com/api/subscriptions/subscription_id')
expect(endpoint).to eq('http://example.com/api/subscriptions/1234')
expect(options[:headers]['Content-Type']).to eq('application/json')
jwt = Canvas::Security::ServicesJwt.new(options[:headers]['Authorization'].gsub('Bearer ',''), false).original_token
expect(jwt['DeveloperKey']).to eq('10000000000003')