panda_pal 5.19.0 → 5.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -1,733 +1,182 @@
1
- # PandaPal
2
-
3
- ## LTI Configuration
4
- A standard Canvas LTI may have a configuration similar to the one below
1
+ # PandaPal.
2
+
3
+ PandaPal is a mountable Rails engine for building LTI tools (primarily for Canvas). It handles
4
+ launches, sessions that work inside `<iframe>`s, per-customer data separation, and installing the
5
+ tool into Canvas, so your app can focus on what the tool does.
6
+
7
+ ## Features.
8
+
9
+ Each feature lists its key APIs, so you can spot them without opening the page. Before you write a
10
+ helper of your own, check [before-you-build.md](docs/before-you-build.md).
11
+
12
+ - **LTI 1.3 launches:** OIDC login, `id_token` validation against the platform JWKS, trusted-issuer
13
+ checks, and a public JWKS endpoint. See [routing-and-launches.md](docs/routing-and-launches.md).
14
+ - Key APIs: `lti_nav`, `validate_launch!`, `current_session.launch_params`, `current_lti_platform`
15
+ - **LTI 1.0/1.1 launches:** OAuth 1.0a signature validation, plus Basic Outcomes
16
+ [grade passback](docs/grade-passback.md).
17
+ - Key APIs: `validate_launch!(version: :v1p0)`, `PandaPal::Jobs::GradePassbackJob`
18
+ - **Tool configuration:** LTI 1.3 JSON and LTI 1.0 XML configs, generated from your placements and
19
+ routes. See [configuration.md](docs/configuration.md).
20
+ - Key APIs: `PandaPal.lti_options`, `PandaPal.stage_placement`, `PandaPal.lti_custom_params`,
21
+ `PandaPal.lti_environments`
22
+ - **Deep Linking (LTI 1.3):** content-item builders and the signed response.
23
+ See [deep-linking.md](docs/deep-linking.md).
24
+ - Key APIs: `PandaPal::DeepLinkingHelpers`, `build_link_content_item`, `render_deep_linking_response`
25
+ - **Assignment and Grade Services (LTI 1.3):** post scores, and read line items and results.
26
+ See [ags.md](docs/ags.md).
27
+ - Key APIs: `PandaPal::AgsHelpers`, `ags_client.post_score`, `ags_client.line_items`,
28
+ `ags_client.results`, `PandaPal.lti_scopes`
29
+ - **Multiple tools per app:** each with its own Developer Key, placements, and custom params.
30
+ See [multiple-tools.md](docs/multiple-tools.md).
31
+ - Key APIs: `PandaPal.define_tool`, `lti_nav ..., tool:`, `current_session.lti_tool`,
32
+ `PandaPal::LtiRegistration`
33
+ - **Cookieless sessions:** database-backed sessions carried by signed panda tokens, with
34
+ single-use, IP-locked, expiring, and single-URL variants. See [sessions.md](docs/sessions.md).
35
+ - Key APIs: `current_session`, `current_session.session_key`, `redirect_with_session_to`,
36
+ `url_with_session`, `link_nonce`, `current_session.cache`
37
+ - **Multi-tenancy:** one Postgres schema per Organization, with optional multi-database sharding.
38
+ See [multi-tenancy.md](docs/multi-tenancy.md).
39
+ - Key APIs: `org.switch_tenant`, `switch_tenant(org)`, `org.tenant_name`, `current_organization`,
40
+ `PandaPal::SpecHelper.with_multiple_shards`
41
+ - **Canvas installs:** create the Developer Key and install the tool from the console or a rake task,
42
+ with install callbacks. See [installation.md](docs/installation.md).
43
+ - Key APIs: `org.install_lti`, `after_lti_install`, `current_lti_install`, `panda_pal:org:new`,
44
+ `panda_pal:org:install`
45
+ - **Organizations and settings:** encrypted, schema-validated settings per Organization, plus Canvas
46
+ helpers. See [organizations.md](docs/organizations.md).
47
+ - Key APIs: `OrganizationExtension`, `settings_structure`, `define_setting`, `org.bearcat_client`,
48
+ `org.canvas_lti_launch_url`
49
+ - **Roles:** parsed LTI roles, Canvas role labels, inherited account admin roles, and CanCan ability
50
+ helpers. See [roles-and-abilities.md](docs/roles-and-abilities.md).
51
+ - Key APIs: `lti_roles`, `canvas_role_labels`, `canvas_account_role_labels(inherited:)`,
52
+ `canvas_site_admin?`, `PandaPal::Concerns::AbilityHelper`, `cache_on_session`
53
+ - **ActionCable:** session lookup on connections, tenant-scoped broadcasts, and CanCan in channels.
54
+ See [action-cable.md](docs/action-cable.md).
55
+ - Key APIs: `connection.panda_session`, `authorize!` in channels, `Apartment::Tenant.with_lazy_tenant`
56
+ - **Scheduled tasks:** per-Organization cron tasks. See [task-scheduling.md](docs/task-scheduling.md).
57
+ - Key APIs: `scheduled_task`, `settings[:task_schedules]`
58
+ - **Generated API calls:** console-minted URLs that run code for UAT. See
59
+ [api-calls.md](docs/api-calls.md).
60
+ - Key APIs: `org.create_api`, `PandaPal::ApiCall`
61
+ - **Frontend support:** `@inst_proserv/toolkit` handles tokens, API calls, and ActionCable in the
62
+ browser. See [frontend.md](docs/frontend.md).
63
+ - Key APIs: `APIBase`, `signURL`, `subscribe_channel`, `useModelSubscription`, `createLtiHistory`
64
+ - **Security defaults:** secure headers, iframe-friendly cookies, and session enforcement. See
65
+ [security.md](docs/security.md).
66
+ - Key APIs: `forbid_access_if_lacking_session`, `valid_session?`, `PandaPal::SecureHeaders.apply_defaults`
67
+
68
+ ## What PandaPal integrates with.
69
+
70
+ PandaPal sets these up for you when they're present:
71
+
72
+ | Tool | What PandaPal does |
73
+ | --- | --- |
74
+ | Canvas | Launches, trusted issuers (production, beta, test, and loadtest), beta/test environment forwarding, Developer Key and external tool installs, and handling of deploy-locked LTI Registrations. |
75
+ | [ros-apartment](https://github.com/rails-on-services/apartment) | Tenant schemas, a request elevator, tenant-scoped `Rails.cache`, `load_async` support, and multi-database sharding. |
76
+ | Sidekiq | Requires `ros-apartment-sidekiq`, so jobs run in the tenant that enqueued them. |
77
+ | sidekiq-scheduler | Per-Organization scheduled tasks, plus daily session cleanup. |
78
+ | Delayed Job | A plugin that runs jobs in the tenant that enqueued them. |
79
+ | ActionCable | Session lookup on connections, lazy per-tenant workers, and tenant-scoped broadcasts. See [action-cable.md](docs/action-cable.md). |
80
+ | CanCan | `authorize!` and `authorized?` in ActionCable channels, and rejected subscriptions on `AccessDenied`. |
81
+ | Bearcat and CanvasSync | Canvas API calls for installs and role lookups. CanvasSync's `canvas_sync_client` is used when it's defined. |
82
+ | secure_headers | A default, LTI-friendly header config. |
83
+ | [@inst_proserv/toolkit](docs/frontend.md) | The frontend half: reads the session from a `js-env` meta tag, sends it on API calls, signs `T-URL` tokens in the browser, and opens authenticated ActionCable connections that refresh their own tokens. |
84
+ | Rails console (IRB and Pry) | A prompt that shows the app, environment, and tenant, and auto-switching to the local Organization. |
85
+
86
+ ## Quick start.
87
+
88
+ ```ruby
89
+ # Gemfile
90
+ gem 'panda_pal'
91
+ gem 'bearcat' # Optional: installs into Canvas
92
+ gem 'ros-apartment-sidekiq' # Required if you use Sidekiq
93
+ ```
5
94
 
6
95
  ```ruby
7
96
  # config/initializers/lti.rb
8
97
  PandaPal.lti_options = { title: 'Teacher Reports' }
9
- PandaPal.lti_properties = {oauth_compliant: 'true'}
10
- # Environments reflect those used by Canvas
11
98
  PandaPal.lti_environments = { domain: ENV['PROD_DOMAIN'], beta_domain: ENV['BETA_DOMAIN'], test_domain: ENV['TEST_DOMAIN'] }
12
- PandaPal.lti_custom_params = { custom_canvas_role: '$Canvas.membership.roles' }
13
- # :user_navigation, :course_navigation for user context and course context endpoints respectively
14
- PandaPal::stage_navigation(:account_navigation, {
15
- enabled: true,
16
- # url: :account_index, # Optional if using lti_nav
17
- text: 'Teacher Reports',
18
- visibility: 'admins',
19
- })
20
- ```
21
-
22
- Configuration data for an installation can be set by creating a `PandaPal::Organization` record. Due to the nature of the data segregation, once created, the name of the organization should not be changed (and will raise a validation error).
23
-
24
- ### Canvas Installation
25
- As of version 5.5.0, LTIs can be installed into Canvas via the console:
26
- ```ruby
27
- org.install_lti(
28
- host: "https://your_lti.herokuapp.com",
29
- context: "account/self", # (Optional) Or "account/3", "course/1", etc
30
- exists: :error, # (Optional) Action to take if an LTI with the same Key already exists. Options are :error, :replace, :duplicate, :update
31
- version: "v1p3", # (Optional, default `v1p3`) LTI Version. Accepts `v1p0` or `v1p3`.
32
- dedicated_deployment: false, # (Optional) If true, the Organization will be updated to link to a single deployment rather then to the general LTI Key. (experimental, LTI 1.3 only)
33
- tool: :default, # (Optional) Which tool to install - see Multiple Tools below
34
- )
35
- ```
36
-
37
- ### Multiple Tools
38
- One app can publish more than one LTI tool - for example a teacher-facing tool and a separate, student-facing editor button with its own label and visibility. In Canvas each tool is its own Developer Key (LTI 1.3) or its own External Tool (LTI 1.0), with its own placements.
39
-
40
- Everything configured without a `tool:` belongs to the default tool, so single-tool apps don't change.
41
-
42
- ```ruby
43
- # config/initializers/lti.rb
44
- PandaPal.define_tool(:translate,
45
- title: 'Teacher Reports - Translate', # Merged over lti_options for this tool's config
46
- custom_params: { mode: 'translate' }, # Merged over lti_custom_params for this tool's config
47
- )
48
-
49
- PandaPal.stage_placement(:editor_button, tool: :translate, text: 'Translate', visibility: 'public')
50
- ```
51
-
52
- ```ruby
53
- # config/routes.rb
54
- lti_nav editor_button: 'translate#editor_button', tool: :translate # Served at <mount>/translate/editor_button
55
- ```
56
-
57
- - **Config:** each tool has its own config URL: `/v1p3/config?tool=translate` and `/v1p0/config?tool=translate`. The default tool's URLs are unchanged.
58
- - **Install:** `org.install_lti(host: ..., tool: :translate)`. On LTI 1.3 this creates (or reuses) the tool's own Developer Key and records it as a `PandaPal::LtiRegistration` on the Organization. The Organization's `key` keeps pointing at the default tool. On LTI 1.0 both tools share the Organization's consumer key and secret.
59
- - **Install hooks:** `before_lti_install` / `after_lti_install` callbacks can read `current_lti_install` (`tool`, `host`, `context`, `version`, `exists`, `dedicated_deployment`, and, in `after_lti_install`, the resulting Canvas `installation`). That lets you install a second tool alongside the default one:
60
- ```ruby
61
- # app/models/organization_extension.rb
62
- included do
63
- after_lti_install :install_translate_tool
64
- end
65
-
66
- def install_translate_tool
67
- install = current_lti_install
68
- return unless install.tool == PandaPal::DEFAULT_TOOL # the nested :translate install runs these callbacks too
69
- install_lti(**install.to_h.except(:installation), tool: :translate)
70
- end
71
- ```
72
- - **Launches:** `current_session.lti_tool` says which tool was launched. On LTI 1.3 the tool comes from the registration matching the launch's client ID (and deployment ID). On LTI 1.0 it comes from the signed `custom_panda_pal_tool` field that every non-default tool config includes. PandaPal routes each launch, and each deep-link redirect, using that tool's placements.
73
- - **Registering by hand:** if a tool's Developer Key was set up outside `install_lti`, register it yourself: `org.lti_registrations.create!(tool: 'translate', client_id: '10000000000123', deployment_id: nil)`. A registration with a `deployment_id` takes priority over one without.
74
-
75
- ### LTI 1.3 Configuration
76
- LTI 1.3 has some additional configuration steps required to setup an LTI:
77
-
78
- 1. If you're running Canvas locally, make sure the `config/redis.yml` and `config/dynamic_settings.yml` files exist in Canvas.
79
- 2. Also make sure `config/security.yml` is present and set `development.lti_iss` to `'http://localhost:3000'` (where 3000 is the port you're running Canvas on).
80
- 3. In prod, you'll need to generate a RSA Private Key for the LTI to use. You have several options:
81
- - Set the `LTI_PRIVATE_KEY` environment variable with the PEM-encoded private key content
82
- - Set the `LTI_PRIVATE_KEY_PATH` environment variable with the path to your private key file (relative to Rails.root or absolute path)
83
- - For encrypted private keys, set the `LTI_PRIVATE_KEY_PASSWORD` environment variable with the decryption password
84
- - Manually set `PandaPal.lti_private_key = OpenSSL::PKey::RSA.new(key)`
85
- 4. Make sure you have Redis installed and linked correctly
86
- 5. Your PandaPal::Organization's `key` should be `CLIENT_ID/DEPLOYMENT_ID` (which can be found in Canvas). If a Deployment ID is not given, the key should just be `CLIENT_ID`.
87
-
88
-
89
- To generate an encrypted private key using OpenSSL:
90
- ```bash
91
- # Generate new encrypted private key
92
- openssl genrsa -aes256 -out encrypted_private_key.pem 2048
93
-
94
- # Or encrypt existing private key
95
- openssl rsa -aes256 -in existing_private_key.pem -out encrypted_private_key.pem
96
- ```
97
-
98
- **Note**: The private key file path supports both relative paths (relative to Rails.root) and absolute paths.
99
-
100
- ### Launch URL property
101
- LTI Spec: `The launch_url contains the URL to which the LTI Launch is to be sent. The secure_launch_url is the URL to use if secure http is required. One of either the launch_url or the secure_launch_url must be specified.`
102
-
103
- Bridge is now validating that this launch URL exists and requires it per the LTI spec. As of PandaPal 3.1.0, you now have 6 options to use this.
104
- Use one of these 6 options in `PandaPal.lti_options` hash.
105
- 1. Use option `secure_launch_url: 'http://domain.com/path'` when a full static secure domain is needed.
106
- 2. Use option `secure_launch_path: '/path'` when you need the secure options and want the host to be dynamic and just need to provide a path.
107
- 3. Use option `launch_url: 'http://domain.com/path'` when a full static domain is needed.
108
- 4. Use option `launch_path: '/path'` when you don't need the secure options and want the host to be dynamic and just need to provide a path.
109
- 5. Leave this property off, and you will get the dynamic host with the root path ('http://appdomain.com/') by default.
110
- 6. If you really do not want this property use the option `launch_url: false` for it to be left off.
111
-
112
- ### Task Scheduling
113
- `PandaPal` includes an integration with `sidekiq-scheduler`. You can define tasks on an Organization class Stub like so:
114
- ```ruby
115
- # <your_app>/app/models/organization_extension.rb
116
- module OrganizationExtension
117
- extend ActiveSupport::Concern
118
-
119
- # Will invoke CanvasSyncStarterWorker.perform_async() according to the cron schedule
120
- scheduled_task '0 15 05 * * *', :identifier, worker: CanvasSyncStarterWorker
121
-
122
- # Will invoke the method 'organization_method' on the Organization
123
- scheduled_task '0 15 05 * * *', :organization_method_and_identifier
124
-
125
- # If you need to invoke the same method on multiple schedules
126
- scheduled_task '0 15 05 * * *', :identifier, worker: :organization_method
127
-
128
- # You can also use a block
129
- scheduled_task '0 15 05 * * *', :identifier do
130
- # Do Stuff
131
- end
132
-
133
- # You can use a Proc (called in the context of the Organization) to determine the schedule
134
- scheduled_task -> { settings[:cron] }, :identifier
135
-
136
- # You can specify a timezone. If a TZ is not coded and settings[:timezone] is present, it will be appended automatically
137
- scheduled_task '0 15 05 * * * America/Denver', :identifier, worker: :organization_method
138
-
139
- # Setting settings[:task_schedules][:identifier] will override the code cron schedule. Setting it to false will disable the Task
140
- # :identifer values _must_ be unique, but can be nil, in which case they will be determined by where (lineno etc) scheduled_task is called
141
- end
142
- ```
143
-
144
- ### Organization Attributes
145
- `id`: Primary Key
146
- `name`: Name of the organization. Used to on requests to select the tenant
147
- `key`: Key field from CanvasLMS
148
- `secret`: Secret field from CanvasLMS
149
- `canvas_account_id`: ID of the corresponding Canvas account.
150
- `settings`: Hash of settings for this Organization
151
- `salesforce_id`: ID of this organization in Salesforce
152
-
153
- XML for an installation can be generated by visiting `/lti/config` in your application.
154
-
155
- ### Generated API Methods
156
- It's common to need to manually trigger logic during UAT. PandaPal 5.6.0+ adds a feature to enable clients to do this themselves.
157
- This is done by a deployer accessing the console and creating a `PandaPal::ApiCall`. This can be done like so:
158
- ```ruby
159
- org.create_api(<<~RUBY, expiration: 30.days.from_now, uses: 10)
160
- arg1 = p[:some_param] # `p` is an in-scope variable that is a Hash of the params sent to the triggering request.
161
- PandaPal::Organization.current.trigger_canvas_sync()
162
- { foo: "bar" } # Will be returned to the client. Can be anything that is JSON serializable.
163
- RUBY
164
- # OR
165
- org.create_api(:symbol_of_method_on_organization, expiration: 30.days.from_now, uses: 10)
166
- ```
167
- This will return a URL like such: `/panda_pal/call_logic?some_param=TODO&token=JWT.TOKEN.IS.HERE` that can be either GET'd or POST'd.
168
- The URL generator will *attempt* to determine which params the logic accepts and insert them as `param=TODO` in the generated URL.
169
-
170
- When triggered, the return value of the code will be wrapped and sent to the client:
171
- ```json
172
- {
173
- "status": "ok",
174
- "uses_remaining": 9,
175
- "result": {
176
- "foo": "bar",
177
- }
99
+ PandaPal.lti_custom_params = {
100
+ custom_canvas_user_id: '$Canvas.user.id',
101
+ custom_canvas_role: '$Canvas.membership.roles',
178
102
  }
103
+ PandaPal.stage_placement(:account_navigation, text: 'Teacher Reports', visibility: 'admins')
179
104
  ```
180
105
 
181
- #### Revoking
182
- A Call URI may be revoked by deleting the `PandaPal::ApiCall` object. `uses:` and logic are stored in the DB (and can thus be altered), but `expiration:` is stored in the JWT and is thus immutable.
183
-
184
- ### Routing
185
-
186
- The following routes should be added to the routes.rb file of the implementing LTI. (substituting `account_navigation` with the other staged navigation routes, if necessary)
187
-
188
106
  ```ruby
189
107
  # config/routes.rb
190
108
  mount PandaPal::Engine, at: '/lti'
191
- root to: 'panda_pal/lti#launch'
192
-
193
- # Add Launch Endpoints:
194
- lti_nav account_navigation: 'accounts#launch', auto_launch: false # (LTI <1.3 Default)
195
- # -- OR --
196
- scope '/organizations/:organization_id' do
197
- lti_nav account_navigation: 'accounts#launch_landing', auto_launch: true # (LTI 1.3 Default)
198
- lti_nav account_navigation: 'accounts#launch_landing' # Automatically sets auto_launch to true because :organization_id is part of the path
199
- # ...
200
- end
201
- ```
202
109
 
203
- `auto_launch`: Setting to `true` will tell PandaPal to handle all of the launch details and session creation, and then pass off to
204
- the defined action. Setting it to `false` indicates that the defined action handles launch validation and setup itself (this has been the legacy approach).
205
- Because `auto_launch: false` is most similar to the previous behavior, it is the default for LTI 1.0/1.1 LTIs. For LTI 1.3 LTIs, `auto_launch: true` is the default. If not specified and `:organization_id` is detected in the Route Path, `auto_launch` will be set to `true`
110
+ # The route an account_navigation launch lands on
111
+ lti_nav account_navigation: 'accounts#launch'
206
112
 
207
- ## Deep Linking (LTI 1.3)
208
-
209
- PandaPal provides comprehensive support for LTI 1.3 Deep Linking, allowing tools to be configured for content selection that can be placed directly into Canvas courses.
210
-
211
- ### Quick Start
212
-
213
- 1. **Configure placements** in your LTI initializer:
214
- ```ruby
215
- # config/initializers/lti.rb
216
- PandaPal.stage_placement(:content_selection, {
217
- deep_linking: true,
218
- deep_link_redirect_url: :content_selection_url,
219
- text: 'Select Content',
220
- enabled: true
221
- })
222
- ```
223
-
224
- 2. **Include helpers** in your controller and build content items with **required `launch_type`**:
225
- ```ruby
226
- class ContentSelectionController < ApplicationController
227
- include PandaPal::DeepLinkingHelpers
228
-
229
- def create
230
- content_items = [
231
- build_link_content_item(
232
- url: panda_pal.v1p3_resource_link_request_url,
233
- title: 'Selected Content',
234
- custom: {
235
- content_id: params[:content_id],
236
- launch_type: :content_launch # REQUIRED for proper routing
237
- }
238
- )
239
- ]
240
- render_deep_linking_response(content_items)
241
- end
242
- end
243
- ```
244
-
245
- 3. **Add corresponding launch routes**:
246
- ```ruby
247
- # config/routes.rb
248
- lti_nav content_launch: 'content#launch'
249
- ```
250
-
251
- **⚠️ IMPORTANT**: All content items **MUST** include `launch_type` in the `custom` hash. This is used by the `resource_link_request` action to determine which route to redirect to when Canvas launches your content.
252
-
253
- **For complete configuration options, implementation examples, helper method documentation, and troubleshooting, see [DEEP_LINKING.md](DEEP_LINKING.md).**
254
-
255
- ## Assignment and Grade Services (LTI 1.3)
256
-
257
- PandaPal can post scores and read line items and results through LTI Advantage AGS.
258
-
259
- 1. **Request the scopes** in your LTI initializer (published in `/v1p3/config`):
260
- ```ruby
261
- # config/initializers/lti.rb
262
- PandaPal.lti_scopes = [PandaPal::LtiConstants::AgsScopes::SCORE]
263
- ```
264
-
265
- 2. **Post a score** from a controller serving the launched session:
266
- ```ruby
267
- class QuizController < ApplicationController
268
- include PandaPal::AgsHelpers
269
-
270
- def submit
271
- ags_client.post_score(score_given: 8, score_maximum: 10)
272
- end
273
- end
274
- ```
275
-
276
- **For scopes, line items, results, service tokens, background jobs and error handling, see [AGS.md](AGS.md).**
277
-
278
- ## Implementating data segregation
279
- This engine uses Apartment to keep data segregated between installations of the implementing LTI tool.
280
- By default, it does this by inspecting the path of the request, and matching URLs containing `orgs` or `organizations`,
281
- followed by the numeric ID of the organization (path such as `/organizations/123/accounts`).
282
- As such, all URLs should be built on top of a routing scope, such as the example below.
283
-
284
- ```ruby
285
113
  scope '/organizations/:organization_id' do
286
- resources :accounts, only: :index do
287
- collection do
288
- get 'teachers' => 'accounts#teachers'
289
- end
290
- end
291
- end
292
- ```
293
-
294
- This can be overriden by creating an initializer, and adding a custom elevator
295
-
296
- ```ruby
297
- Rails.application.config.middleware.delete 'Apartment::Elevators::Generic'
298
- Rails.application.config.middleware.use 'Apartment::Elevators::Generic', lambda { |request|
299
- if match = request.path.match(/\/(?:orgs|organizations)\/(\d+)/)
300
- PandaPal::Organization.find_by(id: match[1]).try(:name)
301
- end
302
- }
303
- ```
304
-
305
- It is also possible to switch tenants by implementing `around_action :switch_tenant` in the controller.
306
- However, it should be noted that calling `switch_tenant` must be used in an `around_action` hook (or given a block), and is only intended to be used for LTI launches, and not subsequent requests.
307
-
308
- ### Sharding
309
- PandaPal 5.12 added support for multi-DB sharding. If you wish to use this, add a `shard` column to the `PandaPal::Organization` model. The implementation should be fairly transparent and should behave as normal PandaPal/Apartment - the only difference is that tenants can be created on different DB shards.
310
-
311
- The list of available shards is computed from the Environment variables. On startup, the application looks for variables with the following formats:
312
- - `SHARD_DB_XYZ_URL`
313
- - `HEROKU_POSTGRESQL_XYZ_URL`
314
- (where `XYZ` is the shard identifier)
315
-
316
- When an `Organization` is created, it is assigned to a random shard (or if no shards were discovered, it is placed in the default database as normal). Alternatively, a specific shard can be specified: `PandaPal::Organization.new(shard: "shard_a")`. An `Organization` can be added to the primary shard with `shard: "default"`
317
-
318
- The `PandaPal::Organization` record is still created in the `public` schema of the default database, as per usual.
319
-
320
- ### Rake tasks and jobs
321
- **Delayed Job Support has been removed. This allows each project to assess it's need to handle background jobs.**
322
-
323
- The Apartment Gem makes it so background jobs need to be run within the current tenant. If using sidekiq, there is a gem that
324
- does this automatically called `apartment-sidkiq`. If Delayed Jobs, see how it's done in version 1 of PandaPal.
325
-
326
- For rake tasks, the current tenant will be set to `public`, and thus special handling must be taken when running the task or queueing jobs as part of it.
327
-
328
- One potential solution is to create a method with switches between tenants, and invoke the job for each individual organization as necessary.
329
-
330
- ```ruby
331
- # config/initializers/global_methods.rb
332
- def switch_tenant(tenant, &block)
333
- if block_given?
334
- Apartment::Tenant.switch(tenant, &block)
335
- else
336
- Apartment::Tenant.switch! tenant
337
- end
338
- end
339
- ```
340
114
 
341
- ```ruby
342
- # lib/tasks/lti_tasks.rake
343
- namespace :lti_tasks do
344
- desc 'some rake task'
345
- task some_rake_task: :environment do
346
- PandaPal::Organization.all.each do |org|
347
- org.switch_tenant do
348
- SomeJob.perform_later
349
- end
350
- end
351
- end
115
+ resources :reports
352
116
  end
353
117
  ```
354
118
 
355
- ## Controller helper methods
356
- Controllers will automatically have access to a number of methods that can be helpful to implement. A list with descriptions is provided below.
357
-
358
- * `validate_launch!` - should be used in any tool launch points, and will verify that the launch request received from Canvas is legitimate based on the saved key and secret.
359
- * `current_session` - PandaPal provides support to use database persisted sessions, rather than the standard rails variety, as these play better when the tool is launched in an `iframe`. The `session_key` attribute from the returned object needs to be served to the client at some point during the launch and during subsequent requests. The benefit from this is that sessions will exist entirely in the frame used to launch the tool, and will not be visible should the user be accessing the tool in other contexts simultaneously. See section below for more information about this method.
360
- * `current_organization` - Used to return the organization related to the currently selected tenant. See section below for more information about this method.
361
- * `session_changed?` - returns true if the session given by `current_session` has been modified during the request.
362
- * `save_session` - Saves the session given by `current_session` to the database.
363
-
364
- ## Sessions and `current_session`
365
- Because of the ever-increasing security around `<iframe>`s and cookies, a custom session implementation has been rolled into `PandaPal`.
366
- The implementation is mostly passive, but requires some changes and caveats as opposed to the native Rails implementation.
367
-
368
- The current session object can be accessed from any controller using `current_session`.
369
- Session data can be accessed and modified using `current_session[:key]`.
370
-
371
- Because cookies are unreliable, it becomes the responsibility of the LTI developer to ensure that a custom "cookie" is passed to the frontend with each response and returned to the backend with each request, otherwise the backend won't be able to access the current session. There are a few ways to do this.
372
-
373
- Generally, the method for passing the session "cookie" to the frontend looks something like:
374
- ```html
375
- <meta name="session_key" content="<%= current_session.session_key %>">
376
- ```
377
-
378
- ### Panda Token Format
379
- The `session_key` used to be a single value secret, but as of PandaPal 5.14, it is formatted as such (and referred to as a "Full-Key Panda Token"):
380
- `<Base64({ v: TOKEN FORMAT, o: ORG ID, s: SESSION ID, typ: "KEY" })>.<SESSION SECRET>`
381
-
382
- Additionally as of 5.14, "session_tokens" were dissolved into the Panda Token format (and now referred to as "Limited-Use Panda Tokens").
383
- These Limited-Use tokens are formatted as such:
384
- `<Base64({ v: TOKEN FORMAT, o: ORG ID, s: SESSION ID, typ: "X", ... })>.<HMAC(key: SESSION_SECRET, content: Base64(...))>`
385
-
386
- Currently, there is support for these `typ`es (also noted with any other params to be included in the B64 content):
387
- - `T-URL` `{ usig: HMAC(key: SESSION_SECRET, content: URL_PATH_AND_QUERY) }` - Use to authenticate `GET`ting a single URL.
388
- - May be generated on the client using `@inst_proserv/toolkit/panda_tokens`
389
- - `T-NONCE` `{ nnc: NONCE }` - Stores a single-use nonce in the Session. Can only be used once and will expire as soon as another `T-NONCE` token is created in another request.
390
- - `T-IP` `{ ip: 1.2.3.4 }` - Token restricted to use by a single IP address
391
- - `T-EXP` `{ exp: }` - Basic, time-limited token
392
-
393
- All Limited-Use tokens may also include these parameters:
394
- - `exp` - ISO8601 Expiration timestamp
395
-
396
- ### Passing the Session
397
- PandaPal looks for Session information in multiple places (in order of precedence):
398
- - `X-Panda-Token` Header
399
- - `Authorization: Bearer <PANDA TOKEN>` Header
400
- - `Authorization: token=<PANDA TOKEN>` Header (Deprecated; legacy-use only). Supports use of legacy or new-format
401
- - `panda_token` param
402
- - `session_key` param (Deprecated; legacy-use only). Supports use of legacy or new-format
403
- - `session_token` param (Deprecated; legacy-use only)
404
-
405
- Use of one of the supported HTTP Headers is preferred.
406
- `panda_token=<Full-Use Token>` param may be used for (non-`GET`) HTML `<form>`s.
407
- `panda_token=<Limited-Use Token>` param should be used for `GET` requests in-which you cannot control a Header.
408
-
409
- ### Redirecting and Multi-Page Applications
410
- Special instructions must be followed when using `link_to` or `redirect_to` to ensure that the Session is passed correctly:
411
-
412
- Add a `session_token:` (notice the use of _`_token`_ as opposed to _`_key`_!) parameter when using `link_to` or `redirect_to`:
413
- ```ruby
414
- link_to "Link Name", somewhere_else_path(panda_token: link_nonce)
415
- redirect_to somewhere_else_path(panda_token: link_nonce)
416
- ```
417
- You can also use the `redirect_with_session_to` helper, which will automatically the `panda_token:` param (and, to support legacy apps, an `organization_id:` param if needed):
418
- ```ruby
419
- redirect_with_session_to :somewhere_else_path, other_param: 1
420
- ```
421
-
422
- For each request (not each call), `link_nonce` Limited-Use Token.
423
- The default `typ`e is `T-NONCE`, but this can be controlled by passing a `type:` param, or by setting `link_nonce_type` in the controller, eg:
424
- ```ruby
425
- link_nonce_type "T-IP"
426
-
427
- def action
428
- redirect_to somewhere_else_path(panda_token: link_nonce(type: "T-IP"))
429
- end
430
- ```
431
-
432
- ### Persisting the session
433
- The session is automatically saved using an `after_action` callback. This callback is poised to run last, but if that causes issues
434
- (eg some other callback running afterwards and needing to modify the session), `skip_after_action :auto_save_session` can be used
435
- and a custom implementation can be supplemented.
436
-
437
- ### Session cleanup
438
- Over time, the sessions table will become bloated with sessions that are no longer active.
439
- If `sidekiq-scheduler` and `TaskScheduling` are enabled, cleanup will be performed automatically. Otherwise you should schedule, `rake panda_pal:clean_sessions` to run periodically (recommend weekly).
440
-
441
- ## Organizations and `current_organization`
442
- Similar to `current_session`, `current_organization` can be returned with a number of methods, shown below
443
-
444
- ## Rake Tasks
445
-
446
- ### `panda_pal:org:new`
447
- Opens `$EDITOR` and creates a new `Organization`. The opened editor is prefilled with all of the available Settings with optional settings pre-commented.
448
-
449
- ### `panda_pal:org:dev`
450
- Similar to `panda_pal:org:new`, but is somewhat preconfigured for development environments.
451
-
452
- You can create a `~/pandapalrc.yml` file to specify additional defaults:
453
- ```yml
454
- lti_host: http://localhost:5000
455
- settings:
456
- canvas:
457
- base_url: http://localhost:3000
458
- api_token: Uo5yckRQuPl96qVxgt2IVxDAa5oSUo1dqbb2dBVOnpId18aTmjSXdYkn4BN9JF2k
459
- ```
460
- NB: `settings` entries present in `~/pandapalrc.yml` that are not defined in the `settings_structure` will be ignored.
461
-
462
- ### `panda_pal:org:edit[name]`
463
- Similar to `panda_pal:org:new` but for editing an existing Organization.
464
-
465
- ### `panda_pal:org:install[orgname, http://url.of.lti]`
466
- Install the LTI into Canvas. Must specify the Organization and the host of the LTI
467
-
468
- ### `panda_pal:org:reinstall[orgname, http://url.of.lti]`
469
- Same as `panda_pal:org:install`, but first deletes an existing installation
470
-
471
- ## Security
472
-
473
- ### Tool Launches
474
- Any time a tool launch request is received, we should **always** validate that it originated from within Canvas and that it wasn't spoofed.
475
- The easiest way to do this is to call `validate_launch!` in any controller action that is expecting to receive launch requests.
476
-
477
- ### Validate active session is present
478
- It is easier to implement this if a purely javascript frontend is used (such as React).
479
- Any controller actions that are not designated as launch points, or externally available API endpoints (such as in the case where an endpoint should be exposed outside the context of a tool launch, and has its own authentication mechanisms in place) should implement a line similar to the one below in `application_controller.rb`
480
-
481
119
  ```ruby
482
120
  # app/controllers/application_controller.rb
483
- prepend_before_action :forbid_access_if_lacking_session
484
- ```
485
-
486
- This will render an unauthorized message any time a request is received and a valid session is not present.
487
- It can be overridden on a action-by-action basis by using `skip_before_action`.
488
-
489
- ```ruby
490
- skip_before_action :forbid_access_if_lacking_session, only: :launch
491
- ```
492
-
493
- ### Secure Headers
494
- PandaPal bundles the `secure_headers` Gem and provides a default config. The PandaPal default works for really basic apps, but you may need to provide
495
- a custom configuration. This can be done by creating a `secure_headers.rb` initializer in you App like so:
496
- ```ruby
497
- SecureHeaders::Configuration.default do |config|
498
- PandaPal::SecureHeaders.apply_defaults(config) # Optional, but recommended
499
- # ...
121
+ class ApplicationController < ActionController::Base
122
+ prepend_before_action :forbid_access_if_lacking_session
500
123
  end
501
- ```
502
-
503
- ## Validating settings in your LTI.
504
-
505
- You can specify a structure that you would like to have enforced for your settings.
506
-
507
- In your panda_pal initializer (i.e. `config/initializers/panda_pal.rb` or `config/initializers/lti.rb`)
508
-
509
- You can specify options that can include a structure for your settings. If specified, PandaPal will
510
- enforce this structure on any new / updated organizations.
511
-
512
- ```ruby
513
- PandaPal.lti_options = {
514
- title: 'LBS Gradebook',
515
- settings_structure: {
516
- allow_additional: true, # Allow additional properties that aren't included in the :properties Hash.
517
- allow_additional: { type: 'String' }, # You can also set :allow_additional to a settings specification that will be used to validate each additional setting
518
- validate: ->(value, spec, **kwargs) {
519
- # kwargs currently includes:
520
- # :errors => [An array to push errors to]
521
- # :path => [An array representation of the current path in the settings object]
522
-
523
- # To add errors, you may:
524
- # Push strings to the kwargs[:errors] Array:
525
- kwargs[:errors] << "Your error message at <path>" unless value < 10
526
- # Or return a string or string array:
527
- value.valid? ? nil : "Your error message at <path>" # <path> will be replaced with the actual path that the error occurred at
528
- },
529
- properties: {
530
- canvas_api_token: { type: 'String', required: true, },
531
- catalog: { # :validate, :allow_additional, :properties keys are all supported at this level as well
532
- type: 'Hash',
533
- required: false,
534
- validate: -> (*args) {},
535
- allow_additional: false,
536
- properties: {
537
124
 
538
- },
539
- }
540
- }
541
- },
542
- }
543
- ```
544
-
545
- #### Legacy Settings Structure:
546
- Here is an example options specification:
547
-
548
- ```ruby
549
- PandaPal.lti_options = {
550
- title: 'LBS Gradebook',
551
- settings_structure: YAML.load("
552
- canvas:
553
- is_required: true
554
- data_type: Hash
555
- api_token:
556
- is_required: true
557
- data_type: String
558
- base_url:
559
- is_required: true
560
- data_type: String
561
- reports:
562
- is_required: true
563
- data_type: Hash
564
- active_term_allowance:
565
- submissions_report_time_length:
566
- is_required: true
567
- data_type: ActiveSupport::Duration
568
- recheck_wait:
569
- data_type: ActiveSupport::Duration
570
- max_recheck_time:
571
- is_required: true
572
- ").deep_symbolize_keys
573
- }
574
- ```
575
- (This loads the structure in from YAML, but you can specify a hash directly if you prefer.)
576
-
577
- Each data attribute can have two children attributes to describe desired structure:
578
-
579
- is_required: If specified, and specified as true, the parent attribute is required in the settings hash.
580
-
581
- data_type: If specified, and a settings hash contains this attribute, attribute data type will be compared
582
- to the specified data type. If you are not sure how to derive your class data type, you can
583
- use `class` to determine data type. For example: 30.minutes.class.to_s => "ActiveSupport::Duration"
584
-
585
- ## Bridge vs Canvas
586
- As of 3.2.0, the LTI XML config can have subtle differences based on whether your platform is bridge, or canvas.
587
- This is determined by `PandaPal.lti_options[:platform]`.
588
- Set this to `platform: 'canvas.instructure.com'` (default)
589
- OR `platform: 'bridgeapp.com'`
590
-
591
- ## Development:
592
- ### Running Specs:
593
- Initialize the Specs DB:
594
- `cd spec/dummy; bundle exec rails db:drop; bundle exec rails db:create; RAILS_ENV=test rails db:migrate`
595
- Then `bundle exec rspec`
596
-
597
- ## Safari Support
598
- Safari is weird (it blocks cookies in `<iframe>`s unless the site has set a cookie _outside_ of an `<iframe>` first), and you'll run into
599
- issues with Rails-native Sessions and CSRF because of that.
600
-
601
- This means that safari will likely refuse to send info about your rails session
602
- back to the LTI, and the application will start up a new session each time the
603
- browser navigates. This likely means a new session each time the LTI launches.
604
-
605
- ### PandaPal 5
606
- It has been a constant struggle to force safari to store and allow
607
- access to a rails session while the application is embedded in Canvas.
608
-
609
- As of PandaPal 5, a session cookie is no longer required by panda_pal.
610
-
611
- See the Section on [Sessions and `current_session`](#sessions-and-current_session)
612
-
613
- You will want to watch out for a few scenarios:
614
- 1) Make sure you are using `redirect_with_session_to` if you need to redirect
615
- and have your PandaPal session_key persisted server side.
616
- 2) Use the `Authorization` header with `token={session_key}` to send your
617
- PandaPal session info into api calls.
618
- 3) If you use `link_to` and navigate in your LTI (apps that are not single page)
619
- make sure you include the `link_nonce` like so:
620
- ```ruby
621
- link_to "Link Name", somewhere_else_path(arg, session_token: link_nonce)
622
- ```
623
- NB: As of PandaPal 5.2.6, you can instead use
624
- ```ruby
625
- link_to "Name", url_with_session(:somewhere_else_path, arg, kwarg: 1)
626
- ```
627
-
628
- Persistent sessions have session_tokens as a way to safely communicate a session key in a way that is hopefully not too persistent in case it is logged somewhere.
629
- Options for communicating session_token -
630
- :nonce (default) - each nonce is good for exactly one communication with the backend server. Once the nonce is used, it is no longer valid.
631
- :fixed_ip - each session_token is good until it expires. It must be used from the same ip the LTI launched from.
632
- :expiring - this is the least secure. Each token is good until it expires.
633
-
634
- For :fixed_ip and :expiring tokens you can override the default expiration period of 15 minutes.
635
-
636
- See the following example of how to override the link_nonce_type and token expiration length.
637
-
638
- ```ruby
639
- class ApplicationController < ActionController::Base
640
- link_nonce_type :fixed_ip
641
- def session_expiration_period_minutes
642
- 120
125
+ # app/controllers/accounts_controller.rb
126
+ class AccountsController < ApplicationController
127
+ def launch
128
+ # PandaPal already validated the launch, started a session, and switched tenants
129
+ @user_id = current_session.canvas_user_id
130
+ @roles = current_session.canvas_role_labels
643
131
  end
644
- ...
645
132
  end
646
133
  ```
