audiences 1.6.2 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/audiences-ujs.js +191 -131
  3. data/app/controllers/audiences/contexts_controller.rb +4 -15
  4. data/app/controllers/audiences/scim_proxy_controller.rb +9 -11
  5. data/app/events/audiences/application_event.rb +7 -0
  6. data/app/events/audiences/persisted_resource_event.rb +10 -0
  7. data/app/models/audiences/context.rb +41 -4
  8. data/app/models/audiences/context_extra_user.rb +8 -0
  9. data/app/models/audiences/criterion.rb +42 -8
  10. data/app/models/audiences/criterion_group.rb +8 -0
  11. data/app/models/audiences/external_user.rb +77 -14
  12. data/app/models/audiences/group.rb +27 -0
  13. data/app/models/audiences/group_membership.rb +16 -0
  14. data/db/migrate/20250520181229_create_audiences_groups.rb +17 -0
  15. data/db/migrate/20250521173148_add_scim_id_to_audiences_external_users.rb +8 -0
  16. data/db/migrate/20250521182852_create_audiences_group_memberships.rb +12 -0
  17. data/db/migrate/20250527203948_populate_external_users_scim_id.rb +15 -0
  18. data/db/migrate/20250528130640_add_display_name_to_audiences_external_users.rb +8 -0
  19. data/db/migrate/20250618184332_add_active_flag_to_external_users_and_groups.rb +8 -0
  20. data/db/migrate/20250620005528_create_audiences_criterion_groups.rb +12 -0
  21. data/db/migrate/20250620005800_rename_audiences_criterion_groups_to_groups_json.rb +7 -0
  22. data/db/migrate/20250623131202_move_group_criterion_to_criterion_groups.rb +27 -0
  23. data/db/migrate/20250624171247_create_audiences_context_extra_users.rb +12 -0
  24. data/db/migrate/20250624171706_rename_audiences_context_extra_users_to_extra_users_json.rb +7 -0
  25. data/db/migrate/20250701173946_move_extra_users_to_context_extra_users.rb +21 -0
  26. data/db/migrate/20260506150000_add_unique_index_to_group_memberships.rb +10 -0
  27. data/docs/CHANGELOG.md +21 -0
  28. data/docs/README.md +0 -11
  29. data/lib/audiences/configuration.rb +88 -45
  30. data/lib/audiences/editor_helper.rb +4 -1
  31. data/lib/audiences/engine.rb +19 -1
  32. data/lib/audiences/model.rb +9 -8
  33. data/lib/audiences/notifications.rb +4 -2
  34. data/lib/audiences/scim/field_mapping.rb +77 -0
  35. data/lib/audiences/scim/observer_base.rb +8 -0
  36. data/lib/audiences/scim/patch_groups_observer.rb +51 -0
  37. data/lib/audiences/scim/patch_op.rb +42 -0
  38. data/lib/audiences/scim/patch_users_observer.rb +38 -0
  39. data/lib/audiences/scim/scim_data.rb +31 -0
  40. data/lib/audiences/scim/upsert_groups_observer.rb +41 -0
  41. data/lib/audiences/scim/upsert_users_observer.rb +62 -0
  42. data/lib/audiences/scim.rb +9 -15
  43. data/lib/audiences/version.rb +1 -1
  44. data/lib/audiences.rb +5 -5
  45. metadata +43 -9
  46. data/app/jobs/audiences/application_job.rb +0 -6
  47. data/app/models/audiences/context_users.rb +0 -32
  48. data/app/models/audiences/criterion_users.rb +0 -28
  49. data/lib/audiences/railtie.rb +0 -12
  50. data/lib/audiences/scim/client.rb +0 -29
  51. data/lib/audiences/scim/resource.rb +0 -48
  52. data/lib/audiences/scim/resources_query.rb +0 -66
@@ -5,6 +5,80 @@ module Audiences
5
5
 
6
6
  # Configuration options
