karrio-server-core 2025.5rc12__py3-none-any.whl → 2026.1.1__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 (112) hide show
  1. karrio/server/core/authentication.py +59 -25
  2. karrio/server/core/config.py +31 -0
  3. karrio/server/core/datatypes.py +30 -4
  4. karrio/server/core/dataunits.py +53 -22
  5. karrio/server/core/exceptions.py +287 -17
  6. karrio/server/core/filters.py +14 -0
  7. karrio/server/core/gateway.py +285 -10
  8. karrio/server/core/logging.py +403 -0
  9. karrio/server/core/management/commands/runserver.py +5 -0
  10. karrio/server/core/middleware.py +104 -2
  11. karrio/server/core/migrations/0006_add_api_log_requested_at_index.py +22 -0
  12. karrio/server/core/models/base.py +34 -1
  13. karrio/server/core/oauth_validators.py +2 -3
  14. karrio/server/core/permissions.py +1 -2
  15. karrio/server/core/serializers.py +183 -10
  16. karrio/server/core/signals.py +22 -28
  17. karrio/server/core/telemetry.py +573 -0
  18. karrio/server/core/tests/__init__.py +27 -0
  19. karrio/server/core/{tests.py → tests/base.py} +6 -7
  20. karrio/server/core/tests/test_exception_level.py +159 -0
  21. karrio/server/core/tests/test_resource_token.py +593 -0
  22. karrio/server/core/utils.py +688 -38
  23. karrio/server/core/validators.py +144 -222
  24. karrio/server/core/views/oauth.py +13 -12
  25. karrio/server/core/views/references.py +2 -2
  26. karrio/server/iam/apps.py +1 -4
  27. karrio/server/iam/migrations/0002_setup_carrier_permission_groups.py +103 -0
  28. karrio/server/iam/migrations/0003_remove_permission_groups.py +91 -0
  29. karrio/server/iam/permissions.py +7 -134
  30. karrio/server/iam/serializers.py +17 -2
  31. karrio/server/iam/signals.py +2 -4
  32. karrio/server/providers/admin.py +1 -1
  33. karrio/server/providers/management/commands/migrate_rate_sheets.py +101 -0
  34. karrio/server/providers/migrations/0082_add_zone_identifiers.py +50 -0
  35. karrio/server/providers/migrations/0083_add_optimized_rate_sheet_structure.py +33 -0
  36. karrio/server/providers/migrations/0084_alter_servicelevel_currency.py +168 -0
  37. karrio/server/providers/migrations/0085_populate_dhl_parcel_de_oauth_credentials.py +82 -0
  38. karrio/server/providers/migrations/0086_rename_dhl_parcel_de_customer_number_to_billing_number.py +71 -0
  39. karrio/server/providers/migrations/0087_alter_carrier_capabilities.py +38 -0
  40. karrio/server/providers/migrations/0088_servicelevel_surcharges.py +24 -0
  41. karrio/server/providers/migrations/0089_servicelevel_cost_max_volume.py +31 -0
  42. karrio/server/providers/migrations/0090_ratesheet_surcharges_servicelevel_zone_surcharge_ids.py +47 -0
  43. karrio/server/providers/migrations/0091_migrate_legacy_zones_surcharges.py +154 -0
  44. karrio/server/providers/models/__init__.py +1 -2
  45. karrio/server/providers/models/carrier.py +103 -18
  46. karrio/server/providers/models/service.py +188 -1
  47. karrio/server/providers/models/sheet.py +371 -0
  48. karrio/server/providers/serializers/base.py +263 -2
  49. karrio/server/providers/signals.py +2 -4
  50. karrio/server/providers/templates/providers/oauth_callback.html +105 -0
  51. karrio/server/providers/tests/__init__.py +5 -0
  52. karrio/server/providers/tests/test_connections.py +895 -0
  53. karrio/server/providers/views/carriers.py +1 -3
  54. karrio/server/providers/views/connections.py +322 -2
  55. karrio/server/samples.py +1 -1
  56. karrio/server/serializers/abstract.py +116 -21
  57. karrio/server/tracing/migrations/0007_tracingrecord_tracing_created_at_idx.py +19 -0
  58. karrio/server/tracing/models.py +2 -0
  59. karrio/server/tracing/utils.py +5 -8
  60. karrio/server/user/migrations/0007_user_metadata.py +25 -0
  61. karrio/server/user/models.py +38 -23
  62. karrio/server/user/serializers.py +1 -0
  63. karrio/server/user/templates/registration/registration_confirm_email.html +1 -1
  64. {karrio_server_core-2025.5rc12.dist-info → karrio_server_core-2026.1.1.dist-info}/METADATA +2 -2
  65. {karrio_server_core-2025.5rc12.dist-info → karrio_server_core-2026.1.1.dist-info}/RECORD +67 -86
  66. karrio/server/providers/extension/__init__.py +0 -1
  67. karrio/server/providers/extension/models/__init__.py +0 -1
  68. karrio/server/providers/extension/models/allied_express.py +0 -22
  69. karrio/server/providers/extension/models/allied_express_local.py +0 -22
  70. karrio/server/providers/extension/models/amazon_shipping.py +0 -27
  71. karrio/server/providers/extension/models/aramex.py +0 -25
  72. karrio/server/providers/extension/models/asendia_us.py +0 -21
  73. karrio/server/providers/extension/models/australiapost.py +0 -20
  74. karrio/server/providers/extension/models/boxknight.py +0 -19
  75. karrio/server/providers/extension/models/bpost.py +0 -21
  76. karrio/server/providers/extension/models/canadapost.py +0 -21
  77. karrio/server/providers/extension/models/canpar.py +0 -19
  78. karrio/server/providers/extension/models/chronopost.py +0 -22
  79. karrio/server/providers/extension/models/colissimo.py +0 -22
  80. karrio/server/providers/extension/models/dhl_express.py +0 -23
  81. karrio/server/providers/extension/models/dhl_parcel_de.py +0 -25
  82. karrio/server/providers/extension/models/dhl_poland.py +0 -22
  83. karrio/server/providers/extension/models/dhl_universal.py +0 -19
  84. karrio/server/providers/extension/models/dicom.py +0 -20
  85. karrio/server/providers/extension/models/dpd.py +0 -37
  86. karrio/server/providers/extension/models/dpdhl.py +0 -26
  87. karrio/server/providers/extension/models/easypost.py +0 -20
  88. karrio/server/providers/extension/models/eshipper.py +0 -21
  89. karrio/server/providers/extension/models/fedex.py +0 -25
  90. karrio/server/providers/extension/models/fedex_ws.py +0 -24
  91. karrio/server/providers/extension/models/freightcom.py +0 -21
  92. karrio/server/providers/extension/models/generic.py +0 -35
  93. karrio/server/providers/extension/models/geodis.py +0 -22
  94. karrio/server/providers/extension/models/hay_post.py +0 -22
  95. karrio/server/providers/extension/models/laposte.py +0 -19
  96. karrio/server/providers/extension/models/locate2u.py +0 -22
  97. karrio/server/providers/extension/models/nationex.py +0 -22
  98. karrio/server/providers/extension/models/purolator.py +0 -21
  99. karrio/server/providers/extension/models/roadie.py +0 -18
  100. karrio/server/providers/extension/models/royalmail.py +0 -19
  101. karrio/server/providers/extension/models/sendle.py +0 -22
  102. karrio/server/providers/extension/models/tge.py +0 -63
  103. karrio/server/providers/extension/models/tnt.py +0 -23
  104. karrio/server/providers/extension/models/ups.py +0 -23
  105. karrio/server/providers/extension/models/usps.py +0 -23
  106. karrio/server/providers/extension/models/usps_international.py +0 -23
  107. karrio/server/providers/extension/models/usps_wt.py +0 -24
  108. karrio/server/providers/extension/models/usps_wt_international.py +0 -24
  109. karrio/server/providers/extension/models/zoom2u.py +0 -23
  110. karrio/server/providers/tests.py +0 -3
  111. {karrio_server_core-2025.5rc12.dist-info → karrio_server_core-2026.1.1.dist-info}/WHEEL +0 -0
  112. {karrio_server_core-2025.5rc12.dist-info → karrio_server_core-2026.1.1.dist-info}/top_level.txt +0 -0
