scimitar 1.8.1 → 1.10.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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +27 -20
  3. data/app/controllers/scimitar/active_record_backed_resources_controller.rb +5 -4
  4. data/app/controllers/scimitar/resource_types_controller.rb +0 -2
  5. data/app/controllers/scimitar/resources_controller.rb +0 -2
  6. data/app/controllers/scimitar/schemas_controller.rb +361 -3
  7. data/app/controllers/scimitar/service_provider_configurations_controller.rb +0 -1
  8. data/app/models/scimitar/engine_configuration.rb +3 -1
  9. data/app/models/scimitar/lists/query_parser.rb +88 -3
  10. data/app/models/scimitar/resources/base.rb +48 -14
  11. data/app/models/scimitar/resources/mixin.rb +531 -71
  12. data/app/models/scimitar/schema/name.rb +2 -2
  13. data/app/models/scimitar/schema/user.rb +10 -10
  14. data/config/initializers/scimitar.rb +41 -0
  15. data/lib/scimitar/engine.rb +57 -12
  16. data/lib/scimitar/support/hash_with_indifferent_case_insensitive_access.rb +140 -10
  17. data/lib/scimitar/support/utilities.rb +60 -0
  18. data/lib/scimitar/version.rb +2 -2
  19. data/spec/apps/dummy/app/models/mock_user.rb +18 -3
  20. data/spec/apps/dummy/config/initializers/scimitar.rb +31 -2
  21. data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +1 -0
  22. data/spec/apps/dummy/db/schema.rb +1 -0
  23. data/spec/controllers/scimitar/schemas_controller_spec.rb +342 -54
  24. data/spec/models/scimitar/lists/query_parser_spec.rb +70 -0
  25. data/spec/models/scimitar/resources/base_spec.rb +20 -12
  26. data/spec/models/scimitar/resources/base_validation_spec.rb +16 -3
  27. data/spec/models/scimitar/resources/mixin_spec.rb +754 -122
  28. data/spec/models/scimitar/schema/user_spec.rb +2 -2
  29. data/spec/requests/active_record_backed_resources_controller_spec.rb +312 -5
  30. data/spec/requests/engine_spec.rb +75 -0
  31. data/spec/spec_helper.rb +1 -1
  32. data/spec/support/hash_with_indifferent_case_insensitive_access_spec.rb +108 -0
  33. metadata +22 -22
@@ -6,8 +6,8 @@ module Scimitar
6
6
 
7
7
  def self.scim_attributes
