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.
- api_foundry_query_engine/.pre-commit-config.yaml +22 -0
- api_foundry_query_engine/__init__.py +1 -0
- api_foundry_query_engine/adapters/adapter.py +73 -0
- api_foundry_query_engine/adapters/case_change_adapter.py +79 -0
- api_foundry_query_engine/adapters/gateway_adapter.py +191 -0
- api_foundry_query_engine/adapters/security_adapter.py +106 -0
- api_foundry_query_engine/connectors/connection.py +32 -0
- api_foundry_query_engine/connectors/connection_factory.py +115 -0
- api_foundry_query_engine/connectors/oracle_connector.py +29 -0
- api_foundry_query_engine/connectors/postgres_connection.py +142 -0
- api_foundry_query_engine/dao/batch_operation_handler.py +295 -0
- api_foundry_query_engine/dao/dao.py +23 -0
- api_foundry_query_engine/dao/operation_dao.py +186 -0
- api_foundry_query_engine/dao/sql_custom_query_handler.py +68 -0
- api_foundry_query_engine/dao/sql_delete_query_handler.py +146 -0
- api_foundry_query_engine/dao/sql_insert_query_handler.py +187 -0
- api_foundry_query_engine/dao/sql_query_handler.py +713 -0
- api_foundry_query_engine/dao/sql_restore_query_handler.py +195 -0
- api_foundry_query_engine/dao/sql_select_query_handler.py +433 -0
- api_foundry_query_engine/dao/sql_subselect_query_handler.py +66 -0
- api_foundry_query_engine/dao/sql_update_query_handler.py +198 -0
- api_foundry_query_engine/lambda_handler.py +63 -0
- api_foundry_query_engine/operation.py +104 -0
- api_foundry_query_engine/services/service.py +75 -0
- api_foundry_query_engine/services/transactional_service.py +52 -0
- api_foundry_query_engine/utils/api_model.py +380 -0
- api_foundry_query_engine/utils/app_exception.py +22 -0
- api_foundry_query_engine/utils/claims_check.py +471 -0
- api_foundry_query_engine/utils/dependency_resolver.py +157 -0
- api_foundry_query_engine/utils/gateway_operation.py +279 -0
- api_foundry_query_engine/utils/logger.py +60 -0
- api_foundry_query_engine/utils/reference_resolver.py +222 -0
- api_foundry_query_engine/utils/token_decoder.py +624 -0
- api_foundry_query_engine-0.8.39.dist-info/METADATA +21 -0
- api_foundry_query_engine-0.8.39.dist-info/RECORD +37 -0
- api_foundry_query_engine-0.8.39.dist-info/WHEEL +4 -0
- 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,,
|