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
models/enums.py ADDED
@@ -0,0 +1,255 @@
1
+ from enum import StrEnum
2
+ import redis.commands.search.reducers as reducers
3
+
4
+
5
+ class RequestType(StrEnum):
6
+ create = "create"
7
+ update = "update"
8
+ patch = "patch"
9
+ update_acl = "update_acl"
10
+ assign = "assign"
11
+ delete = "delete"
12
+ move = "move"
13
+
14
+
15
+ class Language(StrEnum):
16
+ ar = "arabic"
17
+ en = "english"
18
+ ku = "kurdish"
19
+ fr = "french"
20
+ tr = "turkish"
21
+
22
+ @staticmethod
23
+ def code(lang_str):
24
+ codes = {
25
+ "arabic": "ar",
26
+ "english": "en",
27
+ "kurdish": "ku",
28
+ "french": "fr",
29
+ "turkish": "tr",
30
+ }
31
+ return codes[lang_str]
32
+
33
+
34
+ class PublicSubmitResourceType(StrEnum):
35
+ content = "content"
36
+ ticket = "ticket"
37
+
38
+
39
+ class ResourceType(StrEnum):
40
+ user = "user"
41
+ group = "group"
42
+ folder = "folder"
43
+ schema = "schema"
44
+ content = "content"
45
+ log = "log"
46
+ acl = "acl"
47
+ comment = "comment"
48
+ media = "media"
49
+ data_asset = "data_asset"
50
+ locator = "locator"
51
+ relationship = "relationship"
52
+ alteration = "alteration"
53
+ history = "history"
54
+ space = "space"
55
+ permission = "permission"
56
+ role = "role"
57
+ ticket = "ticket"
58
+ json = "json"
59
+ lock = "lock"
60
+ post = "post"
61
+ reaction = "reaction"
62
+ reply = "reply"
63
+ share = "share"
64
+ plugin_wrapper = "plugin_wrapper"
65
+ notification = "notification"
66
+ csv = "csv"
67
+ jsonl = "jsonl"
68
+ sqlite = "sqlite"
69
+ duckdb = "duckdb"
70
+ parquet = "parquet"
71
+
72
+
73
+ class DataAssetType(StrEnum):
74
+ csv = "csv"
75
+ jsonl = "jsonl"
76
+ sqlite = "sqlite"
77
+ duckdb = "duckdb"
78
+ parquet = "parquet"
79
+
80
+
81
+ class AttachmentType(StrEnum):
82
+ reaction = "reaction"
83
+ share = "share"
84
+ json = "json"
85
+ reply = "reply"
86
+ comment = "comment"
87
+ lock = "lock"
88
+ media = "media"
89
+ data_asset = "data_asset"
90
+ relationship = "relationship"
91
+ alteration = "alteration"
92
+
93
+
94
+ class ContentType(StrEnum):
95
+ text = "text"
96
+ comment = "comment"
97
+ reaction = "reaction"
98
+ markdown = "markdown"
99
+ html = "html"
100
+ json = "json"
101
+ image = "image"
102
+ python = "python"
103
+ pdf = "pdf"
104
+ audio = "audio"
105
+ video = "video"
106
+ csv = "csv"
107
+ parquet = "parquet"
108
+ jsonl = "jsonl"
109
+ apk = "apk"
110
+ duckdb = "duckdb"
111
+ sqlite = "sqlite"
112
+
113
+ @staticmethod
114
+ def inline_types() -> list:
115
+ return [ContentType.text, ContentType.markdown, ContentType.html]
116
+
117
+
118
+ class TaskType(StrEnum):
119
+ query = "query"
120
+
121
+
122
+ class UserType(StrEnum):
123
+ web = "web"
124
+ mobile = "mobile"
125
+ bot = "bot"
126
+
127
+
128
+ class ValidationEnum(StrEnum):
129
+ valid = "valid"
130
+ invalid = "invalid"
131
+
132
+
133
+ class LockAction(StrEnum):
134
+ fetch = "fetch"
135
+ lock = "lock"
136
+ extend = "extend"
137
+ unlock = "unlock"
138
+ cancel = "cancel"
139
+
140
+
141
+ class NotificationType(StrEnum):
142
+ admin = "admin"
143
+ system = "system"
144
+
145
+
146
+ class NotificationPriority(StrEnum):
147
+ high = "high"
148
+ medium = "medium"
149
+ low = "low"
150
+
151
+
152
+ class StrBoolEnum(StrEnum):
153
+ true = "yes"
154
+ false = "no"
155
+
156
+
157
+ class ActionType(StrEnum):
158
+ query = "query"
159
+ view = "view"
160
+ update = "update"
161
+ create = "create"
162
+ delete = "delete"
163
+ attach = "attach"
164
+ assign = "assign"
165
+ move = "move"
166
+ progress_ticket = "progress_ticket"
167
+ lock = "lock"
168
+ unlock = "unlock"
169
+
170
+
171
+ class ConditionType(StrEnum):
172
+ is_active = "is_active"
173
+ own = "own"
174
+
175
+
176
+ class PluginType(StrEnum):
177
+ hook = "hook"
178
+ api = "api"
179
+
180
+
181
+ class EventListenTime(StrEnum):
182
+ before = "before"
183
+ after = "after"
184
+
185
+
186
+ class QueryType(StrEnum):
187
+ search = "search"
188
+ subpath = "subpath"
189
+ events = "events"
190
+ history = "history"
191
+ tags = "tags"
192
+ random = "random"
193
+ spaces = "spaces"
194
+ counters = "counters"
195
+ reports = "reports"
196
+ aggregation = "aggregation"
197
+ attachments = "attachments"
198
+ attachments_aggregation = "attachments_aggregation"
199
+
200
+
201
+ class SortType(StrEnum):
202
+ ascending = "ascending"
203
+ descending = "descending"
204
+
205
+
206
+ class Status(StrEnum):
207
+ success = "success"
208
+ failed = "failed"
209
+
210
+
211
+ class RedisReducerName(StrEnum):
212
+ count_distinct = "count_distinct"
213
+ r_count = "r_count"
214
+ # Same as COUNT_DISTINCT - but provide an approximation instead of an exact count,
215
+ # at the expense of less memory and CPU in big groups
216
+ count_distinctish = "count_distinctish"
217
+ sum = "sum"
218
+ min = "min"
219
+ max = "max"
220
+ avg = "avg"
221
+ # Returns all the matched properties in a list
222
+ tolist = "tolist"
223
+ quantile = "quantile"
224
+ stddev = "stddev"
225
+ # Selects the first value within the group according to sorting parameters
226
+ first_value = "first_value"
227
+ # Returns a random sample of items from the dataset, from the given property
228
+ random_sample = "random_sample"
229
+
230
+ @staticmethod
231
+ def mapper(red: str):
232
+ map = {
233
+ "r_count": reducers.count,
234
+ "count_distinct": reducers.count_distinct,
235
+ "count_distinctish": reducers.count_distinctish,
236
+ "sum": reducers.sum,
237
+ "min": reducers.min,
238
+ "max": reducers.max,
239
+ "avg": reducers.avg,
240
+ "tolist": reducers.tolist,
241
+ "quantile": reducers.quantile,
242
+ "stddev": reducers.stddev,
243
+ "first_value": reducers.first_value,
244
+ "random_sample": reducers.random_sample,
245
+ }
246
+ return map[red]
247
+
248
+
249
+ class ReactionType(StrEnum):
250
+ like = "like"
251
+ dislike = "dislike"
252
+ love = "love"
253
+ care = "care"
254
+ laughing = "laughing"
255
+ sad = "sad"
password_gen.py ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from utils.password_hashing import hash_password
4
+
5
+ print("Enter the password: ", end="")
6
+ password = input()
7
+ print("Generated hash: ", end="")
8
+ print(hash_password(password))
plugins/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,121 @@
1
+ import sys
2
+ from typing import Any
3
+ import aiofiles
4
+ from utils.middleware import get_request_data
5
+ from models.core import ActionType, PluginBase, Event, Payload
6
+ from models.enums import ContentType, ResourceType
7
+ from models.core import Action, Locator, Meta
8
+ from utils.helpers import camel_case
9
+ from utils.settings import settings
10
+ from datetime import datetime
11
+ from fastapi.logger import logger
12
+ from data_adapters.adapter import data_adapter as db
13
+
14
+
15
+ class Plugin(PluginBase):
16
+ async def hook(self, data: Event):
17
+ if settings.active_data_db == "sql":
18
+ return
19
+
20
+ if (
21
+ not isinstance(data.shortname, str)
22
+ or not isinstance(data.action_type, ActionType)
23
+ or not isinstance(data.resource_type, ResourceType)
24
+ or not isinstance(data.attributes, dict)
25
+ ):
26
+ logger.warning("invalid data at action_log")
27
+ return
28
+
29
+ class_type = getattr(
30
+ sys.modules["models.core"], camel_case(ResourceType(data.resource_type))
31
+ )
32
+
33
+ if data.action_type == ActionType.delete:
34
+ entry = data.attributes["entry"]
35
+ else:
36
+ entry = await db.load_or_none(
37
+ space_name=data.space_name,
38
+ subpath=data.subpath,
39
+ shortname=data.shortname,
40
+ class_type=class_type,
41
+ user_shortname=data.user_shortname,
42
+ )
43
+
44
+ if entry is None:
45
+ return
46
+
47
+ action_attributes = {}
48
+ if data.action_type == ActionType.create:
49
+ payload: dict[str,Any] = {}
50
+ if(
51
+ entry.payload and
52
+ isinstance(entry.payload, Payload) and
53
+ entry.payload.content_type and
54
+ entry.payload.content_type == ContentType.json
55
+ and entry.payload.body
56
+ ):
57
+ mypayload = await db.load_resource_payload(
58
+ space_name=data.space_name,
59
+ subpath=data.subpath,
60
+ filename=entry.payload.body if isinstance(entry.payload.body, str) else data.shortname,
61
+ class_type=class_type,
62
+ )
63
+ payload = mypayload if mypayload else {}
64
+ action_attributes = self.generate_create_event_attributes(entry, payload)
65
+
66
+ elif data.action_type == ActionType.update:
67
+ action_attributes = data.attributes.get("history_diff", {})
68
+
69
+ action_attributes = {**action_attributes, **get_request_data()}
70
+ action_attributes.pop("_sa_instance_state", None)
71
+ event_obj = Action(
72
+ resource=Locator(
73
+ uuid=entry.uuid,
74
+ type=data.resource_type,
75
+ space_name=data.space_name,
76
+ subpath=data.subpath,
77
+ shortname=data.shortname,
78
+ displayname=entry.displayname,
79
+ description=entry.description,
80
+ tags=entry.tags,
81
+ ),
82
+ user_shortname=data.user_shortname,
83
+ request=data.action_type,
84
+ timestamp=datetime.now(),
85
+ attributes=action_attributes,
86
+ )
87
+
88
+ events_file_path = (
89
+ settings.spaces_folder
90
+ / data.space_name
91
+ / ".dm"
92
+ )
93
+ events_file_path.mkdir(parents=True, exist_ok=True)
94
+ events_file_path = events_file_path / "events.jsonl"
95
+
96
+ # Remove binary content in the event object before serializing to json
97
+ if isinstance(event_obj.attributes, dict) and "media" in event_obj.attributes:
98
+ del event_obj.attributes["media"]
99
+
100
+ file_content = (
101
+ f"{event_obj.model_dump_json()}\n"
102
+ )
103
+ async with aiofiles.open(events_file_path, "a") as events_file:
104
+ await events_file.write(file_content)
105
+
106
+ def generate_create_event_attributes(self, entry: Meta, attributes: dict):
107
+ generated_attributes = {}
108
+ for key, value in entry.__dict__.items():
109
+ if key not in Meta.model_fields:
110
+ generated_attributes[key] = value
111
+
112
+ if entry.payload:
113
+ if isinstance(entry.payload, Payload):
114
+ generated_attributes["payload"] = entry.payload.model_dump()
115
+ else:
116
+ generated_attributes["payload"] = entry.payload
117
+
118
+ if attributes:
119
+ generated_attributes["payload"]["body"] = attributes
120
+
121
+ return generated_attributes
File without changes
@@ -0,0 +1,124 @@
1
+ import json
2
+ from sys import modules as sys_modules
3
+
4
+ from models import api
5
+ from models.core import Notification, NotificationData, PluginBase, Event, Translation
6
+ from utils.helpers import camel_case
7
+ from utils.notification import NotificationManager
8
+ from utils.settings import settings
9
+ from fastapi.logger import logger
10
+ from data_adapters.adapter import data_adapter as db
11
+
12
+ class Plugin(PluginBase):
13
+ async def hook(self, data: Event):
14
+ """
15
+ after creating a new admin notification request
16
+ 1- get the notification request
17
+ 2- if it's scheduled for later, ignore it and let the cron job handle it
18
+ 3- else send it to all its msisdns
19
+ """
20
+
21
+ # Type narrowing for PyRight
22
+ if not isinstance(data.shortname, str):
23
+ logger.warning(
24
+ "data.shortname is None and str is required at system_notification_sender"
25
+ )
26
+ return
27
+
28
+ notification_request_meta = await db.load(
29
+ data.space_name,
30
+ data.subpath,
31
+ data.shortname,
32
+ getattr(sys_modules["models.core"], camel_case(data.resource_type)),
33
+ data.user_shortname,
34
+ )
35
+
36
+ notification_dict = notification_request_meta.dict()
37
+ notification_dict["subpath"] = data.subpath
38
+ notification_request_payload = await db.get_payload_from_event(data)
39
+
40
+ notification_dict.update(notification_request_payload)
41
+
42
+ if not notification_dict or notification_dict.get("scheduled_at", False):
43
+ return
44
+
45
+ # Get msisdns users
46
+ search_criteria = notification_dict.get('search_string', '')
47
+ if not search_criteria:
48
+ search_criteria = '@msisdn:' + '|'.join(notification_dict.get('msisdns', ''))
49
+
50
+ total, receivers = await db.query(api.Query(
51
+ space_name=data.space_name,
52
+ subpath=notification_dict['subpath'],
53
+ filters={},
54
+ search=search_criteria,
55
+ limit=10000,
56
+ offset=0
57
+ ))
58
+ if total == 0:
59
+ return
60
+
61
+ sub_receivers: dict = receivers[0].model_dump()
62
+
63
+ receivers_shortnames = set()
64
+ for receiver in sub_receivers["data"]:
65
+ receivers_shortnames.add(json.loads(receiver)["shortname"])
66
+
67
+ # await send_notification(
68
+ # notification_dict=notification_dict,
69
+ # receivers=receivers_shortnames
70
+ # )
71
+ notification_manager = NotificationManager()
72
+ formatted_req = await self.prepare_request(notification_dict)
73
+ for receiver in set(receivers_shortnames):
74
+ if not formatted_req["push_only"]:
75
+ notification_obj = await Notification.from_request(notification_dict)
76
+ await db.internal_save_model(
77
+ "personal",
78
+ f"people/{receiver}/notifications",
79
+ notification_obj,
80
+ )
81
+ for platform in formatted_req["platforms"]:
82
+ await notification_manager.send(
83
+ platform=platform,
84
+ data=NotificationData(
85
+ receiver=receiver,
86
+ title=formatted_req["title"],
87
+ body=formatted_req["body"],
88
+ image_urls=formatted_req["images_urls"],
89
+ ),
90
+ )
91
+
92
+
93
+ notification_request_payload["status"] = "finished"
94
+ await db.save_payload_from_json(
95
+ space_name=data.space_name,
96
+ subpath=data.subpath,
97
+ meta=notification_request_meta,
98
+ payload_data=notification_request_payload,
99
+ )
100
+
101
+ async def prepare_request(self, notification_dict) -> dict:
102
+ # Get Notification Request Images
103
+ attachments_path = (
104
+ settings.spaces_folder
105
+ / f"{settings.management_space}/"
106
+ f"{notification_dict['subpath']}/.dm/{notification_dict['shortname']}"
107
+ )
108
+ notification_attachments = await db.get_entry_attachments(
109
+ subpath=f"{notification_dict['subpath']}/{notification_dict['shortname']}",
110
+ attachments_path=attachments_path,
111
+ )
112
+ notification_images = {
113
+ "en": notification_attachments.get("media", {}).get("en"),
114
+ "ar": notification_attachments.get("media", {}).get("ar"),
115
+ "ku": notification_attachments.get("media", {}).get("ku"),
116
+ }
117
+
118
+ return {
119
+ "platforms": notification_dict["types"],
120
+ "title": Translation(**notification_dict["displayname"]),
121
+ "body": Translation(**notification_dict["description"]),
122
+ "images_urls": Translation(**notification_images),
123
+ "push_only": notification_dict.get("push_only", False),
124
+ }
File without changes
@@ -0,0 +1,100 @@
1
+ from fastapi.logger import logger
2
+ from models.core import Event, PluginBase, User
3
+ from models.enums import ActionType
4
+ from utils.settings import settings
5
+ from data_adapters.adapter import data_adapter as db
6
+
7
+ from ldap3 import AUTO_BIND_NO_TLS, MODIFY_REPLACE, Server, Connection, ALL
8
+
9
+
10
+ class Plugin(PluginBase):
11
+
12
+ def __init__(self) -> None:
13
+ super().__init__()
14
+ try:
15
+ server = Server(settings.ldap_url, get_info=ALL)
16
+ self.conn = Connection(
17
+ server,
18
+ user=settings.ldap_admin_dn,
19
+ password=settings.ldap_pass,
20
+ auto_bind=AUTO_BIND_NO_TLS
21
+ )
22
+ except Exception:
23
+ logger.error(
24
+ "Failed to connect to LDAP"
25
+ )
26
+
27
+
28
+
29
+ async def hook(self, data: Event):
30
+ if not hasattr(self, "conn"):
31
+ return
32
+
33
+ # Type narrowing for PyRight
34
+ if not isinstance(data.shortname, str):
35
+ logger.warning(
36
+ "data.shortname is None and str is required at ldap_manager"
37
+ )
38
+ return
39
+
40
+ if data.action_type == ActionType.delete:
41
+ self.delete(data.shortname)
42
+ return
43
+
44
+ user_model: User = await db.load(
45
+ space_name=settings.management_space,
46
+ subpath=data.subpath,
47
+ shortname=data.shortname,
48
+ class_type=User
49
+ )
50
+
51
+ if data.action_type == ActionType.create:
52
+ self.add(data.shortname, user_model)
53
+
54
+ elif data.action_type == ActionType.update:
55
+ self.modify(data.shortname, user_model)
56
+
57
+
58
+ elif data.action_type == ActionType.move and "src_shortname" in data.attributes:
59
+ self.delete(data.attributes['src_shortname'])
60
+ self.add(data.shortname, user_model)
61
+
62
+
63
+
64
+ def delete(self, shortname: str):
65
+ self.conn.delete(f"cn={shortname},{settings.ldap_root_dn}")
66
+
67
+ def add(
68
+ self,
69
+ shortname: str,
70
+ user_model: User
71
+ ):
72
+ self.conn.add(
73
+ f"cn={shortname},{settings.ldap_root_dn}",
74
+ 'dmartUser',
75
+ {
76
+ "cn": shortname.encode(),
77
+ "sn": shortname.encode(),
78
+ "gn": str(getattr(user_model, "displayname", "")).encode(),
79
+ "userPassword": getattr(user_model, "password", "").encode()
80
+ }
81
+ )
82
+
83
+ def modify(
84
+ self,
85
+ shortname: str,
86
+ user_model: User
87
+ ):
88
+ self.conn.modify(
89
+ f"cn={shortname},{settings.ldap_root_dn}",
90
+ {
91
+ "gn": [(
92
+ MODIFY_REPLACE,
93
+ [str(getattr(user_model, "displayname", "")).encode()]
94
+ )],
95
+ "userPassword": [(
96
+ MODIFY_REPLACE,
97
+ [getattr(user_model, "password", "").encode()]
98
+ )],
99
+ }
100
+ )
File without changes