hubssolib 3.2.1 → 3.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ff87f5cf7f12fbe119c62700a1967efe14365bd60ac1a9c4be277825320e62eb
4
- data.tar.gz: 91a60374a009856c6090365fd711a0ad4a3e1b2d5deceaa845e10d215717fd43
3
+ metadata.gz: c1a83a70b87a6c3cabd8bbb821dfc06ee467d382c9593cb7c1733152e136f5a1
4
+ data.tar.gz: 87f1dda92220f25326c8bbce597ecd97e74c3dd7e22fd0fbf89bfed6269b6b23
5
5
  SHA512:
6
- metadata.gz: c77c64ac36ca1ddb3b31fd96b0061cb8bb63e4ca512c9ef59fe010fb4917310dcabf1bff99a5b404b7105d6607bd81adfaece9803d4b0f48aa57e861d1084864
7
- data.tar.gz: ff31f8a5fb859f35f1c0e777c49127ed02692e00056ef3dd7bfdd74e034066096b89358cc2e834850a84d33d64b7d020edcc0d400648307ad92c6f28f910e682
6
+ metadata.gz: cf2ae3afb91a4fcc3391cf4b156e3dd9e4d0be4cecafc10204cf6dc8a7d6b9d07d51dc08cd7fd22ee12a478025d03730227fa2602a194d4b0e6785d72af21295
7
+ data.tar.gz: 6ac8d8a0a1920d7f93604204ef0c254a094bd4a8559110bba2f630c1fe406f87966b0ba2289d017a9a48e90cd0d6e2eab0e1ae3872032c7bad389942965fe2b2
data/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
1
+ ## 3.4.0, 24-Mar-2025
2
+
3
+ The session interface has now been cleaned up. While the public API is unchanged, you:
4
+
5
+ * ...are encouraged to change uses of `hubssolib_current_user = foo` over to `hubssolib_log_in()`
6
+ * ...are likewise encouraged to change `hubssolib_current_user = nil` over to `hubssolib_log_out()`
7
+
8
+ There are many fixes in the overhauled session management:
9
+
10
+ * Session data is only stored when a user logs in, else no session cookie is built
11
+ * If a user logs in, possible stale older sessions they might have are swept out
12
+ * When the user logs out their session data is entirely deleted
13
+ * The Hub app includes a Rake task that makes use of a new internal session factory feature to clean up sessions with a very old idle timeout (more than 3 times the usual in-app 'you were logged out' notification mechanism's delay via `HUB_IDLE_TIME_LIMIT`, or 2 days, whichever is larger) and delete such sessions - yes, this means that if a user did then come in even later on a now-deleted session ID, they'd simply be logged out without the warning message about why, but it was a serious oversight prior to allow the sessions to just accumulate.
14
+
15
+ In addition:
16
+
17
+ * Iteration over the sessions object for active user enumeration could cause failures. The process could be time consuming and, during it, the client side is iterating over a remote object that the DRb server maintains for any new, inbound sessions which may be started by other web server request threads into the Hub app. This would lead to `can't add a new key into hash during iteration` exceptions. This is fixed via more aggressive mutex use.
18
+
19
+
20
+ ## 3.3.0, 16-Feb-2025
21
+
22
+ Sentry support, for use by the DRb server. If you use Sentry, define your account's `SENTRY_DSN` in the environment where the DRb server runs and exceptions will be reported.
23
+
1
24
  ## 3.2.1, 16-Feb-2025
2
25
 
3
26
  The conditional login return-via-referrer mechanism never really worked, so instead have the login status indicator link generate a return-to URL in the query string instead and forward that, if present, in preference.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- hubssolib (3.1.0)
4
+ hubssolib (3.4.0)
5
5
  base64 (~> 0.2)
6
6
  drb (~> 2.2)
7
7
 
@@ -29,7 +29,7 @@ GEM
29
29
  psych (5.2.3)
30
30
  date
31
31
  stringio
32
- rdoc (6.12.0)
32
+ rdoc (6.13.0)
33
33
  psych (>= 4.0.0)
34
34
  reline (0.6.0)
35
35
  io-console (~> 0.5)
@@ -52,7 +52,7 @@ GEM
52
52
  simplecov_json_formatter (~> 0.1)
53
53
  simplecov-html (0.13.1)
54
54
  simplecov_json_formatter (0.1.4)
55
- stringio (3.1.3)
55
+ stringio (3.1.5)
56
56
 
57
57
  PLATFORMS
58
58
  ruby
data/README.md CHANGED
@@ -202,6 +202,14 @@ The four arguments are guaranteed to be present and non-empty, with leading or t
202
202
 
203
203
 
204
204
 
205
+ ## Session maintenance
206
+
207
+ User-idle session expiry is routinely handled in the "beforehand" callback so that we can see the session is valid but expired, expire it and add a via-flash warning about what happened so the user is fully informed. The Hub gem tries to be nice about `POST`, `PATCH` and `PUT` operations too and allows those to complete before the expected subsequent redirection `GET` causing expiry, to try and avoid users losing form submission data entirely.
208
+
209
+ Users who log in but then go idle will not cause any natural session expiry by virtue of there being no new page fetch activity provoking the idle timeout check. For users who've just gone away full stop, this means a session record is left hanging around in DRb server memory (or if future iterations support other storage methods, they'd persist in whatever storage that is). For this reason, the Hub _application_ provides a Rake task which sweeps sessions based on things idle for three times the `HUB_IDLE_TIME_LIMIT`, or two days, whichever is longer (so short idle timers still give users a couple of days to potentially come back to their old session and get the "nice" expiry message). Run `bundle exec rake hub_sessions:sweep` whenever you want (in practice, once a day is enough). **Be sure to set up any custom Hub environment variables for your setup** so that the Rake task knows where to find the DRb socket for the running server, what your expiry time is if so customised, and so-on.
210
+
211
+
212
+
205
213
  ## Hub library API