647
134
 
648
- ### Previous Safari Instructions
649
- Safari is weird and you'll potentially run into issues getting `POST` requests to properly validate CSRF if you don't do the following:
650
-
651
- - Make sure you have both a `launch` controller and your normal controllers. The `launch` controller should call `before_action :validate_launch!` and then redirect
652
- to your other controller.
653
- - Make sure your other controller calls `before_action :forbid_access_if_lacking_session`
654
-
655
- This will allow `PandaPal` to apply an iframe cookie fix that will allow CSRF validation to work.
656
-
657
- ## Migrating Major Versions
658
-
659
- ### Upgrading to version 3
660
-
661
- Before upgrading save existing settings somewhere safe in case you need to
662
- restore them for whatever reason.
663
-
664
- panda_pal v3 introduces an encrypted settings hash. This should provide more security in the case where a
665
- customer may have gained access to Organization information in the database. Settings cannot be decrypted
666
- without knowing the secret decryption key. For panda_pal, we are relying on the secret_key_base to be set
667
- (`typically in config/secrets.yml`), if that is not available we are falling back to directly use
668
- `ENV['SECRET_KEY_BASE']`. For production environments, `config/secrets.yml` should not have a plain-text secret,
669
- it should be referencing an ENV variable. Make sure your secret is not plain-text committed to a repository!
670
-
671
- The secret key is used to encrypt / decrypt the settings hash as necessary.
672
-
673
- Before upgrading to version 3, you should confirm that the secret key is set to a consistent value (if the
674
- value is lost your settings will be hosed).
675
-
676
- You should also rollback any local encryption you have done on the settings hash. The settings hash
677
- should just be plainly visible when you upgrade to V3. Otherwise the panda_pal migrations will
678
- probably fail.
679
-
680
- Once you have upgraded your gem, you will need to run migrations. Before doing that, I would store off your
681
- unencrypted settings (just in case).
682
-
683
- `rake db:migrate`
684
-
685
- If all goes well, you should be set! Log into console, and verify that you can still access settings:
686
-
687
- `PandaPal::Organization.first.settings`
688
-
689
- should show unencrypted settings in your console.
690
-
691
- If anything goes wrong, you should be able to rollback the change:
692
-
693
- `rake db:rollback STEP=2`
694
-
695
- If you need to give up on the change, just make sure to change your gem version for panda_pal to be < 3.
696
-
697
- ### Upgrading from PandaPal 4 to 5:
698
-
699
- If your tool is setup according to a pretty standard pattern (see pace_plans,
700
- canvas_group_enrollment, etc), you shouldn't have to do anything to upgrade.
701
-
702
- You will want to make sure that IF your launch controller is redirecting, it is
703
- using "redirect_with_session_to".
704
-
705
- Here is an example launch / account controller setup, assuming an account launch.
706
-
707
- ```ruby
708
- class LaunchController < ApplicationController
709
- # We don't verify CSRF on launch because the LTI launch is done via a POST
710
- # request, and Canvas wouldn't know anything about the CSRF
711
- skip_before_action :verify_authenticity_token
712
- skip_before_action :forbid_access_if_lacking_session # We don't have a session yet
713
- before_action :validate_launch!
714
- around_action :switch_tenant
715
- before_action :handle_launch
135
+ ```erb
136
+ <%# app/views/layouts/application.html.erb — hand the session token to the frontend %>
137
+ <%= csrf_meta_tags %>
138
+ <meta name="js-env" content="<%= { session_key: current_session&.session_key }.to_json %>">
139
+ ```
716
140
 
717
- def handle_launch
718
- current_session[:canvas_user_id] = params[:custom_canvas_user_id]
719
- current_session[:canvas_course_id] = params[:custom_canvas_course_id]
720
- end
141
+ ```ts
142
+ // With @inst_proserv/toolkit, API calls and ActionCable pick up the token automatically
143
+ import { APIBase } from "@inst_proserv/toolkit/api";
144
+ const api = new APIBase({ subURL: "/organizations/1" });
145
+ await api.get("/reports");
146
+ ```
721
147
 
722
- def account
723
- redirect_with_session_to :accounts_url
724
- end
725
- end
148
+ Without toolkit, send the token back on every request as an `X-Panda-Token` header. See
149
+ [frontend.md](docs/frontend.md) and [sessions.md](docs/sessions.md).
726
150
 
727
- class AccountController < ApplicationController
728
- prepend_before_action :forbid_access_if_lacking_session
151
+ Then create an Organization and install the tool:
729
152
 
730
- def index
731
- end
732
- end
733
- ```
153
+ ```bash
154
+ rails db:migrate
155
+ rails "panda_pal:org:new[acme]"
156
+ rails "panda_pal:org:install[acme,https://reports.example.com]"
157
+ ```
158
+
159
+ ## Docs.
160
+
161
+ | Topic | Page |
162
+ | --- | --- |
163
+ | How the pieces fit together, with request flow diagrams | [architecture.md](docs/architecture.md) |
164
+ | Common needs PandaPal already handles | [before-you-build.md](docs/before-you-build.md) |
165
+ | Initializer options, placements, environments, and the private key | [configuration.md](docs/configuration.md) |
166
+ | Engine routes, `lti_nav`, `auto_launch`, and validating launches | [routing-and-launches.md](docs/routing-and-launches.md) |
167
+ | Installing into Canvas, install callbacks, and rake tasks | [installation.md](docs/installation.md) |
168
+ | More than one tool per app | [multiple-tools.md](docs/multiple-tools.md) |
169
+ | Sessions, panda tokens, links, and redirects | [sessions.md](docs/sessions.md) |
170
+ | Organization attributes, settings, and Canvas helpers | [organizations.md](docs/organizations.md) |
171
+ | Tenants, background jobs, and sharding | [multi-tenancy.md](docs/multi-tenancy.md) |
172
+ | LTI roles, Canvas role labels, and ability helpers | [roles-and-abilities.md](docs/roles-and-abilities.md) |
173
+ | Frontend with `@inst_proserv/toolkit` | [frontend.md](docs/frontend.md) |
174
+ | ActionCable | [action-cable.md](docs/action-cable.md) |
175
+ | Deep Linking (LTI 1.3) | [deep-linking.md](docs/deep-linking.md) |
176
+ | Assignment and Grade Services (LTI 1.3) | [ags.md](docs/ags.md) |
177
+ | Grade passback (LTI 1.1) | [grade-passback.md](docs/grade-passback.md) |
178
+ | Scheduled tasks | [task-scheduling.md](docs/task-scheduling.md) |
179
+ | Generated API calls | [api-calls.md](docs/api-calls.md) |
180
+ | Security | [security.md](docs/security.md) |
181
+ | Upgrading between versions | [upgrading.md](docs/upgrading.md) |
182
+ | Working on PandaPal itself | [development.md](docs/development.md) |