howler-api 3.3.0.dev767__py3-none-any.whl → 3.3.0.dev768__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.
- howler/api/v1/user.py +2 -4
- howler/common/classification.py +162 -82
- howler/config.py +1 -1
- howler/helper/azure.py +7 -2
- howler/helper/oauth.py +40 -16
- howler/helper/search.py +1 -1
- howler/odm/models/config.py +1 -1
- howler/odm/models/hit.py +7 -0
- howler/odm/models/user.py +1 -0
- howler/odm/random_data.py +14 -1
- howler/odm/randomizer.py +6 -4
- howler/services/jwt_service.py +2 -2
- howler/services/user_service.py +38 -14
- {howler_api-3.3.0.dev767.dist-info → howler_api-3.3.0.dev768.dist-info}/METADATA +1 -1
- {howler_api-3.3.0.dev767.dist-info → howler_api-3.3.0.dev768.dist-info}/RECORD +17 -17
- {howler_api-3.3.0.dev767.dist-info → howler_api-3.3.0.dev768.dist-info}/WHEEL +0 -0
- {howler_api-3.3.0.dev767.dist-info → howler_api-3.3.0.dev768.dist-info}/entry_points.txt +0 -0
howler/api/v1/user.py
CHANGED
|
@@ -128,9 +128,7 @@ def add_user_account(username, **_):
|
|
|
128
128
|
data["name"] = data["uname"]
|
|
129
129
|
|
|
130
130
|
# Add dynamic classification group
|
|
131
|
-
data["classification"] = user_service.get_dynamic_classification(
|
|
132
|
-
cast(str | None, data.get("classification", None)), data["email"]
|
|
133
|
-
)
|
|
131
|
+
data["classification"] = user_service.get_dynamic_classification(data)
|
|
134
132
|
|
|
135
133
|
# Clear non user account data
|
|
136
134
|
avatar = data.pop("avatar", None)
|
|
@@ -290,7 +288,7 @@ def set_user_account(username: str, **kwargs): # noqa: C901
|
|
|
290
288
|
data.pop("new_pass_confirm", None)
|
|
291
289
|
|
|
292
290
|
# Apply dynamic classification
|
|
293
|
-
data["classification"] = user_service.get_dynamic_classification(data
|
|
291
|
+
data["classification"] = user_service.get_dynamic_classification(data)
|
|
294
292
|
|
|
295
293
|
ret_val = user_service.save_user_account(username, data, kwargs["user"])
|
|
296
294
|
return ok({"success": ret_val})
|
howler/common/classification.py
CHANGED
|
@@ -38,7 +38,7 @@ class Classification(object):
|
|
|
38
38
|
"description",
|
|
39
39
|
]
|
|
40
40
|
self.original_definition = classification_definition
|
|
41
|
-
self.levels_map: dict[str,
|
|
41
|
+
self.levels_map: dict[str, int] = {}
|
|
42
42
|
self.levels_map_stl = {}
|
|
43
43
|
self.levels_map_lts = {}
|
|
44
44
|
self.levels_styles_map = {}
|
|
@@ -64,6 +64,9 @@ class Classification(object):
|
|
|
64
64
|
|
|
65
65
|
self.enforce = False
|
|
66
66
|
self.dynamic_groups = False
|
|
67
|
+
# dynamic group type is one of: email | group | all
|
|
68
|
+
# defaults to email for original behavior
|
|
69
|
+
self.dynamic_groups_type = "email"
|
|
67
70
|
|
|
68
71
|
# Add Invalid classification
|
|
69
72
|
self.levels_map["INV"] = self.INVALID_LVL
|
|
@@ -84,12 +87,10 @@ class Classification(object):
|
|
|
84
87
|
raise HowlerKeyError("Enforce not set!")
|
|
85
88
|
|
|
86
89
|
self.dynamic_groups = classification_definition.get("dynamic_groups", None)
|
|
87
|
-
if self.
|
|
90
|
+
if self.dynamic_groups is None:
|
|
88
91
|
raise HowlerKeyError("Dynamic groups not set!")
|
|
89
92
|
|
|
90
|
-
|
|
91
|
-
self._classification_cache = self.list_all_classification_combinations()
|
|
92
|
-
self._classification_cache_short = self.list_all_classification_combinations(long_format=False)
|
|
93
|
+
self.dynamic_groups_type = classification_definition.get("dynamic_groups_type", self.dynamic_groups_type)
|
|
93
94
|
|
|
94
95
|
if classification_definition.get("levels", None) is None:
|
|
95
96
|
raise HowlerKeyError("No classification levels provided!")
|
|
@@ -186,6 +187,16 @@ class Classification(object):
|
|
|
186
187
|
self.description[short_name] = x.get("description", "N/A")
|
|
187
188
|
self.description[name] = self.description[short_name]
|
|
188
189
|
|
|
190
|
+
# Build the classification cache AFTER all maps are populated so
|
|
191
|
+
# that normalize_classification can fully validate each combination.
|
|
192
|
+
# Using normalized=True filters out invalid combos (e.g. subgroups
|
|
193
|
+
# that violate limited_to_group constraints).
|
|
194
|
+
if self.enforce:
|
|
195
|
+
self._classification_cache = self.list_all_classification_combinations(normalized=True)
|
|
196
|
+
self._classification_cache_short = self.list_all_classification_combinations(
|
|
197
|
+
long_format=False, normalized=True
|
|
198
|
+
)
|
|
199
|
+
|
|
189
200
|
if not self.is_valid(classification_definition["unrestricted"]):
|
|
190
201
|
raise InvalidDefinition("Classification definition's unrestricted classification is invalid.")
|
|
191
202
|
|
|
@@ -238,40 +249,46 @@ class Classification(object):
|
|
|
238
249
|
|
|
239
250
|
return items
|
|
240
251
|
|
|
241
|
-
def _get_c12n_level_index(self, c12n: str) -> str:
|
|
252
|
+
def _get_c12n_level_index(self, c12n: str) -> tuple[int, str]:
|
|
242
253
|
# Parse classifications in uppercase mode only
|
|
243
254
|
c12n = c12n.upper()
|
|
244
255
|
|
|
245
|
-
lvl = c12n.
|
|
256
|
+
lvl, _, remain = c12n.partition("//")
|
|
246
257
|
if lvl in self.levels_map:
|
|
247
|
-
return self.levels_map[lvl]
|
|
258
|
+
return self.levels_map[lvl], remain
|
|
248
259
|
elif lvl in self.levels_map_lts:
|
|
249
|
-
return self.levels_map[self.levels_map_lts[lvl]]
|
|
260
|
+
return self.levels_map[self.levels_map_lts[lvl]], remain
|
|
250
261
|
elif lvl in self.levels_aliases:
|
|
251
|
-
return self.levels_map[self.levels_aliases[lvl]]
|
|
262
|
+
return self.levels_map[self.levels_aliases[lvl]], remain
|
|
252
263
|
else:
|
|
253
264
|
raise InvalidClassification(
|
|
254
265
|
"Classification level '%s' was not found in your classification definition." % lvl
|
|
255
266
|
)
|
|
256
267
|
|
|
257
268
|
def _get_c12n_level_text(self, lvl_idx: int, long_format: bool = True) -> str:
|
|
258
|
-
text = self.levels_map.get(str(lvl_idx), None)
|
|
269
|
+
text: Any = self.levels_map.get(str(lvl_idx), None)
|
|
270
|
+
|
|
259
271
|
if not text:
|
|
260
272
|
raise InvalidClassification(
|
|
261
273
|
"Classification level number '%s' was not found in your classification definition." % lvl_idx
|
|
262
274
|
)
|
|
275
|
+
|
|
263
276
|
if long_format:
|
|
264
277
|
return self.levels_map_stl[text]
|
|
278
|
+
|
|
265
279
|
return text
|
|
266
280
|
|
|
267
|
-
def _get_c12n_required(self, c12n: str, long_format: bool = True) -> List:
|
|
281
|
+
def _get_c12n_required(self, c12n: str, long_format: bool = True) -> tuple[List, list[str]]:
|
|
268
282
|
# Parse classifications in uppercase mode only
|
|
269
283
|
c12n = c12n.upper()
|
|
270
284
|
|
|
271
285
|
return_set = set()
|
|
272
286
|
part_set = set(c12n.split("/"))
|
|
287
|
+
unused = []
|
|
273
288
|
|
|
274
289
|
for p in part_set:
|
|
290
|
+
if not p:
|
|
291
|
+
continue
|
|
275
292
|
if p in self.access_req_map_lts:
|
|
276
293
|
return_set.add(self.access_req_map_lts[p])
|
|
277
294
|
elif p in self.access_req_map_stl:
|
|
@@ -279,27 +296,37 @@ class Classification(object):
|
|
|
279
296
|
elif p in self.access_req_aliases:
|
|
280
297
|
for a in self.access_req_aliases[p]:
|
|
281
298
|
return_set.add(a)
|
|
299
|
+
else:
|
|
300
|
+
unused.append(p)
|
|
282
301
|
|
|
283
302
|
if long_format:
|
|
284
|
-
return sorted([self.access_req_map_stl[r] for r in return_set])
|
|
285
|
-
return sorted(list(return_set))
|
|
303
|
+
return sorted([self.access_req_map_stl[r] for r in return_set]), unused
|
|
304
|
+
return sorted(list(return_set)), unused
|
|
286
305
|
|
|
287
|
-
def _get_c12n_groups(
|
|
306
|
+
def _get_c12n_groups(
|
|
307
|
+
self, c12n: list[str], long_format: bool = True, get_dynamic_groups: bool = True, auto_select: bool = False
|
|
308
|
+
) -> Tuple[List, List, list[str]]:
|
|
288
309
|
# Parse classifications in uppercase mode only
|
|
289
|
-
c12n = c12n.upper()
|
|
290
310
|
|
|
291
311
|
g1_set = set()
|
|
292
312
|
g2_set = set()
|
|
293
313
|
others = set()
|
|
294
314
|
|
|
295
|
-
grp_part = c12n.split("//")
|
|
296
315
|
groups = []
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
316
|
+
subgroups = []
|
|
317
|
+
|
|
318
|
+
for gp in c12n:
|
|
319
|
+
# If there is a rel marking we know we have groups
|
|
320
|
+
if gp.startswith("REL "):
|
|
321
|
+
gp = gp.replace("REL TO ", "")
|
|
322
|
+
gp = gp.replace("REL ", "")
|
|
323
|
+
temp_group = set([x.strip() for x in gp.split(",")])
|
|
324
|
+
for t in temp_group:
|
|
325
|
+
groups.extend(t.split("/"))
|
|
326
|
+
else:
|
|
327
|
+
# if there is not a rel marking we either have a subgroup or a solitary_display_name
|
|
328
|
+
# alias for a group, which we will filter out later
|
|
329
|
+
subgroups.append(gp)
|
|
303
330
|
|
|
304
331
|
for g in groups:
|
|
305
332
|
if g in self.groups_map_lts:
|
|
@@ -309,33 +336,63 @@ class Classification(object):
|
|
|
309
336
|
elif g in self.groups_aliases:
|
|
310
337
|
for a in self.groups_aliases[g]:
|
|
311
338
|
g1_set.add(a)
|
|
312
|
-
|
|
339
|
+
else:
|
|
340
|
+
others.add(g)
|
|
341
|
+
|
|
342
|
+
for g in subgroups:
|
|
343
|
+
if g in self.subgroups_map_lts:
|
|
313
344
|
g2_set.add(self.subgroups_map_lts[g])
|
|
314
345
|
elif g in self.subgroups_map_stl:
|
|
315
346
|
g2_set.add(g)
|
|
316
347
|
elif g in self.subgroups_aliases:
|
|
317
348
|
for a in self.subgroups_aliases[g]:
|
|
318
349
|
g2_set.add(a)
|
|
350
|
+
# Here is where we catch any solitary_display_name aliases for groups within the subgroup sections
|
|
351
|
+
elif g in self.groups_aliases:
|
|
352
|
+
# Check that this alias is actually a solitary name, don't
|
|
353
|
+
# let other aliases leak outside the REL marking
|
|
354
|
+
groups = self.groups_aliases[g]
|
|
355
|
+
if len(groups) > 1:
|
|
356
|
+
raise InvalidClassification(f"Unclear use of alias: {g}")
|
|
357
|
+
g1_set.add(groups[0])
|
|
319
358
|
else:
|
|
320
|
-
|
|
359
|
+
raise InvalidClassification(f"Unknown Subgroup: {g}")
|
|
360
|
+
|
|
361
|
+
# If dynamic groups are active all remaining parts should be groups found under a
|
|
362
|
+
# REL TO marking that we can merge in with the other groups
|
|
363
|
+
if self.dynamic_groups and get_dynamic_groups:
|
|
364
|
+
g1_set.update(others)
|
|
365
|
+
others = set()
|
|
366
|
+
|
|
367
|
+
# Check if there are any required group assignments
|
|
368
|
+
for subgroup in g2_set:
|
|
369
|
+
required = self.params_map.get(subgroup, {}).get("require_group", None)
|
|
370
|
+
if required:
|
|
371
|
+
g1_set.add(required)
|
|
372
|
+
|
|
373
|
+
# Check if there are any forbidden group assignments
|
|
374
|
+
for subgroup in g2_set:
|
|
375
|
+
limited_to_group = self.params_map.get(subgroup, {}).get("limited_to_group", None)
|
|
376
|
+
if limited_to_group is not None:
|
|
377
|
+
if len(g1_set) > 1 or (len(g1_set) == 1 and g1_set != set([limited_to_group])):
|
|
378
|
+
raise InvalidClassification(
|
|
379
|
+
f"Subgroup {subgroup} is limited to group {limited_to_group} (found: {', '.join(g1_set)})"
|
|
380
|
+
)
|
|
321
381
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
and o not in self.access_req_aliases
|
|
328
|
-
and o not in self.levels_map
|
|
329
|
-
and o not in self.levels_map_lts
|
|
330
|
-
and o not in self.levels_aliases
|
|
331
|
-
):
|
|
332
|
-
g1_set.add(o)
|
|
382
|
+
# Do auto select
|
|
383
|
+
if auto_select and g1_set:
|
|
384
|
+
g1_set.update(self.groups_auto_select_short)
|
|
385
|
+
if auto_select and g2_set:
|
|
386
|
+
g2_set.update(self.subgroups_auto_select_short)
|
|
333
387
|
|
|
388
|
+
# Swap to long format if required
|
|
334
389
|
if long_format:
|
|
335
|
-
return
|
|
336
|
-
[self.
|
|
390
|
+
return (
|
|
391
|
+
sorted([self.groups_map_stl.get(r, r) for r in g1_set]),
|
|
392
|
+
sorted([self.subgroups_map_stl[r] for r in g2_set]),
|
|
393
|
+
list(others),
|
|
337
394
|
)
|
|
338
|
-
return sorted(list(g1_set)), sorted(list(g2_set))
|
|
395
|
+
return sorted(list(g1_set)), sorted(list(g2_set)), list(others)
|
|
339
396
|
|
|
340
397
|
@staticmethod
|
|
341
398
|
def _can_see_required(user_req: List, req: List) -> bool:
|
|
@@ -446,11 +503,22 @@ class Classification(object):
|
|
|
446
503
|
return out
|
|
447
504
|
|
|
448
505
|
def _get_classification_parts(
|
|
449
|
-
self,
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
506
|
+
self,
|
|
507
|
+
c12n: str,
|
|
508
|
+
long_format: bool = True,
|
|
509
|
+
get_dynamic_groups: bool = True,
|
|
510
|
+
auto_select: bool = False,
|
|
511
|
+
ignore_unused: bool = False,
|
|
512
|
+
) -> Tuple[int, list[str], list[str], list[str]]:
|
|
513
|
+
lvl_idx, unused = self._get_c12n_level_index(c12n)
|
|
514
|
+
req, unused_parts = self._get_c12n_required(unused, long_format=long_format)
|
|
515
|
+
|
|
516
|
+
groups, subgroups, unused_parts = self._get_c12n_groups(
|
|
517
|
+
unused_parts, long_format=long_format, get_dynamic_groups=get_dynamic_groups, auto_select=auto_select
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
if unused_parts and not ignore_unused:
|
|
521
|
+
raise InvalidClassification(f"Unparsable classification parts: {', '.join(unused_parts)}")
|
|
454
522
|
|
|
455
523
|
return lvl_idx, req, groups, subgroups
|
|
456
524
|
|
|
@@ -473,7 +541,7 @@ class Classification(object):
|
|
|
473
541
|
# Public functions
|
|
474
542
|
# ++++++++++++++++++++++++
|
|
475
543
|
# noinspection PyUnusedLocal
|
|
476
|
-
def list_all_classification_combinations(self, long_format: bool = True) -> Set:
|
|
544
|
+
def list_all_classification_combinations(self, long_format: bool = True, normalized: bool = False) -> Set:
|
|
477
545
|
combinations = set()
|
|
478
546
|
|
|
479
547
|
levels = self._list_items_and_aliases(self.original_definition["levels"], long_format=long_format)
|
|
@@ -529,7 +597,10 @@ class Classification(object):
|
|
|
529
597
|
|
|
530
598
|
temp_combinations = copy(combinations)
|
|
531
599
|
for p in itertools.product(temp_combinations, sgrp_cbs):
|
|
532
|
-
|
|
600
|
+
# A combo already has a group part when it contains "//REL TO " (explicit)
|
|
601
|
+
# or ends with a solitary display name like "//ANY" (after replacement).
|
|
602
|
+
has_group = "//REL TO " in p[0] or any(p[0].endswith(f"//{sol_name}") for sol_name in solitary_names)
|
|
603
|
+
if has_group:
|
|
533
604
|
cl = "/".join(p)
|
|
534
605
|
|
|
535
606
|
if cl.endswith("/"):
|
|
@@ -537,13 +608,25 @@ class Classification(object):
|
|
|
537
608
|
else:
|
|
538
609
|
combinations.add(cl)
|
|
539
610
|
else:
|
|
540
|
-
|
|
611
|
+
# No group present — subgroups are joined with "//" (not "//REL TO ")
|
|
612
|
+
# to match the format produced by _get_normalized_classification_text.
|
|
613
|
+
# "REL TO" is reserved for groups in _get_c12n_groups.
|
|
614
|
+
cl = "//".join(p)
|
|
541
615
|
|
|
542
|
-
if cl.endswith("//
|
|
543
|
-
combinations.add(cl[:-
|
|
616
|
+
if cl.endswith("//"):
|
|
617
|
+
combinations.add(cl[:-2])
|
|
544
618
|
else:
|
|
545
619
|
combinations.add(cl)
|
|
546
620
|
|
|
621
|
+
if normalized:
|
|
622
|
+
good = []
|
|
623
|
+
for x in combinations:
|
|
624
|
+
try:
|
|
625
|
+
good.append(self.normalize_classification(x, long_format=long_format))
|
|
626
|
+
except InvalidClassification:
|
|
627
|
+
pass
|
|
628
|
+
return set(good)
|
|
629
|
+
|
|
547
630
|
return combinations
|
|
548
631
|
|
|
549
632
|
# noinspection PyUnusedLocal
|
|
@@ -581,7 +664,8 @@ class Classification(object):
|
|
|
581
664
|
return out
|
|
582
665
|
|
|
583
666
|
def get_access_control_parts(self, c12n: str, user_classification: bool = False) -> Dict:
|
|
584
|
-
"""
|
|
667
|
+
"""
|
|
668
|
+
Returns a dictionary containing the different access parameters Lucene needs to build it's queries
|
|
585
669
|
|
|
586
670
|
Args:
|
|
587
671
|
c12n: The classification to get the parts from
|
|
@@ -591,12 +675,8 @@ class Classification(object):
|
|
|
591
675
|
c12n = self.UNRESTRICTED
|
|
592
676
|
|
|
593
677
|
try:
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
access_lvl = self._get_c12n_level_index(c12n)
|
|
598
|
-
access_req = self._get_c12n_required(c12n, long_format=False)
|
|
599
|
-
access_grp1, access_grp2 = self._get_c12n_groups(c12n, long_format=False)
|
|
678
|
+
parts = self._get_classification_parts(c12n, long_format=False, auto_select=not user_classification)
|
|
679
|
+
access_lvl, access_req, access_grp1, access_grp2 = parts
|
|
600
680
|
|
|
601
681
|
return {
|
|
602
682
|
"__access_lvl__": access_lvl,
|
|
@@ -679,7 +759,8 @@ class Classification(object):
|
|
|
679
759
|
)
|
|
680
760
|
|
|
681
761
|
def is_accessible(self, user_c12n: str, c12n: str, ignore_invalid: bool = False) -> bool:
|
|
682
|
-
"""
|
|
762
|
+
"""
|
|
763
|
+
Given a user classification, check if a user is allow to see a certain classification
|
|
683
764
|
|
|
684
765
|
Args:
|
|
685
766
|
user_c12n: Maximum classification for the user
|
|
@@ -698,16 +779,10 @@ class Classification(object):
|
|
|
698
779
|
return True
|
|
699
780
|
|
|
700
781
|
try:
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
c12n = self.normalize_classification(c12n, skip_auto_select=True)
|
|
782
|
+
user_lvl, user_req, user_groups, user_subgroups = self._get_classification_parts(user_c12n)
|
|
783
|
+
lvl, req, groups, subgroups = self._get_classification_parts(c12n)
|
|
704
784
|
|
|
705
|
-
|
|
706
|
-
user_groups, user_subgroups = self._get_c12n_groups(user_c12n)
|
|
707
|
-
req = self._get_c12n_required(c12n)
|
|
708
|
-
groups, subgroups = self._get_c12n_groups(c12n)
|
|
709
|
-
|
|
710
|
-
if self._get_c12n_level_index(user_c12n) >= self._get_c12n_level_index(c12n):
|
|
785
|
+
if int(user_lvl) >= int(lvl):
|
|
711
786
|
if not self._can_see_required(user_req, req):
|
|
712
787
|
return False
|
|
713
788
|
if not self._can_see_groups(user_groups, groups):
|
|
@@ -812,7 +887,7 @@ class Classification(object):
|
|
|
812
887
|
|
|
813
888
|
return True
|
|
814
889
|
|
|
815
|
-
def max_classification(self, c12n_1: str, c12n_2: str, long_format: bool = True) -> str:
|
|
890
|
+
def max_classification(self, c12n_1: str | None, c12n_2: str | None, long_format: bool = True) -> str:
|
|
816
891
|
"""Mixes to classification and returns to most restrictive form for them
|
|
817
892
|
|
|
818
893
|
Args:
|
|
@@ -833,9 +908,10 @@ class Classification(object):
|
|
|
833
908
|
c12n_2 = self.normalize_classification(c12n_2)
|
|
834
909
|
|
|
835
910
|
if c12n_1 is None:
|
|
836
|
-
return c12n_2
|
|
911
|
+
return c12n_2 if c12n_2 else self.UNRESTRICTED
|
|
912
|
+
|
|
837
913
|
if c12n_2 is None:
|
|
838
|
-
return c12n_1
|
|
914
|
+
return c12n_1 if c12n_1 else self.UNRESTRICTED
|
|
839
915
|
|
|
840
916
|
lvl_idx_1, req_1, groups_1, subgroups_1 = self._get_classification_parts(c12n_1, long_format=long_format)
|
|
841
917
|
lvl_idx_2, req_2, groups_2, subgroups_2 = self._get_classification_parts(c12n_2, long_format=long_format)
|
|
@@ -899,8 +975,16 @@ class Classification(object):
|
|
|
899
975
|
long_format=long_format, # type: ignore
|
|
900
976
|
)
|
|
901
977
|
|
|
902
|
-
def normalize_classification(
|
|
903
|
-
|
|
978
|
+
def normalize_classification(
|
|
979
|
+
self,
|
|
980
|
+
c12n: str,
|
|
981
|
+
long_format: bool = True,
|
|
982
|
+
skip_auto_select: bool = False,
|
|
983
|
+
get_dynamic_groups: bool = True,
|
|
984
|
+
ignore_unused: bool = False,
|
|
985
|
+
) -> str:
|
|
986
|
+
"""
|
|
987
|
+
Normalize a given classification by applying the rules defined in the classification definition.
|
|
904
988
|
This function will remove any invalid parts and add missing parts to the classification.
|
|
905
989
|
It will also ensure that the display of the classification is always done the same way
|
|
906
990
|
|
|
@@ -916,19 +1000,16 @@ class Classification(object):
|
|
|
916
1000
|
return self.UNRESTRICTED
|
|
917
1001
|
|
|
918
1002
|
# Has the classification has already been normalized before?
|
|
919
|
-
if long_format and c12n in self._classification_cache:
|
|
1003
|
+
if long_format and c12n in self._classification_cache and get_dynamic_groups:
|
|
920
1004
|
return c12n
|
|
921
|
-
if not long_format and c12n in self._classification_cache_short:
|
|
1005
|
+
if not long_format and c12n in self._classification_cache_short and get_dynamic_groups:
|
|
922
1006
|
return c12n
|
|
923
1007
|
|
|
924
|
-
lvl_idx, req, groups, subgroups = self._get_classification_parts(
|
|
1008
|
+
lvl_idx, req, groups, subgroups = self._get_classification_parts(
|
|
1009
|
+
c12n, long_format=long_format, get_dynamic_groups=get_dynamic_groups, ignore_unused=ignore_unused
|
|
1010
|
+
)
|
|
925
1011
|
new_c12n = self._get_normalized_classification_text(
|
|
926
|
-
lvl_idx,
|
|
927
|
-
req,
|
|
928
|
-
groups,
|
|
929
|
-
subgroups,
|
|
930
|
-
long_format=long_format,
|
|
931
|
-
skip_auto_select=skip_auto_select,
|
|
1012
|
+
lvl_idx, req, groups, subgroups, long_format=long_format, skip_auto_select=skip_auto_select
|
|
932
1013
|
)
|
|
933
1014
|
if long_format:
|
|
934
1015
|
self._classification_cache.add(new_c12n)
|
|
@@ -937,7 +1018,7 @@ class Classification(object):
|
|
|
937
1018
|
|
|
938
1019
|
return new_c12n
|
|
939
1020
|
|
|
940
|
-
def build_user_classification(self, c12n_1: str
|
|
1021
|
+
def build_user_classification(self, c12n_1: str, c12n_2: str, long_format: bool = True) -> str:
|
|
941
1022
|
"""Mixes to classification and return the classification marking that would give access to the most data
|
|
942
1023
|
|
|
943
1024
|
Args:
|
|
@@ -959,7 +1040,6 @@ class Classification(object):
|
|
|
959
1040
|
|
|
960
1041
|
if c12n_1 is None:
|
|
961
1042
|
return c12n_2
|
|
962
|
-
|
|
963
1043
|
if c12n_2 is None:
|
|
964
1044
|
return c12n_1
|
|
965
1045
|
|
howler/config.py
CHANGED
|
@@ -10,7 +10,7 @@ from howler.remote.datatypes.user_quota_tracker import UserQuotaTracker
|
|
|
10
10
|
#################################################################
|
|
11
11
|
# Configuration
|
|
12
12
|
|
|
13
|
-
CLASSIFICATION = loader.get_classification()
|
|
13
|
+
CLASSIFICATION = loader.get_classification(os.getenv("HWL_CLASSIFICATION_PATH", None))
|
|
14
14
|
|
|
15
15
|
AUDIT = config.ui.audit
|
|
16
16
|
|
howler/helper/azure.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import requests
|
|
2
2
|
|
|
3
|
-
from howler.common.exceptions import HowlerException
|
|
3
|
+
from howler.common.exceptions import HowlerAttributeError, HowlerException
|
|
4
4
|
from howler.common.logging import get_logger
|
|
5
5
|
from howler.config import config
|
|
6
6
|
from howler.utils.str_utils import default_string_value
|
|
@@ -20,7 +20,12 @@ def azure_obo(token: str) -> str:
|
|
|
20
20
|
Returns:
|
|
21
21
|
str: The new access token with updated privileges
|
|
22
22
|
"""
|
|
23
|
-
|
|
23
|
+
if "azure" in config.auth.oauth.providers:
|
|
24
|
+
azure_provider_config = config.auth.oauth.providers["azure"]
|
|
25
|
+
elif "entraid" in config.auth.oauth.providers:
|
|
26
|
+
azure_provider_config = config.auth.oauth.providers["entraid"]
|
|
27
|
+
else:
|
|
28
|
+
raise HowlerAttributeError("No azure/entraid-based provider configured!")
|
|
24
29
|
|
|
25
30
|
logger.debug("OBOing to MS Graph")
|
|
26
31
|
# Azure is a special case here, as we need to OBO to MS Graph
|
howler/helper/oauth.py
CHANGED
|
@@ -60,7 +60,7 @@ def parse_profile(profile: dict[str, Any], provider_config: OAuthProvider) -> di
|
|
|
60
60
|
uname = profile.get("uname", profile.get("preferred_username", email_adr))
|
|
61
61
|
|
|
62
62
|
# Did we default to email?
|
|
63
|
-
if
|
|
63
|
+
if uname is not None and email_adr is not None and uname.lower() == email_adr.lower():
|
|
64
64
|
# 1. Use provided regex matcher
|
|
65
65
|
if provider_config.uid_regex:
|
|
66
66
|
match = re.match(provider_config.uid_regex, uname)
|
|
@@ -93,6 +93,8 @@ def parse_profile(profile: dict[str, Any], provider_config: OAuthProvider) -> di
|
|
|
93
93
|
# Compute access, roles and classification using auto_properties
|
|
94
94
|
access = True
|
|
95
95
|
roles = ["user"]
|
|
96
|
+
groups: list[str] = profile.get("groups", [])
|
|
97
|
+
assignments = []
|
|
96
98
|
classification = CLASSIFICATION_ENGINE.UNRESTRICTED
|
|
97
99
|
if provider_config.auto_properties:
|
|
98
100
|
for auto_prop in provider_config.auto_properties:
|
|
@@ -129,18 +131,33 @@ def parse_profile(profile: dict[str, Any], provider_config: OAuthProvider) -> di
|
|
|
129
131
|
classification = CLASSIFICATION_ENGINE.build_user_classification(
|
|
130
132
|
classification, auto_prop.value
|
|
131
133
|
)
|
|
134
|
+
logger.debug("Classification: %s", classification)
|
|
132
135
|
break
|
|
133
136
|
|
|
134
|
-
|
|
135
|
-
|
|
137
|
+
# Append groups from matching patterns
|
|
138
|
+
elif auto_prop.type == "group":
|
|
139
|
+
if re.match(auto_prop.pattern, value):
|
|
140
|
+
groups.append(auto_prop.value)
|
|
141
|
+
break
|
|
142
|
+
|
|
143
|
+
# Append assignments from matching patterns
|
|
144
|
+
elif auto_prop.type == "assignment":
|
|
145
|
+
if re.match(auto_prop.pattern, value):
|
|
146
|
+
assignments.append(auto_prop.value)
|
|
147
|
+
break
|
|
148
|
+
|
|
149
|
+
# Infer roles from groups (legacy)
|
|
150
|
+
if groups and provider_config.role_map:
|
|
136
151
|
for user_type in USER_TYPES:
|
|
137
152
|
if (
|
|
138
153
|
user_type in provider_config.role_map
|
|
139
|
-
and provider_config.role_map[user_type] in
|
|
154
|
+
and provider_config.role_map[user_type] in groups
|
|
140
155
|
and user_type not in roles
|
|
141
156
|
):
|
|
142
157
|
roles.append(user_type)
|
|
143
158
|
|
|
159
|
+
# TODO: re-export assignments once they're actually used.
|
|
160
|
+
# This may need a refactor once the tags stuff is figured out
|
|
144
161
|
return dict(
|
|
145
162
|
access=access,
|
|
146
163
|
type=roles,
|
|
@@ -150,7 +167,7 @@ def parse_profile(profile: dict[str, Any], provider_config: OAuthProvider) -> di
|
|
|
150
167
|
email=email_adr,
|
|
151
168
|
password="__NO_PASSWORD__", # noqa: S106
|
|
152
169
|
avatar=profile.get("picture", provider_config.picture_url or alternate),
|
|
153
|
-
groups=
|
|
170
|
+
groups=groups,
|
|
154
171
|
)
|
|
155
172
|
|
|
156
173
|
|
|
@@ -166,9 +183,9 @@ def fetch_avatar( # noqa: C901
|
|
|
166
183
|
# Generic picture url endpoint, i.e. MS Graph
|
|
167
184
|
if url == provider_config.picture_url:
|
|
168
185
|
headers = {}
|
|
169
|
-
|
|
170
186
|
token: str | None = None
|
|
171
|
-
|
|
187
|
+
|
|
188
|
+
if oauth_provider in ["entraid", "azure"]:
|
|
172
189
|
if not access_token:
|
|
173
190
|
raise HowlerValueError("An azure access token is necessary to retrieve the profile picture") # noqa: TRY301
|
|
174
191
|
|
|
@@ -206,13 +223,13 @@ def fetch_avatar( # noqa: C901
|
|
|
206
223
|
return None
|
|
207
224
|
|
|
208
225
|
|
|
209
|
-
def fetch_groups(token: str):
|
|
210
|
-
"""Fetch a user's groups
|
|
226
|
+
def fetch_groups(token: str): # noqa: C901
|
|
227
|
+
"""Fetch a user's groups from an external endpoint"""
|
|
211
228
|
oauth_provider = jwt_service.get_provider(token)
|
|
212
229
|
oauth_provider_config = config.auth.oauth.providers[oauth_provider]
|
|
213
230
|
|
|
214
231
|
if oauth_provider_config.groups_url:
|
|
215
|
-
if oauth_provider
|
|
232
|
+
if oauth_provider in ["entraid", "azure"]:
|
|
216
233
|
try:
|
|
217
234
|
token = azure_obo(token)
|
|
218
235
|
except HowlerException:
|
|
@@ -232,13 +249,20 @@ def fetch_groups(token: str):
|
|
|
232
249
|
result = result[part]
|
|
233
250
|
|
|
234
251
|
detailed_group_data = []
|
|
252
|
+
|
|
235
253
|
for group in result:
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
254
|
+
# Only process groups that are groups (not directory roles)
|
|
255
|
+
if group.get("@odata.type") == "#microsoft.graph.group":
|
|
256
|
+
group_id = group.get("id")
|
|
257
|
+
display_name = group.get("displayName")
|
|
258
|
+
if display_name: # Only add if display name exists
|
|
259
|
+
detailed_group_data.append(
|
|
260
|
+
{
|
|
261
|
+
"id": group_id,
|
|
262
|
+
"name": display_name,
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
logger.debug("Added group: %s (%s)", display_name, group_id)
|
|
242
266
|
|
|
243
267
|
return sorted(detailed_group_data, key=lambda g: g.get("name", "").lower())
|
|
244
268
|
|
howler/helper/search.py
CHANGED
|
@@ -5,7 +5,7 @@ from howler.datastore.collection import ESCollection
|
|
|
5
5
|
from howler.odm.models.user import User
|
|
6
6
|
|
|
7
7
|
# List of indices where queries are protected with classification access control
|
|
8
|
-
ACCESS_CONTROLLED_INDICES:
|
|
8
|
+
ACCESS_CONTROLLED_INDICES: set[str] = {"hit"}
|
|
9
9
|
|
|
10
10
|
ADMIN_INDEX_MAP: dict[str, Callable[[], ESCollection]] = {}
|
|
11
11
|
|
howler/odm/models/config.py
CHANGED
|
@@ -176,7 +176,7 @@ class OAuthAutoProperty(BaseModel):
|
|
|
176
176
|
|
|
177
177
|
field: str = Field(description="Field to apply `pattern` to")
|
|
178
178
|
pattern: str = Field(description="Regex pattern for auto-prop assignment")
|
|
179
|
-
type: Literal["access", "classification", "role"] = Field(
|
|
179
|
+
type: Literal["access", "classification", "role", "group", "assignment"] = Field(
|
|
180
180
|
description="Type of property assignment on pattern match",
|
|
181
181
|
)
|
|
182
182
|
value: str = Field(description="Assigned property value")
|
howler/odm/models/hit.py
CHANGED
|
@@ -3,6 +3,7 @@ from typing import Optional
|
|
|
3
3
|
|
|
4
4
|
from howler import odm
|
|
5
5
|
from howler.common.logging import get_logger
|
|
6
|
+
from howler.config import CLASSIFICATION
|
|
6
7
|
from howler.odm.models.assemblyline import AssemblyLine
|
|
7
8
|
from howler.odm.models.aws import AWS
|
|
8
9
|
from howler.odm.models.azure import Azure
|
|
@@ -67,6 +68,12 @@ class Hit(odm.Model):
|
|
|
67
68
|
description="Custom key/value pairs.",
|
|
68
69
|
reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-base.html",
|
|
69
70
|
)
|
|
71
|
+
classification: str = odm.Classification(
|
|
72
|
+
is_user_classification=False,
|
|
73
|
+
copyto="__text__",
|
|
74
|
+
default=CLASSIFICATION.UNRESTRICTED,
|
|
75
|
+
description="Maximum classification for the hit",
|
|
76
|
+
)
|
|
70
77
|
tags: list[str] = odm.List(
|
|
71
78
|
odm.Keyword(),
|
|
72
79
|
default=[],
|
howler/odm/models/user.py
CHANGED
|
@@ -60,6 +60,7 @@ class User(odm.Model):
|
|
|
60
60
|
is_active: bool = odm.Boolean(default=True, description="Is the user active?")
|
|
61
61
|
name: str = odm.Keyword(copyto="__text__", description="Full name of the user")
|
|
62
62
|
password: str = odm.Keyword(index=False, store=False, description="BCrypt hash of the user's password")
|
|
63
|
+
access_control: str = odm.Keyword(index=False, store=False, optional=True, description="Access control filter")
|
|
63
64
|
type: list[str] = odm.List(
|
|
64
65
|
odm.Enum(values=loader.USER_TYPES),
|
|
65
66
|
default=["user", "automation_basic"],
|
howler/odm/random_data.py
CHANGED
|
@@ -45,7 +45,7 @@ from howler.odm.models.user import User
|
|
|
45
45
|
from howler.odm.models.view import View
|
|
46
46
|
from howler.odm.randomizer import get_random_string, get_random_user, get_random_word, random_model_obj
|
|
47
47
|
from howler.security.utils import get_password_hash
|
|
48
|
-
from howler.services import analytic_service
|
|
48
|
+
from howler.services import analytic_service, user_service
|
|
49
49
|
|
|
50
50
|
classification = loader.get_classification()
|
|
51
51
|
|
|
@@ -155,6 +155,7 @@ def create_users(ds):
|
|
|
155
155
|
{
|
|
156
156
|
"name": "Dwight Schrute",
|
|
157
157
|
"email": "user@howler.cyber.gc.ca",
|
|
158
|
+
"classification": classification.RESTRICTED,
|
|
158
159
|
"apikeys": {
|
|
159
160
|
"devkey": {"acl": ["R", "W"], "password": user_hash},
|
|
160
161
|
"impersonate_admin": {
|
|
@@ -174,6 +175,10 @@ def create_users(ds):
|
|
|
174
175
|
}
|
|
175
176
|
)
|
|
176
177
|
|
|
178
|
+
c12n = user_service.get_dynamic_classification(user_data.as_primitives())
|
|
179
|
+
if c12n:
|
|
180
|
+
user_data.classification = c12n
|
|
181
|
+
|
|
177
182
|
user_view = run_modifications("view", user_view)
|
|
178
183
|
user_data = run_modifications("user", user_data)
|
|
179
184
|
|
|
@@ -215,6 +220,7 @@ def create_users(ds):
|
|
|
215
220
|
"password": huey_hash,
|
|
216
221
|
},
|
|
217
222
|
},
|
|
223
|
+
"classification": classification.UNRESTRICTED,
|
|
218
224
|
"password": huey_hash,
|
|
219
225
|
"uname": "huey",
|
|
220
226
|
"favourite_views": [huey_view.view_id],
|
|
@@ -249,6 +255,7 @@ def create_users(ds):
|
|
|
249
255
|
"apikeys": {},
|
|
250
256
|
"type": ["admin", "user"],
|
|
251
257
|
"groups": ["group1", "group2"],
|
|
258
|
+
"classification": classification.UNRESTRICTED,
|
|
252
259
|
"password": get_password_hash(shawnh_pass),
|
|
253
260
|
"uname": "shawn-h",
|
|
254
261
|
"favourite_views": [shawnh_view.view_id],
|
|
@@ -279,6 +286,7 @@ def create_users(ds):
|
|
|
279
286
|
"apikeys": {},
|
|
280
287
|
"type": ["admin", "user"],
|
|
281
288
|
"groups": ["group1", "group2"],
|
|
289
|
+
"classification": classification.RESTRICTED,
|
|
282
290
|
"password": get_password_hash(goose_pass),
|
|
283
291
|
"uname": "goose",
|
|
284
292
|
"favourite_views": [goose_view.view_id],
|
|
@@ -527,6 +535,10 @@ def create_hits(ds: HowlerDatastore, hit_count: int = 200):
|
|
|
527
535
|
for hit_idx in range(hit_count):
|
|
528
536
|
hit = generate_useful_hit(lookups, [user.uname for user in users], prune_hit=False)
|
|
529
537
|
|
|
538
|
+
# Ensure the first 20 hits have unrestricted classification for test access
|
|
539
|
+
if hit_idx < 20:
|
|
540
|
+
hit.classification = classification.UNRESTRICTED
|
|
541
|
+
|
|
530
542
|
if hit_idx + 1 == hit_count:
|
|
531
543
|
hit.howler.analytic = "SecretAnalytic"
|
|
532
544
|
hit.howler.detection = None
|
|
@@ -588,6 +600,7 @@ def create_bundles(ds: HowlerDatastore):
|
|
|
588
600
|
for i in range(3):
|
|
589
601
|
bundle_hit: Hit = generate_useful_hit(lookups, users)
|
|
590
602
|
bundle_hit.howler.is_bundle = True
|
|
603
|
+
bundle_hit.classification = classification.UNRESTRICTED
|
|
591
604
|
|
|
592
605
|
for hit in ds.hit.search("howler.is_bundle:false", rows=randint(3, 10), offset=(i * 2))["items"]:
|
|
593
606
|
if hit.howler.id not in hits:
|
howler/odm/randomizer.py
CHANGED
|
@@ -454,7 +454,7 @@ def random_data_for_field(field: _Field, name: str, minimal: bool = False) -> _A
|
|
|
454
454
|
return random.choice([True, False])
|
|
455
455
|
elif isinstance(field, Classification):
|
|
456
456
|
if field.engine.enforce:
|
|
457
|
-
possible_classifications = list(field.engine.
|
|
457
|
+
possible_classifications = list(field.engine.list_all_classification_combinations(normalized=True))
|
|
458
458
|
possible_classifications.extend([field.engine.UNRESTRICTED, field.engine.RESTRICTED])
|
|
459
459
|
else:
|
|
460
460
|
possible_classifications = [field.engine.UNRESTRICTED]
|
|
@@ -490,11 +490,13 @@ def random_data_for_field(field: _Field, name: str, minimal: bool = False) -> _A
|
|
|
490
490
|
elif isinstance(field, Date):
|
|
491
491
|
return get_random_iso_date()
|
|
492
492
|
elif isinstance(field, Integer):
|
|
493
|
-
return random.randint(
|
|
493
|
+
return random.randint(-2_147_483_648, 2_147_483_647)
|
|
494
494
|
elif isinstance(field, Long):
|
|
495
|
-
|
|
495
|
+
# Generate a random 64-bit signed integer
|
|
496
|
+
return random.randint(-9_223_372_036_854_775_808, 9_223_372_036_854_775_807)
|
|
496
497
|
elif isinstance(field, Float):
|
|
497
|
-
|
|
498
|
+
# Generate a random float in a reasonable range
|
|
499
|
+
return random.uniform(-1e6, 1e6)
|
|
498
500
|
elif isinstance(field, MD5):
|
|
499
501
|
return get_random_hash(32)
|
|
500
502
|
elif isinstance(field, SHA1):
|
howler/services/jwt_service.py
CHANGED
|
@@ -112,8 +112,8 @@ def get_audience(oauth_provider: str) -> str:
|
|
|
112
112
|
elif provider_data.client_id:
|
|
113
113
|
audience = provider_data.client_id
|
|
114
114
|
|
|
115
|
-
if oauth_provider
|
|
116
|
-
raise HowlerValueError("Azure scope must contain the <client_id>/.default claim!")
|
|
115
|
+
if oauth_provider in ["entraid", "azure"] and f"{audience}/.default" not in provider_data.scope:
|
|
116
|
+
raise HowlerValueError("Azure/Entra ID scope must contain the <client_id>/.default claim!")
|
|
117
117
|
|
|
118
118
|
return audience
|
|
119
119
|
|
howler/services/user_service.py
CHANGED
|
@@ -176,7 +176,7 @@ def parse_user_data( # noqa: C901
|
|
|
176
176
|
username = user_data["uname"]
|
|
177
177
|
|
|
178
178
|
# Add add dynamic classification group
|
|
179
|
-
user_data["classification"] = get_dynamic_classification(user_data
|
|
179
|
+
user_data["classification"] = get_dynamic_classification(user_data)
|
|
180
180
|
|
|
181
181
|
# Make sure the user exists in howler and is in sync
|
|
182
182
|
if (not current_user and oauth_provider_config.auto_create) or (
|
|
@@ -193,20 +193,26 @@ def parse_user_data( # noqa: C901
|
|
|
193
193
|
avatar = current_user.pop("avatar", None)
|
|
194
194
|
|
|
195
195
|
# Save updated user if there are changes to sync or it doesn't exist
|
|
196
|
+
log_id: str | None = user_id if not isinstance(user_id, list) else user_id[0]
|
|
196
197
|
if old_user != current_user:
|
|
197
|
-
if
|
|
198
|
-
logger.info("Updating %s with new data",
|
|
198
|
+
if log_id:
|
|
199
|
+
logger.info("Updating %s with new data", log_id)
|
|
200
|
+
current_user["id"] = log_id
|
|
199
201
|
else:
|
|
200
202
|
logger.info("Creating new user %s", username)
|
|
201
203
|
|
|
202
|
-
if user_id:
|
|
203
|
-
current_user["id"] = user_id
|
|
204
|
-
|
|
205
204
|
if avatar:
|
|
206
205
|
current_user["avatar"] = avatar
|
|
207
206
|
|
|
207
|
+
add_access_control(current_user)
|
|
208
|
+
storage.user.save(username, current_user)
|
|
209
|
+
# Ensure access_control is always present, even if user data hasn't changed
|
|
210
|
+
elif "access_control" not in current_user:
|
|
211
|
+
logger.info("Adding access control for user %s", log_id or username)
|
|
212
|
+
add_access_control(current_user)
|
|
208
213
|
storage.user.save(username, current_user)
|
|
209
|
-
|
|
214
|
+
else:
|
|
215
|
+
logger.debug("User is up to date!")
|
|
210
216
|
|
|
211
217
|
if not skip_setup:
|
|
212
218
|
if avatar:
|
|
@@ -336,18 +342,36 @@ def save_user_account(username: str, data: dict[str, Any], user: dict[str, Any])
|
|
|
336
342
|
return storage.user.save(username, data)
|
|
337
343
|
|
|
338
344
|
|
|
339
|
-
def get_dynamic_classification(
|
|
345
|
+
def get_dynamic_classification(user_info: dict[str, Any]) -> str | None:
|
|
340
346
|
"""Get the classification of the user
|
|
341
347
|
|
|
342
348
|
Args:
|
|
343
349
|
current_c12n (str): The current classification of the user
|
|
344
|
-
|
|
350
|
+
user_info (dict): The user definition
|
|
345
351
|
|
|
346
352
|
Returns:
|
|
347
|
-
str: The classification
|
|
353
|
+
str: The normalized classification with dynamic groups applied
|
|
348
354
|
"""
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
355
|
+
new_c12n = CLASSIFICATION.normalize_classification(
|
|
356
|
+
user_info.get("classification", CLASSIFICATION.UNRESTRICTED),
|
|
357
|
+
skip_auto_select=True,
|
|
358
|
+
get_dynamic_groups=False,
|
|
359
|
+
ignore_unused=True,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
if CLASSIFICATION.dynamic_groups:
|
|
363
|
+
email = user_info.get("email", None)
|
|
364
|
+
groups = user_info.get("groups", [])
|
|
365
|
+
|
|
366
|
+
if CLASSIFICATION.dynamic_groups_type in ["email", "all"] and email:
|
|
367
|
+
dyn_group = email.upper().split("@")[1]
|
|
368
|
+
new_c12n = CLASSIFICATION.build_user_classification(
|
|
369
|
+
new_c12n, f"{CLASSIFICATION.UNRESTRICTED}//REL {dyn_group}"
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
if CLASSIFICATION.dynamic_groups_type in ["group", "all"] and groups:
|
|
373
|
+
new_c12n = CLASSIFICATION.build_user_classification(
|
|
374
|
+
new_c12n, f"{CLASSIFICATION.UNRESTRICTED}//REL {', '.join(groups)}"
|
|
375
|
+
)
|
|
352
376
|
|
|
353
|
-
return
|
|
377
|
+
return new_c12n
|
|
@@ -27,14 +27,14 @@ howler/api/v1/overview.py,sha256=hHP9B6mWpBSnOM_ospSusFsruiWX37eTG6ecNtd0-j4,501
|
|
|
27
27
|
howler/api/v1/search.py,sha256=gdKd_Goo2bFWB4nwvLT99dxnSrTRnZN0GHw_bCY54Po,27815
|
|
28
28
|
howler/api/v1/template.py,sha256=q4QSPPM1YGiuvufO14IUKWENTrFRZBpvbXX88Oa0fm8,5785
|
|
29
29
|
howler/api/v1/tool.py,sha256=Y7-iF_dfZxlEUjfOvAK4yY4WQWuXNP56Q-7ie5Nt0oI,6752
|
|
30
|
-
howler/api/v1/user.py,sha256=
|
|
30
|
+
howler/api/v1/user.py,sha256=fROPhzcrW5KXczt7hzQMW3031JA0BxtWVFi2C7vibT0,13816
|
|
31
31
|
howler/api/v1/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
32
|
howler/api/v1/utils/etag.py,sha256=IGB4WUkecHbBt0KKLQFCxCn0mN3T_aSPG0y45l_zT9I,3431
|
|
33
33
|
howler/api/v1/view.py,sha256=gneyowjUIWg2H9QUdIWuLe3J2p2kCWSkNDh0VdpE4DE,8223
|
|
34
34
|
howler/app.py,sha256=h8ZlGjiZEB7w_GhKQz6dXbZ5rnxYxpTJbNXUBbBAtsU,6799
|
|
35
35
|
howler/common/README.md,sha256=8uPpDqh14Ne7Rtn9BwPwE-JpuFkauVsTr2s0s4YG5Bg,5637
|
|
36
36
|
howler/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
37
|
-
howler/common/classification.py,sha256=
|
|
37
|
+
howler/common/classification.py,sha256=oOpyfZh4SudrCcLjBpJIOi3Ye_iy5Kc1Nu27KScp9og,43057
|
|
38
38
|
howler/common/classification.yml,sha256=3VBqzKYkiqz8bCsSuqNvqxeqUPN1NmXBKHusGFO3hbg,4092
|
|
39
39
|
howler/common/exceptions.py,sha256=An8MW-qB8g7EYLTxddnoIiyxNDQAqU9AmH8MrDFfnwQ,5804
|
|
40
40
|
howler/common/loader.py,sha256=VgoNwf8G95t4MIe9TBYhnSDmU1XZlxsmeNEP1wFcFrg,5455
|
|
@@ -45,7 +45,7 @@ howler/common/net.py,sha256=H4KrP-aUeYVxjLYmC3yNTSriQEmqC__Fzho7p86rAlI,1795
|
|
|
45
45
|
howler/common/net_static.py,sha256=q5ccJRFft4flMwhtQSN14M8Fn_GWxQ6yLK4mzSqCW6g,20589
|
|
46
46
|
howler/common/random_user.py,sha256=0NEJzLBz08dcSCk7-M6HQpC89qc4QUTKa_zC84hfds4,4920
|
|
47
47
|
howler/common/swagger.py,sha256=XqFVxgCQg_HJrLdl6b37alwyanOWmcAU1bbVi6eDQrA,3752
|
|
48
|
-
howler/config.py,sha256=
|
|
48
|
+
howler/config.py,sha256=aMWL40YnN29moAIjm3NXlPHPQrIeagRd68sjuuDwK2I,2170
|
|
49
49
|
howler/cronjobs/__init__.py,sha256=GEhNsxPGATumlroMa-g6z5Dt9yy0QT93s3uuNC-GwIM,909
|
|
50
50
|
howler/cronjobs/retention.py,sha256=qpUT7A2UoIJIyHZ_zpcgOe1o3do6xLRsV-uUxEJ5m94,1778
|
|
51
51
|
howler/cronjobs/rules.py,sha256=-yAjGTyYI7jeQ9wigjoupRMq3b6DakU0EW0XCS8xowQ,10511
|
|
@@ -76,11 +76,11 @@ howler/external/wipe_databases.py,sha256=CO_mUdezp24h6xz6Di_K5-Mid61d9EVcBO4ap5q
|
|
|
76
76
|
howler/gunicorn_config.py,sha256=0X7DPFcVDu3qAgMbNsdYGqCuvQGn5ZNx_eapOMyuXuI,750
|
|
77
77
|
howler/healthz.py,sha256=nvb8MBBERYIkA_UxxLIyNEQazYOnPCcm0sH0Jm5nF0k,769
|
|
78
78
|
howler/helper/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
79
|
-
howler/helper/azure.py,sha256=
|
|
79
|
+
howler/helper/azure.py,sha256=evI413uz-PdI6C5HIHh8LPRx83mTO8y6nad5ed_qJ5Y,1830
|
|
80
80
|
howler/helper/discover.py,sha256=j1nPHOPZ9CwvcfLT2xd-uvvpEex-nhPscl9aZwutwDk,2221
|
|
81
81
|
howler/helper/hit.py,sha256=Us2B4Dngu_ghWCZ40o7mZ0iLKKDr2onV6oB2OLuE5Fc,7863
|
|
82
|
-
howler/helper/oauth.py,sha256=
|
|
83
|
-
howler/helper/search.py,sha256=
|
|
82
|
+
howler/helper/oauth.py,sha256=08yApXF9l-mCVFUodmcZDDj6Y0B4tFge10OrDYqnQmg,10725
|
|
83
|
+
howler/helper/search.py,sha256=XQ_6ScHsmzfqXnQ3Rt-j9ADmddaSVsAcG6348L-WtPY,2882
|
|
84
84
|
howler/helper/workflow.py,sha256=ATvEv5CI7Bm2bTEy0U0EPEdz6-fZfyqIkc-gZCYmRY8,4803
|
|
85
85
|
howler/helper/ws.py,sha256=im7pFQJDmGs3EyaBviGD1csYmTTRwLx8Gfih6-MlPhs,15911
|
|
86
86
|
howler/odm/README.md,sha256=Ihc_DyjVQlLaIOEbPoQNPkum9Ecn8kn37-PMFQsX77s,5645
|
|
@@ -97,7 +97,7 @@ howler/odm/models/aws.py,sha256=pJVadJqubdgT27riCfp7bEKVP4XsMZB0ZUnKAbmCMd0,895
|
|
|
97
97
|
howler/odm/models/azure.py,sha256=o7MZMMo9jh1SB8xXCajl_YSKP2nnnWsjx_DPT6LnQKg,710
|
|
98
98
|
howler/odm/models/cbs.py,sha256=onUiJOGUxK3iy_-4XkGGwHxFiFq9Td_p59Kum4XaR-w,1366
|
|
99
99
|
howler/odm/models/clue.py,sha256=TNJV1t9bEhRTcJuSC2qNKQ-XpwFqb5iQstRpvHOrTHE,636
|
|
100
|
-
howler/odm/models/config.py,sha256=
|
|
100
|
+
howler/odm/models/config.py,sha256=cEFdR8Tzl0VUwRTj-zJ8ARBsRAcnHarQcAiggmpoz0E,22604
|
|
101
101
|
howler/odm/models/dossier.py,sha256=Ob2qROrG2-DYzmVo2XVe4NJ8HjWGCoRAu2gPo6p9XGU,1244
|
|
102
102
|
howler/odm/models/ecs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
103
103
|
howler/odm/models/ecs/agent.py,sha256=idSooyFCLuQAB7_RyEWTYW4-x9w5a3wpy2ct_-EDRQs,713
|
|
@@ -138,17 +138,17 @@ howler/odm/models/ecs/user.py,sha256=A1KoIZk5LLPR-boE3O9LEbMRDBbDcrXhdXjlFF1exYU
|
|
|
138
138
|
howler/odm/models/ecs/user_agent.py,sha256=LM4ZjbdEkkF02zZu6Pg7j8JLuAyfliTC6PVAEw9W2S4,845
|
|
139
139
|
howler/odm/models/ecs/vulnerability.py,sha256=iX2TUmCZTJjwyMcOmxiHg-pfoEwX8Wx-h8cxo5QsJmo,1553
|
|
140
140
|
howler/odm/models/gcp.py,sha256=FLwaQVcfFeE5AbIhEB_Ge2UL-HjPdldY04Nrm9Mq7kk,675
|
|
141
|
-
howler/odm/models/hit.py,sha256=
|
|
141
|
+
howler/odm/models/hit.py,sha256=Gxr_afLleKzK0dM0VfgtftYTU5zo-egEggQgk0V_tX4,13657
|
|
142
142
|
howler/odm/models/howler_data.py,sha256=7anqSVFyvecqTlmvg1_ULGgJmWii4AanTanbbqtMUqo,12515
|
|
143
143
|
howler/odm/models/lead.py,sha256=lqapGWZ4u22Asib48o7wAzramnFY9EkRybmb4olsyrA,904
|
|
144
144
|
howler/odm/models/localized_label.py,sha256=G7gfQ1cngiI4KprqldHWE1KHkAgK4AG_JsfHxVRdsRs,361
|
|
145
145
|
howler/odm/models/overview.py,sha256=kvZcMYPDlkJEGa0L1jq9pG0RFjLOVudC64-2GTWVu2w,684
|
|
146
146
|
howler/odm/models/pivot.py,sha256=ZewcGh91xbE64snZ5Ahz2So1onAqO8U9H4CFe6jBOXs,1405
|
|
147
147
|
howler/odm/models/template.py,sha256=-Tqq_36qD_3nQ4jv13OPeH_EyyERKhc55wT03CU-Kbk,979
|
|
148
|
-
howler/odm/models/user.py,sha256=
|
|
148
|
+
howler/odm/models/user.py,sha256=T1VTbUfRHU4g48iccW3TS7lWSgGXVfd1CuSx-b3TDjE,3388
|
|
149
149
|
howler/odm/models/view.py,sha256=DYFrYcx4NT-Z_0GUg_5qjRRLwrMMFK78NQ-20iuFx8o,1323
|
|
150
|
-
howler/odm/random_data.py,sha256=
|
|
151
|
-
howler/odm/randomizer.py,sha256=
|
|
150
|
+
howler/odm/random_data.py,sha256=96HRJu1O47hDvZ3pA55UPN6ckPh458oxrxQrmupmJng,29490
|
|
151
|
+
howler/odm/randomizer.py,sha256=2gBwQA6karZQhX82cz0oBhgCXvmVlFXhdKXGdjoAjLM,23870
|
|
152
152
|
howler/patched.py,sha256=Br4BGU5raaqjSMDLD7ogb5A8Yn0dzecouh6uWVV2jlQ,77
|
|
153
153
|
howler/plugins/__init__.py,sha256=P5P-t4KgIInOzp4NmIturNIhUbb3jPO81n55Q_b_gM0,841
|
|
154
154
|
howler/plugins/config.py,sha256=75sGAPQPU9_dGkJJ8_Q1H1qIYEhu9iv8c0_PDgAJDAs,4802
|
|
@@ -177,12 +177,12 @@ howler/services/config_service.py,sha256=tmSXHKMqJHT3ZoCb250Ad5l32Gh3MiWApIRSI8b
|
|
|
177
177
|
howler/services/dossier_service.py,sha256=nn4lGJ8caJVBYQyp_sViVmMm6m0MetY0Wml5ITaZhb4,10559
|
|
178
178
|
howler/services/event_service.py,sha256=4Mf2n02Rj2cfirwj_dIWkFGXzw_nrW8kfveAT-Kwx0M,2985
|
|
179
179
|
howler/services/hit_service.py,sha256=jeqWfDvzNIvR0SxhZeCfdeVvA3zd0pBI74YE8hTgu98,34716
|
|
180
|
-
howler/services/jwt_service.py,sha256=
|
|
180
|
+
howler/services/jwt_service.py,sha256=2ULBu1GqosowIgtMpqgn0imRsOGfwFOioH-uVOmXZpw,5601
|
|
181
181
|
howler/services/lucene_service.py,sha256=PRDM6cEnIA9qb1elpsiD2EVIXRo0rg6q0ki1QTu9UQA,12752
|
|
182
182
|
howler/services/notebook_service.py,sha256=_MWllCnuVxt7lCcvWghXnaS926FbvBRE3DrBt--zO7U,3968
|
|
183
183
|
howler/services/overview_service.py,sha256=-Jk6m1jRA0RQ9QuBxaguQbI0baH1Rz6N6H6R8E4jzJ8,1704
|
|
184
184
|
howler/services/template_service.py,sha256=ts2vF6uimyuQbsocPSu-7wrI0-Av3ZjDAYWb53ERW-A,2051
|
|
185
|
-
howler/services/user_service.py,sha256=
|
|
185
|
+
howler/services/user_service.py,sha256=ic_vnw5bXycSQmO1ohUifqvwDAJ6AKcNqBLP8hFPY54,13758
|
|
186
186
|
howler/telemetry.py,sha256=bXjXhZXwiwcGDJAD2ZNN_Zh6aZd19cQqtmn3G47Ckac,2584
|
|
187
187
|
howler/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
188
188
|
howler/utils/annotations.py,sha256=GLuDbjbXp8esDji3qhQY_uQyOWVqfIdpF_zs2t0IaMI,878
|
|
@@ -196,7 +196,7 @@ howler/utils/path.py,sha256=DfOU4i4zSs4wchHoE8iE7aWVLkTxiC_JRGepF2hBYBk,690
|
|
|
196
196
|
howler/utils/socket_utils.py,sha256=nz1SklC9xBHUSfHyTJjpq3mbozX1GDf01WzdGxfaUII,2212
|
|
197
197
|
howler/utils/str_utils.py,sha256=HE8Hqh2HlOLaj16w0H9zKOyDJLp-f1LQ50y_WeGZaEk,8389
|
|
198
198
|
howler/utils/uid.py,sha256=p9dsqyvZ-lpiAuzZWCPCeEM99kdk0Ly9czf04HNdSuw,1341
|
|
199
|
-
howler_api-3.3.0.
|
|
200
|
-
howler_api-3.3.0.
|
|
201
|
-
howler_api-3.3.0.
|
|
202
|
-
howler_api-3.3.0.
|
|
199
|
+
howler_api-3.3.0.dev768.dist-info/METADATA,sha256=0ShN3xopxsQbeWQ2LoELT33OvpjicY67jiTl-jpDGJ0,2879
|
|
200
|
+
howler_api-3.3.0.dev768.dist-info/WHEEL,sha256=Vz2fHgx6HFtSwhs8KvkHLqH5Ea4w1_rner5uNVGCeIE,88
|
|
201
|
+
howler_api-3.3.0.dev768.dist-info/entry_points.txt,sha256=Lu9SBGvwe0wczJHmc-RudC24lmQk7tv3ZBXon9RIihg,259
|
|
202
|
+
howler_api-3.3.0.dev768.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|