api-foundry-query-engine 0.8.57__tar.gz → 0.8.59__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.57 → api_foundry_query_engine-0.8.59}/PKG-INFO +1 -1
- api_foundry_query_engine-0.8.59/api_foundry_query_engine/__init__.py +1 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/adapters/gateway_adapter.py +18 -23
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/lambda_handler.py +1 -1
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/utils/claims_check.py +30 -37
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/pyproject.toml +7 -0
- api_foundry_query_engine-0.8.57/api_foundry_query_engine/__init__.py +0 -1
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/.gitignore +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/LICENSE +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/.pre-commit-config.yaml +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/adapters/adapter.py +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/adapters/case_change_adapter.py +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/adapters/security_adapter.py +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/connectors/connection.py +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/connectors/connection_factory.py +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/connectors/oracle_connector.py +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/connectors/postgres_connection.py +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/dao/batch_operation_handler.py +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/dao/dao.py +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/dao/operation_dao.py +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/dao/sql_custom_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/dao/sql_delete_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/dao/sql_insert_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/dao/sql_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/dao/sql_restore_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/dao/sql_select_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/dao/sql_subselect_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/dao/sql_update_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/operation.py +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/services/service.py +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/services/transactional_service.py +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/utils/api_model.py +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/utils/app_exception.py +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/utils/dependency_resolver.py +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/utils/logger.py +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/utils/reference_resolver.py +0 -0
- {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/utils/token_decoder.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.59
|
|
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.59"
|
|
@@ -4,7 +4,6 @@ from typing import Any, Dict, List, Optional, Tuple
|
|
|
4
4
|
|
|
5
5
|
from api_foundry_query_engine.adapters.adapter import Adapter
|
|
6
6
|
from api_foundry_query_engine.operation import Operation
|
|
7
|
-
from api_foundry_query_engine.utils.app_exception import ApplicationException
|
|
8
7
|
|
|
9
8
|
log = logging.getLogger(__name__)
|
|
10
9
|
|
|
@@ -40,12 +39,12 @@ class GatewayAdapter(Adapter):
|
|
|
40
39
|
- tuple: Tuple containing data, query and metadata parameters.
|
|
41
40
|
"""
|
|
42
41
|
resource = event.get("resource")
|
|
43
|
-
if resource
|
|
42
|
+
if resource:
|
|
44
43
|
parts = [p for p in resource.split("/") if p] # Remove empty strings
|
|
45
44
|
# Skip common API prefixes like "api", "v1", "v2", etc.
|
|
46
45
|
api_prefixes = {"api", "v1", "v2", "v3"}
|
|
47
|
-
|
|
48
|
-
entity =
|
|
46
|
+
literal_parts = [p for p in parts if p not in api_prefixes and not p.startswith("{")]
|
|
47
|
+
entity = "_".join(literal_parts) if literal_parts else None
|
|
49
48
|
else:
|
|
50
49
|
entity = None
|
|
51
50
|
|
|
@@ -57,14 +56,15 @@ class GatewayAdapter(Adapter):
|
|
|
57
56
|
|
|
58
57
|
# Handle different authorizer types
|
|
59
58
|
if isinstance(claims, dict):
|
|
60
|
-
# Parse JSON-stringified fields (roles, permissions)
|
|
61
|
-
|
|
59
|
+
# Parse JSON-stringified fields (roles, permissions)
|
|
60
|
+
# from token validator
|
|
61
|
+
for key in ["roles", "permissions"]:
|
|
62
62
|
if key in claims and isinstance(claims[key], str):
|
|
63
63
|
try:
|
|
64
64
|
claims[key] = json.loads(claims[key])
|
|
65
65
|
except (json.JSONDecodeError, ValueError):
|
|
66
66
|
pass # Keep as string if not valid JSON
|
|
67
|
-
|
|
67
|
+
|
|
68
68
|
# TOKEN authorizer puts claims directly in authorizer object
|
|
69
69
|
if "sub" in claims or "iss" in claims or "roles" in claims:
|
|
70
70
|
# Already have JWT claims at top level
|
|
@@ -103,19 +103,18 @@ class GatewayAdapter(Adapter):
|
|
|
103
103
|
if path_parameters is not None:
|
|
104
104
|
event_params.update(path_parameters)
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if queryStringParameters is not None:
|
|
110
|
-
event_params.update(queryStringParameters)
|
|
111
|
-
|
|
106
|
+
query_string_parameters = self._convert_parameters(event.get("queryStringParameters"))
|
|
107
|
+
if query_string_parameters is not None:
|
|
108
|
+
event_params.update(query_string_parameters)
|
|
112
109
|
query_params, metadata_params = self.split_params(event_params)
|
|
110
|
+
log.info("Unmarshalled query parameters: %s", query_params)
|
|
111
|
+
log.info("Unmarshalled metadata parameters: %s", metadata_params)
|
|
113
112
|
|
|
114
113
|
store_params = {}
|
|
115
114
|
body = event.get("body")
|
|
116
115
|
if body is not None and len(body) > 0:
|
|
117
116
|
store_params = json.loads(body)
|
|
118
|
-
|
|
117
|
+
|
|
119
118
|
return Operation(
|
|
120
119
|
entity=entity,
|
|
121
120
|
action=action,
|
|
@@ -125,9 +124,7 @@ class GatewayAdapter(Adapter):
|
|
|
125
124
|
claims=claims,
|
|
126
125
|
)
|
|
127
126
|
|
|
128
|
-
def _convert_parameters(
|
|
129
|
-
self, parameters: Optional[Dict[str, Any]]
|
|
130
|
-
) -> Optional[Dict[str, Any]]:
|
|
127
|
+
def _convert_parameters(self, parameters: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
|
131
128
|
"""
|
|
132
129
|
Convert parameters to appropriate types.
|
|
133
130
|
|
|
@@ -151,9 +148,7 @@ class GatewayAdapter(Adapter):
|
|
|
151
148
|
result[parameter] = value
|
|
152
149
|
return result
|
|
153
150
|
|
|
154
|
-
def split_params(
|
|
155
|
-
self, parameters: Dict[str, Any]
|
|
156
|
-
) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
|
151
|
+
def split_params(self, parameters: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
|
157
152
|
"""
|
|
158
153
|
Split a dictionary into two dictionaries based on keys.
|
|
159
154
|
|
|
@@ -174,8 +169,8 @@ class GatewayAdapter(Adapter):
|
|
|
174
169
|
else:
|
|
175
170
|
query_params[key] = value
|
|
176
171
|
|
|
177
|
-
log.info(
|
|
178
|
-
log.info(
|
|
179
|
-
log.info(
|
|
172
|
+
log.info("split_params input: %s", parameters)
|
|
173
|
+
log.info("query_params: %s", query_params)
|
|
174
|
+
log.info("metadata_params: %s", metadata_params)
|
|
180
175
|
|
|
181
176
|
return query_params, metadata_params
|
|
@@ -48,7 +48,7 @@ class QueryEngine:
|
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
@token_decoder()
|
|
51
|
-
@claims_check(validate_path_scope=False)
|
|
51
|
+
@claims_check(validate_path_scope=False, validate_scope_format=False)
|
|
52
52
|
def handler(event, _):
|
|
53
53
|
if not hasattr(handler, "engine_config"):
|
|
54
54
|
log.info("Loading engine config from environment variables")
|
|
@@ -83,17 +83,13 @@ def claims_check(
|
|
|
83
83
|
config_require_authentication = require_authentication or os.getenv(
|
|
84
84
|
"REQUIRE_AUTHENTICATION", ""
|
|
85
85
|
).lower() in ("true", "1", "yes")
|
|
86
|
-
log.debug(
|
|
87
|
-
"config_require_authentication: %s", config_require_authentication
|
|
88
|
-
)
|
|
86
|
+
log.debug("config_require_authentication: %s", config_require_authentication)
|
|
89
87
|
|
|
90
88
|
# Extract claims from the event (set by token_decoder)
|
|
91
89
|
claims = _extract_claims(event)
|
|
92
90
|
if not claims:
|
|
93
91
|
if config_require_authentication:
|
|
94
|
-
raise ApplicationException(
|
|
95
|
-
status_code=401, message="No authentication claims found"
|
|
96
|
-
)
|
|
92
|
+
raise ApplicationException(status_code=401, message="No authentication claims found")
|
|
97
93
|
else:
|
|
98
94
|
log.debug("Found claims: %s", list(claims.keys()))
|
|
99
95
|
|
|
@@ -111,9 +107,7 @@ def claims_check(
|
|
|
111
107
|
|
|
112
108
|
# Check specific scopes if required
|
|
113
109
|
if required_scopes:
|
|
114
|
-
_validate_required_scopes(
|
|
115
|
-
claims, required_scopes, event, operation_type, entity_name
|
|
116
|
-
)
|
|
110
|
+
_validate_required_scopes(claims, required_scopes, event, operation_type, entity_name)
|
|
117
111
|
|
|
118
112
|
# Check specific permissions if required
|
|
119
113
|
if required_permissions:
|
|
@@ -157,9 +151,7 @@ def _extract_claims(event: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
|
157
151
|
|
|
158
152
|
# Check direct authorizer context (most common for TOKEN authorizer)
|
|
159
153
|
claim_keys = ["sub", "scope", "permissions", "roles"]
|
|
160
|
-
if isinstance(authorizer, dict) and any(
|
|
161
|
-
key in authorizer for key in claim_keys
|
|
162
|
-
):
|
|
154
|
+
if isinstance(authorizer, dict) and any(key in authorizer for key in claim_keys):
|
|
163
155
|
log.debug("Found claims in requestContext.authorizer")
|
|
164
156
|
return authorizer
|
|
165
157
|
|
|
@@ -202,12 +194,12 @@ def _validate_scope_format(claims: Dict[str, Any]) -> None:
|
|
|
202
194
|
elif not isinstance(scopes, list):
|
|
203
195
|
scopes = []
|
|
204
196
|
|
|
205
|
-
#
|
|
197
|
+
# Scopes and permissions are now optional - they may be present but are not required
|
|
198
|
+
# The x-af-permissions in the API spec handles role-based access control
|
|
206
199
|
permissions = claims.get("permissions", [])
|
|
207
200
|
if not scopes and not permissions:
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
)
|
|
201
|
+
log.debug("No scopes or permissions found in token - relying on role-based permissions")
|
|
202
|
+
return
|
|
211
203
|
|
|
212
204
|
# Validate scope format: should be operation:resource or operation:*
|
|
213
205
|
for scope in scopes:
|
|
@@ -238,9 +230,7 @@ def _validate_min_scope_level(claims: Dict[str, Any], min_level: str) -> None:
|
|
|
238
230
|
user_max_level = max(user_max_level, scope_value)
|
|
239
231
|
|
|
240
232
|
if user_max_level < min_level_value:
|
|
241
|
-
raise ApplicationException(
|
|
242
|
-
status_code=403, message=f"Insufficient scope level. Required: {min_level}"
|
|
243
|
-
)
|
|
233
|
+
raise ApplicationException(status_code=403, message=f"Insufficient scope level. Required: {min_level}")
|
|
244
234
|
|
|
245
235
|
|
|
246
236
|
def _validate_path_scope(
|
|
@@ -293,8 +283,7 @@ def _validate_path_scope(
|
|
|
293
283
|
if not has_scope and not has_permission:
|
|
294
284
|
raise ApplicationException(
|
|
295
285
|
status_code=403,
|
|
296
|
-
message=f"Access denied. Required scope: {required_scope} "
|
|
297
|
-
f"or permission: {required_permission}",
|
|
286
|
+
message=f"Access denied. Required scope: {required_scope} " f"or permission: {required_permission}",
|
|
298
287
|
)
|
|
299
288
|
|
|
300
289
|
log.debug("Path scope validation passed for %s", required_scope)
|
|
@@ -321,14 +310,10 @@ def _validate_required_scopes(
|
|
|
321
310
|
# Check each required scope
|
|
322
311
|
for required_scope in required_scopes:
|
|
323
312
|
if not _scope_matches(user_scopes, required_scope, operation, entity):
|
|
324
|
-
raise ApplicationException(
|
|
325
|
-
status_code=403, message=f"Required scope not found: {required_scope}"
|
|
326
|
-
)
|
|
313
|
+
raise ApplicationException(status_code=403, message=f"Required scope not found: {required_scope}")
|
|
327
314
|
|
|
328
315
|
|
|
329
|
-
def _validate_required_permissions(
|
|
330
|
-
claims: Dict[str, Any], required_permissions: list
|
|
331
|
-
) -> None:
|
|
316
|
+
def _validate_required_permissions(claims: Dict[str, Any], required_permissions: list) -> None:
|
|
332
317
|
"""Validate that user has required permissions."""
|
|
333
318
|
user_permissions = claims.get("permissions", [])
|
|
334
319
|
if isinstance(user_permissions, str):
|
|
@@ -418,22 +403,30 @@ def _extract_entity_from_path(event: Dict[str, Any]) -> Optional[str]:
|
|
|
418
403
|
# Remove leading slash and split by slash
|
|
419
404
|
path_parts = path.lstrip("/").split("/")
|
|
420
405
|
|
|
421
|
-
# Find
|
|
406
|
+
# Find literal path parts (ignoring params and common prefixes)
|
|
422
407
|
# For paths like "/comment/123" or "/comment/123/version/1"
|
|
423
|
-
# Skip common API prefixes like "api", "v1", etc.
|
|
424
408
|
api_prefixes = {"api", "v1", "v2", "v3"}
|
|
409
|
+
literal_parts = []
|
|
425
410
|
for part in path_parts:
|
|
426
|
-
if part
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
411
|
+
if not part or part.startswith("{"):
|
|
412
|
+
continue
|
|
413
|
+
if part in api_prefixes:
|
|
414
|
+
continue
|
|
415
|
+
# Skip numeric IDs and known path keywords
|
|
416
|
+
if part.isdigit() or part in {"version", "batch"}:
|
|
417
|
+
continue
|
|
418
|
+
literal_parts.append(part)
|
|
419
|
+
|
|
420
|
+
if not literal_parts:
|
|
421
|
+
return None
|
|
422
|
+
|
|
423
|
+
if len(literal_parts) == 1:
|
|
424
|
+
return literal_parts[0]
|
|
430
425
|
|
|
431
|
-
return
|
|
426
|
+
return "_".join(literal_parts)
|
|
432
427
|
|
|
433
428
|
|
|
434
|
-
def _scope_matches(
|
|
435
|
-
user_scopes: list, required_scope: str, operation: str, entity: str
|
|
436
|
-
) -> bool:
|
|
429
|
+
def _scope_matches(user_scopes: list, required_scope: str, operation: str, entity: str) -> bool:
|
|
437
430
|
"""Check if user scopes match the required scope."""
|
|
438
431
|
_ = entity # Silence unused warning
|
|
439
432
|
# Check for exact match
|
|
@@ -90,3 +90,10 @@ serve = "mkdocs serve --dev-addr localhost:8000"
|
|
|
90
90
|
|
|
91
91
|
[tool.hatch.publish.index]
|
|
92
92
|
disable = true # Disable publishing by default for safety
|
|
93
|
+
|
|
94
|
+
# Linting / formatting configuration
|
|
95
|
+
[tool.ruff]
|
|
96
|
+
line-length = 120
|
|
97
|
+
|
|
98
|
+
[tool.black]
|
|
99
|
+
line-length = 120
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.8.57"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|