8
8
  @scim_attributes ||= [
9
- Attribute.new(name: 'familyName', type: 'string', required: true),
10
- Attribute.new(name: 'givenName', type: 'string', required: true),
9
+ Attribute.new(name: 'familyName', type: 'string'),
10
+ Attribute.new(name: 'givenName', type: 'string'),
11
11
  Attribute.new(name: 'middleName', type: 'string'),
12
12
  Attribute.new(name: 'formatted', type: 'string'),
13
13
  Attribute.new(name: 'honorificPrefix', type: 'string'),
@@ -20,7 +20,7 @@ module Scimitar
20
20
  [
21
21
  Attribute.new(name: 'userName', type: 'string', uniqueness: 'server', required: true),
22
22
 
23
- Attribute.new(name: 'name', complexType: Scimitar::ComplexTypes::Name),
23
+ Attribute.new(name: 'name', complexType: Scimitar::ComplexTypes::Name),
24
24
 
25
25
  Attribute.new(name: 'displayName', type: 'string'),
26
26
  Attribute.new(name: 'nickName', type: 'string'),
@@ -35,15 +35,15 @@ module Scimitar
35
35
 
36
36
  Attribute.new(name: 'password', type: 'string', mutability: 'writeOnly', returned: 'never'),
37
37
 
38
- Attribute.new(name: 'emails', multiValued: true, complexType: Scimitar::ComplexTypes::Email),
39
- Attribute.new(name: 'phoneNumbers', multiValued: true, complexType: Scimitar::ComplexTypes::PhoneNumber),
40
- Attribute.new(name: 'ims', multiValued: true, complexType: Scimitar::ComplexTypes::Ims),
41
- Attribute.new(name: 'photos', multiValued: true, complexType: Scimitar::ComplexTypes::Photo),
42
- Attribute.new(name: 'addresses', multiValued: true, complexType: Scimitar::ComplexTypes::Address),
43
- Attribute.new(name: 'groups', multiValued: true, complexType: Scimitar::ComplexTypes::ReferenceGroup, mutability: 'readOnly'),
44
- Attribute.new(name: 'entitlements', multiValued: true, complexType: Scimitar::ComplexTypes::Entitlement),
45
- Attribute.new(name: 'roles', multiValued: true, complexType: Scimitar::ComplexTypes::Role),
46
- Attribute.new(name: 'x509Certificates', multiValued: true, complexType: Scimitar::ComplexTypes::X509Certificate),
38
+ Attribute.new(name: 'emails', multiValued: true, complexType: Scimitar::ComplexTypes::Email),
39
+ Attribute.new(name: 'phoneNumbers', multiValued: true, complexType: Scimitar::ComplexTypes::PhoneNumber),
40
+ Attribute.new(name: 'ims', multiValued: true, complexType: Scimitar::ComplexTypes::Ims),
41
+ Attribute.new(name: 'photos', multiValued: true, complexType: Scimitar::ComplexTypes::Photo),
42
+ Attribute.new(name: 'addresses', multiValued: true, complexType: Scimitar::ComplexTypes::Address),
43
+ Attribute.new(name: 'groups', multiValued: true, complexType: Scimitar::ComplexTypes::ReferenceGroup, mutability: 'readOnly'),
44
+ Attribute.new(name: 'entitlements', multiValued: true, complexType: Scimitar::ComplexTypes::Entitlement),
45
+ Attribute.new(name: 'roles', multiValued: true, complexType: Scimitar::ComplexTypes::Role),
46
+ Attribute.new(name: 'x509Certificates', multiValued: true, complexType: Scimitar::ComplexTypes::X509Certificate),
47
47
  ]
48
48
  end
49
49
 
@@ -106,6 +106,47 @@ Rails.application.config.to_prepare do # (required for >= Rails 7 / Zeitwerk)
106
106
  # whatever that means for you receiving system in your model code.
107
107
  #
108
108
  # optional_value_fields_required: false
109
+
110
+ # The SCIM standard `/Schemas` endpoint lists, by default, all known schema
111
+ # definitions with the mutabilty (read-write, read-only, write-only) state
112
+ # described by those definitions, and includes all defined attributes. For
113
+ # user-defined schema, this will typically exactly match your underlying
114
+ # mapped attribute and model capability - it wouldn't make sense to define
115
+ # your own schema that misrepresented the implementation! For core SCIM RFC
116
+ # schema, though, you might want to only list actually mapped attributes.
117
+ # Further, if you happen to have a non-compliant implementation especially
118
+ # in relation to mutability of some attributes, you may want to report that
119
+ # accurately in the '/Schemas' list, for auto-discovery purposes. To switch
120
+ # to a significantly slower but more accurate render method for the list,
121
+ # driven by your resource subclasses and their attribute maps, set:
122
+ #
123
+ # schema_list_from_attribute_mappings: [...array...]
124
+ #
125
+ # ...where you provide an Array of *models*, your classes that include the
126
+ # Scimitar::Resources::Mixin module and, therefore, define an attribute map
127
+ # translating SCIM schema attributes into actual implemented data. These
128
+ # must *uniquely* describe, via the Scimitar resources they each declare in
129
+ # their Scimitar::Resources::Mixin::scim_resource_type implementation, the
130
+ # set of schemas and extended schemas you want to render. Should resources
131
+ # share schema, the '/Schemas' endpoint will fail since it cannot determine
132
+ # which model attribute map it should use and it needs the map in order to
133
+ # resolve the differences (if any) between what the schema might say, and
134
+ # what the actual underlying model supports.
135
+ #
136
+ # It is further _very_ _strongly_ _recommended_ that, for any
137
+ # +scim_attributes_map+ containing a collection which has "list:" key (for
138
+ # an associative array of zero or more entities; the Groups to which a User
139
+ # might belong is a good example) then you should also specify the "class:"
140
+ # key, giving the class used for objects in that associated collection. The
141
+ # class *must* include Scimitar::Resources::Mixin, since its own attribute
142
+ # map is consulted in order to render the part of the schema describing
143
+ # those associated properties in the owning resource. If you don't do this,
144
+ # and if you're using ActiveRecord, then Scimitar attempts association
145
+ # reflection to determine the collection class - but that's more fragile
146
+ # than just being told the exact class in the attribute map. No matter how
147
+ # this class is determined, though, it must be possible to create a simple
148
+ # instance with +new+ and no parameters, since that's needed in order to
149
+ # call Scimitar::Resources::Mixin#scim_mutable_attributes.
109
150
  })
