Commit Graph

27 Commits

Author SHA1 Message Date
Jacob Burroughs 3b7130c161 Remove a lot of settings
[ignore-stage-results=Flakey Spec Catcher]

refs AE-551

Change-Id: If7b5191c20cfadc438cdc2bc8b489eb2806582fe
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/334831
Reviewed-by: Isaac Moore <isaac.moore@instructure.com>
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
QA-Review: Jacob Burroughs <jburroughs@instructure.com>
Product-Review: Jacob Burroughs <jburroughs@instructure.com>
2024-01-09 21:32:17 +00:00
Jacob Burroughs 7dcc507d0a Rubocop for ruby 3.1
[skip-stages=Flakey]

Change-Id: I6abefdfa9fed6dd4525c8786e93efa548b3710f2
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/319603
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Isaac Moore <isaac.moore@instructure.com>
QA-Review: Jacob Burroughs <jburroughs@instructure.com>
Product-Review: Jacob Burroughs <jburroughs@instructure.com>
Build-Review: Jacob Burroughs <jburroughs@instructure.com>
Migration-Review: Jacob Burroughs <jburroughs@instructure.com>
2023-06-06 16:44:26 +00:00
Isaac Moore d6584b490a Remove unnecessary require statements
closes AE-30

flag=none

test plan:
- verify Canvas boots in CD
- verify no influx of new errors in CD

[fsc-timeout=30]

Change-Id: Ifa04bebe1b09f01c6d3b8b2d8f3bb424759730f5
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/308067
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Jacob Burroughs <jburroughs@instructure.com>
QA-Review: Isaac Moore <isaac.moore@instructure.com>
Product-Review: Isaac Moore <isaac.moore@instructure.com>
Build-Review: James Butters <jbutters@instructure.com>
2023-01-04 21:38:21 +00:00
Cody Cutrer c2cba46851 RuboCop: Style/StringLiterals, Style/StringLiteralsInInterpolation
[skip-stages=Flakey]

auto-corrected

Change-Id: I4a0145abfd50f126669b20f3deaeae8377bac24d
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/279535
Tested-by: Cody Cutrer <cody@instructure.com>
QA-Review: Cody Cutrer <cody@instructure.com>
Product-Review: Cody Cutrer <cody@instructure.com>
Migration-Review: Cody Cutrer <cody@instructure.com>
Reviewed-by: Jacob Burroughs <jburroughs@instructure.com>
2021-11-25 14:03:06 +00:00
Cody Cutrer 4d43809cae RuboCop: Style/PercentLiteralDelimiters
[skip-stages=Flakey]

auto-corrected, with a post-review looking for multiline strings
to convert to heredocs

Change-Id: I7f7afb11edd63415cde10866822dd2ac5ba0d8be
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/278669
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Jacob Burroughs <jburroughs@instructure.com>
QA-Review: Cody Cutrer <cody@instructure.com>
Product-Review: Cody Cutrer <cody@instructure.com>
Migration-Review: Cody Cutrer <cody@instructure.com>
2021-11-18 23:05:50 +00:00
Cody Cutrer c2f9a66192 RuboCop: Style/RescueStandardError
[skip-stages=Flakey]

auto-corrected

Change-Id: I55e4217ad864f0c0422ba70f4b2789c0c9aee68b
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/278620
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Jacob Burroughs <jburroughs@instructure.com>
QA-Review: Cody Cutrer <cody@instructure.com>
Product-Review: Cody Cutrer <cody@instructure.com>
2021-11-18 01:28:36 +00:00
Cody Cutrer cf213ee24b RuboCop: Style/RedundantFreeze
[skip-stages=Flakey]

auto-corrected

Change-Id: Id1b8bafdd744219a4797e6e1ba5891cd7ce4bccd
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/277888
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Simon Williams <simon@instructure.com>
QA-Review: Cody Cutrer <cody@instructure.com>
Product-Review: Cody Cutrer <cody@instructure.com>
2021-11-11 02:06:47 +00:00
Cody Cutrer 20533e467f RuboCop: Lint/NoReturnInBeginEndBlocks
all manual

