trainmaster 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +286 -0
  4. data/Rakefile +38 -0
  5. data/app/controllers/trainmaster/application_controller.rb +9 -0
  6. data/app/controllers/trainmaster/sessions_controller.rb +141 -0
  7. data/app/controllers/trainmaster/users_controller.rb +199 -0
  8. data/app/helpers/trainmaster/application_helper.rb +313 -0
  9. data/app/helpers/trainmaster/sessions_helper.rb +4 -0
  10. data/app/helpers/trainmaster/users_helper.rb +4 -0
  11. data/app/jobs/trainmaster/sessions_cleanup_job.rb +13 -0
  12. data/app/mailers/application_mailer.rb +4 -0
  13. data/app/mailers/trainmaster/user_mailer.rb +14 -0
  14. data/app/models/trainmaster/session.rb +56 -0
  15. data/app/models/trainmaster/user.rb +77 -0
  16. data/app/views/layouts/mailer.html.erb +5 -0
  17. data/app/views/layouts/mailer.text.erb +1 -0
  18. data/app/views/layouts/trainmaster/application.html.erb +14 -0
  19. data/app/views/trainmaster/user_mailer/email_verification.html.erb +12 -0
  20. data/app/views/trainmaster/user_mailer/email_verification.text.erb +13 -0
  21. data/app/views/trainmaster/user_mailer/password_reset.html.erb +14 -0
  22. data/app/views/trainmaster/user_mailer/password_reset.text.erb +15 -0
  23. data/config/routes.rb +10 -0
  24. data/db/migrate/20161120020344_create_trainmaster_users.rb +23 -0
  25. data/db/migrate/20161120020722_create_trainmaster_sessions.rb +11 -0
  26. data/lib/tasks/trainmaster_tasks.rake +4 -0
  27. data/lib/trainmaster.rb +10 -0
  28. data/lib/trainmaster/cache.rb +28 -0
  29. data/lib/trainmaster/engine.rb +9 -0
  30. data/lib/trainmaster/roles.rb +12 -0
  31. data/lib/trainmaster/version.rb +3 -0
  32. data/test/controllers/trainmaster/application_controller_test.rb +106 -0
  33. data/test/controllers/trainmaster/sessions_controller_test.rb +275 -0
  34. data/test/controllers/trainmaster/users_controller_test.rb +335 -0
  35. data/test/dummy/README.rdoc +28 -0
  36. data/test/dummy/Rakefile +6 -0
  37. data/test/dummy/app/assets/javascripts/application.js +13 -0
  38. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  39. data/test/dummy/app/controllers/application_controller.rb +5 -0
  40. data/test/dummy/app/helpers/application_helper.rb +2 -0
  41. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  42. data/test/dummy/bin/bundle +3 -0
  43. data/test/dummy/bin/rails +4 -0
  44. data/test/dummy/bin/rake +4 -0
  45. data/test/dummy/bin/setup +29 -0
  46. data/test/dummy/config.ru +4 -0
  47. data/test/dummy/config/application.rb +34 -0
  48. data/test/dummy/config/boot.rb +5 -0
  49. data/test/dummy/config/database.yml +25 -0
  50. data/test/dummy/config/environment.rb +5 -0
  51. data/test/dummy/config/environments/development.rb +41 -0
  52. data/test/dummy/config/environments/production.rb +79 -0
  53. data/test/dummy/config/environments/test.rb +44 -0
  54. data/test/dummy/config/initializers/assets.rb +11 -0
  55. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  56. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  57. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  58. data/test/dummy/config/initializers/inflections.rb +16 -0
  59. data/test/dummy/config/initializers/mime_types.rb +4 -0
  60. data/test/dummy/config/initializers/session_store.rb +3 -0
  61. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  62. data/test/dummy/config/locales/en.yml +23 -0
  63. data/test/dummy/config/routes.rb +4 -0
  64. data/test/dummy/config/secrets.yml +22 -0
  65. data/test/dummy/public/404.html +67 -0
  66. data/test/dummy/public/422.html +67 -0
  67. data/test/dummy/public/500.html +66 -0
  68. data/test/dummy/public/favicon.ico +0 -0
  69. data/test/fixtures/trainmaster/sessions.yml +36 -0
  70. data/test/fixtures/trainmaster/users.yml +27 -0
  71. data/test/integration/navigation_test.rb +10 -0
  72. data/test/jobs/trainmaster/sessions_cleanup_job_test.rb +9 -0
  73. data/test/mailers/previews/trainmaster/user_mailer_preview.rb +6 -0
  74. data/test/mailers/trainmaster/user_mailer_test.rb +9 -0
  75. data/test/models/trainmaster/session_test.rb +26 -0
  76. data/test/models/trainmaster/user_test.rb +52 -0
  77. data/test/test_helper.rb +33 -0
  78. data/test/trainmaster.rb +12 -0
  79. metadata +327 -0
@@ -0,0 +1,199 @@
1
+ require_dependency "trainmaster/application_controller"
2
+
3
+ module Trainmaster
4
+
5
+ ##
6
+ # Users controller that performs CRUD on users.
7
+ #
8
+ class UsersController < ApplicationController
9
+
10
+ # All except user creation requires a session token. Note that reset
11
+ # token is also a legit session token, so :require_token will suffice.
12
+ prepend_before_action :require_auth, only: [:show, :destroy]
13
+ prepend_before_action :accept_auth, only: [:update, :create]
14
+ prepend_before_action :require_admin_auth, only: [:index]
15
+
16
+ # Some actions must have a user specified.
17
+ before_action :get_user, only: [:show, :destroy]
18
+
19
+ ##
20
+ # List all users (but only works for admin user).
21
+ #
22
+ def index
23
+ @users = User.all
24
+ render json: @users, except: [:password_digest]
25
+ end
26
+
27
+ ##
28
+ # Creates a new user. This action does not require any auth although it
29
+ # is optional.
30
+ #
31
+ def create
32
+ logger.debug("Create new user")
33
+ @user = User.new(user_params)
34
+ if @user.save
35
+
36
+ # Save succeeded. Render the response based on the created user.
37
+ render json: @user,
38
+ except: [:verification_token, :reset_token, :password_digest],
39
+ status: 201
40
+
41
+ # Then, issue the verification token and send the email for
42
+ # verification.
43
+ @user.issue_token(:verification_token)
44
+ @user.save
45
+ user_mailer.email_verification(@user).deliver_later
46
+ else
47
+ render_errors 400, @user.errors.full_messages
48
+ end
49
+ end
50
+
51
+ ##
52
+ # Renders a user data.
53
+ #
54
+ def show
55
+ render json: @user, except: [:password_digest]
56
+ end
57
+
58
+ ##
59
+ # Patches the user object. There are four notable operations:
60
+ #
61
+ # - issue reset token
62
+ # - issue verification token
63
+ # - change password
64
+ # - others
65
+ #
66
+ # Issuing either reset token or verification token requires NO
67
+ # authentication. However, for that reason, the request does not get any
68
+ # meaningful response. Instead, an email is sent out for either request.
69
+ #
70
+ # For changing password, there are two ways. One is to use old password
71
+ # and the other is to use reset token.
72
+ #
73
+ # Otherwise, it's a normal update operation.
74
+ #
75
+ def update
76
+ if params[:issue_reset_token] || params[:issue_verification_token]
77
+ # For issuing a reset token, one does not need an auth token. so do
78
+ # not authorize the request. For consistency, we require the id to
79
+ # be "current".
80
+ raise ApplicationController::UNAUTHORIZED_ERROR unless params[:id] == "current"
81
+ get_user_for_token()
82
+ if params[:issue_reset_token]
83
+ update_token(:reset_token)
84
+ else
85
+ update_token(:verification_token)
86
+ end
87
+ else
88
+ get_user()
89
+ allow_password_change? if params[:password]
90
+ update_user(user_params)
91
+ end
92
+ end
93
+
94
+ ##
95
+ # Deletes a user.
96
+ #
97
+ def destroy
98
+ if @user.destroy
99
+ render body: '', status: 204
100
+ else
101
+ # :nocov:
102
+ render_error 400, @user.errors.full_messages
103
+ # :nocov:
104
+ end
105
+ end
106
+
107
+ protected
108
+
109
+ ##
110
+ # Override this method to app specific mailer.
111
+ #
112
+ def user_mailer
113
+ return UserMailer
114
+ end
115
+
116
+ ##
117
+ # Check if password change should be allowed. Two ways to do this: one
118
+ # is to use old password or to use a valid reset token.
119
+ #
120
+ # A ApplicationController::UNAUTHORIZED_ERROR is thrown for invalid old password or
121
+ # invalid reset token
122
+ #
123
+ def allow_password_change?
124
+ if params[:old_password]
125
+ unless @user.authenticate(params[:old_password])
126
+ raise ApplicationController::UNAUTHORIZED_ERROR
127
+ end
128
+ else
129
+ unless @token == @user.reset_token
130
+ raise ApplicationController::UNAUTHORIZED_ERROR
131
+ end
132
+ end
133
+ return true
134
+ end
135
+
136
+ ##
137
+ # This method normally updates the user using permitted params.
138
+ #
139
+ def update_user(update_user_params)
140
+ if @user.update_attributes(update_user_params)
141
+ render json: @user, except: [:password_digest]
142
+ else
143
+ render_errors 400, @user.errors.full_messages
144
+ end
145
+ end
146
+
147
+ ##
148
+ # This method updates user with a new reset token. Only used for this
149
+ # operation.
150
+ #
151
+ def update_token(kind)
152
+ @user.issue_token(kind)
153
+ @user.save
154
+ if kind == :reset_token
155
+ user_mailer.password_reset(@user).deliver_later
156
+ else
157
+ user_mailer.email_verification(@user).deliver_later
158
+ end
159
+ render body: '', status: 204
160
+ end
161
+
162
+ private
163
+
164
+ ##
165
+ # This overrides the application controller's get_user method. Since
166
+ # resource object of this users controller is user, the id is
167
+ # specified in :id param.
168
+ #
169
+ def get_user
170
+ if params[:id] == "current"
171
+ raise ApplicationController::UNAUTHORIZED_ERROR if @auth_user.nil?
172
+ params[:id] = @auth_user.uuid
173
+ end
174
+ @user = find_object(User, params[:id])
175
+ authorize_for!(@user)
176
+ return @user
177
+ end
178
+
179
+ ##
180
+ # For issuing a new reset or for re-issuing a verification token, use
181
+ # this method to get user.
182
+ #
183
+ def get_user_for_token
184
+ @user = User.find_by_username(params[:username])
185
+ raise Repia::Errors::NotFound if @user.nil?
186
+ return @user
187
+ end
188
+
189
+ def user_params
190
+ # Only ADMIN can assign the attribute role. The attribute value will
191
+ # be ignored if the user is not an ADMIN.
192
+ if has_admin_auth?
193
+ params.permit(:username, :password, :password_confirmation, :role, :verified)
194
+ else
195
+ params.permit(:username, :password, :password_confirmation, :verified)
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,313 @@
1
+ module Trainmaster
2
+ module ApplicationHelper
3
+ include Repia::Helper::Base
4
+
5
+ # Respect the config first. If not specified, use 401 Unauthorized.
6
+ UNAUTHORIZED_ERROR = Rails.application.config.try(:unauthorized_error) ||
7
+ Repia::Errors::Unauthorized
8
+
9
+ ##
10
+ # Determines if the authenticated user is admin or not.
11
+ #
12
+ def has_admin_auth?
13
+ return instance_variable_defined?(:@auth_user) &&
14
+ @auth_user.try(:role).try(:>=, Roles::ADMIN)
15
+ end
16
+
17
+ ##
18
+ # Helper method to get the user object in the request, which is
19
+ # specified by :user_id parameter. There are two ways to specify the
20
+ # user id--one in the routing or the auth context.
21
+ #
22
+ # A UNAUTHORIZED_ERROR is raised if the authenticated user is
23
+ # not authorized for the specified user information.
24
+ #
25
+ # A Repia::Errors::NotFound is raised if the specified user cannot
26
+ # be found.
27
+ #
28
+ def get_user(fallback: true)
29
+ user_id = params[:user_id]
30
+ logger.debug("Attempting to get user #{user_id}")
31
+ if !user_id.nil? && user_id != "current"
32
+ @user = find_object(User, params[:user_id]) # will throw error if nil
33
+ authorize_for!(@user)
34
+ elsif fallback || user_id == "current"
35
+ @user = @auth_user
36
+ else
37
+ # :nocov:
38
+ raise Repia::Errors::NotFound, "User #{user_id} does not exist"
39
+ # :nocov:
40
+ end
41
+ end
42
+
43
+ ##
44
+ # :method: require_auth
45
+ #
46
+ # Requires authentication. Either token or api key must be present.
47
+ #
48
+
49
+ ##
50
+ # :method: require_admin_auth
51
+ #
52
+ # Requires admin authentication. Either token or api key of admin must
53
+ # be present.
54
+ #
55
+
56
+ ##
57
+ # :method: accept_auth
58
+ #
59
+ # Accepts authentication if present. Either token or api key is accepted.
60
+ #
61
+
62
+ ##
63
+ # :method: require_token
64
+ #
65
+ # Requires authentication. Token must be present.
66
+ #
67
+
68
+ ##
69
+ # :method: require_admin_token
70
+ #
71
+ # Requires admin authentication. Admin token must # be present.
72
+ #
73
+
74
+ ##
75
+ # :method: accept_token
76
+ #
77
+ # Accepts authentication if present. Only token is accepted.
78
+ #
79
+ ##
80
+ # :method: require_api_key
81
+ #
82
+ # Requires authentication. API key must be present.
83
+ #
84
+
85
+ ##
86
+ # :method: require_admin_api_key
87
+ #
88
+ # Requires admin authentication. Admin api key must be present.
89
+ #
90
+
91
+ ##
92
+ # :method: accept_api_key
93
+ #
94
+ # Accepts authentication if present. Only api key is accepted.
95
+ #
96
+
97
+ #
98
+ # Metaprogramming baby
99
+ #
100
+ ["auth", "token", "api_key"].each do |suffix|
101
+
102
+ define_method "require_#{suffix}" do
103
+ self.method("get_#{suffix}").call
104
+ end
105
+
106
+ define_method "require_admin_#{suffix}" do
107
+ self.method("get_#{suffix}").call(required_role: Roles::ADMIN)
108
+ end
109
+
110
+ define_method "accept_#{suffix}" do
111
+ begin
112
+ self.method("get_#{suffix}").call
113
+ rescue StandardError
114
+ logger.debug("Suppressing error")
115
+ return false
116
+ end
117
+ end
118
+ end
119
+
120
+ ##
121
+ # Determines if the user is authorized for the object. The user must be
122
+ # either the creator of the object or must be an admin or above.
123
+ #
124
+ def authorized_for?(obj)
125
+ logger.debug("Checking to see if authorized to access object")
126
+ if @auth_user.nil?
127
+ # :nocov:
128
+ return false
129
+ # :nocov:
130
+ elsif @auth_user.role >= Roles::ADMIN
131
+ return true
132
+ elsif obj.is_a? User
133
+ return obj == @auth_user
134
+ else
135
+ return obj.try(:user) == @auth_user
136
+ end
137
+ end
138
+ alias_method :authorize_for?, :authorized_for?
139
+
140
+ ##
141
+ # Deprecated: use authorized_for? instead.
142
+ #
143
+ def authorized?(obj); authorized_for?(obj) end
144
+
145
+ ##
146
+ # Authorize the user for a specified object. If the user does not have
147
+ # permission, it will throw an exception. Note that it is sometimes not
148
+ # desirable to provide detailed information about authorization failure.
149
+ # Note that this will not include this detail in the exception.
150
+ #
151
+ # A UNAUTHORIZED_ERROR is raised.
152
+ #
153
+ def authorize_for!(obj)
154
+ if !authorized_for?(obj)
155
+ logger.error("User #{@auth_user.uuid} does not have permission " +
156
+ "to access #{obj}")
157
+ raise UNAUTHORIZED_ERROR, "User is not authorized"
158
+ end
159
+ end
160
+
161
+ protected
162
+
163
+ ##
164
+ # Attempts to retrieve the payload encoded in the token. It checks if
165
+ # the token is "valid" according to JWT definition and not expired.
166
+ #
167
+ # A UNAUTHORIZED_ERROR is raised if token cannot be decoded.
168
+ #
169
+ def get_token_payload(token)
170
+
171
+ # Attempt to decode without verifying. May raise DecodeError.
172
+ decoded = JWT.decode token, nil, false
173
+ payload = decoded[0]
174
+
175
+ # At this point, we know that the token is not expired and
176
+ # well formatted. Find out if the payload is well defined.
177
+ if payload.nil?
178
+ # :nocov:
179
+ logger.error("Token payload is nil: #{token}")
180
+ raise UNAUTHORIZED_ERROR, "Invalid token"
181
+ # :nocov:
182
+ end
183
+
184
+ return payload
185
+
186
+ rescue JWT::DecodeError => e
187
+ logger.error("Token decode error: #{e.message}")
188
+ raise UNAUTHORIZED_ERROR, "Invalid token"
189
+ end
190
+
191
+ ##
192
+ # Truly verifies the token and its payload. It ensures the user and
193
+ # session specified in the token payload are indeed valid. The
194
+ # required role is also checked.
195
+ #
196
+ # A UNAUTHORIZED_ERROR is thrown for all cases where token is
197
+ # invalid.
198
+ #
199
+ def verify_token(token)
200
+ logger.debug("Verifying token: #{token}")
201
+
202
+ # First get the payload of the token. This will also verify whether
203
+ # or not the token is welformed.
204
+ payload = get_token_payload(token)
205
+
206
+ # Next, the payload should define user UUID and session UUID.
207
+ user_uuid = payload["user_uuid"]
208
+ session_uuid = payload["session_uuid"]
209
+ if user_uuid.nil? || session_uuid.nil?
210
+ logger.error("User or session is not specified")
211
+ raise UNAUTHORIZED_ERROR, "Invalid token"
212
+ end
213
+ logger.debug("Token well defined: #{token}")
214
+
215
+ # But, the user UUID and session UUID better be valid too. That is,
216
+ # they must be real user and session, and the session must belong to
217
+ # the user.
218
+ auth_user = User.find_by_uuid(user_uuid)
219
+ if auth_user.nil?
220
+ # :nocov:
221
+ logger.error("Specified user doesn't exist #{user_uuid}")
222
+ raise UNAUTHORIZED_ERROR, "Invalid token"
223
+ # :nocov:
224
+ end
225
+ auth_session = Session.find_by_uuid(session_uuid)
226
+ if auth_session.nil? || auth_session.user != auth_user
227
+ logger.error("Specified session doesn't exist #{session_uuid}")
228
+ raise UNAUTHORIZED_ERROR, "Invalid token"
229
+ end
230
+
231
+ # Finally, decode the token using the secret. Also check expiration
232
+ # here too.
233
+ JWT.decode token, auth_session.secret, true
234
+ logger.debug("Token well formatted and verified. Set cache.")
235
+
236
+ # Return the corresponding session
237
+ return auth_session
238
+
239
+ rescue JWT::DecodeError => e
240
+ logger.error(e.message)
241
+ raise UNAUTHORIZED_ERROR, "Invalid token"
242
+ end
243
+
244
+ ##
245
+ # Attempt to get a token for the session. Token must be specified in
246
+ # query string or part of the JSON object.
247
+ #
248
+ # Raises a UNAUTHORIZED_ERROR if cached session has less role
249
+ # than what's required.
250
+ #
251
+ def get_token(required_role: Roles::PUBLIC)
252
+ token = params[:token]
253
+
254
+ # Look up the cache. If present, use it and skip the verification.
255
+ # Use token itself (and not a session UUID) as part of the key so
256
+ # it can be considered *verified*.
257
+ @auth_session = Cache.get(kind: :session, token: token)
258
+
259
+ # Cache miss. So proceed to verify the token and get user and
260
+ # session data from database. Then set the cache for later.
261
+ if @auth_session.nil?
262
+ @auth_session = verify_token(token)
263
+ @auth_session.role # NOTE: no-op
264
+ Cache.set({kind: :session, token: token}, @auth_session)
265
+ end
266
+
267
+ # Obtained session may not have enough permission. Check here.
268
+ if @auth_session.role < required_role
269
+ logger.error("Not enough permission (role: #{@auth_session.role})")
270
+ raise UNAUTHORIZED_ERROR, "Invalid token"
271
+ end
272
+ @auth_user = @auth_session.user
273
+ @token = @auth_session.token
274
+ return true
275
+ end
276
+
277
+ ##
278
+ # Get API key from the request.
279
+ #
280
+ # Raises a UNAUTHORIZED_ERROR if API key is not valid (or not
281
+ # provided).
282
+ #
283
+ def get_api_key(required_role: Roles::PUBLIC)
284
+ api_key = params[:api_key]
285
+ if api_key.nil?
286
+ # This case is not likely, but as a safeguard in case migration
287
+ # has not gone well.
288
+ # :nocov:
289
+ raise UNAUTHORIZED_ERROR, "Invalid api key"
290
+ # :nocov:
291
+ end
292
+ auth_user = User.find_by_api_key(api_key)
293
+ if auth_user.nil? || auth_user.role < required_role
294
+ raise UNAUTHORIZED_ERROR, "Invalid api key"
295
+ end
296
+ @auth_user = auth_user
297
+ @auth_session = nil
298
+ @token = nil
299
+ return true
300
+ end
301
+
302
+ ##
303
+ # Get auth data from the request. The token takes the precedence.
304
+ #
305
+ def get_auth(required_role: Roles::USER)
306
+ if params[:token]
307
+ get_token(required_role: required_role)
308
+ else
309
+ get_api_key(required_role: required_role)
310
+ end
311
+ end
312
+ end
313
+ end