110
151
 
111
152
  end
@@ -1,15 +1,38 @@
1
+ require 'rails/engine'
2
+
1
3
  module Scimitar
2
4
  class Engine < ::Rails::Engine
3
5
  isolate_namespace Scimitar
4
6
 
7
+ config.autoload_once_paths = %W(
8
+ #{root}/app/controllers
9
+ #{root}/app/models
10
+ )
11
+
5
12
  Mime::Type.register 'application/scim+json', :scim
6
13
 
7
14
  ActionDispatch::Request.parameter_parsers[Mime::Type.lookup('application/scim+json').symbol] = lambda do |body|
8
15
  JSON.parse(body)
9
16
  end
10
17
 
18
+ # Return an Array of all supported default and custom resource classes.
19
+ # See also :add_custom_resource and :set_default_resources.
20
+ #
11
21
  def self.resources
12
- default_resources + custom_resources
22
+ self.default_resources() + self.custom_resources()
23
+ end
24
+
25
+ # Returns a flat array of instances of all resource schema included in the
26
+ # resource classes returned by ::resources.
27
+ #
28
+ def self.schemas
29
+ self.resources().map(&:schemas).flatten.uniq.map(&:new)
30
+ end
31
+
32
+ # Returns the list of custom resources, if any.
33
+ #
34
+ def self.custom_resources
35
+ @custom_resources ||= []
13
36
  end
14
37
 
15
38
  # Can be used to add a new resource type which is not provided by the gem.
@@ -30,7 +53,7 @@ module Scimitar
30
53
  # Scimitar::Engine.add_custom_resource Scim::Resources::ShinyResource
31
54
  #
32
55
  def self.add_custom_resource(resource)
33
- custom_resources << resource
56
+ self.custom_resources() << resource
34
57
  end
35
58
 
36
59
  # Resets the resource list to default. This is really only intended for use
@@ -40,23 +63,45 @@ module Scimitar
40
63
  @custom_resources = []
41
64
  end
42
65
 
43
- # Returns the list of custom resources, if any.
44
- #
45
- def self.custom_resources
46
- @custom_resources ||= []
47
- end
48
-
49
- # Returns the default resources added in this gem:
66
+ # Returns the default resources added in this gem - by default, these are:
50
67
  #
51
68
  # * Scimitar::Resources::User
52
69
  # * Scimitar::Resources::Group
53
70
  #
71
+ # ...but if an implementation does not e.g. support Group, it can
72
+ # be overridden via ::set_default_resources to help with service
73
+ # auto-discovery.
74
+ #
54
75
  def self.default_resources
55
- [ Resources::User, Resources::Group ]
76
+ @standard_default_resources = [ Resources::User, Resources::Group ]
77
+ @default_resources ||= @standard_default_resources.dup()
56
78
  end
57
79
 
58
- def self.schemas
59
- resources.map(&:schemas).flatten.uniq.map(&:new)
80
+ # Override the resources returned by ::default_resources.
81
+ #
82
+ # +resource_array+:: An Array containing one or both of
83
+ # Scimitar::Resources::User and/or
84
+ # Scimitar::Resources::Group, and nothing else.
85
+ #
86
+ def self.set_default_resources(resource_array)
87
+ self.default_resources()
88
+ unrecognised_resources = resource_array - @standard_default_resources
89
+
90
+ if unrecognised_resources.any?
91
+ raise "Scimitar::Engine::set_default_resources: Only #{@standard_default_resources.map(&:name).join(', ')} are supported"
92
+ elsif resource_array.empty?
93
+ raise 'Scimitar::Engine::set_default_resources: At least one resource must be given'
94
+ end
95
+
96
+ @default_resources = resource_array
97
+ end
98
+
99
+ # Resets the default resource list. This is really only intended for use
100
+ # during testing, to avoid one test polluting another.
101
+ #
102
+ def self.reset_default_resources
103
+ self.default_resources()
104
+ @default_resources = @standard_default_resources
60
105
  end
