karrio-server-core 2025.5rc31__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.
- karrio/server/core/authentication.py +38 -20
- karrio/server/core/config.py +31 -0
- karrio/server/core/datatypes.py +30 -4
- karrio/server/core/dataunits.py +26 -7
- karrio/server/core/exceptions.py +287 -17
- karrio/server/core/filters.py +14 -0
- karrio/server/core/gateway.py +284 -11
- karrio/server/core/logging.py +403 -0
- karrio/server/core/middleware.py +104 -2
- karrio/server/core/models/base.py +34 -1
- karrio/server/core/oauth_validators.py +2 -3
- karrio/server/core/permissions.py +1 -2
- karrio/server/core/serializers.py +154 -7
- karrio/server/core/signals.py +22 -28
- karrio/server/core/telemetry.py +573 -0
- karrio/server/core/tests/__init__.py +27 -0
- karrio/server/core/{tests.py → tests/base.py} +6 -7
- karrio/server/core/tests/test_exception_level.py +159 -0
- karrio/server/core/tests/test_resource_token.py +593 -0
- karrio/server/core/utils.py +688 -38
- karrio/server/core/validators.py +144 -222
- karrio/server/core/views/oauth.py +13 -12
- karrio/server/core/views/references.py +2 -2
- karrio/server/iam/apps.py +1 -4
- karrio/server/iam/migrations/0002_setup_carrier_permission_groups.py +103 -0
- karrio/server/iam/migrations/0003_remove_permission_groups.py +91 -0
- karrio/server/iam/permissions.py +7 -134
- karrio/server/iam/serializers.py +9 -3
- karrio/server/iam/signals.py +2 -4
- karrio/server/providers/admin.py +1 -1
- karrio/server/providers/migrations/0085_populate_dhl_parcel_de_oauth_credentials.py +82 -0
- karrio/server/providers/migrations/0086_rename_dhl_parcel_de_customer_number_to_billing_number.py +71 -0
- karrio/server/providers/migrations/0087_alter_carrier_capabilities.py +38 -0
- karrio/server/providers/migrations/0088_servicelevel_surcharges.py +24 -0
- karrio/server/providers/migrations/0089_servicelevel_cost_max_volume.py +31 -0
- karrio/server/providers/migrations/0090_ratesheet_surcharges_servicelevel_zone_surcharge_ids.py +47 -0
- karrio/server/providers/migrations/0091_migrate_legacy_zones_surcharges.py +154 -0
- karrio/server/providers/models/carrier.py +101 -29
- karrio/server/providers/models/service.py +182 -125
- karrio/server/providers/models/sheet.py +342 -198
- karrio/server/providers/serializers/base.py +263 -2
- karrio/server/providers/signals.py +2 -4
- karrio/server/providers/templates/providers/oauth_callback.html +105 -0
- karrio/server/providers/tests/__init__.py +5 -0
- karrio/server/providers/tests/test_connections.py +895 -0
- karrio/server/providers/views/carriers.py +1 -3
- karrio/server/providers/views/connections.py +322 -2
- karrio/server/serializers/abstract.py +112 -21
- karrio/server/tracing/utils.py +5 -8
- karrio/server/user/models.py +36 -34
- karrio/server/user/serializers.py +1 -0
- {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/METADATA +2 -2
- {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/RECORD +55 -38
- karrio/server/providers/tests.py +0 -3
- {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/WHEEL +0 -0
- {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/top_level.txt +0 -0
karrio/server/core/validators.py
CHANGED
|
@@ -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
|
-
|
|
49
|
-
|
|
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
|
|
63
|
-
|
|
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
|
|
77
|
-
|
|
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
|
-
|
|
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
|
|
91
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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"
|
|
176
|
-
"length": data.get("length"
|
|
177
|
-
"height": data.get("height"
|
|
178
|
-
"dimension_unit": data.get(
|
|
179
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
+
logger.debug("Grant type updated", grant_type=request.POST.get('grant_type'))
|
|
70
71
|
else:
|
|
71
|
-
|
|
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
|
|
76
|
+
from karrio.server.core.logging import logger
|
|
77
77
|
|
|
78
|
-
|
|
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
|
|
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
|
+
]
|