# PandaPal ## LTI Configuration A standard Canvas LTI may have a configuration similar to the one below ```ruby # config/initializers/lti.rb PandaPal.lti_options = { title: 'Teacher Reports' } PandaPal.lti_properties = {oauth_compliant: 'true'} # Environments reflect those used by Canvas PandaPal.lti_environments = { domain: ENV['PROD_DOMAIN'], beta_domain: ENV['BETA_DOMAIN'], test_domain: ENV['TEST_DOMAIN'] } PandaPal.lti_custom_params = { custom_canvas_role: '$Canvas.membership.roles' } # :user_navigation, :course_navigation for user context and course context endpoints respectively PandaPal::stage_navigation(:account_navigation, { enabled: true, text: 'Teacher Reports', visibility: 'admins' }) ``` 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). ### Launch URL property 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.` 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. Use one of these 6 options in `PandaPal.lti_options` hash. 1. Use option `secure_launch_url: 'http://domain.com/path'` when a full static secure domain is needed. 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. 3. Use option `launch_url: 'http://domain.com/path'` when a full static domain is needed. 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. 5. Leave this property off, and you will get the dynamic host with the root path ('http://appdomain.com/') by default. 6. If you really do not want this property use the option `launch_url: false` for it to be left off. # Organization Attributes id: Primary Key name: Name of the organization. Used to on requests to select the tenant key: Key field from CanvasLMS secret: Secret field from CanvasLMS canvas_account_id: ID of the corresponding Canvas account. settings: Hash of settings for this Organization salesforce_id: ID of this organization in Salesforce XML for an installation can be generated by visiting /lti/config in your application. ### Routing 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) ```ruby # config/routes.rb mount PandaPal::Engine, at: '/lti' lti_nav account_navigation: 'accounts#launch' root to: 'panda_pal/lti#launch' ``` ## Implementating data segregation This engine uses Apartment to keep data segregated between installations of the implementing LTI tool. By default, it does this by inspecting the path of the request, and matching URLs containing `orgs` or `organizations`, followed by the numeric ID of the organization (path such as `/organizations/123/accounts`). As such, all URLs should be built on top of a routing scope, such as the example below. ```ruby scope '/organizations/:organization_id' do resources :accounts, only: :index do collection do get 'teachers' => 'accounts#teachers' end end end ``` This can be overriden by creating an initializer, and adding a custom elevator ```ruby Rails.application.config.middleware.delete 'Apartment::Elevators::Generic' Rails.application.config.middleware.use 'Apartment::Elevators::Generic', lambda { |request| if match = request.path.match(/\/(?:orgs|organizations)\/(\d+)/) PandaPal::Organization.find_by(id: match[1]).try(:name) end } ``` It is also possible to switch tenants by implementing `around_action :switch_tenant` in the controller. 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. ### Rake tasks and jobs # Delayed Job Support has been removed. This allows each project to assess it's need to handle background jobs. The Apartment Gem makes it so background jobs need to be run within the current tenant. If using sidekiq, there is a gem that does this automatically called apartment-sidkiq. If Delayed Jobs, see how it's done in version 1 of PandaPal. 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. One potential solution is to create a method with switches between tenants, and invoke the job for each individual organization as necessary. ```ruby # config/initializers/global_methods.rb def switch_tenant(tenant, &block) if block_given? Apartment::Tenant.switch(tenant, &block) else Apartment::Tenant.switch! tenant end end ``` ```ruby # lib/tasks/lti_tasks.rake namespace :lti_tasks do desc 'some rake task' task some_rake_task: :environment do PandaPal::Organization.all.each do |org| switch_tenant(org.name) do SomeJob.perform_later end end end end ``` ## Controller helper methods Controllers will automatically have access to a number of methods that can be helpful to implement. A list with descriptions is provided below. * `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. * `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. * `current_session_data` - Shortcut method to access the `data` hash given by the above method. * `current_organization` - Used to return the organization related to the currently selected tenant. See section below for more information about this method. * `session_changed?` - returns true if the session given by `current_session` has been modified during the request. * `save_session` - Saves the session given by `current_session` to the database. ## Sessions and `current_session` A session_key returned by `current_session` is essentially an access token, and while we can't prevent users from sharing if they're so inclined, it should still be kept hidden as much as possible (namely by not including it as a URL param at any time). A good way to pass the `session_key` if using ajax or something similar, is to define it as a header in `$.ajaxSetup`. This is particularly useful if a javascript framework such as React is being utilized. PandaPal provides a number of ways to find a session, shown in the example below ```ruby def session_key params[:session_key] || session_key_header || flash[:session_key] || session[:session_key] end def session_key_header if match = request.headers['Authorization'].try(:match, /token=(.+)/) match[1] end end ``` ### Persisting the session In order to reduce the number of sessions saved to the database, a session should only be saved if data has been added to it. A good way to accomplish this is to add `append_after_action :save_session, if: proc { session_changed? }` to the application_controller.rb ### Session cleanup Over time, the sessions table will become bloated with sessions that are no longer active. As such, `rake panda_pal:clean_sessions` should be run periodically to clean up sessions that haven't been updated in over a week. ## Organizations and `current_organization` Similar to `current_session`, `current_organization` can be returned with a number of methods, shown below ```ruby def organization_key params[:oauth_consumer_key] || params[:organization_id] || current_session_data[:organization_key] || session[:organization_key] end ``` ## Security ### Tool Launches Any time a tool launch request is received, we should **always** validate that it originated from within Canvas and that it wasn't spoofed. The easiest way to do this is to call `validate_launch!` in any controller action that is expecting to receive launch requests. ### Validate active session is present It is easier to implement this if a purely javascript frontend is used (such as React). 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` ```ruby # app/controllers/application_controller.rb prepend_before_action :forbid_access_if_lacking_session ``` This will render an unauthorized message any time a request is received and a valid session is not present. It can be overridden on a action-by-action basis by using `skip_before_action`. ```ruby skip_before_action :forbid_access_if_lacking_session, only: :launch ``` ### Upgrading to version 3 Before upgrading save existing settings somewhere safe in case you need to restore them for whatever reason. panda_pal v3 introduces an encrypted settings hash. This should provide more security in the case where a customer may have gained access to Organization information in the database. Settings cannot be decrypted without knowing the secret decryption key. For panda_pal, we are relying on the secret_key_base to be set (typically in config/secrets.yml), if that is not available we are falling back to directly use ENV['SECRET_KEY_BASE']. For production environments, config/secrets.yml should not have a plain-text secret, it should be referencing an ENV variable. Make sure your secret is not plain-text committed to a repository! The secret key is used to encrypt / decrypt the settings hash as necessary. Before upgrading to version 3, you should confirm that the secret key is set to a consistent value (if the value is lost your settings will be hosed). You should also rollback any local encryption you have done on the settings hash. The settings hash should just be plainly visible when you upgrade to V3. Otherwise the panda_pal migrations will probably fail. Once you have upgraded your gem, you will need to run migrations. Before doing that, I would store off your unencrypted settings (just in case). `rake db:migrate` If all goes well, you should be set! Log into console, and verify that you can still access settings: `PandaPal::Organization.first.settings` should show unencrypted settings in your console. If anything goes wrong, you should be able to rollback the change: `rake db:rollback STEP=2` If you need to give up on the change, just make sure to change your gem version for panda_pal to be < 3. ### Validating settings in your LTI. You can specify a structure that you would like to have enforced for your settings. In your panda_pal initializer (i.e. config/initializers/panda_pal.rb or config/initializers/lti.rb) You can specify options that can include a structure for your settings. If specified, PandaPal will enforce this structure on any new / updated organizations. Here is an example options specification: ``` PandaPal.lti_options = { title: 'LBS Gradebook', settings_structure: YAML.load(" canvas: is_required: true data_type: Hash api_token: is_required: true data_type: String base_url: is_required: true data_type: String reports: is_required: true data_type: Hash active_term_allowance: submissions_report_time_length: is_required: true data_type: ActiveSupport::Duration recheck_wait: data_type: ActiveSupport::Duration max_recheck_time: is_required: true ").deep_symbolize_keys } ``` (This loads the structure in from YAML, but you can specify a hash directly if you prefer.) Each data attribute can have two children attributes to describe desired structure: is_required: If specified, and specified as true, the parent attribute is required in the settings hash. data_type: If specified, and a settings hash contains this attribute, attribute data type will be compared to the specified data type. If you are not sure how to derive your class data type, you can use `class` to determine data type. For example: 30.minutes.class.to_s => "ActiveSupport::Duration" ## Bridge vs Canvas As of 3.2.0, the LTI XML config can have subtle differences based on whether your platform is bridge, or canvas. This is determined by `PandaPal.lti_options[:platform]`. Set this to `platform: 'canvas.instructure.com'` (default) OR `platform: 'bridgeapp.com'` ### Safari Support Safari is weird, and you'll potentially run into issues getting `POST` requests to properly validate CSRF if you don't do the following: - Make sure you have both a `launch` controller and your normal controllers. The `launch` controller should call `before_action :validate_launch!` and then redirect to your other controller. - Make sure your other controller calls `before_action :forbid_access_if_lacking_session` This will allow `PandaPal` to apply an iframe cookie fix that will allow CSRF validation to work.