206
214
 
207
215
  The Hub component interfaces that should be used by application authors when integrating with the Hub single sign-on mechanism are described below. If you want a complete list of all public interfaces, consult the file `hub_sso_lib.rb` inside the Hub gem. All functions and classes therein are fully commented to describe the purpose of each class, along with the purpose, input parameters and return values of class methods and instance methods.
data/hubssolib.gemspec CHANGED
@@ -4,7 +4,7 @@ spec = Gem::Specification.new do |s|
4
4
  s.platform = Gem::Platform::RUBY
5
5
  s.name = 'hubssolib'
6
6
 
7
- s.version = '3.2.1'
7
+ s.version = '3.4.0'
8
8
  s.author = 'Andrew Hodgkinson and others'
9
9
  s.email = 'ahodgkin@rowing.org.uk'
10
10
  s.homepage = 'http://pond.org.uk/'
data/lib/hub_sso_lib.rb CHANGED
@@ -51,9 +51,9 @@ module HubSsoLib
51
51
  #
52
52
  HUB_PATH_PREFIX = ENV['HUB_PATH_PREFIX'] || ''
53
53
 
54
- # Time limit, *in seconds*, for the account inactivity timeout.
55
- # If a user performs no Hub actions during this time they will
56
- # be automatically logged out upon their next action.
54
+ # Time limit, *in seconds*, for the account inactivity timeout. If a user
55
+ # performs no Hub actions during this time they will be automatically logged
56
+ # out upon their next action, with a Flash message set to explain why.
57
57
  #
58
58
  HUB_IDLE_TIME_LIMIT = ENV['HUB_IDLE_TIME_LIMIT']&.to_i || 4 * 60 * 60
59
59
 
@@ -402,16 +402,20 @@ module HubSsoLib
402
402
  attr_accessor :session_return_to
403
403
  attr_accessor :session_flash
404
404
  attr_accessor :session_user
405
- attr_accessor :session_key_rotation
405
+ attr_accessor :session_rotated_key
406
406
  attr_accessor :session_ip
407
407
 
408
408
  def initialize
409
- @session_last_used = Time.now.utc
410
- @session_return_to = nil
411
- @session_flash = {}
412
- @session_user = HubSsoLib::User.new
413
- @session_key_rotation = nil
414
- @session_ip = nil
409
+ @session_last_used = Time.now.utc
410
+ @session_return_to = nil
411
+ @session_flash = {}
412
+ @session_user = HubSsoLib::User.new
413
+ @session_rotated_key = nil
414
+ @session_ip = nil
415
+
416
+ rescue => e
417
+ Sentry.capture_exception(e) if defined?(Sentry) && Sentry.respond_to?(:capture_exception)
418
+ raise
415
419
  end
416
420
  end # Session class
417
421
 
@@ -419,8 +423,10 @@ module HubSsoLib
419
423
  # Class: SessionFactory #
420
424
  # (C) Hipposoft 2006 #
421
425
  # #
422
- # Purpose: Build Session objects for DRb server clients. Maintains a #
423
- # hash of Session objects. #
426
+ # Purpose: Manage Session objects for DRb server clients. This class #
427
+ # implements the API exposed by the HubSsoLib::Server DRb #
428
+ # endpoint, so this is the remote object that clients will #
429
+ # be calling into. #
424
430
  # #
425
431
  # Author: A.D.Hodgkinson #
426
432
  # #
@@ -433,6 +439,10 @@ module HubSsoLib
433
439
  @hub_sessions = {}
434
440
 
435
441
  puts "Session factory: Awaken" unless @hub_be_quiet
442
+
443
+ rescue => e
444
+ Sentry.capture_exception(e) if defined?(Sentry) && Sentry.respond_to?(:capture_exception)
445
+ raise
436
446
  end
437
447
 
438
448
  # Get a session using a given key (a UUID). Generates a new session if
@@ -440,7 +450,7 @@ module HubSsoLib
440
450
  # recorded in existing session data.
441
451
  #
442
452
  # Whether new or pre-existing, the returned session will have changed key
443
- # as a result of being read; check the #session_key_rotation property to
453
+ # as a result of being read; check the #session_rotated_key property to
444
454
  # find out the new key. If you fail to do this, you'll lose access to the
