panda_pal 5.4.11 → 5.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 -0
- data/app/models/panda_pal/platform.rb +141 -6
- 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
|
@@ -54,8 +54,63 @@ module PandaPal
|
|
54
54
|
include ext
|
55
55
|
end
|
56
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
|
+
|
57
108
|
private
|
58
109
|
|
110
|
+
def platform_org_extension_module
|
111
|
+
defined?(lti_platform_type::OrgExtension) ? lti_platform_type::OrgExtension : nil
|
112
|
+
end
|
113
|
+
|
59
114
|
def create_schema
|
60
115
|
Apartment::Tenant.create name
|
61
116
|
end
|
@@ -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') %>
|
data/spec/dummy/db/schema.rb
CHANGED
@@ -10,12 +10,20 @@
|
|
10
10
|
#
|
11
11
|
# It's strongly recommended that you check this file into your version control system.
|
12
12
|
|
13
|
-
ActiveRecord::Schema.define(version:
|
13
|
+
ActiveRecord::Schema.define(version: 2022_07_21_095653) do
|
14
14
|
|
15
15
|
# These are extensions that must be enabled in order to support this database
|
16
16
|
enable_extension "plpgsql"
|
17
17
|
|
18
|
-
create_table "
|
18
|
+
create_table "panda_pal_api_calls", id: :serial, force: :cascade do |t|
|
19
|
+
t.text "logic"
|
20
|
+
t.string "expiration"
|
21
|
+
t.integer "uses_remaining"
|
22
|
+
t.datetime "created_at", null: false
|
23
|
+
t.datetime "updated_at", null: false
|
24
|
+
end
|
25
|
+
|
26
|
+
create_table "panda_pal_organizations", id: :serial, force: :cascade do |t|
|
19
27
|
t.string "name"
|
20
28
|
t.string "key"
|
21
29
|
t.string "secret"
|
@@ -29,7 +37,7 @@ ActiveRecord::Schema.define(version: 20171205194657) do
|
|
29
37
|
t.index ["name"], name: "index_panda_pal_organizations_on_name", unique: true
|
30
38
|
end
|
31
39
|
|
32
|
-
create_table "panda_pal_sessions", force: :cascade do |t|
|
40
|
+
create_table "panda_pal_sessions", id: :serial, force: :cascade do |t|
|
33
41
|
t.string "session_key"
|
34
42
|
t.text "data"
|
35
43
|
t.datetime "created_at", null: false
|