@@ -1,23 +1,12 @@
1
1
  import re
2
- import typing
3
- import logging
4
- import requests # type: ignore
5
2
  import phonenumbers
6
- from constance import config
7
3
  from datetime import datetime
4
+ from karrio.server.core.logging import logger
8
5
 
9
6
  import karrio.lib as lib
10
7
  import karrio.core.units as units
11
8
  import karrio.server.serializers as serializers
12
- import karrio.server.core.datatypes as datatypes
13
9
 
14
- # Try to import the references module, which may not be available in all environments
15
- try:
16
- import karrio.references as references
17
- except ImportError:
18
- references = None
19
-
20
- logger = logging.getLogger(__name__)
21
10
  DIMENSIONS = ["width", "height", "length"]
22
11
 
23
12
 
@@ -45,9 +34,13 @@ def dimensions_required_together(value):
45
34
  )
46
35
 
47
36
 
48
- def valid_time_format(prop: str):
49
- def validate(value):
37
+ class TimeFormatValidator:
38
+ """Validator for HH:MM time format that can be pickled."""
39
+
40
+ def __init__(self, prop: str):
41
+ self.prop = prop
50
42
 
43
+ def __call__(self, value):
51
44
  try:
52
45
  datetime.strptime(value, "%H:%M")
53
46
  except Exception:
