audiences 3.1.0 → 3.2.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: 298fbad100f5af1295e605b5f1d82f92fd0a807b6278c96c4515dd4e82da124c
4
- data.tar.gz: 4c08ece17beeb6944ee5f503780d3d7a18a1e55d95d63eeb4ba4c70841bdfa96
3
+ metadata.gz: 772d8496fc779d86cc53c50e02bea87dfaeac00483d13a2bf301415160d8254d
4
+ data.tar.gz: 31d137e6332e906dba696ab61fba1ec7c35362b6b663c123ae1a87192e9cf3c5
5
5
  SHA512:
6
- metadata.gz: 98dc2b2dcdc121b15c92c494c5d0dfec7a41a431b21908b2ce09319abf39058ae3743e7aa59b2ef88eeeb3f3cadb2ce759a66d078d5672a6b7515f4c05390e69
7
- data.tar.gz: 5c227cdbef5540de3b592591fb3bdf9d251c95773c88e59fd4d94d7bc6c094c58e71fe1bf956dc00018d94979de3733859105195b78a9873c0e9d5c827934334
6
+ metadata.gz: fc0c07c9cd9a8a672b38d4a6be95e504a55ddc7d96a7644c24cc076bc72e2eab4e8462ef2b28e4e243ddadfca29016ea436be19cf6d298dd2d5725c18a64ed59
7
+ data.tar.gz: da35f519039d7932891b25925095f31cd4655f27ffd1274575b074f14084577d95f81fafd3196e6f0e83fd0b9646ab09d072717264ab27625a954e050fa0d2c1
@@ -2,23 +2,17 @@
2
2
 
3
3
  module Audiences
4
4
  class ScimProxyController < ApplicationController
5
+ # Reads resources through the configured read source. The source is
6
+ # swappable via Audiences.config.read_source (defaults to the legacy
7
+ # source that reads Audiences' own projection), so the controller stays
8
+ # agnostic to where the data comes from.
5
9
  def get
6
- resources = scope.search(params[:query])
7
- .offset(params[:startIndex])
8
- .limit(params[:count])
9
-
10
- render json: resources
11
- end
12
-
13
- private
14
-
15
- def scope
16
- if params[:scim_path].eql?("Users")
17
- Audiences::ExternalUser.instance_exec(&Audiences.default_users_scope)
18
- else
19
- Audiences::Group.where(resource_type: params[:scim_path])
20
- .instance_exec(&Audiences.default_groups_scope)
21
- end
10
+ render json: Audiences.read_source.fetch(
11
+ resource_type: params[:scim_path],
12
+ query: params[:query],
13
+ start_index: params[:startIndex],
14
+ count: params[:count]
15
+ )
22
16
  end
23
17
  end
24
18
  end
@@ -27,14 +27,12 @@ module Audiences
27
27
  private
28
28
 
29
29
  def result
30
- @result ||= @scope.where("#{data_attribute_query} LIKE ?", "%#{@query}%")
31
- end
32
-
33
- def data_attribute_query
34
- case @scope.connection.adapter_name
35
- when "PostgreSQL" then "CAST(data AS TEXT)"
36
- else "CAST(data AS CHAR)"
37
- end
30
+ sql = if @scope.connection.adapter_name == "PostgreSQL"
31
+ "CAST(data AS TEXT) LIKE ?"
32
+ else
33
+ "CAST(data AS CHAR) LIKE ?"
34
+ end
35
+ @result ||= @scope.where(sql, "%#{@query}%")
38
36
  end
39
37
  end
40
38
  end
