scimitar 1.7.1 → 2.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/scimitar/active_record_backed_resources_controller.rb +10 -49
  3. data/app/controllers/scimitar/application_controller.rb +11 -35
  4. data/app/controllers/scimitar/schemas_controller.rb +0 -5
  5. data/app/models/scimitar/engine_configuration.rb +5 -13
  6. data/app/models/scimitar/error_response.rb +0 -12
  7. data/app/models/scimitar/lists/query_parser.rb +10 -25
  8. data/app/models/scimitar/resources/base.rb +4 -14
  9. data/app/models/scimitar/resources/mixin.rb +13 -137
  10. data/app/models/scimitar/schema/address.rb +0 -1
  11. data/app/models/scimitar/schema/attribute.rb +5 -14
  12. data/app/models/scimitar/schema/base.rb +1 -1
  13. data/app/models/scimitar/schema/vdtp.rb +1 -1
  14. data/app/models/scimitar/service_provider_configuration.rb +3 -14
  15. data/config/initializers/scimitar.rb +3 -28
  16. data/lib/scimitar/version.rb +2 -2
  17. data/lib/scimitar.rb +2 -6
  18. data/spec/apps/dummy/app/controllers/mock_groups_controller.rb +1 -1
  19. data/spec/apps/dummy/app/models/mock_group.rb +1 -1
  20. data/spec/apps/dummy/app/models/mock_user.rb +8 -36
  21. data/spec/apps/dummy/config/application.rb +1 -0
  22. data/spec/apps/dummy/config/environments/test.rb +28 -5
  23. data/spec/apps/dummy/config/initializers/scimitar.rb +10 -61
  24. data/spec/apps/dummy/config/routes.rb +6 -15
  25. data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +1 -10
  26. data/spec/apps/dummy/db/migrate/20210308044214_create_join_table_mock_groups_mock_users.rb +3 -8
  27. data/spec/apps/dummy/db/schema.rb +4 -11
  28. data/spec/controllers/scimitar/application_controller_spec.rb +3 -72
  29. data/spec/controllers/scimitar/resource_types_controller_spec.rb +2 -2
  30. data/spec/controllers/scimitar/schemas_controller_spec.rb +2 -10
  31. data/spec/models/scimitar/complex_types/email_spec.rb +2 -0
  32. data/spec/models/scimitar/lists/query_parser_spec.rb +9 -76
  33. data/spec/models/scimitar/resources/base_spec.rb +70 -208
  34. data/spec/models/scimitar/resources/base_validation_spec.rb +2 -27
  35. data/spec/models/scimitar/resources/mixin_spec.rb +43 -768
  36. data/spec/models/scimitar/schema/attribute_spec.rb +3 -22
  37. data/spec/models/scimitar/schema/base_spec.rb +1 -1
  38. data/spec/models/scimitar/schema/user_spec.rb +0 -10
  39. data/spec/requests/active_record_backed_resources_controller_spec.rb +64 -423
  40. data/spec/requests/application_controller_spec.rb +3 -16
  41. metadata +7 -11
  42. data/LICENSE.txt +0 -21
  43. data/README.md +0 -671
  44. data/spec/apps/dummy/app/controllers/custom_save_mock_users_controller.rb +0 -24
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e45655762e444503f3ee367b077dce8b2699752d17e0b49e8b73acd1303e8d1
4
- data.tar.gz: f1a45bbc7655e1050251aed693d9c7e7c4df9f6a8d26c9f1cde6dba9cf2bc18a
3
+ metadata.gz: 95a2166cc921a400959f9d8d4398f6bf8ecb772f8d7a0a0a73950892e85d808a
4
+ data.tar.gz: cdf5aab3812f10f69c96304e738a150f4208850267527b66d36eeb99548d7b1f
5
5
  SHA512:
6
- metadata.gz: 12f7e2e62278accd66eb1c98ca51041eea8a808dbbb6e2f2dd887acbfce2ffbcdb173566b9af3152bbd9d9aa9df629eb6689d5a28ee33aae776502135581672f
7
- data.tar.gz: d282d93b1154cce8e3d868e8e50f3de35be2af9d0c681862da1495436cb5430443576c2dbae1c5296d964ca61193fb30769da11ece3bd72238c0ad121bbee5d2
6
+ metadata.gz: f0925517599b107e44fd93db9be142aebe608892c2c5069c50d22b353c51238290710474b062a002fdb010be3d783c4dea3b314f72f47b4aca3c2385a8fc1377
7
+ data.tar.gz: eef6eebfc64bb2d4adabfca110f26e3a6c9e227f47387da6fb384925899ddf0ae260cf68176f24040a6bb356cf34f6576c920bd44264d4a1fee415aeadc237e6
@@ -21,8 +21,6 @@ module Scimitar
21
21
 