7
7
 
8
+ # Sync groups and users with TwoPercent
9
+ config_accessor(:logger)
10
+
11
+ # Sync groups and users with TwoPercent
12
+ config_accessor(:observe_scim) { true }
13
+
14
+ # Group types that can form an audience
15
+ config_accessor :group_types do
16
+ %w[Groups]
17
+ end
18
+
19
+ # Group types that must be present in a user provisioning event
20
+ config_accessor :required_group_types do
21
+ []
22
+ end
23
+
24
+ DEFAULT_TERRITORY_ABBREVIATIONS = {
25
+ "Philadelphia" => "PHL", "New Jersey" => "NJ", "Maryland" => "MD", "Connecticut" => "CT",
26
+ "Long Island" => "LI", "Boston" => "BOS", "Atlanta" => "ATL", "Chicago" => "CHI",
27
+ "Detroit" => "DET", "Houston" => "HOU", "Dallas" => "DAL", "Denver" => "DEN", "Tampa" => "TPA",
28
+ "Austin" => "AUS", "Charlotte" => "CLT", "Nashville" => "NSH", "Phoenix" => "PHX",
29
+ "Pittsburgh" => "PIT", "San Antonio" => "SAO", "Fort Lauderdale" => "FLL", "Las Vegas" => "LVS",
30
+ "Orlando" => "ORL", "Cincinnati" => "CIN", "Columbus" => "CLB", "Jacksonville" => "JAX",
31
+ "Oklahoma City" => "OKC", "Raleigh" => "RLD", "Cleveland" => "CLE"
32
+ }.freeze
33
+
34
+ config_accessor(:territory_abbreviations) { DEFAULT_TERRITORY_ABBREVIATIONS }
35
+
36
+ # Defines a default scope for users, so the users that are part of an audience can
37
+ # be filtered (i.e.: only active, only users in a specific group, etc)
38
+ #
39
+ # By default, only active users are listed.
40
+ #
41
+ # I.e.:
42
+ #
43
+ # # Allowing inactive users
44
+ # Audiences.configure do |config|
45
+ # config.default_users_scope = -> { all }
46
+ # end
47
+ #
48
+ # # Accepting only users in certain groups
49
+ # Audiences.configure do |config|
50
+ # config.default_users_scope = -> { includes(:groups).merge(Audiences::Group.where(scim_id: ALLOWED_GROUPS)) }
51
+ # end
52
+ #
53
+ # This configuration defaults to `-> { active }`
54
+ #
55
+ config_accessor :default_users_scope do
56
+ ->(*) { active }
57
+ end
58
+
59
+ # Defines a default scope for groups, so the groups that are part of an audience can
60
+ # be filtered (i.e.: only active, only specific groups, etc)
61
+ #
62
+ # By default, only active groups are listed.
63
+ #
64
+ # I.e.:
65
+ #
66
+ # # Allowing inactive groups
67
+ # Audiences.configure do |config|
68
+ # config.default_groups_scope = -> { all }
69
+ # end
70
+ #
71
+ # # Accepting only groups in certain groups
72
+ # Audiences.configure do |config|
73
+ # config.default_groups_scope = -> { where(scim_id: ALLOWED_GROUPS) }
74
+ # end
75
+ #
76
+ # This configuration defaults to `-> { active }`
77
+ #
78
+ config_accessor :default_groups_scope do
79
+ ->(*) { active }
80
+ end
81
+
8
82
  # These are the user attributes that will be exposed in the audiences endpoints.
9
83
  # They're required by the UI to display the user information.
10
84
  #
@@ -37,7 +111,20 @@ module Audiences
37
111
  # end
38
112
  #
39
113
  config_accessor :authenticate do
