Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,10 @@ def set_lti_launch_values
@is_lti_launch = true
@canvas_url = current_application_instance.site.url
@app_name = current_application_instance.application.client_application_name
context_id = params[:context_id]
if current_user&.can_author?(context_id, current_application_instance)
@can_author = true
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it may be AA specific that accidentally got merged in upstream? I can see the method exists on the user as well, which is kinda weird.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it means the lti roles are teacher or admin, which I'm using to determine when the user can resolve a launch issue. There might be a better way to do this.

end
end

def set_lti_advantage_launch_values
Expand All @@ -202,6 +206,11 @@ def set_lti_advantage_launch_values
@app_name = current_application_instance.application.client_application_name
@title = current_application_instance.application.name
@description = current_application_instance.application.description

context_id = @lti_token[LtiAdvantage::Definitions::CONTEXT_CLAIM]["id"]
if current_user&.can_author?(context_id, current_application_instance)
@can_author = true
end
end

def targeted_app_instance
Expand Down
23 changes: 21 additions & 2 deletions app/controllers/concerns/deep_linking.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def create_deep_link_jwt(
jwt_context_id: jwt_context_id,
jwt_tool_consumer_instance_guid: jwt_tool_consumer_instance_guid,
host: host,
application_instance: application_instance,
),
}

Expand All @@ -48,14 +49,32 @@ def content_items(
params:,
jwt_context_id:,
jwt_tool_consumer_instance_guid:,
host:
host:,
application_instance:
)
out = []

if params[:type] == "html"
out << {
"type" => "html",
"html" => "<h1>Atomic Jolt</h1>",
"html" => "<h1>#{params[:title] || 'Atomic Jolt'}</h1>",
}
elsif params[:type] == "ltiResourceLink"
lti_launch = LtiLaunch.create!(
tool_consumer_instance_guid: jwt_tool_consumer_instance_guid,
context_id: jwt_context_id,
application_instance_id: application_instance.id,
config: { title: params[:title] },
)
url = Rails.application.routes.url_helpers.lti_launch_url(
lti_launch,
protocol: "https",
host: host,
)
out << {
"type" => "ltiResourceLink",
"title" => "Atomic Jolt",
"url" => url,
}
end

Expand Down
28 changes: 28 additions & 0 deletions app/controllers/concerns/lti_copy_support.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module Concerns
module LtiCopySupport
extend ActiveSupport::Concern

def copy_lti_launch(
application_instance:,
lti_launch_id:,
source_lti_launch_id:,
secure_token:
)
lti_launch = LtiLaunch.find(lti_launch_id)
source_lti_launch = LtiLaunch.find(source_lti_launch_id)
source_context = source_lti_launch.lti_context
if !source_context&.validate_token(secure_token)
raise Exceptions::InvalidSecureTokenError
end

# Copy configuration from the source lti launch to the new one

lti_launch.update!(
is_configured: true,
config: source_lti_launch.config,
parent: source_lti_launch,
)
lti_launch
end
end
end
9 changes: 7 additions & 2 deletions app/controllers/concerns/open_id_connect_support.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@ module OpenIdConnectSupport

protected

