audiences 1.6.1 → 2.0.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/audiences-ujs.js +204 -1318
  3. data/app/controllers/audiences/contexts_controller.rb +4 -15
  4. data/app/controllers/audiences/scim_proxy_controller.rb +9 -11
  5. data/app/models/audiences/context.rb +41 -4
  6. data/app/models/audiences/context_extra_user.rb +8 -0
  7. data/app/models/audiences/criterion.rb +42 -8
  8. data/app/models/audiences/criterion_group.rb +8 -0
  9. data/app/models/audiences/external_user.rb +92 -14
  10. data/app/models/audiences/group.rb +27 -0
  11. data/app/models/audiences/group_membership.rb +14 -0
  12. data/db/migrate/20250520181229_create_audiences_groups.rb +17 -0
  13. data/db/migrate/20250521173148_add_scim_id_to_audiences_external_users.rb +8 -0
  14. data/db/migrate/20250521182852_create_audiences_group_memberships.rb +12 -0
  15. data/db/migrate/20250527203948_populate_external_users_scim_id.rb +15 -0
  16. data/db/migrate/20250528130640_add_display_name_to_audiences_external_users.rb +8 -0
  17. data/db/migrate/20250618184332_add_active_flag_to_external_users_and_groups.rb +8 -0
  18. data/db/migrate/20250620005528_create_audiences_criterion_groups.rb +12 -0
  19. data/db/migrate/20250620005800_rename_audiences_criterion_groups_to_groups_json.rb +7 -0
  20. data/db/migrate/20250623131202_move_group_criterion_to_criterion_groups.rb +27 -0
  21. data/db/migrate/20250624171247_create_audiences_context_extra_users.rb +12 -0
  22. data/db/migrate/20250624171706_rename_audiences_context_extra_users_to_extra_users_json.rb +7 -0
  23. data/db/migrate/20250701173946_move_extra_users_to_context_extra_users.rb +21 -0
  24. data/docs/CHANGELOG.md +17 -0
  25. data/docs/README.md +0 -11
  26. data/lib/audiences/configuration.rb +71 -45
  27. data/lib/audiences/editor_helper.rb +4 -1
  28. data/lib/audiences/engine.rb +19 -1
  29. data/lib/audiences/model.rb +9 -8
  30. data/lib/audiences/notifications.rb +4 -2
  31. data/lib/audiences/scim/field_mapping.rb +49 -0
  32. data/lib/audiences/scim/observer_base.rb +8 -0
  33. data/lib/audiences/scim/patch_groups_observer.rb +51 -0
  34. data/lib/audiences/scim/patch_op.rb +42 -0
  35. data/lib/audiences/scim/patch_users_observer.rb +38 -0
  36. data/lib/audiences/scim/scim_data.rb +31 -0
  37. data/lib/audiences/scim/upsert_groups_observer.rb +40 -0
  38. data/lib/audiences/scim/upsert_users_observer.rb +48 -0
  39. data/lib/audiences/scim.rb +9 -15
  40. data/lib/audiences/version.rb +1 -1
  41. data/lib/audiences.rb +5 -5
  42. metadata +40 -9
  43. data/app/jobs/audiences/application_job.rb +0 -6
  44. data/app/models/audiences/context_users.rb +0 -32
  45. data/app/models/audiences/criterion_users.rb +0 -28
  46. data/lib/audiences/railtie.rb +0 -12
  47. data/lib/audiences/scim/client.rb +0 -29
  48. data/lib/audiences/scim/resource.rb +0 -48
  49. data/lib/audiences/scim/resources_query.rb +0 -66
@@ -3,11 +3,11 @@
3
3
  module Audiences
4
4
  class ContextsController < ApplicationController
5
5
  def show
6
- render_context Audiences::Context.load(params.require(:key))
6
+ render json: Audiences::Context.load(params.require(:key))
7
7
  end
8
8
 
9
9
  def update
10
- render_context Audiences.update(params.require(:key), **context_params)
10
+ render json: Audiences.update(params.require(:key), **context_params)
11
11
  end
12
12
 
13
13
  def users
@@ -17,7 +17,7 @@ module Audiences
17
17
  limit: params[:limit],
18
18
  offset: params[:offset])
19
19
 
