Class: HubSsoLib::SessionFactory

Inherits:
Object
  • Object
show all
Defined in:
lib/hub_sso_lib.rb

Overview

Class: SessionFactory #

(C) Hipposoft 2006                                         #
                                                           #

Purpose: Manage Session objects for DRb server clients. This class #

implements the API exposed by the HubSsoLib::Server DRb    #
endpoint, so this is the remote object that clients will   #
be calling into.                                           #
                                                           #

Author: A.D.Hodgkinson #

#

History: 26-Oct-2006 (ADH): Created. #

Instance Method Summary collapse

Constructor Details

#initializeSessionFactory

Returns a new instance of SessionFactory.



455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
# File 'lib/hub_sso_lib.rb', line 455

def initialize
  @hub_be_quiet = (ENV['HUB_QUIET_SERVER'] == 'yes')
  @hub_sessions = {}

  puts "Session factory: Awakening..." unless @hub_be_quiet

  if File.exist?(HUB_SESSION_ARCHIVE)
    begin
      restored_sessions = ::YAML.load_file(
        HUB_SESSION_ARCHIVE,
        permitted_classes: [
          ::HubSsoLib::Session,
          ::HubSsoLib::User,
          Time,
          Symbol
        ]
      )

      @hub_sessions = restored_sessions || {}
      self.destroy_ancient_sessions()
      puts "Session factory: Reloaded #{@hub_sessions.size} from archive" unless @hub_be_quiet

    rescue => e
      puts "Session factory: Ignored archive due to error #{e.message.inspect}" unless @hub_be_quiet

    ensure
      File.unlink(HUB_SESSION_ARCHIVE)

    end
  end

  puts "Session factory: ...Awakened" unless @hub_be_quiet

rescue => e
  Sentry.capture_exception(e) if defined?(Sentry) && Sentry.respond_to?(:capture_exception)
  raise
end

Instance Method Details

#destroy_ancient_sessionsObject

WARNING: Slow.

This is a housekeeping task which checks sessions against Hub expiry and, if the session keys look to be substantially older than the value set in HUB_IDLE_TIME_LIMIT, the session simply deleted. If a user does return later, they’ll see themselves in logged out state without the Flash warning them of an expired session, but we can’t allow session keys to just hang around forever so some kind of sweep is needed.

This method clearly needs to iterate over all sessions under a mutex and makes relatively complex checks for each, so it’s fairly slow compared to most methods. Call it infrequently; any and all other attempts to read session data while the method runs will block until method finishes.



718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
# File 'lib/hub_sso_lib.rb', line 718

def destroy_ancient_sessions
  unless @hub_be_quiet
    puts "Session factory: Sweeping sessions inactive for more than #{ HUB_ARCHIVE_TIME_LIMIT } seconds..."
  end

  destroyed = 0

  HUB_MUTEX.synchronize do
    count_before = @hub_sessions.size

    @hub_sessions.reject! do | key, session |
      last_used = session&.session_last_used
      last_used.nil? || Time.now.utc - last_used > HUB_ARCHIVE_TIME_LIMIT
    end

    count_after = @hub_sessions.size
    destroyed   = count_before - count_after
  end

  unless @hub_be_quiet
    puts "Session factory: ...Destroyed #{destroyed} session(s)"
  end

rescue => e
  Sentry.capture_exception(e) if defined?(Sentry) && Sentry.respond_to?(:capture_exception)
  raise
end

#destroy_session_by_key(key) ⇒ Object

Given a session key (which, if a session has been looked up and the key thus rotated, ought to be that new, rotated key), destroy the associated session data. Does nothing if the key is not found.



641
642
643
644
645
646
647
648
649
650
651
652
653
# File 'lib/hub_sso_lib.rb', line 641

def destroy_session_by_key(key)
  unless @hub_be_quiet
    puts "Session factory: Deleting session with key #{key}"
  end

  HUB_MUTEX.synchronize do
    @hub_sessions.delete(key)
  end

rescue => e
  Sentry.capture_exception(e) if defined?(Sentry) && Sentry.respond_to?(:capture_exception)
  raise
end

#destroy_sessions_by_user_id(user_id) ⇒ Object

WARNING: Comparatively slow.

This is called in rare cases such as user deletion or being asked for a session under an old key, indicating loss of key rotation sequence. Removes all sessions found for a given user ID.