61
106
 
62
107
  end
@@ -23,8 +23,8 @@ class Hash
23
23
  #
24
24
  def self.deep_indifferent_case_insensitive_access(object)
25
25
  if object.is_a?(Hash)
26
- new_hash = Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess.new(object)
27
- new_hash.each do | key, value |
26
+ new_hash = Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess.new
27
+ object.each do | key, value |
28
28
  new_hash[key] = deep_indifferent_case_insensitive_access(value)
29
29
  end
30
30
  new_hash
@@ -49,34 +49,164 @@ module Scimitar
49
49
  # in a case-insensitive fashion too.
50
50
  #
51
51
  # During enumeration, Hash keys will always be returned in whatever case
52
- # they were originally set.
52
+ # they were originally set. Just as with
53
+ # ActiveSupport::HashWithIndifferentAccess, though, the type of the keys is
54
+ # always returned as a String, even if originally set as a Symbol - only
55
+ # the upper/lower case nature of the original key is preserved.
56
+ #
57
+ # If a key is written more than once with the same effective meaning in a
58
+ # to-string, to-downcase form, then whatever case was used *first* wins;
59
+ # e.g. if you did hash['User'] = 23, then hash['USER'] = 42, the result
60
+ # would be {"User" => 42}.
61
+ #
62
+ # It's important to remember that Hash#merge is shallow and replaces values
63
+ # found at existing keys in the target ("this") hash with values in the
64
+ # inbound Hash. If that new value that is itself a Hash, this *replaces*
65
+ # the value. For example:
66
+ #
67
+ # * Original: <tt>'Foo' => { 'Bar' => 42 }</tt>
68
+ # * Merge: <tt>'FOO' => { 'BAR' => 24 }</tt>
69
+ #
70
+ # ...results in "this" target hash's key +Foo+ being addressed in the merge
71
+ # by inbound key +FOO+, so the case doesn't change. But the value for +Foo+
72
+ # is _replaced_ by the merging-in Hash completely:
73
+ #
74
+ # * Result: <tt>'Foo' => { 'BAR' => 24 }</tt>
75
+ #
76
+ # ...and of course we might've replaced with a totally different type, such
77
+ # as +true+:
78
+ #
79
+ # * Original: <tt>'Foo' => { 'Bar' => 42 }</tt>
80
+ # * Merge: <tt>'FOO' => true</tt>
81
+ # * Result: <tt>'Foo' => true</tt>
82
+ #
83
+ # If you're intending to merge nested Hashes, then use ActiveSupport's
84
+ # #deep_merge or an equivalent. This will have the expected outcome, where
85
+ # the hash with 'BAR' is _merged_ into the existing value and, therefore,
86
+ # the original 'Bar' key case is preserved:
87
+ #
88
+ # * Original: <tt>'Foo' => { 'Bar' => 42 }</tt>
89
+ # * Deep merge: <tt>'FOO' => { 'BAR' => 24 }</tt>
90
+ # * Result: <tt>'Foo' => { 'Bar' => 24 }</tt>
53
91
  #
54
92
  class HashWithIndifferentCaseInsensitiveAccess < ActiveSupport::HashWithIndifferentAccess
55
93
  def with_indifferent_case_insensitive_access
56
94
  self
57
95
  end
58
96
 