20
- render json: search, only: Audiences.exposed_user_attributes
20
+ render json: search
21
21
  end
22
22
 
23
23
  private
@@ -32,22 +32,11 @@ module Audiences
32
32
  @current_criterion ||= current_context.criteria.find(params[:criterion_id])
33
33
  end
34
34
 
35
- def render_context(context)
36
- json_setting = {
37
- only: %i[match_all],
38
- methods: %i[count],
39
- include: { criteria: { only: %i[id groups], methods: %i[count] } },
40
- }
41
- extra_users = context.extra_users.as_json(only: Audiences.exposed_user_attributes)
42
-
43
- render json: { extra_users: extra_users, **context.as_json(json_setting) }
44
- end
45
-
46
35
  def context_params
47
36
  params.permit(
48
37
  :match_all,
49
38
  criteria: [groups: {}],
50
- extra_users: %i[externalId]
39
+ extra_users: %i[id externalId]
51
40
  ).to_h.symbolize_keys
52
41
  end
53
42
  end
@@ -3,23 +3,21 @@
3
3
  module Audiences
4
4
  class ScimProxyController < ApplicationController
5
5
  def get
6
- resources = Audiences::Scim.resource(params[:scim_path].to_sym)
7
- .query(
8
- filter: filter_param,
9
- startIndex: params[:startIndex], count: params[:count],
10
- attributes: Audiences.exposed_user_attributes.join(",")
11
- )
6
+ resources = scope.search(params[:query])
7
+ .offset(params[:startIndex])
8
+ .limit(params[:count])
12
9
 
13
- render json: resources, except: %w[schemas meta]
10
+ render json: resources
14
11
  end
15
12
 
16
13
  private
17
14
 
18
- def filter_param
19
- if params[:query]
20
- "displayName co \"#{params[:query]}\""
15
+ def scope
16
+ if params[:scim_path].eql?("Users")
17
+ Audiences::ExternalUser.instance_exec(&Audiences.default_users_scope)
21
18
  else
22
- params[:filter]
19
+ Audiences::Group.where(resource_type: params[:scim_path])
20
+ .instance_exec(&Audiences.default_groups_scope)
23
21
  end
24
22
  end
25
23
  end
@@ -7,22 +7,59 @@ module Audiences
7
7
  #
8
8
  class Context < ApplicationRecord
9
9
  include Locating
10
- include ::Audiences::MembershipGroup
11
10
 
12
11
  belongs_to :owner, polymorphic: true
13
12
  has_many :criteria, class_name: "Audiences::Criterion",
14
13
  autosave: true,
15
14
  dependent: :destroy
16
15
 
16
+ has_many :context_extra_users, class_name: "Audiences::ContextExtraUser"
17
+ has_many :extra_users, class_name: "Audiences::ExternalUser",
18
+ through: :context_extra_users,
19
+ source: :external_user
20
+
21
+ scope :relevant_to, ->(group) do
22
+ joins(:criteria).merge(Criterion.relevant_to(group))
23
+ end
24
+
17
25
  before_save if: :match_all do
18
26
  self.criteria = []
19
27
  self.extra_users = []
20
28
  end
21
29
 
22
- def refresh_users!
23
- criteria.each(&:refresh_users!)
24
- update!(users: ContextUsers.new(self).to_a)
30
+ after_commit :notify_subscriptions, on: :update
31
+
32
+ def users
33
+ matching_external_users.instance_exec(&Audiences.default_users_scope)
34
+ end
35
+
36
+ delegate :count, to: :users
37
+
38
+ def as_json(...)
39
+ {
40
+ match_all: match_all,
41
+ count: count,
42
+ extra_users: extra_users.instance_exec(&Audiences.default_users_scope),
43
+ criteria: criteria,
44
+ }.as_json(...)
45
+ end
46
+
47
+ private
48
+
49
+ def notify_subscriptions
25
50
  Notifications.publish(self)
26
51
  end
52
+
53
+ def matching_external_users
54
+ match_all ? ExternalUser.all : matching_extra_users.or(matching_criteria)
55
+ end
56
+
57
+ def matching_extra_users
58
+ ExternalUser.where(id: extra_users.select(:id))
59
+ end
60
+
61
+ def matching_criteria
62
+ criteria.any? ? ExternalUser.matching_any(*criteria) : ExternalUser.none
63
+ end
27
64
  end