data/docs/CHANGELOG.md CHANGED
@@ -1,4 +1,20 @@
1
- # Unreleased
1
+ # Version 3.2.0 (2026-10-21)
2
+
3
+ **Breaking Changes:**
4
+ - Require Rails 7.2 or newer and Ruby 3.1 or newer. Rails 6.1, 7.0, and 7.1 are no longer supported. [#567](https://github.com/powerhome/audiences/pull/567)
5
+
6
+ **Improvements:**
7
+ - Add `Audiences.config.read_source` and the `Audiences.read_source` accessor so applications can supply a custom source for `GET /scim(/*scim_path)` through a `fetch(resource_type:, query:, start_index:, count:)` interface. [#565](https://github.com/powerhome/audiences/pull/565)
8
+ - Default to `Audiences::ReadSources::Legacy`, preserving local user/group reads, configured scopes, search, pagination, and JSON serialization. Context endpoints and audience calculations continue to use the local projection.
9
+ - Document read-source configuration, reload-safe initialization, the request/response contract, and scope requirements.
10
+
11
+ **Development:**
12
+ - Update the development bundle to Rails 8.1 and cover Rails 7.2, 8.0, and 8.1 with both MySQL and PostgreSQL in CI. Refresh development and appraisal dependencies. [#567](https://github.com/powerhome/audiences/pull/567)
13
+
14
+ # Version 3.1.0 (2026-09-23)
15
+
16
+ **Development:**
17
+ - Update the dummy application's Playbook UI dependency from a prerelease to the released 16.11.0 version. [#562](https://github.com/powerhome/audiences/pull/562)
2
18
 
3
19
  # Version 3.0.0 (2026-06-18)
4
20
 
data/docs/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  ## Installation
6
6
 
7
+ Requires Ruby 3.1 or newer and Rails 7.2 or newer. Rails versions may impose a higher Ruby requirement; the Rails 8.1 development bundle requires Ruby 3.2 or newer.
8
+
7
9
  Add this line to your application's Gemfile:
8
10
 
9
11
  ```ruby
@@ -42,7 +44,97 @@ For more details, refer to [editor_helper](../lib/audiences/editor_helper.rb).
42
44
 
43
45
  ### Configuring Audiences
44
46
 
45
- ### Adding Audiences to a Model
47
+ #### Read Source
48
+
49
+ The read source backs the resource endpoint, `GET /scim(/*scim_path)`. `ScimProxyController#get` renders whatever the configured source returns:
50
+
51
+ ```ruby
52
+ render json: Audiences.read_source.fetch(
53
+ resource_type: params[:scim_path],
54
+ query: params[:query],
55
+ start_index: params[:startIndex],
56
+ count: params[:count]
57
+ )
58
+ ```
59
+
60
+ `Audiences.config.read_source` defaults to `Audiences::ReadSources::Legacy`, which reads Audiences' own `ExternalUser` and `Group` projection. Replacing it with any object that satisfies the `#fetch` contract below lets reads come from another store without Audiences depending on that store.
61
+
62
+ That covers only this endpoint. The context endpoints (`GET /:key` and `GET /:key/users`) and audience calculation keep reading the local projection no matter what the read source does.
63
+
64
+ Assign a replacement from an initializer:
65
+
66
+ ```ruby
67
+ Audiences.configure do |config|
68
+ config.read_source = MyReadSource.new
69
+ end
70
+ ```
71
+
72
+ When the implementation is an autoloaded constant, assign it inside a `to_prepare` block so each reload picks up the new class. Keep the block inside `configure` so `config` stays in scope:
73
+
74
+ ```ruby
75
+ Audiences.configure do |config|
76
+ Rails.application.config.to_prepare do
77
+ config.read_source = MyReadSource.new
78
+ end
79
+ end
80
+ ```
81
+
82
+ ##### The `#fetch` contract
83
+
84
+ ```ruby
85
+ def fetch(resource_type:, query:, start_index:, count:)
86
+ ```
87
+
88
+ All four arguments come straight from request params with no casting or defaulting, so each one is a `String` or `nil`.
89
+
90
+ - `resource_type` is the wildcard path segment: `"Users"`, `"Groups"`, `"Departments"`, and so on. The segment is optional in the route, so `GET /scim` passes `nil`.
91
+ - `query` is a substring to match against the display name. `nil` applies no filter.
92
+ - `start_index` is an offset and `count` is a limit, both as strings such as `"2"`. `nil` means no restriction, so omitting `count` returns the whole catalogue. A source doing arithmetic on either has to call `to_i` first.
93
+
94
+ ##### The return contract
95
+
96
+ Return a collection `render json:` can serialize: an `ActiveRecord::Relation`, serialized through each record's `as_json`, or an array of hashes rendered as given.
97
+
98
+ Every resource type except `"Users"` returns elements of this shape, where `id` is the SCIM id rather than the primary key:
99
+
100
+ ```json
101
+ { "id": "<scim_id>", "externalId": "<external_id>", "displayName": "<display_name>" }
102
+ ```
103
+
104
+ A `"Users"` element is the stored SCIM data plus these keys, derived from the user's group memberships:
105
+
106
+ ```json
107
+ {
108
+ "groups": [{ "value": "<group scim_id>", "display": "<group display_name>" }],
109
+ "title": "<Titles group display name>",
110
+ "urn:ietf:params:scim:schemas:extension:authservice:2.0:User": {
111
+ "role": "<Roles group display name>",
112
+ "department": "<Departments group display name>",
113
+ "territory": "<Territories group display name>",
114
+ "territoryAbbr": "<territory_abbreviations lookup>"
115
+ }
116
+ }
117
+ ```
118
+
119
+ That payload is then filtered to the keys in `exposed_user_attributes`, which holds string keys and defaults to `id`, `externalId`, `displayName`, and `photos`. The filter drops any configured key the payload lacks, so listing `title`, `groups`, or the extension URN only works if the source emits them. Legacy builds all of this in `ExternalUser#as_scim`.
120
+
121
+ ##### Scopes
122
+
123
+ Legacy applies the configured scopes inside `#fetch`. The controller does not:
124
+
125
+ ```ruby
126
+ if resource_type == "Users"
127
+ ExternalUser.instance_exec(&Audiences.default_users_scope)
128
+ else
129
+ Group.where(resource_type: resource_type).instance_exec(&Audiences.default_groups_scope)
130
+ end
131
+ ```
132
+
133
+ Both default to `-> { active }`. A replacement source has to apply them itself; skipping them serves inactive records and bypasses whatever membership restriction the application configured.
134
+
135
+ Each proc is `instance_exec`'d against whatever relation the calling code holds, and there is one `default_users_scope` for the whole gem — the context endpoints always run it against `ExternalUser`. A proc may therefore only call scopes that exist on every model it reaches, and a source backed by another model needs equivalent scopes defined there.
136
+
137
+ #### Adding Audiences to a Model
46
138
 
47
139
  A model object can contain multiple audience contexts using the `has_audience` module helper, which is added to ActiveRecord automatically when configured:
48
140
 
@@ -64,7 +156,7 @@ class Survey < ApplicationRecord
64
156
  end
65
157
  ```
66
158
 
67
- ### Listening to Audience Changes
159
+ #### Listening to Audience Changes
68
160
 
69
161
  Audiences allows your app to keep up with mutable groups of people. To react to audience changes, subscribe to audiences related to a certain owner type and handle changes through a block:
70
162
 
@@ -97,21 +189,10 @@ See a working example in our dummy app:
97
189
  - [Job class](../spec/dummy/app/jobs/update_memberships_job.rb)
98
190
  - [Example owning model](../spec/dummy/app/models/example_owner.rb)
99
191
 
100
- ### SCIM Resource Attributes
101
-
102
- Configure which attributes are requested from the SCIM backend for each resource type. `Audiences` includes `id`, `externalId`, and `displayName` by default in every resource type. It also requests `photos.type` and `photos.value` for users by default. To request additional attributes:
103
-
104
- ```ruby
105
- Audiences.configure do |config|
106
- config.resource :Users, attributes: ["name" => %w[givenName familyName formatted]]
107
- config.resource :Groups, attributes: %w[mfaRequired]
108
- end
109
- ```
110
-
111
192
  ## Contributing
112
193
 
113
194
  For more information, see the [development guide](../../docs/development.md).
114
195
 
115
196
  ## License
116
197
 
117
- This gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
198
+ This gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -79,6 +79,24 @@ module Audiences
79
79
  ->(*) { active }
80
80
  end
81
81
 
82
+ # The source that backs the read endpoint (ScimProxyController#get).
83
+ #
84
+ # Defaults to Audiences::ReadSources::Legacy, which reads Audiences' own
85
+ # projection (ExternalUser / Group). Applications can inject an alternative
86
+ # source (e.g. one backed by an external store) as long as it responds to
87
+ # `#fetch(resource_type:, query:, start_index:, count:)` and returns records
88
+ # renderable as the client-facing contract. The controller never references
89
+ # the source's backing models directly, so the read source is swappable
90
+ # without coupling Audiences to any particular provider.
91
+ #
92
+ # I.e.:
93
+ #
94
+ # Audiences.configure do |config|
95
+ # config.read_source = MyExternalReadSource.new
96
+ # end
97
+ #
98
+ config_accessor(:read_source) { Audiences::ReadSources::Legacy.new }
99
+
82
100
  # These are the user attributes that will be exposed in the audiences endpoints.
83
101
  # They're required by the UI to display the user information.
84
102
  #
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audiences
4
+ module ReadSources
5
+ # Default read source.
6
+ #
7
+ # Reads Audiences' own projection (ExternalUser / Group), preserving the exact
8
+ # scoping, search and pagination behavior the read endpoint has always had.
9
+ # Records are returned as-is so the controller's `render json:` uses each
10
+ # model's `as_json` to produce the client-facing contract.
11
+ class Legacy
12
+ def fetch(resource_type:, query:, start_index:, count:)
13
+ scope(resource_type)
14
+ .search(query)
15
+ .offset(start_index)
16
+ .limit(count)
17
+ end
18
+
19
+ private
20
+
21
+ def scope(resource_type)
22
+ if resource_type == "Users"
23
+ ExternalUser.instance_exec(&Audiences.default_users_scope)
24
+ else
25
+ Group.where(resource_type: resource_type).instance_exec(&Audiences.default_groups_scope)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Audiences
4
- VERSION = "3.1.0"
4
+ VERSION = "3.2.0"
5
5
  end
data/lib/audiences.rb CHANGED
@@ -12,6 +12,10 @@ module Audiences
12
12
  autoload :Integrations, "audiences/integrations"
13
13
  autoload :VERSION, "audiences/version"
14
14
 
15
+ module ReadSources
16
+ autoload :Legacy, "audiences/read_sources/legacy"
17
+ end
18
+
15
19
  module_function
16
20
 
17
21
  # Updates the given context
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: audiences
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0
4
+ version: 3.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carlos Palhares
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-23 00:00:00.000000000 Z
11
+ date: 2026-10-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aether_observatory
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '6.0'
33
+ version: '7.2'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '6.0'
40
+ version: '7.2'
41
41
  description: Audiences notify the Rails app when a SCIM backend updates a user affecting
42
42
  matching audiences
43
43
  email:
@@ -100,6 +100,7 @@ files:
100
100
  - lib/audiences/integrations/upsert_users_observer.rb
101
101
  - lib/audiences/model.rb
102
102
  - lib/audiences/notifications.rb
103
+ - lib/audiences/read_sources/legacy.rb
103
104
  - lib/audiences/version.rb
104
105
  - lib/tasks/audiences_tasks.rake
105
106
  homepage: https://github.com/powerhome/audiences
@@ -108,7 +109,7 @@ licenses:
108
109
  metadata:
109
110
  homepage_uri: https://github.com/powerhome/audiences
110
111
  source_code_uri: https://github.com/powerhome/audiences
111
- changelog_uri: https://github.com/powerhome/audiences/blob/main/packages/audiences/docs/CHANGELOG.md
112
+ changelog_uri: https://github.com/powerhome/audiences/blob/main/audiences/docs/CHANGELOG.md
112
113
  rubygems_mfa_required: 'true'
113
114
  post_install_message:
114
115
  rdoc_options: []
@@ -118,7 +119,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
118
119
  requirements:
119
120
  - - ">="
120
121
  - !ruby/object:Gem::Version
121
- version: '2.7'
122
+ version: '3.1'
122
123
  required_rubygems_version: !ruby/object:Gem::Requirement
123
124
  requirements:
124
125
  - - ">="