hubssolib 3.7.1 → 3.8.1

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: 68326a50593cfc8b53f1d5ba20e668a0a18316d8e3c6f9a9b245f213522f46f7
4
- data.tar.gz: 9267e261fb2af094e0801cea965007c8ee5760d93886a3525640ecca5603f52a
3
+ metadata.gz: 5fea64186dc1f80987fb5b531c46ac8eb9fff78878520b38e392a91dad8de7a9
4
+ data.tar.gz: 2ebe2f6599cbd9bf9dbfbaa6ebbbb81e99fe6f4922c5f8c19e250e380222c3c9
5
5
  SHA512:
6
- metadata.gz: 1849bcd713bb28f152fae7ca328f0d5f9cbb3b49aef31485019e930ba083af8f79cc3f804d04cf81628aed1bcfdbf669acada4c9cf97b2d190eefe4bc4cf6e9d
7
- data.tar.gz: 22c27953133bda55f46e83636298a7b978480bf2d05160a22caf16cecdf4579d39ddff294185dc8510e415fa94a04bd087a6875a983c6cf3df0238e4eac647ee
6
+ metadata.gz: 44b066f10c3615a752b258d85ccf5316358c33af6589c9702ab6b2ae8036f6e7eda9f99ab2a2707dc9acce9e9377ce5e73f28c3533eaec7c7312ac5e6e828fe3
7
+ data.tar.gz: 1f7ae4d9e29916eb3d14e3c057b61f95192be76954270fd8570dd596a5552495561df2bddd1f16f887ac613bbcd3292fd75228f4bbddf87faeda698c2a866067
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## 3.8.1, 09-Apr-2025
2
+
3
+ * It's been too long since I worked with DRb! Important fix for "live" (undumpable) vs copied objects which could lead to session enumeration problems in clients. Now, sessions enumerated by user ID are copies as is user data returned by HubSsoLib::Core#hubssolib_enumerate_users.
4
+ * Other data is still live, since there's a fair amount of write-back that expects to be updating objects held by the DRb server itself. This will all get replaced by database storage in HubSsoLib 4, so I'm not going to address the weaknesses present wherein a session might be expired or deleted "under the feet" of a client. The Hub gem's own write-based operations are all on a "current" session, so that could only vanish if an admin independently elected to destroy all user sessions via the Hub Rails app front-end. The implication is that the other user was probably up to no good, and I don't really mind if they just seem to be suddenly logged out, or see a 500 error due to a _very_ inconveniently-timed page fetch.
5
+
6
+ ## 3.8.0, 03-Apr-2025
7
+
8
+ * Adds helper `HubSsoLib::Core#hubssolib_flash_markup` to compliment `HubSsoLib::Core#hubssolib_flash_data` and help make Flash presentation simpler and more consistent across integrated applications.
9
+ * Deprecates core methods `hubssolib_store_location` (which is now a no-op) and `hubssolib_redirect_back_or_default` (which now always redirects to the default location). **These methods will be removed in Hub 4**. Ephemeral return-to location storage has only worked when sessions are _on_, which once upon a time was always - logged in or not - but changed in Hub 3.4.0, to avoid very large numbers of anonymous sessions building up into a very RAM-hungry pools in the DRb server. This has in hindsight amounted to a breaking change, and I should've bumped to Hub 4 per SemVer at that time, but it was overlooked; apologies. At least in general all it really usually means is that, instead of redirecting back to some calling application after login, the user is left in their Hub account view.
10
+ * Hub's UI element added to navigation bars by using `HubSsoLib::Core#hubssolib_account_link` plus permissions and must-log-in checking in the `HubSsoLib::Core#hubssolib_beforehand` before-action callback already dealt with and continue to deal with redirect-to-origin automatically, so often, applications did't need to store a location and then manually redirect to Hub anyway. If you do need this for some reason, though, the new workflow is create a link or redirection to the URL returned by `HubSsoLib::Core#hubssolib_returnable_account_url`. This simply encodes a return-to address in the URL where needed, removing any requirement for cross-application session storage. The method's documentation describes its uses but, for the majority of use cases, you'll probably only want to be presenting a link for the case where a user must log _in_. In that case, call `hubssolib_returnable_account_url(logged_in: false)`.
11
+
1
12
  ## 3.7.0 and 3.7.1, 28-Mar-2025
2
13
 
3
14
  * Login indicator cookie wasn't updated on session timeout, so when the page loaded for pages that do *not* require authorisation, the warning flash about being timed out would show, but the indicator would still show a "logged in" state until the next page fetch.
@@ -85,7 +96,7 @@ In Hub v1 and v2, login indication was done via an image that was served by the
85
96
 
86
97
  ```html
87
98
  <a class="img" href="<%= ENV['HUB_PATH_PREFIX'] %>/account/login_conditional">
88
- <img src="<%= ENV['HUB_PATH_PREFIX'] %>/account/login_indication" alt="Account" height="22" width="90" />
99
+ <img src="<%= ENV['HUB_PATH_PREFIX'] %>/account/login_indication" alt="Account" height="22" width="90">
89
100
  </a>
90
101
  ```
91
102
 
data/Gemfile.lock CHANGED
@@ -1,14 +1,91 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- hubssolib (3.7.1)
4
+ hubssolib (3.8.1)
5
5
  base64 (~> 0.2)
