karrio-server-core 2025.5__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 (213) hide show
  1. karrio/server/conf.py +54 -0
  2. karrio/server/core/__init__.py +3 -0
  3. karrio/server/core/admin.py +1 -0
  4. karrio/server/core/apps.py +10 -0
  5. karrio/server/core/authentication.py +347 -0
  6. karrio/server/core/config.py +31 -0
  7. karrio/server/core/context_processors.py +12 -0
  8. karrio/server/core/datatypes.py +394 -0
  9. karrio/server/core/dataunits.py +187 -0
  10. karrio/server/core/exceptions.py +404 -0
  11. karrio/server/core/fields.py +12 -0
  12. karrio/server/core/filters.py +837 -0
  13. karrio/server/core/gateway.py +1011 -0
  14. karrio/server/core/logging.py +403 -0
  15. karrio/server/core/management/commands/cli.py +19 -0
  16. karrio/server/core/management/commands/create_oauth_client.py +41 -0
  17. karrio/server/core/management/commands/runserver.py +5 -0
  18. karrio/server/core/middleware.py +197 -0
  19. karrio/server/core/migrations/0001_initial.py +28 -0
  20. karrio/server/core/migrations/0002_apilogindex.py +69 -0
  21. karrio/server/core/migrations/0003_apilogindex_test_mode.py +62 -0
  22. karrio/server/core/migrations/0004_metafield.py +74 -0
  23. karrio/server/core/migrations/0005_alter_metafield_type_alter_metafield_value.py +23 -0
  24. karrio/server/core/migrations/0006_add_api_log_requested_at_index.py +22 -0
  25. karrio/server/core/migrations/__init__.py +0 -0
  26. karrio/server/core/models/__init__.py +48 -0
  27. karrio/server/core/models/base.py +103 -0
  28. karrio/server/core/models/entity.py +24 -0
  29. karrio/server/core/models/metafield.py +144 -0
  30. karrio/server/core/models/third_party.py +21 -0
  31. karrio/server/core/oauth_validators.py +170 -0
  32. karrio/server/core/permissions.py +36 -0
  33. karrio/server/core/renderers.py +11 -0
  34. karrio/server/core/router.py +3 -0
  35. karrio/server/core/serializers.py +1971 -0
  36. karrio/server/core/signals.py +55 -0
  37. karrio/server/core/telemetry.py +573 -0
  38. karrio/server/core/tests.py +99 -0
  39. karrio/server/core/tests_resource_token.py +411 -0
  40. karrio/server/core/urls.py +12 -0
  41. karrio/server/core/utils.py +1025 -0
  42. karrio/server/core/validators.py +264 -0
  43. karrio/server/core/views/__init__.py +2 -0
  44. karrio/server/core/views/api.py +133 -0
  45. karrio/server/core/views/metadata.py +44 -0
  46. karrio/server/core/views/oauth.py +75 -0
  47. karrio/server/core/views/references.py +82 -0
  48. karrio/server/core/views/schema.py +310 -0
  49. karrio/server/filters/__init__.py +2 -0
  50. karrio/server/filters/abstract.py +26 -0
  51. karrio/server/iam/__init__.py +0 -0
  52. karrio/server/iam/admin.py +3 -0
  53. karrio/server/iam/apps.py +21 -0
  54. karrio/server/iam/migrations/0001_initial.py +33 -0
  55. karrio/server/iam/migrations/__init__.py +0 -0
  56. karrio/server/iam/models.py +48 -0
  57. karrio/server/iam/permissions.py +155 -0
  58. karrio/server/iam/serializers.py +54 -0
  59. karrio/server/iam/signals.py +18 -0
  60. karrio/server/iam/tests.py +3 -0
  61. karrio/server/iam/views.py +3 -0
  62. karrio/server/openapi.py +75 -0
  63. karrio/server/providers/__init__.py +1 -0
  64. karrio/server/providers/admin.py +364 -0
  65. karrio/server/providers/apps.py +10 -0
  66. karrio/server/providers/management/commands/migrate_rate_sheets.py +101 -0
  67. karrio/server/providers/migrations/0001_initial.py +140 -0
  68. karrio/server/providers/migrations/0002_carrier_active.py +18 -0
  69. karrio/server/providers/migrations/0003_auto_20201230_0820.py +24 -0
  70. karrio/server/providers/migrations/0004_auto_20210212_0554.py +178 -0
  71. karrio/server/providers/migrations/0005_auto_20210212_0555.py +18 -0
  72. karrio/server/providers/migrations/0006_australiapostsettings.py +29 -0
  73. karrio/server/providers/migrations/0007_auto_20210213_0206.py +21 -0
  74. karrio/server/providers/migrations/0008_auto_20210214_0409.py +30 -0
  75. karrio/server/providers/migrations/0009_auto_20210308_0302.py +18 -0
  76. karrio/server/providers/migrations/0010_auto_20210409_0852.py +32 -0
  77. karrio/server/providers/migrations/0011_auto_20210409_0853.py +21 -0
  78. karrio/server/providers/migrations/0012_alter_carrier_options.py +17 -0
  79. karrio/server/providers/migrations/0013_tntsettings.py +30 -0
  80. karrio/server/providers/migrations/0014_auto_20210612_1608.py +46 -0
  81. karrio/server/providers/migrations/0015_auto_20210615_1601.py +28 -0
  82. karrio/server/providers/migrations/0016_alter_purolatorsettings_user_token.py +18 -0
  83. karrio/server/providers/migrations/0017_auto_20210805_0359.py +1293 -0
  84. karrio/server/providers/migrations/0018_alter_fedexsettings_user_key.py +18 -0
  85. karrio/server/providers/migrations/0019_dhlpolandsettings_servicelevel.py +65 -0
  86. karrio/server/providers/migrations/0020_genericsettings_labeltemplate.py +52 -0
  87. karrio/server/providers/migrations/0021_auto_20211231_2353.py +40 -0
  88. karrio/server/providers/migrations/0022_carrier_metadata.py +18 -0
  89. karrio/server/providers/migrations/0023_auto_20220124_1916.py +27 -0
  90. karrio/server/providers/migrations/0024_alter_genericsettings_custom_carrier_name.py +19 -0
  91. karrio/server/providers/migrations/0025_alter_servicelevel_service_code.py +19 -0
  92. karrio/server/providers/migrations/0026_auto_20220208_0132.py +59 -0
  93. karrio/server/providers/migrations/0027_auto_20220304_1340.py +29 -0
  94. karrio/server/providers/migrations/0028_auto_20220323_1500.py +33 -0
  95. karrio/server/providers/migrations/0029_easypostsettings.py +27 -0
  96. karrio/server/providers/migrations/0030_amazonmwssettings.py +29 -0
  97. karrio/server/providers/migrations/0031_delete_amazonmwssettings.py +18 -0
  98. karrio/server/providers/migrations/0032_alter_carrier_test.py +18 -0
  99. karrio/server/providers/migrations/0033_auto_20220708_1350.py +22 -0
  100. karrio/server/providers/migrations/0034_amazonmwssettings_dpdhlsettings.py +47 -0
  101. karrio/server/providers/migrations/0035_alter_carrier_capabilities.py +43 -0
  102. karrio/server/providers/migrations/0036_upsfreightsettings.py +31 -0
  103. karrio/server/providers/migrations/0037_chronopostsettings.py +29 -0
  104. karrio/server/providers/migrations/0038_alter_genericsettings_label_template.py +19 -0
  105. karrio/server/providers/migrations/0039_auto_20220906_0612.py +23 -0
  106. karrio/server/providers/migrations/0040_dpdhlsettings_services.py +18 -0
  107. karrio/server/providers/migrations/0041_auto_20221105_0705.py +38 -0
  108. karrio/server/providers/migrations/0042_auto_20221215_1642.py +23 -0
  109. karrio/server/providers/migrations/0043_alter_genericsettings_account_number_and_more.py +39 -0
  110. karrio/server/providers/migrations/0044_carrier_carrier_capabilities.py +64 -0
  111. karrio/server/providers/migrations/0045_alter_carrier_active_alter_carrier_carrier_id.py +31 -0
  112. karrio/server/providers/migrations/0046_remove_dpdhlsettings_signature_and_more.py +41 -0
  113. karrio/server/providers/migrations/0047_dpdsettings.py +286 -0
  114. karrio/server/providers/migrations/0048_servicelevel_min_weight_servicelevel_transit_days_and_more.py +64 -0
  115. karrio/server/providers/migrations/0049_boxknightsettings_geodissettings_lapostesettings_and_more.py +156 -0
  116. karrio/server/providers/migrations/0050_carrier_is_system_alter_carrier_metadata_and_more.py +106 -0
  117. karrio/server/providers/migrations/0051_rename_username_upssettings_client_id_and_more.py +31 -0
  118. karrio/server/providers/migrations/0052_alter_upssettings_account_number_and_more.py +20 -0
  119. karrio/server/providers/migrations/0053_locate2usettings.py +281 -0
  120. karrio/server/providers/migrations/0054_zoom2usettings.py +280 -0
  121. karrio/server/providers/migrations/0055_rename_amazonmwssettings_amazonshippingsettings_and_more.py +44 -0
  122. karrio/server/providers/migrations/0056_asendiaussettings_geodissettings_code_client_and_more.py +75 -0
  123. karrio/server/providers/migrations/0057_alter_servicelevel_weight_unit_belgianpostsettings.py +51 -0
  124. karrio/server/providers/migrations/0058_alliedexpresssettings.py +38 -0
  125. karrio/server/providers/migrations/0059_ratesheet.py +81 -0
  126. karrio/server/providers/migrations/0060_belgianpostsettings_rate_sheet_and_more.py +73 -0
  127. karrio/server/providers/migrations/0061_alliedexpresssettings_service_type.py +17 -0
  128. karrio/server/providers/migrations/0062_sendlesettings_account_country_code.py +257 -0
  129. karrio/server/providers/migrations/0063_servicelevel_metadata.py +25 -0
  130. karrio/server/providers/migrations/0064_alliedexpresslocalsettings.py +43 -0
  131. karrio/server/providers/migrations/0065_servicelevel_carrier_service_code_and_more.py +66 -0
  132. karrio/server/providers/migrations/0066_rename_fedexsettings_fedexwssettings_and_more.py +28 -0
  133. karrio/server/providers/migrations/0067_fedexsettings.py +283 -0
  134. karrio/server/providers/migrations/0068_fedexsettings_track_api_key_and_more.py +38 -0
  135. karrio/server/providers/migrations/0069_alter_canadapostsettings_contract_id_and_more.py +23 -0
  136. karrio/server/providers/migrations/0070_tgesettings_alter_carrier_capabilities.py +65 -0
  137. karrio/server/providers/migrations/0071_alter_tgesettings_my_toll_token.py +18 -0
  138. karrio/server/providers/migrations/0072_rename_eshippersettings_eshipperxmlsettings_and_more.py +28 -0
  139. karrio/server/providers/migrations/0073_delete_eshipperxmlsettings.py +41 -0
  140. karrio/server/providers/migrations/0074_eshippersettings.py +38 -0
  141. karrio/server/providers/migrations/0075_haypostsettings.py +40 -0
  142. karrio/server/providers/migrations/0076_rename_customer_registration_id_uspsinternationalsettings_account_number_and_more.py +125 -0
  143. karrio/server/providers/migrations/0077_uspswtinternationalsettings_uspswtsettings_and_more.py +165 -0
  144. karrio/server/providers/migrations/0078_auto_20240813_1552.py +120 -0
  145. karrio/server/providers/migrations/0079_alter_carrier_options_alter_ratesheet_created_by.py +31 -0
  146. karrio/server/providers/migrations/0080_alter_aramexsettings_account_country_code_and_more.py +3025 -0
  147. karrio/server/providers/migrations/0081_remove_alliedexpresssettings_carrier_ptr_and_more.py +338 -0
  148. karrio/server/providers/migrations/0082_add_zone_identifiers.py +50 -0
  149. karrio/server/providers/migrations/0083_add_optimized_rate_sheet_structure.py +33 -0
  150. karrio/server/providers/migrations/0084_alter_servicelevel_currency.py +168 -0
  151. karrio/server/providers/migrations/0085_populate_dhl_parcel_de_oauth_credentials.py +82 -0
  152. karrio/server/providers/migrations/0086_rename_dhl_parcel_de_customer_number_to_billing_number.py +71 -0
  153. karrio/server/providers/migrations/__init__.py +0 -0
  154. karrio/server/providers/models/__init__.py +16 -0
  155. karrio/server/providers/models/carrier.py +387 -0
  156. karrio/server/providers/models/config.py +30 -0
  157. karrio/server/providers/models/service.py +192 -0
  158. karrio/server/providers/models/sheet.py +287 -0
  159. karrio/server/providers/models/template.py +39 -0
  160. karrio/server/providers/models/utils.py +58 -0
  161. karrio/server/providers/router.py +3 -0
  162. karrio/server/providers/serializers/__init__.py +3 -0
  163. karrio/server/providers/serializers/base.py +538 -0
  164. karrio/server/providers/signals.py +25 -0
  165. karrio/server/providers/templates/providers/oauth_callback.html +105 -0
  166. karrio/server/providers/tests/__init__.py +5 -0
  167. karrio/server/providers/tests/test_connections.py +895 -0
  168. karrio/server/providers/urls.py +11 -0
  169. karrio/server/providers/views/__init__.py +0 -0
  170. karrio/server/providers/views/carriers.py +267 -0
  171. karrio/server/providers/views/connections.py +496 -0
  172. karrio/server/samples.py +352 -0
  173. karrio/server/serializers/__init__.py +2 -0
  174. karrio/server/serializers/abstract.py +602 -0
  175. karrio/server/tracing/__init__.py +0 -0
  176. karrio/server/tracing/admin.py +63 -0
  177. karrio/server/tracing/apps.py +8 -0
  178. karrio/server/tracing/migrations/0001_initial.py +41 -0
  179. karrio/server/tracing/migrations/0002_auto_20220710_1307.py +22 -0
  180. karrio/server/tracing/migrations/0003_auto_20221105_0317.py +43 -0
  181. karrio/server/tracing/migrations/0004_tracingrecord_carrier_account_idx.py +24 -0
  182. karrio/server/tracing/migrations/0005_optimise_tracingrecord_request_log_idx.py +25 -0
  183. karrio/server/tracing/migrations/0006_alter_tracingrecord_options_and_more.py +49 -0
  184. karrio/server/tracing/migrations/0007_tracingrecord_tracing_created_at_idx.py +19 -0
  185. karrio/server/tracing/migrations/__init__.py +0 -0
  186. karrio/server/tracing/models.py +82 -0
  187. karrio/server/tracing/tests.py +3 -0
  188. karrio/server/tracing/utils.py +109 -0
  189. karrio/server/user/__init__.py +0 -0
  190. karrio/server/user/admin.py +96 -0
  191. karrio/server/user/apps.py +7 -0
  192. karrio/server/user/forms.py +35 -0
  193. karrio/server/user/migrations/0001_initial.py +41 -0
  194. karrio/server/user/migrations/0002_token.py +29 -0
  195. karrio/server/user/migrations/0003_token_test_mode.py +20 -0
  196. karrio/server/user/migrations/0004_group.py +26 -0
  197. karrio/server/user/migrations/0005_token_label.py +21 -0
  198. karrio/server/user/migrations/0006_workspaceconfig.py +63 -0
  199. karrio/server/user/migrations/0007_user_metadata.py +25 -0
  200. karrio/server/user/migrations/__init__.py +0 -0
  201. karrio/server/user/models.py +218 -0
  202. karrio/server/user/serializers.py +47 -0
  203. karrio/server/user/templates/registration/login.html +108 -0
  204. karrio/server/user/templates/registration/registration_confirm_email.html +10 -0
  205. karrio/server/user/templates/registration/registration_confirm_email.txt +3 -0
  206. karrio/server/user/tests.py +3 -0
  207. karrio/server/user/urls.py +10 -0
  208. karrio/server/user/utils.py +60 -0
  209. karrio/server/user/views.py +9 -0
  210. karrio_server_core-2025.5.dist-info/METADATA +32 -0
  211. karrio_server_core-2025.5.dist-info/RECORD +213 -0
  212. karrio_server_core-2025.5.dist-info/WHEEL +5 -0
  213. karrio_server_core-2025.5.dist-info/top_level.txt +2 -0
