spec: allow a tool to to hit LTI API in a provider state

This stubs a few parts of our JWT verification process so that
an expired JWT can be used to make API calls when the contract
test build runs. The JWT still needs to be valid, aside from
the iat and exp timestamps, and should be signed with the key
in the live-events-lti repository.

test plan:
- Set up the live-events-subscription-service locally
- Run the contract tests for the live-events-lti repo
  and save the generated JSON file somewhere
- In canvas-lms, run
  rails pact:verify:at[./path_to_contracts.json]
- It should pass

fixes PLAT-5101

Change-Id: Iff5a6a91aa2ad9868511d3396c73d8225587f640
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/216596
Tested-by: Jenkins
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Marc Phillips <mphillips@instructure.com>
QA-Review: Tucker Mcknight <tmcknight@instructure.com>
Product-Review: Tucker Mcknight <tmcknight@instructure.com>
This commit is contained in:
Tucker McKnight 2019-11-08 14:27:26 -07:00 committed by Tucker Mcknight
parent 560ca2712d
commit cf928b37cd
2 changed files with 167 additions and 28 deletions

View File

@ -15,39 +15,112 @@
# 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 LtiProviderStateHelper
def self.set_lti_context_id(account)
# Set lti_context_id to the same one used when generating the contracts
# in the live-events-lti repo.
account.update!(lti_context_id: "794d72b707af6ea82cfe3d5d473f16888a8366c7")
end
def self.jwk
{
"kty" => 'RSA',
"e" => 'test',
"n" => 'test',
"kid" => 'test',
"alg" => 'RS256',
"use" => 'test',
"iss" => 'test',
"aud" => 'http://example.org/login/oauth2/token',
"sub" => 'test',
"exp" => (Time.zone.now + 10.minutes).to_i,
"iat" => Time.zone.now.to_i,
"jti" => 'test',
}
end
def self.developer_key(jwk)
account = Pact::Canvas.base_state.account
developer_key = account.developer_keys.create!(
public_jwk: jwk,
public_jwk_url: 'example.org',
scopes: [
"https://canvas.instructure.com/lti/public_jwk/scope/update",
"https://canvas.instructure.com/lti/data_services/scope/create",
"https://canvas.instructure.com/lti/data_services/scope/show",
"https://canvas.instructure.com/lti/data_services/scope/update",
"https://canvas.instructure.com/lti/data_services/scope/list",
"https://canvas.instructure.com/lti/data_services/scope/destroy",
"https://canvas.instructure.com/lti/data_services/scope/list_event_types",
"https://canvas.instructure.com/lti/feature_flags/scope/show",
]
)
enable_developer_key_account_binding!(developer_key)
developer_key.developer_key_account_bindings.first.workflow_state = 'on'
developer_key.developer_key_account_bindings.first.save!
developer_key
end
def self.create_external_tool(developer_key)
configuration = {
"title":"Canvas Data Services",
"scopes":[
"https://canvas.instructure.com/lti/public_jwk/scope/update",
"https://canvas.instructure.com/lti/data_services/scope/create",
"https://canvas.instructure.com/lti/data_services/scope/show",
"https://canvas.instructure.com/lti/data_services/scope/update",
"https://canvas.instructure.com/lti/data_services/scope/list",
"https://canvas.instructure.com/lti/data_services/scope/destroy",
"https://canvas.instructure.com/lti/data_services/scope/list_event_types",
"https://canvas.instructure.com/lti/feature_flags/scope/show"
],
"public_jwk_url":"http://live-events-lti/api/jwks",
"description":"Data service management for Canvas LMS",
"target_link_uri":"http://live-events-lti/resource_link_request",
"oidc_initiation_url":"http://live-events-lti/login",
"extensions":[
{
"platform":"canvas.instructure.com",
"domain":"http://live-events-lti",
"privacy_level":"public",
"settings":{
"placements":[
{
"text":"Data Services",
"enabled":true,
"placement":"account_navigation",
"target_link_uri":"http://live-events-lti/resource_link_request",
"required_permissions":"manage_data_services"
}
]
}
}
],
"custom_fields":{
"canvas_account_uuid":"$vnd.Canvas.root_account.uuid",
"canvas_api_domain":"$Canvas.api.domain",
"canvas_user_uuid":"$Canvas.user.globalId",
"canvas_high_contrast_enabled":"$Canvas.user.prefersHighContrast"
}
}
tool_config = Lti::ToolConfiguration.create!(developer_key: developer_key, settings: configuration, privacy_level: 'public')
external_tool = tool_config.new_external_tool(developer_key.account)
external_tool.save!
end
end
Pact.provider_states_for PactConfig::Consumers::ALL do
provider_state 'an account with an LTI developer key' do
set_up do
account = Pact::Canvas.base_state.account
jwk = {
"kty" => 'RSA',
"e" => 'test',
"n" => 'test',
"kid" => 'test',
"alg" => 'RS256',
"use" => 'test',
"iss" => 'test',
"aud" => 'http://example.org/login/oauth2/token',
"sub" => 'test',
"exp" => (Time.zone.now + 10.minutes).to_i,
"iat" => Time.zone.now.to_i,
"jti" => 'test',
}
developer_key = account.developer_keys.create!(
public_jwk: jwk,
public_jwk_url: 'example.org',
scopes: [
"https://canvas.instructure.com/lti/public_jwk/scope/update",
"https://canvas.instructure.com/lti/data_services/scope/create",
"https://canvas.instructure.com/lti/data_services/scope/show",
"https://canvas.instructure.com/lti/data_services/scope/update",
"https://canvas.instructure.com/lti/data_services/scope/list",
"https://canvas.instructure.com/lti/data_services/scope/destroy",
"https://canvas.instructure.com/lti/data_services/scope/list_event_types",
"https://canvas.instructure.com/lti/feature_flags/scope/show",
]
)
LtiProviderStateHelper.set_lti_context_id(account)
jwk = LtiProviderStateHelper.jwk
developer_key = LtiProviderStateHelper.developer_key(jwk)
allow_any_instance_of(Canvas::Oauth::Provider).
to receive(:key).and_return(developer_key)
@ -58,6 +131,45 @@ Pact.provider_states_for PactConfig::Consumers::ALL do
provider_state 'a course with live events' do
set_up do
jwk = LtiProviderStateHelper.jwk
developer_key = LtiProviderStateHelper.developer_key(jwk)
LtiProviderStateHelper.create_external_tool(developer_key)
account = Pact::Canvas.base_state.account
LtiProviderStateHelper.set_lti_context_id(account)
allow_any_instance_of(Canvas::Oauth::Provider).
to receive(:key).and_return(developer_key)
allow_any_instance_of(Canvas::Oauth::ClientCredentialsProvider).
to receive(:get_jwk_from_url).and_return(jwk)
# The jwt_signing_key file is the same one used to sign the JWTs in the contract
# tests in the live-events-lti repo. Make that key be the one that Canvas uses
# to decode JWTs.
lti_tool_key = OpenSSL::PKey::RSA.new(File.read('../../jwt_signing_key'))
allow(Canvas::Security).to receive(:encryption_keys).and_return([lti_tool_key])
# The JWT in the contracts will be expired; tell Canvas to accept it anyway.
a_long_time = Time.zone.now.to_i + 3600
allow(Setting).to receive(:get).and_call_original
allow(Setting).to receive(:get).with("oauth2_jwt_iat_ago_in_seconds", anything).and_return(a_long_time.to_s)
allow_any_instance_of(Canvas::Security::JwtValidator).to receive(:exp).and_return(true)
# DynamicSettings is not available on Jenkins -- need to stub it to return these values.
allow(Canvas::DynamicSettings).to receive(:find).with(any_args).and_call_original
allow(Canvas::DynamicSettings).to receive(:find).with("canvas").and_return(
{
"signing-secret" => "astringthatisactually32byteslong",
"encryption-secret" => "astringthatisactually32byteslong"
}
)
allow(Canvas::DynamicSettings).to receive(:find).
with('live-events-subscription-service', any_args).and_return({
'app-host' => ENV.fetch('SUBSCRIPTION_SERVICE_HOST', 'http://les.docker:80')
})
# Always set ignore_expiration to true when calling the decode_jwt method.
Canvas::Security.class_eval do
@old_decode_jwt = self.method(:decode_jwt)

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA/x5VIHHRvgKe5siSCn6NjfDTlsiitmldZ6o7EDy7+xWY8KVR
ynR0Nl+lgQ7Pcp5B5wTGrur+awv/sA6lPp72H/l8EJhFR9NOelEzW4zzRFswMa+N
hA2FEOLM9u4744SilbXu0rlLnMCFtS0PVa01Gh7kGvZ4KM534egdXo2ZSAieeH7G
bZ0ZjRJfbv6l7Ctc+sKaAtPcpPrCc86WpmgVOePZa4VsOkfkbchdejrxHYxtHoua
F06AsVJXdwor/gkYS1ahh5SO/NgRo+J8vNXIibzY17LLHN7matO3HcZoN4c8rNII
hWiWJckd3GKb2b/HXq/5qoYGkFGVVVP0sOkrXQIDAQABAoIBAQCUHWcs5AfyuhDJ
Sk9HmnvSmawukaOuJfQduH58CdbVio91v3WCBiRmYRd0m0WjdPAsEODNMw+s1JWJ
AKe9eIrKu4zlEZK/hZW9fCFGGMovuIV9gz+1GChWSmbXQi8xA5NlOfBDFWMpybiX
HGcXxezbkm26nbfbcSu2040hlTIV2ApyZV5Wy5/rrU0/IC3OeQ+XNSE3vDRZCnpz
E1k0NLVV8zGaAF4sILBnYA+q5RKlhn+1+ajWixnHug7G8XB6ZvNNkElaaroGigiw
K/VeMvwwl/O4cZ4eSGPkYmtU1uCw2tX4PM/9IMLz4xT+Ff2BxomPnL8gvUujfbfP
KOksFrRBAoGBAP+9es3qnO1BdjCCLV00eLCeC0Wb9D+FZof2v1efJvjUqDKiL5Z1
ZmBav6HT+owX6uE/bmPejk5KXFsq+CjcCW9w5gwn2GA0p4B3fs9axDnZq+FL2r69
hoCoVKI6utvmqjWOuALogtiUOirmP013N7neI3/rVI03iHNGy+RVYm7tAoGBAP9g
sO1BIB0RRoAod3F8YiFGlRSZfJTRs6AiSNSotfLYNrx6DQBgDbWsiLq26YfE2e1D
6bqoVMJkkRYpA+FYU2MJ0oF7E+v56s0bRMBBKKObxDW+2Q98/i9go85LEWC7jGKb
UHU/PH4xDlq8TzeJoOeB1qYMbgSrG+8/GOfXK7AxAoGAEAfEhtvJ8mVED05ZoZoE
Zq3Bbx+Tc9fc0XD6FXf4bWiHEoVwDjJVtHx7vp0W+2kUZAIh3Ui6CtZGa8CJxaXl
QYMGKITm30DtrvPOkxjRa/7k8z5Z+9LNd4sVowWjaN1QlgLYLfZ9HS5NZxr/pM9w
QspV11Lc/e0ZNICfjzR68xECgYEAwXyK0FdFc4CBP9xpEuzAlKGbli3sO/zd8XfI
Yocow8OZRRfb/erIuFruhTjMmvdEfgW0cp3TCi2T14xfyj5Xf3QTr9KGd4W0po4A
ewFjPwJnmKjuYFO9ajv4H/a0RewTIyq1vP+aX6nfTFPcWSHHbV/sN4a3XIYf9haC
UjWufiECgYB90++PvEdQI31ryw51rlZBvm4V2oa1tSvsW9qOIevc72hF1VgGfUAz
GojzzT7Q1yT90+U/IKuUgzQzdEasmoy0qnvZGhD+LcqaTA9pLX1jplQVAAYO1q2a
JUvBaReqn/v5v/f8F1iL0cSMQezUD2f/dezlwq8uaHBs7RhPw/Iogg==
-----END RSA PRIVATE KEY-----