40
- ->(*) { true }
114
+ ->(*) do
115
+ Audiences.logger.warn(<<~MESSAGE)
116
+ Audiences authenticate is currently configured using a default and is blocking authenticaiton.
117
+
118
+ To make this warning go away provide a configuration for `Audiences.config.authenticate`.
119
+
120
+ The value should:
121
+ 1. Be callable like a Proc.
122
+ 2. Return true when the request is permitted.
123
+ 3. Return false when the request is not permitted.
124
+ MESSAGE
125
+
126
+ false
127
+ end
41
128
  end
42
129
 
43
130
  #
@@ -51,50 +138,6 @@ module Audiences
51
138
  #
52
139
  config_accessor(:identity_key) { :id }
53
140
 
54
- #
55
- # SCIM service configurations. This should be a Hash containint, at least, the URI.
56
- #
57
- # I.e.:
58
- #
59
- # Audiences.configure do |config|
60
- # config.scim = { uri: "http://localhost/api/scim" }
61
- # end
62
- #
63
- # It can also contain HTTP headers, such as "Authorization":
64
- #
65
- # I.e.:
66
- #
67
- # Audiences.configure do |config|
68
- # config.scim = {
69
- # uri: "http://localhost/api/scim",
70
- # headers: { "Authorization" => "Bearer auth-token" }
71
- # }
72
- # end
73
- #
74
- config_accessor :scim
75
-
76
- #
77
- # Resources defaults. Change this configuration via the `resource` helper.
78
- # This configuration lists the current Audiences accessible resource defaults,
79
- # and defaults to Users only. To add other resource types for criteria building.
80
- #
81
- # @see `resource`.
82
- #
83
- config_accessor :resources do
84
- { Users: Scim::Resource.new(type: :Users, attributes: ["active", { "photos" => %w[type value] }],
85
- filter: "active eq true") }
86
- end
87
-
88
- #
89
- # Configures a resource default.
90
- #
91
- # @param type [Symbol] the resource type in plural, as in scim (i.e.: :Users)
92
- # @param attributes [String] the list of attributes to fetch for the resource (i.e.: "id,externalId,displayName")
93
- # @see [Audiences::Scim::Resource]
94
- def config.resource(type, **kwargs)
95
- resources[type] = Scim::Resource.new(type: type, **kwargs)
96
- end
97
-
98
141
  #
99
142
  # Notifications configurations.
100
143
  # Within this block, you should be able to easily register job classes to execute as
@@ -3,12 +3,15 @@
3
3
  module Audiences
4
4
  module EditorHelper
5
5
  def render_audiences_editor(context, html_class: "audiences-editor",
6
- uri: Audiences::Engine.routes.url_helpers.root_path)
6
+ uri: Audiences::Engine.routes.url_helpers.root_path,
7
+ allow_match_all: true, allow_individuals: true)
7
8
  content_tag(:div, "",
8
9
  data: {
9
10
  react_class: "AudiencesEditor",
10
11
  audiences_uri: uri,
11
12
  audiences_context: context.signed_key,
13
+ allow_match_all: allow_match_all,
14
+ allow_individuals: allow_individuals,
12
15
  },
13
16
  class: html_class)
14
17
  end
@@ -10,8 +10,17 @@ module Audiences
10
10
  class Engine < ::Rails::Engine
11
11
  isolate_namespace Audiences
12
12
 
13
- initializer "audiences.assets.precompile" do |app|
13
+ initializer "audiences.editor_helper" do |app|
14
14
  app.config.assets.precompile += %w[audiences-ujs.js] if app.config.respond_to?(:assets)
15
+
16
+ ActiveSupport.on_load(:action_view) do
17
+ require "audiences/editor_helper"
18
+ include Audiences::EditorHelper
19
+ end
20
+ end
21
+
22
+ initializer "audiences.logger" do
23
+ Audiences.config.logger ||= Rails.logger.tagged("Audiences")
15
24
  end
16
25
 
17
26
  initializer "audiences.model" do
@@ -21,5 +30,14 @@ module Audiences
21
30
  end
22
31
  end
23
32
  end