IN THE CURRENT IMPLEMENTATION THIS JUST SEQUENTIALLY SCANS ALL ACTIVE SESSIONS IN THE HASH and must therefore lock on mutex for the duration.



664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
# File 'lib/hub_sso_lib.rb', line 664

def destroy_sessions_by_user_id(user_id)
  unless @hub_be_quiet
    puts "Session factory: Deleting all session records for user ID #{user_id.inspect}"
  end

  HUB_MUTEX.synchronize do
    @hub_sessions.reject! do | key, session |
      session&.session_user&.user_id == user_id
    end
  end

rescue => e
  Sentry.capture_exception(e) if defined?(Sentry) && Sentry.respond_to?(:capture_exception)
  raise
end

#dump_sessions!Object

Lock the session store and dump all sessions with a non-nil session user ID to a YAML file at HUB_SESSION_ARCHIVE. This is expected to only be called by the graceful shutdown code in HubSsoLib::Server.



750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
# File 'lib/hub_sso_lib.rb', line 750

def dump_sessions!
  written_record_count = 0

  # Why not just do ::YAML.dump(@hub_sessions)? Well, it'd be faster, but
  # it builds the YAML data all in RAM which would cause a huge RAM spike
  # of unknown size (depends on live session count) and that's Bad.
  #
  # If any no-user sessions have crept in for any reason, this also gives
  # us a chance to skip them.
  #
  HUB_MUTEX.synchronize do
    File.open(HUB_SESSION_ARCHIVE, 'w') do | f |
      f.write("---\n") # (document marker)

      @hub_sessions.each do | key, session |
        next if session&.session_user&.user_id.nil? # NOTE EARLY LOOP RESTART

        session.session_flash = nil

        dump = ::YAML.dump({key => session})
        dump.sub!(/^---\n/, '') # (avoid multiple document markers)

        f.write(dump)
        written_record_count += 1
      end
    end
  end

  # Simple if slightly inefficient way to deal with zero actual useful
  # session records being present - an unusual real-world edge case.
  #
  File.unlink(HUB_SESSION_ARCHIVE) if written_record_count == 0
end

#enumerate_hub_session_idsObject

Returns all currently known session keys. This is a COPY of the internal keyset, since otherwise it would be necessary to try and enumerate keys on a Hash which is subject to change at any moment if a user logs in or out. Since Ruby raises an exception should an under-iteration Hash be changed, we can’t do that, which is why the keys are returned as a copied array instead.

Keys are ordered by least-recently-active first to most-recent last.

Call #retrieve_session_by_key(…) to get session details for that key. Bear in mind that nil returns are possible, since the session data may be changing rapidly and a user might’ve logged out or had their session expired in the time between you retrieving the list of current keys here, then requesting details of the session for that key later.

To avoid unbounded RAM requirements arising, the maximum number of keys returned herein is limited to HUB_SESSION_ENUMERATION_KEY_MAX.

Returns a Hash with Symbol keys that have values as follows:

count

The number of known sessions - just a key count.

keys

If count exceeds HUB_SESSION_ENUMERATION_KEY_MAX, this is nil, else an array of zero or more session keys.



593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
# File 'lib/hub_sso_lib.rb', line 593

def enumerate_hub_session_ids()
  count = @hub_sessions.size
  keys  = if count > HUB_SESSION_ENUMERATION_KEY_MAX
    nil
  else
    HUB_MUTEX.synchronize do
      @hub_sessions.keys # (Hash#keys returns a new array)
    end
  end

  return { count: count, keys: keys }

rescue => e
  Sentry.capture_exception(e) if defined?(Sentry) && Sentry.respond_to?(:capture_exception)
  raise
end

#enumerate_hub_sessionsObject

THIS INTERFACE IS DEPRECATED and will be removed in Hub 4. Change to #enumerate_hub_session_keys instead.

Enumerate all currently known sessions. The format is a Hash, with the session key UUIDs as keys and the related HubSsoLib::Session instances as values. If you attempt it iterate over this data YOU MUST USE A COPY of the keys to do so, since Hub users may log in or out at any time and Ruby will raise an exception if the session data changes during enumeration - end users will see errors.



561
562
563
564
565
566
567
# File 'lib/hub_sso_lib.rb', line 561

def enumerate_hub_sessions()
  @hub_sessions

