auth-gate 0.2.0__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.
Files changed (26) hide show
  1. {auth_gate-0.2.0/src/auth_gate.egg-info → auth_gate-0.2.3}/PKG-INFO +39 -1
  2. {auth_gate-0.2.0 → auth_gate-0.2.3}/README.md +38 -0
  3. {auth_gate-0.2.0 → auth_gate-0.2.3}/pyproject.toml +1 -1
  4. {auth_gate-0.2.0 → auth_gate-0.2.3}/setup.cfg +1 -1
  5. {auth_gate-0.2.0 → auth_gate-0.2.3}/src/auth_gate/__init__.py +37 -5
  6. {auth_gate-0.2.0 → auth_gate-0.2.3}/src/auth_gate/middleware.py +192 -5
  7. auth_gate-0.2.3/src/auth_gate/s2s_auth.py +352 -0
  8. {auth_gate-0.2.0 → auth_gate-0.2.3}/src/auth_gate/user_auth.py +1 -1
  9. {auth_gate-0.2.0 → auth_gate-0.2.3/src/auth_gate.egg-info}/PKG-INFO +39 -1
  10. {auth_gate-0.2.0 → auth_gate-0.2.3}/src/auth_gate.egg-info/SOURCES.txt +2 -0
  11. {auth_gate-0.2.0 → auth_gate-0.2.3}/src/tests/conftest.py +19 -3
  12. {auth_gate-0.2.0 → auth_gate-0.2.3}/src/tests/test_intergration.py +2 -2
  13. {auth_gate-0.2.0 → auth_gate-0.2.3}/src/tests/test_middleware.py +344 -1
  14. auth_gate-0.2.3/src/tests/test_s2s_auth.py +295 -0
  15. {auth_gate-0.2.0 → auth_gate-0.2.3}/src/tests/test_user_auth.py +1 -1
  16. {auth_gate-0.2.0 → auth_gate-0.2.3}/LICENSE +0 -0
  17. {auth_gate-0.2.0 → auth_gate-0.2.3}/src/auth_gate/config.py +0 -0
  18. {auth_gate-0.2.0 → auth_gate-0.2.3}/src/auth_gate/fastapi_utils.py +0 -0
  19. {auth_gate-0.2.0 → auth_gate-0.2.3}/src/auth_gate/schemas.py +0 -0
  20. {auth_gate-0.2.0 → auth_gate-0.2.3}/src/auth_gate.egg-info/dependency_links.txt +0 -0
  21. {auth_gate-0.2.0 → auth_gate-0.2.3}/src/auth_gate.egg-info/requires.txt +0 -0
  22. {auth_gate-0.2.0 → auth_gate-0.2.3}/src/auth_gate.egg-info/top_level.txt +0 -0
  23. {auth_gate-0.2.0 → auth_gate-0.2.3}/src/tests/__init__.py +0 -0
  24. {auth_gate-0.2.0 → auth_gate-0.2.3}/src/tests/test_config.py +0 -0
  25. {auth_gate-0.2.0 → auth_gate-0.2.3}/src/tests/test_fastapi_utils.py +0 -0
  26. {auth_gate-0.2.0 → 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.0
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.0"
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"
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = auth-gate
3
- version = 0.2.0
3
+ version = 0.2.3
4
4
  author = Brian Mburu
5
5
  author_email = brian.mburu@students.jkuat.ac.ke
6
6
  description = Enterprise-grade authentication for microservices with Kong and Keycloak integration
@@ -4,41 +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 .schemas import ServiceContext, UserContext
29
- from .user_auth import UserValidator
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
30
43
 
31
- __version__ = "0.2.0"
44
+ __version__ = "0.2.3"
32
45
 
33
46
  __all__ = [
34
47
  # Configuration
35
48
  "AuthSettings",
36
49
  "AuthMode",
37
- # Schemas
50
+ "get_settings",
51
+ "reset_settings",
52
+ # Schemas & Type Aliases
38
53
  "UserContext",
39
54
  "ServiceContext",
55
+ "AuthContext",
56
+ "BaseAuthContext",
40
57
  # User Authentication
41
58
  "UserValidator",
59
+ "HMACVerifier",
60
+ "get_user_validator",
61
+ # Service-to-Service
62
+ "ServiceAuthClient",
63
+ "CircuitBreaker",
64
+ "CircuitBreakerOpenError",
65
+ "CircuitState",
66
+ "ServiceToken",
67
+ "get_service_auth_client",
42
68
  # Middleware
43
69
  "AuthMiddleware",
44
70
  # FastAPI Dependencies
@@ -48,6 +74,8 @@ __all__ = [
48
74
  "get_optional_auth",
49
75
  "get_optional_user",
50
76
  "require_roles",
77
+ "require_user_roles",
78
+ "require_service_roles",
51
79
  "require_scopes",
52
80
  "require_admin",
53
81
  "require_supplier",
@@ -59,4 +87,8 @@ __all__ = [
59
87
  "require_user_customer",
60
88
  "require_user_supplier",
61
89
  "require_user_moderator",
90
+ # Mode Utilities
91
+ "is_using_kong",
92
+ "is_using_keycloak",
93
+ "is_bypass_mode",
62
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
- """Check if path is excluded from authentication for the given method"""
99
- # Exact match
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
- """Check if path has optional authentication for the given method"""
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):