22
22
  rescue_from ActiveRecord::RecordNotFound, with: :handle_resource_not_found # See Scimitar::ApplicationController
23
23
 
24
- before_action :obtain_id_column_name_from_attribute_map
25
-
26
24
  # GET (list)
27
25
  #
28
26
  def index
@@ -39,13 +37,12 @@ module Scimitar
39
37
  pagination_info = scim_pagination_info(query.count())
40
38
 
41
39
  page_of_results = query
42
- .order(@id_column => :asc)
43
40
  .offset(pagination_info.offset)
44
41
  .limit(pagination_info.limit)
45
42
  .to_a()
46
43
 
47
44
  super(pagination_info, page_of_results) do | record |
48
- record_to_scim(record)
45
+ record.to_scim(location: url_for(action: :show, id: record.id))
49
46
  end
50
47
  end
51
48
 
@@ -54,7 +51,7 @@ module Scimitar
54
51
  def show
55
52
  super do |record_id|
56
53
  record = self.find_record(record_id)
57
- record_to_scim(record)
54
+ record.to_scim(location: url_for(action: :show, id: record_id))
58
55
  end
59
56
  end
60
57
 
@@ -66,7 +63,7 @@ module Scimitar
66
63
  record = self.storage_class().new
67
64
  record.from_scim!(scim_hash: scim_resource.as_json())
68
65
  self.save!(record)
69
- record_to_scim(record)
66
+ record.to_scim(location: url_for(action: :show, id: record.id))
70
67
  end
71
68
  end
72
69
  end
@@ -79,7 +76,7 @@ module Scimitar
79
76
  record = self.find_record(record_id)
80
77
  record.from_scim!(scim_hash: scim_resource.as_json())
81
78
  self.save!(record)
82
- record_to_scim(record)
79
+ record.to_scim(location: url_for(action: :show, id: record.id))
83
80
  end
84
81
  end
85
82
  end
@@ -92,7 +89,7 @@ module Scimitar
92
89
  record = self.find_record(record_id)
93
90
  record.from_scim_patch!(patch_hash: patch_hash)
94
91
  self.save!(record)
95
- record_to_scim(record)
92
+ record.to_scim(location: url_for(action: :show, id: record.id))
96
93
  end
97
94
  end
98
95
  end
@@ -140,26 +137,13 @@ module Scimitar
140
137
  # +record_id+:: Record ID (SCIM schema 'id' value - "our" ID).
141
138
  #
142
139
  def find_record(record_id)
143
- self.storage_scope().find_by!(@id_column => record_id)
144
- end
145
-
146
- # DRY up controller actions - pass a record; returns the SCIM
147
- # representation, with a "show" location specified via #url_for.
148
- #
149
- def record_to_scim(record)
150
- record.to_scim(location: url_for(action: :show, id: record.send(@id_column)))
140
+ self.storage_scope().find(record_id)
151
141
  end
152
142
 
153
143
  # Save a record, dealing with validation exceptions by raising SCIM
154
144
  # errors.
155
145
  #
156
- # +record+:: ActiveRecord subclass to save.
157
- #
158
- # If you just let this superclass handle things, it'll call the standard
159
- # +#save!+ method on the record. If you pass a block, then this block is
160
- # invoked and passed the ActiveRecord model instance to be saved. You can
161
- # then do things like calling a different method, using a service object
162
- # of some kind, perform audit-related operations and so-on.
146
+ # +record+:: ActiveRecord subclass to save (via #save!).
163
147
  #
164
148
  # The return value is not used internally, making life easier for
165
149
  # overriding subclasses to "do the right thing" / avoid mistakes (instead
@@ -167,22 +151,10 @@ module Scimitar
167
151
  # and relying upon this to generate correct response payloads - an early
168
152
  # version of the gem did this and it caused a confusing subclass bug).
