Remove extraneous fields from IMS Registrations

Additionally, mark registrations as soft deletable.

why:
- previously, we were validating that application type, grant_types,
  response_types, and token_endpoint_auth_method are all the same for
  every dynamic registration request. We don't need to store these
  fields.
- We were previously hard deleting IMS Registrations, which could cause
  some unexpected headaches.

closes INTEROP-8503

test-plan:
- Pull the commit down.
- Run the migrations and ensure all is well.
- Ensure that Lti::IMS::Registrations are soft deletable:
```ruby
reg = Lti::IMS::Registration.first
reg.destroy
reg.reload.workflow_state == "deleted"
reg.update!(workflow_state: "active")
```
- Run through the dynamic reg process and ensure that all goes smoothly.
- Revert all of the migrations and ensure that all is well.

Change-Id: I2e92ce36d3b719e072bb78e3a79d6940e7131fbf
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/346408
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Migration-Review: Cody Cutrer <cody@instructure.com>
Reviewed-by: Xander Moffatt <xmoffatt@instructure.com>
QA-Review: Xander Moffatt <xmoffatt@instructure.com>
Product-Review: Ryan Hawkins <ryan.hawkins@instructure.com>
This commit is contained in:
Ryan Hawkins 2024-04-29 14:01:53 -06:00
parent b203cff6b1
commit e0648b1833
9 changed files with 191 additions and 134 deletions

View File