6
6
  drb (~> 2.2)
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
+ actioncable (8.0.2)
12
+ actionpack (= 8.0.2)
13
+ activesupport (= 8.0.2)
14
+ nio4r (~> 2.0)
15
+ websocket-driver (>= 0.6.1)
16
+ zeitwerk (~> 2.6)
17
+ actionmailbox (8.0.2)
18
+ actionpack (= 8.0.2)
19
+ activejob (= 8.0.2)
20
+ activerecord (= 8.0.2)
21
+ activestorage (= 8.0.2)
22
+ activesupport (= 8.0.2)
23
+ mail (>= 2.8.0)
24
+ actionmailer (8.0.2)
25
+ actionpack (= 8.0.2)
26
+ actionview (= 8.0.2)
27
+ activejob (= 8.0.2)
28
+ activesupport (= 8.0.2)
29
+ mail (>= 2.8.0)
30
+ rails-dom-testing (~> 2.2)
31
+ actionpack (8.0.2)
32
+ actionview (= 8.0.2)
33
+ activesupport (= 8.0.2)
34
+ nokogiri (>= 1.8.5)
35
+ rack (>= 2.2.4)
36
+ rack-session (>= 1.0.1)
37
+ rack-test (>= 0.6.3)
38
+ rails-dom-testing (~> 2.2)
39
+ rails-html-sanitizer (~> 1.6)
40
+ useragent (~> 0.16)
41
+ actiontext (8.0.2)
42
+ actionpack (= 8.0.2)
43
+ activerecord (= 8.0.2)
44
+ activestorage (= 8.0.2)
45
+ activesupport (= 8.0.2)
46
+ globalid (>= 0.6.0)
47
+ nokogiri (>= 1.8.5)
48
+ actionview (8.0.2)
49
+ activesupport (= 8.0.2)
50
+ builder (~> 3.1)
51
+ erubi (~> 1.11)
52
+ rails-dom-testing (~> 2.2)
53
+ rails-html-sanitizer (~> 1.6)
54
+ activejob (8.0.2)
55
+ activesupport (= 8.0.2)
56
+ globalid (>= 0.3.6)
57
+ activemodel (8.0.2)
58
+ activesupport (= 8.0.2)
59
+ activerecord (8.0.2)
60
+ activemodel (= 8.0.2)
61
+ activesupport (= 8.0.2)
62
+ timeout (>= 0.4.0)
63
+ activestorage (8.0.2)
64
+ actionpack (= 8.0.2)
65
+ activejob (= 8.0.2)
66
+ activerecord (= 8.0.2)
67
+ activesupport (= 8.0.2)
68
+ marcel (~> 1.0)
69
+ activesupport (8.0.2)
70
+ base64
71
+ benchmark (>= 0.3)
72
+ bigdecimal
73
+ concurrent-ruby (~> 1.0, >= 1.3.1)
74
+ connection_pool (>= 2.2.5)
75
+ drb
76
+ i18n (>= 1.6, < 2)
77
+ logger (>= 1.4.2)
78
+ minitest (>= 5.1)
79
+ securerandom (>= 0.3)
80
+ tzinfo (~> 2.0, >= 2.0.5)
81
+ uri (>= 0.13.1)
11
82
  base64 (0.2.0)
83
+ benchmark (0.4.0)
84
+ bigdecimal (3.1.9)
85
+ builder (3.3.0)
86
+ concurrent-ruby (1.3.5)
87
+ connection_pool (2.5.0)
88
+ crass (1.0.6)
12
89
  date (3.4.1)
13
90
  debug (1.10.0)
14
91
  irb (~> 1.10)
@@ -18,20 +95,90 @@ GEM
18
95
  doggo (1.4.0)
19
96
  rspec-core (~> 3.13)
20
97
  drb (2.2.1)
98
+ erubi (1.13.1)
99
+ globalid (1.2.1)
100
+ activesupport (>= 6.1)
101
+ i18n (1.14.7)
102
+ concurrent-ruby (~> 1.0)
21
103
  io-console (0.8.0)
22
- irb (1.15.1)
104
+ irb (1.15.2)
23
105
  pp (>= 0.6.0)
24
106
  rdoc (>= 4.0.0)
25
107
  reline (>= 0.4.2)
108
+ logger (1.7.0)
109
+ loofah (2.24.0)
110
+ crass (~> 1.0.2)
111
+ nokogiri (>= 1.12.0)
112
+ mail (2.8.1)
113
+ mini_mime (>= 0.1.1)
114
+ net-imap
115
+ net-pop
116
+ net-smtp
117
+ marcel (1.0.4)
118
+ mini_mime (1.1.5)
119
+ mini_portile2 (2.8.8)
120
+ minitest (5.25.5)
121
+ net-imap (0.5.6)
122
+ date
123
+ net-protocol
124
+ net-pop (0.1.2)
125
+ net-protocol
126
+ net-protocol (0.2.2)
127
+ timeout
128
+ net-smtp (0.5.1)
129
+ net-protocol
130
+ nio4r (2.7.4)
131
+ nokogiri (1.18.7)
132
+ mini_portile2 (~> 2.8.2)
133
+ racc (~> 1.4)
26
134
  pp (0.6.2)
27
135
  prettyprint
28
136
  prettyprint (0.2.0)
29
137
  psych (5.2.3)
30
138
  date
31
139
  stringio
32
- rdoc (6.13.0)
140
+ racc (1.8.1)
141
+ rack (3.1.12)
142
+ rack-session (2.1.0)
143
+ base64 (>= 0.1.0)
144
+ rack (>= 3.0.0)
145
+ rack-test (2.2.0)
146
+ rack (>= 1.3)
147
+ rackup (2.2.1)
148
+ rack (>= 3)
149
+ rails (8.0.2)
150
+ actioncable (= 8.0.2)
151
+ actionmailbox (= 8.0.2)
152
+ actionmailer (= 8.0.2)
153
+ actionpack (= 8.0.2)
154
+ actiontext (= 8.0.2)
155
+ actionview (= 8.0.2)
156
+ activejob (= 8.0.2)
157
+ activemodel (= 8.0.2)
158
+ activerecord (= 8.0.2)
159
+ activestorage (= 8.0.2)
160
+ activesupport (= 8.0.2)
161
+ bundler (>= 1.15.0)
162
+ railties (= 8.0.2)
163
+ rails-dom-testing (2.2.0)
164
+ activesupport (>= 5.0.0)
165
+ minitest
166
+ nokogiri (>= 1.6)
167
+ rails-html-sanitizer (1.6.2)
168
+ loofah (~> 2.21)
169
+ nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
170
+ railties (8.0.2)
171
+ actionpack (= 8.0.2)
172
+ activesupport (= 8.0.2)
173
+ irb (~> 1.13)
174
+ rackup (>= 1.0.0)
175
+ rake (>= 12.2)
176
+ thor (~> 1.0, >= 1.2.2)
177
+ zeitwerk (~> 2.6)
178
+ rake (13.2.1)
179
+ rdoc (6.13.1)
33
180
  psych (>= 4.0.0)
