hubssolib 3.1.0 → 3.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 59532e45ebc82569e9f43aab07611c7092c5cf8f140db8bc08a6b98043d7e500
4
- data.tar.gz: 0e59ad5183becc64d8ff59b71062eec2b18c77b916f4030053010214ae8e6289
3
+ metadata.gz: ff87f5cf7f12fbe119c62700a1967efe14365bd60ac1a9c4be277825320e62eb
4
+ data.tar.gz: 91a60374a009856c6090365fd711a0ad4a3e1b2d5deceaa845e10d215717fd43
5
5
  SHA512:
6
- metadata.gz: f046c3981c7749598f57e68a4db7fd6528c1d97e74bb4fe437fc4a72c31ceb011eb4a7c8f4cd59a820b0d14ee18e5947da533b987b2a73f91101582c6e4e1286
7
- data.tar.gz: e3389646a064fd1e077ea4b5649d7d984b723933643003974134dd02c0f718ee063ea523b24a369b328a04ffcff9fd5cc3842f3bd9e5510f44fbe60a3af878a3
6
+ metadata.gz: c77c64ac36ca1ddb3b31fd96b0061cb8bb63e4ca512c9ef59fe010fb4917310dcabf1bff99a5b404b7105d6607bd81adfaece9803d4b0f48aa57e861d1084864
7
+ data.tar.gz: ff31f8a5fb859f35f1c0e777c49127ed02692e00056ef3dd7bfdd74e034066096b89358cc2e834850a84d33d64b7d020edcc0d400648307ad92c6f28f910e682
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 3.2.1, 16-Feb-2025
2
+
3
+ The conditional login return-via-referrer mechanism never really worked, so instead have the login status indicator link generate a return-to URL in the query string instead and forward that, if present, in preference.
4
+
5
+ ## 3.2.0, 15-Feb-2025
6
+
7
+ Introduces the user change handler mechanism, a scheme whereby an external application tells Hub about a Rake task that the application has implemented, through which Hub can inform if of changes to a user's e-mail address or "real" name. See the gem's `README.md` for details, under "Applications with an existing user model".
8
+
1
9
  ## 3.1.0, 14-Feb-2025
2
10
 
3
11
  Environment variable `HUB_IDLE_TIME_LIMIT` can be used to override the idle timeout, with a value expressed in seconds. It must be set in the environment of any application using Hub, including the Hub application itself.
data/README.md CHANGED
@@ -118,20 +118,20 @@ before_action :hubssolib_beforehand
118
118
  after_action :hubssolib_afterwards</pre>
119
119
  ```
120
120
 
121
- Within any controller which has actions which you wish to protect with Hub login, define a variable `@@hubssolib_permissions` and provide an accessor method for it. I'll deal with the accessor method first; for a controller called `FooController`, add the following to `foo_controller.rb`:
121
+ Within any controller which has actions which you wish to protect with Hub login, define a constant `HUBSSOLIB_PERMISSIONS` and provide an accessor method for it. I'll deal with the accessor method first; in any controller for which Hub is to guard access, add the following:
122
122
 
123
123
  ```ruby
124
- def FooController.hubssolib_permissions
125
- @@hubssolib_permissions
124
+ def self.hubssolib_permissions
125
+ HUBSSOLIB_PERMISSIONS
126
126
  end
127
127
  ```
128
128
 
129
129
  More details are provided [below](#permissions) but, in brief, to define the permissions variable you create an instance of `HubSsoLib::Permissions`. The constructor is passed a hash. The hash keys are symbolized names of the controller actions you want to protect. The hash values are an array of privileges required to access the action, from a choice of one or more of `:admin`, `:webmaster`, `:privileged` and `:normal`. These relate to the roles you can assign to accounts as Hub administrator. For example:
130
130
 
131
131
  ```ruby
