api-foundry-query-engine 0.8.35__tar.gz → 0.8.37__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 (37) hide show
  1. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/PKG-INFO +1 -1
  2. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/connectors/postgres_connection.py +4 -4
  3. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/dao/sql_delete_query_handler.py +1 -1
  4. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/dao/sql_insert_query_handler.py +1 -1
  5. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/dao/sql_restore_query_handler.py +3 -2
  6. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/dao/sql_select_query_handler.py +1 -1
  7. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/dao/sql_update_query_handler.py +3 -2
  8. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/utils/token_decoder.py +280 -67
  9. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/pyproject.toml +1 -1
  10. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/.gitignore +0 -0
  11. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/LICENSE +0 -0
  12. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/.pre-commit-config.yaml +0 -0
  13. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/__init__.py +0 -0
  14. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/adapters/adapter.py +0 -0
  15. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/adapters/case_change_adapter.py +0 -0
  16. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/adapters/gateway_adapter.py +0 -0
  17. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/adapters/security_adapter.py +0 -0
  18. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/connectors/connection.py +0 -0
  19. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/connectors/connection_factory.py +0 -0
  20. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/connectors/oracle_connector.py +0 -0
  21. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/dao/batch_operation_handler.py +0 -0
  22. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/dao/dao.py +0 -0
  23. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/dao/operation_dao.py +0 -0
  24. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/dao/sql_custom_query_handler.py +0 -0
  25. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/dao/sql_query_handler.py +0 -0
  26. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/dao/sql_subselect_query_handler.py +0 -0
  27. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/lambda_handler.py +0 -0
  28. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/operation.py +0 -0
  29. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/services/service.py +0 -0
  30. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/services/transactional_service.py +0 -0
  31. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/utils/api_model.py +0 -0
  32. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/utils/app_exception.py +0 -0
  33. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/utils/claims_check.py +0 -0
  34. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/utils/dependency_resolver.py +0 -0
  35. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/utils/gateway_operation.py +0 -0
  36. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/utils/logger.py +0 -0
  37. {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/utils/reference_resolver.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: api-foundry-query-engine
3
- Version: 0.8.35
3
+ Version: 0.8.37
4
4
  Summary: The AWS lambda service handler use by the `api_foundry` project is a powerful tool designed to automate the deployment of REST APIs on AWS using Lambda services to access and interact with relational databases (RDBMS). This project leverages the OpenAPI specification to define and manage the APIs
5
5
  Project-URL: Documentation, https://github.com/DanRepik/api-foundry
6
6
  Project-URL: Source, https://github.com/DanRepik/api-foundry
@@ -69,18 +69,18 @@ class PostgresCursor(Cursor):
69
69
  class PostgresConnection(Connection):
70
70
  """
71
71
  PostgreSQL database connection wrapper.
72
-
72
+
73
73
  Supports two configuration formats:
74
74
  1. DSN-based (preferred for testing with fixture_foundry):
75
75
  {"dsn": "postgresql://user:pass@host:port/dbname"}
76
-
76
+
77
77
  2. Individual parameters (for production AWS Secrets Manager):
78
78
  {"host": "...", "port": 5432, "database": "...", "username": "...", "password": "..."}
79
-
79
+
80
80
  The get_connection() method prioritizes DSN if present, otherwise builds connection
81
81
  from individual parameters.
82
82
  """
83
-
83
+
84
84
  def __init__(self, db_config: dict) -> None:
85
85
  super().__init__(db_config)
86
86
  self.__connection = self.get_connection()
@@ -110,7 +110,7 @@ class SQLDeleteSchemaQueryHandler(SQLSchemaQueryHandler):
110
110
  def sql(self) -> str:
111
111
  if not self.check_permission():
112
112
  raise ApplicationException(
113
- 402, f"Subject is not allowed to delete {self.schema_object.api_name}"
113
+ 403, f"Subject is not allowed to delete {self.schema_object.api_name}"
114
114
  )
115
115
 
116
116
  concurrency_property = self.schema_object.concurrency_property
@@ -106,7 +106,7 @@ class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
106
106
  raise ApplicationException(400, f"Invalid property: {name}")
107
107
  else:
108
108
  raise ApplicationException(
109
- 402,
109
+ 403,
110
110
  f"Subject is not allowed to create with property: {parts[0]}",
111
111
  )
112
112
 
@@ -171,8 +171,9 @@ class SQLRestoreSchemaQueryHandler(SQLSchemaQueryHandler):
171
171
  def sql(self) -> str:
172
172
  if not self.check_permission():
173
173
  raise ApplicationException(
174
- 402,
175
- "Subject is not allowed to restore " + f"{self.schema_object.api_name}",
174
+ 403,
175
+ f"Subject is not allowed to restore "
176
+ f"{self.schema_object.api_name}",
176
177
  )
177
178
 
178
179
  if not self.schema_object.has_soft_delete_support():
@@ -283,7 +283,7 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
283
283
 
284
284
  if len(self._selection_results) == 0:
285
285
  raise ApplicationException(
286
- 402,
286
+ 403,
287
287
  (
288
288
  "After applying permissions there are no properties "
289
289
  "returned in response"
@@ -169,8 +169,9 @@ class SQLUpdateSchemaQueryHandler(SQLSchemaQueryHandler):
169
169
 
170
170
  if invalid_columns:
171
171
  raise ApplicationException(
172
- 402,
173
- f"Subject does not have permission to update properties: {invalid_columns}",
172
+ 403,
173
+ f"Subject does not have permission to update properties: "
174
+ f"{invalid_columns}",
174
175
  )
175
176
  return f" SET {', '.join(columns)}"
176
177
 
@@ -30,68 +30,173 @@ def _log_jwt_configuration():
30
30
  "JWT_ALLOWED_AUDIENCES: %s", os.getenv("JWT_ALLOWED_AUDIENCES", "NOT_SET")
31
31
  )
32
32
  log.debug("ANONYMOUS_ROLE: %s", os.getenv("ANONYMOUS_ROLE", "NOT_SET"))
33
+ log.debug(
34
+ "TOKEN_VALIDATOR_LAMBDA_ARN: %s",
35
+ os.getenv("TOKEN_VALIDATOR_LAMBDA_ARN", "NOT_SET"),
36
+ )
33
37
  log.debug("Logging level: %s", logging.getLogger().getEffectiveLevel())
34
38
  log.debug("===============================")
35
39
 
36
40
 
41
+ class LambdaTokenValidator:
42
+ """Validates tokens by invoking an AWS Lambda TOKEN authorizer."""
43
+
44
+ def __init__(self, lambda_arn: str):
45
+ """
46
+ Initialize Lambda token validator.
47
+
48
+ Args:
49
+ lambda_arn: ARN or name of the Lambda authorizer function
50
+ """
51
+ self.lambda_arn = lambda_arn
52
+ try:
53
+ import boto3
54
+
55
+ self.lambda_client = boto3.client("lambda")
56
+ log.debug(
57
+ "Lambda client initialized for validator: %s", lambda_arn
58
+ )
59
+ except ImportError:
60
+ log.error("boto3 not available for Lambda token validation")
61
+ raise ImportError(
62
+ "boto3 is required for Lambda token validation"
63
+ )
64
+
65
+ def validate(
66
+ self, token: str, method_arn: str = "arn:aws:execute-api:*:*:*"
67
+ ) -> Dict[str, Any]:
68
+ """
69
+ Invoke AWS Lambda TOKEN authorizer.
70
+
71
+ Args:
72
+ token: JWT token (without "Bearer " prefix)
73
+ method_arn: Method ARN for resource-based policies
74
+
75
+ Returns:
76
+ Claims dict (extracted from context field)
77
+
78
+ Raises:
79
+ ValueError: If validation fails
80
+ """
81
+ log.debug("Invoking Lambda authorizer: %s", self.lambda_arn)
82
+
83
+ payload = {
84
+ "type": "TOKEN",
85
+ "authorizationToken": f"Bearer {token}",
86
+ "methodArn": method_arn,
87
+ }
88
+
89
+ try:
90
+ response = self.lambda_client.invoke(
91
+ FunctionName=self.lambda_arn,
92
+ InvocationType="RequestResponse",
93
+ Payload=json.dumps(payload),
94
+ )
95
+
96
+ result = json.loads(response["Payload"].read())
97
+ log.debug("Lambda authorizer response received")
98
+
99
+ # Check for Lambda errors
100
+ if "FunctionError" in response:
101
+ error_msg = result.get("errorMessage", "Unknown error")
102
+ log.error("Lambda authorizer error: %s", error_msg)
103
+ raise ValueError(f"Token validation failed: {error_msg}")
104
+
105
+ # Extract context (claims) from authorizer response
106
+ # AWS API Gateway places the 'context' object into
107
+ # requestContext.authorizer
108
+ context = result.get("context", {})
109
+ if not context:
110
+ log.warning("Authorizer returned no context/claims")
111
+ raise ValueError("Authorizer returned no context/claims")
112
+
113
+ log.debug(
114
+ "Token validated successfully, claims extracted from context"
115
+ )
116
+ return context
117
+
118
+ except Exception as e:
119
+ log.error("Lambda authorizer invocation failed: %s", e)
120
+ raise ValueError(f"Token validation failed: {str(e)}")
121
+
122
+
37
123
  def token_decoder(
38
124
  jwks_url: Optional[str] = None,
39
125
  audience: Optional[str] = None,
40
126
  issuer: Optional[str] = None,
41
127
  algorithms: Optional[list[str]] = None,
42
128
  anonymous_role: Optional[str] = None,
129
+ lambda_validator_arn: Optional[str] = None,
43
130
  ):
44
131
  """
45
132
  JWT token decoder decorator for AWS Lambda handlers.
46
133
 
47
134
  This filter extracts JWT tokens from the Authorization header, validates
48
- them, and passes the decoded token as a keyword argument to the handler
49
- function.
135
+ them using JWKS, Lambda authorizer, or falls back to anonymous access.
136
+
137
+ Validation priority:
138
+ 1. Check if requestContext.authorizer exists (gateway validated)
139
+ 2. Try Lambda validation if TOKEN_VALIDATOR_LAMBDA_ARN is set
140
+ 3. Try JWKS validation if JWKS_HOST is set
141
+ 4. Fall back to anonymous role if ANONYMOUS_ROLE is set
142
+ 5. Reject with error
50
143
 
51
144
  Args:
52
- jwks_url: URL to fetch JWKS from (e.g., "https://oauth.local/.well-known/jwks.json")
145
+ jwks_url: URL to fetch JWKS from
53
146
  audience: Expected audience claim (or list of audiences)
54
147
  issuer: Expected issuer claim
55
148
  algorithms: List of allowed algorithms (defaults to ["RS256"])
56
- anonymous_role: Role to assign to anonymous (unauthenticated) requests.
57
- If not set and no token is provided, request is rejected.
58
- Set via environment variable ANONYMOUS_ROLE or parameter.
149
+ anonymous_role: Role to assign to anonymous requests.
150
+ Set via ANONYMOUS_ROLE environment variable.
151
+ lambda_validator_arn: ARN of Lambda TOKEN authorizer function.
152
+ Set via TOKEN_VALIDATOR_LAMBDA_ARN env var.
59
153
 
60
154
  Returns:
61
- Decorated handler function that processes JWT tokens before execution
155
+ Decorated handler function that processes JWT tokens
62
156
 
63
157
  Example:
64
- # Secured endpoint - requires valid JWT token
158
+ # JWKS validation
65
159
  @token_decoder(
66
- jwks_url="https://your-oauth-server/.well-known/jwks.json",
160
+ jwks_url="https://oauth.local/.well-known/jwks.json",
67
161
  audience="test-api",
68
162
  issuer="https://oauth.local/",
69
- algorithms=["RS256"]
70
163
  )
71
- def secured_handler(event, context):
72
- # Claims available in event['requestContext']['authorizer']
73
- return {'statusCode': 200, 'body': 'Secured endpoint'}
164
+ def handler(event, context):
165
+ return {'statusCode': 200, 'body': 'Secured'}
74
166
 
75
- # Public endpoint - allows anonymous with 'public' role
167
+ # Lambda authorizer validation
168
+ @token_decoder() # Uses TOKEN_VALIDATOR_LAMBDA_ARN env var
169
+ def handler(event, context):
170
+ return {'statusCode': 200, 'body': 'Secured'}
171
+
172
+ # Public endpoint
76
173
  @token_decoder(anonymous_role="public")
77
- def public_handler(event, context):
78
- # Works without Authorization header, roles=["public"]
79
- return {'statusCode': 200, 'body': 'Public endpoint'}
174
+ def handler(event, context):
175
+ return {'statusCode': 200, 'body': 'Public'}
80
176
  """
81
177
 
82
178
  # Validate configuration at decorator time
83
179
  config_anonymous_role = anonymous_role or os.getenv("ANONYMOUS_ROLE")
84
-
85
- # If no anonymous role is configured and no JWKS host, check if we're in test mode
86
- if not config_anonymous_role and not jwks_url and not os.getenv("JWKS_HOST"):
87
- # Allow decorator to be applied without config for testing
88
- # Actual validation will happen at runtime when handler is called
180
+ config_lambda_arn = lambda_validator_arn or os.getenv(
181
+ "TOKEN_VALIDATOR_LAMBDA_ARN"
182
+ )
183
+
184
+ # If no config provided, allow for runtime configuration
185
+ if (
186
+ not config_anonymous_role
187
+ and not jwks_url
188
+ and not os.getenv("JWKS_HOST")
189
+ and not config_lambda_arn
190
+ ):
89
191
  pass
90
192
 
91
193
  def decorator(handler: Callable) -> Callable:
92
194
  @functools.wraps(handler)
93
195
  def wrapper(event: dict, context: Any) -> dict:
94
- log.debug("JWT token decoder starting for function: %s", handler.__name__)
196
+ log.debug(
197
+ "JWT token decoder starting for function: %s",
198
+ handler.__name__,
199
+ )
95
200
 
96
201
  # Log configuration for debugging
97
202
  if log.isEnabledFor(logging.DEBUG):
@@ -99,68 +204,168 @@ def token_decoder(
99
204
 
100
205
  log.debug("Event structure: %s", json.dumps(event, default=str))
101
206
 
102
- # Determine configuration source
207
+ # Check if authorizer already exists (gateway validated)
208
+ if event.get("requestContext", {}).get("authorizer"):
209
+ log.debug(
210
+ "Authorizer already exists, skipping JWT processing"
211
+ )
212
+ return handler(event, context)
213
+
214
+ # Determine configuration sources
215
+ config_lambda_arn = lambda_validator_arn or os.getenv(
216
+ "TOKEN_VALIDATOR_LAMBDA_ARN"
217
+ )
103
218
  config_jwks_url = (
104
- jwks_url or f"{os.getenv('JWKS_HOST')}/.well-known/jwks.json"
219
+ jwks_url
220
+ or f"{os.getenv('JWKS_HOST')}/.well-known/jwks.json"
105
221
  if os.getenv("JWKS_HOST")
106
222
  else None
107
223
  )
108
224
  config_issuer = issuer or os.getenv("JWT_ISSUER")
109
225
  config_audience = (
110
- audience or os.getenv("JWT_ALLOWED_AUDIENCES", "").split(",")
226
+ audience
227
+ or os.getenv("JWT_ALLOWED_AUDIENCES", "").split(",")
111
228
  if os.getenv("JWT_ALLOWED_AUDIENCES")
112
229
  else []
113
230
  )
114
231
  config_algorithms = algorithms or ["RS256"]
115
- config_anonymous_role = anonymous_role or os.getenv("ANONYMOUS_ROLE")
232
+ config_anonymous_role = anonymous_role or os.getenv(
233
+ "ANONYMOUS_ROLE"
234
+ )
116
235
  log.debug("anonymous_role parameter: %s", anonymous_role)
117
- log.debug("ANONYMOUS_ROLE env: %s", os.getenv("ANONYMOUS_ROLE", ""))
236
+ log.debug(
237
+ "ANONYMOUS_ROLE env: %s", os.getenv("ANONYMOUS_ROLE", "")
238
+ )
118
239
  log.debug("config_anonymous_role: %s", config_anonymous_role)
240
+ log.debug("config_lambda_arn: %s", config_lambda_arn)
119
241
 
120
- # Skip JWT decoding if no JWKS URL is configured
121
- if not config_jwks_url:
122
- log.debug("No JWKS URL configured, skipping JWT decoding")
242
+ # Skip all validation if no method configured
243
+ if not config_lambda_arn and not config_jwks_url:
244
+ log.debug("No validation method configured")
245
+ if config_anonymous_role:
246
+ log.debug(
247
+ "Using anonymous role: %s", config_anonymous_role
248
+ )
249
+ if "requestContext" not in event:
250
+ event["requestContext"] = {}
251
+ event["requestContext"]["authorizer"] = {
252
+ "roles": [config_anonymous_role]
253
+ }
123
254
  return handler(event, context)
124
255
 
125
256
  try:
126
257
  log.debug("Processing JWT token extraction and validation")
127
258
 
128
- # Check if authorizer already exists (preserve context)
129
- if event.get("requestContext", {}).get("authorizer"):
130
- log.debug("Authorizer already exists, skipping JWT processing")
131
- return handler(event, context)
132
-
133
- # Set up a singleton JWTDecoder instance
259
+ # Set up validation instances
134
260
  if not hasattr(wrapper, "_jwt_decoder"):
135
- log.debug("Creating new JWTDecoder instance")
136
- log.debug(
137
- "JWT config - URL: %s, issuer: %s, aud: %s, alg: %s",
138
- config_jwks_url,
139
- config_issuer,
140
- config_audience,
141
- config_algorithms,
261
+ log.debug("Creating validator instances")
262
+
263
+ # Lambda validator takes priority
264
+ if config_lambda_arn:
265
+ log.debug(
266
+ "Configuring Lambda validator: %s",
267
+ config_lambda_arn,
268
+ )
269
+ wrapper._lambda_validator = LambdaTokenValidator(
270
+ config_lambda_arn
271
+ )
272
+ else:
273
+ wrapper._lambda_validator = None
274
+
275
+ # JWKS validator as fallback
276
+ if config_jwks_url:
277
+ log.debug(
278
+ "Configuring JWKS validator: %s",
279
+ config_jwks_url,
280
+ )
281
+ wrapper._jwt_decoder = JWTDecoder(
282
+ jwks_url=config_jwks_url,
283
+ issuer=config_issuer,
284
+ allowed_audiences=set(config_audience)
285
+ if config_audience
286
+ else None,
287
+ algorithms=config_algorithms,
288
+ anonymous_role=config_anonymous_role,
289
+ )
290
+ else:
291
+ wrapper._jwt_decoder = None
292
+
293
+ wrapper._anonymous_role = config_anonymous_role
294
+
295
+ log.debug("Parsing token from event")
296
+ token = None
297
+ try:
298
+ # Extract token
299
+ auth_header = (
300
+ event.get("authorizationToken")
301
+ or event.get("headers", {}).get("Authorization")
302
+ or event.get("headers", {}).get("authorization")
142
303
  )
143
- wrapper._jwt_decoder = JWTDecoder(
144
- jwks_url=config_jwks_url,
145
- issuer=config_issuer,
146
- allowed_audiences=set(config_audience)
147
- if config_audience
148
- else None,
149
- algorithms=config_algorithms,
150
- anonymous_role=config_anonymous_role,
151
- )
152
- else:
153
- log.debug("Using existing JWTDecoder instance")
154
- jwt_decoder = wrapper._jwt_decoder
155
304
 
156
- log.debug("Decoding JWT token")
157
- decoded_token = jwt_decoder.decode(event)
305
+ if auth_header:
306
+ auth_parts = auth_header.split(" ")
307
+ if (
308
+ len(auth_parts) == 2
309
+ and auth_parts[0].lower() == "bearer"
310
+ ):
311
+ token = auth_parts[1]
312
+ log.debug("Token extracted, length: %d", len(token))
313
+ except Exception:
314
+ log.debug("Token extraction failed")
315
+
316
+ decoded_token = None
317
+
318
+ # Try Lambda validator first
319
+ if token and wrapper._lambda_validator:
320
+ try:
321
+ log.debug("Attempting Lambda validation")
322
+ decoded_token = wrapper._lambda_validator.validate(
323
+ token
324
+ )
325
+ log.debug("Lambda validation successful")
326
+ except Exception as e:
327
+ log.warning("Lambda validation failed: %s", e)
328
+ # Fall through to JWKS
329
+
330
+ # Try JWKS validator if Lambda failed or unavailable
331
+ if token and not decoded_token and wrapper._jwt_decoder:
332
+ try:
333
+ log.debug("Attempting JWKS validation")
334
+ decoded_token = wrapper._jwt_decoder.decode_token(
335
+ token
336
+ )
337
+ log.debug("JWKS validation successful")
338
+ except Exception as e:
339
+ log.warning("JWKS validation failed: %s", e)
340
+
341
+ # Fall back to anonymous if configured
342
+ if not decoded_token:
343
+ if wrapper._anonymous_role:
344
+ log.debug(
345
+ "Using anonymous role: %s",
346
+ wrapper._anonymous_role,
347
+ )
348
+ decoded_token = {
349
+ "roles": [wrapper._anonymous_role]
350
+ }
351
+ else:
352
+ log.error("No valid token and no anonymous access")
353
+ return {
354
+ "statusCode": 401,
355
+ "headers": {"Content-Type": "application/json"},
356
+ "body": json.dumps(
357
+ {"error": "Unauthorized"}
358
+ ),
359
+ }
158
360
 
159
361
  log.debug(
160
362
  "JWT token result: %s",
161
- json.dumps(decoded_token, default=str) if decoded_token else "None",
363
+ json.dumps(decoded_token, default=str)
364
+ if decoded_token
365
+ else "None",
162
366
  )
163
- # Populate requestContext for Lambda handler compatibility
367
+
368
+ # Populate requestContext
164
369
  if "requestContext" not in event:
165
370
  event["requestContext"] = {}
166
371
  if "authorizer" not in event["requestContext"]:
@@ -193,22 +398,26 @@ class JWTDecoder:
193
398
  ):
194
399
  log.debug(
195
400
  "Initializing JWTDecoder with jwks_url: %s, issuer: %s, anonymous_role: %s",
196
- jwks_url, issuer, anonymous_role
401
+ jwks_url,
402
+ issuer,
403
+ anonymous_role,
197
404
  )
198
405
  self.jwks_url = jwks_url
199
406
  self.issuer = issuer
200
407
  self.allowed_audiences = allowed_audiences or set()
201
408
  self.algorithms = algorithms or ["RS256"]
202
409
  self.anonymous_role = anonymous_role
203
-
410
+
204
411
  # Fetch public key only if JWKS URL is provided
205
412
  if jwks_url:
206
413
  self.public_key = self.fetch_public_key_from_jwks(jwks_url)
207
414
  if not self.public_key:
208
- raise ValueError(f"Failed to fetch public key from JWKS URL: {jwks_url}")
415
+ raise ValueError(
416
+ f"Failed to fetch public key from JWKS URL: {jwks_url}"
417
+ )
209
418
  else:
210
419
  self.public_key = None
211
-
420
+
212
421
  log.debug(
213
422
  "JWTDecoder initialized with %d allowed audiences, algorithms: %s",
214
423
  len(self.allowed_audiences),
@@ -288,11 +497,15 @@ class JWTDecoder:
288
497
  if token:
289
498
  return self.decode_token(token)
290
499
  elif self.anonymous_role:
291
- log.debug("No token found, using anonymous role: %s", self.anonymous_role)
500
+ log.debug(
501
+ "No token found, using anonymous role: %s", self.anonymous_role
502
+ )
292
503
  return {"roles": [self.anonymous_role]}
293
504
  else:
294
505
  log.error("No token found and no anonymous role configured")
295
- raise ValueError("Authentication required: no token provided and no anonymous access configured")
506
+ raise ValueError(
507
+ "Authentication required: no token provided and no anonymous access configured"
508
+ )
296
509
  except ValueError as e:
297
510
  if self.anonymous_role and "No authorization header" in str(e):
298
511
  log.debug("Token parsing failed, using anonymous role: %s", str(e))
@@ -334,7 +547,7 @@ class JWTDecoder:
334
547
  def decode_token(self, token: str) -> Dict[str, Any]:
335
548
  """Validate and decode the JWT using the PEM public key."""
336
549
  log.debug("Starting JWT token validation and decoding")
337
-
550
+
338
551
  if not self.public_key:
339
552
  raise ValueError("No public key available for token validation")
340
553
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "api-foundry-query-engine"
7
- version = "0.8.35"
7
+ version = "0.8.37"
8
8
  authors = [
9
9
  { name="Daniel Repik", email="danrepik@icloud.com" },
10
10
  ]