@@ -56,12 +49,14 @@ def valid_time_format(prop: str):
56
49
  code="invalid",
57
50
  )
58
51
 
59
- return validate
60
52
 
53
+ class DateFormatValidator:
54
+ """Validator for YYYY-MM-DD date format that can be pickled."""
61
55
 
62
- def valid_date_format(prop: str):
63
- def validate(value):
56
+ def __init__(self, prop: str):
57
+ self.prop = prop
64
58
 
59
+ def __call__(self, value):
65
60
  try:
66
61
  datetime.strptime(value, "%Y-%m-%d")
67
62
  except Exception:
@@ -70,12 +65,14 @@ def valid_date_format(prop: str):
70
65
  code="invalid",
71
66
  )
72
67
 
73
- return validate
74
68
 
69
+ class DateTimeFormatValidator:
70
+ """Validator for YYYY-MM-DD HH:MM datetime format that can be pickled."""
75
71
 
76
- def valid_datetime_format(prop: str):
77
- def validate(value):
72
+ def __init__(self, prop: str):
73
+ self.prop = prop
78
74
 
75
+ def __call__(self, value):
79
76
  try:
80
77
  datetime.strptime(value, "%Y-%m-%d %H:%M")
81
78
  except Exception:
@@ -84,21 +81,40 @@ def valid_datetime_format(prop: str):
84
81
  code="invalid",
85
82
  )
86
83
 
87
- return validate
84
+
85
+ def valid_time_format(prop: str):
86
+ """Factory function for time format validator."""
87
+ return TimeFormatValidator(prop)
88
88
 
89
89
 
90
- def valid_base64(prop: str, max_size: int = 5242880):
91
- def validate(value: str):
90
+ def valid_date_format(prop: str):
91
+ """Factory function for date format validator."""
92
+ return DateFormatValidator(prop)
93
+
94
+
95
+ def valid_datetime_format(prop: str):
96
+ """Factory function for datetime format validator."""
97
+ return DateTimeFormatValidator(prop)
98
+
99
+
100
+ class Base64Validator:
101
+ """Validator for base64 encoded content that can be pickled."""
102
+
103
+ def __init__(self, prop: str, max_size: int = 5242880):
104
+ self.prop = prop
105
+ self.max_size = max_size
106
+
107
+ def __call__(self, value: str):
92
108
  error = None