karrio/server/conf.py ADDED
@@ -0,0 +1,54 @@
1
+ from django.db import connection
2
+ from django.conf import settings as base_settings
3
+
4
+ FEATURE_FLAGS = {
5
+ k: getattr(base_settings, k, True) for k, _ in base_settings.FEATURE_FLAGS
6
+ }
7
+ DEFAULT_ALLOWED_CONFIG = [
8
+ "APP_NAME",
9
+ "APP_WEBSITE",
10
+ "SUPPORT_EMAIL",
11
+ "BASE_TEMPLATE",
12
+ "BASE_FOOTER_TEMPLATE",
13
+ ]
14
+ FALLBACK_VALUES = {
15
+ "APP_NAME": "Karrio",
16
+ "APP_WEBSITE": "https://karrio.io",
17
+ "SUPPORT_EMAIL": "hello@karrio.io",
18
+ "BASE_TEMPLATE": "karrio/base_site.html",
19
+ "BASE_FOOTER_TEMPLATE": "karrio/base_footer.html",
20
+ }
21
+
22
+
23
+ class _Settings:
24
+ def __getattr__(self, item):
25
+ if item == "tenant":
26
+ return self._get_tenant()
27
+
28
+ if item == "schema":
29
+ return self._get_schema()
30
+
31
+ if item == "APP_NAME":
32
+ return getattr(self._get_tenant(), "name", FALLBACK_VALUES.get(item))
33
+
34
+ if item == "APP_WEBSITE":
35
+ return getattr(self._get_tenant(), "website", FALLBACK_VALUES.get(item))
36
+
37
+ if item in FEATURE_FLAGS and self._get_tenant() is not None:
38
+ feature_flags = getattr(self._get_tenant(), "feature_flags", {})
39
+
40
+ return feature_flags.get(item, getattr(base_settings, item, None))
41
+
42
+ return getattr(base_settings, item, FALLBACK_VALUES.get(item))
43
+
44
+ def _get_schema(self):
45
+ return connection.get_schema() if base_settings.MULTI_TENANTS else None
46
+
47
+ def _get_tenant(self):
48
+ return connection.get_tenant() if base_settings.MULTI_TENANTS else None
49
+
50
+ def get(self, item):
51
+ return getattr(self, item)
52
+
53
+
54
+ settings = _Settings()
@@ -0,0 +1,3 @@
1
+ __path__ = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore
2
+
3
+ default_app_config = 'karrio.server.core.apps.CoreConfig'
@@ -0,0 +1 @@
1
+ from django.contrib import admin
@@ -0,0 +1,10 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class CoreConfig(AppConfig):
5
+ name = "karrio.server.core"
6
+
7
+ def ready(self):
8
+ from karrio.server.core.signals import register_signals
9
+
10
+ register_signals()
@@ -0,0 +1,347 @@
1
+ import yaml # type: ignore
2
+ import pydoc
3
+ import functools
4
+ from django.db.utils import ProgrammingError
5
+ from django.conf import settings
6
+ from django.contrib.auth import mixins, get_user_model
7
+ from django.utils.translation import gettext_lazy as _
8
+ from django.utils.functional import SimpleLazyObject
9
+ from django.contrib.auth.middleware import (
10
+ AuthenticationMiddleware as BaseAuthenticationMiddleware,
11
+ )
12
+ from rest_framework import status
13
+ from rest_framework import exceptions
14
+ from rest_framework.authentication import (
15
+ TokenAuthentication as BaseTokenAuthentication,
16
+ BasicAuthentication as BaseBasicAuthentication,
17
+ )
18
+ from rest_framework_simplejwt.authentication import (
19
+ JWTAuthentication as BaseJWTAuthentication,
20
+ )
21
+ from oauth2_provider.contrib.rest_framework import (
22
+ OAuth2Authentication as BaseOAuth2Authentication,
23
+ )
24
+ from django_otp.middleware import OTPMiddleware
25
+ from karrio.server.core.logging import logger
26
+
27
+ UserModel = get_user_model()
28
+ AUTHENTICATION_CLASSES = getattr(settings, "AUTHENTICATION_CLASSES", [])
29
+
30
+
31
+ def catch_auth_exception(func):
32
+ @functools.wraps(func)
33
+ def wrapper(*args, **kwargs):
34
+ try:
35
+ return func(*args, **kwargs)
36
+ except exceptions.AuthenticationFailed:
37
+ from karrio.server.core.exceptions import APIException
38
+
39
+ raise APIException(
40
+ "Given token not valid for any token type",
41
+ code="invalid_token",
42
+ status_code=status.HTTP_401_UNAUTHORIZED,
43
+ )
44
+
45
+ return wrapper
46
+
47
+
48
+ class TokenAuthentication(BaseTokenAuthentication):
49
+ def get_model(self):
50
+ if self.model is not None:
51
+ return self.model
52
+ from karrio.server.user.models import Token
53
+
54
+ return Token
55
+
56
+ @catch_auth_exception
57
+ def authenticate(self, request):
58
+ auth = super().authenticate(request)
59
+
60
+ if auth is not None:
61
+ user, token = auth
62
+ request.user = user or request.user
63
+ request.token = token
64
+ request.test_mode = token.test_mode
65
+ request.org = SimpleLazyObject(
66
+ functools.partial(
67
+ get_request_org,
68
+ request,
69
+ user,
70
+ default_org=token.organization,
71
+ )
72
+ )
73
+
74
+ return auth
75
+
76
+
77
+ class TokenBasicAuthentication(BaseBasicAuthentication):
78
+ @catch_auth_exception
79
+ def authenticate(self, request):
80
+ auth = super(TokenBasicAuthentication, self).authenticate(request)
81
+
82
+ if auth is not None:
83
+ user, token = auth
84
+ request.user = user or request.user
85
+ request.token = token
86
+ request.test_mode = token.test_mode
87
+ request.org = SimpleLazyObject(
88
+ functools.partial(
89
+ get_request_org,
90
+ request,
91
+ user,
92
+ default_org=token.organization,
93
+ )
94
+ )
95
+
96
+ return auth
97
+
98
+ def authenticate_credentials(self, api_key, *args, **kwargs):
99
+ """
100
+ Authenticate the api token with optional request for context.
101
+ """
102
+ from karrio.server.user.models import Token
103
+
104
+ token = Token.objects.filter(key=api_key).first()
105
+ user = getattr(token, "user", None)
106
+
107
+ if user is None:
108
+ raise exceptions.AuthenticationFailed(_("Invalid username/password."))
109
+
110
+ if not user.is_active:
111
+ raise exceptions.AuthenticationFailed(_("User inactive or deleted."))
112
+
113
+ return (user, token)
114
+
115
+
116
+ class JWTAuthentication(BaseJWTAuthentication):
117
+ @catch_auth_exception
118
+ def authenticate(self, request):
119
+ auth = super().authenticate(request)
120
+
121
+ if auth is not None:
122
+ user, token = auth
123
+
124
+ request.user = user
125
+ request.token = token
126
+ request.test_mode = get_request_test_mode(request)
127
+ request.otp_is_verified = token.get("is_verified") or False
128
+ request.org = SimpleLazyObject(
129
+ functools.partial(
130
+ get_request_org,
131
+ request,
132
+ user,
133
+ org_id=request.META.get("HTTP_X_ORG_ID"),
134
+ )
135
+ )
136
+
137
+ if not token.get("is_verified"):
138
+ raise exceptions.AuthenticationFailed(
139
+ _("Authentication token not verified"), code="otp_not_verified"
140
+ )
141
+
142
+ return auth
143
+
144
+
145
+ class OAuth2Authentication(BaseOAuth2Authentication):
146
+ @catch_auth_exception
147
+ def authenticate(self, request):
148
+ auth = super().authenticate(request)
149
+
150
+ if auth is not None:
151
+ user, token = auth
152
+
153
+ # Enhanced user context handling for different OAuth flows
154
+ if user is None:
155
+ # For client credentials flow, the user might be None from the base class
156
+ # but our custom validator should have set it on the request
157
+ if (
158
+ hasattr(request, "user")
159
+ and request.user
160
+ and not request.user.is_anonymous
161
+ ):
162
+ user = request.user
163
+ elif hasattr(request, "oauth_user"):
164
+ user = request.oauth_user
165
+ # If we still don't have a user, try to get it from the OAuth application
166
+ elif hasattr(token, "application") and token.application:
167
+ user = getattr(token.application, "user", None)
168
+
169
+ # Set request context
170
+ request.user = user or request.user
171
+ request.token = token
172
+ request.test_mode = get_request_test_mode(request)
173
+
174
+ # Enhanced organization context for OAuth apps
175
+ default_org = None
176
+ if hasattr(token, "application") and hasattr(
177
+ token.application, "oauth_app"
178
+ ):
179
+ # If this is an OAuth app token, use the app owner's organization
180
+ oauth_app = token.application.oauth_app
181
+ if hasattr(oauth_app, "created_by") and oauth_app.created_by:
182
+ app_owner = oauth_app.created_by
183
+ if hasattr(app_owner, "organizations"):
184
+ default_org = app_owner.organizations.filter(
185
+ is_active=True
186
+ ).first()
187
+
188
+ request.org = SimpleLazyObject(
189
+ functools.partial(
190
+ get_request_org,
191
+ request,
192
+ user,
193
+ org_id=request.META.get("HTTP_X_ORG_ID"),
194
+ default_org=default_org,
195
+ )
196
+ )
197
+
198
+ return auth
199
+
200
+
201
+ class TwoFactorAuthenticationMiddleware(OTPMiddleware):
202
+ pass
203
+
204
+
205
+ class AccessMixin(mixins.AccessMixin):
206
+ """Verify that the current user is authenticated."""
207
+
208
+ def dispatch(self, request, *args, **kwargs):
209
+ if (
210
+ not hasattr(request, "user")
211
+ or request.user is None
212
+ or not request.user.is_authenticated
213
+ ):
214
+ authenticate_user(request)
215
+
216
+ request.user = SimpleLazyObject(
217
+ functools.partial(get_request_user, request, request.user)
218
+ )
219
+
220
+ return super().dispatch(request, *args, **kwargs)
221
+
222
+
223
+ class AuthenticationMiddleware(BaseAuthenticationMiddleware):
224
+ def process_response(self, request, response):
225
+ if getattr(request, "org", None) is not None:
226
+ response.set_cookie("org_id", getattr(request.org, "id", None))
227
+ response["X-org-id"] = getattr(request.org, "id", None)
228
+
229
+ if getattr(request, "test_mode", None) is not None:
230
+ response.set_cookie("test_mode", request.test_mode)
231
+ response["X-test-mode"] = request.test_mode
232
+
233
+ return response
234
+
235
+ def process_request(self, request):
236
+ super().process_request(request)
237
+
238
+ request = authenticate_user(request)
239
+
240
+ if hasattr(request, "user") and getattr(request, "org", None) is None:
241
+ request.org = get_request_org(
242
+ request,
243
+ request.user,
244
+ org_id=request.META.get("HTTP_X_ORG_ID"),
245
+ )
246
+
247
+ if not hasattr(request, "test_mode"):
248
+ request.test_mode = get_request_test_mode(request)
249
+
250
+
251
+ def authenticate_user(request):
252
+ def authenticate(request, authenticator):
253
+ # Check if user exists and is not authenticated
254
+ if (
255
+ not hasattr(request, "user")
256
+ or request.user is None
257
+ or not getattr(request.user, "is_authenticated", False)
258
+ ):
259
+ logger.debug(f"Trying authenticator: {authenticator}")
260
+ try:
261
+ auth_instance = pydoc.locate(authenticator)()
262
+ auth = auth_instance.authenticate(request)
263
+
264
+ if auth is not None:
265
+ user, token = auth
266
+ request.user = user
267
+ request.token = token
268
+ except AttributeError as e:
269
+ # Skip SessionAuthentication if it fails with _request attribute error
270
+ if "'WSGIRequest' object has no attribute '_request'" in str(e):
271
+ logger.debug(
272
+ f"Skipping {authenticator} - incompatible with middleware context"
273
+ )
274
+ else:
275
+ raise
276
+ except exceptions.AuthenticationFailed:
277
+ # Silently skip authentication failures - let the next authenticator try
278
+ logger.debug(f"Authentication failed with {authenticator}, trying next")
279
+ pass
280
+
281
+ return request
282
+
283
+ try:
284
+ logger.debug(f"Auth classes to try: {AUTHENTICATION_CLASSES}")
285
+ return functools.reduce(authenticate, AUTHENTICATION_CLASSES, request)
286
+ except Exception as e:
287
+ logger.debug(f"Authentication error: {e}")
288
+ return request
289
+
290
+
291
+ def get_request_org(request, user, org_id: str = None, default_org=None):
292
+ """
293
+ Attempts to find and return an organization.
294
+ """
295
+ if settings.MULTI_ORGANIZATIONS:
296
+ try:
297
+ from karrio.server.orgs.models import Organization
298
+
299
+ if default_org is not None:
300
+ org = default_org
301
+ elif user and hasattr(user, "id") and user.id:
302
+ orgs = Organization.objects.filter(users__id=user.id)
303
+ org = None
304
+
305
+ if org_id is not None:
306
+ org = orgs.filter(id=org_id).first()
307
+
308
+ if org is None:
309
+ org = orgs.filter(is_active=True).first()
310
+ else:
311
+ org = None
312
+
313
+ if org is not None and not org.is_active:
314
+ raise exceptions.AuthenticationFailed(
315
+ _("Organization is inactive"), code="inactive_organization"
316
+ )
317
+
318
+ if org is None and org_id is not None:
319
+ raise exceptions.AuthenticationFailed(
320
+ _("No active organization found with the given credentials"),
321
+ code="invalid_organization",
322
+ )
323
+
324
+ return org
325
+ except ProgrammingError:
326
+ pass
327
+
328
+ return None
329
+
330
+
331
+ def get_request_user(request, user):
332
+ if not getattr(request, "otp_is_verified", True):
333
+ raise exceptions.AuthenticationFailed(
334
+ _("Authentication token not verified"), code="otp_not_verified"
335
+ )
336
+
337
+ if user is not None:
338
+ user.otp_device = None
339
+ user.is_verified = functools.partial(
340
+ lambda _: getattr(request, "otp_is_verified", True), user
341
+ )
342
+
343
+ return user
344
+
345
+
346
+ def get_request_test_mode(request):
347
+ return yaml.safe_load(request.META.get("HTTP_X_TEST_MODE", "")) or False
@@ -0,0 +1,31 @@
1
+ """Karrio server system configuration adapter."""
2
+
3
+ import typing
4
+ import karrio.lib as lib
5
+
6
+
7
+ class ConstanceConfig(lib.AbstractSystemConfig):
8
+ """Django constance configuration adapter.
9
+
10
+ Provides access to runtime configuration values stored
11
+ in Django constance (database-backed settings).
12
+ """
13
+
14
+ def get(self, key: str, default: typing.Any = None) -> typing.Any:
15
+ from constance import config as constance_config
16
+
17
+ return getattr(constance_config, key, default)
18
+
19
+ def __getitem__(self, key: str) -> typing.Any:
20
+ from constance import config as constance_config
21
+
22
+ return getattr(constance_config, key)
23
+
24
+ def __contains__(self, key: str) -> bool:
25
+ from constance import config as constance_config
26
+
27
+ return hasattr(constance_config, key)
28
+
29
+
30
+ # Singleton instance for use across the server
31
+ config = ConstanceConfig()
@@ -0,0 +1,12 @@
1
+ from karrio.server.conf import settings, DEFAULT_ALLOWED_CONFIG
2
+
3
+
4
+ TEMPLATE_SETTINGS_ACCESS_LIST = (
5
+ getattr(settings, "TEMPLATE_SETTINGS_ACCESS_LIST", None) or DEFAULT_ALLOWED_CONFIG
6
+ )
7
+
8
+
9
+ def get_settings(request):
10
+ return {
11
+ name: getattr(settings, name, None) for name in TEMPLATE_SETTINGS_ACCESS_LIST
12
+ }