## Description
This PR enables Open EdX to act as an LTI 1.3 tool (formerly k…nown as "tool provider") for content managed by Content Libraries and backed up by blockstore. It allows instructors and staff users to embed content within other LMS and platforms, for example, Canvas.
The support for LTI features is limited to a subset of use-cases, as follows:
1. Tool setup and configuration through administration interfaces.
2. Retrieval and rendering of blockstore-based content (XBlocks) in a runtime suitable for LTI resource link launches. That is IFrames within the platform.
3. Send grades associated with content, i.e. assessments, to the platform gradebooks.
Once merged, Open edX will be capable of serving any content managed by Content Libraries to an LTI-supporting platform that was previously configured and authorized.
This PR's technical approach and decision are available in the [ADR for LTI providers in content libraries](https://github.com/edx/edx-platform/pull/27089).
**JIRA:** [SE-4257](https://tasks.opencraft.com/browse/SE-4257)
**Reviewers**:
- [X] @arbrandes
- [ ] @marcotuts
## Supporting information
[ADR` for LTI providers in content libraries](https://github.com/edx/edx-platform/pull/27089).
## Concerns
There are two. I believe they don't block the current implementation. But, we might need to follow-up later to accommodate future use cases. They are not blockers to those future use cases AFAICT.
First, grade syncing assumes a 1:1 relationship between XBlocks and grade lineitem. Hence, the logic ignores the potential tree relationship of XBlocks and how the tree should be mapped to a single LTI launch. Lack of a deeper understanding of this relationship within content libraries and the whole XBlock use-cases is the main reason for the current approach. Meaning, I understood users of LTI want to expose individual blocks hosting one assessment. This conclusion was the result of my own tests within Content Libraries.
Second, since each assessment can only hold one score, grade syncing is done "synchronously" rather than carried out by celery. Throughout my tests, the grade update event seemed to fail synchronously if the LTI platform was unavailable and it won't block retries. My reasoning is, this is a better user experience than keeping a work log of grades to sync and simplifies the implementation.
## Testing instructions
These are steps to manually test the changes in development environments of openedx as the tool and canvas as the platform.
### 1. Setup devstacks
1. Deploy an instance of [edx-platform devstack](https://github.com/edx/devstack#id8).
2. Deploy an instance of [canvas' development stack](https://github.com/instructure/canvas-lms/wiki/Quick-Start#automated-setup).
Some notes:
- Use their automated setup.
- Make sure to enable ``dory``, which will offer a local DNS resolver and HTTP proxy to route requests to ``canvas.docker``.
3. Deploy an instance of [edx/blockstore](https://github.com/edx/blockstore/#using-with-docker-devstack).
4. Follow the steps at [frontend-app-library-authoring](https://github.com/edx/frontend-app-library-authoring/).
At the end you should have a development instance of Canvas and Open edX, with Content Libraries V2 enabled, in your workstation.
You need to allow routing from edX studio container to the Canvas container. One straightforward way to achieve that is:
1. Find the host IP in the docker bridge of your workstation:
```
% ip -br addr show docker0
docker0 UP 1.2.3.4/16 fe80::42:39ff:fec3:140c/64
%
```
In the above the IP is `1.2.3.4`.
2. Overwrite `/etc/hosts in the edX studio container:
```
% cd ~/src/edx-devstack
% docker-compose exec studio bash
root@studio:/edx/app/edx_ansible/edx_ansible/docker/plays# echo 1.2.3.4 canvas.docker >> /etc/hosts
root@studio:/edx/app/edx_ansible/edx_ansible/docker/plays# ^D
%
```
This will allow the LTI view to contact the Canvas JWK endpoints to retrieve keys during the LTI launch steps.
3. Finally, disable DJDT in Studio. I personally disabled here: https://github.com/open-craft/edx-platform/blob/master/cms/envs/devstack.py#L99
This is required for the `launch` responses rendering XBlocks to not get tainted by DJDT.
### 2. Setup Canvas
Now, go to `http://canvas.docker/`:
1. Login with the credentials used during Canvas devstack setup.
1. In the left toolbar, click on "Admin" to open the administration panel.
1. Select the the user you created during Canvas devstack setup (do not select "Site Admin").
1. On the menu on the left, click "Developer Keys".
1. Clik on the "+ Developer Key" button, select "+ LTI".
1. On the "method" drop-down, select "Paste JSON".
1. Paste the following JSON:
```
{
"title": "edX Content Library",
"scopes": [],
"extensions": [
{
"platform": "canvas.instructure.com",
"settings": {
"platform": "canvas.instructure.com",
"placements": [
{
"placement": "assignment_selection",
"message_type": "LtiDeepLinkingRequest"
}
]
},
"privacy_level": "anonymous"
}
],
"public_jwk": {},
"description": "edX Content Library",
"custom_fields": {},
"public_jwk_url": "http://127.0.0.1:18010/api/libraries/v2/lti/1.3/pub/jwk/",
"target_link_uri": "http://127.0.0.1:18010/api/libraries/v2/lti/1.3/launch/",
"oidc_initiation_url": "http://127.0.0.1:18010/api/libraries/v2/lti/1.3/login/"
}
```
1. Give the key a name, e.g. "edx tool".
1. On "Redirect URIs" field, add `http://127.0.0.1:18010/api/libraries/v2/lti/1.3/launch/`.
1. Enable the key, switching from `OFF` to `ON`.
1. Copy the "client id" specified here:
![Client ID Screenshot](https://user-images.githubusercontent.com/291493/115919353-410f2e00-a468-11eb-9e32-c7a93cde6313.png)
1. On the left menu, go to "Settings"
1. Go to the "Apps" tab.
1. Click on the "+ App" button.
1. On "Configuration Type" select "By Client ID".
1. Enter the client id you copied above.
1. Copy the "deployment ids":
![Deployment ID Screenshot](https://raw.githubusercontent.com/dmitry-viskov/repos-assets/master/pylti1p3/examples/canvas-lms/004.png)
### 3. Setup edX.
1. [Generate a key pair for the tool](https://gist.github.com/ygotthilf/baa58da5c3dd1f69fae9).
1. Go to `http://localhost:18010/admin/lti1p3_tool_config/`, login with `edx`.
1. Add a "ti 1.3 tool key".
1. Add the private key and public keys from above.
1. Add a "Lti 1.3 tool"
1. Use the following information:
```
"issuer": "https://canvas.instructure.com",
// from Canvas: Developer Keys -> value from Details column
"client_id": "<client id>",
// static URL
"auth_login_url": "http://canvas.docker/api/lti/authorize_redirect",
// static URL
"auth_token_url": "http://canvas.docker/login/oauth2/token",
// static URL to get Platform's public key
"key_set_url": "http://canvas.docker/api/lti/security/jwks",
// copy deployment ID from the Canvas created app (screenshot above)
"deployment_ids": ["<client id>"]
```
1. Select the key you created before for "Tool Key".
### 4. Create a library and consume assignment
1. Go to studio and create a library, use the steps described in [`frontend-app-libary-authoring` Devstack Installation](https://github.com/edx/frontend-app-library-authoring/). Library should be of type "Complex (beta)".
2. Click on "Edit" in one assignment. In the URL, copy the "block usage key". For example, if the URL is `http://localhost:3001/library/lib:jvdm:jvdm/blocks/lb:jvdm:jvdm:done:35bb60f2-6d99-4da9-b8cc-9f018671e4f5/` the usage key is `lb:jvdm:jvdm:done:35bb60f2-6d99-4da9-b8cc-9f018671e4f5`.
3. Create the LTI launch URL for this block: For example: `http://127.0.0.1:18010/api/libraries/v2/lti/1.3/launch/?id=lb:jvdm:jvdm:poll:ae5a61a8-f163-44b9-bee0-bddfc219ba33`.
4. Go to Canvas and create an "External Tool" assignment, by performing [this step](https://github.com/dmitry-viskov/pylti1.3/wiki/Configure-Canvas-as-LTI-1.3-Platform#-1) and then [this one](https://github.com/dmitry-viskov/pylti1.3/wiki/Configure-Canvas-as-LTI-1.3-Platform#-2).
### 5. Expected result
The assignment should be exposed in the Canvas UI
## Deadline
None.
## Other information
This change doesn't depend on changes anywhere else.
This change is *incomplete* and should not be merged to production branches, due to security issues on the new URL endpoints being introduced.
Finally, some blocks are not being rendered correctly on Canvas IFrame in some browsers. Experiment with different blocks to evaluate the change.
## OCIM Setting
**Settings**
```yaml
EDXAPP_FEATURES:
ENABLE_LIBRARY_AUTHORING_MICROFRONTEND: true
EDXAPP_LIBRARY_AUTHORING_MICROFRONTEND_URL: "https://library-authoring.{{ EDXAPP_LMS_BASE }}"
TRUSTED_DOMAINS:
- "library-authoring.{{ EDXAPP_LMS_BASE }}"
- "blockstore.{{ EDXAPP_LMS_BASE }}"
EDXAPP_ENABLE_CORS_HEADERS: yes
EDXAPP_ENABLE_CROSS_DOMAIN_CSRF_COOKIE: yes
EDXAPP_CORS_ORIGIN_WHITELIST: "{{ TRUSTED_DOMAINS }}"
EDXAPP_LOGIN_REDIRECT_WHITELIST: "{{ TRUSTED_DOMAINS }}"
EDXAPP_CSRF_TRUSTED_ORIGINS: "{{ TRUSTED_DOMAINS }}"
MFE_DEPLOY_STANDALONE_NGINX: yes
MFES:
- name: library-authoring
repo: frontend-app-library-authoring
env_extra:
STUDIO_BASE_URL: https://{{ EDXAPP_CMS_BASE }}
```