445
455
  # session data as you won't know which key it lies under.
446
456
  #
@@ -455,17 +465,17 @@ module HubSsoLib
455
465
  # invalid and discarded.
456
466
  #
457
467
  def get_hub_session_proxy(key, remote_ip)
458
- hub_session = @hub_sessions[key]
468
+ hub_session = HUB_MUTEX.synchronize { @hub_sessions[key] }
459
469
  message = hub_session.nil? ? 'Created' : 'Retrieving'
460
470
  new_key = SecureRandom.uuid
461
471
 
462
472
  unless @hub_be_quiet
463
- puts "#{ message } session for key #{ key } and rotating to #{ new_key }"
473
+ puts "Session factory: #{ message } session for key #{ key } and rotating to #{ new_key }"
464
474
  end
465
475
 
466
476
  unless hub_session.nil? || hub_session.session_ip == remote_ip
467
477
  unless @hub_be_quiet
468
- puts "WARNING: IP address changed from #{ hub_session.session_ip } to #{ remote_ip } -> discarding session"
478
+ puts "Session factory: WARNING: IP address changed from #{ hub_session.session_ip } to #{ remote_ip } -> discarding session"
469
479
  end
470
480
 
471
481
  hub_session = nil
@@ -476,16 +486,126 @@ module HubSsoLib
476
486
  hub_session.session_ip = remote_ip
477
487
  end
478
488
 
479
- @hub_sessions.delete(key)
480
- @hub_sessions[new_key] = hub_session
489
+ HUB_MUTEX.synchronize do
490
+ @hub_sessions.delete(key)
491
+ @hub_sessions[new_key] = hub_session
492
+ end
481
493
 
482
- hub_session.session_key_rotation = new_key
494
+ hub_session.session_rotated_key = new_key
483
495
  return hub_session
496
+
497
+ rescue => e
498
+ Sentry.capture_exception(e) if defined?(Sentry) && Sentry.respond_to?(:capture_exception)
499
+ raise
484
500
  end
485
501
 
502
+ # Enumerate all currently known sessions. The format is a Hash, with the
503
+ # session key UUIDs as keys and the related HubSsoLib::Session instances as
504
+ # values.
505
+ #
506
+ # WARNING: This returns all Hub sessions maintained by the server instance
507
+ # but it's returning a reference to the live object with everything being
508
+ # managed over DRb. If a caller iterates over this object, then it can fall
509
+ # foul of other request threads making changes to the underlying data and
510
+ # Ruby raising exceptions about e.g. hashes being modified during
511
+ # enumeration.
512
+ #
513
+ # To prevent this, if intending to iterate over the collection, really the
514
+ # only safe way is to duplicate it first on "your side" (the client side)
515
+ # of the DRb connection. This of course consumes RAM, so you might choose
516
+ # to evaluate e.g. the number of keys in the returned session data and only
517
+ # permit enumeration if they fall below some previously-measured "allowed"
518
+ # value wherein RAM consumption is acceptable.
519
+ #
520
+ # See HubSsoLib::Core#hubssolib_enumerate_users for an example.
521
+ #
486
522
  def enumerate_hub_sessions()
487
523
  @hub_sessions
524
+
525
+ rescue => e
526
+ Sentry.capture_exception(e) if defined?(Sentry) && Sentry.respond_to?(:capture_exception)
527
+ raise
488
528
  end
529
+
530
+ # Given a session key (which, if a session has been looked up and the key
531
+ # thus rotated, ought to be that new, rotated key), destroy the associated
532
+ # session data. Does nothing if the key is not found.
533
+ #
534
+ def destroy_session_by_key(key)
535
+ HUB_MUTEX.synchronize do
536
+ @hub_sessions.delete(key)
537
+ end
538
+
539
+ rescue => e
540
+ Sentry.capture_exception(e) if defined?(Sentry) && Sentry.respond_to?(:capture_exception)
541
+ raise
542
+ end
543
+
544
+ # WARNING: Comparatively slow
545
+ #
546
+ # This is called in rare cases such as user deletion or being asked for a
547
+ # session under an old key, indicating loss of key rotation sequence.
548
+ # Removes all sessions found for a given user ID.
549
+ #
550
+ # IN THE CURRENT IMPLEMENTATION THIS JUST SEQUENTIALLY SCANS ALL ACTIVE
551
+ # SESSIONS IN THE HASH and must therefore lock on mutex for the duration.
552
+ #
553
+ def destroy_sessions_by_user_id(user_id)
554
+ HUB_MUTEX.synchronize do
555
+ @hub_sessions.reject! do | key, session |
556
+ session&.session_user&.user_id == user_id
557
+ end
558
+ end
559
+
560
+ rescue => e
561
+ Sentry.capture_exception(e) if defined?(Sentry) && Sentry.respond_to?(:capture_exception)
562
+ raise
563
+ end
564
+
565
+ # WARNING: Slow
566
+ #
567
+ # This is a housekeeping task which checks sessions against Hub expiry and,
568
+ # if the session keys look to be substantially older than the value set in
569
+ # HUB_IDLE_TIME_LIMIT, the session simply deleted. If a user does return
570
+ # later, they'll see themselves in logged out state without the Flash
571
+ # warning them of an expired session, but we can't allow session keys to
572
+ # just hang around forever so *some* kind of sweep is needed.
573
+ #
574
+ # This method clearly needs to iterate over all sessions under a mutex and
575
+ # makes relatively complex checks for each, so it's fairly slow compared
576
+ # to most methods. Call it infrequently; any and all other attempts to read
577
+ # session data while the method runs will block until method finishes.
578
+ #
579
+ def destroy_ancient_sessions
580
+ time_limit = HUB_IDLE_TIME_LIMIT * 3 # (TODO: This is fairly arbitrary...)
581
+ time_limit = 172_800 if time_limit < 172_800 # (2 days)
582
+ destroyed = 0
583
+
584
+ unless @hub_be_quiet
585
+ puts "Session factory: Sweeping sessions inactive for more than #{ time_limit } seconds..."
586
+ end
587
+
588
+ HUB_MUTEX.synchronize do
589
+ count_before = @hub_sessions.keys.size
590
+
591
+ @hub_sessions.reject! do | key, session |
592
+ last_used = session&.session_last_used
593
+ last_used.nil? || Time.now.utc - last_used > time_limit
594
+ end
595
+
596
+ count_after = @hub_sessions.keys.size
597
+ destroyed = count_before - count_after
598
+ end
599
+
600
+ unless @hub_be_quiet
601
+ puts "Session factory: ...Destroyed #{destroyed} session(s)"
602
+ end
603
+
604
+ rescue => e
605
+ Sentry.capture_exception(e) if defined?(Sentry) && Sentry.respond_to?(:capture_exception)
606
+ raise
607
+ end
608
+
489
609
  end