33
+
34
+ initializer "audiences.observers" do
35
+ if Audiences.config.observe_scim
36
+ Audiences::Scim::UpsertUsersObserver.start
37
+ Audiences::Scim::UpsertGroupsObserver.start
38
+ Audiences::Scim::PatchGroupsObserver.start
39
+ Audiences::Scim::PatchUsersObserver.start
40
+ end
41
+ end
24
42
  end
25
43
  end
@@ -10,25 +10,26 @@ module Audiences
10
10
  #
11
11
  # @param name [Symbol,String] the member relationship name
12
12
  #
13
- # rubocop:disable Naming/PredicateName,Metrics/MethodLength,Metrics/AbcSize
13
+ # rubocop:disable Naming/PredicateName,Metrics/MethodLength
14
14
  def has_audience(name)
15
15
  has_one :"#{name}_context", -> { where(relation: name) },
16
16
  as: :owner, dependent: :destroy,
17
17
  class_name: "Audiences::Context"
18
- has_many :"#{name}_external_users",
19
- through: :"#{name}_context", source: :users,
20
- class_name: "Audiences::ExternalUser"
21
- has_many name, -> { readonly }, through: :"#{name}_external_users", source: :identity
22
18
 
23
- scope :"with_#{name}", -> { includes(name) }
19
+ delegate :users, to: :"#{name}_context", prefix: :"#{name}_external"
20
+
21
+ define_method(name) do
22
+ send(:"#{name}_context").users.includes(:identity)
23
+ .map(&:identity)
24
+ end
25
+
24
26
  scope :"with_#{name}_context", -> { includes(:"#{name}_context") }
25
- scope :"with_#{name}_external_users", -> { includes(:"#{name}_external_users") }
26
27
 
27
28
  after_initialize if: :new_record? do
28
29
  association(:"#{name}_context").build
29
30
  end
30
31
  end
31
- # rubocop:enable Naming/PredicateName,Metrics/MethodLength,Metrics/AbcSize
32
+ # rubocop:enable Naming/PredicateName,Metrics/MethodLength
32
33
  end
33
34
  end
34
35
  end
@@ -29,8 +29,10 @@ module Audiences
29
29
  #
30
30
  # @param context [Audiences::Context] updated context
31
31
  #
32
- def publish(context)
33
- subscriptions[context.owner.class]&.call(context)
32
+ def publish(*contexts)
33
+ contexts.each do |context|
34
+ subscriptions[context.owner.class]&.call(context)
35
+ end
34
36
  end
35
37
  end
36
38
  end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audiences