Change-Id: I55146b72afcf57838123736cb91976ee8649357b
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/277555
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Simon Williams <simon@instructure.com>
QA-Review: Cody Cutrer <cody@instructure.com>
Product-Review: Cody Cutrer <cody@instructure.com>
2021-11-08 15:50:27 +00:00
Ethan Vizitei 5f4bf2e8ea Inflect OAuth2 for Zeitwerk
refs FOO-2476
flag=none

TEST PLAN:
  1) specs pass

Change-Id: Ic5b67c658913a31dd0a6ac25a8813397745e6c2b
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/275754
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Jacob Burroughs <jburroughs@instructure.com>
QA-Review: Ethan Vizitei <evizitei@instructure.com>
Product-Review: Ethan Vizitei <evizitei@instructure.com>
2021-10-13 20:25:00 +00:00
Ethan Vizitei bb3453ed0f Inflect "IMS" consistently
closes FOO-2467
flag=none
[skip-stages=Flakey]

Since canvas and it's plugins
share an autoloader, they need
to make consistent choices about how to inflect
the same acronyms

TEST PLAN:
  1) tests pass

Change-Id: Icb133f33ed3e719b616e42e497bac1dc65bd9370
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/275496
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Cody Cutrer <cody@instructure.com>
Reviewed-by: Jacob Burroughs <jburroughs@instructure.com>
QA-Review: Ethan Vizitei <evizitei@instructure.com>
Product-Review: Ethan Vizitei <evizitei@instructure.com>
2021-10-08 20:11:15 +00:00
Cody Cutrer c65d57737a RuboCop: Layout lib
Change-Id: I0655d9a9d750f2debd6378b03d8ddc1403ebc31b
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/274158
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Simon Williams <simon@instructure.com>
QA-Review: Cody Cutrer <cody@instructure.com>
Product-Review: Cody Cutrer <cody@instructure.com>
2021-09-22 20:01:52 +00:00
Cody Cutrer 06763dd519 add # frozen_string_literal: true for lib
Change-Id: I59b751cac52367a89e03f572477f0cf1d607b405
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/251155
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Simon Williams <simon@instructure.com>
QA-Review: Cody Cutrer <cody@instructure.com>
Product-Review: Cody Cutrer <cody@instructure.com>
2020-10-27 20:49:50 +00:00
Dan McCallum 0ec6447118 Refactor NRPS v2 access token validations to shared concern
- Remove `Lti::Ims::NamesAndRolesController`'s inclusion of
  `Lti::Ims::AccessTokenHelper` in favor of a recently
  introduced concern: `Lti::Ims::Concerns::AdvantageServices`. This
  includes a reimplementation of `Lti::Oauth2::AccessToken` as
  `Lti::Ims::Concerns::AdvantageServices::AccessToken`, which has
  many of the same capabilities, but none of the LTI 2 baggage.
  - Most low-level JWT validation is delegated to `Canvas::Security`
  and `Canvas::Security::JwtValidator`. The former provides symmetry
  with `Canvas::Oauth::ClientCredentialsProvider` eso w/r/t using
  Canvas encryption keys as signing keys. The latter also aligns well
  with that same provider, though its `jti` handling may need to
  be relaxed in the future.
  - ***This commit intentionally avoids extensive modifications to
  `Lti::Ims::Concerns::GradebookServices` and related controllers. The
  expectation is that `GradebookServices` should just include
  `AdvantageServices` and could thus eliminate many of its methods.
  But this has been deferred, partly to keep commit size down and
  partly to avoid unilateral contract changes to dependent controllers.
  - Most of this commit, then, is really just suffling tests around
  from `names_and_roles_controller_spec.rb` into
  `advantage_services_shared_examples.rb` so other LTI Advantage
  controllers can integrate the same validations.
  `advantage_services_shared_context.rb` expresses the data setup
  needs of `advantage_services_shared_examples.rb`, along with a few
  helper methods.
  - Eliminates custom tool/account chain walking in
  `NamesAndRolesController` in favor of
  `ContextExternalTool#all_tools_for`. Latter's use is in
  `AdvantageServices`, so should be readily reusable from other
  LTI Advantage controllers.

Closes LTIA-28