169
153
  #
170
- def save!(record, &block)
171
- if block_given?
172
- yield(record)
173
- else
174
- record.save!
175
- end
176
- rescue ActiveRecord::RecordInvalid => exception
177
- handle_invalid_record(exception.record)
178
- end
154
+ def save!(record)
155
+ record.save!
179
156
 
180
- # Deal with validation errors by responding with an appropriate SCIM
181
- # error.
182
- #
183
- # +record+:: The record with validation errors.
184
- #
185
- def handle_invalid_record(record)
157
+ rescue ActiveRecord::RecordInvalid => exception
186
158
  joined_errors = record.errors.full_messages.join('; ')
187
159
 
188
160
  # https://tools.ietf.org/html/rfc7644#page-12
@@ -204,16 +176,5 @@ module Scimitar
204
176
  end
205
177
  end
206
178
 
207
- # Called via +before_action+ - stores in @id_column the name of whatever
208
- # model column is used to store the record ID, via
209
- # Scimitar::Resources::Mixin::scim_attributes_map.
210
- #
211
- # Default is <tt>:id</tt>.
212
- #
213
- def obtain_id_column_name_from_attribute_map
214
- attrs = storage_class().scim_attributes_map() || {}
215
- @id_column = attrs[:id] || :id
216
- end
217
-
218
179
  end
219
180
  end
@@ -25,11 +25,10 @@ module Scimitar
25
25
  #
26
26
  # ...to "globally" invoke this handler if you wish.
27
27
  #
28
- # +exception+:: Exception instance, used for a configured error reporter
29
- # via #handle_scim_error (if present).
28
+ # +_exception+:: Exception instance (currently unused).
30
29
  #
31
- def handle_resource_not_found(exception)
32
- handle_scim_error(NotFoundError.new(params[:id]), exception)
30
+ def handle_resource_not_found(_exception)
31
+ handle_scim_error(NotFoundError.new(params[:id]))
33
32
  end
34
33
 
35
34
  # This base controller uses:
@@ -39,22 +38,9 @@ module Scimitar
39
38
  # ...to "globally" invoke this handler for all Scimitar errors (including
40
39
  # subclasses).
41
40
  #
42
- # Mandatory parameters are:
43
- #
44
41
  # +error_response+:: Scimitar::ErrorResponse (or subclass) instance.
45
42
  #
46
- # Optional parameters are:
47
- #
48
- # *exception+:: If a Ruby exception was the reason this method is being
49
- # called, pass it here. Any configured exception reporting
50
- # mechanism will be invokved with the given parameter.
51
- # Otherwise, the +error_response+ value is reported.
52
- #
53
- def handle_scim_error(error_response, exception = error_response)
54
- unless Scimitar.engine_configuration.exception_reporter.nil?
55
- Scimitar.engine_configuration.exception_reporter.call(exception)
56
- end
57
-
43
+ def handle_scim_error(error_response)
58
44
  render json: error_response, status: error_response.status
59
45
  end
60
46
 
@@ -69,7 +55,7 @@ module Scimitar
69
55
  # +exception+:: Exception instance.
70
56
  #
71
57
  def handle_bad_json_error(exception)
72
- handle_scim_error(ErrorResponse.new(status: 400, detail: "Invalid JSON - #{exception.message}"), exception)
58
+ handle_scim_error(ErrorResponse.new(status: 400, detail: "Invalid JSON - #{exception.message}"))
73
59
  end
74
60
 
75
61
  # This base controller uses:
@@ -82,7 +68,7 @@ module Scimitar
82
68
  #
83
69
  def handle_unexpected_error(exception)
84
70
  Rails.logger.error("#{exception.message}\n#{exception.backtrace}")
85
- handle_scim_error(ErrorResponse.new(status: 500, detail: exception.message), exception)
71
+ handle_scim_error(ErrorResponse.new(status: 500, detail: exception.message))
86
72
  end
87
73
 
88
74
  # =========================================================================
@@ -96,17 +82,12 @@ module Scimitar
96
82
  # request and subclass processing.
97
83
  #
98
84
  def require_scim
