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.



473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
# File 'lib/hub_sso_lib.rb', line 473

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.



702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
# File 'lib/hub_sso_lib.rb', line 702

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.



657
658
659
660
661
662
663
664
665
# File 'lib/hub_sso_lib.rb', line 657

def destroy_session_by_key(key)
  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.



676
677
678
679
680
681
682
683
684
685
686
# File 'lib/hub_sso_lib.rb', line 676

def destroy_sessions_by_user_id(user_id)
  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.



734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
# File 'lib/hub_sso_lib.rb', line 734

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.



611
612
613
614
615
616
617
618
619
620
621
622
623
624
# File 'lib/hub_sso_lib.rb', line 611

def enumerate_hub_session_ids()
  count = @hub_sessions.size
  keys  = if count > HUB_SESSION_ENUMERATION_KEY_MAX
    nil
  else
    @hub_sessions.keys # (Hash#keys returns a new array)
  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.



579
580
581
582
583
584
585
# File 'lib/hub_sso_lib.rb', line 579

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.



543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
# File 'lib/hub_sso_lib.rb', line 543

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.



630
631
632
# File 'lib/hub_sso_lib.rb', line 630

def retrieve_session_by_key(key)
  @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.



645
646
647
648
649
650
651
# File 'lib/hub_sso_lib.rb', line 645

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