scimitar 2.8.0 → 2.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE.txt +1 -1
- data/README.md +23 -18
- data/app/controllers/scimitar/application_controller.rb +4 -5
- data/app/controllers/scimitar/resource_types_controller.rb +7 -1
- data/app/controllers/scimitar/schemas_controller.rb +361 -1
- data/app/models/scimitar/engine_configuration.rb +3 -1
- data/app/models/scimitar/lists/query_parser.rb +10 -10
- data/app/models/scimitar/resource_type.rb +4 -6
- data/app/models/scimitar/resources/base.rb +37 -6
- data/app/models/scimitar/resources/mixin.rb +15 -10
- data/app/models/scimitar/schema/base.rb +1 -1
- data/config/initializers/scimitar.rb +41 -0
- data/lib/scimitar/engine.rb +50 -12
- data/lib/scimitar/support/utilities.rb +8 -3
- data/lib/scimitar/version.rb +2 -2
- data/spec/apps/dummy/app/models/mock_user.rb +11 -3
- data/spec/apps/dummy/config/initializers/scimitar.rb +29 -1
- data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +1 -0
- data/spec/apps/dummy/db/schema.rb +1 -0
- data/spec/controllers/scimitar/resource_types_controller_spec.rb +8 -4
- data/spec/controllers/scimitar/schemas_controller_spec.rb +342 -54
- data/spec/models/scimitar/lists/query_parser_spec.rb +5 -0
- data/spec/models/scimitar/resources/base_spec.rb +11 -11
- data/spec/models/scimitar/resources/base_validation_spec.rb +1 -1
- data/spec/models/scimitar/resources/mixin_spec.rb +31 -12
- data/spec/requests/active_record_backed_resources_controller_spec.rb +86 -2
- data/spec/requests/engine_spec.rb +75 -0
- data/spec/spec_helper.rb +1 -1
- metadata +21 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f2e3306d8e05674efa4a53f63e833304623187a3191ecd6ef5685ac03a3ae8a7
|
4
|
+
data.tar.gz: 346c86fe3728208d6e4afb6a2d684a2b46de6326549d1b5d3b1e96622f220dd4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 723b88d08085f32e36caca03eceeee22c6653fb9b8fcbe94aa7c35aa6ae37f1de59fef29201ea42a00b4ec01540c3d77375c5d55ea07be4cee1447cdb3cc5e9f
|
7
|
+
data.tar.gz: 00e7ced581373571d0f086a0df9f18334b7b73ae6078ecd095edbfd2b5d7f5c6181515074bbbd49be3c8819531dbad9affa782a7f5087dec7d51a371c79646ea
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -321,7 +321,7 @@ end
|
|
321
321
|
|
322
322
|
#### Other source types
|
323
323
|
|
324
|
-
If you do _not_ use ActiveRecord to store data, or if you have very esoteric read-write requirements, you can subclass [`
|
324
|
+
If you do _not_ use ActiveRecord to store data, or if you have very esoteric read-write requirements, you can subclass [`Scimitar::ResourcesController`](https://www.rubydoc.info/gems/scimitar/Scimitar/ResourcesController) in a manner similar to this:
|
325
325
|
|
326
326
|
```ruby
|
327
327
|
class UsersController < Scimitar::ResourcesController
|
@@ -457,7 +457,9 @@ You can extend schema with custom data by defining an extension class and callin
|
|
457
457
|
* Must call `super` in `def initialize`, providing data as shown in the example below
|
458
458
|
* Must define class methods for `::id` and `::scim_attributes`
|
459
459
|
|
460
|
-
The `::id` class method defines a unique schema ID that is used to namespace payloads or paths in JSON responses describing extended resources, JSON payloads creating them or PATCH paths modifying them. The
|
460
|
+
The `::id` class method defines a unique schema ID that is used to namespace payloads or paths in JSON responses describing extended resources, JSON payloads creating them or PATCH paths modifying them. The RFCs require this to be a URN ([see RFC 2141](https://tools.ietf.org/html/rfc2141)). Your extension's ID URN must be globally unique. Depending on your expected use case, you should review the [IANA registration considerations that RFC 7643 describes](https://tools.ietf.org//html/rfc7643#section-10) and definitely review the [syntactic structure declaration therein](https://tools.ietf.org/html/rfc7643#section-10.2.1) (`urn:ietf:params:scim:{type}:{name}{:other}`).
|
461
|
+
|
462
|
+
For example, we might choose to use the [RFC-defined User extension schema](https://tools.ietf.org/html/rfc7643#section-4.3) to define a couple of extra fields our User model happens to support:
|
461
463
|
|
462
464
|
```ruby
|
463
465
|
class UserEnterpriseExtension < Scimitar::Schema::Base
|
@@ -582,6 +584,23 @@ And write to it like this:
|
|
582
584
|
}
|
583
585
|
```
|
584
586
|
|
587
|
+
### Helping with auto-discovery
|
588
|
+
|
589
|
+
If you have an API consumer entity querying your Scimitar-based SCIM API provider endpoint and want to enable a degree of auto-discovery for that entity, then depending on your implementation, there may be customisations you wish to make.
|
590
|
+
|
591
|
+
#### Default resources
|
592
|
+
|
593
|
+
By default, Scimitar advertises (via things like [the `/Schemas` endpoint](https://tools.ietf.org/html/rfc7644#section-4)) support for both a `User` and `Group` resource, but if you (say) only support a `User` concept, you override the default using code such as this in your `config/initializers/scimitar.rb` file:
|
594
|
+
|
595
|
+
```ruby
|
596
|
+
Rails.application.config.to_prepare do
|
597
|
+
Scimitar::Engine.set_default_resources([Scimitar::Resources::User])
|
598
|
+
# ...other Scimitar configuration / initialisation code...
|
599
|
+
end
|
600
|
+
```
|
601
|
+
|
602
|
+
|
603
|
+
|
585
604
|
## Security
|
586
605
|
|
587
606
|
One vital feature of SCIM is its authorisation and security model. The best resource I've found to describe this in any detail is [section 2 of the protocol RFC, 7644](https://tools.ietf.org/html/rfc7644#section-2).
|
@@ -600,8 +619,6 @@ Often, you'll find that bearer tokens are in use by SCIM API consumers, but the
|
|
600
619
|
|
601
620
|
### Specification versus implementation
|
602
621
|
|
603
|
-
* The `name` complex type of a User has `givenName` and `familyName` fields which [the RFC 7643 core schema](https://tools.ietf.org/html/rfc7643#section-8.7.1) describes as optional. Scimitar marks these as required, in the belief that most user synchronisation scenarios between clients and a Scimitar-based provider would require at least those names for basic user management on the provider side, in conjunction with the in-spec-required `userName` field. That's only if the whole `name` type is given at all - at the top level, this itself remains optional per spec, but if you're going to bother specifying names at all, Scimitar wants at least those two pieces of data.
|
604
|
-
|
605
622
|
* Several complex types for User contain the same set of `value`, `display`, `type` and `primary` fields, all used in synonymous ways.
|
606
623
|
|
607
624
|
- The `value` field - which is e.g. an e-mail address or phone number - is described as optional by [the RFC 7643 core schema](https://tools.ietf.org/html/rfc7643#section-8.7.1), also using "SHOULD" rather than "MUST" in field descriptions elsewhere. Scimitar marks this as required by default, since there's not much point being sent (say) an e-mail section which has entries that don't provide the e-mail address. Some services might send `null` values here regardless so, if you need to be able to accept such data, you can set [engine configuration option `optional_value_fields_required`](https://github.com/RIPAGlobal/scimitar/blob/main/config/initializers/scimitar.rb) to `false`.
|
@@ -620,6 +637,8 @@ Often, you'll find that bearer tokens are in use by SCIM API consumers, but the
|
|
620
637
|
|
621
638
|
* [RFC 7644 indicates](https://tools.ietf.org/html/rfc7644#page-35) that a resource might only return its core schema in the `schemas` attribute if it was created without any extension fields used. Only if e.g. a subsequent `PATCH` operation added data provided by extension schema, would that extension also appear in `schemas`. This behaviour is extremely difficult to implement and Scimitar does not try - it will always return a resource's core schema and any/all defined extension schemas in the `schemas` array at all times.
|
622
639
|
|
640
|
+
* As noted earlier, extension schema attribute names must be unique across your entire combined schema, regardless of schema IDs (URNs) used.
|
641
|
+
|
623
642
|
If you believe choices made in this section may be incorrect, please [create a GitHub issue](https://github.com/RIPAGlobal/scimitar/issues/new) describing the problem.
|
624
643
|
|
625
644
|
### Omissions
|
@@ -632,20 +651,6 @@ If you believe choices made in this section may be incorrect, please [create a G
|
|
632
651
|
|
633
652
|
It's very strange just specifying `emails co...`, since this is an Array which contains complex types. Is the filter there meant to try and match every attribute of the nested types in all array entries? I.e. if `type` happened to contain `example.com`, is that meant to match? It's strongly implied, because the next part of the filter specifically says `emails.value`. Again, we have to reach a little and assume that `emails.value` means "in _any_ of the objects in the `emails` Array, match all things where `value` contains `example.org`. It seems likely that this is a specification error and both of the specifiers should be `emails.value`.
|
634
653
|
|
635
|
-
Adding even more complexity - the specification shows filters _which include filters within them_. In the same way that PATCH operations use paths to identify attributes not just by name, but by filter matches within collections - e.g. `emails[type eq "work"]`, for all e-mail objects inside the `emails` array with a `type` attribute that has a value of `work`) - so also can a filter _contain a filter_, which isn't supported. So, this [example from the RFC](https://tools.ietf.org/html/rfc7644#page-23) is not supported by Scimitar:
|
636
|
-
|
637
|
-
- `filter=userType eq "Employee" and emails[type eq "work" and value co "@example.com"]`
|
638
|
-
|
639
|
-
Another filter shows a potential workaround:
|
640
|
-
|
641
|
-
- `filter=userType eq "Employee" and (emails.type eq "work")`
|
642
|
-
|
643
|
-
...which is just a match on `emails.type`, so if you have a queryable attribute mapping defined for `emails.type`, that would become queryable. Likewise, you could rewrite the more complex prior example thus:
|
644
|
-
|
645
|
-
- `filter=userType eq "Employee" and emails.type eq "work" and emails.value co "@example.com"`
|
646
|
-
|
647
|
-
...so adding a mapping for `emails.value` would then allow a database query to be constructed.
|
648
|
-
|
649
654
|
* Currently filtering for lists is always matched case-insensitive regardless of schema declarations that might indicate otherwise, for `eq`, `ne`, `co`, `sw` and `ew` operators; for greater/less-thank style filters, case is maintained with simple `>`, `<` etc. database operations in use. The standard Group and User schema have `caseExact` set to `false` for just about anything readily queryable, so this hopefully would only ever potentially be an issue for custom schema.
|
650
655
|
|
651
656
|
* As an exception to the above, attributes `id`, `externalId` and `meta.*` are matched case-sensitive. Filters that use `eq` on such attributes will end up a comparison using `=` rather than e.g. `ILIKE` (arising from https://github.com/RIPAGlobal/scimitar/issues/36).
|
@@ -9,10 +9,6 @@ module Scimitar
|
|
9
9
|
before_action :add_mandatory_response_headers
|
10
10
|
before_action :authenticate
|
11
11
|
|
12
|
-
if Scimitar.engine_configuration.application_controller_mixin
|
13
|
-
include Scimitar.engine_configuration.application_controller_mixin
|
14
|
-
end
|
15
|
-
|
16
12
|
# =========================================================================
|
17
13
|
# PROTECTED INSTANCE METHODS
|
18
14
|
# =========================================================================
|
@@ -47,7 +43,7 @@ module Scimitar
|
|
47
43
|
#
|
48
44
|
# *exception+:: If a Ruby exception was the reason this method is being
|
49
45
|
# called, pass it here. Any configured exception reporting
|
50
|
-
# mechanism will be
|
46
|
+
# mechanism will be invoked with the given parameter.
|
51
47
|
# Otherwise, the +error_response+ value is reported.
|
52
48
|
#
|
53
49
|
def handle_scim_error(error_response, exception = error_response)
|
@@ -153,5 +149,8 @@ module Scimitar
|
|
153
149
|
return result
|
154
150
|
end
|
155
151
|
|
152
|
+
if Scimitar.engine_configuration.application_controller_mixin
|
153
|
+
include Scimitar.engine_configuration.application_controller_mixin
|
154
|
+
end
|
156
155
|
end
|
157
156
|
end
|
@@ -5,7 +5,13 @@ module Scimitar
|
|
5
5
|
resource.resource_type(scim_resource_type_url(name: resource.resource_type_id))
|
6
6
|
end
|
7
7
|
|
8
|
-
render json:
|
8
|
+
render json: {
|
9
|
+
schemas: [
|
10
|
+
'urn:ietf:params:scim:api:messages:2.0:ListResponse'
|
11
|
+
],
|
12
|
+
totalResults: resource_types.size,
|
13
|
+
Resources: resource_types
|
14
|
+
}
|
9
15
|
end
|
10
16
|
|
11
17
|
def show
|
@@ -12,8 +12,368 @@ module Scimitar
|
|
12
12
|
hash
|
13
13
|
end
|
14
14
|
|
15
|
-
|
15
|
+
list = if params.key?(:name)
|
16
|
+
[ schemas_by_id[params[:name]] ]
|
17
|
+
else
|
18
|
+
schemas
|
19
|
+
end
|
20
|
+
|
21
|
+
# Now we either have a simple render method, or a complex one.
|
22
|
+
#
|
23
|
+
schemas_to_render = if Scimitar.engine_configuration.schema_list_from_attribute_mappings.empty?
|
24
|
+
list
|
25
|
+
else
|
26
|
+
self.redraw_schema_list_using_mappings(list)
|
27
|
+
end
|
28
|
+
|
29
|
+
render(json: {
|
30
|
+
schemas: [
|
31
|
+
'urn:ietf:params:scim:api:messages:2.0:ListResponse'
|
32
|
+
],
|
33
|
+
totalResults: schemas_to_render.size,
|
34
|
+
startIndex: 1,
|
35
|
+
itemsPerPage: schemas_to_render.size,
|
36
|
+
Resources: schemas_to_render
|
37
|
+
})
|
16
38
|
end
|
17
39
|
|
40
|
+
# =========================================================================
|
41
|
+
# PRIVATE INSTANCE METHODS
|
42
|
+
# =========================================================================
|
43
|
+
#
|
44
|
+
private
|
45
|
+
|
46
|
+
# Given a list of schema *instances*, find all Scimitar::Resources::Mixin
|
47
|
+
# inclusions to obtain classes with a ::scim_resource_type implementation
|
48
|
+
# that is invoked to get at the associated Scimitar resource class; each
|
49
|
+
# resource class then describes the schema it uses; the list is filtered
|
50
|
+
# to include only those found schemas. Then, for each, use the discovered
|
51
|
+
# class' attribute maps to walk the schema attribute tree alongside the
|
52
|
+
# map and render only mapped attributes. This is done via calling down to
|
53
|
+
# ::redraw_schema_using_mappings for each remaining schema in the list.
|
54
|
+
#
|
55
|
+
# An array of new schema data is returned.
|
56
|
+
#
|
57
|
+
# +list+:: Array of schema instances to examine.
|
58
|
+
#
|
59
|
+
def redraw_schema_list_using_mappings(list)
|
60
|
+
|
61
|
+
# Iterate over the configured model classes to build a mapping from
|
62
|
+
# Scimitar schema class to an array of one or more model classes that
|
63
|
+
# seem to use it. This is to detect the error condition wherein some
|
64
|
+
# schema gets used more than once, leading to multiple possible
|
65
|
+
# attribute map choices.
|
66
|
+
#
|
67
|
+
classes_using_scimitar_mixin = Scimitar.engine_configuration.schema_list_from_attribute_mappings
|
68
|
+
schema_to_resource_map = {}
|
69
|
+
|
70
|
+
classes_using_scimitar_mixin.each do | model_class |
|
71
|
+
resource_class = model_class.scim_resource_type()
|
72
|
+
schemas = resource_class.extended_schemas + [resource_class.schema]
|
73
|
+
|
74
|
+
schemas.each do | schema_class |
|
75
|
+
schema_to_resource_map[schema_class] ||= []
|
76
|
+
schema_to_resource_map[schema_class] << model_class
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Take the schema list and map to rewritten versions based on finding
|
81
|
+
# out which of the above resource classes use a given schema and then
|
82
|
+
# walking this schema's attribute tree while comparing against the
|
83
|
+
# resource class's attribute map. Unmapped attributes are removed. The
|
84
|
+
# reality of resource class attribute mutability might give different
|
85
|
+
# answers for the corresponding schema attribute's mutability; for any
|
86
|
+
# custom schema we'd expect a match, but for core schema where local
|
87
|
+
# resources don't quite work to spec, at least the /Schemas endpoint
|
88
|
+
# can try to reflect reality and aid auto-discovery.
|
89
|
+
#
|
90
|
+
redrawn_list = list.map do | schema_instance |
|
91
|
+
resource_classes_using_schema = schema_to_resource_map[schema_instance.class]
|
92
|
+
|
93
|
+
if resource_classes_using_schema.nil?
|
94
|
+
next # NOTE EARLY LOOP RESTART (schema not used by a resource)
|
95
|
+
elsif resource_classes_using_schema.size > 1
|
96
|
+
raise "Cannot infer attribute map for engine configuration 'schema_list_from_attribute_mappings: true' because multiple resource classes use schema '#{schema_instance.class.name}': #{resource_classes_using_schema.map(&:name).join(', ')}"
|
97
|
+
end
|
98
|
+
|
99
|
+
found_class = classes_using_scimitar_mixin.find do | class_using_scimitar_mixin |
|
100
|
+
resource_class = class_using_scimitar_mixin.scim_resource_type()
|
101
|
+
|
102
|
+
resource_class.schema == schema_instance.class ||
|
103
|
+
resource_class.extended_schemas.include?(schema_instance.class)
|
104
|
+
end
|
105
|
+
|
106
|
+
rebuilt_schema_instance = if found_class
|
107
|
+
redraw_schema_using_mappings(
|
108
|
+
original_schema_instance: schema_instance,
|
109
|
+
instance_including_mixin: found_class.new
|
110
|
+
)
|
111
|
+
else
|
112
|
+
nil
|
113
|
+
end
|
114
|
+
|
115
|
+
rebuilt_schema_instance
|
116
|
+
end
|
117
|
+
|
118
|
+
redrawn_list.compact!
|
119
|
+
redrawn_list
|
120
|
+
end
|
121
|
+
|
122
|
+
# "Redraw" a schema, by walking its attribute tree alongside a related
|
123
|
+
# resource class's attribute map. Only mapped attributes are included.
|
124
|
+
# The mapped model is checked for a read accessor and write ability is
|
125
|
+
# determined via Scimitar::Resources::Mixin#scim_mutable_attributes. This
|
126
|
+
# gives the actual read/write ability of the mapped attribute; if the
|
127
|
+
# schema's declared mutability differs, the *most restrictive* is chosen.
|
128
|
+
# For example, if the schema says read-write but the mapped model only
|
129
|
+
# has read ability, then "readOnly" is used. Conversely, if the schema
|
130
|
+
# says read-only but the mapped model has read-write, the schema's
|
131
|
+
# "readOnly" is chosen instead as the source of truth.
|
132
|
+
#
|
133
|
+
# See the implementation's comments for a table describing exactly how
|
134
|
+
# all mutability conflict cases are resolved.
|
135
|
+
#
|
136
|
+
# The returned schema instance may be a full or partial duplicate of the
|
137
|
+
# one given on input - some or all attributes and/or sub-attributes may
|
138
|
+
# have been duplicated due to e.g. mutability differences. Do not assume
|
139
|
+
# or rely upon this as a caller.
|
140
|
+
#
|
141
|
+
# Mandatory named parameters for external callers are:
|
142
|
+
#
|
143
|
+
# +original_schema_instance+:: The Scimitar::Schema::Base subclass
|
144
|
+
# schema *instance* that is to be examined
|
145
|
+
# and possibly "redrawn".
|
146
|
+
#
|
147
|
+
# +instance_including_mixin+:: Instance of the model class including
|
148
|
+
# Scimitar::Resources::Mixin, providing
|
149
|
+
# the attribute map to be examined.
|
150
|
+
#
|
151
|
+
# Named parameters used internally for recursive calls are:
|
152
|
+
#
|
153
|
+
# +scim_attributes_map+:: The fragment of the attribute map found
|
154
|
+
# from +instance_including_mixin+'s class
|
155
|
+
# initially, which is relevant to the
|
156
|
+
# current recursion level. E.g. it might
|
157
|
+
# be the sub-attributes map of "name",
|
158
|
+
# for things like a "familyName" mapping.
|
159
|
+
#
|
160
|
+
# +schema_attributes+:: An array of schema attributes for the
|
161
|
+
# current recursion level, corresponding
|
162
|
+
# to +scim_attributes_map+.
|
163
|
+
#
|
164
|
+
# +rebuilt_attribute_array+:: Redrawn schema attributes are collected
|
165
|
+
# into this array, which is altered in
|
166
|
+
# place. It is usually a 'subAttributes'
|
167
|
+
# property of a schema attribute that's
|
168
|
+
# provoked recursion in order to examine
|
169
|
+
# and rebuild its sub-attributes directly.
|
170
|
+
#
|
171
|
+
def redraw_schema_using_mappings(
|
172
|
+
original_schema_instance:,
|
173
|
+
instance_including_mixin:,
|
174
|
+
|
175
|
+
scim_attributes_map: nil,
|
176
|
+
schema_attributes: nil,
|
177
|
+
rebuilt_attribute_array: nil
|
178
|
+
)
|
179
|
+
schema_attributes ||= original_schema_instance.scim_attributes
|
180
|
+
scim_attributes_map ||= instance_including_mixin
|
181
|
+
.class
|
182
|
+
.scim_attributes_map()
|
183
|
+
.with_indifferent_case_insensitive_access()
|
184
|
+
|
185
|
+
rebuilt_schema_instance = nil
|
186
|
+
|
187
|
+
if rebuilt_attribute_array.nil?
|
188
|
+
rebuilt_schema_instance = self.duplicate_attribute(original_schema_instance)
|
189
|
+
rebuilt_schema_instance.scim_attributes = []
|
190
|
+
rebuilt_attribute_array = rebuilt_schema_instance.scim_attributes
|
191
|
+
end
|
192
|
+
|
193
|
+
schema_attributes.each do | schema_attribute |
|
194
|
+
if schema_attribute.multiValued && schema_attribute.subAttributes&.any?
|
195
|
+
mapped_multivalue_attribute = scim_attributes_map[schema_attribute.name]
|
196
|
+
|
197
|
+
# We expect either an array in the attribute map to correspond with
|
198
|
+
# a multivalued schema attribute, or nothing. If we get some other
|
199
|
+
# non-Array, not-nil thing, it's just ignored.
|
200
|
+
#
|
201
|
+
if mapped_multivalue_attribute.is_a?(Array)
|
202
|
+
|
203
|
+
# A single-entry array with "list using" semantics, for a
|
204
|
+
# collection of an arbitrary number of same-class items - e.g.
|
205
|
+
# Groups to which a User belongs.
|
206
|
+
#
|
207
|
+
# If this is an up-to-date mapping, there's a "class" entry that
|
208
|
+
# tells us what the collection is compromised of. If not, then we
|
209
|
+
# check for ActiveRecord collections as a fallback and if that is
|
210
|
+
# the case here, can use reflection to try and find the class. If
|
211
|
+
# all else fails, we drop to generic schema for the collection.
|
212
|
+
#
|
213
|
+
if mapped_multivalue_attribute.first&.dig(:list)
|
214
|
+
associated_resource_class = mapped_multivalue_attribute.first[:class]
|
215
|
+
|
216
|
+
if (
|
217
|
+
associated_resource_class.nil? &&
|
218
|
+
instance_including_mixin.is_a?(ActiveRecord::Base)
|
219
|
+
)
|
220
|
+
associated_resource_class = instance_including_mixin
|
221
|
+
.class
|
222
|
+
.reflect_on_association(mapped_multivalue_attribute.first[:list])
|
223
|
+
&.klass
|
224
|
+
end
|
225
|
+
|
226
|
+
if associated_resource_class.nil? || ! associated_resource_class.include?(Scimitar::Resources::Mixin)
|
227
|
+
rebuilt_attribute_array << schema_attribute
|
228
|
+
else
|
229
|
+
rebuilt_schema_attribute = self.duplicate_attribute(schema_attribute)
|
230
|
+
rebuilt_schema_attribute.subAttributes = []
|
231
|
+
rebuilt_attribute_array << rebuilt_schema_attribute
|
232
|
+
|
233
|
+
redraw_schema_using_mappings(
|
234
|
+
original_schema_instance: original_schema_instance,
|
235
|
+
instance_including_mixin: associated_resource_class.new,
|
236
|
+
scim_attributes_map: mapped_multivalue_attribute.first[:using],
|
237
|
+
schema_attributes: schema_attribute.subAttributes,
|
238
|
+
rebuilt_attribute_array: rebuilt_schema_attribute.subAttributes
|
239
|
+
)
|
240
|
+
end
|
241
|
+
|
242
|
+
# A one-or-more entry array with "match with" semantics, to match
|
243
|
+
# discrete mapped items with a particular value in a particular
|
244
|
+
# field - e.g. an e-mail of type "work" mapping the SCIM "value"
|
245
|
+
# to a local attribute of "work_email_address".
|
246
|
+
#
|
247
|
+
# Mutability or supported attributes here might vary per matched
|
248
|
+
# type. There's no way for SCIM schema to represent that so we
|
249
|
+
# just merge all the "using" mappings together, in order of array
|
250
|
+
# appearance, and have that combined attribute map treated as the
|
251
|
+
# data the schema response will use.
|
252
|
+
#
|
253
|
+
elsif mapped_multivalue_attribute.first&.dig(:match)
|
254
|
+
union_of_mappings = {}
|
255
|
+
|
256
|
+
mapped_multivalue_attribute.each do | mapped_multivalue_attribute_description |
|
257
|
+
union_of_mappings.merge!(mapped_multivalue_attribute_description[:using])
|
258
|
+
end
|
259
|
+
|
260
|
+
rebuilt_schema_attribute = self.duplicate_attribute(schema_attribute)
|
261
|
+
rebuilt_schema_attribute.subAttributes = []
|
262
|
+
rebuilt_attribute_array << rebuilt_schema_attribute
|
263
|
+
|
264
|
+
redraw_schema_using_mappings(
|
265
|
+
original_schema_instance: original_schema_instance,
|
266
|
+
instance_including_mixin: instance_including_mixin,
|
267
|
+
scim_attributes_map: union_of_mappings,
|
268
|
+
schema_attributes: schema_attribute.subAttributes,
|
269
|
+
rebuilt_attribute_array: rebuilt_schema_attribute.subAttributes
|
270
|
+
)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
elsif schema_attribute.subAttributes&.any?
|
275
|
+
mapped_subattributes = scim_attributes_map[schema_attribute.name]
|
276
|
+
|
277
|
+
if mapped_subattributes.is_a?(Hash)
|
278
|
+
rebuilt_schema_attribute = self.duplicate_attribute(schema_attribute)
|
279
|
+
rebuilt_schema_attribute.subAttributes = []
|
280
|
+
rebuilt_attribute_array << rebuilt_schema_attribute
|
281
|
+
|
282
|
+
redraw_schema_using_mappings(
|
283
|
+
original_schema_instance: original_schema_instance,
|
284
|
+
instance_including_mixin: instance_including_mixin,
|
285
|
+
scim_attributes_map: mapped_subattributes,
|
286
|
+
schema_attributes: schema_attribute.subAttributes,
|
287
|
+
rebuilt_attribute_array: rebuilt_schema_attribute.subAttributes
|
288
|
+
)
|
289
|
+
end
|
290
|
+
|
291
|
+
else
|
292
|
+
mapped_attribute = scim_attributes_map[schema_attribute.name]
|
293
|
+
|
294
|
+
unless mapped_attribute.nil?
|
295
|
+
rebuilt_schema_attribute = self.duplicate_attribute(schema_attribute)
|
296
|
+
has_mapped_reader = true
|
297
|
+
has_mapped_writer = false
|
298
|
+
|
299
|
+
if mapped_attribute.is_a?(String) || mapped_attribute.is_a?(Symbol)
|
300
|
+
has_mapped_reader = instance_including_mixin.respond_to?(mapped_attribute)
|
301
|
+
has_mapped_writer = instance_including_mixin.scim_mutable_attributes().include?(mapped_attribute.to_sym)
|
302
|
+
end
|
303
|
+
|
304
|
+
# The schema is taken as the primary source of truth, leading to
|
305
|
+
# a matrix of "do we override it or not?" based on who is the
|
306
|
+
# more limited. When both have the same mutability there is no
|
307
|
+
# more work to do, so we just need to consider differences:
|
308
|
+
#
|
309
|
+
# Actual class support Schema says Result
|
310
|
+
# =============================================================
|
311
|
+
# readWrite readOnly readOnly (schema wins)
|
312
|
+
# readWrite writeOnly writeOnly (schema wins)
|
313
|
+
# readOnly readWrite readOnly (class wins)
|
314
|
+
# writeOnly readWrite writeOnly (class wins)
|
315
|
+
#
|
316
|
+
# Those cases are easy. But there are gnarly cases too, where we
|
317
|
+
# have no good answer and the class's mapped implementation is in
|
318
|
+
# essence broken compared to the schema. Since it is not useful
|
319
|
+
# to insist on the schema's not-reality version, the class wins.
|
320
|
+
#
|
321
|
+
# Actual class support Schema says Result
|
322
|
+
# ====================== =======================================
|
323
|
+
# readOnly writeOnly readOnly (class "wins")
|
324
|
+
# writeOnly readOnly writeOnly (class "wins")
|
325
|
+
|
326
|
+
schema_attribute_mutability = schema_attribute.mutability.downcase
|
327
|
+
|
328
|
+
if has_mapped_reader && has_mapped_writer
|
329
|
+
#
|
330
|
+
# Read-write Nothing to do. Schema always "wins" by matching or
|
331
|
+
# being more restrictive than the class's actual abilities.
|
332
|
+
|
333
|
+
elsif has_mapped_reader && ! has_mapped_writer
|
334
|
+
#
|
335
|
+
# Read-only. Class is more restrictive if schema is 'readWrite'
|
336
|
+
# or if there's the broken clash of schema 'writeOnly'.
|
337
|
+
#
|
338
|
+
if schema_attribute_mutability == 'readwrite' || schema_attribute_mutability == 'writeonly'
|
339
|
+
rebuilt_schema_attribute.mutability = 'readOnly'
|
340
|
+
end
|
341
|
+
|
342
|
+
elsif has_mapped_writer && ! has_mapped_reader
|
343
|
+
#
|
344
|
+
# Opposite to the above case.
|
345
|
+
#
|
346
|
+
if schema_attribute_mutability == 'readwrite' || schema_attribute_mutability == 'readonly'
|
347
|
+
rebuilt_schema_attribute.mutability = 'writeOnly'
|
348
|
+
end
|
349
|
+
|
350
|
+
# ...else we cannot fathom how this class works - it appears to
|
351
|
+
# have no read or write accessor for the mapped attribute. Keep
|
352
|
+
# the schema's declaration as-is.
|
353
|
+
#
|
354
|
+
end
|
355
|
+
|
356
|
+
rebuilt_attribute_array << rebuilt_schema_attribute
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
return rebuilt_schema_instance # (meaningless except for topmost call)
|
362
|
+
end
|
363
|
+
|
364
|
+
# Small helper that duplicates Scimitar::Schema::Attribute instances, but
|
365
|
+
# then removes their 'errors' collection which otherwise gets initialised
|
366
|
+
# to an empty value and is rendered as if part of the schema (which isn't
|
367
|
+
# a valid entry in a SCIM schema representation).
|
368
|
+
#
|
369
|
+
# +schema_attribute+:: Scimitar::Schema::Attribute to be duplicated.
|
370
|
+
# A renderable duplicate is returned.
|
371
|
+
#
|
372
|
+
def duplicate_attribute(schema_attribute)
|
373
|
+
duplicated_schema_attribute = schema_attribute.dup()
|
374
|
+
duplicated_schema_attribute.remove_instance_variable('@errors')
|
375
|
+
duplicated_schema_attribute
|
376
|
+
end
|
377
|
+
|
18
378
|
end
|
19
379
|
end
|
@@ -14,6 +14,7 @@ module Scimitar
|
|
14
14
|
:application_controller_mixin,
|
15
15
|
:exception_reporter,
|
16
16
|
:optional_value_fields_required,
|
17
|
+
:schema_list_from_attribute_mappings,
|
17
18
|
)
|
18
19
|
|
19
20
|
def initialize(attributes = {})
|
@@ -22,7 +23,8 @@ module Scimitar
|
|
22
23
|
# Set defaults that may be overridden by the initializer.
|
23
24
|
#
|
24
25
|
defaults = {
|
25
|
-
optional_value_fields_required:
|
26
|
+
optional_value_fields_required: true,
|
27
|
+
schema_list_from_attribute_mappings: []
|
26
28
|
}
|
27
29
|
|
28
30
|
super(defaults.merge(attributes))
|
@@ -342,14 +342,14 @@ module Scimitar
|
|
342
342
|
skip_next_component = false
|
343
343
|
|
344
344
|
components.each.with_index do | component, index |
|
345
|
-
if skip_next_component
|
345
|
+
if skip_next_component
|
346
346
|
skip_next_component = false
|
347
347
|
next
|
348
348
|
end
|
349
349
|
|
350
350
|
downcased = component.downcase.strip
|
351
351
|
|
352
|
-
if
|
352
|
+
if expecting_attribute
|
353
353
|
if downcased.match?(/[^\\]\[/) # Not backslash then literal '['
|
354
354
|
attribute_prefix = component.match(/(.*?[^\\])\[/ )[1] # Everything before no-backslash-then-literal (unescaped) '['
|
355
355
|
first_attribute_inside = component.match( /[^\\]\[(.*)/)[1] # Everything after no-backslash-then-literal (unescaped) '['
|
@@ -362,7 +362,7 @@ module Scimitar
|
|
362
362
|
expecting_attribute = false
|
363
363
|
expecting_operator = true
|
364
364
|
|
365
|
-
elsif
|
365
|
+
elsif expecting_operator
|
366
366
|
rewritten << component
|
367
367
|
if BINARY_OPERATORS.include?(downcased)
|
368
368
|
expecting_operator = false
|
@@ -374,7 +374,7 @@ module Scimitar
|
|
374
374
|
raise 'Expected operator'
|
375
375
|
end
|
376
376
|
|
377
|
-
elsif
|
377
|
+
elsif expecting_value
|
378
378
|
matches = downcased.match(/([^\\])\](.*)/) # Contains no-backslash-then-literal (unescaped) ']'; also capture anything after
|
379
379
|
unless matches.nil? # Contains no-backslash-then-literal (unescaped) ']'
|
380
380
|
character_before_closing_bracket = matches[1]
|
@@ -395,7 +395,7 @@ module Scimitar
|
|
395
395
|
# So - NOTE RECURSION AND EARLY EXIT POSSIBILITY HEREIN.
|
396
396
|
#
|
397
397
|
if (
|
398
|
-
!
|
398
|
+
!attribute_prefix.nil? &&
|
399
399
|
OPERATORS.key?(components[index + 1]&.downcase) &&
|
400
400
|
characters_after_closing_bracket.match?(/^\.#{ATTRNAME}$/)
|
401
401
|
)
|
@@ -437,7 +437,7 @@ module Scimitar
|
|
437
437
|
if downcased.start_with?('"')
|
438
438
|
expecting_closing_quote = true
|
439
439
|
downcased = downcased[1..-1] # Strip off opening '"' to avoid false-positive on 'contains closing quote' check below
|
440
|
-
elsif expecting_closing_quote
|
440
|
+
elsif !expecting_closing_quote # If not expecting a closing quote, then the component must be the entire no-spaces value
|
441
441
|
expecting_value = false
|
442
442
|
expecting_logic_word = true
|
443
443
|
end
|
@@ -450,7 +450,7 @@ module Scimitar
|
|
450
450
|
end
|
451
451
|
end
|
452
452
|
|
453
|
-
elsif
|
453
|
+
elsif expecting_logic_word
|
454
454
|
if downcased == 'and' || downcased == 'or'
|
455
455
|
rewritten << component
|
456
456
|
next_downcased_component = components[index + 1].downcase.strip
|
@@ -586,11 +586,11 @@ module Scimitar
|
|
586
586
|
|
587
587
|
# Recursively process an expression tree. Calls itself with nested tree
|
588
588
|
# fragments. Each inner expression fragment calculates on the given
|
589
|
-
# base scope, with
|
589
|
+
# base scope, with aggregation at each level into a wider query using
|
590
590
|
# AND or OR depending on the expression tree contents.
|
591
591
|
#
|
592
592
|
# +base_scope+:: Base scope (ActiveRecord::Relation, e.g. User.all
|
593
|
-
# -
|
593
|
+
# - never changes during recursion).
|
594
594
|
#
|
595
595
|
# +expression_tree+:: Top-level expression tree or fragments inside if
|
596
596
|
# self-calling recursively.
|
@@ -748,7 +748,7 @@ module Scimitar
|
|
748
748
|
|
749
749
|
# Returns the mapped-to-your-domain column name(s) that a filter string
|
750
750
|
# is operating upon, in an Array. If empty, the attribute is to be
|
751
|
-
# ignored. Raises an exception if
|
751
|
+
# ignored. Raises an exception if entirely unmapped (thus unsupported).
|
752
752
|
#
|
753
753
|
# Note plural - the return value is always an array any of which should
|
754
754
|
# be used (implicit 'OR').
|
@@ -17,12 +17,10 @@ module Scimitar
|
|
17
17
|
|
18
18
|
def as_json(options = {})
|
19
19
|
without_extensions = super(except: 'schemaExtensions')
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
without_extensions
|
25
|
-
end
|
20
|
+
return without_extensions unless schemaExtensions.present? # NOTE EARLY EXIT
|
21
|
+
|
22
|
+
extensions = schemaExtensions.map{|extension| {"schema" => extension, "required" => false}}
|
23
|
+
without_extensions.merge('schemaExtensions' => extensions)
|
26
24
|
end
|
27
25
|
|
28
26
|
end
|