canvas_oauth_engine 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.md +102 -0
  3. data/Rakefile +28 -0
  4. data/app/controllers/canvas_oauth/application_controller.rb +5 -0
  5. data/app/controllers/canvas_oauth/canvas_controller.rb +25 -0
  6. data/app/models/canvas_oauth/authorization.rb +26 -0
  7. data/config/canvas.yml.example +12 -0
  8. data/config/routes.rb +3 -0
  9. data/db/migrate/20121121005358_create_canvas_oauth_authorizations.rb +15 -0
  10. data/lib/canvas_oauth.rb +25 -0
  11. data/lib/canvas_oauth/canvas_api.rb +249 -0
  12. data/lib/canvas_oauth/canvas_api_extensions.rb +8 -0
  13. data/lib/canvas_oauth/canvas_application.rb +66 -0
  14. data/lib/canvas_oauth/canvas_config.rb +28 -0
  15. data/lib/canvas_oauth/config.rb +1 -0
  16. data/lib/canvas_oauth/engine.rb +15 -0
  17. data/lib/canvas_oauth/version.rb +3 -0
  18. data/lib/tasks/canvas_oauth_tasks.rake +1 -0
  19. data/spec/controllers/canvas_oauth/canvas_controller_spec.rb +80 -0
  20. data/spec/dummy/README.rdoc +261 -0
  21. data/spec/dummy/Rakefile +7 -0
  22. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  23. data/spec/dummy/app/controllers/welcome_controller.rb +5 -0
  24. data/spec/dummy/config.ru +4 -0
  25. data/spec/dummy/config/application.rb +55 -0
  26. data/spec/dummy/config/boot.rb +10 -0
  27. data/spec/dummy/config/canvas.yml +12 -0
  28. data/spec/dummy/config/database.yml +25 -0
  29. data/spec/dummy/config/environment.rb +5 -0
  30. data/spec/dummy/config/environments/development.rb +26 -0
  31. data/spec/dummy/config/environments/production.rb +69 -0
  32. data/spec/dummy/config/environments/test.rb +33 -0
  33. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  34. data/spec/dummy/config/initializers/inflections.rb +15 -0
  35. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  36. data/spec/dummy/config/initializers/secret_token.rb +8 -0
  37. data/spec/dummy/config/initializers/session_store.rb +8 -0
  38. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  39. data/spec/dummy/config/locales/en.yml +5 -0
  40. data/spec/dummy/config/routes.rb +4 -0
  41. data/spec/dummy/db/migrate/20130326194409_create_canvas_oauth_authorizations.canvas_oauth.rb +13 -0
  42. data/spec/dummy/db/schema.rb +25 -0
  43. data/spec/dummy/db/test.sqlite3 +0 -0
  44. data/spec/dummy/log/test.log +8127 -0
  45. data/spec/dummy/public/404.html +26 -0
  46. data/spec/dummy/public/422.html +26 -0
  47. data/spec/dummy/public/500.html +25 -0
  48. data/spec/dummy/public/favicon.ico +0 -0
  49. data/spec/dummy/public/robots.txt +5 -0
  50. data/spec/dummy/script/rails +6 -0
  51. data/spec/lib/canvas_oauth/canvas_api_extensions_spec.rb +13 -0
  52. data/spec/lib/canvas_oauth/canvas_api_spec.rb +228 -0
  53. data/spec/models/canvas_oauth/authorization_spec.rb +47 -0
  54. data/spec/spec_helper.rb +57 -0
  55. metadata +353 -0
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2012 - 2015 Instructure, Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # CanvasOauth
2
+
3
+ CanvasOauth is a mountable engine for handling the oauth workflow with canvas
4
+ and making api calls from your rails app. This is tested with Rails 3.2, we'll
5
+ be looking at verifying with Rails 4 soon.
6
+
7
+ ## Installation
8
+
9
+ Add the gem to your `Gemfile` with the following line, and then `bundle install`
10
+
11
+ ```
12
+ gem 'canvas_oauth_engine', :require => 'canvas_oauth'
13
+ ```
14
+
15
+ Then, mount the engine to your app by adding this line to your `routes.rb` file
16
+
17
+ ```
18
+ mount CanvasOauth::Engine => "/canvas_oauth"
19
+ ```
20
+
21
+ Next, include the engine in your `ApplicationController`
22
+
23
+ ```
24
+ class ApplicationController < ActionController::Base
25
+ include CanvasOauth::CanvasApplication
26
+
27
+ ...
28
+ end
29
+ ```
30
+
31
+ After that, create an `canvas.yml` file in your `config/` folder that looks something
32
+ like this (or see `config/canvas.yml.example` for a template):
33
+
34
+ ```
35
+ default: &default
36
+ key: your_key
37
+ secret: your_secret
38
+
39
+ development:
40
+ <<: *default
41
+
42
+ test:
43
+ <<: *default
44
+
45
+ production:
46
+ <<: *default
47
+ ```
48
+
49
+ The values of key and secret should be set from the developer key that you
50
+ generate in the canvas application.
51
+
52
+ Finally, run migrations:
53
+
54
+ ```
55
+ bundle exec install
56
+ bundle exec rake railties:install:migrations
57
+ bundle exec rake db:migrate
58
+ ```
59
+
60
+ This will create the `canvas_oauth_authorizations` table which stores
61
+ successful oauth tokens for later use, so the user does not have to accept the
62
+ oauth login each time they use the app.
63
+
64
+ ## Usage
65
+
66
+ The engine only uses one url, whatever it is mounted to, to serve as the
67
+ `redirect_url` in the oauth workflow.
68
+
69
+ The engine sets up a global `before_filter`, which checks for a valid oauth
70
+ token, and if one does not exist, starts the oauth login flow. It handles
71
+ posting the oauth request, verifying the result and redirecting to the
72
+ application root. It exposes the following methods to your controllers:
73
+
74
+ * `canvas`
75
+ * `canvas_token`
76
+
77
+ The first is an instance of HTTParty ready to make api requests to your canvas
78
+ application. The second is if you need access to the oauth token directly.
79
+
80
+ ## Configuring the Tool Consumer
81
+
82
+ You will a developer key and secret from canvas, which should be entered into
83
+ you `canvas.yml` file.
84
+
85
+ ## Example
86
+
87
+ You can see and interact with an example of an app using this engine by looking
88
+ at `spec/dummy`. This is a full rails app which integrates the gem and has
89
+ a simple index page that says 'Hello Oauth' if the app is launched and the
90
+ oauth flow is successful.
91
+
92
+ ## About Oauth
93
+
94
+ TODO...
95
+
96
+ ## Contributing
97
+
98
+ 1. Fork it
99
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
100
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
101
+ 4. Push to the branch (`git push origin my-new-feature`)
102
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'CanvasOauth'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+ APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
24
+ load 'rails/tasks/engine.rake'
25
+
26
+ Bundler::GemHelper.install_tasks
27
+
28
+ task :default => :spec
@@ -0,0 +1,5 @@
1
+ module CanvasOauth
2
+ class ApplicationController < ActionController::Base
3
+ include CanvasOauth::CanvasApplication
4
+ end
5
+ end
@@ -0,0 +1,25 @@
1
+ module CanvasOauth
2
+ class CanvasController < CanvasOauth::ApplicationController
3
+ skip_before_filter :request_canvas_authentication
4
+
5
+ def oauth
6
+ if verify_oauth2_state(params[:state]) && params[:code]
7
+ if token = canvas.get_access_token(params[:code])
8
+ if CanvasOauth::Authorization.cache_token(token, user_id, tool_consumer_instance_guid)
9
+ redirect_to main_app.root_path
10
+ else
11
+ render text: "Error: unable to save token"
12
+ end
13
+ else
14
+ render text: "Error: invalid code - #{params[:code]}"
15
+ end
16
+ else
17
+ render text: "#{CanvasOauth::Config.tool_name} needs access to your account in order to function properly. Please try again and click log in to approve the integration."
18
+ end
19
+ end
20
+
21
+ def verify_oauth2_state(callback_state)
22
+ callback_state.present? && callback_state == session.delete(:oauth2_state)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ module CanvasOauth
2
+ class Authorization < ActiveRecord::Base
3
+ validates :canvas_user_id, :token, :last_used_at, presence: true
4
+
5
+ def self.cache_token(token, user_id, tool_consumer_instance_guid)
6
+ create(
7
+ token: token,
8
+ canvas_user_id: user_id,
9
+ last_used_at: Time.now,
10
+ tool_consumer_instance_guid: tool_consumer_instance_guid
11
+ )
12
+ end
13
+
14
+ def self.fetch_token(user_id, tool_consumer_instance_guid)
15
+ user_tokens = where(canvas_user_id: user_id, tool_consumer_instance_guid: tool_consumer_instance_guid).order("created_at DESC")
16
+ if canvas_auth = user_tokens.first
17
+ canvas_auth.update_attribute(:last_used_at, Time.now)
18
+ return canvas_auth.token
19
+ end
20
+ end
21
+
22
+ def self.clear_tokens(user_id, tool_consumer_instance_guid)
23
+ where(canvas_user_id: user_id, tool_consumer_instance_guid: tool_consumer_instance_guid).destroy_all
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,12 @@
1
+ default: &default
2
+ key: your_key
3
+ secret: your_secret
4
+
5
+ development:
6
+ <<: *default
7
+
8
+ test:
9
+ <<: *default
10
+
11
+ cucumber:
12
+ <<: *default
data/config/routes.rb ADDED
@@ -0,0 +1,3 @@
1
+ CanvasOauth::Engine.routes.draw do
2
+ match "/", to: "canvas#oauth", via: :get, as: :canvas_oauth
3
+ end
@@ -0,0 +1,15 @@
1
+ class CreateCanvasOauthAuthorizations < ActiveRecord::Migration
2
+ def change
3
+ create_table "canvas_oauth_authorizations", :force => true do |t|
4
+ t.integer "canvas_user_id", :limit => 8
5
+ t.string "tool_consumer_instance_guid", :null => false
6
+ t.string "token"
7
+ t.datetime "last_used_at"
8
+ t.datetime "created_at", :null => false
9
+ t.datetime "updated_at", :null => false
10
+ end
11
+
12
+ add_index :canvas_oauth_authorizations, [:canvas_user_id, :tool_consumer_instance_guid],
13
+ name: 'index_canvas_oauth_auths_on_user_id_and_tciguid'
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ require 'ostruct'
2
+
3
+ require 'httparty'
4
+ require 'link_header'
5
+
6
+ require "canvas_oauth/config"
7
+ require "canvas_oauth/canvas_application"
8
+ require 'canvas_oauth/canvas_api'
9
+ require 'canvas_oauth/canvas_api_extensions'
10
+ require 'canvas_oauth/canvas_config'
11
+
12
+ module CanvasOauth
13
+ mattr_accessor :app_root
14
+
15
+ def self.setup
16
+ yield self
17
+ end
18
+
19
+ def self.config
20
+ yield(CanvasOauth::Config)
21
+ end
22
+ end
23
+
24
+ require "canvas_oauth/engine"
25
+
@@ -0,0 +1,249 @@
1
+ module CanvasOauth
2
+ class CanvasApi
3
+ include HTTParty
4
+ PER_PAGE = 50
5
+
6
+ attr_accessor :token, :key, :secret
7
+ attr_reader :canvas_url
8
+
9
+ def initialize(canvas_url, token, key, secret)
10
+ self.canvas_url = canvas_url
11
+ self.token = token
12
+ self.key = key
13
+ self.secret = secret
14
+ end
15
+
16
+ def authenticated_request(method, *params)
17
+ params << {} if params.size == 1
18
+
19
+ params.last[:headers] ||= {}
20
+ params.last[:headers]['Authorization'] = "Bearer #{token}"
21
+
22
+ start = Time.now
23
+
24
+ response = self.class.send(method, *params)
25
+
26
+ Rails.logger.info {
27
+ stop = Time.now
28
+ elapsed = ((stop - start) * 1000).round(2)
29
+
30
+ params.last[:headers].reject! { |k| k == 'Authorization' }
31
+ "API call (#{elapsed}ms): #{method} #{params.inspect}"
32
+ }
33
+
34
+ if response && response.unauthorized?
35
+ if response.headers['WWW-Authenticate'].present?
36
+ raise CanvasApi::Authenticate
37
+ else
38
+ raise CanvasApi::Unauthorized
39
+ end
40
+ else
41
+ return response
42
+ end
43
+ end
44
+
45
+ def paginated_get(*params)
46
+ params[1] ||= {}
47
+ params[1][:query] ||= {}
48
+ params[1][:query][:per_page] = PER_PAGE
49
+
50
+ all_pages = []
51
+
52
+ while params[0] do
53
+ if current_page = authenticated_get(*params)
54
+ all_pages += current_page if valid_page?(current_page)
55
+
56
+ links = LinkHeader.parse(current_page.headers['link'])
57
+ params[0] = links.find_link(["rel", "next"]).try(:href)
58
+ else
59
+ params[0] = nil
60
+ end
61
+ end
62
+
63
+ all_pages
64
+ end
65
+
66
+ def get_report(account_id, report_type, params)
67
+ report = authenticated_post("/api/v1/accounts/#{account_id}/reports/#{report_type}", { body: params })
68
+ report = authenticated_get "/api/v1/accounts/#{account_id}/reports/#{report_type}/#{report['id']}"
69
+ while report['status'] == 'running'
70
+ sleep(4)
71
+ report = authenticated_get "/api/v1/accounts/#{account_id}/reports/#{report_type}/#{report['id']}"
72
+ end
73
+
74
+ if report['status'] == 'complete'
75
+ file_id = report['file_url'].match(/files\/([0-9]+)\/download/)[1]
76
+ file = get_file(file_id)
77
+ return hash_csv(self.class.get(file['url'], limit: 15).parsed_response)
78
+ else
79
+ return report
80
+ end
81
+ end
82
+
83
+ def valid_page?(page)
84
+ page && page.size > 0
85
+ end
86
+
87
+ def get_file(file_id)
88
+ authenticated_get "/api/v1/files/#{file_id}"
89
+ end
90
+
91
+ def get_accounts_provisioning_report(account_id)
92
+ get_report(account_id, :provisioning_csv, 'parameters[accounts]' => true)
93
+ end
94
+
95
+ #Needs to be refactored to somewhere more generic
96
+ def hash_csv(csv_string)
97
+ require 'csv'
98
+
99
+ csv = CSV.parse(csv_string)
100
+ headers = csv.shift
101
+ output = []
102
+
103
+ csv.each do |row|
104
+ hash = {}
105
+ headers.each do |header|
106
+ hash[header] = row.shift.to_s
107
+ end
108
+ output << hash
109
+ end
110
+
111
+ return output
112
+ end
113
+
114
+ def authenticated_get(*params)
115
+ authenticated_request(:get, *params)
116
+ end
117
+
118
+ def authenticated_post(*params)
119
+ authenticated_request(:post, *params)
120
+ end
121
+
122
+ def authenticated_put(*params)
123
+ authenticated_request(:put, *params)
124
+ end
125
+
126
+ def get_courses
127
+ paginated_get "/api/v1/courses"
128
+ end
129
+
130
+ def get_account(account_id)
131
+ authenticated_get "/api/v1/accounts/#{account_id}"
132
+ end
133
+
134
+ def get_account_sub_accounts(account_id)
135
+ paginated_get "/api/v1/accounts/#{account_id}/sub_accounts", { query: { :recursive => true } }
136
+ end
137
+
138
+ def get_account_courses(account_id)
139
+ paginated_get "/api/v1/accounts/#{account_id}/courses"
140
+ end
141
+
142
+ def get_account_users(account_id)
143
+ paginated_get "/api/v1/accounts/#{account_id}/users"
144
+ end
145
+
146
+ def get_course(course_id)
147
+ authenticated_get "/api/v1/courses/#{course_id}"
148
+ end
149
+
150
+ def get_section_enrollments(section_id)
151
+ paginated_get "/api/v1/sections/#{section_id}/enrollments"
152
+ end
153
+
154
+ def get_user_enrollments(user_id)
155
+ paginated_get "/api/v1/users/#{user_id}/enrollments"
156
+ end
157
+
158
+ def get_course_users(course_id)
159
+ paginated_get "/api/v1/courses/#{course_id}/users"
160
+ end
161
+
162
+ def get_all_course_users(course_id)
163
+ paginated_get "/api/v1/courses/#{course_id}/users", { query: {enrollment_state: ["active","invited","rejected","completed","inactive"] } }
164
+ end
165
+
166
+ def get_course_teachers_and_tas(course_id)
167
+ paginated_get "/api/v1/courses/#{course_id}/users", { query: { enrollment_type: ['teacher', 'ta'] } }
168
+ end
169
+
170
+ def get_course_students(course_id)
171
+ paginated_get "/api/v1/courses/#{course_id}/students"
172
+ end
173
+
174
+ def get_section(section_id)
175
+ authenticated_get "/api/v1/sections/#{section_id}"
176
+ end
177
+
178
+ def get_sections(course_id)
179
+ paginated_get "/api/v1/courses/#{course_id}/sections", { query: { :include => ['students', 'avatar_url', 'enrollments'] } }
180
+ end
181
+
182
+ def get_assignments(course_id)
183
+ paginated_get "/api/v1/courses/#{course_id}/assignments"
184
+ end
185
+
186
+ def get_assignment(course_id, assignment_id)
187
+ authenticated_get "/api/v1/courses/#{course_id}/assignments/#{assignment_id}"
188
+ end
189
+
190
+ def get_user_profile(user_id)
191
+ authenticated_get "/api/v1/users/#{user_id}/profile"
192
+ end
193
+
194
+ def create_assignment(course_id, params)
195
+ authenticated_post "/api/v1/courses/#{course_id}/assignments", { body: { assignment: params } }
196
+ end
197
+
198
+ def grade_assignment(course_id, assignment_id, user_id, params)
199
+ authenticated_put "/api/v1/courses/#{course_id}/assignments/#{assignment_id}/submissions/#{user_id}", { body: params }
200
+ end
201
+
202
+ def course_account_id(course_id)
203
+ course = get_course(course_id)
204
+ course['account_id'] if course
205
+ end
206
+
207
+ def root_account_id(account_id)
208
+ if account_id && account = get_account(account_id)
209
+ root_id = account['root_account_id']
210
+ end
211
+
212
+ root_id || account_id
213
+ end
214
+
215
+ def course_root_account_id(course_id)
216
+ root_account_id(course_account_id(course_id))
217
+ end
218
+
219
+ def auth_url(redirect_uri, oauth2_state)
220
+ "#{canvas_url}/login/oauth2/auth?client_id=#{key}&response_type=code&state=#{oauth2_state}&redirect_uri=#{redirect_uri}"
221
+ end
222
+
223
+ def get_access_token(code)
224
+ params = {
225
+ body: {
226
+ client_id: key,
227
+ client_secret: secret,
228
+ code: code
229
+ }
230
+ }
231
+
232
+ response = self.class.post '/login/oauth2/token', params
233
+ self.token = response['access_token']
234
+ end
235
+
236
+ def hex_sis_id(name, value)
237
+ hex = value.unpack("H*")[0]
238
+ return "hex:#{name}:#{hex}"
239
+ end
240
+
241
+ def canvas_url=(value)
242
+ @canvas_url = value
243
+ self.class.base_uri(value)
244
+ end
245
+ end
246
+
247
+ class CanvasApi::Unauthorized < StandardError ; end
248
+ class CanvasApi::Authenticate < StandardError ; end
249
+ end