dmart 1.4.17__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.
- alembic.ini +117 -0
- api/__init__.py +0 -0
- api/info/__init__.py +0 -0
- api/info/router.py +109 -0
- api/managed/__init__.py +0 -0
- api/managed/router.py +1541 -0
- api/managed/utils.py +1879 -0
- api/public/__init__.py +0 -0
- api/public/router.py +758 -0
- api/qr/__init__.py +0 -0
- api/qr/router.py +108 -0
- api/user/__init__.py +0 -0
- api/user/model/__init__.py +0 -0
- api/user/model/errors.py +14 -0
- api/user/model/requests.py +165 -0
- api/user/model/responses.py +11 -0
- api/user/router.py +1413 -0
- api/user/service.py +270 -0
- bundler.py +55 -0
- config/__init__.py +0 -0
- config/channels.json +11 -0
- config/notification.json +17 -0
- cxb/__init__.py +0 -0
- cxb/client/__init__.py +0 -0
- cxb/client/assets/@codemirror-Rn7_6DkE.js +10 -0
- cxb/client/assets/@edraj-CS4NwVbD.js +1 -0
- cxb/client/assets/@floating-ui-BwwcF-xh.js +1 -0
- cxb/client/assets/@formatjs-yKEsAtjs.js +1 -0
- cxb/client/assets/@fortawesome-DRW1UCdr.js +9 -0
- cxb/client/assets/@jsonquerylang-laKNoFFq.js +12 -0
- cxb/client/assets/@lezer-za4Q-8Ew.js +1 -0
- cxb/client/assets/@marijn-DXwl3gUT.js +1 -0
- cxb/client/assets/@popperjs-l0sNRNKZ.js +1 -0
- cxb/client/assets/@replit--ERk53eB.js +1 -0
- cxb/client/assets/@roxi-CGMFK4i8.js +6 -0
- cxb/client/assets/@typewriter-cCzskkIv.js +17 -0
- cxb/client/assets/@zerodevx-BlBZjKxu.js +1 -0
- cxb/client/assets/@zerodevx-CVEpe6WZ.css +1 -0
- cxb/client/assets/BreadCrumbLite-DAhOx38v.js +1 -0
- cxb/client/assets/EntryRenderer-25YDhRen.js +32 -0
- cxb/client/assets/EntryRenderer-DXytdFp9.css +1 -0
- cxb/client/assets/ListView-BpAycA2h.js +16 -0
- cxb/client/assets/ListView-U8of-_c-.css +1 -0
- cxb/client/assets/Prism--hMplq-p.js +3 -0
- cxb/client/assets/Prism-Uh6uStUw.css +1 -0
- cxb/client/assets/Table2Cols-BsbwicQm.js +1 -0
- cxb/client/assets/_..-BvT6vdHa.css +1 -0
- cxb/client/assets/_...404_-fuLH_rX9.js +2 -0
- cxb/client/assets/_...fallback_-Ba_NLmAE.js +1 -0
- cxb/client/assets/_module-Bfk8MiCs.js +3 -0
- cxb/client/assets/_module-CEW0D5oI.js +4 -0
- cxb/client/assets/_module-Dgq0ZVtz.js +1 -0
- cxb/client/assets/ajv-Cpj98o6Y.js +1 -0
- cxb/client/assets/axios-CG2WSiiR.js +6 -0
- cxb/client/assets/clsx-B-dksMZM.js +1 -0
- cxb/client/assets/codemirror-wrapped-line-indent-DPhKvljI.js +1 -0
- cxb/client/assets/compare-C3AjiGFR.js +1 -0
- cxb/client/assets/compute-scroll-into-view-Bl8rNFhg.js +1 -0
- cxb/client/assets/consolite-DlCuI0F9.js +1 -0
- cxb/client/assets/crelt-C8TCjufn.js +1 -0
- cxb/client/assets/date-fns-l0sNRNKZ.js +1 -0
- cxb/client/assets/deepmerge-rn4rBaHU.js +1 -0
- cxb/client/assets/dmart_services-AL6-IdDE.js +1 -0
- cxb/client/assets/downloadFile-D08i0YDh.js +1 -0
- cxb/client/assets/easy-signal-BiPFIK3O.js +1 -0
- cxb/client/assets/esm-env-rsSWfq8L.js +1 -0
- cxb/client/assets/export-OF_rTiXu.js +1 -0
- cxb/client/assets/fast-deep-equal-l0sNRNKZ.js +1 -0
- cxb/client/assets/fast-diff-C-IidNf4.js +1 -0
- cxb/client/assets/fast-uri-l0sNRNKZ.js +1 -0
- cxb/client/assets/flowbite-svelte-BLvjb-sa.js +1 -0
- cxb/client/assets/flowbite-svelte-CD54FDqW.css +1 -0
- cxb/client/assets/flowbite-svelte-icons-BI8GVhw_.js +1 -0
- cxb/client/assets/github-slugger-CQ4oX9Ud.js +1 -0
- cxb/client/assets/global-igKv-1g9.js +1 -0
- cxb/client/assets/hookar-BMRD9G9H.js +1 -0
- cxb/client/assets/immutable-json-patch-DtRO2E_S.js +1 -0
- cxb/client/assets/import-1vE3gBat.js +1 -0
- cxb/client/assets/index-B-eTh-ZX.js +1 -0
- cxb/client/assets/index-BVyxzKtH.js +1 -0
- cxb/client/assets/index-BdeNM69f.js +1 -0
- cxb/client/assets/index-C6cPO4op.js +1 -0
- cxb/client/assets/index-CC-A1ipE.js +1 -0
- cxb/client/assets/index-CTxJ-lDp.js +1 -0
- cxb/client/assets/index-Cd-F5j_k.js +1 -0
- cxb/client/assets/index-D742rwaM.js +1 -0
- cxb/client/assets/index-DTfhnhwd.js +1 -0
- cxb/client/assets/index-DdXRK7n9.js +2 -0
- cxb/client/assets/index-DtiCmB4o.js +1 -0
- cxb/client/assets/index-NBrXBlLA.css +2 -0
- cxb/client/assets/index-ac-Buu_H.js +4 -0
- cxb/client/assets/index-iYkH7C67.js +1 -0
- cxb/client/assets/info-B986lRiM.js +1 -0
- cxb/client/assets/intl-messageformat-Dc5UU-HB.js +3 -0
- cxb/client/assets/jmespath-l0sNRNKZ.js +1 -0
- cxb/client/assets/json-schema-traverse-l0sNRNKZ.js +1 -0
- cxb/client/assets/json-source-map-DRgZidqy.js +5 -0
- cxb/client/assets/jsonpath-plus-l0sNRNKZ.js +1 -0
- cxb/client/assets/jsonrepair-B30Dx381.js +8 -0
- cxb/client/assets/lodash-es-DZVAA2ox.js +1 -0
- cxb/client/assets/marked-DKjyhwJX.js +56 -0
- cxb/client/assets/marked-gfm-heading-id-U5zO829x.js +2 -0
- cxb/client/assets/marked-mangle-CDMeiHC6.js +1 -0
- cxb/client/assets/memoize-one-BdPwpGay.js +1 -0
- cxb/client/assets/natural-compare-lite-Bg2Xcf-o.js +7 -0
- cxb/client/assets/pagination-svelte-D5CyoiE_.js +13 -0
- cxb/client/assets/pagination-svelte-v10nAbbM.css +1 -0
- cxb/client/assets/plantuml-encoder-C47mzt9T.js +1 -0
- cxb/client/assets/prismjs-DTUiLGJu.js +9 -0
- cxb/client/assets/profile-BUf-tKMe.js +1 -0
- cxb/client/assets/query-CNmXTsgf.js +1 -0
- cxb/client/assets/queryHelpers-C9iBWwqe.js +1 -0
- cxb/client/assets/scroll-into-view-if-needed-KR58zyjF.js +1 -0
- cxb/client/assets/spaces-0oyGvpii.js +1 -0
- cxb/client/assets/style-mod-Bs6eFhZE.js +3 -0
- cxb/client/assets/svelte-B2XmcTi_.js +4 -0
- cxb/client/assets/svelte-awesome-COLlx0DN.css +1 -0
- cxb/client/assets/svelte-awesome-DhnMA6Q_.js +1 -0
- cxb/client/assets/svelte-datatables-net-CY7LBj6I.js +1 -0
- cxb/client/assets/svelte-floating-ui-BlS3sOAQ.js +1 -0
- cxb/client/assets/svelte-i18n-CT2KkQaN.js +3 -0
- cxb/client/assets/svelte-jsoneditor-BzfX6Usi.css +1 -0
- cxb/client/assets/svelte-jsoneditor-CUGSvWId.js +25 -0
- cxb/client/assets/svelte-select-CegQKzqH.css +1 -0
- cxb/client/assets/svelte-select-CjHAt_85.js +6 -0
- cxb/client/assets/tailwind-merge-CJvxXMcu.js +1 -0
- cxb/client/assets/tailwind-variants-Cj20BoQ3.js +1 -0
- cxb/client/assets/toast-B9WDyfyI.js +1 -0
- cxb/client/assets/tslib-pJfR_DrR.js +1 -0
- cxb/client/assets/typewriter-editor-DkTVIJdm.js +25 -0
- cxb/client/assets/user-DeK_NB5v.js +1 -0
- cxb/client/assets/vanilla-picker-l5rcX3cq.js +8 -0
- cxb/client/assets/w3c-keyname-Vcq4gwWv.js +1 -0
- cxb/client/config.json +11 -0
- cxb/client/config.sample.json +11 -0
- cxb/client/favicon.ico +0 -0
- cxb/client/favicon.png +0 -0
- cxb/client/index.html +28 -0
- data_adapters/__init__.py +0 -0
- data_adapters/adapter.py +16 -0
- data_adapters/base_data_adapter.py +467 -0
- data_adapters/file/__init__.py +0 -0
- data_adapters/file/adapter.py +2043 -0
- data_adapters/file/adapter_helpers.py +1013 -0
- data_adapters/file/archive.py +150 -0
- data_adapters/file/create_index.py +331 -0
- data_adapters/file/create_users_folders.py +52 -0
- data_adapters/file/custom_validations.py +68 -0
- data_adapters/file/drop_index.py +40 -0
- data_adapters/file/health_check.py +560 -0
- data_adapters/file/redis_services.py +1110 -0
- data_adapters/helpers.py +27 -0
- data_adapters/sql/__init__.py +0 -0
- data_adapters/sql/adapter.py +3218 -0
- data_adapters/sql/adapter_helpers.py +491 -0
- data_adapters/sql/create_tables.py +451 -0
- data_adapters/sql/create_users_folders.py +53 -0
- data_adapters/sql/db_to_json_migration.py +485 -0
- data_adapters/sql/health_check_sql.py +232 -0
- data_adapters/sql/json_to_db_migration.py +454 -0
- data_adapters/sql/update_query_policies.py +101 -0
- data_generator.py +81 -0
- dmart-1.4.17.dist-info/METADATA +65 -0
- dmart-1.4.17.dist-info/RECORD +289 -0
- dmart-1.4.17.dist-info/WHEEL +5 -0
- dmart-1.4.17.dist-info/entry_points.txt +2 -0
- dmart-1.4.17.dist-info/top_level.txt +24 -0
- dmart.py +623 -0
- dmart_migrations/README +1 -0
- dmart_migrations/__init__.py +0 -0
- dmart_migrations/__pycache__/__init__.cpython-314.pyc +0 -0
- dmart_migrations/__pycache__/env.cpython-314.pyc +0 -0
- dmart_migrations/env.py +100 -0
- dmart_migrations/notes.txt +11 -0
- dmart_migrations/script.py.mako +28 -0
- dmart_migrations/scripts/__init__.py +0 -0
- dmart_migrations/scripts/calculate_checksums.py +77 -0
- dmart_migrations/scripts/migration_f7a4949eed19.py +28 -0
- dmart_migrations/versions/0f3d2b1a7c21_add_authz_materialized_views.py +87 -0
- dmart_migrations/versions/10d2041b94d4_last_checksum_history.py +62 -0
- dmart_migrations/versions/1cf4e1ee3cb8_ext_permission_with_filter_fields_values.py +33 -0
- dmart_migrations/versions/26bfe19b49d4_rm_failedloginattempts.py +42 -0
- dmart_migrations/versions/3c8bca2219cc_add_otp_table.py +38 -0
- dmart_migrations/versions/6675fd9dfe42_remove_unique_from_sessions_table.py +36 -0
- dmart_migrations/versions/71bc1df82e6a_adding_user_last_login_at.py +43 -0
- dmart_migrations/versions/74288ccbd3b5_initial.py +264 -0
- dmart_migrations/versions/7520a89a8467_rm_activesession_table.py +39 -0
- dmart_migrations/versions/848b623755a4_make_created_nd_updated_at_required.py +138 -0
- dmart_migrations/versions/8640dcbebf85_add_notes_to_users.py +32 -0
- dmart_migrations/versions/91c94250232a_adding_fk_on_owner_shortname.py +104 -0
- dmart_migrations/versions/98ecd6f56f9a_ext_meta_with_owner_group_shortname.py +66 -0
- dmart_migrations/versions/9aae9138c4ef_indexing_created_at_updated_at.py +80 -0
- dmart_migrations/versions/__init__.py +0 -0
- dmart_migrations/versions/__pycache__/0f3d2b1a7c21_add_authz_materialized_views.cpython-314.pyc +0 -0
- dmart_migrations/versions/__pycache__/10d2041b94d4_last_checksum_history.cpython-314.pyc +0 -0
- dmart_migrations/versions/__pycache__/1cf4e1ee3cb8_ext_permission_with_filter_fields_values.cpython-314.pyc +0 -0
- dmart_migrations/versions/__pycache__/26bfe19b49d4_rm_failedloginattempts.cpython-314.pyc +0 -0
- dmart_migrations/versions/__pycache__/3c8bca2219cc_add_otp_table.cpython-314.pyc +0 -0
- dmart_migrations/versions/__pycache__/6675fd9dfe42_remove_unique_from_sessions_table.cpython-314.pyc +0 -0
- dmart_migrations/versions/__pycache__/71bc1df82e6a_adding_user_last_login_at.cpython-314.pyc +0 -0
- dmart_migrations/versions/__pycache__/74288ccbd3b5_initial.cpython-314.pyc +0 -0
- dmart_migrations/versions/__pycache__/7520a89a8467_rm_activesession_table.cpython-314.pyc +0 -0
- dmart_migrations/versions/__pycache__/848b623755a4_make_created_nd_updated_at_required.cpython-314.pyc +0 -0
- dmart_migrations/versions/__pycache__/8640dcbebf85_add_notes_to_users.cpython-314.pyc +0 -0
- dmart_migrations/versions/__pycache__/91c94250232a_adding_fk_on_owner_shortname.cpython-314.pyc +0 -0
- dmart_migrations/versions/__pycache__/98ecd6f56f9a_ext_meta_with_owner_group_shortname.cpython-314.pyc +0 -0
- dmart_migrations/versions/__pycache__/9aae9138c4ef_indexing_created_at_updated_at.cpython-314.pyc +0 -0
- dmart_migrations/versions/__pycache__/b53f916b3f6d_json_to_jsonb.cpython-314.pyc +0 -0
- dmart_migrations/versions/__pycache__/eb5f1ec65156_adding_user_locked_to_device.cpython-314.pyc +0 -0
- dmart_migrations/versions/__pycache__/f7a4949eed19_adding_query_policies_to_meta.cpython-314.pyc +0 -0
- dmart_migrations/versions/b53f916b3f6d_json_to_jsonb.py +492 -0
- dmart_migrations/versions/eb5f1ec65156_adding_user_locked_to_device.py +36 -0
- dmart_migrations/versions/f7a4949eed19_adding_query_policies_to_meta.py +60 -0
- get_settings.py +7 -0
- info.json +1 -0
- languages/__init__.py +0 -0
- languages/arabic.json +15 -0
- languages/english.json +16 -0
- languages/kurdish.json +14 -0
- languages/loader.py +12 -0
- main.py +560 -0
- migrate.py +24 -0
- models/__init__.py +0 -0
- models/api.py +203 -0
- models/core.py +597 -0
- models/enums.py +255 -0
- password_gen.py +8 -0
- plugins/__init__.py +0 -0
- plugins/action_log/__init__.py +0 -0
- plugins/action_log/plugin.py +121 -0
- plugins/admin_notification_sender/__init__.py +0 -0
- plugins/admin_notification_sender/plugin.py +124 -0
- plugins/ldap_manager/__init__.py +0 -0
- plugins/ldap_manager/plugin.py +100 -0
- plugins/local_notification/__init__.py +0 -0
- plugins/local_notification/plugin.py +123 -0
- plugins/realtime_updates_notifier/__init__.py +0 -0
- plugins/realtime_updates_notifier/plugin.py +58 -0
- plugins/redis_db_update/__init__.py +0 -0
- plugins/redis_db_update/plugin.py +188 -0
- plugins/resource_folders_creation/__init__.py +0 -0
- plugins/resource_folders_creation/plugin.py +81 -0
- plugins/system_notification_sender/__init__.py +0 -0
- plugins/system_notification_sender/plugin.py +188 -0
- plugins/update_access_controls/__init__.py +0 -0
- plugins/update_access_controls/plugin.py +9 -0
- pytests/__init__.py +0 -0
- pytests/api_user_models_erros_test.py +16 -0
- pytests/api_user_models_requests_test.py +98 -0
- pytests/archive_test.py +72 -0
- pytests/base_test.py +300 -0
- pytests/get_settings_test.py +14 -0
- pytests/json_to_db_migration_test.py +237 -0
- pytests/service_test.py +26 -0
- pytests/test_info.py +55 -0
- pytests/test_status.py +15 -0
- run_notification_campaign.py +85 -0
- scheduled_notification_handler.py +121 -0
- schema_migration.py +208 -0
- schema_modulate.py +192 -0
- set_admin_passwd.py +55 -0
- sync.py +202 -0
- utils/__init__.py +0 -0
- utils/access_control.py +306 -0
- utils/async_request.py +8 -0
- utils/exporter.py +309 -0
- utils/firebase_notifier.py +57 -0
- utils/generate_email.py +37 -0
- utils/helpers.py +352 -0
- utils/hypercorn_config.py +12 -0
- utils/internal_error_code.py +60 -0
- utils/jwt.py +124 -0
- utils/logger.py +167 -0
- utils/middleware.py +99 -0
- utils/notification.py +75 -0
- utils/password_hashing.py +16 -0
- utils/plugin_manager.py +202 -0
- utils/query_policies_helper.py +128 -0
- utils/regex.py +44 -0
- utils/repository.py +529 -0
- utils/router_helper.py +19 -0
- utils/settings.py +166 -0
- utils/sms_notifier.py +21 -0
- utils/social_sso.py +67 -0
- utils/templates/activation.html.j2 +26 -0
- utils/templates/reminder.html.j2 +17 -0
- utils/ticket_sys_utils.py +203 -0
- utils/web_notifier.py +29 -0
- websocket.py +231 -0
utils/access_control.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import sys
|
|
3
|
+
from models.core import Meta, ACL, ActionType, ConditionType, Group, Permission, Role, User
|
|
4
|
+
from models.enums import ResourceType
|
|
5
|
+
from utils.helpers import camel_case, flatten_dict
|
|
6
|
+
from utils.settings import settings
|
|
7
|
+
from data_adapters.adapter import data_adapter as db
|
|
8
|
+
from utils.regex import FILE_PATTERN
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AccessControl:
|
|
12
|
+
permissions: dict[str, Permission] = {}
|
|
13
|
+
groups: dict[str, Group] = {}
|
|
14
|
+
roles: dict[str, Role] = {}
|
|
15
|
+
users: dict[str, User] = {}
|
|
16
|
+
|
|
17
|
+
async def load_permissions_and_roles(self) -> None:
|
|
18
|
+
if settings.active_data_db == "file":
|
|
19
|
+
management_path = settings.spaces_folder / settings.management_space
|
|
20
|
+
|
|
21
|
+
management_modules: dict[str, type[Meta]] = {
|
|
22
|
+
"groups": Group,
|
|
23
|
+
"roles": Role,
|
|
24
|
+
"permissions": Permission
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for module_name, module_value in management_modules.items():
|
|
28
|
+
self_module = getattr(self, module_name)
|
|
29
|
+
self_module = {}
|
|
30
|
+
path = management_path / module_name
|
|
31
|
+
entries_glob = ".dm/*/meta.*.json"
|
|
32
|
+
if path.exists():
|
|
33
|
+
for one in path.glob(entries_glob):
|
|
34
|
+
match = FILE_PATTERN.search(str(one))
|
|
35
|
+
if not match or not one.is_file():
|
|
36
|
+
continue
|
|
37
|
+
shortname = match.group(1)
|
|
38
|
+
try:
|
|
39
|
+
resource_obj: Meta = await db.load(
|
|
40
|
+
settings.management_space,
|
|
41
|
+
module_name,
|
|
42
|
+
shortname,
|
|
43
|
+
module_value,
|
|
44
|
+
"anonymous",
|
|
45
|
+
)
|
|
46
|
+
if resource_obj.is_active:
|
|
47
|
+
self_module[shortname] = resource_obj # store in redis doc
|
|
48
|
+
except Exception as ex:
|
|
49
|
+
# print(f"Error processing @{settings.management_space}/{module_name}/{shortname} ... ", ex)
|
|
50
|
+
raise ex
|
|
51
|
+
|
|
52
|
+
await db.create_user_premission_index()
|
|
53
|
+
await db.store_modules_to_redis(self.roles, self.groups, self.permissions)
|
|
54
|
+
await db.delete_user_permissions_map_in_redis()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def check_access(
|
|
58
|
+
self,
|
|
59
|
+
user_shortname: str,
|
|
60
|
+
space_name: str,
|
|
61
|
+
subpath: str,
|
|
62
|
+
resource_type: ResourceType,
|
|
63
|
+
action_type: ActionType,
|
|
64
|
+
resource_is_active: bool = False,
|
|
65
|
+
resource_owner_shortname: str | None = None,
|
|
66
|
+
resource_owner_group: str | None = None,
|
|
67
|
+
record_attributes: dict = {},
|
|
68
|
+
entry_shortname: str | None = None
|
|
69
|
+
):
|
|
70
|
+
# print("Checking access for", user_shortname, space_name, subpath, resource_type, action_type)
|
|
71
|
+
if resource_type == ResourceType.space and entry_shortname:
|
|
72
|
+
return await self.check_space_access(
|
|
73
|
+
user_shortname,
|
|
74
|
+
entry_shortname
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if entry_shortname:
|
|
78
|
+
acl_access = await self.check_access_control_list(
|
|
79
|
+
space_name,
|
|
80
|
+
subpath,
|
|
81
|
+
resource_type,
|
|
82
|
+
entry_shortname,
|
|
83
|
+
action_type,
|
|
84
|
+
user_shortname
|
|
85
|
+
)
|
|
86
|
+
if acl_access:
|
|
87
|
+
return True
|
|
88
|
+
# print("Checking check_space_access access")
|
|
89
|
+
user_permissions = await db.get_user_permissions(user_shortname)
|
|
90
|
+
|
|
91
|
+
user_groups = (await db.load_user_meta(user_shortname)).groups or []
|
|
92
|
+
|
|
93
|
+
# Generate set of achevied conditions on the resource
|
|
94
|
+
# ex: {"is_active", "own"}
|
|
95
|
+
resource_achieved_conditions: set[ConditionType] = set()
|
|
96
|
+
if resource_is_active:
|
|
97
|
+
resource_achieved_conditions.add(ConditionType.is_active)
|
|
98
|
+
if resource_owner_shortname == user_shortname or resource_owner_group in user_groups:
|
|
99
|
+
resource_achieved_conditions.add(ConditionType.own)
|
|
100
|
+
|
|
101
|
+
# Allow checking for root permissions
|
|
102
|
+
subpath_parts = ["/"]
|
|
103
|
+
subpath_parts += list(filter(None, subpath.strip("/").split("/")))
|
|
104
|
+
if resource_type == ResourceType.folder and entry_shortname:
|
|
105
|
+
subpath_parts.append(entry_shortname)
|
|
106
|
+
|
|
107
|
+
search_subpath = ""
|
|
108
|
+
for subpath_part in subpath_parts:
|
|
109
|
+
search_subpath += subpath_part
|
|
110
|
+
# Check if the user has global access
|
|
111
|
+
global_access = self.has_global_access(
|
|
112
|
+
space_name,
|
|
113
|
+
user_permissions,
|
|
114
|
+
search_subpath,
|
|
115
|
+
action_type,
|
|
116
|
+
resource_type,
|
|
117
|
+
resource_achieved_conditions,
|
|
118
|
+
record_attributes
|
|
119
|
+
)
|
|
120
|
+
if global_access:
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
permission_key = f"{space_name}:{search_subpath}:{resource_type}"
|
|
124
|
+
if (
|
|
125
|
+
permission_key in user_permissions
|
|
126
|
+
and action_type in user_permissions[permission_key]["allowed_actions"]
|
|
127
|
+
and self.check_access_conditions(
|
|
128
|
+
set(user_permissions[permission_key]["conditions"]),
|
|
129
|
+
set(resource_achieved_conditions),
|
|
130
|
+
action_type,
|
|
131
|
+
)
|
|
132
|
+
and self.check_access_restriction(
|
|
133
|
+
user_permissions[permission_key]["restricted_fields"],
|
|
134
|
+
user_permissions[permission_key]["allowed_fields_values"],
|
|
135
|
+
action_type,
|
|
136
|
+
record_attributes
|
|
137
|
+
)
|
|
138
|
+
):
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
if search_subpath == "/":
|
|
142
|
+
search_subpath = ""
|
|
143
|
+
else:
|
|
144
|
+
search_subpath += "/"
|
|
145
|
+
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
async def check_access_control_list(
|
|
149
|
+
self,
|
|
150
|
+
space_name: str,
|
|
151
|
+
subpath: str,
|
|
152
|
+
resource_type: ResourceType,
|
|
153
|
+
entry_shortname: str,
|
|
154
|
+
action_type: ActionType,
|
|
155
|
+
user_shortname: str,
|
|
156
|
+
) -> bool:
|
|
157
|
+
resource_cls = getattr(
|
|
158
|
+
sys.modules["models.core"], camel_case(resource_type)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
entry = await db.load(
|
|
163
|
+
space_name=space_name,
|
|
164
|
+
subpath=subpath,
|
|
165
|
+
shortname=entry_shortname,
|
|
166
|
+
class_type=resource_cls
|
|
167
|
+
)
|
|
168
|
+
except Exception:
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
if not entry.acl:
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
user_acl: ACL | None = None
|
|
175
|
+
for access in entry.acl:
|
|
176
|
+
if access.user_shortname == user_shortname:
|
|
177
|
+
user_acl = access
|
|
178
|
+
break
|
|
179
|
+
|
|
180
|
+
if not user_acl:
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
return action_type in user_acl.allowed_actions
|
|
184
|
+
|
|
185
|
+
def has_global_access(
|
|
186
|
+
self,
|
|
187
|
+
space_name: str,
|
|
188
|
+
user_permissions: dict,
|
|
189
|
+
search_subpath: str,
|
|
190
|
+
action_type: ActionType,
|
|
191
|
+
resource_type: str,
|
|
192
|
+
resource_achieved_conditions: set,
|
|
193
|
+
record_attributes: dict
|
|
194
|
+
) -> bool:
|
|
195
|
+
"""
|
|
196
|
+
check if has access to global subpath by replacing the following
|
|
197
|
+
subpath = / => __all_subpaths__
|
|
198
|
+
subpath = {subpath} => __all_subpaths__
|
|
199
|
+
subpath = {subpath}/protected => __all_subpaths__/protected
|
|
200
|
+
subpath = {subpath}/protected/mine => {subpath}/__all_subpaths__/mine
|
|
201
|
+
"""
|
|
202
|
+
original_subpath = search_subpath
|
|
203
|
+
search_subpath_parts = search_subpath.split("/")
|
|
204
|
+
if len(search_subpath_parts) > 1:
|
|
205
|
+
search_subpath_parts[-2] = settings.all_subpaths_mw
|
|
206
|
+
search_subpath = "/".join(search_subpath_parts)
|
|
207
|
+
elif len(search_subpath_parts) == 1:
|
|
208
|
+
search_subpath = settings.all_subpaths_mw
|
|
209
|
+
if search_subpath[-1] == "/" and len(search_subpath) > 1:
|
|
210
|
+
search_subpath = search_subpath[:-1]
|
|
211
|
+
|
|
212
|
+
permission_key = None
|
|
213
|
+
# check if has access to all spaces
|
|
214
|
+
if f"{settings.all_spaces_mw}:{search_subpath}:{resource_type}" in user_permissions:
|
|
215
|
+
permission_key = f"{settings.all_spaces_mw}:{search_subpath}:{resource_type}"
|
|
216
|
+
|
|
217
|
+
# check if has access to current spaces
|
|
218
|
+
if f"{space_name}:{search_subpath}:{resource_type}" in user_permissions:
|
|
219
|
+
permission_key = f"{space_name}:{search_subpath}:{resource_type}"
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# check if has access to current subpath
|
|
223
|
+
if f"{settings.all_spaces_mw}:{original_subpath}:{resource_type}" in user_permissions:
|
|
224
|
+
permission_key = f"{settings.all_spaces_mw}:{original_subpath}:{resource_type}"
|
|
225
|
+
|
|
226
|
+
if not permission_key:
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
if (
|
|
230
|
+
action_type in user_permissions[permission_key]["allowed_actions"]
|
|
231
|
+
and self.check_access_conditions(
|
|
232
|
+
set(user_permissions[permission_key]["conditions"]),
|
|
233
|
+
set(resource_achieved_conditions),
|
|
234
|
+
action_type,
|
|
235
|
+
)
|
|
236
|
+
and self.check_access_restriction(
|
|
237
|
+
user_permissions[permission_key]["restricted_fields"],
|
|
238
|
+
user_permissions[permission_key]["allowed_fields_values"],
|
|
239
|
+
action_type,
|
|
240
|
+
record_attributes
|
|
241
|
+
)
|
|
242
|
+
):
|
|
243
|
+
return True
|
|
244
|
+
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
def check_access_conditions(
|
|
248
|
+
self,
|
|
249
|
+
premission_conditions: set,
|
|
250
|
+
resource_achieved_conditions: set,
|
|
251
|
+
action_type: ActionType,
|
|
252
|
+
):
|
|
253
|
+
# actions of type query will be handled in the query function
|
|
254
|
+
# actions of type create shouldn't check for permission conditions
|
|
255
|
+
if action_type in [ActionType.create, ActionType.query]:
|
|
256
|
+
return True
|
|
257
|
+
|
|
258
|
+
return premission_conditions.issubset(resource_achieved_conditions)
|
|
259
|
+
|
|
260
|
+
def check_access_restriction(
|
|
261
|
+
self,
|
|
262
|
+
restricted_fields: list,
|
|
263
|
+
allowed_fields_values: dict,
|
|
264
|
+
action_type: ActionType,
|
|
265
|
+
record_attributes: dict
|
|
266
|
+
):
|
|
267
|
+
"""
|
|
268
|
+
in case of create or update action, check access for the record fields
|
|
269
|
+
via permission.restricted_fields and permission.allowed_fields_values
|
|
270
|
+
"""
|
|
271
|
+
if action_type not in [ActionType.create, ActionType.update]:
|
|
272
|
+
return True
|
|
273
|
+
|
|
274
|
+
flattened_attributes = flatten_dict(record_attributes)
|
|
275
|
+
|
|
276
|
+
for restricted_field in restricted_fields:
|
|
277
|
+
if restricted_field in flattened_attributes:
|
|
278
|
+
return False
|
|
279
|
+
for flattened_key in flattened_attributes.keys():
|
|
280
|
+
if flattened_key == restricted_field or flattened_key.startswith(f"{restricted_field}."):
|
|
281
|
+
return False
|
|
282
|
+
|
|
283
|
+
for field_name, field_values in allowed_fields_values.items():
|
|
284
|
+
if field_name not in flattened_attributes:
|
|
285
|
+
continue
|
|
286
|
+
if (
|
|
287
|
+
isinstance(flattened_attributes[field_name], list) and
|
|
288
|
+
isinstance(field_values[0], list) and
|
|
289
|
+
not any(all(i in allowed_values for i in flattened_attributes[field_name]) for allowed_values in field_values)
|
|
290
|
+
):
|
|
291
|
+
return False
|
|
292
|
+
elif (
|
|
293
|
+
not isinstance(flattened_attributes[field_name], list) and
|
|
294
|
+
flattened_attributes[field_name] not in field_values
|
|
295
|
+
):
|
|
296
|
+
return False
|
|
297
|
+
|
|
298
|
+
return True
|
|
299
|
+
|
|
300
|
+
async def check_space_access(self, user_shortname: str, space_name: str) -> bool:
|
|
301
|
+
user_permissions = await db.get_user_permissions(user_shortname)
|
|
302
|
+
prog = re.compile(f"{space_name}:*|{settings.all_spaces_mw}:*")
|
|
303
|
+
return bool(list(filter(prog.match, user_permissions.keys())))
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
access_control = AccessControl()
|
utils/async_request.py
ADDED
utils/exporter.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import asyncio
|
|
4
|
+
from hashlib import blake2b, md5
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
import copy
|
|
10
|
+
import jsonschema
|
|
11
|
+
from aiofiles import open as aopen
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
# from pydantic import config
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def hashing_data(data: str, hashed_data: dict):
|
|
18
|
+
hash = blake2b(salt=md5(data.encode()).digest())
|
|
19
|
+
hash.update(data.encode())
|
|
20
|
+
hashed_val = md5(hash.digest()).hexdigest()
|
|
21
|
+
hashed_data[hashed_val] = data
|
|
22
|
+
return hashed_val
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def exit_with_error(msg: str):
|
|
26
|
+
print("ERROR!!", msg)
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
SECURED_FIELDS = [
|
|
31
|
+
"name",
|
|
32
|
+
"email",
|
|
33
|
+
"ip",
|
|
34
|
+
"pin",
|
|
35
|
+
"RechargeNumber",
|
|
36
|
+
"CallingNumber",
|
|
37
|
+
"shortname",
|
|
38
|
+
"contact_number",
|
|
39
|
+
"pin",
|
|
40
|
+
"msisdn",
|
|
41
|
+
"imsi",
|
|
42
|
+
"sender_msisdn",
|
|
43
|
+
"old_username",
|
|
44
|
+
"firstname",
|
|
45
|
+
"lastname",
|
|
46
|
+
]
|
|
47
|
+
OUTPUT_FOLDER_NAME = "spaces_data"
|
|
48
|
+
|
|
49
|
+
def meta_path(
|
|
50
|
+
space_path: Path,
|
|
51
|
+
subpath: str,
|
|
52
|
+
file_path: str,
|
|
53
|
+
resource_type: str
|
|
54
|
+
) -> Path:
|
|
55
|
+
return space_path / f"{subpath}/.dm/{file_path}/meta.{resource_type}.json"
|
|
56
|
+
|
|
57
|
+
async def get_meta(
|
|
58
|
+
*,
|
|
59
|
+
space_path: Path,
|
|
60
|
+
subpath: str,
|
|
61
|
+
file_path: str,
|
|
62
|
+
resource_type: str
|
|
63
|
+
):
|
|
64
|
+
meta_content = meta_path(space_path, subpath, file_path, resource_type)
|
|
65
|
+
async with aopen(meta_content, "r") as f:
|
|
66
|
+
return json.loads(await f.read())
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def validate_config(config_obj: dict):
|
|
70
|
+
if (
|
|
71
|
+
not config_obj.get("space")
|
|
72
|
+
or not config_obj.get("subpath")
|
|
73
|
+
or not config_obj.get("resource_type")
|
|
74
|
+
or not config_obj.get("schema_shortname")
|
|
75
|
+
):
|
|
76
|
+
return False
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def remove_fields(src: dict, restricted_keys: list):
|
|
81
|
+
for k in list(src.keys()):
|
|
82
|
+
if isinstance(src[k], list):
|
|
83
|
+
for item in src[k]:
|
|
84
|
+
if isinstance(item, dict):
|
|
85
|
+
item = remove_fields(item, restricted_keys)
|
|
86
|
+
elif isinstance(src[k], dict):
|
|
87
|
+
src[k] = remove_fields(src[k], restricted_keys)
|
|
88
|
+
|
|
89
|
+
if k in restricted_keys:
|
|
90
|
+
del src[k]
|
|
91
|
+
|
|
92
|
+
return src
|
|
93
|
+
|
|
94
|
+
def enc_dict(d: dict, hashed_data: dict):
|
|
95
|
+
for k, v in d.items():
|
|
96
|
+
if isinstance(v, dict):
|
|
97
|
+
d[k] = enc_dict(v, hashed_data)
|
|
98
|
+
elif isinstance(d[k], list):
|
|
99
|
+
for item in d[k]:
|
|
100
|
+
if isinstance(item, dict):
|
|
101
|
+
item = enc_dict(item, hashed_data)
|
|
102
|
+
|
|
103
|
+
# if k == "msisdn":
|
|
104
|
+
# d[k] = hashing_data(str(v))
|
|
105
|
+
# try:
|
|
106
|
+
# print("ADD TO SQLLITE")
|
|
107
|
+
# cur.execute(f"INSERT INTO migrated_data VALUES('{v}', '{d[k]}')")
|
|
108
|
+
# con.commit()
|
|
109
|
+
# print("FINSIHED")
|
|
110
|
+
# except sqlite3.IntegrityError as e:
|
|
111
|
+
# if "migrated_data.hash" in str(e): # msisdn already in db
|
|
112
|
+
# raise Exception(f"Collision on: msisdn {v}, hash {d[k]}")
|
|
113
|
+
elif k in SECURED_FIELDS:
|
|
114
|
+
d[k] = hashing_data(str(v), hashed_data)
|
|
115
|
+
|
|
116
|
+
return d
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def prepare_output(
|
|
120
|
+
meta: dict,
|
|
121
|
+
payload: dict,
|
|
122
|
+
included_meta_fields: dict,
|
|
123
|
+
excluded_payload_fields: dict,
|
|
124
|
+
):
|
|
125
|
+
out = payload
|
|
126
|
+
for field_meta in included_meta_fields:
|
|
127
|
+
field_name = field_meta.get("field_name")
|
|
128
|
+
rename_to = field_meta.get("rename_to")
|
|
129
|
+
if not field_name:
|
|
130
|
+
continue
|
|
131
|
+
if rename_to:
|
|
132
|
+
out[rename_to] = meta.get(field_name, "")
|
|
133
|
+
else:
|
|
134
|
+
out[field_name] = meta.get(field_name, "")
|
|
135
|
+
|
|
136
|
+
out = remove_fields(
|
|
137
|
+
out,
|
|
138
|
+
[field["field_name"] for field in excluded_payload_fields]
|
|
139
|
+
)
|
|
140
|
+
return out
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
async def extract(
|
|
146
|
+
space : str,
|
|
147
|
+
subpath : str, # = config_obj.get("subpath")
|
|
148
|
+
resource_type : str, # = config_obj.get("resource_type")
|
|
149
|
+
schema_shortname : str, # = config_obj.get("schema_shortname")
|
|
150
|
+
included_meta_fields : dict, # = config_obj.get("included_meta_fields", [])
|
|
151
|
+
excluded_payload_fields : dict, # = config_obj.get("excluded_payload_fields", [])
|
|
152
|
+
spaces_path: str,
|
|
153
|
+
output_path : str,
|
|
154
|
+
entries_since = None
|
|
155
|
+
) -> None:
|
|
156
|
+
hashed_data: dict[str, str] = {}
|
|
157
|
+
|
|
158
|
+
space_path = Path(f"{spaces_path}/{space}")
|
|
159
|
+
subpath_schema_obj = None
|
|
160
|
+
with open(space_path / f"schema/{schema_shortname}.json", "r") as f:
|
|
161
|
+
subpath_schema_obj = json.load(f)
|
|
162
|
+
input_subpath_schema_obj = copy.deepcopy(subpath_schema_obj)
|
|
163
|
+
|
|
164
|
+
output_subpath = Path(f"{output_path}/{OUTPUT_FOLDER_NAME}/{space}/{subpath}")
|
|
165
|
+
if not output_subpath.is_dir():
|
|
166
|
+
os.makedirs(output_subpath)
|
|
167
|
+
|
|
168
|
+
# Generat output schema
|
|
169
|
+
schema_fil = output_subpath / "schema.json"
|
|
170
|
+
for field in included_meta_fields:
|
|
171
|
+
if "oneOf" in subpath_schema_obj:
|
|
172
|
+
for schema in subpath_schema_obj["oneOf"]:
|
|
173
|
+
schema["properties"][field["field_name"]] = field["schema_entry"]
|
|
174
|
+
if field.get("rename_to"):
|
|
175
|
+
schema["properties"][field["rename_to"]] = schema[
|
|
176
|
+
"properties"
|
|
177
|
+
].pop(field["field_name"])
|
|
178
|
+
else:
|
|
179
|
+
subpath_schema_obj["properties"][field["field_name"]] = field["schema_entry"]
|
|
180
|
+
if field.get("rename_to"):
|
|
181
|
+
subpath_schema_obj["properties"][field["rename_to"]] = subpath_schema_obj[
|
|
182
|
+
"properties"
|
|
183
|
+
].pop(field["field_name"])
|
|
184
|
+
if "oneOf" in subpath_schema_obj:
|
|
185
|
+
for schema in subpath_schema_obj["oneOf"]:
|
|
186
|
+
schema["properties"] = remove_fields(
|
|
187
|
+
schema["properties"],
|
|
188
|
+
[field["field_name"] for field in excluded_payload_fields]
|
|
189
|
+
)
|
|
190
|
+
else:
|
|
191
|
+
subpath_schema_obj["properties"] = remove_fields(
|
|
192
|
+
subpath_schema_obj["properties"],
|
|
193
|
+
[field["field_name"] for field in excluded_payload_fields]
|
|
194
|
+
)
|
|
195
|
+
open(schema_fil, "w").write(json.dumps(subpath_schema_obj) + "\n")
|
|
196
|
+
|
|
197
|
+
# Generat output content file
|
|
198
|
+
data_file = output_subpath / "data.ljson"
|
|
199
|
+
path = os.path.join(spaces_path, space, subpath)
|
|
200
|
+
for file_name in os.listdir(path):
|
|
201
|
+
if not file_name.endswith(".json"):
|
|
202
|
+
continue
|
|
203
|
+
if entries_since:
|
|
204
|
+
payload_ts = int(round(
|
|
205
|
+
os.path.getmtime(os.path.join(path, file_name)) * 1000
|
|
206
|
+
))
|
|
207
|
+
meta_ts = int(round(os.path.getmtime(meta_path(
|
|
208
|
+
space_path,
|
|
209
|
+
subpath,
|
|
210
|
+
file_name.split(".")[0],
|
|
211
|
+
resource_type
|
|
212
|
+
)) * 1000))
|
|
213
|
+
if payload_ts <= entries_since and meta_ts <= entries_since:
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
async with aopen(os.path.join(path, file_name), "r") as f:
|
|
217
|
+
content = await f.read()
|
|
218
|
+
try:
|
|
219
|
+
payload = json.loads(content)
|
|
220
|
+
jsonschema.validate(
|
|
221
|
+
instance=payload, schema=input_subpath_schema_obj
|
|
222
|
+
)
|
|
223
|
+
meta = await get_meta(
|
|
224
|
+
space_path=space_path,
|
|
225
|
+
subpath=subpath,
|
|
226
|
+
file_path=file_name.split(".")[0],
|
|
227
|
+
resource_type=resource_type,
|
|
228
|
+
)
|
|
229
|
+
except Exception:
|
|
230
|
+
# print(f"ERROR: {error.args = }")
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
out = prepare_output(
|
|
234
|
+
meta, payload, included_meta_fields, excluded_payload_fields
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# jsonschema.validate(instance=out, schema=subpath_schema_obj)
|
|
238
|
+
|
|
239
|
+
encrypted_out = enc_dict(out, hashed_data)
|
|
240
|
+
open(data_file, "a").write(json.dumps(encrypted_out) + "\n")
|
|
241
|
+
|
|
242
|
+
open(f"{output_subpath}/hashed_data.json", "w").write(json.dumps(hashed_data))
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
async def main(tasks):
|
|
247
|
+
await asyncio.gather(*tasks)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
if __name__ == "__main__":
|
|
251
|
+
parser = argparse.ArgumentParser()
|
|
252
|
+
parser.add_argument(
|
|
253
|
+
"--config", required=True, help="Json config relative path from the script"
|
|
254
|
+
)
|
|
255
|
+
parser.add_argument(
|
|
256
|
+
"--spaces", required=True, help="Spaces relative path from the script"
|
|
257
|
+
)
|
|
258
|
+
parser.add_argument(
|
|
259
|
+
"--output",
|
|
260
|
+
help="Output relative path from the script (the default path is the current script path",
|
|
261
|
+
)
|
|
262
|
+
parser.add_argument(
|
|
263
|
+
"--since",
|
|
264
|
+
help="Export entries created/updated since the provided timestamp",
|
|
265
|
+
)
|
|
266
|
+
args = parser.parse_args()
|
|
267
|
+
since = None
|
|
268
|
+
output_path = ""
|
|
269
|
+
if args.output:
|
|
270
|
+
output_path = args.output
|
|
271
|
+
|
|
272
|
+
if args.since:
|
|
273
|
+
since = int(round(float(args.since) * 1000))
|
|
274
|
+
|
|
275
|
+
if not os.path.isdir(args.spaces):
|
|
276
|
+
exit_with_error(f"The spaces folder {args.spaces} is not found.")
|
|
277
|
+
|
|
278
|
+
out_path = os.path.join(output_path, OUTPUT_FOLDER_NAME)
|
|
279
|
+
if os.path.isdir(out_path):
|
|
280
|
+
shutil.rmtree(out_path)
|
|
281
|
+
|
|
282
|
+
# con = sqlite3.connect(f"../../exporter_data/output/data.db")
|
|
283
|
+
# cur = con.cursor()
|
|
284
|
+
# cur.execute("DROP TABLE IF EXISTS migrated_data")
|
|
285
|
+
# cur.execute(
|
|
286
|
+
# "CREATE TABLE migrated_data(hash VARCHAR UNIQUE, msisdn VARCHAR UNIQUE)"
|
|
287
|
+
# )
|
|
288
|
+
# print(con)
|
|
289
|
+
|
|
290
|
+
tasks = []
|
|
291
|
+
with open(args.config, "r") as f:
|
|
292
|
+
config_objs = json.load(f)
|
|
293
|
+
|
|
294
|
+
for config_obj in config_objs:
|
|
295
|
+
if not validate_config(config_obj):
|
|
296
|
+
continue
|
|
297
|
+
tasks.append(extract(config_obj.get("space", ""),
|
|
298
|
+
config_obj.get("subpath", ""),
|
|
299
|
+
config_obj.get("resource_type", ""),
|
|
300
|
+
config_obj.get("schema_shortname", ""),
|
|
301
|
+
config_obj.get("included_meta_fields", {}),
|
|
302
|
+
config_obj.get("excluded_payload_fields", {}),
|
|
303
|
+
args.spaces, output_path, since))
|
|
304
|
+
|
|
305
|
+
asyncio.run(main(tasks))
|
|
306
|
+
|
|
307
|
+
print(
|
|
308
|
+
f"Output path: {os.path.abspath(os.path.join(output_path, OUTPUT_FOLDER_NAME))}"
|
|
309
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from firebase_admin import credentials, messaging, initialize_app # type: ignore
|
|
2
|
+
from utils.notification import Notifier
|
|
3
|
+
from utils.helpers import lang_code
|
|
4
|
+
from utils.settings import settings
|
|
5
|
+
from models.core import NotificationData
|
|
6
|
+
|
|
7
|
+
class FirebaseNotifier(Notifier):
|
|
8
|
+
|
|
9
|
+
def _init_connection(self) -> None:
|
|
10
|
+
if not hasattr(self, "_firebase_app"):
|
|
11
|
+
firebase_cred = credentials.Certificate(settings.google_application_credentials)
|
|
12
|
+
self._firebase_app = initialize_app(firebase_cred, name="[DEFAULT]")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def send(
|
|
17
|
+
self,
|
|
18
|
+
data: NotificationData
|
|
19
|
+
) -> bool:
|
|
20
|
+
self._init_connection()
|
|
21
|
+
# Receiver should be user.firebase_token
|
|
22
|
+
if "firebase_token" not in data.receiver:
|
|
23
|
+
raise Exception("Missing token for user shortname:"\
|
|
24
|
+
f"{data.receiver.get('shortname')} - msisdn {data.receiver.get('msisdn')}")
|
|
25
|
+
user_lang = lang_code(data.receiver.get("language", "ar"))
|
|
26
|
+
title = data.title.__getattribute__(user_lang)
|
|
27
|
+
body = data.body.__getattribute__(user_lang)
|
|
28
|
+
image_url = (
|
|
29
|
+
data.image_urls.__getattribute__(user_lang) if data.image_urls else ""
|
|
30
|
+
) or ""
|
|
31
|
+
|
|
32
|
+
alert = messaging.ApsAlert(title = title, body = body)
|
|
33
|
+
aps = messaging.Aps( alert = alert, sound = "default", content_available = True )
|
|
34
|
+
apns = messaging.APNSConfig(
|
|
35
|
+
payload=messaging.APNSPayload(aps),
|
|
36
|
+
fcm_options=messaging.APNSFCMOptions(image=image_url),
|
|
37
|
+
)
|
|
38
|
+
# apns = messaging.APNSConfig( payload = messaging.APNSPayload(aps))
|
|
39
|
+
android_notification_settings = messaging.AndroidNotification(
|
|
40
|
+
priority="high",
|
|
41
|
+
channel_id="FCM_CHANNEL_ID",
|
|
42
|
+
visibility="public",
|
|
43
|
+
image=image_url
|
|
44
|
+
)
|
|
45
|
+
android_config = messaging.AndroidConfig(priority="high", notification=android_notification_settings)
|
|
46
|
+
web_push = messaging.WebpushConfig(headers={"image": image_url})
|
|
47
|
+
message = messaging.Message(
|
|
48
|
+
notification=messaging.Notification(title=title, body=body),
|
|
49
|
+
token=data.receiver["firebase_token"],
|
|
50
|
+
apns=apns,
|
|
51
|
+
android=android_config,
|
|
52
|
+
webpush=web_push,
|
|
53
|
+
data={**data.deep_link, "id": data.entry_id}
|
|
54
|
+
)
|
|
55
|
+
messaging.send(message, app=self._firebase_app)
|
|
56
|
+
return True
|
|
57
|
+
|