canvas-lms/lib/token_scopes.rb

107 lines
6.4 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
Account lookup endpoint for CDC Event Transformer Endpoint needed by CDC Event Tranformer to look up UUIDs for root accounts it hasn't seen. closes PLAT-5515 flag=none Test plan: - get an LTI Advantage token for a site admin dev key with the new scope. This can be done by adding logging to the live-events-lti tool (I think in app/services/access_token_service.rb#16) or using my cdc-event-transformer test in CanvasClientTest.java and printing out cc.getToken(). - hit the new endpoint with header "Authorization: Bearer MYACCESSTOKEN" web.canvas-lms.docker/api/lti/accounts/123 with varying values of account ID, including global IDs, accounts that do not exist, global IDs where the shard does not exist... - ideally we'd like to test that a dev key only available for one shard cannot access account info for another shard. This could be done by creating two dev keys that have the same ID but are on different shards, and use a token for one dev key to try to access an account on the other shard. This is probably hard to do locally though. I did test on prod that DeveloperKey#account_binding_for will return nil for an account in another shard even if there is a dev key with the same local ID in the account's shard that has access to it. Change-Id: I1299ebce9b94ce00d7cb62db01891b81908915ff Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/229580 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Weston Dransfield <wdransfield@instructure.com> QA-Review: Xander Moffatt <xmoffatt@instructure.com> Product-Review: Evan Battaglia <ebattaglia@instructure.com>
2020-03-11 07:11:46 +08:00
LTI_ACCOUNT_LOOKUP_SCOPE = "https://canvas.instructure.com/lti/account_lookup/scope/show".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_LIST_DATA_SERVICE_SUBSCRIPTION_SCOPE = "https://canvas.instructure.com/lti/data_services/scope/list".freeze
LTI_DESTROY_DATA_SERVICE_SUBSCRIPTION_SCOPE = "https://canvas.instructure.com/lti/data_services/scope/destroy".freeze
LTI_LIST_EVENT_TYPES_DATA_SERVICE_SUBSCRIPTION_SCOPE = "https://canvas.instructure.com/lti/data_services/scope/list_event_types".freeze
LTI_SHOW_FEATURE_FLAG_SCOPE = "https://canvas.instructure.com/lti/feature_flags/scope/show".freeze
LTI_CREATE_ACCOUNT_EXTERNAL_TOOLS_SCOPE = "https://canvas.instructure.com/lti/account_external_tools/scope/create".freeze
LTI_DESTROY_ACCOUNT_EXTERNAL_TOOLS_SCOPE = "https://canvas.instructure.com/lti/account_external_tools/scope/destroy".freeze
LTI_LIST_ACCOUNT_EXTERNAL_TOOLS_SCOPE = "https://canvas.instructure.com/lti/account_external_tools/scope/list".freeze
LTI_SHOW_ACCOUNT_EXTERNAL_TOOLS_SCOPE = "https://canvas.instructure.com/lti/account_external_tools/scope/show".freeze
LTI_UPDATE_ACCOUNT_EXTERNAL_TOOLS_SCOPE = "https://canvas.instructure.com/lti/account_external_tools/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."),
Account lookup endpoint for CDC Event Transformer Endpoint needed by CDC Event Tranformer to look up UUIDs for root accounts it hasn't seen. closes PLAT-5515 flag=none Test plan: - get an LTI Advantage token for a site admin dev key with the new scope. This can be done by adding logging to the live-events-lti tool (I think in app/services/access_token_service.rb#16) or using my cdc-event-transformer test in CanvasClientTest.java and printing out cc.getToken(). - hit the new endpoint with header "Authorization: Bearer MYACCESSTOKEN" web.canvas-lms.docker/api/lti/accounts/123 with varying values of account ID, including global IDs, accounts that do not exist, global IDs where the shard does not exist... - ideally we'd like to test that a dev key only available for one shard cannot access account info for another shard. This could be done by creating two dev keys that have the same ID but are on different shards, and use a token for one dev key to try to access an account on the other shard. This is probably hard to do locally though. I did test on prod that DeveloperKey#account_binding_for will return nil for an account in another shard even if there is a dev key with the same local ID in the account's shard that has access to it. Change-Id: I1299ebce9b94ce00d7cb62db01891b81908915ff Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/229580 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Weston Dransfield <wdransfield@instructure.com> QA-Review: Xander Moffatt <xmoffatt@instructure.com> Product-Review: Evan Battaglia <ebattaglia@instructure.com>
2020-03-11 07:11:46 +08:00
LTI_ACCOUNT_LOOKUP_SCOPE => I18n.t("Can lookup Account information"),
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."),
LTI_LIST_DATA_SERVICE_SUBSCRIPTION_SCOPE => I18n.t("Can list subscriptions to data service data."),
LTI_DESTROY_DATA_SERVICE_SUBSCRIPTION_SCOPE => I18n.t("Can destroy subscription to data service data."),
LTI_LIST_EVENT_TYPES_DATA_SERVICE_SUBSCRIPTION_SCOPE => I18n.t("Can list categorized event types."),
LTI_SHOW_FEATURE_FLAG_SCOPE => I18n.t('Can view feature flags')
}.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
LTI_HIDDEN_SCOPES = {
LTI_CREATE_ACCOUNT_EXTERNAL_TOOLS_SCOPE => I18n.t("Can create extneral tools."),
LTI_DESTROY_ACCOUNT_EXTERNAL_TOOLS_SCOPE => I18n.t("Can destroy external tools."),
LTI_LIST_ACCOUNT_EXTERNAL_TOOLS_SCOPE => I18n.t("Can list external tools."),
LTI_SHOW_ACCOUNT_EXTERNAL_TOOLS_SCOPE => I18n.t("Can show external tools."),
LTI_UPDATE_ACCOUNT_EXTERNAL_TOOLS_SCOPE => I18n.t("Can update external tools."),
}.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, *LTI_HIDDEN_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