hubssolib 0.2.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/VERSION +1 -0
  2. data/lib/hub_sso_lib.rb +1345 -0
  3. metadata +68 -0
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.9 (04-May-2014)
@@ -0,0 +1,1345 @@
1
+ #######################################################################
2
+ # Module: HubSsoLib #
3
+ # (C) Hipposoft 2006-2014 #
4
+ # #
5
+ # Purpose: Cross-application same domain single sign-on support. #
6
+ # #
7
+ # Author: A.D.Hodgkinson #
8
+ # #
9
+ # History: 20-Oct-2006 (ADH): First version of stand-alone library, #
10
+ # split from Hub application. #
11
+ # 08-Dec-2006 (ADH): DRB URI, path prefix and random file #
12
+ # path come from environment variables. #
13
+ # 09-Mar-2011 (ADH): Updated for Hub on Rails 2.3.11 along #
14
+ # with several important bug fixes. #
15
+ # 04-May-2014 (ADH): HUB_BYPASS_SSL environment variable #
16
+ # aids testing with e.g. Mongrel by #
17
+ # allowing non-SSL use. #
18
+ #######################################################################
19
+
20
+ module HubSsoLib
21
+
22
+ require 'drb'
23
+
24
+ # DRb connection
25
+ HUBSSOLIB_DRB_URI = ENV['HUB_CONNECTION_URI']
26
+
27
+ # Location of Hub application root.
28
+ HUB_PATH_PREFIX = ENV['HUB_PATH_PREFIX']
29
+
30
+ # Time limit, *in seconds*, for the account inactivity timeout.
31
+ # If a user performs no Hub actions during this time they will
32
+ # be automatically logged out upon their next action.
33
+ HUBSSOLIB_IDLE_TIME_LIMIT = 60 * 60
34
+
35
+ # Random file location.
36
+ HUBSSOLIB_RND_FILE_PATH = ENV['HUB_RANDOM_FILE']
37
+
38
+ # Shared cookie name and path.
39
+ HUBSSOLIB_COOKIE_NAME = 'hubapp_shared_id'
40
+ HUBSSOLIB_COOKIE_PATH = ENV['HUB_COOKIE_PATH']
41
+
42
+ # Bypass SSL, for testing purposes?
43
+ HUBSSOLIB_BYPASS_SSL = ( ENV['HUB_BYPASS_SSL'] == "true" )
44
+
45
+ # Cache the random data. Assuming FCGI or similar, this code gets
46
+ # executed only once per FCGI instance initialisation rather than
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
59
+
60
+ #######################################################################
61
+ # Class: Crypto #
62
+ # (C) Hipposoft 2006 #
63
+ # #
64
+ # Purpose: Encryption and decryption utilities. #
65
+ # #
66
+ # Author: A.D.Hodgkinson #
67
+ # #
68
+ # History: 28-Aug-2006 (ADH): First version. #
69
+ # 20-Oct-2006 (ADH): Integrated into HubSsoLib, renamed to #
70
+ # 'Crypto' from 'HubSsoCrypto'. #
71
+ #######################################################################
72
+
73
+ # Encryption and decryption utility object. Once instantiated, a
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.
87
+ #
88
+ # The block of random data is obtained from the DRb server. It is usually
89
+ # a RAM-cached near-random file. The important behaviour is that the
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
250
+
251
+ def decode_object(data, passphrase)
252
+ HubSsoLib::Crypto.decode_object(data, passphrase)
253
+ end
254
+
255
+ # "Scramble" a passphrase. Cookie data encryption is done purely so that
256
+ # some hypothetical malicious user cannot easily examine or modify the
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)
304
+ end
305
+
306
+ end # Crypto class
307
+
308
+ #######################################################################
309
+ # Class: Roles #
310
+ # (C) Hipposoft 2006 #
311
+ # #
312
+ # Purpose: Shared methods for handling user account roles. #
313
+ # #
314
+ # Author: A.D.Hodgkinson #
315
+ # #
316
+ # History: 17-Oct-2006 (ADH): Adapted from Clubhouse. #
317
+ # 20-Oct-2006 (ADH): Integrated into HubSsoLib. #
318
+ #######################################################################
319
+
320
+ class Roles
321
+
322
+ # Association of symbolic role names to display names, in no
323
+ # particular order.
324
+ #
325
+ ROLES = {
326
+ :admin => 'Administrator',
327
+ :webmaster => 'Webmaster',
328
+ :privileged => 'Advanced user',
329
+ :normal => 'Normal user'
330
+ }
331
+
332
+ ADMIN = :admin
333
+ NORMAL = :normal
334
+
335
+ # Return the display name of a given role symbol. Class method.
336
+ #
337
+ def self.get_display_name(symbol)
338
+ ROLES[symbol]
339
+ end
340
+
341
+ # Return all display names in an array. Class method.
342
+
343
+ def self.get_display_names
344
+ ROLES.values
345
+ end
346
+
347
+ # Return an array of known role symbols. They can be used with
348
+ # methods like get_display_name. Class method.
349
+
350
+ def self.get_role_symbols
351
+ ROLES.keys
352
+ end
353
+
354
+ # Initialize a new Roles object. Pass 'true' if this is for
355
+ # an admin user account, else 'false'. Default is 'false'. Note
356
+ # that further down in this file, the String, Symbol and Array
357
+ # classes are extended with to_authenticated_roles methods, which
358
+ # provide other ways of creating Roles objects.
359
+ #
360
+ def initialize(admin = false)
361
+ if (admin)
362
+ @role_array = [ ADMIN ]
363
+ else
364
+ @role_array = [ NORMAL ]
365
+ end
366
+ end
367
+
368
+ # Adds a role, supplied as a string or symbol, to the internal list.
369
+ # A non-nil return indicates that the role was already present.
370
+ #
371
+ def add(role)
372
+ @role_array.push(role.to_s.intern).uniq!
373
+ end
374
+
375
+ # Deletes a role, supplied as a string or symbol, from the internal
376
+ # list. A nil return indicates that the role was not in the list.
377
+ #
378
+ def delete(role)
379
+ @role_array.delete(role.to_s.intern)
380
+ end
381
+
382
+ # Delete all roles from the internal list.
383
+ #
384
+ def clear
385
+ @role_array.clear
386
+ end
387
+
388
+ # Return a copy of the internal roles list as a string.
389
+ #
390
+ def to_s
391
+ return @role_array.join(',')
392
+ end
393
+
394
+ # Return a copy of the internal roles list as an array.
395
+ #
396
+ def to_a
397
+ return @role_array.dup
398
+ end
399
+
400
+ # Return a copy of the intenal roles list as a human readable string.
401
+ #
402
+ def to_human_s
403
+ human_names = []
404
+
405
+ @role_array.each do |role|
406
+ human_names.push(HubSsoLib::Roles.get_display_name(role))
407
+ end
408
+
409
+ if (human_names.length == 0)
410
+ return ''
411
+ elsif (human_names.length == 1)
412
+ return human_names[0]
413
+ else
414
+ return human_names[0..-2].join(', ') + ' and ' + human_names.last
415
+ end
416
+ end
417
+
418
+ # Do nothing - this is just useful for polymorphic code, where a function
419
+ # can take a String, Array, Symbol or Roles object and make the
420
+ # same method call to return a Roles object in return.
421
+ #
422
+ def to_authenticated_roles
423
+ return self
424
+ end
425
+
426
+ # Does the internal list of roles include the supplied role or roles?
427
+ # The roles can be given as an array of individual role symbols or
428
+ # equivalent strings, or as a single symbol or single equivalent
429
+ # symbol, or as a string containing equivalents of role symbols in a
430
+ # comma-separated list (no white space or other spurious characters).
431
+ # Returns 'true' if the internal list of roles includes at least one
432
+ # of the supplied roles, else 'false'.
433
+ #
434
+ def include?(roles)
435
+ return false if roles.nil?
436
+
437
+ # Ensure we've an array of roles, one way or another
438
+ roles = roles.to_s if roles.class == Symbol
439
+ roles = roles.split(',') if roles.class == String
440
+
441
+ roles.each do |role|
442
+ return true if @role_array.include?(role.to_s.intern)
443
+ end
444
+
445
+ return false
446
+ end
447
+
448
+ # Synonym for 'include?'.
449
+ #
450
+ alias includes? include?
451
+
452
+ # Validate the list of roles. Validation means ensuring that all
453
+ # roles in this object are found in the internal ROLES hash. Returns
454
+ # true if the roles validate or false if unknown roles are found.
455
+ #
456
+ def validate
457
+ return false if @role_array.empty?
458
+
459
+ @role_array.each do |role|
460
+ return false unless ROLES[role]
461
+ end
462
+
463
+ return true
464
+ end
465
+
466
+ end # Roles class
467
+
468
+ #######################################################################
469
+ # Class: Permissions #
470
+ # (C) Hipposoft 2006 #
471
+ # #
472
+ # Purpose: Methods to help, in conjunction with Roles, determine the #
473
+ # access permissions a particular user is granted. #
474
+ # #
475
+ # Author: A.D.Hodgkinson #
476
+ # #
477
+ # History: 17-Oct-2006 (ADH): Adapted from Clubhouse. #
478
+ # 20-Oct-2006 (ADH): Integrated into HubSsoLib. #
479
+ #######################################################################
480
+
481
+ class Permissions
482
+
483
+ # Initialize a permissions object. The map is a hash which maps action
484
+ # names, expressed as symbols, to roles, expressed as individual symbols,
485
+ # equivalent strings, or arrays of multiple strings or symbols. Use 'nil'
486
+ # to indicate permission for the general public - no login required - or
487
+ # simply omit the action (unlisted actions are permitted).
488
+ #
489
+ # Example mapping for a generic controller:
490
+ #
491
+ # {
492
+ # :new => [ :admin, :webmaster, :privileged, :normal ],
493
+ # :create => [ :admin, :webmaster, :privileged, :normal ],
494
+ # :edit => [ :admin, :webmaster, :privileged, :normal ],
495
+ # :update => [ :admin, :webmaster, :privileged, :normal ],
496
+ # :delete => [ :admin, :webmaster, :privileged ],
497
+ # :list => nil,
498
+ # :show => nil
499
+ # }
500
+ #
501
+ def initialize(pmap)
502
+ @permissions = pmap
503
+ end
504
+
505
+ # Does the given Roles object grant permission for the given action,
506
+ # expressed as a string or symbol? Returns 'true' if so, else 'false'.
507
+ #
508
+ # If a role is given as some other type, an attempt is made to convert
509
+ # it to a Roles object internally (so you could pass a role symbol,
510
+ # string, array of symbols or strings, or comma-separated string).
511
+ #
512
+ # Passing an empty roles string will tell you whether or not the
513
+ # action requires login. Only actions not in the permissions list or
514
+ # those with a 'nil' list of roles will generate a result 'true',
515
+ # since any other actions will require your empty roles string to
516
+ # include at least one role (which it obviously doesn't).
517
+ #
518
+ def permitted?(roles, action)
519
+ action = action.to_s.intern
520
+ roles = roles.to_authenticated_roles
521
+
522
+ return true unless @permissions.include?(action)
523
+ return true if @permissions[action].nil?
524
+ return roles.include?(@permissions[action])
525
+ end
526
+ end # Permissions class
527
+
528
+ #######################################################################
529
+ # Class: User #
530
+ # (C) Hipposoft 2006 #
531
+ # #
532
+ # Purpose: A representation of the Hub application's User Model in #
533
+ # terms of a simple set of properties, so that applications #
534
+ # don't need User access to understand user attributes. #
535
+ # #
536
+ # Author: A.D.Hodgkinson #
537
+ # #
538
+ # History: 21-Oct-2006 (ADH): Created. #
539
+ #######################################################################
540
+
541
+ class User
542
+
543
+ # This *must not* be 'undumped', since it gets passed from clients
544
+ # back to the persistent DRb server process. A client thread may
545
+ # disappear and be recreated by the web server at any time; if the
546
+ # user object is undumpable, then the DRb server has to *call back
547
+ # to the client* (in DRb, clients are also servers...!) to find out
548
+ # about the object. Trouble is, if the client thread has been
549
+ # recreated, the server will be trying to access to stale objects
550
+ # that only exist if the garbage collector hasn't got to them yet.
551
+
552
+ attr_accessor :user_salt
553
+ attr_accessor :user_roles
554
+ attr_accessor :user_updated_at
555
+ attr_accessor :user_activated_at
556
+ attr_accessor :user_real_name
557
+ attr_accessor :user_crypted_password
558
+ attr_accessor :user_remember_token_expires_at
559
+ attr_accessor :user_activation_code
560
+ attr_accessor :user_member_id
561
+ attr_accessor :user_id
562
+ attr_accessor :user_password_reset_code
563
+ attr_accessor :user_remember_token
564
+ attr_accessor :user_email
565
+ attr_accessor :user_created_at
566
+ attr_accessor :user_password_reset_code_expires_at
567
+
568
+ def initialize
569
+ @user_salt = nil
570
+ @user_roles = nil
571
+ @user_updated_at = nil
572
+ @user_activated_at = nil
573
+ @user_real_name = nil
574
+ @user_crypted_password = nil
575
+ @user_remember_token_expires_at = nil
576
+ @user_activation_code = nil
577
+ @user_member_id = nil
578
+ @user_id = nil
579
+ @user_password_reset_code = nil
580
+ @user_remember_token = nil
581
+ @user_email = nil
582
+ @user_created_at = nil
583
+ @user_password_reset_code_expires_at = nil
584
+ end
585
+ end # User class
586
+
587
+ #######################################################################
588
+ # Class: Session #
589
+ # (C) Hipposoft 2006 #
590
+ # #
591
+ # Purpose: Session support object, used to store session metadata in #
592
+ # an insecure cross-application cookie. #
593
+ # #
594
+ # Author: A.D.Hodgkinson #
595
+ # #
596
+ # History: 22-Oct-2006 (ADH): Created. #
597
+ #######################################################################
598
+
599
+ class Session
600
+
601
+ # Unlike a User, this *is* undumpable since it only gets passed from
602
+ # server to client. The server's always here to service requests
603
+ # from the client and used sessions are never garbage collected
604
+ # since the DRb server's front object, a SessionFactory, keeps them
605
+ # in a hash held within an instance variable.
606
+
607
+ include DRb::DRbUndumped
608
+
609
+ attr_accessor :session_last_used
610
+ attr_accessor :session_return_to
611
+ attr_accessor :session_flash
612
+ attr_accessor :session_user
613
+
614
+ def initialize
615
+ @session_last_used = Time.now.utc
616
+ @session_return_to = nil
617
+ @session_flash = {}
618
+ @session_user = HubSsoLib::User.new
619
+ end
620
+ end # Session class
621
+
622
+ #######################################################################
623
+ # Class: SessionFactory #
624
+ # (C) Hipposoft 2006 #
625
+ # #
626
+ # Purpose: Build Session objects for DRb server clients. Maintains a #
627
+ # hash of Session objects. #
628
+ # #
629
+ # Author: A.D.Hodgkinson #
630
+ # #
631
+ # History: 26-Oct-2006 (ADH): Created. #
632
+ #######################################################################
633
+
634
+ class SessionFactory
635
+ def initialize
636
+ @sessions = {}
637
+ end
638
+
639
+ def get_session(key)
640
+ unless (@sessions.has_key? key)
641
+ @sessions[key] = HubSsoLib::Session.new
642
+ end
643
+
644
+ return @sessions[key]
645
+ end
646
+
647
+ def enumerate_sessions
648
+ @sessions
649
+ end
650
+ end
651
+
652
+ #######################################################################
653
+ # Module: Server #
654
+ # (C) Hipposoft 2006 #
655
+ # #
656
+ # Purpose: DRb server to provide shared data across applications. #
657
+ # Thanks to RubyPanther, rubyonrails IRC, for suggesting #
658
+ # this after a cookie-based scheme failed. #
659
+ # #
660
+ # Include the module then call hubssolib_launch_server. The #
661
+ # call will not return as the server runs indefinitely. #
662
+ # #
663
+ # Author: A.D.Hodgkinson #
664
+ # #
665
+ # History: 26-Oct-2006 (ADH): Created. #
666
+ #######################################################################
667
+
668
+ module Server
669
+ def hubssolib_launch_server
670
+ @@session_factory = HubSsoLib::SessionFactory.new
671
+ DRb.start_service(HUBSSOLIB_DRB_URI, @@session_factory)
672
+ DRb.thread.join
673
+ end
674
+ end # Server module
675
+
676
+ #######################################################################
677
+ # Module: Core #
678
+ # Various authors #
679
+ # #
680
+ # Purpose: The barely recognisable core of acts_as_authenticated's #
681
+ # AuthenticatedSystem module, modified to work with the #
682
+ # other parts of HubSsoLib. You should include this module #
683
+ # to use its facilities. #
684
+ # #
685
+ # Author: Various; adaptation by A.D.Hodgkinson #
686
+ # #
687
+ # History: 20-Oct-2006 (ADH): Integrated into HubSsoLib. #
688
+ #######################################################################
689
+
690
+ module Core
691
+
692
+ # Returns true or false if the user is logged in.
693
+ #
694
+ # Preloads @hubssolib_current_user with user data if logged in.
695
+ #
696
+ def hubssolib_logged_in?
697
+ !!self.hubssolib_current_user
698
+ end
699
+
700
+ # Check if the user is authorized to perform the current action. If calling
701
+ # from a helper, pass the action name and class name; otherwise by default,
702
+ # the current action name and 'self.class' will be used.
703
+ #
704
+ # Override this method in your controllers if you want to restrict access
705
+ # to a different set of actions. Presently, the current user's roles are
706
+ # compared against the caller's permissions hash and the action name.
707
+ #
708
+ def hubssolib_authorized?(action = action_name, classname = self.class)
709
+
710
+ # Classes with no permissions object always authorise everything.
711
+ # Otherwise, ask the permissions object for its opinion.
712
+
713
+ if (classname.respond_to? :hubssolib_permissions)
714
+ return classname.hubssolib_permissions.permitted?(hubssolib_get_user_roles, action)
715
+ else
716
+ return true
717
+ end
718
+ end
719
+
720
+ # Is the current user privileged? Anything other than normal user
721
+ # privileges will suffice. Can be called if not logged in. Returns
722
+ # 'false' for logged out or normal user privileges only, else 'true'.
723
+ #
724
+ def hubssolib_privileged?
725
+ return false unless hubssolib_logged_in?
726
+
727
+ pnormal = HubSsoLib::Roles.new(false).to_s
728
+ puser = hubssolib_get_user_roles.to_s
729
+
730
+ return (puser && !puser.empty? && puser != pnormal)
731
+ end
732
+
733
+ # Log out the user. Very few applications should ever need to call this,
734
+ # though Hub certainly does and it gets used internally too.
735
+ #
736
+ def hubssolib_log_out
737
+ # Causes the "hubssolib_current_[foo]=" methods to run, which
738
+ # deal with everything else.
739
+ self.hubssolib_current_user = nil
740
+ self.hubssolib_current_session = nil
741
+ end
742
+
743
+ # Accesses the current user, via the DRb server if necessary
744
+ #
745
+ def hubssolib_current_user
746
+ hubssolib_get_user_data
747
+ end
748
+
749
+ # Store the given user data in the cookie
750
+ #
751
+ def hubssolib_current_user=(new_user)
752
+ hubssolib_set_user_data(new_user)
753
+ end
754
+
755
+ # Accesses the current session from the cookie. Creates a new
756
+ # session object if need be.
757
+ #
758
+ def hubssolib_current_session
759
+ @hubssolib_current_session ||= hubssolib_get_session_data
760
+ end
761
+
762
+ # Store the given session data.
763
+ #
764
+ def hubssolib_current_session=(new_session)
765
+ @hubssolib_current_session = new_session
766
+ end
767
+
768
+ # Public read-only accessor methods for common user activities:
769
+ # return the current user's roles as a Roles object, or nil if
770
+ # there's no user.
771
+ #
772
+ def hubssolib_get_user_roles
773
+ user = self.hubssolib_current_user
774
+ user ? user.user_roles.to_authenticated_roles : nil
775
+ end
776
+
777
+ # Public read-only accessor methods for common user activities:
778
+ # return the current user's name as a string, or nil if there's
779
+ # no user. See also hubssolib_unique_name.
780
+ #
781
+ def hubssolib_get_user_name
782
+ user = self.hubssolib_current_user
783
+ user ? user.user_real_name : nil
784
+ end
785
+
786
+ # Public read-only accessor methods for common user activities:
787
+ # return the Hub database ID of the current user account, or
788
+ # nil if there's no user. See also hubssolib_unique_name.
789
+ #
790
+ def hubssolib_get_user_id
791
+ user = self.hubssolib_current_user
792
+ user ? user.user_id : nil
793
+ end
794
+
795
+ # Public read-only accessor methods for common user activities:
796
+ # return the current user's e-mail address, or nil if there's
797
+ # no user.
798
+ #
799
+ def hubssolib_get_user_address
800
+ user = self.hubssolib_current_user
801
+ user ? user.user_email : nil
802
+ end
803
+
804
+ # Return a human-readable unique ID for a user. We don't want to
805
+ # have e-mail addresses all over the place, but don't want to rely
806
+ # on real names as unique - they aren't. Instead, produce a
807
+ # composite of the user's account database ID (which must be
808
+ # unique by definition) and their real name. See also
809
+ # hubssolib_get_name.
810
+ #
811
+ def hubssolib_unique_name
812
+ user = hubssolib_current_user
813
+ user ? "#{user.user_real_name} (#{user.user_id})" : 'Anonymous'
814
+ end
815
+
816
+ # Main filter method to implement HubSsoLib permissions management,
817
+ # session expiry and so-on. Call from controllers only, always as a
818
+ # before_fitler.
819
+ #
820
+ def hubssolib_beforehand
821
+
822
+ # Does this action require a logged in user?
823
+
824
+ if (self.class.respond_to? :hubssolib_permissions)
825
+ login_is_required = !self.class.hubssolib_permissions.permitted?('', action_name)
826
+ else
827
+ login_is_required = false
828
+ end
829
+
830
+ # If we require login but we're logged out, redirect to Hub login.
831
+
832
+ logged_in = hubssolib_logged_in?
833
+
834
+ if (login_is_required and logged_in == false)
835
+ hubssolib_store_location
836
+ return hubssolib_must_login
837
+ end
838
+
839
+ # If we reach here the user is either logged, or the method does
840
+ # not require them to be. In the latter case, if we're not logged
841
+ # in there is no more work to do - exit early.
842
+
843
+ return true unless logged_in # true -> let action processing continue
844
+
845
+ # So we reach here knowing we're logged in, but the action may or
846
+ # may not require authorisation.
847
+
848
+ unless (login_is_required)
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
865
+
866
+ # Login *is* required for this action. If the session expires,
867
+ # redirect to Hub's login page via its expiry action. Otherwise
868
+ # check authorisation and allow action processing to continue
869
+ # if OK, else indicate that access is denied.
870
+
871
+ if (hubssolib_session_expired?)
872
+ hubssolib_store_location
873
+ hubssolib_log_out
874
+ hubssolib_set_flash(:attention, 'Sorry, your session timed out; you need to log in again to continue.')
875
+
876
+ # We mean this: redirect_to :controller => 'account', :action => 'login'
877
+ # ...except for the Hub, rather than the current application (whatever
878
+ # it may be).
879
+ redirect_to HUB_PATH_PREFIX + '/account/login'
880
+ else
881
+ hubssolib_set_last_used(Time.now.utc)
882
+ return hubssolib_authorized? ? true : hubssolib_access_denied
883
+ end
884
+
885
+ end
886
+ end
887
+
888
+ # Main after_filter method to tidy up after running state changes.
889
+ #
890
+ def hubssolib_afterwards
891
+ begin
892
+ DRb.current_server
893
+ DRb.stop_service()
894
+ rescue DRb::DRbServerNotFound
895
+ # Nothing to do; no service is running.
896
+ end
897
+ end
898
+
899
+ # Store the URI of the current request in the session, or store the
900
+ # optional supplied specific URI.
901
+ #
902
+ # We can return to this location by calling #redirect_back_or_default.
903
+ #
904
+ def hubssolib_store_location(uri_str = request.request_uri)
905
+
906
+ if (uri_str && !uri_str.empty?)
907
+ uri_str = hubssolib_promote_uri_to_ssl(uri_str, request.host) unless request.ssl?
908
+ hubssolib_set_return_to(uri_str)
909
+ else
910
+ hubssolib_set_return_to(nil)
911
+ end
912
+
913
+ end
914
+
915
+ # Redirect to the URI stored by the most recent store_location call or
916
+ # to the passed default.
917
+ def hubssolib_redirect_back_or_default(default)
918
+ url = hubssolib_get_return_to
919
+ hubssolib_set_return_to(nil)
920
+
921
+ redirect_to(url || default)
922
+ end
923
+
924
+ # Take a URI and pass an optional host parameter. Decomposes the URI,
925
+ # sets the host you provide (or leaves it alone if you omit the
926
+ # parameter), then forces the scheme to 'https'. Returns the result
927
+ # as a flat string.
928
+
929
+ def hubssolib_promote_uri_to_ssl(uri_str, host = nil)
930
+ uri = URI.parse(uri_str)
931
+ uri.host = host if host
932
+ uri.scheme = HUBSSOLIB_BYPASS_SSL ? 'http' : 'https'
933
+ return uri.to_s
934
+ end
935
+
936
+ # Ensure the current request is carried out over HTTPS by redirecting
937
+ # back to the current URL with the HTTPS protocol if it isn't. Returns
938
+ # 'true' if not redirected (already HTTPS), else 'false'.
939
+ #
940
+ def hubssolib_ensure_https
941
+ if request.ssl? || HUBSSOLIB_BYPASS_SSL
942
+ return true
943
+ else
944
+ # This isn't reliable: redirect_to({ :protocol => 'https://' })
945
+ redirect_to( hubssolib_promote_uri_to_ssl( request.request_uri, request.host ) )
946
+ return false
947
+ end
948
+ end
949
+
950
+ # Public methods to set some data that would normally go in @session,
951
+ # but can't because it needs to be accessed across applications. It is
952
+ # put in an insecure support cookie instead. There are some related
953
+ # private methods for things like session expiry too.
954
+ #
955
+ def hubssolib_get_flash()
956
+ f = self.hubssolib_current_session ? self.hubssolib_current_session.session_flash : nil
957
+ return f || {}
958
+ end
959
+
960
+ def hubssolib_set_flash(symbol, message)
961
+ return unless self.hubssolib_current_session
962
+ f = hubssolib_get_flash
963
+ f[symbol] = message
964
+ self.hubssolib_current_session.session_flash = f
965
+ end
966
+
967
+ def hubssolib_clear_flash
968
+ return unless self.hubssolib_current_session
969
+ self.hubssolib_current_session.session_flash = {}
970
+ end
971
+
972
+ # Helper methods to output flash data. It isn't merged into the standard
973
+ # application flash with a filter because the rather daft and difficult
974
+ # to manage lifecycle model of the standard flash gets in the way.
975
+ #
976
+ # First, return tags for a flash using the given key, clearing the
977
+ # result in the flash hash now it has been used.
978
+ #
979
+ def hubssolib_flash_tag(key)
980
+ value = hubssolib_get_flash[key]
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.
991
+ #
992
+ def hubssolib_standard_flash_tag(key)
993
+ value = flash[key] if defined?(flash)
994
+
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
+ # These known key values are used to guarantee an order in the output
1008
+ # for cases where multiple messages are defined.
1009
+
1010
+ tags = hubssolib_flash_tag(:notice) <<
1011
+ hubssolib_flash_tag(:attention) <<
1012
+ hubssolib_flash_tag(:alert)
1013
+
1014
+ tags << hubssolib_standard_flash_tag(:notice) <<
1015
+ hubssolib_standard_flash_tag(:attention) <<
1016
+ hubssolib_standard_flash_tag(:alert)
1017
+
1018
+ # Now pick up anything else.
1019
+
1020
+ hubssolib_get_flash.each do |key, value|
1021
+ tags << hubssolib_flash_tag(key) if (value and !value.empty?)
1022
+ end
1023
+
1024
+ flash.each do |key, value|
1025
+ tags << hubssolib_standard_flash_tag(key) if (value and !value.empty?)
1026
+ end if defined?(flash)
1027
+
1028
+ return tags
1029
+ end
1030
+
1031
+ # Retrieve the message of an exception stored as an object in the given
1032
+ # string.
1033
+ #
1034
+ def hubssolib_get_exception_message(id_data)
1035
+ hubssolib_get_exception_data(CGI::unescape(id_data))
1036
+ end
1037
+
1038
+ # Inclusion hook to make various methods available as ActionView
1039
+ # helper methods.
1040
+ #
1041
+ def self.included(base)
1042
+ base.send :helper_method,
1043
+ :hubssolib_current_user,
1044
+ :hubssolib_unique_name,
1045
+ :hubssolib_logged_in?,
1046
+ :hubssolib_authorized?,
1047
+ :hubssolib_privileged?,
1048
+ :hubssolib_flash_tags
1049
+ rescue
1050
+ # We're not always included in controllers...
1051
+ nil
1052
+ end
1053
+
1054
+ private
1055
+
1056
+ # Indicate that the user must log in to complete their request.
1057
+ # Returns false to enable a before_filter to return through this
1058
+ # method while ensuring that the previous action processing is
1059
+ # halted (since the overall return value is therefore 'false').
1060
+ #
1061
+ def hubssolib_must_login
1062
+ # If HTTP, redirect to the same place, but HTTPS. Then we can store the
1063
+ # flash and return-to in the session data. We'll have the same set of
1064
+ # before-filter operations running and they'll find out we're either
1065
+ # authorised after all, or come back to this very function, which will
1066
+ # now be happily running from an HTTPS connection and will go on to set
1067
+ # the flash and redirect to the login page.
1068
+
1069
+ if hubssolib_ensure_https
1070
+ hubssolib_set_flash(:alert, 'You must log in before you can continue.')
1071
+ redirect_to HUB_PATH_PREFIX + '/account/login'
1072
+ end
1073
+
1074
+ return false
1075
+ end
1076
+
1077
+ # Indicate access is denied for a given logged in user's request.
1078
+ # Returns false to enable a before_filter to return through this
1079
+ # method while ensuring that the previous action processing is
1080
+ # halted (since the overall return value is therefore 'false').
1081
+ #
1082
+ def hubssolib_access_denied
1083
+ # See hubsso_must_login for the reason behind the following call.
1084
+
1085
+ if hubssolib_ensure_https
1086
+ hubssolib_set_flash(:alert, 'You do not have permission to carry out that action on this site.')
1087
+ redirect_to HUB_PATH_PREFIX + '/'
1088
+ end
1089
+
1090
+ return false
1091
+ end
1092
+
1093
+ # Check conditions for session expiry. Returns 'true' if session's
1094
+ # last_used date indicates expiry, else 'false'.
1095
+ #
1096
+ def hubssolib_session_expired?
1097
+
1098
+ # 23-Oct-2006 (ADH):
1099
+ #
1100
+ # An exception, which is also a security hole of sorts. POST requests
1101
+ # cannot be redirected because HTTP doesn't have that concept. If a user
1102
+ # is editing a Wiki page, say, then goes away, comes back later and now
1103
+ # finishes their edits, their session may have timed out. They submit
1104
+ # the page but it's by POST so their submission details are lost. If they
1105
+ # are lucky their browser might remember the form contents if they go
1106
+ # back but not all do and not all users would think of doing that.
1107
+ #
1108
+ # To work around this, don't enforce a timeout for POST requests. Should
1109
+ # a user on a public computer not log out, then a hacker arrive *after*
1110
+ # the session expiry time (if they arrive before it expires then the
1111
+ # except for POSTs is irrelevant), they could recover the session by
1112
+ # constructing a POST request. It's a convoluted path, requires a user to
1113
+ # have not logged out anyway, and the Hub isn't intended for Fort Knox.
1114
+ # At the time of writing the trade-off of usability vs security is
1115
+ # considered acceptable, though who knows, the view may change in future.
1116
+
1117
+ last_used = hubssolib_get_last_used
1118
+ (request.method != :post && last_used && Time.now.utc - last_used > HUBSSOLIB_IDLE_TIME_LIMIT)
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)
1125
+ end
1126
+
1127
+ # Set the given cookie to a value of the given data, which
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
+
1187
+ # If we're not using SSL, forget it
1188
+ return nil unless request.ssl? || HUBSSOLIB_BYPASS_SSL
1189
+
1190
+ # If we've no cookie, we need a new session ID
1191
+ key = hubssolib_get_secure_cookie_data(HUBSSOLIB_COOKIE_NAME)
1192
+
1193
+ unless (key)
1194
+ key = HubSsoLib::Crypto.pack64(HubSsoLib::Crypto.random_data(48))
1195
+ hubssolib_set_secure_cookie_data(HUBSSOLIB_COOKIE_NAME, key)
1196
+ end
1197
+
1198
+ return hubssolib_factory().get_session(key)
1199
+
1200
+ rescue Exception => e
1201
+
1202
+ # At this point there tends to be no Session data, so we're
1203
+ # going to have to encode the exception data into the URI...
1204
+ # It must be escaped twice, as many servers treat "%2F" in a
1205
+ # URI as a "/" and Apache may flat refuse to serve the page,
1206
+ # raising a 404 error unless "AllowEncodedSlashes on" is
1207
+ # specified in its configuration.
1208
+
1209
+ suffix = '/' + CGI::escape(CGI::escape(hubssolib_set_exception_data(e)))
1210
+ new_path = HUB_PATH_PREFIX + '/tasks/service'
1211
+ redirect_to new_path + suffix unless request.path.include?(new_path)
1212
+ return nil
1213
+ end
1214
+
1215
+ def hubssolib_set_session_data(session)
1216
+ # Nothing to do presently - DRb handles everything
1217
+ end
1218
+
1219
+ # Return an array of Hub User objects representing users based
1220
+ # on a list of known sessions returned by the DRb server. Note
1221
+ # that if an application exposes this method to a view, it is
1222
+ # up to the application to ensure sufficient access permission
1223
+ # protection for that view according to the webmaster's choice
1224
+ # of site security level. Generally, normal users should not
1225
+ # be allowed access.
1226
+ #
1227
+ def hubssolib_enumerate_users
1228
+ sessions = hubssolib_factory().enumerate_sessions()
1229
+ users = []
1230
+
1231
+ sessions.each do |key, value|
1232
+ user = value.session_user
1233
+ users.push(user) if (user && user.respond_to?(:user_id) && user.user_id)
1234
+ end
1235
+
1236
+ return users
1237
+
1238
+ rescue Exception => e
1239
+
1240
+ # At this point there tends to be no Session data, so we're
1241
+ # going to have to encode the exception data into the URI...
1242
+ # See earlier for double-escaping rationale.
1243
+
1244
+ suffix = '/' + CGI::escape(CGI::escape(hubssolib_set_exception_data(e)))
1245
+ new_path = HUB_PATH_PREFIX + '/tasks/service'
1246
+ redirect_to new_path + suffix unless request.path.include?(new_path)
1247
+ return nil
1248
+ end
1249
+
1250
+ # Encode exception data into a string suitable for using in a URL
1251
+ # if CGI escaped first. Pass the exception object; stores only the
1252
+ # message.
1253
+ #
1254
+ def hubssolib_set_exception_data(e)
1255
+ HubSsoLib::Crypto.encode_object(e.message, request.remote_ip)
1256
+ end
1257
+
1258
+ # Decode exception data encoded with hubssolib_set_exception_data.
1259
+ # Returns the originally stored message string or 'nil' if there
1260
+ # are any decoding problems. Pass the encoded data.
1261
+ #
1262
+ def hubssolib_get_exception_data(data)
1263
+ HubSsoLib::Crypto.decode_object(data, request.remote_ip)
1264
+ end
1265
+
1266
+ # Various accessors that ultimately run through the DRb server if
1267
+ # the session data is available, else return default values.
1268
+
1269
+ def hubssolib_get_last_used
1270
+ session = self.hubssolib_current_session
1271
+ session ? session.session_last_used : Time.now.utc
1272
+ end
1273
+
1274
+ def hubssolib_set_last_used(time)
1275
+ return unless self.hubssolib_current_session
1276
+ self.hubssolib_current_session.session_last_used = time
1277
+ end
1278
+
1279
+ def hubssolib_get_return_to
1280
+ session = self.hubssolib_current_session
1281
+ session ? session.session_return_to : nil
1282
+ end
1283
+
1284
+ def hubssolib_set_return_to(uri)
1285
+ return unless self.hubssolib_current_session
1286
+ self.hubssolib_current_session.session_return_to = uri
1287
+ end
1288
+
1289
+ end # Core module
1290
+ end # HubSsoLib module
1291
+
1292
+ #######################################################################
1293
+ # Classes: Standard class extensions for HubSsoLib Roles operations. #
1294
+ # (C) Hipposoft 2006 #
1295
+ # #
1296
+ # Purpose: Extensions to standard classes to support HubSsoLib. #
1297
+ # #
1298
+ # Author: A.D.Hodgkinson #
1299
+ # #
1300
+ # History: 20-Oct-2006 (ADH): Integrated into HubSsoLib. #
1301
+ #######################################################################
1302
+
1303
+ # Method to return a Roles object created from the
1304
+ # contents of the String the method is invoked upon. The
1305
+ # string may contain a single role or a comma-separated list
1306
+ # with no white space.
1307
+ #
1308
+ class String
1309
+ def to_authenticated_roles
1310
+ roles = HubSsoLib::Roles.new
1311
+ array = self.split(',')
1312
+
1313
+ roles.clear
1314
+ array.each { |role| roles.add(role) }
1315
+
1316
+ return roles
1317
+ end
1318
+ end # String class
1319
+
1320
+ # Method to return a Roles object created from the
1321
+ # contents of the Symbol the method is invoked upon.
1322
+ #
1323
+ class Symbol
1324
+ def to_authenticated_roles
1325
+ return self.to_s.to_authenticated_roles
1326
+ end
1327
+ end # Symbol class
1328
+
1329
+ # Method to return a Roles object created from the
1330
+ # contents of the Array the method is invoked upon. The array
1331
+ # contents will be flattened. After that, each entry must be
1332
+ # a single role symbol or string equivalent. Comma-separated
1333
+ # lists are not currently allowed (improvements to the roles
1334
+ # class could easily give this, but the bloat isn't needed).
1335
+ #
1336
+ class Array
1337
+ def to_authenticated_roles
1338
+ roles = HubSsoLib::Roles.new
1339
+ roles.clear
1340
+
1341
+ self.flatten.each { |entry| roles.add(entry.to_s) }
1342
+
1343
+ return roles
1344
+ end
1345
+ end # Array class