490
610
 
491
611
  #######################################################################
@@ -511,6 +631,10 @@ module HubSsoLib
511
631
  @@hub_session_factory = HubSsoLib::SessionFactory.new
512
632
  DRb.start_service(HUB_CONNECTION_URI, @@hub_session_factory, { :safe_level => 1 })
513
633
  DRb.thread.join
634
+
635
+ rescue => e
636
+ Sentry.capture_exception(e) if defined?(Sentry) && Sentry.respond_to?(:capture_exception)
637
+ raise
514
638
  end
515
639
  end # Server module
516
640
 
@@ -530,14 +654,65 @@ module HubSsoLib
530
654
 
531
655
  module Core
532
656
 
533
- # Returns true or false if the user is logged in.
657
+ # Log in the user. This is just syntax sugar for setting the current user
658
+ # via #hubssolib_current_user, really. You can freely use either approach
659
+ # according to your preferred aesthetics, but this method is preferred.
660
+ #
661
+ # +user+:: A valid HubSsoLib::User instance. If this has a +nil+ value for
662
+ # +user_id+, or if +user+ is itself +nil+, you'll cause the same
663
+ # effect as if explicitly logging *out*.
664
+ #
665
+ def hubssolib_log_in(user)
666
+ self.hubssolib_current_user = user # (which deals with all related session and cookie consequences)
667
+ end
668
+
669
+ # Log out the user. Very few applications should ever need to call this,
670
+ # though Hub certainly does and it gets used internally too.
534
671
  #
535
- # Preloads @hubssolib_current_user with user data if logged in.
672
+ def hubssolib_log_out
673
+ self.hubssolib_current_user = nil # (which deals with all related session and cookie consequences)
674
+ end
675
+
676
+ # Returns true or false if a user is logged in or not, respectively.
536
677
  #
537
678
  def hubssolib_logged_in?
538
679
  !!self.hubssolib_current_user
539
680
  end
540
681
 
682
+ # Accesses the current user, via the DRb server if necessary. Returns a
683
+ # HubSsoLib::User object, or +nil+ if none is available (this indicates
684
+ # that nobody is logged in, but #hubssolib_logged_in? should be used to
685
+ # check that, to allow for possible future more advanced logic within).
686
+ #
687
+ def hubssolib_current_user
688
+ hub_session = self.hubssolib_get_session()
689
+ user = hub_session&.session_user
690
+
691
+ return (user&.user_id.nil? ? nil : user)
692
+ end
693
+
694
+ # Sets the currently signed in user. Note that although this works and is
695
+ # maintained, it is recommended that #hubssolib_log_in gets called instead.
696
+ #
697
+ # +user+:: A valid HubSsoLib::User. This will replace any existing logged
698
+ # in user. If there is no session yet, one will be created.
699
+ #
700
+ def hubssolib_current_user=(user)
701
+ if user.nil?
702
+ self.hubssolib_destroy_session!
703
+ else
704
+
705
+ # Stale sessions may exist for this user. The frequency of logins is
706
+ # very low compared to frequency of page requests, so we perform this
707
+ # expensive method here as a least-worst option!
708
+ #
709
+ hubssolib_factory().destroy_sessions_by_user_id(user.user_id)
710
+
711
+ hub_session = self.hubssolib_create_session()
712
+ hub_session.session_user = user
713
+ end
714
+ end
715
+
541
716
  # Returns markup for a link that leads to Hub's conditional login endpoint,