rescue => e
  Sentry.capture_exception(e) if defined?(Sentry) && Sentry.respond_to?(:capture_exception)
  raise
end

#get_hub_session_proxy(key, _ignored = nil, create: true) ⇒ Object

Get a session using a given key (a UUID). Generates a new session if the key is unrecognised or if the IP address given mismatches the one recorded in existing session data.

Whether new or pre-existing, the returned session will have changed key as a result of being read; check the #session_rotated_key property to find out the new key. If you fail to do this, you’ll lose access to the session data as you won’t know which key it lies under.

The returned object is proxied via DRb - it is shared between processes.

key

Session key; lazy-initialises a new session under this key if none is found, then immediately rotates it by default, but may return no session for unrecognised keys depending on the create parameter, described below.

The “_ignored” parameter is for backwards compatibility for older clients calling into a newer gem. This used to take an IP address of the request and would discard a session if the current IP address had changed, but since DHCP is a Thing then - even though in practice most IP addresses from ISPs are very stable - this wasn’t really a valid security measure. It required us to process and store IP data which is now often considered PII and we’d rather not (especially given their arising storage in HUB_SESSION_ARCHIVE on shutdown). This is, of course, quite ironic given the reason for removal is IP address unreliability when used as PII!

In addition, the following optional named parameters can be given:

create

Default true - an unknown key causes creation of an empty, new session under that key. If false, attempts to read with an unrecognised key yield nil.



525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
# File 'lib/hub_sso_lib.rb', line 525

def get_hub_session_proxy(key, _ignored = nil, create: true)
  hub_session = HUB_MUTEX.synchronize { @hub_sessions[key] }
  return nil if create == false && hub_session.nil? # NOTE EARLY EXIT

  message = hub_session.nil? ? 'Created' : 'Retrieving'
  new_key = SecureRandom.uuid

  unless @hub_be_quiet
    puts "Session factory: #{ message } session for key #{ key } and rotating to #{ new_key }"
  end

  hub_session = HubSsoLib::Session.new if hub_session.nil?

  HUB_MUTEX.synchronize do
    @hub_sessions.delete(key)
    @hub_sessions[new_key] = hub_session
  end

  hub_session.session_rotated_key = new_key
  return hub_session

rescue => e
  Sentry.capture_exception(e) if defined?(Sentry) && Sentry.respond_to?(:capture_exception)
  raise
end

#retrieve_session_by_key(key) ⇒ Object

Retrieve session data (as a HubSsoLib::Session instance) based on the given session key. No key rotation occurs. Returns nil if no entry is found for that key.



614
615
616
# File 'lib/hub_sso_lib.rb', line 614

def retrieve_session_by_key(key)
  HUB_MUTEX.synchronize { @hub_sessions[key] }
end

#retrieve_sessions_by_user_id(user_id) ⇒ Object

WARNING: Comparatively slow.

This is usually only called in administrative interfaces to look at the known sessions for a specific user of interest. An Hash of session key values yielding HubSsoLib::Session instances as values is returned.

The array is ordered by least-recently-active first to most-recent last.

IN THE CURRENT IMPLEMENTATION THIS JUST SEQUENTIALLY SCANS ALL ACTIVE SESSIONS IN THE HASH and must therefore lock on mutex for the duration.



629
630
631
632
633
634
635
# File 'lib/hub_sso_lib.rb', line 629

def retrieve_sessions_by_user_id(user_id)
  HUB_MUTEX.synchronize do
    @hub_sessions.select do | key, session |
      session&.session_user&.user_id == user_id
    end
  end
end

#update_sessions_by_user_id(user_id, user) ⇒ Object

WARNING: Comparatively slow.

Call only if you MUST update details of a session user inside all Hub sessions. Pass the user ID and HubSsoLib::User details that are to be stored for all sessions owned by that user ID.



686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
# File 'lib/hub_sso_lib.rb', line 686

def update_sessions_by_user_id(user_id, user)
  unless @hub_be_quiet
    puts "Session factory: Updating all session records for user ID #{user_id.inspect}"
  end

  HUB_MUTEX.synchronize do
    @hub_sessions.each do | key, session |
      if session&.session_user&.user_id == user_id
        session.session_user = user
      end
    end
  end

rescue => e
  Sentry.capture_exception(e) if defined?(Sentry) && Sentry.respond_to?(:capture_exception)
  raise
end