99
- scim_mime_type = Mime::Type.lookup_by_extension(:scim).to_s
100
-
101
- if request.media_type.nil? || request.media_type.empty?
102
- request.format = :scim
103
- request.headers['CONTENT_TYPE'] = scim_mime_type
104
- elsif request.media_type.downcase == scim_mime_type
85
+ if request.content_type&.downcase == Mime::Type.lookup_by_extension(:scim).to_s
105
86
  request.format = :scim
106
87
  elsif request.format == :scim
107
- request.headers['CONTENT_TYPE'] = scim_mime_type
88
+ request.headers['CONTENT_TYPE'] = Mime::Type.lookup_by_extension(:scim).to_s
108
89
  else
109
- handle_scim_error(ErrorResponse.new(status: 406, detail: "Only #{scim_mime_type} type is accepted."))
90
+ handle_scim_error(ErrorResponse.new(status: 406, detail: "Only #{Mime::Type.lookup_by_extension(:scim)} type is accepted."))
110
91
  end
111
92
  end
112
93
 
@@ -124,13 +105,8 @@ module Scimitar
124
105
  #
125
106
  # https://stackoverflow.com/questions/10239970/what-is-the-delimiter-for-www-authenticate-for-multiple-schemes
126
107
  #
127
- response.set_header('WWW-Authenticate', 'Basic' ) if Scimitar.engine_configuration.basic_authenticator.present?
128
- response.set_header('WWW-Authenticate', 'Bearer') if Scimitar.engine_configuration.token_authenticator.present?
129
-
130
- # No matter what a caller might request via headers, the only content
131
- # type we can ever respond with is JSON-for-SCIM.
132
- #
133
- response.set_header('Content-Type', "#{Mime::Type.lookup_by_extension(:scim)}; charset=utf-8")
108
+ response.set_header('WWW_AUTHENTICATE', 'Basic' ) if Scimitar.engine_configuration.basic_authenticator.present?
109
+ response.set_header('WWW_AUTHENTICATE', 'Bearer') if Scimitar.engine_configuration.token_authenticator.present?
134
110
  end
135
111
 
136
112
  def authenticate
@@ -4,11 +4,6 @@ module Scimitar
4
4
  class SchemasController < ApplicationController
5
5
  def index
6
6
  schemas = Scimitar::Engine.schemas
7
-
8
- schemas.each do |schema|
9
- schema.meta.location = scim_schemas_url(name: schema.id)
10
- end
11
-
12
7
  schemas_by_id = schemas.reduce({}) do |hash, schema|
13
8
  hash[schema.id] = schema
14
9
  hash
@@ -7,23 +7,15 @@ module Scimitar
7
7
  class EngineConfiguration
8
8
  include ActiveModel::Model
9
9
 
10
- attr_accessor(
11
- :uses_defaults,
12
- :basic_authenticator,
13
- :token_authenticator,
14
- :application_controller_mixin,
15
- :exception_reporter,
16
- :optional_value_fields_required,
17
- )
10
+ attr_accessor :basic_authenticator,
11
+ :token_authenticator,
12
+ :application_controller_mixin
18
13
 
19
14
  def initialize(attributes = {})
20
- @uses_defaults = attributes.empty?
21
15
 
22
- # Set defaults that may be overridden by the initializer.
16
+ # No defaults yet - reserved for future use.
23
17
  #
24
- defaults = {
25
- optional_value_fields_required: true
26
- }
18
+ defaults = {}
27
19
 
28
20
  super(defaults.merge(attributes))
29
21
  end
@@ -16,17 +16,5 @@ module Scimitar
16
16
  data['scimType'] = scimType if scimType
17
17
  data
18
18
  end
19
-
20
- # Originally Scimitar used attribute "detail" for exception text; it was
21
- # only for JSON responses at the time, but in hindsight was a bad choice.
22
- # It should have been "message" given inheritance from StandardError, which
23
- # then works properly with e.g. error reporting services.
24
- #
25
- # The "detail" attribute is still present, for backwards compatibility with
26
- # any client code that might be using this class.
27
- #
28
- def message
29
- self.detail
30
- end
31
19
  end
32
20
  end
@@ -78,7 +78,7 @@ module Scimitar
78
78
  # method's return value here.
79
79
  #
80
80
  def initialize(attribute_map)
81
- @attribute_map = attribute_map.with_indifferent_case_insensitive_access()
81
+ @attribute_map = attribute_map
82
82
  end
83
83
 
84
84
  # Parse SCIM filter query into RPN stack
