scimaenaga 0.6.1 → 0.9.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +15 -0
  3. data/app/controllers/concerns/scim_rails/exception_handler.rb +74 -22
  4. data/app/controllers/scim_rails/scim_groups_controller.rb +14 -3
  5. data/app/controllers/scim_rails/scim_schemas_controller.rb +42 -0
  6. data/app/controllers/scim_rails/scim_users_controller.rb +22 -4
  7. data/app/libraries/scim_patch.rb +15 -9
  8. data/app/libraries/scim_patch_operation.rb +50 -71
  9. data/app/libraries/scim_patch_operation_converter.rb +90 -0
  10. data/app/libraries/scim_patch_operation_group.rb +100 -0
  11. data/app/libraries/scim_patch_operation_user.rb +53 -0
  12. data/config/routes.rb +14 -11
  13. data/lib/generators/scim_rails/templates/initializer.rb +106 -0
  14. data/lib/scim_rails/config.rb +8 -5
  15. data/lib/scim_rails/version.rb +1 -1
  16. data/spec/controllers/scim_rails/scim_groups_controller_spec.rb +25 -7
  17. data/spec/controllers/scim_rails/scim_schemas_controller_spec.rb +238 -0
  18. data/spec/controllers/scim_rails/scim_schemas_request_spec.rb +39 -0
  19. data/spec/controllers/scim_rails/scim_users_controller_spec.rb +361 -220
  20. data/spec/dummy/app/models/user.rb +9 -0
  21. data/spec/dummy/config/initializers/scim_rails_config.rb +27 -24
  22. data/spec/dummy/db/migrate/20220131090107_add_deletable_to_users.rb +5 -0
  23. data/spec/dummy/db/schema.rb +2 -1
  24. data/spec/dummy/db/seeds.rb +10 -1
  25. data/spec/factories/user.rb +2 -0
  26. data/spec/libraries/scim_patch_operation_group_spec.rb +165 -0
  27. data/spec/libraries/scim_patch_operation_user_spec.rb +101 -0
  28. data/spec/libraries/scim_patch_spec.rb +135 -53
  29. metadata +80 -80
  30. data/spec/dummy/db/development.sqlite3 +0 -0
  31. data/spec/dummy/db/test.sqlite3 +0 -0
  32. data/spec/dummy/log/development.log +0 -0
  33. data/spec/dummy/log/test.log +0 -377
  34. data/spec/dummy/put_group.http +0 -5
  35. data/spec/dummy/tmp/restart.txt +0 -0
  36. data/spec/libraries/scim_patch_operation_spec.rb +0 -96
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 348227d90785d1980e12b0f2d6ff1c71a6861761a4b35c5c10b5147894f949de
4
- data.tar.gz: 8bc3cfb0ba591b1eff717044b91c9db4d34f9fbc13285d234bf526a52be8ce48
3
+ metadata.gz: 58fc798213cd88360e3f55430717995ad195c6bac873128907233e82261fa4b1
4
+ data.tar.gz: 5e0e88484123fb42ffd42e1174439c88773615b4be4b1c8aa2e3f7eb38f7b8c3
5
5
  SHA512:
6
- metadata.gz: d1ed9e73ffc03e6362f2bd498ad37f5f4a915a7fd09c64470700ee729e8baec062a43e91f1f6a937f37fec325533351fc12bd1d2f1e2b5f067efd5c5c209871d
7
- data.tar.gz: a7810a0c341f779cb8be98213d3c9eb6d5ac98669b5dea67e389bec787a3f65a8ed1e378a005e3d0415d0b7563f49fa0cf3dafdda2917220c92d38ce3a7c65d0
6
+ metadata.gz: 66e68eba027f29f0f87bb94c339356c9b6bb5dce37561f1a9f35a96fd8d34d8fc2f7fbb7d0901106b2cf66fdc7fef18cf3861c593c6795c68834e8d67793ba32
7
+ data.tar.gz: 6809bbeab71b8cb0bf645ecdca6819c43e2bbfa5bf26936b8772a28fdace71b47c63b1211c5454033168b8812b4ba249c7ae0423c6a1b047faafb16b6caf69ee
data/README.md CHANGED
@@ -250,6 +250,21 @@ ScimRails.configure do |config|
250
250
  end