@ -120,13 +120,19 @@ module Lti
return return
end end
root_account.shard.activate do
registration_params = params.permit(*expected_registration_params) registration_params = params.permit(*expected_registration_params)
registration_params["lti_tool_configuration"] = registration_params["https://purl.imsglobal.org/spec/lti-tool-configuration"] registration_params["lti_tool_configuration"] = registration_params["https://purl.imsglobal.org/spec/lti-tool-configuration"]
registration_params.delete("https://purl.imsglobal.org/spec/lti-tool-configuration") registration_params.delete("https://purl.imsglobal.org/spec/lti-tool-configuration")
scopes = registration_params["scope"].split scopes = registration_params["scope"].split
registration_params.delete("scope") registration_params.delete("scope")
error_messages = validate_registration_params(registration_params)
if error_messages.present?
render status: :unprocessable_entity, json: { errors: error_messages }
return
end
root_account.shard.activate do
developer_key = DeveloperKey.new( developer_key = DeveloperKey.new(
name: registration_params["client_name"], name: registration_params["client_name"],
account: root_account.site_admin? ? nil : root_account, account: root_account.site_admin? ? nil : root_account,
@ -164,15 +170,15 @@ module Lti
def render_registration(registration, developer_key) def render_registration(registration, developer_key)
render json: { render json: {
client_id: developer_key.global_id.to_s, client_id: developer_key.global_id.to_s,
application_type: registration.application_type, application_type: Lti::IMS::Registration::REQUIRED_APPLICATION_TYPE,
grant_types: registration.grant_types, grant_types: Lti::IMS::Registration::REQUIRED_GRANT_TYPES,
initiate_login_uri: registration.initiate_login_uri, initiate_login_uri: registration.initiate_login_uri,
redirect_uris: registration.redirect_uris, redirect_uris: registration.redirect_uris,
response_types: registration.response_types, response_types: Lti::IMS::Registration::REQUIRED_RESPONSE_TYPES,
client_name: registration.client_name, client_name: registration.client_name,
jwks_uri: registration.jwks_uri, jwks_uri: registration.jwks_uri,
logo_uri: developer_key.icon_url, logo_uri: developer_key.icon_url,
token_endpoint_auth_method: registration.token_endpoint_auth_method, token_endpoint_auth_method: Lti::IMS::Registration::REQUIRED_TOKEN_ENDPOINT_AUTH_METHOD,
scope: registration.scopes.join(" "), scope: registration.scopes.join(" "),
"https://purl.imsglobal.org/spec/lti-tool-configuration": registration.lti_tool_configuration.merge( "https://purl.imsglobal.org/spec/lti-tool-configuration": registration.lti_tool_configuration.merge(
{ {
@ -183,12 +189,35 @@ module Lti
end end
def respond_with_error(status_code, message) def respond_with_error(status_code, message)
head status_code render status: status_code,
render json: { json: {
errorMessage: message errorMessage: message
} }
end end
def validate_registration_params(registration_params)
grant_types = registration_params.delete("grant_types") || []
response_types = registration_params.delete("response_types") || []
application_type = registration_params.delete("application_type")
token_endpoint_auth_method = registration_params.delete("token_endpoint_auth_method")
errors = []
if (Lti::IMS::Registration::REQUIRED_GRANT_TYPES - grant_types).present?
errors << { field: :grant_types, message: "Must include #{Lti::IMS::Registration::REQUIRED_GRANT_TYPES.join(", ")}" }
end
if (Lti::IMS::Registration::REQUIRED_RESPONSE_TYPES - response_types).present?
errors << { field: :response_types, message: "Must include #{Lti::IMS::Registration::REQUIRED_RESPONSE_TYPES.join(", ")}" }
end
if token_endpoint_auth_method != Lti::IMS::Registration::REQUIRED_TOKEN_ENDPOINT_AUTH_METHOD
errors << { field: :token_endpoint_auth_method, message: "Must be 'private_key_jwt'" }
end
if application_type != Lti::IMS::Registration::REQUIRED_APPLICATION_TYPE
errors << { field: :application_type, message: "Must be 'web'" }
end
errors
end
def require_dynamic_registration_flag def require_dynamic_registration_flag
unless account_context.feature_enabled? :lti_dynamic_registration unless account_context.feature_enabled? :lti_dynamic_registration
render status: :not_found, template: "shared/errors/404_message" render status: :not_found, template: "shared/errors/404_message"

View File

@ -18,6 +18,7 @@
# with this program. If not, see <http://www.gnu.org/licenses/>. # with this program. If not, see <http://www.gnu.org/licenses/>.
class Lti::IMS::Registration < ApplicationRecord class Lti::IMS::Registration < ApplicationRecord
include Canvas::SoftDeletable
extend RootAccountResolver extend RootAccountResolver
CANVAS_EXTENSION_LABEL = "canvas.instructure.com" CANVAS_EXTENSION_LABEL = "canvas.instructure.com"
self.table_name = "lti_ims_registrations" self.table_name = "lti_ims_registrations"
@ -31,19 +32,16 @@ class Lti::IMS::Registration < ApplicationRecord
PLACEMENT_VISIBILITY_OPTIONS = %(admins members public) PLACEMENT_VISIBILITY_OPTIONS = %(admins members public)
validates :application_type, self.ignored_columns += %i[application_type grant_types response_types token_endpoint_auth_method]
:grant_types,
:response_types, validates :redirect_uris,
:redirect_uris,
:initiate_login_uri, :initiate_login_uri,
:client_name, :client_name,
:jwks_uri, :jwks_uri,
:token_endpoint_auth_method,
:lti_tool_configuration, :lti_tool_configuration,
presence: true presence: true
validate :required_values_are_present, validate :redirect_uris_contains_uris,
:redirect_uris_contains_uris,
:lti_tool_configuration_is_valid, :lti_tool_configuration_is_valid,
:scopes_are_valid :scopes_are_valid
@ -238,15 +236,15 @@ class Lti::IMS::Registration < ApplicationRecord
developer_key_id: developer_key.global_id.to_s, developer_key_id: developer_key.global_id.to_s,
overlay: registration_overlay, overlay: registration_overlay,
lti_tool_configuration:, lti_tool_configuration:,
application_type:, application_type: REQUIRED_APPLICATION_TYPE,
grant_types:, grant_types: REQUIRED_GRANT_TYPES,
response_types:, response_types: REQUIRED_RESPONSE_TYPES,
redirect_uris:, redirect_uris:,
initiate_login_uri:, initiate_login_uri:,
client_name:, client_name:,
jwks_uri:, jwks_uri:,
logo_uri:, logo_uri:,
token_endpoint_auth_method:, token_endpoint_auth_method: REQUIRED_TOKEN_ENDPOINT_AUTH_METHOD,
contacts:, contacts:,
client_uri:, client_uri:,
policy_uri:, policy_uri:,
@ -262,23 +260,6 @@ class Lti::IMS::Registration < ApplicationRecord
private private
def required_values_are_present
if (REQUIRED_GRANT_TYPES - grant_types).present?
errors.add(:grant_types, "Must include #{REQUIRED_GRANT_TYPES.join(", ")}")
end
if (REQUIRED_RESPONSE_TYPES - response_types).present?
errors.add(:response_types, "Must include #{REQUIRED_RESPONSE_TYPES.join(", ")}")
end
if token_endpoint_auth_method != REQUIRED_TOKEN_ENDPOINT_AUTH_METHOD
errors.add(:token_endpoint_auth_method, "Must be 'private_key_jwt'")
end
if application_type != REQUIRED_APPLICATION_TYPE
errors.add(:application_type, "Must be 'web'")
end
end
def redirect_uris_contains_uris def redirect_uris_contains_uris
return if redirect_uris.all? { |uri| uri.match? URI::DEFAULT_PARSER.make_regexp(["http", "https"]) } return if redirect_uris.all? { |uri| uri.match? URI::DEFAULT_PARSER.make_regexp(["http", "https"]) }

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
#
# Copyright (C) 2024 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
class AddWorkflowStateToLtiIMSRegistrations < ActiveRecord::Migration[7.0]
tag :predeploy
def change
add_column :lti_ims_registrations, :workflow_state, :string, limit: 255, default: "active"
end
end

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
#
# Copyright (C) 2024 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
class MakeExtraneousIMSRegistrationFieldsNullable < ActiveRecord::Migration[7.0]
tag :predeploy
def change
change_table :lti_ims_registrations do |t|
t.change_null :application_type, true
t.change_null :grant_types, true
t.change_null :response_types, true
t.change_null :token_endpoint_auth_method, true
end
end
end

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
#
# Copyright (C) 2024 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
class RemoveExtraneousIMSRegistrationFields < ActiveRecord::Migration[7.0]
tag :postdeploy
def change
change_table :lti_ims_registrations, bulk: true do |t|
t.remove :application_type, type: :string
t.remove :grant_types, type: :string, array: true
t.remove :response_types, type: :string, array: true
t.remove :token_endpoint_auth_method, type: :string
end
end
end

View File

@ -171,6 +171,7 @@ describe DeveloperKeysController, type: :request do
format: "json", format: "json",
account_id: account.id account_id: account.id
}) })
expect(response).to be_successful
key_json = json.detect { |r| r["id"] == developer_key.global_id } key_json = json.detect { |r| r["id"] == developer_key.global_id }
expect(key_json["lti_registration"]).to eq ims_registration.as_json expect(key_json["lti_registration"]).to eq ims_registration.as_json

View File

@ -162,6 +162,7 @@ describe Lti::IMS::DynamicRegistrationController do
post :create, params: invalid_registration_params post :create, params: invalid_registration_params
end end
context "with invalid grant types" do
let(:invalid_registration_params) do let(:invalid_registration_params) do
wrong_grant_types = registration_params wrong_grant_types = registration_params
wrong_grant_types["grant_types"] = ["not_part_of_the_spec", "implicit"] wrong_grant_types["grant_types"] = ["not_part_of_the_spec", "implicit"]
@ -175,9 +176,44 @@ describe Lti::IMS::DynamicRegistrationController do
end end
it "doesn't create a stray developer key" do it "doesn't create a stray developer key" do
previous_key_count = DeveloperKey.count expect { subject }.not_to change { DeveloperKey.count }
end
end
context "with invalid response types" do
let(:invalid_registration_params) do
wrong_response_types = registration_params
wrong_response_types["response_types"] = ["not_part_of_the_spec"]
wrong_response_types
end
it "returns a 422 with validation errors" do
subject subject
expect(DeveloperKey.count).to eq(previous_key_count) expect(response).to have_http_status(:unprocessable_entity)
expect(response.body).to include("Must include id_token")
end
it "doesn't create a stray developer key" do
expect { subject }.not_to change { DeveloperKey.count }
end
end
context "with invalid token endpoint auth method" do
let(:invalid_registration_params) do
wrong_token_endpoint_auth_method = registration_params
wrong_token_endpoint_auth_method["token_endpoint_auth_method"] = "not_part_of_the_spec"
wrong_token_endpoint_auth_method
end
it "returns a 422 with validation errors" do
subject
expect(response).to have_http_status(:unprocessable_entity)
expect(response.body).to include("Must be 'private_key_jwt'")
end
it "doesn't create a stray developer key" do
expect { subject }.not_to change { DeveloperKey.count }
end
end end
end end
end end

View File

@ -20,19 +20,14 @@
module Factories module Factories
LTI_IMS_REGISTRATION_BASE_ATTRS = LTI_IMS_REGISTRATION_BASE_ATTRS =
{ {
application_type: "web",
guid: "6378c81b-5996-4754-b850-5de78e6d22f4", guid: "6378c81b-5996-4754-b850-5de78e6d22f4",
client_name: "Test Dynamic Registration", client_name: "Test Dynamic Registration",
client_uri: "https://example.com", client_uri: "https://example.com",
grant_types: %w[client_credentials implicit],
jwks_uri: "https://example.com/api/registrations/3/jwks", jwks_uri: "https://example.com/api/registrations/3/jwks",
initiate_login_uri: "https://example.com/api/registrations/3/login", initiate_login_uri: "https://example.com/api/registrations/3/login",
redirect_uris: [ redirect_uris: [
"https://example.com/api/registrations/3/launch" "https://example.com/api/registrations/3/launch"
], ],
response_types: [
"id_token"
],
scopes: %w[ scopes: %w[
https://purl.imsglobal.org/spec/lti-ags/scope/lineitem https://purl.imsglobal.org/spec/lti-ags/scope/lineitem
https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly
@ -43,7 +38,6 @@ module Factories
https://canvas.instructure.com/lti/account_lookup/scope/show https://canvas.instructure.com/lti/account_lookup/scope/show
https://canvas.instructure.com/lti-ags/progress/scope/show https://canvas.instructure.com/lti-ags/progress/scope/show
], ],
token_endpoint_auth_method: "private_key_jwt",
logo_uri: "https://example.com/api/apps/1/icon.svg", logo_uri: "https://example.com/api/apps/1/icon.svg",
lti_tool_configuration: { lti_tool_configuration: {
claims: %w[ claims: %w[

View File

@ -20,9 +20,6 @@
module Lti::IMS module Lti::IMS
describe Registration do describe Registration do
let(:application_type) { :web }
let(:grant_types) { [:client_credentials, :implicit] }
let(:response_types) { [:id_token] }
let(:redirect_uris) { ["http://example.com"] } let(:redirect_uris) { ["http://example.com"] }
let(:initiate_login_uri) { "http://example.com/login" } let(:initiate_login_uri) { "http://example.com/login" }
let(:client_name) { "Example Tool" } let(:client_name) { "Example Tool" }
@ -31,7 +28,6 @@ module Lti::IMS
let(:client_uri) { "http://example.com/" } let(:client_uri) { "http://example.com/" }
let(:tos_uri) { "http://example.com/tos" } let(:tos_uri) { "http://example.com/tos" }
let(:policy_uri) { "http://example.com/policy" } let(:policy_uri) { "http://example.com/policy" }
let(:token_endpoint_auth_method) { "private_key_jwt" }
let(:lti_tool_configuration) do let(:lti_tool_configuration) do
{ {
domain: "example.com", domain: "example.com",
@ -43,9 +39,6 @@ module Lti::IMS
let(:registration) do let(:registration) do
r = Registration.new({ r = Registration.new({
application_type:,
grant_types:,
response_types:,
redirect_uris:, redirect_uris:,
initiate_login_uri:, initiate_login_uri:,
client_name:, client_name:,
@ -54,7 +47,6 @@ module Lti::IMS
client_uri:, client_uri:,
tos_uri:, tos_uri:,
policy_uri:, policy_uri:,
token_endpoint_auth_method:,
lti_tool_configuration:, lti_tool_configuration:,
scopes: scopes:
}.compact) }.compact)
@ -63,6 +55,11 @@ module Lti::IMS
end end
let(:developer_key) { DeveloperKey.create } let(:developer_key) { DeveloperKey.create }
it "is soft_deleted when destroy is called" do
registration.destroy
expect(registration.reload.workflow_state).to eq("deleted")
end
describe "associations" do describe "associations" do
subject { Lti::IMS::Registration.new } subject { Lti::IMS::Registration.new }
it { is_expected.to belong_to(:lti_registration).class_name("Lti::Registration") } it { is_expected.to belong_to(:lti_registration).class_name("Lti::Registration") }
@ -76,64 +73,6 @@ module Lti::IMS
it { is_expected.to be true } it { is_expected.to be true }
end end
context "application_type" do
context "is \"web\"" do
it { is_expected.to be true }
end
context "is not \"web\"" do
let(:application_type) { "native" }
it { is_expected.to be false }
end
context "is not included" do
let(:application_type) { nil }
it { is_expected.to be false }
end
end
context "grant_types" do
context "includes other types" do
let(:grant_types) { %i[client_credentials implicit foo bar] }
it { is_expected.to be true }
end
context "does not include implicit" do
let(:grant_types) { [:client_credentials, :foo] }
it { is_expected.to be false }
end
context "does not include client_credentials" do
let(:grant_types) { [:implicit, :foo] }
it { is_expected.to be false }
end
end
context "response_types" do
context "includes other types" do
let(:response_types) { %i[id_token foo bar] }
it { is_expected.to be true }
end
context "is not included" do
let(:response_types) { nil }
it { is_expected.to be false }
end
context "does not include id_token" do
let(:response_types) { [:foo, :bar] }
it { is_expected.to be false }
end
end
context "redirect_uris" do context "redirect_uris" do
context "includes valid uris" do context "includes valid uris" do
let(:redirect_uris) { ["https://example.com", "https://example.com/foo"] } let(:redirect_uris) { ["https://example.com", "https://example.com/foo"] }
@ -196,14 +135,6 @@ module Lti::IMS
end end
end end
context "token_endpoint_auth_method" do
context "is not \"private_key_jwt\"" do
let(:token_endpoint_auth_method) { "asdf" }
it { is_expected.to be false }
end
end
context "logo_uri" do context "logo_uri" do
context "is not a valid uri" do context "is not a valid uri" do
let(:logo_uri) { "asdf" } let(:logo_uri) { "asdf" }