@@ -192,7 +192,7 @@ module Scimitar
192
192
 
193
193
  ast.push(self.start_group? ? self.parse_group() : self.pop())
194
194
 
195
- if ast.last.is_a?(String) && !UNARY_OPERATORS.include?(ast.last.downcase) || ast.last.is_a?(Array)
195
+ unless ! ast.last.is_a?(String) || UNARY_OPERATORS.include?(ast.last.downcase)
196
196
  expect_op ^= true
197
197
  end
198
198
  end
@@ -601,27 +601,12 @@ module Scimitar
601
601
  column_names = self.activerecord_columns(scim_attribute)
602
602
  value = self.activerecord_parameter(scim_parameter)
603
603
  value_for_like = self.sql_modified_value(scim_operator, value)
604
- arel_columns = column_names.map do |column|
605
- if base_scope.model.column_names.include?(column.to_s)
606
- arel_table[column]
607
- elsif column.is_a?(Arel::Attribute)
608
- column
609
- end
610
- end
611
-
612
- raise Scimitar::FilterError unless arel_columns.all?
604
+ all_supported = column_names.all? { | column_name | base_scope.model.column_names.include?(column_name.to_s) }
613
605
 
614
- unless case_sensitive
615
- lc_scim_attribute = scim_attribute.downcase()
616
-
617
- case_sensitive = (
618
- lc_scim_attribute == 'id' ||
619
- lc_scim_attribute == 'externalid' ||
620
- lc_scim_attribute.start_with?('meta.')
621
- )
622
- end
606
+ raise Scimitar::FilterError unless all_supported
623
607
 
624
- arel_columns.each.with_index do | arel_column, index |
608
+ column_names.each.with_index do | column_name, index |
609
+ arel_column = arel_table[column_name]
625
610
  arel_operation = case scim_operator
626
611
  when 'eq'
627
612
  if case_sensitive
@@ -646,9 +631,9 @@ module Scimitar
646
631
  when 'co', 'sw', 'ew'
647
632
  arel_column.matches(value_for_like, nil, case_sensitive)
648
633
  when 'pr'
649
- arel_column.relation.grouping(arel_column.not_eq_all(['', nil]))
634
+ arel_table.grouping(arel_column.not_eq_all(['', nil]))
650
635
  else
651
- raise Scimitar::FilterError.new("Unsupported operator: '#{scim_operator}'")
636
+ raise Scimitar::FilterError
652
637
  end
653
638
 
654
639
  if index == 0
@@ -671,10 +656,10 @@ module Scimitar
671
656
  # +scim_attribute+:: SCIM attribute from a filter string.
672
657
  #
673
658
  def activerecord_columns(scim_attribute)
674
- raise Scimitar::FilterError.new("No scim_attribute provided") if scim_attribute.blank?
659
+ raise Scimitar::FilterError if scim_attribute.blank?
675
660
 
676
661
  mapped_attribute = self.attribute_map()[scim_attribute]
677
- raise Scimitar::FilterError.new("Unable to find domain attribute from SCIM attribute: '#{scim_attribute}'") if mapped_attribute.blank?
662
+ raise Scimitar::FilterError if mapped_attribute.blank?
678
663
 
679
664
  if mapped_attribute[:ignore]
680
665
  return []
@@ -112,7 +112,7 @@ module Scimitar
112
112
  end
113
113
 
114
114
  def self.complex_scim_attributes
115
- schemas.flat_map(&:scim_attributes).select(&:complexType).group_by(&:name)
115
+ schema.scim_attributes.select(&:complexType).group_by(&:name)
116
116
  end
117
117
 
118
118
  def complex_type_from_hash(scim_attribute, attr_value)
@@ -138,24 +138,14 @@ module Scimitar
138
138
  end
139
139
 
140
140
  def as_json(options = {})
141
- self.meta = Meta.new unless self.meta && self.meta.is_a?(Meta)
142
- self.meta.resourceType = self.class.resource_type_id
143
-
144
- non_returnable_attributes = self.class
145
- .schemas
146
- .flat_map(&:scim_attributes)
147
- .filter_map { |attribute| attribute.name if attribute.returned == 'never' }
148
-
149
- non_returnable_attributes << 'errors'
150
-
151
- original_hash = super(options).except(*non_returnable_attributes)
141
+ self.meta = Meta.new unless self.meta
142
+ meta.resourceType = self.class.resource_type_id
143
+ original_hash = super(options).except('errors')
152
144
  original_hash.merge!('schemas' => self.class.schemas.map(&:id))
