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 +4 -4
- data/lib/softwear/auth/model.rb +7 -464
- data/lib/softwear/auth/standard_model.rb +471 -0
- data/lib/softwear/auth/stubbed_model.rb +81 -0
- data/lib/softwear/lib/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 46309929f4ab433f39e2249d28c52de5083e9c0c
|
4
|
+
data.tar.gz: 214c83ab78624f336433d4ea95c12664a0b1de47
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6294137fd3a37d5881187f5bddeecc78a34a5dfe957f1aca785bc94454f9ec62f196ff88ed42712e6d1ac8b7b94825c5f5f17fb5fb63dcfba50fa20fc4e23d56
|
7
|
+
data.tar.gz: e59c12a9c183fb7fb1de46f33d9741bb2f9b6c5b5b0299c0330ea00f3c89297bedeec5fc6cee311976d71766b62eee897d500f16bee62f2ca38ab4558033fedb
|
data/lib/softwear/auth/model.rb
CHANGED
@@ -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
|
-
|
4
|
-
|
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
|
-
|
12
|
-
|
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
|
data/lib/softwear/lib/version.rb
CHANGED
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.
|
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-
|
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
|