softwear-lib 1.7.0 → 1.7.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3e9245f4938f0485bf4003d9ddaa38b8d81c2bc3
4
- data.tar.gz: bd3b86ff184a2ecc9c5e3de2f0f42e483387ee6d
3
+ metadata.gz: 46309929f4ab433f39e2249d28c52de5083e9c0c
4
+ data.tar.gz: 214c83ab78624f336433d4ea95c12664a0b1de47
5
5
  SHA512:
6
- metadata.gz: e6c38fe0ee80ca9c9d507020d24f580b4f9748bd88241cf557059529d0ed3933ecb5e94de57be7a6a3f33007110a0a477b3991b7e2dae0d20561041b894a3ebc
7
- data.tar.gz: 89600a8a7de982f5d195574ab38563b20d233d43c26cfd07c34dd45ee0fa56bdb936297e67dda6e3c9aeb528f390b2a554bb842617114ae9543be3f69ab57faf
6
+ metadata.gz: 6294137fd3a37d5881187f5bddeecc78a34a5dfe957f1aca785bc94454f9ec62f196ff88ed42712e6d1ac8b7b94825c5f5f17fb5fb63dcfba50fa20fc4e23d56
7
+ data.tar.gz: e59c12a9c183fb7fb1de46f33d9741bb2f9b6c5b5b0299c0330ea00f3c89297bedeec5fc6cee311976d71766b62eee897d500f16bee62f2ca38ab4558033fedb
@@ -1,470 +1,13 @@
1
+ require 'softwear/auth/standard_model'
2
+ require 'softwear/auth/stubbed_model'
3
+
1
4
  module Softwear
2
5
  module Auth
3
- class Model
4
- include ActiveModel::Model
5
- include ActiveModel::Conversion
6
-
7
- class AccessDeniedError < StandardError
8
- end
9
- class InvalidCommandError < StandardError
6
+ if Rails.env.development? && ENV['AUTH_SERVER'].blank?
7
+ class Model < StubbedModel
10
8
  end
