fastapi-rtk 0.2.27__py3-none-any.whl → 1.0.13__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 (98) hide show
  1. fastapi_rtk/__init__.py +39 -35
  2. fastapi_rtk/_version.py +1 -0
  3. fastapi_rtk/api/model_rest_api.py +476 -221
  4. fastapi_rtk/auth/auth.py +0 -9
  5. fastapi_rtk/backends/generic/__init__.py +6 -0
  6. fastapi_rtk/backends/generic/column.py +21 -12
  7. fastapi_rtk/backends/generic/db.py +42 -7
  8. fastapi_rtk/backends/generic/filters.py +21 -16
  9. fastapi_rtk/backends/generic/interface.py +14 -8
  10. fastapi_rtk/backends/generic/model.py +19 -11
  11. fastapi_rtk/backends/sqla/__init__.py +1 -0
  12. fastapi_rtk/backends/sqla/db.py +77 -17
  13. fastapi_rtk/backends/sqla/extensions/audit/audit.py +401 -189
  14. fastapi_rtk/backends/sqla/extensions/geoalchemy2/filters.py +15 -12
  15. fastapi_rtk/backends/sqla/filters.py +50 -21
  16. fastapi_rtk/backends/sqla/interface.py +96 -34
  17. fastapi_rtk/backends/sqla/model.py +56 -39
  18. fastapi_rtk/bases/__init__.py +20 -0
  19. fastapi_rtk/bases/db.py +94 -7
  20. fastapi_rtk/bases/file_manager.py +47 -3
  21. fastapi_rtk/bases/filter.py +22 -0
  22. fastapi_rtk/bases/interface.py +49 -5
  23. fastapi_rtk/bases/model.py +3 -0
  24. fastapi_rtk/bases/session.py +2 -0
  25. fastapi_rtk/cli/cli.py +62 -9
  26. fastapi_rtk/cli/commands/__init__.py +23 -0
  27. fastapi_rtk/cli/{db.py → commands/db/__init__.py} +107 -50
  28. fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/env.py +2 -3
  29. fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/env.py +10 -9
  30. fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/script.py.mako +3 -1
  31. fastapi_rtk/cli/{export.py → commands/export.py} +12 -10
  32. fastapi_rtk/cli/{security.py → commands/security.py} +73 -7
  33. fastapi_rtk/cli/commands/translate.py +299 -0
  34. fastapi_rtk/cli/decorators.py +9 -4
  35. fastapi_rtk/cli/utils.py +46 -0
  36. fastapi_rtk/config.py +41 -1
  37. fastapi_rtk/const.py +29 -1
  38. fastapi_rtk/db.py +76 -40
  39. fastapi_rtk/decorators.py +1 -1
  40. fastapi_rtk/dependencies.py +134 -62
  41. fastapi_rtk/exceptions.py +51 -1
  42. fastapi_rtk/fastapi_react_toolkit.py +186 -171
  43. fastapi_rtk/file_managers/file_manager.py +8 -6
  44. fastapi_rtk/file_managers/s3_file_manager.py +69 -33
  45. fastapi_rtk/globals.py +22 -12
  46. fastapi_rtk/lang/__init__.py +3 -0
  47. fastapi_rtk/lang/babel/__init__.py +4 -0
  48. fastapi_rtk/lang/babel/cli.py +40 -0
  49. fastapi_rtk/lang/babel/config.py +17 -0
  50. fastapi_rtk/lang/babel.cfg +1 -0
  51. fastapi_rtk/lang/lazy_text.py +120 -0
  52. fastapi_rtk/lang/messages.pot +238 -0
  53. fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.mo +0 -0
  54. fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.po +248 -0
  55. fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.mo +0 -0
  56. fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.po +244 -0
  57. fastapi_rtk/manager.py +355 -37
  58. fastapi_rtk/mixins.py +12 -0
  59. fastapi_rtk/routers.py +208 -72
  60. fastapi_rtk/schemas.py +142 -39
  61. fastapi_rtk/security/sqla/apis.py +39 -13
  62. fastapi_rtk/security/sqla/models.py +8 -23
  63. fastapi_rtk/security/sqla/security_manager.py +369 -11
  64. fastapi_rtk/setting.py +446 -88
  65. fastapi_rtk/types.py +94 -27
  66. fastapi_rtk/utils/__init__.py +8 -0
  67. fastapi_rtk/utils/async_task_runner.py +286 -61
  68. fastapi_rtk/utils/csv_json_converter.py +243 -40
  69. fastapi_rtk/utils/hooks.py +34 -0
  70. fastapi_rtk/utils/merge_schema.py +3 -3
  71. fastapi_rtk/utils/multiple_async_contexts.py +21 -0
  72. fastapi_rtk/utils/pydantic.py +46 -1
  73. fastapi_rtk/utils/run_utils.py +31 -1
  74. fastapi_rtk/utils/self_dependencies.py +1 -1
  75. fastapi_rtk/utils/use_default_when_none.py +1 -1
  76. fastapi_rtk/version.py +6 -1
  77. fastapi_rtk-1.0.13.dist-info/METADATA +28 -0
  78. fastapi_rtk-1.0.13.dist-info/RECORD +133 -0
  79. {fastapi_rtk-0.2.27.dist-info → fastapi_rtk-1.0.13.dist-info}/WHEEL +1 -2
  80. fastapi_rtk/backends/gremlinpython/__init__.py +0 -108
  81. fastapi_rtk/backends/gremlinpython/column.py +0 -208
  82. fastapi_rtk/backends/gremlinpython/db.py +0 -228
  83. fastapi_rtk/backends/gremlinpython/exceptions.py +0 -34
  84. fastapi_rtk/backends/gremlinpython/filters.py +0 -461
  85. fastapi_rtk/backends/gremlinpython/interface.py +0 -734
  86. fastapi_rtk/backends/gremlinpython/model.py +0 -364
  87. fastapi_rtk/backends/gremlinpython/session.py +0 -23
  88. fastapi_rtk/cli/commands.py +0 -295
  89. fastapi_rtk-0.2.27.dist-info/METADATA +0 -23
  90. fastapi_rtk-0.2.27.dist-info/RECORD +0 -126
  91. fastapi_rtk-0.2.27.dist-info/top_level.txt +0 -1
  92. /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/README +0 -0
  93. /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/alembic.ini.mako +0 -0
  94. /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/script.py.mako +0 -0
  95. /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/README +0 -0
  96. /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/alembic.ini.mako +0 -0
  97. {fastapi_rtk-0.2.27.dist-info → fastapi_rtk-1.0.13.dist-info}/entry_points.txt +0 -0
  98. {fastapi_rtk-0.2.27.dist-info → fastapi_rtk-1.0.13.dist-info}/licenses/LICENSE +0 -0
