api-foundry-query-engine 0.8.69__tar.gz → 0.8.71__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.69 → api_foundry_query_engine-0.8.71}/PKG-INFO +1 -1
  2. api_foundry_query_engine-0.8.71/api_foundry_query_engine/__init__.py +1 -0
  3. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/connectors/postgres_connection.py +9 -4
  4. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/utils/api_model.py +1 -1
  5. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/utils/token_decoder.py +121 -2
  6. api_foundry_query_engine-0.8.69/api_foundry_query_engine/__init__.py +0 -1
  7. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/.gitignore +0 -0
  8. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/LICENSE +0 -0
  9. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/.pre-commit-config.yaml +0 -0
  10. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/adapters/adapter.py +0 -0
  11. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/adapters/case_change_adapter.py +0 -0
  12. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/adapters/gateway_adapter.py +0 -0
  13. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/adapters/security_adapter.py +0 -0
  14. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/connectors/connection.py +0 -0
  15. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/connectors/connection_factory.py +0 -0
  16. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/connectors/oracle_connector.py +0 -0
  17. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/dao/batch_operation_handler.py +0 -0
  18. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/dao/dao.py +0 -0
  19. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/dao/operation_dao.py +0 -0
  20. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/dao/sql_custom_query_handler.py +0 -0
  21. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/dao/sql_delete_query_handler.py +0 -0
  22. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/dao/sql_insert_query_handler.py +0 -0
  23. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/dao/sql_query_handler.py +0 -0
  24. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/dao/sql_restore_query_handler.py +0 -0
  25. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/dao/sql_select_query_handler.py +0 -0
  26. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/dao/sql_subselect_query_handler.py +0 -0
  27. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/dao/sql_update_query_handler.py +0 -0
  28. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/lambda_handler.py +0 -0
  29. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/operation.py +0 -0
  30. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/services/service.py +0 -0
  31. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/services/transactional_service.py +0 -0
  32. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/utils/app_exception.py +0 -0
  33. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/utils/claims_check.py +0 -0
  34. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/utils/dependency_resolver.py +0 -0
  35. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/utils/logger.py +0 -0
  36. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/api_foundry_query_engine/utils/reference_resolver.py +0 -0
  37. {api_foundry_query_engine-0.8.69 → api_foundry_query_engine-0.8.71}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: api-foundry-query-engine
3
- Version: 0.8.69
3
+ Version: 0.8.71
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
@@ -0,0 +1 @@
1
+ __version__ = "0.8.71"
@@ -32,12 +32,17 @@ class PostgresCursor(Cursor):
32
32
  try:
33
33
  # Execute the SQL statement with parameters
34
34
  self.__cursor.execute(sql, params)
35
+ selected_columns = set(selection_results)
36
+ column_names = [desc[0] for desc in (self.__cursor.description or ())]
35
37
  result = []
36
38
  for record in self.__cursor:
37
- # Convert record tuple to dictionary using selection_results
38
- result.append(
39
- {col: value for col, value in zip(selection_results, record)}
40
- )
39
+ # Use cursor metadata so SQL aliases define the output mapping.
40
+ row = {
41
+ col: value
42
+ for col, value in zip(column_names, record)
43
+ if col in selected_columns
44
+ }
45
+ result.append(row)
41
46
 
42
47
  return result
43
48
  except IntegrityError as err:
@@ -321,7 +321,7 @@ class PathOperation:
321
321
  self.outputs: Dict[str, SchemaObjectProperty] = {
322
322
  name: SchemaObjectProperty(output_data) for name, output_data in data.get("outputs", {}).items()
323
323
  }
324
- self.permissions = data.get("security")
324
+ self.permissions = data.get("permissions") or data.get("security")
325
325
 
326
326
  def __repr__(self):
327
327
  return f"PathOperation(entity={self.entity}, action={self.action})"
@@ -20,6 +20,13 @@ __all__ = ["token_decoder"]
20
20
 
21
21
  log = logging.getLogger(__name__)
22
22
 
23
+ METHOD_TO_ACTION = {
24
+ "GET": "read",
25
+ "POST": "create",
26
+ "PUT": "update",
27
+ "DELETE": "delete",
28
+ }
29
+
23
30
 
24
31
  def _log_jwt_configuration():
25
32
  """Log current JWT configuration for debugging."""
@@ -114,6 +121,87 @@ class LambdaTokenValidator:
114
121
  raise ValueError(f"Token validation failed: {str(e)}")
115
122
 
116
123
 
