scimaenaga 0.5.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +1 -0
  3. data/README.md +2 -14
  4. data/app/controllers/concerns/scim_rails/exception_handler.rb +43 -1
  5. data/app/controllers/scim_rails/scim_groups_controller.rb +64 -40
  6. data/app/controllers/scim_rails/scim_users_controller.rb +39 -65
  7. data/app/libraries/scim_patch.rb +15 -10
  8. data/app/libraries/scim_patch_operation.rb +127 -24
  9. data/app/models/scim_rails/scim_query_parser.rb +5 -3
  10. data/config/routes.rb +2 -0
  11. data/lib/generators/scim_rails/templates/initializer.rb +0 -6
  12. data/lib/scim_rails/config.rb +1 -2
  13. data/lib/scim_rails/version.rb +1 -1
  14. data/spec/controllers/scim_rails/scim_groups_controller_spec.rb +249 -136
  15. data/spec/controllers/scim_rails/scim_users_controller_spec.rb +413 -203
  16. data/spec/dummy/app/models/user.rb +21 -0
  17. data/spec/dummy/bin/setup +2 -0
  18. data/spec/dummy/config/initializers/scim_rails_config.rb +6 -4
  19. data/spec/dummy/db/development.sqlite3 +0 -0
  20. data/spec/dummy/db/migrate/20220117095407_add_country_to_users.rb +5 -0
  21. data/spec/dummy/db/migrate/20220131090107_add_deletable_to_users.rb +5 -0
  22. data/spec/dummy/db/schema.rb +7 -5
  23. data/spec/dummy/db/seeds.rb +15 -1
  24. data/spec/dummy/db/test.sqlite3 +0 -0
  25. data/spec/dummy/log/development.log +0 -0
  26. data/spec/dummy/log/test.log +5770 -0
  27. data/spec/dummy/put_group.http +5 -0
  28. data/spec/dummy/tmp/restart.txt +0 -0
  29. data/spec/factories/user.rb +2 -0
  30. data/spec/libraries/scim_patch_operation_spec.rb +61 -31
  31. data/spec/libraries/scim_patch_spec.rb +38 -29
  32. data/spec/models/scim_query_parser_spec.rb +30 -0
  33. metadata +83 -67
  34. data/spec/support/scim_rails_config.rb +0 -59
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b1fa2914fdf977f0496f84a023f14ba49f61c893683fd04a85ca7ea4a35c4641
4
- data.tar.gz: 0c23e5d65e10a641d965e34a0cc78fc72c0c5d9e80a438ba86a1aa59f70e4360
3
+ metadata.gz: 6d2d55daa03a9b1e4771b1f2e1287a75075514d57faeb0075ea8784a7dd8cc9e
4
+ data.tar.gz: b4a38c2c629cce871703a2a1874c3be93e34d55f0d5eacadae8da9b8c5e5c438
5
5
  SHA512:
6
- metadata.gz: 98e1cecc1b60630c69c78380a59492f9dd293c25b267f16d985bcb76570cac0549648781dd9d3f749713df9e40df329349f88708af1e953ec65b7b18ec560648
7
- data.tar.gz: 86d28c8634b38f8af5d85d7670b27e3e7caac314dc90131cd6de9910a284a66dd7a5763ee222c0a8c5adfcafc382b4a919c082fe7f150614359c9f95d1535e59
6
+ metadata.gz: 43e2ec416e2f9bad5c185042aff3e6fd576437d3d9bff5ac6ad5d1f930246fa0b59b12e1940a5498e386c8c8f385a245d531907e7ab5850cc8ebc5a7c35df607
7
+ data.tar.gz: 284bead26bc660ca0d4cc229dd5f13102501904b7658e3e780f5ea5657eb367c52cd5399d4a29dcd20af66eee19ce4592af7defa62c2f182bb7b23f76ebe6a31
data/MIT-LICENSE CHANGED
@@ -1,4 +1,5 @@
1
1
  Copyright 2018 Lessonly
2
+ Copyright 2021 Studist Corporation
2
3
 
