two_percent 0.5.0 → 1.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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +5 -1
  3. data/app/controllers/two_percent/application_controller.rb +79 -1
  4. data/app/controllers/two_percent/bulk_controller.rb +18 -2
  5. data/app/controllers/two_percent/scim_controller.rb +200 -8
  6. data/app/models/two_percent/application_record.rb +7 -0
  7. data/app/models/two_percent/scim_group.rb +182 -0
  8. data/app/models/two_percent/scim_group_membership.rb +29 -0
  9. data/app/models/two_percent/scim_user.rb +138 -0
  10. data/lib/generators/two_percent/install/install_generator.rb +46 -0
  11. data/lib/generators/two_percent/install/templates/INSTALL_README +84 -0
  12. data/lib/generators/two_percent/install/templates/create_two_percent_scim_group_memberships.rb.erb +20 -0
  13. data/lib/generators/two_percent/install/templates/create_two_percent_scim_groups.rb.erb +24 -0
  14. data/lib/generators/two_percent/install/templates/create_two_percent_scim_users.rb.erb +25 -0
  15. data/lib/generators/two_percent/install/templates/two_percent.rb.erb +85 -0
  16. data/lib/two_percent/bulk_processor.rb +145 -5
  17. data/lib/two_percent/configuration.rb +15 -0
  18. data/lib/two_percent/domain/events/base_event.rb +27 -0
  19. data/lib/two_percent/domain/events/group_events.rb +54 -0
  20. data/lib/two_percent/domain/events/user_events.rb +51 -0
  21. data/lib/two_percent/domain/events.rb +16 -0
  22. data/lib/two_percent/domain.rb +8 -0
  23. data/lib/two_percent/scim/patch_processor.rb +119 -0
  24. data/lib/two_percent/scim/schema.rb +152 -0
  25. data/lib/two_percent/scim.rb +8 -0
  26. data/lib/two_percent/syncable.rb +198 -0
  27. data/lib/two_percent/version.rb +1 -1
  28. data/lib/two_percent.rb +3 -1
  29. metadata +27 -16
  30. data/app/events/two_percent/application_event.rb +0 -7
  31. data/app/events/two_percent/create_event.rb +0 -11
  32. data/app/events/two_percent/delete_event.rb +0 -11
  33. data/app/events/two_percent/replace_event.rb +0 -12
  34. data/app/events/two_percent/update_event.rb +0 -12
  35. data/lib/two_percent/event_handler.rb +0 -26
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TwoPercent
4
+ module Domain
5
+ module Events
6
+ # Domain event: User was created
7
+ class UserCreated < BaseEvent
8
+ event_name "user.created"
9
+
10
+ attribute :user_attributes # Domain attributes, not SCIM
11
+
12
+ def user_id
13
+ user_attributes[:scim_id]
14
+ end
15
+
16
+ # Apply this event to a domain model class
17
+ def apply_to_model(model_class)
18
+ model_class.syncable_model.sync_created(user_attributes, model_class)
19
+ end
20
+ end
21
+
22
+ # Domain event: User was updated
23
+ class UserUpdated < BaseEvent
24
+ event_name "user.updated"
25
+
26
+ attribute :user_attributes
27
+
28
+ def user_id
29
+ user_attributes[:scim_id]
30
+ end
31
+
32
+ # Apply this event to a domain model class
33
+ def apply_to_model(model_class)
34
+ model_class.syncable_model.sync_updated(user_attributes, model_class)
35
+ end
36
+ end
37
+
38
+ # Domain event: User was deleted
39
+ class UserDeleted < BaseEvent
40
+ event_name "user.deleted"
41
+
42
+ attribute :user_id # Just the ID for deletion
43
+
44
+ # Apply this event to a domain model class
45
+ def apply_to_model(model_class)
46
+ model_class.syncable_model.sync_deleted(user_id, model_class)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TwoPercent
4
+ module Domain
5
+ # Domain events (provider-agnostic)
6
+ module Events
7
+ autoload :BaseEvent, "two_percent/domain/events/base_event"
8
+ autoload :UserCreated, "two_percent/domain/events/user_events"
9
+ autoload :UserUpdated, "two_percent/domain/events/user_events"
10
+ autoload :UserDeleted, "two_percent/domain/events/user_events"
11
+ autoload :GroupCreated, "two_percent/domain/events/group_events"
12
+ autoload :GroupUpdated, "two_percent/domain/events/group_events"
13
+ autoload :GroupDeleted, "two_percent/domain/events/group_events"
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TwoPercent
4
+ # Domain layer - contains domain events and business logic
5
+ module Domain
6
+ autoload :Events, "two_percent/domain/events"
7
+ end
8
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TwoPercent
4
+ module Scim
5
+ # SCIM RFC 7644 PATCH operation processor
6
+ # Handles add, replace, remove operations on SCIM resources
7
+ class PatchProcessor
8
+ attr_reader :operations
9
+
10
+ def initialize(patch_request)
11
+ @operations = parse_operations(patch_request)
12
+ end
13
+
14
+ def apply_to_hash(scim_hash)
15
+ result = scim_hash.deep_dup
16
+
17
+ operations.each do |operation|
18
+ case operation[:op].downcase
19
+ when "add"
20
+ apply_add(result, operation[:path], operation[:value])
21
+ when "replace"
22
+ apply_replace(result, operation[:path], operation[:value])
23
+ when "remove"
24
+ apply_remove(result, operation[:path])
25
+ else
26
+ raise ArgumentError, "Unknown PATCH operation: #{operation[:op]}"
27
+ end
28
+ end
29
+
30
+ result
31
+ end
32
+
33
+ private
34
+
35
+ def parse_operations(patch_request)
36
+ ops = patch_request["Operations"] || patch_request[:Operations]
37
+ raise ArgumentError, "PATCH request must contain 'Operations' array" unless ops.is_a?(Array)
38
+
39
+ ops.flat_map do |op|
40
+ derive_operation(op)
41
+ end
42
+ end
43
+
44
+ def derive_operation(operation)
45
+ # Handle nested value hashes by flattening to path notation
46
+ case operation.fetch("value") { operation[:value] }
47
+ when Hash
48
+ operation.fetch("value") { operation[:value] }.flat_map do |key, value|
49
+ path = [operation.fetch("path") { operation[:path] }, key].compact.join(".")
50
+ derive_operation(
51
+ "op" => operation.fetch("op") { operation[:op] },
52
+ "path" => path,
53
+ "value" => value
54
+ )
55
+ end
56
+ else
57
+ [{
58
+ op: operation.fetch("op") { operation[:op] },
59
+ path: operation.fetch("path") { operation[:path] },
60
+ value: operation.fetch("value") { operation[:value] },
61
+ }]
62
+ end
63
+ end
64
+
65
+ def apply_add(hash, path, value)
66
+ if path.nil? || path.empty?
67
+ # No path means add to root
68
+ hash.merge!(value) if value.is_a?(Hash)
69
+ else
70
+ keys = path.split(".")
71
+ target = navigate_to_parent(hash, keys[0..-2])
72
+ last_key = keys.last
73
+
74
+ target[last_key] = if target[last_key].is_a?(Array)
75
+ (target[last_key] + [value]).flatten
76
+ else
77
+ value
78
+ end
79
+ end
80
+ end
81
+
82
+ def apply_replace(hash, path, value)
83
+ if path.nil? || path.empty?
84
+ # No path means replace root attributes
85
+ hash.merge!(value) if value.is_a?(Hash)
86
+ else
87
+ keys = path.split(".")
88
+ target = navigate_to_parent(hash, keys[0..-2])
89
+ target[keys.last] = value
90
+ end
91
+ end
92
+
93
+ def apply_remove(hash, path)
94
+ return if path.nil? || path.empty?
95
+
96
+ keys = path.split(".")
97
+ target = navigate_to_parent(hash, keys[0..-2])
98
+ last_key = keys.last
99
+
100
+ # Special handling for members array - set to empty array instead of deleting
101
+ # This ensures upsert_from_scim can sync memberships to empty state
102
+ if last_key == "members"
103
+ target[last_key] = []
104
+ else
105
+ target.delete(last_key)
106
+ end
107
+ end
108
+
109
+ def navigate_to_parent(hash, keys)
110
+ return hash if keys.empty?
111
+
112
+ keys.reduce(hash) do |current, key|
113
+ current[key] ||= {}
114
+ current[key]
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TwoPercent
4
+ module Scim
5
+ # SCIM Schema definition based on RFC 7644
6
+ class Schema
7
+ CORE_USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User"
8
+ CORE_GROUP_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Group"
9
+ EXTENSION_SCHEMA = "urn:ietf:params:scim:schemas:extension:authservice:2.0:User"
10
+
11
+ # Core User attributes per RFC 7644 Section 4.1
12
+ CORE_USER_ATTRIBUTES = %w[
13
+ id
14
+ externalId
15
+ userName
16
+ displayName
17
+ name
18
+ emails
19
+ phoneNumbers
20
+ addresses
21
+ photos
22
+ userType
23
+ title
24
+ active
25
+ groups
26
+ meta
27
+ schemas
28
+ ].freeze
29
+
30
+ # Core Group attributes per RFC 7644 Section 4.2
31
+ CORE_GROUP_ATTRIBUTES = %w[
32
+ id
33
+ externalId
34
+ displayName
35
+ members
36
+ meta
37
+ schemas
38
+ ].freeze
39
+
40
+ # Extension attributes (custom per IDP)
41
+ EXTENSION_USER_ATTRIBUTES = %w[
42
+ department
43
+ territory
44
+ territoryAbbr
45
+ role
46
+ mfaRequired
47
+ ].freeze
48
+
49
+ def self.validate_user(scim_hash, require_id: true)
50
+ # Accept either core schema or extension schemas
51
+ validate_schemas_present(scim_hash)
52
+
53
+ # Only require id for updates, not creation
54
+ required_attrs = require_id ? %w[id externalId] : %w[externalId]
55
+ validate_required_attributes(scim_hash, required_attrs)
56
+ validate_attribute_types(scim_hash)
57
+
58
+ # Return validated data with schemas normalized
59
+ normalize_user(scim_hash)
60
+ end
61
+
62
+ def self.validate_group(scim_hash, require_id: true)
63
+ # Accept either core schema or extension schemas
64
+ validate_schemas_present(scim_hash)
65
+
66
+ # Only require id for updates, not creation
67
+ required_attrs = require_id ? %w[id displayName] : %w[displayName]
68
+ validate_required_attributes(scim_hash, required_attrs)
69
+
70
+ normalize_group(scim_hash)
71
+ end
72
+
73
+ def self.normalize_user(scim_hash)
74
+ {
75
+ core: extract_core_attributes(scim_hash, CORE_USER_ATTRIBUTES),
76
+ extensions: extract_extensions(scim_hash),
77
+ }
78
+ end
79
+
80
+ def self.normalize_group(scim_hash)
81
+ {
82
+ core: extract_core_attributes(scim_hash, CORE_GROUP_ATTRIBUTES),
83
+ extensions: extract_extensions(scim_hash),
84
+ }
85
+ end
86
+
87
+ def self.extract_core_attributes(scim_hash, allowed_attrs)
88
+ scim_hash.slice(*allowed_attrs)
89
+ end
90
+
91
+ def self.extract_extensions(scim_hash)
92
+ scim_hash.select { |key, _| key.start_with?("urn:ietf:params:scim:schemas:extension:") }
93
+ end
94
+
95
+ def self.validate_schemas(scim_hash, required_schemas)
96
+ schemas = scim_hash["schemas"] || []
97
+ missing = required_schemas - schemas
98
+
99
+ return unless missing.any?
100
+
101
+ raise ArgumentError, "Missing required schemas: #{missing.join(', ')}"
102
+ end
103
+
104
+ def self.validate_schemas_present(scim_hash)
105
+ schemas = scim_hash["schemas"] || []
106
+
107
+ return unless schemas.empty?
108
+
109
+ raise ArgumentError, "schemas attribute is required"
110
+ end
111
+
112
+ def self.validate_required_attributes(scim_hash, required_attrs)
113
+ missing = required_attrs.select { |attr| scim_hash[attr].nil? }
114
+
115
+ return unless missing.any?
116
+
117
+ raise ArgumentError, "Missing required attributes: #{missing.join(', ')}"
118
+ end
119
+
120
+ def self.validate_attribute_types(scim_hash)
121
+ # Validate complex attribute structures
122
+ validate_name_structure(scim_hash["name"]) if scim_hash["name"]
123
+ validate_multi_valued(scim_hash["emails"], %w[value type]) if scim_hash["emails"]
124
+ validate_multi_valued(scim_hash["phoneNumbers"], %w[value type]) if scim_hash["phoneNumbers"]
125
+ validate_multi_valued(scim_hash["addresses"], %w[type]) if scim_hash["addresses"]
126
+ validate_multi_valued(scim_hash["photos"], %w[value type]) if scim_hash["photos"]
127
+ end
128
+
129
+ def self.validate_name_structure(name)
130
+ return unless name.is_a?(Hash)
131
+
132
+ valid_keys = %w[formatted familyName givenName middleName honorificPrefix honorificSuffix]
133
+ invalid = name.keys - valid_keys
134
+
135
+ return unless invalid.any?
136
+
137
+ raise ArgumentError, "Invalid name attributes: #{invalid.join(', ')}"
138
+ end
139
+
140
+ def self.validate_multi_valued(array, required_keys)
141
+ return unless array.is_a?(Array)
142
+
143
+ array.each_with_index do |item, idx|
144
+ raise ArgumentError, "Multi-valued attribute item #{idx} must be an object" unless item.is_a?(Hash)
145
+
146
+ missing = required_keys - item.keys
147
+ raise ArgumentError, "Multi-valued attribute item #{idx} missing: #{missing.join(', ')}" if missing.any?
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TwoPercent
4
+ module Scim
5
+ autoload :Schema, "two_percent/scim/schema"
6
+ autoload :PatchProcessor, "two_percent/scim/patch_processor"
7
+ end
8
+ end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TwoPercent
4
+ # Syncable concern for syncing SCIM data to domain models
5
+ #
6
+ # This concern provides one-way synchronization from SCIM to your domain models,
7
+ # ensuring SCIM remains the source of truth for identity data.
8
+ #
9
+ # Usage:
10
+ # class User < ApplicationRecord
11
+ # include TwoPercent::Syncable
12
+ #
13
+ # syncable_as :user, scim_id_column: :scim_id do |scim_attrs|
14
+ # {
15
+ # first_name: scim_attrs.dig(:name, :givenName),
16
+ # last_name: scim_attrs.dig(:name, :familyName),
17
+ # email: scim_attrs[:email],
18
+ # active: scim_attrs[:active]
19
+ # }
20
+ # end
21
+ # end
22
+ #
23
+ # class Group < ApplicationRecord
24
+ # include TwoPercent::Syncable
25
+ #
26
+ # syncable_as :group, scim_id_column: :scim_id do |scim_attrs|
27
+ # { name: scim_attrs[:display_name], active: scim_attrs[:active] }
28
+ # end
29
+ # end
30
+ #
31
+ # This provides:
32
+ # - user.scim_user => linked ScimUser record
33
+ # - user.refresh_from_scim => pull latest data from SCIM
34
+ # - User.sync_from_scim_event(event) => sync from SCIM domain events
35
+ #
36
+ module Syncable
37
+ # Encapsulates type-specific SCIM synchronization logic
38
+ #
39
+ # This object replaces case statements and conditionals in the Syncable concern
40
+ # by encapsulating all knowledge about User vs Group differences.
41
+ #
42
+ class Model
43
+ attr_reader :scim_model_class, :scim_id_column, :resource_type, :options, :attribute_mapper_block
44
+
45
+ def initialize(scim_model_class:, scim_id_column:, resource_type:, **options, &block)
46
+ @scim_model_class = scim_model_class
47
+ @scim_id_column = scim_id_column
48
+ @resource_type = resource_type
49
+ @options = options
50
+ @attribute_mapper_block = block
51
+ end
52
+
53
+ # Setup association and validations on the domain model class
54
+ #
55
+ # @param domain_model_class [Class] The ActiveRecord model including Syncable
56
+ def setup_association(domain_model_class)
57
+ if scim_model_class == TwoPercent::ScimUser
58
+ setup_user_syncable(domain_model_class)
59
+ else
60
+ setup_group_syncable(domain_model_class)
61
+ end
62
+ end
63
+
64
+ # Setup ScimUser association and validations
65
+ #
66
+ # @param domain_model_class [Class] The ActiveRecord model including Syncable
67
+ def setup_user_syncable(domain_model_class)
68
+ domain_model_class.belongs_to :scim_user,
69
+ class_name: "TwoPercent::ScimUser",
70
+ foreign_key: scim_id_column,
71
+ primary_key: "scim_id",
72
+ optional: true
73
+
74
+ domain_model_class.validates scim_id_column, uniqueness: true, allow_nil: true
75
+ end
76
+
77
+ # Setup ScimGroup association and validations
78
+ #
79
+ # @param domain_model_class [Class] The ActiveRecord model including Syncable
80
+ def setup_group_syncable(domain_model_class)
81
+ domain_model_class.belongs_to :scim_group,
82
+ class_name: "TwoPercent::ScimGroup",
83
+ foreign_key: scim_id_column,
84
+ primary_key: "scim_id",
85
+ optional: true
86
+
87
+ domain_model_class.validates scim_id_column, uniqueness: true, allow_nil: true
88
+ end
89
+
90
+ # Sync created event to domain model
91
+ #
92
+ # @param attributes [Hash] SCIM attributes from event
93
+ # @param domain_model_class [Class] The domain model class
94
+ # @return [ActiveRecord::Base] The synced record
95
+ def sync_created(attributes, domain_model_class)
96
+ sync_upsert(attributes, domain_model_class)
97
+ end
98
+
99
+ # Sync updated event to domain model
100
+ #
101
+ # @param attributes [Hash] SCIM attributes from event
102
+ # @param domain_model_class [Class] The domain model class
103
+ # @return [ActiveRecord::Base] The synced record
104
+ def sync_updated(attributes, domain_model_class)
105
+ sync_upsert(attributes, domain_model_class)
106
+ end
107
+
108
+ # Sync deleted event to domain model
109
+ #
110
+ # @param scim_id [String] SCIM ID of deleted resource
111
+ # @param domain_model_class [Class] The domain model class
112
+ # @return [ActiveRecord::Base, nil] The destroyed record
113
+ def sync_deleted(scim_id, domain_model_class)
114
+ record = domain_model_class.find_by(scim_id_column => scim_id)
115
+ record&.destroy
116
+ end
117
+
118
+ private
119
+
120
+ # Shared logic for created/updated events
121
+ def sync_upsert(attributes, domain_model_class)
122
+ scim_id = attributes[:scim_id]
123
+ return unless scim_id
124
+
125
+ unless attribute_mapper_block
126
+ raise ArgumentError, "No attribute mapper block provided. Define one in syncable_as."
127
+ end
128
+
129
+ record = domain_model_class.find_or_initialize_by(scim_id_column => scim_id)
130
+ mapped_attrs = attribute_mapper_block.call(attributes)
131
+ record.assign_attributes(mapped_attrs)
132
+ record.save! if record.changed?
133
+ record
134
+ end
135
+ end
136
+
137
+ extend ActiveSupport::Concern
138
+
139
+ included do
140
+ class_attribute :syncable_model
141
+ end
142
+
143
+ class_methods do
144
+ # Configure this model as syncable with SCIM
145
+ #
146
+ # @param type [Symbol] :user or :group
147
+ # @param scim_id_column [Symbol] Column storing SCIM ID (default: :scim_id)
148
+ # @param options [Hash] Additional configuration options
149
+ # @option options [String] :resource_type Resource type for groups (default: "Groups")
150
+ # @param block [Proc] Required block for custom attribute mapping from SCIM to domain
151
+ #
152
+ def syncable_as(type, scim_id_column: :scim_id, **options, &block)
153
+ scim_model_class = type == :user ? TwoPercent::ScimUser : TwoPercent::ScimGroup
154
+
155
+ self.syncable_model = Model.new(
156
+ scim_model_class: scim_model_class,
157
+ scim_id_column: scim_id_column,
158
+ resource_type: type,
159
+ **options,
160
+ &block
161
+ )
162
+
163
+ syncable_model.setup_association(self)
164
+ end
165
+
166
+ # Sync from a SCIM domain event
167
+ #
168
+ # Uses polymorphic dispatch - events know how to apply themselves
169
+ #
170
+ # @param event [TwoPercent::Domain::Events::Base] Domain event
171
+ # @return [ActiveRecord::Base, nil] The affected record, if any
172
+ #
173
+ def sync_from_scim_event(event)
174
+ event.apply_to_model(self)
175
+ end
176
+ end
177
+
178
+ # Instance methods
179
+
180
+ # Refresh this record from SCIM data
181
+ def refresh_from_scim
182
+ model = self.class.syncable_model
183
+ association_name = model.scim_model_class == TwoPercent::ScimUser ? :scim_user : :scim_group
184
+ scim_record = public_send(association_name)
185
+
186
+ return unless scim_record
187
+
188
+ unless model.attribute_mapper_block
189
+ raise ArgumentError, "No attribute mapper block provided. Define one in syncable_as."
190
+ end
191
+
192
+ attrs = scim_record.to_domain_attributes
193
+ mapped_attrs = model.attribute_mapper_block.call(attrs)
194
+ assign_attributes(mapped_attrs)
195
+ save! if changed?
196
+ end
197
+ end
198
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TwoPercent
4
- VERSION = "0.5.0"
4
+ VERSION = "1.0.0"
5
5
  end