34
- reline (0.6.0)
181
+ reline (0.6.1)
35
182
  io-console (~> 0.5)
36
183
  rspec (3.13.0)
37
184
  rspec-core (~> 3.13.0)
@@ -46,6 +193,7 @@ GEM
46
193
  diff-lcs (>= 1.2.0, < 2.0)
47
194
  rspec-support (~> 3.13.0)
48
195
  rspec-support (3.13.2)
196
+ securerandom (0.4.1)
49
197
  simplecov (0.22.0)
50
198
  docile (~> 1.1)
51
199
  simplecov-html (~> 0.11)
@@ -53,6 +201,17 @@ GEM
53
201
  simplecov-html (0.13.1)
54
202
  simplecov_json_formatter (0.1.4)
55
203
  stringio (3.1.6)
204
+ thor (1.3.2)
205
+ timeout (0.4.3)
206
+ tzinfo (2.0.6)
207
+ concurrent-ruby (~> 1.0)
208
+ uri (1.0.3)
209
+ useragent (0.16.11)
210
+ websocket-driver (0.7.7)
211
+ base64
212
+ websocket-extensions (>= 0.1.0)
213
+ websocket-extensions (0.1.5)
214
+ zeitwerk (2.7.2)
56
215
 
57
216
  PLATFORMS
58
217
  ruby
@@ -61,6 +220,7 @@ DEPENDENCIES
61
220
  debug (~> 1.1)
62
221
  doggo (~> 1.4)
63
222
  hubssolib!
223
+ rails (~> 8.0)
64
224
  rspec (~> 3.13)
65
225
  rspec-mocks (~> 3.13)
66
226
  simplecov (~> 0.22)
data/README.md CHANGED
@@ -449,4 +449,25 @@ end
449
449
  * Your previously submitted action scheme and URL are also given in case you need them because you've decided to implement one callback handler for different kinds of untrusted action.
450
450
  * The `action_payload` is where all your important data, meaningful only to you, resides and this must contain everything else, other than the above parameters, required to complete the action previously sent for review, _while making sure things are attributed to the correct user_.
451
451
 
452
+ Use `head ...` to indicate non-200 responses. If you don't want the originating user to get notified of a successful run, then on-success, also reply with `head :ok` or `head 200` (according to style preference). If you want to ask Hub to notify the user, then on-success instead do this:
453
+
454
+ ```ruby
455
+ render json: { mail_item_url: ..., mail_subject: ... }, status: :ok
456
+ ```
457
+
458
+ The payload items shown above are both mandatory:
459
+
460
+ * `mail_item_url` is the full URL of the entity that you just successfully edited, updated, or some other sensible URL to take the user (this will be included verbatim in the e-mail)
461
+ * `mail_subject` is a subject line of your choosing - for example, `"Your new forum post is now visible"`. Hub might add a prefix such as `[ORGNAME] ...`
462
+
463
+ If either item is missing or blank, or if for any reason Hub finds itself unable to associated the action with a user record on Hub's side, then no e-mail message will be sent.
464
+
465
+ **IMPORTANT:** The Hub application's database migration at the time you updated to 3.7.0 will have set existing users to trusted for historic data, but new users are untrusted. If you introduce trust integration to your site's other apps after this, you might want to enter the console to update any new users added since likewise; inside `app/hub`, issue:
466
+
467
+ ```
468
+ $ bundle exec rails c
469
+ > User.update_all(trusted: true)
470
+ > exit
471
+ ```
472
+
452
473
  The trust mechanism involves a fair amount of effort on the integrating app's side but it can be very useful if you have a site where, despite your best efforts, sometimes spam/bot accounts manage to get inside and try to flood the system with spam. It's just one of many different potential protection and mitigation mechanisms that your site might choose to employ.
Binary file
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.7.1'
7
+ s.version = '3.8.1'
8
8
  s.author = 'Andrew Hodgkinson and others'
9
9
  s.email = 'ahodgkin@rowing.org.uk'
10
10
  s.homepage = 'http://pond.org.uk/'
@@ -28,6 +28,7 @@ spec = Gem::Specification.new do |s|
28
28
  s.add_dependency 'base64', '~> 0.2'
29
29
 
30
30
  s.add_development_dependency 'debug', '~> 1.1'
31
+ s.add_development_dependency 'rails', '~> 8.0'
31
32
  s.add_development_dependency 'simplecov', '~> 0.22'
32
33
  s.add_development_dependency 'doggo', '~> 1.4'
33
34
  s.add_development_dependency 'rspec', '~> 3.13'
data/lib/hub_sso_lib.rb CHANGED
@@ -69,7 +69,7 @@ module HubSsoLib
69
69
 
70
70
  # Location of Hub application root.
71
71
  #
72
- HUB_PATH_PREFIX = ENV['HUB_PATH_PREFIX'] || ''
72
+ HUB_PATH_PREFIX = (ENV['HUB_PATH_PREFIX'] || '').sub(/(\/)+$/, '')
73
73
 
74
74
  # Time limit, *in seconds*, for the account inactivity timeout. If a user
75
75
  # performs no Hub actions during this time they will be automatically logged
@@ -366,17 +366,16 @@ module HubSsoLib
366
366
  # 26-Feb-2025 (ADH): Add 'trusted' concept. #
367
367
  #######################################################################
368
368
 
369
+ # This *must not* be 'undumped', since it gets passed from clients back to
370
+ # the persistent DRb server process. A client thread may disappear and be
371
+ # recreated by the web server at any time; if the user object is undumpable,
372
+ # then the DRb server has to *call back to the client* (in DRb, clients are
373
+ # also servers...!) to find out about the object. Trouble is, if the client
374
+ # thread has been recreated, the server will be trying to access to stale
375
+ # objects that only exist if the garbage collector hasn't got to them yet.
376
+ #
369
377
  class User
370
378
 