93
109
 
94
110
  try:
95
111
  buffer = lib.to_buffer(value, validate=True)
96
112
 
97
- if buffer.getbuffer().nbytes > max_size:
98
- error = f"Error: file size exceeds {max_size} bytes."
113
+ if buffer.getbuffer().nbytes > self.max_size:
114
+ error = f"Error: file size exceeds {self.max_size} bytes."
99
115
 
100
116
  except Exception as e:
101
- logger.exception(e)
117
+ logger.error("Invalid base64 file content", error=str(e))
102
118
  error = "Invalid base64 file content"
103
119
  raise serializers.ValidationError(
104
120
  error,
@@ -108,7 +124,10 @@ def valid_base64(prop: str, max_size: int = 5242880):
108
124
  if error is not None:
109
125
  raise serializers.ValidationError(error, code="invalid")
110
126
 
111
- return validate
127
+
128
+ def valid_base64(prop: str, max_size: int = 5242880):
129
+ """Factory function for base64 validator."""
130
+ return Base64Validator(prop, max_size)
112
131
 
113
132
 
114
133
  class OptionDefaultSerializer(serializers.Serializer):
@@ -158,32 +177,114 @@ class PresetSerializer(serializers.Serializer):
158
177
  dimensions_required_together(data)
159
178
 
160
179
  if data is not None and "package_preset" in data:
161
- preset = next(
162
- (
163
- presets[data["package_preset"]]
164
- for _, presets in dataunits.REFERENCE_MODELS[
165
- "package_presets"
166
- ].items()
167
- if data["package_preset"] in presets
168
- ),
169
- {},
180
+ package_presets = dataunits.REFERENCE_MODELS.get("package_presets", {})
181
+ preset_name = data["package_preset"]
182
+
183
+ # Find the preset across all carriers
184
+ preset = lib.identity(
185
+ next(
186
+ (
187
+ presets[preset_name]
188
+ for carrier_id, presets in package_presets.items()
189
+ if preset_name in presets
190
+ ),
191
+ None,
192
+ )
193
+ or {}
170
194
  )
171
195
 
172
196
  data.update(
173
197
  {
174
198
  **data,
175
- "width": data.get("width", preset.get("width")),
176
- "length": data.get("length", preset.get("length")),
177
- "height": data.get("height", preset.get("height")),
178
- "dimension_unit": data.get(
179
- "dimension_unit", preset.get("dimension_unit")
180
- ),
199
+ "width": data.get("width") or preset.get("width"),
200
+ "length": data.get("length") or preset.get("length"),
201
+ "height": data.get("height") or preset.get("height"),
202
+ "dimension_unit": data.get("dimension_unit")
203
+ or preset.get("dimension_unit"),
181
204
  }
182
205
  )
183
206
 
184
207
  return data
185
208
 
186
209
 
210
+ def shipment_documents_accessor(cls=None, *, include_base64: bool = False):
211
+ """
212
+ Class decorator that computes shipping_documents for Shipment serializers.
213
+
214
+ When applied to a serializer class, this decorator overrides to_representation()
215
+ to dynamically build the shipping_documents list based on the shipment's
216
+ label and invoice fields.
217
+
218
+ Args:
219
+ include_base64: If True, includes base64 content in shipping_documents.
220
+ If False (default), only includes URLs.
221
+
222
+ Usage:
223
+ @shipment_documents_accessor
224
+ class Shipment(Serializer):
225
+ ... # shipping_documents will have URLs only
226
+
227
+ @shipment_documents_accessor(include_base64=True)
228
+ class PurchasedShipment(Shipment):
229
+ ... # shipping_documents will include base64 content
230
+ """
231
+
232
+ def decorator(klass):
233
+ # Store the flag on the class for reference
234
+ klass._include_base64_documents = include_base64
235
+
236
+ # Store original to_representation
237
+ original_to_representation = klass.to_representation
238
+
239
+ def to_representation(self, instance):
240
+ # Get the original serialized data
241
+ data = original_to_representation(self, instance)
242
+
243
+ # Build shipping_documents dynamically
244
+ documents = []
245
+
246
+ # Add label document if exists
247
+ label = getattr(instance, "label", None)
248
+ if label:
249
+ label_format = getattr(instance, "label_type", None) or "PDF"
250
+ documents.append(
251
+ {
252
+ "category": "label",
253
+ "format": label_format,
254
+ "url": getattr(instance, "label_url", None),
255
+ "base64": label if include_base64 else None,
256
+ }
257
+ )
258
+
259
+ # Add invoice document if exists
260
+ invoice = getattr(instance, "invoice", None)
261
+ if invoice:
262
+ documents.append(
263
+ {
264
+ "category": "invoice",
265
+ "format": "PDF",
266
+ "url": getattr(instance, "invoice_url", None),
267
+ "base64": invoice if include_base64 else None,
268
+ }
269
+ )
270
+
271
+ # Update the data with computed shipping_documents
272
+ data["shipping_documents"] = documents
273
+
274
+ return data
275
+
276
+ klass.to_representation = to_representation
277
+ return klass
278
+
279
+ # Handle both @shipment_documents_accessor and @shipment_documents_accessor(...)
280
+ if cls is not None:
281
+ # Called as @shipment_documents_accessor without parentheses
282
+ return decorator(cls)
283
+ else:
284
+ # Called as @shipment_documents_accessor(...) with arguments
285
+ return decorator
286
+
287
+
187
288
  class AugmentedAddressSerializer(serializers.Serializer):
188
289
  def validate(self, data):
189
290
  # Format and validate Postal Code
@@ -233,188 +334,9 @@ class AugmentedAddressSerializer(serializers.Serializer):
233
334
  }
234
335
  )
