scimitar 1.7.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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'),