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.
- {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/PKG-INFO +1 -1
- {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/connectors/postgres_connection.py +4 -4
- {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
- {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
- {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
- {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
- {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
- {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/utils/token_decoder.py +280 -67
- {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/pyproject.toml +1 -1
- {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/.gitignore +0 -0
- {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/LICENSE +0 -0
- {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/.pre-commit-config.yaml +0 -0
- {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/__init__.py +0 -0
- {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/adapters/adapter.py +0 -0
- {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
- {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/adapters/gateway_adapter.py +0 -0
- {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/adapters/security_adapter.py +0 -0
- {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/connectors/connection.py +0 -0
- {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/connectors/connection_factory.py +0 -0
- {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/connectors/oracle_connector.py +0 -0
- {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
- {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/dao/dao.py +0 -0
- {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/dao/operation_dao.py +0 -0
- {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
- {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
- {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
- {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/lambda_handler.py +0 -0
- {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/operation.py +0 -0
- {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/services/service.py +0 -0
- {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/services/transactional_service.py +0 -0
- {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/utils/api_model.py +0 -0
- {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/utils/app_exception.py +0 -0
- {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/utils/claims_check.py +0 -0
- {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/utils/dependency_resolver.py +0 -0
- {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/utils/gateway_operation.py +0 -0
- {api_foundry_query_engine-0.8.35 → api_foundry_query_engine-0.8.37}/api_foundry_query_engine/utils/logger.py +0 -0
- {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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
"Subject is not allowed to restore "
|
|
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():
|
|
@@ -169,8 +169,9 @@ class SQLUpdateSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
169
169
|
|
|
170
170
|
if invalid_columns:
|
|
171
171
|
raise ApplicationException(
|
|
172
|
-
|
|
173
|
-
f"Subject does not have permission to update properties:
|
|
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
|
|
49
|
-
|
|
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
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
155
|
+
Decorated handler function that processes JWT tokens
|
|
62
156
|
|
|
63
157
|
Example:
|
|
64
|
-
#
|
|
158
|
+
# JWKS validation
|
|
65
159
|
@token_decoder(
|
|
66
|
-
jwks_url="https://
|
|
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
|
|
72
|
-
|
|
73
|
-
return {'statusCode': 200, 'body': 'Secured endpoint'}
|
|
164
|
+
def handler(event, context):
|
|
165
|
+
return {'statusCode': 200, 'body': 'Secured'}
|
|
74
166
|
|
|
75
|
-
#
|
|
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
|
|
78
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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(
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
121
|
-
if not config_jwks_url:
|
|
122
|
-
log.debug("No
|
|
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
|
-
#
|
|
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
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
157
|
-
|
|
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)
|
|
363
|
+
json.dumps(decoded_token, default=str)
|
|
364
|
+
if decoded_token
|
|
365
|
+
else "None",
|
|
162
366
|
)
|
|
163
|
-
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|