Add API Token Scope Docs

Closes PLAT-3394

Test Plan:
- Run the `doc:api` rake task
- Navigate to /doc/api/index.html and verify there
  are two new links in the OAuth2 section ("Developer
  Keys" and "API Token Scopes")
- Verify both links work
- Verify the token scopes documentation has a table
  for each scope group and includes all Canvas
  scopes
- Verify "Resources" documentation pages now display
  the scope along with each API endpoint

Change-Id: I2fea0ff531744dbaf63d24619b3c0e9655a25a7a
Reviewed-on: https://gerrit.instructure.com/151010
QA-Review: August Thornton <august@instructure.com>
Reviewed-by: Nathan Mills <nathanm@instructure.com>
Tested-by: Jenkins
Product-Review: Jesse Poulos <jpoulos@instructure.com>
This commit is contained in:
wdransfield 2018-05-21 10:03:24 -06:00 committed by Weston Dransfield
parent 1ef680d63a
commit 8b0740be29
13 changed files with 176 additions and 19 deletions

3
.gitignore vendored
View File

@ -73,3 +73,6 @@ docker-compose.local.*
# generated scope names
/lib/api_scope_mapper.rb
# generated scopes markdown
/doc/api/api_token_scopes.md

View File

@ -70,16 +70,8 @@ class ScopesApiController < ApplicationController
#
# @returns [Scope]
def index
named_scopes = TokenScopes.named_scopes
if authorized_action(@context, @current_user, :manage_role_overrides)
named_scopes = TokenScopes::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]}
scopes = params[:group_by] == "resource_name" ? named_scopes.group_by {|route| route[:resource_name]} : named_scopes
render json: scopes
end

View File

@ -0,0 +1,48 @@
#
# 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 ApiScopeMarkdownWriter
def initialize(named_scopes)
@output_file = Rails.root.join("doc", "api", "api_token_scopes.md")
@named_scopes = named_scopes
end
def generate_markdown!
File.open(@output_file, 'w+') do |f|
f.write(header)
f.write("<style>table { width: 100%; }</style>")
f.write(intro)
@named_scopes.each do |resource, scopes|
f.write("\n## #{resource}\n")
f.write("|Verb|Endpoint|Scope|\n")
f.write("|---|---|---|\n")
scopes.each { |s| f.write("|#{s[:verb]}|#{s[:path]}|#{s[:scope].gsub('|', '&#124;')}|\n") }
end
end
end
private
def header
"Api Token Scopes\n==========================\n".freeze
end
def intro
'Below is a list of all API token scopes (See [here](/doc/api/file.developer_keys.html)). '\
'Scopes may also be found beneath their corresponding endpoints in the "resources" documentation pages.'.freeze
end
end

58
doc/api/developer_keys.md Normal file
View File