97
+ def initialize(constructor = nil)
98
+ @scimitar_hash_with_indifferent_case_insensitive_access_key_map = {}
99
+ super
100
+ end
101
+
102
+ # It's vital that the attribute map is carried over when one of these
103
+ # objects is duplicated. Duplication of this ivar state does *not* happen
104
+ # when 'dup' is called on our superclass, so we have to do that manually.
105
+ #
106
+ def dup
107
+ duplicate = super
108
+ duplicate.instance_variable_set(
109
+ '@scimitar_hash_with_indifferent_case_insensitive_access_key_map',
110
+ @scimitar_hash_with_indifferent_case_insensitive_access_key_map
111
+ )
112
+
113
+ return duplicate
114
+ end
115
+
116
+ # Override the individual key writer.
117
+ #
118
+ def []=(key, value)
119
+ string_key = scimitar_hash_with_indifferent_case_insensitive_access_string(key)
120
+ indifferent_key = scimitar_hash_with_indifferent_case_insensitive_access_downcase(string_key)
121
+ converted_value = convert_value(value, conversion: :assignment)
122
+
123
+ # Note '||=', as there might have been a prior use of the "same" key in
124
+ # a different case. The earliest one is preserved since the actual Hash
125
+ # underneath all this is already using that variant of the key.
126
+ #
127
+ key_for_writing = (
128
+ @scimitar_hash_with_indifferent_case_insensitive_access_key_map[indifferent_key] ||= string_key
129
+ )
130
+
131
+ regular_writer(key_for_writing, converted_value)
132
+ end
133
+
134
+ # Override #merge to express it in terms of #merge! (also overridden), so
135
+ # that merged hashes can have their keys treated indifferently too.
136
+ #
137
+ def merge(*other_hashes, &block)
138
+ dup.merge!(*other_hashes, &block)
139
+ end
140
+
141
+ # Modifies-self version of #merge, overriding Hash#merge!.
142
+ #
143
+ def merge!(*hashes_to_merge_to_self, &block)
144
+ if block_given?
145
+ hashes_to_merge_to_self.each do |hash_to_merge_to_self|
146
+ hash_to_merge_to_self.each_pair do |key, value|
147
+ value = block.call(key, self[key], value) if self.key?(key)
148
+ self[key] = value
149
+ end
150
+ end
151
+ else
152
+ hashes_to_merge_to_self.each do |hash_to_merge_to_self|
153
+ hash_to_merge_to_self.each_pair do |key, value|
154
+ self[key] = value
155
+ end
156
+ end
157
+ end
158
+
159
+ self
160
+ end
161
+
162
+ # =======================================================================
163
+ # PRIVATE INSTANCE METHODS
164
+ # =======================================================================
165
+ #
59
166
  private
60
167
 
61
168
  if Symbol.method_defined?(:name)
62
- def convert_key(key)
63
- key.kind_of?(Symbol) ? key.name.downcase : key.downcase
169
+ def scimitar_hash_with_indifferent_case_insensitive_access_string(key)
170
+ key.kind_of?(Symbol) ? key.name : key
64
171
  end
65
172
  else
66
- def convert_key(key)
67
- key.kind_of?(Symbol) ? key.to_s.downcase : key.downcase
173
+ def scimitar_hash_with_indifferent_case_insensitive_access_string(key)
174
+ key.kind_of?(Symbol) ? key.to_s : key
175
+ end
176
+ end
177
+
178
+ def scimitar_hash_with_indifferent_case_insensitive_access_downcase(key)
179
+ key.kind_of?(String) ? key.downcase : key
180
+ end
181
+
182
+ def convert_key(key)
183
+ string_key = scimitar_hash_with_indifferent_case_insensitive_access_string(key)
184
+ indifferent_key = scimitar_hash_with_indifferent_case_insensitive_access_downcase(string_key)
185
+
186
+ @scimitar_hash_with_indifferent_case_insensitive_access_key_map[indifferent_key] || string_key
187
+ end
188
+
189
+ def convert_value(value, conversion: nil)
190
+ if value.is_a?(Hash)
191
+ if conversion == :to_hash
192
+ value.to_hash
193
+ else
194
+ value.with_indifferent_case_insensitive_access
195
+ end
196
+ else
197
+ super
68
198
  end
69
199
  end
70
200
 
71
201
  def update_with_single_argument(other_hash, block)
72
- if other_hash.is_a? HashWithIndifferentCaseInsensitiveAccess
202
+ if other_hash.is_a?(HashWithIndifferentCaseInsensitiveAccess)
73
203
  regular_update(other_hash, &block)
74
204
  else
75
205
  other_hash.to_hash.each_pair do |key, value|
76
206
  if block && key?(key)
77
- value = block.call(convert_key(key), self[key], value)
207
+ value = block.call(self.convert_key(key), self[key], value)
78
208
  end
79
- regular_writer(convert_key(key), convert_value(value))
209
+ self.[]=(key, value)
80
210
  end
81
211
  end
82
212
  end
@@ -46,6 +46,66 @@ module Scimitar
46
46
  hash[array.shift()] = self.dot_path(array, value)
47
47
  end
48
48
  end
