api-foundry-query-engine 0.0.4__tar.gz → 0.0.6__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.0.4 → api_foundry_query_engine-0.0.6}/PKG-INFO +3 -2
- {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/adapters/gateway_adapter.py +8 -0
- api_foundry_query_engine-0.0.6/api_foundry_query_engine/adapters/security_adapter.py +106 -0
- api_foundry_query_engine-0.0.6/api_foundry_query_engine/connectors/athena_connector.py +120 -0
- {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/connectors/connection_factory.py +2 -3
- {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/connectors/postgres_connection.py +2 -3
- {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/dao/operation_dao.py +19 -6
- {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/dao/sql_custom_query_handler.py +8 -6
- {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/dao/sql_delete_query_handler.py +35 -0
- {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/dao/sql_insert_query_handler.py +18 -3
- {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/dao/sql_query_handler.py +87 -22
- {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/dao/sql_select_query_handler.py +60 -47
- {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/dao/sql_subselect_query_handler.py +6 -3
- {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/dao/sql_update_query_handler.py +21 -10
- {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/lambda_handler.py +0 -1
- api_foundry_query_engine-0.0.6/api_foundry_query_engine/operation.py +59 -0
- api_foundry_query_engine-0.0.6/api_foundry_query_engine/services/security_service.py +105 -0
- {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/services/transactional_service.py +7 -2
- {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/utils/api_model.py +26 -10
- {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/pyproject.toml +1 -2
- api_foundry_query_engine-0.0.4/api_foundry_query_engine/operation.py +0 -15
- {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/.gitignore +0 -0
- {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/LICENSE +0 -0
- {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/__init__.py +0 -0
- {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/adapters/adapter.py +0 -0
- {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/adapters/case_change_adapter.py +0 -0
- {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/connectors/connection.py +0 -0
- {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/connectors/oracle_connector.py +0 -0
- {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/dao/dao.py +0 -0
- {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/services/service.py +0 -0
- {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/utils/app_exception.py +0 -0
- {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/utils/logger.py +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: api-foundry-query-engine
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.6
|
|
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
|
|
7
7
|
Author-email: Daniel Repik <danrepik@icloud.com>
|
|
8
|
+
License-File: LICENSE
|
|
8
9
|
Classifier: Operating System :: OS Independent
|
|
9
10
|
Classifier: Programming Language :: Python :: 3.9
|
|
10
11
|
Requires-Python: >=3.9
|
|
@@ -56,12 +56,20 @@ class GatewayAdapter(Adapter):
|
|
|
56
56
|
if body is not None and len(body) > 0:
|
|
57
57
|
store_params = json.loads(body)
|
|
58
58
|
|
|
59
|
+
roles = []
|
|
60
|
+
authorizer_info = event.get("requestContext", {}).get("authorizer", {})
|
|
61
|
+
claims = authorizer_info.get("claims", {})
|
|
62
|
+
roles = claims.get("roles", [])
|
|
63
|
+
subject = claims.get("subject")
|
|
64
|
+
|
|
59
65
|
return Operation(
|
|
60
66
|
entity=entity,
|
|
61
67
|
action=action,
|
|
62
68
|
store_params=store_params,
|
|
63
69
|
query_params=query_params,
|
|
64
70
|
metadata_params=metadata_params,
|
|
71
|
+
roles=roles,
|
|
72
|
+
subject=subject,
|
|
65
73
|
)
|
|
66
74
|
|
|
67
75
|
def _convert_parameters(self, parameters):
|
|
@@ -0,0 +1,106 @@
|
|
|
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(f"adapter result: {result}")
|
|
103
|
+
return self.marshal(result)
|
|
104
|
+
except PermissionError as e:
|
|
105
|
+
log.error(f"Permission error: {e}")
|
|
106
|
+
return {"error": str(e), "status": "permission_denied"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from api_foundry_query_engine.connectors.connection import Connection, Cursor
|
|
2
|
+
from api_foundry_query_engine.utils.logger import logger
|
|
3
|
+
import boto3
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
# Initialize the logger
|
|
7
|
+
log = logger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AthenaCursor(Cursor):
|
|
11
|
+
def __init__(self, athena_client, db_config):
|
|
12
|
+
self.athena_client = athena_client
|
|
13
|
+
self.db_config = db_config
|
|
14
|
+
|
|
15
|
+
def execute(self, sql: str, parameters: dict, result_columns: list[str]) -> list:
|
|
16
|
+
"""
|
|
17
|
+
Execute a SQL statement on the Athena database.
|
|
18
|
+
|
|
19
|
+
Parameters:
|
|
20
|
+
- sql (str): The SQL query to execute.
|
|
21
|
+
- parameters (dict): Parameters for the query (currently unused; substitute manually in the query).
|
|
22
|
+
- result_columns (list[str]): Column names for the results.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
- list[dict]: Query results as a list of dictionaries.
|
|
26
|
+
"""
|
|
27
|
+
# Substitute parameters into the query manually (Athena doesn't support placeholders)
|
|
28
|
+
for key, value in parameters.items():
|
|
29
|
+
placeholder = f":{key}"
|
|
30
|
+
if isinstance(value, str):
|
|
31
|
+
value = f"'{value}'" # Wrap strings in quotes
|
|
32
|
+
sql = sql.replace(placeholder, str(value))
|
|
33
|
+
|
|
34
|
+
log.info(f"Executing query: {sql}")
|
|
35
|
+
|
|
36
|
+
# Start the query execution
|
|
37
|
+
response = self.athena_client.start_query_execution(
|
|
38
|
+
QueryString=sql,
|
|
39
|
+
QueryExecutionContext={"Database": self.db_config["database"]},
|
|
40
|
+
ResultConfiguration={"OutputLocation": self.db_config["output_location"]},
|
|
41
|
+
)
|
|
42
|
+
query_execution_id = response["QueryExecutionId"]
|
|
43
|
+
log.info(f"Query execution started: {query_execution_id}")
|
|
44
|
+
|
|
45
|
+
# Wait for the query to complete
|
|
46
|
+
while True:
|
|
47
|
+
status_response = self.athena_client.get_query_execution(
|
|
48
|
+
QueryExecutionId=query_execution_id
|
|
49
|
+
)
|
|
50
|
+
status = status_response["QueryExecution"]["Status"]["State"]
|
|
51
|
+
|
|
52
|
+
if status in ["SUCCEEDED", "FAILED", "CANCELLED"]:
|
|
53
|
+
break
|
|
54
|
+
time.sleep(2)
|
|
55
|
+
|
|
56
|
+
if status == "FAILED":
|
|
57
|
+
raise Exception(
|
|
58
|
+
f"Query failed: {status_response['QueryExecution']['Status']['StateChangeReason']}"
|
|
59
|
+
)
|
|
60
|
+
elif status == "CANCELLED":
|
|
61
|
+
raise Exception("Query was cancelled.")
|
|
62
|
+
|
|
63
|
+
# Fetch results
|
|
64
|
+
results_response = self.athena_client.get_query_results(
|
|
65
|
+
QueryExecutionId=query_execution_id
|
|
66
|
+
)
|
|
67
|
+
rows = results_response["ResultSet"]["Rows"]
|
|
68
|
+
|
|
69
|
+
# Convert rows to dictionaries
|
|
70
|
+
result = []
|
|
71
|
+
for row in rows[1:]: # Skip the first row (header row)
|
|
72
|
+
record = {
|
|
73
|
+
col: value.get("VarCharValue", None)
|
|
74
|
+
for col, value in zip(result_columns, row["Data"])
|
|
75
|
+
}
|
|
76
|
+
result.append(record)
|
|
77
|
+
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
def close(self):
|
|
81
|
+
log.info("Closing Athena cursor (no persistent connection to close).")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class AthenaConnection(Connection):
|
|
85
|
+
def __init__(self, db_config: dict) -> None:
|
|
86
|
+
"""
|
|
87
|
+
Initialize the AthenaConnection with the given configuration.
|
|
88
|
+
|
|
89
|
+
Parameters:
|
|
90
|
+
- db_config (dict): A dictionary containing Athena connection configuration.
|
|
91
|
+
Required keys:
|
|
92
|
+
- 'region': AWS region of the Athena service.
|
|
93
|
+
- 'output_location': S3 bucket location for query results.
|
|
94
|
+
- 'database': Athena database name.
|
|
95
|
+
"""
|
|
96
|
+
super().__init__(db_config)
|
|
97
|
+
self.athena_client = self.get_athena_client()
|
|
98
|
+
|
|
99
|
+
def engine(self) -> str:
|
|
100
|
+
return "athena"
|
|
101
|
+
|
|
102
|
+
def cursor(self) -> Cursor:
|
|
103
|
+
return AthenaCursor(self.athena_client, self.db_config)
|
|
104
|
+
|
|
105
|
+
def commit(self):
|
|
106
|
+
# Athena queries are read-only; no commit operation is required
|
|
107
|
+
log.info("Athena does not support transactions. Commit is a no-op.")
|
|
108
|
+
|
|
109
|
+
def close(self):
|
|
110
|
+
# Athena uses a stateless API, so there's nothing to close
|
|
111
|
+
log.info("Closing Athena connection (no persistent connection to close).")
|
|
112
|
+
|
|
113
|
+
def get_athena_client(self):
|
|
114
|
+
"""
|
|
115
|
+
Get a Boto3 client for Athena.
|
|
116
|
+
"""
|
|
117
|
+
region = self.db_config.get("region")
|
|
118
|
+
if not region:
|
|
119
|
+
raise ValueError("Athena configuration must include 'region'.")
|
|
120
|
+
return boto3.client("athena", region_name=region)
|
|
@@ -34,7 +34,7 @@ class ConnectionFactory:
|
|
|
34
34
|
db_config = self.db_config_map.get(database)
|
|
35
35
|
if not db_config:
|
|
36
36
|
secret_name = json.loads(os.environ.get("SECRETS", "{}")).get(database)
|
|
37
|
-
log.
|
|
37
|
+
log.debug(f"secret_name: {secret_name}")
|
|
38
38
|
|
|
39
39
|
if secret_name:
|
|
40
40
|
db_config = self.__get_secret(secret_name)
|
|
@@ -70,7 +70,7 @@ class ConnectionFactory:
|
|
|
70
70
|
sts_client = boto3.client("sts", endpoint_url=endpoint_url)
|
|
71
71
|
|
|
72
72
|
secret_account_id = os.environ.get("SECRET_ACCOUNT_ID", None)
|
|
73
|
-
log.
|
|
73
|
+
log.debug(f"secret_account_id: {secret_account_id}")
|
|
74
74
|
|
|
75
75
|
if secret_account_id:
|
|
76
76
|
# If a secret account ID is provided, assume a role in that account
|
|
@@ -99,7 +99,6 @@ class ConnectionFactory:
|
|
|
99
99
|
|
|
100
100
|
# Get the secret value from AWS Secrets Manager
|
|
101
101
|
log.info(f"db_secret_name: {db_secret_name}")
|
|
102
|
-
db_secret = secretsmanager.describe_secret(SecretId=db_secret_name)
|
|
103
102
|
db_secret = secretsmanager.get_secret_value(SecretId=db_secret_name)
|
|
104
103
|
log.debug(f"loading secret name: {db_secret}")
|
|
105
104
|
|
|
@@ -26,11 +26,10 @@ class PostgresCursor(Cursor):
|
|
|
26
26
|
"""
|
|
27
27
|
from psycopg2 import Error, IntegrityError, ProgrammingError
|
|
28
28
|
|
|
29
|
-
log.info(f"sql: {sql}
|
|
29
|
+
log.info(f"sql: {sql}")
|
|
30
30
|
|
|
31
31
|
try:
|
|
32
32
|
# Execute the SQL statement with parameters
|
|
33
|
-
log.info(f"sql: {self.__cursor.mogrify(sql, parameters)}")
|
|
34
33
|
self.__cursor.execute(sql, parameters)
|
|
35
34
|
result = []
|
|
36
35
|
for record in self.__cursor:
|
|
@@ -99,7 +98,7 @@ class PostgresConnection(Connection):
|
|
|
99
98
|
|
|
100
99
|
connection_params.update(additional_config)
|
|
101
100
|
|
|
102
|
-
log.info(f"connection_params: {
|
|
101
|
+
log.info(f"connection_params: dbname: {dbname}, user: {user}, host: {host}, port: {port}")
|
|
103
102
|
|
|
104
103
|
# Create a connection to the PostgreSQL database
|
|
105
104
|
return connect(**connection_params)
|
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
from typing import Union
|
|
2
2
|
|
|
3
3
|
from api_foundry_query_engine.dao.sql_custom_query_handler import SQLCustomQueryHandler
|
|
4
|
-
from api_foundry_query_engine.dao.sql_delete_query_handler import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
from api_foundry_query_engine.dao.
|
|
8
|
-
|
|
4
|
+
from api_foundry_query_engine.dao.sql_delete_query_handler import (
|
|
5
|
+
SQLDeleteSchemaQueryHandler,
|
|
6
|
+
)
|
|
7
|
+
from api_foundry_query_engine.dao.sql_insert_query_handler import (
|
|
8
|
+
SQLInsertSchemaQueryHandler,
|
|
9
|
+
)
|
|
10
|
+
from api_foundry_query_engine.dao.sql_select_query_handler import (
|
|
11
|
+
SQLSelectSchemaQueryHandler,
|
|
12
|
+
)
|
|
13
|
+
from api_foundry_query_engine.dao.sql_subselect_query_handler import (
|
|
14
|
+
SQLSubselectSchemaQueryHandler,
|
|
15
|
+
)
|
|
16
|
+
from api_foundry_query_engine.dao.sql_update_query_handler import (
|
|
17
|
+
SQLUpdateSchemaQueryHandler,
|
|
18
|
+
)
|
|
9
19
|
from api_foundry_query_engine.utils.app_exception import ApplicationException
|
|
10
20
|
from api_foundry_query_engine.dao.dao import DAO
|
|
11
21
|
from api_foundry_query_engine.connectors.connection import Cursor
|
|
12
22
|
from api_foundry_query_engine.operation import Operation
|
|
13
|
-
from api_foundry_query_engine.utils.api_model import
|
|
23
|
+
from api_foundry_query_engine.utils.api_model import (
|
|
24
|
+
get_schema_object,
|
|
25
|
+
get_path_operation,
|
|
26
|
+
)
|
|
14
27
|
from api_foundry_query_engine.dao.sql_query_handler import SQLQueryHandler
|
|
15
28
|
|
|
16
29
|
|
|
@@ -33,9 +33,14 @@ class SQLCustomQueryHandler(SQLQueryHandler):
|
|
|
33
33
|
def select_list_columns(self) -> List[SchemaObjectProperty]:
|
|
34
34
|
raise NotImplementedError()
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
@property
|
|
37
|
+
def selection_results(self) -> Dict:
|
|
38
|
+
if not hasattr(self, "_selection_results"):
|
|
39
|
+
self._selection_results = self.check_permissions(
|
|
40
|
+
"read", self.path_operation.permissions, self.path_operation.outputs
|
|
41
|
+
)
|
|
42
|
+
log.debug(f"selection_results: {self._selection_results}")
|
|
43
|
+
return self._selection_results
|
|
39
44
|
|
|
40
45
|
def _compile(self):
|
|
41
46
|
placeholder_pattern = re.compile(r":(\w+)")
|
|
@@ -59,8 +64,5 @@ class SQLCustomQueryHandler(SQLQueryHandler):
|
|
|
59
64
|
if placeholder_name in self.operation.query_params
|
|
60
65
|
else property.default
|
|
61
66
|
)
|
|
62
|
-
log.info(f"placeholder_name: {placeholder_name}")
|
|
63
|
-
log.info(f"value: {value}, default: {property.default}")
|
|
64
|
-
log.info(f"placeholders: {self.generate_placeholders(property, value)}")
|
|
65
67
|
self._placeholders.update(self.generate_placeholders(property, value))
|
|
66
68
|
return self.placeholder(property, placeholder_name)
|
|
@@ -2,6 +2,9 @@ from api_foundry_query_engine.dao.sql_query_handler import SQLSchemaQueryHandler
|
|
|
2
2
|
from api_foundry_query_engine.operation import Operation
|
|
3
3
|
from api_foundry_query_engine.utils.app_exception import ApplicationException
|
|
4
4
|
from api_foundry_query_engine.utils.api_model import SchemaObject
|
|
5
|
+
from api_foundry_query_engine.utils.logger import logger
|
|
6
|
+
|
|
7
|
+
log = logger(__name__)
|
|
5
8
|
|
|
6
9
|
|
|
7
10
|
class SQLDeleteSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
@@ -10,8 +13,40 @@ class SQLDeleteSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
10
13
|
) -> None:
|
|
11
14
|
super().__init__(operation, schema_object, engine)
|
|
12
15
|
|
|
16
|
+
def check_permission(self) -> bool:
|
|
17
|
+
"""
|
|
18
|
+
Checks the user's permissions for the specified permission type.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
permission_type (str): The type of permission to check ("read" or "write").
|
|
22
|
+
properties (List[str], optional): Specific properties to check. If None,
|
|
23
|
+
all schema properties are checked.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
List[str]: A list of properties the user is permitted to access.
|
|
27
|
+
"""
|
|
28
|
+
# if permissions are not defined then no restrictions are applied
|
|
29
|
+
if not self.schema_object.permissions:
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
for role in self.operation.roles:
|
|
33
|
+
role_permissions = self.schema_object.permissions.get(role, {})
|
|
34
|
+
log.info(f"role: {role}, role_permissions: {role_permissions}")
|
|
35
|
+
if len(role_permissions) == 0:
|
|
36
|
+
continue
|
|
37
|
+
allowed = role_permissions.get("delete", False)
|
|
38
|
+
if allowed:
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
return False
|
|
42
|
+
|
|
13
43
|
@property
|
|
14
44
|
def sql(self) -> str:
|
|
45
|
+
if not self.check_permission():
|
|
46
|
+
raise ApplicationException(
|
|
47
|
+
402, f"Subject is not allowed to delete {self.schema_object.api_name}"
|
|
48
|
+
)
|
|
49
|
+
|
|
15
50
|
concurrency_property = self.schema_object.concurrency_property
|
|
16
51
|
if concurrency_property:
|
|
17
52
|
if not self.operation.query_params.get(concurrency_property.api_name):
|
|
@@ -3,6 +3,9 @@ from api_foundry_query_engine.dao.sql_query_handler import SQLSchemaQueryHandler
|
|
|
3
3
|
from api_foundry_query_engine.operation import Operation
|
|
4
4
|
from api_foundry_query_engine.utils.app_exception import ApplicationException
|
|
5
5
|
from api_foundry_query_engine.utils.api_model import SchemaObject, SchemaObjectProperty
|
|
6
|
+
from api_foundry_query_engine.utils.logger import logger
|
|
7
|
+
|
|
8
|
+
log = logger(__name__)
|
|
6
9
|
|
|
7
10
|
|
|
8
11
|
class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
@@ -67,6 +70,10 @@ class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
67
70
|
placeholders = []
|
|
68
71
|
columns = []
|
|
69
72
|
|
|
73
|
+
allowed_properties = self.check_permissions(
|
|
74
|
+
"write", self.schema_object.permissions, self.schema_object.properties
|
|
75
|
+
)
|
|
76
|
+
log.info(f"allowed properties: {allowed_properties}")
|
|
70
77
|
for name, value in self.operation.store_params.items():
|
|
71
78
|
parts = name.split(".")
|
|
72
79
|
|
|
@@ -77,13 +84,21 @@ class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
77
84
|
"Properties can not be set on associated objects " + name,
|
|
78
85
|
)
|
|
79
86
|
|
|
80
|
-
property =
|
|
87
|
+
property = allowed_properties[parts[0]]
|
|
81
88
|
except KeyError:
|
|
82
|
-
|
|
89
|
+
if parts[0] not in self.schema_object.properties:
|
|
90
|
+
raise ApplicationException(400, f"Invalid property: {name}")
|
|
91
|
+
else:
|
|
92
|
+
raise ApplicationException(
|
|
93
|
+
402,
|
|
94
|
+
f"Subject is not allowed to create with property: {parts[0]}",
|
|
95
|
+
)
|
|
83
96
|
|
|
84
97
|
columns.append(property.column_name)
|
|
85
98
|
placeholders.append(self.placeholder(property, property.api_name))
|
|
86
|
-
self.store_placeholders[property.api_name] = property.convert_to_db_value(
|
|
99
|
+
self.store_placeholders[property.api_name] = property.convert_to_db_value(
|
|
100
|
+
value
|
|
101
|
+
)
|
|
87
102
|
|
|
88
103
|
if self.key_property:
|
|
89
104
|
if self.key_property.key_type == "sequence":
|
|
@@ -108,10 +108,12 @@ class SQLQueryHandler:
|
|
|
108
108
|
operation: Operation
|
|
109
109
|
engine: str
|
|
110
110
|
|
|
111
|
-
def __init__(
|
|
111
|
+
def __init__(
|
|
112
|
+
self, operation: Operation, engine: str
|
|
113
|
+
): # , schema_object: SchemaObject):
|
|
112
114
|
self.operation = operation
|
|
113
|
-
self.__select_list_columns = None
|
|
114
115
|
self.engine = engine
|
|
116
|
+
self.__select_list_columns = None
|
|
115
117
|
|
|
116
118
|
@property
|
|
117
119
|
def sql(self) -> str:
|
|
@@ -124,14 +126,26 @@ class SQLQueryHandler:
|
|
|
124
126
|
@property
|
|
125
127
|
def select_list_columns(self) -> List[str]:
|
|
126
128
|
if not self.__select_list_columns:
|
|
129
|
+
# Filter columns based on read permissions
|
|
127
130
|
self.__select_list_columns = list(self.selection_results.keys())
|
|
128
131
|
return self.__select_list_columns
|
|
129
132
|
|
|
130
133
|
def marshal_record(self, record: dict) -> dict:
|
|
134
|
+
"""
|
|
135
|
+
Converts a database record to an API-compatible dictionary,
|
|
136
|
+
removing properties the user is not allowed to read.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
record (dict): A dictionary representing a single database record.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
dict: A dictionary with only the properties the user is allowed to see.
|
|
143
|
+
"""
|
|
131
144
|
result = {}
|
|
132
145
|
for name, value in record.items():
|
|
133
|
-
|
|
134
|
-
|
|
146
|
+
if name in self.selection_results: # Check if the property is allowed
|
|
147
|
+
property = self.selection_results[name]
|
|
148
|
+
result[property.api_name] = property.convert_to_api_value(value)
|
|
135
149
|
return result
|
|
136
150
|
|
|
137
151
|
def placeholder(self, property: SchemaObjectProperty, param: str = "") -> str:
|
|
@@ -148,11 +162,59 @@ class SQLQueryHandler:
|
|
|
148
162
|
return f":{param}"
|
|
149
163
|
return f"%({param})s"
|
|
150
164
|
|
|
165
|
+
def check_permissions(
|
|
166
|
+
self,
|
|
167
|
+
permission_type: str,
|
|
168
|
+
permissions: Optional[dict],
|
|
169
|
+
properties: Optional[List[str]],
|
|
170
|
+
) -> Dict[str, SchemaObjectProperty]:
|
|
171
|
+
"""
|
|
172
|
+
Checks the user's permissions for the specified permission type.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
permission_type (str): The type of permission to check ("read" or "write").
|
|
176
|
+
properties (List[str], optional): Specific properties to check. If None,
|
|
177
|
+
all schema properties are checked.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
List[str]: A list of properties the user is permitted to access.
|
|
181
|
+
"""
|
|
182
|
+
# if permissions are not defined then no restrictions are applied
|
|
183
|
+
log.info(
|
|
184
|
+
f"checking permissions permission_type: {permission_type}, permissions: {permissions}"
|
|
185
|
+
)
|
|
186
|
+
if not permissions:
|
|
187
|
+
return properties
|
|
188
|
+
|
|
189
|
+
allowed_properties = {}
|
|
190
|
+
|
|
191
|
+
for role in self.operation.roles:
|
|
192
|
+
role_permissions = permissions.get(role, {})
|
|
193
|
+
log.info(f"role: {role}, role_permissions: {role_permissions}")
|
|
194
|
+
if len(role_permissions) == 0:
|
|
195
|
+
continue
|
|
196
|
+
for prop_name, property in properties.items():
|
|
197
|
+
log.info(f"prop_name: {prop_name}, property: {property}")
|
|
198
|
+
if permission_type == "read" and re.match(
|
|
199
|
+
role_permissions.get("read", ""), prop_name
|
|
200
|
+
):
|
|
201
|
+
allowed_properties[prop_name] = property
|
|
202
|
+
if permission_type == "write" and re.match(
|
|
203
|
+
role_permissions.get("write", ""), prop_name
|
|
204
|
+
):
|
|
205
|
+
allowed_properties[prop_name] = property
|
|
206
|
+
|
|
207
|
+
log.info(f"allowed_properties: {allowed_properties}")
|
|
208
|
+
return allowed_properties
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def selection_results(self) -> Dict:
|
|
212
|
+
raise NotImplementedError()
|
|
213
|
+
|
|
151
214
|
def generate_sql_condition(
|
|
152
215
|
self, property: SchemaObjectProperty, value, prefix: Optional[str] = None
|
|
153
216
|
) -> str:
|
|
154
217
|
operand = "="
|
|
155
|
-
|
|
156
218
|
if isinstance(value, str):
|
|
157
219
|
parts = value.split("::", 1)
|
|
158
220
|
operand = RELATIONAL_TYPES.get(parts[0], "=") if len(parts) > 1 else "="
|
|
@@ -223,15 +285,6 @@ class SQLQueryHandler:
|
|
|
223
285
|
placeholders = self.generate_placeholders(property, value, prefix)
|
|
224
286
|
return sql_condition, placeholders
|
|
225
287
|
|
|
226
|
-
@property
|
|
227
|
-
def selection_results(self) -> Dict:
|
|
228
|
-
if not hasattr(self, "_selection_results"):
|
|
229
|
-
self._selection_results = self.selection_result_map()
|
|
230
|
-
return self._selection_results
|
|
231
|
-
|
|
232
|
-
def selection_result_map(self) -> Dict:
|
|
233
|
-
raise NotImplementedError()
|
|
234
|
-
|
|
235
288
|
|
|
236
289
|
class SQLSchemaQueryHandler(SQLQueryHandler):
|
|
237
290
|
schema_object: SchemaObject
|
|
@@ -295,6 +348,26 @@ class SQLSchemaQueryHandler(SQLQueryHandler):
|
|
|
295
348
|
def table_expression(self) -> str:
|
|
296
349
|
return self.schema_object.table_name
|
|
297
350
|
|
|
351
|
+
@property
|
|
352
|
+
def selection_results(self) -> Dict:
|
|
353
|
+
"""
|
|
354
|
+
Filters the schema properties to include only those the user is allowed to read.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
dict: A dictionary of allowed schema properties.
|
|
358
|
+
"""
|
|
359
|
+
log.info("selection_result")
|
|
360
|
+
if not hasattr(self, "__selection_results"):
|
|
361
|
+
log.info(f"prefix_map: {self.prefix_map}")
|
|
362
|
+
filters = self.operation.metadata_params.get("_properties", ".*").split()
|
|
363
|
+
allowed_properties = self.check_permissions(
|
|
364
|
+
"read", self.schema_object.permissions, self.schema_object.properties
|
|
365
|
+
)
|
|
366
|
+
self.__selection_results = self.filter_and_prefix_keys(
|
|
367
|
+
filters, allowed_properties
|
|
368
|
+
)
|
|
369
|
+
return self.__selection_results
|
|
370
|
+
|
|
298
371
|
@property
|
|
299
372
|
def search_condition(self) -> str:
|
|
300
373
|
self.search_placeholders = {}
|
|
@@ -330,14 +403,6 @@ class SQLSchemaQueryHandler(SQLQueryHandler):
|
|
|
330
403
|
self.search_placeholders.update(holders)
|
|
331
404
|
return f" WHERE {' AND '.join(conditions)}" if conditions else ""
|
|
332
405
|
|
|
333
|
-
def selection_result_map(self) -> dict:
|
|
334
|
-
if not self.__selection_result_map:
|
|
335
|
-
filters = self.operation.metadata_params.get("_properties", ".*").split()
|
|
336
|
-
self.__selection_result_map = self.filter_and_prefix_keys(
|
|
337
|
-
filters, self.schema_object.properties
|
|
338
|
-
)
|
|
339
|
-
return self.__selection_result_map
|
|
340
|
-
|
|
341
406
|
def filter_and_prefix_keys(
|
|
342
407
|
self, regex_list: List[str], properties: dict, prefix: Optional[str] = None
|
|
343
408
|
) -> dict:
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
from api_foundry_query_engine.dao.sql_query_handler import SQLSchemaQueryHandler
|
|
2
2
|
from api_foundry_query_engine.operation import Operation
|
|
3
3
|
from api_foundry_query_engine.utils.app_exception import ApplicationException
|
|
4
|
-
from api_foundry_query_engine.utils.api_model import
|
|
5
|
-
SchemaObject,
|
|
6
|
-
SchemaObjectProperty
|
|
7
|
-
)
|
|
4
|
+
from api_foundry_query_engine.utils.api_model import SchemaObject, SchemaObjectProperty
|
|
8
5
|
from api_foundry_query_engine.utils.logger import logger
|
|
9
6
|
|
|
10
7
|
log = logger(__name__)
|
|
@@ -119,52 +116,64 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
119
116
|
+ (f" {' '.join(joins)}" if len(joins) > 0 else "")
|
|
120
117
|
)
|
|
121
118
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
"
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
# Extract the schema object for the current entity
|
|
139
|
-
relation_property = self.schema_object.relations.get(relation)
|
|
140
|
-
|
|
141
|
-
if relation_property:
|
|
142
|
-
if relation_property.type == "array":
|
|
143
|
-
continue
|
|
144
|
-
|
|
145
|
-
# Use a default value if relation_property is None
|
|
146
|
-
schema_object = relation_property.child_schema_object
|
|
147
|
-
else:
|
|
148
|
-
schema_object = self.schema_object
|
|
119
|
+
@property
|
|
120
|
+
def selection_results(self) -> dict:
|
|
121
|
+
if not hasattr(self, "_selection_results"):
|
|
122
|
+
self._selection_results = {}
|
|
123
|
+
if "count" in self.operation.metadata_params:
|
|
124
|
+
self._selection_results = {
|
|
125
|
+
"count": SchemaObjectProperty(
|
|
126
|
+
{
|
|
127
|
+
"api_name": "count",
|
|
128
|
+
"api_type": "integer",
|
|
129
|
+
"column_name": "count(*)",
|
|
130
|
+
"column_type": "integer",
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
return self._selection_results
|
|
149
135
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
136
|
+
filter_str = self.operation.metadata_params.get("properties", ".*")
|
|
137
|
+
|
|
138
|
+
for relation, reg_exs in self.get_regex_map(filter_str).items():
|
|
139
|
+
# Extract the schema object for the current entity
|
|
140
|
+
relation_property = self.schema_object.relations.get(relation)
|
|
141
|
+
|
|
142
|
+
if relation_property:
|
|
143
|
+
if relation_property.type == "array":
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
# Use a default value if relation_property is None
|
|
147
|
+
schema_object = relation_property.child_schema_object
|
|
148
|
+
else:
|
|
149
|
+
schema_object = self.schema_object
|
|
150
|
+
|
|
151
|
+
if relation not in self.prefix_map:
|
|
152
|
+
raise ApplicationException(
|
|
153
|
+
400,
|
|
154
|
+
"Bad object association: "
|
|
155
|
+
+ schema_object.api_name
|
|
156
|
+
+ " does not have a "
|
|
157
|
+
+ relation
|
|
158
|
+
+ " property",
|
|
159
|
+
)
|
|
160
|
+
# Filter and prefix keys for the current entity
|
|
161
|
+
# and regular expressions
|
|
162
|
+
allowed_properties = self.check_permissions(
|
|
163
|
+
"read", schema_object.permissions, schema_object.properties
|
|
164
|
+
)
|
|
165
|
+
filtered_keys = self.filter_and_prefix_keys(
|
|
166
|
+
reg_exs, allowed_properties, self.prefix_map[relation]
|
|
158
167
|
)
|
|
159
|
-
# Filter and prefix keys for the current entity
|
|
160
|
-
# and regular expressions
|
|
161
|
-
filtered_keys = self.filter_and_prefix_keys(
|
|
162
|
-
reg_exs, schema_object.properties, self.prefix_map[relation]
|
|
163
|
-
)
|
|
164
168
|
|
|
165
|
-
|
|
166
|
-
|
|
169
|
+
# Extend the result map with the filtered keys
|
|
170
|
+
self._selection_results.update(filtered_keys)
|
|
167
171
|
|
|
172
|
+
if len(self._selection_results) == 0:
|
|
173
|
+
raise ApplicationException(
|
|
174
|
+
402,
|
|
175
|
+
"After applying permissions there are no properties returned in response",
|
|
176
|
+
)
|
|
168
177
|
return self._selection_results
|
|
169
178
|
|
|
170
179
|
def get_regex_map(self, filter_str: str) -> dict[str, list]:
|
|
@@ -190,7 +199,11 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
190
199
|
for name, value in record.items():
|
|
191
200
|
property = self.selection_results[name]
|
|
192
201
|
parts = name.split(".")
|
|
193
|
-
component =
|
|
202
|
+
component = (
|
|
203
|
+
parts[0]
|
|
204
|
+
if len(parts) > 1
|
|
205
|
+
else self.prefix_map[self.schema_object.api_name]
|
|
206
|
+
)
|
|
194
207
|
object = object_set.get(component, {})
|
|
195
208
|
if not object:
|
|
196
209
|
object_set[component] = object
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
|
|
3
3
|
from api_foundry_query_engine.dao.sql_query_handler import SQLSchemaQueryHandler
|
|
4
|
-
from api_foundry_query_engine.dao.sql_select_query_handler import
|
|
4
|
+
from api_foundry_query_engine.dao.sql_select_query_handler import (
|
|
5
|
+
SQLSelectSchemaQueryHandler,
|
|
6
|
+
)
|
|
5
7
|
from api_foundry_query_engine.operation import Operation
|
|
6
8
|
from api_foundry_query_engine.utils.api_model import SchemaObjectAssociation
|
|
7
9
|
|
|
@@ -19,8 +21,9 @@ class SQLSubselectSchemaQueryHandler(SQLSelectSchemaQueryHandler):
|
|
|
19
21
|
self.relation = relation
|
|
20
22
|
self.parent_generator = parent_generator
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
@property
|
|
25
|
+
def selection_results(self) -> dict:
|
|
26
|
+
filter_str = self.operation.metadata_params.get("properties", ".*")
|
|
24
27
|
result = {self.relation.child_property: self.relation.child_property}
|
|
25
28
|
|
|
26
29
|
for relation_name, reg_exs in self.get_regex_map(filter_str).items():
|
|
@@ -39,21 +39,32 @@ class SQLUpdateSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
39
39
|
|
|
40
40
|
@property
|
|
41
41
|
def update_values(self) -> str:
|
|
42
|
+
allowed_properties = self.check_permissions(
|
|
43
|
+
"write", self.schema_object.permissions, self.schema_object.properties
|
|
44
|
+
)
|
|
42
45
|
self.store_placeholders = {}
|
|
43
46
|
columns = []
|
|
47
|
+
invalid_columns = []
|
|
44
48
|
|
|
45
49
|
for name, value in self.operation.store_params.items():
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
400, f"Search condition column not found {name}"
|
|
51
|
-
)
|
|
50
|
+
if name not in allowed_properties:
|
|
51
|
+
invalid_columns.append(name)
|
|
52
|
+
else:
|
|
53
|
+
property = allowed_properties.get(name, None)
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
placeholder = property.api_name
|
|
56
|
+
column_name = property.column_name
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
columns.append(
|
|
59
|
+
f"{column_name} = {self.placeholder(property, placeholder)}"
|
|
60
|
+
)
|
|
61
|
+
self.store_placeholders[placeholder] = property.convert_to_db_value(
|
|
62
|
+
value
|
|
63
|
+
)
|
|
58
64
|
|
|
65
|
+
if invalid_columns:
|
|
66
|
+
raise ApplicationException(
|
|
67
|
+
402,
|
|
68
|
+
f"Subject does not have permission to update properties: {invalid_columns}",
|
|
69
|
+
)
|
|
59
70
|
return f" SET {', '.join(columns)}"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
class Operation:
|
|
2
|
+
"""
|
|
3
|
+
Represents an action to be performed on an entity.
|
|
4
|
+
|
|
5
|
+
The `Operation` class encapsulates the details required to execute an operation
|
|
6
|
+
on a given entity, including query parameters for selecting records, parameters
|
|
7
|
+
for storing or updating values, metadata for operational instructions, and
|
|
8
|
+
roles defining the contexts in which the operation can be performed.
|
|
9
|
+
|
|
10
|
+
Attributes:
|
|
11
|
+
entity (str): The name of the resource or object being targeted (e.g., "User", "Order").
|
|
12
|
+
action (str): The type of action being performed (e.g., "create", "read", "update", "delete").
|
|
13
|
+
query_params (dict): Parameters used to filter or select the records affected by the operation.
|
|
14
|
+
store_params (dict): Parameters that define the values to be stored or updated for the selected records.
|
|
15
|
+
metadata_params (dict): Additional instructions for the operation, such as sorting or pagination.
|
|
16
|
+
roles (dict): Defines the roles under which the operation is allowed to be performed.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
*,
|
|
22
|
+
entity: str,
|
|
23
|
+
action: str,
|
|
24
|
+
query_params: dict = {},
|
|
25
|
+
store_params: dict = {},
|
|
26
|
+
metadata_params: dict = {},
|
|
27
|
+
roles: dict = {},
|
|
28
|
+
subject: str = None,
|
|
29
|
+
):
|
|
30
|
+
"""
|
|
31
|
+
Initializes the Operation instance.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
entity (str): The name of the entity to perform the operation on.
|
|
35
|
+
action (str): The action to perform on the entity.
|
|
36
|
+
query_params (dict, optional): Parameters for selecting affected records (default: {}).
|
|
37
|
+
store_params (dict, optional): Parameters defining new or updated values (default: {}).
|
|
38
|
+
metadata_params (dict, optional): Metadata for the operation, such as sorting or pagination (default: {}).
|
|
39
|
+
roles (dict, optional): Defines the roles allowed to perform the operation (default: {}).
|
|
40
|
+
"""
|
|
41
|
+
# The target entity for the operation (e.g., "User", "Order").
|
|
42
|
+
self.entity = entity
|
|
43
|
+
|
|
44
|
+
# The type of action to perform (e.g., "create", "read", "update", "delete").
|
|
45
|
+
self.action = action
|
|
46
|
+
|
|
47
|
+
# Query parameters to filter or identify the affected records.
|
|
48
|
+
self.query_params = query_params
|
|
49
|
+
|
|
50
|
+
# Parameters defining the values to be stored or updated for the operation.
|
|
51
|
+
self.store_params = store_params
|
|
52
|
+
|
|
53
|
+
# Metadata for operational instructions like sorting, limiting, or offsetting results.
|
|
54
|
+
self.metadata_params = metadata_params
|
|
55
|
+
|
|
56
|
+
# Roles defining the context in which the operation is allowed.
|
|
57
|
+
self.roles = roles
|
|
58
|
+
|
|
59
|
+
self.subject = subject
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from api_foundry_query_engine.utils.logger import logger
|
|
2
|
+
from api_foundry_query_engine.operation import Operation
|
|
3
|
+
from api_foundry_query_engine.services.service import ServiceAdapter
|
|
4
|
+
|
|
5
|
+
log = logger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SecurityService(ServiceAdapter):
|
|
9
|
+
def execute(self, operation: Operation) -> list[dict]:
|
|
10
|
+
"""
|
|
11
|
+
Execute the operation after validating security instructions.
|
|
12
|
+
|
|
13
|
+
Parameters:
|
|
14
|
+
- operation (Operation): The operation to execute.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
- list[dict]: The result of the operation, filtered based on security rules.
|
|
18
|
+
"""
|
|
19
|
+
log.debug(f"Executing operation: {operation}")
|
|
20
|
+
|
|
21
|
+
# Resolve the schema or path operation to retrieve the `security` attribute
|
|
22
|
+
schema_or_path = self.schema_or_path_resolver(
|
|
23
|
+
operation.entity, operation.action
|
|
24
|
+
)
|
|
25
|
+
if not schema_or_path or "security" not in schema_or_path:
|
|
26
|
+
raise ValueError(
|
|
27
|
+
f"No security instructions found for entity {operation.entity}"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
security_rules = schema_or_path["security"]
|
|
31
|
+
|
|
32
|
+
# Validate query and store parameters
|
|
33
|
+
self._validate_query_params(operation.query_params, security_rules)
|
|
34
|
+
self._validate_store_params(operation.store_params, security_rules)
|
|
35
|
+
|
|
36
|
+
# Execute the next service
|
|
37
|
+
result = self.next_service.execute(operation)
|
|
38
|
+
|
|
39
|
+
# Filter the result based on security rules
|
|
40
|
+
filtered_result = self._filter_result(result, security_rules)
|
|
41
|
+
return filtered_result
|
|
42
|
+
|
|
43
|
+
def _validate_query_params(self, query_params: dict, security_rules: dict):
|
|
44
|
+
"""
|
|
45
|
+
Validate query parameters against read permissions.
|
|
46
|
+
|
|
47
|
+
Parameters:
|
|
48
|
+
- query_params (dict): Query parameters to validate.
|
|
49
|
+
- security_rules (dict): Security rules for validation.
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
- PermissionError: If a query parameter violates read permissions.
|
|
53
|
+
"""
|
|
54
|
+
read_permissions = security_rules.get("read", [])
|
|
55
|
+
invalid_params = [
|
|
56
|
+
key
|
|
57
|
+
for key in query_params
|
|
58
|
+
if key not in read_permissions and read_permissions != ["*"]
|
|
59
|
+
]
|
|
60
|
+
if invalid_params:
|
|
61
|
+
raise PermissionError(f"Query parameters not permitted: {invalid_params}")
|
|
62
|
+
|
|
63
|
+
def _validate_store_params(self, store_params: dict, security_rules: dict):
|
|
64
|
+
"""
|
|
65
|
+
Validate store parameters against write permissions.
|
|
66
|
+
|
|
67
|
+
Parameters:
|
|
68
|
+
- store_params (dict): Store parameters to validate.
|
|
69
|
+
- security_rules (dict): Security rules for validation.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
- PermissionError: If a store parameter violates write permissions.
|
|
73
|
+
"""
|
|
74
|
+
write_permissions = security_rules.get("write", [])
|
|
75
|
+
invalid_params = [
|
|
76
|
+
key
|
|
77
|
+
for key in store_params
|
|
78
|
+
if key not in write_permissions and write_permissions != ["*"]
|
|
79
|
+
]
|
|
80
|
+
if invalid_params:
|
|
81
|
+
raise PermissionError(f"Store parameters not permitted: {invalid_params}")
|
|
82
|
+
|
|
83
|
+
def _filter_result(self, result: list[dict], security_rules: dict) -> list[dict]:
|
|
84
|
+
"""
|
|
85
|
+
Filter the result based on read permissions.
|
|
86
|
+
|
|
87
|
+
Parameters:
|
|
88
|
+
- result (list[dict]): The original result set.
|
|
89
|
+
- security_rules (dict): Security rules for filtering.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
- list[dict]: The filtered result set.
|
|
93
|
+
"""
|
|
94
|
+
read_permissions = security_rules.get("read", [])
|
|
95
|
+
if read_permissions == ["*"]:
|
|
96
|
+
return result # No filtering needed
|
|
97
|
+
|
|
98
|
+
filtered_result = []
|
|
99
|
+
for record in result:
|
|
100
|
+
filtered_record = {
|
|
101
|
+
key: value for key, value in record.items() if key in read_permissions
|
|
102
|
+
}
|
|
103
|
+
filtered_result.append(filtered_record)
|
|
104
|
+
|
|
105
|
+
return filtered_result
|
|
@@ -6,7 +6,10 @@ from api_foundry_query_engine.operation import Operation
|
|
|
6
6
|
from api_foundry_query_engine.services.service import ServiceAdapter
|
|
7
7
|
from api_foundry_query_engine.connectors.connection_factory import connection_factory
|
|
8
8
|
from api_foundry_query_engine.dao.operation_dao import OperationDAO
|
|
9
|
-
from api_foundry_query_engine.utils.api_model import
|
|
9
|
+
from api_foundry_query_engine.utils.api_model import (
|
|
10
|
+
get_path_operation,
|
|
11
|
+
get_schema_object,
|
|
12
|
+
)
|
|
10
13
|
|
|
11
14
|
log = logger(__name__)
|
|
12
15
|
|
|
@@ -21,7 +24,9 @@ class TransactionalService(ServiceAdapter):
|
|
|
21
24
|
if schema_object:
|
|
22
25
|
database = schema_object.database
|
|
23
26
|
else:
|
|
24
|
-
raise ApplicationException(
|
|
27
|
+
raise ApplicationException(
|
|
28
|
+
500, f"Unknown operation: {operation.entity}"
|
|
29
|
+
)
|
|
25
30
|
|
|
26
31
|
connection = connection_factory.get_connection(database)
|
|
27
32
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
import yaml
|
|
3
2
|
|
|
4
3
|
from datetime import datetime
|
|
@@ -29,7 +28,7 @@ class SchemaObjectProperty:
|
|
|
29
28
|
|
|
30
29
|
def __repr__(self):
|
|
31
30
|
return f"SchemaObjectProperty(api_name={self.api_name}, column_name={self.column_name}, type={self.type})"
|
|
32
|
-
|
|
31
|
+
|
|
33
32
|
def convert_to_db_value(self, value: str) -> Optional[Any]:
|
|
34
33
|
if value is None:
|
|
35
34
|
return None
|
|
@@ -63,7 +62,6 @@ class SchemaObjectProperty:
|
|
|
63
62
|
return conversion_func(value)
|
|
64
63
|
|
|
65
64
|
|
|
66
|
-
|
|
67
65
|
class SchemaObjectAssociation:
|
|
68
66
|
"""Represents an association (relationship) between schema objects."""
|
|
69
67
|
|
|
@@ -77,21 +75,32 @@ class SchemaObjectAssociation:
|
|
|
77
75
|
|
|
78
76
|
@property
|
|
79
77
|
def child_property(self) -> str:
|
|
80
|
-
return
|
|
78
|
+
return (
|
|
79
|
+
self._child_property
|
|
80
|
+
if self._child_property
|
|
81
|
+
else get_schema_object(self.schema_name).primary_key.column_name
|
|
82
|
+
)
|
|
81
83
|
|
|
82
84
|
@property
|
|
83
85
|
def parent_property(self) -> str:
|
|
84
|
-
return
|
|
86
|
+
return (
|
|
87
|
+
self._parent_property
|
|
88
|
+
if self._parent_property
|
|
89
|
+
else get_schema_object(self.parent_schema).primary_key.column_name
|
|
90
|
+
)
|
|
85
91
|
|
|
86
92
|
def __repr__(self):
|
|
87
|
-
return
|
|
93
|
+
return (
|
|
94
|
+
f"SchemaObjectAssociation(name={self.api_name}, "
|
|
95
|
+
+ f"child_property={self._child_property}, "
|
|
96
|
+
+ f"parent_property={self.parent_property})"
|
|
97
|
+
)
|
|
88
98
|
|
|
89
99
|
@property
|
|
90
100
|
def child_schema_object(self) -> "SchemaObject":
|
|
91
101
|
return get_schema_object(self.schema_name)
|
|
92
102
|
|
|
93
103
|
|
|
94
|
-
|
|
95
104
|
class SchemaObject:
|
|
96
105
|
"""Represents a schema object in the API configuration."""
|
|
97
106
|
|
|
@@ -113,6 +122,7 @@ class SchemaObject:
|
|
|
113
122
|
else None
|
|
114
123
|
)
|
|
115
124
|
self._primary_key = data.get("primary_key")
|
|
125
|
+
self.permissions = data.get("permissions")
|
|
116
126
|
|
|
117
127
|
def __repr__(self):
|
|
118
128
|
return f"SchemaObject(table_name={self.table_name}, primary_key={self.primary_key})"
|
|
@@ -121,6 +131,7 @@ class SchemaObject:
|
|
|
121
131
|
def primary_key(self):
|
|
122
132
|
return self.properties.get(self._primary_key)
|
|
123
133
|
|
|
134
|
+
|
|
124
135
|
class PathOperation:
|
|
125
136
|
"""Represents a path operation in the API configuration."""
|
|
126
137
|
|
|
@@ -137,6 +148,7 @@ class PathOperation:
|
|
|
137
148
|
name: SchemaObjectProperty(output_data)
|
|
138
149
|
for name, output_data in data.get("outputs", {}).items()
|
|
139
150
|
}
|
|
151
|
+
self.permissions = data.get("security")
|
|
140
152
|
|
|
141
153
|
def __repr__(self):
|
|
142
154
|
return f"PathOperation(entity={self.entity}, method={self.method})"
|
|
@@ -145,12 +157,14 @@ class PathOperation:
|
|
|
145
157
|
schema_objects = None
|
|
146
158
|
path_operations = None
|
|
147
159
|
|
|
160
|
+
|
|
148
161
|
def get_schema_object(name: str) -> Optional[SchemaObject]:
|
|
149
162
|
"""Returns a schema object by name."""
|
|
150
163
|
global schema_objects
|
|
151
164
|
return schema_objects.get(name)
|
|
152
165
|
|
|
153
|
-
|
|
166
|
+
|
|
167
|
+
def get_path_operation(path: str, method: str) -> Optional[PathOperation]:
|
|
154
168
|
"""Returns a path operation by name."""
|
|
155
169
|
log.info(f"path: {path}, method: {method}")
|
|
156
170
|
global path_operations
|
|
@@ -174,10 +188,12 @@ class APIModel:
|
|
|
174
188
|
}
|
|
175
189
|
|
|
176
190
|
def __repr__(self):
|
|
177
|
-
return
|
|
191
|
+
return (
|
|
192
|
+
f"APIModel(schema_objects={list(self.schema_objects.keys())}, "
|
|
193
|
+
+ f"path_operations={list(self.path_operations.keys())})"
|
|
194
|
+
)
|
|
178
195
|
|
|
179
196
|
|
|
180
197
|
def load_api(filename: str):
|
|
181
198
|
with open(filename, "r") as file:
|
|
182
199
|
APIModel(yaml.safe_load(file))
|
|
183
|
-
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "api-foundry-query-engine"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.6"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Daniel Repik", email="danrepik@icloud.com" },
|
|
10
10
|
]
|
|
@@ -37,4 +37,3 @@ markers = [
|
|
|
37
37
|
"unit: marks tests as unit tests that do not require a database",
|
|
38
38
|
"integration: marks tests that operate on a real database",
|
|
39
39
|
]
|
|
40
|
-
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
class Operation:
|
|
2
|
-
def __init__(
|
|
3
|
-
self,
|
|
4
|
-
*,
|
|
5
|
-
entity: str,
|
|
6
|
-
action: str,
|
|
7
|
-
query_params={},
|
|
8
|
-
store_params={},
|
|
9
|
-
metadata_params={},
|
|
10
|
-
):
|
|
11
|
-
self.entity = entity
|
|
12
|
-
self.action = action
|
|
13
|
-
self.query_params = query_params
|
|
14
|
-
self.store_params = store_params
|
|
15
|
-
self.metadata_params = metadata_params
|
|
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
|