Test Plan

  * This is largely a technical refactoring so testing should focus
  primarily on regression checks against NRPS v2 callbacks, especially
  to ensure they still require valid access tokens issues via
  client credentials.
  * Functional differences are limited to newly added validation of
  JWT claims embedded in those access tokens. These are difficult to
  test since Canvas is responsible for generating the tokens. So
  either Canvas code needs to be temporarily modified to generate
  variously incorrect access tokens, or an out of band tool needs to
  be implemented to generate such tokens. Claims to be validated:

  1. `sub` - must be an active developer key's global ID, e.g.
  "10000000000020"
  2. `aud` - must be the OAuth2 access token issuance URL, i.e.
  `<host>/login/oauth2/token`
  3. `iat` - must be in the past
  4. `exp` - must be in the future
  5. `jti` - must be present
  6. `scopes` - must include
  https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly
  7. <signature> - Must use `HS256` algorithm and use the Canvas
  encryption key as the signing secret.

Change-Id: I14b431e11180f06e9edd4f5dfd3b04ed931ea73e
Reviewed-on: https://gerrit.instructure.com/167361
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-16 16:33:58 +00:00
Dan McCallum b87f7b743b 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-16 16:26:07 +00:00
Marc Phillips efdba8d77c Add client_credentials grant_type
Oauth2.0 client_credentials grant_type is added as a means
to support LTI Advantage services. Will accept only the
client_assertion_type of jwt-bearer and returns a JWS as
the access token. LTI services using the jws will be able to
authenticate, but other api endpoints will fail when using
this jwt.

closes PLAT-3659

Test Plan:
 - Create an oauth 2.0 request using a jwt signed by a
   developer key
 - Request should be validated and returns a jwt with
   the correct scopes

Change-Id: I786b71e39f8d3c2c9c71aa3eff4ea490f6d56285
Reviewed-on: https://gerrit.instructure.com/161245
Tested-by: Jenkins
QA-Review: Weston Dransfield <wdransfield@instructure.com>
Reviewed-by: Weston Dransfield <wdransfield@instructure.com>
Product-Review: Marc Alan Phillips <mphillips@instructure.com>
2018-09-10 17:07:05 +00:00
Simon Williams 87a5b6032f allow creating developer keys that only work against test clusters
Some production installations have a beta or test environment that is
refreshed with data from production, and is used as a place to test
integrations or new features. In that case, you may want to create
a developer key that only works against this test instance, which has
traditionally meant making the following tradeoff:
- Create the key in the test instance directly, which means it will be
  removed the next time the data is refreshed
- Create the key in production, which means the key works against the
  production instance as well

This new functionality allows the best of both worlds: create the key in
production for persistance, but only allow it to function against a test
cluster.

To enable test cluster functionality, you need a plugin that overrides
`ApplicationController.test_cluster?` to return appropriately for the
environment.

To see the functionality, you need to set:
  `Setting.set("dev_key_test_cluster_checks_enabled", true)`

closes PLAT-3392
[ci no-db-snapshot]

test plan:
- First ensure that all existing developer key functionality works and
no new functionality appears without any action taken
- Then set Setting.set("dev_key_test_cluster_checks_enabled", true), you
should see the new option available in the new dev key UI
- Create a key with and without the new option checked. Access tokens
from the key without it check should still work normally. Tokens from
the key with it checked should not work
- Now manually override `ApplicationController.test_cluster?` to be
true.
- Tokens from both keys should now work

Change-Id: I5bbb46782d19c26a7b703834aaa507b0cb10039a
Reviewed-on: https://gerrit.instructure.com/153035
Reviewed-by: Cody Cutrer <cody@instructure.com>
Reviewed-by: Weston Dransfield <wdransfield@instructure.com>
Tested-by: Jenkins
QA-Review: August Thornton <august@instructure.com>
Product-Review: Karl Lloyd <karl@instructure.com>
2018-06-29 15:50:08 +00:00
Nathan Mills 8486eda208 add jwt launches support for lti 2.1
refs PLAT-2649

test plan:
you can do an LTI2 JWT launch for a tool that suports the
correct security profile