235
336
  except Exception as e:
236
- logger.warning(e)
337
+ logger.warning("Invalid phone number format", error=str(e))
237
338
  raise serializers.ValidationError(
238
339
  {"phone_number": "Invalid phone number format"}
239
340
  )
240
341
 
241
342
  return data
242
-
243
-
244
- class AddressValidatorAbstract:
245
- """
246
- Abstract base class for address validators.
247
-
248
- This class defines the interface that all address validators must implement.
249
- Note: This class is kept here for backwards compatibility.
250
- New validators should use the karrio.validators.abstract.AddressValidatorAbstract class.
251
- """
252
-
253
- @staticmethod
254
- def get_info(is_authenticated: bool = None) -> dict:
255
- """
256
- Get information about the validator.
257
-
258
- Args:
259
- is_authenticated: Whether authenticated information should be returned
260
-
261
- Returns:
262
- Dictionary with information about the validator
263
-
264
- Raises:
265
- Exception: If the method is not implemented
266
- """
267
- raise Exception("get_info method is not implemented")
268
-
269
- @staticmethod
270
- def validate(address: datatypes.Address) -> datatypes.AddressValidation:
271
- """
272
- Validate an address using the service.
273
-
274
- Args:
275
- address: Address object to validate
276
-
277
- Returns:
278
- Address validation result
279
-
280
- Raises:
281
- Exception: If the method is not implemented
282
- """
283
- raise Exception("validate method is not implemented")
284
-
285
-
286
- class Address:
287
- """
288
- Address validation service provider.
289
-
290
- This class provides methods to validate addresses using various service providers
291
- which are loaded as plugins.
292
- """
293
-
294
- @staticmethod
295
- def get_info(is_authenticated: bool = True) -> dict:
296
- """
297
- Get information about the available address validation services.
298
-
299
- Args:
300
- is_authenticated: Whether to include sensitive information like API keys
301
-
302
- Returns:
303
- Dictionary with information about the available validators
304
- """
305
- # If references module is available, get validator plugins info
306
- refs = references.REFERENCES
307
- if len(refs.get("address_validators", {})) > 0:
308
- # Return information about the first available validator
309
- validator_name = next(iter(refs["address_validators"].keys()))
310
- validator_class = None
311
-
312
- # Try to get the validator class from the references
313
- try:
314
- # Import the validator module dynamically
315
- import importlib
316
-
317
- module = importlib.import_module(f"karrio.validators.{validator_name}")
318
- if hasattr(module, "METADATA"):
319
- validator_class = module.METADATA.Validator
320
- except (ImportError, AttributeError) as e:
321
- logger.warning(f"Could not import validator {validator_name}: {e}")
322
-
323
- if validator_class is not None:
324
- # Return validator info with is_enabled=True
325
- validator_info = validator_class.get_info(is_authenticated)
326
- return {"is_enabled": True, **validator_info}
327
-
328
- # Check for legacy config-based validation
329
- is_enabled = any(
330
- [config.GOOGLE_CLOUD_API_KEY, config.CANADAPOST_ADDRESS_COMPLETE_API_KEY]
331
- )
332
-
333
- if is_enabled:
334
- # Legacy validation is enabled, use the legacy validator
335
- return {
336
- "is_enabled": is_enabled,
337
- **Address._get_legacy_validator().get_info(is_authenticated),
338
- }
339
-
340
- # No validation is available
341
- return dict(is_enabled=False)
342
-
343
- @staticmethod
344
- def _get_legacy_validator() -> typing.Type[AddressValidatorAbstract]:
345
- """
346
- Get a legacy validator instance based on configuration.
347
-
348
- This method is used for backwards compatibility with the old validation system.
349
-
350
- Returns:
351
- An instance of a validator class
352
-
353
- Raises:
354
- Exception: If no validator is configured
355
- """
356
- # For backwards compatibility, check if Google or Canada Post is configured
357
- if any(config.GOOGLE_CLOUD_API_KEY or ""):
358
- from karrio.validators.googlegeocoding import Validator as GoogleGeocode
359
-
360
- return GoogleGeocode
361
- elif any(config.CANADAPOST_ADDRESS_COMPLETE_API_KEY or ""):
362
- from karrio.validators.addresscomplete import Validator as AddressComplete
363
-
364
- return AddressComplete
365
-
366
- raise Exception("No address validation service provider configured")
367
-
368
- @staticmethod
369
- def get_validator() -> typing.Type:
370
- """
371
- Get a validator instance based on configuration or plugins.
372
-
373
- This method first checks for validator plugins, then falls back to
374
- legacy validators if no plugins are available.
375
-
376
- Returns:
377
- An instance of a validator class
378
-
379
- Raises:
380
- Exception: If no validator is configured
381
- """
382
- # If references module is available, check for validator plugins
383
- refs = references.REFERENCES
384
- if len(refs.get("address_validators", {})) > 0:
385
- # Get the first available validator
386
- validator_name = next(iter(refs["address_validators"].keys()))
387
-
388
- # Try to get the validator class from the references
389
- try:
390
- # Import the validator module dynamically
391
- import importlib
392
-
393
- module = importlib.import_module(f"karrio.validators.{validator_name}")
394
- if hasattr(module, "METADATA"):
395
- return module.METADATA.Validator
396
- except (ImportError, AttributeError) as e:
397
- logger.warning(f"Could not import validator {validator_name}: {e}")
398
-
399
- # Fall back to legacy validator
400
- return Address._get_legacy_validator()
401
-
402
- @staticmethod
403
- def validate(address: datatypes.Address) -> datatypes.AddressValidation:
404
- """
405
- Validate an address using the configured validator.
406
-
407
- Args:
408
- address: The address to validate
409
-
410
- Returns:
411
- AddressValidation object with validation results
412
- """
413
- validator = Address.get_validator()
414
- result = validator.validate(address)
415
-
416
- # Handle the case where the validator returns a dict instead of AddressValidation
417
- if isinstance(result, dict):
418
- return datatypes.AddressValidation(**result)
419
-
420
- return result
@@ -1,9 +1,7 @@
1
- import logging
2
1
  from django.views.decorators.csrf import csrf_exempt
