hubssolib 3.7.0 → 3.8.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: 13d41f0609a0ebf34e61267f91db31652699915b531010ddbda76ecc63110239
4
- data.tar.gz: aea0e7149740e0d25714bc1a0b9a2b5333910058497a0ec24906a65364475218
3
+ metadata.gz: 32438e5c41d655367f4bb0e8385b9d972645f2649871a09ab7b09dc5d617fe9e
4
+ data.tar.gz: a999f01f88325690644675acc1b06b33b7b275374e980c6c3f2fa346c3875d93
5
5
  SHA512:
6
- metadata.gz: 30c7c7f431d456cb286bd439be4e7ec3d80abde85d856e6f8914daf3d46e8f5ca0d36ac380a7240ddecde149f225fd8c80a5738070d9ed9404e87e77823e1e85
7
- data.tar.gz: c48fefd6183cbfe3d5f8c9bd9e5f503d31f24524ff000121f73b0f2651d7d081e72fa5f4870d0ae6ec6d7599d900eea8b4b4b283380d36a3f2184e2da46a69dc
6
+ metadata.gz: 881e57fa9c2e42ed8465b2faabf98cabdacf2dcea5df20d3c617836e50ff74d2255bbe1b8c11ca39b0bb3d57f2927e229ca1a390de11d0fcff9b8e4b32851ae6
7
+ data.tar.gz: 15aa2e97054fe9a8a86e45d901bdfa36bff0d0a6473ba88d279ec0bf622ce8413e6bc1a7b0427bac1f0e2bc0c2b994d9a3550d48897a8efec7cf2acf0c74abfa
data/CHANGELOG.md CHANGED
@@ -1,7 +1,14 @@
1
- ## 3.7.0, 28-Mar-2025
1
+ ## 3.8.0, 03-Apr-2025
2
+
3
+ * 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.
4
+ * 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.
5
+ * 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)`.
6
+
7
+ ## 3.7.0 and 3.7.1, 28-Mar-2025
2
8
 
3
9
  * 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.
4
10
  * User trust mechanism introduced, including HubSsoLib::Core#hubssolib_trusted? convenience accessor for Hub-integrated applications to check on user trust and `HubSsoLib::Core#hubssolib_review_action` convenience alias for `HubSsoLib::Trust.get_trust_object().review_action`.
11
+ * 3.7.1, released only a few minutes after 3.7.0, added the aforementioned `#hubssolib_retrieve_action` method, previously overlooked.
5
12
 
6
13
  ## 3.6.1, 27-Mar-2025
7
14
 
@@ -84,7 +91,7 @@ In Hub v1 and v2, login indication was done via an image that was served by the
84
91
 
85
92
  ```html
86
93
  <a class="img" href="<%= ENV['HUB_PATH_PREFIX'] %>/account/login_conditional">
87
- <img src="<%= ENV['HUB_PATH_PREFIX'] %>/account/login_indication" alt="Account" height="22" width="90" />
94
+ <img src="<%= ENV['HUB_PATH_PREFIX'] %>/account/login_indication" alt="Account" height="22" width="90">
88
95
  </a>
89
96
  ```
90
97
 
data/Gemfile.lock CHANGED
@@ -1,14 +1,91 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- hubssolib (3.7.0)
4
+ hubssolib (3.8.0)
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,18 +95,88 @@ 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
181
  reline (0.6.0)
35
182
  io-console (~> 0.5)
@@ -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
@@ -376,3 +376,98 @@ In the example of "permissions for arbitrary user" you may well not have ready a
376
376
  When a user logs in with a traditional log-in system, there's usually some message shown on the page presented when log-in is successful. This is achieved through the Hub equivalent of the Rails `flash` hash. Replace your preferred mechanism for including contents of the `flash` hash into your views (usually via one or more layout files) with an equivalent which calls Hub's flash handling code, which aggregates both current application and cross-application flash content into one.
