dmart 0.1.9__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.
Files changed (149) hide show
  1. alembic/__init__.py +0 -0
  2. alembic/env.py +91 -0
  3. alembic/scripts/__init__.py +0 -0
  4. alembic/scripts/calculate_checksums.py +77 -0
  5. alembic/scripts/migration_f7a4949eed19.py +28 -0
  6. alembic/versions/0f3d2b1a7c21_add_authz_materialized_views.py +87 -0
  7. alembic/versions/10d2041b94d4_last_checksum_history.py +62 -0
  8. alembic/versions/1cf4e1ee3cb8_ext_permission_with_filter_fields_values.py +33 -0
  9. alembic/versions/26bfe19b49d4_rm_failedloginattempts.py +42 -0
  10. alembic/versions/3c8bca2219cc_add_otp_table.py +38 -0
  11. alembic/versions/6675fd9dfe42_remove_unique_from_sessions_table.py +36 -0
  12. alembic/versions/71bc1df82e6a_adding_user_last_login_at.py +43 -0
  13. alembic/versions/74288ccbd3b5_initial.py +264 -0
  14. alembic/versions/7520a89a8467_rm_activesession_table.py +39 -0
  15. alembic/versions/848b623755a4_make_created_nd_updated_at_required.py +138 -0
  16. alembic/versions/8640dcbebf85_add_notes_to_users.py +32 -0
  17. alembic/versions/91c94250232a_adding_fk_on_owner_shortname.py +104 -0
  18. alembic/versions/98ecd6f56f9a_ext_meta_with_owner_group_shortname.py +66 -0
  19. alembic/versions/9aae9138c4ef_indexing_created_at_updated_at.py +80 -0
  20. alembic/versions/__init__.py +0 -0
  21. alembic/versions/b53f916b3f6d_json_to_jsonb.py +492 -0
  22. alembic/versions/eb5f1ec65156_adding_user_locked_to_device.py +36 -0
  23. alembic/versions/f7a4949eed19_adding_query_policies_to_meta.py +60 -0
  24. api/__init__.py +0 -0
  25. api/info/__init__.py +0 -0
  26. api/info/router.py +109 -0
  27. api/managed/__init__.py +0 -0
  28. api/managed/router.py +1541 -0
  29. api/managed/utils.py +1850 -0
  30. api/public/__init__.py +0 -0
  31. api/public/router.py +758 -0
  32. api/qr/__init__.py +0 -0
  33. api/qr/router.py +108 -0
  34. api/user/__init__.py +0 -0
  35. api/user/model/__init__.py +0 -0
  36. api/user/model/errors.py +14 -0
  37. api/user/model/requests.py +165 -0
  38. api/user/model/responses.py +11 -0
  39. api/user/router.py +1401 -0
  40. api/user/service.py +270 -0
  41. bundler.py +44 -0
  42. config/__init__.py +0 -0
  43. config/channels.json +11 -0
  44. config/notification.json +17 -0
  45. data_adapters/__init__.py +0 -0
  46. data_adapters/adapter.py +16 -0
  47. data_adapters/base_data_adapter.py +467 -0
  48. data_adapters/file/__init__.py +0 -0
  49. data_adapters/file/adapter.py +2043 -0
  50. data_adapters/file/adapter_helpers.py +1013 -0
  51. data_adapters/file/archive.py +150 -0
  52. data_adapters/file/create_index.py +331 -0
  53. data_adapters/file/create_users_folders.py +52 -0
  54. data_adapters/file/custom_validations.py +68 -0
  55. data_adapters/file/drop_index.py +40 -0
  56. data_adapters/file/health_check.py +560 -0
  57. data_adapters/file/redis_services.py +1110 -0
  58. data_adapters/helpers.py +27 -0
  59. data_adapters/sql/__init__.py +0 -0
  60. data_adapters/sql/adapter.py +3210 -0
  61. data_adapters/sql/adapter_helpers.py +491 -0
  62. data_adapters/sql/create_tables.py +451 -0
  63. data_adapters/sql/create_users_folders.py +53 -0
  64. data_adapters/sql/db_to_json_migration.py +482 -0
  65. data_adapters/sql/health_check_sql.py +232 -0
  66. data_adapters/sql/json_to_db_migration.py +454 -0
  67. data_adapters/sql/update_query_policies.py +101 -0
  68. data_generator.py +81 -0
  69. dmart-0.1.9.dist-info/METADATA +64 -0
  70. dmart-0.1.9.dist-info/RECORD +149 -0
  71. dmart-0.1.9.dist-info/WHEEL +5 -0
  72. dmart-0.1.9.dist-info/entry_points.txt +2 -0
  73. dmart-0.1.9.dist-info/top_level.txt +23 -0
  74. dmart.py +513 -0
  75. get_settings.py +7 -0
  76. languages/__init__.py +0 -0
  77. languages/arabic.json +15 -0
  78. languages/english.json +16 -0
  79. languages/kurdish.json +14 -0
  80. languages/loader.py +13 -0
  81. main.py +506 -0
  82. migrate.py +24 -0
  83. models/__init__.py +0 -0
  84. models/api.py +203 -0
  85. models/core.py +597 -0
  86. models/enums.py +255 -0
  87. password_gen.py +8 -0
  88. plugins/__init__.py +0 -0
  89. plugins/action_log/__init__.py +0 -0
  90. plugins/action_log/plugin.py +121 -0
  91. plugins/admin_notification_sender/__init__.py +0 -0
  92. plugins/admin_notification_sender/plugin.py +124 -0
  93. plugins/ldap_manager/__init__.py +0 -0
  94. plugins/ldap_manager/plugin.py +100 -0
  95. plugins/local_notification/__init__.py +0 -0
  96. plugins/local_notification/plugin.py +123 -0
  97. plugins/realtime_updates_notifier/__init__.py +0 -0
  98. plugins/realtime_updates_notifier/plugin.py +58 -0
  99. plugins/redis_db_update/__init__.py +0 -0
  100. plugins/redis_db_update/plugin.py +188 -0
  101. plugins/resource_folders_creation/__init__.py +0 -0
  102. plugins/resource_folders_creation/plugin.py +81 -0
  103. plugins/system_notification_sender/__init__.py +0 -0
  104. plugins/system_notification_sender/plugin.py +188 -0
  105. plugins/update_access_controls/__init__.py +0 -0
  106. plugins/update_access_controls/plugin.py +9 -0
  107. pytests/__init__.py +0 -0
  108. pytests/api_user_models_erros_test.py +16 -0
  109. pytests/api_user_models_requests_test.py +98 -0
  110. pytests/archive_test.py +72 -0
  111. pytests/base_test.py +300 -0
  112. pytests/get_settings_test.py +14 -0
  113. pytests/json_to_db_migration_test.py +237 -0
  114. pytests/service_test.py +26 -0
  115. pytests/test_info.py +55 -0
  116. pytests/test_status.py +15 -0
  117. run_notification_campaign.py +98 -0
  118. scheduled_notification_handler.py +121 -0
  119. schema_migration.py +208 -0
  120. schema_modulate.py +192 -0
  121. set_admin_passwd.py +55 -0
  122. sync.py +202 -0
  123. utils/__init__.py +0 -0
  124. utils/access_control.py +306 -0
  125. utils/async_request.py +8 -0
  126. utils/exporter.py +309 -0
  127. utils/firebase_notifier.py +57 -0
  128. utils/generate_email.py +38 -0
  129. utils/helpers.py +352 -0
  130. utils/hypercorn_config.py +12 -0
  131. utils/internal_error_code.py +60 -0
  132. utils/jwt.py +124 -0
  133. utils/logger.py +167 -0
  134. utils/middleware.py +99 -0
  135. utils/notification.py +75 -0
  136. utils/password_hashing.py +16 -0
  137. utils/plugin_manager.py +215 -0
  138. utils/query_policies_helper.py +112 -0
  139. utils/regex.py +44 -0
  140. utils/repository.py +529 -0
  141. utils/router_helper.py +19 -0
  142. utils/settings.py +165 -0
  143. utils/sms_notifier.py +21 -0
  144. utils/social_sso.py +67 -0
  145. utils/templates/activation.html.j2 +26 -0
  146. utils/templates/reminder.html.j2 +17 -0
  147. utils/ticket_sys_utils.py +203 -0
  148. utils/web_notifier.py +29 -0
  149. websocket.py +231 -0