371
- # This *must not* be 'undumped', since it gets passed from clients
372
- # back to the persistent DRb server process. A client thread may
373
- # disappear and be recreated by the web server at any time; if the
374
- # user object is undumpable, then the DRb server has to *call back
375
- # to the client* (in DRb, clients are also servers...!) to find out
376
- # about the object. Trouble is, if the client thread has been
377
- # recreated, the server will be trying to access to stale objects
378
- # that only exist if the garbage collector hasn't got to them yet.
379
-
380
379
  attr_accessor :user_salt
381
380
  attr_accessor :user_roles
382
381
  attr_accessor :user_updated_at
@@ -394,29 +393,11 @@ module HubSsoLib
394
393
  attr_accessor :user_password_reset_code_expires_at
395
394
  attr_accessor :user_trusted
396
395
 
397
- def initialize
398
- @user_salt = nil
399
- @user_roles = nil
400
- @user_updated_at = nil
401
- @user_activated_at = nil
402
- @user_real_name = nil
403
- @user_crypted_password = nil
404
- @user_remember_token_expires_at = nil
405
- @user_activation_code = nil
406
- @user_member_id = nil
407
- @user_id = nil
408
- @user_password_reset_code = nil
409
- @user_remember_token = nil
410
- @user_email = nil
411
- @user_created_at = nil
412
- @user_password_reset_code_expires_at = nil
413
- @user_trusted = nil
414
- end
415
396
  end # User class
416
397
 
417
398
  #######################################################################
418
- # Class: Session #
419
- # (C) Hipposoft 2006 #
399
+ # Class: Session family #
400
+ # (C) Hipposoft 2006-2025 #
420
401
  # #
421
402
  # Purpose: Session support object, used to store session metadata in #
422
403
  # an insecure cross-application cookie. #
@@ -424,37 +405,67 @@ module HubSsoLib
424
405
  # Author: A.D.Hodgkinson #
425
406
  # #
426
407
  # History: 22-Oct-2006 (ADH): Created. #
408
+ # 09-Apr-2025 (ADH): Dumpable/undumpable variants created. #
427
409
  #######################################################################
428
410
 
429
- class Session
430
-
431
- # Unlike a User, this *is* undumpable since it only gets passed from
432
- # server to client. The server's always here to service requests
433
- # from the client and used sessions are never garbage collected
434
- # since the DRb server's front object, a SessionFactory, keeps them
435
- # in a hash held within an instance variable.
436
-
437
- include DRb::DRbUndumped
438
-
439
- attr_accessor :session_last_used
440
- attr_accessor :session_return_to
441
- attr_accessor :session_flash
442
- attr_accessor :session_user
443
- attr_accessor :session_rotated_key
444
-
445
- def initialize
446
- @session_last_used = Time.now.utc
447
- @session_return_to = nil
448
- @session_flash = {}
449
- @session_user = HubSsoLib::User.new
450
- @session_rotated_key = nil
411
+ # A dumpable base class that can be used for undumpable proxies, where a
412
+ # change made in an object passed to a client is reflected on the server
413
+ # where the object actually lives; or for dumpable clones of sessions sent
414
+ # as read-only copies to clients which won't disppear due to server side
415
+ # deletion from key rotation, expiry or admin-driven deletion.
416
+ #
417
+ class SessionBase
418
+
419
+ ATTRIBUTES = %i{
420
+ session_last_used
421
+ session_flash
422
+ session_user
423
+ session_rotated_key
424
+ }
425
+
426
+ ATTRIBUTES.each { | attr | attr_accessor(attr) }
427
+ attr_reader :session_return_to # DEPRECATED
428
+
429
+ def initialize(copying_session: nil)
430
+ if copying_session.nil?
431
+ self.session_last_used = Time.now.utc
432
+ self.session_flash = {}
433
+ self.session_user = HubSsoLib::User.new
434
+ self.session_rotated_key = nil
435
+ else
436
+ ATTRIBUTES.each do | attr |
437
+ self.send("#{attr}=", copying_session.send(attr))
438
+ end
439
+ end
451
440
 
452
441
  rescue => e
453
442
  Sentry.capture_exception(e) if defined?(Sentry) && Sentry.respond_to?(:capture_exception)
454
443
  raise
455
444
  end
445
+
446
+ end # SessionBase class
447
+
448
+ # Unlike a User, this *is* undumpable since it only gets passed from server
449
+ # to client. The server's always here to service requests from the client and
450
+ # used sessions are never garbage collected since the DRb server's front
451
+ # object, a SessionFactory, keeps them in a hash held within an instance
452
+ # variable.
453
+ #
454
+ # A recognised weakness here is that a session might disappear from under the
455
+ # feet of an accessing application, due to key rotation or explicit deletion.
456
+ #
457
+ class Session < SessionBase
458
+ include DRb::DRbUndumped
456
459
  end # Session class
457
460
 
461
+ # No special conditions - this is just a dumpable session. You should usually
462
+ # create it based on an undumpable, 'live' session thus:
463
+ #
464
+ # SessionCopy.new(copying_session: undumpable_session)
465
+ #
466
+ class SessionCopy < SessionBase
467
+ end # SessionCopy class
468
+
458
469
  #######################################################################
459
470
  # Class: SessionFactory #
460
471
  # (C) Hipposoft 2006 #
@@ -613,7 +624,9 @@ module HubSsoLib
613
624
  keys = if count > HUB_SESSION_ENUMERATION_KEY_MAX
614
625
  nil
615
626
  else
616
- @hub_sessions.keys # (Hash#keys returns a new array)
627
+ HUB_MUTEX.synchronize do
628
+ @hub_sessions.keys # (Hash#keys returns a new array)
629
+ end
617
630
  end
618
631
 
619
632
  return { count: count, keys: keys }
@@ -627,8 +640,18 @@ module HubSsoLib
627
640
  # given session key. No key rotation occurs. Returns +nil+ if no entry is
628
641
  # found for that key.
629
642
  #
643
+ # This returns a *LIVE OBJECT* owned by the DRb server. Writes made to this
644
+ # object will affect the live session state. However, the session might be
645
+ # invalidated at any time by actions such as key rotation, expiration or
646
+ # explicit user deletion. Attempts to read or write properties would then
647
+ # lead to exceptions such as:
648
+ #
649
+ # "424" is not id value (RangeError)
650
+ #
651
+ # ...where 424 is the internal object ID of meaning only to the DRb system.
652
+ #
630
653
  def retrieve_session_by_key(key)