49
+
50
+ # Schema ID-aware splitter handling ":" or "." separators. Adapted from
51
+ # contribution by @bettysteger and @MorrisFreeman in:
52
+ #
53
+ # https://github.com/RIPAGlobal/scimitar/issues/48
54
+ # https://github.com/RIPAGlobal/scimitar/pull/49
55
+ #
56
+ # +schemas:: Array of extension schemas, e.g. a SCIM resource class'
57
+ # <tt>scim_resource_type.extended_schemas</tt> value. The
58
+ # Array should be empty if there are no extensions.
59
+ #
60
+ # +path_str+:: Path String, e.g. <tt>"password"</tt>, <tt>"name.givenName"</tt>,
61
+ # <tt>"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"</tt> (special case),
62
+ # <tt>"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:organization"</tt>
63
+ # (if given a Symbol, it'll be converted to a String).
64
+ #
65
+ # Returns an array of components, e.g. <tt>["password"]</tt>, <tt>["name",
66
+ # "givenName"]</tt>,
67
+ # <tt>["urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"]</tt> (special case),
68
+ # <tt>["urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", "organization"]</tt>.
69
+ #
70
+ # The called-out special case is for a schema ID without any appended
71
+ # path components, which is returned as a single element ID to aid in
72
+ # traversal particularly of things like PATCH requests. There, a "value"
73
+ # attribute might have a key string that's simply a schema ID, with an
74
+ # object beneath that's got attribute-name pairs, possibly nested, in a
75
+ # path-free payload.
76
+ #
77
+ def self.path_str_to_array(schemas, path_str)
78
+ path_str = path_str.to_s
79
+ components = []
80
+
81
+ # Note the ":" separating the schema ID (URN) from the attribute.
82
+ # The nature of JSON rendering / other payloads might lead you to
83
+ # expect a "." as with any complex types, but that's not the case;
84
+ # see https://tools.ietf.org/html/rfc7644#section-3.10, or
85
+ # https://tools.ietf.org/html/rfc7644#section-3.5.2 of which in
86
+ # particular, https://tools.ietf.org/html/rfc7644#page-35.
87
+ #
88
+ if path_str.include?(':')
89
+ lower_case_path_str = path_str.downcase()
90
+
91
+ schemas.each do |schema|
92
+ lower_case_schema_id = schema.id.downcase()
93
+ attributes_after_schema_id = lower_case_path_str.split(lower_case_schema_id + ':').drop(1)
94
+
95
+ if attributes_after_schema_id.empty?
96
+ components += [schema.id] if lower_case_path_str == lower_case_schema_id
97
+ else
98
+ attributes_after_schema_id.each do |component|
99
+ components += [schema.id] + component.split('.')
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ components = path_str.split('.') if components.empty?
106
+ return components
107
+ end
108
+
49
109
  end
50
110
  end
51
111
  end
@@ -3,11 +3,11 @@ module Scimitar
3
3
  # Gem version. If this changes, be sure to re-run "bundle install" or
4
4
  # "bundle update".
5
5
  #
6
- VERSION = '1.8.1'
6
+ VERSION = '1.10.0'
7
7
 
8
8
  # Date for VERSION. If this changes, be sure to re-run "bundle install"
9
9
  # or "bundle update".
10
10
  #
11
- DATE = '2024-01-16'
11
+ DATE = '2024-06-27'
12
12
 
13
13
  end
@@ -18,6 +18,7 @@ class MockUser < ActiveRecord::Base
18
18
  work_phone_number
19
19
  organization
20
20
  department
21
+ manager
21
22
  mock_groups
22
23
  }
23
24
 
@@ -48,6 +49,7 @@ class MockUser < ActiveRecord::Base
48
49
  externalId: :scim_uid,
49
50
  userName: :username,
50
51
  password: :password,
52
+ active: :is_active,
51
53
  name: {
52
54
  givenName: :first_name,
53
55
  familyName: :last_name
@@ -80,8 +82,11 @@ class MockUser < ActiveRecord::Base
80
82
  }
81
83
  },
82
84
  ],
