canvas-lms/lib/token_scopes.rb

85 lines
4.3 KiB
Ruby
Raw Normal View History

#
# Copyright (C) 2018 - 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 TokenScopes
OAUTH2_SCOPE_NAMESPACE = '/auth/'.freeze
USER_INFO_SCOPE = {
resource: :oauth2,
verb: "GET",
scope: "#{OAUTH2_SCOPE_NAMESPACE}userinfo"
}.freeze
Enforce Tool-Course visibility rules in NRPS V2 calls - NRPS v2 invocations referencing a `Course` Context now attempt to resolve a `ContextExternalTool` (CET) given the JWT `AccessToken` attached to the request. In order to return memberships, that CET must be active and must either be bound directly to the `Course` or to an `Account` in the `Course`'s' `Account` chain. - `AccessToken` must be associated with an active `DeveloperKey` (DK), and the search for the "operative" CET for the current request is executed against that DK's list of active CETs. - `Course`-level CETs are preferred, followed by `Account`-level CETs. - LTI 1.3/Advantage features must be turned on at the CET and root `Account` levels. - The `AccessToken`'s'JWT signature and security claims are not themselves validated... that comes later. - `Group` Context support also comes later. Closes LTIA-26 Test Plan: - Via Rails console create a `DeveloperKey` associated with the public key of a Tool configured in the IMS LTI 1.3/Advantage Reference Implementation (RI) and the root `Account` in your env - Via Rails console, create a LTI 1.3-enabled `ContextExternalTool` with a `course_navigation` placement and linked to the just-created `DeveloperKey` and its `Account` - For a `Course` owned by this `Account`, verify that direct invocations of the NRPS v2 API. (`GET /api/lti/courses/:course_id/names_and_roles`) fail with a 401 and a message complaining about a missing access token. - Navigate to the `Course` and click the newly created nav link, which should successfully launch the RI. - Click the 'Request Names and Roles' link in the RI. Verify course is reported in the NRPS v2 format. - Deactivate the `DeveloperKey`. Click 'Request Names and Roles' link in the RI. Verify a (non-descript) on-screen error message. - Re-enable the `DeveloperKey` and re-verify the same behavior for a `Course` associated with a sub-`Account`. - Delete the CET, verify that NRPS v2 invocations from the RI fail. - Via Rails console, create a new CET linked to the same `DeveloperKey`, but now attached to the sub-`Account` `Course`. - Re-verify NRPS v2 invocation from the RI. - *Consult JIRA for full acceptance criteria. Change-Id: Ie9625ea8d6ce5e6f59e3c7ce1d10d0a47291afa4 Reviewed-on: https://gerrit.instructure.com/167183 Tested-by: Jenkins QA-Review: Samuel Barney <sbarney@instructure.com> Reviewed-by: Marc Phillips <mphillips@instructure.com> Product-Review: Karl Lloyd <karl@instructure.com>
2018-10-03 11:23:45 +08:00
LTI_AGS_LINE_ITEM_SCOPE = "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem".freeze
LTI_AGS_LINE_ITEM_READ_ONLY_SCOPE = "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly".freeze
LTI_AGS_RESULT_READ_ONLY_SCOPE = "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly".freeze
LTI_AGS_SCORE_SCOPE = "https://purl.imsglobal.org/spec/lti-ags/scope/score".freeze
LTI_NRPS_V2_SCOPE = "https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly".freeze
LTI_UPDATE_PUBLIC_JWK_SCOPE = "https://canvas.instructure.com/lti/public_jwk/scope/update".freeze
LTI_CREATE_DATA_SERVICE_SUBSCRIPTION_SCOPE = "https://canvas.instructure.com/lti/data_services/scope/create".freeze
LTI_SHOW_DATA_SERVICE_SUBSCRIPTION_SCOPE = "https://canvas.instructure.com/lti/data_services/scope/show".freeze
LTI_UPDATE_DATA_SERVICE_SUBSCRIPTION_SCOPE = "https://canvas.instructure.com/lti/data_services/scope/update".freeze
LTI_SCOPES = {
LTI_AGS_LINE_ITEM_SCOPE => I18n.t("Can create and view assignment data in the gradebook associated with the tool."),
LTI_AGS_LINE_ITEM_READ_ONLY_SCOPE => I18n.t("Can view assignment data in the gradebook associated with the tool."),
LTI_AGS_RESULT_READ_ONLY_SCOPE => I18n.t("Can view submission data for assignments associated with the tool."),
LTI_AGS_SCORE_SCOPE => I18n.t("Can create and update submission results for assignments associated with the tool."),
LTI_NRPS_V2_SCOPE => I18n.t("Can retrieve user data associated with the context the tool is installed in."),
LTI_UPDATE_PUBLIC_JWK_SCOPE => I18n.t("Can update public jwk for LTI services."),
LTI_CREATE_DATA_SERVICE_SUBSCRIPTION_SCOPE => I18n.t("Can create subscription to data service data."),
LTI_SHOW_DATA_SERVICE_SUBSCRIPTION_SCOPE => I18n.t("Can show subscription to data service data."),
LTI_UPDATE_DATA_SERVICE_SUBSCRIPTION_SCOPE => I18n.t("Can update subscription to data service data.")
}.freeze
Include AGS claim in LTI 1.3 resource link launches - LTI 1.3 launches now include an AGS claim (`https://purl.imsglobal.org/spec/lti-ags/claim/endpoint`) if the current tool's `DeveloperKey` has been granted any AGS scope. - If the launched link is an `Assignment`, the AGS claim will include a `lineitem` sub-claim set to the `Assignment`'s LTI Advantage `LineItem` API URL (`/api/lti/courses/:course_id/line_items/:line_item_id`). - In any AGS-enabled launch from from a `Course` or `Group`, the AGS claim will include `lineitems` sub-claim set the `Course`'s LTI Advantage `LineItem` collection API URL (`/api/lti/courses/:course_id/line_items`.) Closes LTIA-49 Test Plan: 1. Create an LTI 1.3 tool with at least one AGS scope granted to its `DeveloperKey`. Those scopes are: - `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/result.readonly` - `https://purl.imsglobal.org/spec/lti-ags/scope/score` 2. Launch the tool from a course navigation link. 3. Verify that the `https://purl.imsglobal.org/spec/lti-ags/claim/endpoint` claim is present and: 3.1. Sets all the granted scopes into the `scope` sub-claim 3.2. Sets the `lineitems` sub-claim to `/api/lti/courses/:course_id/line_items` 3.3. The `lineitem` sub-claim is not present. 4. Bind the tool to an `Assignment` and launch from that `Assignment`. 5. Verify that the `https://purl.imsglobal.org/spec/lti-ags/claim/endpoint` claim is present and: 5.1. Sets all the granted scopes from step 1 into the `scope` sub-claim 5.2. Sets the `lineitems` sub-claim to `/api/lti/courses/:course_id/line_items` 5.3. Sets the `lineitem` sub-claim to `/api/lti/courses/:course_id/line_items/:line_item_id` To find :line_item_id for step 5.3 either use the console or database query. E.g. in the console: `Assignment.find(Assignment.maximum(:id)).line_items.find(&:assignment_line_item?).id` 6. Create another LTI 1.3 tool but do not grant any AGS scopes to its `DeveloperKey`. 7. Launch the tool from a course navigation link. 8. Verify that the `https://purl.imsglobal.org/spec/lti-ags/claim/endpoint` claim is not present. 9. Bind the tool to an `Assignment` and launch from that `Assignment`. 10. Verify that the `https://purl.imsglobal.org/spec/lti-ags/claim/endpoint` claim is not present. Change-Id: I787d3e99c60993ed3d28ede08455617e601f3d30 Reviewed-on: https://gerrit.instructure.com/171345 Tested-by: Jenkins Reviewed-by: Weston Dransfield <wdransfield@instructure.com> QA-Review: Weston Dransfield <wdransfield@instructure.com> Product-Review: Marc Phillips <mphillips@instructure.com>
2018-11-08 02:49:11 +08:00
LTI_AGS_SCOPES = [ LTI_AGS_LINE_ITEM_SCOPE, LTI_AGS_LINE_ITEM_READ_ONLY_SCOPE, LTI_AGS_RESULT_READ_ONLY_SCOPE, LTI_AGS_SCORE_SCOPE ].freeze
def self.named_scopes
return @_named_scopes if @_named_scopes
named_scopes = detailed_scopes.each_with_object([]) do |frozen_scope, arr|
scope = frozen_scope.dup
api_scope_mapper_class = ApiScopeMapperLoader.load
scope[:resource] ||= api_scope_mapper_class.lookup_resource(scope[:controller], scope[:action])
scope[:resource_name] = api_scope_mapper_class.name_for_resource(scope[:resource])
arr << scope if scope[:resource_name]
scope
end
@_named_scopes = Canvas::ICU.collate_by(named_scopes) {|s| s[:resource_name]}.freeze
end
def self.all_scopes
@_all_scopes ||= [USER_INFO_SCOPE[:scope], *api_routes.map {|route| route[:scope]}, *LTI_SCOPES.keys].freeze
end
def self.detailed_scopes
@_detailed_scopes ||= [USER_INFO_SCOPE, *api_routes].freeze
end
private_class_method :detailed_scopes
def self.api_routes
return @_api_routes if @_api_routes
routes = Rails.application.routes.routes.select {|route| /^\/api\/(v1|sis)/ =~ route.path.spec.to_s}.map do |route|
{
controller: route.defaults[:controller]&.to_sym,
action: route.defaults[:action]&.to_sym,
verb: route.verb,
path: route.path.spec.to_s.gsub(/\(\.:format\)$/, ''),
scope: TokenScopesHelper.scope_from_route(route).freeze,
}
end
@_api_routes = routes.uniq {|route| route[:scope]}.freeze
end
private_class_method :api_routes
end