3
4
  Permission is hereby granted, free of charge, to any person obtaining
4
5
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Scimaenaga
4
4
 
5
- NOTE: This Gem is not yet fully SCIM complaint. It was developed with the main function of interfacing with Okta. There are features of SCIM that this Gem does not implement as described in the SCIM documentation or that have been left out completely.
5
+ NOTE: This Gem is not yet fully SCIM complaint. It was developed with the main function of interfacing with Azure AD. There are features of SCIM that this Gem does not implement as described in the SCIM documentation or that have been left out completely.
6
6
 
7
7
  #### What is SCIM?
8
8
 
@@ -10,7 +10,7 @@ SCIM stands for System for Cross-domain Identity Management. At its core, it is
10
10
 
11
11
  To learn more about SCIM 2.0 you can read the documentation at [RFC 7643](https://tools.ietf.org/html/rfc7643) and [RFC 7644](https://tools.ietf.org/html/rfc7644).
12
12
 
13
- The goal of the Gem is to offer a relatively painless way of adding SCIM 2.0 to your app. This Gem should be fully compatible with Okta's SCIM implementation. This project is ongoing and will hopefully be fully SCIM compliant in time. Pull requests that assist in meeting that goal are welcome!
13
+ The goal of the Gem is to offer a relatively painless way of adding SCIM 2.0 to your app. This Gem should be fully compatible with Azure AD's SCIM implementation. This project is ongoing and will hopefully be fully SCIM compliant in time. Pull requests that assist in meeting that goal are welcome!
14
14
 
15
15
  ## Installation
16
16
 
@@ -236,18 +236,6 @@ Sample request:
236
236
  $ curl -X PUT 'http://username:password@localhost:3000/scim/v2/Users/1' -d '{"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],"userName":"test@example.com","name":{"givenName":"Test","familyName":"User"},"emails":[{"primary":true,"value":"test@example.com","type":"work"}],"displayName":"Test User","active":true}' -H 'Content-Type: application/scim+json'
237
237
  ```
238
238
 
239
- ### Deprovision / Reprovision
240
-
241
- The PATCH request was implemented to work with Okta. Okta updates profiles with PUT and deprovisions / reprovisions with PATCH. This implementation of PATCH is not SCIM compliant as it does not update a single attribute on the user profile but instead only sends a status update request to the record.
242
-
243
- We would like to implement PATCH to be fully SCIM compliant in future releases.
244
-
245
- Sample request:
246
-
247
- ```bash
248
- $ curl -X PATCH 'http://username:password@localhost:3000/scim/v2/Users/1' -d '{"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], "Operations": [{"op": "replace", "value": { "active": false }}]}' -H 'Content-Type: application/scim+json'
249
- ```
250
-
251
239
  ### Error Handling
252
240
 
253
241
  By default, scimaenaga will output any unhandled exceptions to your configured rails logs.
@@ -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,12 @@ 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
+
19
28
  included do
20
29
  if Rails.env.production?
21
30
  rescue_from StandardError do |exception|
@@ -47,6 +56,17 @@ module ScimRails
47
56
  )
48
57
  end
49
58
 
59
+ rescue_from ScimRails::ExceptionHandler::InvalidRequest do |e|
60
+ json_response(
61
+ {
62
+ schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
63
+ detail: "Invalid request. #{e.message}",
64
+ status: "400"
65
+ },
66
+ :bad_request
67
+ )
68
+ end
69
+
50
70
  rescue_from ScimRails::ExceptionHandler::InvalidQuery do
51
71
  json_response(
52
72
  {
@@ -63,7 +83,7 @@ module ScimRails
63
83
  json_response(
64
84
  {
65
85
  schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
66
- detail: "Invalid PATCH request. This PATCH endpoint only supports deprovisioning and reprovisioning records.",
86
+ detail: "Invalid PATCH request.",
67
87
  status: "422"
68
88
  },
69
89
  :unprocessable_entity
@@ -81,6 +101,28 @@ module ScimRails
81
101
  )
82
102
  end
83
103
 
104
+ rescue_from ScimRails::ExceptionHandler::InvalidConfiguration do |e|
105
+ json_response(
106
+ {
107
+ schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
108
+ detail: "Invalid configuration. #{e.message}",
109
+ status: "500"
110
+ },
111
+ :internal_server_error
112
+ )
113
+ end
114
+
115
+ rescue_from ScimRails::ExceptionHandler::UnexpectedError do |e|
116
+ json_response(
117
+ {
118
+ schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
119
+ detail: "Unexpected Error. #{e.message}",
120
+ status: "500"
121
+ },
122
+ :internal_server_error
123
+ )
124
+ end
125
+
84
126
  rescue_from ActiveRecord::RecordNotFound do |e|
85
127
  json_response(
86
128
  {
@@ -9,17 +9,19 @@ module ScimRails
9
9
  )
10
10
 
11
11
  groups = @company
12
- .public_send(ScimRails.config.scim_groups_scope)
13
- .where(
14
- "#{ScimRails.config.scim_groups_model.connection.quote_column_name(query.attribute)} #{query.operator} ?",
15
- query.parameter
16
- )
17
- .order(ScimRails.config.scim_groups_list_order)
12
+ .public_send(ScimRails.config.scim_groups_scope)
13
+ .where(
14
+ "#{ScimRails.config.scim_groups_model
15
+ .connection.quote_column_name(query.attribute)}
16
+ #{query.operator} ?",
17
+ query.parameter
18
+ )
19
+ .order(ScimRails.config.scim_groups_list_order)
18
20
  else
19
21
  groups = @company
20
- .public_send(ScimRails.config.scim_groups_scope)
21
- .preload(:users)
22
- .order(ScimRails.config.scim_groups_list_order)
22
+ .public_send(ScimRails.config.scim_groups_scope)
23
+ .preload(:users)
24
+ .order(ScimRails.config.scim_groups_list_order)
23
25
  end
24
26
 
25
27
  counts = ScimCount.new(
@@ -33,64 +35,86 @@ module ScimRails
33
35
 
34
36
  def show
35
37
  group = @company
36
- .public_send(ScimRails.config.scim_groups_scope)
37
- .find(params[:id])
38
+ .public_send(ScimRails.config.scim_groups_scope)
39
+ .find(params[:id])
38
40
  json_scim_response(object: group)
39
41
  end
40
42
 
41
43
  def create
42
44
  group = @company
43
- .public_send(ScimRails.config.scim_groups_scope)
44
- .create!(permitted_group_params)
45
+ .public_send(ScimRails.config.scim_groups_scope)
46
+ .create!(permitted_group_params)
45
47
 
46
48
  json_scim_response(object: group, status: :created)
47
49
  end
48
50
 
49
51
  def put_update
50
52
  group = @company
51
- .public_send(ScimRails.config.scim_groups_scope)
52
- .find(params[:id])
53
+ .public_send(ScimRails.config.scim_groups_scope)
54
+ .find(params[:id])
53
55
  group.update!(permitted_group_params)
54
56
  json_scim_response(object: group)
55
57
  end
56
58
 
59
+ def patch_update
60
+ group = @company
61
+ .public_send(ScimRails.config.scim_groups_scope)
62
+ .find(params[:id])
63
+ patch = ScimPatch.new(params, ScimRails.config.mutable_group_attributes_schema)
64
+ patch.save(group)
65
+
66
+ json_scim_response(object: group)
67
+ end
68
+
57
69
  def destroy
58
70
  unless ScimRails.config.group_destroy_method
59
- raise ScimRails::ExceptionHandler::UnsupportedDeleteRequest
71
+ raise ScimRails::ExceptionHandler::InvalidConfiguration
60
72
  end
73
+
61
74
  group = @company
62
- .public_send(ScimRails.config.scim_groups_scope)
63
- .find(params[:id])
64
- group.public_send(ScimRails.config.group_destroy_method)
75
+ .public_send(ScimRails.config.scim_groups_scope)
76
+ .find(params[:id])
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
+
65
89
  head :no_content
66
90
  end
67
91
 
68
92
  private
69
93
 
70
- def permitted_group_params
71
- converted = mutable_attributes.each.with_object({}) do |attribute, hash|
72
- hash[attribute] = find_value_for(attribute)
73
- end
74
- return converted unless params[:members]
94
+ def permitted_group_params
95
+ converted = mutable_attributes.each.with_object({}) do |attribute, hash|
96
+ hash[attribute] = find_value_for(attribute)
97
+ end
98
+ return converted unless params[:members]
75
99
 
76
- converted.merge(member_params)
77
- end
100
+ converted.merge(member_params)
101
+ end
78
102
 
79
- def member_params
80
- {
81
- ScimRails.config.group_member_relation_attribute =>
82
- params[:members].map do |member|
83
- member[ScimRails.config.group_member_relation_schema.keys.first]
84
- end
85
- }
86
- end
103
+ def member_params
104
+ {
105
+ ScimRails.config.group_member_relation_attribute =>
106
+ params[:members].map do |member|
107
+ member[ScimRails.config.group_member_relation_schema.keys.first]
108
+ end,
109
+ }
110
+ end
87
111
 
88
- def mutable_attributes
89
- ScimRails.config.mutable_group_attributes
90
- end
112
+ def mutable_attributes
113
+ ScimRails.config.mutable_group_attributes
114
+ end
91
115
 
92
- def controller_schema
93
- ScimRails.config.mutable_group_attributes_schema
94
- end
116
+ def controller_schema
117
+ ScimRails.config.mutable_group_attributes_schema
118
+ end
95
119
  end
96
120
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ScimRails
4
- class ScimUsersController < ScimRails::ApplicationController # rubocop:disable Metrics/ClassLength
5
- # rubocop:disable Metrics/AbcSize
6
- # rubocop:disable Metrics/MethodLength
4
+ class ScimUsersController < ScimRails::ApplicationController
5
+
6
+
7
7
  def index
8
8
  if params[:filter].present?
9
9
  query = ScimRails::ScimQueryParser.new(
@@ -11,17 +11,17 @@ module ScimRails
11
11
  )
12
12
 
13
13
  users = @company
14
- .public_send(ScimRails.config.scim_users_scope)
15
- .where(
16
- "#{ScimRails.config.scim_users_model
14
+ .public_send(ScimRails.config.scim_users_scope)
15
+ .where(
16
+ "#{ScimRails.config.scim_users_model
17
17
  .connection.quote_column_name(query.attribute)} #{query.operator} ?",
18
- query.parameter
19
- )
20
- .order(ScimRails.config.scim_users_list_order)
18
+ query.parameter
19
+ )
20
+ .order(ScimRails.config.scim_users_list_order)
21
21
  else
22
22
  users = @company
23
- .public_send(ScimRails.config.scim_users_scope)
24
- .order(ScimRails.config.scim_users_list_order)
23
+ .public_send(ScimRails.config.scim_users_scope)
24
+ .order(ScimRails.config.scim_users_list_order)
25
25
  end
26
26
 
27
27
  counts = ScimCount.new(
@@ -36,22 +36,21 @@ module ScimRails
36
36
  def create
37
37
  if ScimRails.config.scim_user_prevent_update_on_create
38
38
  user = @company
39
- .public_send(ScimRails.config.scim_users_scope)
40
- .create!(permitted_user_params)
39
+ .public_send(ScimRails.config.scim_users_scope)
40
+ .create!(permitted_user_params)
41
41
  else
42
42
  username_key = ScimRails.config.queryable_user_attributes[:userName]
43
43
  find_by_username = {}
44
44
  find_by_username[username_key] = permitted_user_params[username_key]
45
45
  user = @company
46
- .public_send(ScimRails.config.scim_users_scope)
47
- .find_or_create_by(find_by_username)
46
+ .public_send(ScimRails.config.scim_users_scope)
47
+ .find_or_create_by(find_by_username)
48
48
  user.update!(permitted_user_params)
49
49
  end
50
- update_status(user) unless put_active_param.nil?
51
50
  json_scim_response(object: user, status: :created)
52
51
  end
53
- # rubocop:enable Metrics/AbcSize
54
- # rubocop:enable Metrics/MethodLength
52
+
53
+
55
54
 
56
55
  def show
57
56
  user = @company.public_send(ScimRails.config.scim_users_scope).find(params[:id])
@@ -60,7 +59,6 @@ module ScimRails
60
59
 
61
60
  def put_update
62
61
  user = @company.public_send(ScimRails.config.scim_users_scope).find(params[:id])
63
- update_status(user) unless put_active_param.nil?
64
62
  user.update!(permitted_user_params)
65
63
  json_scim_response(object: user)
66
64
  end
@@ -68,13 +66,32 @@ module ScimRails
68
66
  def patch_update
69
67
  user = @company.public_send(ScimRails.config.scim_users_scope).find(params[:id])
70
68
  patch = ScimPatch.new(params, ScimRails.config.mutable_user_attributes_schema)
71
- patch.apply(user)
72
- user.save
69
+ patch.save(user)
73
70
 
74
- # update_status(user)
75
71
  json_scim_response(object: user)
76
72
  end
77
73
 
74
+ def destroy
75
+ unless ScimRails.config.user_destroy_method
76
+ raise ScimRails::ExceptionHandler::InvalidConfiguration
77
+ end
78
+
79
+ user = @company.public_send(ScimRails.config.scim_users_scope).find(params[:id])
80
+ raise ActiveRecord::RecordNotFound unless user
81
+
82
+ begin
83
+ user.public_send(ScimRails.config.user_destroy_method)
84
+ rescue NoMethodError => e
85
+ raise ScimRails::ExceptionHandler::InvalidConfiguration, e.message
86
+ rescue ActiveRecord::RecordNotDestroyed => e
87
+ raise ScimRails::ExceptionHandler::InvalidRequest, e.message
88
+ rescue => e
89
+ raise ScimRails::ExceptionHandler::UnexpectedError, e.message
90
+ end
91
+
92
+ head :no_content
93
+ end
94
+
78
95
  private
79
96
 
80
97
  def permitted_user_params
@@ -86,48 +103,5 @@ module ScimRails
86
103
  def controller_schema
87
104
  ScimRails.config.mutable_user_attributes_schema
88
105
  end
89
-
90
- def update_status(user)
91
- user.public_send(ScimRails.config.user_reprovision_method) if active?
92
- user.public_send(ScimRails.config.user_deprovision_method) unless active?
93
- end
94
-
95
- def active?
96
- active = put_active_param
97
- active = patch_active_param if active.nil?
98
-
99
- case active
100
- when true, "true", 1
101
- true
102
- when false, "false", 0
103
- false
104
- else
105
- raise ActiveRecord::RecordInvalid
106
- end
107
- end
108
-
109
- def put_active_param
110
- params[:active]
111
- end
112
-
113
- def patch_active_param
114
- handle_invalid = lambda do
115
- raise ScimRails::ExceptionHandler::UnsupportedPatchRequest
116
- end
117
-
118
- operations = params["Operations"] || {}
119
-
120
- valid_operation = operations.find(handle_invalid) do |operation|
121
- valid_patch_operation?(operation)
122
- end
123
-
124
- valid_operation.dig("value", "active")
125
- end
126
-
127
- def valid_patch_operation?(operation)
128
- operation["op"].casecmp("replace") &&
129
- operation["value"] &&
130
- [true, false].include?(operation["value"]["active"])
131
- end
132
106
  end
133
107
  end
@@ -5,24 +5,29 @@ class ScimPatch
5
5
  attr_accessor :operations
6
6
 
7
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
8
+ unless params['schemas'] == ['urn:ietf:params:scim:api:messages:2.0:PatchOp']
9
+ raise ScimRails::ExceptionHandler::UnsupportedPatchRequest
11
10
  end
12
- if params["Operations"].nil?
11
+ if params['Operations'].nil?
13
12
  raise ScimRails::ExceptionHandler::UnsupportedPatchRequest
14
13
  end
15
14
 
16
- @operations = params["Operations"].map do |operation|
17
- ScimPatchOperation.new(operation["op"], operation["path"], operation["value"],
15
+ @operations = params['Operations'].map do |operation|
16
+ ScimPatchOperation.new(operation['op'], operation['path'], operation['value'],
18
17
  mutable_attributes_schema)
19
18
  end
20
19
  end
21
20
 
22
- def apply(model)
23
- @operations.each do |operation|
24
- operation.apply(model)
21
+ def save(model)
22
+ model.transaction do
23
+ @operations.each do |operation|
24
+ operation.save(model)
25
+ end
26
+ model.save! if model.changed?
25
27
  end
26
- model
28
+ rescue ActiveRecord::RecordNotFound
29
+ raise
30
+ rescue StandardError
31
+ raise ScimRails::ExceptionHandler::UnsupportedPatchRequest
27
32
  end
28
33
  end
@@ -2,38 +2,126 @@
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
+
6
+ # 1 Operation means 1 attribute change
7
+ # If 1 value is specified, @operations has 1 "Operation" struct.
8
+ # If 3 value is specified, @operations has 3 "Operation" struct.
9
+ attr_accessor :operations
10
+
11
+ Operation = Struct.new('Operation', :op, :path_scim, :path_sp, :value)
6
12
 
7
13
  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 = value
14
+ op_downcase = op.downcase
15
+
16
+ unless op_downcase.in? %w[add replace remove]
17
+ raise ScimRails::ExceptionHandler::UnsupportedPatchRequest
18
+ end
19
+
20
+ @operations = []
21
+
22
+ # To handle request pattern A and B in the same way,
23
+ # convert complex-value to path + single-value
24
+ #
25
+ # pattern A
26
+ # {
27
+ # "op": "replace",
28
+ # "value": {
29
+ # "displayName": "Suzuki Taro",
30
+ # "name.givenName": "taro"
31
+ # }
32
+ # }
33
+ # => [{path: displayName, value: "Suzuki Taro"}, {path: name.givenName, value: "taro"}]
34
+ #
35
+ # pattern B
36
+ # [
37
+ # {
38
+ # "op": "replace",
39
+ # "path": "displayName"
40
+ # "value": "Suzuki Taro",
41
+ # },
42
+ # {
43
+ # "op": "replace",
44
+ # "path": "name.givenNAme"
45
+ # "value": "taro",
46
+ # },
47
+ # ]
48
+ if value.instance_of?(Hash) || value.instance_of?(ActionController::Parameters)
49
+ create_multiple_operations(op_downcase, path, value, mutable_attributes_schema)
50
+ else
51
+ create_operation(op_downcase, path, value, mutable_attributes_schema)
52
+ end
20
53
  end
21
54
 
22
- # WIP
23
- def apply(model)
24
- case @op
25
- when :add
26
- model.attributes = { @path_sp => @value }
27
- when :replace
28
- model.attributes = { @path_sp => @value }
29
- when :remove
30
- model.attributes = { @path_sp => nil }
55
+ def save(model)
56
+ @operations.each do |operation|
57
+ apply_operation(model, operation)
31
58
  end
32
59
  end
33
60
 
34
61
  private
35
62
 
63
+ def apply_operation(model, operation)
64
+ if operation.path_scim == 'members' # Only members are supported for value is an array
65
+ update_member_ids = operation.value.map do |v|
66
+ v[ScimRails.config.group_member_relation_schema.keys.first].to_s
67
+ end
68
+
69
+ current_member_ids = model.public_send(
70
+ ScimRails.config.group_member_relation_attribute
71
+ ).map(&:to_s)
72
+ case operation.op
73
+ when :add
74
+ member_ids = current_member_ids.concat(update_member_ids)
75
+ when :replace
76
+ member_ids = current_member_ids.concat(update_member_ids)
77
+ when :remove
78
+ member_ids = current_member_ids - update_member_ids
79
+ end
80
+
81
+ # Only the member addition process is saved by each ids
82
+ model.public_send("#{ScimRails.config.group_member_relation_attribute}=",
83
+ member_ids.uniq)
84
+ return
85
+ end
86
+
87
+ case operation.op
88
+ when :add, :replace
89
+ model.attributes = { operation.path_sp => operation.value }
90
+ when :remove
91
+ model.attributes = { operation.path_sp => nil }
92
+ end
93
+ end
94
+
95
+ def create_operation(op, path_scim, value, mutable_attributes_schema)
96
+ path_sp = convert_path(path_scim, mutable_attributes_schema)
97
+ value = convert_bool_if_string(value, path_scim)
98
+ @operations << Operation.new(op.to_sym, path_scim, path_sp, value)
99
+ end
100
+
101
+ # convert hash value to 1 path + 1 value
102
+ # each path is created by path_scim_base + key of value
103
+ def create_multiple_operations(op, path_scim_base, hash_value, mutable_attributes_schema)
104
+ hash_value.each do |k, v|
105
+ # Typical request is path_scim_base = nil and value = complex-value:
106
+ # {
107
+ # "op": "replace",
108
+ # "value": {
109
+ # "displayName": "Taro Suzuki",
110
+ # "name.givenName": "taro"
111
+ # }
112
+ # }
113
+ path_scim = if path_scim_base.present?
114
+ "#{path_scim_base}.#{k}"
115
+ else
116
+ k
117
+ end
118
+ create_operation(op, path_scim, v, mutable_attributes_schema)
119
+ end
120
+ end
121
+
36
122
  def convert_path(path, mutable_attributes_schema)
123
+ return nil if path.nil?
124
+
37
125
  # For now, library does not support Multi-Valued Attributes properly.
38
126
  # examle:
39
127
  # path = 'emails[type eq "work"].value'
@@ -47,9 +135,24 @@ class ScimPatchOperation
47
135
  #
48
136
  # Library ignores filter conditions (like [type eq "work"])
49
137
  # and always uses the first element of the array
50
- dig_keys = path.gsub(/\[(.+?)\]/, ".0").split(".").map do |step|
51
- step == "0" ? 0 : step.to_sym
138
+ dig_keys = path.gsub(/\[(.+?)\]/, '.0').split('.').map do |step|
139
+ step == '0' ? 0 : step.to_sym
52
140
  end
53
141
  mutable_attributes_schema.dig(*dig_keys)
54
142
  end
143
+
144
+ def convert_bool_if_string(value, path)
145
+ # This method correct value in requests from Azure AD according to SCIM.
146
+ # When path is not active, do nothing and return
147
+ return value if path != 'active'
148
+
149
+ case value
150
+ when 'true', 'True'
151
+ return true
152
+ when 'false', 'False'
153
+ return false
154
+ else
155
+ return value
156
+ end
157
+ end
55
158
  end
@@ -5,7 +5,7 @@ module ScimRails
5
5
  attr_accessor :query_elements, :query_attributes
6
6
 
7
7
  def initialize(query_string, queryable_attributes)
8
- self.query_elements = query_string.split
8
+ self.query_elements = query_string.gsub(/\[(.+?)\]/, ".0").split
9
9
  self.query_attributes = queryable_attributes
10
10
  end
11
11
 
@@ -13,9 +13,11 @@ module ScimRails
13
13
  attribute = query_elements[0]
14
14
  raise ScimRails::ExceptionHandler::InvalidQuery if attribute.blank?
15
15
 
16
- attribute = attribute.to_sym
16
+ dig_keys = attribute.split(".").map do |step|
17
+ step == "0" ? 0 : step.to_sym
18
+ end
17
19
 
18
- mapped_attribute = query_attributes[attribute]
20
+ mapped_attribute = query_attributes.dig(*dig_keys)
19
21
  raise ScimRails::ExceptionHandler::InvalidQuery if mapped_attribute.blank?
20
22
 
21
23
  mapped_attribute