api-foundry-query-engine 0.8.39__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. api_foundry_query_engine/.pre-commit-config.yaml +22 -0
  2. api_foundry_query_engine/__init__.py +1 -0
  3. api_foundry_query_engine/adapters/adapter.py +73 -0
  4. api_foundry_query_engine/adapters/case_change_adapter.py +79 -0
  5. api_foundry_query_engine/adapters/gateway_adapter.py +191 -0
  6. api_foundry_query_engine/adapters/security_adapter.py +106 -0
  7. api_foundry_query_engine/connectors/connection.py +32 -0
  8. api_foundry_query_engine/connectors/connection_factory.py +115 -0
  9. api_foundry_query_engine/connectors/oracle_connector.py +29 -0
  10. api_foundry_query_engine/connectors/postgres_connection.py +142 -0
  11. api_foundry_query_engine/dao/batch_operation_handler.py +295 -0
  12. api_foundry_query_engine/dao/dao.py +23 -0
  13. api_foundry_query_engine/dao/operation_dao.py +186 -0
  14. api_foundry_query_engine/dao/sql_custom_query_handler.py +68 -0
  15. api_foundry_query_engine/dao/sql_delete_query_handler.py +146 -0
  16. api_foundry_query_engine/dao/sql_insert_query_handler.py +187 -0
  17. api_foundry_query_engine/dao/sql_query_handler.py +713 -0
  18. api_foundry_query_engine/dao/sql_restore_query_handler.py +195 -0
  19. api_foundry_query_engine/dao/sql_select_query_handler.py +433 -0
  20. api_foundry_query_engine/dao/sql_subselect_query_handler.py +66 -0
  21. api_foundry_query_engine/dao/sql_update_query_handler.py +198 -0
  22. api_foundry_query_engine/lambda_handler.py +63 -0
  23. api_foundry_query_engine/operation.py +104 -0
  24. api_foundry_query_engine/services/service.py +75 -0
  25. api_foundry_query_engine/services/transactional_service.py +52 -0
  26. api_foundry_query_engine/utils/api_model.py +380 -0
  27. api_foundry_query_engine/utils/app_exception.py +22 -0
  28. api_foundry_query_engine/utils/claims_check.py +471 -0
  29. api_foundry_query_engine/utils/dependency_resolver.py +157 -0
  30. api_foundry_query_engine/utils/gateway_operation.py +279 -0
  31. api_foundry_query_engine/utils/logger.py +60 -0
  32. api_foundry_query_engine/utils/reference_resolver.py +222 -0
  33. api_foundry_query_engine/utils/token_decoder.py +624 -0
  34. api_foundry_query_engine-0.8.39.dist-info/METADATA +21 -0
  35. api_foundry_query_engine-0.8.39.dist-info/RECORD +37 -0
  36. api_foundry_query_engine-0.8.39.dist-info/WHEEL +4 -0
  37. api_foundry_query_engine-0.8.39.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,624 @@