fastapi_rtk/manager.py CHANGED
@@ -1,7 +1,9 @@
1
+ import ssl
1
2
  import typing
2
3
  from datetime import UTC, datetime
3
4
  from typing import Any, Callable, Coroutine, Dict, Optional, overload
4
5
 
6
+ import fastapi
5
7
  from fastapi import HTTPException, Request, Response
6
8
  from fastapi.security import OAuth2PasswordRequestForm
7
9
  from fastapi_users import BaseUserManager, exceptions
@@ -11,6 +13,7 @@ from fastapi_users.password import PasswordHelperProtocol
11
13
  from pydantic import BaseModel
12
14
  from sqlalchemy import select
13
15
 
16
+ from .const import ErrorCode, logger
14
17
  from .db import UserDatabase
15
18
  from .exceptions import RolesMismatchException
16
19
  from .schemas import (
@@ -111,17 +114,205 @@ class UserManager(IDParser, BaseUserManager[User, int]):
111
114
  credentials.password, user.hashed_password
112
115
  )
113
116
  if not verified:
114
- await self.user_db.update(
115
- user, {"fail_login_count": (user.fail_login_count or 0) + 1}
116
- )
117
+ await self.on_after_authenticate(user, False)
117
118
  return None
118
119
  # Update password hash to a more robust one if needed