Change-Id: I31a06243a171aaabe28552b7ee3866e807a86c30
Reviewed-on: https://gerrit.instructure.com/113018
Tested-by: Jenkins
Reviewed-by: Andrew Butterfield <abutterfield@instructure.com>
QA-Review: August Thornton <august@instructure.com>
Product-Review: Nathan Mills <nathanm@instructure.com>
2017-07-07 15:34:43 +00:00
Andrew Butterfield 119251f6e0 Persist LTI 2 registration url during registration
fixes PLAT-2633

Test plan:
* Install an LTI 2 in a course
* Export that course
* Ensure that the registration url is persisted in the imscc file

Change-Id: Ifc3b1ab9b7aeed3985bcddc7ae709e69215eb051
Reviewed-on: https://gerrit.instructure.com/117314
Reviewed-by: Weston Dransfield <wdransfield@instructure.com>
Tested-by: Jenkins
Reviewed-by: Nathan Mills <nathanm@instructure.com>
QA-Review: August Thornton <august@instructure.com>
Product-Review: Andrew Butterfield <abutterfield@instructure.com>
2017-07-05 17:17:43 +00:00
wdransfield af3263b37b Globalize attachment/tool proxy lookup for LTI submission service
Closes PLAT-2680

Test Plan:
- As a student submit to an assignment associted with an
  LTI 2 tool.
- Using the LTI 2 submission service retrieve the submission
- Request the submission attachment using the value of
  the `url` string provided in the submission JSON payload (
  This will require retrieveing and using a JWT access token).
- Veriy that the attachment is downloaded.
- From a different shard attempt to use the same URL to download
  the attachment.
- Verify the attachment is downloaded.

Change-Id: Ia60cd3dfccdea835ead109ba4b7d6fa2147b3a71
Reviewed-on: https://gerrit.instructure.com/116093
Tested-by: Jenkins
Reviewed-by: Nathan Mills <nathanm@instructure.com>
QA-Review: August Thornton <august@instructure.com>
Product-Review: Weston Dransfield <wdransfield@instructure.com>
2017-06-29 13:18:48 +00:00
Landon Wilkins d198a52517 da licença part 31
add consistent license headers to all source files
(ruby, coffeescript, javascript)

except for vendor files

Change-Id: I7b353d1ec31c36c0b12df7feb09fff743dc8a985
Reviewed-on: https://gerrit.instructure.com/110056
Tested-by: Jenkins
Reviewed-by: Jon Jensen <jon@instructure.com>
Product-Review: Jon Jensen <jon@instructure.com>
QA-Review: Jon Jensen <jon@instructure.com>
2017-04-27 21:59:58 +00:00
Nathan Mills 236fc456ac allow reg_key to be used for lti2 registration
fixes PLAT-2422

test plan:

- use the oauth2 flow to register a tool proxy using just
the reg_key and reg_password

-it should work

-the developer credentials should still work

-the legacy oauth1 should still work

Change-Id: I74f754c06f0d4a46158588f39bee75321fad3a3d
Reviewed-on: https://gerrit.instructure.com/106623
Tested-by: Jenkins
Reviewed-by: Weston Dransfield <wdransfield@instructure.com>
QA-Review: August Thornton <august@instructure.com>
Product-Review: Nathan Mills <nathanm@instructure.com>
2017-03-29 16:06:17 +00:00
Nathan Mills 7aec3da945 lti2 submission attachment download endpoint
fixes PLAT-2354

test plan:
-create a submission with an attachment on an assignment that has
the originality detection tool associated
-hit the lti2 endpoint to get the submission json
-from the submission json get the attachment url
-use the lti2 credentials with the attachment url to download
the attachment
-it should download the attachment
-try the following, they should all return 401:
-try to download the attachment without the lti2 credentials
-try to download the attachment using a canvas session
-try to monkey with the url to download a different attachment you
shouldn't have access to, i.e. on a different assignment

Change-Id: Ib38bbdbe9a1a649826a6bc98dd0d19b71a32635e
Reviewed-on: https://gerrit.instructure.com/104989
Tested-by: Jenkins
Reviewed-by: Michael Brewer-Davis <mbd@instructure.com>
Reviewed-by: Andrew Butterfield <abutterfield@instructure.com>
QA-Review: August Thornton <august@instructure.com>
Product-Review: Nathan Mills <nathanm@instructure.com>
2017-03-21 01:48:19 +00:00
Nathan Mills 03aa58570e tool proxy custom tcp and tp authorization code
fixes PLAT-2323

