canvas-lms/lib/token_scopes.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

129 lines
6.7 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
#
# 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/"
USER_INFO_SCOPE = {
resource: :oauth2,
verb: "GET",
scope: "#{OAUTH2_SCOPE_NAMESPACE}userinfo"
}.freeze
# Allows interaction with Canvas Data service
CD2_SCOPE = {
resource: :peer_services,
verb: "GET",
scope: "cd2"
}.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"
LTI_AGS_LINE_ITEM_READ_ONLY_SCOPE = "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly"
LTI_AGS_RESULT_READ_ONLY_SCOPE = "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly"
LTI_AGS_SCORE_SCOPE = "https://purl.imsglobal.org/spec/lti-ags/scope/score"
LTI_AGS_SHOW_PROGRESS_SCOPE = "https://canvas.instructure.com/lti-ags/progress/scope/show"
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_NRPS_V2_SCOPE = "https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly"
LTI_UPDATE_PUBLIC_JWK_SCOPE = "https://canvas.instructure.com/lti/public_jwk/scope/update"
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"
LTI_CREATE_DATA_SERVICE_SUBSCRIPTION_SCOPE = "https://canvas.instructure.com/lti/data_services/scope/create"
LTI_SHOW_DATA_SERVICE_SUBSCRIPTION_SCOPE = "https://canvas.instructure.com/lti/data_services/scope/show"
LTI_UPDATE_DATA_SERVICE_SUBSCRIPTION_SCOPE = "https://canvas.instructure.com/lti/data_services/scope/update"
LTI_LIST_DATA_SERVICE_SUBSCRIPTION_SCOPE = "https://canvas.instructure.com/lti/data_services/scope/list"
LTI_DESTROY_DATA_SERVICE_SUBSCRIPTION_SCOPE = "https://canvas.instructure.com/lti/data_services/scope/destroy"
LTI_LIST_EVENT_TYPES_DATA_SERVICE_SUBSCRIPTION_SCOPE = "https://canvas.instructure.com/lti/data_services/scope/list_event_types"
LTI_SHOW_FEATURE_FLAG_SCOPE = "https://canvas.instructure.com/lti/feature_flags/scope/show"
LTI_CREATE_ACCOUNT_EXTERNAL_TOOLS_SCOPE = "https://canvas.instructure.com/lti/account_external_tools/scope/create"
LTI_DESTROY_ACCOUNT_EXTERNAL_TOOLS_SCOPE = "https://canvas.instructure.com/lti/account_external_tools/scope/destroy"
LTI_LIST_ACCOUNT_EXTERNAL_TOOLS_SCOPE = "https://canvas.instructure.com/lti/account_external_tools/scope/list"
LTI_SHOW_ACCOUNT_EXTERNAL_TOOLS_SCOPE = "https://canvas.instructure.com/lti/account_external_tools/scope/show"
LTI_UPDATE_ACCOUNT_EXTERNAL_TOOLS_SCOPE = "https://canvas.instructure.com/lti/account_external_tools/scope/update"
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_AGS_SHOW_PROGRESS_SCOPE => I18n.t("Can view Progress records associated with the context the tool is installed in"),
}.freeze
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,
LTI_AGS_SHOW_PROGRESS_SCOPE
].freeze
LTI_HIDDEN_SCOPES = {
LTI_CREATE_ACCOUNT_EXTERNAL_TOOLS_SCOPE => I18n.t("Can create external 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."),
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
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
scope[:resource] ||= ApiScopeMapper.lookup_resource(scope[:controller], scope[:action])
scope[:resource_name] = ApiScopeMapper.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], CD2_SCOPE[:scope], *api_routes.pluck(:scope), *LTI_SCOPES.keys, *LTI_HIDDEN_SCOPES.keys].freeze
end
def self.detailed_scopes
@_detailed_scopes ||= [USER_INFO_SCOPE, CD2_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| %r{^/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: TokenScopesHelper.path_without_format(route),
scope: TokenScopesHelper.scope_from_route(route).freeze,
}
end
@_api_routes = routes.uniq { |route| route[:scope] }.freeze
end
def self.reset!
@_api_routes = nil
@_all_scopes = nil
@_detailed_scopes = nil
@_named_scopes = nil
end
end