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
@@ -5,6 +5,63 @@ 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
+ # Defines a default scope for users, so the users that are part of an audience can
20
+ # be filtered (i.e.: only active, only users in a specific group, etc)
21
+ #
22
+ # By default, only active users are listed.
23
+ #
24
+ # I.e.:
25
+ #
26
+ # # Allowing inactive users
27
+ # Audiences.configure do |config|
28
+ # config.default_users_scope = -> { all }
29
+ # end
30
+ #
31
+ # # Accepting only users in certain groups
32
+ # Audiences.configure do |config|
33
+ # config.default_users_scope = -> { includes(:groups).merge(Audiences::Group.where(scim_id: ALLOWED_GROUPS)) }
34
+ # end
35
+ #
36
+ # This configuration defaults to `-> { active }`
37
+ #
38
+ config_accessor :default_users_scope do
39
+ ->(*) { active }
40
+ end
41
+
42
+ # Defines a default scope for groups, so the groups that are part of an audience can
43
+ # be filtered (i.e.: only active, only specific groups, etc)
44
+ #
45
+ # By default, only active groups are listed.
46
+ #
47
+ # I.e.:
48
+ #
49
+ # # Allowing inactive groups
50
+ # Audiences.configure do |config|
51
+ # config.default_groups_scope = -> { all }
52
+ # end
53
+ #
54
+ # # Accepting only groups in certain groups
55
+ # Audiences.configure do |config|
56
+ # config.default_groups_scope = -> { where(scim_id: ALLOWED_GROUPS) }
57
+ # end
58
+ #
59
+ # This configuration defaults to `-> { active }`
60
+ #
61
+ config_accessor :default_groups_scope do
62
+ ->(*) { active }
63
+ end
64
+
8
65
  # These are the user attributes that will be exposed in the audiences endpoints.
9
66
  # They're required by the UI to display the user information.
10
67
  #
@@ -37,7 +94,20 @@ module Audiences
37
94
  # end
38
95
  #
39
96
  config_accessor :authenticate do
40
- ->(*) { true }
97
+ ->(*) do
98
+ Audiences.logger.warn(<<~MESSAGE)
99
+ Audiences authenticate is currently configured using a default and is blocking authenticaiton.
100
+
101
+ To make this wraning go away provide a configuration for `Audiences.config.authenticate`.
102
+
103
+ The value should:
104
+ 1. Be callable like a Proc.
105
+ 2. Return true when the request is permitted.
106
+ 3. Return false when the request is not permitted.
107
+ MESSAGE
108
+
109
+ false
110
+ end
41
111
  end
42
112
 
43
113
  #
@@ -51,50 +121,6 @@ module Audiences
51
121
  #
52
122
  config_accessor(:identity_key) { :id }
53
123
 
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
124
  #
99
125
  # Notifications configurations.
100
126
  # 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,49 @@
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
+ current = object.send to(path)
12
+ _set object, path, current - value(path, val)
13
+ end
14
+
15
+ def add(object, path, val)
16
+ current = object.send to(path)
17
+ _set object, path, current + value(path, val)
18
+ end
19
+
20
+ def replace(object, path, val)
21
+ _set object, path, value(path, val)
22
+ end
23
+
24
+ private
25
+
26
+ def _set(object, path, val)
27
+ return unless @map.key?(path)
28
+
29
+ object.send :"#{to(path)}=", val if @map[path]
30
+ end
31
+
32
+ def has?(...) = @map.key?(...)
33
+
34
+ def to(path)
35
+ case @map[path]
36
+ in { to: to } then to
37
+ in Symbol then @map[path]
38
+ end
39
+ end
40
+
41
+ def value(path, val)
42
+ case @map[path]
43
+ in { find: find } then [val].flatten.pluck("value").map(&find)
44
+ else val
45
+ end
46
+ end
47
+ end
48
+ end
49
+ 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,40 @@
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
+ rescue => e
16
+ Audiences.logger.error e
17
+ raise
18
+ end
19
+
20
+ private
21
+
22
+ def upsert_action = group.persisted? ? "Updating" : "Creating"
23
+
24
+ def new_external_id = event_payload.params["externalId"]
25
+
26
+ def new_display_name = event_payload.params["displayName"]
27
+
28
+ def new_active
29
+ active = event_payload.params.dig("urn:ietf:params:scim:schemas:extension:authservice:2.0:Group", "active")
30
+ active.nil? || active
31
+ end
32
+
33
+ def group
34
+ @group ||= Audiences::Group.where(resource_type: event_payload.resource,
35
+ scim_id: event_payload.params["id"])
36
+ .first_or_initialize
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,48 @@
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
+ Audiences.logger.info "#{upsert_action} group #{event_payload.params['displayName']} (#{scim_id})"
11
+
12
+ external_user.update! updated_attributes
13
+ rescue => e
14
+ Audiences.logger.error e
15
+ raise
16
+ end
17
+
18
+ private
19
+
20
+ def scim_id = event_payload.params["id"]
21
+
22
+ def external_user
23
+ @external_user ||= Audiences::ExternalUser.where(scim_id: scim_id).first_or_initialize
24
+ end
25
+
26
+ def upsert_action = external_user.persisted? ? "Updating" : "Creating"
27
+
28
+ def updated_attributes
29
+ {
30
+ user_id: event_payload.params["externalId"],
31
+ display_name: event_payload.params["displayName"],
32
+ picture_urls: new_picture_urls,
33
+ data: event_payload.params,
34
+ groups: new_groups,
35
+ active: event_payload.params.fetch("active", false),
36
+ }
37
+ end
38
+
39
+ def new_picture_urls = event_payload.params["photos"]&.pluck("value")
40
+
41
+ def new_groups
42
+ event_payload.params.fetch("groups", []).filter_map do |group|
43
+ Audiences::Group.find_by(scim_id: group["value"])
44
+ end
45
+ end
46
+ end
47
+ end
48
+ 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.1"
4
+ VERSION = "2.0.0"
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"