Class: TwoPercent::ScimUser
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- TwoPercent::ScimUser
- Defined in:
- app/models/two_percent/scim_user.rb
Class Method Summary collapse
- .destroy_by_scim_id(scim_id) ⇒ Object
- .exists_by_scim_id?(scim_id) ⇒ Boolean
- .find_by_scim_id(scim_id) ⇒ Object
-
.upsert_from_scim(scim_hash, correlation_id: nil) ⇒ TwoPercent::ScimUser
Creates or updates a user from SCIM data.
Instance Method Summary collapse
- #extension_attributes(schema_urn = nil) ⇒ Object
-
#groups_representation ⇒ Array<Hash>
Build SCIM groups representation dynamically from join table Per RFC 7643 Section 4.1.2, User.groups is read-only and must reflect Group memberships.
-
#scim_attribute(path) ⇒ Object?
Extracts a nested attribute from the scim_data JSON.
-
#sync_groups(groups_data) ⇒ Object
Syncs user's group memberships from SCIM groups array.
-
#to_domain_attributes ⇒ Hash
Extracts domain attributes for publishing in domain events.
-
#to_scim_representation ⇒ Hash
Returns full SCIM representation for HTTP responses.
-
#update_from_scim!(validated_data, groups_data, correlation_id: nil) ⇒ Object
Updates user attributes from validated SCIM data.
Class Method Details
.destroy_by_scim_id(scim_id) ⇒ Object
51 52 53 |
# File 'app/models/two_percent/scim_user.rb', line 51 def self.destroy_by_scim_id(scim_id) find_by_scim_id(scim_id)&.destroy end |
.exists_by_scim_id?(scim_id) ⇒ Boolean
47 48 49 |
# File 'app/models/two_percent/scim_user.rb', line 47 def self.exists_by_scim_id?(scim_id) exists?(scim_id: scim_id) end |
.find_by_scim_id(scim_id) ⇒ Object
43 44 45 |
# File 'app/models/two_percent/scim_user.rb', line 43 def self.find_by_scim_id(scim_id) find_by(scim_id: scim_id) end |
.upsert_from_scim(scim_hash, correlation_id: nil) ⇒ TwoPercent::ScimUser
Creates or updates a user from SCIM data
Generates a UUID for the id field if not present (for POST/create operations). Validates the SCIM data against the User schema before persisting.
28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
# File 'app/models/two_percent/scim_user.rb', line 28 def self.upsert_from_scim(scim_hash, correlation_id: nil) # Generate SCIM id (stored as scim_id) if not provided (typical for POST operations) scim_hash = scim_hash.dup scim_hash["id"] ||= SecureRandom.uuid # Extract groups before validation to prevent storage in scim_data JSONB # Groups are synced to join table only (single source of truth) groups = scim_hash.delete("groups") validated_data = TwoPercent::Scim::Schema.validate_user(scim_hash, require_id: true) scim_user = find_or_initialize_by(scim_id: scim_hash["id"]) scim_user.update_from_scim!(validated_data, groups, correlation_id: correlation_id) scim_user end |
Instance Method Details
#extension_attributes(schema_urn = nil) ⇒ Object
161 162 163 164 165 166 167 |
# File 'app/models/two_percent/scim_user.rb', line 161 def extension_attributes(schema_urn = nil) if schema_urn scim_data[schema_urn] || {} else scim_data.select { |k, _| k.start_with?("urn:ietf:params:scim:schemas:extension:") } end end |
#groups_representation ⇒ Array<Hash>
Build SCIM groups representation dynamically from join table Per RFC 7643 Section 4.1.2, User.groups is read-only and must reflect Group memberships
96 97 98 99 100 101 102 103 104 105 |
# File 'app/models/two_percent/scim_user.rb', line 96 def groups_representation scim_groups.map do |group| { "value" => group.scim_id, "display" => group.display_name, "$ref" => "#{group.resource_type}/#{group.scim_id}", "type" => group.resource_type, } end end |
#scim_attribute(path) ⇒ Object?
Extracts a nested attribute from the scim_data JSON
156 157 158 159 |
# File 'app/models/two_percent/scim_user.rb', line 156 def scim_attribute(path) keys = path.split(".") scim_data.dig(*keys) end |
#sync_groups(groups_data) ⇒ Object
Syncs user's group memberships from SCIM groups array
Accepts a groups array from SCIM User payload and updates associations. This enables bulk sync operations where clients send User.groups arrays.
142 143 144 145 146 147 148 |
# File 'app/models/two_percent/scim_user.rb', line 142 def sync_groups(groups_data) return if groups_data.nil? group_ids = groups_data.filter_map { |g| g["value"] } groups = TwoPercent::ScimGroup.where(scim_id: group_ids) self.scim_groups = groups end |
#to_domain_attributes ⇒ Hash
Extracts domain attributes for publishing in domain events
Returns key attributes for event payloads (thin events - no associations). Group memberships are queried separately by consumers who need them.
61 62 63 64 65 66 67 68 69 70 |
# File 'app/models/two_percent/scim_user.rb', line 61 def to_domain_attributes { scim_id: scim_id, external_id: external_id, user_name: user_name, display_name: display_name, email: email, active: active, }.compact end |
#to_scim_representation ⇒ Hash
Returns full SCIM representation for HTTP responses
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'app/models/two_percent/scim_user.rb', line 75 def to_scim_representation representation = scim_data.merge( "id" => scim_id, "meta" => { "resourceType" => "User", "created" => created_at.iso8601, "lastModified" => updated_at.iso8601, } ) # Build groups dynamically from join table (RFC 7643: User.groups is read-only) # Single source of truth is join table - only included when association is loaded representation["groups"] = groups_representation if scim_groups.loaded? representation end |
#update_from_scim!(validated_data, groups_data, correlation_id: nil) ⇒ Object
Updates user attributes from validated SCIM data
NOTE: This method processes the User.groups array from the request payload and syncs group memberships, which is a deviation from strict RFC 7643 compliance (User.groups is specified as read-only in Section 4.1.2). This behavior is intentionally maintained for compatibility with existing SCIM clients that rely on this functionality for bulk sync operations.
118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'app/models/two_percent/scim_user.rb', line 118 def update_from_scim!(validated_data, groups_data, correlation_id: nil) core_data = validated_data[:core] self.scim_data = core_data.merge(validated_data[:extensions]) self.scim_id = core_data["id"] self.external_id = core_data["externalId"] self.user_name = core_data["userName"] self.display_name = core_data["displayName"] self.email = core_data.dig("emails", 0, "value") self.active = core_data.fetch("active", true) self.correlation_id = correlation_id save! # Sync groups to join table only (never stored in scim_data) sync_groups(groups_data) if groups_data end |