542
717
  # inline-styled as a red "Log in" or green "Account" button. This can be
543
718
  # used in page templates to avoid needing any additional images or other
@@ -550,7 +725,6 @@ module HubSsoLib
550
725
  #
551
726
  def hubssolib_account_link
552
727
  logged_in = self.hubssolib_logged_in?()
553
-
554
728
  ui_href = "#{HUB_PATH_PREFIX}/account/login_conditional"
555
729
  noscript_img_src = "#{HUB_PATH_PREFIX}/account/login_indication.png"
556
730
  noscript_img_tag = helpers.image_tag(noscript_img_src, size: '90x22', border: '0', alt: 'Log in or out')
@@ -647,51 +821,12 @@ module HubSsoLib
647
821
  return (puser && !puser.empty? && puser != pnormal)
648
822
  end
649
823
 
650
- # Log out the user. Very few applications should ever need to call this,
651
- # though Hub certainly does and it gets used internally too.
652
- #
653
- def hubssolib_log_out
654
- # Causes the "hubssolib_current_[foo]=" methods to run, which
655
- # deal with everything else.
656
- self.hubssolib_current_user = nil
657
- @hubssolib_current_session_proxy = nil
658
- end
659
-
660
- # Accesses the current session from the cookie. Creates a new session
661
- # object if need be, but can return +nil+ if e.g. attempting to access
662
- # session cookie data without SSL.
663
- #
664
- def hubssolib_current_session
665
- @hubssolib_current_session_proxy ||= hubssolib_get_session_proxy()
666
- end
667
-
668
- # Accesses the current user, via the DRb server if necessary.
669
- #
670
- def hubssolib_current_user
671
- hub_session = self.hubssolib_current_session
672
- user = hub_session.nil? ? nil : hub_session.session_user
673
-
674
- if (user && user.user_id)
675
- return user
676
- else
677
- return nil
678
- end
679
- end
680
-
681
- # Store the given user data in the cookie
682
- #
683
- def hubssolib_current_user=(user)
684
- hub_session = self.hubssolib_current_session
685
- hub_session.session_user = user unless hub_session.nil?
686
- end
687
-
688
824
  # Public read-only accessor methods for common user activities:
689
825
  # return the current user's roles as a Roles object, or nil if
690
826
  # there's no user.
691
827
  #
692
828
  def hubssolib_get_user_roles
693
- user = self.hubssolib_current_user
694
- user ? user.user_roles.to_authenticated_roles : nil
829
+ self.hubssolib_current_user&.user_roles&.to_authenticated_roles
695
830
  end
696
831
 
697
832
  # Public read-only accessor methods for common user activities:
@@ -699,8 +834,7 @@ module HubSsoLib
699
834
  # no user. See also hubssolib_unique_name.
700
835
  #
701
836
  def hubssolib_get_user_name
702
- user = self.hubssolib_current_user
703
- user ? user.user_real_name : nil
837
+ self.hubssolib_current_user&.user_real_name
704
838
  end
705
839
 
706
840
  # Public read-only accessor methods for common user activities:
@@ -708,8 +842,7 @@ module HubSsoLib
708
842
  # nil if there's no user. See also hubssolib_unique_name.
709
843
  #
710
844
  def hubssolib_get_user_id
711
- user = self.hubssolib_current_user
712
- user ? user.user_id : nil
845
+ self.hubssolib_current_user&.user_id
713
846
  end
714
847
 
715
848
  # Public read-only accessor methods for common user activities:
@@ -717,8 +850,7 @@ module HubSsoLib
717
850
  # no user.
718
851
  #
719
852
  def hubssolib_get_user_address
720
- user = self.hubssolib_current_user
721
- user ? user.user_email : nil
853
+ self.hubssolib_current_user&.user_email
722
854
  end
723
855
 
724
856
  # Return a human-readable unique ID for a user. We don't want to
@@ -843,15 +975,13 @@ module HubSsoLib
843
975
  cookies.delete(HUB_LOGIN_INDICATOR_COOKIE, domain: :all, path: '/')
844
976
 
845
977
  if login_is_required
846
- hubssolib_store_location
847
- return hubssolib_must_login
978
+ hubssolib_store_location()
979
+ return hubssolib_must_login()
848
980
  else
849
981
  return true
850
982
  end
851
983
  end
852
984
 