data/lib/two_percent.rb CHANGED
@@ -4,8 +4,10 @@ require "aether_observatory"
4
4
 
5
5
  require "two_percent/version"
6
6
  require "two_percent/configuration"
7
- require "two_percent/event_handler"
7
+ require "two_percent/domain"
8
8
  require "two_percent/bulk_processor"
9
+ require "two_percent/scim"
10
+ require "two_percent/syncable"
9
11
 
10
12
  module TwoPercent
11
13
  # Logger used by TwoPercent. Defaults to Rails.logger
metadata CHANGED
@@ -1,16 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: two_percent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carlos Palhares
8
8
  - Katie Edgar
9
9
  - Dan Smith
10
- autorequire:
11
10
  bindir: bin
12
11
  cert_chain: []
13
- date: 2025-05-29 00:00:00.000000000 Z
12
+ date: 1980-01-02 00:00:00.000000000 Z
14
13
  dependencies:
15
14
  - !ruby/object:Gem::Dependency
16
15
  name: aether_observatory
@@ -40,8 +39,10 @@ dependencies:
40
39
  - - ">="
41
40
  - !ruby/object:Gem::Version
42
41
  version: '6.1'
43
- description: Adds a thin SCIM interface, and delegates the actions taken on write
44
- calls to observers
42
+ description: TwoPercent is a SCIM 2.0 Rails Engine with partial RFC 7644 protocol
43
+ compliance for IdP provisioning. Supports POST/PUT/PATCH/DELETE/Bulk operations
44
+ for Users and Groups, publishes domain events via AetherObservatory, and integrates
45
+ seamlessly with Rails applications.
45
46
  email:
46
47
  - carlos.palhares@powerhrg.com
47
48
  - katie.weber@powerhrg.com
@@ -55,18 +56,31 @@ files:
55
56
  - app/controllers/two_percent/application_controller.rb
56
57
  - app/controllers/two_percent/bulk_controller.rb
57
58
  - app/controllers/two_percent/scim_controller.rb
58
- - app/events/two_percent/application_event.rb
59
- - app/events/two_percent/create_event.rb
60
- - app/events/two_percent/delete_event.rb
61
- - app/events/two_percent/replace_event.rb
62
- - app/events/two_percent/update_event.rb
59
+ - app/models/two_percent/application_record.rb
60
+ - app/models/two_percent/scim_group.rb
61
+ - app/models/two_percent/scim_group_membership.rb
62
+ - app/models/two_percent/scim_user.rb
63
63
  - config/routes.rb
64
+ - lib/generators/two_percent/install/install_generator.rb
65
+ - lib/generators/two_percent/install/templates/INSTALL_README
66
+ - lib/generators/two_percent/install/templates/create_two_percent_scim_group_memberships.rb.erb
67
+ - lib/generators/two_percent/install/templates/create_two_percent_scim_groups.rb.erb
68
+ - lib/generators/two_percent/install/templates/create_two_percent_scim_users.rb.erb
69
+ - lib/generators/two_percent/install/templates/two_percent.rb.erb
64
70
  - lib/tasks/two_percent_tasks.rake
