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 +4 -4
- data/app/models/audiences/external_user.rb +6 -2
- data/docs/CHANGELOG.md +14 -0
- data/lib/audiences/engine.rb +5 -4
- data/lib/audiences/integrations/delete_groups_observer.rb +43 -0
- data/lib/audiences/integrations/delete_users_observer.rb +39 -0
- data/lib/audiences/integrations/observer_base.rb +36 -0
- data/lib/audiences/integrations/upsert_groups_observer.rb +71 -0
- data/lib/audiences/integrations/upsert_users_observer.rb +93 -0
- data/lib/audiences/integrations.rb +15 -0
- data/lib/audiences/version.rb +1 -1
- data/lib/audiences.rb +3 -3
- metadata +8 -13
- data/app/events/audiences/application_event.rb +0 -7
- data/app/events/audiences/persisted_resource_event.rb +0 -10
- data/lib/audiences/scim/field_mapping.rb +0 -77
- data/lib/audiences/scim/observer_base.rb +0 -8
- data/lib/audiences/scim/patch_groups_observer.rb +0 -51
- data/lib/audiences/scim/patch_op.rb +0 -42
- data/lib/audiences/scim/patch_users_observer.rb +0 -38
- data/lib/audiences/scim/scim_data.rb +0 -31
- data/lib/audiences/scim/upsert_groups_observer.rb +0 -41
- data/lib/audiences/scim/upsert_users_observer.rb +0 -62
- data/lib/audiences/scim.rb +0 -15
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f4ea4e324141ab297f05ffe8089810777cb41c5ae9d2e2dc5453c930828b2856
|
|
4
|
+
data.tar.gz: 439e1e4491f03fcda92f60fecfa8254b2fe774e5bbec5b5b5266e1e145d5a4c3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
40
|
-
|
|
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"
|
data/lib/audiences/engine.rb
CHANGED
|
@@ -33,10 +33,11 @@ module Audiences
|
|
|
33
33
|
|
|
34
34
|
initializer "audiences.observers" do
|
|
35
35
|
if Audiences.config.observe_scim
|
|
36
|
-
|
|
37
|
-
Audiences::
|
|
38
|
-
Audiences::
|
|
39
|
-
Audiences::
|
|
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
|
data/lib/audiences/version.rb
CHANGED
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
|
|
7
|
-
#
|
|
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 :
|
|
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:
|
|
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-
|
|
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,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,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
|
data/lib/audiences/scim.rb
DELETED
|
@@ -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
|