631
- @hub_sessions[key]
654
+ HUB_MUTEX.synchronize { @hub_sessions[key] }
632
655
  end
633
656
 
634
657
  # WARNING: Comparatively slow.
@@ -638,15 +661,24 @@ module HubSsoLib
638
661
  # values yielding HubSsoLib::Session instances as values is returned.
639
662
  #
640
663
  # The array is ordered by least-recently-active first to most-recent last.
664
+ # Returned data is a copy of internal session information and should be
665
+ # considered read-only; changes made will have no effect outside your own
666
+ # application.
641
667
  #
642
668
  # IN THE CURRENT IMPLEMENTATION THIS JUST SEQUENTIALLY SCANS ALL ACTIVE
643
669
  # SESSIONS IN THE HASH and must therefore lock on mutex for the duration.
644
670
  #
645
671
  def retrieve_sessions_by_user_id(user_id)
646
672
  HUB_MUTEX.synchronize do
647
- @hub_sessions.select do | key, session |
648
- session&.session_user&.user_id == user_id
673
+ collection = {}
674
+
675
+ @hub_sessions.each do | key, session |
676
+ if session&.session_user&.user_id == user_id
677
+ collection[key] = SessionCopy.new(copying_session: session)
678
+ end
649
679
  end
680
+
681
+ collection
650
682
  end
651
683
  end
652
684
 
@@ -655,10 +687,16 @@ module HubSsoLib
655
687
  # session data. Does nothing if the key is not found.
656
688
  #
657
689
  def destroy_session_by_key(key)
690
+ unless @hub_be_quiet
691
+ puts "Session factory: Deleting session with key #{key}"
692
+ end
693
+
658
694
  HUB_MUTEX.synchronize do
659
695
  @hub_sessions.delete(key)
660
696
  end
661
697
 
698
+ return nil
699
+
662
700
  rescue => e
663
701
  Sentry.capture_exception(e) if defined?(Sentry) && Sentry.respond_to?(:capture_exception)
664
702
  raise
@@ -674,12 +712,44 @@ module HubSsoLib
674
712
  # SESSIONS IN THE HASH and must therefore lock on mutex for the duration.
675
713
  #
676
714
  def destroy_sessions_by_user_id(user_id)
715
+ unless @hub_be_quiet
716
+ puts "Session factory: Deleting all session records for user ID #{user_id.inspect}"
717
+ end
718
+
677
719
  HUB_MUTEX.synchronize do
678
720
  @hub_sessions.reject! do | key, session |
679
721
  session&.session_user&.user_id == user_id
680
722
  end
681
723
  end
682
724
 
725
+ return nil
726
+
727
+ rescue => e
728
+ Sentry.capture_exception(e) if defined?(Sentry) && Sentry.respond_to?(:capture_exception)
729
+ raise
730
+ end
731
+
732
+ # WARNING: Comparatively slow.
733
+ #
734
+ # Call only if you MUST update details of a session user inside all Hub
735
+ # sessions. Pass the user ID and HubSsoLib::User details that are to be
736
+ # stored for all sessions owned by that user ID.
737
+ #
738
+ def update_sessions_by_user_id(user_id, user)
739
+ unless @hub_be_quiet
740
+ puts "Session factory: Updating all session records for user ID #{user_id.inspect}"
741
+ end
742
+
743
+ HUB_MUTEX.synchronize do
744
+ @hub_sessions.each do | key, session |
745
+ if session&.session_user&.user_id == user_id
746
+ session.session_user = user
747
+ end
748
+ end
749
+ end
750
+
751
+ return nil
752
+
683
753
  rescue => e
684
754
  Sentry.capture_exception(e) if defined?(Sentry) && Sentry.respond_to?(:capture_exception)
685
755
  raise
@@ -722,6 +792,8 @@ module HubSsoLib
722
792
  puts "Session factory: ...Destroyed #{destroyed} session(s)"
723
793
  end
724
794
 
795
+ return nil
796
+
725
797
  rescue => e
726
798
  Sentry.capture_exception(e) if defined?(Sentry) && Sentry.respond_to?(:capture_exception)
727
799
  raise
@@ -964,34 +1036,97 @@ module HubSsoLib
964
1036
  end
965
1037
  end
966
1038
 