251
251
  ```
252
252
 
253
+ ### Schemas endpoint
254
+
255
+ If you need Schemas endpoint configure `schemas`.
256
+ (Azure AD requires Schemas endpoint when registering to Application Gallery.)
257
+
258
+ You have to configure `schemas` as
259
+ - corresponding with other configurations.
260
+ e.g.) When `userName` is defined in `mutable_user_attributes`, configure `userName` as `mutability: 'readWrite'`.
261
+
262
+ - corresponding with your model.
263
+ e.g.) When `userName` must be specified configure `userName` as `required: true`
264
+
265
+ Sample config (with comment) is written in lib/generators/scim_rails/templates/initializer.rb.
266
+ For more details, read [Schema Definition](https://datatracker.ietf.org/doc/html/rfc7643#section-7), and [Schema Representation](https://datatracker.ietf.org/doc/html/rfc7643#section-8.7)
267
+
253
268
  ## Contributing
254
269
 
255
270
  ### [Code of Conduct](https://github.com/StudistCorporation/scimaenaga/blob/master/CODE_OF_CONDUCT.md)
@@ -7,6 +7,9 @@ module ScimRails
7
7
  class InvalidCredentials < StandardError
8
8
  end
9
9
 
10
+ class InvalidRequest < StandardError
11
+ end
12
+
10
13
  class InvalidQuery < StandardError
11
14
  end
12
15
 
@@ -16,6 +19,21 @@ module ScimRails
16
19
  class UnsupportedDeleteRequest < StandardError
17
20
  end
18
21
 
22
+ class InvalidConfiguration < StandardError
23
+ end
24
+
25
+ class UnexpectedError < StandardError
26
+ end
27
+
28
+ class ResourceNotFound < StandardError
29
+ attr_reader :id
30
+
31
+ def initialize(id)
32
+ super
33
+ @id = id
34
+ end
35
+ end
36
+
19
37
  included do
20
38
  if Rails.env.production?
21
39
  rescue_from StandardError do |exception|
@@ -28,8 +46,8 @@ module ScimRails
28
46
 
29
47
  json_response(
30
48
  {
31
- schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
32
- status: "500"
49
+ schemas: ['urn:ietf:params:scim:api:messages:2.0:Error'],
50
+ status: '500',
33
51
  },
34
52
  :internal_server_error
35
53
  )
@@ -39,21 +57,32 @@ module ScimRails
39
57
  rescue_from ScimRails::ExceptionHandler::InvalidCredentials do
40
58
  json_response(
41
59
  {
42
- schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
43
- detail: "Authorization failure. The authorization header is invalid or missing.",
44
- status: "401"
60
+ schemas: ['urn:ietf:params:scim:api:messages:2.0:Error'],
61
+ detail: 'Authorization failure. The authorization header is invalid or missing.',
62
+ status: '401',
45
63
  },
46
64
  :unauthorized
47
65
  )
48
66
  end
49
67
 
68
+ rescue_from ScimRails::ExceptionHandler::InvalidRequest do |e|
69
+ json_response(
70
+ {
71
+ schemas: ['urn:ietf:params:scim:api:messages:2.0:Error'],
72
+ detail: "Invalid request. #{e.message}",
73
+ status: '400',
74
+ },
75
+ :bad_request
76
+ )
77
+ end
78
+
50
79
  rescue_from ScimRails::ExceptionHandler::InvalidQuery do
51
80
  json_response(
52
81
  {
53
- schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
54
- scimType: "invalidFilter",
55
- detail: "The specified filter syntax was invalid, or the specified attribute and filter comparison combination is not supported.",
56
- status: "400"
82
+ schemas: ['urn:ietf:params:scim:api:messages:2.0:Error'],
83
+ scimType: 'invalidFilter',
84
+ detail: 'The specified filter syntax was invalid, or the specified attribute and filter comparison combination is not supported.',
85
+ status: '400',
57
86
  },
58
87
  :bad_request
59
88
  )
@@ -62,9 +91,9 @@ module ScimRails
62
91
  rescue_from ScimRails::ExceptionHandler::UnsupportedPatchRequest do
63
92
  json_response(
64
93
  {
65
- schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
66
- detail: "Invalid PATCH request. This PATCH endpoint only supports deprovisioning and reprovisioning records.",
67
- status: "422"
94
+ schemas: ['urn:ietf:params:scim:api:messages:2.0:Error'],
95
+ detail: 'Invalid PATCH request.',
96
+ status: '422',
68
97
  },
69
98
  :unprocessable_entity
70
99
  )
@@ -73,20 +102,43 @@ module ScimRails
73
102
  rescue_from ScimRails::ExceptionHandler::UnsupportedDeleteRequest do
74
103
  json_response(
75
104
  {
76
- schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
77
- detail: "Delete operation is disabled for the requested resource.",
78
- status: "501"
105
+ schemas: ['urn:ietf:params:scim:api:messages:2.0:Error'],
106
+ detail: 'Delete operation is disabled for the requested resource.',
107
+ status: '501',
79
108
  },
80
109
  :not_implemented
81
110
  )
82
111
  end
83
112
 
84
- rescue_from ActiveRecord::RecordNotFound do |e|
113
+ rescue_from ScimRails::ExceptionHandler::InvalidConfiguration do |e|
114
+ json_response(
115
+ {
116
+ schemas: ['urn:ietf:params:scim:api:messages:2.0:Error'],
117
+ detail: "Invalid configuration. #{e.message}",
118
+ status: '500',
119
+ },
120
+ :internal_server_error
121
+ )
122
+ end
123
+
124
+ rescue_from ScimRails::ExceptionHandler::UnexpectedError do |e|
125
+ json_response(
126
+ {
127
+ schemas: ['urn:ietf:params:scim:api:messages:2.0:Error'],
128
+ detail: "Unexpected Error. #{e.message}",
129
+ status: '500',
130
+ },
131
+ :internal_server_error
132
+ )
133
+ end
134
+
135
+ rescue_from ActiveRecord::RecordNotFound,
136
+ ScimRails::ExceptionHandler::ResourceNotFound do |e|
85
137
  json_response(
86
138
  {
87
- schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
139
+ schemas: ['urn:ietf:params:scim:api:messages:2.0:Error'],
88
140
  detail: "Resource #{e.id} not found.",
89
- status: "404"
141
+ status: '404',
90
142
  },
91
143
  :not_found
92
144
  )
@@ -97,18 +149,18 @@ module ScimRails
97
149
  when /has already been taken/
98
150
  json_response(
99
151
  {
100
- schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
152
+ schemas: ['urn:ietf:params:scim:api:messages:2.0:Error'],
101
153
  detail: e.message,
102
- status: "409"
154
+ status: '409',
103
155
  },
104
156
  :conflict
105
157
  )
106
158
  else
107
159
  json_response(
108
160
  {
109
- schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
161
+ schemas: ['urn:ietf:params:scim:api:messages:2.0:Error'],
110
162
  detail: e.message,
111
- status: "422"
163
+ status: '422',
112
164
  },
113
165
  :unprocessable_entity
114
166
  )
@@ -60,7 +60,7 @@ module ScimRails
60
60
  group = @company
61
61
  .public_send(ScimRails.config.scim_groups_scope)
62
62
  .find(params[:id])
63
- patch = ScimPatch.new(params, ScimRails.config.mutable_group_attributes_schema)
63
+ patch = ScimPatch.new(params, :group)
64
64
  patch.save(group)
65
65
 
66
66
  json_scim_response(object: group)
@@ -68,13 +68,24 @@ module ScimRails
68
68
 
69
69
  def destroy
70
70
  unless ScimRails.config.group_destroy_method
71
- raise ScimRails::ExceptionHandler::UnsupportedDeleteRequest
71
+ raise ScimRails::ExceptionHandler::InvalidConfiguration
72
72
  end
73
73
 
74
74
  group = @company
75
75
  .public_send(ScimRails.config.scim_groups_scope)
76
76
  .find(params[:id])
77
- group.public_send(ScimRails.config.group_destroy_method)
77
+ raise ActiveRecord::RecordNotFound unless group
78
+
79
+ begin
80
+ group.public_send(ScimRails.config.group_destroy_method)
81
+ rescue NoMethodError => e
82
+ raise ScimRails::ExceptionHandler::InvalidConfiguration, e.message
83
+ rescue ActiveRecord::RecordNotDestroyed => e
84
+ raise ScimRails::ExceptionHandler::InvalidRequest, e.message
85
+ rescue => e
86
+ raise ScimRails::ExceptionHandler::UnexpectedError, e.message
87
+ end
88
+
78
89
  head :no_content
79
90
  end
80
91
 
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ScimRails
4
+ class ScimSchemasController < ScimRails::ApplicationController
5
+ def index
6
+ schemas = ScimRails.config.schemas
7
+
8
+ counts = ScimCount.new(
9
+ start_index: params[:startIndex],
10
+ limit: params[:count],
11
+ total: schemas.count
12
+ )
13
+
14
+ list_schemas_response(schemas, counts)
15
+ end
16
+
17
+ def show
18
+ schema = ScimRails.config.schemas.find do |s|
19
+ s[:id] == params[:id]
20
+ end
21
+
22
+ raise ScimRails::ExceptionHandler::ResourceNotFound, params[:id] if schema.nil?
23
+
24
+ json_response(schema)
25
+ end
26
+
27
+ private
28
+
29
+ def list_schemas_response(schemas, counts)
30
+ response = {
31
+ schemas: [
32
+ 'urn:ietf:params:scim:api:messages:2.0:ListResponse'
33
+ ],
34
+ totalResults: counts.total,
35
+ startIndex: counts.start_index,
36
+ itemsPerPage: counts.limit,
37
+ Resources: schemas[counts.offset...counts.offset + counts.limit],
38
+ }
39
+ json_response(response)
40
+ end
41
+ end
42
+ end
@@ -3,7 +3,6 @@
3
3
  module ScimRails
4
4
  class ScimUsersController < ScimRails::ApplicationController
5
5
 
6
-
7
6
  def index
8
7
  if params[:filter].present?
9
8
  query = ScimRails::ScimQueryParser.new(
@@ -50,8 +49,6 @@ module ScimRails
50
49
  json_scim_response(object: user, status: :created)
51
50
  end
52
51
 
53
-
54
-
55
52
  def show
56
53
  user = @company.public_send(ScimRails.config.scim_users_scope).find(params[:id])
57
54
  json_scim_response(object: user)
@@ -65,12 +62,33 @@ module ScimRails
65
62
 
66
63
  def patch_update
67
64
  user = @company.public_send(ScimRails.config.scim_users_scope).find(params[:id])
68
- patch = ScimPatch.new(params, ScimRails.config.mutable_user_attributes_schema)
65
+ patch = ScimPatch.new(params, :user)
69
66
  patch.save(user)
70
67
 
71
68
  json_scim_response(object: user)
72
69
  end
73
70
 
71
+ def destroy
72
+ unless ScimRails.config.user_destroy_method
73
+ raise ScimRails::ExceptionHandler::InvalidConfiguration
74
+ end
75
+
76
+ user = @company.public_send(ScimRails.config.scim_users_scope).find(params[:id])
77
+ raise ActiveRecord::RecordNotFound unless user
78
+
79
+ begin
80
+ user.public_send(ScimRails.config.user_destroy_method)
81
+ rescue NoMethodError => e
82
+ raise ScimRails::ExceptionHandler::InvalidConfiguration, e.message
83
+ rescue ActiveRecord::RecordNotDestroyed => e
84
+ raise ScimRails::ExceptionHandler::InvalidRequest, e.message
85
+ rescue => e
86
+ raise ScimRails::ExceptionHandler::UnexpectedError, e.message
87
+ end
88
+
89
+ head :no_content
90
+ end
91
+
74
92
  private
75
93
 
76
94
  def permitted_user_params
@@ -4,18 +4,24 @@
4
4
  class ScimPatch
5
5
  attr_accessor :operations
6
6
 
7
- def initialize(params, mutable_attributes_schema)
8
- # FIXME: raise proper error.
9
- unless params['schemas'] == ['urn:ietf:params:scim:api:messages:2.0:PatchOp']
10
- raise StandardError
11
- end
12
- if params['Operations'].nil?
7
+ def initialize(params, resource_type)
8
+ if params['schemas'] != ['urn:ietf:params:scim:api:messages:2.0:PatchOp'] ||
9
+ params['Operations'].nil?
13
10
  raise ScimRails::ExceptionHandler::UnsupportedPatchRequest
14
11
  end
15
12
 
16
- @operations = params['Operations'].map do |operation|
17
- ScimPatchOperation.new(operation['op'], operation['path'], operation['value'],
18
- mutable_attributes_schema)
13
+ # complex-value(Hash) operation is converted to multiple single-value operations
14
+ converted_operations = ScimPatchOperationConverter.convert(params['Operations'])
15
+ @operations = converted_operations.map do |o|
16
+ create_operation(resource_type, o['op'], o['path'], o['value'])
17
+ end
18
+ end
19
+
20
+ def create_operation(resource_type, op, path, value)
21
+ if resource_type == :user
22
+ ScimPatchOperationUser.new(op, path, value)
23
+ else
24
+ ScimPatchOperationGroup.new(op, path, value)
19
25
  end
20
26
  end
21
27
 
@@ -2,88 +2,67 @@
2
2
 
3
3
  # Parse One of "Operations" in PATCH request
4
4
  class ScimPatchOperation
5
- attr_accessor :op, :path_scim, :path_sp, :value
5
+ attr_reader :op, :path_scim, :path_sp, :value
6
6
 
7
- def initialize(op, path, value, mutable_attributes_schema)
8
- # FIXME: Raise proper Error
9
- raise StandardError unless op.downcase.in? %w[add replace remove]
10
-
11
- # No path is not supported.
12
- # FIXME: Raise proper Error
13
- raise ScimRails::ExceptionHandler::UnsupportedPatchRequest if path.nil?
14
- raise ScimRails::ExceptionHandler::UnsupportedPatchRequest if value.nil?
15
-
16
- @op = op.downcase.to_sym
17
- @path_scim = path
18
- @path_sp = convert_path(path, mutable_attributes_schema)
19
- @value = convert_bool_if_string(value, @path_scim)
20
- end
21
-
22
- def save(model)
23
- if @path_scim == 'members' # Only members are supported for value is an array
24
- update_member_ids = @value.map do |v|
25
- v[ScimRails.config.group_member_relation_schema.keys.first]
26
- end
7
+ # path presence is guaranteed by ScimPatchOperationConverter
8
+ #
9
+ # value must be String or Array.
10
+ # complex-value(Hash) is converted to multiple single-value operations by ScimPatchOperationConverter
11
+ def initialize(op, path, value)
12
+ if !op.in?(%w[add replace remove]) || path.nil?
13
+ raise ScimRails::ExceptionHandler::UnsupportedPatchRequest
14
+ end
27
15
 
28
- current_member_ids = model
29
- .public_send(ScimRails.config.group_member_relation_attribute)
30
- case @op
31
- when :add
32
- member_ids = current_member_ids.concat(update_member_ids)
33
- when :replace
34
- member_ids = current_member_ids.concat(update_member_ids)
35
- when :remove
36
- member_ids = current_member_ids - update_member_ids
37
- end
16
+ # define validate method in the inherited class
17
+ validate(op, path, value)
38
18
 
39
- # Only the member addition process is saved by each ids
40
- model.public_send("#{ScimRails.config.group_member_relation_attribute}=",
41
- member_ids.uniq)
42
- return
43
- end
19
+ @op = op
20
+ @value = value
21
+ @path_scim = parse_path_scim(path)
22
+ @path_sp = path_scim_to_path_sp(@path_scim)
44
23
 
45
- case @op
46
- when :add, :replace
47
- model.attributes = { @path_sp => @value }
48
- when :remove
49
- model.attributes = { @path_sp => nil }
50
- end
24
+ # define parse method in the inherited class
51
25
  end
52
26
 
53
27
  private
54
28
 
55
- def convert_path(path, mutable_attributes_schema)
56
- # For now, library does not support Multi-Valued Attributes properly.
57
- # examle:
58
- # path = 'emails[type eq "work"].value'
59
- # mutable_attributes_schema = {
60
- # emails: [
61
- # {
62
- # value: :mail_address,
63
- # }
64
- # ],
65
- # }
29
+ def parse_path_scim(path)
30
+ # 'emails[type eq "work"].value' is parsed as follows:
66
31
  #
67
- # Library ignores filter conditions (like [type eq "work"])
68
- # and always uses the first element of the array
69
- dig_keys = path.gsub(/\[(.+?)\]/, '.0').split('.').map do |step|
70
- step == '0' ? 0 : step.to_sym
71
- end
72
- mutable_attributes_schema.dig(*dig_keys)
73
- end
32
+ # {
33
+ # attribute: 'emails',
34
+ # filter: {
35
+ # attribute: 'type',
36
+ # operator: 'eq',
37
+ # parameter: 'work'
38
+ # },
39
+ # rest_path: ['value']
40
+ # }
41
+ #
42
+ # This method suport only single operator
74
43
 
75
- def convert_bool_if_string(value, path)
76
- # This method correct value in requests from Azure AD according to SCIM.
77
- # When path is not active, do nothing and return
78
- return value if path != 'active'
44
+ # path: emails.value
45
+ # filter_string: type eq "work"
46
+ path_str = path.dup
47
+ filter_string = path_str.slice!(/\[(.+?)\]/, 0)&.slice(/\[(.+?)\]/, 1)
79
48
 
80
- case value
81
- when 'true', 'True' then
82
- return true
83
- when 'false', 'False' then
84
- return false
85
- else
86
- return value
49
+ # path_elements: ['emails', 'value']
50
+ path_elements = path_str.split('.')
51
+
52
+ # filter_elements: ['type', 'eq', '"work"']
53
+ filter_elements = filter_string&.split(' ')
54
+ path_scim = { attribute: path_elements[0],
55
+ rest_path: path_elements.slice(1...path_elements.length), }
56
+ if filter_elements.present?
57
+ path_scim[:filter] = {
58
+ attribute: filter_elements[0],
59
+ operator: filter_elements[1],
60
+ # delete double quotation
61
+ parameter: filter_elements[2].slice(1...filter_elements[2].length - 1),
62
+ }
87
63
  end
64
+
65
+ path_scim
88
66
  end
67
+
89
68
  end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ # 1. Convert each multi-value operation to single-value operations.
4
+ # 2. Fix wrong "value".
5
+ # 3. convert "op" to lower case
6
+ #
7
+ # About #1
8
+ # to handle request pattern A and B in the same way,
9
+ # convert complex-value to path + single-value
10
+ #
11
+ # Typical request is path_base = nil and value = complex-value:
12
+ # {
13
+ # "op": "replace",
14
+ # "value": {
15
+ # "displayName": "Taro Suzuki",
16
+ # "name.givenName": "taro"
17
+ # }
18
+ # }
19
+ #
20
+ # This request is converted to:
21
+ # [
22
+ # {
23
+ # "op": "replace",
24
+ # "path": "displayName",
25
+ # "value": "Taro Suzuki",
26
+ # }
27
+ # {
28
+ # "op": "replace",
29
+ # "path": "name.givenName",
30
+ # "value": "taro"
31
+ # }
32
+ # ]
33
+ class ScimPatchOperationConverter
34
+ class << self
35
+ def convert(operations)
36
+ operations.each_with_object([]) do |o, result|
37
+ value = o['value']
38
+ value = value.permit!.to_h if value.instance_of?(ActionController::Parameters)
39
+
40
+ if value.is_a?(Hash)
41
+ converted_operations = convert_to_single_value_operations(o['op'], o['path'],
42
+ value)
43
+ result.concat(converted_operations)
44
+ next
45
+ end
46
+
47
+ result << fix_operation_format(o['op'], o['path'], value)
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def convert_to_single_value_operations(op, path_base, hash_value)
54
+ hash_value.map do |k, v|
55
+ path = if path_base.present?
56
+ "#{path_base}.#{k}"
57
+ else
58
+ k
59
+ end
60
+ fix_operation_format(op, path, v)
61
+ end
62
+ end
63
+
64
+ def fix_operation_format(op, path, value)
65
+ { 'op' => op.downcase, 'path' => path, 'value' => fix_value(value, path) }
66
+ end
67
+
68
+ def fix_value(value, path)
69
+ if path == 'active'
70
+ convert_bool_if_string(value)
71
+ else
72
+ value
73
+ end
74
+ end
75
+
76
+ # According to SCIM, correct value in requests from Azure AD.
77
+ # When using non-application gallery in Azure AD, and feature flag is not specified,
78
+ # Azure AD sends string for 'active' attribute
79
+ def convert_bool_if_string(value)
80
+ case value
81
+ when 'true', 'True'
82
+ return true
83
+ when 'false', 'False'
84
+ return false
85
+ else
86
+ return value
87
+ end
88
+ end
89
+ end
90
+ end