audiences 2.0.2 → 3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a472729613c93580a6c651fd6a970868704cd3907bb9368b51bc855598b3d73b
4
- data.tar.gz: 7ce7f85dc49b241fcc331d6f884b80e7ddfd5575ae93ae5fa33b14666d8d0ac9
3
+ metadata.gz: f4ea4e324141ab297f05ffe8089810777cb41c5ae9d2e2dc5453c930828b2856
4
+ data.tar.gz: 439e1e4491f03fcda92f60fecfa8254b2fe774e5bbec5b5b5266e1e145d5a4c3
5
5
  SHA512:
6
- metadata.gz: b46ea33b5f2953cf2e6a67bdfe6cc4d480ea296722616f38a8554bc1860c67def0158c2e1d77916b450ab8d5ad62576ad3fae26fc256d22584caa8252c2052f2
7
- data.tar.gz: 051aec8ea8523cace8fdf9f70e034e0cdeda82ec360ec0987f269b41c1d657af76b85e3432a77038228d118bff4f65b99897e24cb619825ad8f76be13d738b3c
6
+ metadata.gz: ae8b27668cd3aadd892bc9584db4c1ec8e9b376a14d525ecf7cf7df01a74c671a8697d222bff37b1a4f22dbc826ba3110d72895a68b17b572a2053ccfe1f4db0
7
+ data.tar.gz: ff5ede737617035689267d76fc42bf022e8d00347836c25cb6097749fe2b9d2c54facef9bbc25f5955ee4f921cf3232a5606dec18de6688996c3fd508ae67e90
@@ -36,8 +36,12 @@ module Audiences
36
36
  end
37
37
 
38
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))
39
+ # Flatten in case array is passed
40
+ json_array = scim_json.flatten
41
+ ids = json_array.filter_map { |h| h.is_a?(Hash) ? (h["id"] || h[:id]) : nil }
42
+ external_ids = json_array.filter_map { |h| h.is_a?(Hash) ? (h["externalId"] || h[:externalId]) : nil }
43
+
44
+ where(scim_id: ids).or(where(user_id: external_ids))
41
45
  end
42
46
 
43
47
  scope :matching, ->(criterion) do
data/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Unreleased
2
2
 
3
+ # Version 3.0.0 (2026-06-18)
4
+
5
+ **Breaking Changes:**
6
+ - Removed TwoPercent gem dependency from Gemfile - tests now use internal TestDomainEvents module
7
+
8
+ **Improvements:**
9
+ - Refactored all integration observers (UpsertUsers, DeleteUsers, UpsertGroups, DeleteGroups) to use shared block-based log_sync_operation method in ObserverBase for cleaner, more maintainable code
10
+ - Added Ruby 3.4 compatibility via drb gem dependency
11
+ - Fixed Rubocop violations in all observers by extracting helper methods
12
+
13
+ **Code Cleanup:**
14
+ - Removed PersistedResourceEvent and ApplicationEvent classes (dead code from old SCIM architecture)
15
+ - Removed lib/audiences/scim/ directory containing deprecated SCIM observers (replaced by Integrations observers in v2.0)
16
+
3
17
  # Version 2.0.2 (2026-06-11)
4
18
 
5
19
  - Update territory name from "Raleigh" to "Raleigh-Durham"
@@ -33,10 +33,11 @@ module Audiences
33
33
 
34
34
  initializer "audiences.observers" do
35
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
36
+ # Domain event observers (provider-agnostic)
37
+ Audiences::Integrations::UpsertUsersObserver.start
38
+ Audiences::Integrations::UpsertGroupsObserver.start
39
+ Audiences::Integrations::DeleteUsersObserver.start
40
+ Audiences::Integrations::DeleteGroupsObserver.start
40
41
  end
41
42
  end