65
71
  - lib/two_percent.rb
66
72
  - lib/two_percent/bulk_processor.rb
67
73
  - lib/two_percent/configuration.rb
74
+ - lib/two_percent/domain.rb
75
+ - lib/two_percent/domain/events.rb
76
+ - lib/two_percent/domain/events/base_event.rb
77
+ - lib/two_percent/domain/events/group_events.rb
78
+ - lib/two_percent/domain/events/user_events.rb
68
79
  - lib/two_percent/engine.rb
69
- - lib/two_percent/event_handler.rb
80
+ - lib/two_percent/scim.rb
81
+ - lib/two_percent/scim/patch_processor.rb
82
+ - lib/two_percent/scim/schema.rb
83
+ - lib/two_percent/syncable.rb
70
84
  - lib/two_percent/version.rb
71
85
  homepage: https://github.com/powerhome/power-tools
72
86
  licenses:
@@ -76,7 +90,6 @@ metadata:
76
90
  source_code_uri: https://github.com/powerhome/power-tools
77
91
  changelog_uri: https://github.com/powerhome/power-tools/blob/main/packages/two_percent/docs/CHANGELOG.md
78
92
  rubygems_mfa_required: 'true'
79
- post_install_message:
80
93
  rdoc_options: []
81
94
  require_paths:
82
95
  - lib
@@ -91,9 +104,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
91
104
  - !ruby/object:Gem::Version
92
105
  version: '0'
93
106
  requirements: []
94
- rubygems_version: 3.5.22
95
- signing_key:
107
+ rubygems_version: 3.6.9
96
108
  specification_version: 4
97
- summary: Adds a thin SCIM interface, and delegates the actions taken on write calls
98
- to observers
109
+ summary: SCIM 2.0 Rails Engine for identity provisioning with domain event publishing
99
110
  test_files: []
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TwoPercent
4
- class ApplicationEvent < AetherObservatory::EventBase
5
- event_prefix "two_percent.scim"
6
- end
7
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TwoPercent
4
- class CreateEvent < ApplicationEvent
5
- event_name "create.all"
6
- event_name { "create.#{resource}" }
7
-
8
- attribute :resource
9
- attribute :params
10
- end
11
- end