28
65
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audiences
4
+ class ContextExtraUser < ApplicationRecord
5
+ belongs_to :context
6
+ belongs_to :external_user
7
+ end
8
+ end
@@ -2,19 +2,53 @@
2
2
 
3
3
  module Audiences
4
4
  class Criterion < ApplicationRecord
5
- include ::Audiences::MembershipGroup
6
-
7
5
  belongs_to :context, class_name: "Audiences::Context"
6
+ validates :groups, presence: true
7
+
8
+ has_many :criterion_groups, autosave: true, dependent: :destroy
9
+ has_many :groups, through: :criterion_groups
10
+
11
+ scope :relevant_to, ->(group) do
12
+ joins(:criterion_groups).where(criterion_groups: { group: group })
13
+ end
8
14
 
15
+ # Maps an array of attribute hashes to Criterion objects.
16
+ #
17
+ # Each attribute hash should have a :groups key, whose value is a hash
18
+ # mapping resource types (e.g., "Departments", "Territories") to arrays of group hashes,
19
+ # each containing an :id key (the SCIM group ID).
20
+ #
21
+ # Example input:
22
+ #
23
+ # [
24
+ # { groups: { Departments: [{ id: "1" }] } },
25
+ # { groups: { Territories: [{ id: "2" }], Departments: [{ id: "3" }] } },
26
+ # ]
27
+ #
28
+ # Returns an array of new Criterion objects, each initialized with the corresponding group criterion.
29
+ #
30
+ # @param [Array<Hash>] criteria Array of attribute hashes describing groups
31
+ # @return [Array<Criterion>] Array of Criterion objects
9
32
  def self.map(criteria)
10
- Array(criteria).map { new(_1) }
33
+ Array(criteria).map do |attrs|
34
+ groups = attrs["groups"]&.flat_map do |resource_type, scim_groups|
35
+ Audiences::Group.from_scim(resource_type, *scim_groups).to_a
36
+ end
37
+ new(groups: groups)
38
+ end
11
39
  end
12
40
 
13
- def refresh_users!
14
- update!(
15
- users: CriterionUsers.new(groups || {}).to_a,
16
- refreshed_at: Time.current
17
- )
41
+ def as_json(...)
42
+ groups = self.groups.group_by(&:resource_type)
43
+
44
+ { id: id, count: count, groups: groups }.as_json(...)
45
+ end
46
+
47
+ def users
48
+ Audiences::ExternalUser.matching(self)
49
+ .instance_exec(&Audiences.default_users_scope)
18
50
  end
51
+
52
+ delegate :count, to: :users
19
53
  end
20
54
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audiences
4
+ class CriterionGroup < ApplicationRecord
5
+ belongs_to :group
6
+ belongs_to :criterion
7
+ end
8
+ end
@@ -2,6 +2,12 @@
2
2
 
3
3
  module Audiences
4
4
  class ExternalUser < ApplicationRecord
5
+ has_many :group_memberships, dependent: :destroy
6
+ has_many :groups, through: :group_memberships, dependent: :destroy
7
+
8
+ has_many :context_extra_users, class_name: "Audiences::ContextExtraUser", dependent: :destroy
9
+ has_many :contexts, through: :context_extra_users, source: :context
10
+
5
11
  if Audiences.config.identity_class
6
12
  belongs_to :identity, class_name: Audiences.config.identity_class, # rubocop:disable Rails/ReflectionClassName
7
13
  primary_key: Audiences.config.identity_key,
@@ -10,28 +16,100 @@ module Audiences
10
16
  inverse_of: false
11
17
  end
12
18
 
13
- def self.fetch(external_ids, count: 100)
14
- return [] unless external_ids.any?
15
-
16
- Array(external_ids).in_groups_of(count, false).flat_map do |ids|
17
- filter = Array(ids).map { "externalId eq #{_1}" }.join(" OR ")
18
- Audiences::Scim.resource(:Users).all(count: count, filter: filter).to_a
19
+ after_commit if: :active_previously_changed?, on: %i[create update destroy] do
20
+ group_contexts = groups.flat_map do |group|
21
+ Audiences::Context.relevant_to(group).to_a
19
22
  end