42
43
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audiences
4
+ module Integrations
5
+ # Observer for Group deletion domain events
6
+ # Consumes domain events (NOT SCIM-specific)
7
+ class DeleteGroupsObserver < ObserverBase
8
+ subscribe_to "two_percent.domain.group.deleted"
9
+
10
+ def process
11
+ log_sync_operation(action: "delete", resource_type: resource_type, scim_id: scim_id) do
12
+ delete_group
13
+ end
14
+ rescue => e
15
+ Audiences.logger.error e
16
+ raise
17
+ end
18
+
19
+ private
20
+
21
+ def scim_id
22
+ event_payload.group_id
23
+ end
24
+
25
+ def resource_type
26
+ event_payload.resource_type
27
+ end
28
+
29
+ def delete_group
30
+ Audiences.logger.info "Deleting group #{scim_id} (#{resource_type})"
31
+
32
+ group = Audiences::Group.find_by(resource_type: resource_type, scim_id: scim_id)
33
+
34
+ if group
35
+ group.destroy!
36
+ Audiences.logger.info "Group #{scim_id} deleted from Audiences cache"
37
+ else
38
+ Audiences.logger.warn "Group #{scim_id} not found in Audiences cache"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audiences
4
+ module Integrations
5
+ # Observer for User deletion domain events
6
+ # Consumes domain events (NOT SCIM-specific)
7
+ class DeleteUsersObserver < ObserverBase
8
+ subscribe_to "two_percent.domain.user.deleted"
9
+
10
+ def process
11
+ log_sync_operation(action: "delete", resource_type: "Users", scim_id: scim_id) do
12
+ delete_user
13
+ end
14
+ rescue => e
15
+ Audiences.logger.error e
16
+ raise
17
+ end
18
+
19
+ private
20
+
21
+ def scim_id
22
+ event_payload.user_id
23
+ end
24
+
25
+ def delete_user
26
+ Audiences.logger.info "Deleting user #{scim_id}"
27
+
28
+ user = Audiences::ExternalUser.find_by(scim_id: scim_id)
29
+
30
+ if user
31
+ user.destroy!
32
+ Audiences.logger.info "User #{scim_id} deleted from Audiences cache"
33
+ else
34
+ Audiences.logger.warn "User #{scim_id} not found in Audiences cache"
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audiences
4
+ module Integrations
5
+ class ObserverBase < AetherObservatory::ObserverBase
6
+ # Logs the start and completion of a sync operation
7
+ # Automatically logs "start" before yielding and "complete" in ensure block
8
+ #
9
+ # @param action [String] The action being performed (e.g., "create", "update", "delete")
10
+ # @param resource_type [String] The type of resource (e.g., "Users", "Groups")
11
+ # @param scim_id [String] The SCIM ID of the resource
12
+ # @param correlation_id [String] The correlation ID from the event (defaults to event_payload.correlation_id)
13
+ # @yield The block containing the sync operation
14
+ def log_sync_operation(action:, resource_type:, scim_id:, correlation_id: event_payload.correlation_id)
15
+ log_data = build_log_data(action: action, resource_type: resource_type, scim_id: scim_id,
16
+ correlation_id: correlation_id)
17
+ Audiences.logger.info({ **log_data, stage: "start" }.to_json)
18
+ yield
19
+ ensure
20
+ Audiences.logger.info({ **log_data, stage: "complete" }.to_json)
21
+ end
22
+
23
+ private
24
+
25
+ def build_log_data(action:, resource_type:, scim_id:, correlation_id:)
26
+ {
27
+ correlation_id: correlation_id,
28
+ scim_id: scim_id,
29
+ action: action,
30
+ resource_type: resource_type,
31
+ service: "audiences",
32
+ }
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audiences
4
+ module Integrations
5
+ # Observer for Group creation/update domain events
6
+ # Consumes domain events (NOT SCIM-specific)
7
+ class UpsertGroupsObserver < ObserverBase
8
+ subscribe_to "two_percent.domain.group.created"
9
+ subscribe_to "two_percent.domain.group.updated"
10
+
11
+ def process
12
+ log_sync_operation(action: upsert_action.downcase, resource_type: resource_type, scim_id: scim_id) do
13
+ upsert_group
14
+ sync_members_if_present
15
+ end
16
+ rescue => e
17
+ Audiences.logger.error e
18
+ raise
19
+ end
20
+
21
+ private
22
+
23
+ def group_attrs
24
+ @group_attrs ||= event_payload.group_attributes.with_indifferent_access
25
+ end
26
+
27
+ def correlation_id
28
+ event_payload.correlation_id
29
+ end
30
+
31
+ def scim_id
32
+ group_attrs[:scim_id]
33
+ end
34
+
35
+ def resource_type
36
+ event_payload.resource_type
37
+ end
38
+
39
+ def upsert_action
40
+ group.persisted? ? "Updating" : "Creating"
41
+ end
42
+
43
+ def group
44
+ @group ||= Audiences::Group.where(
45
+ resource_type: resource_type,
46
+ scim_id: scim_id
47
+ ).first_or_initialize
48
+ end
49
+
50
+ def upsert_group
51
+ Audiences.logger.info "#{upsert_action} group #{group_attrs[:display_name]} (#{scim_id})"
52
+
53
+ group.update!(
54
+ external_id: group_attrs[:external_id],
55
+ display_name: group_attrs[:display_name],
56
+ active: group_attrs.fetch(:active, true)
57
+ )
58
+ end
59
+
60
+ def sync_members_if_present
61
+ sync_members if group_attrs[:members].present?
62
+ end
63
+
64
+ def sync_members
65
+ member_scim_ids = group_attrs[:members].filter_map { |m| m[:scim_id] || m["scim_id"] }
66
+ users = Audiences::ExternalUser.where(scim_id: member_scim_ids).to_a
67
+ group.external_users = users
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audiences
4
+ module Integrations
5
+ # Observer for User creation/update domain events
6
+ # Consumes domain events (NOT SCIM-specific)
7
+ class UpsertUsersObserver < ObserverBase
8
+ subscribe_to "two_percent.domain.user.created"
9
+ subscribe_to "two_percent.domain.user.updated"
10
+
11
+ def process
12
+ log_sync_operation(action: upsert_action.downcase, resource_type: "Users", scim_id: scim_id) do
13
+ upsert_user
14
+ sync_groups
15
+ end
16
+ rescue => e
17
+ Audiences.logger.error e
18
+ raise
19
+ end
20
+
21
+ private
22
+
23
+ def user_attrs
24
+ @user_attrs ||= event_payload.user_attributes.with_indifferent_access
25
+ end
26
+
27
+ def scim_id
28
+ user_attrs[:scim_id]
29
+ end
30
+
31
+ def correlation_id
32
+ event_payload.correlation_id
33
+ end
34
+
35
+ def external_user
36
+ @external_user ||= Audiences::ExternalUser.where(scim_id: scim_id).first_or_initialize
37
+ end
38
+
39
+ def upsert_action
40
+ external_user.persisted? ? "Updating" : "Creating"
41
+ end
42
+
43
+ def updated_attributes
44
+ {
45
+ user_id: user_attrs[:external_id],
46
+ display_name: user_attrs[:display_name],
47
+ picture_urls: extract_picture_urls,
48
+ data: build_data_hash,
49
+ active: user_attrs.fetch(:active, false),
50
+ }
51
+ end
52
+
53
+ def extract_picture_urls
54
+ photos = user_attrs[:photos]
55
+ return [] unless photos.is_a?(Array)
56
+
57
+ photos.filter_map { |photo| photo["value"] || photo[:value] }
58
+ end
59
+
60
+ def find_associated_groups
61
+ groups_data = user_attrs[:groups]
62
+ return [] unless groups_data.is_a?(Array)
63
+
64
+ group_scim_ids = groups_data.filter_map { |g| g[:scim_id] || g["scim_id"] }
65
+ Audiences::Group.where(scim_id: group_scim_ids).to_a
66
+ end
67
+
68
+ def upsert_user
69
+ Audiences.logger.info "#{upsert_action} user #{user_attrs[:display_name]} (#{scim_id})"
70
+ external_user.update!(updated_attributes)
71
+ end
72
+
73
+ def sync_groups
74
+ found_groups = find_associated_groups
75
+ external_user.groups = found_groups
76
+ end
77
+
78
+ # Build minimal data hash for Audiences API responses
79
+ # Group-derived attributes (title, department, territory, role) are built dynamically
80
+ # in ExternalUser#groups_as_scim from GroupMembership associations
81
+ def build_data_hash
82
+ {
83
+ "id" => scim_id,
84
+ "externalId" => user_attrs[:external_id],
85
+ "displayName" => user_attrs[:display_name],
86
+ "userName" => user_attrs[:user_name],
87
+ "photos" => user_attrs[:photos],
88
+ "active" => user_attrs[:active],
89
+ }.compact
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audiences
4
+ # Integration observers for syncing domain events from external identity providers
5
+ # This module does NOT contain SCIM protocol logic (e.g., RFC 7644 operations)
6
+ # It only contains observers that react to domain events and sync data to Audiences cache
7
+ module Integrations
8
+ # Domain event observers
9
+ autoload :ObserverBase, "audiences/integrations/observer_base"
10
+ autoload :UpsertGroupsObserver, "audiences/integrations/upsert_groups_observer"
11
+ autoload :UpsertUsersObserver, "audiences/integrations/upsert_users_observer"
12
+ autoload :DeleteGroupsObserver, "audiences/integrations/delete_groups_observer"
13
+ autoload :DeleteUsersObserver, "audiences/integrations/delete_users_observer"
14
+ end
15
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Audiences
4
- VERSION = "2.0.2"
4
+ VERSION = "3.0.0"
5
5
  end
