panda_pal 5.4.9 → 5.6.0
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.
- checksums.yaml +4 -4
- data/README.md +39 -0
- data/app/controllers/panda_pal/api_call_controller.rb +36 -0
- data/app/lib/panda_pal/launch_url_helpers.rb +20 -6
- data/app/models/panda_pal/api_call.rb +58 -0
- data/app/models/panda_pal/organization.rb +55 -3
- data/app/models/panda_pal/organization_concerns/task_scheduling.rb +4 -2
- data/app/models/panda_pal/platform.rb +141 -6
- data/app/models/panda_pal/session.rb +1 -1
- data/config/routes.rb +3 -0
- data/db/migrate/20220721095653_create_panda_pal_api_calls.rb +11 -0
- data/lib/panda_pal/engine.rb +5 -7
- data/lib/panda_pal/helpers/controller_helper.rb +2 -9
- data/lib/panda_pal/version.rb +1 -1
- data/panda_pal.gemspec +12 -2
- data/spec/controllers/panda_pal/api_call_controller_spec.rb +27 -0
- data/spec/dummy/config/database.yml +12 -12
- data/spec/dummy/db/schema.rb +11 -3
- data/spec/dummy/log/development.log +162 -0
- data/spec/dummy/log/test.log +12627 -0
- data/spec/models/panda_pal/api_call_spec.rb +30 -0
- data/spec/models/panda_pal/organization/task_scheduling_spec.rb +1 -0
- metadata +84 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 57786e37a6af09156402d16afed167b7984105dde04a1e1cc32aad5d8505567a
|
4
|
+
data.tar.gz: 1d9cc2abbe1dce34ce0d52aa0234f6f8e909dca3a5c9eabd59b57d2b91de9f90
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1943d3e91f8c510f28eeedd1793a8853545800608b098aa29e217a3ab7a155ad6e6ea414e6c661b13206b0980b527425564612c91f65a97e17537436ac30e449
|
7
|
+
data.tar.gz: d762fe3b44e97fda8eaded8595ebf4f53a8a2e551de8fd5ab8e74433068dcb2a52fcc0dcbf5af637ca0e28c13ee88d3d751a3860bd8d873d61f85a3199d44055
|
data/README.md
CHANGED
@@ -21,6 +21,16 @@ PandaPal::stage_navigation(:account_navigation, {
|
|
21
21
|
|
22
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
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
|
31
|
+
)
|
32
|
+
```
|
33
|
+
|
24
34
|
### LTI 1.3 Configuration
|
25
35
|
LTI 1.3 has some additional configuration steps required to setup an LTI:
|
26
36
|
|
@@ -86,6 +96,35 @@ end
|
|
86
96
|
|
87
97
|
XML for an installation can be generated by visiting `/lti/config` in your application.
|
88
98
|
|
99
|
+
### Generated API Methods
|
100
|
+
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.
|
101
|
+
This is done by a deployer accessing the console and creating a `PandaPal::ApiCall`. This can be done like so:
|
102
|
+
```ruby
|
103
|
+
org.create_api(<<~RUBY, expiration: 30.days.from_now, uses: 10)
|
104
|
+
arg1 = p[:some_param] # `p` is an in-scope variable that is a Hash of the params sent to the triggering request.
|
105
|
+
PandaPal::Organization.current.trigger_canvas_sync()
|
106
|
+
{ foo: "bar" } # Will be returned to the client. Can be anything that is JSON serializable.
|
107
|
+
RUBY
|
108
|
+
# OR
|
109
|
+
org.create_api(:symbol_of_method_on_organization, expiration: 30.days.from_now, uses: 10)
|
110
|
+
```
|
111
|
+
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.
|
112
|
+
The URL generator will *attempt* to determine which params the logic accepts and insert them as `param=TODO` in the generated URL.
|
113
|
+
|
114
|
+
When triggered, the return value of the code will be wrapped and sent to the client:
|
115
|
+
```json
|
116
|
+
{
|
117
|
+
"status": "ok",
|
118
|
+
"uses_remaining": 9,
|
119
|
+
"result": {
|
120
|
+
"foo": "bar",
|
121
|
+
}
|
122
|
+
}
|
123
|
+
```
|
124
|
+
|
125
|
+
#### Revoking
|
126
|
+
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.
|
127
|
+
|
89
128
|
### Routing
|
90
129
|
|
91
130
|
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)
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'jwt'
|
2
|
+
require_dependency "panda_pal/application_controller"
|
3
|
+
|
4
|
+
module PandaPal
|
5
|
+
class ApiCallController < ApplicationController
|
6
|
+
rescue_from StandardError do |err|
|
7
|
+
render json: { status: 'error' }, status: 500
|
8
|
+
end
|
9
|
+
rescue_from ::JWT::DecodeError do |err|
|
10
|
+
render json: { status: 'error', error: "Invalid JWT" }, status: 403
|
11
|
+
end
|
12
|
+
|
13
|
+
around_action do |blk|
|
14
|
+
payload = ApiCall.decode_jwt(params[:token])
|
15
|
+
org_id = payload['organization_id']
|
16
|
+
if org_id
|
17
|
+
PandaPal::Organization.find(org_id).switch_tenant do
|
18
|
+
blk.call
|
19
|
+
end
|
20
|
+
else
|
21
|
+
blk.call
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def call
|
26
|
+
ac = ApiCall.from_jwt(params.require(:token))
|
27
|
+
result = ac.call(params.to_unsafe_h)
|
28
|
+
|
29
|
+
render json: {
|
30
|
+
status: 'ok',
|
31
|
+
uses_remaining: ac.uses_remaining,
|
32
|
+
result: result,
|
33
|
+
}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module PandaPal
|
2
2
|
module LaunchUrlHelpers
|
3
|
-
def self.absolute_launch_url(launch_type, host
|
3
|
+
def self.absolute_launch_url(launch_type, host: uri_host, launch_handler: nil, default_auto_launch: false)
|
4
4
|
opts = PandaPal.lti_paths[launch_type]
|
5
5
|
auto_launch = opts[:auto_launch] != nil ? opts[:auto_launch] : default_auto_launch
|
6
6
|
auto_launch = auto_launch && launch_handler.present?
|
@@ -18,12 +18,10 @@ module PandaPal
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def self.url_for(options)
|
21
|
-
request = Thread.current[:controller]&.request
|
22
|
-
host = "#{request.scheme}://#{request.host_with_port}"
|
23
21
|
if options.is_a?(Symbol)
|
24
|
-
Rails.application.routes.url_helpers.send(options, host:
|
22
|
+
Rails.application.routes.url_helpers.send(options, host: uri_host)
|
25
23
|
else
|
26
|
-
options[:host] ||=
|
24
|
+
options[:host] ||= uri_host
|
27
25
|
Rails.application.routes.url_helpers.url_for(options)
|
28
26
|
end
|
29
27
|
end
|
@@ -74,7 +72,23 @@ module PandaPal
|
|
74
72
|
uri.to_s
|
75
73
|
end
|
76
74
|
|
77
|
-
|
75
|
+
def self.with_uri_host(uri)
|
76
|
+
uri = URI.parse(uri) unless uri.is_a?(URI)
|
77
|
+
raise "host: param must have a protocal and no path" if uri.path.present?
|
78
|
+
|
79
|
+
initial = Thread.current[:panda_pal_access_uri]
|
80
|
+
begin
|
81
|
+
Thread.current[:panda_pal_access_uri] = uri
|
82
|
+
yield
|
83
|
+
ensure
|
84
|
+
Thread.current[:panda_pal_access_uri] = initial
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.uri_host
|
89
|
+
request = Thread.current[:controller]&.request
|
90
|
+
Thread.current[:panda_pal_access_uri]&.to_s || "#{request.scheme}://#{request.host_with_port}"
|
91
|
+
end
|
78
92
|
|
79
93
|
def self.resolve_route(key, *arguments, engine: 'PandaPal', **kwargs)
|
80
94
|
return key if key.is_a?(String)
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'jwt'
|
2
|
+
|
3
|
+
module PandaPal
|
4
|
+
class ApiCall < ActiveRecord::Base
|
5
|
+
# TODO Add Rate Limiting?
|
6
|
+
|
7
|
+
def self.decode_jwt(jwt)
|
8
|
+
jwt_payload, jwt_header = ::JWT.decode(jwt, Rails.application.secret_key_base, true, { algorithm: 'HS256' })
|
9
|
+
jwt_payload
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.from_jwt(jwt)
|
13
|
+
jwt = decode_jwt(jwt) if jwt.is_a?(String)
|
14
|
+
ApiCall.find(jwt['api_call_id'])
|
15
|
+
end
|
16
|
+
|
17
|
+
before_validation on: [:update] do
|
18
|
+
errors.add(:expiration, 'is not changeable') if expiration_changed?
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(params, internal: false)
|
22
|
+
if !internal && uses_remaining != nil
|
23
|
+
self.uses_remaining -= 1
|
24
|
+
if uses_remaining <= 0
|
25
|
+
destroy!
|
26
|
+
else
|
27
|
+
save!
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
prc = eval("->(p) {
|
32
|
+
#{logic}
|
33
|
+
}")
|
34
|
+
|
35
|
+
prc.call(params)
|
36
|
+
end
|
37
|
+
|
38
|
+
def call_url(host: nil, params: {}, **kwargs)
|
39
|
+
func_params = logic.scan(/\bp\[:(\w+)\]/).flatten
|
40
|
+
phash = {}
|
41
|
+
func_params.each.with_index do |p, i|
|
42
|
+
phash[p.to_sym] = params[p.to_sym] || "TODO"
|
43
|
+
end
|
44
|
+
|
45
|
+
PandaPal::Engine.routes.url_helpers.call_logic_url(self, token: jwt_token(**kwargs), only_path: !host, host: host, **phash, format: nil)
|
46
|
+
end
|
47
|
+
|
48
|
+
def jwt_token(expiration: self.expiration)
|
49
|
+
payload = {
|
50
|
+
api_call_id: id,
|
51
|
+
organization_id: Organization.current&.id,
|
52
|
+
}
|
53
|
+
payload[:exp] = expiration.iso8601 if expiration.present?
|
54
|
+
::JWT.encode(payload, Rails.application.secret_key_base, 'HS256')
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -1,6 +1,3 @@
|
|
1
|
-
require_relative './organization_concerns/settings_validation'
|
2
|
-
require_relative './organization_concerns/task_scheduling'
|
3
|
-
|
4
1
|
module PandaPal
|
5
2
|
module OrganizationConcerns; end
|
6
3
|
|
@@ -57,8 +54,63 @@ module PandaPal
|
|
57
54
|
include ext
|
58
55
|
end
|
59
56
|
|
57
|
+
def create_api(logic, expiration: nil, uses: nil, host: nil)
|
58
|
+
switch_tenant do
|
59
|
+
logic = "current_organization.#{logic}" if logic.is_a?(Symbol)
|
60
|
+
ac = ApiCall.create!(
|
61
|
+
logic: logic,
|
62
|
+
expiration: expiration,
|
63
|
+
uses_remaining: uses,
|
64
|
+
)
|
65
|
+
ac.call_url(host: host)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def lti_platform
|
70
|
+
lti_platform_type.new(self)
|
71
|
+
end
|
72
|
+
|
73
|
+
def lti_platform_type
|
74
|
+
platform = PandaPal.lti_options[:platform] || 'canvas.instructure.com'
|
75
|
+
case platform
|
76
|
+
when 'canvas.instructure.com'
|
77
|
+
PandaPal::Platform::Canvas
|
78
|
+
# when 'bridgeapp.com'
|
79
|
+
# TODO Add support for Bridge?
|
80
|
+
else
|
81
|
+
raise "Unknown platform '#{platform}'"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def rename!(new_name)
|
86
|
+
do_switch = Apartment::Tenant.current == name
|
87
|
+
ActiveRecord::Base.connection.execute(
|
88
|
+
"ALTER SCHEMA \"#{name}\" RENAME TO \"#{new_name}\";"
|
89
|
+
)
|
90
|
+
self.class.where(id: id).update_all(name: new_name)
|
91
|
+
reload
|
92
|
+
switch_tenant if do_switch
|
93
|
+
end
|
94
|
+
|
95
|
+
def respond_to_missing?(name, include_private = false)
|
96
|
+
(platform_org_extension_module&.instance_method(name) rescue nil) || super
|
97
|
+
end
|
98
|
+
|
99
|
+
def method_missing(method, *args, &block)
|
100
|
+
ext = platform_org_extension_module
|
101
|
+
if (ext.instance_method(method) rescue nil)
|
102
|
+
ext.instance_method(method).bind_call(self, *args, &block)
|
103
|
+
else
|
104
|
+
super
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
60
108
|
private
|
61
109
|
|
110
|
+
def platform_org_extension_module
|
111
|
+
defined?(lti_platform_type::OrgExtension) ? lti_platform_type::OrgExtension : nil
|
112
|
+
end
|
113
|
+
|
62
114
|
def create_schema
|
63
115
|
Apartment::Tenant.create name
|
64
116
|
end
|
@@ -13,8 +13,6 @@ unless defined?(Sidekiq.schedule)
|
|
13
13
|
return
|
14
14
|
end
|
15
15
|
|
16
|
-
require_relative 'settings_validation'
|
17
|
-
|
18
16
|
module PandaPal
|
19
17
|
module OrganizationConcerns
|
20
18
|
module TaskScheduling
|
@@ -68,6 +66,10 @@ module PandaPal
|
|
68
66
|
}
|
69
67
|
end
|
70
68
|
|
69
|
+
def task_scheduled?(name_or_method)
|
70
|
+
_schedule_descriptors.key?(name_or_method.to_s)
|
71
|
+
end
|
72
|
+
|
71
73
|
def remove_scheduled_task(name_or_method)
|
72
74
|
dval = _schedule_descriptors.delete(name_or_method.to_s)
|
73
75
|
Rails.logger.warn("No task with key '#{name_or_method}' to delete!") unless dval.present?
|
@@ -1,3 +1,8 @@
|
|
1
|
+
begin
|
2
|
+
require 'bearcat'
|
3
|
+
rescue LoadError
|
4
|
+
end
|
5
|
+
|
1
6
|
module PandaPal
|
2
7
|
class Platform
|
3
8
|
def public_jwks
|
@@ -8,13 +13,24 @@ module PandaPal
|
|
8
13
|
rescue
|
9
14
|
nil
|
10
15
|
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def self.find_org_setting(paths, org = current_organization)
|
20
|
+
paths.each do |p|
|
21
|
+
p = p.split('.').map(&:to_sym)
|
22
|
+
val = org.settings.dig(*p)
|
23
|
+
return val if val.present?
|
24
|
+
end
|
25
|
+
nil
|
26
|
+
end
|
11
27
|
end
|
12
28
|
|
13
29
|
class Platform::Canvas < Platform
|
14
|
-
attr_accessor :
|
30
|
+
attr_accessor :organization
|
15
31
|
|
16
|
-
def initialize(
|
17
|
-
@
|
32
|
+
def initialize(org)
|
33
|
+
@organization = org
|
18
34
|
end
|
19
35
|
|
20
36
|
def host
|
@@ -32,9 +48,128 @@ module PandaPal
|
|
32
48
|
def grant_url
|
33
49
|
"#{base_url}/login/oauth2/token"
|
34
50
|
end
|
35
|
-
end
|
36
51
|
|
37
|
-
|
38
|
-
|
52
|
+
def base_url; org.canvas_url; end
|
53
|
+
|
54
|
+
module OrgExtension
|
55
|
+
def install_lti(host: nil, context: :root_account, version: 'v1p1', exists: :error)
|
56
|
+
raise "Automatically installing this LTI requires Bearcat." unless defined?(Bearcat)
|
57
|
+
|
58
|
+
context = context.to_s
|
59
|
+
context = 'account/self' if context == 'root_account'
|
60
|
+
cid, ctype = context.split(/[\.\/]/).reverse
|
61
|
+
ctype ||= 'account'
|
62
|
+
|
63
|
+
existing_installs = bearcat_client.send(:"#{ctype}_external_tools", cid).all_pages_each.filter do |cet|
|
64
|
+
cet[:consumer_key] == self.key
|
65
|
+
end
|
66
|
+
|
67
|
+
if existing_installs.present?
|
68
|
+
case exists
|
69
|
+
when :error
|
70
|
+
raise "Tool with key #{self.key} already installed"
|
71
|
+
when :duplicate
|
72
|
+
when :replace
|
73
|
+
existing_installs.each do |install|
|
74
|
+
bearcat_client.send(:"delete_#{ctype}_external_tool", cid, install[:id])
|
75
|
+
end
|
76
|
+
else
|
77
|
+
raise "exists: #{exists} is not supported"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# TODO LTI 1.3 Support
|
82
|
+
|
83
|
+
conf = {
|
84
|
+
name: PandaPal.lti_options[:title],
|
85
|
+
description: PandaPal.lti_options[:description],
|
86
|
+
consumer_key: self.key,
|
87
|
+
shared_secret: self.secret,
|
88
|
+
privacy_level: "public",
|
89
|
+
config_type: 'by_url',
|
90
|
+
config_url: PandaPal::LaunchUrlHelpers.resolve_route(:v1p0_config_url, host: host),
|
91
|
+
}
|
92
|
+
|
93
|
+
bearcat_client.send(:"create_#{ctype}_external_tool", cid, conf)
|
94
|
+
end
|
95
|
+
|
96
|
+
def lti_api_configuration(host: nil)
|
97
|
+
PandaPal::LaunchUrlHelpers.with_uri_host(host) do
|
98
|
+
domain = PandaPal.lti_properties[:domain] || host.host
|
99
|
+
launch_url = PandaPal.lti_options[:secure_launch_url] ||
|
100
|
+
"#{domain}#{PandaPal.lti_options[:secure_launch_path]}" ||
|
101
|
+
PandaPal.lti_options[:launch_url] ||
|
102
|
+
"#{domain}#{PandaPal.lti_options[:launch_path]}" ||
|
103
|
+
domain
|
104
|
+
|
105
|
+
lti_json = {
|
106
|
+
name: PandaPal.lti_options[:title],
|
107
|
+
description: PandaPal.lti_options[:description],
|
108
|
+
domain: host.host,
|
109
|
+
url: launch_url,
|
110
|
+
consumer_key: self.key,
|
111
|
+
shared_secret: self.secret,
|
112
|
+
privacy_level: "public",
|
113
|
+
|
114
|
+
custom_fields: {},
|
115
|
+
|
116
|
+
environments: PandaPal.lti_environments,
|
117
|
+
}
|
118
|
+
|
119
|
+
lti_json = lti_json.with_indifferent_access
|
120
|
+
lti_json.merge!(PandaPal.lti_properties)
|
121
|
+
|
122
|
+
(PandaPal.lti_options[:custom_fields] || []).each do |k, v|
|
123
|
+
lti_json[:custom_fields][k] = v
|
124
|
+
end
|
125
|
+
|
126
|
+
PandaPal.lti_paths.each do |k, options|
|
127
|
+
options = PandaPal::LaunchUrlHelpers.normalize_lti_launch_desc(options)
|
128
|
+
options[:url] = PandaPal::LaunchUrlHelpers.absolute_launch_url(
|
129
|
+
k.to_sym,
|
130
|
+
host: host,
|
131
|
+
launch_handler: :v1p0_launch_path,
|
132
|
+
default_auto_launch: false
|
133
|
+
).to_s
|
134
|
+
lti_json[k] = options
|
135
|
+
end
|
136
|
+
|
137
|
+
lti_json
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def canvas_url
|
142
|
+
PandaPal::Platform.find_org_setting([
|
143
|
+
"canvas.base_url",
|
144
|
+
"canvas_url",
|
145
|
+
"canvas_base_url",
|
146
|
+
"canvas.url",
|
147
|
+
"base_url",
|
148
|
+
], self) || (Rails.env.development? && 'http://localhost:3000') || 'https://canvas.instructure.com'
|
149
|
+
end
|
150
|
+
|
151
|
+
def canvas_api_token
|
152
|
+
PandaPal::Platform.find_org_setting([
|
153
|
+
"canvas.api_token",
|
154
|
+
"canvas.api_key",
|
155
|
+
"canvas.token",
|
156
|
+
"canvas_api_token",
|
157
|
+
"canvas_token",
|
158
|
+
"api_token",
|
159
|
+
], self)
|
160
|
+
end
|
161
|
+
|
162
|
+
if defined?(Bearcat)
|
163
|
+
def bearcat_client
|
164
|
+
return canvas_sync_client if defined?(canvas_sync_client)
|
165
|
+
|
166
|
+
Bearcat::Client.new(
|
167
|
+
prefix: canvas_url,
|
168
|
+
token: canvas_token,
|
169
|
+
master_rate_limit: (Rails.env.production? && !!defined?(Redis) && ENV['REDIS_URL'].present?),
|
170
|
+
)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
39
174
|
end
|
40
175
|
end
|
data/config/routes.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
PandaPal::Engine.routes.draw do
|
2
2
|
get '/config' => 'lti_v1_p0#tool_config' # Legacy Support
|
3
3
|
|
4
|
+
get "/call_logic" => "api_call#call"
|
5
|
+
post "/call_logic" => "api_call#call"
|
6
|
+
|
4
7
|
scope '/v1p0', as: 'v1p0' do
|
5
8
|
get '/config' => 'lti_v1_p0#tool_config'
|
6
9
|
post '/launch' => 'lti_v1_p0#launch'
|
data/lib/panda_pal/engine.rb
CHANGED
@@ -15,13 +15,11 @@ module PandaPal
|
|
15
15
|
end
|
16
16
|
|
17
17
|
initializer :append_migrations do |app|
|
18
|
-
|
19
|
-
config.paths["db/migrate"]
|
20
|
-
app.config.paths["db/migrate"] << expanded_path
|
21
|
-
end
|
22
|
-
# Apartment will modify this, but it doesn't fully support engine migrations, so we'll reset it here
|
23
|
-
ActiveRecord::Migrator.migrations_paths = Rails.application.paths['db/migrate'].to_a
|
18
|
+
config.paths["db/migrate"].expanded.each do |expanded_path|
|
19
|
+
app.config.paths["db/migrate"] << expanded_path
|
24
20
|
end
|
21
|
+
# Apartment will modify this, but it doesn't fully support engine migrations, so we'll reset it here
|
22
|
+
ActiveRecord::Migrator.migrations_paths = Rails.application.paths['db/migrate'].to_a
|
25
23
|
end
|
26
24
|
|
27
25
|
initializer 'interop dependencies' do
|
@@ -70,7 +68,7 @@ module PandaPal
|
|
70
68
|
end
|
71
69
|
|
72
70
|
initializer "panda_pal.assets.precompile" do |app|
|
73
|
-
app.config.assets.precompile << "panda_pal_manifest.js"
|
71
|
+
app.config.assets.precompile << "panda_pal_manifest.js" rescue nil
|
74
72
|
end
|
75
73
|
|
76
74
|
initializer :secure_headers do |app|
|
@@ -13,14 +13,7 @@ module PandaPal::Helpers
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def current_lti_platform
|
16
|
-
|
17
|
-
# TODO: (Future) This could be expanded more to take better advantage of the LTI 1.3 Multi-Tenancy model.
|
18
|
-
if (canvas_url = current_organization&.settings&.dig(:canvas, :base_url)).present?
|
19
|
-
@current_lti_platform ||= PandaPal::Platform::Canvas.new(canvas_url)
|
20
|
-
end
|
21
|
-
@current_lti_platform ||= PandaPal::Platform::Canvas.new('http://localhost:3000') if Rails.env.development?
|
22
|
-
@current_lti_platform ||= PandaPal::Platform::CANVAS
|
23
|
-
@current_lti_platform
|
16
|
+
@current_lti_platform ||= current_organization.lti_platform
|
24
17
|
end
|
25
18
|
|
26
19
|
def lti_launch_params
|
@@ -41,7 +34,7 @@ module PandaPal::Helpers
|
|
41
34
|
authorized = false
|
42
35
|
# We should verify the timestamp is recent (within 5 minutes). The approved timestamp is part of the signature,
|
43
36
|
# so we don't need to worry about malicious users messing with it. We should deny requests that come too long
|
44
|
-
# after the approved timestamp.
|
37
|
+
# after the approved timestamp.
|
45
38
|
good_timestamp = params['oauth_timestamp'] && params['oauth_timestamp'].to_i > Time.now.to_i - 300
|
46
39
|
if @organization = good_timestamp && params['oauth_consumer_key'] && PandaPal::Organization.find_by_key(params['oauth_consumer_key'])
|
47
40
|
sanitized_params = request.request_parameters
|
data/lib/panda_pal/version.rb
CHANGED
data/panda_pal.gemspec
CHANGED
@@ -1,11 +1,16 @@
|
|
1
1
|
$:.push File.expand_path("../lib", __FILE__)
|
2
2
|
|
3
|
-
|
3
|
+
begin
|
4
|
+
require "panda_pal/version"
|
5
|
+
version = PandaPal::VERSION
|
6
|
+
rescue LoadError
|
7
|
+
version = "0.0.0.docker"
|
8
|
+
end
|
4
9
|
|
5
10
|
# Describe your gem and declare its dependencies:
|
6
11
|
Gem::Specification.new do |s|
|
7
12
|
s.name = "panda_pal"
|
8
|
-
s.version =
|
13
|
+
s.version = version
|
9
14
|
s.authors = ["Instructure ProServe"]
|
10
15
|
s.email = ["pseng@instructure.com"]
|
11
16
|
s.homepage = "http://instructure.com"
|
@@ -22,10 +27,15 @@ Gem::Specification.new do |s|
|
|
22
27
|
s.add_dependency 'attr_encrypted', '~> 3.0.0'
|
23
28
|
s.add_dependency 'secure_headers', '~> 6.1'
|
24
29
|
s.add_dependency 'json-jwt'
|
30
|
+
s.add_dependency 'jwt'
|
25
31
|
s.add_dependency 'httparty'
|
26
32
|
|
33
|
+
s.add_development_dependency "rails", "~> 5.2"
|
34
|
+
s.add_development_dependency 'pg'
|
27
35
|
s.add_development_dependency 'sidekiq'
|
28
36
|
s.add_development_dependency 'sidekiq-scheduler'
|
37
|
+
s.add_development_dependency 'ros-apartment', '~> 2.11'
|
38
|
+
s.add_development_dependency 'ros-apartment-sidekiq', '~> 1.2'
|
29
39
|
s.add_development_dependency 'rspec-rails'
|
30
40
|
s.add_development_dependency 'factory_girl_rails'
|
31
41
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
module PandaPal
|
4
|
+
RSpec.describe ApiCallController, type: :controller do
|
5
|
+
let!(:api_call) { ApiCall.create!(logic: '"#{p[:foo]}_bar"') }
|
6
|
+
|
7
|
+
def json_body
|
8
|
+
JSON.parse(response.body)
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "#call" do
|
12
|
+
it "calls the function with parameters" do
|
13
|
+
get :call, params: { token: api_call.jwt_token, foo: 'bar', use_route: 'panda_pal' }
|
14
|
+
expect(response).to be_successful
|
15
|
+
expect(json_body["status"]).to eq 'ok'
|
16
|
+
expect(json_body["result"]).to eq 'bar_bar'
|
17
|
+
end
|
18
|
+
|
19
|
+
it "fails given a bad JWT" do
|
20
|
+
get :call, params: { token: "bad.jwt.token", foo: 'bar', use_route: 'panda_pal' }
|
21
|
+
expect(response).not_to be_successful
|
22
|
+
expect(json_body["status"]).to eq 'error'
|
23
|
+
expect(json_body["error"]).to eq 'Invalid JWT'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -1,25 +1,25 @@
|
|
1
|
-
|
2
|
-
# gem install sqlite3
|
3
|
-
#
|
4
|
-
# Ensure the SQLite 3 gem is defined in your Gemfile
|
5
|
-
# gem 'sqlite3'
|
6
|
-
#
|
1
|
+
|
7
2
|
default: &default
|
8
3
|
adapter: postgresql
|
9
|
-
|
10
|
-
|
4
|
+
encoding: unicode
|
5
|
+
# For details on connection pooling, see Rails configuration guide
|
6
|
+
# http://guides.rubyonrails.org/configuring.html#database-pooling
|
7
|
+
pool: <%= ENV.fetch("DB_POOL_SIZE", nil) || (ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i + 10) %>
|
8
|
+
username: <%= ENV.fetch("DB_USERNAME", "") %>
|
9
|
+
password: <%= ENV.fetch("DB_PASSWORD", "") %>
|
10
|
+
host: <%= ENV.fetch("DB_ADDRESS", "localhost") %>
|
11
11
|
|
12
12
|
development:
|
13
13
|
<<: *default
|
14
14
|
database: panda_pal_development
|
15
15
|
|
16
|
-
# Warning: The database defined as "test" will be erased and
|
17
|
-
# re-generated from your development database when you run "rake".
|
18
|
-
# Do not set this db to the same as development or production.
|
19
16
|
test:
|
20
17
|
<<: *default
|
21
18
|
database: panda_pal_test
|
22
19
|
|
23
20
|
production:
|
24
21
|
<<: *default
|
25
|
-
|
22
|
+
host: <%= ENV.fetch('DB_ADDRESS', 'localhost') %>
|
23
|
+
database: panda_pal_production
|
24
|
+
username: <%= ENV.fetch("DB_USERNAME", "panda_pal_specs_postgres_user") %>
|
25
|
+
password: <%= ENV.fetch("DB_PASSWORD", 'panda_pal_specs_postgres_password') %>
|