11
- class AuthServerError < StandardError
12
- end
13
- class AuthServerDown < StandardError
14
- end
15
-
16
- # ============================= CLASS METHODS ======================
17
- class << self
18
- def abstract_class?
19
- true
20
- end
21
-
22
- attr_writer :query_cache
23
- attr_accessor :total_query_cache
24
- attr_writer :query_cache_expiry
25
- alias_method :expire_query_cache_every, :query_cache_expiry=
26
- attr_accessor :auth_server_went_down_at
27
- attr_accessor :sent_auth_server_down_email
28
- attr_accessor :time_before_down_email
29
- alias_method :email_when_down_after, :time_before_down_email=
30
-
31
- # ====================
32
- # Returns true if the authentication server was unreachable for the previous query.
33
- # ====================
34
- def auth_server_down?
35
- !!auth_server_went_down_at
36
- end
37
-
38
- # ====================
39
- # The query cache takes message keys (such as "get 12") with response values straight from
40
- # the server. So yes, this will cache error responses.
41
- # You can clear this with <User Class>.query_cache.clear or <User Class>.query_cache = nil
42
- # ====================
43
- def query_cache
44
- @query_cache ||= ThreadSafe::Cache.new
45
- end
46
-
47
- def query_cache_expiry
48
- @query_cache_expiry || Figaro.env.query_cache_expiry.try(:to_f) || 1.hour
49
- end
50
-
51
- # ===================
52
- # Override this in your subclasses! The mailer should have auth_server_down(time) and
53
- # auth_server_up(time)
54
- # ===================
55
- def auth_server_down_mailer
56
- # override me
57
- end
58
-
59
- # ======================================
60
- def primary_key
61
- :id
62
- end
63
-
64
- def base_class
65
- self
66
- end
67
-
68
- def relation_delegate_class(*)
69
- self
70
- end
71
-
72
- def unscoped
73
- self
74
- end
75
-
76
- def new(*args)
77
- if args.size == 3
78
- assoc_class = args[2].owner.class.name
79
- assoc_name = args[2].reflection.name
80
- raise "Unsupported user association: #{assoc_class}##{assoc_name}. If this is a belongs_to "\
81
- "association, you may have #{assoc_class} include Softwear::Auth::BelongsToUser and call "\
82
- "`belongs_to_user_called :#{assoc_name}' instead of the traditional rails method."
83
- else
84
- super
85
- end
86
- end
87
- # ======================================
88
-
89
- # ====================
90
- # Not a fully featured has_many - must specify foreign_key if the association doesn't match
91
- # the model name, through is inefficient.
92
- # ====================
93
- def has_many(assoc, options = {})
94
- assoc = assoc.to_s
95
-
96
- if through = options[:through]
97
- source = options[:source] || assoc
98
-
99
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
100
- def #{assoc}
101
- #{through}.flat_map(&:#{source})
102
- end
103
- RUBY
104
-
105
- else
106
- class_name = options[:class_name] || assoc.singularize.camelize
107
- foreign_key = options[:foreign_key] || 'user_id'
108
-
109
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
110
- def #{assoc}
111
- #{class_name}.where(#{foreign_key}: id)
112
- end
113
- RUBY
114
- end
115
- end
116
-
117
- # ====================
118
- # Pretty much a map function - for activerecord compatibility.
119
- # ====================
120
- def pluck(*attrs)
121
- if attrs.size == 1
122
- all.map do |user|
123
- user.send(attrs.first)
124
- end
125
- else
126
- all.map do |user|
127
- attrs.map { |a| user.send(a) }
128
- end
129
- end
130
- end
131
-
132
- def arel_table
133
- @arel_table ||= Arel::Table.new(model_name.plural, self)
134
- end
135
-
136
- # ====================
137
- # This is only used to record how long it takes to perform queries for development.
138
- # ====================
139
- def record(before, after, type, body)
140
- ms = (after - before) * 1000
141
- # The garbage in this string gives us the bold and color
142
- Rails.logger.info " \033[1m\033[33m#{type} (#{'%.1f' % ms}ms)\033[0m #{body}"
143
- end
144
-
145
- # ====================
146
- # Host of the auth server, from 'auth_server_endpoint' env variable.
147
- # Defaults to localhost.
148
- # ====================
149
- def auth_server_host
150
- endpoint = Figaro.env.auth_server_endpoint
151
- if endpoint.blank?
152
- 'localhost'
153
- elsif endpoint.include?(':')
154
- endpoint.split(':').first
155
- else
156
- endpoint
157
- end
158
- end
159
-
160
- # ====================
161
- # Port of the auth server, from 'auth_server_endpoint' env variable.
162
- # Defaults to 2900.
163
- # ====================
164
- def auth_server_port
165
- endpoint = Figaro.env.auth_server_endpoint
166
- if endpoint.try(:include?, ':')
167
- endpoint.split(':').last
168
- else
169
- 2900
170
- end
171
- end
172
-
173
- def default_socket
174
- @default_socket ||= TCPSocket.open(auth_server_host, auth_server_port)
175
- end
176
-
177
- # ====================
178
- # Bare minimum query function - sends a message and returns the response, and
179
- # handles a broken socket. #query and #force_query call this function.
180
- # ====================
181
- def raw_query(message)
182
- begin
183
- default_socket.puts message
184
-
185
- rescue Errno::EPIPE => e
186
- @default_socket = TCPSocket.open(auth_server_host, auth_server_port)
187
- @default_socket.puts message
188
- end
189
-
190
- response = default_socket.gets.try(:chomp)
191
- if response.nil?
192
- @default_socket.close rescue nil
193
- @default_socket = nil
194
- return raw_query(message)
195
- end
196
- response
197
-
198
- rescue Errno::ECONNREFUSED => e
199
- raise AuthServerDown, "Unable to connect to the authentication server."
200
-
201
- rescue Errno::ETIMEDOUT => e
202
- raise AuthServerDown, "Connection to authentication server timed out."
203
- end
204
-
205
- # ====================
206
- # Expires the query cache, setting a new expiration time as well as merging
207
- # with the previous query cache, in case of an auth server outage.
208
- # ====================
209
- def expire_query_cache
210
- before = Time.now
211
- if total_query_cache
212
- query_cache.each_pair do |key, value|
213
- total_query_cache[key] = value
214
- end
215
- else
216
- self.total_query_cache = query_cache.clone
217
- end
218
-
219
- query_cache.clear
220
- query_cache['_expire_at'] = (query_cache_expiry || 1.hour).from_now
221
- after = Time.now
222
-
223
- record(before, after, "Authentication Expire Cache", "")
224
- end
225
-
226
- # ====================
227
- # Queries the authentication server only if there isn't a cached response.
228
- # Also keeps track of whether or not the server is reachable, and sends emails
229
- # when the server goes down and back up.
230
- # ====================
231
- def query(message)
232
- before = Time.now
233
-
234
- expire_at = query_cache['_expire_at']
235
- expire_query_cache if expire_at.blank? || Time.now > expire_at
236
-
237
- if cached_response = query_cache[message]
238
- response = cached_response
239
- action = "Authentication Cache"
240
- else
241
- begin
242
- response = raw_query(message)
243
- action = "Authentication Query"
244
- query_cache[message] = response
245
-
246
- if auth_server_went_down_at
247
- self.auth_server_went_down_at = nil
248
-
249
- if sent_auth_server_down_email
250
- self.sent_auth_server_down_email = false
251
- if (mailer = auth_server_down_mailer) && mailer.respond_to?(:auth_server_up)
252
- mailer.auth_server_up(Time.now).deliver_now
253
- end
254
- end
255
- end
256
-
257
- rescue AuthServerError => e
258
- raise unless total_query_cache
259
-
260
- old_response = total_query_cache[message]
261
- if old_response
262
- response = old_response
263
- action = "Authentication Cache (due to error)"
264
- Rails.logger.error "AUTHENTICATION: The authentication server encountered an error. "\
265
- "You should probably check the auth server's logs. "\
266
- "A cached response was used."
267
- else
268
- raise
269
- end
270
-
271
- rescue AuthServerDown => e
272
- if auth_server_went_down_at.nil?
273
- self.auth_server_went_down_at = Time.now
274
- expire_query_cache
275
-
276
- elsif auth_server_went_down_at > (time_before_down_email || 5.minutes).ago
277
- unless sent_auth_server_down_email
278
- self.sent_auth_server_down_email = true
279
-
280
- if (mailer = auth_server_down_mailer) && mailer.respond_to?(:auth_server_down)
281
- mailer.auth_server_down(auth_server_went_down_at).deliver_now
282
- end
283
- end
284
- end
285
-
286
- old_response = total_query_cache[message]
287
- if old_response
288
- response = old_response
289
- action = "Authentication Cache (server down)"
290
- else
291
- raise AuthServerDown, "An uncached query was attempted, and the authentication server is down."
292
- end
293
- end
294
- end
295
- after = Time.now
296
-
297
- record(before, after, action, message)
298
- response
299
- end
300
-
301
- # ====================
302
- # Runs a query through the server without error or cache checking.
303
- # ====================
304
- def force_query(message)
305
- before = Time.now
306
- response = raw_query(message)
307
- after = Time.now
308
-
309
- record(before, after, "Authentication Query (forced)", message)
310
- response
311
- end
312
-
313
- # ====================
314
- # Expects a response string returned from #query and raises an error for the
315
- # following cases:
316
- #
317
- # - Access denied (AccessDeniedError)
318
- # - Invalid command (bad query message) (InvalidCommandError)
319
- # - Error on auth server's side (AuthServerError)
320
- # ====================
321
- def validate_response(response_string)
322
- case response_string
323
- when 'denied' then raise AccessDeniedError, "Denied"
324
- when 'invalid' then raise InvalidCommandError, "Invalid command"
325
- when 'sorry'
326
- expire_query_cache
327
- raise AuthServerError, "Authentication server encountered an error"
328
- else
329
- response_string
330
- end
331
- end
332
-
333
- # ====================
334
- # Finds a user with the given ID
335
- # ====================
336
- def find(target_id)
337
- json = validate_response query "get #{target_id}"
338
-
339
- if json == 'nosuchuser'
340
- nil
341
- else
342
- object = new(JSON.parse(json))
343
- object.instance_variable_set(:@persisted, true)
344
- object
345
- end
346
- end
347
-
348
- def filter_all(method, options)
349
- all.send(method) do |user|
350
- options.all? { |field, wanted_value| user.send(field) == wanted_value }
351
- end
352
- end
353
-
354
- # ====================
355
- # Finds a user with the given attributes (just queries for 'all' and uses ruby filters)
356
- # ====================
357
- def find_by(options)
358
- filter_all(:find, options)
359
- end
360
-
361
- # ====================
362
- # Finds users with the given attributes (just queries for 'all' and uses ruby filters)
363
- # ====================
364
- def where(options)
365
- filter_all(:select, options)
366
- end
367
-
368
- # ====================
369
- # Returns an array of all registered users
370
- # ====================
371
- def all
372
- json = validate_response query "all"
373
-
374
- objects = JSON.parse(json).map(&method(:new))
375
- objects.each { |u| u.instance_variable_set(:@persisted, true) }
376
- objects
377
- end
378
-
379
- # ====================
380
- # Given a valid signin token:
381
- # Returns the authenticated user for the given token
382
- # Given an invalid signin token:
383
- # Returns false
384
- # ====================
385
- def auth(token)
386
- response = validate_response query "auth #{Figaro.env.hub_app_name} #{token}"
387
-
388
- return false unless response =~ /^yes .+$/
389
-
390
- _yes, json = response.split(' ', 2)
391
- object = new(JSON.parse(json))
392
- object.instance_variable_set(:@persisted, true)
393
- object
394
- end
395
-
396
- # ====================
397
- # Overridable logger method used when recording query benchmarks
398
- # ====================
399
- def logger
400
- Rails.logger
401
- end
402
- end
403
-
404
- # ============================= INSTANCE METHODS ======================
405
-
406
- REMOTE_ATTRIBUTES = [
407
- :id, :email, :first_name, :last_name,
408
- :profile_picture_url
409
- ]
410
- REMOTE_ATTRIBUTES.each(&method(:attr_accessor))
411
-
412
- attr_reader :persisted
413
- alias_method :persisted?, :persisted
414
-
415
- # ====================
416
- # Various class methods accessible on instances
417
- def query(*a)
418
- self.class.query(*a)
419
- end
420
- def raw_query(*a)
421
- self.class.raw_query(*a)
422
- end
423
- def force_query(*a)
424
- self.class.force_query(*a)
425
- end
426
- def logger
427
- self.class.logger
428
- end
429
- # ====================
430
-
431
- def initialize(attributes = {})
432
- update_attributes(attributes)
433
- end
434
-
435
- def update_attributes(attributes={})
436
- return if attributes.blank?
437
- attributes = attributes.with_indifferent_access
438
-
439
- REMOTE_ATTRIBUTES.each do |attr|
440
- instance_variable_set("@#{attr}", attributes[attr])
441
- end
442
- end
443
-
444
- def to_json
445
- {
446
- id: @id,
447
- email: @email,
448
- first_name: @first_name,
449
- last_name: @last_name
450
- }
451
- .to_json
452
- end
453
-
454
- def reload
455
- json = validate_response query "get #{id}"
456
-
457
- update_attributes(JSON.parse(json))
458
- @persisted = true
459
- self
460
- end
461
-
462
- def full_name
463
- "#{@first_name} #{@last_name}"
464
- end
465
-
466
- def valid_password?(pass)
467
- query("pass #{id} #{pass}") == 'yes'
9
+ else
10
+ class Model < StandardModel
468
11
  end
