Class: TwoPercent::ScimGroup

Inherits:
ApplicationRecord show all
Defined in:
app/models/two_percent/scim_group.rb

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.destroy_by_scim_id(scim_id) ⇒ Object



62
63
64
# File 'app/models/two_percent/scim_group.rb', line 62

def self.destroy_by_scim_id(scim_id)
  find_by_scim_id(scim_id)&.destroy
end

.exists_by_scim_id?(scim_id) ⇒ Boolean

Returns:

  • (Boolean)


58
59
60
# File 'app/models/two_percent/scim_group.rb', line 58

def self.exists_by_scim_id?(scim_id)
  exists?(scim_id: scim_id)
end

.find_by_scim_id(scim_id) ⇒ Object



54
55
56
# File 'app/models/two_percent/scim_group.rb', line 54

def self.find_by_scim_id(scim_id)
  find_by(scim_id: scim_id)
end

.upsert_from_scim(resource_type, scim_hash, correlation_id: nil) ⇒ TwoPercent::ScimGroup

Creates or updates a group from SCIM data

Generates a UUID for the id field if not present (for POST/create operations). Validates the SCIM data against the Group schema before persisting.

Parameters:

  • resource_type (String)

    The resource type (e.g., "Groups", "Departments", "Territories")

  • scim_hash (Hash)

    SCIM Group resource hash conforming to RFC 7643

  • correlation_id (String, nil) (defaults to: nil)

    Optional correlation ID for tracking changes across network hops (e.g., App A -> App B -> App C)

Returns:

Raises:

  • (TwoPercent::Scim::ValidationError)

    If SCIM data fails schema validation



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'app/models/two_percent/scim_group.rb', line 32

def self.upsert_from_scim(resource_type, 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 members before validation to prevent storage in scim_data JSONB
  members = scim_hash.delete("members")

  validated_data = TwoPercent::Scim::Schema.validate_group(scim_hash, require_id: true)

  # Wrap in transaction to ensure rollback on member validation failure
  transaction do
    scim_group = find_or_initialize_by(scim_id: scim_hash["id"])
    scim_group.update_from_scim!(resource_type, validated_data, correlation_id: correlation_id)

    # Sync members to join table only (never stored in scim_data)
    scim_group.replace_members(members) if members

    scim_group
  end
end

Instance Method Details

#members_for_patchArray<Hash>

Build members array with value field only (for PatchProcessor) Uses pluck to avoid loading full AR objects

Returns:

  • (Array<Hash>)

    Array of member values



164
165
166
167
168
169
# File 'app/models/two_percent/scim_group.rb', line 164

def members_for_patch
  scim_group_memberships
    .joins(:scim_user)
    .pluck("two_percent_scim_users.scim_id")
    .map { |id| { "value" => id } }
end

#members_representationArray<Hash>

Build SCIM members representation from join table Optimized to bypass ActiveRecord and load only needed columns

Returns:

  • (Array<Hash>)

    Array of member references



147
148
149
150
151
152
153
154
155
156
157
158
# File 'app/models/two_percent/scim_group.rb', line 147

def members_representation
  scim_group_memberships
    .joins(:scim_user)
    .pluck("two_percent_scim_users.scim_id", "two_percent_scim_users.display_name")
    .map do |scim_id, display_name|
      {
        "value" => scim_id,
        "display" => display_name,
        "$ref" => "Users/#{scim_id}",
      }
    end
end

#replace_members(members_array) ⇒ Object



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'app/models/two_percent/scim_group.rb', line 113

def replace_members(members_array)
  member_scim_ids = members_array.filter_map { |m| m["value"] }

  # Get current member scim_ids efficiently (just IDs, no full records)
  current_member_scim_ids = scim_group_memberships
                            .joins(:scim_user)
                            .pluck("two_percent_scim_users.scim_id")

  # Calculate diff in Ruby (cheap for ID arrays)
  scim_ids_to_add = member_scim_ids - current_member_scim_ids
  scim_ids_to_remove = current_member_scim_ids - member_scim_ids

  # Only validate and add NEW members (not existing ones)
  add_members_by_scim_id(scim_ids_to_add) if scim_ids_to_add.any?

  # Only remove members that need removing
  remove_members_by_scim_id(scim_ids_to_remove) if scim_ids_to_remove.any?
end

#scim_attribute(path) ⇒ Object?

Extracts a nested attribute from the scim_data JSON

Examples:

group.scim_attribute("members.0.value") # => "user-id-123"

Parameters:

  • path (String)

    Dot-separated path to the attribute (e.g., "displayName")

Returns:

  • (Object, nil)

    The attribute value or nil if not found



138
139
140
141
# File 'app/models/two_percent/scim_group.rb', line 138

def scim_attribute(path)
  keys = path.split(".")
  scim_data.dig(*keys)
end

#to_domain_attributesHash

Extracts domain attributes for publishing in domain events

Returns key attributes for event payloads. Members are NOT included - consumers should query TwoPercent models directly for current state.

Returns:

  • (Hash)

    Domain attributes



71
72
73
74
75
76
77
78
79
# File 'app/models/two_percent/scim_group.rb', line 71

def to_domain_attributes
  {
    scim_id: scim_id,
    external_id: external_id,
    display_name: display_name,
    resource_type: resource_type,
    active: active,
  }.compact
end

#to_scim_representationHash

Returns full SCIM representation for HTTP responses

Returns:

  • (Hash)

    RFC 7644 compliant SCIM Group resource



84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'app/models/two_percent/scim_group.rb', line 84

def to_scim_representation
  representation = scim_data.merge(
    "id" => scim_id,
    "meta" => {
      "resourceType" => resource_type,
      "created" => created_at.iso8601,
      "lastModified" => updated_at.iso8601,
    }
  )

  representation["members"] = members_representation if scim_users.loaded?
  representation
end

#update_from_scim!(resource_type, validated_data, correlation_id: nil) ⇒ Object



98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'app/models/two_percent/scim_group.rb', line 98

def update_from_scim!(resource_type, validated_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.display_name = core_data["displayName"]
  self.resource_type = resource_type

  extension_data = validated_data[:extensions]
  self.active = extension_data.dig("urn:ietf:params:scim:schemas:extension:authservice:2.0:Group",
                                   "active") != false
  self.correlation_id = correlation_id
  save!
end