Class: HubSsoLib::SessionFactory
- Inherits:
-
Object
- Object
- HubSsoLib::SessionFactory
- 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
-
#destroy_ancient_sessions ⇒ Object
WARNING: Slow.
-
#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.
-
#destroy_sessions_by_user_id(user_id) ⇒ Object
WARNING: Comparatively slow.
-
#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.
-
#enumerate_hub_session_ids ⇒ Object
Returns all currently known session keys.
-
#enumerate_hub_sessions ⇒ Object
THIS INTERFACE IS DEPRECATED and will be removed in Hub 4.
-
#get_hub_session_proxy(key, _ignored = nil, create: true) ⇒ Object
Get a session using a given key (a UUID).
-
#initialize ⇒ SessionFactory
constructor
A new instance of SessionFactory.
-
#retrieve_session_by_key(key) ⇒ Object
Retrieve session data (as a HubSsoLib::Session instance) based on the given session key.
-
#retrieve_sessions_by_user_id(user_id) ⇒ Object
WARNING: Comparatively slow.
-
#update_sessions_by_user_id(user_id, user) ⇒ Object
WARNING: Comparatively slow.
Constructor Details
#initialize ⇒ SessionFactory
Returns a new instance of SessionFactory.
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 510 511 512 513 514 515 516 517 518 519 520 |
# File 'lib/hub_sso_lib.rb', line 484 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..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_sessions ⇒ Object
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.
772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 |
# File 'lib/hub_sso_lib.rb', line 772 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 return nil 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.
689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 |
# File 'lib/hub_sso_lib.rb', line 689 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 return nil 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.
714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 |
# File 'lib/hub_sso_lib.rb', line 714 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 return nil 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.
806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 |
# File 'lib/hub_sso_lib.rb', line 806 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_ids ⇒ Object
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
countexceeds HUB_SESSION_ENUMERATION_KEY_MAX, this isnil, else an array of zero or more session keys.
622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 |
# File 'lib/hub_sso_lib.rb', line 622 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_sessions ⇒ Object
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.
590 591 592 593 594 595 596 |
# File 'lib/hub_sso_lib.rb', line 590 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
createparameter, 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. Iffalse, attempts to read with an unrecognised key yieldnil.
554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 |
# File 'lib/hub_sso_lib.rb', line 554 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 = hub_session.nil? ? 'Created' : 'Retrieving' new_key = SecureRandom.uuid unless @hub_be_quiet puts "Session factory: #{ } 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.
This returns a *LIVE OBJECT* owned by the DRb server. Writes made to this object will affect the live session state. However, the session might be invalidated at any time by actions such as key rotation, expiration or explicit user deletion. Attempts to read or write properties would then lead to exceptions such as:
"424" is not id value (RangeError)
…where 424 is the internal object ID of meaning only to the DRb system.
653 654 655 |
# File 'lib/hub_sso_lib.rb', line 653 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. Returned data is a copy of internal session information and should be considered read-only; changes made will have no effect outside your own application.
IN THE CURRENT IMPLEMENTATION THIS JUST SEQUENTIALLY SCANS ALL ACTIVE SESSIONS IN THE HASH and must therefore lock on mutex for the duration.
671 672 673 674 675 676 677 678 679 680 681 682 683 |
# File 'lib/hub_sso_lib.rb', line 671 def retrieve_sessions_by_user_id(user_id) HUB_MUTEX.synchronize do collection = {} @hub_sessions.each do | key, session | if session&.session_user&.user_id == user_id collection[key] = SessionCopy.new(copying_session: session) end end collection 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.
738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 |
# File 'lib/hub_sso_lib.rb', line 738 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 return nil rescue => e Sentry.capture_exception(e) if defined?(Sentry) && Sentry.respond_to?(:capture_exception) raise end |