124
+ def _normalize_permission_action(action: str) -> str:
125
+ return "write" if action in ("create", "update") else action
126
+
127
+
128
+ def _extract_event_entity_and_action(event: Dict[str, Any]) -> tuple[Optional[str], str]:
129
+ resource = event.get("resource") or event.get("path") or ""
130
+ parts = [p for p in str(resource).split("/") if p]
131
+ api_prefixes = {"api", "v1", "v2", "v3"}
132
+ literal_parts = [
133
+ p
134
+ for p in parts
135
+ if p not in api_prefixes and not (p.startswith("{") and p.endswith("}"))
136
+ ]
137
+ entity = "_".join(literal_parts) if literal_parts else None
138
+ method = str(event.get("httpMethod", "")).upper()
139
+ action = METHOD_TO_ACTION.get(method, "read")
140
+ return entity, action
141
+
142
+
143
+ def _normalize_permissions(permissions: Optional[dict]) -> dict:
144
+ if not permissions or not isinstance(permissions, dict):
145
+ return {}
146
+
147
+ if "default" in permissions:
148
+ return permissions
149
+
150
+ normalized = {"default": {"read": {}, "write": {}, "delete": {}}}
151
+ for role, actions in permissions.items():
152
+ if not isinstance(actions, dict):
153
+ continue
154
+ for action, rule in actions.items():
155
+ normalized_action = _normalize_permission_action(action)
156
+ if normalized_action in normalized["default"]:
157
+ normalized["default"][normalized_action][role] = rule
158
+ return normalized
159
+
160
+
161
+ def _get_route_permissions(event: Dict[str, Any]) -> dict:
162
+ from api_foundry_query_engine.utils import api_model as api_model_module
163
+ from api_foundry_query_engine.utils.api_model import (
164
+ get_path_operation,
165
+ get_schema_object,
166
+ set_api_model,
167
+ )
168
+
169
+ if api_model_module.api_model is None:
170
+ if not os.getenv("API_SPEC"):
171
+ return {}
172
+ try:
173
+ set_api_model(os.environ)
174
+ except Exception as exc:
175
+ log.warning("Failed to load API model for anonymous-route check: %s", exc)
176
+ return {}
177
+
178
+ entity, action = _extract_event_entity_and_action(event)
179
+ if not entity:
180
+ return {}
181
+
182
+ path_operation = get_path_operation(entity, action)
183
+ if path_operation:
184
+ return path_operation.permissions or {}
185
+
186
+ schema_object = get_schema_object(entity)
187
+ if schema_object:
188
+ return schema_object.permissions or {}
189
+
190
+ return {}
191
+
192
+
193
+ def _route_explicitly_allows_role(event: Dict[str, Any], role: Optional[str]) -> bool:
194
+ if not role:
195
+ return False
196
+
197
+ _, action = _extract_event_entity_and_action(event)
198
+ permissions = _normalize_permissions(_get_route_permissions(event))
199
+ action_permissions = permissions.get("default", {}).get(
200
+ _normalize_permission_action(action), {}
201
+ )
202
+ return role in action_permissions
203
+
204
+
117
205
  def token_decoder(
118
206
  jwks_url: Optional[str] = None,
119
207
  audience: Optional[str] = None,
@@ -223,16 +311,47 @@ def token_decoder(
223
311
  log.debug("config_anonymous_role: %s", config_anonymous_role)
224
312
  log.debug("config_lambda_arn: %s", config_lambda_arn)
225
313
 
314
+ # Some API Gateway routes are intentionally public but still receive
315
+ # browser Authorization headers from the SPA. In Lambda proxy mode
316
+ # those public routes do not have requestContext.authorizer populated,
317
+ # and invoking a Lambda TOKEN validator from a VPC-backed API Lambda
318
+ # can hang until the API timeout. When anonymous access is explicitly
319
+ # enabled and claims checks are disabled, treat the request as public
320
+ # unless API Gateway already supplied verified claims above.
321
+ if (
322
+ config_anonymous_role
323
+ and os.getenv("SKIP_CLAIMS_CHECK", "").lower() == "true"
324
+ and _route_explicitly_allows_role(event, config_anonymous_role)
325
+ ):
326
+ log.debug(
327
+ "SKIP_CLAIMS_CHECK enabled; using anonymous role without inline token validation"
328
+ )
329
+ if "requestContext" not in event:
330
+ event["requestContext"] = {}
331
+ event["requestContext"]["authorizer"] = {
332
+ "roles": [config_anonymous_role]
333
+ }
334
+ return handler(event, context)
335
+
226
336
  # Skip all validation if no method configured
227
337
  if not config_lambda_arn and not config_jwks_url:
228
338
  log.debug("No validation method configured")
229
- if config_anonymous_role:
339
+ if _route_explicitly_allows_role(event, config_anonymous_role):
230
340
  log.debug("Using anonymous role: %s", config_anonymous_role)
231
341
  if "requestContext" not in event:
232
342
  event["requestContext"] = {}
233
343
  event["requestContext"]["authorizer"] = {
234
344
  "roles": [config_anonymous_role]
235
345
  }
346
+ elif config_anonymous_role:
347
+ log.error(
348
+ "Anonymous role configured but route does not explicitly allow it"
349
+ )
350
+ return {
351
+ "statusCode": 401,
352
+ "headers": {"Content-Type": "application/json"},
353
+ "body": json.dumps({"error": "Unauthorized"}),
354
+ }
236
355
  return handler(event, context)
237
356
 
238
357
  try:
@@ -315,7 +434,7 @@ def token_decoder(
315
434
 
316
435
  # Fall back to anonymous if configured
317
436
  if not decoded_token:
318
- if wrapper._anonymous_role:
437
+ if _route_explicitly_allows_role(event, wrapper._anonymous_role):
319
438
  log.debug(
320
439
  "Using anonymous role: %s",
321
440
  wrapper._anonymous_role,
@@ -1 +0,0 @@
1
- __version__ = "0.8.69"