api-foundry-query-engine 0.8.73__tar.gz → 0.8.74__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.73 → api_foundry_query_engine-0.8.74}/PKG-INFO +1 -1
- api_foundry_query_engine-0.8.74/api_foundry_query_engine/__init__.py +1 -0
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/dao/batch_operation_handler.py +33 -20
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/dao/operation_dao.py +17 -38
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/dao/sql_delete_query_handler.py +79 -10
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/dao/sql_restore_query_handler.py +77 -21
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/dao/sql_subselect_query_handler.py +14 -9
- api_foundry_query_engine-0.8.73/api_foundry_query_engine/__init__.py +0 -1
- api_foundry_query_engine-0.8.73/api_foundry_query_engine/adapters/case_change_adapter.py +0 -79
- api_foundry_query_engine-0.8.73/api_foundry_query_engine/adapters/security_adapter.py +0 -106
- api_foundry_query_engine-0.8.73/api_foundry_query_engine/connectors/oracle_connector.py +0 -29
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/.gitignore +0 -0
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/LICENSE +0 -0
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/.pre-commit-config.yaml +0 -0
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/adapters/adapter.py +0 -0
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/adapters/gateway_adapter.py +0 -0
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/connectors/connection.py +0 -0
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/connectors/connection_factory.py +0 -0
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/connectors/postgres_connection.py +0 -0
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/dao/dao.py +0 -0
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/dao/sql_custom_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/dao/sql_insert_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/dao/sql_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/dao/sql_select_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/dao/sql_update_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/lambda_handler.py +0 -0
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/operation.py +0 -0
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/services/service.py +0 -0
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/services/transactional_service.py +0 -0
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/utils/api_model.py +0 -0
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/utils/app_exception.py +0 -0
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/utils/claims_check.py +0 -0
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/utils/dependency_resolver.py +0 -0
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/utils/logger.py +0 -0
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/utils/reference_resolver.py +0 -0
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/utils/token_decoder.py +0 -0
- {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/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.
|
|
3
|
+
Version: 0.8.74
|
|
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.74"
|
|
@@ -19,7 +19,13 @@ log = logger(__name__)
|
|
|
19
19
|
class BatchOperationHandler:
|
|
20
20
|
"""Handles execution of batch operations with dependencies."""
|
|
21
21
|
|
|
22
|
-
def __init__(
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
batch_request: Dict[str, Any],
|
|
25
|
+
connection,
|
|
26
|
+
engine: str,
|
|
27
|
+
claims: Optional[Dict[str, Any]] = None,
|
|
28
|
+
):
|
|
23
29
|
"""
|
|
24
30
|
Initialize the batch operation handler.
|
|
25
31
|
|
|
@@ -27,11 +33,16 @@ class BatchOperationHandler:
|
|
|
27
33
|
batch_request: Batch request with 'operations' and 'options'
|
|
28
34
|
connection: Database connection for executing operations
|
|
29
35
|
engine: Database engine type (postgres, mysql, etc.)
|
|
36
|
+
claims: The authenticated caller's claims (roles, sub, etc.),
|
|
37
|
+
applied to every sub-operation in the batch. This must come
|
|
38
|
+
from the token-validated top-level request, never from the
|
|
39
|
+
batch request body.
|
|
30
40
|
"""
|
|
31
41
|
self.operations = batch_request.get("operations", [])
|
|
32
42
|
self.options = batch_request.get("options", {})
|
|
33
43
|
self.connection = connection
|
|
34
44
|
self.engine = engine
|
|
45
|
+
self.claims = claims or {}
|
|
35
46
|
self.results: Dict[str, Dict[str, Any]] = {}
|
|
36
47
|
self.failed_operations: List[str] = []
|
|
37
48
|
|
|
@@ -45,15 +56,12 @@ class BatchOperationHandler:
|
|
|
45
56
|
def _validate_batch_request(self):
|
|
46
57
|
"""Validate the batch request structure."""
|
|
47
58
|
if not self.operations:
|
|
48
|
-
raise ApplicationException(
|
|
49
|
-
400, "Batch request must contain at least one operation"
|
|
50
|
-
)
|
|
59
|
+
raise ApplicationException(400, "Batch request must contain at least one operation")
|
|
51
60
|
|
|
52
61
|
if len(self.operations) > 100:
|
|
53
62
|
raise ApplicationException(
|
|
54
63
|
400,
|
|
55
|
-
f"Batch size exceeds maximum (100). "
|
|
56
|
-
f"Requested: {len(self.operations)}",
|
|
64
|
+
f"Batch size exceeds maximum (100). " f"Requested: {len(self.operations)}",
|
|
57
65
|
)
|
|
58
66
|
|
|
59
67
|
# Auto-generate IDs for operations that don't have them
|
|
@@ -88,6 +96,18 @@ class BatchOperationHandler:
|
|
|
88
96
|
f"Operation '{op['id']}' has invalid action " f"'{op['action']}'",
|
|
89
97
|
)
|
|
90
98
|
|
|
99
|
+
# Sub-operations always run with the authenticated caller's
|
|
100
|
+
# claims (see __init__). A client-supplied 'claims' field would
|
|
101
|
+
# let a request forge its own roles/subject/tenant for a
|
|
102
|
+
# sub-operation, so reject it outright rather than silently
|
|
103
|
+
# ignoring it.
|
|
104
|
+
if "claims" in op:
|
|
105
|
+
raise ApplicationException(
|
|
106
|
+
400,
|
|
107
|
+
f"Operation '{op['id']}' must not specify 'claims'; "
|
|
108
|
+
"the authenticated caller's claims are always used",
|
|
109
|
+
)
|
|
110
|
+
|
|
91
111
|
def execute(self) -> Dict[str, Any]:
|
|
92
112
|
"""
|
|
93
113
|
Execute all operations in dependency order.
|
|
@@ -238,27 +258,20 @@ class BatchOperationHandler:
|
|
|
238
258
|
|
|
239
259
|
# Resolve references in parameters
|
|
240
260
|
ref_resolver = ReferenceResolver(self.results)
|
|
241
|
-
query_params = ref_resolver.resolve_parameters(
|
|
242
|
-
|
|
243
|
-
)
|
|
244
|
-
store_params = ref_resolver.resolve_parameters(
|
|
245
|
-
op_def.get("store_params", {}), op_id
|
|
246
|
-
)
|
|
247
|
-
metadata_params = ref_resolver.resolve_parameters(
|
|
248
|
-
op_def.get("metadata_params", {}), op_id
|
|
249
|
-
)
|
|
250
|
-
|
|
251
|
-
# Get claims from operation or use empty dict
|
|
252
|
-
claims = op_def.get("claims", {})
|
|
261
|
+
query_params = ref_resolver.resolve_parameters(op_def.get("query_params", {}), op_id)
|
|
262
|
+
store_params = ref_resolver.resolve_parameters(op_def.get("store_params", {}), op_id)
|
|
263
|
+
metadata_params = ref_resolver.resolve_parameters(op_def.get("metadata_params", {}), op_id)
|
|
253
264
|
|
|
254
|
-
# Create Operation object
|
|
265
|
+
# Create Operation object. Always use the authenticated batch
|
|
266
|
+
# caller's claims (self.claims) - never anything from the request
|
|
267
|
+
# body - so a sub-operation cannot run with forged roles/subject.
|
|
255
268
|
operation = Operation(
|
|
256
269
|
entity=op_def["entity"],
|
|
257
270
|
action=op_def["action"],
|
|
258
271
|
query_params=query_params,
|
|
259
272
|
store_params=store_params,
|
|
260
273
|
metadata_params=metadata_params,
|
|
261
|
-
claims=claims,
|
|
274
|
+
claims=dict(self.claims),
|
|
262
275
|
)
|
|
263
276
|
|
|
264
277
|
log.debug(
|
|
@@ -53,44 +53,26 @@ class OperationDAO(DAO):
|
|
|
53
53
|
@property
|
|
54
54
|
def query_handler(self) -> SQLQueryHandler:
|
|
55
55
|
if not hasattr(self, "_query_handler"):
|
|
56
|
-
path_operation = get_path_operation(
|
|
57
|
-
self.operation.entity, self.operation.action
|
|
58
|
-
)
|
|
56
|
+
path_operation = get_path_operation(self.operation.entity, self.operation.action)
|
|
59
57
|
if path_operation:
|
|
60
|
-
self._query_handler = SQLCustomQueryHandler(
|
|
61
|
-
self.operation, path_operation, self.engine
|
|
62
|
-
)
|
|
58
|
+
self._query_handler = SQLCustomQueryHandler(self.operation, path_operation, self.engine)
|
|
63
59
|
return self._query_handler
|
|
64
60
|
|
|
65
61
|
schema_object = get_schema_object(self.operation.entity)
|
|
66
62
|
if not schema_object:
|
|
67
|
-
raise ApplicationException(
|
|
68
|
-
500, f"Unknown operation: {self.operation.entity}"
|
|
69
|
-
)
|
|
63
|
+
raise ApplicationException(500, f"Unknown operation: {self.operation.entity}")
|
|
70
64
|
if self.operation.action == "read":
|
|
71
|
-
self._query_handler = SQLSelectSchemaQueryHandler(
|
|
72
|
-
self.operation, schema_object, self.engine
|
|
73
|
-
)
|
|
65
|
+
self._query_handler = SQLSelectSchemaQueryHandler(self.operation, schema_object, self.engine)
|
|
74
66
|
elif self.operation.action == "create":
|
|
75
|
-
self._query_handler = SQLInsertSchemaQueryHandler(
|
|
76
|
-
self.operation, schema_object, self.engine
|
|
77
|
-
)
|
|
67
|
+
self._query_handler = SQLInsertSchemaQueryHandler(self.operation, schema_object, self.engine)
|
|
78
68
|
elif self.operation.action == "update":
|
|
79
|
-
self._query_handler = SQLUpdateSchemaQueryHandler(
|
|
80
|
-
self.operation, schema_object, self.engine
|
|
81
|
-
)
|
|
69
|
+
self._query_handler = SQLUpdateSchemaQueryHandler(self.operation, schema_object, self.engine)
|
|
82
70
|
elif self.operation.action == "delete":
|
|
83
|
-
self._query_handler = SQLDeleteSchemaQueryHandler(
|
|
84
|
-
self.operation, schema_object, self.engine
|
|
85
|
-
)
|
|
71
|
+
self._query_handler = SQLDeleteSchemaQueryHandler(self.operation, schema_object, self.engine)
|
|
86
72
|
elif self.operation.action == "restore":
|
|
87
|
-
self._query_handler = SQLRestoreSchemaQueryHandler(
|
|
88
|
-
self.operation, schema_object, self.engine
|
|
89
|
-
)
|
|
73
|
+
self._query_handler = SQLRestoreSchemaQueryHandler(self.operation, schema_object, self.engine)
|
|
90
74
|
else:
|
|
91
|
-
raise ApplicationException(
|
|
92
|
-
400, f"Invalid operation action: {self.operation.action}"
|
|
93
|
-
)
|
|
75
|
+
raise ApplicationException(400, f"Invalid operation action: {self.operation.action}")
|
|
94
76
|
return self._query_handler
|
|
95
77
|
|
|
96
78
|
def execute(self, connector, operation=None) -> Union[list[dict], dict]:
|
|
@@ -118,8 +100,11 @@ class OperationDAO(DAO):
|
|
|
118
100
|
# Extract batch request from store_params
|
|
119
101
|
batch_request = op.store_params
|
|
120
102
|
|
|
121
|
-
# Execute batch
|
|
122
|
-
|
|
103
|
+
# Execute batch. The authenticated caller's claims (op.claims)
|
|
104
|
+
# are passed explicitly so every sub-operation runs with the
|
|
105
|
+
# real caller's identity/roles, never anything from the batch
|
|
106
|
+
# request body.
|
|
107
|
+
handler = BatchOperationHandler(batch_request, connector, self.engine, claims=op.claims)
|
|
123
108
|
return handler.execute()
|
|
124
109
|
|
|
125
110
|
# Standard operation handling
|
|
@@ -147,9 +132,7 @@ class OperationDAO(DAO):
|
|
|
147
132
|
continue
|
|
148
133
|
|
|
149
134
|
child_set = self.__fetch_record_set(
|
|
150
|
-
SQLSubselectSchemaQueryHandler(
|
|
151
|
-
self.operation, relation, self.query_handler # type: ignore
|
|
152
|
-
),
|
|
135
|
+
SQLSubselectSchemaQueryHandler(self.operation, relation, self.query_handler), # type: ignore
|
|
153
136
|
cursor,
|
|
154
137
|
)
|
|
155
138
|
if len(child_set) == 0:
|
|
@@ -168,16 +151,12 @@ class OperationDAO(DAO):
|
|
|
168
151
|
if parent:
|
|
169
152
|
parent[name].append(child)
|
|
170
153
|
|
|
171
|
-
def __fetch_record_set(
|
|
172
|
-
self, query_handler: SQLQueryHandler, cursor: Cursor
|
|
173
|
-
) -> list[dict]:
|
|
154
|
+
def __fetch_record_set(self, query_handler: SQLQueryHandler, cursor: Cursor) -> list[dict]:
|
|
174
155
|
sql = query_handler.sql
|
|
175
156
|
if not sql:
|
|
176
157
|
return []
|
|
177
158
|
|
|
178
|
-
record_set = cursor.execute(
|
|
179
|
-
sql, query_handler.placeholders, query_handler.selection_results
|
|
180
|
-
)
|
|
159
|
+
record_set = cursor.execute(sql, query_handler.placeholders, query_handler.selection_results)
|
|
181
160
|
result = []
|
|
182
161
|
for record in record_set:
|
|
183
162
|
object = query_handler.marshal_record(record)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Match
|
|
1
3
|
from api_foundry_query_engine.dao.sql_query_handler import SQLSchemaQueryHandler
|
|
2
4
|
from api_foundry_query_engine.operation import Operation
|
|
3
5
|
from api_foundry_query_engine.utils.app_exception import ApplicationException
|
|
@@ -8,11 +10,83 @@ log = logger(__name__)
|
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
class SQLDeleteSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
11
|
-
def __init__(
|
|
12
|
-
self, operation: Operation, schema_object: SchemaObject, engine: str
|
|
13
|
-
) -> None:
|
|
13
|
+
def __init__(self, operation: Operation, schema_object: SchemaObject, engine: str) -> None:
|
|
14
14
|
super().__init__(operation, schema_object, engine)
|
|
15
15
|
|
|
16
|
+
def _template_where(self, expr: str) -> str:
|
|
17
|
+
"""Template substitution for WHERE clause expressions with claim values."""
|
|
18
|
+
if not expr:
|
|
19
|
+
return expr
|
|
20
|
+
|
|
21
|
+
def _quote(val: object) -> str:
|
|
22
|
+
if val is None:
|
|
23
|
+
return "NULL"
|
|
24
|
+
if isinstance(val, (int, float)):
|
|
25
|
+
return str(val)
|
|
26
|
+
s = str(val).replace("'", "''")
|
|
27
|
+
return f"'{s}'"
|
|
28
|
+
|
|
29
|
+
def _replace(m: Match[str]) -> str:
|
|
30
|
+
key = m.group(1)
|
|
31
|
+
return _quote(self.extract_injected_value(f"claim:{key}"))
|
|
32
|
+
|
|
33
|
+
return re.sub(r"\$\{claims\.([A-Za-z0-9_]+)\}", _replace, expr)
|
|
34
|
+
|
|
35
|
+
def _row_where_clause(self) -> str:
|
|
36
|
+
"""
|
|
37
|
+
Generate row-level WHERE clause based on delete permissions, so a
|
|
38
|
+
role's delete grant is scoped to the rows the permission model
|
|
39
|
+
allows (e.g. tenant/ownership), the same way select and update
|
|
40
|
+
already are.
|
|
41
|
+
"""
|
|
42
|
+
perms = getattr(self.schema_object, "permissions", None) or {}
|
|
43
|
+
if "default" in perms:
|
|
44
|
+
provider = perms.get("default", {}) or {}
|
|
45
|
+
delete_map = provider.get("delete", {}) or {}
|
|
46
|
+
role_permissions = perms.get("default", {})
|
|
47
|
+
else:
|
|
48
|
+
delete_map = {}
|
|
49
|
+
role_permissions = perms
|
|
50
|
+
for role, role_perms in perms.items():
|
|
51
|
+
if isinstance(role_perms, dict):
|
|
52
|
+
delete_map[role] = role_perms.get("delete")
|
|
53
|
+
|
|
54
|
+
filters = []
|
|
55
|
+
for role in self.operation.roles or []:
|
|
56
|
+
role_where = None
|
|
57
|
+
if isinstance(role_permissions.get(role), dict):
|
|
58
|
+
role_where = role_permissions[role].get("where")
|
|
59
|
+
|
|
60
|
+
operation_where = None
|
|
61
|
+
rule = delete_map.get(role)
|
|
62
|
+
if isinstance(rule, dict):
|
|
63
|
+
operation_where = rule.get("where")
|
|
64
|
+
|
|
65
|
+
where_clause = operation_where if operation_where else role_where
|
|
66
|
+
|
|
67
|
+
if isinstance(where_clause, str) and where_clause.strip():
|
|
68
|
+
filters.append(self._template_where(where_clause))
|
|
69
|
+
|
|
70
|
+
if not filters:
|
|
71
|
+
return ""
|
|
72
|
+
return "(" + ") OR (".join(filters) + ")"
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def search_condition(self) -> str:
|
|
76
|
+
"""
|
|
77
|
+
Add permission-based row-level WHERE clause on top of the base
|
|
78
|
+
query-param search condition, matching SQLUpdateSchemaQueryHandler.
|
|
79
|
+
"""
|
|
80
|
+
base_condition = super().search_condition
|
|
81
|
+
row_filter = self._row_where_clause()
|
|
82
|
+
|
|
83
|
+
if base_condition and row_filter:
|
|
84
|
+
return f"{base_condition} AND ({row_filter})"
|
|
85
|
+
elif row_filter:
|
|
86
|
+
return f" WHERE ({row_filter})"
|
|
87
|
+
else:
|
|
88
|
+
return base_condition
|
|
89
|
+
|
|
16
90
|
def check_permission(self) -> bool:
|
|
17
91
|
"""
|
|
18
92
|
Checks the user's permissions for the specified permission type.
|
|
@@ -108,9 +182,7 @@ class SQLDeleteSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
108
182
|
@property
|
|
109
183
|
def sql(self) -> str:
|
|
110
184
|
if not self.check_permission():
|
|
111
|
-
raise ApplicationException(
|
|
112
|
-
403, f"Subject is not allowed to delete {self.schema_object.api_name}"
|
|
113
|
-
)
|
|
185
|
+
raise ApplicationException(403, f"Subject is not allowed to delete {self.schema_object.api_name}")
|
|
114
186
|
|
|
115
187
|
concurrency_property = self.schema_object.concurrency_property
|
|
116
188
|
if concurrency_property:
|
|
@@ -139,7 +211,4 @@ class SQLDeleteSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
139
211
|
)
|
|
140
212
|
else:
|
|
141
213
|
# Fall back to hard delete for tables without soft delete support
|
|
142
|
-
return
|
|
143
|
-
f"DELETE FROM {self.table_expression}{self.search_condition} "
|
|
144
|
-
+ f"RETURNING {self.select_list}"
|
|
145
|
-
)
|
|
214
|
+
return f"DELETE FROM {self.table_expression}{self.search_condition} " + f"RETURNING {self.select_list}"
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Match
|
|
1
3
|
from api_foundry_query_engine.dao.sql_query_handler import (
|
|
2
4
|
SQLSchemaQueryHandler,
|
|
3
5
|
)
|
|
@@ -12,9 +14,7 @@ log = logger(__name__)
|
|
|
12
14
|
class SQLRestoreSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
13
15
|
"""Handler for restoring soft-deleted records."""
|
|
14
16
|
|
|
15
|
-
def __init__(
|
|
16
|
-
self, operation: Operation, schema_object: SchemaObject, engine: str
|
|
17
|
-
) -> None:
|
|
17
|
+
def __init__(self, operation: Operation, schema_object: SchemaObject, engine: str) -> None:
|
|
18
18
|
super().__init__(operation, schema_object, engine)
|
|
19
19
|
|
|
20
20
|
def check_permission(self) -> bool:
|
|
@@ -42,9 +42,7 @@ class SQLRestoreSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
42
42
|
|
|
43
43
|
# If still not found, check wildcard "*"
|
|
44
44
|
if role_permissions is None:
|
|
45
|
-
role_permissions = restore_permissions.get(
|
|
46
|
-
"*"
|
|
47
|
-
) or write_permissions.get("*")
|
|
45
|
+
role_permissions = restore_permissions.get("*") or write_permissions.get("*")
|
|
48
46
|
log.info("Fallback to wildcard role '*': %s", role_permissions)
|
|
49
47
|
if role_permissions is None:
|
|
50
48
|
continue
|
|
@@ -61,6 +59,67 @@ class SQLRestoreSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
61
59
|
|
|
62
60
|
return False
|
|
63
61
|
|
|
62
|
+
def _template_where(self, expr: str) -> str:
|
|
63
|
+
"""Template substitution for WHERE clause expressions with claim values."""
|
|
64
|
+
if not expr:
|
|
65
|
+
return expr
|
|
66
|
+
|
|
67
|
+
def _quote(val: object) -> str:
|
|
68
|
+
if val is None:
|
|
69
|
+
return "NULL"
|
|
70
|
+
if isinstance(val, (int, float)):
|
|
71
|
+
return str(val)
|
|
72
|
+
s = str(val).replace("'", "''")
|
|
73
|
+
return f"'{s}'"
|
|
74
|
+
|
|
75
|
+
def _replace(m: Match[str]) -> str:
|
|
76
|
+
key = m.group(1)
|
|
77
|
+
return _quote(self.extract_injected_value(f"claim:{key}"))
|
|
78
|
+
|
|
79
|
+
return re.sub(r"\$\{claims\.([A-Za-z0-9_]+)\}", _replace, expr)
|
|
80
|
+
|
|
81
|
+
def _row_where_clause(self) -> str:
|
|
82
|
+
"""
|
|
83
|
+
Generate row-level WHERE clause based on restore permissions
|
|
84
|
+
(falling back to write permissions, matching check_permission's own
|
|
85
|
+
restore->write fallback), so a role's restore grant is scoped to
|
|
86
|
+
the rows the permission model allows.
|
|
87
|
+
"""
|
|
88
|
+
perms = getattr(self.schema_object, "permissions", None) or {}
|
|
89
|
+
if "default" in perms:
|
|
90
|
+
provider = perms.get("default", {}) or {}
|
|
91
|
+
restore_map = provider.get("restore", {}) or {}
|
|
92
|
+
write_map = provider.get("write", {}) or {}
|
|
93
|
+
role_permissions = perms.get("default", {})
|
|
94
|
+
else:
|
|
95
|
+
restore_map = {}
|
|
96
|
+
write_map = {}
|
|
97
|
+
role_permissions = perms
|
|
98
|
+
for role, role_perms in perms.items():
|
|
99
|
+
if isinstance(role_perms, dict):
|
|
100
|
+
restore_map[role] = role_perms.get("restore")
|
|
101
|
+
write_map[role] = role_perms.get("write")
|
|
102
|
+
|
|
103
|
+
filters = []
|
|
104
|
+
for role in self.operation.roles or []:
|
|
105
|
+
role_where = None
|
|
106
|
+
if isinstance(role_permissions.get(role), dict):
|
|
107
|
+
role_where = role_permissions[role].get("where")
|
|
108
|
+
|
|
109
|
+
operation_where = None
|
|
110
|
+
rule = restore_map.get(role) or write_map.get(role)
|
|
111
|
+
if isinstance(rule, dict):
|
|
112
|
+
operation_where = rule.get("where")
|
|
113
|
+
|
|
114
|
+
where_clause = operation_where if operation_where else role_where
|
|
115
|
+
|
|
116
|
+
if isinstance(where_clause, str) and where_clause.strip():
|
|
117
|
+
filters.append(self._template_where(where_clause))
|
|
118
|
+
|
|
119
|
+
if not filters:
|
|
120
|
+
return ""
|
|
121
|
+
return "(" + ") OR (".join(filters) + ")"
|
|
122
|
+
|
|
64
123
|
def _get_restore_update_values(self) -> str:
|
|
65
124
|
"""Generate SET clause for restore operation."""
|
|
66
125
|
self.store_placeholders = {}
|
|
@@ -114,14 +173,10 @@ class SQLRestoreSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
114
173
|
|
|
115
174
|
for name, value in self.operation.query_params.items():
|
|
116
175
|
if "." in name:
|
|
117
|
-
raise ApplicationException(
|
|
118
|
-
400, "Selection on relations is not supported"
|
|
119
|
-
)
|
|
176
|
+
raise ApplicationException(400, "Selection on relations is not supported")
|
|
120
177
|
prop = self.schema_object.properties.get(name)
|
|
121
178
|
if not prop:
|
|
122
|
-
raise ApplicationException(
|
|
123
|
-
500, f"Search condition column not found {name}"
|
|
124
|
-
)
|
|
179
|
+
raise ApplicationException(500, f"Search condition column not found {name}")
|
|
125
180
|
|
|
126
181
|
assignment, holders = self.search_value_assignment(prop, value)
|
|
127
182
|
conditions.append(assignment)
|
|
@@ -132,6 +187,12 @@ class SQLRestoreSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
132
187
|
if soft_delete_conditions:
|
|
133
188
|
conditions.append(f"({soft_delete_conditions})")
|
|
134
189
|
|
|
190
|
+
# Add row-level permission filter, scoping which rows a role may
|
|
191
|
+
# restore (e.g. tenant/ownership), matching update/delete.
|
|
192
|
+
row_filter = self._row_where_clause()
|
|
193
|
+
if row_filter:
|
|
194
|
+
conditions.append(f"({row_filter})")
|
|
195
|
+
|
|
135
196
|
return f" WHERE {' AND '.join(conditions)}" if conditions else ""
|
|
136
197
|
|
|
137
198
|
def _get_soft_delete_restore_conditions(self) -> str:
|
|
@@ -157,8 +218,7 @@ class SQLRestoreSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
157
218
|
if excluded_values:
|
|
158
219
|
# For restore, we want records that ARE in the excluded values
|
|
159
220
|
formatted_values = ", ".join(
|
|
160
|
-
f"'{val}'" if isinstance(val, str) else str(val)
|
|
161
|
-
for val in excluded_values
|
|
221
|
+
f"'{val}'" if isinstance(val, str) else str(val) for val in excluded_values
|
|
162
222
|
)
|
|
163
223
|
conditions.append(f"{column_name} IN ({formatted_values})")
|
|
164
224
|
|
|
@@ -177,17 +237,13 @@ class SQLRestoreSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
177
237
|
if not self.schema_object.has_soft_delete_support():
|
|
178
238
|
raise ApplicationException(
|
|
179
239
|
400,
|
|
180
|
-
f"Schema object {self.schema_object.api_name} does not support "
|
|
181
|
-
+ "soft delete operations",
|
|
240
|
+
f"Schema object {self.schema_object.api_name} does not support " + "soft delete operations",
|
|
182
241
|
)
|
|
183
242
|
|
|
184
243
|
update_clause = self._get_restore_update_values()
|
|
185
244
|
if not update_clause:
|
|
186
|
-
raise ApplicationException(
|
|
187
|
-
400, f"No restore fields available for {self.schema_object.api_name}"
|
|
188
|
-
)
|
|
245
|
+
raise ApplicationException(400, f"No restore fields available for {self.schema_object.api_name}")
|
|
189
246
|
|
|
190
247
|
return (
|
|
191
|
-
f"UPDATE {self.table_expression}{update_clause}"
|
|
192
|
-
+ f"{self.search_condition} RETURNING {self.select_list}"
|
|
248
|
+
f"UPDATE {self.table_expression}{update_clause}" + f"{self.search_condition} RETURNING {self.select_list}"
|
|
193
249
|
)
|
|
@@ -15,9 +15,7 @@ class SQLSubselectSchemaQueryHandler(SQLSelectSchemaQueryHandler):
|
|
|
15
15
|
relation: SchemaObjectAssociation,
|
|
16
16
|
parent_generator: SQLSchemaQueryHandler,
|
|
17
17
|
) -> None:
|
|
18
|
-
super().__init__(
|
|
19
|
-
operation, relation.child_schema_object, parent_generator.engine
|
|
20
|
-
)
|
|
18
|
+
super().__init__(operation, relation.child_schema_object, parent_generator.engine)
|
|
21
19
|
self.relation = relation
|
|
22
20
|
self.parent_generator = parent_generator
|
|
23
21
|
|
|
@@ -32,11 +30,20 @@ class SQLSubselectSchemaQueryHandler(SQLSelectSchemaQueryHandler):
|
|
|
32
30
|
|
|
33
31
|
schema_object = self.relation.child_schema_object
|
|
34
32
|
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
# Enforce read permissions on the associated entity's
|
|
34
|
+
# properties the same way the top-level select handler does.
|
|
35
|
+
# Without this, a property restricted at the top level (e.g.
|
|
36
|
+
# a field limited to a privileged role) would leak through
|
|
37
|
+
# when fetched as a nested one-to-many association.
|
|
38
|
+
allowed_properties = self.check_permissions(
|
|
39
|
+
"read",
|
|
40
|
+
schema_object.permissions,
|
|
41
|
+
schema_object.properties,
|
|
38
42
|
)
|
|
39
43
|
|
|
44
|
+
# Filter and prefix keys for the current entity and regular expressions
|
|
45
|
+
filtered_keys = self.filter_and_prefix_keys(reg_exs, allowed_properties)
|
|
46
|
+
|
|
40
47
|
# Extend the result map with the filtered keys
|
|
41
48
|
result.update(filtered_keys)
|
|
42
49
|
|
|
@@ -51,9 +58,7 @@ class SQLSubselectSchemaQueryHandler(SQLSelectSchemaQueryHandler):
|
|
|
51
58
|
if len(self.select_list_columns) == 1: # then it only contains the key
|
|
52
59
|
return None
|
|
53
60
|
|
|
54
|
-
parent_prefix = self.parent_generator.prefix_map[
|
|
55
|
-
str(self.parent_generator.schema_object.api_name)
|
|
56
|
-
]
|
|
61
|
+
parent_prefix = self.parent_generator.prefix_map[str(self.parent_generator.schema_object.api_name)]
|
|
57
62
|
sql = (
|
|
58
63
|
f"SELECT {self.select_list} "
|
|
59
64
|
+ f"FROM {self.relation.child_schema_object.qualified_name} "
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.8.73"
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
from humps import camelize, decamelize
|
|
2
|
-
|
|
3
|
-
from api_foundry_query_engine.adapters.adapter import Adapter
|
|
4
|
-
from api_foundry_query_engine.utils.logger import logger
|
|
5
|
-
from api_foundry_query_engine.operation import Operation
|
|
6
|
-
|
|
7
|
-
log = logger(__name__)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class CaseChangeAdapter(Adapter):
|
|
11
|
-
"""
|
|
12
|
-
Handles changing case from snake to camel and back
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
def unmarshal(self, event) -> Operation:
|
|
16
|
-
"""
|
|
17
|
-
Unmarshal the event in a tuple for processing
|
|
18
|
-
|
|
19
|
-
Parameters:
|
|
20
|
-
- event (dict): Lambda event object.
|
|
21
|
-
|
|
22
|
-
Returns:
|
|
23
|
-
- tuple: Tuple containing entity operation, store_params, query_params and metadata params.
|
|
24
|
-
"""
|
|
25
|
-
operation = super().unmarshal(event)
|
|
26
|
-
|
|
27
|
-
# determine case
|
|
28
|
-
self.camel_case = (
|
|
29
|
-
operation.metadata_params.get("_case", "snake") == "camel"
|
|
30
|
-
or self.__check_camel_case(operation.store_params)
|
|
31
|
-
or self.__check_camel_case(operation.query_params)
|
|
32
|
-
)
|
|
33
|
-
log.info("camel_case: %s", self.camel_case)
|
|
34
|
-
|
|
35
|
-
if self.camel_case:
|
|
36
|
-
return Operation(
|
|
37
|
-
path=operation.entity,
|
|
38
|
-
action=operation.action,
|
|
39
|
-
store_params=decamelize(operation.store_params),
|
|
40
|
-
query_params=decamelize(operation.query_params),
|
|
41
|
-
metadata_params=operation.metadata_params,
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
return operation
|
|
45
|
-
|
|
46
|
-
def marshal(self, result: list[dict]):
|
|
47
|
-
"""
|
|
48
|
-
Marshal the result into a event response
|
|
49
|
-
|
|
50
|
-
Parameters:
|
|
51
|
-
- result (list): the data set to return in the response
|
|
52
|
-
|
|
53
|
-
Returns:
|
|
54
|
-
- the event response
|
|
55
|
-
"""
|
|
56
|
-
super().marshal(result)
|
|
57
|
-
|
|
58
|
-
if not self.camel_case:
|
|
59
|
-
return result
|
|
60
|
-
|
|
61
|
-
converted_result = []
|
|
62
|
-
for item in result:
|
|
63
|
-
converted_result.append(camelize(item))
|
|
64
|
-
|
|
65
|
-
# convert back to camel case if needed
|
|
66
|
-
return converted_result
|
|
67
|
-
|
|
68
|
-
def __check_camel_case(self, params: dict) -> bool:
|
|
69
|
-
if params is not None:
|
|
70
|
-
# check the keys for an upper case character
|
|
71
|
-
for param in params:
|
|
72
|
-
if (
|
|
73
|
-
param != param.lower()
|
|
74
|
-
and param != param.upper()
|
|
75
|
-
and "_" not in param
|
|
76
|
-
):
|
|
77
|
-
return True
|
|
78
|
-
|
|
79
|
-
return False
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
from typing import Optional
|
|
2
|
-
|
|
3
|
-
from api_foundry_query_engine.operation import Operation
|
|
4
|
-
from api_foundry_query_engine.services.service import Service
|
|
5
|
-
from api_foundry_query_engine.adapters.adapter import Adapter
|
|
6
|
-
from api_foundry_query_engine.utils.logger import logger
|
|
7
|
-
|
|
8
|
-
log = logger(__name__)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class SecurityAdapter(Adapter):
|
|
12
|
-
def __init__(self, service: Optional[Service] = None, permissions: dict = None):
|
|
13
|
-
"""
|
|
14
|
-
Initialize SecurityAdapter with a service and permissions.
|
|
15
|
-
|
|
16
|
-
Parameters:
|
|
17
|
-
- service (Service): The service used to process operations.
|
|
18
|
-
- permissions (dict): A dictionary containing the user's permissions,
|
|
19
|
-
structured as:
|
|
20
|
-
{
|
|
21
|
-
"read": ["field1", "field2", ...],
|
|
22
|
-
"write": ["field3", "field4", ...]
|
|
23
|
-
}
|
|
24
|
-
"""
|
|
25
|
-
super().__init__(service)
|
|
26
|
-
self.permissions = permissions or {"read": [], "write": []}
|
|
27
|
-
|
|
28
|
-
def unmarshal(self, event) -> Operation:
|
|
29
|
-
"""
|
|
30
|
-
Unmarshal the event into an Operation object after validating query and store params.
|
|
31
|
-
|
|
32
|
-
Parameters:
|
|
33
|
-
- event (dict): Lambda event object.
|
|
34
|
-
|
|
35
|
-
Returns:
|
|
36
|
-
- Operation: The validated operation.
|
|
37
|
-
"""
|
|
38
|
-
entity = event.get("entity")
|
|
39
|
-
action = event.get("action")
|
|
40
|
-
query_params = event.get("query_params", {})
|
|
41
|
-
store_params = event.get("store_params", {})
|
|
42
|
-
|
|
43
|
-
# Validate read permissions for query_params
|
|
44
|
-
invalid_query_params = [
|
|
45
|
-
key for key in query_params if key not in self.permissions["read"]
|
|
46
|
-
]
|
|
47
|
-
if invalid_query_params:
|
|
48
|
-
raise PermissionError(
|
|
49
|
-
f"Query parameters not permitted: {invalid_query_params}"
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
# Validate write permissions for store_params
|
|
53
|
-
invalid_store_params = [
|
|
54
|
-
key for key in store_params if key not in self.permissions["write"]
|
|
55
|
-
]
|
|
56
|
-
if invalid_store_params:
|
|
57
|
-
raise PermissionError(
|
|
58
|
-
f"Store parameters not permitted: {invalid_store_params}"
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
return Operation(
|
|
62
|
-
entity=entity,
|
|
63
|
-
action=action,
|
|
64
|
-
query_params=query_params,
|
|
65
|
-
store_params=store_params,
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
def marshal(self, result: list[dict]):
|
|
69
|
-
"""
|
|
70
|
-
Filter the result based on read permissions before returning.
|
|
71
|
-
|
|
72
|
-
Parameters:
|
|
73
|
-
- result (list[dict]): The data set to return in the response.
|
|
74
|
-
|
|
75
|
-
Returns:
|
|
76
|
-
- list[dict]: Filtered response with only permitted fields.
|
|
77
|
-
"""
|
|
78
|
-
filtered_result = []
|
|
79
|
-
for record in result:
|
|
80
|
-
filtered_record = {
|
|
81
|
-
key: value
|
|
82
|
-
for key, value in record.items()
|
|
83
|
-
if key in self.permissions["read"]
|
|
84
|
-
}
|
|
85
|
-
filtered_result.append(filtered_record)
|
|
86
|
-
|
|
87
|
-
return filtered_result
|
|
88
|
-
|
|
89
|
-
def process_event(self, event):
|
|
90
|
-
"""
|
|
91
|
-
Process Lambda event using a domain function.
|
|
92
|
-
|
|
93
|
-
Parameters:
|
|
94
|
-
- event (dict): Lambda event object.
|
|
95
|
-
|
|
96
|
-
Returns:
|
|
97
|
-
- any: Result of the domain function.
|
|
98
|
-
"""
|
|
99
|
-
try:
|
|
100
|
-
operation = self.unmarshal(event)
|
|
101
|
-
result = self.service.execute(operation)
|
|
102
|
-
log.debug("adapter result: %s", result)
|
|
103
|
-
return self.marshal(result)
|
|
104
|
-
except PermissionError as e:
|
|
105
|
-
log.error("Permission error: %s", e)
|
|
106
|
-
return {"error": str(e), "status": "permission_denied"}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
from api_foundry_query_engine.utils.logger import logger
|
|
2
|
-
from api_foundry_query_engine.utils.app_exception import ApplicationException
|
|
3
|
-
from api_foundry_query_engine.connectors.connection import Connector
|
|
4
|
-
|
|
5
|
-
log = logger(__name__)
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class OracleConnnector(Connector):
|
|
9
|
-
def __init__(self, db_secret_name: str) -> None:
|
|
10
|
-
super().__init__(db_secret_name)
|
|
11
|
-
|
|
12
|
-
def close(self):
|
|
13
|
-
pass
|
|
14
|
-
|
|
15
|
-
def execute(self, cursor, sql: str, parameters: dict):
|
|
16
|
-
from oracledb import Error, IntegrityError, ProgrammingError
|
|
17
|
-
|
|
18
|
-
log.debug("sql: %s, parameters: %s", sql, parameters)
|
|
19
|
-
try:
|
|
20
|
-
cursor.execute(sql, parameters)
|
|
21
|
-
except IntegrityError as err:
|
|
22
|
-
(error,) = err.args
|
|
23
|
-
raise ApplicationException(409, error.message)
|
|
24
|
-
except ProgrammingError as err:
|
|
25
|
-
(error,) = err.args
|
|
26
|
-
raise ApplicationException(400, error.message)
|
|
27
|
-
except Error as err:
|
|
28
|
-
(error,) = err.args
|
|
29
|
-
raise ApplicationException(500, error.message)
|
|
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
|