83
- groups: [ # NB read-only, so no :find_with key
85
+ groups: [
84
86
  {
87
+ # Read-only, so no :find_with key. There's no 'class' specified here
88
+ # either, to help test the "/Schemas" endpoint's reflection code.
89
+ #
85
90
  list: :mock_groups,
86
91
  using: {
87
92
  value: :id,
@@ -89,13 +94,16 @@ class MockUser < ActiveRecord::Base
89
94
  }
90
95
  }
91
96
  ],
92
- active: :is_active,
93
97
 
94
98
  # Custom extension schema - see configuration in
95
99
  # "spec/apps/dummy/config/initializers/scimitar.rb".
96
100
  #
97
101
  organization: :organization,
98
102
  department: :department,
103
+ primaryEmail: :scim_primary_email,
104
+
105
+ manager: :manager,
106
+
99
107
  userGroups: [
100
108
  {
101
109
  list: :mock_groups,
@@ -124,9 +132,16 @@ class MockUser < ActiveRecord::Base
124
132
  'groups.value' => { column: MockGroup.arel_table[:id] },
125
133
  'emails' => { columns: [ :work_email_address, :home_email_address ] },
126
134
  'emails.value' => { columns: [ :work_email_address, :home_email_address ] },
127
- 'emails.type' => { ignore: true } # We can't filter on that; it'll just search all e-mails
135
+ 'emails.type' => { ignore: true }, # We can't filter on that; it'll just search all e-mails
136
+ 'primaryEmail' => { column: :scim_primary_email },
128
137
  }
129
138
  end
130
139
 
140
+ # Custom attribute reader
141
+ #
142
+ def scim_primary_email
143
+ work_email_address
144
+ end
145
+
131
146
  include Scimitar::Resources::Mixin
132
147
  end
@@ -40,10 +40,13 @@ Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
40
40
 
41
41
  module ScimSchemaExtensions
42
42
  module User
43
+
44
+ # This "looks like" part of the standard Enterprise extension.
45
+ #
43
46
  class Enterprise < Scimitar::Schema::Base
44
47
  def initialize(options = {})
45
48
  super(
46
- name: 'ExtendedUser',
49
+ name: 'EnterpriseExtendedUser',
47
50
  description: 'Enterprise extension for a User',
48
51
  id: self.class.id,
49
52
  scim_attributes: self.class.scim_attributes
@@ -57,7 +60,32 @@ module ScimSchemaExtensions
57
60
  def self.scim_attributes
58
61
  [
59
62
  Scimitar::Schema::Attribute.new(name: 'organization', type: 'string'),
60
- Scimitar::Schema::Attribute.new(name: 'department', type: 'string')
63
+ Scimitar::Schema::Attribute.new(name: 'department', type: 'string'),
64
+ Scimitar::Schema::Attribute.new(name: 'primaryEmail', type: 'string'),
65
+ ]
66
+ end
67
+ end
68
+
69
+ # In https://github.com/RIPAGlobal/scimitar/issues/122 we learn that with
70
+ # more than one extension, things can go wrong - so now we test with two.
71
+ #
72
+ class Manager < Scimitar::Schema::Base
73
+ def initialize(options = {})
74
+ super(
75
+ name: 'ManagementExtendedUser',
76
+ description: 'Management extension for a User',
77
+ id: self.class.id,
78
+ scim_attributes: self.class.scim_attributes
79
+ )
80
+ end
81
+
82
+ def self.id
83
+ 'urn:ietf:params:scim:schemas:extension:manager:1.0:User'
84
+ end
85
+
86
+ def self.scim_attributes
87
+ [
88
+ Scimitar::Schema::Attribute.new(name: 'manager', type: 'string')
61
89
  ]
62
90
  end
63
91
  end
@@ -65,3 +93,4 @@ module ScimSchemaExtensions
65
93
  end
66
94
 
67
95
  Scimitar::Resources::User.extend_schema ScimSchemaExtensions::User::Enterprise
96
+ Scimitar::Resources::User.extend_schema ScimSchemaExtensions::User::Manager
@@ -19,6 +19,7 @@ class CreateMockUsers < ActiveRecord::Migration[6.1]
19
19
  #
20
20
  t.text :organization
21
21
  t.text :department
22
+ t.text :manager
22
23
  end
23
24
  end
24
25
  end
@@ -42,6 +42,7 @@ ActiveRecord::Schema.define(version: 2021_03_08_044214) do
42
42
  t.text "work_phone_number"
43
43
  t.text "organization"
44
44
  t.text "department"
45
+ t.text "manager"
45
46
  end
46
47
 
47
48
  add_foreign_key "mock_groups_users", "mock_groups"