data/lib/audiences.rb CHANGED
@@ -3,13 +3,13 @@
3
3
  require "aether_observatory"
4
4
 
5
5
  # Audiences system
6
- # Audiences pushes notifications to your rails app when a
7
- # SCIM backend updates a user, notifying matching audiences.
6
+ # Audiences pushes notifications to your rails app when an
7
+ # identity provider updates a user, notifying matching audiences.
8
8
  #
9
9
  module Audiences
10
10
  autoload :Model, "audiences/model"
11
11
  autoload :Notifications, "audiences/notifications"
12
- autoload :Scim, "audiences/scim"
12
+ autoload :Integrations, "audiences/integrations"
13
13
  autoload :VERSION, "audiences/version"
14
14
 
15
15
  module_function
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: 2.0.2
4
+ version: 3.0.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-06-12 00:00:00.000000000 Z
11
+ date: 2026-06-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aether_observatory
@@ -51,8 +51,6 @@ files:
51
51
  - app/controllers/audiences/application_controller.rb
52
52
  - app/controllers/audiences/contexts_controller.rb
53
53
  - app/controllers/audiences/scim_proxy_controller.rb
54
- - app/events/audiences/application_event.rb
55
- - app/events/audiences/persisted_resource_event.rb
56
54
  - app/models/audiences/application_record.rb