test plan:

-attempt to register a TP using the authorization code workflow
-it should let you create a tool proxy

-attempt to register a TP using the client credentials JWT workflow
-it should give you a 401

-attempt to register using a custom Tool Consumer Profile
-it should let you

there is a problem with using developer keys to register tools
since we aren't using a onetime token it no longer requires a admin
to kick off the process to install the tool.  We will need to do
something to address this, and ensure they have permission to install
in this context

Change-Id: I95ed14a8f818f02dab8340dfde3cc6327c06c793
Reviewed-on: https://gerrit.instructure.com/103267
QA-Review: August Thornton <august@instructure.com>
Reviewed-by: Weston Dransfield <wdransfield@instructure.com>
Reviewed-by: Andrew Butterfield <abutterfield@instructure.com>
Product-Review: Nathan Mills <nathanm@instructure.com>
Tested-by: Jenkins
2017-03-02 16:58:57 +00:00
wdransfield 3b860bc9c3 New LTI2 Tool Registration
Fixes PLAT-2226

Test Plan:
- Attempt to retrieve a AuthorizationJWT following the
  documentation in authorization_controller. Use a valid
  Canvas dev key global id as the 'sub' claim.
- Verify an AuthorizationJWT is returned
- Verify the 'sub' claim of the AuthorizationJWT
  is set to the canvas developer key used in the
  original request
- Register an LTI2 tool, but do the following in the
  tool proxy create post request:
       * Do not send any OAuth headers (even reg_key/password)
       * Send the AuthorizationJWT as an authorization header like this:
       	 Authorization Bearer <AuthorizationJWT>
- Verify the tool proxy is regeistered.
- Verify registering a tool with reg_key/password still works

Change-Id: I4266c672ad0cda7b2300de882c856c32b31be61e
Reviewed-on: https://gerrit.instructure.com/101783
Reviewed-by: Nathan Mills <nathanm@instructure.com>
Tested-by: Jenkins
QA-Review: August Thornton <august@instructure.com>
Product-Review: Weston Dransfield <wdransfield@instructure.com>
2017-02-14 22:02:04 +00:00
Nathan Mills adf9ff3dac remove iss and kid claims
fixes PLAT-2209

test plan:

you don't need the iss or kid claims anymore to get an
LTI2 access token

Change-Id: I12964994d089ee599adcdb035a6bf07dd644b696
Reviewed-on: https://gerrit.instructure.com/101432
Tested-by: Jenkins
QA-Review: August Thornton <august@instructure.com>
Reviewed-by: Weston Dransfield <wdransfield@instructure.com>
Product-Review: Nathan Mills <nathanm@instructure.com>
2017-02-10 01:43:36 +00:00
Nathan Mills 85673f081f create jwt access_tokens for LTI2 services
fixes: PLAT-2127

test plan:
- Hit the LTI2 Auth Token endpoint to get an AccessToken
- The access token should be a canvas signed JWT

Change-Id: If09dfc658ecc40fc525b9c49d49110539573e657
Reviewed-on: https://gerrit.instructure.com/99946
Reviewed-by: Weston Dransfield <wdransfield@instructure.com>
Tested-by: Jenkins
QA-Review: August Thornton <august@instructure.com>
Product-Review: Nathan Mills <nathanm@instructure.com>
2017-01-27 20:20:00 +00:00
Nathan Mills cfdad0bf42 endpoint to exchange lti2 jwt for access token
fixes PLAT-1966

test plan:

create JWT folowing the documentation
you should get an access token

Change-Id: I5e159fb4fd4e40174a3f8712013cd43b3582ee0e
Reviewed-on: https://gerrit.instructure.com/99608
QA-Review: August Thornton <august@instructure.com>
Tested-by: Jenkins
Reviewed-by: Weston Dransfield <wdransfield@instructure.com>
Product-Review: Nathan Mills <nathanm@instructure.com>
2017-01-24 22:03:52 +00:00