377
377
 
378
378
  Just using `<%= hubssolib_flash_tags -%>` in your layout(s) and/or view(s) will output merged flash data. HTML is generated consisting of `h2` tags with class names derived from the keys used in the flash tag. Each heading tag encapsulates the value for the key and is followed by an empty paragraph tag for spacing reasons on older browsers when more than one key is present, though normally there is only one. Hub itself commonly uses keys `:notice` ("green" / general information - e.g. "you are now logged in"), `:attention` ("orange" / something unusual happened - e.g. "your session timed out so you were logged out") and `:alert` ("red" / something bad happened - e.g. "incorrect password given").
379
+
380
+
381
+
382
+ ## Trust mechanism
383
+
384
+ Hub 3.7.0 and later introduce a trust mechanism. Users have a "trusted" flag that's off by default for any new user. Participating applications can check the current user trust with `hubssolib_trusted?`. For actions where you care about trust - which are typically for creations (e.g. post to a forum, comment on an article) then:
385
+
386
+ * The relevant controller action proceeds as normal to the point of being ready to save new or updated record(s)
387
+ * If the user is untrusted and the to-be-saved record(s) is/are valid, and so should save OK...
388
+ - ...instead assemble information about the action and send it to Hub
389
+ - ...and *do not* save anything but instead exit with an appropriate flash message (e.g. "Thanks, that's gone into our moderation queue and should show up soon - please check back later!").
390
+
391
+ You send an action to Hub with an alias method `hubssolib_review_action` which has this effective signature:
392
+
393
+ ```ruby
394
+ def hubssolib_review_action(
395
+ user_email:,
396
+ action_scheme:,
397
+ action_url:,
398
+ action_payload:,
399
+ action_as_html:,
400
+ callback_url:
401
+ )
402
+ ```
403
+
404
+ ...where you pass:
405
+
406
+ * The user's email address _as known to Hub_, which is used to look up the user on the Hub side
407
+ * The HTTP method that was being used as a string, case insensitive, e.g. `POST`, for display to admins only
408
+ * The URL in full upon which this current request was made, for display to admins only
409
+ * The payload - usually just `params`, as-is - you'll get send this back again later, so include everything you need to reprocess the action
410
+ * Your choice of **safe** HTML that the Hub application will show as a preview (this may undergo some allow-list sanitisation; Hub does not expect a malicious integrated application to send it bad HTML which attempts skullduggery, but it shouldn't compromise its view layer just because something unsafe got sent accidentally, either!)
411
+ * **A callback URL** described below
412
+
413
+ If an admin thinks that the submission is bad - e.g. spam - then you won't hear about it again. Otherwise, the admin thinks the delayed action should now be taken for real. To accomplish this, Hub will `POST` back (and always uses `POST`) to your `callback_url` **in JSON** with an object that just has a key, `id`.
414
+
415
+ You create a controller action for your endpoint which retrieves the action details using code similar to this:
416
+
417
+ ```ruby
418
+ # The request format might be JSON because ".json" ended the path of the
419
+ # URI used to call here, but a missing or incorrect Content-Type header
420
+ # may mean that Rack/Rails doesn't process the body into "params" as
421
+ # expected. The latter would result in a nonsense database lookup which
422
+ # is harmless but wasteful, so don't do it.
423
+ #
424
+ if request.format.json? && request.headers['Content-Type']&.downcase == 'application/json'
425
+ @details = hubssolib_retrieve_action(id: params[:id])
426
+ head :unprocessable_entity if @details.nil?
427
+ else
428
+ head :unsupported_media_type
429
+ end
430
+ ```
431
+
432
+ ...then uses the object to work out what to do next. The retrieved object is a Hash, with **Symbol keys at the top level** but **String keys in `action_payload`**:
433
+
434
+ ```
435
+ {
436
+ timestamp: untrusted_action.created_at,
437
+ user_email: untrusted_action.user_email,
438
+ user_unique_name: user_unique_name,
439
+ action_scheme: untrusted_action.action_scheme,
440
+ action_url: untrusted_action.action_url,
441
+ action_payload: untrusted_action.action_payload
442
+ }
443
+ ```
444
+
445
+ ...so that's:
446
+
447
+ * The created-at time when you sent the action for review. This is usually only needed if you're dealing with a delayed edit/update action, where you might need to make sure that nobody else had edited the same entity in the mean time.
448
+ * The previously submitted user e-mail is given so you know _who_ it was that is now being trusted, along with the associated Hub "unique display name" (e.g. `Jane Doe (14510)`).
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
+ * 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
+
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
+
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.
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.0'
7
+ s.version = '3.8.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/'
@@ -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
@@ -394,24 +394,6 @@ module HubSsoLib
394
394
  attr_accessor :user_password_reset_code_expires_at
395
395
  attr_accessor :user_trusted
396
396
 
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
397
  end # User class
416
398
 
417
399
  #######################################################################
@@ -437,7 +419,7 @@ module HubSsoLib
437
419
  include DRb::DRbUndumped
438
420
 
439
421
  attr_accessor :session_last_used
440
- attr_accessor :session_return_to
422
+ attr_reader :session_return_to # DEPRECATED
441
423
  attr_accessor :session_flash
442
424
  attr_accessor :session_user
443
425
  attr_accessor :session_rotated_key
@@ -613,7 +595,9 @@ module HubSsoLib
613
595
  keys = if count > HUB_SESSION_ENUMERATION_KEY_MAX
614
596
  nil
615
597
  else
616
- @hub_sessions.keys # (Hash#keys returns a new array)
598
+ HUB_MUTEX.synchronize do
599
+ @hub_sessions.keys # (Hash#keys returns a new array)
600
+ end
617
601
  end
618
602
 
619
603
  return { count: count, keys: keys }
@@ -628,7 +612,7 @@ module HubSsoLib
628
612
  # found for that key.
629
613
  #
630
614
  def retrieve_session_by_key(key)
631
- @hub_sessions[key]
615
+ HUB_MUTEX.synchronize { @hub_sessions[key] }
632
616
  end
633
617
 
634
618
  # WARNING: Comparatively slow.
@@ -655,6 +639,10 @@ module HubSsoLib
655
639
  # session data. Does nothing if the key is not found.
656
640
  #
657
641
  def destroy_session_by_key(key)
642
+ unless @hub_be_quiet
643
+ puts "Session factory: Deleting session with key #{key}"
644
+ end
645
+
658
646
  HUB_MUTEX.synchronize do
659
647
  @hub_sessions.delete(key)
660
648
  end
@@ -674,6 +662,10 @@ module HubSsoLib
674
662
  # SESSIONS IN THE HASH and must therefore lock on mutex for the duration.
675
663
  #
676
664
  def destroy_sessions_by_user_id(user_id)
665
+ unless @hub_be_quiet
666
+ puts "Session factory: Deleting all session records for user ID #{user_id.inspect}"
667
+ end
668
+
677
669
  HUB_MUTEX.synchronize do
678
670
  @hub_sessions.reject! do | key, session |
679
671
  session&.session_user&.user_id == user_id
@@ -685,6 +677,30 @@ module HubSsoLib
685
677
  raise
686
678
  end
687
679
 
680
+ # WARNING: Comparatively slow.
681
+ #
682
+ # Call only if you MUST update details of a session user inside all Hub
683
+ # sessions. Pass the user ID and HubSsoLib::User details that are to be
684
+ # stored for all sessions owned by that user ID.
685
+ #
686
+ def update_sessions_by_user_id(user_id, user)
687
+ unless @hub_be_quiet
688
+ puts "Session factory: Updating all session records for user ID #{user_id.inspect}"
689
+ end
690
+
691
+ HUB_MUTEX.synchronize do
692
+ @hub_sessions.each do | key, session |
693
+ if session&.session_user&.user_id == user_id
694
+ session.session_user = user
695
+ end
696
+ end
697
+ end
698
+
699
+ rescue => e
700
+ Sentry.capture_exception(e) if defined?(Sentry) && Sentry.respond_to?(:capture_exception)
701
+ raise
702
+ end
703
+
688
704
  # WARNING: Slow.
689
705
  #
690
706
  # This is a housekeeping task which checks sessions against Hub expiry and,
@@ -964,34 +980,97 @@ module HubSsoLib
964
980
  end
965
981
  end
966
982
 
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.
983
+ # Return a URL that leads to a Hub "log in" page if the user is not logged
984
+ # in currently, else an "account" page for logged-in users.
985
+ #
986
+ # +universal+:: Use +true+ if concerned that a "Back" browser button action
987
+ # might cause a page to appear that's got a cached link which
988
+ # may no longer reflect the current state. This jumps to Hub
989
+ # and redirects to the "log in" or "account" destinations
990
+ # depending on current state. If using this, bear in mind
991
+ # that the link text must be ambiguous, because the eventual
992
+ # destination isn't known.
993
+ #
994
+ # It's often better to just use #hubssolib_account_link to
995
+ # create markup for this in a navigation area.
996
+ #
997
+ # The default is +false+, which means that a bespoke link
998
+ # for the exact current instantaneous log in state is
999
+ # generated; the +logged_in+ parameter applies (see below).
1000
+ #
1001
+ # +logged_in+:: If +universal+ is +false+, then this parameter defaults to
1002
+ # the current user logged-in state, but can be overridden
1003
+ # with +true+ (act as if someone is logged in) or +false+
1004
+ # (act as if someone is not logged in).
1005
+ #
1006
+ # +return_to+:: Overrides the use of <tt>request.original_url</tt> to give
1007
+ # an alternative return-after-logging-in URL. Will be of no
1008
+ # use for users already logged in. This is rarely used, but
1009
+ # might be helpful if you (say) want them to return to the
1010
+ # current page, but with a specific fragment added ("#foo").
1011
+ # Specify as a String, Symbol or URI, at your preference.
1012
+ #
1013
+ # Note that the universal links, or a not-logged-in state link will include
1014
+ # a query string which takes the value of <tt>request.original_url</tt> or,
1015
+ # if not defined, tries <tt>request.referrer</tt> (else is absent). This is
1016
+ # a URL used for the redirection after successful login. The logged-in
1017
+ # state link does not require this addition so omits it for brevity.
1018
+ #
1019
+ def hubssolib_returnable_account_url(
1020
+ universal: false,
1021
+ logged_in: self.hubssolib_logged_in?,
1022
+ return_to: nil
1023
+ )
1024
+ account_url = if universal
1025
+ "#{HUB_PATH_PREFIX}/account/login_conditional"
1026
+ else
1027
+ "#{HUB_PATH_PREFIX}/#{'account/login' unless logged_in}"
1028
+ end
1029
+
1030
+ # Attach a return-to URL always for the universal case which needs to
1031
+ # work regardless of login state; else we only need it when logged out,
1032
+ # so that we can return to the originating location after logging in.
1033
+ # There's no functional difference - just a simpler URL that omits what
1034
+ # would otherwise be an unused query string parameter.
1035
+ #
1036
+ if universal or not logged_in
1037
+ if return_to.nil?
1038
+ request_obj = self.try(:request)
1039
+ return_to = request_obj.try(:original_url) || request_obj.try(:referrer)
1040
+ end
1041
+
1042
+ if return_to.present?
1043
+ account_url << "?return_to_url=#{CGI.escape(return_to.to_s)}"
1044
+ end
1045
+ end
1046
+
1047
+ return account_url
1048
+ end
1049
+
1050
+ # Returns markup for a link that leads to Hub's login or logout link, using
1051
+ # pure HTML + CSS for styling. A universal "conditional login" link is used
1052
+ # since page data may be old due to e.g. a "back" button being used, after
1053
+ # the user either logged in or out elsewhere. This possibility is also why
1054
+ # JavaScript is used, updating the button styling the correct login state
1055
+ # if needed. This requires the "pageshow" event to be supported. NOSCRIPT
1056
+ # browsers use the a no-cache image-based fallback, which is much less
1057
+ # efficient, but works.
971
1058
  #
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.
1059
+ # Although the JavaScript-powered option could use the non-conditional link
1060
+ # for log in/out and swap those, it's simpler just to use generate the same
1061
+ # link for all states - script-capable or otherwise, logged in or out,
976
1062
  #
977
1063
  def hubssolib_account_link
978
- logged_in = self.hubssolib_logged_in?()
979
- ui_href = "#{HUB_PATH_PREFIX}/account/login_conditional"
1064
+ logged_in_url = self.hubssolib_returnable_account_url(universal: false, logged_in: true)
1065
+ logged_out_url = self.hubssolib_returnable_account_url(universal: false, logged_in: false)
1066
+ universal_url = self.hubssolib_returnable_account_url(universal: true)
1067
+
980
1068
  noscript_img_src = "#{HUB_PATH_PREFIX}/account/login_indication.png"
981
1069
  noscript_img_tag = helpers.image_tag(noscript_img_src, size: '90x22', border: '0', alt: 'Log in or out')
982
1070
 
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')
1071
+ logged_in_link = helpers.link_to('Account', logged_in_url, id: 'hubssolib_logged_in_link')
1072
+ logged_out_link = helpers.link_to('Log in', logged_out_url, id: 'hubssolib_logged_out_link')
1073
+ noscript_link = helpers.link_to(noscript_img_tag, universal_url, id: 'hubssolib_login_noscript')
995
1074
 
996
1075
  # Yes, it's ugly, but yes, it works and it's a lot better for the server
997
1076
  # to avoid the repeated image fetches. It probably works out as overall
@@ -1092,6 +1171,13 @@ module HubSsoLib
1092
1171
  HubSsoLib::Trust.get_trust_object().review_action(**args)
1093
1172
  end
1094
1173
 
1174
+ # The equivalent of #hubssolib_review_action, but for retrieving details of
1175
+ # an action when Hub invokes your callback via POST request.
1176
+ #
1177
+ def hubssolib_retrieve_action(**args)
1178
+ HubSsoLib::Trust.get_trust_object().retrieve_action(**args)
1179
+ end
1180
+
1095
1181
  # Public read-only accessor methods for common user activities:
1096
1182
  # return the current user's roles as a Roles object, or nil if
1097
1183
  # there's no user.
@@ -1210,6 +1296,18 @@ module HubSsoLib
1210
1296
  hubssolib_factory().destroy_sessions_by_user_id(hub_user_id) unless hub_user_id.nil?
1211
1297
  end
1212
1298
 
1299
+ # WARNING: Comparatively slow.
1300
+ #
1301
+ # If a Hub user record changes, make sure their session records reflect
1302
+ # the updated demographics according to the HubSsoLib::User provided.
1303
+ #
1304
+ # For information about performance limitations, see
1305
+ # HubSsoLib::SessionFactory#destroy_sessions_by_user_id.
1306
+ #
1307
+ def hubssolib_update_user_sessions(hub_user_id, hub_user)
1308
+ hubssolib_factory().update_sessions_by_user_id(hub_user_id, hub_user) unless hub_user_id.nil?
1309
+ end
1310
+
1213
1311
  # If an application needs to know about changes of a user e-mail address
1214
1312
  # or display name (e.g. because of sync to a local relational store of
1215
1313
  # users related to other application-managed resources, with therefore a
@@ -1320,7 +1418,6 @@ module HubSsoLib
1320
1418
  cookies.delete(HUB_LOGIN_INDICATOR_COOKIE, domain: :all, path: '/')
1321
1419
 
1322
1420
  if login_is_required
1323
- hubssolib_store_location()
1324
1421
  return hubssolib_must_login()
1325
1422
  else
1326
1423
  return true
@@ -1347,14 +1444,10 @@ module HubSsoLib
1347
1444
  # if OK, else indicate that access is denied.
1348
1445
 
1349
1446
  if (hubssolib_session_expired?)
1350
- hubssolib_store_location()
1351
1447
  hubssolib_log_out()
1352
1448
  hubssolib_set_flash(:attention, 'Sorry, your session timed out; you need to log in again to continue.')
1353
1449
 
1354
- # We mean this: redirect_to :controller => 'account', :action => 'login'
1355
- # ...except for the Hub, rather than the current application (whatever
1356
- # it may be).
1357
- redirect_to HUB_PATH_PREFIX + '/account/login'
1450
+ redirect_to hubssolib_returnable_account_url(logged_in: false)
1358
1451
  else
1359
1452
  hubssolib_set_last_used(Time.now.utc)
1360
1453
  return hubssolib_authorized? ? true : hubssolib_access_denied()
@@ -1403,28 +1496,17 @@ module HubSsoLib
1403
1496
  end
1404
1497
  end
1405
1498
 
1406
- # Store the URI of the current request in the session, or store the
1407
- # optional supplied specific URI.
1408
- #
1409
- # We can return to this location by calling #redirect_back_or_default.
1499
+ # Deprecated. Don't use this. See #hubssolib_returnable_account_url.
1410
1500
  #
1411
1501
  def hubssolib_store_location(uri_str = request.url)
1412
- if (uri_str && !uri_str.empty?)
1413
- uri_str = hubssolib_promote_uri_to_ssl(uri_str, request.host) unless request.ssl?
1414
- hubssolib_set_return_to(uri_str)
1415
- else
1416
- hubssolib_set_return_to(nil)
1417
- end
1502
+ Rails.logger.warn('hubssolib_store_location: DEPRECATED (no-op)') rescue nil
1418
1503
  end
1419
1504
 
1420
- # Redirect to the URI stored by the most recent store_location call or
1421
- # to the passed default.
1505
+ # Deprecated. Don't use this. See #hubssolib_returnable_account_url.
1422
1506
  #
1423
1507
  def hubssolib_redirect_back_or_default(default)
1424
- url = hubssolib_get_return_to()
1425
- hubssolib_set_return_to(nil)
1426
-
1427
- redirect_to(url || default)
1508
+ Rails.logger.warn('hubssolib_redirect_back_or_default: DEPRECATED (always redirects to default)') rescue nil
1509
+ redirect_to(default)
1428
1510
  end
1429
1511
 
1430
1512
  # Take a URI and pass an optional host parameter. Decomposes the URI,
@@ -1433,9 +1515,10 @@ module HubSsoLib
1433
1515
  # as a flat string.
1434
1516
  #
1435
1517
  def hubssolib_promote_uri_to_ssl(uri_str, host = nil)
1436
- uri = URI.parse(uri_str)
1437
- uri.host = host if host
1518
+ uri = URI.parse(uri_str)
1519
+ uri.host = host if host
1438
1520
  uri.scheme = hub_bypass_ssl? ? 'http' : 'https'
1521
+
1439
1522
  return uri.to_s
1440
1523
  end
1441
1524
 
@@ -1447,8 +1530,7 @@ module HubSsoLib
1447
1530
  if request.ssl? || hub_bypass_ssl?
1448
1531
  return true
1449
1532
  else
1450
- # This isn't reliable: redirect_to({ :protocol => 'https://' })
1451
- redirect_to( hubssolib_promote_uri_to_ssl( request.request_uri, request.host ) )
1533
+ redirect_to(hubssolib_promote_uri_to_ssl(request.original_url))
1452
1534
  return false
1453
1535
  end
1454
1536
  end
@@ -1458,21 +1540,32 @@ module HubSsoLib
1458
1540
  # dropped. However, we also want this to work without being logged in, so
1459
1541
  # in that case it uses the normal flash as a backup when *writing*.
1460
1542
  #
1543
+ # This method returns the Hub flash if logged in, else +nil+.
1544
+ #
1461
1545
  def hubssolib_get_flash
1462
1546
  session = self.hubssolib_get_session()
1463
- session&.session_flash || {}
1547
+ session&.session_flash
1464
1548
  end
1465
1549
 
1550
+ # Set Flash information under the given symbol with the given text message,
1551
+ # using the Hub cross-application flash store if logged in, else the
1552
+ # this-application local session flash store otherwise.
1553
+ #
1466
1554
  def hubssolib_set_flash(symbol, message)
1467
1555
  session = self.hubssolib_get_session()
1468
- f = hubssolib_get_flash() unless session.nil?
1469
- f = self.flash if f.nil? && self.respond_to?(:flash)
1556
+
1557
+ f = hubssolib_get_flash()
1558
+ f ||= self.flash if self.respond_to?(:flash)
1559
+ f ||= {}
1470
1560
 
1471
1561
  f[symbol] = message
1472
1562
 
1473
1563
  session.session_flash = f unless session.nil?
1474
1564
  end
1475
1565
 
1566
+ # Clears the *hub* flash for logged-in users, but not the local application
1567
+ # session flash.
1568
+ #
1476
1569
  def hubssolib_clear_flash
1477
1570
  session = self.hubssolib_get_session()
1478
1571
  session.session_flash = {} unless session.nil?
@@ -1488,6 +1581,10 @@ module HubSsoLib
1488
1581
  # values. This allows both the Hub and standard flashes to have values
1489
1582
  # inside them under the same key. All keys are strings.
1490
1583
  #
1584
+ # You may well prefer to use #hubssolib_flash_markup to obtain something
1585
+ # that can be written straight into a view, unless it doesn't meet your
1586
+ # markup requirements.
1587
+ #
1491
1588
  def hubssolib_flash_data
1492
1589
 
1493
1590
  # These known key values are used to guarantee an order in the output
@@ -1503,14 +1600,14 @@ module HubSsoLib
1503
1600
  # Get an array of keys for the Hub flash with the ordered key items
1504
1601
  # first and store data from that flash; same again for standard.
1505
1602
 
1506
- hash = hubssolib_get_flash()
1603
+ hash = hubssolib_get_flash() || {}
1507
1604
  keys = ordered_keys | hash.keys
1508
1605
 
1509
1606
  keys.each do | key |
1510
1607
  compiled_data['hub'][key] = hash[key] if hash.key?(key)
1511
1608
  end
1512
1609
 
1513
- if defined?( flash )
1610
+ if self.respond_to?( :flash )
1514
1611
  hash = flash.to_h()
1515
1612
  keys = ordered_keys | hash.keys
1516
1613
 
@@ -1520,11 +1617,60 @@ module HubSsoLib
1520
1617
  end
1521
1618
 
1522
1619
  hubssolib_clear_flash()
1523
- flash.discard()
1620
+ flash.clear()
1524
1621
 
1525
1622
  return compiled_data
1526
1623
  end
1527
1624
 
1625
+ # A companion to #hubssolib_flash_data which returns standardised Flash
1626
+ # markup for your view. THIS MUST ONLY BE USED IN A VIEW / HELPER, or at
1627
+ # least somewhere that ActionView::Helpers::TagHelper#tag is available.
1628
+ #
1629
+ # * An outer DIV with class "flash" wraps the content.
1630
+ #
1631
+ # * Within that, +H2+ tags wrap each message in the flash data. These
1632
+ # tags also have class "flash", along with an additional tag which is
1633
+ # equal to the key under which the message was found - so if adding a
1634
+ # flash message under, say, <tt>:alert</tt>, the rendered result would
1635
+ # be <tt>&lt;h2 class="flash alert"&gt;...&lt;/h2&gt;</tt>.
1636
+ #
1637
+ # * As a special case, a key with the suffix <tt>_html_safe</tt> is taken
1638
+ # to contain HTML-safe strings and potential markup, so you could do
1639
+ # things like add emphasis, other classes or styles to the data written
1640
+ # inside the +H2+ tag. Be very careful to make sure any non-markup data
1641
+ # is properly escaped first. The key-related class in the arising +H2+
1642
+ # tag _excludes_ the suffix, so e.g. <tt>:notice_html_safe</tt> will
1643
+ # lead to class +notice+.
1644
+ #
1645
+ # Hub session-sourced and Rails local app session-sourced flash data is
1646
+ # treated the same, with Hub-sourced data listed first. The other is
1647
+ # otherwise undefined (it's rare for there to be more than one thing in
1648
+ # the flash at any given time, though).
1649
+ #
1650
+ def hubssolib_flash_markup
1651
+ data = hubssolib_flash_data()
1652
+ proc = Proc.new do | key, value |
1653
+ key = key.to_s
1654
+
1655
+ if key.end_with?( '_html_safe' )
1656
+ value = value.html_safe()
1657
+ key = key.chomp( '_html_safe' )
1658
+ end
1659
+
1660
+ helpers.tag.h2(value, class: "flash #{ key }")
1661
+ end
1662
+
1663
+ return helpers.tag.div(class: 'flash') do
1664
+ data['hub'].each do | key, value |
1665
+ helpers.concat(proc.call(key, value))
1666
+ end
1667
+
1668
+ data['standard'].each do | key, value |
1669
+ helpers.concat(proc.call(key, value))
1670
+ end
1671
+ end
1672
+ end
1673
+
1528
1674
  # Retrieve the message of an exception stored as an object in the given
1529
1675
  # string.
1530
1676
  #
@@ -1541,8 +1687,10 @@ module HubSsoLib
1541
1687
 
1542
1688
  :hubssolib_current_user,
1543
1689
  :hubssolib_unique_name,
1690
+ :hubssolib_returnable_account_url,
1544
1691
  :hubssolib_account_link,
1545
1692
  :hubssolib_flash_data,
1693
+ :hubssolib_flash_markup,
1546
1694
 
1547
1695
  :hubssolib_logged_in?,
1548
1696
  :hubssolib_authorized?,
@@ -1696,7 +1844,7 @@ module HubSsoLib
1696
1844
  #
1697
1845
  if hubssolib_ensure_https
1698
1846
  hubssolib_set_flash(:alert, 'You must log in before you can continue.')
1699
- redirect_to HUB_PATH_PREFIX + '/account/login'
1847
+ redirect_to hubssolib_returnable_account_url(logged_in: false)
1700
1848
  end
1701
1849
 
1702
1850
  return false
@@ -1788,16 +1936,6 @@ module HubSsoLib
1788
1936
  session = self.hubssolib_get_session()
1789
1937
  session.session_last_used = time unless session.nil?
1790
1938
  end
1791
-
1792
- def hubssolib_get_return_to
1793
- self.hubssolib_get_session()&.session_return_to
1794
- end
1795
-
1796
- def hubssolib_set_return_to(uri)
1797
- session = self.hubssolib_get_session()
1798
- session.session_return_to = uri unless session.nil?
1799
- end
1800
-
1801
1939
  end # Core module
1802
1940
  end # HubSsoLib module
1803
1941
 
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.0
4
+ version: 3.8.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-03-28 00:00:00.000000000 Z
10
+ date: 2025-04-04 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