softwear-lib 1.7.0 → 1.7.1

Sign up to get free protection for your applications and to get access to all the features.
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