scim2-server 0.1.0__py3-none-any.whl
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.
- scim2_server/__init__.py +0 -0
- scim2_server/backend.py +398 -0
- scim2_server/cli.py +98 -0
- scim2_server/filter.py +128 -0
- scim2_server/operators.py +478 -0
- scim2_server/provider.py +558 -0
- scim2_server/resources/default-resource-types.json +31 -0
- scim2_server/resources/default-schemas.json +944 -0
- scim2_server/utils.py +220 -0
- scim2_server-0.1.0.dist-info/LICENSE +201 -0
- scim2_server-0.1.0.dist-info/METADATA +73 -0
- scim2_server-0.1.0.dist-info/RECORD +14 -0
- scim2_server-0.1.0.dist-info/WHEEL +4 -0
- scim2_server-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Dict
|
|
3
|
+
from typing import List
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from typing import Tuple
|
|
6
|
+
from typing import Type
|
|
7
|
+
|
|
8
|
+
from scim2_filter_parser.lexer import SCIMLexer
|
|
9
|
+
from scim2_filter_parser.parser import SCIMParser
|
|
10
|
+
from scim2_models import BaseModel
|
|
11
|
+
from scim2_models import CaseExact
|
|
12
|
+
from scim2_models import Error
|
|
13
|
+
from scim2_models import Mutability
|
|
14
|
+
from scim2_models import PatchOperation
|
|
15
|
+
from scim2_models import Required
|
|
16
|
+
from scim2_models import Resource
|
|
17
|
+
from scim2_models import Returned
|
|
18
|
+
|
|
19
|
+
from scim2_server.filter import evaluate_filter
|
|
20
|
+
from scim2_server.utils import SCIMException
|
|
21
|
+
from scim2_server.utils import get_by_alias
|
|
22
|
+
from scim2_server.utils import get_or_create
|
|
23
|
+
from scim2_server.utils import handle_extension
|
|
24
|
+
from scim2_server.utils import is_multi_valued
|
|
25
|
+
from scim2_server.utils import parse_new_value
|
|
26
|
+
|
|
27
|
+
ATTRIBUTE_PATH_REGEX = re.compile(
|
|
28
|
+
r"^(?P<attribute>\w+)(\[(?P<condition>.*)\])?(\.(?P<sub_attribute>\w+))?$"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def patch_resource(resource: Resource, operation: PatchOperation):
|
|
33
|
+
"""Runs a patch operation against a resource."""
|
|
34
|
+
match operation.op:
|
|
35
|
+
case PatchOperation.Op.add:
|
|
36
|
+
operator = AddOperator(operation.path, operation.value)
|
|
37
|
+
operator(resource)
|
|
38
|
+
case PatchOperation.Op.remove:
|
|
39
|
+
operator = RemoveOperator(operation.path, None)
|
|
40
|
+
operator(resource)
|
|
41
|
+
case _: # PatchOperation.Op.replace
|
|
42
|
+
operator = ReplaceOperator(operation.path, operation.value)
|
|
43
|
+
operator(resource)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_attribute_path(attribute_path: Optional[str]) -> Optional[Dict[str, any]]:
|
|
47
|
+
"""Parses an attribute path and returns a dictionary of attributes.
|
|
48
|
+
|
|
49
|
+
The attributes are the named captures in the regex
|
|
50
|
+
ATTRIBUTE_PATH_REGEX.
|
|
51
|
+
"""
|
|
52
|
+
if not attribute_path:
|
|
53
|
+
return {}
|
|
54
|
+
|
|
55
|
+
if not hasattr(parse_attribute_path, "cache"):
|
|
56
|
+
parse_attribute_path.cache = {}
|
|
57
|
+
if attribute_path in parse_attribute_path.cache:
|
|
58
|
+
return parse_attribute_path.cache[attribute_path]
|
|
59
|
+
|
|
60
|
+
match = ATTRIBUTE_PATH_REGEX.match(attribute_path)
|
|
61
|
+
if not match:
|
|
62
|
+
raise SCIMException(Error.make_invalid_path_error())
|
|
63
|
+
parse_attribute_path.cache[attribute_path] = match.groupdict()
|
|
64
|
+
return match.groupdict()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Operator:
|
|
68
|
+
"""An operator operates on a resource and is constructed using a path and a
|
|
69
|
+
value."""
|
|
70
|
+
|
|
71
|
+
OPERATE_ON_ROOT = True # Whether the operator may operate on the root of a resource (e.g. remove may not)
|
|
72
|
+
REQUIRES_VALUE = True # Whether the operator modifies the resource or not
|
|
73
|
+
RETURNS_VALUE = False # Whether the operator returns a result or not
|
|
74
|
+
|
|
75
|
+
def __init__(self, path: Optional[str], value: Optional[any]):
|
|
76
|
+
self.path = path
|
|
77
|
+
self.value = value
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def init_return(
|
|
81
|
+
cls, model: BaseModel, attribute: str, sub_attribute: Optional[str], value: any
|
|
82
|
+
):
|
|
83
|
+
"""Initializes the return value if the operator returns something."""
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
def do_return(self):
|
|
87
|
+
"""Returns the return value for the operator."""
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def operation(
|
|
92
|
+
cls, model: BaseModel, attribute: str, value: any, index: Optional[int] = None
|
|
93
|
+
):
|
|
94
|
+
"""Performs the actual operation of the operator."""
|
|
95
|
+
raise NotImplementedError
|
|
96
|
+
|
|
97
|
+
def parse_path(self, model: BaseModel):
|
|
98
|
+
"""Parses a path and optionally handles model extensions.
|
|
99
|
+
|
|
100
|
+
:return: A tuple of the model to operate on and the parsed path.
|
|
101
|
+
"""
|
|
102
|
+
path = self.path
|
|
103
|
+
if isinstance(model, Resource):
|
|
104
|
+
model, path = handle_extension(model, self.path)
|
|
105
|
+
return model, parse_attribute_path(path)
|
|
106
|
+
|
|
107
|
+
def __call__(self, model: BaseModel):
|
|
108
|
+
"""Executes the operator against a model."""
|
|
109
|
+
if not self.path:
|
|
110
|
+
self.call_on_root(model)
|
|
111
|
+
return self.do_return()
|
|
112
|
+
else:
|
|
113
|
+
model, path = self.parse_path(model)
|
|
114
|
+
|
|
115
|
+
match path:
|
|
116
|
+
case {
|
|
117
|
+
"attribute": attribute,
|
|
118
|
+
"condition": None,
|
|
119
|
+
"sub_attribute": None,
|
|
120
|
+
}:
|
|
121
|
+
self.match_attribute(attribute, model)
|
|
122
|
+
return self.do_return()
|
|
123
|
+
case {
|
|
124
|
+
"attribute": attribute,
|
|
125
|
+
"condition": None,
|
|
126
|
+
"sub_attribute": sub_path,
|
|
127
|
+
}:
|
|
128
|
+
self.match_complex_attribute(attribute, model, sub_path)
|
|
129
|
+
return self.do_return()
|
|
130
|
+
case {
|
|
131
|
+
"attribute": attribute,
|
|
132
|
+
"condition": condition,
|
|
133
|
+
"sub_attribute": None,
|
|
134
|
+
}:
|
|
135
|
+
self.match_multi_valued_attribute(attribute, condition, model)
|
|
136
|
+
return self.do_return()
|
|
137
|
+
case {
|
|
138
|
+
"attribute": attribute,
|
|
139
|
+
"condition": condition,
|
|
140
|
+
"sub_attribute": sub_attribute,
|
|
141
|
+
}:
|
|
142
|
+
self.match_multi_valued_attribute_sub(
|
|
143
|
+
attribute, condition, model, sub_attribute
|
|
144
|
+
)
|
|
145
|
+
return self.do_return()
|
|
146
|
+
case _:
|
|
147
|
+
raise SCIMException(Error.make_invalid_path_error())
|
|
148
|
+
|
|
149
|
+
def match_multi_valued_attribute_sub(
|
|
150
|
+
self, attribute: str, condition: str, model: BaseModel, sub_attribute: str
|
|
151
|
+
):
|
|
152
|
+
attribute_name = get_by_alias(model, attribute)
|
|
153
|
+
multi_valued_attribute = get_or_create(model, attribute_name, True)
|
|
154
|
+
if not isinstance(multi_valued_attribute, list):
|
|
155
|
+
raise SCIMException(Error.make_invalid_path_error())
|
|
156
|
+
token_stream = SCIMLexer().tokenize(condition)
|
|
157
|
+
condition = SCIMParser().parse(token_stream)
|
|
158
|
+
self.init_return(model, attribute_name, sub_attribute, self.value)
|
|
159
|
+
for value in multi_valued_attribute:
|
|
160
|
+
if evaluate_filter(value, condition):
|
|
161
|
+
self.operation(value, sub_attribute, self.value)
|
|
162
|
+
|
|
163
|
+
def match_multi_valued_attribute(
|
|
164
|
+
self, attribute: str, condition: str, model: BaseModel
|
|
165
|
+
):
|
|
166
|
+
if self.REQUIRES_VALUE and not isinstance(self.value, dict):
|
|
167
|
+
raise SCIMException(Error.make_invalid_value_error())
|
|
168
|
+
attribute_name = get_by_alias(model, attribute)
|
|
169
|
+
multi_valued_attribute = get_or_create(
|
|
170
|
+
model, attribute_name, self.REQUIRES_VALUE
|
|
171
|
+
)
|
|
172
|
+
if not isinstance(multi_valued_attribute, list):
|
|
173
|
+
raise SCIMException(Error.make_invalid_path_error())
|
|
174
|
+
token_stream = SCIMLexer().tokenize(condition)
|
|
175
|
+
condition = SCIMParser().parse(token_stream)
|
|
176
|
+
if self.REQUIRES_VALUE:
|
|
177
|
+
for value in multi_valued_attribute:
|
|
178
|
+
if evaluate_filter(value, condition):
|
|
179
|
+
for k, v in self.value.items():
|
|
180
|
+
self.operation(value, k, v)
|
|
181
|
+
elif self.RETURNS_VALUE:
|
|
182
|
+
self.init_return(model, attribute_name, None, self.value)
|
|
183
|
+
for index, value in enumerate(multi_valued_attribute):
|
|
184
|
+
if evaluate_filter(value, condition):
|
|
185
|
+
self.operation(model, attribute_name, self.value, index)
|
|
186
|
+
else:
|
|
187
|
+
new_value = [
|
|
188
|
+
value
|
|
189
|
+
for value in multi_valued_attribute
|
|
190
|
+
if not evaluate_filter(value, condition)
|
|
191
|
+
]
|
|
192
|
+
setattr(model, attribute_name, new_value)
|
|
193
|
+
|
|
194
|
+
def match_complex_attribute(self, attribute: str, model: BaseModel, sub_path: str):
|
|
195
|
+
complex_attribute = get_or_create(
|
|
196
|
+
model, get_by_alias(model, attribute), self.REQUIRES_VALUE
|
|
197
|
+
)
|
|
198
|
+
if isinstance(complex_attribute, list) and complex_attribute:
|
|
199
|
+
for value in complex_attribute:
|
|
200
|
+
self.match_attribute(sub_path, value)
|
|
201
|
+
else:
|
|
202
|
+
if not isinstance(complex_attribute, BaseModel):
|
|
203
|
+
raise SCIMException(Error.make_invalid_path_error())
|
|
204
|
+
self.match_attribute(sub_path, complex_attribute)
|
|
205
|
+
|
|
206
|
+
def match_attribute(self, attribute: str, model: BaseModel):
|
|
207
|
+
self.init_return(model, attribute, None, self.value)
|
|
208
|
+
self.operation(model, attribute, self.value)
|
|
209
|
+
|
|
210
|
+
def call_on_root(self, model: Resource):
|
|
211
|
+
if not self.OPERATE_ON_ROOT:
|
|
212
|
+
raise SCIMException(Error.make_no_target_error())
|
|
213
|
+
if not isinstance(self.value, dict):
|
|
214
|
+
raise SCIMException(Error.make_invalid_value_error())
|
|
215
|
+
for k, v in self.value.items():
|
|
216
|
+
ext, scim_name = handle_extension(model, k)
|
|
217
|
+
if ext == model:
|
|
218
|
+
self.operation(model, scim_name, v)
|
|
219
|
+
else:
|
|
220
|
+
type(self)(self.path, v)(ext)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class AddOperator(Operator):
|
|
224
|
+
"""The implementation for the PATCH "add" operator."""
|
|
225
|
+
|
|
226
|
+
@classmethod
|
|
227
|
+
def operation(cls, model: BaseModel, attribute: str, value: any):
|
|
228
|
+
alias = get_by_alias(model, attribute)
|
|
229
|
+
if is_multi_valued(model, alias) and isinstance(value, list):
|
|
230
|
+
for v in value:
|
|
231
|
+
cls.operation(model, attribute, v)
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
existing_value = getattr(model, alias)
|
|
235
|
+
new_value = parse_new_value(model, alias, value)
|
|
236
|
+
if new_value == existing_value:
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
if model.get_field_annotation(alias, Mutability) == Mutability.read_only:
|
|
240
|
+
raise SCIMException(Error.make_mutability_error())
|
|
241
|
+
|
|
242
|
+
if is_multi_valued(model, alias):
|
|
243
|
+
if getattr(model, alias) is None:
|
|
244
|
+
setattr(model, alias, [])
|
|
245
|
+
if getattr(new_value, "primary", False):
|
|
246
|
+
for value in getattr(model, alias):
|
|
247
|
+
value.primary = False
|
|
248
|
+
getattr(model, alias).append(new_value)
|
|
249
|
+
else:
|
|
250
|
+
if (
|
|
251
|
+
model.get_field_annotation(alias, Required) == Required.true
|
|
252
|
+
and not new_value
|
|
253
|
+
):
|
|
254
|
+
raise SCIMException(Error.make_invalid_value_error())
|
|
255
|
+
setattr(model, alias, new_value)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class RemoveOperator(Operator):
|
|
259
|
+
"""The implementation for the PATCH "remove" operator."""
|
|
260
|
+
|
|
261
|
+
OPERATE_ON_ROOT = False
|
|
262
|
+
REQUIRES_VALUE = False
|
|
263
|
+
|
|
264
|
+
@classmethod
|
|
265
|
+
def operation(cls, model: BaseModel, attribute: str, value: any):
|
|
266
|
+
alias = get_by_alias(model, attribute)
|
|
267
|
+
existing_value = getattr(model, alias)
|
|
268
|
+
if not existing_value:
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
if model.get_field_annotation(alias, Mutability) in (
|
|
272
|
+
Mutability.read_only,
|
|
273
|
+
Mutability.immutable,
|
|
274
|
+
):
|
|
275
|
+
raise SCIMException(Error.make_mutability_error())
|
|
276
|
+
|
|
277
|
+
if model.get_field_annotation(alias, Required) == Required.true:
|
|
278
|
+
raise SCIMException(Error.make_invalid_value_error())
|
|
279
|
+
|
|
280
|
+
setattr(model, alias, None)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class ReplaceOperator(Operator):
|
|
284
|
+
"""The implementation for the PATCH "replace" operator."""
|
|
285
|
+
|
|
286
|
+
@classmethod
|
|
287
|
+
def operation(cls, model: BaseModel, attribute: str, value: any):
|
|
288
|
+
alias = get_by_alias(model, attribute)
|
|
289
|
+
if is_multi_valued(model, alias) and not isinstance(value, list):
|
|
290
|
+
raise SCIMException(Error.make_invalid_value_error())
|
|
291
|
+
|
|
292
|
+
existing_value = getattr(model, alias)
|
|
293
|
+
new_value = parse_new_value(model, alias, value)
|
|
294
|
+
if new_value == existing_value:
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
if model.get_field_annotation(alias, Mutability) == Mutability.read_only:
|
|
298
|
+
raise SCIMException(Error.make_mutability_error())
|
|
299
|
+
|
|
300
|
+
if (
|
|
301
|
+
model.get_field_annotation(alias, Required) == Required.true
|
|
302
|
+
and not new_value
|
|
303
|
+
):
|
|
304
|
+
raise SCIMException(Error.make_invalid_value_error())
|
|
305
|
+
setattr(model, alias, new_value)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class ResolveResult:
|
|
309
|
+
"""A descriptor for the result returned from the "ResolveOperator", used to
|
|
310
|
+
resolve attributes from a model."""
|
|
311
|
+
|
|
312
|
+
def __init__(self):
|
|
313
|
+
self.records = []
|
|
314
|
+
self.result_idx = 0
|
|
315
|
+
self.model = None
|
|
316
|
+
self.attribute = None
|
|
317
|
+
self.sub_attribute = None
|
|
318
|
+
|
|
319
|
+
def add_result(self, model: BaseModel, attribute_name: str):
|
|
320
|
+
"""Adds a result to the descriptor."""
|
|
321
|
+
self.records.append((model, attribute_name))
|
|
322
|
+
|
|
323
|
+
def add_result_index(self, model: BaseModel, attribute_name: str, index: int):
|
|
324
|
+
"""Adds a result to the descriptor.
|
|
325
|
+
|
|
326
|
+
The resulting attribute is part of a multi-valued attribute
|
|
327
|
+
described by its index.
|
|
328
|
+
"""
|
|
329
|
+
self.records.append((model, attribute_name, index))
|
|
330
|
+
|
|
331
|
+
def _evaluate_result(self, record: Tuple[str, str] | Tuple[str, str, int]):
|
|
332
|
+
if len(record) == 2:
|
|
333
|
+
return getattr(*record)
|
|
334
|
+
else:
|
|
335
|
+
return getattr(record[0], record[1])[record[2]]
|
|
336
|
+
|
|
337
|
+
def get_field_annotation(self, annotation_type: Type):
|
|
338
|
+
if not self.model:
|
|
339
|
+
return None
|
|
340
|
+
return self.model.get_field_annotation(self.attribute, annotation_type)
|
|
341
|
+
|
|
342
|
+
def get_values(self):
|
|
343
|
+
if not self.records:
|
|
344
|
+
return None
|
|
345
|
+
if len(self.records) == 1:
|
|
346
|
+
return self._evaluate_result(self.records[0])
|
|
347
|
+
return [self._evaluate_result(record) for record in self.records]
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class ResolveOperator(Operator):
|
|
351
|
+
"""An operator that resolves attribute values from a model."""
|
|
352
|
+
|
|
353
|
+
REQUIRES_VALUE = False
|
|
354
|
+
RETURNS_VALUE = True
|
|
355
|
+
|
|
356
|
+
def __init__(self, path: Optional[str]):
|
|
357
|
+
super().__init__(path, ResolveResult())
|
|
358
|
+
|
|
359
|
+
def do_return(self):
|
|
360
|
+
ret = self.value
|
|
361
|
+
self.value = ResolveResult()
|
|
362
|
+
return ret
|
|
363
|
+
|
|
364
|
+
@classmethod
|
|
365
|
+
def init_return(
|
|
366
|
+
cls,
|
|
367
|
+
model: BaseModel,
|
|
368
|
+
attribute: str,
|
|
369
|
+
sub_attribute: Optional[str],
|
|
370
|
+
value: ResolveResult,
|
|
371
|
+
):
|
|
372
|
+
alias = get_by_alias(model, attribute)
|
|
373
|
+
value.model = model
|
|
374
|
+
value.attribute = alias
|
|
375
|
+
value.sub_attribute = sub_attribute
|
|
376
|
+
if (
|
|
377
|
+
model.get_field_annotation(alias, Mutability) == Mutability.write_only
|
|
378
|
+
or model.get_field_annotation(alias, Returned) == Returned.never
|
|
379
|
+
):
|
|
380
|
+
raise SCIMException(Error.make_sensitive_error())
|
|
381
|
+
|
|
382
|
+
@classmethod
|
|
383
|
+
def operation(
|
|
384
|
+
cls, model: BaseModel, attribute: str, value: any, index: Optional[int] = None
|
|
385
|
+
):
|
|
386
|
+
alias = get_by_alias(model, attribute)
|
|
387
|
+
if index is None:
|
|
388
|
+
value.add_result(model, alias)
|
|
389
|
+
else:
|
|
390
|
+
value.add_result_index(model, alias, index)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class ResolveSortOperator(ResolveOperator):
|
|
394
|
+
"""
|
|
395
|
+
Helper-Operator that implements sorting, according to RFC 7644, Section 3.4.2.3
|
|
396
|
+
The ResolveResult returned by this operator contains at most 1 value, according to
|
|
397
|
+
the specification:
|
|
398
|
+
"[...] if it's a multi-valued attribute, resources are sorted by the value of the
|
|
399
|
+
primary attribute (see Section 2.4 of [RFC7643]), if any, or else the first value
|
|
400
|
+
in the list, if any. [...]"
|
|
401
|
+
|
|
402
|
+
Since a Query can result in resources of different types, sorting by an attribute
|
|
403
|
+
that is not defined for a certain resource type does not result in an error. No
|
|
404
|
+
value is returned and the resource is sorted as if the attribute on the resource
|
|
405
|
+
is not set.
|
|
406
|
+
"""
|
|
407
|
+
|
|
408
|
+
def __init__(self, path: Optional[str]):
|
|
409
|
+
super().__init__(path)
|
|
410
|
+
|
|
411
|
+
def alias_forbidden(self, model: BaseModel, alias: Optional[str]) -> bool:
|
|
412
|
+
return (
|
|
413
|
+
not alias
|
|
414
|
+
or model.get_field_annotation(alias, Mutability) == Mutability.write_only
|
|
415
|
+
or model.get_field_annotation(alias, Returned) == Returned.never
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
def set_value_case_exact(self, value: any, case_exact: CaseExact):
|
|
419
|
+
if isinstance(value, str) and case_exact == CaseExact.false:
|
|
420
|
+
value = value.lower()
|
|
421
|
+
self.value = value
|
|
422
|
+
|
|
423
|
+
def evaluate_value_for_complex(self, model: BaseModel, alias: str):
|
|
424
|
+
sub_attribute_alias = get_by_alias(model, alias, True)
|
|
425
|
+
if self.alias_forbidden(model, sub_attribute_alias):
|
|
426
|
+
return
|
|
427
|
+
case_exact = model.get_field_annotation(sub_attribute_alias, CaseExact)
|
|
428
|
+
sub_attribute_value = getattr(model, sub_attribute_alias)
|
|
429
|
+
self.set_value_case_exact(sub_attribute_value, case_exact)
|
|
430
|
+
|
|
431
|
+
def __call__(self, model: BaseModel):
|
|
432
|
+
self.value = None
|
|
433
|
+
if self.path:
|
|
434
|
+
model, path = self.parse_path(model)
|
|
435
|
+
if not path:
|
|
436
|
+
return
|
|
437
|
+
sub_attribute = path["sub_attribute"] or "value"
|
|
438
|
+
|
|
439
|
+
attribute_alias = get_by_alias(model, path["attribute"], True)
|
|
440
|
+
if self.alias_forbidden(model, attribute_alias):
|
|
441
|
+
return
|
|
442
|
+
|
|
443
|
+
case_exact = model.get_field_annotation(attribute_alias, CaseExact)
|
|
444
|
+
attribute_value = getattr(model, attribute_alias)
|
|
445
|
+
if not attribute_value:
|
|
446
|
+
return
|
|
447
|
+
|
|
448
|
+
if isinstance(attribute_value, list):
|
|
449
|
+
if path["condition"]:
|
|
450
|
+
token_stream = SCIMLexer().tokenize(path["condition"])
|
|
451
|
+
condition = SCIMParser().parse(token_stream)
|
|
452
|
+
attribute_value = [
|
|
453
|
+
model
|
|
454
|
+
for model in attribute_value
|
|
455
|
+
if evaluate_filter(model, condition)
|
|
456
|
+
]
|
|
457
|
+
candidate = self.select_candidate(attribute_value)
|
|
458
|
+
if isinstance(candidate, BaseModel):
|
|
459
|
+
self.evaluate_value_for_complex(candidate, sub_attribute)
|
|
460
|
+
else:
|
|
461
|
+
self.set_value_case_exact(candidate, case_exact)
|
|
462
|
+
elif isinstance(attribute_value, BaseModel):
|
|
463
|
+
if not path["condition"]:
|
|
464
|
+
self.evaluate_value_for_complex(attribute_value, sub_attribute)
|
|
465
|
+
else:
|
|
466
|
+
if not path["condition"] and not path["sub_attribute"]:
|
|
467
|
+
self.set_value_case_exact(attribute_value, case_exact)
|
|
468
|
+
return self.value
|
|
469
|
+
|
|
470
|
+
def select_candidate(self, values: List[any]) -> Tuple[Optional[any], int]:
|
|
471
|
+
"""Selects a viable candidate from a list of possible values."""
|
|
472
|
+
for value in values:
|
|
473
|
+
primary = getattr(value, "primary", False)
|
|
474
|
+
if primary:
|
|
475
|
+
return value
|
|
476
|
+
if values:
|
|
477
|
+
return values[0]
|
|
478
|
+
return None
|