967
- # Returns markup for a link that leads to Hub's conditional login endpoint,
968
- # inline-styled as a red "Log in" or green "Account" button. This can be
969
- # used in page templates to avoid needing any additional images or other
970
- # such resources and using pure HTML + CSS for the login indication.
1039
+ # Return a URL that leads to a Hub "log in" page if the user is not logged
1040
+ # in currently, else an "account" page for logged-in users.
1041
+ #
1042
+ # +universal+:: Use +true+ if concerned that a "Back" browser button action
1043
+ # might cause a page to appear that's got a cached link which
1044
+ # may no longer reflect the current state. This jumps to Hub
1045
+ # and redirects to the "log in" or "account" destinations
1046
+ # depending on current state. If using this, bear in mind
1047
+ # that the link text must be ambiguous, because the eventual
1048
+ # destination isn't known.
1049
+ #
1050
+ # It's often better to just use #hubssolib_account_link to
1051
+ # create markup for this in a navigation area.
1052
+ #
1053
+ # The default is +false+, which means that a bespoke link
1054
+ # for the exact current instantaneous log in state is
1055
+ # generated; the +logged_in+ parameter applies (see below).
1056
+ #
1057
+ # +logged_in+:: If +universal+ is +false+, then this parameter defaults to
1058
+ # the current user logged-in state, but can be overridden
1059
+ # with +true+ (act as if someone is logged in) or +false+
1060
+ # (act as if someone is not logged in).
1061
+ #
1062
+ # +return_to+:: Overrides the use of <tt>request.original_url</tt> to give
1063
+ # an alternative return-after-logging-in URL. Will be of no
1064
+ # use for users already logged in. This is rarely used, but
1065
+ # might be helpful if you (say) want them to return to the
1066
+ # current page, but with a specific fragment added ("#foo").
1067
+ # Specify as a String, Symbol or URI, at your preference.
1068
+ #
1069
+ # Note that the universal links, or a not-logged-in state link will include
1070
+ # a query string which takes the value of <tt>request.original_url</tt> or,
1071
+ # if not defined, tries <tt>request.referrer</tt> (else is absent). This is
1072
+ # a URL used for the redirection after successful login. The logged-in
1073
+ # state link does not require this addition so omits it for brevity.
1074
+ #
1075
+ def hubssolib_returnable_account_url(
1076
+ universal: false,
1077
+ logged_in: self.hubssolib_logged_in?,
1078
+ return_to: nil
1079
+ )
1080
+ account_url = if universal
1081
+ "#{HUB_PATH_PREFIX}/account/login_conditional"
1082
+ else
1083
+ "#{HUB_PATH_PREFIX}/#{'account/login' unless logged_in}"
1084
+ end
1085
+
1086
+ # Attach a return-to URL always for the universal case which needs to
1087
+ # work regardless of login state; else we only need it when logged out,
1088
+ # so that we can return to the originating location after logging in.
1089
+ # There's no functional difference - just a simpler URL that omits what
1090
+ # would otherwise be an unused query string parameter.
1091
+ #
1092
+ if universal or not logged_in
1093
+ if return_to.nil?
1094
+ request_obj = self.try(:request)
1095
+ return_to = request_obj.try(:original_url) || request_obj.try(:referrer)
1096
+ end
1097
+
1098
+ if return_to.present?
1099
+ account_url << "?return_to_url=#{CGI.escape(return_to.to_s)}"
1100
+ end
1101
+ end
1102
+
1103
+ return account_url
1104
+ end
1105
+
1106
+ # Returns markup for a link that leads to Hub's login or logout link, using
1107
+ # pure HTML + CSS for styling. A universal "conditional login" link is used
1108
+ # since page data may be old due to e.g. a "back" button being used, after
1109
+ # the user either logged in or out elsewhere. This possibility is also why
1110
+ # JavaScript is used, updating the button styling the correct login state
1111
+ # if needed. This requires the "pageshow" event to be supported. NOSCRIPT
1112
+ # browsers use the a no-cache image-based fallback, which is much less
1113
+ # efficient, but works.
971
1114
  #
972
- # JavaScript is used so that e.g. "back" button fully-cached displays by a
973
- # browser will get updated with the correct login state, where needed (so
974
- # long as the 'pageshow' event is supported). NOSCRIPT browsers use the old
975
- # no-cache image fallback, which is much less efficient, but works.
1115
+ # Although the JavaScript-powered option could use the non-conditional link
1116
+ # for log in/out and swap those, it's simpler just to use generate the same
1117
+ # link for all states - script-capable or otherwise, logged in or out,
976
1118
  #
977
1119
  def hubssolib_account_link
978
- logged_in = self.hubssolib_logged_in?()
979
- ui_href = "#{HUB_PATH_PREFIX}/account/login_conditional"
1120
+ logged_in_url = self.hubssolib_returnable_account_url(universal: false, logged_in: true)
1121
+ logged_out_url = self.hubssolib_returnable_account_url(universal: false, logged_in: false)
1122
+ universal_url = self.hubssolib_returnable_account_url(universal: true)
1123
+
980
1124
  noscript_img_src = "#{HUB_PATH_PREFIX}/account/login_indication.png"
981
1125
  noscript_img_tag = helpers.image_tag(noscript_img_src, size: '90x22', border: '0', alt: 'Log in or out')
982
1126
 
983
- if self.respond_to?(:request)
984
- return_to_url = self.request.try(:original_url)
985
-
986
- if return_to_url.present?
987
- return_query = URI.encode_www_form({ return_to_url: return_to_url.to_s })
988
- ui_href << "?#{return_query}"
989
- end
990
- end
991
-
992
- logged_in_link = helpers.link_to('Account', ui_href, id: 'hubssolib_logged_in_link')
993
- logged_out_link = helpers.link_to('Log in', ui_href, id: 'hubssolib_logged_out_link')
994
- noscript_link = helpers.link_to(noscript_img_tag, ui_href, id: 'hubssolib_login_noscript')
1127
+ logged_in_link = helpers.link_to('Account', logged_in_url, id: 'hubssolib_logged_in_link')
1128
+ logged_out_link = helpers.link_to('Log in', logged_out_url, id: 'hubssolib_logged_out_link')
1129
+ noscript_link = helpers.link_to(noscript_img_tag, universal_url, id: 'hubssolib_login_noscript')
995
1130
 
996
1131
  # Yes, it's ugly, but yes, it works and it's a lot better for the server
997
1132
  # to avoid the repeated image fetches. It probably works out as overall
@@ -1172,8 +1307,8 @@ module HubSsoLib
1172
1307
 
1173
1308
  unless hub_session_info[:keys].nil? # (keyset too large, enumeration prohibited)
1174
1309
  hub_session_info[:keys].each do | key |
1175
- session = hubssolib_factory().retrieve_session_by_key(key)
1176
- hub_users << session.session_user unless session&.session_user&.user_id.nil?
1310
+ session_user = hubssolib_factory().retrieve_session_by_key(key)&.session_user
1311
+ hub_users << session_user unless session_user&.user_id.nil?
1177
1312
  end
1178
1313
  end
1179
1314
 
@@ -1217,6 +1352,18 @@ module HubSsoLib
1217
1352
  hubssolib_factory().destroy_sessions_by_user_id(hub_user_id) unless hub_user_id.nil?
1218
1353
  end
1219
1354
 
1355
+ # WARNING: Comparatively slow.
1356
+ #
1357
+ # If a Hub user record changes, make sure their session records reflect
1358
+ # the updated demographics according to the HubSsoLib::User provided.
1359
+ #
1360
+ # For information about performance limitations, see
1361
+ # HubSsoLib::SessionFactory#destroy_sessions_by_user_id.
1362
+ #
1363
+ def hubssolib_update_user_sessions(hub_user_id, hub_user)
1364
+ hubssolib_factory().update_sessions_by_user_id(hub_user_id, hub_user) unless hub_user_id.nil?
1365
+ end
1366
+
1220
1367
  # If an application needs to know about changes of a user e-mail address