3
2
  from django.utils.decorators import method_decorator
4
3
  from oauth2_provider.views import TokenView as BaseTokenView
5
-
6
- logger = logging.getLogger(__name__)
4
+ from karrio.server.core.logging import logger
7
5
 
8
6
 
9
7
  @method_decorator(csrf_exempt, name='dispatch')
@@ -20,11 +18,12 @@ class CustomTokenView(BaseTokenView):
20
18
  """
21
19
  Handle token requests with grant type conversion.
22
20
  """
23
- print(f"CustomTokenView called")
24
- print(f"Request method: {request.method}")
25
- print(f"Request content type: {request.content_type}")
26
- print(f"Request POST: {dict(request.POST)}")
27
- print(f"Request body: {request.body.decode('utf-8') if request.body else 'Empty'}")
21
+ logger.debug("CustomTokenView called")
22
+ logger.debug("Processing token request",
23
+ method=request.method,
24
+ content_type=request.content_type,
25
+ post_data=dict(request.POST),
26
+ body=request.body.decode('utf-8') if request.body else 'Empty')
28
27
 
29
28
  # Parse the request body if POST data is empty
30
29
  if not request.POST and request.body:
@@ -54,21 +53,23 @@ class CustomTokenView(BaseTokenView):
54
53
  }
55
54
 
56
55
  original_grant_type = request.POST.get('grant_type')
57
- print(f"Original grant type after parsing: {original_grant_type}")
56
+ logger.debug("Grant type parsed", grant_type=original_grant_type)
58
57
 
59
58
  if original_grant_type in grant_type_mapping:
60
59
  # Create a mutable copy of the POST data
61
60
  post_data = request.POST.copy()
62
61
  converted_grant_type = grant_type_mapping[original_grant_type]
63
- print(f"Converting grant type from '{original_grant_type}' to '{converted_grant_type}'")
62
+ logger.debug("Converting grant type",
63
+ original=original_grant_type,
64
+ converted=converted_grant_type)
64
65
  post_data['grant_type'] = converted_grant_type
65
66
 
66
67
  # Replace the request POST data
67
68
  request.POST = post_data
68
69
  request._post = post_data
69
- print(f"Updated grant type: {request.POST.get('grant_type')}")
70
+ logger.debug("Grant type updated", grant_type=request.POST.get('grant_type'))
70
71
  else:
71
- print(f"No conversion needed for grant type: {original_grant_type}")
72
+ logger.debug("No grant type conversion needed", grant_type=original_grant_type)
72
73
 
73
74
  # Call the parent token view with converted grant type
74
75
  return super().post(request, *args, **kwargs)
@@ -73,9 +73,9 @@ def references(request: Request):
73
73
  status=status.HTTP_200_OK,
74
74
  )
75
75
  except Exception as e:
76
- import logging
76
+ from karrio.server.core.logging import logger
77
77
 
78
- logging.exception(e)
78
+ logger.exception("Failed to retrieve references", error=str(e))
79
79
  raise e
80
80
 
81
81
 
karrio/server/iam/apps.py CHANGED
@@ -9,13 +9,10 @@ class IamConfig(AppConfig):
9
9
 
10
10
  def ready(self):
11
11
  from karrio.server.core import utils
12
- from karrio.server.iam import signals, permissions
12
+ from karrio.server.iam import signals
13
13
 
14
14
  @utils.skip_on_commands()
15
15
  def _init():
16
16
  signals.register_all()
17
17
 
18
- # Setup default permission groups and apply to existing orgs on start up
19
- utils.run_on_all_tenants(permissions.setup_groups)()
20
-
21
18
  _init()