153
-
154
145
  self.class.extended_schemas.each do |extension_schema|
155
146
  extension_attributes = extension_schema.scim_attributes.map(&:name)
156
147
  original_hash.merge!(extension_schema.id => original_hash.extract!(*extension_attributes))
157
148
  end
158
-
159
149
  original_hash
160
150
  end
161
151
 
@@ -220,8 +220,13 @@ module Scimitar
220
220
  # allow for different client searching "styles", given ambiguities in RFC
221
221
  # 7644 filter examples).
222
222
  #
223
- # Each value is a hash of queryable SCIM attribute options, described
224
- # below - for example:
223
+ # Each value is a Hash with Symbol keys ':column', naming just one simple
224
+ # column for a mapping; ':columns', with an Array of column names that you
225
+ # want to map using 'OR' for a single search on the corresponding SCIM
226
+ # attribute; or ':ignore' with value 'true', which means that a fitler on
227
+ # the matching attribute is ignored rather than resulting in an "invalid
228
+ # filter" exception - beware possibilities for surprised clients getting a
229
+ # broader result set than expected. Example:
225
230
  #
226
231
  # def self.scim_queryable_attributes
227
232
  # return {
@@ -229,27 +234,10 @@ module Scimitar
229
234
  # 'name.familyName' => { column: :last_name },
230
235
  # 'emails' => { columns: [ :work_email_address, :home_email_address ] },
231
236
  # 'emails.value' => { columns: [ :work_email_address, :home_email_address ] },
232
- # 'emails.type' => { ignore: true },
233
- # 'groups.value' => { column: Group.arel_table[:id] }
237
+ # 'emails.type' => { ignore: true }
234
238
  # }
235
239
  # end
236
240
  #
237
- # Column references can be either a Symbol representing a column within
238
- # the resource model table, or an <tt>Arel::Attribute</tt> instance via
239
- # e.g. <tt>MyModel.arel_table[:my_column]</tt>.
240
- #
241
- # === Queryable SCIM attribute options
242
- #
243
- # +:column+:: Just one simple column for a mapping.
244
- #
245
- # +:columns+:: An Array of columns that you want to map using 'OR' for a
246
- # single search of the corresponding entity.
247
- #
248
- # +:ignore+:: When set to +true+, the matching attribute is ignored rather
249
- # than resulting in an "invalid filter" exception. Beware
250
- # possibilities for surprised clients getting a broader result
251
- # set than expected, since a constraint may have been ignored.
252
- #
253
241
  # Filtering is currently limited and searching within e.g. arrays of data
254
242
  # is not supported; only simple top-level keys can be mapped.
255
243
  #
@@ -418,11 +406,8 @@ module Scimitar
418
406
  def from_scim_patch!(patch_hash:)
419
407
  frozen_ci_patch_hash = patch_hash.with_indifferent_case_insensitive_access().freeze()
420
408
  ci_scim_hash = self.to_scim(location: '(unused)').as_json().with_indifferent_case_insensitive_access()
421
- operations = frozen_ci_patch_hash['operations']
422
-
423
- raise Scimitar::InvalidSyntaxError.new("Missing PATCH \"operations\"") unless operations
424
409
 
425
- operations.each do |operation|
410
+ frozen_ci_patch_hash['operations'].each do |operation|
426
411
  nature = operation['op' ]&.downcase
427
412
  path_str = operation['path' ]
428
413
  value = operation['value']
@@ -458,30 +443,9 @@ module Scimitar
458
443
  ci_scim_hash = { 'root' => ci_scim_hash }.with_indifferent_case_insensitive_access()
459
444
  end
460
445
 
