auth-gate 0.2.2__tar.gz → 0.2.3__tar.gz
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.
- {auth_gate-0.2.2/src/auth_gate.egg-info → auth_gate-0.2.3}/PKG-INFO +39 -1
- {auth_gate-0.2.2 → auth_gate-0.2.3}/README.md +38 -0
- {auth_gate-0.2.2 → auth_gate-0.2.3}/pyproject.toml +1 -1
- {auth_gate-0.2.2 → auth_gate-0.2.3}/setup.cfg +1 -1
- {auth_gate-0.2.2 → auth_gate-0.2.3}/src/auth_gate/__init__.py +33 -6
- {auth_gate-0.2.2 → auth_gate-0.2.3}/src/auth_gate/middleware.py +192 -5
- {auth_gate-0.2.2 → auth_gate-0.2.3}/src/auth_gate/user_auth.py +1 -1
- {auth_gate-0.2.2 → auth_gate-0.2.3/src/auth_gate.egg-info}/PKG-INFO +39 -1
- {auth_gate-0.2.2 → auth_gate-0.2.3}/src/tests/test_intergration.py +2 -2
- {auth_gate-0.2.2 → auth_gate-0.2.3}/src/tests/test_middleware.py +344 -1
- {auth_gate-0.2.2 → auth_gate-0.2.3}/src/tests/test_user_auth.py +1 -1
- {auth_gate-0.2.2 → auth_gate-0.2.3}/LICENSE +0 -0
- {auth_gate-0.2.2 → auth_gate-0.2.3}/src/auth_gate/config.py +0 -0
- {auth_gate-0.2.2 → auth_gate-0.2.3}/src/auth_gate/fastapi_utils.py +0 -0
- {auth_gate-0.2.2 → auth_gate-0.2.3}/src/auth_gate/s2s_auth.py +0 -0
- {auth_gate-0.2.2 → auth_gate-0.2.3}/src/auth_gate/schemas.py +0 -0
- {auth_gate-0.2.2 → auth_gate-0.2.3}/src/auth_gate.egg-info/SOURCES.txt +0 -0
- {auth_gate-0.2.2 → auth_gate-0.2.3}/src/auth_gate.egg-info/dependency_links.txt +0 -0
- {auth_gate-0.2.2 → auth_gate-0.2.3}/src/auth_gate.egg-info/requires.txt +0 -0
- {auth_gate-0.2.2 → auth_gate-0.2.3}/src/auth_gate.egg-info/top_level.txt +0 -0
- {auth_gate-0.2.2 → auth_gate-0.2.3}/src/tests/__init__.py +0 -0
- {auth_gate-0.2.2 → auth_gate-0.2.3}/src/tests/conftest.py +0 -0
- {auth_gate-0.2.2 → auth_gate-0.2.3}/src/tests/test_config.py +0 -0
- {auth_gate-0.2.2 → auth_gate-0.2.3}/src/tests/test_fastapi_utils.py +0 -0
- {auth_gate-0.2.2 → auth_gate-0.2.3}/src/tests/test_s2s_auth.py +0 -0
- {auth_gate-0.2.2 → auth_gate-0.2.3}/src/tests/test_schema.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: auth-gate
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Enterprise-grade authentication for microservices with Kong and Keycloak integration
|
|
5
5
|
Home-page: https://github.com/tradelink-org/auth-gate
|
|
6
6
|
Author: Brian Mburu
|
|
@@ -290,6 +290,44 @@ app.add_middleware(
|
|
|
290
290
|
)
|
|
291
291
|
```
|
|
292
292
|
|
|
293
|
+
### Parameterized Paths with UUID Matching
|
|
294
|
+
|
|
295
|
+
You can exclude or make paths optional using UUID v4 parameters:
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
app.add_middleware(
|
|
299
|
+
AuthMiddleware,
|
|
300
|
+
excluded_paths={
|
|
301
|
+
"/api/v1/categories/{category_id:uuid}": {"GET"}, # Public read
|
|
302
|
+
"/api/v1/products/{product_id:uuid}": {"GET"},
|
|
303
|
+
},
|
|
304
|
+
excluded_prefixes={
|
|
305
|
+
"/api/{version:uuid}": {"GET"}, # Version-specific docs
|
|
306
|
+
},
|
|
307
|
+
optional_auth_paths={
|
|
308
|
+
"/api/v1/recommendations/{user_id:uuid}": {"GET"}, # Personalized if authenticated
|
|
309
|
+
}
|
|
310
|
+
)
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**Pattern Syntax:**
|
|
314
|
+
- `{param:uuid}` - Matches valid UUID v4 format (case-insensitive)
|
|
315
|
+
- Works with exact paths, prefixes, and optional auth paths
|
|
316
|
+
- Supports method-specific exclusions
|
|
317
|
+
- Exact matches take precedence over patterns
|
|
318
|
+
|
|
319
|
+
**Example Behavior:**
|
|
320
|
+
```python
|
|
321
|
+
# Matches: /api/v1/categories/7b5bcc8f-2c99-43c0-9c7d-e27c10881bd2
|
|
322
|
+
# Does not match: /api/v1/categories/invalid-id
|
|
323
|
+
# Does not match: /api/v1/categories/all
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**UUID v4 Validation:**
|
|
327
|
+
- Must have version digit "4" in the correct position
|
|
328
|
+
- Must have variant bits (8, 9, a, or b) in the correct position
|
|
329
|
+
- Accepts uppercase, lowercase, or mixed case
|
|
330
|
+
|
|
293
331
|
### Direct Validator Usage
|
|
294
332
|
|
|
295
333
|
```python
|
|
@@ -249,6 +249,44 @@ app.add_middleware(
|
|
|
249
249
|
)
|
|
250
250
|
```
|
|
251
251
|
|
|
252
|
+
### Parameterized Paths with UUID Matching
|
|
253
|
+
|
|
254
|
+
You can exclude or make paths optional using UUID v4 parameters:
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
app.add_middleware(
|
|
258
|
+
AuthMiddleware,
|
|
259
|
+
excluded_paths={
|
|
260
|
+
"/api/v1/categories/{category_id:uuid}": {"GET"}, # Public read
|
|
261
|
+
"/api/v1/products/{product_id:uuid}": {"GET"},
|
|
262
|
+
},
|
|
263
|
+
excluded_prefixes={
|
|
264
|
+
"/api/{version:uuid}": {"GET"}, # Version-specific docs
|
|
265
|
+
},
|
|
266
|
+
optional_auth_paths={
|
|
267
|
+
"/api/v1/recommendations/{user_id:uuid}": {"GET"}, # Personalized if authenticated
|
|
268
|
+
}
|
|
269
|
+
)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Pattern Syntax:**
|
|
273
|
+
- `{param:uuid}` - Matches valid UUID v4 format (case-insensitive)
|
|
274
|
+
- Works with exact paths, prefixes, and optional auth paths
|
|
275
|
+
- Supports method-specific exclusions
|
|
276
|
+
- Exact matches take precedence over patterns
|
|
277
|
+
|
|
278
|
+
**Example Behavior:**
|
|
279
|
+
```python
|
|
280
|
+
# Matches: /api/v1/categories/7b5bcc8f-2c99-43c0-9c7d-e27c10881bd2
|
|
281
|
+
# Does not match: /api/v1/categories/invalid-id
|
|
282
|
+
# Does not match: /api/v1/categories/all
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**UUID v4 Validation:**
|
|
286
|
+
- Must have version digit "4" in the correct position
|
|
287
|
+
- Must have variant bits (8, 9, a, or b) in the correct position
|
|
288
|
+
- Accepts uppercase, lowercase, or mixed case
|
|
289
|
+
|
|
252
290
|
### Direct Validator Usage
|
|
253
291
|
|
|
254
292
|
```python
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "auth-gate"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.3"
|
|
8
8
|
description = "Enterprise-grade authentication for microservices with Kong and Keycloak integration"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -4,46 +4,67 @@ Tradelink Authentication Client
|
|
|
4
4
|
Enterprise authentication client for microservices with Kong/Keycloak integration.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from .config import AuthMode, AuthSettings
|
|
7
|
+
from .config import AuthMode, AuthSettings, get_settings, reset_settings
|
|
8
8
|
from .fastapi_utils import (
|
|
9
9
|
get_current_auth,
|
|
10
10
|
get_current_service,
|
|
11
11
|
get_current_user,
|
|
12
12
|
get_optional_auth,
|
|
13
13
|
get_optional_user,
|
|
14
|
+
is_bypass_mode,
|
|
15
|
+
is_using_keycloak,
|
|
16
|
+
is_using_kong,
|
|
14
17
|
require_admin,
|
|
15
18
|
require_customer,
|
|
16
19
|
require_moderator,
|
|
17
20
|
require_roles,
|
|
18
21
|
require_scopes,
|
|
22
|
+
require_service_roles,
|
|
19
23
|
require_supplier,
|
|
20
24
|
require_supplier_or_admin,
|
|
21
25
|
require_user_admin,
|
|
22
26
|
require_user_customer,
|
|
23
27
|
require_user_moderator,
|
|
28
|
+
require_user_roles,
|
|
24
29
|
require_user_supplier,
|
|
25
30
|
verify_hmac_signature,
|
|
26
31
|
)
|
|
27
32
|
from .middleware import AuthMiddleware
|
|
28
|
-
from .s2s_auth import
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
from .s2s_auth import (
|
|
34
|
+
CircuitBreaker,
|
|
35
|
+
CircuitBreakerOpenError,
|
|
36
|
+
CircuitState,
|
|
37
|
+
ServiceAuthClient,
|
|
38
|
+
ServiceToken,
|
|
39
|
+
get_service_auth_client,
|
|
40
|
+
)
|
|
41
|
+
from .schemas import AuthContext, BaseAuthContext, ServiceContext, UserContext
|
|
42
|
+
from .user_auth import HMACVerifier, UserValidator, get_user_validator
|
|
31
43
|
|
|
32
|
-
__version__ = "0.2.
|
|
44
|
+
__version__ = "0.2.3"
|
|
33
45
|
|
|
34
46
|
__all__ = [
|
|
35
47
|
# Configuration
|
|
36
48
|
"AuthSettings",
|
|
37
49
|
"AuthMode",
|
|
38
|
-
|
|
50
|
+
"get_settings",
|
|
51
|
+
"reset_settings",
|
|
52
|
+
# Schemas & Type Aliases
|
|
39
53
|
"UserContext",
|
|
40
54
|
"ServiceContext",
|
|
55
|
+
"AuthContext",
|
|
56
|
+
"BaseAuthContext",
|
|
41
57
|
# User Authentication
|
|
42
58
|
"UserValidator",
|
|
59
|
+
"HMACVerifier",
|
|
60
|
+
"get_user_validator",
|
|
43
61
|
# Service-to-Service
|
|
44
62
|
"ServiceAuthClient",
|
|
45
63
|
"CircuitBreaker",
|
|
46
64
|
"CircuitBreakerOpenError",
|
|
65
|
+
"CircuitState",
|
|
66
|
+
"ServiceToken",
|
|
67
|
+
"get_service_auth_client",
|
|
47
68
|
# Middleware
|
|
48
69
|
"AuthMiddleware",
|
|
49
70
|
# FastAPI Dependencies
|
|
@@ -53,6 +74,8 @@ __all__ = [
|
|
|
53
74
|
"get_optional_auth",
|
|
54
75
|
"get_optional_user",
|
|
55
76
|
"require_roles",
|
|
77
|
+
"require_user_roles",
|
|
78
|
+
"require_service_roles",
|
|
56
79
|
"require_scopes",
|
|
57
80
|
"require_admin",
|
|
58
81
|
"require_supplier",
|
|
@@ -64,4 +87,8 @@ __all__ = [
|
|
|
64
87
|
"require_user_customer",
|
|
65
88
|
"require_user_supplier",
|
|
66
89
|
"require_user_moderator",
|
|
90
|
+
# Mode Utilities
|
|
91
|
+
"is_using_kong",
|
|
92
|
+
"is_using_keycloak",
|
|
93
|
+
"is_bypass_mode",
|
|
67
94
|
]
|
|
@@ -3,8 +3,9 @@ FastAPI middleware for authentication (with service-to-service support)
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
+
import re
|
|
6
7
|
import time
|
|
7
|
-
from typing import Dict, Optional, Set, Union
|
|
8
|
+
from typing import Dict, List, Optional, Pattern, Set, Tuple, Union
|
|
8
9
|
|
|
9
10
|
from fastapi import HTTPException, Request, status
|
|
10
11
|
from fastapi.responses import JSONResponse
|
|
@@ -16,6 +17,12 @@ from .user_auth import get_user_validator
|
|
|
16
17
|
|
|
17
18
|
logger = logging.getLogger(__name__)
|
|
18
19
|
|
|
20
|
+
# UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
|
21
|
+
# where y is 8, 9, a, or b (variant bits)
|
|
22
|
+
TYPE_PATTERNS = {
|
|
23
|
+
"uuid": r"[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}",
|
|
24
|
+
}
|
|
25
|
+
|
|
19
26
|
|
|
20
27
|
class AuthMiddleware(BaseHTTPMiddleware):
|
|
21
28
|
"""
|
|
@@ -73,6 +80,16 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|
|
73
80
|
# Paths where authentication is optional
|
|
74
81
|
self.optional_auth_dict = self._normalize_paths(optional_auth_paths or set())
|
|
75
82
|
|
|
83
|
+
# Compile parameterized patterns
|
|
84
|
+
self._excluded_patterns = self._compile_patterns(self.excluded_paths_dict)
|
|
85
|
+
self._excluded_prefix_patterns = self._compile_patterns(
|
|
86
|
+
self.excluded_prefixes_dict, is_prefix=True
|
|
87
|
+
)
|
|
88
|
+
self._optional_patterns = self._compile_patterns(self.optional_auth_dict)
|
|
89
|
+
|
|
90
|
+
# Remove parameterized paths from literal dicts (they're now in pattern lists)
|
|
91
|
+
self._remove_parameterized_from_dicts()
|
|
92
|
+
|
|
76
93
|
self.validator = get_user_validator()
|
|
77
94
|
self.settings = get_settings()
|
|
78
95
|
|
|
@@ -94,25 +111,195 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|
|
94
111
|
return normalized
|
|
95
112
|
raise ValueError("Invalid type for paths: must be set or dict")
|
|
96
113
|
|
|
114
|
+
@staticmethod
|
|
115
|
+
def _has_parameters(path: str) -> bool:
|
|
116
|
+
"""Check if path contains parameter placeholders like {param:type}"""
|
|
117
|
+
return "{" in path and "}" in path
|
|
118
|
+
|
|
119
|
+
def _compile_path_pattern(self, path: str, is_prefix: bool = False) -> Pattern:
|
|
120
|
+
"""
|
|
121
|
+
Compile a parameterized path pattern to regex.
|
|
122
|
+
|
|
123
|
+
Supports syntax: /path/{param:type}/more
|
|
124
|
+
|
|
125
|
+
Types:
|
|
126
|
+
- uuid: UUID v4 format (case-insensitive)
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
path: Path pattern with {param:type} placeholders
|
|
130
|
+
is_prefix: If True, don't anchor end of pattern (for prefix matching)
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Compiled regex pattern
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
ValueError: If syntax is invalid or type is unsupported
|
|
137
|
+
|
|
138
|
+
Examples:
|
|
139
|
+
>>> _compile_path_pattern('/api/{id:uuid}')
|
|
140
|
+
re.compile(r'^/api/[0-9a-f]{8}-...$', re.IGNORECASE)
|
|
141
|
+
|
|
142
|
+
>>> _compile_path_pattern('/api/{version:uuid}', is_prefix=True)
|
|
143
|
+
re.compile(r'^/api/[0-9a-f]{8}-...', re.IGNORECASE)
|
|
144
|
+
"""
|
|
145
|
+
# Pattern to find parameter placeholders: {param:type}
|
|
146
|
+
param_pattern = r"\{([^}:]+)(?::([^}]+))?\}"
|
|
147
|
+
|
|
148
|
+
def replace_param(match: re.Match) -> str:
|
|
149
|
+
param_name = match.group(1).strip()
|
|
150
|
+
param_type = match.group(2).strip() if match.group(2) else None
|
|
151
|
+
|
|
152
|
+
if param_type is None:
|
|
153
|
+
raise ValueError(
|
|
154
|
+
f"Parameter '{{{param_name}}}' missing type annotation. "
|
|
155
|
+
f"Use {{param:type}} syntax (e.g., {{id:uuid}})"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if param_type not in TYPE_PATTERNS:
|
|
159
|
+
supported = ", ".join(TYPE_PATTERNS.keys())
|
|
160
|
+
raise ValueError(
|
|
161
|
+
f"Unsupported type '{param_type}' for parameter '{{{param_name}}}'. "
|
|
162
|
+
f"Supported types: {supported}"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
return f"({TYPE_PATTERNS[param_type]})"
|
|
166
|
+
|
|
167
|
+
# Escape special regex characters in path
|
|
168
|
+
escaped_path = re.escape(path)
|
|
169
|
+
|
|
170
|
+
# Unescape { and } for parameter replacement
|
|
171
|
+
escaped_path = escaped_path.replace(r"\{", "{").replace(r"\}", "}")
|
|
172
|
+
|
|
173
|
+
# Replace parameters with regex patterns
|
|
174
|
+
regex_pattern = re.sub(param_pattern, replace_param, escaped_path)
|
|
175
|
+
|
|
176
|
+
# Anchor pattern
|
|
177
|
+
if is_prefix:
|
|
178
|
+
regex_pattern = f"^{regex_pattern}"
|
|
179
|
+
else:
|
|
180
|
+
regex_pattern = f"^{regex_pattern}$"
|
|
181
|
+
|
|
182
|
+
# Compile with case-insensitive flag for UUIDs
|
|
183
|
+
try:
|
|
184
|
+
return re.compile(regex_pattern, re.IGNORECASE)
|
|
185
|
+
except re.error as e:
|
|
186
|
+
raise ValueError(f"Failed to compile pattern for path '{path}': {e}")
|
|
187
|
+
|
|
188
|
+
def _compile_patterns(
|
|
189
|
+
self, paths_dict: Dict[str, Optional[Set[str]]], is_prefix: bool = False
|
|
190
|
+
) -> List[Tuple[Pattern, Optional[Set[str]]]]:
|
|
191
|
+
"""
|
|
192
|
+
Extract parameterized paths and compile them to regex patterns.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
paths_dict: Dictionary of paths with optional method restrictions
|
|
196
|
+
is_prefix: If True, compile as prefix patterns (don't anchor end)
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
List of (compiled_pattern, methods) tuples
|
|
200
|
+
|
|
201
|
+
Raises:
|
|
202
|
+
ValueError: If pattern syntax is invalid or type is unsupported
|
|
203
|
+
"""
|
|
204
|
+
patterns = []
|
|
205
|
+
|
|
206
|
+
for path, methods in paths_dict.items():
|
|
207
|
+
if self._has_parameters(path):
|
|
208
|
+
try:
|
|
209
|
+
compiled_pattern = self._compile_path_pattern(path, is_prefix)
|
|
210
|
+
patterns.append((compiled_pattern, methods))
|
|
211
|
+
except ValueError as e:
|
|
212
|
+
raise ValueError(f"Invalid pattern in path '{path}': {e}")
|
|
213
|
+
|
|
214
|
+
return patterns
|
|
215
|
+
|
|
216
|
+
def _remove_parameterized_from_dicts(self):
|
|
217
|
+
"""
|
|
218
|
+
Remove parameterized paths from literal path dictionaries.
|
|
219
|
+
They are now stored in pattern lists.
|
|
220
|
+
"""
|
|
221
|
+
for path_dict in [
|
|
222
|
+
self.excluded_paths_dict,
|
|
223
|
+
self.excluded_prefixes_dict,
|
|
224
|
+
self.optional_auth_dict,
|
|
225
|
+
]:
|
|
226
|
+
parameterized_keys = [key for key in path_dict.keys() if self._has_parameters(key)]
|
|
227
|
+
for key in parameterized_keys:
|
|
228
|
+
del path_dict[key]
|
|
229
|
+
|
|
97
230
|
def is_excluded(self, path: str, method: str) -> bool:
|
|
98
|
-
"""
|
|
99
|
-
|
|
231
|
+
"""
|
|
232
|
+
Check if path is excluded from authentication for the given method.
|
|
233
|
+
|
|
234
|
+
Matching order (highest to lowest priority):
|
|
235
|
+
1. Exact literal match in excluded_paths
|
|
236
|
+
2. Parameterized pattern match in excluded_paths
|
|
237
|
+
3. Prefix literal match in excluded_prefixes
|
|
238
|
+
4. Prefix pattern match in excluded_prefixes
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
path: Request path (e.g., '/api/v1/categories/7b5bcc8f-...')
|
|
242
|
+
method: HTTP method (e.g., 'GET', 'POST')
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
True if path is excluded from authentication for this method
|
|
246
|
+
"""
|
|
247
|
+
# 1. Exact literal match (O(1) dict lookup)
|
|
100
248
|
for p, methods in self.excluded_paths_dict.items():
|
|
101
249
|
if path == p and (methods is None or method in methods):
|
|
250
|
+
logger.debug(f"Exact match excluded: {path} [{method}]")
|
|
251
|
+
return True
|
|
252
|
+
|
|
253
|
+
# 2. Parameterized pattern match (O(n) pattern checks)
|
|
254
|
+
for pattern, methods in self._excluded_patterns:
|
|
255
|
+
if pattern.match(path) and (methods is None or method in methods):
|
|
256
|
+
logger.debug(f"Pattern match excluded: {path} [{method}] via {pattern.pattern}")
|
|
102
257
|
return True
|
|
103
258
|
|
|
104
|
-
# Prefix match
|
|
259
|
+
# 3. Prefix literal match (O(n) prefix checks)
|
|
105
260
|
for p, methods in self.excluded_prefixes_dict.items():
|
|
106
261
|
if path.startswith(p) and (methods is None or method in methods):
|
|
262
|
+
logger.debug(f"Prefix match excluded: {path} [{method}]")
|
|
263
|
+
return True
|
|
264
|
+
|
|
265
|
+
# 4. Prefix pattern match (O(n) pattern checks)
|
|
266
|
+
for pattern, methods in self._excluded_prefix_patterns:
|
|
267
|
+
# For prefix patterns, check if path starts with pattern match
|
|
268
|
+
match = pattern.match(path)
|
|
269
|
+
if match and (methods is None or method in methods):
|
|
270
|
+
logger.debug(
|
|
271
|
+
f"Prefix pattern match excluded: {path} [{method}] via {pattern.pattern}"
|
|
272
|
+
)
|
|
107
273
|
return True
|
|
108
274
|
|
|
109
275
|
return False
|
|
110
276
|
|
|
111
277
|
def is_optional_auth(self, path: str, method: str) -> bool:
|
|
112
|
-
"""
|
|
278
|
+
"""
|
|
279
|
+
Check if path has optional authentication for the given method.
|
|
280
|
+
|
|
281
|
+
Checks both literal paths and parameterized patterns.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
path: Request path
|
|
285
|
+
method: HTTP method
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
True if authentication is optional for this path and method
|
|
289
|
+
"""
|
|
290
|
+
# Exact literal match
|
|
113
291
|
for p, methods in self.optional_auth_dict.items():
|
|
114
292
|
if (path == p) and (methods is None or method in methods):
|
|
115
293
|
return True
|
|
294
|
+
|
|
295
|
+
# Parameterized pattern match
|
|
296
|
+
for pattern, methods in self._optional_patterns:
|
|
297
|
+
if pattern.match(path) and (methods is None or method in methods):
|
|
298
|
+
logger.debug(
|
|
299
|
+
f"Pattern match optional auth: {path} [{method}] via {pattern.pattern}"
|
|
300
|
+
)
|
|
301
|
+
return True
|
|
302
|
+
|
|
116
303
|
return False
|
|
117
304
|
|
|
118
305
|
async def dispatch(self, request: Request, call_next):
|
|
@@ -347,7 +347,7 @@ class UserValidator:
|
|
|
347
347
|
# Testing mode - return mock user
|
|
348
348
|
logger.warning("SECURITY BYPASS MODE - FOR TESTING ONLY")
|
|
349
349
|
return UserContext(
|
|
350
|
-
user_id="
|
|
350
|
+
user_id="00000000-0000-0000-0000-000000000000",
|
|
351
351
|
username="testuser",
|
|
352
352
|
roles=["admin"],
|
|
353
353
|
auth_source="bypass",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: auth-gate
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Enterprise-grade authentication for microservices with Kong and Keycloak integration
|
|
5
5
|
Home-page: https://github.com/tradelink-org/auth-gate
|
|
6
6
|
Author: Brian Mburu
|
|
@@ -290,6 +290,44 @@ app.add_middleware(
|
|
|
290
290
|
)
|
|
291
291
|
```
|
|
292
292
|
|
|
293
|
+
### Parameterized Paths with UUID Matching
|
|
294
|
+
|
|
295
|
+
You can exclude or make paths optional using UUID v4 parameters:
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
app.add_middleware(
|
|
299
|
+
AuthMiddleware,
|
|
300
|
+
excluded_paths={
|
|
301
|
+
"/api/v1/categories/{category_id:uuid}": {"GET"}, # Public read
|
|
302
|
+
"/api/v1/products/{product_id:uuid}": {"GET"},
|
|
303
|
+
},
|
|
304
|
+
excluded_prefixes={
|
|
305
|
+
"/api/{version:uuid}": {"GET"}, # Version-specific docs
|
|
306
|
+
},
|
|
307
|
+
optional_auth_paths={
|
|
308
|
+
"/api/v1/recommendations/{user_id:uuid}": {"GET"}, # Personalized if authenticated
|
|
309
|
+
}
|
|
310
|
+
)
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**Pattern Syntax:**
|
|
314
|
+
- `{param:uuid}` - Matches valid UUID v4 format (case-insensitive)
|
|
315
|
+
- Works with exact paths, prefixes, and optional auth paths
|
|
316
|
+
- Supports method-specific exclusions
|
|
317
|
+
- Exact matches take precedence over patterns
|
|
318
|
+
|
|
319
|
+
**Example Behavior:**
|
|
320
|
+
```python
|
|
321
|
+
# Matches: /api/v1/categories/7b5bcc8f-2c99-43c0-9c7d-e27c10881bd2
|
|
322
|
+
# Does not match: /api/v1/categories/invalid-id
|
|
323
|
+
# Does not match: /api/v1/categories/all
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**UUID v4 Validation:**
|
|
327
|
+
- Must have version digit "4" in the correct position
|
|
328
|
+
- Must have variant bits (8, 9, a, or b) in the correct position
|
|
329
|
+
- Accepts uppercase, lowercase, or mixed case
|
|
330
|
+
|
|
293
331
|
### Direct Validator Usage
|
|
294
332
|
|
|
295
333
|
```python
|
|
@@ -299,7 +299,7 @@ class TestEndToEndIntegration:
|
|
|
299
299
|
# Mock the validator's get_current_user to return bypass user
|
|
300
300
|
mock_validator.return_value.get_current_user = AsyncMock(
|
|
301
301
|
return_value=UserContext(
|
|
302
|
-
user_id="
|
|
302
|
+
user_id="00000000-0000-0000-0000-000000000000",
|
|
303
303
|
username="testuser",
|
|
304
304
|
roles=["admin"],
|
|
305
305
|
auth_source="bypass",
|
|
@@ -314,7 +314,7 @@ class TestEndToEndIntegration:
|
|
|
314
314
|
# Should work without any headers
|
|
315
315
|
response = client.get("/api/profile")
|
|
316
316
|
assert response.status_code == 200
|
|
317
|
-
assert response.json()["user_id"] == "
|
|
317
|
+
assert response.json()["user_id"] == "00000000-0000-0000-0000-000000000000"
|
|
318
318
|
|
|
319
319
|
# Admin endpoint should work (due to "admin" role)
|
|
320
320
|
response = client.get("/api/admin/users")
|
|
@@ -162,7 +162,7 @@ class TestAuthMiddleware:
|
|
|
162
162
|
# Should work without any auth headers in bypass mode
|
|
163
163
|
response = client.get("/protected")
|
|
164
164
|
assert response.status_code == 200
|
|
165
|
-
assert response.json()["user_id"] == "
|
|
165
|
+
assert response.json()["user_id"] == "00000000-0000-0000-0000-000000000000"
|
|
166
166
|
|
|
167
167
|
|
|
168
168
|
class TestMethodSpecificExclusions:
|
|
@@ -416,3 +416,346 @@ class TestMethodSpecificExclusions:
|
|
|
416
416
|
|
|
417
417
|
response = client.get("/protected", headers=kong_headers)
|
|
418
418
|
assert response.status_code == 200
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
class TestParameterizedPathMatching:
|
|
422
|
+
"""Test parameterized path matching with UUID v4 patterns"""
|
|
423
|
+
|
|
424
|
+
def test_uuid_path_parameter_basic(self, mock_settings, kong_headers):
|
|
425
|
+
"""Test basic UUID parameter matching"""
|
|
426
|
+
mock_settings.AUTH_MODE = "kong_headers"
|
|
427
|
+
|
|
428
|
+
with patch("auth_gate.middleware.get_settings", return_value=mock_settings):
|
|
429
|
+
app = FastAPI()
|
|
430
|
+
|
|
431
|
+
valid_uuid = "7b5bcc8f-2c99-43c0-9c7d-e27c10881bd2"
|
|
432
|
+
|
|
433
|
+
@app.get(f"/api/v1/categories/{valid_uuid}")
|
|
434
|
+
async def get_category():
|
|
435
|
+
return {"id": valid_uuid}
|
|
436
|
+
|
|
437
|
+
app.add_middleware(
|
|
438
|
+
AuthMiddleware,
|
|
439
|
+
excluded_paths={
|
|
440
|
+
"/api/v1/categories/{category_id:uuid}": None,
|
|
441
|
+
},
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
client = TestClient(app)
|
|
445
|
+
|
|
446
|
+
# Valid UUID should be excluded (no auth needed)
|
|
447
|
+
response = client.get(f"/api/v1/categories/{valid_uuid}")
|
|
448
|
+
assert response.status_code == 200
|
|
449
|
+
assert response.json()["id"] == valid_uuid
|
|
450
|
+
|
|
451
|
+
def test_uuid_path_parameter_with_methods(self, mock_settings, kong_headers):
|
|
452
|
+
"""Test UUID parameters with method-specific exclusions"""
|
|
453
|
+
mock_settings.AUTH_MODE = "kong_headers"
|
|
454
|
+
|
|
455
|
+
with patch("auth_gate.middleware.get_settings", return_value=mock_settings):
|
|
456
|
+
app = FastAPI()
|
|
457
|
+
|
|
458
|
+
valid_uuid = "a1b2c3d4-e5f6-4789-abcd-ef0123456789"
|
|
459
|
+
|
|
460
|
+
@app.get(f"/api/v1/products/{valid_uuid}")
|
|
461
|
+
async def get_product():
|
|
462
|
+
return {"id": valid_uuid, "method": "GET"}
|
|
463
|
+
|
|
464
|
+
@app.post(f"/api/v1/products/{valid_uuid}")
|
|
465
|
+
async def update_product(request: Request):
|
|
466
|
+
user = request.state.user
|
|
467
|
+
return {"id": valid_uuid, "method": "POST", "user": user.user_id}
|
|
468
|
+
|
|
469
|
+
app.add_middleware(
|
|
470
|
+
AuthMiddleware,
|
|
471
|
+
excluded_paths={
|
|
472
|
+
"/api/v1/products/{product_id:uuid}": {"GET"}, # Only GET excluded
|
|
473
|
+
},
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
client = TestClient(app)
|
|
477
|
+
|
|
478
|
+
# GET should work without auth (excluded)
|
|
479
|
+
response = client.get(f"/api/v1/products/{valid_uuid}")
|
|
480
|
+
assert response.status_code == 200
|
|
481
|
+
assert response.json()["method"] == "GET"
|
|
482
|
+
|
|
483
|
+
# POST should require auth (not excluded)
|
|
484
|
+
response = client.post(f"/api/v1/products/{valid_uuid}")
|
|
485
|
+
assert response.status_code == 401
|
|
486
|
+
|
|
487
|
+
# POST with auth should work
|
|
488
|
+
response = client.post(f"/api/v1/products/{valid_uuid}", headers=kong_headers)
|
|
489
|
+
assert response.status_code == 200
|
|
490
|
+
assert response.json()["user"] == "test-user-123"
|
|
491
|
+
|
|
492
|
+
def test_uuid_invalid_format_requires_auth(self, mock_settings):
|
|
493
|
+
"""Test that invalid UUID formats don't match pattern"""
|
|
494
|
+
mock_settings.AUTH_MODE = "kong_headers"
|
|
495
|
+
|
|
496
|
+
with patch("auth_gate.middleware.get_settings", return_value=mock_settings):
|
|
497
|
+
app = FastAPI()
|
|
498
|
+
|
|
499
|
+
@app.get("/api/v1/categories/{category_id}")
|
|
500
|
+
async def get_category(category_id: str):
|
|
501
|
+
return {"id": category_id}
|
|
502
|
+
|
|
503
|
+
app.add_middleware(
|
|
504
|
+
AuthMiddleware,
|
|
505
|
+
excluded_paths={
|
|
506
|
+
"/api/v1/categories/{category_id:uuid}": None,
|
|
507
|
+
},
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
client = TestClient(app)
|
|
511
|
+
|
|
512
|
+
# Invalid UUIDs should require auth
|
|
513
|
+
invalid_ids = [
|
|
514
|
+
"invalid-id",
|
|
515
|
+
"123",
|
|
516
|
+
"all",
|
|
517
|
+
"not-a-uuid",
|
|
518
|
+
"7b5bcc8f", # Too short
|
|
519
|
+
"7b5bcc8f-2c99-43c0-9c7d-e27c10881bd2-extra", # Too long
|
|
520
|
+
"7b5bcc8f-2c99-53c0-9c7d-e27c10881bd2", # Wrong version (5 instead of 4)
|
|
521
|
+
]
|
|
522
|
+
|
|
523
|
+
for invalid_id in invalid_ids:
|
|
524
|
+
response = client.get(f"/api/v1/categories/{invalid_id}")
|
|
525
|
+
assert response.status_code == 401, f"Expected 401 for {invalid_id}"
|
|
526
|
+
|
|
527
|
+
def test_exact_match_takes_precedence_over_pattern(self, mock_settings, kong_headers):
|
|
528
|
+
"""Test that exact paths have higher priority than patterns"""
|
|
529
|
+
mock_settings.AUTH_MODE = "kong_headers"
|
|
530
|
+
|
|
531
|
+
with patch("auth_gate.middleware.get_settings", return_value=mock_settings):
|
|
532
|
+
app = FastAPI()
|
|
533
|
+
|
|
534
|
+
@app.get("/api/v1/categories/featured")
|
|
535
|
+
async def get_featured():
|
|
536
|
+
return {"type": "featured"}
|
|
537
|
+
|
|
538
|
+
@app.post("/api/v1/categories/featured")
|
|
539
|
+
async def update_featured():
|
|
540
|
+
return {"type": "updated"}
|
|
541
|
+
|
|
542
|
+
app.add_middleware(
|
|
543
|
+
AuthMiddleware,
|
|
544
|
+
excluded_paths={
|
|
545
|
+
"/api/v1/categories/featured": None, # Exact match - all methods
|
|
546
|
+
"/api/v1/categories/{category_id:uuid}": {"GET"}, # Pattern - GET only
|
|
547
|
+
},
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
client = TestClient(app)
|
|
551
|
+
|
|
552
|
+
# Exact match should work for all methods (None = all methods)
|
|
553
|
+
response = client.get("/api/v1/categories/featured")
|
|
554
|
+
assert response.status_code == 200
|
|
555
|
+
|
|
556
|
+
response = client.post("/api/v1/categories/featured")
|
|
557
|
+
assert response.status_code == 200 # Exact match wins, POST excluded
|
|
558
|
+
|
|
559
|
+
def test_multiple_uuid_parameters(self, mock_settings):
|
|
560
|
+
"""Test paths with multiple UUID parameters"""
|
|
561
|
+
mock_settings.AUTH_MODE = "kong_headers"
|
|
562
|
+
|
|
563
|
+
with patch("auth_gate.middleware.get_settings", return_value=mock_settings):
|
|
564
|
+
app = FastAPI()
|
|
565
|
+
|
|
566
|
+
store_id = "550e8400-e29b-41d4-a716-446655440000"
|
|
567
|
+
product_id = "7b5bcc8f-2c99-43c0-9c7d-e27c10881bd2"
|
|
568
|
+
|
|
569
|
+
@app.get(f"/api/v1/stores/{store_id}/products/{product_id}")
|
|
570
|
+
async def get_product():
|
|
571
|
+
return {"store_id": store_id, "product_id": product_id}
|
|
572
|
+
|
|
573
|
+
app.add_middleware(
|
|
574
|
+
AuthMiddleware,
|
|
575
|
+
excluded_paths={
|
|
576
|
+
"/api/v1/stores/{store_id:uuid}/products/{product_id:uuid}": {"GET"},
|
|
577
|
+
},
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
client = TestClient(app)
|
|
581
|
+
|
|
582
|
+
# Valid UUIDs should match
|
|
583
|
+
response = client.get(f"/api/v1/stores/{store_id}/products/{product_id}")
|
|
584
|
+
assert response.status_code == 200
|
|
585
|
+
assert response.json()["store_id"] == store_id
|
|
586
|
+
assert response.json()["product_id"] == product_id
|
|
587
|
+
|
|
588
|
+
# Invalid UUID in either position should require auth
|
|
589
|
+
response = client.get(f"/api/v1/stores/invalid/products/{product_id}")
|
|
590
|
+
assert response.status_code == 401
|
|
591
|
+
|
|
592
|
+
response = client.get(f"/api/v1/stores/{store_id}/products/invalid")
|
|
593
|
+
assert response.status_code == 401
|
|
594
|
+
|
|
595
|
+
def test_pattern_match_with_optional_auth(self, mock_settings, kong_headers):
|
|
596
|
+
"""Test parameterized paths with optional authentication"""
|
|
597
|
+
mock_settings.AUTH_MODE = "kong_headers"
|
|
598
|
+
|
|
599
|
+
with patch("auth_gate.middleware.get_settings", return_value=mock_settings):
|
|
600
|
+
app = FastAPI()
|
|
601
|
+
|
|
602
|
+
user_id = "123e4567-e89b-42d3-a456-426614174000"
|
|
603
|
+
|
|
604
|
+
@app.get(f"/api/v1/recommendations/{user_id}")
|
|
605
|
+
async def get_recommendations(request: Request):
|
|
606
|
+
user = getattr(request.state, "user", None)
|
|
607
|
+
if user:
|
|
608
|
+
return {"personalized": True, "user_id": user.user_id}
|
|
609
|
+
return {"personalized": False}
|
|
610
|
+
|
|
611
|
+
app.add_middleware(
|
|
612
|
+
AuthMiddleware,
|
|
613
|
+
optional_auth_paths={
|
|
614
|
+
"/api/v1/recommendations/{user_id:uuid}": {"GET"},
|
|
615
|
+
},
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
client = TestClient(app)
|
|
619
|
+
|
|
620
|
+
# Without auth - should work but not personalized
|
|
621
|
+
response = client.get(f"/api/v1/recommendations/{user_id}")
|
|
622
|
+
assert response.status_code == 200
|
|
623
|
+
assert response.json()["personalized"] is False
|
|
624
|
+
|
|
625
|
+
# With auth - should work and be personalized
|
|
626
|
+
response = client.get(f"/api/v1/recommendations/{user_id}", headers=kong_headers)
|
|
627
|
+
assert response.status_code == 200
|
|
628
|
+
assert response.json()["personalized"] is True
|
|
629
|
+
assert response.json()["user_id"] == "test-user-123"
|
|
630
|
+
|
|
631
|
+
def test_prefix_pattern_matching(self, mock_settings):
|
|
632
|
+
"""Test parameterized prefix matching"""
|
|
633
|
+
mock_settings.AUTH_MODE = "kong_headers"
|
|
634
|
+
|
|
635
|
+
with patch("auth_gate.middleware.get_settings", return_value=mock_settings):
|
|
636
|
+
app = FastAPI()
|
|
637
|
+
|
|
638
|
+
version_id = "f47ac10b-58cc-4372-a567-0e02b2c3d479"
|
|
639
|
+
|
|
640
|
+
@app.get(f"/api/{version_id}/docs")
|
|
641
|
+
async def get_docs():
|
|
642
|
+
return {"version": version_id, "type": "docs"}
|
|
643
|
+
|
|
644
|
+
@app.get(f"/api/{version_id}/swagger")
|
|
645
|
+
async def get_swagger():
|
|
646
|
+
return {"version": version_id, "type": "swagger"}
|
|
647
|
+
|
|
648
|
+
app.add_middleware(
|
|
649
|
+
AuthMiddleware,
|
|
650
|
+
excluded_prefixes={
|
|
651
|
+
"/api/{version:uuid}": {"GET"},
|
|
652
|
+
},
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
client = TestClient(app)
|
|
656
|
+
|
|
657
|
+
# Both should be excluded (prefix match)
|
|
658
|
+
response = client.get(f"/api/{version_id}/docs")
|
|
659
|
+
assert response.status_code == 200
|
|
660
|
+
|
|
661
|
+
response = client.get(f"/api/{version_id}/swagger")
|
|
662
|
+
assert response.status_code == 200
|
|
663
|
+
|
|
664
|
+
def test_backward_compatibility_no_regression(self, mock_settings, kong_headers):
|
|
665
|
+
"""Test that existing configurations still work"""
|
|
666
|
+
mock_settings.AUTH_MODE = "kong_headers"
|
|
667
|
+
|
|
668
|
+
with patch("auth_gate.middleware.get_settings", return_value=mock_settings):
|
|
669
|
+
app = FastAPI()
|
|
670
|
+
|
|
671
|
+
@app.get("/health")
|
|
672
|
+
async def health():
|
|
673
|
+
return {"status": "ok"}
|
|
674
|
+
|
|
675
|
+
@app.get("/api/docs/swagger")
|
|
676
|
+
async def swagger():
|
|
677
|
+
return {"docs": "swagger"}
|
|
678
|
+
|
|
679
|
+
@app.get("/protected")
|
|
680
|
+
async def protected(request: Request):
|
|
681
|
+
user = request.state.user
|
|
682
|
+
return {"user_id": user.user_id}
|
|
683
|
+
|
|
684
|
+
app.add_middleware(
|
|
685
|
+
AuthMiddleware,
|
|
686
|
+
excluded_paths={"/health"}, # Set format (legacy)
|
|
687
|
+
excluded_prefixes={"/api/docs": {"GET"}}, # Dict with methods
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
client = TestClient(app)
|
|
691
|
+
|
|
692
|
+
# Exact path exclusion still works
|
|
693
|
+
response = client.get("/health")
|
|
694
|
+
assert response.status_code == 200
|
|
695
|
+
|
|
696
|
+
# Prefix exclusion still works
|
|
697
|
+
response = client.get("/api/docs/swagger")
|
|
698
|
+
assert response.status_code == 200
|
|
699
|
+
|
|
700
|
+
# Protected endpoint still requires auth
|
|
701
|
+
response = client.get("/protected")
|
|
702
|
+
assert response.status_code == 401
|
|
703
|
+
|
|
704
|
+
response = client.get("/protected", headers=kong_headers)
|
|
705
|
+
assert response.status_code == 200
|
|
706
|
+
|
|
707
|
+
def test_invalid_pattern_syntax_raises_error(self, mock_settings):
|
|
708
|
+
"""Test that invalid patterns raise clear errors"""
|
|
709
|
+
mock_settings.AUTH_MODE = "kong_headers"
|
|
710
|
+
|
|
711
|
+
with patch("auth_gate.middleware.get_settings", return_value=mock_settings):
|
|
712
|
+
with patch("auth_gate.middleware.get_user_validator"):
|
|
713
|
+
# Missing type annotation
|
|
714
|
+
with pytest.raises(ValueError, match="missing type annotation"):
|
|
715
|
+
AuthMiddleware(
|
|
716
|
+
app=None,
|
|
717
|
+
excluded_paths={
|
|
718
|
+
"/api/{id}": None, # No :type specified
|
|
719
|
+
},
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
# Unsupported type
|
|
723
|
+
with pytest.raises(ValueError, match="Unsupported type"):
|
|
724
|
+
AuthMiddleware(
|
|
725
|
+
app=None,
|
|
726
|
+
excluded_paths={
|
|
727
|
+
"/api/{id:invalid}": None,
|
|
728
|
+
},
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
def test_case_insensitive_uuid_matching(self, mock_settings):
|
|
732
|
+
"""Test that UUID matching is case-insensitive"""
|
|
733
|
+
mock_settings.AUTH_MODE = "kong_headers"
|
|
734
|
+
|
|
735
|
+
with patch("auth_gate.middleware.get_settings", return_value=mock_settings):
|
|
736
|
+
app = FastAPI()
|
|
737
|
+
|
|
738
|
+
@app.get("/api/v1/items/{item_id}")
|
|
739
|
+
async def get_item(item_id: str):
|
|
740
|
+
return {"id": item_id}
|
|
741
|
+
|
|
742
|
+
app.add_middleware(
|
|
743
|
+
AuthMiddleware,
|
|
744
|
+
excluded_paths={
|
|
745
|
+
"/api/v1/items/{item_id:uuid}": None,
|
|
746
|
+
},
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
client = TestClient(app)
|
|
750
|
+
|
|
751
|
+
# Test different case variations (all valid UUID v4)
|
|
752
|
+
uuids = [
|
|
753
|
+
"7b5bcc8f-2c99-43c0-9c7d-e27c10881bd2", # lowercase
|
|
754
|
+
"7B5BCC8F-2C99-43C0-9C7D-E27C10881BD2", # uppercase
|
|
755
|
+
"7b5bCc8f-2C99-43c0-9C7d-E27c10881bD2", # mixed case
|
|
756
|
+
]
|
|
757
|
+
|
|
758
|
+
for uuid_val in uuids:
|
|
759
|
+
response = client.get(f"/api/v1/items/{uuid_val}")
|
|
760
|
+
assert response.status_code == 200, f"Failed for UUID: {uuid_val}"
|
|
761
|
+
assert response.json()["id"] == uuid_val
|
|
@@ -217,7 +217,7 @@ class TestUserValidator:
|
|
|
217
217
|
user = await validator.get_current_user(request)
|
|
218
218
|
|
|
219
219
|
assert isinstance(user, UserContext)
|
|
220
|
-
assert user.user_id == "
|
|
220
|
+
assert user.user_id == "00000000-0000-0000-0000-000000000000"
|
|
221
221
|
assert user.username == "testuser"
|
|
222
222
|
assert user.roles == ["admin"]
|
|
223
223
|
assert user.auth_source == "bypass"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|