scimaenaga 0.5.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/MIT-LICENSE +1 -0
- data/README.md +2 -14
- data/app/controllers/concerns/scim_rails/exception_handler.rb +43 -1
- data/app/controllers/scim_rails/scim_groups_controller.rb +64 -40
- data/app/controllers/scim_rails/scim_users_controller.rb +39 -65
- data/app/libraries/scim_patch.rb +15 -10
- data/app/libraries/scim_patch_operation.rb +127 -24
- data/app/models/scim_rails/scim_query_parser.rb +5 -3
- data/config/routes.rb +2 -0
- data/lib/generators/scim_rails/templates/initializer.rb +0 -6
- data/lib/scim_rails/config.rb +1 -2
- data/lib/scim_rails/version.rb +1 -1
- data/spec/controllers/scim_rails/scim_groups_controller_spec.rb +249 -136
- data/spec/controllers/scim_rails/scim_users_controller_spec.rb +413 -203
- data/spec/dummy/app/models/user.rb +21 -0
- data/spec/dummy/bin/setup +2 -0
- data/spec/dummy/config/initializers/scim_rails_config.rb +6 -4
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/migrate/20220117095407_add_country_to_users.rb +5 -0
- data/spec/dummy/db/migrate/20220131090107_add_deletable_to_users.rb +5 -0
- data/spec/dummy/db/schema.rb +7 -5
- data/spec/dummy/db/seeds.rb +15 -1
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +0 -0
- data/spec/dummy/log/test.log +5770 -0
- data/spec/dummy/put_group.http +5 -0
- data/spec/dummy/tmp/restart.txt +0 -0
- data/spec/factories/user.rb +2 -0
- data/spec/libraries/scim_patch_operation_spec.rb +61 -31
- data/spec/libraries/scim_patch_spec.rb +38 -29
- data/spec/models/scim_query_parser_spec.rb +30 -0
- metadata +83 -67
- data/spec/support/scim_rails_config.rb +0 -59
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6d2d55daa03a9b1e4771b1f2e1287a75075514d57faeb0075ea8784a7dd8cc9e
|
4
|
+
data.tar.gz: b4a38c2c629cce871703a2a1874c3be93e34d55f0d5eacadae8da9b8c5e5c438
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 43e2ec416e2f9bad5c185042aff3e6fd576437d3d9bff5ac6ad5d1f930246fa0b59b12e1940a5498e386c8c8f385a245d531907e7ab5850cc8ebc5a7c35df607
|
7
|
+
data.tar.gz: 284bead26bc660ca0d4cc229dd5f13102501904b7658e3e780f5ea5657eb367c52cd5399d4a29dcd20af66eee19ce4592af7defa62c2f182bb7b23f76ebe6a31
|
data/MIT-LICENSE
CHANGED
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
|
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
|
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.
|
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
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
37
|
-
|
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
|
-
|
44
|
-
|
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
|
-
|
52
|
-
|
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::
|
71
|
+
raise ScimRails::ExceptionHandler::InvalidConfiguration
|
60
72
|
end
|
73
|
+
|
61
74
|
group = @company
|
62
|
-
|
63
|
-
|
64
|
-
group
|
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
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
77
|
-
|
100
|
+
converted.merge(member_params)
|
101
|
+
end
|
78
102
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
89
|
-
|
90
|
-
|
112
|
+
def mutable_attributes
|
113
|
+
ScimRails.config.mutable_group_attributes
|
114
|
+
end
|
91
115
|
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
5
|
-
|
6
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
19
|
-
|
20
|
-
|
18
|
+
query.parameter
|
19
|
+
)
|
20
|
+
.order(ScimRails.config.scim_users_list_order)
|
21
21
|
else
|
22
22
|
users = @company
|
23
|
-
|
24
|
-
|
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
|
-
|
40
|
-
|
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
|
-
|
47
|
-
|
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
|
-
|
54
|
-
|
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.
|
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
|
data/app/libraries/scim_patch.rb
CHANGED
@@ -5,24 +5,29 @@ class ScimPatch
|
|
5
5
|
attr_accessor :operations
|
6
6
|
|
7
7
|
def initialize(params, mutable_attributes_schema)
|
8
|
-
|
9
|
-
|
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[
|
11
|
+
if params['Operations'].nil?
|
13
12
|
raise ScimRails::ExceptionHandler::UnsupportedPatchRequest
|
14
13
|
end
|
15
14
|
|
16
|
-
@operations = params[
|
17
|
-
ScimPatchOperation.new(operation[
|
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
|
23
|
-
|
24
|
-
operation
|
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
|
-
|
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
|
-
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
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(/\[(.+?)\]/,
|
51
|
-
step ==
|
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
|
-
|
16
|
+
dig_keys = attribute.split(".").map do |step|
|
17
|
+
step == "0" ? 0 : step.to_sym
|
18
|
+
end
|
17
19
|
|
18
|
-
mapped_attribute = query_attributes
|
20
|
+
mapped_attribute = query_attributes.dig(*dig_keys)
|
19
21
|
raise ScimRails::ExceptionHandler::InvalidQuery if mapped_attribute.blank?
|
20
22
|
|
21
23
|
mapped_attribute
|