@@ -0,0 +1,103 @@
1
+ # Generated by Django migration for carrier permission groups
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ def setup_carrier_groups(apps, schema_editor):
7
+ """
8
+ Create read_carriers and write_carriers permission groups and update
9
+ existing organization user permissions to include read_carriers.
10
+
11
+ This migration addresses the permission regression where:
12
+ 1. read_carriers and write_carriers groups were added to ROLES_GROUPS
13
+ 2. But existing user ContextPermissions weren't updated with these groups
14
+ """
15
+ Group = apps.get_model('user', 'Group')
16
+ Permission = apps.get_model('auth', 'Permission')
17
+ ContentType = apps.get_model('contenttypes', 'ContentType')
18
+ ContextPermission = apps.get_model('iam', 'ContextPermission')
19
+
20
+ # Create the new permission groups if they don't exist
21
+ read_carriers_group, _ = Group.objects.get_or_create(name='read_carriers')
22
+ write_carriers_group, _ = Group.objects.get_or_create(name='write_carriers')
23
+
24
+ # Get providers content types for permissions
25
+ try:
26
+ providers_ct = ContentType.objects.filter(app_label='providers')
27
+
28
+ # Set up read_carriers with view permissions
29
+ view_perms = Permission.objects.filter(
30
+ content_type__in=providers_ct,
31
+ codename__icontains='view'
32
+ )
33
+ if view_perms.exists():
34
+ read_carriers_group.permissions.set(view_perms)
35
+
36
+ # Set up write_carriers with all providers permissions
37
+ all_provider_perms = Permission.objects.filter(content_type__in=providers_ct)
38
+ if all_provider_perms.exists():
39
+ write_carriers_group.permissions.set(all_provider_perms)
40
+ except Exception:
41
+ # Providers app may not be installed yet
42
+ pass
43
+
44
+ # Update all existing ContextPermissions that have manage_carriers to also include read_carriers
45
+ manage_carriers_group = Group.objects.filter(name='manage_carriers').first()
46
+
47
+ if manage_carriers_group:
48
+ # Find all context permissions that have manage_carriers
49
+ context_perms_with_manage = ContextPermission.objects.filter(
50
+ groups=manage_carriers_group
51
+ )
52
+
53
+ # Add read_carriers and write_carriers to these context permissions
54
+ for ctx_perm in context_perms_with_manage:
55
+ ctx_perm.groups.add(read_carriers_group)
56
+ ctx_perm.groups.add(write_carriers_group)
57
+
58
+ # Also update any OrganizationUser context permissions based on their roles
59
+ # This ensures all users who should have read_carriers get it
60
+ try:
61
+ OrganizationUser = apps.get_model('orgs', 'OrganizationUser')
62
+ org_user_ct = ContentType.objects.get_for_model(OrganizationUser)
63
+
64
+ # Roles that should have read_carriers (all roles)
65
+ org_user_context_perms = ContextPermission.objects.filter(
66
+ content_type=org_user_ct
67
+ )
68
+
69
+ for ctx_perm in org_user_context_perms:
70
+ # All organization users should have read_carriers by default
71
+ ctx_perm.groups.add(read_carriers_group)
72
+ except Exception:
73
+ # Orgs app may not be installed
74
+ pass
75
+
76
+
77
+ def reverse_migration(apps, schema_editor):
78
+ """Reverse migration - remove the new groups from context permissions."""
79
+ Group = apps.get_model('user', 'Group')
80
+ ContextPermission = apps.get_model('iam', 'ContextPermission')
81
+
82
+ read_carriers_group = Group.objects.filter(name='read_carriers').first()
83
+ write_carriers_group = Group.objects.filter(name='write_carriers').first()
84
+
85
+ if read_carriers_group:
86
+ for ctx_perm in ContextPermission.objects.filter(groups=read_carriers_group):
87
+ ctx_perm.groups.remove(read_carriers_group)
88
+
89
+ if write_carriers_group:
90
+ for ctx_perm in ContextPermission.objects.filter(groups=write_carriers_group):
91
+ ctx_perm.groups.remove(write_carriers_group)
92
+
93
+
94
+ class Migration(migrations.Migration):
95
+
96
+ dependencies = [
97
+ ('iam', '0001_initial'),
98
+ ('user', '0004_group'),
99
+ ]
100
+
101
+ operations = [
102
+ migrations.RunPython(setup_carrier_groups, reverse_migration),
103
+ ]