23
+ match_all_contexts = Audiences::Context.where(match_all: true)
24
+
25
+ Audiences::Notifications.publish(*[*contexts, *group_contexts, *match_all_contexts].uniq)
26
+ end
27
+
28
+ scope :active, -> { where(active: true) }
29
+
30
+ scope :members_of, ->(groups) do
31
+ where(id: Audiences::GroupMembership.where(group: groups).select(:external_user_id))
32
+ end
33
+
34
+ scope :search, ->(display_name) do
35
+ where(arel_table[:display_name].matches("%#{display_name}%"))
20
36
  end
21
37
 
22
- def self.wrap(resources)
23
- return [] unless resources&.any?
38
+ scope :from_scim, ->(*scim_json) do
39
+ where(scim_id: scim_json.pluck("id").compact)
40
+ .or(where(user_id: scim_json.pluck("externalId").compact))
41
+ end
42
+
43
+ scope :matching, ->(criterion) do
44
+ return none if criterion.groups.empty?
24
45
 
25
- attrs = resources.map do |data|
26
- { user_id: data["externalId"], data: data, created_at: Time.current, updated_at: Time.current }
46
+ criterion.groups
47
+ .group_by(&:resource_type)
48
+ .values
49
+ .reduce(self) do |scope, groups|
50
+ scope.members_of(groups)
27
51
  end
28
- unique_by = :user_id if connection.supports_insert_conflict_target?
29
- upsert_all(attrs, unique_by: unique_by) # rubocop:disable Rails/SkipsModelValidations
30
- where(user_id: attrs.pluck(:user_id))
52
+ end
53
+
54
+ scope :matching_any, ->(first, *others) do
55
+ others.reduce(matching(first)) do |scope, criterion|
56
+ scope.or matching(criterion)
57
+ end
58
+ end
59
+
60
+ def picture_urls = [picture_url]
61
+
62
+ def picture_urls=(urls)
63
+ self.picture_url = urls&.first
31
64
  end
32
65
 
33
66
  def as_json(...)
34
- data.as_json(...)
67
+ as_scim.slice(*Audiences.exposed_user_attributes)
35
68
  end
69
+
70
+ def as_scim(...)
71
+ (data || {}).merge(groups_as_scim)
72
+ end
73
+
74
+ def groups_as_scim
75
+ names = groups.reduce({}) { |nam, group| nam.merge(group.resource_type => group.display_name) }
76
+
77
+ {
78
+ "groups" => groups.map { |g| { "value" => g.scim_id, "display" => g.display_name } },
79
+ "title" => names["Titles"],
80
+ "urn:ietf:params:scim:schemas:extension:authservice:2.0:User" => {
81
+ "role" => names["Roles"], "department" => names["Departments"],
82
+ "territory" => names["Territories"], "territoryAbbr" => TERRITORY_ABBRS[names["Territories"]]
83
+ },
84
+ }
85
+ end
86
+
87
+ TERRITORY_ABBRS = {
88
+ "Philadelphia" => "PHL",
89
+ "New Jersey" => "NJ",
90
+ "Maryland" => "MD",
91
+ "Connecticut" => "CT",
92
+ "Long Island" => "LI",
93
+ "Boston" => "BOS",
94
+ "Atlanta" => "ATL",
95
+ "Chicago" => "CHI",
96
+ "Detroit" => "DET",
97
+ "Houston" => "HOU",
98
+ "Dallas" => "DAL",
99
+ "Denver" => "DEN",
100
+ "Tampa" => "TPA",
101
+ "Austin" => "AUS",
102
+ "Charlotte" => "CLT",
103
+ "Nashville" => "NSH",
104
+ "Phoenix" => "PHX",
105
+ "Pittsburgh" => "PIT",
106
+ "San Antonio" => "SAO",
107
+ "Fort Lauderdale" => "FLL",
108
+ "Las Vegas" => "LVS",
109
+ "Orlando" => "ORL",
110
+ "Cincinnati" => "CIN",
111
+ "Columbus" => "CLB",
112
+ "Jacksonville" => "JAX",
113
+ }.freeze
36
114
  end