def build_response(state, params, nonce)
def build_response(
state: state,
params: params,
nonce: nonce,
redirect_uri: redirect_uri
)
# The request doesn't contain any information to help us find the right application instance
# so we have to use predefined URLs
uri = URI.parse(current_application.oidc_url(params["iss"], params[:client_id]))
uri_params = Rack::Utils.parse_query(uri.query)
auth_params = {
response_type: "id_token",
redirect_uri: params[:target_link_uri],
redirect_uri: redirect_uri,
response_mode: "form_post",
client_id: params[:client_id],
scope: "openid",
Expand Down
43 changes: 38 additions & 5 deletions app/controllers/lti_launches_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,40 @@ class LtiLaunchesController < ApplicationController
before_action :do_lti, except: [:init, :launch]

def index
# This is an LTI 1.2 launch with no launch token, or an LTI 1.3
# launch.
if current_application_instance.disabled_at
render file: File.join(Rails.root, "public", "disabled.html")
end

# LTI advantage example code
if @lti_token
@lti_advantage_examples = LtiAdvantage::Examples.new(@lti_token, current_application_instance)
@lti_advantage_examples.run
# LTI 1.3
token = lti_advantage_launch_token
if token
@lti_launch = LtiLaunch.lti_advantage_launch(
token: token,
lti_params: LtiAdvantage::Params.new(@lti_token),
application_instance: current_application_instance,
)

# LTI advantage example code
@lti_advantage_examples = LtiAdvantage::Examples.new(@lti_token, current_application_instance)
@lti_advantage_examples.run
end
end

setup_lti_response
end

def show
@lti_launch = LtiLaunch.find_by(token: params[:id], context_id: params[:context_id])
# This is an LTI 1.2 launch with the token as a path parameter
token = params[:id]
@lti_launch = LtiLaunch.lti_launch(
token: token,
params: params,
application_instance: current_application_instance,
)

setup_lti_response
render :index
end
Expand All @@ -42,7 +61,12 @@ def launch
# Support Open ID connect flow for LTI 1.3
def init
nonce = SecureRandom.hex(64)
url = build_response(LtiAdvantage::OpenId.state, params, nonce)
url = build_response(
state: LtiAdvantage::OpenId.state,
params: params,
nonce: nonce,
redirect_uri: lti_launches_url
)
respond_to do |format|
format.html { redirect_to url }
end
Expand All @@ -60,4 +84,13 @@ def setup_lti_response
set_lti_launch_values
end

def lti_advantage_launch_token
# For LTI advantage, we use the target_link_uri claim to find the token
uri = URI.parse(@lti_token[LtiAdvantage::Definitions::TARGET_LINK_URI_CLAIM])
uri_params = Rack::Utils.parse_query(uri.query)

# Accept token as either query parameter or path parameter
uri_params[:lti_launch_token] ||
/^#{Regexp.quote(lti_launches_path)}\/(.+)$/.match(uri.path) { |m| m[1] }
end
end
19 changes: 19 additions & 0 deletions app/graphql/mutations/copy_lti_launch_mutation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class Mutations::CopyLtiLaunchMutation < Mutations::BaseMutation
include Concerns::LtiCopySupport

argument :id, ID, required: true
argument :source_id, ID, required: true
argument :secure_token, String, required: true

field :lti_launch, Types::LtiLaunchType, null: false

def resolve(id:, source_id:, secure_token:)
lti_launch = copy_lti_launch(
application_instance: context[:current_application_instance],
lti_launch_id: id,
source_lti_launch_id: source_id,
secure_token: secure_token,
)
{ lti_launch: lti_launch }
end
end
4 changes: 3 additions & 1 deletion app/graphql/mutations/create_lti_deep_link_jwt_mutation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ class Mutations::CreateLtiDeepLinkJwtMutation < Mutations::BaseMutation
null true

argument :type, String, required: true
argument :title, String, required: false

field :deep_link_jwt, String, null: false
field :errors, [String], null: false

def resolve(type:)
def resolve(type:, title:)
params = {}
params[:type] = type
params[:title] = title

deep_link_jwt = create_deep_link_jwt(
application_instance: context[:current_application_instance],
Expand Down
6 changes: 6 additions & 0 deletions app/graphql/types/hello_world_mutation_type.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
class Types::HelloWorldMutationType < Types::BaseObject
field :create_lti_deep_link_jwt, mutation: Mutations::CreateLtiDeepLinkJwtMutation
field :copy_lti_launch, mutation: Mutations::CopyLtiLaunchMutation do
guard ->(_obj, _args, ctx) {
ctx[:current_user].admin? ||
ctx[:current_user].can_author?(ctx[:jwt_context_id], ctx[:current_application_instance])
}
end
end
11 changes: 11 additions & 0 deletions app/graphql/types/lti_launch_type.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class Types::LtiLaunchType < Types::BaseObject
description "An lti launch"

field :id, ID, null: false
field :resource_link_id, String, null: false
field :parent_id, ID, null: true
field :config, String, null: true
field :is_configured, Boolean, null: false
field :created_at, GraphQL::Types::ISO8601DateTime, null: true
field :updated_at, GraphQL::Types::ISO8601DateTime, null: true
end
8 changes: 8 additions & 0 deletions app/graphql/types/mutation_type.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class Types::MutationType < Types::BaseObject
field :copy_lti_launch, mutation: Mutations::CopyLtiLaunchMutation do
guard ->(_obj, _args, ctx) {
ctx[:current_user].admin? ||
ctx[:current_user].can_author?(ctx[:jwt_context_id], ctx[:current_application_instance])
}
end
end
20 changes: 20 additions & 0 deletions app/graphql/types/query_type.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
class Types::QueryType < Types::BaseObject
description "The root query of this schema"

field :lti_launch, Types::LtiLaunchType, null: false do
description "An lti launch with the given id"
argument :id, ID, required: true
guard ->(_obj, _args, ctx) {
ctx[:current_user].admin? ||
ctx[:current_user].can_author?(ctx[:jwt_context_id], ctx[:current_application_instance])
}
end

def lti_launch(id:)
lti_launch = LtiLaunch.find_by(id: id, application_instance_id: ctx[:current_application_instance].id)
if lti_launch.lti_context&.context_id != ctx[:jwt_context_id]
return nil
end

lti_launch
end
2 changes: 2 additions & 0 deletions app/lib/exceptions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ class LtiConfigMissing < StandardError
end
class InvalidImsccTokenError < StandardError
end
class InvalidSecureTokenError < StandardError
end
class FileMvException < StandardError
end
class CanvasApiTokenRequired < LMS::Canvas::CanvasException
Expand Down
14 changes: 14 additions & 0 deletions app/lib/lti/roles.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,23 @@ module Roles
INST_ADMIN = "urn:lti:instrole:ims\/lis\/Administrator".freeze
INST_INSTRUCTOR = "urn:lti:instrole:ims\/lis\/Instructor".freeze

INSTRUCTOR_ROLES = [
INSTRUCTOR,
LtiAdvantage::Definitions::INSTRUCTOR_CONTEXT_ROLE,
].freeze

TA_ROLES = [
TA,
LtiAdvantage::Definitions::TA_CONTEXT_ROLE,
].freeze

ADMIN_ROLES = [
ADMIN,
SYS_SYSADMIN,
SYS_ADMIN,
INST_ADMIN,
LtiAdvantage::Definitions::ADMINISTRATOR_INSTITUTION_ROLE,
LtiAdvantage::Definitions::ADMINISTRATOR_CONTEXT_ROLE,
].freeze

NON_STUDENT_ROLES = [
Expand All @@ -29,6 +41,8 @@ module Roles
SYS_SYSADMIN,
SYS_ADMIN,
INST_ADMIN,
LtiAdvantage::Definitions::INSTRUCTOR_CONTEXT_ROLE,
LtiAdvantage::Definitions::TA_CONTEXT_ROLE,
].freeze
end
end
2 changes: 2 additions & 0 deletions app/lib/lti_advantage/definitions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ def self.scopes
MANAGER_CONTEXT_ROLE = "http://purl.imsglobal.org/vocab/lis/v2/membership#Manager".freeze
MEMBER_CONTEXT_ROLE = "http://purl.imsglobal.org/vocab/lis/v2/membership#Member".freeze
OFFICER_CONTEXT_ROLE = "http://purl.imsglobal.org/vocab/lis/v2/membership#Officer".freeze
## Sub context roles
TA_CONTEXT_ROLE = "http://purl.imsglobal.org/vocab/lis/v2/membership/Instructor#TeachingAssistant".freeze

def self.lms_host(payload)
host = if deep_link_launch?(payload)
Expand Down
Loading