scimitar 1.7.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/scimitar/active_record_backed_resources_controller.rb +10 -49
- data/app/controllers/scimitar/application_controller.rb +11 -35
- data/app/controllers/scimitar/schemas_controller.rb +0 -5
- data/app/models/scimitar/engine_configuration.rb +5 -13
- data/app/models/scimitar/error_response.rb +0 -12
- data/app/models/scimitar/lists/query_parser.rb +10 -25
- data/app/models/scimitar/resources/base.rb +4 -14
- data/app/models/scimitar/resources/mixin.rb +13 -137
- data/app/models/scimitar/schema/address.rb +0 -1
- data/app/models/scimitar/schema/attribute.rb +5 -14
- data/app/models/scimitar/schema/base.rb +1 -1
- data/app/models/scimitar/schema/vdtp.rb +1 -1
- data/app/models/scimitar/service_provider_configuration.rb +3 -14
- data/config/initializers/scimitar.rb +3 -28
- data/lib/scimitar/version.rb +2 -2
- data/lib/scimitar.rb +2 -6
- data/spec/apps/dummy/app/controllers/mock_groups_controller.rb +1 -1
- data/spec/apps/dummy/app/models/mock_group.rb +1 -1
- data/spec/apps/dummy/app/models/mock_user.rb +8 -36
- data/spec/apps/dummy/config/application.rb +1 -0
- data/spec/apps/dummy/config/environments/test.rb +28 -5
- data/spec/apps/dummy/config/initializers/scimitar.rb +10 -61
- data/spec/apps/dummy/config/routes.rb +6 -15
- data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +1 -10
- data/spec/apps/dummy/db/migrate/20210308044214_create_join_table_mock_groups_mock_users.rb +3 -8
- data/spec/apps/dummy/db/schema.rb +4 -11
- data/spec/controllers/scimitar/application_controller_spec.rb +3 -72
- data/spec/controllers/scimitar/resource_types_controller_spec.rb +2 -2
- data/spec/controllers/scimitar/schemas_controller_spec.rb +2 -10
- data/spec/models/scimitar/complex_types/email_spec.rb +2 -0
- data/spec/models/scimitar/lists/query_parser_spec.rb +9 -76
- data/spec/models/scimitar/resources/base_spec.rb +70 -208
- data/spec/models/scimitar/resources/base_validation_spec.rb +2 -27
- data/spec/models/scimitar/resources/mixin_spec.rb +43 -768
- data/spec/models/scimitar/schema/attribute_spec.rb +3 -22
- data/spec/models/scimitar/schema/base_spec.rb +1 -1
- data/spec/models/scimitar/schema/user_spec.rb +0 -10
- data/spec/requests/active_record_backed_resources_controller_spec.rb +64 -423
- data/spec/requests/application_controller_spec.rb +3 -16
- metadata +7 -11
- data/LICENSE.txt +0 -21
- data/README.md +0 -671
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 95a2166cc921a400959f9d8d4398f6bf8ecb772f8d7a0a0a73950892e85d808a
|
4
|
+
data.tar.gz: cdf5aab3812f10f69c96304e738a150f4208850267527b66d36eeb99548d7b1f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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().
|
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
|
171
|
-
|
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
|
-
|
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
|
-
# +
|
29
|
-
# via #handle_scim_error (if present).
|
28
|
+
# +_exception+:: Exception instance (currently unused).
|
30
29
|
#
|
31
|
-
def handle_resource_not_found(
|
32
|
-
handle_scim_error(NotFoundError.new(params[:id])
|
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
|
-
|
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}")
|
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)
|
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
|
-
|
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'] =
|
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 #{
|
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('
|
128
|
-
response.set_header('
|
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
|
-
|
12
|
-
|
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
|
-
#
|
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
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
634
|
+
arel_table.grouping(arel_column.not_eq_all(['', nil]))
|
650
635
|
else
|
651
|
-
raise Scimitar::FilterError
|
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
|
659
|
+
raise Scimitar::FilterError if scim_attribute.blank?
|
675
660
|
|
676
661
|
mapped_attribute = self.attribute_map()[scim_attribute]
|
677
|
-
raise Scimitar::FilterError
|
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
|
-
|
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
|
142
|
-
|
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
|
224
|
-
#
|
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:
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
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:
|
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'),
|