Class: TwoPercent::Scim::Schema

Inherits:
Object
  • Object
show all
Defined in:
lib/two_percent/scim/schema.rb

Overview

SCIM Schema definition based on RFC 7644

Constant Summary collapse

CORE_USER_SCHEMA =
"urn:ietf:params:scim:schemas:core:2.0:User"
CORE_GROUP_SCHEMA =
"urn:ietf:params:scim:schemas:core:2.0:Group"
EXTENSION_SCHEMA =
"urn:ietf:params:scim:schemas:extension:authservice:2.0:User"
CORE_USER_ATTRIBUTES =

Core User attributes per RFC 7644 Section 4.1

%w[
  id
  externalId
  userName
  displayName
  name
  emails
  phoneNumbers
  addresses
  photos
  userType
  title
  active
  groups
  meta
  schemas
].freeze
CORE_GROUP_ATTRIBUTES =

Core Group attributes per RFC 7644 Section 4.2

%w[
  id
  externalId
  displayName
  members
  meta
  schemas
].freeze
EXTENSION_USER_ATTRIBUTES =

Extension attributes (custom per IDP)

%w[
  department
  territory
  territoryAbbr
  role
  mfaRequired
].freeze

Class Method Summary collapse

Class Method Details

.extract_core_attributes(scim_hash, allowed_attrs) ⇒ Object



87
88
89
# File 'lib/two_percent/scim/schema.rb', line 87

def self.extract_core_attributes(scim_hash, allowed_attrs)
  scim_hash.slice(*allowed_attrs)
end

.extract_extensions(scim_hash) ⇒ Object



91
92
93
# File 'lib/two_percent/scim/schema.rb', line 91

def self.extract_extensions(scim_hash)
  scim_hash.select { |key, _| key.start_with?("urn:ietf:params:scim:schemas:extension:") }
end

.normalize_group(scim_hash) ⇒ Object



80
81
82
83
84
85
# File 'lib/two_percent/scim/schema.rb', line 80

def self.normalize_group(scim_hash)
  {
    core: extract_core_attributes(scim_hash, CORE_GROUP_ATTRIBUTES),
    extensions: extract_extensions(scim_hash),
  }
end

.normalize_user(scim_hash) ⇒ Object



73
74
75
76
77
78
# File 'lib/two_percent/scim/schema.rb', line 73

def self.normalize_user(scim_hash)
  {
    core: extract_core_attributes(scim_hash, CORE_USER_ATTRIBUTES),
    extensions: extract_extensions(scim_hash),
  }
end

.validate_attribute_types(scim_hash) ⇒ Object



120
121
122
123
124
125
126
127
# File 'lib/two_percent/scim/schema.rb', line 120

def self.validate_attribute_types(scim_hash)
  # Validate complex attribute structures
  validate_name_structure(scim_hash["name"]) if scim_hash["name"]
  validate_multi_valued(scim_hash["emails"], %w[value type]) if scim_hash["emails"]
  validate_multi_valued(scim_hash["phoneNumbers"], %w[value type]) if scim_hash["phoneNumbers"]
  validate_multi_valued(scim_hash["addresses"], %w[type]) if scim_hash["addresses"]
  validate_multi_valued(scim_hash["photos"], %w[value type]) if scim_hash["photos"]
end

.validate_group(scim_hash, require_id: true) ⇒ Object



62
63
64
65
66
67
68
69
70
71
# File 'lib/two_percent/scim/schema.rb', line 62

def self.validate_group(scim_hash, require_id: true)
  # Accept either core schema or extension schemas
  validate_schemas_present(scim_hash)

  # Only require id for updates, not creation
  required_attrs = require_id ? %w[id displayName] : %w[displayName]
  validate_required_attributes(scim_hash, required_attrs)

  normalize_group(scim_hash)
end

.validate_multi_valued(array, required_keys) ⇒ Object



140
141
142
143
144
145
146
147
148
149
# File 'lib/two_percent/scim/schema.rb', line 140

def self.validate_multi_valued(array, required_keys)
  return unless array.is_a?(Array)

  array.each_with_index do |item, idx|
    raise ArgumentError, "Multi-valued attribute item #{idx} must be an object" unless item.is_a?(Hash)

    missing = required_keys - item.keys
    raise ArgumentError, "Multi-valued attribute item #{idx} missing: #{missing.join(', ')}" if missing.any?
  end
end

.validate_name_structure(name) ⇒ Object

Raises:

  • (ArgumentError)


129
130
131
132
133
134
135
136
137
138
# File 'lib/two_percent/scim/schema.rb', line 129

def self.validate_name_structure(name)
  return unless name.is_a?(Hash)

  valid_keys = %w[formatted familyName givenName middleName honorificPrefix honorificSuffix]
  invalid = name.keys - valid_keys

  return unless invalid.any?

  raise ArgumentError, "Invalid name attributes: #{invalid.join(', ')}"
end

.validate_required_attributes(scim_hash, required_attrs) ⇒ Object

Raises:

  • (ArgumentError)


112
113
114
115
116
117
118
# File 'lib/two_percent/scim/schema.rb', line 112

def self.validate_required_attributes(scim_hash, required_attrs)
  missing = required_attrs.select { |attr| scim_hash[attr].nil? }

  return unless missing.any?

  raise ArgumentError, "Missing required attributes: #{missing.join(', ')}"
end

.validate_schemas(scim_hash, required_schemas) ⇒ Object

Raises:

  • (ArgumentError)


95
96
97
98
99
100
101
102
# File 'lib/two_percent/scim/schema.rb', line 95

def self.validate_schemas(scim_hash, required_schemas)
  schemas = scim_hash["schemas"] || []
  missing = required_schemas - schemas

  return unless missing.any?

  raise ArgumentError, "Missing required schemas: #{missing.join(', ')}"
end

.validate_schemas_present(scim_hash) ⇒ Object

Raises:

  • (ArgumentError)


104
105
106
107
108
109
110
# File 'lib/two_percent/scim/schema.rb', line 104

def self.validate_schemas_present(scim_hash)
  schemas = scim_hash["schemas"] || []

  return unless schemas.empty?

  raise ArgumentError, "schemas attribute is required"
end

.validate_user(scim_hash, require_id: true) ⇒ Object



49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/two_percent/scim/schema.rb', line 49

def self.validate_user(scim_hash, require_id: true)
  # Accept either core schema or extension schemas
  validate_schemas_present(scim_hash)

  # Only require id for updates, not creation
  required_attrs = require_id ? %w[id externalId] : %w[externalId]
  validate_required_attributes(scim_hash, required_attrs)
  validate_attribute_types(scim_hash)

  # Return validated data with schemas normalized
  normalize_user(scim_hash)
end