1221
1368
  # or display name (e.g. because of sync to a local relational store of
1222
1369
  # users related to other application-managed resources, with therefore a
@@ -1327,7 +1474,6 @@ module HubSsoLib
1327
1474
  cookies.delete(HUB_LOGIN_INDICATOR_COOKIE, domain: :all, path: '/')
1328
1475
 
1329
1476
  if login_is_required
1330
- hubssolib_store_location()
1331
1477
  return hubssolib_must_login()
1332
1478
  else
1333
1479
  return true
@@ -1354,14 +1500,10 @@ module HubSsoLib
1354
1500
  # if OK, else indicate that access is denied.
1355
1501
 
1356
1502
  if (hubssolib_session_expired?)
1357
- hubssolib_store_location()
1358
1503
  hubssolib_log_out()
1359
1504
  hubssolib_set_flash(:attention, 'Sorry, your session timed out; you need to log in again to continue.')
1360
1505
 
1361
- # We mean this: redirect_to :controller => 'account', :action => 'login'
1362
- # ...except for the Hub, rather than the current application (whatever
1363
- # it may be).
1364
- redirect_to HUB_PATH_PREFIX + '/account/login'
1506
+ redirect_to hubssolib_returnable_account_url(logged_in: false)
1365
1507
  else
1366
1508
  hubssolib_set_last_used(Time.now.utc)
1367
1509
  return hubssolib_authorized? ? true : hubssolib_access_denied()
@@ -1410,28 +1552,17 @@ module HubSsoLib
1410
1552
  end
1411
1553
  end
1412
1554
 
1413
- # Store the URI of the current request in the session, or store the
1414
- # optional supplied specific URI.
1415
- #
1416
- # We can return to this location by calling #redirect_back_or_default.
1555
+ # Deprecated. Don't use this. See #hubssolib_returnable_account_url.
1417
1556
  #
1418
1557
  def hubssolib_store_location(uri_str = request.url)
1419
- if (uri_str && !uri_str.empty?)
1420
- uri_str = hubssolib_promote_uri_to_ssl(uri_str, request.host) unless request.ssl?
1421
- hubssolib_set_return_to(uri_str)
1422
- else
1423
- hubssolib_set_return_to(nil)
1424
- end
1558
+ Rails.logger.warn('hubssolib_store_location: DEPRECATED (no-op)') rescue nil
1425
1559
  end
1426
1560
 
1427
- # Redirect to the URI stored by the most recent store_location call or
1428
- # to the passed default.
1561
+ # Deprecated. Don't use this. See #hubssolib_returnable_account_url.
1429
1562
  #
1430
1563
  def hubssolib_redirect_back_or_default(default)
1431
- url = hubssolib_get_return_to()
1432
- hubssolib_set_return_to(nil)
1433
-
1434
- redirect_to(url || default)
1564
+ Rails.logger.warn('hubssolib_redirect_back_or_default: DEPRECATED (always redirects to default)') rescue nil
1565
+ redirect_to(default)
1435
1566
  end
1436
1567
 
1437
1568
  # Take a URI and pass an optional host parameter. Decomposes the URI,
@@ -1440,9 +1571,10 @@ module HubSsoLib
1440
1571
  # as a flat string.
1441
1572
  #
1442
1573
  def hubssolib_promote_uri_to_ssl(uri_str, host = nil)
1443
- uri = URI.parse(uri_str)
1444
- uri.host = host if host
1574
+ uri = URI.parse(uri_str)
1575
+ uri.host = host if host
1445
1576
  uri.scheme = hub_bypass_ssl? ? 'http' : 'https'
1577
+
1446
1578
  return uri.to_s
1447
1579
  end
1448
1580
 
@@ -1454,8 +1586,7 @@ module HubSsoLib
1454
1586
  if request.ssl? || hub_bypass_ssl?
1455
1587
  return true
1456
1588
  else
1457
- # This isn't reliable: redirect_to({ :protocol => 'https://' })
1458
- redirect_to( hubssolib_promote_uri_to_ssl( request.request_uri, request.host ) )
1589
+ redirect_to(hubssolib_promote_uri_to_ssl(request.original_url))
1459
1590
  return false
1460
1591
  end
1461
1592
  end
@@ -1465,21 +1596,32 @@ module HubSsoLib
1465
1596
  # dropped. However, we also want this to work without being logged in, so
1466
1597
  # in that case it uses the normal flash as a backup when *writing*.
1467
1598
  #
1599
+ # This method returns the Hub flash if logged in, else +nil+.
1600
+ #
1468
1601
  def hubssolib_get_flash
1469
1602
  session = self.hubssolib_get_session()
1470
- session&.session_flash || {}
1603
+ session&.session_flash
1471
1604
  end
1472
1605
 
1606
+ # Set Flash information under the given symbol with the given text message,
1607
+ # using the Hub cross-application flash store if logged in, else the
1608
+ # this-application local session flash store otherwise.
1609
+ #
1473
1610
  def hubssolib_set_flash(symbol, message)
1474
1611
  session = self.hubssolib_get_session()
1475
- f = hubssolib_get_flash() unless session.nil?
1476
- f = self.flash if f.nil? && self.respond_to?(:flash)
1612
+
1613
+ f = hubssolib_get_flash()
1614
+ f ||= self.flash if self.respond_to?(:flash)
1615
+ f ||= {}
1477
1616
 
1478
1617
  f[symbol] = message
1479
1618
 
1480
1619
  session.session_flash = f unless session.nil?
1481
1620
  end
1482
1621
 
1622
+ # Clears the *hub* flash for logged-in users, but not the local application
1623
+ # session flash.
1624
+ #
1483
1625
  def hubssolib_clear_flash
1484
1626
  session = self.hubssolib_get_session()
1485
1627
  session.session_flash = {} unless session.nil?
@@ -1495,6 +1637,10 @@ module HubSsoLib
1495
1637
  # values. This allows both the Hub and standard flashes to have values
1496
1638
  # inside them under the same key. All keys are strings.
1497
1639
  #
1640
+ # You may well prefer to use #hubssolib_flash_markup to obtain something
1641
+ # that can be written straight into a view, unless it doesn't meet your
1642
+ # markup requirements.
1643
+ #
1498
1644
  def hubssolib_flash_data