37
115
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audiences
4
+ class Group < ApplicationRecord
5
+ has_many :group_memberships, dependent: :destroy
6
+ has_many :external_users, through: :group_memberships, dependent: :destroy
7
+
8
+ validates :display_name, presence: true
9
+ validates :external_id, presence: true
10
+ validates :scim_id, presence: true
11
+
12
+ scope :active, -> { where(active: true) }
13
+
14
+ scope :search, ->(display_name) do
15
+ where(arel_table[:display_name].matches("%#{display_name}%"))
16
+ end
17
+
18
+ scope :from_scim, ->(resource_type, *scim_json) do
19
+ where(scim_id: scim_json.pluck("id"))
20
+ .or(where(resource_type: resource_type, external_id: scim_json.pluck("externalId")))
21
+ end
22
+
23
+ def as_json(...)
24
+ { "id" => scim_id, "externalId" => external_id, "displayName" => display_name }.as_json(...)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audiences
4
+ class GroupMembership < ApplicationRecord
5
+ belongs_to :external_user
6
+ belongs_to :group
7
+
8
+ after_commit on: %i[create destroy] do
9
+ relevant_groups = Audiences::Context.relevant_to(group)
10
+
11
+ Audiences::Notifications.publish(*relevant_groups.to_a)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateAudiencesGroups < ActiveRecord::Migration[6.1]
4
+ def change
5
+ create_table :audiences_groups do |t|
6
+ t.string :external_id
7
+ t.string :scim_id
8
+ t.string :display_name
9
+ t.string :resource_type
10
+
11
+ t.timestamps
12
+
13
+ t.index %i[resource_type external_id], unique: true
14
+ t.index %i[resource_type scim_id], unique: true
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddScimIdToAudiencesExternalUsers < ActiveRecord::Migration[6.1]
4
+ def change
5
+ add_column :audiences_external_users, :scim_id, :string
6
+ add_index :audiences_external_users, :scim_id, unique: true
7
+ end
8
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateAudiencesGroupMemberships < ActiveRecord::Migration[6.1]
4
+ def change
5
+ create_table :audiences_group_memberships do |t|
6
+ t.belongs_to :external_user, null: false, foreign_key: false, table: :audiences_external_users
7
+ t.belongs_to :group, null: false, foreign_key: false, table: :audiences_external_users
8
+
9
+ t.timestamps
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PopulateExternalUsersScimId < ActiveRecord::Migration[6.1]
4
+ def down; end
5
+
6
+ def up
7
+ Audiences::ExternalUser.unscoped.find_each do |user|
8
+ user.update!(
9
+ scim_id: user.data&.fetch("id", nil),
10
+ display_name: user.data&.fetch("displayName", nil),
11
+ picture_urls: user.data&.fetch("photos", [])&.pluck("value")
12
+ )
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddDisplayNameToAudiencesExternalUsers < ActiveRecord::Migration[6.1]
4
+ def change
5
+ add_column :audiences_external_users, :display_name, :string
6
+ add_column :audiences_external_users, :picture_url, :string
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddActiveFlagToExternalUsersAndGroups < ActiveRecord::Migration[6.1]
4
+ def change
5
+ add_column :audiences_external_users, :active, :boolean, default: true, null: false
6
+ add_column :audiences_groups, :active, :boolean, default: true, null: false
7
+ end
8
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateAudiencesCriterionGroups < ActiveRecord::Migration[6.1]
4
+ def change
5
+ create_table :audiences_criterion_groups do |t|
6
+ t.references :criterion, foreign_key: false
7
+ t.references :group, foreign_key: false
8
+
9
+ t.timestamps
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RenameAudiencesCriterionGroupsToGroupsJson < ActiveRecord::Migration[6.1]
4
+ def change
5
+ rename_column :audiences_criterions, :groups, :groups_json
6
+ end
7
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MoveGroupCriterionToCriterionGroups < ActiveRecord::Migration[6.1]
4
+ def up
5
+ Audiences::Criterion.find_each do |criterion|
6
+ next if criterion.groups_json.blank?
7
+
8
+ groups = criterion.groups_json.values.flat_map do |scim_groups|
9
+ scim_groups.pluck("id")
10
+ end
11
+
12
+ criterion.update!(groups: Audiences::Group.where(scim_id: groups))
13
+ end
14
+ end
15
+
16
+ def down
17
+ Audiences::Criterion.find_each do |criterion|
18
+ next if criterion.groups.blank?
19
+
20
+ groups_json = criterion.groups.group_by(&:resource_type).transform_values do |groups|
21
+ groups.map(&:as_json)
22
+ end
23
+
24
+ criterion.update!(groups_json: groups_json)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateAudiencesContextExtraUsers < ActiveRecord::Migration[6.1]
4
+ def change
5
+ create_table :audiences_context_extra_users do |t|
6
+ t.references :external_user, foreign_key: false
7
+ t.references :context, foreign_key: false
8
+
9
+ t.timestamps
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RenameAudiencesContextExtraUsersToExtraUsersJson < ActiveRecord::Migration[6.1]
4
+ def change
5
+ rename_column :audiences_contexts, :extra_users, :extra_users_json
6
+ end
7
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MoveExtraUsersToContextExtraUsers < ActiveRecord::Migration[6.1]
4
+ def up
5
+ Audiences::Context.find_each do |context|
6
+ next if context.extra_users_json.blank?
7
+
8
+ users = Audiences::ExternalUser.from_scim(*context.extra_users_json)
9
+
10
+ context.update!(extra_users: users)
11
+ end
12
+ end
13
+
14
+ def down
15
+ Audiences::Context.find_each do |context|
16
+ next if context.extra_users.blank?
17
+
18
+ context.update!(extra_users_json: context.extra_users.map(&:as_json))
19
+ end
20
+ end
21
+ end
data/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Unreleased
2
2
 