469
12
  end
470
13
  end
@@ -0,0 +1,471 @@
1
+ module Softwear
2
+ module Auth
3
+ class StandardModel
4
+ include ActiveModel::Model
5
+ include ActiveModel::Conversion
6
+
7
+ class AccessDeniedError < StandardError
8
+ end
9
+ class InvalidCommandError < StandardError
10
+ end
11
+ class AuthServerError < StandardError
12
+ end
13
+ class AuthServerDown < StandardError
14
+ end
15
+
16
+ # ============================= CLASS METHODS ======================
17
+ class << self
18
+ def abstract_class?
19
+ true
20
+ end
21
+
22
+ attr_writer :query_cache
23
+ attr_accessor :total_query_cache
24
+ attr_writer :query_cache_expiry
25
+ alias_method :expire_query_cache_every, :query_cache_expiry=
26
+ attr_accessor :auth_server_went_down_at
27
+ attr_accessor :sent_auth_server_down_email
28
+ attr_accessor :time_before_down_email
29
+ alias_method :email_when_down_after, :time_before_down_email=
30
+
31
+ # ====================
32
+ # Returns true if the authentication server was unreachable for the previous query.
33
+ # ====================
34
+ def auth_server_down?
35
+ !!auth_server_went_down_at
36
+ end
37
+
38
+ # ====================
39
+ # The query cache takes message keys (such as "get 12") with response values straight from
40
+ # the server. So yes, this will cache error responses.
41
+ # You can clear this with <User Class>.query_cache.clear or <User Class>.query_cache = nil
42
+ # ====================
43
+ def query_cache
44
+ @query_cache ||= ThreadSafe::Cache.new
45
+ end
46
+
47
+ def query_cache_expiry
48
+ @query_cache_expiry || Figaro.env.query_cache_expiry.try(:to_f) || 1.hour
49
+ end
50
+
51
+ # ===================
52
+ # Override this in your subclasses! The mailer should have auth_server_down(time) and
53
+ # auth_server_up(time)
54
+ # ===================
55
+ def auth_server_down_mailer
56
+ # override me
57
+ end
58
+
59
+ # ======================================
60
+ def primary_key
61
+ :id
62
+ end
63
+
64
+ def base_class
65
+ self
66
+ end
67
+
68
+ def relation_delegate_class(*)
69
+ self
70
+ end
71
+
72
+ def unscoped
73
+ self
74
+ end
75
+
76
+ def new(*args)
77
+ if args.size == 3
78
+ assoc_class = args[2].owner.class.name
79
+ assoc_name = args[2].reflection.name
80
+ raise "Unsupported user association: #{assoc_class}##{assoc_name}. If this is a belongs_to "\
81
+ "association, you may have #{assoc_class} include Softwear::Auth::BelongsToUser and call "\
82
+ "`belongs_to_user_called :#{assoc_name}' instead of the traditional rails method."
83
+ else
84
+ super
85
+ end
86
+ end
87
+ # ======================================
88
+
89
+ # ====================
90
+ # Not a fully featured has_many - must specify foreign_key if the association doesn't match
91
+ # the model name, through is inefficient.
92
+ # ====================
93
+ def has_many(assoc, options = {})
94
+ assoc = assoc.to_s
95
+
96
+ if through = options[:through]
97
+ source = options[:source] || assoc
98
+
99
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
100
+ def #{assoc}
101
+ #{through}.flat_map(&:#{source})
102
+ end
103
+ RUBY
104
+
105
+ else
106
+ class_name = options[:class_name] || assoc.singularize.camelize
107
+ foreign_key = options[:foreign_key] || 'user_id'
108
+
109
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
110
+ def #{assoc}
111
+ #{class_name}.where(#{foreign_key}: id)
112
+ end
113
+ RUBY
114
+ end
115
+ end
116
+
117
+ # ====================
118
+ # Pretty much a map function - for activerecord compatibility.
119
+ # ====================
120
+ def pluck(*attrs)
121
+ if attrs.size == 1
122
+ all.map do |user|
123
+ user.send(attrs.first)
124
+ end
125
+ else
126
+ all.map do |user|
127
+ attrs.map { |a| user.send(a) }
128
+ end
129
+ end
130
+ end
131
+
132
+ def arel_table
133
+ @arel_table ||= Arel::Table.new(model_name.plural, self)
134
+ end
135
+
136
+ # ====================
137
+ # This is only used to record how long it takes to perform queries for development.
138
+ # ====================
139
+ def record(before, after, type, body)
140
+ ms = (after - before) * 1000
141
+ # The garbage in this string gives us the bold and color
142
+ Rails.logger.info " \033[1m\033[33m#{type} (#{'%.1f' % ms}ms)\033[0m #{body}"
143
+ end
144
+
145
+ # ====================
146
+ # Host of the auth server, from 'auth_server_endpoint' env variable.
147
+ # Defaults to localhost.
148
+ # ====================
149
+ def auth_server_host
150
+ endpoint = Figaro.env.auth_server_endpoint
151
+ if endpoint.blank?
152
+ 'localhost'
153
+ elsif endpoint.include?(':')
154
+ endpoint.split(':').first
155
+ else
156
+ endpoint
157
+ end
158
+ end
159
+
160
+ # ====================
161
+ # Port of the auth server, from 'auth_server_endpoint' env variable.
162
+ # Defaults to 2900.
163
+ # ====================
164
+ def auth_server_port
165
+ endpoint = Figaro.env.auth_server_endpoint
166
+ if endpoint.try(:include?, ':')
167
+ endpoint.split(':').last
168
+ else
169
+ 2900
170
+ end
171
+ end
172
+
173
+ def default_socket
174
+ @default_socket ||= TCPSocket.open(auth_server_host, auth_server_port)
175
+ end
176
+
177
+ # ====================
178
+ # Bare minimum query function - sends a message and returns the response, and
179
+ # handles a broken socket. #query and #force_query call this function.
180
+ # ====================
181
+ def raw_query(message)
182
+ begin
183
+ default_socket.puts message
184
+
185
+ rescue Errno::EPIPE => e
186
+ @default_socket = TCPSocket.open(auth_server_host, auth_server_port)
187
+ @default_socket.puts message
188
+ end
189
+
190
+ response = default_socket.gets.try(:chomp)
191
+ if response.nil?
192
+ @default_socket.close rescue nil
193
+ @default_socket = nil
194
+ return raw_query(message)
195
+ end
196
+ response
197
+
198
+ rescue Errno::ECONNREFUSED => e
199
+ raise AuthServerDown, "Unable to connect to the authentication server."
200
+
201
+ rescue Errno::ETIMEDOUT => e
202
+ raise AuthServerDown, "Connection to authentication server timed out."
203
+ end
204
+
205
+ # ====================
206
+ # Expires the query cache, setting a new expiration time as well as merging
207
+ # with the previous query cache, in case of an auth server outage.
208
+ # ====================
209
+ def expire_query_cache
210
+ before = Time.now
211
+ if total_query_cache
212
+ query_cache.each_pair do |key, value|
213
+ total_query_cache[key] = value
214
+ end
215
+ else
216
+ self.total_query_cache = query_cache.clone
217
+ end
218
+
219
+ query_cache.clear
220
+ query_cache['_expire_at'] = (query_cache_expiry || 1.hour).from_now
221
+ after = Time.now
222
+
223
+ record(before, after, "Authentication Expire Cache", "")
224
+ end
225
+
226
+ # ====================
227
+ # Queries the authentication server only if there isn't a cached response.
228
+ # Also keeps track of whether or not the server is reachable, and sends emails
229
+ # when the server goes down and back up.
230
+ # ====================
231
+ def query(message)
232
+ before = Time.now
233
+
234
+ expire_at = query_cache['_expire_at']
235
+ expire_query_cache if expire_at.blank? || Time.now > expire_at
236
+
237
+ if cached_response = query_cache[message]
238
+ response = cached_response
239
+ action = "Authentication Cache"
240
+ else
241
+ begin
242
+ response = raw_query(message)
243
+ action = "Authentication Query"
244
+ query_cache[message] = response
245
+
246
+ if auth_server_went_down_at
247
+ self.auth_server_went_down_at = nil
248
+
249
+ if sent_auth_server_down_email
250
+ self.sent_auth_server_down_email = false
251
+ if (mailer = auth_server_down_mailer) && mailer.respond_to?(:auth_server_up)
252
+ mailer.auth_server_up(Time.now).deliver_now
253
+ end
254
+ end
255
+ end
256
+
257
+ rescue AuthServerError => e
258
+ raise unless total_query_cache
259
+
260
+ old_response = total_query_cache[message]
261
+ if old_response
262
+ response = old_response
263
+ action = "Authentication Cache (due to error)"
264
+ Rails.logger.error "AUTHENTICATION: The authentication server encountered an error. "\
265
+ "You should probably check the auth server's logs. "\
266
+ "A cached response was used."
267
+ else
268
+ raise
269
+ end
270
+
271
+ rescue AuthServerDown => e
272
+ if auth_server_went_down_at.nil?
273
+ self.auth_server_went_down_at = Time.now
274
+ expire_query_cache
275
+
276
+ elsif auth_server_went_down_at > (time_before_down_email || 5.minutes).ago
277
+ unless sent_auth_server_down_email
278
+ self.sent_auth_server_down_email = true
279
+
280
+ if (mailer = auth_server_down_mailer) && mailer.respond_to?(:auth_server_down)
281
+ mailer.auth_server_down(auth_server_went_down_at).deliver_now
282
+ end
283
+ end
284
+ end
285
+
286
+ old_response = total_query_cache[message]
287
+ if old_response
288
+ response = old_response
289
+ action = "Authentication Cache (server down)"
290
+ else
291
+ raise AuthServerDown, "An uncached query was attempted, and the authentication server is down."
292
+ end
293
+ end
294
+ end
295
+ after = Time.now
296
+
297
+ record(before, after, action, message)
298
+ response
299
+ end
300
+
301
+ # ====================
302
+ # Runs a query through the server without error or cache checking.
303
+ # ====================
304
+ def force_query(message)
305
+ before = Time.now
306
+ response = raw_query(message)
307
+ after = Time.now
308
+
309
+ record(before, after, "Authentication Query (forced)", message)
310
+ response
311
+ end
312
+
313
+ # ====================
314
+ # Expects a response string returned from #query and raises an error for the
315
+ # following cases:
316
+ #
317
+ # - Access denied (AccessDeniedError)
318
+ # - Invalid command (bad query message) (InvalidCommandError)
319
+ # - Error on auth server's side (AuthServerError)
320
+ # ====================
321
+ def validate_response(response_string)
322
+ case response_string
323
+ when 'denied' then raise AccessDeniedError, "Denied"
324
+ when 'invalid' then raise InvalidCommandError, "Invalid command"
325
+ when 'sorry'
326
+ expire_query_cache
327
+ raise AuthServerError, "Authentication server encountered an error"
328
+ else
329
+ response_string
330
+ end
331
+ end
332
+
333
+ # ====================
334
+ # Finds a user with the given ID
335
+ # ====================
336
+ def find(target_id)
337
+ json = validate_response query "get #{target_id}"
338
+
339
+ if json == 'nosuchuser'
340
+ nil
341
+ else
342
+ object = new(JSON.parse(json))
343
+ object.instance_variable_set(:@persisted, true)
344
+ object
345
+ end
346
+ end
347
+
348
+ def filter_all(method, options)
349
+ all.send(method) do |user|
350
+ options.all? { |field, wanted_value| user.send(field) == wanted_value }
351
+ end
352
+ end
353
+
354
+ # ====================
355
+ # Finds a user with the given attributes (just queries for 'all' and uses ruby filters)
356
+ # ====================
357
+ def find_by(options)
358
+ filter_all(:find, options)
359
+ end
360
+
361
+ # ====================
362
+ # Finds users with the given attributes (just queries for 'all' and uses ruby filters)
363
+ # ====================
364
+ def where(options)
365
+ filter_all(:select, options)
366
+ end
367
+
368
+ # ====================
369
+ # Returns an array of all registered users
370
+ # ====================
371
+ def all
372
+ json = validate_response query "all"
373
+
374
+ objects = JSON.parse(json).map(&method(:new))
375
+ objects.each { |u| u.instance_variable_set(:@persisted, true) }
376
+ objects
377
+ end
378
+
379
+ # ====================
380
+ # Given a valid signin token:
381
+ # Returns the authenticated user for the given token
382
+ # Given an invalid signin token:
383
+ # Returns false
384
+ # ====================
385
+ def auth(token)
386
+ response = validate_response query "auth #{Figaro.env.hub_app_name} #{token}"
387
+
388
+ return false unless response =~ /^yes .+$/
389
+
390
+ _yes, json = response.split(' ', 2)
391
+ object = new(JSON.parse(json))
392
+ object.instance_variable_set(:@persisted, true)
393
+ object
394
+ end
395
+
396
+ # ====================
397
+ # Overridable logger method used when recording query benchmarks
398
+ # ====================
399
+ def logger
400
+ Rails.logger
401
+ end
402
+ end
403
+
404
+ # ============================= INSTANCE METHODS ======================
405
+
406
+ REMOTE_ATTRIBUTES = [
407
+ :id, :email, :first_name, :last_name,
408
+ :profile_picture_url
409
+ ]
410
+ REMOTE_ATTRIBUTES.each(&method(:attr_accessor))
411
+
412
+ attr_reader :persisted
413
+ alias_method :persisted?, :persisted
414
+
415
+ # ====================
416
+ # Various class methods accessible on instances
417
+ def query(*a)
418
+ self.class.query(*a)
419
+ end
420
+ def raw_query(*a)
421
+ self.class.raw_query(*a)
422
+ end
423
+ def force_query(*a)
424
+ self.class.force_query(*a)
425
+ end
426
+ def logger
427
+ self.class.logger
428
+ end
429
+ # ====================
430
+
431
+ def initialize(attributes = {})
432
+ update_attributes(attributes)
433
+ end
434
+
435
+ def update_attributes(attributes={})
436
+ return if attributes.blank?
437
+ attributes = attributes.with_indifferent_access
438
+
439
+ REMOTE_ATTRIBUTES.each do |attr|
440
+ instance_variable_set("@#{attr}", attributes[attr])
441
+ end
442
+ end
443
+
444
+ def to_json
445
+ {
446
+ id: @id,
447
+ email: @email,
448
+ first_name: @first_name,
449
+ last_name: @last_name
450
+ }
451
+ .to_json
452
+ end
453
+
454
+ def reload
455
+ json = validate_response query "get #{id}"
456
+
457
+ update_attributes(JSON.parse(json))
458
+ @persisted = true
459
+ self
460
+ end
461
+
462
+ def full_name
463
+ "#{@first_name} #{@last_name}"
464
+ end
465
+
466
+ def valid_password?(pass)
467
+ query("pass #{id} #{pass}") == 'yes'
468
+ end
469
+ end
470
+ end
471
+ end
@@ -0,0 +1,81 @@
1
+ module Softwear
2
+ module Auth
3
+ class StubbedModel < Softwear::Auth::StandardModel
4
+ class << self
5
+ def raw_query(*)
6
+ raise "Cannot perform auth server queries on stubbed auth model."
7
+ end
8
+
9
+ def users_yml_file
10
+ Rails.root.join('config', 'users.yml').to_s
11
+ end
12
+
13
+ def users_yml
14
+ if @users_yml
15
+ yml_mtime = File.mtime(users_yml_file)
16
+
17
+ if @users_yml_modified.nil? || yml_mtime > @users_yml_modified
18
+ @users_yml_modified = yml_mtime
19
+ @users_yml = nil
20
+ end
21
+ else
22
+ @users_yml_modified = File.mtime(users_yml_file)
23
+ end
24
+
25
+ if @users_yml.nil?
26
+ @users_yml = YAML.load(IO.read(users_yml_file)).with_indifferent_access
27
+ @users_yml[:users].to_a.each_with_index do |entry, i|
28
+ entry[1][:id] ||= i + 1
29
+ end
30
+ end
31
+ @users_yml
32
+ end
33
+
34
+ def yml_entry(entry, id_if_default = nil)
35
+ attributes = {}.with_indifferent_access
36
+
37
+ if entry.nil?
38
+ entry = ['usernotfound@example.com', { first_name: 'Unknown', last_name: 'User', id: id_if_default || -1 }]
39
+ end
40
+
41
+ attributes[:email] = entry[0]
42
+ attributes.merge!(entry[1])
43
+ if attributes[:profile_picture]
44
+ attributes[:profile_picture_url] ||= "file://#{attributes[:profile_picture]}"
45
+ end
46
+ new(attributes).tap { |u| u.instance_variable_set(:@persisted, true) }
47
+ end
48
+
49
+ def find(target_id)
50
+ yml_entry users_yml[:users].to_a[target_id - 1], target_id
51
+ end
52
+
53
+ def all
54
+ users_yml[:users].to_a.map(&method(:yml_entry))
55
+ end
56
+
57
+ def auth(_token)
58
+ signed_in = users_yml[:signed_in]
59
+
60
+ yml_entry [signed_in, users_yml[:users][signed_in]] unless signed_in.blank?
61
+ end
62
+ end
63
+
64
+ def yml_entry(*args)
65
+ self.class.yml_entry(*args)
66
+ end
67
+ def users_yml(*args)
68
+ self.class.users_yml(*args)
69
+ end
70
+
71
+ def reload
72
+ update_attributes yml_entry users_yml[:users].to_a[id - 1]
73
+ self
74
+ end
75
+
76
+ def valid_password?
77
+ true
78
+ end
79
+ end
80
+ end
81
+ end
@@ -1,5 +1,5 @@
1
1
  module Softwear
2
2
  module Lib
3
- VERSION = "1.7.0"
3
+ VERSION = "1.7.1"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: softwear-lib
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.0
4
+ version: 1.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nigel Baillie
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-03-03 00:00:00.000000000 Z
11
+ date: 2016-03-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -88,6 +88,8 @@ files:
88
88
  - lib/softwear/auth/helper.rb
89
89
  - lib/softwear/auth/model.rb
90
90
  - lib/softwear/auth/spec.rb
91
+ - lib/softwear/auth/standard_model.rb
92
+ - lib/softwear/auth/stubbed_model.rb
91
93
  - lib/softwear/auth/token_authentication.rb
92
94
  - lib/softwear/lib.rb
93
95
  - lib/softwear/lib/api_controller.rb