461
- # Handle extension schema. Contributed by @bettysteger and
462
- # @MorrisFreeman via:
463
- #
464
- # https://github.com/RIPAGlobal/scimitar/issues/48
465
- # https://github.com/RIPAGlobal/scimitar/pull/49
466
- #
467
- # Note the ":" separating the schema ID (URN) from the attribute.
468
- # The nature of JSON rendering / other payloads might lead you to
469
- # expect a "." as with any complex types, but that's not the case;
470
- # see https://tools.ietf.org/html/rfc7644#section-3.10, or
471
- # https://tools.ietf.org/html/rfc7644#section-3.5.2 of which in
472
- # particular, https://tools.ietf.org/html/rfc7644#page-35.
473
- #
474
- paths = []
475
- self.class.scim_resource_type.extended_schemas.each do |schema|
476
- path_str.downcase.split(schema.id.downcase + ':').drop(1).each do |path|
477
- paths += [schema.id] + path.split('.')
478
- end
479
- end
480
- paths = path_str.split('.') if paths.empty?
481
-
482
446
  self.from_patch_backend!(
483
447
  nature: nature,
484
- path: paths,
448
+ path: (path_str || '').split('.'),
485
449
  value: value,
486
450
  altering_hash: ci_scim_hash
487
451
  )
@@ -652,19 +616,7 @@ module Scimitar
652
616
  attrs_map_or_leaf_value.each do | scim_attribute, sub_attrs_map_or_leaf_value |
653
617
  next if scim_attribute&.to_s&.downcase == 'id' && path.empty?
654
618
 
655
- # Handle extension schema. Contributed by @bettysteger and
656
- # @MorrisFreeman via:
657
- #
658
- # https://github.com/RIPAGlobal/scimitar/issues/48
659
- # https://github.com/RIPAGlobal/scimitar/pull/49
660
- #
661
- attribute_tree = []
662
- resource_class.extended_schemas.each do |schema|
663
- attribute_tree << schema.id and break if schema.scim_attributes.any? { |attribute| attribute.name == scim_attribute.to_s }
664
- end
665
- attribute_tree << scim_attribute.to_s
666
-
667
- sub_scim_hash_or_leaf_value = scim_hash_or_leaf_value&.dig(*attribute_tree)
619
+ sub_scim_hash_or_leaf_value = scim_hash_or_leaf_value&.dig(scim_attribute.to_s)
668
620
 