1
+ """JWT Decoder Filter for AWS Lambda Functions.
2
+
3
+ This module provides a decorator-based JWT token decoder that extracts JWT
4
+ tokens from Authorization headers and exposes the claims through
5
+ requestContext.authorizer to maintain compatibility with API Gateway TOKEN
6
+ authorizers.
7
+
8
+ The filter acts like a Java servlet filter but for Lambda functions, providing
9
+ a clean separation of concerns between authentication and business logic.
10
+ """
11
+
12
+ import json
13
+ import logging
14
+ import functools
15
+ import os
16
+ import base64
17
+ from typing import Callable, Any, Dict, Optional
18
+
19
+ __all__ = ["token_decoder"]
20
+
21
+ log = logging.getLogger(__name__)
22
+
23
+
24
+ def _log_jwt_configuration():
25
+ """Log current JWT configuration for debugging."""
26
+ log.debug("=== JWT Configuration Debug ===")
27
+ log.debug("JWKS_HOST: %s", os.getenv("JWKS_HOST", "NOT_SET"))
28
+ log.debug("JWT_ISSUER: %s", os.getenv("JWT_ISSUER", "NOT_SET"))
29
+ log.debug(
30
+ "JWT_ALLOWED_AUDIENCES: %s", os.getenv("JWT_ALLOWED_AUDIENCES", "NOT_SET")
31
+ )
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
+ )
37
+ log.debug("Logging level: %s", logging.getLogger().getEffectiveLevel())
38
+ log.debug("===============================")
39
+
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
+
123
+ def token_decoder(
124
+ jwks_url: Optional[str] = None,
125
+ audience: Optional[str] = None,
126
+ issuer: Optional[str] = None,
127
+ algorithms: Optional[list[str]] = None,
128
+ anonymous_role: Optional[str] = None,
129
+ lambda_validator_arn: Optional[str] = None,
130
+ ):
131
+ """
132
+ JWT token decoder decorator for AWS Lambda handlers.
133
+
134
+ This filter extracts JWT tokens from the Authorization header, validates
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
143
+
144
+ Args:
145
+ jwks_url: URL to fetch JWKS from
146
+ audience: Expected audience claim (or list of audiences)
147
+ issuer: Expected issuer claim
148
+ algorithms: List of allowed algorithms (defaults to ["RS256"])
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.
153
+
154
+ Returns:
155
+ Decorated handler function that processes JWT tokens
156
+
157
+ Example:
158
+ # JWKS validation
159
+ @token_decoder(
160
+ jwks_url="https://oauth.local/.well-known/jwks.json",
161
+ audience="test-api",
162
+ issuer="https://oauth.local/",
163
+ )
164
+ def handler(event, context):
165
+ return {'statusCode': 200, 'body': 'Secured'}
166
+
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
173
+ @token_decoder(anonymous_role="public")
174
+ def handler(event, context):
175
+ return {'statusCode': 200, 'body': 'Public'}
176
+ """
177
+
178
+ # Validate configuration at decorator time
179
+ config_anonymous_role = anonymous_role or os.getenv("ANONYMOUS_ROLE")
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
+ ):
191
+ pass
192
+
193
+ def decorator(handler: Callable) -> Callable:
194
+ @functools.wraps(handler)
195
+ def wrapper(event: dict, context: Any) -> dict:
196
+ log.debug(
197
+ "JWT token decoder starting for function: %s",
198
+ handler.__name__,
199
+ )
200
+
201
+ # Log configuration for debugging
202
+ if log.isEnabledFor(logging.DEBUG):
203
+ _log_jwt_configuration()
204
+
205
+ log.debug("Event structure: %s", json.dumps(event, default=str))
206
+
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
+ )
218
+ config_jwks_url = (
219
+ jwks_url
220
+ or f"{os.getenv('JWKS_HOST')}/.well-known/jwks.json"
221
+ if os.getenv("JWKS_HOST")
222
+ else None
223
+ )
224
+ config_issuer = issuer or os.getenv("JWT_ISSUER")
225
+ config_audience = (
226
+ audience
227
+ or os.getenv("JWT_ALLOWED_AUDIENCES", "").split(",")
228
+ if os.getenv("JWT_ALLOWED_AUDIENCES")
229
+ else []
230
+ )
231
+ config_algorithms = algorithms or ["RS256"]
232
+ config_anonymous_role = anonymous_role or os.getenv(
233
+ "ANONYMOUS_ROLE"
234
+ )
235
+ log.debug("anonymous_role parameter: %s", anonymous_role)
236
+ log.debug(
237
+ "ANONYMOUS_ROLE env: %s", os.getenv("ANONYMOUS_ROLE", "")
238
+ )
239
+ log.debug("config_anonymous_role: %s", config_anonymous_role)
240
+ log.debug("config_lambda_arn: %s", config_lambda_arn)
241
+
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
+ }
254
+ return handler(event, context)
255
+
256
+ try:
257
+ log.debug("Processing JWT token extraction and validation")
258
+
259
+ # Set up validation instances
260
+ if not hasattr(wrapper, "_jwt_decoder"):
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")
303
+ )
304
+
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
+ }
360
+
361
+ log.debug(
362
+ "JWT token result: %s",
363
+ json.dumps(decoded_token, default=str)
364
+ if decoded_token
365
+ else "None",
366
+ )
367
+
368
+ # Populate requestContext
369
+ if "requestContext" not in event:
370
+ event["requestContext"] = {}
371
+ if "authorizer" not in event["requestContext"]:
372
+ event["requestContext"]["authorizer"] = {}
373
+ event["requestContext"]["authorizer"] = decoded_token
374
+
375
+ return handler(event, context)
376
+
377
+ except Exception as e:
378
+ log.error("JWT filter critical error: %s", str(e))
379
+ return {
380
+ "statusCode": 500,
381
+ "headers": {"Content-Type": "application/json"},
382
+ "body": json.dumps({"error": "Internal server error"}),
383
+ }
384
+
385
+ return wrapper
386
+
387
+ return decorator
388
+
389
+
390
+ class JWTDecoder:
391
+ def __init__(
392
+ self,
393
+ jwks_url: Optional[str] = None,
394
+ issuer: Optional[str] = None,
395
+ allowed_audiences: Optional[set] = None,
396
+ algorithms: Optional[list] = None,
397
+ anonymous_role: Optional[str] = None,
398
+ ):
399
+ log.debug(
400
+ "Initializing JWTDecoder with jwks_url: %s, issuer: %s, anonymous_role: %s",
401
+ jwks_url,
402
+ issuer,
403
+ anonymous_role,
404
+ )
405
+ self.jwks_url = jwks_url
406
+ self.issuer = issuer
407
+ self.allowed_audiences = allowed_audiences or set()
408
+ self.algorithms = algorithms or ["RS256"]
409
+ self.anonymous_role = anonymous_role
410
+
411
+ # Fetch public key only if JWKS URL is provided
412
+ if jwks_url:
413
+ self.public_key = self.fetch_public_key_from_jwks(jwks_url)
414
+ if not self.public_key:
415
+ raise ValueError(
416
+ f"Failed to fetch public key from JWKS URL: {jwks_url}"
417
+ )
418
+ else:
419
+ self.public_key = None
420
+
421
+ log.debug(
422
+ "JWTDecoder initialized with %d allowed audiences, algorithms: %s",
423
+ len(self.allowed_audiences),
424
+ self.algorithms,
425
+ )
426
+
427
+ def fetch_public_key_from_jwks(
428
+ self, jwks_url: str, kid: Optional[str] = None
429
+ ) -> Optional[str]:
430
+ """
431
+ Fetch the public key from a JWKS endpoint.
432
+
433
+ Args:
434
+ jwks_url: The full JWKS endpoint URL
435
+ kid: Optional key ID to select a specific key
436
+
437
+ Returns:
438
+ PEM-formatted public key string, or None if not found
439
+ """
440
+ import requests
441
+ from cryptography.hazmat.primitives import serialization
442
+ from cryptography.hazmat.primitives.asymmetric import rsa
443
+ from cryptography.hazmat.backends import default_backend
444
+
445
+ try:
446
+ log.debug("Fetching JWKS from URL: %s", jwks_url)
447
+ resp = requests.get(jwks_url, timeout=5)
448
+ resp.raise_for_status()
449
+ log.debug("JWKS request successful, status: %d", resp.status_code)
450
+
451
+ jwks = resp.json()
452
+ keys = jwks.get("keys", [])
453
+ log.debug("Found %d keys in JWKS response", len(keys))
454
+
455
+ if not keys:
456
+ log.warning("No keys found in JWKS response")
457
+ return None
458
+
459
+ # Select key by kid if provided, else use first key
460
+ key = None
461
+ if kid:
462
+ log.debug("Looking for specific key ID: %s", kid)
463
+ for k in keys:
464
+ if k.get("kid") == kid:
465
+ key = k
466
+ log.debug("Found matching key for kid: %s", kid)
467
+ break
468
+ if not key:
469
+ log.warning("Key ID %s not found, using first key", kid)
470
+ if not key:
471
+ key = keys[0]
472
+ log.debug(
473
+ "Using first available key with kid: %s", key.get("kid", "unknown")
474
+ )
475
+ # Convert JWK to PEM (requires cryptography)
476
+
477
+ def b64url_decode(val):
478
+ val += "=" * (-len(val) % 4)
479
+ return base64.urlsafe_b64decode(val)
480
+
481
+ n = int.from_bytes(b64url_decode(key["n"]), "big")
482
+ e = int.from_bytes(b64url_decode(key["e"]), "big")
483
+ pubkey = rsa.RSAPublicNumbers(e, n).public_key(default_backend())
484
+ pem = pubkey.public_bytes(
485
+ encoding=serialization.Encoding.PEM,
486
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
487
+ )
488
+ return pem.decode("utf-8")
489
+ except Exception as e:
490
+ log.error("Failed to fetch public key from JWKS: %s", e)
491
+ return None
492
+
493
+ def decode(self, event: Dict[str, Any]) -> Dict[str, Any]:
494
+ """Decode the JWT token and return the claims, or anonymous claims if no token."""
495
+ try:
496
+ token = self.parse_token_from_event(event)
497
+ if token:
498
+ return self.decode_token(token)
499
+ elif self.anonymous_role:
500
+ log.debug(
501
+ "No token found, using anonymous role: %s", self.anonymous_role
502
+ )
503
+ return {"roles": [self.anonymous_role]}
504
+ else:
505
+ log.error("No token found and no anonymous role configured")
506
+ raise ValueError(
507
+ "Authentication required: no token provided and no anonymous access configured"
508
+ )
509
+ except ValueError as e:
510
+ if self.anonymous_role and "No authorization header" in str(e):
511
+ log.debug("Token parsing failed, using anonymous role: %s", str(e))
512
+ return {"roles": [self.anonymous_role]}
513
+ else:
514
+ raise
515
+
516
+ def parse_token_from_event(self, event: Dict[str, Any]) -> Optional[str]:
517
+ """Extract the Bearer token from the authorization header."""
518
+ log.debug("Parsing JWT token from event")
519
+
520
+ auth_header = (
521
+ event.get("authorizationToken")
522
+ or event.get("headers", {}).get("Authorization")
523
+ or event.get("headers", {}).get("authorization")
524
+ )
525
+
526
+ log.debug("Authorization header found: %s", "Yes" if auth_header else "No")
527
+
528
+ if not auth_header:
529
+ log.debug("No authorization header found")
530
+ raise ValueError("No authorization header found")
531
+
532
+ auth_token_parts = auth_header.split(" ")
533
+ log.debug("Authorization header parts: %d", len(auth_token_parts))
534
+
535
+ if (
536
+ len(auth_token_parts) != 2
537
+ or auth_token_parts[0].lower() != "bearer"
538
+ or not auth_token_parts[1]
539
+ ):
540
+ log.error("Invalid authorization header format")
541
+ raise ValueError("Invalid AuthorizationToken.")
542
+
543
+ token = auth_token_parts[1]
544
+ log.debug("JWT token extracted successfully, length: %d", len(token))
545
+ return token
546
+
547
+ def decode_token(self, token: str) -> Dict[str, Any]:
548
+ """Validate and decode the JWT using the PEM public key."""
549
+ log.debug("Starting JWT token validation and decoding")
550
+
551
+ if not self.public_key:
552
+ raise ValueError("No public key available for token validation")
553
+
554
+ import jwt
555
+ from jwt import (
556
+ InvalidTokenError,
557
+ ExpiredSignatureError,
558
+ InvalidAudienceError, # type: ignore
559
+ )
560
+
561
+ try:
562
+ # First decode without audience enforcement; we'll validate
563
+ # audience against the configured allowed set derived from
564
+ # config.yaml. This supports multi-audience tokens.
565
+ log.debug("Configuring JWT decode options")
566
+ decode_options = {"verify_aud": False}
567
+ decode_args = {
568
+ "algorithms": self.algorithms,
569
+ "options": decode_options,
570
+ "key": self.public_key,
571
+ "token": token,
572
+ }
573
+
574
+ if self.issuer:
575
+ log.debug("Using issuer validation: %s", self.issuer)
576
+ decode_args["issuer"] = self.issuer
577
+ else:
578
+ log.debug("Skipping issuer validation")
579
+ decode_options["verify_iss"] = False
580
+
581
+ if self.allowed_audiences:
582
+ log.debug("Allowed audiences configured: %s", self.allowed_audiences)
583
+ # We'll validate audience manually after decoding
584
+ decode_options["verify_aud"] = False
585
+ else:
586
+ log.debug("No audience validation configured")
587
+ decode_options["verify_aud"] = False
588
+
589
+ log.debug("Decoding JWT token with PyJWT")
590
+ # Extract token from decode_args as it needs to be the first positional argument
591
+ token_arg = decode_args.pop("token")
592
+ decoded_token = jwt.decode(token_arg, **decode_args)
593
+ log.debug("JWT token decoded successfully")
594
+
595
+ token_aud = decoded_token.get("aud")
596
+ log.debug("Token audience claim: %s", token_aud)
597
+
598
+ # Normalize token audience to a list for comparison
599
+ token_auds = (
600
+ [token_aud] if isinstance(token_aud, str) else list(token_aud or [])
601
+ )
602
+ token_auds = [str(a).strip() for a in token_auds if str(a).strip()]
603
+ log.debug("Normalized token audiences: %s", token_auds)
604
+
605
+ # Validate audience: token must contain at least one allowed aud
606
+ if self.allowed_audiences:
607
+ if not any(a in self.allowed_audiences for a in token_auds):
608
+ log.error(
609
+ "Audience validation failed. Required: %s, Found: %s",
610
+ self.allowed_audiences,
611
+ token_auds,
612
+ )
613
+ raise InvalidAudienceError("Audience not allowed")
614
+
615
+ log.debug("All JWT validations passed successfully")
616
+ log.debug("Decoded token claims: %s", list(decoded_token.keys()))
617
+ return decoded_token
618
+
619
+ except ExpiredSignatureError:
620
+ log.error("Token has expired")
621
+ raise
622
+ except InvalidTokenError as e:
623
+ log.error("Token validation failed: %s", e)
624
+ raise
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: api-foundry-query-engine
3
+ Version: 0.8.39
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
+ Project-URL: Documentation, https://github.com/DanRepik/api-foundry
6
+ Project-URL: Source, https://github.com/DanRepik/api-foundry
7
+ Author-email: Daniel Repik <danrepik@icloud.com>
8
+ License-File: LICENSE
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Requires-Python: >=3.9
12
+ Requires-Dist: boto3
13
+ Requires-Dist: pyhumps
14
+ Requires-Dist: pyyaml
15
+ Provides-Extra: dev
16
+ Requires-Dist: black>=23.0.0; extra == 'dev'
17
+ Requires-Dist: fixture-foundry>=0.1.0; extra == 'dev'
18
+ Requires-Dist: isort>=5.12.0; extra == 'dev'
19
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
20
+ Requires-Dist: pytest>=7.0; extra == 'dev'
21
+ Requires-Dist: simple-oauth-server; extra == 'dev'
@@ -0,0 +1,37 @@
1
+ api_foundry_query_engine/.pre-commit-config.yaml,sha256=wU1QWGIiFE8tks1CKqpQHZ-3cEXSHt5fMxf5_pHqI8Y,476
2
+ api_foundry_query_engine/__init__.py,sha256=GMgPHo3aMPQN3ddhz8XdWR1fi0qN24otKLOxOuDaDjg,23
3
+ api_foundry_query_engine/lambda_handler.py,sha256=KnQ4j2Ftvq873eqterU9dU29cS6b0i2nA0LUgVY8KP8,2327
4
+ api_foundry_query_engine/operation.py,sha256=dHGqA5AYxUD-GnT7usxZTxqaPYcAiky_2dpcaU43J3A,3892
5
+ api_foundry_query_engine/adapters/adapter.py,sha256=5DlJmQqa5Y8LyEGleK6Sao5Mh8g943njNO4n7aC7bXs,2058
6
+ api_foundry_query_engine/adapters/case_change_adapter.py,sha256=jsr1OIpuHdZ5TFa1TIBdE12ZQLvqcRWFIb2wzGJWx8U,2315
7
+ api_foundry_query_engine/adapters/gateway_adapter.py,sha256=FE6-IM5cl8yylUWW7E_6-08cNweAO7tjD-M4mJZqb6g,6139
8
+ api_foundry_query_engine/adapters/security_adapter.py,sha256=y0y8pm7kZd7dJnjBVB3dBZApgIUngyOnzh74zZOwYkU,3562
9
+ api_foundry_query_engine/connectors/connection.py,sha256=aGm6R5wt1MW8_5wsrPYW6DI3jnaDhFc9a6CYzLuNVlk,702
10
+ api_foundry_query_engine/connectors/connection_factory.py,sha256=WSumhjo3tXTTNo6hNIEMiGDlRWmTtYFNYCAZWneZ7iw,4203
11
+ api_foundry_query_engine/connectors/oracle_connector.py,sha256=gopPlD1dzeIus1KekVe1FaRzBkFrGekTMaJXNrD4LiI,1023
12
+ api_foundry_query_engine/connectors/postgres_connection.py,sha256=tsa0WDM0L7pLhLd3-cJuro_6FnMmqMTUTOjEJELHDbI,4725
13
+ api_foundry_query_engine/dao/batch_operation_handler.py,sha256=jE3I-ANowVQp1s6LPgfL0lDYrO1Qwq0LcU0iOAZ94mM,10706
14
+ api_foundry_query_engine/dao/dao.py,sha256=aBFJ0WT0PhMitCJa5_PRBjDeBNj9hLDePRa-awnLtRg,684
15
+ api_foundry_query_engine/dao/operation_dao.py,sha256=J1cdQT1KiHAQED6NYc4DZRMgRAS-T1C3XK_3ze15EKE,6733
16
+ api_foundry_query_engine/dao/sql_custom_query_handler.py,sha256=pqVeKQU-2zt-aGrjZx5b54VwFPH9FiAECzBfVYZxNes,2434
17
+ api_foundry_query_engine/dao/sql_delete_query_handler.py,sha256=EMruo-wYExEwDWbMAcqZsUd4piMCLLn87WduhS4oBvU,6270
18
+ api_foundry_query_engine/dao/sql_insert_query_handler.py,sha256=UsyMp6OK4_24uyAIM4mVsAcgvr9MiMmZXZA0wxoRD_c,7958
19
+ api_foundry_query_engine/dao/sql_query_handler.py,sha256=3pwMmYvOhekOGjCCdWQfP1MQv-G2og8B3Unluq9P-s4,25306
20
+ api_foundry_query_engine/dao/sql_restore_query_handler.py,sha256=pP1t53RzUqKgxV9JIPr1iL7ycVVlK44YepBpMgunLX0,8044
21
+ api_foundry_query_engine/dao/sql_select_query_handler.py,sha256=-Pon0EF56P_TWe2ANDv4VludjxCVToPCB6vlnuLeVAA,16926
22
+ api_foundry_query_engine/dao/sql_subselect_query_handler.py,sha256=Esz4bEdNUAKhkRGOY3rCW6x15k-ZqmBe8x0YVzaqSmQ,2418
23
+ api_foundry_query_engine/dao/sql_update_query_handler.py,sha256=TMEPQ2bo4jhXdMrN3tRmiWvYXptEq6h6jMUJHcM3a3c,8104
24
+ api_foundry_query_engine/services/service.py,sha256=x7J8T67S3WQsgKyyLJx3T8dJz_IrP8xsSs3b93dlZTg,2316
25
+ api_foundry_query_engine/services/transactional_service.py,sha256=W5soEcfBhUoS-KzlxkhCG_a3hZsaIsaxFuRIz93CBzs,1968
26
+ api_foundry_query_engine/utils/api_model.py,sha256=JopMgUPqMzJcBSo-8eBtGZQPug0Xb6AaoaV5a-_u704,14194
27
+ api_foundry_query_engine/utils/app_exception.py,sha256=8F5DEL8ovY0vJBXE3RERTA26u7kthcSX0VqViplZhdE,704
28
+ api_foundry_query_engine/utils/claims_check.py,sha256=iEYbX9DzwMr2eIujhv1Cwlg5Yt9yMvvmsUH29BB9ER8,17130
29
+ api_foundry_query_engine/utils/dependency_resolver.py,sha256=ZTegYlu9MiaQ4Bxs9_0Y8gm53oaG5bk1LbB4b4Z7bTI,5377
30
+ api_foundry_query_engine/utils/gateway_operation.py,sha256=6KT_jKHgBKygIso97LERqsyW0KN-Qm3fPKLzQAnXPlA,8736
31
+ api_foundry_query_engine/utils/logger.py,sha256=24u7AAVyzaPuS2nHSCq1SDemerJ8pCF5lde2NtIbwuo,1892
32
+ api_foundry_query_engine/utils/reference_resolver.py,sha256=DgNZLKcbcSLW4FFws7woMh_plXmao9wdPzOULttcsaY,7555
33
+ api_foundry_query_engine/utils/token_decoder.py,sha256=oDi8eHZy9571bkqUXG1ApLPllVpX5PSUPyGaOTpnE5g,23925
34
+ api_foundry_query_engine-0.8.39.dist-info/METADATA,sha256=rQP3qUg4ktwU1I1ONXrP1ysMMV4fmaAuvVCCtAGsWgs,1069
35
+ api_foundry_query_engine-0.8.39.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
36
+ api_foundry_query_engine-0.8.39.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
37
+ api_foundry_query_engine-0.8.39.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any