4
+ module Scim
5
+ class FieldMapping
6
+ def initialize(mapping)
7
+ @map = mapping
8
+ end
9
+
10
+ def remove(object, path, val)
11
+ return unless @map.key?(path)
12
+
13
+ case @map[path]
14
+ in { to: to, find: find }
15
+ remove_from_association(object, to, find, val)
16
+ else
17
+ current = object.send to(path)
18
+ _set object, path, current - value(path, val)
19
+ end
20
+ end
21
+
22
+ def add(object, path, val)
23
+ return unless @map.key?(path)
24
+
25
+ case @map[path]
26
+ in { to: to, find: find }
27
+ add_to_association(object, to, find, val)
28
+ else
29
+ current = object.send to(path)
30
+ _set object, path, current + value(path, val)
31
+ end
32
+ end
33
+
34
+ def replace(object, path, val)
35
+ _set object, path, value(path, val)
36
+ end
37
+
38
+ private
39
+
40
+ def _set(object, path, val)
41
+ return unless @map.key?(path)
42
+
43
+ object.send :"#{to(path)}=", val if @map[path]
44
+ end
45
+
46
+ def has?(...) = @map.key?(...)
47
+
48
+ def to(path)
49
+ case @map[path]
50
+ in { to: to } then to
51
+ in Symbol then @map[path]
52
+ end
53
+ end
54
+
55
+ def value(path, val)
56
+ case @map[path]
57
+ in { find: find } then [val].flatten.pluck("value").map(&find)
58
+ else val
59
+ end
60
+ end
61
+
62
+ def add_to_association(object, to, find, val)
63
+ # Use << operator to avoid loading all records
64
+ collection = object.send(to)
65
+ new_items = [val].flatten.pluck("value").filter_map(&find)
66
+ new_items.each { |item| collection << item unless collection.include?(item) }
67
+ end
68
+
69
+ def remove_from_association(object, to, find, val)
70
+ # Use delete operator to avoid loading all records
71
+ collection = object.send(to)
72
+ items_to_remove = [val].flatten.pluck("value").filter_map(&find)
73
+ collection.delete(*items_to_remove)
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audiences
4
+ module Scim
5
+ class ObserverBase < AetherObservatory::ObserverBase
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audiences
4
+ module Scim
5
+ class PatchGroupsObserver < ObserverBase
6
+ Audiences.config.group_types.each do |group_type|
7
+ subscribe_to "two_percent.scim.update.#{group_type}"
8
+ end
9
+
10
+ def process
11
+ Audiences.logger.info "Patching group #{group.display_name} (#{group.scim_id})"
12
+
13
+ patch_op.process(group, attributes_mapping)
14
+
15
+ group.save!
16
+
17
+ propagate_changes_to_users!
18
+ rescue => e
19
+ Audiences.logger.error e
20
+ raise
21
+ end
22
+
23
+ private
24
+
25
+ def patch_op = PatchOp.new(event_payload.params)
26
+
27
+ def attributes_mapping
28
+ FieldMapping.new("displayName" => :display_name,
29
+ "externalId" => :external_id,
30
+ "urn:ietf:params:scim:schemas:extension:authservice:2.0:Group:active" => :active,
31
+ "members" => { to: :external_users,
32
+ find: ->(value) { ExternalUser.find_by(user_id: value) } })
33
+ end
34
+
35
+ def group
36
+ @group ||= Group.find_by!(resource_type: event_payload.resource,
37
+ scim_id: event_payload.id)
38
+ end
39
+
40
+ def propagate_changes_to_users!
41
+ patch_op.operations.each do |operation|
42
+ next unless operation.path == "members"
43
+
44
+ ExternalUser.where(user_id: operation.value.pluck("value")).find_each do |user|
45
+ TwoPercent::ReplaceEvent.create(resource: "Users", id: user.scim_id, params: user.as_scim)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audiences
4
+ module Scim
5
+ class PatchOp
6
+ attr_reader :operations
7
+
8
+ def initialize(patch_op)
9
+ @operations = patch_op["Operations"].flat_map do |operation|
10
+ derive_operation(operation)
11
+ end
12
+ end
13
+
14
+ def process(object, operator)
15
+ @operations.each { _1.process(object, operator) }
16
+ end
17
+
18
+ private
19
+
20
+ Operation = Struct.new(:op, :path, :value) do
21
+ def process(object, operator)
22
+ raise "Operation #{op} is unknown to #{operator.class}" unless operator.respond_to?(op)
23
+
24
+ operator.public_send(op, object, path, value)
25
+ end
26
+ end
27
+
28
+ def derive_operation(operation)
29
+ case operation["value"]
30
+ when Hash
31
+ operation["value"].flat_map do |key, value|
32
+ derive_operation("op" => operation["op"],
33
+ "path" => [operation["path"], key].compact.join("."),
34
+ "value" => value)
35
+ end
36
+ else
37
+ [Operation.new(operation["op"], operation["path"], operation["value"])]
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audiences
4
+ module Scim
5
+ class PatchUsersObserver < ObserverBase
6
+ subscribe_to "two_percent.scim.update.Users"
7
+
8
+ def process
9
+ Audiences.logger.info "Patching user #{user.display_name} (#{user.scim_id})"
10
+
11
+ process_attributes!
12
+ process_data!
13
+
14
+ user.save!
15
+ rescue => e
16
+ Audiences.logger.error e
17
+ raise
18
+ end
19
+
20
+ private
21
+
22
+ def patch_op = PatchOp.new(event_payload.params)
23
+
24
+ def process_data! = patch_op.process(user, ScimData.new)
25
+
26
+ def process_attributes!
27
+ patch_op.process user, FieldMapping.new("externalId" => :user_id,
28
+ "displayName" => :display_name,
29
+ "active" => :active,
30
+ "photos" => { to: :picture_urls, find: :itself })
31
+ end
32
+
33
+ def user
34
+ @user ||= Audiences::ExternalUser.find_by!(scim_id: event_payload.id)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audiences
4
+ module Scim
5
+ class ScimData
6
+ def replace(object, key, value) = _replace(object.data || {}, key.split("."), value)
7
+
8
+ def add(object, key, val)
9
+ value = object.data&.dig(*key.split(".")) || []
10
+ replace(key, [...value, val])
11
+ end
12
+
13
+ def remove(object, key, val)
14
+ values = object.data&.dig(*key.split(".")) || []
15
+ to_remove = [val].flatten.pluck("value")
16
+ replace(key, values&.reject { |value| to_remove.include?(value["value"]) })
17
+ end
18
+
19
+ private
20
+
21
+ def _replace(data, key, val)
22
+ first_key, *rest_keys = key
23
+ if rest_keys.empty?
24
+ data[first_key] = val
25
+ else
26
+ _replace(data[first_key] ||= {}, rest_keys, val)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audiences
4
+ module Scim
5
+ class UpsertGroupsObserver < ObserverBase
6
+ Audiences.config.group_types.each do |group_type|
7
+ subscribe_to "two_percent.scim.create.#{group_type}"
8
+ subscribe_to "two_percent.scim.replace.#{group_type}"
9
+ end
10
+
11
+ def process
12
+ Audiences.logger.info "#{upsert_action} group #{new_display_name} (#{new_external_id})"
13
+
14
+ group.update! external_id: new_external_id, display_name: new_display_name, active: new_active
15
+ Audiences::PersistedResourceEvent.create(resource_type: "Groups", params: event_payload.params)
16
+ rescue => e
17
+ Audiences.logger.error e
18
+ raise
19
+ end
20
+
21
+ private
22
+
23
+ def upsert_action = group.persisted? ? "Updating" : "Creating"
24
+
25
+ def new_external_id = event_payload.params["externalId"]
26
+
27
+ def new_display_name = event_payload.params["displayName"]
28
+
29
+ def new_active
30
+ active = event_payload.params.dig("urn:ietf:params:scim:schemas:extension:authservice:2.0:Group", "active")
31
+ active.nil? || active
32
+ end
33
+
34
+ def group
35
+ @group ||= Audiences::Group.where(resource_type: event_payload.resource,
36
+ scim_id: event_payload.params["id"])
37
+ .first_or_initialize
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audiences
4
+ module Scim
5
+ class UpsertUsersObserver < ObserverBase
6
+ subscribe_to "two_percent.scim.create.Users"
7
+ subscribe_to "two_percent.scim.replace.Users"
8
+
9
+ def process
10
+ log_upsert_action
11
+ external_user.update! updated_attributes
12
+ return unless valid_group_types?
13
+
14
+ Audiences::PersistedResourceEvent.create(resource_type: "Users", params: event_payload.params)
15
+ rescue => e
16
+ Audiences.logger.error e
17
+ raise
18
+ end
19
+
20
+ private
21
+
22
+ def log_upsert_action
23
+ Audiences.logger.info "#{upsert_action} user #{event_payload.params['displayName']} (#{scim_id})"
24
+ end
25
+
26
+ def scim_id = event_payload.params["id"]
27
+
28
+ def external_user
29
+ @external_user ||= Audiences::ExternalUser.where(scim_id: scim_id).first_or_initialize
30
+ end
31
+
32
+ def upsert_action = external_user.persisted? ? "Updating" : "Creating"
33
+
34
+ def updated_attributes
35
+ {
36
+ user_id: event_payload.params["externalId"],
37
+ display_name: event_payload.params["displayName"],
38
+ picture_urls: new_picture_urls,
39
+ data: event_payload.params,
40
+ groups: new_groups,
41
+ active: event_payload.params.fetch("active", false),
42
+ }
43
+ end
44
+
45
+ def new_picture_urls = event_payload.params["photos"]&.pluck("value")
46
+
47
+ def new_groups
48
+ event_payload.params.fetch("groups", []).filter_map do |group|
49
+ Audiences::Group.find_by(scim_id: group["value"])
50
+ end
51
+ end
52
+
53
+ def valid_group_types?
54
+ missing = external_user.missing_group_types
55
+ return true if missing.empty?
56
+
57
+ Audiences.logger.warn "Provisioning event for user #{scim_id} with missing group types: #{missing.join(', ')}"
58
+ false
59
+ end
60
+ end
61
+ end
62
+ end
@@ -2,20 +2,14 @@
2
2
 
3
3
  module Audiences
4
4
  module Scim
5
- autoload :Client, "audiences/scim/client"
6
- autoload :Resource, "audiences/scim/resource"
7
- autoload :ResourcesQuery, "audiences/scim/resources_query"
8
-
9
- module_function
10
-
11
- def client
12
- Client.new(**Audiences.config.scim)
13
- end
14
-
15
- def resource(type)
16
- Audiences.config.resources.fetch(type) do
17
- Resource.new(type: type)
18
- end
19
- end
5
+ autoload :ScimData, "audiences/scim/scim_data"
6
+ autoload :FieldMapping, "audiences/scim/field_mapping"
7
+ autoload :PatchOp, "audiences/scim/patch_op"
8
+
9
+ autoload :ObserverBase, "audiences/scim/observer_base"
10
+ autoload :PatchGroupsObserver, "audiences/scim/patch_groups_observer"
11
+ autoload :PatchUsersObserver, "audiences/scim/patch_users_observer"
12
+ autoload :UpsertGroupsObserver, "audiences/scim/upsert_groups_observer"
13
+ autoload :UpsertUsersObserver, "audiences/scim/upsert_users_observer"
20
14
  end
21
15
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Audiences
4
- VERSION = "1.6.2"
4
+ VERSION = "2.0.1"
5
5
  end
data/lib/audiences.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "aether_observatory"
4
+
3
5
  # Audiences system
4
6
  # Audiences pushes notifications to your rails app when a
5
7
  # SCIM backend updates a user, notifying matching audiences.
@@ -17,7 +19,7 @@ module_function
17
19
  # Params might contain:
18
20
  #
19
21
  # match_all: Boolean
20
- # criteria: Array<{ <group_type>: Array<Integer> }>
22
+ # criteria: { groups: Array<{ <group_type>: Array<{ id: Integer }> }> }
21
23
  #
22
24
  # @param token [String] a signed token (see #sign)
23
25
  # @param params [Hash] the updated params
@@ -27,13 +29,11 @@ module_function
27
29
  Audiences::Context.load(key) do |context|
28
30
  context.update!(
29
31
  match_all: match_all,
30
- criteria: ::Audiences::Criterion.map(criteria),
31
- extra_users: ::Audiences::ExternalUser.fetch(extra_users.pluck("externalId"))
32
+ extra_users: ::Audiences::ExternalUser.from_scim(*extra_users.map(&:with_indifferent_access)),
33
+ criteria: ::Audiences::Criterion.map(criteria.map(&:with_indifferent_access))
32
34
  )
33
- context.refresh_users!
34
35
  end
35
36
  end
36
37
  end
37
38
 
38
39
  require "audiences/configuration"
39
- require "audiences/railtie"