utils/logger.py ADDED
@@ -0,0 +1,167 @@
1
+ import re
2
+ import json
3
+ import logging
4
+ import os
5
+
6
+ from utils.settings import settings
7
+ import socket
8
+
9
+
10
+ class JSONEncoder(json.JSONEncoder):
11
+ def default(self, o):
12
+ return str(o)
13
+
14
+
15
+ COMBINED_SENSITIVE_PATTERN = re.compile(
16
+ r'(?i)(?:'
17
+ r'(your\s+otp\s+code\s+is\s+)\d{4,8}|'
18
+ r'(otp\s+for\s+\d+\s+is\s+)\d{4,8}|'
19
+ r'(zip\s+pw\s+for\s+\d+\s+is\s+)[a-f0-9]{6,}|'
20
+ r'(your\s+password\s+for\s+the\s+export\s+zip\s+is\s+)[a-f0-9]{6,}|'
21
+ r'(invitation=)[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+|'
22
+ r'("pin"\s*:\s*)"[^"]*"|'
23
+ r'("authorization"\s*:\s*)"[^"]*"|'
24
+ r'("auth_token"\s*:\s*)"[^"]*"|'
25
+ r'("firebase_token"\s*:\s*)"[^"]*"|'
26
+ r'("evd-device-id"\s*:\s*)"[^"]*"|'
27
+ r'("access_token"\s*:\s*)"[^"]*"|'
28
+ r'("cookie"\s*:\s*)"[^"]*"|'
29
+ r'("set-cookie"\s*:\s*)"[^"]*"'
30
+ r')'
31
+ )
32
+
33
+
34
+ SENSITIVE_KEYWORDS = ("authorization", "token", "password", "otp", "pin", "cookie", "auth", "firebase_token",
35
+ "evd-device-id")
36
+
37
+
38
+ def mask_replacement(match):
39
+ """Replace matched groups with a general mask."""
40
+ groups = match.groups()
41
+ for i, group in enumerate(groups):
42
+ if group is not None:
43
+ # For JSON format: "key": "value" → "key": "******"
44
+ if '"' in group:
45
+ return group + '"' + ('*' * 6) + '"'
46
+ # For plain format: key: value → key: ******
47
+ else:
48
+ return group + ('*' * 6)
49
+ return match.group(0)
50
+
51
+
52
+ def mask_sensitive_data_string(log_string: str) -> str:
53
+ """Mask sensitive data only if keywords present."""
54
+ if not isinstance(log_string, str):
55
+ return log_string
56
+ # Quick keyword check first (very fast)
57
+ if not any(keyword in log_string.lower() for keyword in SENSITIVE_KEYWORDS):
58
+ return log_string
59
+ # Only apply regex if keywords found
60
+ return COMBINED_SENSITIVE_PATTERN.sub(mask_replacement, log_string)
61
+
62
+
63
+ class CustomFormatter(logging.Formatter):
64
+ """
65
+ Emits one JSON line with this exact key order:
66
+ correlation_id, time, level, message, props, thread, process
67
+ """
68
+ def __init__(self) -> None:
69
+ log_dir = os.path.dirname(settings.log_file)
70
+ if not os.path.exists(log_dir):
71
+ os.mkdir(log_dir)
72
+ super().__init__()
73
+
74
+ def format(self, record):
75
+ correlation_id = getattr(record, "correlation_id", "")
76
+ if correlation_id == "ROOT" and getattr(record, "props", None):
77
+ correlation_id = getattr(record, "props", {})\
78
+ .get("response", {}).get("headers", {}).get("x-correlation-id", "")
79
+
80
+ props = getattr(record, "props", {})
81
+
82
+ # Extract hostname
83
+ hostname = socket.gethostname()
84
+
85
+ data = {
86
+ "hostname": hostname,
87
+ "correlation_id": correlation_id,
88
+ "time": self.formatTime(record),
89
+ "level": record.levelname,
90
+ "message": record.getMessage(),
91
+ "props": props,
92
+ "thread": record.threadName,
93
+ "process": record.process,
94
+ }
95
+ try:
96
+ log_string = json.dumps(data, cls=JSONEncoder)
97
+ masked_log = mask_sensitive_data_string(log_string)
98
+ return masked_log
99
+ except Exception as e:
100
+ return json.dumps({"error": str(e), "message": record.getMessage()})
101
+
102
+
103
+ logging_schema : dict = {
104
+ "version": 1,
105
+ "disable_existing_loggers": False,
106
+ "filters": {
107
+ "correlation_id": {
108
+ "()": "asgi_correlation_id.CorrelationIdFilter",
109
+ "uuid_length": 32,
110
+ "default_value": "ROOT",
111
+ },
112
+ },
113
+ "formatters": {
114
+ "json": {"()": "utils.logger.CustomFormatter"},
115
+ },
116
+ "handlers": {
117
+ "console": {
118
+ "class": "logging.StreamHandler",
119
+ "filters": ["correlation_id"],
120
+ "level": "INFO",
121
+ "formatter": "json",
122
+ "stream": "ext://sys.stdout", # Default is stderr
123
+ },
124
+ "file": {
125
+ "class": "concurrent_log_handler.ConcurrentRotatingFileHandler",
126
+ "filters": ["correlation_id"],
127
+ "filename": settings.log_file,
128
+ "backupCount": 5,
129
+ "maxBytes": 0x10000000,
130
+ "use_gzip": False,
131
+ "formatter": "json",
132
+ },
133
+ },
134
+ "root": {
135
+ "handlers": settings.log_handlers,
136
+ "level": "INFO",
137
+ },
138
+ "loggers": {
139
+ "fastapi": {
140
+ "handlers": settings.log_handlers,
141
+ "level": logging.INFO,
142
+ "propagate": False,
143
+ },
144
+ "hypercorn": {
145
+ "handlers": settings.log_handlers,
146
+ "level": logging.INFO,
147
+ "propagate": False,
148
+ },
149
+ "hypercorn.error": {
150
+ "handlers": settings.log_handlers,
151
+ "level": logging.INFO,
152
+ "propagate": False,
153
+ },
154
+ "hypercorn.access": {
155
+ "handlers": settings.log_handlers,
156
+ "level": logging.INFO,
157
+ "propagate": False,
158
+ },
159
+ },
160
+ }
161
+
162
+
163
+ def changeLogFile(log_file: str | None = None) -> None:
164
+ global logging_schema
165
+ if (log_file and "handlers" in logging_schema and "file" in logging_schema["handlers"]
166
+ and "filename" in logging_schema["handlers"]["file"]):
167
+ logging_schema["handlers"]["file"]["filename"] = log_file
utils/middleware.py ADDED
@@ -0,0 +1,99 @@
1
+ from contextvars import ContextVar
2
+ from typing import Any
3
+ from starlette.types import ASGIApp, Receive, Scope, Send
4
+ from starlette.requests import Request
5
+ from utils.internal_error_code import InternalErrorCode
6
+ from utils.settings import settings
7
+ import models.api as api
8
+ from fastapi import status
9
+
10
+ REQUEST_DATA_CTX_KEY = "request_data"
11
+
12
+ _request_data_ctx_var: ContextVar[dict] = ContextVar(REQUEST_DATA_CTX_KEY, default={})
13
+
14
+ def get_request_data() -> dict:
15
+ return _request_data_ctx_var.get()
16
+
17
+ class CustomRequestMiddleware:
18
+ def __init__(
19
+ self,
20
+ app: ASGIApp,
21
+ ) -> None:
22
+ self.app = app
23
+
24
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
25
+ if scope["type"] not in ["http", "websocket"]:
26
+ try:
27
+ await self.app(scope, receive, send)
28
+ except Exception as _:
29
+ return
30
+
31
+
32
+ request = Request(scope, receive)
33
+ request_headers = {}
34
+ for k,v in request.headers.items():
35
+ if k in ['cookie', 'authorization']:
36
+ continue
37
+ request_headers[k] = v
38
+
39
+ request_data = _request_data_ctx_var.set({
40
+ "request_headers": request_headers,
41
+ })
42
+
43
+ await self.app(scope, receive, send)
44
+
45
+ _request_data_ctx_var.reset(request_data)
46
+
47
+
48
+ class ChannelMiddleware:
49
+ def __init__(
50
+ self,
51
+ app: ASGIApp,
52
+ ) -> None:
53
+ self.app = app
54
+
55
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
56
+ if scope["type"] not in ["http", "websocket"] or not settings.enable_channel_auth:
57
+ await self.app(scope, receive, send)
58
+ return
59
+
60
+ request = Request(scope, receive)
61
+ channel_key = request.headers.get("x-channel-key")
62
+ if not channel_key:
63
+ for channel in settings.channels:
64
+ for pattern in channel["allowed_api_patterns"]:
65
+ if pattern.search(request.scope['path']):
66
+ raise api.Exception(
67
+ status_code=status.HTTP_403_FORBIDDEN,
68
+ error=api.Error(
69
+ type="channel_auth", code=InternalErrorCode.NOT_ALLOWED, message="Requested method or path is forbidden"
70
+ ),
71
+ )
72
+ await self.app(scope, receive, send)
73
+ return
74
+
75
+ request_channel: dict[str, Any] | None = None
76
+ for channel in settings.channels:
77
+ if channel_key in channel.get("keys", []):
78
+ request_channel = channel
79
+ break
80
+
81
+ if not request_channel:
82
+ raise api.Exception(
83
+ status_code=status.HTTP_403_FORBIDDEN,
84
+ error=api.Error(
85
+ type="channel_auth", code=InternalErrorCode.NOT_ALLOWED, message="Requested method or path is forbidden [2]"
86
+ ),
87
+ )
88
+
89
+ for pattern in request_channel["allowed_api_patterns"]:
90
+ if pattern.search(request.scope['path']):
91
+ await self.app(scope, receive, send)
92
+ return
93
+
94
+ raise api.Exception(
95
+ status_code=status.HTTP_403_FORBIDDEN,
96
+ error=api.Error(
97
+ type="channel_auth", code=InternalErrorCode.NOT_ALLOWED, message="Requested method or path is forbidden [3]"
98
+ ),
99
+ )
utils/notification.py ADDED
@@ -0,0 +1,75 @@
1
+ from abc import ABC, abstractmethod
2
+ from importlib.util import find_spec, module_from_spec
3
+ import json
4
+ import sys
5
+ from typing import Any
6
+ from pathlib import Path
7
+
8
+ from models.core import NotificationData
9
+ from utils.settings import settings
10
+ from models.core import User
11
+ from data_adapters.adapter import data_adapter as db
12
+ from fastapi.logger import logger
13
+
14
+
15
+ class Notifier(ABC):
16
+
17
+ @abstractmethod
18
+ async def send(self, data: NotificationData) -> bool:
19
+ pass
20
+
21
+ async def _load_user(self, shortname: str) -> Any:
22
+ if not hasattr(self, "user"):
23
+
24
+ self.user = await db.load(
25
+ space_name=settings.management_space,
26
+ subpath=settings.users_subpath,
27
+ shortname=shortname,
28
+ class_type=User,
29
+ user_shortname="__system__",
30
+ )
31
+ return self.user
32
+
33
+
34
+ class NotificationManager:
35
+
36
+ notifiers: dict[str, Notifier] = {}
37
+
38
+ def __init__(self) -> None:
39
+ # Load the notifiers depending on config/notification.json file
40
+ if not self.notifiers:
41
+ config_path = Path(__file__).resolve().parent.parent / "config/notification.json"
42
+ if config_path.exists():
43
+ with open(config_path) as conf_file:
44
+ conf_data = json.load(conf_file)
45
+
46
+ for platform, data in conf_data.items():
47
+ if not data["active"]:
48
+ continue
49
+
50
+ module_name = data["module"]
51
+ module_specs = find_spec(module_name)
52
+ if not module_specs or not module_specs.loader:
53
+ continue
54
+ module = module_from_spec(module_specs)
55
+ sys.modules[module_name] = module
56
+ module_specs.loader.exec_module(module)
57
+ self.notifiers[platform] = getattr(module, data["class"])()
58
+
59
+ async def send(self, platform, data: NotificationData) -> bool:
60
+ if platform not in self.notifiers:
61
+ return False
62
+ try:
63
+ await self.notifiers[platform].send(data)
64
+ return True
65
+ except Exception as e:
66
+ logger.warning(
67
+ "Notification",
68
+ extra={
69
+ "props": {
70
+ "title": f"FAIL at {self.notifiers[platform]}.send",
71
+ "message": str(e),
72
+ }
73
+ },
74
+ )
75
+ return False
@@ -0,0 +1,16 @@
1
+ from argon2 import PasswordHasher
2
+
3
+ ph = PasswordHasher(
4
+ memory_cost=102400,
5
+ time_cost=1,
6
+ parallelism=8
7
+ )
8
+
9
+ def verify_password(plain_password: str, hashed_password: str):
10
+ try:
11
+ return ph.verify(hashed_password, plain_password)
12
+ except Exception:
13
+ return False
14
+
15
+ def hash_password(password: str):
16
+ return ph.hash(password)
@@ -0,0 +1,215 @@
1
+ import asyncio
2
+ import os
3
+ import sys
4
+ from importlib.util import find_spec, module_from_spec
5
+ from inspect import iscoroutine
6
+ from pathlib import Path
7
+ import aiofiles
8
+ from fastapi import Depends, FastAPI
9
+ from fastapi.logger import logger
10
+ from models.core import (
11
+ ActionType,
12
+ PluginWrapper,
13
+ PluginBase,
14
+ Event,
15
+ EventFilter,
16
+ EventListenTime,
17
+ )
18
+ from models.enums import ResourceType, PluginType
19
+ from utils.settings import settings
20
+
21
+ CUSTOM_PLUGINS_PATH = settings.spaces_folder / "custom_plugins"
22
+
23
+ # Allow python to search for modules inside the custom plugins
24
+ # be including the path to the parent folder of the custom plugins to sys.path
25
+ back_out_of_project = 2
26
+ back_to_spaces = 0
27
+
28
+ for part in CUSTOM_PLUGINS_PATH.parts:
29
+ if part == "..":
30
+ back_to_spaces += 1
31
+
32
+ if __file__.endswith(".pyc"):
33
+ back_out_of_project += 1
34
+
35
+ sys.path.append(
36
+ "/".join(__file__.split("/")[:-(back_out_of_project+back_to_spaces)]) +
37
+ "/" +
38
+ "/".join(CUSTOM_PLUGINS_PATH.parts[back_to_spaces:-1])
39
+ )
40
+
41
+ class PluginManager:
42
+
43
+ plugins_wrappers: dict[
44
+ ActionType, list[PluginWrapper]
45
+ ] = {} # {action_type: list_of_plugins_wrappers]}
46
+
47
+ async def load_plugins(self, app: FastAPI, capture_body):
48
+ # Load core plugins
49
+ current = Path(__file__).resolve().parent
50
+ parent = current.parent
51
+ path = parent / "plugins"
52
+ if path.is_dir():
53
+ await self.load_path_plugins(path, app, capture_body)
54
+
55
+ # Load custom plugins
56
+ path = CUSTOM_PLUGINS_PATH
57
+ if path.is_dir():
58
+ await self.load_path_plugins(path, app, capture_body)
59
+ self.sort_plugins()
60
+
61
+
62
+ async def load_path_plugins(self, path: Path, app: FastAPI, capture_body):
63
+
64
+ plugins_iterator = os.scandir(path)
65
+ for plugin_path in plugins_iterator:
66
+ config_file_path = Path(f"{plugin_path.path}/config.json")
67
+ plugin_file_path = Path(f"{plugin_path.path}/plugin.py")
68
+ if(
69
+ not config_file_path.is_file() or
70
+ not plugin_file_path.is_file()
71
+ ):
72
+ continue
73
+
74
+ # Load plugin config file
75
+ async with aiofiles.open(config_file_path, "r") as config_file:
76
+ plugin_wrapper: PluginWrapper = PluginWrapper.model_validate_json(
77
+ await config_file.read()
78
+ )
79
+ plugin_wrapper.shortname = plugin_path.name
80
+ if not plugin_wrapper.is_active:
81
+ continue
82
+ # Load the plugin module
83
+ module_name = f"{path.parts[-1]}.{plugin_wrapper.shortname}.plugin"
84
+ spec = find_spec(module_name)
85
+ if not spec:
86
+ continue
87
+ module = module_from_spec(spec)
88
+ sys.modules[module_name] = module
89
+ if not spec.loader:
90
+ continue
91
+ spec.loader.exec_module(module)
92
+ try:
93
+ # Register the API plugin routes
94
+ if plugin_wrapper.type == PluginType.api:
95
+ app.include_router(
96
+ module.router,
97
+ prefix=f"/{plugin_wrapper.shortname}",
98
+ tags=[plugin_wrapper.shortname],
99
+ dependencies=[Depends(capture_body)],
100
+ )
101
+
102
+ # Add the Hook Plugin to the loaded plugins
103
+ elif plugin_wrapper.type == PluginType.hook:
104
+ plugin_wrapper.object = getattr(module, "Plugin")()
105
+
106
+ self.store_plugin_in_its_action_dict(plugin_wrapper)
107
+ except Exception as e:
108
+ logger.error(
109
+ f"PLUGIN_ERROR, PLUGIN API {plugin_wrapper.shortname} Failed to load, error: {e.args}"
110
+ )
111
+
112
+ plugins_iterator.close()
113
+
114
+ def store_plugin_in_its_action_dict(self, plugin_wrapper: PluginWrapper):
115
+ if plugin_wrapper.filters:
116
+ for action in plugin_wrapper.filters.actions:
117
+ self.plugins_wrappers.setdefault(action, []).append(plugin_wrapper)
118
+
119
+ def sort_plugins(self):
120
+ """Sort plugins based on plugin_wrapper.ordinal"""
121
+
122
+ for action_type, plugins in self.plugins_wrappers.items():
123
+ self.plugins_wrappers[action_type] = sorted(
124
+ plugins, key=lambda x: x.ordinal
125
+ )
126
+
127
+ def matched_filters(self, plugin_filters: EventFilter, event: Event):
128
+ formats_of_subpath = [event.subpath]
129
+ if event.subpath and event.subpath[0] == "/":
130
+ formats_of_subpath.append(event.subpath[1:])
131
+ else:
132
+ formats_of_subpath.append(f"/{event.subpath}")
133
+
134
+ if "__ALL__" not in plugin_filters.subpaths and not any(
135
+ subpath in plugin_filters.subpaths for subpath in formats_of_subpath
136
+ ):
137
+ return False
138
+
139
+ if event.resource_type == ResourceType.content and (
140
+ "__ALL__" not in plugin_filters.schema_shortnames
141
+ and event.schema_shortname not in plugin_filters.schema_shortnames
142
+ ):
143
+ return False
144
+
145
+ if (
146
+ plugin_filters.resource_types
147
+ and "__ALL__" not in plugin_filters.resource_types
148
+ and event.resource_type not in plugin_filters.resource_types
149
+ ):
150
+ return False
151
+
152
+ return True
153
+
154
+ async def before_action(self, event: Event):
155
+ if event.action_type not in self.plugins_wrappers:
156
+ return
157
+
158
+ from data_adapters.adapter import data_adapter as db
159
+ space = await db.fetch_space(event.space_name)
160
+ if space is None:
161
+ return
162
+ space_plugins = space.active_plugins
163
+
164
+ loop = asyncio.get_event_loop()
165
+ for plugin_model in self.plugins_wrappers[event.action_type]:
166
+ if (
167
+ plugin_model.shortname in space_plugins
168
+ and plugin_model.listen_time == EventListenTime.before
169
+ and plugin_model.filters
170
+ and self.matched_filters(plugin_model.filters, event)
171
+ ):
172
+ try:
173
+ object = plugin_model.object
174
+ if isinstance(object, PluginBase):
175
+ plugin_execution = object.hook(event)
176
+ if iscoroutine(plugin_execution):
177
+ loop.create_task(plugin_execution)
178
+ except Exception as e:
179
+ # print(f"Plugin:{plugin_model}:{str(e)}")
180
+ logger.error(f"Plugin:{plugin_model}:{str(e)}")
181
+
182
+ async def after_action(self, event: Event):
183
+ if event.action_type not in self.plugins_wrappers:
184
+ return
185
+
186
+ from data_adapters.adapter import data_adapter as db
187
+ space = await db.fetch_space(event.space_name)
188
+ if space is None:
189
+ return
190
+ space_plugins = space.active_plugins
191
+
192
+ loop = asyncio.get_event_loop()
193
+ _plugin_model = None
194
+ try:
195
+ for plugin_model in self.plugins_wrappers[event.action_type]:
196
+ _plugin_model = plugin_model
197
+ if (
198
+ plugin_model.shortname in space_plugins
199
+ and plugin_model.listen_time == EventListenTime.after
200
+ and plugin_model.filters
201
+ and self.matched_filters(plugin_model.filters, event)
202
+ ):
203
+ try:
204
+ object = plugin_model.object
205
+ if isinstance(object, PluginBase):
206
+ plugin_execution = object.hook(event)
207
+ if iscoroutine(plugin_execution):
208
+ loop.create_task(plugin_execution)
209
+ except Exception as e:
210
+ logger.error(f"Plugin:{plugin_model}:{str(e)}")
211
+ except Exception as e:
212
+ logger.error(f"Plugin:{_plugin_model}:{str(e)}")
213
+
214
+
215
+ plugin_manager = PluginManager()
@@ -0,0 +1,112 @@
1
+ from models.enums import ResourceType, ConditionType
2
+ from utils.settings import settings
3
+
4
+ def generate_query_policies(
5
+ space_name: str,
6
+ subpath: str,
7
+ resource_type: str,
8
+ is_active: bool,
9
+ owner_shortname: str,
10
+ owner_group_shortname: str | None,
11
+ entry_shortname: str | None = None,
12
+ ) -> list:
13
+ subpath_parts = ["/"]
14
+ subpath_parts += subpath.strip("/").split("/")
15
+
16
+ if resource_type == ResourceType.folder and entry_shortname:
17
+ subpath_parts.append(entry_shortname)
18
+
19
+ query_policies: list = []
20
+ full_subpath = ""
21
+ for subpath_part in subpath_parts:
22
+ full_subpath += subpath_part
23
+ query_policies.append(
24
+ f"{space_name}:{full_subpath.strip('/')}:{resource_type}:{str(is_active).lower()}:{owner_shortname}"
25
+ )
26
+ if owner_group_shortname is None:
27
+ query_policies.append(
28
+ f"{space_name}:{full_subpath.strip('/')}:{resource_type}:{str(is_active).lower()}"
29
+ )
30
+ else:
31
+ query_policies.append(
32
+ f"{space_name}:{full_subpath.strip('/')}:{resource_type}:{str(is_active).lower()}:{owner_group_shortname}"
33
+ )
34
+
35
+ full_subpath_parts = full_subpath.split("/")
36
+ if len(full_subpath_parts) > 1:
37
+ subpath_with_magic_keyword = (
38
+ "/".join(full_subpath_parts[:1]) + "/" + settings.all_subpaths_mw
39
+ )
40
+ if len(full_subpath_parts) > 2:
41
+ subpath_with_magic_keyword += "/" + "/".join(full_subpath_parts[2:])
42
+ query_policies.append(
43
+ f"{space_name}:{subpath_with_magic_keyword.strip('/')}:{resource_type}:{str(is_active).lower()}"
44
+ )
45
+
46
+ if full_subpath == "/":
47
+ full_subpath = ""
48
+ else:
49
+ full_subpath += "/"
50
+
51
+ return query_policies
52
+
53
+
54
+ async def get_user_query_policies(
55
+ db,
56
+ user_shortname: str,
57
+ space_name: str,
58
+ subpath: str,
59
+ is_space: bool = False,
60
+ ) -> list:
61
+ """
62
+ Generate list of query policies based on user's permissions
63
+ ex: [
64
+ "products:offers:content:true:admin_shortname", # IF conditions = {"is_active", "own"}
65
+ "products:offers:content:true:*", # IF conditions = {"is_active"}
66
+ "products:offers:content:false:admin_shortname|products:offers:content:true:admin_shortname",
67
+ # ^^^ IF conditions = {"own"}
68
+ "products:offers:content:*", # IF conditions = {}
69
+ ]
70
+ """
71
+ user_permissions = await db.get_user_permissions(user_shortname)
72
+ user_groups = (await db.load_user_meta(user_shortname)).groups or []
73
+ user_groups.append(user_shortname)
74
+
75
+ filtered_permissions = {
76
+ perm_key: permission
77
+ for perm_key, permission in user_permissions.items()
78
+ if (
79
+ is_space or
80
+ perm_key.startswith(f'{space_name}:{subpath.lstrip("/")}') or
81
+ perm_key.startswith(f'{space_name}:__all_subpaths__') or
82
+ perm_key.startswith(settings.all_spaces_mw)
83
+ )
84
+ and 'query' in permission.get('allowed_actions', [])
85
+ }
86
+
87
+ sql_query_policies = []
88
+ for perm_key, permission in filtered_permissions.items():
89
+ perm_key = perm_key.replace(settings.all_spaces_mw, space_name)
90
+ perm_key = perm_key.replace(settings.all_subpaths_mw, subpath.strip("/"))
91
+ perm_key = perm_key.strip("/")
92
+ if (
93
+ ConditionType.is_active in permission["conditions"]
94
+ and ConditionType.own in permission["conditions"]
95
+ ):
96
+ for user_group in user_groups:
97
+ sql_query_policies.append(f"{perm_key}:true:{user_group}")
98
+ elif ConditionType.is_active in permission["conditions"]:
99
+ sql_query_policies.append(f"{perm_key}:true:*")
100
+ elif ConditionType.own in permission["conditions"]:
101
+ for user_group in user_groups:
102
+ if settings.active_data_db == 'file':
103
+ sql_query_policies.append(
104
+ f"{perm_key}:true:{user_shortname}|{perm_key}:false:{user_group}"
105
+ )
106
+ else:
107
+ sql_query_policies.append(f"{perm_key}:true:{user_shortname}")
108
+ sql_query_policies.append(f"{perm_key}:false:{user_shortname}")
109
+
110
+ else:
111
+ sql_query_policies.append(f"{perm_key}:*")
112
+ return sql_query_policies