132
- @@hubssolib_permissions = HubSsoLib::Permissions.new({
133
- :show => [ :admin, :webmaster, :privileged, :normal ],
134
- :edit => [ :admin, :webmaster ]
132
+ HUBSSOLIB_PERMISSIONS = HubSsoLib::Permissions.new({
133
+ show: [ :admin, :webmaster, :privileged, :normal ],
134
+ edit: [ :admin, :webmaster ]
135
135
  })
136
136
  ```
137
137
 
@@ -140,14 +140,15 @@ In this example, any user can access the controller's `show` action but only use
140
140
  A user's role(s) must match at least one of the privileges in the array for a given action — so even if your account has an administrator role (and _only_ an administrator role), it won't be able to access a protected action unless `:admin` is included in the array given within the hash to the `HubSsoLib::Permissions` constructor. For example:
141
141
 
142
142
  ```ruby
143
- @@hubssolib_permissions = HubSsoLib::Permissions.new({
144
- :weblist => [ :webmaster, :privileged ]
143
+ HUBSSOLIB_PERMISSIONS = HubSsoLib::Permissions.new({
144
+ weblist: [ :webmaster, :privileged ]
145
145
  })
146
146
  ```
147
147
 
148
148
  Here, only accounts with the webmaster or privileged role associated can access the `weblist` action. If an account has only normal and/or administrative roles, it won't be allowed through.
149
149
 
150
150
  ### Applications with an existing user model
151
+ #### General concerns
151
152
 
152
153
  If you want to integrate Hub with an application which already has the concept of user accounts, logging in and logging out, there are two main approaches.
153
154
 
@@ -156,6 +157,51 @@ If you want to integrate Hub with an application which already has the concept o
156
157
 
157
158
  Neither approach is problem-free and both require quite a lot of effort and testing. Automated testing is very hard because the modified application's behaviour depends upon logging in or out of Hub, which is running elsewhere. Unfortunately Rails doesn't offer a universally supported single sign-on mechanism so applications all use different approaches to user management; this means that there is no magic bullet to integration with Hub. You have to learn and understand the structure of the application being integrated and be prepared to make changes that are potentially quite extensive.
158
159
 
160
+ #### Synchronisation concerns, where applicable
161
+
162
+ Some applications might use local User records for relational purposes. Suppose users could author Posts. We could simply freeze the author name with a Post using a column in that table. This means the author name of any Post is easy to display. However, if that author changed their name, only new Posts would show the new name. If the application wanted to answer a question such as, "List all Posts by a given author", it would be relatively expensive. And if an application wanted to internally keep a record of author e-mail addresses for things like e-mailed notifications of some kind, it would get worse; the author might change their e-mail address in Hub and the integrated application would not know.
163
+
164
+ To solve these problems, external applications integrated with Hub can participate in the user change handler mechanism. The Hub-integrated external application calls `HubSsoLib::Core#hubssolib_register_user_change_handler` to register an interest in Hub user alterations. The mechanism invokes an application-defined Rake task, which can update user records however it sees fit. The method is given the application's name, it's Rails application root - the filesystem location of the application, since we need to use that as a current working directory to issue a `bundle exec rake your:task:name...` - and the name of the Rake task to run. Registration is usually done in `config/application.rb` - for example:
165
+
166
+ ```ruby
167
+ module Foo
168
+ class Application < Rails::Application
169
+ include HubSsoLib::Core
170
+
171
+ hubssolib_register_user_change_handler(
172
+ app_name: Rails.application.name,
173
+ app_root: Rails.root,
174
+ task_name: 'hub:update_user'
175
+ )
176
+
177
+ # ...etc...
178
+ end
179
+ end
180
+ ```
181
+
182
+ The Rake task can have any name that works for your application. A "hub" namespace is recommended but entirely optional. If a Hub user edits their details, then the task is invoked with four positional parameters - the user's old e-mail address and old Hub unique name, as returned by `HubSsoLib::Core#hubssolib_unique_name` - followed by the new e-mail address and new unique name (at least one, or both of those will always have actually changed). The Rake task can then look up the external application's User record via the old address or unique name and, if found, update it. It should either throw an exception or `exit` with a non-zero status code if it fails to store the updated details - in that case, Hub will roll back user changes on its side and warn the end user.
183
+
184
+ For example, `lib/tasks/hub.rake` might look like this:
185
+
186
+ ```ruby
187
+ namespace :hub do
188
+ desc 'Update a user with details sent from Hub'
189
+ task :update_user, [:old_email, :old_name, :new_email, :new_name] => :environment do | t, args |
190
+ user = User.find_by_email_address(args[:old_email])
191
+
192
+ # ...or use "user&.update_columns" for speed and to bypass validations, as
193
+ # Hub's validations may not be as strict as yours but it doesn't matter if
194
+ # you're happy syncing Hub data in general.
195
+ #
196
+ user&.update!(email_address: args[:new_email], display_name: args[:new_name])
197
+ end
198
+ end
199
+ ```
200
+
201
+ The four arguments are guaranteed to be present and non-empty, with leading or trailing white space already stripped. You don't need to waste time checking for `nil` or blank values. The upper/lower case **is preserved**, as entered by the user, for all arguments - so e-mail addresses in particular might contain a mixture of upper and lower case letters. If this matters to your application, be sure to apply whatever normalisation you previously used when your user record was originally created with the old Hub-sourced e-mail and/or name.
202
+
203
+
204
+
159
205
  ## Hub library API
160
206
 
161
207
  The Hub component interfaces that should be used by application authors when integrating with the Hub single sign-on mechanism are described below. If you want a complete list of all public interfaces, consult the file `hub_sso_lib.rb` inside the Hub gem. All functions and classes therein are fully commented to describe the purpose of each class, along with the purpose, input parameters and return values of class methods and instance methods.
@@ -178,9 +224,9 @@ Hub protects against access to actions in controller by using a `before_action`
178
224
  Permitted roles are expressed as single symbols or their equivalent strings, or an array containing many symbols or equivalent strings. Most often, an array of symbols is used. To create a permissions object, instantiate `HubSsoLib::Permissions`. For example:
179
225
 
180
226
  ```ruby
181
- @@hubssolib_permissions = HubSsoLib::Permissions.new({
182
- :show => [ :admin, :webmaster, :privileged, :normal ],
183
- :edit => [ :admin, :webmaster ]
227
+ HUBSSOLIB_PERMISSIONS = HubSsoLib::Permissions.new({
228
+ show: [ :admin, :webmaster, :privileged, :normal ],
229
+ edit: [ :admin, :webmaster ]
184
230
  })
185
231
  ```
186
232
 
@@ -191,7 +237,7 @@ The above line of code typically appears at the start of the class definition fo
191
237
  ```ruby
192
238
  class AccountController < ApplicationController
193
239
 
194
- @@hubssolib_permissions = HubSsoLib::Permissions.new({
240
+ HUBSSOLIB_PERMISSIONS = HubSsoLib::Permissions.new({
195
241
  # ...permissions here...
196
242
  })
197
243
 
@@ -199,11 +245,11 @@ class AccountController < ApplicationController
199
245
  end
200
246
  ```
201
247
 
202
- Having created the permissions object, you need to expose variable `@@hubssolib_permissions` to Hub in a way that it understands. To do this, create an instance method called `hubssolib_permissions` that just returns the variable:
248
+ Having created the permissions object, you need to expose constant `HUBSSOLIB_PERMISSIONS` to Hub in a way that it understands. To do this, create a class method called `hubssolib_permissions` that just returns the variable:
203
249
 
204
250
  ```ruby
205
- def AccountController.hubssolib_permissions
206
- @@hubssolib_permissions
251
+ def self.hubssolib_permissions
252
+ HUBSSOLIB_PERMISSIONS
207
253
  end
208
254
  ```
209
255
 
@@ -212,12 +258,12 @@ So the full preamble in this example is:
212
258
  ```ruby
213
259
  class AccountController < ApplicationController
214
260
 
215
- @@hubssolib_permissions = HubSsoLib::Permissions.new({
261
+ HUBSSOLIB_PERMISSIONS = HubSsoLib::Permissions.new({
216
262
  ...permissions here...
217
263
  })
218
264
 
219
- def AccountController.hubssolib_permissions
220
- @@hubssolib_permissions
265
+ def self.hubssolib_permissions
266
+ HUBSSOLIB_PERMISSIONS
221
267
  end
222
268
 
223
269
  ...existing class contents here...
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.1.0'
7
+ s.version = '3.2.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/'
data/lib/hub_sso_lib.rb CHANGED
@@ -19,6 +19,7 @@ module HubSsoLib
19
19
 
20
20
  require 'drb'
21
21
  require 'securerandom'
22
+ require 'json'
22
23
 
23
24
  # DRb connection
24
25
  HUB_CONNECTION_URI = ENV['HUB_CONNECTION_URI'] || 'drbunix:' + File.join( ENV['HOME'] || '/', '/.hub_drb')
@@ -33,6 +34,19 @@ module HubSsoLib
33
34
  raise 'Exiting'
34
35
  end
35
36
 
37
+ # External application command registry for on-user-change events
38
+ HUB_COMMAND_REGISTRY = ENV['HUB_COMMAND_REGISTRY'] || File.join( ENV['HOME'] || '/', '/.hub_cmd_reg')
39
+
40
+ unless Dir.exist?(File.dirname(HUB_COMMAND_REGISTRY))
41
+ puts
42
+ puts '*' * 80
43
+ puts "Invalid path specified by HUB_COMMAND_REGISTRY (#{ HUB_COMMAND_REGISTRY.inspect })"
44
+ puts '*' * 80
45
+ puts
46
+
47
+ raise 'Exiting'
48
+ end
49
+
36
50
  # Location of Hub application root.
37
51
  #
38
52
  HUB_PATH_PREFIX = ENV['HUB_PATH_PREFIX'] || ''
@@ -541,6 +555,15 @@ module HubSsoLib
541
555
  noscript_img_src = "#{HUB_PATH_PREFIX}/account/login_indication.png"
542
556
  noscript_img_tag = helpers.image_tag(noscript_img_src, size: '90x22', border: '0', alt: 'Log in or out')
543
557
 
558
+ if self.respond_to?(:request)
559
+ return_to_url = self.request.try(:original_url)
560
+
561
+ if return_to_url.present?
562
+ return_query = URI.encode_www_form({ return_to_url: return_to_url.to_s })
563
+ ui_href << "?#{return_query}"
564
+ end
565
+ end
566
+
544
567
  logged_in_link = helpers.link_to('Account', ui_href, id: 'hubssolib_logged_in_link')
545
568
  logged_out_link = helpers.link_to('Log in', ui_href, id: 'hubssolib_logged_out_link')
546
569
  noscript_link = helpers.link_to(noscript_img_tag, ui_href, id: 'hubssolib_login_noscript')
@@ -710,9 +733,96 @@ module HubSsoLib
710
733
  user ? "#{user.user_real_name} (#{user.user_id})" : 'Anonymous'
711
734
  end
712
735
 
713
- # Main filter method to implement HubSsoLib permissions management,
714
- # session expiry and so-on. Call from controllers only, always as a
715
- # before_fitler.
736
+ # If an application needs to know about changes of a user e-mail address
737
+ # or display name (e.g. because of sync to a local relational store of
738
+ # users related to other application-managed resources, with therefore a
739
+ # desire to keep that store up to date), it can register a task to run
740
+ # on-change here. When a user edits their information, Hub runs through
741
+ # all such commands, allowing external applications to manage their own
742
+ # state with no need for coupled configuration or other duplication.
743
+ #
744
+ # The registered name must be a Rake task and the application must specify
745
+ # its location in the filesystem so that the PWD can be changed there, in
746
+ # order to execute the Rake task via "bundle exec". The task is passed the
747
+ # following parameters, in the specified order:
748
+ #
749
+ # - User's old e-mail address
750
+ # - User's old unique display name (as returned by #hubssolib_unique_name)
751
+ # - User's new e-mail address (which might be the same as the old)
752
+ # - User's old unique display name (which might be unchanged too)
753
+ #
754
+ # This is a newer Hub interface which uses named parameters rather than
755
+ # positional:
756
+ #
757
+ # +app_name+:: Application name, e.g. "beast"; make sure this is unique.
758
+ # +app_root+:: Application's Rails root, e.g. "/home/fred/rails/beast".
759
+ # +task_name+:: Rake task name, e.g. "hub:update_user".
760
+ #
761
+ # An example invocation in "config/application.rb" might look like this:
762
+ #
763
+ # module Foo
764
+ # class Application < Rails::Application
765
+ #  require HubSsoLib::Core
766
+ #
767
+ # hubssolib_register_user_change_handler(
768
+ # app_name: Rails.application.name,
769
+ # app_root: Rails.root,
770
+ # task_name: 'hub:update_user'
771
+ # )
772
+ #
773
+ # config.load_defaults 8.0 # ...etc...
774
+ # end
775
+ # end
776
+ #
777
+ def hubssolib_register_user_change_handler(app_name:, app_root:, task_name:)
778
+ File.open(HUB_COMMAND_REGISTRY, File::RDWR | File::CREAT) do |file|
779
+ file.flock(File::LOCK_EX)
780
+
781
+ commands_json = file.read()
782
+ commands_hash = (JSON.parse(commands_json) rescue nil) if commands_json.present?
783
+ commands_hash ||= {}
784
+
785
+ file.rewind()
786
+
787
+ commands_hash[app_name] = {
788
+ root: app_root,
789
+ task: task_name
790
+ }
791
+
792
+ file.write(JSON.fast_generate(commands_hash))
793
+ file.truncate(file.pos)
794
+ end
795
+ end
796
+
797
+ # Returns all change handlers registered by prior calls made to
798
+ # #hubssolib_register_user_change_handler. Returns a Hash, keyed by Rails
799
+ # application name, with values of another Hash:
800
+ #
801
+ # * +root+ => Rails application root
802
+ # * +task+ => Name of Rake task to be run
803
+ #
804
+ # All keys are Strings.
805
+ #
806
+ # This is usually called by the Hub application only, when it is processing
807
+ # a user's request to change their information.
808
+ #
809
+ def hubssolib_registered_user_change_handlers
810
+ commands_hash = {}
811
+
812
+ File.open(HUB_COMMAND_REGISTRY, File::RDWR | File::CREAT) do |file|
813
+ file.flock(File::LOCK_EX)
814
+
815
+ commands_json = file.read()
816
+ commands_hash = (JSON.parse(commands_json) rescue nil) if commands_json.present?
817
+ commands_hash ||= {}
818
+ end
819
+
820
+ return commands_hash
821
+ end
822
+
823
+ # Mandatory controller "before_action" callback method which activates
824
+ # HubSsoLib permissions management, session expiry and so-on. Usually
825
+ # invoked in ApplicationController.
716
826
  #
717
827
  def hubssolib_beforehand
718
828
 
@@ -794,7 +904,8 @@ module HubSsoLib
794
904
  end
795
905
  end
796
906
 
797
- # Main after_filter method to tidy up after running state changes.
907
+ # Mandatory controller "after_action" callback method to tidy up after Hub
908
+ # actions during a request. Usually invoked in ApplicationController.
798
909
  #
799
910
  def hubssolib_afterwards
800
911
  begin
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.1.0
4
+ version: 3.2.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-02-14 00:00:00.000000000 Z
10
+ date: 2025-02-16 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: drb