57
55
  - app/models/audiences/context.rb
58
56
  - app/models/audiences/context/locating.rb
@@ -94,17 +92,14 @@ files:
94
92
  - lib/audiences/configuration.rb
95
93
  - lib/audiences/editor_helper.rb
96
94
  - lib/audiences/engine.rb
95
+ - lib/audiences/integrations.rb
96
+ - lib/audiences/integrations/delete_groups_observer.rb
97
+ - lib/audiences/integrations/delete_users_observer.rb
98
+ - lib/audiences/integrations/observer_base.rb
99
+ - lib/audiences/integrations/upsert_groups_observer.rb
100
+ - lib/audiences/integrations/upsert_users_observer.rb
97
101
  - lib/audiences/model.rb
98
102
  - lib/audiences/notifications.rb
99
- - lib/audiences/scim.rb
100
- - lib/audiences/scim/field_mapping.rb
101
- - lib/audiences/scim/observer_base.rb
102
- - lib/audiences/scim/patch_groups_observer.rb
103
- - lib/audiences/scim/patch_op.rb
104
- - lib/audiences/scim/patch_users_observer.rb
105
- - lib/audiences/scim/scim_data.rb
106
- - lib/audiences/scim/upsert_groups_observer.rb
107
- - lib/audiences/scim/upsert_users_observer.rb
108
103
  - lib/audiences/version.rb
109
104
  - lib/tasks/audiences_tasks.rake
110
105
  homepage: https://github.com/powerhome/audiences
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Audiences
4
- class ApplicationEvent < AetherObservatory::EventBase
5
- event_prefix "audiences"
6
- end
7
- end
@@ -1,10 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Audiences
4
- class PersistedResourceEvent < ApplicationEvent
5
- event_name { "persisted.#{resource_type}" }
6
-
7
- attribute :params
8
- attribute :resource_type
9
- end
10
- end
@@ -1,77 +0,0 @@
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
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Audiences
4
- module Scim
5
- class ObserverBase < AetherObservatory::ObserverBase
6
- end
7
- end
8
- end
@@ -1,51 +0,0 @@
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
@@ -1,42 +0,0 @@
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
@@ -1,38 +0,0 @@
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
@@ -1,31 +0,0 @@
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
@@ -1,41 +0,0 @@
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
@@ -1,62 +0,0 @@
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
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Audiences
4
- module Scim
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"
14
- end
15
- end