3
+ # Version 2.0 (2025-08-25)
4
+
5
+ The all new 2.0 release inverts the SCIM logic, where now Audiences no longer pulls data from SCIM, but rather it will receive and cache SCIM data, allowing for an in database calculation of audiences. This improved process allows audiences to be more independent from SCIM, while still compatible with the protocol.
6
+
7
+ - Update ExternalUser on membership changeas [#541](https://github.com/powerhome/audiences/pull/541)
8
+ - Filter extra_users in context JSON [#540](https://github.com/powerhome/audiences/pull/540)
9
+ - Introduce helpful scopes [#538](https://github.com/powerhome/audiences/pull/538)
10
+ - Allow inactive groups in criteria [#537](https://github.com/powerhome/audiences/pull/537)
11
+ - Allow creating external users and groups with internal and external ids [#536](https://github.com/powerhome/audiences/pull/536)
12
+ - Reactive audience recalculation [#535](https://github.com/powerhome/audiences/pull/535)
13
+ - match group criteria [#534](https://github.com/powerhome/audiences/pull/534)
14
+ - Configurable default user and groups scope [#533](https://github.com/powerhome/audiences/pull/533)
15
+ - Default authentication to reject requests [#532](https://github.com/powerhome/audiences/pull/532)
16
+ - scim proxy with local data [#531](https://github.com/powerhome/audiences/pull/531)
17
+ - Local audience calculations [#530](https://github.com/powerhome/audiences/pull/530)
18
+ - Local user and group info updated with TwoPercent [#519](https://github.com/powerhome/audiences/pull/519)
19
+
3
20
  # Version 1.6 (2025-03-19)
4
21
 
5
22
  - Prepackaged audiences-react in UJS [#510](https://github.com/powerhome/audiences/pull/510)
data/docs/README.md CHANGED
@@ -42,17 +42,6 @@ For more details, refer to [editor_helper](../lib/audiences/editor_helper.rb).
42
42
 
43
43
  ### Configuring Audiences
44
44
 
45
- The `Audience.config.scim` should point to the SCIM endpoint. Configure the endpoint and the credentials/headers as follows:
46
-
47
- ```ruby
48
- Audiences.configure do |config|
49
- config.scim = {
50
- uri: ENV.fetch("SCIM_V2_API"),
51
- headers: { "Authorization" => "Bearer #{ENV.fetch('SCIM_V2_TOKEN')}" }
52
- }
53
- end
54
- ```
55
-
56
45
  ### Adding Audiences to a Model
57
46
 
58
47
  A model object can contain multiple audience contexts using the `has_audience` module helper, which is added to ActiveRecord automatically when configured: