Skip to content

Commit

Permalink
Merge pull request #432 from ucfopen/issue/406-scoped-keys
Browse files Browse the repository at this point in the history
Issue/406 scoped keys
  • Loading branch information
bagofarms authored Dec 17, 2019
2 parents c350e0b + d896020 commit ae8fbd9
Show file tree
Hide file tree
Showing 11 changed files with 255 additions and 88 deletions.
45 changes: 37 additions & 8 deletions HEROKU.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ After clicking the Heroku button above:

1. Create an account (if you don't have one already).
2. Give the app a name.
3. Fill out the `OAUTH2_ID` and `OAUTH2_KEY` fields with dummy data. (We'll fix it later.)
4. Fill out the `OAUTH2_URI` field with `https://yourapp.herokuapp.com/oauth2response.php`. (Replace 'yourapp' with the name you gave in step 2.)
5. Fill out the `CANVAS_NAV_ITEM_NAME` field with the name you would like the app to appear as in the course navigation menu. This is useful if your instance will be use for a pilot. The normal value to use here is ***UDOIT***.
6. (optional) Copy and paste your Google/YouTube API key into the `GOOGLE_API_KEY` field.
7. (optional) Copy and paste your Vimeo API key into the `VIMEO_API_KEY` field.
8. (optional) If you have a Google Analytics account, you can paste your site tracking code into the `GA_TRACKING_CODE` field.
9. (optional) If you would like to enable the Admin Panel, change the `ADMIN_PANEL_ENABLED` field to `true`.
10. (optional) If you would like to disable API Caching for dev or testing purposes, set the field to `false`.
3. Set `OAUTH2_ENFORCE_SCOPES` to true if you have a scoped developer key.
4. Fill out the `OAUTH2_ID` and `OAUTH2_KEY` fields with dummy data. (We'll fix it later.)
5. Fill out the `OAUTH2_URI` field with `https://yourapp.herokuapp.com/oauth2response.php`. (Replace 'yourapp' with the name you gave in step 2.)
6. Fill out the `CANVAS_NAV_ITEM_NAME` field with the name you would like the app to appear as in the course navigation menu. This is useful if your instance will be use for a pilot. The normal value to use here is ***UDOIT***.
7. (optional) Copy and paste your Google/YouTube API key into the `GOOGLE_API_KEY` field.
8. (optional) Copy and paste your Vimeo API key into the `VIMEO_API_KEY` field.
9. (optional) If you have a Google Analytics account, you can paste your site tracking code into the `GA_TRACKING_CODE` field.
10. (optional) If you would like to enable the Admin Panel, change the `ADMIN_PANEL_ENABLED` field to `true`.
11. Click the Deploy button and wait for the process to complete.

### Step 3: Request a Developer Key
Expand All @@ -34,6 +34,35 @@ UDOIT uses Oauth2 to take actions on behalf of the user, so you'll need to ask y
* This should be `https://yourapp.herokuapp.com/oauth2response.php`. (Replace 'yourapp' with the name of your UDOIT instance on Heroku.)
* ***Icon URL:*** The URL of the UDOIT icon. This is `https://yourapp.herokuapp.com/assets/img/udoit_icon.png`. (Replace ***yourapp*** with the name of your UDOIT instance on Heroku.)

#### Scoped Developer Keys
If you'd like to use this option, you'll need set the following scopes for your developer key.
* Assignments
* url:GET|/api/v1/courses/:course_id/assignments
* url:GET|/api/v1/courses/:course_id/assignments/:id
* url:PUT|/api/v1/courses/:course_id/assignments/:id
* Courses
* url:PUT|/api/v1/courses/:id
* url:GET|/api/v1/courses/:id
* url:POST|/api/v1/courses/:course_id/files
* Discussion Topics
* url:GET|/api/v1/courses/:course_id/discussion_topics
* url:GET|/api/v1/courses/:course_id/discussion_topics/:topic_id
* url:PUT|/api/v1/courses/:course_id/discussion_topics/:topic_id
* Files
* url:GET|/api/v1/courses/:course_id/files
* url:GET|/api/v1/courses/:course_id/folders/:id
* url:GET|/api/v1/folders/:id/folders
* url:GET|/api/v1/folders/:id/files
* Modules
* url:GET|/api/v1/courses/:course_id/modules
* url:GET|/api/v1/courses/:course_id/modules/:module_id/items
* Pages
* url:GET|/api/v1/courses/:course_id/pages
* url:GET|/api/v1/courses/:course_id/pages/:url
* url:PUT|/api/v1/courses/:course_id/pages/:url
* Users
* url:GET|/api/v1/users/:user_id/profile

### Step 4: Add your Developer Key to UDOIT
1. In Heroku, click the 'Manage App' button for your install of UDOIT.
2. Go to the 'Settings' tab.
Expand Down
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,35 @@ UDOIT uses Oauth2 to take actions on behalf of the user, so you'll need to ask y
* If you did a normal install into the web root of your server, it would be `https://www.example.com/public/oauth2response.php`. (Replace 'www.example.com' with the url of your UDOIT server.)
* ***Icon URL:*** The URL of the UDOIT icon. This is `https://www.example.com/public/assets/img/udoit_icon.png`. (Replace 'www.example.com' with the url of your UDOIT server.)

#### Scoped Developer Keys
If you'd like to use this option, you'll need set the following scopes for your developer key.
* Assignments
* url:GET|/api/v1/courses/:course_id/assignments
* url:GET|/api/v1/courses/:course_id/assignments/:id
* url:PUT|/api/v1/courses/:course_id/assignments/:id
* Courses
* url:PUT|/api/v1/courses/:id
* url:GET|/api/v1/courses/:id
* url:POST|/api/v1/courses/:course_id/files
* Discussion Topics
* url:GET|/api/v1/courses/:course_id/discussion_topics
* url:GET|/api/v1/courses/:course_id/discussion_topics/:topic_id
* url:PUT|/api/v1/courses/:course_id/discussion_topics/:topic_id
* Files
* url:GET|/api/v1/courses/:course_id/files
* url:GET|/api/v1/courses/:course_id/folders/:id
* url:GET|/api/v1/folders/:id/folders
* url:GET|/api/v1/folders/:id/files
* Modules
* url:GET|/api/v1/courses/:course_id/modules
* url:GET|/api/v1/courses/:course_id/modules/:module_id/items
* Pages
* url:GET|/api/v1/courses/:course_id/pages
* url:GET|/api/v1/courses/:course_id/pages/:url
* url:PUT|/api/v1/courses/:course_id/pages/:url
* Users
* url:GET|/api/v1/users/:user_id/profile

After you receive your Developer Key from your Canvas admin, edit the following variables in `config/localConfig.php`:

* `$oauth2_id`: The Client_ID your Canvas admin gives you
Expand Down Expand Up @@ -273,6 +302,9 @@ The `Deploy to Heroku` button installs the latest release of UDOIT when clicked.
* Allow inbound traffic from world to UDOIT on 80 and 443
* Allow outbound traffic from UDOIT to Canvas on 443

### Why am I recieving a "Due to LMS limitations, UDOIT is unable to scan this section." error?
When an institution installs UDOIT and uses a scoped developer key, certain features of the Canvas API are unavailable to UDOIT, including retrieving content from the Syllabus tool. This limitation does not affect UDOIT installations that use a non-scoped developer key. For more information, refer to the "Canvas API Includes" section of the <a href="https://canvas.instructure.com/doc/api/file.developer_keys.html">Canvas API Documentation</a>.

# Developing and Testing

For quick local development, set `$UDOIT_ENV = ENV_DEV;` in `config/localConfig.php`. This flag disables authentication and allows you to quickly see a sample test report for most template, js, and css development. Use this along with the quick dev server below.
Expand Down
4 changes: 4 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
"description": "Full url to your oauth2responce.php file",
"value": "https://your.herokuapp.com/oauth2response.php"
},
"OAUTH2_ENFORCE_SCOPES": {
"description": "Set to true if you have a scoped developer key",
"value": "false"
},
"GOOGLE_API_KEY": {
"description": "Google API key for caption detection support",
"required": false
Expand Down
1 change: 1 addition & 0 deletions config/herokuConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
$oauth2_id = getenv('OAUTH2_ID');
$oauth2_key = getenv('OAUTH2_KEY');
$oauth2_uri = getenv('OAUTH2_URI');
$oauth2_enforce_scopes = (getenv('OAUTH2_ENFORCE_SCOPES')) == 'true';

/* Tool name for display in Canvas Navigation */
$canvas_nav_item_name = getenv('CANVAS_NAV_ITEM_NAME');
Expand Down
7 changes: 4 additions & 3 deletions config/localConfig.template.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
$shared_secret = '';

/* Canvas Developer Key Oauth 2.0 Settings */
$oauth2_id = ''; // Provided by your Canvas Admin
$oauth2_key = ''; // Provided by your Canvas Admin
$oauth2_uri = ''; // EX: https://udoit.my-org.edu/oauth2response.php or https://udoit.my-org.edu/udoit/public/oauth2response.php
$oauth2_id = ''; // Provided by your Canvas Admin
$oauth2_key = ''; // Provided by your Canvas Admin
$oauth2_uri = ''; // EX: https://udoit.my-org.edu/oauth2response.php or https://udoit.my-org.edu/udoit/public/oauth2response.php
$oauth2_enforce_scopes = false; // Set to true if you have a scoped developer key.

/* Disable headings check character count */
$doc_length = '1500';
Expand Down
30 changes: 29 additions & 1 deletion config/settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,35 @@
isset($UDOIT_ENV) || $UDOIT_ENV = ENV_PROD; // !! override in your localConfig.php

// SET UP OAUTH
UdoitUtils::setupOauth($oauth2_id, $oauth2_key, $oauth2_uri, $consumer_key, $shared_secret, $curl_ssl_verify);
$oauth2_scopes = [
// assigments
'url:GET|/api/v1/courses/:course_id/assignments',
'url:GET|/api/v1/courses/:course_id/assignments/:id',
'url:PUT|/api/v1/courses/:course_id/assignments/:id',
// courses
'url:PUT|/api/v1/courses/:id',
'url:GET|/api/v1/courses/:id',
'url:POST|/api/v1/courses/:course_id/files',
// discussion topics
'url:GET|/api/v1/courses/:course_id/discussion_topics',
'url:GET|/api/v1/courses/:course_id/discussion_topics/:topic_id',
'url:PUT|/api/v1/courses/:course_id/discussion_topics/:topic_id',
// files
'url:GET|/api/v1/courses/:course_id/files',
'url:GET|/api/v1/courses/:course_id/folders/:id',
'url:GET|/api/v1/folders/:id/folders',
'url:GET|/api/v1/folders/:id/files',
// modules
'url:GET|/api/v1/courses/:course_id/modules',
'url:GET|/api/v1/courses/:course_id/modules/:module_id/items',
// pages
'url:GET|/api/v1/courses/:course_id/pages',
'url:GET|/api/v1/courses/:course_id/pages/:url',
'url:PUT|/api/v1/courses/:course_id/pages/:url',
// users
'url:GET|/api/v1/users/:user_id/profile',
];
UdoitUtils::setupOauth($oauth2_id, $oauth2_key, $oauth2_uri, $consumer_key, $shared_secret, $curl_ssl_verify, $oauth2_enforce_scopes, $oauth2_scopes);

// SET UP DATABASE
UdoitDB::setup($db_type, $dsn, $db_user, $db_password, $db_options);
Expand Down
106 changes: 73 additions & 33 deletions lib/Udoit.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,18 +97,30 @@ public static function retrieveAndScan($api_key, $canvas_api_url, $course_id, $c

$logger->addInfo("Finished retrieveAndScan - course: {$course_id}, content: {$content_type}");

$content_return = [
'title' => $content_type,
'items' => $items_with_issues,
'amount' => $content['amount'],
'time' => $content['time'],
];

// If there was an api error, and it's set to true, pass it up
if (isset($content['api_error']) && $content['api_error']) {
$content_return['api_error'] = $content['api_error'];
}

// If there was a scope error, and it's set to true, pass it up
if (isset($content['scope_error']) && $content['scope_error']) {
$content_return['scope_error'] = $content['scope_error'];
}

return [
'total_results' => $totals,
'scan_results' => [
'unscannable' => $content['unscannable'],
'error_summary' => $error_summary,
'suggestion_summary' => $suggestion_summary,
$content_type => [
'title' => $content_type,
'items' => $items_with_issues,
'amount' => $content['amount'],
'time' => $content['time'],
],
$content_type => $content_return,
],
];
}
Expand Down Expand Up @@ -196,7 +208,6 @@ public static function scanContent(array $content_items, $report_type, $course_l
public static function getCourseContent($api_key, $canvas_api_url, $course_id, $type, $content_flag)
{
global $logger;

$api_url = "{$canvas_api_url}/api/v1/courses/{$course_id}/";
$content_result = [
'items' => [], // array of items of this type
Expand Down Expand Up @@ -338,24 +349,40 @@ public static function getCourseContent($api_key, $canvas_api_url, $course_id, $
break;

case 'module_urls':
$url = "{$api_url}modules?include[]=items&";
$url = "{$api_url}modules";
$search = '/(youtube|vimeo)/';
$resp = static::apiGetAllLinks($api_key, $url);
$resp = static::apiGet($url, $api_key)->send()->body;
$count = 0;

//For each module
foreach ($resp as $r) {
foreach ($r->items as $c) {
if (($content_flag) || $c->published == "true") {
$count++;
$external_url = (isset($c->external_url) ? $c->external_url : '');

if (preg_match($search, $external_url) === 1) {
$content_result['items'][] = [
'id' => $c->id,
'external_url' => $c->external_url,
'title' => $c->title,
'url' => $c->html_url,
];
// Skip the module if it's unpublished and the user selected that option
if (($content_flag) || $r->published == "true") {
// Grab item data from item url
$items_url = $r->items_url;
$items = static::apiGet($items_url, $api_key)->send()->body;

if (isset($items->errors) && count($items->errors) > 0) {
foreach ($items->errors as $error) {
$logger->addError("Canvas API responded with an error for {$items_url}: $error->message");
}
break;
}

foreach ($items as $c) {
// Skip the item if it's unpublished and the user selected that option
if (($content_flag) || $c->published == "true") {
$count++;
$external_url = (isset($c->external_url) ? $c->external_url : '');

if (preg_match($search, $external_url) === 1) {
$content_result['items'][] = [
'id' => $c->id,
'external_url' => $c->external_url,
'title' => $c->title,
'url' => $c->html_url,
];
}
}
}
}
Expand All @@ -369,15 +396,26 @@ public static function getCourseContent($api_key, $canvas_api_url, $course_id, $
case 'syllabus':
$url = "{$api_url}?include[]=syllabus_body";
$response = static::apiGet($url, $api_key)->send();
if (($content_flag) || $response->body->published == "true") {
if (!empty($response->body->syllabus_body)) {
$content_result['items'][] = [
'id' => $response->body->id,
'content' => $response->body->syllabus_body,
'title' => 'Syllabus',
'url' => "{$canvas_api_url}/courses/{$course_id}/assignments/syllabus",
];

if (isset($response->body->syllabus_body)) {
$logger->addInfo("Syllabus body found, adding to report.");
$content_result['items'][] = [
'id' => $response->body->id,
'content' => $response->body->syllabus_body,
'title' => 'Syllabus',
'url' => "{$canvas_api_url}/courses/{$course_id}/assignments/syllabus",
];
} elseif (isset($response->body->errors) && count($response->body->errors) > 0) {
foreach ($response->body->errors as $error) {
$logger->addError("Canvas API responded with an error for {$url}: $error->message");
}
// Report this error back to the user.
$content_result['api_error'] = true;
} else {
// This is likely caused by a scoped developer key not having sufficient scopes
// Or it could be due to a limitation in canvas that doesn't allow includes for scoped keys
$logger->addError("Unable to scan Syllabus due to scoped developer key. Displaying message to user.");
$content_result['scope_error'] = true;
}
break;

Expand Down Expand Up @@ -430,15 +468,17 @@ protected static function apiGetAllLinks($api_key, $url)

do {
$response = static::apiGet("{$url}page=1&per_page={$per_page}", $api_key)->send();
if ($response->status > 400) {
$logger->addError("Canvas api responded with an error for {$filtered_url}");
if (isset($response->body->errors) && count($response->body->errors) > 0) {
foreach ($response->body->errors as $error) {
$logger->addError("Canvas API responded with an error for {$url}: $error->message");
}
break;
}

$links = static::apiParseLinks($response->headers->toArray()['link']);

if (empty($response->body)) {
$logger->addError("Canvas API returned empty body for {$filtered_url}");
$logger->addError("Canvas API returned empty body for {$url}");
break;
}

Expand All @@ -452,7 +492,7 @@ protected static function apiGetAllLinks($api_key, $url)

usleep(250000); // 1/4 sec
} while (isset($links['next']) && $cur_page < $limit);

return $results;
}

Expand Down
14 changes: 13 additions & 1 deletion lib/UdoitUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class UdoitUtils
public static $canvas_secret_key;
public static $canvas_base_url;
public static $curl_ssl_verify;
public static $canvas_enforce_scopes;
public static $canvas_scopes;
public static $regex = [
'@youtube\.com/embed/([^"\& ]+)@i',
'@youtube\.com/v/([^"\& ]+)@i',
Expand All @@ -50,14 +52,16 @@ public static function instance()
return self::$instance;
}

public static function setupOauth($id, $key, $uri, $consumer_key, $secret, $curl_ssl_verify = true)
public static function setupOauth($id, $key, $uri, $consumer_key, $secret, $curl_ssl_verify = true, $enforce_scopes = false, $scopes = [])
{
self::$canvas_oauth_id = $id;
self::$canvas_oauth_key = $key;
self::$canvas_oauth_uri = $uri;
self::$canvas_consumer_key = $consumer_key;
self::$canvas_secret_key = $secret;
self::$curl_ssl_verify = $curl_ssl_verify;
self::$canvas_enforce_scopes = $enforce_scopes;
self::$canvas_scopes = $scopes;
}

public function getYouTubeId($link_url)
Expand Down Expand Up @@ -221,6 +225,13 @@ public function authorizeNewApiKey($base_url, $code)
'code' => $code,
];

if (true === self::$canvas_enforce_scopes) {
// if we are enforcing scopes we need to take our predefined scope array
// and implode it to be a long string with spaces between each scope
// and then URL encode it.
$post_data['scope'] = urlencode(implode(" ", self::$canvas_scopes));
}

return $this->curlOauthToken($base_url, $post_data);
}

Expand Down Expand Up @@ -283,6 +294,7 @@ public function sortReportGroups($report_groups)
foreach ($report_groups as $rg) {
if (!array_key_exists($rg->title, $ordered_report_groups)) {
$logger->addWarning("{$rg->title} is an unknown report title, it will be omitted from the report.");
$logger->addInfo("Contents of the unknown report:".print_r($rg, true));
} else {
// place the known titles at the correct index
$ordered_report_groups[$rg->title] = $rg;
Expand Down
Loading

0 comments on commit ae8fbd9

Please sign in to comment.