119
120
  if updated_password_hash is not None:
120
121
  await self.user_db.update(user, {"hashed_password": updated_password_hash})
121
122
 
122
- await self.user_db.update(user, {"fail_login_count": 0})
123
+ await self.on_after_authenticate(user)
123
124
  return user
124
125
 
126
+ async def authenticate_ldap(self, credentials: OAuth2PasswordRequestForm):
127
+ """
128
+ Authenticate a user using LDAP.
129
+
130
+ Args:
131
+ credentials (OAuth2PasswordRequestForm): The credentials to authenticate the user.
132
+
133
+ Returns:
134
+ User | None: The user if the credentials are valid, None otherwise.
135
+ """
136
+ try:
137
+ user = await self.get_by_username(credentials.username)
138
+ except exceptions.UserNotExists:
139
+ # Run the hasher to mitigate timing attack
140
+ # Inspired from Django: https://code.djangoproject.com/ticket/20760
141
+ self.password_helper.hash(credentials.password)
142
+ user = None
143
+
144
+ # If no username is provided, go away
145
+ if not credentials.username:
146
+ return None
147
+
148
+ # If user is not active, go away
149
+ if user and not user.is_active:
150
+ return None
151
+
152
+ # If user is not registered, and not self-registration, go away
153
+ if not user and not Setting.AUTH_USER_REGISTRATION:
154
+ logger.warning(
155
+ "Tried to login using ldap with username %s, but self-registration is disabled.",
156
+ credentials.username,
157
+ )
158
+ return None
159
+
160
+ # Ensure ldap3 is installed
161
+ try:
162
+ import ldap3
163
+ import ldap3.core.exceptions
164
+ except ImportError:
165
+ logger.error(
166
+ "You tried to use LDAP authentication, but 'ldap3' is not installed."
167
+ )
168
+ return None
169
+
170
+ # --- 1. Configure TLS ---
171
+ # ldap3 centralizes TLS settings into a Tls object.
172
+ assert Setting.AUTH_LDAP_SERVER
173
+ tls_config = None
174
+ use_ssl = Setting.AUTH_LDAP_USE_TLS or "ldaps://" in Setting.AUTH_LDAP_SERVER
175
+ if use_ssl:
176
+ validate_cert = ssl.CERT_REQUIRED
177
+ if Setting.AUTH_LDAP_ALLOW_SELF_SIGNED:
178
+ validate_cert = ssl.CERT_OPTIONAL
179
+ elif not Setting.AUTH_LDAP_TLS_DEMAND:
180
+ # Default behavior is to require certs if TLS is on
181
+ pass
182
+
183
+ tls_config = ldap3.Tls(
184
+ local_private_key_file=Setting.AUTH_LDAP_TLS_KEYFILE,
185
+ local_certificate_file=Setting.AUTH_LDAP_TLS_CERTFILE,
186
+ validate=validate_cert,
187
+ ca_certs_path=Setting.AUTH_LDAP_TLS_CACERTDIR,
188
+ ca_certs_file=Setting.AUTH_LDAP_TLS_CACERTFILE,
189
+ version=ssl.PROTOCOL_TLS,
190
+ )
191
+
192
+ # --- 2. Define the Server ---
193
+ server = ldap3.Server(
194
+ Setting.AUTH_LDAP_SERVER,
195
+ use_ssl=use_ssl,
196
+ tls=tls_config,
197
+ get_info=None, # No need for get_info for authentication
198
+ )
199
+
200
+ try:
201
+ user_dn = None
202
+ user_attributes = {}
203
+
204
+ # --- Flow 1 (Indirect Search Bind) ---
205
+ if Setting.AUTH_LDAP_BIND_USER:
206
+ # Bind with service account, search, then rebind with user credentials.
207
+ # The 'with' statement handles connection and unbinding.
208
+ with ldap3.Connection(
209
+ server,
210
+ Setting.AUTH_LDAP_BIND_USER,
211
+ Setting.AUTH_LDAP_BIND_PASSWORD,
212
+ auto_bind=True,
213
+ auto_referrals=False, # Equivalent to OPT_REFERRALS = 0
214
+ ) as conn:
215
+ assert Setting.AUTH_LDAP_SEARCH
216
+
217
+ # Search for the user
218
+ user_dn, user_attributes = self._ldap3_search(
219
+ conn, credentials.username
220
+ )
221
+
222
+ if not user_dn:
223
+ logger.info(
224
+ "No LDAP object found for user '%s'.", credentials.username
225
+ )
226
+ return None
227
+
228
+ # Validate user credentials by attempting a rebind
229
+ if not conn.rebind(user_dn, credentials.password):
230
+ logger.info("Login Failed for user: %s", credentials.username)
231
+ if user:
232
+ await self.on_after_authenticate(user, False)
233
+ return None
234
+
235
+ # --- Flow 2 (Direct Search Bind) ---
236
+ else:
237
+ # Bind directly with the end-user's credentials.
238
+ bind_username = credentials.username
239
+ if Setting.AUTH_LDAP_APPEND_DOMAIN:
240
+ bind_username = f"{bind_username}@{Setting.AUTH_LDAP_APPEND_DOMAIN}"
241
+ if Setting.AUTH_LDAP_USERNAME_FORMAT:
242
+ bind_username = (
243
+ Setting.AUTH_LDAP_USERNAME_FORMAT % credentials.username
244
+ )
245
+
246
+ with ldap3.Connection(
247
+ server,
248
+ bind_username,
249
+ credentials.password,
250
+ auto_bind=True,
251
+ auto_referrals=False,
252
+ ) as conn:
253
+ # If we are here, the bind was successful.
254
+
255
+ if Setting.AUTH_LDAP_SEARCH:
256
+ # Search for user attributes
257
+ user_dn, user_attributes = self._ldap3_search(
258
+ conn, credentials.username
259
+ )
260
+ if not user_dn:
261
+ logger.info(
262
+ "No LDAP object found for: %s", credentials.username
263
+ )
264
+ # Bind succeeded but search failed. Decide if this is an error.
265
+ # For now, we exit as the original code did.
266
+ return None
267
+
268
+ # --- 3. Post-Authentication Processing ---
269
+ # (This logic remains largely the same)
270
+ if user and user_attributes and Setting.AUTH_ROLES_SYNC_AT_LOGIN:
271
+ user = await self._ldap3_calculate_user(user, user_attributes)
272
+ logger.debug(
273
+ "Calculated new roles for user='%s' as: %s", user_dn, user.roles
274
+ )
275
+
276
+ if not user and user_attributes and Setting.AUTH_USER_REGISTRATION:
277
+ user = await self.create(
278
+ {
279
+ "username": credentials.username,
280
+ "password": self.password_helper.generate(),
281
+ "first_name": (
282
+ user_attributes.get(Setting.AUTH_LDAP_FIRSTNAME_FIELD)
283
+ or [""]
284
+ )[0],
285
+ "last_name": (
286
+ user_attributes.get(Setting.AUTH_LDAP_LASTNAME_FIELD)
287
+ or [""]
288
+ )[0],
289
+ "email": (
290
+ user_attributes.get(
291
+ Setting.AUTH_LDAP_EMAIL_FIELD,
292
+ )
293
+ or [f"{credentials.username}@email.notfound"]
294
+ )[0],
295
+ }
296
+ )
297
+ user = await self._ldap3_calculate_user(user, user_attributes)
298
+
299
+ if user:
300
+ await self.on_after_authenticate(user)
301
+ return user
302
+ else:
303
+ return None
304
+
305
+ except ldap3.core.exceptions.LDAPBindError:
306
+ # More specific error for failed logins
307
+ logger.info("Login Failed for user: %s", credentials.username)
308
+ if user:
309
+ await self.on_after_authenticate(user, False)
310
+ return None
311
+ except ldap3.core.exceptions.LDAPException as e:
312
+ # Catch all other ldap3 specific errors
313
+ logger.error("LDAP Error %s", e)
314
+ return None
315
+
125
316
  async def create(
126
317
  self,
127
318
  user_create: BaseModel | dict[str, Any],
@@ -309,6 +500,7 @@ class UserManager(IDParser, BaseUserManager[User, int]):
309
500
  # Associate account
310
501
  user = await self.get_by_email(account_email)
311
502
  if not associate_by_email:
503
+ await self.on_after_authenticate(user, False)
312
504
  raise exceptions.UserAlreadyExists()
313
505
  user = await self.user_db.add_oauth_account(user, oauth_account_dict)
314
506
  except exceptions.UserNotExists:
@@ -366,6 +558,7 @@ class UserManager(IDParser, BaseUserManager[User, int]):
366
558
  user = await self._handle_role_keys(
367
559
  user, oauth_callback_params["user_dict"].pop("role_keys", None)
368
560
  )
561
+ await self.on_after_authenticate(user)
369
562
  return user
370
563
 
371
564
  @overload
@@ -386,6 +579,7 @@ class UserManager(IDParser, BaseUserManager[User, int]):
386
579
  :param request: Optional FastAPI request that
387
580
  triggered the operation, defaults to None.
388
581
  :raises UserInactive: The user is inactive.
582
+ :return: None if in a request context, otherwise returns the reset token.
389
583
  """
390
584
  if not user.is_active:
391
585
  raise exceptions.UserInactive()
@@ -405,30 +599,6 @@ class UserManager(IDParser, BaseUserManager[User, int]):
405
599
  if not request:
406
600
  return token
407
601
 
408
- async def on_after_login(
409
- self,
410
- user: User,
411
- request: Request | None = None,
412
- response: Response | None = None,
413
- ) -> None:
414
- """
415
- Perform logic after user login.
416
-
417
- Please call await super().on_after_login(user, request, response) to keep the default behavior.
418
-
419
- *You should overload this method to add your own logic.*
420
-
421
- :param user: The user that is logging in
422
- :param request: Optional FastAPI request
423
- :param response: Optional response built by the transport.
424
- Defaults to None
425
- """
426
- update_fields = {
427
- "last_login": datetime.now(UTC).replace(tzinfo=None),
428
- "login_count": (user.login_count or 0) + 1,
429
- }
430
- await self.user_db.update(user, update_fields)
431
-
432
602
  async def on_after_logout(
433
603
  self,
434
604
  user: User,
@@ -436,16 +606,14 @@ class UserManager(IDParser, BaseUserManager[User, int]):
436
606
  response: Response | None = None,
437
607
  ) -> None:
438
608
  """
439
- Custom `on_after_logout` method to handle user logout.
440
-
441
609
  Perform logic after user logout.
442
610
 
443
611
  *You should overload this method to add your own logic.*
444
612
 
445
- :param user: The user that is logging out
446
- :param request: Optional FastAPI request
447
- :param response: Optional response built by the transport.
448
- Defaults to None
613
+ Args:
614
+ user (User): The user that is logging out.
615
+ request (Request | None, optional): Optional FastAPI request.
616
+ response (Response | None, optional): Optional response built by the transport.
449
617
  """
450
618
  return
451
619
 
@@ -453,19 +621,48 @@ class UserManager(IDParser, BaseUserManager[User, int]):
453
621
  self, user: User, token: str, request: Request | None = None
454
622
  ) -> None:
455
623
  if request:
456
- raise HTTPException(status_code=501, detail="Not implemented")
624
+ raise HTTPException(
625
+ fastapi.status.HTTP_501_NOT_IMPLEMENTED,
626
+ ErrorCode.FEATURE_NOT_IMPLEMENTED,
627
+ )
457
628
 
458
629
  async def on_after_reset_password(
459
630
  self, user: User, request: Request | None = None
460
631
  ) -> None:
461
632
  if request:
462
- raise HTTPException(status_code=501, detail="Not implemented")
633
+ raise HTTPException(
634
+ fastapi.status.HTTP_501_NOT_IMPLEMENTED,
635
+ ErrorCode.FEATURE_NOT_IMPLEMENTED,
636
+ )
463
637
 
464
638
  async def on_after_request_verify(
465
639
  self, user: User, token: str, request: Request | None = None
466
640
  ) -> None:
467
641
  if request:
468
- raise HTTPException(status_code=501, detail="Not implemented")
642
+ raise HTTPException(
643
+ fastapi.status.HTTP_501_NOT_IMPLEMENTED,
644
+ ErrorCode.FEATURE_NOT_IMPLEMENTED,
645
+ )
646
+
647
+ async def on_after_authenticate(self, user: User, success=True):
648
+ """
649
+ Perform logic after authentication attempt.
650
+
651
+ Please call `await super().on_after_authenticate(user, success)` to keep the default behavior.
652
+
653
+ Args:
654
+ user (User): The user that attempted authentication.
655
+ success (bool, optional): Whether the authentication was successful. Defaults to True.
656
+ """
657
+ update_fields = {}
658
+ if success:
659
+ update_fields["fail_login_count"] = 0
660
+ update_fields["last_login"] = datetime.now(UTC).replace(tzinfo=None)
661
+ update_fields["login_count"] = (user.login_count or 0) + 1
662
+ else:
663
+ update_fields["fail_login_count"] = (user.fail_login_count or 0) + 1
664
+ if update_fields:
665
+ await self.user_db.update(user, update_fields)
469
666
 
470
667
  async def _update(self, user: User, update_dict: Dict[str, Any]) -> User:
471
668
  """
@@ -533,3 +730,124 @@ class UserManager(IDParser, BaseUserManager[User, int]):
533
730
  else:
534
731
  user_or_user_dict["roles"] = await self.get_roles_by_names(role_names)
535
732
  return user_or_user_dict
733
+
734
+ """
735
+ --------------------------------------------------------------------------------------------------------
736
+ LDAP3 HELPER METHODS
737
+ --------------------------------------------------------------------------------------------------------
738
+ """
739
+
740
+ def _ldap3_search(self, conn, username: str):
741
+ """
742
+ Helper function to perform an LDAP search using ldap3.
743
+
744
+ Args:
745
+ conn (ldap3.Connection): The LDAP connection object.
746
+ username (str): The username to search for.
747
+
748
+ Returns:
749
+ Tuple[str | None, dict | None]: A tuple containing the user's DN and attributes if found, otherwise (None, None).
750
+ """
751
+ import ldap3.utils.conv
752
+
753
+ # Configuration check remains good practice
754
+ assert Setting.AUTH_LDAP_SEARCH
755
+
756
+ # 1. Filter Construction (with security improvement)
757
+ # ldap3 provides a utility to escape special characters to prevent LDAP injection
758
+ safe_username = ldap3.utils.conv.escape_filter_chars(username)
759
+
760
+ base_filter = f"({Setting.AUTH_LDAP_UID_FIELD}={safe_username})"
761
+ if Setting.AUTH_LDAP_SEARCH_FILTER:
762
+ # Combine the custom filter with the UID search
763
+ filter_str = f"(&{Setting.AUTH_LDAP_SEARCH_FILTER}{base_filter})"
764
+ else:
765
+ filter_str = base_filter
766
+
767
+ # 2. Attribute List Construction
768
+ # Using a set to prevent duplicates if config fields overlap
769
+ attributes_to_fetch = {
770
+ Setting.AUTH_LDAP_FIRSTNAME_FIELD,
771
+ Setting.AUTH_LDAP_LASTNAME_FIELD,
772
+ Setting.AUTH_LDAP_EMAIL_FIELD,
773
+ }
774
+ if Setting.AUTH_ROLES_MAPPING:
775
+ attributes_to_fetch.add(Setting.AUTH_LDAP_GROUP_FIELD)
776
+
777
+ logger.debug(
778
+ "LDAP search for '%s' with filter '%s' in base '%s'",
779
+ username,
780
+ filter_str,
781
+ Setting.AUTH_LDAP_SEARCH,
782
+ )
783
+
784
+ # 3. The Search Method
785
+ # The search operation is a method of the connection object
786
+ conn.search(
787
+ Setting.AUTH_LDAP_SEARCH,
788
+ filter_str,
789
+ search_scope=ldap3.SUBTREE,
790
+ attributes=list(attributes_to_fetch),
791
+ )
792
+ logger.debug("LDAP search returned: %s", conn.entries)
793
+
794
+ # 4. Result Processing
795
+ # Results are stored in the conn.entries property
796
+ if len(conn.entries) > 1:
797
+ logger.error("LDAP search for '%s' returned multiple results.", username)
798
+ return None, None
799
+
800
+ if conn.entries:
801
+ # Get the first (and only) entry
802
+ user_entry = conn.entries[0]
803
+
804
+ # Extract DN and attributes
805
+ user_dn = user_entry.entry_dn
806
+ # .entry_attributes_as_dict provides a simple dict format
807
+ user_attributes = user_entry.entry_attributes_as_dict
808
+
809
+ return user_dn, user_attributes
810
+ else:
811
+ # No results found
812
+ return None, None
813
+
814
+ async def _ldap3_calculate_user(
815
+ self, user: User, user_attributes: dict[str, typing.Any]
816
+ ):
817
+ """
818
+ Calculate the roles for the user.
819
+
820
+ Args:
821
+ user (User): User object to update.
822
+ user_attributes (dict[str, typing.Any]): User attributes from LDAP.
823
+
824
+ Returns:
825
+ User: Updated user object.
826
+ """
827
+ # Apply AUTH_ROLES_MAPPING
828
+ user_role_keys = user_attributes.get(Setting.AUTH_LDAP_GROUP_FIELD, [])
829
+ user = await self._handle_role_keys(user, user_role_keys)
830
+
831
+ # Apply AUTH_USER_REGISTRATION
832
+ if Setting.AUTH_USER_REGISTRATION:
833
+ if not Setting.AUTH_USER_REGISTRATION_ROLE:
834
+ logger.warning(
835
+ "AUTH_USER_REGISTRATION is set but AUTH_USER_REGISTRATION_ROLE is not set"
836
+ )
837
+ return user
838
+
839
+ # lookup registration role in flask db
840
+ db_roles = await self.get_roles_by_names(
841
+ Setting.AUTH_USER_REGISTRATION_ROLE
842
+ )
843
+ if not db_roles:
844
+ logger.warning(
845
+ "Can't find AUTH_USER_REGISTRATION_ROLE in database: %s",
846
+ Setting.AUTH_USER_REGISTRATION_ROLE,
847
+ )
848
+ return user
849
+ await self.update(
850
+ {}, user, list(set(r.name for r in db_roles + user.roles))
851
+ )
852
+
853
+ return user
fastapi_rtk/mixins.py ADDED
@@ -0,0 +1,12 @@
1
+ from .const import logger
2
+ from .utils import lazy
3
+
4
+ __all__ = ["LoggerMixin"]
5
+
6
+
7
+ class LoggerMixin:
8
+ """
9
+ A mixin that provides a logger for the class.
10
+ """
11
+
12
+ logger = lazy(lambda self: logger.getChild(self.__class__.__name__))