669
621
  self.from_scim_backend!(
670
622
  attrs_map_or_leaf_value: sub_attrs_map_or_leaf_value,
@@ -949,86 +901,10 @@ module Scimitar
949
901
  else
950
902
  altering_hash[path_component] = value
951
903
  end
952
-
953
904
  when 'replace'
954
- if path_component == 'root'
955
- altering_hash[path_component].merge!(value)
956
- else
957
- altering_hash[path_component] = value
958
- end
959
-
960
- # The array check handles payloads seen from e.g. Microsoft for
961
- # remove-user-from-group, where contrary to examples in the RFC
962
- # which would imply "payload removes all users", there is the
963
- # clear intent to remove just one.
964
- #
965
- # https://tools.ietf.org/html/rfc7644#section-3.5.2.2
966
- # https://learn.microsoft.com/en-us/azure/active-directory/app-provisioning/use-scim-to-provision-users-and-groups#update-group-remove-members
967
- #
968
- # Since remove-all in the face of remove-one is destructive, we
969
- # do a special check here to see if there's an array value for
970
- # the array path that the payload yielded. If so, we can match
971
- # each value against array items and remove just those items.
972
- #
973
- # There is an additional special case to handle a bad example
974
- # from Salesforce:
975
- #
976
- # https://help.salesforce.com/s/articleView?id=sf.identity_scim_manage_groups.htm&type=5
977
- #
905
+ altering_hash[path_component] = value
978
906
  when 'remove'
979
- if altering_hash[path_component].is_a?(Array) && value.present?
980
-
981
- # Handle bad Salesforce example. That might be simply a
982
- # documentation error, but just in case...
983
- #
984
- value = value.values.first if (
985
- path_component&.downcase == 'members' &&
986
- value.is_a?(Hash) &&
987
- value.keys.size == 1 &&
988
- value.keys.first&.downcase == 'members'
989
- )
990
-
991
- # The Microsoft example provides an array of values, but we
992
- # may as well cope with a value specified 'flat'. Promote
993
- # such a thing to an Array to simplify the following code.
994
- #
995
- value = [value] unless value.is_a?(Array)
996
-
997
- # For each value item, delete matching array entries. The
998
- # concept of "matching" is:
999
- #
1000
- # * For simple non-Hash values (if possible) just delete on
1001
- # an exact match
1002
- #
1003
- # * For Hash-based values, only delete if all 'patch' keys
1004
- # are present in the resource and all values thus match.
1005
- #
1006
- # Special case to ignore '$ref' from the Microsoft payload.
1007
- #
1008
- # Note coercion to strings to account for SCIM vs the usual
1009
- # tricky case of underlying implementations with (say)
1010
- # integer primary keys, which all end up as strings anyway.
1011
- #
1012
- value.each do | value_item |
1013
- altering_hash[path_component].delete_if do | item |
1014
- if item.is_a?(Hash) && value_item.is_a?(Hash)
1015
- matched_all = true
1016
- value_item.each do | value_key, value_value |
1017
- next if value_key == '$ref'
1018
- if ! item.key?(value_key) || item[value_key]&.to_s != value_value&.to_s
1019
- matched_all = false
1020
- end
1021
- end
1022
- matched_all
1023
- else
1024
- item&.to_s == value_item&.to_s
1025
- end
1026
- end
1027
- end
1028
- else
1029
- altering_hash.delete(path_component)
1030
- end
1031
-
907
+ altering_hash.delete(path_component)
1032
908
  end
1033
909
  end
1034
910
  end
@@ -10,7 +10,6 @@ module Scimitar
10
10
  def self.scim_attributes
11
11
  @scim_attributes ||= [
12
12
  Attribute.new(name: 'type', type: 'string'),
13
- Attribute.new(name: 'primary', type: 'boolean'),
14
13
  Attribute.new(name: 'formatted', type: 'string'),
15
14
  Attribute.new(name: 'streetAddress', type: 'string'),
16
15
  Attribute.new(name: 'locality', type: 'string'),
@@ -93,23 +93,14 @@ module Scimitar
93
93
  end
94
94
 
95
95
  def valid_simple_type?(value)
96
- if multiValued
97
- valid = value.is_a?(Array) && value.all? { |v| simple_type?(v) }
98
- errors.add(self.name, "or one of its elements has the wrong type. It has to be an array of #{self.type}s.") unless valid
99
- else
100
- valid = simple_type?(value)
101
- errors.add(self.name, "has the wrong type. It has to be a(n) #{self.type}.") unless valid
102
- end
96
+ valid = (type == 'string' && value.is_a?(String)) ||
97
+ (type == 'boolean' && (value.is_a?(TrueClass) || value.is_a?(FalseClass))) ||
98
+ (type == 'integer' && (value.is_a?(Integer))) ||
99
+ (type == 'dateTime' && valid_date_time?(value))
100
+ errors.add(self.name, "has the wrong type. It has to be a(n) #{self.type}.") unless valid
103
101
  valid
104
102
  end
105
103
 
106
- def simple_type?(value)
107
- (type == 'string' && value.is_a?(String)) ||
108
- (type == 'boolean' && (value.is_a?(TrueClass) || value.is_a?(FalseClass))) ||
109
- (type == 'integer' && (value.is_a?(Integer))) ||
110
- (type == 'dateTime' && valid_date_time?(value))
111
- end
112
-
113
104
  def valid_date_time?(value)
114
105
  !!Time.iso8601(value)
115
106
  rescue ArgumentError
@@ -13,7 +13,7 @@ module Scimitar
13
13
 
14
14
  # Converts the schema to its json representation that will be returned by /SCHEMAS end-point of a SCIM service provider.
15
15
  def as_json(options = {})
16
- @meta.location ||= Scimitar::Engine.routes.url_helpers.scim_schemas_path(name: id)
16
+ @meta.location = Scimitar::Engine.routes.url_helpers.scim_schemas_path(name: id)
17
17
  original = super
18
18
  original.merge('attributes' => original.delete('scim_attributes'))
19
19
  end
@@ -7,7 +7,7 @@ module Scimitar
7
7
  class Vdtp < Base
8
8
  def self.scim_attributes
9
9
  @scim_attributes ||= [
10
- Attribute.new(name: 'value', type: 'string', required: Scimitar.engine_configuration.optional_value_fields_required),
10
+ Attribute.new(name: 'value', type: 'string', required: true),
11
11
  Attribute.new(name: 'display', type: 'string', mutability: 'readOnly'),
12
12
  Attribute.new(name: 'type', type: 'string'),
13
13
  Attribute.new(name: 'primary', type: 'boolean'),