853
- # Definitely logged in.
854
- #
855
985
  cookies[HUB_LOGIN_INDICATOR_COOKIE] = {
856
986
  value: HUB_LOGIN_INDICATOR_COOKIE_VALUE,
857
987
  path: '/',
@@ -872,8 +1002,8 @@ module HubSsoLib
872
1002
  # if OK, else indicate that access is denied.
873
1003
 
874
1004
  if (hubssolib_session_expired?)
875
- hubssolib_store_location
876
- hubssolib_log_out
1005
+ hubssolib_store_location()
1006
+ hubssolib_log_out()
877
1007
  hubssolib_set_flash(:attention, 'Sorry, your session timed out; you need to log in again to continue.')
878
1008
 
879
1009
  # We mean this: redirect_to :controller => 'account', :action => 'login'
@@ -882,7 +1012,7 @@ module HubSsoLib
882
1012
  redirect_to HUB_PATH_PREFIX + '/account/login'
883
1013
  else
884
1014
  hubssolib_set_last_used(Time.now.utc)
885
- return hubssolib_authorized? ? true : hubssolib_access_denied
1015
+ return hubssolib_authorized? ? true : hubssolib_access_denied()
886
1016
  end
887
1017
 
888
1018
  else
@@ -935,7 +1065,7 @@ module HubSsoLib
935
1065
  # Redirect to the URI stored by the most recent store_location call or
936
1066
  # to the passed default.
937
1067
  def hubssolib_redirect_back_or_default(default)
938
- url = hubssolib_get_return_to
1068
+ url = hubssolib_get_return_to()
939
1069
  hubssolib_set_return_to(nil)
940
1070
 
941
1071
  redirect_to(url || default)
@@ -967,26 +1097,29 @@ module HubSsoLib
967
1097
  end
968
1098
  end
969
1099
 
970
- # Public methods to set some data that would normally go in @session,
971
- # but can't because it needs to be accessed across applications. It is
972
- # put in an insecure support cookie instead. There are some related
973
- # private methods for things like session expiry too.
1100
+ # Flash data can be carried across the Hub session, stored in the DRb
1101
+ # server as a result, and is thus cleared automatically if a session gets
1102
+ # dropped. However, we also want this to work without being logged in, so
1103
+ # in that case it uses the normal flash as a backup when *writing*.
974
1104
  #
975
- def hubssolib_get_flash()
976
- f = self.hubssolib_current_session ? self.hubssolib_current_session.session_flash : nil
977
- return f || {}
1105
+ def hubssolib_get_flash
1106
+ session = self.hubssolib_get_session()
1107
+ session&.session_flash || {}
978
1108
  end
979
1109
 
980
1110
  def hubssolib_set_flash(symbol, message)
981
- return unless self.hubssolib_current_session
982
- f = hubssolib_get_flash
983
- f[symbol.to_s] = message
984
- self.hubssolib_current_session.session_flash = f
1111
+ session = self.hubssolib_get_session()
1112
+ f = hubssolib_get_flash() unless session.nil?
1113
+ f = self.flash if f.nil? && self.respond_to?(:flash)
1114
+
1115
+ f[symbol] = message
1116
+
1117
+ session.session_flash = f unless session.nil?
985
1118
  end
986
1119
 
987
1120
  def hubssolib_clear_flash
988
- return unless self.hubssolib_current_session
989
- self.hubssolib_current_session.session_flash = {}
1121
+ session = self.hubssolib_get_session()
1122
+ session.session_flash = {} unless session.nil?
990
1123
  end
991
1124
 
992
1125
  # Return flash data for known keys, then all remaining keys, from both
@@ -1043,21 +1176,22 @@ module HubSsoLib
1043
1176
  hubssolib_get_exception_data(CGI::unescape(id_data))
1044
1177
  end
1045
1178
 
1046
- # Inclusion hook to make various methods available as ActionView
1047
- # helper methods.
1179
+ # Inclusion hook to make various methods available as ActionView helpers.
1048
1180
  #
1049
1181
  def self.included(base)
1050
- base.send :helper_method,
1051
- :hubssolib_current_user,
1052
- :hubssolib_unique_name,
1053
- :hubssolib_logged_in?,
1054
- :hubssolib_account_link,
1055
- :hubssolib_authorized?,
1056
- :hubssolib_privileged?,
1057
- :hubssolib_flash_data
1058
- rescue
1059
- # We're not always included in controllers...
1060
- nil
1182
+ if base.respond_to?(:helper_method)
1183
+ base.send(
1184
+ :helper_method,
1185
+
1186
+ :hubssolib_current_user,
1187
+ :hubssolib_unique_name,
1188
+ :hubssolib_logged_in?,
1189
+ :hubssolib_authorized?,
1190
+ :hubssolib_privileged?,
1191
+ :hubssolib_account_link,
1192
+ :hubssolib_flash_data
1193
+ )
1194
+ end
1061
1195
  end
1062
1196
 
1063
1197
  private
@@ -1084,6 +1218,108 @@ module HubSsoLib
1084
1218
  HUB_BYPASS_SSL || ! Rails.env.production?
1085
1219
  end
1086
1220
 
1221
+ # Create a new session. This MUST ONLY BE CALLED at a "log in" phase, since
1222
+ # it discards any existing session and creates a new, valid one, whether or
1223
+ # not a valid session key was being presented for the old session. It is,
1224
+ # in essence, a "clean slate" start.
1225
+ #
1226
+ # On exit, @hubssolib_session is updated; see #hubssolib_get_session for
1227
+ # the significance of that.
1228
+ #
1229
+ def hubssolib_create_session
1230
+ old_session = self.hubssolib_get_session()
1231
+ self.hubssolib_destroy_session! unless old_session.nil?
1232
+
1233
+ start_key = SecureRandom.uuid
1234
+ @hubssolib_session = hubssolib_factory().get_hub_session_proxy(start_key, request.remote_ip)
1235
+
1236
+ # The session is now stored under the rotated key, so put that into the
1237
+ # Hub session cookie so that on the next request, we can retrieve the
1238
+ # session data (and at that time, once again rotate the key).
1239
+ #
1240
+ next_key = @hubssolib_session.session_rotated_key
1241
+ self.hubssolib_store_key(next_key)
1242
+
1243
+ return @hubssolib_session
1244
+ end
1245
+
1246
+ # Gets session data based on the current +request+ details.
1247
+ #
1248
+ # * If there is no session cookie, returns +nil+
1249
+ # * If there is a session cookie but the session key is invalid, returns
1250
+ # +nil+.
1251
+ # * If there is a session cookie with valid key, session data is retrieved
1252
+ # and returned; the session key is rotated and the session cookie updated
1253
+ # with new key (that is to say that this request's response, under normal
1254
+ # conditions, will include an appropriate Set-Cookie header).
1255
+ #
1256
+ # The ivar @hubssolib_session is consulted first to avoid repeating work
1257
+ # during request processing, where it's likely that more than one call
1258
+ # will occur. This ivar is an known internal implementation detail; it is
1259
+ # also set by #hubssolib_create_session, and is cleared by calls to
1260
+ # #hubssolib_destroy_session!.
1261
+ #
1262
+ def hubssolib_get_session
1263
+ if @hubssolib_session.nil?
1264
+ key = cookies[HUB_COOKIE_NAME]
1265
+
1266
+ if key.nil? || key == ''
1267
+ @hubssolib_session = nil
1268
+ else
1269
+ hub_session = hubssolib_factory().get_hub_session_proxy(key, request.remote_ip)
1270
+
1271
+ if hub_session.nil? # Invalid key in cookie
1272
+ @hubssolib_session = nil
1273
+ else
1274
+ @hubssolib_session = hub_session
1275
+
1276
+ next_key = @hubssolib_session.session_rotated_key
1277
+ self.hubssolib_store_key(next_key)
1278
+ end
1279
+ end
1280
+ end
1281
+
1282
+ return @hubssolib_session
1283
+ end
1284
+
1285
+ # Destroys the current session. If there isn't one, then it has few side
1286
+ # effects other than making sure all session-related cookies are deleted.
1287
+ #
1288
+ def hubssolib_destroy_session!
1289
+ session = self.hubssolib_get_session()
1290
+
1291
+ # Remember, if creating or retrieving a session for a request, the key is
1292
+ # rotated and stored in the session under #session_rotated_key. To delete
1293
+ # that session within that same request - since rotation happens even if
1294
+ # the first place the session gets read is right here, in this method -
1295
+ # we must use the rotated key.
1296
+ #
1297
+ unless session.nil?
1298
+ hubssolib_factory().destroy_session_by_key(session.session_rotated_key)
1299
+ end
1300
+
1301
+ @hubssolib_session = nil
1302
+
1303
+ cookies.delete(HUB_COOKIE_NAME, domain: :all, path: '/')
1304
+ cookies.delete(HUB_LOGIN_INDICATOR_COOKIE, domain: :all, path: '/')
1305
+ end
1306
+
1307
+ # Store the Hub's session key in the Hub cookie.
1308
+ #
1309
+ # +key+:: Session key used to retrieve the session again on the *next*
1310
+ # request. The session has already been stored under that new key
1311
+ # internally. Do not call with +nil+ or blank keys.
1312
+ #
1313
+ def hubssolib_store_key(key)
1314
+ cookies[HUB_COOKIE_NAME] = {
1315
+ value: key,
1316
+ path: '/',
1317
+ domain: :all,
1318
+ secure: ! hub_bypass_ssl?,
1319
+ httponly: true
1320
+ }
1321
+ end
1322
+
1087
1323
  # Indicate that the user must log in to complete their request.
1088
1324
  # Returns false to enable a before_filter to return through this
1089
1325
  # method while ensuring that the previous action processing is
@@ -1144,64 +1380,45 @@ module HubSsoLib
1144
1380
  # have not logged out anyway, and the Hub isn't intended for Fort Knox.
1145
1381
  # At the time of writing the trade-off of usability vs security is
1146
1382
  # considered acceptable, though who knows, the view may change in future.
1383
+ #
1384
+ # Same applies for PATCH and PUT.
1385
+ #
1386
+ last_used = self.hubssolib_get_last_used
1147
1387
 
1148
- last_used = hubssolib_get_last_used
1149
- (request.method != :post && last_used && Time.now.utc - last_used > HUB_IDLE_TIME_LIMIT)
1150
- end
1151
-
1152
- def hubssolib_get_session_proxy
1153
- # If we're not using SSL, forget it
1154
- return nil unless request.ssl? || hub_bypass_ssl?
1155
-
1156
- key = cookies[HUB_COOKIE_NAME] || SecureRandom.uuid
1157
- hub_session = hubssolib_factory().get_hub_session_proxy(key, request.remote_ip)
1158
- key = hub_session.session_key_rotation unless hub_session.nil?
1159
-
1160
- cookies[HUB_COOKIE_NAME] = {
1161
- value: key,
1162
- path: '/',
1163
- domain: :all,
1164
- secure: ! hub_bypass_ssl?,
1165
- httponly: true
1166
- }
1167
-
1168
- return hub_session
1169
-
1170
- rescue Exception => e
1171
-
1172
- # At this point there tends to be no Session data, so we're
1173
- # going to have to encode the exception data into the URI...
1174
- # It must be escaped twice, as many servers treat "%2F" in a
1175
- # URI as a "/" and Apache may flat refuse to serve the page,
1176
- # raising a 404 error unless "AllowEncodedSlashes on" is
1177
- # specified in its configuration.
1178
-
1179
- suffix = '/' + CGI::escape(CGI::escape(hubssolib_set_exception_data(e)))
1180
- new_path = HUB_PATH_PREFIX + '/tasks/service'
1181
- redirect_to(new_path + suffix) unless request.path.include?(new_path)
1182
-
1183
- return nil
1184
- end
1388
+ (
1389
+ request.method != :post &&
1390
+ request.method != :patch &&
1391
+ request.method != :put &&
1185
1392
 
1186
- def hubssolib_set_session_data(session)
1187
- # Nothing to do presently - DRb handles everything
1393
+ last_used && Time.now.utc - last_used > HUB_IDLE_TIME_LIMIT
1394
+ )
1188
1395
  end
1189
1396
 
1190
- # Return an array of Hub User objects representing users based
1191
- # on a list of known sessions returned by the DRb server. Note
1192
- # that if an application exposes this method to a view, it is
1193
- # up to the application to ensure sufficient access permission
1194
- # protection for that view according to the webmaster's choice
1195
- # of site security level. Generally, normal users should not
1196
- # be allowed access.
1397
+ # Return an array of Hub User objects representing users based on a list of
1398
+ # known sessions returned by the DRb server. Note that if an application
1399
+ # exposes this method to a view, it is up to the application to ensure
1400
+ # sufficient access permission protection for that view according to the
1401
+ # webmaster's choice of site security level. Generally, normal users should
1402
+ # not be allowed access!
1403
+ #
1404
+ # Due to the session pool being held in the DRb server and subject to
1405
+ # alteration at any time by other requests (assuming the server supports
1406
+ # more than just one request at a time, that is!) then the session data
1407
+ # must be copied locally before iterating. Otherwise, exceptions arise from
1408
+ # attempts to alter an under-iteration Hash. This in turn raises a worry
1409
+ # about RAM usage. For that reason, a (somewhat arbitrary) limit of
1410
+ # 2000 active users is applied. More than that and the method returns an
1411
+ # empty array.
1197
1412
  #
1198
1413
  def hubssolib_enumerate_users
1199
- sessions = hubssolib_factory().enumerate_hub_sessions()
1200
- users = []
1201
-
1202
- sessions.each do |key, value|
1203
- user = value.session_user
1204
- users.push(user) if (user && user.respond_to?(:user_id) && user.user_id)
1414
+ hub_session_data = hubssolib_factory().enumerate_hub_sessions()
1415
+ return [] if hub_session_data.keys.size > 2000 # NOTE EARLY EXIT
1416
+
1417
+ sessions = hub_session_data.values.dup
1418
+ users = sessions.inject( [] ) do | memo, session |
1419
+ user = session.session_user
1420
+ memo << user unless user&.user_id.nil?
1421
+ memo
1205
1422
  end
1206
1423
 
1207
1424
  return users
@@ -1243,23 +1460,21 @@ module HubSsoLib
1243
1460
  # the session data is available, else return default values.
1244
1461
 
1245
1462
  def hubssolib_get_last_used
1246
- session = self.hubssolib_current_session
1247
- session ? session.session_last_used : Time.now.utc
1463
+ self.hubssolib_get_session()&.session_last_used || Time.now.utc
1248
1464
  end
1249
1465
 
1250
1466
  def hubssolib_set_last_used(time)
1251
- return unless self.hubssolib_current_session
1252
- self.hubssolib_current_session.session_last_used = time
1467
+ session = self.hubssolib_get_session()
1468
+ session.session_last_used = time unless session.nil?
1253
1469
  end
1254
1470
 
1255
1471
  def hubssolib_get_return_to
1256
- session = self.hubssolib_current_session
1257
- session ? session.session_return_to : nil
1472
+ self.hubssolib_get_session()&.session_return_to
1258
1473
  end
1259
1474
 
1260
1475
  def hubssolib_set_return_to(uri)
1261
- return unless self.hubssolib_current_session
1262
- self.hubssolib_current_session.session_return_to = uri
1476
+ session = self.hubssolib_get_session()
1477
+ session.session_return_to = uri unless session.nil?
1263
1478
  end
1264
1479
 
1265
1480
  end # Core module
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hubssolib
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.1
4
+ version: 3.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Hodgkinson and others
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-02-16 00:00:00.000000000 Z
10
+ date: 2025-03-24 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: drb