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.
Files changed (37) hide show
  1. {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/PKG-INFO +1 -1
  2. api_foundry_query_engine-0.8.74/api_foundry_query_engine/__init__.py +1 -0
  3. {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
  4. {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/dao/operation_dao.py +17 -38
  5. {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
  6. {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
  7. {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
  8. api_foundry_query_engine-0.8.73/api_foundry_query_engine/__init__.py +0 -1
  9. api_foundry_query_engine-0.8.73/api_foundry_query_engine/adapters/case_change_adapter.py +0 -79
  10. api_foundry_query_engine-0.8.73/api_foundry_query_engine/adapters/security_adapter.py +0 -106
  11. api_foundry_query_engine-0.8.73/api_foundry_query_engine/connectors/oracle_connector.py +0 -29
  12. {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/.gitignore +0 -0
  13. {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/LICENSE +0 -0
  14. {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/.pre-commit-config.yaml +0 -0
  15. {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/adapters/adapter.py +0 -0
  16. {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/adapters/gateway_adapter.py +0 -0
  17. {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/connectors/connection.py +0 -0
  18. {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/connectors/connection_factory.py +0 -0
  19. {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/connectors/postgres_connection.py +0 -0
  20. {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/dao/dao.py +0 -0
  21. {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
  22. {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
  23. {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
  24. {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
  25. {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
  26. {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/lambda_handler.py +0 -0
  27. {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/operation.py +0 -0
  28. {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/services/service.py +0 -0
  29. {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/services/transactional_service.py +0 -0
  30. {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/utils/api_model.py +0 -0
  31. {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/utils/app_exception.py +0 -0
  32. {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/utils/claims_check.py +0 -0
  33. {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/utils/dependency_resolver.py +0 -0
  34. {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/utils/logger.py +0 -0
  35. {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/utils/reference_resolver.py +0 -0
  36. {api_foundry_query_engine-0.8.73 → api_foundry_query_engine-0.8.74}/api_foundry_query_engine/utils/token_decoder.py +0 -0
  37. {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.73
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__(self, batch_request: Dict[str, Any], connection, engine: str):
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
- op_def.get("query_params", {}), op_id
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
- handler = BatchOperationHandler(batch_request, connector, self.engine)
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
- # Filter and prefix keys for the current entity and regular expressions
36
- filtered_keys = self.filter_and_prefix_keys(
37
- reg_exs, schema_object.properties
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)