1499
1645
 
1500
1646
  # These known key values are used to guarantee an order in the output
@@ -1510,14 +1656,14 @@ module HubSsoLib
1510
1656
  # Get an array of keys for the Hub flash with the ordered key items
1511
1657
  # first and store data from that flash; same again for standard.
1512
1658
 
1513
- hash = hubssolib_get_flash()
1659
+ hash = hubssolib_get_flash() || {}
1514
1660
  keys = ordered_keys | hash.keys
1515
1661
 
1516
1662
  keys.each do | key |
1517
1663
  compiled_data['hub'][key] = hash[key] if hash.key?(key)
1518
1664
  end
1519
1665
 
1520
- if defined?( flash )
1666
+ if self.respond_to?( :flash )
1521
1667
  hash = flash.to_h()
1522
1668
  keys = ordered_keys | hash.keys
1523
1669
 
@@ -1527,11 +1673,60 @@ module HubSsoLib
1527
1673
  end
1528
1674
 
1529
1675
  hubssolib_clear_flash()
1530
- flash.discard()
1676
+ flash.clear()
1531
1677
 
1532
1678
  return compiled_data
1533
1679
  end
1534
1680
 
1681
+ # A companion to #hubssolib_flash_data which returns standardised Flash
1682
+ # markup for your view. THIS MUST ONLY BE USED IN A VIEW / HELPER, or at
1683
+ # least somewhere that ActionView::Helpers::TagHelper#tag is available.
1684
+ #
1685
+ # * An outer DIV with class "flash" wraps the content.
1686
+ #
1687
+ # * Within that, +H2+ tags wrap each message in the flash data. These
1688
+ # tags also have class "flash", along with an additional tag which is
1689
+ # equal to the key under which the message was found - so if adding a
1690
+ # flash message under, say, <tt>:alert</tt>, the rendered result would
1691
+ # be <tt>&lt;h2 class="flash alert"&gt;...&lt;/h2&gt;</tt>.
1692
+ #
1693
+ # * As a special case, a key with the suffix <tt>_html_safe</tt> is taken
1694
+ # to contain HTML-safe strings and potential markup, so you could do
1695
+ # things like add emphasis, other classes or styles to the data written
1696
+ # inside the +H2+ tag. Be very careful to make sure any non-markup data
1697
+ # is properly escaped first. The key-related class in the arising +H2+
1698
+ # tag _excludes_ the suffix, so e.g. <tt>:notice_html_safe</tt> will
1699
+ # lead to class +notice+.
1700
+ #
1701
+ # Hub session-sourced and Rails local app session-sourced flash data is
1702
+ # treated the same, with Hub-sourced data listed first. The other is
1703
+ # otherwise undefined (it's rare for there to be more than one thing in
1704
+ # the flash at any given time, though).
1705
+ #
1706
+ def hubssolib_flash_markup
1707
+ data = hubssolib_flash_data()
1708
+ proc = Proc.new do | key, value |
1709
+ key = key.to_s
1710
+
1711
+ if key.end_with?( '_html_safe' )
1712
+ value = value.html_safe()
1713
+ key = key.chomp( '_html_safe' )
1714
+ end
1715
+
1716
+ helpers.tag.h2(value, class: "flash #{ key }")
1717
+ end
1718
+
1719
+ return helpers.tag.div(class: 'flash') do
1720
+ data['hub'].each do | key, value |
1721
+ helpers.concat(proc.call(key, value))
1722
+ end
1723
+
1724
+ data['standard'].each do | key, value |
1725
+ helpers.concat(proc.call(key, value))
1726
+ end
1727
+ end
1728
+ end
1729
+
1535
1730
  # Retrieve the message of an exception stored as an object in the given
1536
1731
  # string.
1537
1732
  #
@@ -1548,8 +1743,10 @@ module HubSsoLib
1548
1743
 
1549
1744
  :hubssolib_current_user,
1550
1745
  :hubssolib_unique_name,
1746
+ :hubssolib_returnable_account_url,
1551
1747
  :hubssolib_account_link,
1552
1748
  :hubssolib_flash_data,
1749
+ :hubssolib_flash_markup,
1553
1750
 
1554
1751
  :hubssolib_logged_in?,
1555
1752
  :hubssolib_authorized?,
@@ -1703,7 +1900,7 @@ module HubSsoLib
1703
1900
  #
1704
1901
  if hubssolib_ensure_https
1705
1902
  hubssolib_set_flash(:alert, 'You must log in before you can continue.')
1706
- redirect_to HUB_PATH_PREFIX + '/account/login'
1903
+ redirect_to hubssolib_returnable_account_url(logged_in: false)
1707
1904
  end
1708
1905
 
1709
1906
  return false
@@ -1795,16 +1992,6 @@ module HubSsoLib
1795
1992
  session = self.hubssolib_get_session()
1796
1993
  session.session_last_used = time unless session.nil?
1797
1994
  end
1798
-
1799
- def hubssolib_get_return_to
1800
- self.hubssolib_get_session()&.session_return_to
1801
- end
1802
-
1803
- def hubssolib_set_return_to(uri)
1804
- session = self.hubssolib_get_session()
1805
- session.session_return_to = uri unless session.nil?
1806
- end
1807
-
1808
1995
  end # Core module
1809
1996
  end # HubSsoLib module
1810
1997
 
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.7.1
4
+ version: 3.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Hodgkinson and others
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-03-28 00:00:00.000000000 Z
10
+ date: 2025-04-09 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: drb
@@ -51,6 +51,20 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
53
  version: '1.1'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rails
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '8.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '8.0'
54
68
  - !ruby/object:Gem::Dependency
55
69
  name: simplecov
56
70
  requirement: !ruby/object:Gem::Requirement
@@ -124,6 +138,7 @@ files:
124
138
  - Gemfile
125
139
  - Gemfile.lock
126
140
  - README.md
141
+ - hubssolib-3.7.0.gem
127
142
  - hubssolib.gemspec
128
143
  - lib/hub_sso_lib.rb
129
144
  homepage: http://pond.org.uk/