howler-api 3.0.0.dev374__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.
Potentially problematic release.
This version of howler-api might be problematic. Click here for more details.
- howler/__init__.py +0 -0
- howler/actions/__init__.py +168 -0
- howler/actions/add_label.py +111 -0
- howler/actions/add_to_bundle.py +159 -0
- howler/actions/change_field.py +76 -0
- howler/actions/demote.py +160 -0
- howler/actions/example_plugin.py +104 -0
- howler/actions/prioritization.py +93 -0
- howler/actions/promote.py +147 -0
- howler/actions/remove_from_bundle.py +133 -0
- howler/actions/remove_label.py +111 -0
- howler/actions/transition.py +200 -0
- howler/api/__init__.py +249 -0
- howler/api/base.py +88 -0
- howler/api/socket.py +114 -0
- howler/api/v1/__init__.py +97 -0
- howler/api/v1/action.py +372 -0
- howler/api/v1/analytic.py +748 -0
- howler/api/v1/auth.py +382 -0
- howler/api/v1/clue.py +99 -0
- howler/api/v1/configs.py +58 -0
- howler/api/v1/dossier.py +222 -0
- howler/api/v1/help.py +28 -0
- howler/api/v1/hit.py +1181 -0
- howler/api/v1/notebook.py +82 -0
- howler/api/v1/overview.py +191 -0
- howler/api/v1/search.py +788 -0
- howler/api/v1/template.py +206 -0
- howler/api/v1/tool.py +183 -0
- howler/api/v1/user.py +416 -0
- howler/api/v1/utils/__init__.py +0 -0
- howler/api/v1/utils/etag.py +84 -0
- howler/api/v1/view.py +288 -0
- howler/app.py +235 -0
- howler/common/README.md +125 -0
- howler/common/__init__.py +0 -0
- howler/common/classification.py +979 -0
- howler/common/classification.yml +107 -0
- howler/common/exceptions.py +167 -0
- howler/common/loader.py +154 -0
- howler/common/logging/__init__.py +241 -0
- howler/common/logging/audit.py +138 -0
- howler/common/logging/format.py +38 -0
- howler/common/net.py +79 -0
- howler/common/net_static.py +1494 -0
- howler/common/random_user.py +316 -0
- howler/common/swagger.py +117 -0
- howler/config.py +64 -0
- howler/cronjobs/__init__.py +29 -0
- howler/cronjobs/retention.py +61 -0
- howler/cronjobs/rules.py +274 -0
- howler/cronjobs/view_cleanup.py +88 -0
- howler/datastore/README.md +112 -0
- howler/datastore/__init__.py +0 -0
- howler/datastore/bulk.py +72 -0
- howler/datastore/collection.py +2342 -0
- howler/datastore/constants.py +119 -0
- howler/datastore/exceptions.py +41 -0
- howler/datastore/howler_store.py +105 -0
- howler/datastore/migrations/fix_process.py +41 -0
- howler/datastore/operations.py +130 -0
- howler/datastore/schemas.py +90 -0
- howler/datastore/store.py +231 -0
- howler/datastore/support/__init__.py +0 -0
- howler/datastore/support/build.py +215 -0
- howler/datastore/support/schemas.py +90 -0
- howler/datastore/types.py +22 -0
- howler/error.py +91 -0
- howler/external/__init__.py +0 -0
- howler/external/generate_mitre.py +96 -0
- howler/external/generate_sigma_rules.py +31 -0
- howler/external/generate_tlds.py +47 -0
- howler/external/reindex_data.py +66 -0
- howler/external/wipe_databases.py +58 -0
- howler/gunicorn_config.py +25 -0
- howler/healthz.py +47 -0
- howler/helper/__init__.py +0 -0
- howler/helper/azure.py +50 -0
- howler/helper/discover.py +59 -0
- howler/helper/hit.py +236 -0
- howler/helper/oauth.py +247 -0
- howler/helper/search.py +92 -0
- howler/helper/workflow.py +110 -0
- howler/helper/ws.py +378 -0
- howler/odm/README.md +102 -0
- howler/odm/__init__.py +1 -0
- howler/odm/base.py +1543 -0
- howler/odm/charter.txt +146 -0
- howler/odm/helper.py +416 -0
- howler/odm/howler_enum.py +25 -0
- howler/odm/models/__init__.py +0 -0
- howler/odm/models/action.py +33 -0
- howler/odm/models/analytic.py +90 -0
- howler/odm/models/assemblyline.py +48 -0
- howler/odm/models/aws.py +23 -0
- howler/odm/models/azure.py +16 -0
- howler/odm/models/cbs.py +44 -0
- howler/odm/models/config.py +558 -0
- howler/odm/models/dossier.py +33 -0
- howler/odm/models/ecs/__init__.py +0 -0
- howler/odm/models/ecs/agent.py +17 -0
- howler/odm/models/ecs/autonomous_system.py +16 -0
- howler/odm/models/ecs/client.py +149 -0
- howler/odm/models/ecs/cloud.py +141 -0
- howler/odm/models/ecs/code_signature.py +27 -0
- howler/odm/models/ecs/container.py +32 -0
- howler/odm/models/ecs/dns.py +62 -0
- howler/odm/models/ecs/egress.py +10 -0
- howler/odm/models/ecs/elf.py +74 -0
- howler/odm/models/ecs/email.py +122 -0
- howler/odm/models/ecs/error.py +14 -0
- howler/odm/models/ecs/event.py +140 -0
- howler/odm/models/ecs/faas.py +24 -0
- howler/odm/models/ecs/file.py +84 -0
- howler/odm/models/ecs/geo.py +30 -0
- howler/odm/models/ecs/group.py +18 -0
- howler/odm/models/ecs/hash.py +16 -0
- howler/odm/models/ecs/host.py +17 -0
- howler/odm/models/ecs/http.py +37 -0
- howler/odm/models/ecs/ingress.py +12 -0
- howler/odm/models/ecs/interface.py +21 -0
- howler/odm/models/ecs/network.py +30 -0
- howler/odm/models/ecs/observer.py +45 -0
- howler/odm/models/ecs/organization.py +12 -0
- howler/odm/models/ecs/os.py +21 -0
- howler/odm/models/ecs/pe.py +17 -0
- howler/odm/models/ecs/process.py +216 -0
- howler/odm/models/ecs/registry.py +26 -0
- howler/odm/models/ecs/related.py +45 -0
- howler/odm/models/ecs/rule.py +51 -0
- howler/odm/models/ecs/server.py +24 -0
- howler/odm/models/ecs/threat.py +247 -0
- howler/odm/models/ecs/tls.py +58 -0
- howler/odm/models/ecs/url.py +51 -0
- howler/odm/models/ecs/user.py +57 -0
- howler/odm/models/ecs/user_agent.py +20 -0
- howler/odm/models/ecs/vulnerability.py +41 -0
- howler/odm/models/gcp.py +16 -0
- howler/odm/models/hit.py +356 -0
- howler/odm/models/howler_data.py +328 -0
- howler/odm/models/lead.py +24 -0
- howler/odm/models/localized_label.py +13 -0
- howler/odm/models/overview.py +16 -0
- howler/odm/models/pivot.py +40 -0
- howler/odm/models/template.py +24 -0
- howler/odm/models/user.py +83 -0
- howler/odm/models/view.py +34 -0
- howler/odm/random_data.py +888 -0
- howler/odm/randomizer.py +609 -0
- howler/patched.py +5 -0
- howler/plugins/__init__.py +25 -0
- howler/plugins/config.py +123 -0
- howler/remote/__init__.py +0 -0
- howler/remote/datatypes/README.md +355 -0
- howler/remote/datatypes/__init__.py +98 -0
- howler/remote/datatypes/counters.py +63 -0
- howler/remote/datatypes/events.py +66 -0
- howler/remote/datatypes/hash.py +206 -0
- howler/remote/datatypes/lock.py +42 -0
- howler/remote/datatypes/queues/__init__.py +0 -0
- howler/remote/datatypes/queues/comms.py +59 -0
- howler/remote/datatypes/queues/multi.py +32 -0
- howler/remote/datatypes/queues/named.py +93 -0
- howler/remote/datatypes/queues/priority.py +215 -0
- howler/remote/datatypes/set.py +118 -0
- howler/remote/datatypes/user_quota_tracker.py +54 -0
- howler/security/__init__.py +253 -0
- howler/security/socket.py +108 -0
- howler/security/utils.py +185 -0
- howler/services/__init__.py +0 -0
- howler/services/action_service.py +111 -0
- howler/services/analytic_service.py +128 -0
- howler/services/auth_service.py +323 -0
- howler/services/config_service.py +128 -0
- howler/services/dossier_service.py +252 -0
- howler/services/event_service.py +93 -0
- howler/services/hit_service.py +893 -0
- howler/services/jwt_service.py +158 -0
- howler/services/lucene_service.py +286 -0
- howler/services/notebook_service.py +119 -0
- howler/services/overview_service.py +44 -0
- howler/services/template_service.py +45 -0
- howler/services/user_service.py +331 -0
- howler/utils/__init__.py +0 -0
- howler/utils/annotations.py +28 -0
- howler/utils/chunk.py +38 -0
- howler/utils/dict_utils.py +200 -0
- howler/utils/isotime.py +17 -0
- howler/utils/list_utils.py +11 -0
- howler/utils/lucene.py +77 -0
- howler/utils/path.py +27 -0
- howler/utils/socket_utils.py +61 -0
- howler/utils/str_utils.py +256 -0
- howler/utils/uid.py +47 -0
- howler_api-3.0.0.dev374.dist-info/METADATA +71 -0
- howler_api-3.0.0.dev374.dist-info/RECORD +198 -0
- howler_api-3.0.0.dev374.dist-info/WHEEL +4 -0
- howler_api-3.0.0.dev374.dist-info/entry_points.txt +8 -0
|
@@ -0,0 +1,979 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
import logging
|
|
3
|
+
from copy import copy
|
|
4
|
+
from typing import Any, Dict, KeysView, List, Optional, Set, Tuple, Union, cast
|
|
5
|
+
|
|
6
|
+
from howler.common.exceptions import (
|
|
7
|
+
HowlerKeyError,
|
|
8
|
+
InvalidClassification,
|
|
9
|
+
InvalidDefinition,
|
|
10
|
+
)
|
|
11
|
+
from howler.common.loader import APP_NAME
|
|
12
|
+
|
|
13
|
+
log = logging.getLogger(f"{APP_NAME}.classification")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Classification(object):
|
|
17
|
+
MIN_LVL = 1
|
|
18
|
+
MAX_LVL = 10000
|
|
19
|
+
NULL_LVL = 0
|
|
20
|
+
INVALID_LVL = 10001
|
|
21
|
+
NULL_CLASSIFICATION = "NULL"
|
|
22
|
+
INVALID_CLASSIFICATION = "INVALID"
|
|
23
|
+
|
|
24
|
+
def __init__(self, classification_definition: Dict):
|
|
25
|
+
"""Returns the classification class instantiated with the classification_definition
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
classification_definition: The classification definition dictionary,
|
|
29
|
+
see default classification.yml for an example.
|
|
30
|
+
"""
|
|
31
|
+
banned_params_keys = [
|
|
32
|
+
"name",
|
|
33
|
+
"short_name",
|
|
34
|
+
"lvl",
|
|
35
|
+
"aliases",
|
|
36
|
+
"auto_select",
|
|
37
|
+
"css",
|
|
38
|
+
"description",
|
|
39
|
+
]
|
|
40
|
+
self.original_definition = classification_definition
|
|
41
|
+
self.levels_map: dict[str, str] = {}
|
|
42
|
+
self.levels_map_stl = {}
|
|
43
|
+
self.levels_map_lts = {}
|
|
44
|
+
self.levels_styles_map = {}
|
|
45
|
+
self.levels_aliases = {}
|
|
46
|
+
self.access_req_map_lts = {}
|
|
47
|
+
self.access_req_map_stl = {}
|
|
48
|
+
self.access_req_aliases: dict[str, Any] = {}
|
|
49
|
+
self.groups_map_lts = {}
|
|
50
|
+
self.groups_map_stl = {}
|
|
51
|
+
self.groups_aliases: dict[str, Any] = {}
|
|
52
|
+
self.groups_auto_select = []
|
|
53
|
+
self.groups_auto_select_short = []
|
|
54
|
+
self.subgroups_map_lts = {}
|
|
55
|
+
self.subgroups_map_stl = {}
|
|
56
|
+
self.subgroups_aliases: dict[str, Any] = {}
|
|
57
|
+
self.subgroups_auto_select = []
|
|
58
|
+
self.subgroups_auto_select_short = []
|
|
59
|
+
self.params_map = {}
|
|
60
|
+
self.description = {}
|
|
61
|
+
self.invalid_mode = False
|
|
62
|
+
self._classification_cache = set()
|
|
63
|
+
self._classification_cache_short = set()
|
|
64
|
+
|
|
65
|
+
self.enforce = False
|
|
66
|
+
self.dynamic_groups = False
|
|
67
|
+
|
|
68
|
+
# Add Invalid classification
|
|
69
|
+
self.levels_map["INV"] = self.INVALID_LVL
|
|
70
|
+
self.levels_map[str(self.INVALID_LVL)] = "INV"
|
|
71
|
+
self.levels_map_stl["INV"] = self.INVALID_CLASSIFICATION
|
|
72
|
+
self.levels_map_lts[self.INVALID_CLASSIFICATION] = "INV"
|
|
73
|
+
|
|
74
|
+
# Add null classification
|
|
75
|
+
self.levels_map[self.NULL_CLASSIFICATION] = self.NULL_LVL
|
|
76
|
+
self.levels_map[str(self.NULL_LVL)] = self.NULL_CLASSIFICATION
|
|
77
|
+
self.levels_map_stl[self.NULL_CLASSIFICATION] = self.NULL_CLASSIFICATION
|
|
78
|
+
self.levels_map_lts[self.NULL_CLASSIFICATION] = self.NULL_CLASSIFICATION
|
|
79
|
+
|
|
80
|
+
log.debug("Beginning classification parsing")
|
|
81
|
+
try:
|
|
82
|
+
self.enforce = classification_definition.get("enforce", None)
|
|
83
|
+
if self.enforce is None:
|
|
84
|
+
raise HowlerKeyError("Enforce not set!")
|
|
85
|
+
|
|
86
|
+
self.dynamic_groups = classification_definition.get("dynamic_groups", None)
|
|
87
|
+
if self.enforce is None:
|
|
88
|
+
raise HowlerKeyError("Dynamic groups not set!")
|
|
89
|
+
|
|
90
|
+
if self.enforce:
|
|
91
|
+
self._classification_cache = self.list_all_classification_combinations()
|
|
92
|
+
self._classification_cache_short = self.list_all_classification_combinations(long_format=False)
|
|
93
|
+
|
|
94
|
+
if classification_definition.get("levels", None) is None:
|
|
95
|
+
raise HowlerKeyError("No classification levels provided!")
|
|
96
|
+
|
|
97
|
+
for x in classification_definition["levels"]:
|
|
98
|
+
short_name = x["short_name"]
|
|
99
|
+
name = x["name"]
|
|
100
|
+
|
|
101
|
+
if short_name in ["INV", "NULL"] or name in [
|
|
102
|
+
self.INVALID_CLASSIFICATION,
|
|
103
|
+
self.NULL_CLASSIFICATION,
|
|
104
|
+
]:
|
|
105
|
+
raise InvalidDefinition(
|
|
106
|
+
"You cannot use reserved words NULL, INVALID or INV in your " "classification definition."
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
lvl = int(x["lvl"])
|
|
110
|
+
if lvl > self.MAX_LVL:
|
|
111
|
+
raise InvalidDefinition("Level over maximum classification level of %s." % self.MAX_LVL)
|
|
112
|
+
if lvl < self.MIN_LVL:
|
|
113
|
+
raise InvalidDefinition("Level under minimum classification level of %s." % self.MIN_LVL)
|
|
114
|
+
|
|
115
|
+
self.levels_map[short_name] = lvl
|
|
116
|
+
self.levels_map[str(lvl)] = short_name
|
|
117
|
+
self.levels_map_stl[short_name] = name
|
|
118
|
+
self.levels_map_lts[name] = short_name
|
|
119
|
+
for a in x.get("aliases", []):
|
|
120
|
+
self.levels_aliases[a] = short_name
|
|
121
|
+
self.params_map[short_name] = {k: v for k, v in x.items() if k not in banned_params_keys}
|
|
122
|
+
self.params_map[name] = self.params_map[short_name]
|
|
123
|
+
self.levels_styles_map[short_name] = x.get("css", {"color": "default"})
|
|
124
|
+
self.levels_styles_map[name] = self.levels_styles_map[short_name]
|
|
125
|
+
self.description[short_name] = x.get("description", "N/A")
|
|
126
|
+
self.description[name] = self.description[short_name]
|
|
127
|
+
|
|
128
|
+
if classification_definition.get("required", None) is None:
|
|
129
|
+
log.warning("No required tokens specified in classification definition!")
|
|
130
|
+
|
|
131
|
+
for x in classification_definition.get("required", []):
|
|
132
|
+
short_name = x["short_name"]
|
|
133
|
+
name = x["name"]
|
|
134
|
+
self.access_req_map_lts[name] = short_name
|
|
135
|
+
self.access_req_map_stl[short_name] = name
|
|
136
|
+
for a in x.get("aliases", []):
|
|
137
|
+
self.access_req_aliases[a] = self.access_req_aliases.get(a, []) + [short_name]
|
|
138
|
+
self.params_map[short_name] = {k: v for k, v in x.items() if k not in banned_params_keys}
|
|
139
|
+
self.params_map[name] = self.params_map[short_name]
|
|
140
|
+
self.description[short_name] = x.get("description", "N/A")
|
|
141
|
+
self.description[name] = self.description[short_name]
|
|
142
|
+
|
|
143
|
+
if classification_definition.get("groups", None) is None:
|
|
144
|
+
log.debug("No groups specified in classification definition!")
|
|
145
|
+
|
|
146
|
+
for x in classification_definition.get("groups", []):
|
|
147
|
+
short_name = x["short_name"]
|
|
148
|
+
name = x["name"]
|
|
149
|
+
self.groups_map_lts[name] = short_name
|
|
150
|
+
self.groups_map_stl[short_name] = name
|
|
151
|
+
for a in x.get("aliases", []):
|
|
152
|
+
self.groups_aliases[a] = list(set(self.groups_aliases.get(a, []) + [short_name]))
|
|
153
|
+
solitary_display_name = x.get("solitary_display_name", None)
|
|
154
|
+
if solitary_display_name:
|
|
155
|
+
self.groups_aliases[solitary_display_name] = list(
|
|
156
|
+
set(self.groups_aliases.get(solitary_display_name, []) + [short_name])
|
|
157
|
+
)
|
|
158
|
+
if x.get("auto_select", False):
|
|
159
|
+
self.groups_auto_select.append(name)
|
|
160
|
+
self.groups_auto_select_short.append(short_name)
|
|
161
|
+
self.params_map[short_name] = {k: v for k, v in x.items() if k not in banned_params_keys}
|
|
162
|
+
self.params_map[name] = self.params_map[short_name]
|
|
163
|
+
self.description[short_name] = x.get("description", "N/A")
|
|
164
|
+
self.description[name] = self.description[short_name]
|
|
165
|
+
|
|
166
|
+
if classification_definition.get("subgroups", None) is None:
|
|
167
|
+
log.debug("No subgroups specified in classification definition!")
|
|
168
|
+
|
|
169
|
+
for x in classification_definition.get("subgroups", []):
|
|
170
|
+
short_name = x["short_name"]
|
|
171
|
+
name = x["name"]
|
|
172
|
+
self.subgroups_map_lts[name] = short_name
|
|
173
|
+
self.subgroups_map_stl[short_name] = name
|
|
174
|
+
for a in x.get("aliases", []):
|
|
175
|
+
self.subgroups_aliases[a] = list(set(self.subgroups_aliases.get(a, []) + [short_name]))
|
|
176
|
+
solitary_display_name = x.get("solitary_display_name", None)
|
|
177
|
+
if solitary_display_name:
|
|
178
|
+
self.subgroups_aliases[solitary_display_name] = list(
|
|
179
|
+
set(self.subgroups_aliases.get(solitary_display_name, []) + [short_name])
|
|
180
|
+
)
|
|
181
|
+
if x.get("auto_select", False):
|
|
182
|
+
self.subgroups_auto_select.append(name)
|
|
183
|
+
self.subgroups_auto_select_short.append(short_name)
|
|
184
|
+
self.params_map[short_name] = {k: v for k, v in x.items() if k not in banned_params_keys}
|
|
185
|
+
self.params_map[name] = self.params_map[short_name]
|
|
186
|
+
self.description[short_name] = x.get("description", "N/A")
|
|
187
|
+
self.description[name] = self.description[short_name]
|
|
188
|
+
|
|
189
|
+
if not self.is_valid(classification_definition["unrestricted"]):
|
|
190
|
+
raise InvalidDefinition("Classification definition's unrestricted classification is invalid.")
|
|
191
|
+
|
|
192
|
+
if not self.is_valid(classification_definition["restricted"]):
|
|
193
|
+
raise InvalidDefinition("Classification definition's restricted classification is invalid.")
|
|
194
|
+
|
|
195
|
+
self.UNRESTRICTED = classification_definition["unrestricted"]
|
|
196
|
+
self.RESTRICTED = classification_definition["restricted"]
|
|
197
|
+
|
|
198
|
+
self.UNRESTRICTED = self.normalize_classification(classification_definition["unrestricted"])
|
|
199
|
+
self.RESTRICTED = self.normalize_classification(classification_definition["restricted"])
|
|
200
|
+
|
|
201
|
+
except Exception as e:
|
|
202
|
+
self.UNRESTRICTED = self.NULL_CLASSIFICATION
|
|
203
|
+
self.RESTRICTED = self.INVALID_CLASSIFICATION
|
|
204
|
+
|
|
205
|
+
self.invalid_mode = True
|
|
206
|
+
|
|
207
|
+
log.warning(
|
|
208
|
+
"Invalid classification: %s. Setting classification mode to invalid",
|
|
209
|
+
str(e),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
############################
|
|
213
|
+
# Private functions
|
|
214
|
+
############################
|
|
215
|
+
@staticmethod
|
|
216
|
+
def _build_combinations(items: Set, separator: str = "/", solitary_display: Optional[Dict] = None) -> Set:
|
|
217
|
+
if solitary_display is None:
|
|
218
|
+
solitary_display = {}
|
|
219
|
+
|
|
220
|
+
out = {""}
|
|
221
|
+
for i in items:
|
|
222
|
+
others = [x for x in items if x != i]
|
|
223
|
+
for x in range(len(others) + 1):
|
|
224
|
+
for c in itertools.combinations(others, x):
|
|
225
|
+
value = separator.join(sorted([i] + list(c)))
|
|
226
|
+
out.add(solitary_display.get(value, value))
|
|
227
|
+
|
|
228
|
+
return out
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def _list_items_and_aliases(data: List, long_format: bool = True) -> Set:
|
|
232
|
+
items = set()
|
|
233
|
+
for item in data:
|
|
234
|
+
if long_format:
|
|
235
|
+
items.add(item["name"])
|
|
236
|
+
else:
|
|
237
|
+
items.add(item["short_name"])
|
|
238
|
+
|
|
239
|
+
return items
|
|
240
|
+
|
|
241
|
+
def _get_c12n_level_index(self, c12n: str) -> str:
|
|
242
|
+
# Parse classifications in uppercase mode only
|
|
243
|
+
c12n = c12n.upper()
|
|
244
|
+
|
|
245
|
+
lvl = c12n.split("//")[0]
|
|
246
|
+
if lvl in self.levels_map:
|
|
247
|
+
return self.levels_map[lvl]
|
|
248
|
+
elif lvl in self.levels_map_lts:
|
|
249
|
+
return self.levels_map[self.levels_map_lts[lvl]]
|
|
250
|
+
elif lvl in self.levels_aliases:
|
|
251
|
+
return self.levels_map[self.levels_aliases[lvl]]
|
|
252
|
+
else:
|
|
253
|
+
raise InvalidClassification(
|
|
254
|
+
"Classification level '%s' was not found in " "your classification definition." % lvl
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
def _get_c12n_level_text(self, lvl_idx: int, long_format: bool = True) -> str:
|
|
258
|
+
text = self.levels_map.get(str(lvl_idx), None)
|
|
259
|
+
if not text:
|
|
260
|
+
raise InvalidClassification(
|
|
261
|
+
"Classification level number '%s' was not " "found in your classification definition." % lvl_idx
|
|
262
|
+
)
|
|
263
|
+
if long_format:
|
|
264
|
+
return self.levels_map_stl[text]
|
|
265
|
+
return text
|
|
266
|
+
|
|
267
|
+
def _get_c12n_required(self, c12n: str, long_format: bool = True) -> List:
|
|
268
|
+
# Parse classifications in uppercase mode only
|
|
269
|
+
c12n = c12n.upper()
|
|
270
|
+
|
|
271
|
+
return_set = set()
|
|
272
|
+
part_set = set(c12n.split("/"))
|
|
273
|
+
|
|
274
|
+
for p in part_set:
|
|
275
|
+
if p in self.access_req_map_lts:
|
|
276
|
+
return_set.add(self.access_req_map_lts[p])
|
|
277
|
+
elif p in self.access_req_map_stl:
|
|
278
|
+
return_set.add(p)
|
|
279
|
+
elif p in self.access_req_aliases:
|
|
280
|
+
for a in self.access_req_aliases[p]:
|
|
281
|
+
return_set.add(a)
|
|
282
|
+
|
|
283
|
+
if long_format:
|
|
284
|
+
return sorted([self.access_req_map_stl[r] for r in return_set])
|
|
285
|
+
return sorted(list(return_set))
|
|
286
|
+
|
|
287
|
+
def _get_c12n_groups(self, c12n: str, long_format: bool = True) -> Tuple[List, List]:
|
|
288
|
+
# Parse classifications in uppercase mode only
|
|
289
|
+
c12n = c12n.upper()
|
|
290
|
+
|
|
291
|
+
g1_set = set()
|
|
292
|
+
g2_set = set()
|
|
293
|
+
others = set()
|
|
294
|
+
|
|
295
|
+
grp_part = c12n.split("//")
|
|
296
|
+
groups = []
|
|
297
|
+
for gp in grp_part:
|
|
298
|
+
gp = gp.replace("REL TO ", "")
|
|
299
|
+
gp = gp.replace("REL ", "")
|
|
300
|
+
temp_group = set([x.strip() for x in gp.split(",")])
|
|
301
|
+
for t in temp_group:
|
|
302
|
+
groups.extend(t.split("/"))
|
|
303
|
+
|
|
304
|
+
for g in groups:
|
|
305
|
+
if g in self.groups_map_lts:
|
|
306
|
+
g1_set.add(self.groups_map_lts[g])
|
|
307
|
+
elif g in self.groups_map_stl:
|
|
308
|
+
g1_set.add(g)
|
|
309
|
+
elif g in self.groups_aliases:
|
|
310
|
+
for a in self.groups_aliases[g]:
|
|
311
|
+
g1_set.add(a)
|
|
312
|
+
elif g in self.subgroups_map_lts:
|
|
313
|
+
g2_set.add(self.subgroups_map_lts[g])
|
|
314
|
+
elif g in self.subgroups_map_stl:
|
|
315
|
+
g2_set.add(g)
|
|
316
|
+
elif g in self.subgroups_aliases:
|
|
317
|
+
for a in self.subgroups_aliases[g]:
|
|
318
|
+
g2_set.add(a)
|
|
319
|
+
else:
|
|
320
|
+
others.add(g)
|
|
321
|
+
|
|
322
|
+
if self.dynamic_groups:
|
|
323
|
+
for o in others:
|
|
324
|
+
if (
|
|
325
|
+
o not in self.access_req_map_lts
|
|
326
|
+
and o not in self.access_req_map_stl
|
|
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)
|
|
333
|
+
|
|
334
|
+
if long_format:
|
|
335
|
+
return sorted([self.groups_map_stl.get(r, r) for r in g1_set]), sorted(
|
|
336
|
+
[self.subgroups_map_stl[r] for r in g2_set]
|
|
337
|
+
)
|
|
338
|
+
return sorted(list(g1_set)), sorted(list(g2_set))
|
|
339
|
+
|
|
340
|
+
@staticmethod
|
|
341
|
+
def _can_see_required(user_req: List, req: List) -> bool:
|
|
342
|
+
return set(req).issubset(user_req)
|
|
343
|
+
|
|
344
|
+
@staticmethod
|
|
345
|
+
def _can_see_groups(user_groups: List, req: List) -> bool:
|
|
346
|
+
if len(req) == 0:
|
|
347
|
+
return True
|
|
348
|
+
|
|
349
|
+
for g in user_groups:
|
|
350
|
+
if g in req:
|
|
351
|
+
return True
|
|
352
|
+
|
|
353
|
+
return False
|
|
354
|
+
|
|
355
|
+
# noinspection PyTypeChecker
|
|
356
|
+
def _get_normalized_classification_text(
|
|
357
|
+
self,
|
|
358
|
+
lvl_idx: int,
|
|
359
|
+
req: List,
|
|
360
|
+
groups: List,
|
|
361
|
+
subgroups: List,
|
|
362
|
+
long_format: bool = True,
|
|
363
|
+
skip_auto_select: bool = False,
|
|
364
|
+
) -> str:
|
|
365
|
+
# 1. Check for all required items if they need a specific classification lvl
|
|
366
|
+
required_lvl_idx = 0
|
|
367
|
+
for r in req:
|
|
368
|
+
required_lvl_idx = max(required_lvl_idx, self.params_map.get(r, {}).get("require_lvl", 0))
|
|
369
|
+
out = self._get_c12n_level_text(max(lvl_idx, required_lvl_idx), long_format=long_format)
|
|
370
|
+
|
|
371
|
+
# 2. Check for all required items if they should be shown inside the groups display part
|
|
372
|
+
req_grp = []
|
|
373
|
+
for r in req:
|
|
374
|
+
if self.params_map.get(r, {}).get("is_required_group"):
|
|
375
|
+
req_grp.append(r)
|
|
376
|
+
req = list(set(req).difference(set(req_grp)))
|
|
377
|
+
|
|
378
|
+
if req:
|
|
379
|
+
out += "//" + "/".join(req)
|
|
380
|
+
if req_grp:
|
|
381
|
+
out += "//" + "/".join(sorted(req_grp))
|
|
382
|
+
|
|
383
|
+
# 3. Add auto-selected subgroups
|
|
384
|
+
if long_format:
|
|
385
|
+
if len(subgroups) > 0 and len(self.subgroups_auto_select) > 0 and not skip_auto_select:
|
|
386
|
+
subgroups = sorted(list(set(subgroups).union(set(self.subgroups_auto_select))))
|
|
387
|
+
else:
|
|
388
|
+
if len(subgroups) > 0 and len(self.subgroups_auto_select_short) > 0 and not skip_auto_select:
|
|
389
|
+
subgroups = sorted(list(set(subgroups).union(set(self.subgroups_auto_select_short))))
|
|
390
|
+
|
|
391
|
+
# 4. For every subgroup, check if the subgroup requires or is limited to a specific group
|
|
392
|
+
temp_groups = []
|
|
393
|
+
for sg in subgroups:
|
|
394
|
+
required_group = self.params_map.get(sg, {}).get("require_group", None)
|
|
395
|
+
if required_group is not None:
|
|
396
|
+
temp_groups.append(required_group)
|
|
397
|
+
|
|
398
|
+
limited_to_group = self.params_map.get(sg, {}).get("limited_to_group", None)
|
|
399
|
+
if limited_to_group is not None:
|
|
400
|
+
if limited_to_group in temp_groups:
|
|
401
|
+
temp_groups = [limited_to_group]
|
|
402
|
+
else:
|
|
403
|
+
temp_groups = []
|
|
404
|
+
|
|
405
|
+
for g in temp_groups:
|
|
406
|
+
if long_format:
|
|
407
|
+
groups.append(self.groups_map_stl.get(g, g))
|
|
408
|
+
else:
|
|
409
|
+
groups.append(self.groups_map_lts.get(g, g))
|
|
410
|
+
groups = list(set(groups))
|
|
411
|
+
|
|
412
|
+
# 5. Add auto-selected groups
|
|
413
|
+
if long_format:
|
|
414
|
+
if len(groups) > 0 and len(self.groups_auto_select) > 0 and not skip_auto_select:
|
|
415
|
+
groups = sorted(list(set(groups).union(set(self.groups_auto_select))))
|
|
416
|
+
else:
|
|
417
|
+
if len(groups) > 0 and len(self.groups_auto_select_short) > 0 and not skip_auto_select:
|
|
418
|
+
groups = sorted(list(set(groups).union(set(self.groups_auto_select_short))))
|
|
419
|
+
|
|
420
|
+
if groups:
|
|
421
|
+
out += {True: "/", False: "//"}[len(req_grp) > 0]
|
|
422
|
+
if len(groups) == 1:
|
|
423
|
+
# 6. If only one group, check if it has a solitary display name.
|
|
424
|
+
grp = groups[0]
|
|
425
|
+
display_name = self.params_map.get(grp, {}).get("solitary_display_name", grp)
|
|
426
|
+
if display_name != grp:
|
|
427
|
+
out += display_name
|
|
428
|
+
else:
|
|
429
|
+
out += "REL TO " + grp
|
|
430
|
+
else:
|
|
431
|
+
if not long_format:
|
|
432
|
+
# 7. In short format mode, check if there is an alias that can replace multiple groups
|
|
433
|
+
for alias, values in self.groups_aliases.items():
|
|
434
|
+
if len(values) > 1:
|
|
435
|
+
if sorted(values) == groups:
|
|
436
|
+
groups = [alias]
|
|
437
|
+
out += "REL TO " + ", ".join(sorted(groups))
|
|
438
|
+
|
|
439
|
+
if subgroups:
|
|
440
|
+
if len(groups) > 0 or len(req_grp) > 0:
|
|
441
|
+
out += "/"
|
|
442
|
+
else:
|
|
443
|
+
out += "//"
|
|
444
|
+
out += "/".join(sorted(subgroups))
|
|
445
|
+
|
|
446
|
+
return out
|
|
447
|
+
|
|
448
|
+
def _get_classification_parts(
|
|
449
|
+
self, c12n: str, long_format: bool = True
|
|
450
|
+
) -> Tuple[Union[Union[int, str], Any], List, List, List]:
|
|
451
|
+
lvl_idx = self._get_c12n_level_index(c12n)
|
|
452
|
+
req = self._get_c12n_required(c12n, long_format=long_format)
|
|
453
|
+
groups, subgroups = self._get_c12n_groups(c12n, long_format=long_format)
|
|
454
|
+
|
|
455
|
+
return lvl_idx, req, groups, subgroups
|
|
456
|
+
|
|
457
|
+
@staticmethod
|
|
458
|
+
def _max_groups(groups_1: List, groups_2: List) -> List:
|
|
459
|
+
if len(groups_1) > 0 and len(groups_2) > 0:
|
|
460
|
+
groups = set(groups_1) & set(groups_2)
|
|
461
|
+
else:
|
|
462
|
+
groups = set(groups_1) | set(groups_2)
|
|
463
|
+
|
|
464
|
+
if len(groups_1) > 0 and len(groups_2) > 0 and len(groups) == 0:
|
|
465
|
+
# NOTE: Intersection generated nothing, we will raise an InvalidClassification exception
|
|
466
|
+
raise InvalidClassification(
|
|
467
|
+
"Could not find any intersection between the groups. %s & %s" % (groups_1, groups_2)
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
return list(groups)
|
|
471
|
+
|
|
472
|
+
# ++++++++++++++++++++++++
|
|
473
|
+
# Public functions
|
|
474
|
+
# ++++++++++++++++++++++++
|
|
475
|
+
# noinspection PyUnusedLocal
|
|
476
|
+
def list_all_classification_combinations(self, long_format: bool = True) -> Set:
|
|
477
|
+
combinations = set()
|
|
478
|
+
|
|
479
|
+
levels = self._list_items_and_aliases(self.original_definition["levels"], long_format=long_format)
|
|
480
|
+
reqs = self._list_items_and_aliases(self.original_definition["required"], long_format=long_format)
|
|
481
|
+
grps = self._list_items_and_aliases(self.original_definition["groups"], long_format=long_format)
|
|
482
|
+
sgrps = self._list_items_and_aliases(self.original_definition["subgroups"], long_format=long_format)
|
|
483
|
+
|
|
484
|
+
req_cbs = self._build_combinations(reqs)
|
|
485
|
+
if long_format:
|
|
486
|
+
grp_solitary_display = {
|
|
487
|
+
x["name"]: x["solitary_display_name"]
|
|
488
|
+
for x in self.original_definition["groups"]
|
|
489
|
+
if "solitary_display_name" in x
|
|
490
|
+
}
|
|
491
|
+
else:
|
|
492
|
+
grp_solitary_display = {
|
|
493
|
+
x["short_name"]: x["solitary_display_name"]
|
|
494
|
+
for x in self.original_definition["groups"]
|
|
495
|
+
if "solitary_display_name" in x
|
|
496
|
+
}
|
|
497
|
+
solitary_names = [
|
|
498
|
+
x["solitary_display_name"] for x in self.original_definition["groups"] if "solitary_display_name" in x
|
|
499
|
+
]
|
|
500
|
+
|
|
501
|
+
grp_cbs = self._build_combinations(grps, separator=", ", solitary_display=grp_solitary_display)
|
|
502
|
+
sgrp_cbs = self._build_combinations(sgrps)
|
|
503
|
+
|
|
504
|
+
for p in itertools.product(levels, req_cbs):
|
|
505
|
+
cl = "//".join(p)
|
|
506
|
+
if cl.endswith("//"):
|
|
507
|
+
combinations.add(cl[:-2])
|
|
508
|
+
else:
|
|
509
|
+
combinations.add(cl)
|
|
510
|
+
|
|
511
|
+
temp_combinations = copy(combinations)
|
|
512
|
+
for p in itertools.product(temp_combinations, grp_cbs):
|
|
513
|
+
cl = "//REL TO ".join(p)
|
|
514
|
+
if cl.endswith("//REL TO "):
|
|
515
|
+
combinations.add(cl[:-9])
|
|
516
|
+
else:
|
|
517
|
+
combinations.add(cl)
|
|
518
|
+
|
|
519
|
+
for sol_name in solitary_names:
|
|
520
|
+
to_edit = []
|
|
521
|
+
to_find = "REL TO {sol_name}".format(sol_name=sol_name)
|
|
522
|
+
for c in combinations:
|
|
523
|
+
if to_find in c:
|
|
524
|
+
to_edit.append(c)
|
|
525
|
+
|
|
526
|
+
for e in to_edit:
|
|
527
|
+
combinations.add(e.replace(to_find, sol_name))
|
|
528
|
+
combinations.remove(e)
|
|
529
|
+
|
|
530
|
+
temp_combinations = copy(combinations)
|
|
531
|
+
for p in itertools.product(temp_combinations, sgrp_cbs):
|
|
532
|
+
if "//REL TO " in p[0]:
|
|
533
|
+
cl = "/".join(p)
|
|
534
|
+
|
|
535
|
+
if cl.endswith("/"):
|
|
536
|
+
combinations.add(cl[:-1])
|
|
537
|
+
else:
|
|
538
|
+
combinations.add(cl)
|
|
539
|
+
else:
|
|
540
|
+
cl = "//REL TO ".join(p)
|
|
541
|
+
|
|
542
|
+
if cl.endswith("//REL TO "):
|
|
543
|
+
combinations.add(cl[:-9])
|
|
544
|
+
else:
|
|
545
|
+
combinations.add(cl)
|
|
546
|
+
|
|
547
|
+
return combinations
|
|
548
|
+
|
|
549
|
+
# noinspection PyUnusedLocal
|
|
550
|
+
def default_user_classification(self, user: Optional[str] = None, long_format: bool = True) -> str:
|
|
551
|
+
"""You can overload this function to specify a way to get the default classification of a user.
|
|
552
|
+
By default, this function returns the UNRESTRICTED value of your classification definition.
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
user: Which user to get the classification for
|
|
556
|
+
long_format: Request a long classification format or not
|
|
557
|
+
|
|
558
|
+
Returns:
|
|
559
|
+
The classification in the specified format
|
|
560
|
+
"""
|
|
561
|
+
return self.UNRESTRICTED
|
|
562
|
+
|
|
563
|
+
def get_parsed_classification_definition(self) -> Dict:
|
|
564
|
+
"""Returns all dictionary of all the variables inside the classification object that will be used
|
|
565
|
+
to enforce classification throughout the system.
|
|
566
|
+
"""
|
|
567
|
+
from copy import deepcopy
|
|
568
|
+
|
|
569
|
+
out = deepcopy(self.__dict__)
|
|
570
|
+
out["levels_map"].pop("INV", None)
|
|
571
|
+
out["levels_map"].pop(str(self.INVALID_LVL), None)
|
|
572
|
+
out["levels_map_stl"].pop("INV", None)
|
|
573
|
+
out["levels_map_lts"].pop("INVALID", None)
|
|
574
|
+
out["levels_map"].pop("NULL", None)
|
|
575
|
+
out["levels_map"].pop(str(self.NULL_LVL), None)
|
|
576
|
+
out["levels_map_stl"].pop("NULL", None)
|
|
577
|
+
out["levels_map_lts"].pop("NULL", None)
|
|
578
|
+
out.pop("_classification_cache", None)
|
|
579
|
+
out.pop("_classification_cache_short", None)
|
|
580
|
+
out.pop("original_definition", None)
|
|
581
|
+
return out
|
|
582
|
+
|
|
583
|
+
def get_access_control_parts(self, c12n: str, user_classification: bool = False) -> Dict:
|
|
584
|
+
"""Returns a dictionary containing the different access parameters Lucene needs to build it's queries
|
|
585
|
+
|
|
586
|
+
Args:
|
|
587
|
+
c12n: The classification to get the parts from
|
|
588
|
+
user_classification: Is a user classification
|
|
589
|
+
"""
|
|
590
|
+
if not self.enforce or self.invalid_mode:
|
|
591
|
+
c12n = self.UNRESTRICTED
|
|
592
|
+
|
|
593
|
+
try:
|
|
594
|
+
# Normalize the classification before gathering the parts
|
|
595
|
+
c12n = self.normalize_classification(c12n, skip_auto_select=user_classification)
|
|
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)
|
|
600
|
+
|
|
601
|
+
return {
|
|
602
|
+
"__access_lvl__": access_lvl,
|
|
603
|
+
"__access_req__": access_req,
|
|
604
|
+
"__access_grp1__": access_grp1 or ["__EMPTY__"],
|
|
605
|
+
"__access_grp2__": access_grp2 or ["__EMPTY__"],
|
|
606
|
+
}
|
|
607
|
+
except InvalidClassification:
|
|
608
|
+
if not self.enforce or self.invalid_mode:
|
|
609
|
+
return {
|
|
610
|
+
"__access_lvl__": self.NULL_LVL,
|
|
611
|
+
"__access_req__": [],
|
|
612
|
+
"__access_grp1__": ["__EMPTY__"],
|
|
613
|
+
"__access_grp2__": ["__EMPTY__"],
|
|
614
|
+
}
|
|
615
|
+
else:
|
|
616
|
+
raise
|
|
617
|
+
|
|
618
|
+
def get_access_control_req(self) -> Union[KeysView, List]:
|
|
619
|
+
"""Returns a list of the different possible REQUIRED parts"""
|
|
620
|
+
if not self.enforce or self.invalid_mode:
|
|
621
|
+
return []
|
|
622
|
+
|
|
623
|
+
return self.access_req_map_stl.keys()
|
|
624
|
+
|
|
625
|
+
def get_access_control_groups(self) -> Union[KeysView, List]:
|
|
626
|
+
"""Returns a list of the different possible GROUPS"""
|
|
627
|
+
if not self.enforce or self.invalid_mode:
|
|
628
|
+
return []
|
|
629
|
+
|
|
630
|
+
return self.groups_map_stl.keys()
|
|
631
|
+
|
|
632
|
+
def get_access_control_subgroups(self) -> Union[KeysView, List]:
|
|
633
|
+
"""Returns a list of the different possible SUBGROUPS"""
|
|
634
|
+
if not self.enforce or self.invalid_mode:
|
|
635
|
+
return []
|
|
636
|
+
|
|
637
|
+
return self.subgroups_map_stl.keys()
|
|
638
|
+
|
|
639
|
+
def intersect_user_classification(self, user_c12n_1: str, user_c12n_2: str, long_format: bool = True) -> str:
|
|
640
|
+
"""This function intersects two user classification to return the maximum classification
|
|
641
|
+
that both user could see.
|
|
642
|
+
|
|
643
|
+
Args:
|
|
644
|
+
user_c12n_1: First user classification
|
|
645
|
+
user_c12n_2: Second user classification
|
|
646
|
+
long_format: True/False in long format
|
|
647
|
+
|
|
648
|
+
Returns:
|
|
649
|
+
Intersected classification in the desired format
|
|
650
|
+
"""
|
|
651
|
+
if not self.enforce or self.invalid_mode:
|
|
652
|
+
return self.UNRESTRICTED
|
|
653
|
+
|
|
654
|
+
# Normalize classifications before comparing them
|
|
655
|
+
if user_c12n_1 is not None:
|
|
656
|
+
user_c12n_1 = self.normalize_classification(user_c12n_1, skip_auto_select=True)
|
|
657
|
+
if user_c12n_2 is not None:
|
|
658
|
+
user_c12n_2 = self.normalize_classification(user_c12n_2, skip_auto_select=True)
|
|
659
|
+
|
|
660
|
+
if user_c12n_1 is None:
|
|
661
|
+
return user_c12n_2
|
|
662
|
+
if user_c12n_2 is None:
|
|
663
|
+
return user_c12n_1
|
|
664
|
+
|
|
665
|
+
lvl_idx_1, req_1, groups_1, subgroups_1 = self._get_classification_parts(user_c12n_1, long_format=long_format)
|
|
666
|
+
lvl_idx_2, req_2, groups_2, subgroups_2 = self._get_classification_parts(user_c12n_2, long_format=long_format)
|
|
667
|
+
|
|
668
|
+
req = list(set(req_1) & set(req_2))
|
|
669
|
+
groups = list(set(groups_1) & set(groups_2))
|
|
670
|
+
subgroups = list(set(subgroups_1) & set(subgroups_2))
|
|
671
|
+
|
|
672
|
+
return self._get_normalized_classification_text(
|
|
673
|
+
min(lvl_idx_1, lvl_idx_2), # type: ignore
|
|
674
|
+
req,
|
|
675
|
+
groups,
|
|
676
|
+
subgroups,
|
|
677
|
+
long_format=long_format,
|
|
678
|
+
skip_auto_select=True,
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
def is_accessible(self, user_c12n: str, c12n: str, ignore_invalid: bool = False) -> bool:
|
|
682
|
+
"""Given a user classification, check if a user is allow to see a certain classification
|
|
683
|
+
|
|
684
|
+
Args:
|
|
685
|
+
user_c12n: Maximum classification for the user
|
|
686
|
+
c12n: Classification the user which to see
|
|
687
|
+
|
|
688
|
+
Returns:
|
|
689
|
+
True is the user can see the classification
|
|
690
|
+
"""
|
|
691
|
+
if self.invalid_mode:
|
|
692
|
+
return False
|
|
693
|
+
|
|
694
|
+
if not self.enforce:
|
|
695
|
+
return True
|
|
696
|
+
|
|
697
|
+
if c12n is None:
|
|
698
|
+
return True
|
|
699
|
+
|
|
700
|
+
try:
|
|
701
|
+
# Normalize classifications before comparing them
|
|
702
|
+
user_c12n = self.normalize_classification(user_c12n, skip_auto_select=True)
|
|
703
|
+
c12n = self.normalize_classification(c12n, skip_auto_select=True)
|
|
704
|
+
|
|
705
|
+
user_req = self._get_c12n_required(user_c12n)
|
|
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):
|
|
711
|
+
if not self._can_see_required(user_req, req):
|
|
712
|
+
return False
|
|
713
|
+
if not self._can_see_groups(user_groups, groups):
|
|
714
|
+
return False
|
|
715
|
+
if not self._can_see_groups(user_subgroups, subgroups):
|
|
716
|
+
return False
|
|
717
|
+
return True
|
|
718
|
+
return False
|
|
719
|
+
except InvalidClassification:
|
|
720
|
+
if ignore_invalid:
|
|
721
|
+
return False
|
|
722
|
+
else:
|
|
723
|
+
raise
|
|
724
|
+
|
|
725
|
+
def is_valid(self, c12n: str, skip_auto_select: bool = False) -> bool:
|
|
726
|
+
"""Performs a series of checks againts a classification to make sure it is valid in it's current form
|
|
727
|
+
|
|
728
|
+
Args:
|
|
729
|
+
c12n: The classification we want to validate
|
|
730
|
+
skip_auto_select: skip the auto selection phase
|
|
731
|
+
|
|
732
|
+
Returns:
|
|
733
|
+
True if the classification is valid
|
|
734
|
+
"""
|
|
735
|
+
if not self.enforce:
|
|
736
|
+
return True
|
|
737
|
+
|
|
738
|
+
try:
|
|
739
|
+
# Classification normalization test
|
|
740
|
+
n_c12n = self.normalize_classification(c12n, skip_auto_select=skip_auto_select)
|
|
741
|
+
n_lvl_idx, n_req, n_groups, n_subgroups = self._get_classification_parts(n_c12n)
|
|
742
|
+
lvl_idx, req, groups, subgroups = self._get_classification_parts(c12n)
|
|
743
|
+
except InvalidClassification:
|
|
744
|
+
return False
|
|
745
|
+
|
|
746
|
+
if lvl_idx != n_lvl_idx:
|
|
747
|
+
return False
|
|
748
|
+
|
|
749
|
+
if sorted(req) != sorted(n_req):
|
|
750
|
+
return False
|
|
751
|
+
|
|
752
|
+
if sorted(groups) != sorted(n_groups):
|
|
753
|
+
return False
|
|
754
|
+
|
|
755
|
+
if sorted(subgroups) != sorted(n_subgroups):
|
|
756
|
+
return False
|
|
757
|
+
|
|
758
|
+
c12n = c12n.replace("REL TO ", "")
|
|
759
|
+
c12n = c12n.replace("REL ", "")
|
|
760
|
+
parts = c12n.split("//")
|
|
761
|
+
|
|
762
|
+
# There is a maximum of 3 parts
|
|
763
|
+
if len(parts) > 3:
|
|
764
|
+
return False
|
|
765
|
+
|
|
766
|
+
cur_part = parts.pop(0)
|
|
767
|
+
# First parts as to be a classification level part
|
|
768
|
+
if (
|
|
769
|
+
cur_part not in self.levels_aliases.keys()
|
|
770
|
+
and cur_part not in self.levels_map_lts.keys()
|
|
771
|
+
and cur_part not in self.levels_map_stl.keys()
|
|
772
|
+
):
|
|
773
|
+
return False
|
|
774
|
+
|
|
775
|
+
check_groups = False
|
|
776
|
+
while len(parts) > 0:
|
|
777
|
+
# Can't be two groups sections.
|
|
778
|
+
if check_groups:
|
|
779
|
+
return False
|
|
780
|
+
|
|
781
|
+
cur_part = parts.pop(0)
|
|
782
|
+
items = cur_part.split("/")
|
|
783
|
+
comma_idx = None
|
|
784
|
+
for idx, i in enumerate(items):
|
|
785
|
+
if "," in i:
|
|
786
|
+
comma_idx = idx
|
|
787
|
+
|
|
788
|
+
if comma_idx is not None:
|
|
789
|
+
items += [x.strip() for x in items.pop(comma_idx).split(",")]
|
|
790
|
+
|
|
791
|
+
for i in items:
|
|
792
|
+
if not check_groups:
|
|
793
|
+
# If current item not found in access req, we might already be dealing with groups
|
|
794
|
+
if (
|
|
795
|
+
i not in self.access_req_aliases.keys()
|
|
796
|
+
and i not in self.access_req_map_stl.keys()
|
|
797
|
+
and i not in self.access_req_map_lts.keys()
|
|
798
|
+
):
|
|
799
|
+
check_groups = True
|
|
800
|
+
|
|
801
|
+
if check_groups and not self.dynamic_groups:
|
|
802
|
+
# If not groups. That stuff does not exists...
|
|
803
|
+
if (
|
|
804
|
+
i not in self.groups_aliases.keys()
|
|
805
|
+
and i not in self.groups_map_stl.keys()
|
|
806
|
+
and i not in self.groups_map_lts.keys()
|
|
807
|
+
and i not in self.subgroups_aliases.keys()
|
|
808
|
+
and i not in self.subgroups_map_stl.keys()
|
|
809
|
+
and i not in self.subgroups_map_lts.keys()
|
|
810
|
+
):
|
|
811
|
+
return False
|
|
812
|
+
|
|
813
|
+
return True
|
|
814
|
+
|
|
815
|
+
def max_classification(self, c12n_1: str, c12n_2: str, long_format: bool = True) -> str:
|
|
816
|
+
"""Mixes to classification and returns to most restrictive form for them
|
|
817
|
+
|
|
818
|
+
Args:
|
|
819
|
+
c12n_1: First classification
|
|
820
|
+
c12n_2: Second classification
|
|
821
|
+
long_format: True/False in long format
|
|
822
|
+
|
|
823
|
+
Returns:
|
|
824
|
+
The most restrictive classification that we could create out of the two
|
|
825
|
+
"""
|
|
826
|
+
if not self.enforce or self.invalid_mode:
|
|
827
|
+
return self.UNRESTRICTED
|
|
828
|
+
|
|
829
|
+
# Normalize classifications before comparing them
|
|
830
|
+
if c12n_1 is not None:
|
|
831
|
+
c12n_1 = self.normalize_classification(c12n_1)
|
|
832
|
+
if c12n_2 is not None:
|
|
833
|
+
c12n_2 = self.normalize_classification(c12n_2)
|
|
834
|
+
|
|
835
|
+
if c12n_1 is None:
|
|
836
|
+
return c12n_2
|
|
837
|
+
if c12n_2 is None:
|
|
838
|
+
return c12n_1
|
|
839
|
+
|
|
840
|
+
lvl_idx_1, req_1, groups_1, subgroups_1 = self._get_classification_parts(c12n_1, long_format=long_format)
|
|
841
|
+
lvl_idx_2, req_2, groups_2, subgroups_2 = self._get_classification_parts(c12n_2, long_format=long_format)
|
|
842
|
+
|
|
843
|
+
req = list(set(req_1) | set(req_2))
|
|
844
|
+
groups = self._max_groups(groups_1, groups_2)
|
|
845
|
+
subgroups = self._max_groups(subgroups_1, subgroups_2)
|
|
846
|
+
|
|
847
|
+
return self._get_normalized_classification_text(
|
|
848
|
+
cast(int, max(lvl_idx_1, lvl_idx_2)),
|
|
849
|
+
req,
|
|
850
|
+
groups,
|
|
851
|
+
subgroups,
|
|
852
|
+
long_format=long_format, # type: ignore
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
def min_classification(self, c12n_1: str, c12n_2: str, long_format: bool = True) -> str:
|
|
856
|
+
"""Mixes to classification and returns to least restrictive form for them
|
|
857
|
+
|
|
858
|
+
Args:
|
|
859
|
+
c12n_1: First classification
|
|
860
|
+
c12n_2: Second classification
|
|
861
|
+
long_format: True/False in long format
|
|
862
|
+
|
|
863
|
+
Returns:
|
|
864
|
+
The least restrictive classification that we could create out of the two
|
|
865
|
+
"""
|
|
866
|
+
if not self.enforce or self.invalid_mode:
|
|
867
|
+
return self.UNRESTRICTED
|
|
868
|
+
|
|
869
|
+
# Normalize classifications before comparing them
|
|
870
|
+
if c12n_1 is not None:
|
|
871
|
+
c12n_1 = self.normalize_classification(c12n_1)
|
|
872
|
+
if c12n_2 is not None:
|
|
873
|
+
c12n_2 = self.normalize_classification(c12n_2)
|
|
874
|
+
|
|
875
|
+
if c12n_1 is None:
|
|
876
|
+
return c12n_2
|
|
877
|
+
if c12n_2 is None:
|
|
878
|
+
return c12n_1
|
|
879
|
+
|
|
880
|
+
lvl_idx_1, req_1, groups_1, subgroups_1 = self._get_classification_parts(c12n_1, long_format=long_format)
|
|
881
|
+
lvl_idx_2, req_2, groups_2, subgroups_2 = self._get_classification_parts(c12n_2, long_format=long_format)
|
|
882
|
+
|
|
883
|
+
req = list(set(req_1) & set(req_2))
|
|
884
|
+
if len(groups_1) > 0 and len(groups_2) > 0:
|
|
885
|
+
groups = list(set(groups_1) | set(groups_2))
|
|
886
|
+
else:
|
|
887
|
+
groups = []
|
|
888
|
+
|
|
889
|
+
if len(subgroups_1) > 0 and len(subgroups_2) > 0:
|
|
890
|
+
subgroups = list(set(subgroups_1) | set(subgroups_2))
|
|
891
|
+
else:
|
|
892
|
+
subgroups = []
|
|
893
|
+
|
|
894
|
+
return self._get_normalized_classification_text(
|
|
895
|
+
cast(int, min(lvl_idx_1, lvl_idx_2)),
|
|
896
|
+
req,
|
|
897
|
+
groups,
|
|
898
|
+
subgroups,
|
|
899
|
+
long_format=long_format, # type: ignore
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
def normalize_classification(self, c12n: str, long_format: bool = True, skip_auto_select: bool = False) -> str:
|
|
903
|
+
"""Normalize a given classification by applying the rules defined in the classification definition.
|
|
904
|
+
This function will remove any invalid parts and add missing parts to the classification.
|
|
905
|
+
It will also ensure that the display of the classification is always done the same way
|
|
906
|
+
|
|
907
|
+
Args:
|
|
908
|
+
c12n: Classification to normalize
|
|
909
|
+
long_format: True/False in long format
|
|
910
|
+
skip_auto_select: True/False skip group auto adding, use True when dealing with user's classifications
|
|
911
|
+
|
|
912
|
+
Returns:
|
|
913
|
+
A normalized version of the original classification
|
|
914
|
+
"""
|
|
915
|
+
if not self.enforce or self.invalid_mode:
|
|
916
|
+
return self.UNRESTRICTED
|
|
917
|
+
|
|
918
|
+
# Has the classification has already been normalized before?
|
|
919
|
+
if long_format and c12n in self._classification_cache:
|
|
920
|
+
return c12n
|
|
921
|
+
if not long_format and c12n in self._classification_cache_short:
|
|
922
|
+
return c12n
|
|
923
|
+
|
|
924
|
+
lvl_idx, req, groups, subgroups = self._get_classification_parts(c12n, long_format=long_format)
|
|
925
|
+
new_c12n = self._get_normalized_classification_text(
|
|
926
|
+
lvl_idx, # type: ignore
|
|
927
|
+
req,
|
|
928
|
+
groups,
|
|
929
|
+
subgroups,
|
|
930
|
+
long_format=long_format,
|
|
931
|
+
skip_auto_select=skip_auto_select,
|
|
932
|
+
)
|
|
933
|
+
if long_format:
|
|
934
|
+
self._classification_cache.add(new_c12n)
|
|
935
|
+
else:
|
|
936
|
+
self._classification_cache_short.add(new_c12n)
|
|
937
|
+
|
|
938
|
+
return new_c12n
|
|
939
|
+
|
|
940
|
+
def build_user_classification(self, c12n_1: str, c12n_2: str, long_format: bool = True) -> str:
|
|
941
|
+
"""Mixes to classification and return the classification marking that would give access to the most data
|
|
942
|
+
|
|
943
|
+
Args:
|
|
944
|
+
c12n_1: First classification
|
|
945
|
+
c12n_2: Second classification
|
|
946
|
+
long_format: True/False in long format
|
|
947
|
+
|
|
948
|
+
Returns:
|
|
949
|
+
The classification that would give access to the most data
|
|
950
|
+
"""
|
|
951
|
+
if not self.enforce or self.invalid_mode:
|
|
952
|
+
return self.UNRESTRICTED
|
|
953
|
+
|
|
954
|
+
# Normalize classifications before comparing them
|
|
955
|
+
if c12n_1 is not None:
|
|
956
|
+
c12n_1 = self.normalize_classification(c12n_1, skip_auto_select=True)
|
|
957
|
+
if c12n_2 is not None:
|
|
958
|
+
c12n_2 = self.normalize_classification(c12n_2, skip_auto_select=True)
|
|
959
|
+
|
|
960
|
+
if c12n_1 is None:
|
|
961
|
+
return c12n_2
|
|
962
|
+
if c12n_2 is None:
|
|
963
|
+
return c12n_1
|
|
964
|
+
|
|
965
|
+
lvl_idx_1, req_1, groups_1, subgroups_1 = self._get_classification_parts(c12n_1, long_format=long_format)
|
|
966
|
+
lvl_idx_2, req_2, groups_2, subgroups_2 = self._get_classification_parts(c12n_2, long_format=long_format)
|
|
967
|
+
|
|
968
|
+
req = list(set(req_1) | set(req_2))
|
|
969
|
+
groups = list(set(groups_1) | set(groups_2))
|
|
970
|
+
subgroups = list(set(subgroups_1) | set(subgroups_2))
|
|
971
|
+
|
|
972
|
+
return self._get_normalized_classification_text(
|
|
973
|
+
max(lvl_idx_1, lvl_idx_2), # type: ignore
|
|
974
|
+
req,
|
|
975
|
+
groups,
|
|
976
|
+
subgroups,
|
|
977
|
+
long_format=long_format,
|
|
978
|
+
skip_auto_select=True,
|
|
979
|
+
)
|