hubssolib 0.2.9 → 2.0.0
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 +7 -0
- data/CHANGELOG.md +8 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +41 -0
- data/README.md +329 -0
- data/VERSION +1 -1
- data/hubssolib.gemspec +31 -0
- data/lib/hub_sso_lib.rb +215 -439
- metadata +80 -50
data/lib/hub_sso_lib.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
#######################################################################
|
2
2
|
# Module: HubSsoLib #
|
3
|
-
# (C) Hipposoft 2006-
|
3
|
+
# (C) Hipposoft 2006-2019 #
|
4
4
|
# #
|
5
5
|
# Purpose: Cross-application same domain single sign-on support. #
|
6
6
|
# #
|
@@ -12,297 +12,69 @@
|
|
12
12
|
# path come from environment variables. #
|
13
13
|
# 09-Mar-2011 (ADH): Updated for Hub on Rails 2.3.11 along #
|
14
14
|
# with several important bug fixes. #
|
15
|
-
#
|
16
|
-
# aids testing with e.g. Mongrel by #
|
17
|
-
# allowing non-SSL use. #
|
15
|
+
# 01-May-2019 (ADH): Updated for Ruby 2.5.x. #
|
18
16
|
#######################################################################
|
19
17
|
|
20
18
|
module HubSsoLib
|
21
19
|
|
22
20
|
require 'drb'
|
21
|
+
require 'securerandom'
|
23
22
|
|
24
23
|
# DRb connection
|
25
|
-
|
24
|
+
HUB_CONNECTION_URI = ENV['HUB_CONNECTION_URI'] || 'drbunix:' + File.join( ENV['HOME'] || '/', '/.hub_drb')
|
25
|
+
|
26
|
+
unless HUB_CONNECTION_URI.downcase.start_with?('drbunix:')
|
27
|
+
puts
|
28
|
+
puts '*' * 80
|
29
|
+
puts "You *must* use a 'drbunix:' scheme for HUB_CONNECTION_URI (#{ HUB_CONNECTION_URI.inspect } is invalid)"
|
30
|
+
puts '*' * 80
|
31
|
+
puts
|
32
|
+
|
33
|
+
raise 'Exiting'
|
34
|
+
end
|
26
35
|
|
27
36
|
# Location of Hub application root.
|
28
|
-
HUB_PATH_PREFIX = ENV['HUB_PATH_PREFIX']
|
37
|
+
HUB_PATH_PREFIX = ENV['HUB_PATH_PREFIX'] || ''
|
29
38
|
|
30
39
|
# Time limit, *in seconds*, for the account inactivity timeout.
|
31
40
|
# If a user performs no Hub actions during this time they will
|
32
41
|
# be automatically logged out upon their next action.
|
33
|
-
|
34
|
-
|
35
|
-
# Random file location.
|
36
|
-
HUBSSOLIB_RND_FILE_PATH = ENV['HUB_RANDOM_FILE']
|
42
|
+
HUB_IDLE_TIME_LIMIT = 4 * 60 * 60
|
37
43
|
|
38
|
-
# Shared cookie name
|
39
|
-
|
40
|
-
HUBSSOLIB_COOKIE_PATH = ENV['HUB_COOKIE_PATH']
|
44
|
+
# Shared cookie name.
|
45
|
+
HUB_COOKIE_NAME = 'hubapp_shared_id'
|
41
46
|
|
42
|
-
# Bypass SSL, for testing purposes?
|
43
|
-
|
47
|
+
# Bypass SSL, for testing purposes? Rails 'production' mode will
|
48
|
+
# insist on SSL otherwise. Development & test environments do not,
|
49
|
+
# so do not need this variable setting.
|
50
|
+
HUB_BYPASS_SSL = ( ENV['HUB_BYPASS_SSL'] == "true" )
|
44
51
|
|
45
|
-
#
|
46
|
-
|
47
|
-
# once per request.
|
48
|
-
|
49
|
-
attr_reader :HUBSSOLIB_RANDOM_DATA, :HUBSSOLIB_RANDOM_DATA_SIZE
|
50
|
-
|
51
|
-
HUBSSOLIB_RANDOM_DATA_SIZE = File.size(HUBSSOLIB_RND_FILE_PATH)
|
52
|
-
HUBSSOLIB_RANDOM_DATA_SIZE = 16384 if (HUBSSOLIB_RANDOM_DATA_SIZE > 16384)
|
53
|
-
|
54
|
-
if HUBSSOLIB_RANDOM_DATA_SIZE < 1024
|
55
|
-
raise "HubSsoLib needs at least 1024 bytes of random data - file '#{rnd_file}' is too small"
|
56
|
-
else
|
57
|
-
HUBSSOLIB_RANDOM_DATA = File.read(HUBSSOLIB_RND_FILE_PATH)
|
58
|
-
end
|
52
|
+
# Thread safety.
|
53
|
+
HUB_MUTEX = Mutex.new
|
59
54
|
|
60
55
|
#######################################################################
|
61
|
-
# Class:
|
62
|
-
# (C) Hipposoft
|
56
|
+
# Class: Serialiser #
|
57
|
+
# (C) Hipposoft 2020 #
|
63
58
|
# #
|
64
|
-
# Purpose:
|
59
|
+
# Purpose: Simple object serialiser/deserialiser. #
|
65
60
|
# #
|
66
61
|
# Author: A.D.Hodgkinson #
|
67
62
|
# #
|
68
|
-
# History:
|
69
|
-
# 20-Oct-2006 (ADH): Integrated into HubSsoLib, renamed to #
|
70
|
-
# 'Crypto' from 'HubSsoCrypto'. #
|
63
|
+
# History: 18-Apr-2002 (ADH): First version. #
|
71
64
|
#######################################################################
|
72
65
|
|
73
|
-
#
|
74
|
-
# HubSsoLib::Crypto object is used to encrypt and decrypt data with the
|
75
|
-
# AES-256-CBC cipher. A single passphrase is used for both operations.
|
76
|
-
# A SHA-256 hash of that passphrase is used as the encryption key.
|
77
|
-
#
|
78
|
-
# CBC operation requires an initialization vector for the first block of
|
79
|
-
# data during encryption and decryption. A block of random data is used
|
80
|
-
# for this in conjunction with the passphrase used to generate the key. By
|
81
|
-
# so doing, the initialization vector is not revealed to third parties,
|
82
|
-
# even though the source code of the object is available. The weakness is
|
83
|
-
# that for a given passphrase and random data pool the same initialization
|
84
|
-
# vector will always be generated - indeed, this is relied upon, to allow
|
85
|
-
# callers themselves to only have to remember the passphrase. See private
|
86
|
-
# method obtain_iv() for more details.
|
66
|
+
# Simple object serialiser and deserialiser using Marshal and Base64.
|
87
67
|
#
|
88
|
-
|
89
|
-
|
90
|
-
# contents are not know to the outside world and the contents, while they
|
91
|
-
# may change at any point, don't change during the duration of a user
|
92
|
-
# log-in session (at least, if it changes, all current sessions will be
|
93
|
-
# harmlessly invalidated).
|
94
|
-
#
|
95
|
-
class Crypto
|
96
|
-
|
97
|
-
require 'openssl'
|
98
|
-
require 'digest/sha2'
|
99
|
-
require 'digest/md5'
|
100
|
-
|
101
|
-
# # Initialize the HubSsoLib::Crypto object.
|
102
|
-
# #
|
103
|
-
# def initialize()
|
104
|
-
# DRb.start_service()
|
105
|
-
#
|
106
|
-
# factory = DRbObject.new_with_uri(HUBSSOLIB_DRB_URI)
|
107
|
-
# @rnd_data = factory.random_data()
|
108
|
-
# @rnd_size = factory.random_data_size()
|
109
|
-
#
|
110
|
-
# DRb.stop_service()
|
111
|
-
# end
|
112
|
-
|
113
|
-
# Generate a series of pseudo-random bytes of the given length.
|
114
|
-
#
|
115
|
-
def self.random_data(size)
|
116
|
-
data = ''
|
117
|
-
size.times { data << rand(256) }
|
118
|
-
data
|
119
|
-
end
|
120
|
-
|
121
|
-
def random_data(size)
|
122
|
-
HubSsoLib::Crypto.random_data(size)
|
123
|
-
end
|
124
|
-
|
125
|
-
# Encode some given data in base-64 format with no line breaks.
|
126
|
-
#
|
127
|
-
def self.pack64(data)
|
128
|
-
[data].pack('m1000000') # Stupid long number to avoid "\n" in the output
|
129
|
-
end
|
130
|
-
|
131
|
-
def pack64(data)
|
132
|
-
HubSsoLib::Crypto.pack64(data)
|
133
|
-
end
|
134
|
-
|
135
|
-
# Decode some given data from base-64 format with no line breaks.
|
136
|
-
#
|
137
|
-
def self.unpack64(data)
|
138
|
-
data.unpack('m').first
|
139
|
-
end
|
140
|
-
|
141
|
-
def unpack64(data)
|
142
|
-
HubSsoLib::Crypto.unpack64(data)
|
143
|
-
end
|
144
|
-
|
145
|
-
# Encrypt the given data with the AES-256-CBC algorithm using the
|
146
|
-
# given passphrase. Returns the encrypted result in a string.
|
147
|
-
# Distantly based upon:
|
148
|
-
#
|
149
|
-
# http://www.bigbold.com/snippets/posts/show/576
|
150
|
-
#
|
151
|
-
# In the context of Hub, the passphrase tends to be fixed per IP
|
152
|
-
# address (albeit unknown to the public) and the IV is derived from
|
153
|
-
# it. This means the same data will encode to the same result. With
|
154
|
-
# the source data having some parts which are invariant, security
|
155
|
-
# is compromised. To avoid this, data is prefixed by a quantity of
|
156
|
-
# random bytes, effectively supplementing the IV and ensuring that
|
157
|
-
# different size and content data is generated each time.
|
158
|
-
#
|
159
|
-
def encrypt(data, passphrase)
|
160
|
-
cipher = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
|
161
|
-
cipher.encrypt
|
162
|
-
|
163
|
-
cipher.key = Digest::SHA256.digest(passphrase)
|
164
|
-
cipher.iv = obtain_iv(passphrase)
|
165
|
-
|
166
|
-
rsize = rand(32)
|
167
|
-
data = '' << rsize << random_data(rsize) << data
|
168
|
-
|
169
|
-
encrypted = cipher.update(data)
|
170
|
-
encrypted << cipher.final
|
171
|
-
|
172
|
-
return encrypted
|
173
|
-
end
|
174
|
-
|
175
|
-
# Decrypt the given data with the AES-256-CBC algorithm using the
|
176
|
-
# given passphrase. Returns 'nil' if there is any kind of error in
|
177
|
-
# the decryption process. Distantly based upon:
|
178
|
-
#
|
179
|
-
# http://www.bigbold.com/snippets/posts/show/576
|
180
|
-
#
|
181
|
-
def decrypt(data, passphrase)
|
182
|
-
cipher = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
|
183
|
-
cipher.decrypt
|
184
|
-
|
185
|
-
cipher.key = Digest::SHA256.digest(passphrase)
|
186
|
-
cipher.iv = obtain_iv(passphrase)
|
187
|
-
|
188
|
-
decrypted = cipher.update(data)
|
189
|
-
decrypted << cipher.final
|
190
|
-
|
191
|
-
rsize = decrypted[0]
|
192
|
-
return decrypted[rsize + 1..-1]
|
193
|
-
rescue
|
194
|
-
return nil
|
195
|
-
end
|
196
|
-
|
197
|
-
# Encrypt and base-64 encode the given data with the given passphrase.
|
198
|
-
# Returns the encoded result.
|
199
|
-
#
|
200
|
-
def encode(data, passphrase)
|
201
|
-
pack64(encrypt(data, passphrase))
|
202
|
-
end
|
203
|
-
|
204
|
-
# Decrypt and base-64 decode the given data with the given passphrase.
|
205
|
-
# Returns the decoded result or 'nil' on error.
|
206
|
-
#
|
207
|
-
def decode(data, passphrase)
|
208
|
-
decrypt(unpack64(data), passphrase)
|
209
|
-
rescue
|
210
|
-
return nil
|
211
|
-
end
|
212
|
-
|
213
|
-
# Class method that takes an object and passphrase and encrypts
|
214
|
-
# the result. The passphrase is scrambled internally using data
|
215
|
-
# not available to the public, the object serialised (so it must
|
216
|
-
# support serialisation), encrypted and base-64 encoded, and the
|
217
|
-
# 7-bit safe string result returned. On failure, exceptions will
|
218
|
-
# be raised (failure is not expected).
|
219
|
-
#
|
220
|
-
def self.encode_object(object, passphrase)
|
221
|
-
crypto = HubSsoLib::Crypto.new
|
222
|
-
passphrase = crypto.scramble_passphrase(passphrase)
|
223
|
-
|
224
|
-
return crypto.encode(Marshal.dump(object), passphrase)
|
225
|
-
end
|
226
|
-
|
227
|
-
def encode_object(object, passphrase)
|
228
|
-
HubSsoLib::Crypto.encode_object(object, passphrase)
|
229
|
-
end
|
230
|
-
|
231
|
-
# Class method that takes output from Crypto.encode_object and
|
232
|
-
# decodes it, returning an object reference. Since failure may
|
233
|
-
# result from invalid data input and this can be a common case,
|
234
|
-
# rather than raise an exception as with Crypto.encode_object,
|
235
|
-
# this method returns 'nil' should there be any decode problems.
|
236
|
-
#
|
237
|
-
def self.decode_object(data, passphrase)
|
238
|
-
crypto = HubSsoLib::Crypto.new
|
239
|
-
passphrase = crypto.scramble_passphrase(passphrase)
|
240
|
-
object = nil
|
241
|
-
|
242
|
-
if (data && !data.empty?)
|
243
|
-
object = Marshal.load(crypto.decode(data, passphrase))
|
244
|
-
end
|
245
|
-
|
246
|
-
return object
|
247
|
-
rescue
|
248
|
-
return nil
|
249
|
-
end
|
68
|
+
class Serialiser
|
69
|
+
require 'base64'
|
250
70
|
|
251
|
-
def
|
252
|
-
|
71
|
+
def self.serialise_object(object)
|
72
|
+
Base64.strict_encode64(Marshal.dump(object))
|
253
73
|
end
|
254
74
|
|
255
|
-
|
256
|
-
|
257
|
-
# cookie contents for some nefarious purpose. Encryption is done at the
|
258
|
-
# head end. We need to be able to decrypt in the absence of any other
|
259
|
-
# information. A fixed passphrase thus needs to be used, but it cannot be
|
260
|
-
# included in the source code or anyone can read the cookie contents! To
|
261
|
-
# work around this, transform the passphrase into 32 bytes of data from
|
262
|
-
# the random pool if asked. The random pool is not known to the outside
|
263
|
-
# world so security is improved (albeit far from perfect, but this is all
|
264
|
-
# part of little more than an anti-spam measure - not Fort Knox!).
|
265
|
-
#
|
266
|
-
def scramble_passphrase(passphrase)
|
267
|
-
|
268
|
-
# Generate a 16-byte hash of the passphrase using the MD5 algorithm. Get
|
269
|
-
# this as a string of hex digits and convert that into an integer. Strip
|
270
|
-
# off the top bits (since we've no more reason to believe that the top
|
271
|
-
# bits contain more randomly varying data than the bottom bits) so that
|
272
|
-
# the number is bound to between zero and the random pool size, minus
|
273
|
-
# 33, thus providing an offset into the file from which we can safely
|
274
|
-
# read 32 bytes of data.
|
275
|
-
|
276
|
-
offset = Digest::MD5.hexdigest(passphrase).hex % (HubSsoLib::HUBSSOLIB_RANDOM_DATA_SIZE - 32)
|
277
|
-
|
278
|
-
# Return 32 bytes of data from the random pool at the calculated offset.
|
279
|
-
|
280
|
-
return HubSsoLib::HUBSSOLIB_RANDOM_DATA[offset..offset + 31]
|
281
|
-
end
|
282
|
-
|
283
|
-
private
|
284
|
-
|
285
|
-
# Obtain an initialization vector (IV) of 32 bytes (256 bits) length based
|
286
|
-
# on external data loaded when the object was created. Since the data
|
287
|
-
# content is unknown, the IV is unknown. This is important; see:
|
288
|
-
#
|
289
|
-
# http://www.ciphersbyritter.com/GLOSSARY.HTM#CipherBlockChaining
|
290
|
-
#
|
291
|
-
# Weakness: An offset into the supplied data is generated from the given
|
292
|
-
# passphrase. Since the data is cached internally, the same IV will be
|
293
|
-
# produced for any given passphrase (this is as much a feature as it is a
|
294
|
-
# weakness, since the encryption and decryption routines rely on it).
|
295
|
-
#
|
296
|
-
# The passphrase scrambler is used to do the back-end work. Since the
|
297
|
-
# caller may have already scrambled the passphrase once, scrambled data is
|
298
|
-
# used as input; we end up scrambling it twice. This is a desired result -
|
299
|
-
# we don't want the IV being the data that's actually also used for the
|
300
|
-
# encryption passphrase.
|
301
|
-
#
|
302
|
-
def obtain_iv(passphrase)
|
303
|
-
return scramble_passphrase(passphrase)
|
75
|
+
def self.deserialise_object(data)
|
76
|
+
Marshal.load(Base64.strict_decode64(data)) rescue nil
|
304
77
|
end
|
305
|
-
|
306
78
|
end # Crypto class
|
307
79
|
|
308
80
|
#######################################################################
|
@@ -610,12 +382,16 @@ module HubSsoLib
|
|
610
382
|
attr_accessor :session_return_to
|
611
383
|
attr_accessor :session_flash
|
612
384
|
attr_accessor :session_user
|
385
|
+
attr_accessor :session_key_rotation
|
386
|
+
attr_accessor :session_ip
|
613
387
|
|
614
388
|
def initialize
|
615
|
-
@session_last_used
|
616
|
-
@session_return_to
|
617
|
-
@session_flash
|
618
|
-
@session_user
|
389
|
+
@session_last_used = Time.now.utc
|
390
|
+
@session_return_to = nil
|
391
|
+
@session_flash = {}
|
392
|
+
@session_user = HubSsoLib::User.new
|
393
|
+
@session_key_rotation = nil
|
394
|
+
@session_ip = nil
|
619
395
|
end
|
620
396
|
end # Session class
|
621
397
|
|
@@ -633,19 +409,59 @@ module HubSsoLib
|
|
633
409
|
|
634
410
|
class SessionFactory
|
635
411
|
def initialize
|
636
|
-
|
412
|
+
puts "Session factory: Awaken"
|
413
|
+
|
414
|
+
@hub_sessions = {}
|
637
415
|
end
|
638
416
|
|
639
|
-
|
640
|
-
|
641
|
-
|
417
|
+
# Get a session using a given key (a UUID). Generates a new session if
|
418
|
+
# the key is unrecognised or if the IP address given mismatches the one
|
419
|
+
# recorded in existing session data.
|
420
|
+
#
|
421
|
+
# Whether new or pre-existing, the returned session will have changed key
|
422
|
+
# as a result of being read; check the #session_key_rotation property to
|
423
|
+
# find out the new key. If you fail to do this, you'll lose access to the
|
424
|
+
# session data as you won't know which key it lies under.
|
425
|
+
#
|
426
|
+
# The returned object is proxied via DRb - it is shared between processes.
|
427
|
+
#
|
428
|
+
# +key+:: Session key; lazy-initialises a new session under this key
|
429
|
+
# if none is found, then immeediately rotates it.
|
430
|
+
#
|
431
|
+
# +remote_ip+:: Request's remote IP address. If there is an existing
|
432
|
+
# session which matches this, it's returned. If there is an
|
433
|
+
# existing session but the IP mismatches, it's treated as
|
434
|
+
# invalid and discarded.
|
435
|
+
#
|
436
|
+
def get_hub_session_proxy(key, remote_ip)
|
437
|
+
retrieve_existing = @hub_sessions.has_key?(key)
|
438
|
+
message = retrieve_existing ? 'Retrieving' : 'Created'
|
439
|
+
new_key = SecureRandom.uuid
|
440
|
+
|
441
|
+
puts "#{message} session for key #{key} and rotating to #{new_key}"
|
442
|
+
|
443
|
+
if retrieve_existing
|
444
|
+
hub_session = @hub_sessions[key]
|
445
|
+
if remote_ip != hub_session.session_ip
|
446
|
+
puts "WARNING: IP address changed from #{hub_session.session_ip} to #{remote_ip} -> discarding session"
|
447
|
+
hub_session = @hub_sessions[key] = HubSsoLib::Session.new
|
448
|
+
end
|
449
|
+
|
450
|
+
else
|
451
|
+
hub_session = @hub_sessions[key] = HubSsoLib::Session.new
|
452
|
+
hub_session.session_ip = remote_ip
|
453
|
+
|
642
454
|
end
|
643
455
|
|
644
|
-
|
456
|
+
@hub_sessions.delete(key)
|
457
|
+
@hub_sessions[new_key] = hub_session
|
458
|
+
|
459
|
+
hub_session.session_key_rotation = new_key
|
460
|
+
return hub_session
|
645
461
|
end
|
646
462
|
|
647
|
-
def
|
648
|
-
@
|
463
|
+
def enumerate_hub_sessions()
|
464
|
+
@hub_sessions
|
649
465
|
end
|
650
466
|
end
|
651
467
|
|
@@ -667,8 +483,10 @@ module HubSsoLib
|
|
667
483
|
|
668
484
|
module Server
|
669
485
|
def hubssolib_launch_server
|
670
|
-
|
671
|
-
|
486
|
+
puts "Server: Starting at #{ HUB_CONNECTION_URI }"
|
487
|
+
|
488
|
+
@@hub_session_factory = HubSsoLib::SessionFactory.new
|
489
|
+
DRb.start_service(HUB_CONNECTION_URI, @@hub_session_factory, { :safe_level => 1 })
|
672
490
|
DRb.thread.join
|
673
491
|
end
|
674
492
|
end # Server module
|
@@ -725,7 +543,7 @@ module HubSsoLib
|
|
725
543
|
return false unless hubssolib_logged_in?
|
726
544
|
|
727
545
|
pnormal = HubSsoLib::Roles.new(false).to_s
|
728
|
-
puser = hubssolib_get_user_roles.to_s
|
546
|
+
puser = hubssolib_get_user_roles().to_s
|
729
547
|
|
730
548
|
return (puser && !puser.empty? && puser != pnormal)
|
731
549
|
end
|
@@ -736,33 +554,36 @@ module HubSsoLib
|
|
736
554
|
def hubssolib_log_out
|
737
555
|
# Causes the "hubssolib_current_[foo]=" methods to run, which
|
738
556
|
# deal with everything else.
|
739
|
-
self.hubssolib_current_user
|
740
|
-
|
557
|
+
self.hubssolib_current_user = nil
|
558
|
+
@hubssolib_current_session_proxy = nil
|
741
559
|
end
|
742
560
|
|
743
|
-
# Accesses the current
|
561
|
+
# Accesses the current session from the cookie. Creates a new session
|
562
|
+
# object if need be, but can return +nil+ if e.g. attempting to access
|
563
|
+
# session cookie data without SSL.
|
744
564
|
#
|
745
|
-
def
|
746
|
-
|
565
|
+
def hubssolib_current_session
|
566
|
+
@hubssolib_current_session_proxy ||= hubssolib_get_session_proxy()
|
747
567
|
end
|
748
568
|
|
749
|
-
#
|
569
|
+
# Accesses the current user, via the DRb server if necessary.
|
750
570
|
#
|
751
|
-
def hubssolib_current_user
|
752
|
-
|
753
|
-
|
571
|
+
def hubssolib_current_user
|
572
|
+
hub_session = self.hubssolib_current_session
|
573
|
+
user = hub_session.nil? ? nil : hub_session.session_user
|
754
574
|
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
575
|
+
if (user && user.user_id)
|
576
|
+
return user
|
577
|
+
else
|
578
|
+
return nil
|
579
|
+
end
|
760
580
|
end
|
761
581
|
|
762
|
-
# Store the given
|
582
|
+
# Store the given user data in the cookie
|
763
583
|
#
|
764
|
-
def
|
765
|
-
|
584
|
+
def hubssolib_current_user=(user)
|
585
|
+
hub_session = self.hubssolib_current_session
|
586
|
+
hub_session.session_user = user unless hub_session.nil?
|
766
587
|
end
|
767
588
|
|
768
589
|
# Public read-only accessor methods for common user activities:
|
@@ -845,23 +666,7 @@ module HubSsoLib
|
|
845
666
|
# So we reach here knowing we're logged in, but the action may or
|
846
667
|
# may not require authorisation.
|
847
668
|
|
848
|
-
|
849
|
-
|
850
|
-
# We have to update session expiry even for actions that don't
|
851
|
-
# need us to be logged in, since we *are* logged in and need to
|
852
|
-
# maintain that state. If, though, the session expires, we just
|
853
|
-
# quietly log out and let action processing carry on.
|
854
|
-
|
855
|
-
if (hubssolib_session_expired?)
|
856
|
-
hubssolib_log_out
|
857
|
-
hubssolib_set_flash(:attention, 'Your session timed out, so you are no longer logged in.')
|
858
|
-
else
|
859
|
-
hubssolib_set_last_used(Time.now.utc)
|
860
|
-
end
|
861
|
-
|
862
|
-
return true # true -> let action processing continue
|
863
|
-
|
864
|
-
else
|
669
|
+
if (login_is_required)
|
865
670
|
|
866
671
|
# Login *is* required for this action. If the session expires,
|
867
672
|
# redirect to Hub's login page via its expiry action. Otherwise
|
@@ -882,6 +687,22 @@ module HubSsoLib
|
|
882
687
|
return hubssolib_authorized? ? true : hubssolib_access_denied
|
883
688
|
end
|
884
689
|
|
690
|
+
else
|
691
|
+
|
692
|
+
# We have to update session expiry even for actions that don't
|
693
|
+
# need us to be logged in, since we *are* logged in and need to
|
694
|
+
# maintain that state. If, though, the session expires, we just
|
695
|
+
# quietly log out and let action processing carry on.
|
696
|
+
|
697
|
+
if (hubssolib_session_expired?)
|
698
|
+
hubssolib_log_out
|
699
|
+
hubssolib_set_flash(:attention, 'Your session timed out, so you are no longer logged in.')
|
700
|
+
else
|
701
|
+
hubssolib_set_last_used(Time.now.utc)
|
702
|
+
end
|
703
|
+
|
704
|
+
return true # true -> let action processing continue
|
705
|
+
|
885
706
|
end
|
886
707
|
end
|
887
708
|
|
@@ -901,7 +722,7 @@ module HubSsoLib
|
|
901
722
|
#
|
902
723
|
# We can return to this location by calling #redirect_back_or_default.
|
903
724
|
#
|
904
|
-
def hubssolib_store_location(uri_str = request.
|
725
|
+
def hubssolib_store_location(uri_str = request.url)
|
905
726
|
|
906
727
|
if (uri_str && !uri_str.empty?)
|
907
728
|
uri_str = hubssolib_promote_uri_to_ssl(uri_str, request.host) unless request.ssl?
|
@@ -929,7 +750,7 @@ module HubSsoLib
|
|
929
750
|
def hubssolib_promote_uri_to_ssl(uri_str, host = nil)
|
930
751
|
uri = URI.parse(uri_str)
|
931
752
|
uri.host = host if host
|
932
|
-
uri.scheme =
|
753
|
+
uri.scheme = hub_bypass_ssl? ? 'http' : 'https'
|
933
754
|
return uri.to_s
|
934
755
|
end
|
935
756
|
|
@@ -938,7 +759,7 @@ module HubSsoLib
|
|
938
759
|
# 'true' if not redirected (already HTTPS), else 'false'.
|
939
760
|
#
|
940
761
|
def hubssolib_ensure_https
|
941
|
-
if request.ssl? ||
|
762
|
+
if request.ssl? || hub_bypass_ssl?
|
942
763
|
return true
|
943
764
|
else
|
944
765
|
# This isn't reliable: redirect_to({ :protocol => 'https://' })
|
@@ -960,7 +781,7 @@ module HubSsoLib
|
|
960
781
|
def hubssolib_set_flash(symbol, message)
|
961
782
|
return unless self.hubssolib_current_session
|
962
783
|
f = hubssolib_get_flash
|
963
|
-
f[symbol] = message
|
784
|
+
f[symbol.to_s] = message
|
964
785
|
self.hubssolib_current_session.session_flash = f
|
965
786
|
end
|
966
787
|
|
@@ -969,63 +790,51 @@ module HubSsoLib
|
|
969
790
|
self.hubssolib_current_session.session_flash = {}
|
970
791
|
end
|
971
792
|
|
972
|
-
#
|
973
|
-
# application
|
974
|
-
#
|
793
|
+
# Return flash data for known keys, then all remaining keys, from both
|
794
|
+
# the cross-application and standard standard flash hashes. The returned
|
795
|
+
# Hash is of the form:
|
975
796
|
#
|
976
|
-
#
|
977
|
-
# result in the flash hash now it has been used.
|
797
|
+
# { 'hub' => ...data..., 'standard' => ...data... }
|
978
798
|
#
|
979
|
-
|
980
|
-
|
981
|
-
|
982
|
-
if (value)
|
983
|
-
hubssolib_set_flash(key, nil)
|
984
|
-
return "<h2 align=\"left\" class=\"#{key}\">#{value}</h2><p />"
|
985
|
-
else
|
986
|
-
return ''
|
987
|
-
end
|
988
|
-
end
|
989
|
-
|
990
|
-
# Next, return tags for a standard application flash using the given key.
|
799
|
+
# ...where "...data..." is itself a Hash of flash keys yielding flash
|
800
|
+
# values. This allows both the Hub and standard flashes to have values
|
801
|
+
# inside them under the same key. All keys are strings.
|
991
802
|
#
|
992
|
-
def
|
993
|
-
value = flash[key] if defined?(flash)
|
803
|
+
def hubssolib_flash_data
|
994
804
|
|
995
|
-
if (value)
|
996
|
-
flash.delete(key)
|
997
|
-
return "<h2 align=\"left\" class=\"#{key}\">#{value}</h2><p />"
|
998
|
-
else
|
999
|
-
return ''
|
1000
|
-
end
|
1001
|
-
end
|
1002
|
-
|
1003
|
-
# Return flash tags for known keys, then all remaining keys, from both
|
1004
|
-
# the cross-application and standard standard flash hashes.
|
1005
|
-
#
|
1006
|
-
def hubssolib_flash_tags
|
1007
805
|
# These known key values are used to guarantee an order in the output
|
1008
806
|
# for cases where multiple messages are defined.
|
807
|
+
#
|
808
|
+
compiled_data = { 'hub' => {}, 'standard' => {} }
|
809
|
+
ordered_keys = [
|
810
|
+
'notice',
|
811
|
+
'attention',
|
812
|
+
'alert'
|
813
|
+
]
|
814
|
+
|
815
|
+
# Get an array of keys for the Hub flash with the ordered key items
|
816
|
+
# first and store data from that flash; same again for standard.
|
1009
817
|
|
1010
|
-
|
1011
|
-
|
1012
|
-
hubssolib_flash_tag(:alert)
|
818
|
+
hash = hubssolib_get_flash()
|
819
|
+
keys = ordered_keys | hash.keys
|
1013
820
|
|
1014
|
-
|
1015
|
-
|
1016
|
-
|
821
|
+
keys.each do | key |
|
822
|
+
compiled_data[ 'hub' ][ key ] = hash[ key ] if hash.has_key?( key )
|
823
|
+
end
|
1017
824
|
|
1018
|
-
|
825
|
+
if defined?( flash )
|
826
|
+
hash = flash.to_h()
|
827
|
+
keys = ordered_keys | hash.keys
|
1019
828
|
|
1020
|
-
|
1021
|
-
|
829
|
+
keys.each do | key |
|
830
|
+
compiled_data[ 'standard' ][ key ] = hash[ key ] if hash.has_key?( key )
|
831
|
+
end
|
1022
832
|
end
|
1023
833
|
|
1024
|
-
|
1025
|
-
|
1026
|
-
end if defined?(flash)
|
834
|
+
hubssolib_clear_flash()
|
835
|
+
flash.discard()
|
1027
836
|
|
1028
|
-
return
|
837
|
+
return compiled_data
|
1029
838
|
end
|
1030
839
|
|
1031
840
|
# Retrieve the message of an exception stored as an object in the given
|
@@ -1045,7 +854,7 @@ module HubSsoLib
|
|
1045
854
|
:hubssolib_logged_in?,
|
1046
855
|
:hubssolib_authorized?,
|
1047
856
|
:hubssolib_privileged?,
|
1048
|
-
:
|
857
|
+
:hubssolib_flash_data
|
1049
858
|
rescue
|
1050
859
|
# We're not always included in controllers...
|
1051
860
|
nil
|
@@ -1053,6 +862,28 @@ module HubSsoLib
|
|
1053
862
|
|
1054
863
|
private
|
1055
864
|
|
865
|
+
# Establish a single DRb factory connection.
|
866
|
+
#
|
867
|
+
def hubssolib_factory
|
868
|
+
HUB_MUTEX.synchronize do
|
869
|
+
begin
|
870
|
+
DRb.current_server
|
871
|
+
rescue DRb::DRbServerNotFound
|
872
|
+
DRb.start_service()
|
873
|
+
end
|
874
|
+
|
875
|
+
@factory ||= DRbObject.new_with_uri(HUB_CONNECTION_URI)
|
876
|
+
end
|
877
|
+
|
878
|
+
return @factory
|
879
|
+
end
|
880
|
+
|
881
|
+
# Helper that decides if we should insist on SSL (or not).
|
882
|
+
#
|
883
|
+
def hub_bypass_ssl?
|
884
|
+
HUB_BYPASS_SSL || ! Rails.env.production?
|
885
|
+
end
|
886
|
+
|
1056
887
|
# Indicate that the user must log in to complete their request.
|
1057
888
|
# Returns false to enable a before_filter to return through this
|
1058
889
|
# method while ensuring that the previous action processing is
|
@@ -1115,87 +946,26 @@ module HubSsoLib
|
|
1115
946
|
# considered acceptable, though who knows, the view may change in future.
|
1116
947
|
|
1117
948
|
last_used = hubssolib_get_last_used
|
1118
|
-
(request.method != :post && last_used && Time.now.utc - last_used >
|
1119
|
-
end
|
1120
|
-
|
1121
|
-
# Retrieve data from a given cookie with encrypted contents.
|
1122
|
-
#
|
1123
|
-
def hubssolib_get_secure_cookie_data(name)
|
1124
|
-
return HubSsoLib::Crypto.decode_object(cookies[name], request.remote_ip)
|
949
|
+
(request.method != :post && last_used && Time.now.utc - last_used > HUB_IDLE_TIME_LIMIT)
|
1125
950
|
end
|
1126
951
|
|
1127
|
-
|
1128
|
-
# will be encrypted.
|
1129
|
-
#
|
1130
|
-
def hubssolib_set_secure_cookie_data(name, value)
|
1131
|
-
if (@hubssolib_have_written_cookie)
|
1132
|
-
raise "HubSsoLib: Attmept to set cookie '#{name}' more than once"
|
1133
|
-
end
|
1134
|
-
|
1135
|
-
@hubssolib_have_written_cookie = true
|
1136
|
-
|
1137
|
-
# Using cookies.delete *should* work but doesn't. Set the
|
1138
|
-
# cookie with nil data instead.
|
1139
|
-
|
1140
|
-
data = value.nil? ? nil : HubSsoLib::Crypto.encode_object(value, request.remote_ip)
|
1141
|
-
|
1142
|
-
# No expiry time; to aid security, use session cookies only.
|
1143
|
-
|
1144
|
-
cookies[name] = {
|
1145
|
-
:value => data,
|
1146
|
-
:path => HUBSSOLIB_COOKIE_PATH,
|
1147
|
-
:secure => ! HUBSSOLIB_BYPASS_SSL
|
1148
|
-
}
|
1149
|
-
end
|
1150
|
-
|
1151
|
-
# Establish a single DRb factory connection.
|
1152
|
-
#
|
1153
|
-
def hubssolib_factory
|
1154
|
-
|
1155
|
-
# See:
|
1156
|
-
# http://stackoverflow.com/questions/299219/where-is-the-correct-place-to-initialize-the-drb-service-within-a-rails-applicati
|
1157
|
-
#
|
1158
|
-
begin
|
1159
|
-
DRb.current_server
|
1160
|
-
rescue DRb::DRbServerNotFound
|
1161
|
-
DRb.start_service
|
1162
|
-
# Move to different ThreadGroup to stop Mongrel hang on exit.
|
1163
|
-
ThreadGroup.new.add DRb.thread
|
1164
|
-
end
|
1165
|
-
|
1166
|
-
return @factory ||= DRbObject.new_with_uri(HUBSSOLIB_DRB_URI)
|
1167
|
-
end
|
1168
|
-
|
1169
|
-
# Retrieve user data from the DRb server.
|
1170
|
-
#
|
1171
|
-
def hubssolib_get_user_data
|
1172
|
-
user = self.hubssolib_current_session ? self.hubssolib_current_session.session_user : nil
|
1173
|
-
|
1174
|
-
if (user && user.user_id)
|
1175
|
-
return user
|
1176
|
-
else
|
1177
|
-
return nil
|
1178
|
-
end
|
1179
|
-
end
|
1180
|
-
|
1181
|
-
def hubssolib_set_user_data(user)
|
1182
|
-
self.hubssolib_current_session.session_user = user
|
1183
|
-
end
|
1184
|
-
|
1185
|
-
def hubssolib_get_session_data
|
1186
|
-
|
952
|
+
def hubssolib_get_session_proxy
|
1187
953
|
# If we're not using SSL, forget it
|
1188
|
-
return nil unless request.ssl? ||
|
954
|
+
return nil unless request.ssl? || hub_bypass_ssl?
|
1189
955
|
|
1190
|
-
|
1191
|
-
|
956
|
+
key = cookies[HUB_COOKIE_NAME] || SecureRandom.uuid
|
957
|
+
hub_session = hubssolib_factory().get_hub_session_proxy(key, request.remote_ip)
|
958
|
+
key = hub_session.session_key_rotation unless hub_session.nil?
|
1192
959
|
|
1193
|
-
|
1194
|
-
key
|
1195
|
-
|
1196
|
-
|
960
|
+
cookies[HUB_COOKIE_NAME] = {
|
961
|
+
:value => key,
|
962
|
+
:domain => :all,
|
963
|
+
:path => '/',
|
964
|
+
:secure => ! hub_bypass_ssl?,
|
965
|
+
:httponly => true
|
966
|
+
}
|
1197
967
|
|
1198
|
-
return
|
968
|
+
return hub_session
|
1199
969
|
|
1200
970
|
rescue Exception => e
|
1201
971
|
|
@@ -1208,7 +978,8 @@ module HubSsoLib
|
|
1208
978
|
|
1209
979
|
suffix = '/' + CGI::escape(CGI::escape(hubssolib_set_exception_data(e)))
|
1210
980
|
new_path = HUB_PATH_PREFIX + '/tasks/service'
|
1211
|
-
redirect_to
|
981
|
+
redirect_to(new_path + suffix) unless request.path.include?(new_path)
|
982
|
+
|
1212
983
|
return nil
|
1213
984
|
end
|
1214
985
|
|
@@ -1225,7 +996,7 @@ module HubSsoLib
|
|
1225
996
|
# be allowed access.
|
1226
997
|
#
|
1227
998
|
def hubssolib_enumerate_users
|
1228
|
-
sessions = hubssolib_factory().
|
999
|
+
sessions = hubssolib_factory().enumerate_hub_sessions()
|
1229
1000
|
users = []
|
1230
1001
|
|
1231
1002
|
sessions.each do |key, value|
|
@@ -1252,7 +1023,12 @@ module HubSsoLib
|
|
1252
1023
|
# message.
|
1253
1024
|
#
|
1254
1025
|
def hubssolib_set_exception_data(e)
|
1255
|
-
|
1026
|
+
if Rails.env.development?
|
1027
|
+
backtrace = e.backtrace.join( ", " )
|
1028
|
+
HubSsoLib::Serialiser.serialise_object("#{ e.message }: #{ backtrace }"[0..511])
|
1029
|
+
else
|
1030
|
+
HubSsoLib::Serialiser.serialise_object(e.message[0..511])
|
1031
|
+
end
|
1256
1032
|
end
|
1257
1033
|
|
1258
1034
|
# Decode exception data encoded with hubssolib_set_exception_data.
|
@@ -1260,7 +1036,7 @@ module HubSsoLib
|
|
1260
1036
|
# are any decoding problems. Pass the encoded data.
|
1261
1037
|
#
|
1262
1038
|
def hubssolib_get_exception_data(data)
|
1263
|
-
HubSsoLib::
|
1039
|
+
HubSsoLib::Serialiser.deserialise_object(data)
|
1264
1040
|
end
|
1265
1041
|
|
1266
1042
|
# Various accessors that ultimately run through the DRb server if
|