@ -0,0 +1,58 @@
Developer Keys
==============
Developer keys are OAuth2 client ID and secret pairs stored in Canvas that allow third-party applications to request access to Canvas API endpoints via the [OAuth2 flow](https://canvas.instructure.com/doc/api/file.oauth.html). Access is granted after a user authorizes an app and Canvas creates an API access token thats returned in the final request of the OAuth2 flow.
Developer keys created in a root account, by root account administrators or Instructure employees, are only functional for the account they are created in and its sub-accounts. Developer keys created globally, by an Instructure employee, are functional in any Canvas account where they are enabled.
By scoping the tokens, Canvas allows root account administrators to manage the specific API endpoints that tokens issued from a developer key have access to.
## Developer Key Scopes
Developer key scopes allow root account administrators to restrict the tokens issued from developer keys to a subset of Canvas API endpoints in their account.
### What are developer key scopes in Canvas?
Each Canvas API endpoint has an associated scope. Canvas developer key scopes can only be enabled/disabled by a root account administrator or an Instructure employee.
Scopes take the following form:
```
url:<HTTP Verb>|<Canvas API Endpoint Path>
```
For example, the corresponding scope for the `GET /api/v1/courses/:course_id/rubrics` API endpoint would be
```
url:GET|/api/v1/courses/:course_id/rubrics
```
### How do developer key scopes function?
When a client makes any API request, Canvas will verify the requested endpoint's scope has been granted by the account administrator to the developer key of the request's access token.
If the requested endpoint's scope has not been granted Canvas will respond with `401 Unauthorized`.
### Who can grant or revoke scopes for a developer key?
For developer keys created in a specific root account, administrators for that account may grant or revoke scopes. When requesting a developer key, application owners should communicate with administrators which scopes their integrations require.
For global developer keys, an Instructure employee may grant or revoke scopes.
### Where can I see what scopes are available?
A complete list of available scopes can be found [here](/doc/api/file.api_token_scopes.html).
Scopes may also be found beneath their corresponding endpoints in the "resources" documentation pages.
## Developer Key Management
Developer key management features allow root account administrators to turn global developer keys "on" and "off" for only their account.
### What management features are available?
Root account administrators may enable or disable global developer keys for their specific account. This means that vendors who wish to have integrations that work in any Canvas account may request a global developer key from Instructure allowing account administrators enable the key for their account.
To request a global developer key please contact: partnersupport@instructure.com
Please include:
- A list of Redirect URIs that your app uses
- An Icon URL. This will be shown on the authorization screen.
- The scopes the key requires access to and how the data will be used (ex: We need access to `url:GET|/api/v1/courses/:course_id/rubrics` since we return this data in a custom analytics dashboard). Ideally, this information would be already contained within your integration documentation and a link to the documentation should suffice.
- Describe the security policy surrounding how developer keys and tokens will be stored.
- A point of contact (email address) at the company. Ideally, this will be an account that is accessible even if the requester leaves the company.
- Links to any relevant documentation for your integration.
### How do management features function?
When a client uses the [OAuth2 Auth endpoint](https://canvas.instructure.com/doc/api/file.oauth_endpoints.html#get-login-oauth2-auth) as part of the flow to retrieve an access token canvas will check the developer key associated with the `client_id`. If the developer key is not enabled in the requested account, Canvas will respond with `unauthorized_client`.
When a client makes any API request, Canvas will check the developer key associated with the access token used in the request. If the developer key is not enabled for the requested account, Canvas will respond with `401 Unauthorized`.

View File

@ -163,6 +163,11 @@ h3.beta {
border-left: 3px solid #0073ac;
}
.method_details code.scope {
background-color: #eee;
padding: 1px;
}
h2 .defined-in {
float:right;
font-size:10px;

View File

@ -10,6 +10,8 @@
<h2>OAuth2</h2>
<a href="<%= url_for("file.oauth.html") %>" class="<%= 'current' if options[:object] == 'file.oauth.html' %>">OAuth2 Overview</a>
<a href="<%= url_for("file.oauth_endpoints.html") %>" class="<%= 'current' if options[:object] == 'file.oauth_endpoints.html' %>">OAuth2 Endpoints</a>
<a href="<%= url_for("file.developer_keys.html") %>" class="<%= 'current' if options[:object] == 'file.developer_keys.html' %>">Developer Keys</a>
<a href="<%= url_for("file.api_token_scopes.html") %>" class="<%= 'current' if options[:object] == 'file.api_token_scopes.html' %>">API Token Scopes</a>
<h2>Resources</h2>
<a <%= "class='current'" if options[:all_resources] %> href="all_resources.html">All Resources Reference</a>
<% options[:resources].each_with_index do |(resource, controllers), i| %>

View File

@ -1,7 +1,13 @@
<% require 'lib/token_scopes_helper.rb' %>
<% @routes.each do |route| %>
<% route_path = route.path.spec.to_s.sub("(.:format)", "")
next if route_path =~ /\.json$/ %>
<h3 class='endpoint'>
<%= route.verb %> <%= route_path %>
</h3>
<div>
<strong>Scope: </strong>
<code class="scope"><%= TokenScopesHelper.scope_from_route(route) %></code>
</div>
<% end %>

View File

@ -59,13 +59,11 @@ wrong person in, as <a href="http://homakov.blogspot.com/2012/07/saferweb-most-c
</tr>
<tr>
<td class="mono">scope<span class="label optional"></span></td>
<td>This can be used to specify what information the access token
will provide access to. By default an access token will have access to
all api calls that a user can make. The only other accepted value
for this at present is '/auth/userinfo'. When used, this will return only
the current canvas user's identity. Some OAuth libraries may require a
scope parameter to be specified; if so, passing no value for the scope
parameter will behave as if no scope parameter was specified.</td>
<td>
This can be used to specify what information the access token will provide access to.
A complete list of available scopes can be found <a href="/doc/api/file.api_token_scopes.html">here</a>. Scopes may also be found beneath their corresponding endpoints in the "resources" documentation pages.
If the developer key does not require scopes and no scope parameter is specified, the access token will have access to all scopes. If the developer key does require scopes and no scope parameter is specified, Canvas will respond with "invalid_scope."
</td>
</tr>
<tr>
<td class="mono">purpose<span class="label optional"></span></td>

View File

@ -1,6 +1,7 @@
begin
require 'yard'
require 'yard-appendix'
require 'doc/api/api_scope_markdown_writer.rb'
namespace :doc do
DOC_DIR = File.join(%w[public doc api])
@ -15,6 +16,7 @@ namespace :doc do
YARD::Rake::YardocTask.new(:api) do |t|
t.before = proc { FileUtils.rm_rf(API_DOC_DIR) }
t.before = proc { `script/generate_lti_variable_substitution_markdown` }
t.before = proc { `script/generate_api_token_scopes_markdown` }
t.files = %w[
app/controllers/**/*.rb
{gems,vendor}/plugins/*/app/controllers/*.rb
@ -41,6 +43,12 @@ namespace :doc do
"See #{DOC_DIR}/index.html"
end
task :scopes => :environment do
scope_writer = ApiScopeMarkdownWriter.new(
TokenScopes.named_scopes.group_by {|route| route[:resource_name]}.freeze
)
scope_writer.generate_markdown!
end
end
rescue LoadError

View File

@ -23,15 +23,26 @@ class TokenScopes
scope: "#{OAUTH2_SCOPE_NAMESPACE}userinfo"
}.freeze
def self.named_scopes
named_scopes = TokenScopes::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
Canvas::ICU.collate_by(named_scopes) {|s| s[:resource_name]}
end
def self.api_routes
routes = Rails.application.routes.routes.select { |route| /^\/api\/(v1|sis)/ =~ route.path.spec.to_s }.map do |route|
path = route.path.spec.to_s.gsub(/\(\.:format\)$/, '')
{
controller: route.defaults[:controller]&.to_sym,
action: route.defaults[:action]&.to_sym,
verb: route.verb,
path: path,
scope: "url:#{route.verb}|#{path}".freeze,
path: route.path.spec.to_s.gsub(/\(\.:format\)$/, ''),
scope: TokenScopesHelper.scope_from_route(route).freeze,
}
end
routes.uniq {|route| route[:scope]}

View File

@ -0,0 +1,22 @@
#
# 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/>.
#
module TokenScopesHelper
def self.scope_from_route(route)
"url:#{route.verb}|#{route.path.spec.to_s.gsub(/\(\.:format\)$/, '')}"
end
end

View File

@ -0,0 +1,3 @@
#! /bin/sh
bundle exec rake doc:scopes

View File

@ -59,6 +59,7 @@ describe ScopesApiController, type: :request do
it "groups scopes when group_by is passed in" do
scope_params[:group_by] = "resource_name"
json = api_call(:get, "/api/v1/accounts/#{@account.id}/scopes", scope_params)
expect(json["oauth2"]).to eq [{
"resource"=>"oauth2",