api-foundry-query-engine 0.8.34__tar.gz → 0.8.35__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.34 → api_foundry_query_engine-0.8.35}/PKG-INFO +1 -1
- api_foundry_query_engine-0.8.35/api_foundry_query_engine/__init__.py +1 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/connectors/postgres_connection.py +21 -1
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/utils/token_decoder.py +67 -71
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/pyproject.toml +1 -1
- api_foundry_query_engine-0.8.34/api_foundry_query_engine/__init__.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/.gitignore +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/LICENSE +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/.pre-commit-config.yaml +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/adapters/adapter.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/adapters/case_change_adapter.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/adapters/gateway_adapter.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/adapters/security_adapter.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/connectors/connection.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/connectors/connection_factory.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/connectors/oracle_connector.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/dao/batch_operation_handler.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/dao/dao.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/dao/operation_dao.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/dao/sql_custom_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/dao/sql_delete_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/dao/sql_insert_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/dao/sql_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/dao/sql_restore_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/dao/sql_select_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/dao/sql_subselect_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/dao/sql_update_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/lambda_handler.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/operation.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/services/service.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/services/transactional_service.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/utils/api_model.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/utils/app_exception.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/utils/claims_check.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/utils/dependency_resolver.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/utils/gateway_operation.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/utils/logger.py +0 -0
- {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/utils/reference_resolver.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: api-foundry-query-engine
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.35
|
|
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.35"
|
|
@@ -67,6 +67,20 @@ class PostgresCursor(Cursor):
|
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
class PostgresConnection(Connection):
|
|
70
|
+
"""
|
|
71
|
+
PostgreSQL database connection wrapper.
|
|
72
|
+
|
|
73
|
+
Supports two configuration formats:
|
|
74
|
+
1. DSN-based (preferred for testing with fixture_foundry):
|
|
75
|
+
{"dsn": "postgresql://user:pass@host:port/dbname"}
|
|
76
|
+
|
|
77
|
+
2. Individual parameters (for production AWS Secrets Manager):
|
|
78
|
+
{"host": "...", "port": 5432, "database": "...", "username": "...", "password": "..."}
|
|
79
|
+
|
|
80
|
+
The get_connection() method prioritizes DSN if present, otherwise builds connection
|
|
81
|
+
from individual parameters.
|
|
82
|
+
"""
|
|
83
|
+
|
|
70
84
|
def __init__(self, db_config: dict) -> None:
|
|
71
85
|
super().__init__(db_config)
|
|
72
86
|
self.__connection = self.get_connection()
|
|
@@ -96,10 +110,16 @@ class PostgresConnection(Connection):
|
|
|
96
110
|
"""
|
|
97
111
|
from psycopg2 import connect
|
|
98
112
|
|
|
113
|
+
# If DSN is provided, use it directly (simplifies fixture_foundry integration)
|
|
114
|
+
if "dsn" in self.db_config:
|
|
115
|
+
log.info("Connecting using DSN: %s", self.db_config["dsn"])
|
|
116
|
+
return connect(self.db_config["dsn"])
|
|
117
|
+
|
|
118
|
+
# Otherwise, build connection from individual parameters
|
|
99
119
|
dbname = self.db_config["database"]
|
|
100
120
|
user = self.db_config["username"]
|
|
101
121
|
password = self.db_config["password"]
|
|
102
|
-
host = self.db_config
|
|
122
|
+
host = self.db_config.get("host", "localhost")
|
|
103
123
|
port = self.db_config.get("port", 5432)
|
|
104
124
|
additional_config = self.db_config.get("configuration", {})
|
|
105
125
|
|
|
@@ -29,7 +29,7 @@ def _log_jwt_configuration():
|
|
|
29
29
|
log.debug(
|
|
30
30
|
"JWT_ALLOWED_AUDIENCES: %s", os.getenv("JWT_ALLOWED_AUDIENCES", "NOT_SET")
|
|
31
31
|
)
|
|
32
|
-
log.debug("
|
|
32
|
+
log.debug("ANONYMOUS_ROLE: %s", os.getenv("ANONYMOUS_ROLE", "NOT_SET"))
|
|
33
33
|
log.debug("Logging level: %s", logging.getLogger().getEffectiveLevel())
|
|
34
34
|
log.debug("===============================")
|
|
35
35
|
|
|
@@ -39,7 +39,7 @@ def token_decoder(
|
|
|
39
39
|
audience: Optional[str] = None,
|
|
40
40
|
issuer: Optional[str] = None,
|
|
41
41
|
algorithms: Optional[list[str]] = None,
|
|
42
|
-
|
|
42
|
+
anonymous_role: Optional[str] = None,
|
|
43
43
|
):
|
|
44
44
|
"""
|
|
45
45
|
JWT token decoder decorator for AWS Lambda handlers.
|
|
@@ -53,9 +53,11 @@ def token_decoder(
|
|
|
53
53
|
audience: Expected audience claim (or list of audiences)
|
|
54
54
|
issuer: Expected issuer claim
|
|
55
55
|
algorithms: List of allowed algorithms (defaults to ["RS256"])
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
anonymous_role: Role to assign to anonymous (unauthenticated) requests.
|
|
57
|
+
If not set and no token is provided, request is rejected.
|
|
58
|
+
Set via environment variable ANONYMOUS_ROLE or parameter.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
59
61
|
Decorated handler function that processes JWT tokens before execution
|
|
60
62
|
|
|
61
63
|
Example:
|
|
@@ -70,27 +72,21 @@ def token_decoder(
|
|
|
70
72
|
# Claims available in event['requestContext']['authorizer']
|
|
71
73
|
return {'statusCode': 200, 'body': 'Secured endpoint'}
|
|
72
74
|
|
|
73
|
-
# Public endpoint -
|
|
74
|
-
@token_decoder(
|
|
75
|
+
# Public endpoint - allows anonymous with 'public' role
|
|
76
|
+
@token_decoder(anonymous_role="public")
|
|
75
77
|
def public_handler(event, context):
|
|
76
|
-
# Works without Authorization header
|
|
78
|
+
# Works without Authorization header, roles=["public"]
|
|
77
79
|
return {'statusCode': 200, 'body': 'Public endpoint'}
|
|
78
80
|
"""
|
|
79
81
|
|
|
80
82
|
# Validate configuration at decorator time
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if config_require_token and not jwks_url:
|
|
90
|
-
raise ValueError(
|
|
91
|
-
"JWT token validation is required but no jwks_url was provided. "
|
|
92
|
-
"Either provide a jwks_url or set require_authentication=False."
|
|
93
|
-
)
|
|
83
|
+
config_anonymous_role = anonymous_role or os.getenv("ANONYMOUS_ROLE")
|
|
84
|
+
|
|
85
|
+
# If no anonymous role is configured and no JWKS host, check if we're in test mode
|
|
86
|
+
if not config_anonymous_role and not jwks_url and not os.getenv("JWKS_HOST"):
|
|
87
|
+
# Allow decorator to be applied without config for testing
|
|
88
|
+
# Actual validation will happen at runtime when handler is called
|
|
89
|
+
pass
|
|
94
90
|
|
|
95
91
|
def decorator(handler: Callable) -> Callable:
|
|
96
92
|
@functools.wraps(handler)
|
|
@@ -116,14 +112,10 @@ def token_decoder(
|
|
|
116
112
|
else []
|
|
117
113
|
)
|
|
118
114
|
config_algorithms = algorithms or ["RS256"]
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
log.debug("
|
|
123
|
-
log.debug("REQUIRE_TOKEN: %s", os.getenv("REQUIRE_AUTHENTICATION", ""))
|
|
124
|
-
log.debug(
|
|
125
|
-
"config_require_authentication: %s", config_require_authentication
|
|
126
|
-
)
|
|
115
|
+
config_anonymous_role = anonymous_role or os.getenv("ANONYMOUS_ROLE")
|
|
116
|
+
log.debug("anonymous_role parameter: %s", anonymous_role)
|
|
117
|
+
log.debug("ANONYMOUS_ROLE env: %s", os.getenv("ANONYMOUS_ROLE", ""))
|
|
118
|
+
log.debug("config_anonymous_role: %s", config_anonymous_role)
|
|
127
119
|
|
|
128
120
|
# Skip JWT decoding if no JWKS URL is configured
|
|
129
121
|
if not config_jwks_url:
|
|
@@ -155,30 +147,25 @@ def token_decoder(
|
|
|
155
147
|
if config_audience
|
|
156
148
|
else None,
|
|
157
149
|
algorithms=config_algorithms,
|
|
158
|
-
|
|
150
|
+
anonymous_role=config_anonymous_role,
|
|
159
151
|
)
|
|
160
152
|
else:
|
|
161
153
|
log.debug("Using existing JWTDecoder instance")
|
|
162
154
|
jwt_decoder = wrapper._jwt_decoder
|
|
163
155
|
|
|
164
156
|
log.debug("Decoding JWT token")
|
|
165
|
-
decoded_token = jwt_decoder.decode(
|
|
166
|
-
event, require_token=config_require_token
|
|
167
|
-
)
|
|
157
|
+
decoded_token = jwt_decoder.decode(event)
|
|
168
158
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
event["requestContext"]["authorizer"] = decoded_token
|
|
180
|
-
else:
|
|
181
|
-
log.debug("No token found, proceeding without authentication")
|
|
159
|
+
log.debug(
|
|
160
|
+
"JWT token result: %s",
|
|
161
|
+
json.dumps(decoded_token, default=str) if decoded_token else "None",
|
|
162
|
+
)
|
|
163
|
+
# Populate requestContext for Lambda handler compatibility
|
|
164
|
+
if "requestContext" not in event:
|
|
165
|
+
event["requestContext"] = {}
|
|
166
|
+
if "authorizer" not in event["requestContext"]:
|
|
167
|
+
event["requestContext"]["authorizer"] = {}
|
|
168
|
+
event["requestContext"]["authorizer"] = decoded_token
|
|
182
169
|
|
|
183
170
|
return handler(event, context)
|
|
184
171
|
|
|
@@ -198,31 +185,36 @@ def token_decoder(
|
|
|
198
185
|
class JWTDecoder:
|
|
199
186
|
def __init__(
|
|
200
187
|
self,
|
|
201
|
-
jwks_url: str,
|
|
188
|
+
jwks_url: Optional[str] = None,
|
|
202
189
|
issuer: Optional[str] = None,
|
|
203
190
|
allowed_audiences: Optional[set] = None,
|
|
204
191
|
algorithms: Optional[list] = None,
|
|
205
|
-
|
|
192
|
+
anonymous_role: Optional[str] = None,
|
|
206
193
|
):
|
|
207
194
|
log.debug(
|
|
208
|
-
"Initializing JWTDecoder with jwks_url: %s, issuer: %s
|
|
195
|
+
"Initializing JWTDecoder with jwks_url: %s, issuer: %s, anonymous_role: %s",
|
|
196
|
+
jwks_url, issuer, anonymous_role
|
|
209
197
|
)
|
|
210
198
|
self.jwks_url = jwks_url
|
|
211
|
-
self.public_key = self.fetch_public_key_from_jwks(jwks_url)
|
|
212
199
|
self.issuer = issuer
|
|
213
200
|
self.allowed_audiences = allowed_audiences or set()
|
|
214
201
|
self.algorithms = algorithms or ["RS256"]
|
|
215
|
-
self.
|
|
202
|
+
self.anonymous_role = anonymous_role
|
|
203
|
+
|
|
204
|
+
# Fetch public key only if JWKS URL is provided
|
|
205
|
+
if jwks_url:
|
|
206
|
+
self.public_key = self.fetch_public_key_from_jwks(jwks_url)
|
|
207
|
+
if not self.public_key:
|
|
208
|
+
raise ValueError(f"Failed to fetch public key from JWKS URL: {jwks_url}")
|
|
209
|
+
else:
|
|
210
|
+
self.public_key = None
|
|
211
|
+
|
|
216
212
|
log.debug(
|
|
217
213
|
"JWTDecoder initialized with %d allowed audiences, algorithms: %s",
|
|
218
214
|
len(self.allowed_audiences),
|
|
219
215
|
self.algorithms,
|
|
220
216
|
)
|
|
221
217
|
|
|
222
|
-
# Ensure we have a valid public key
|
|
223
|
-
if not self.public_key:
|
|
224
|
-
raise ValueError(f"Failed to fetch public key from JWKS URL: {jwks_url}")
|
|
225
|
-
|
|
226
218
|
def fetch_public_key_from_jwks(
|
|
227
219
|
self, jwks_url: str, kid: Optional[str] = None
|
|
228
220
|
) -> Optional[str]:
|
|
@@ -289,21 +281,26 @@ class JWTDecoder:
|
|
|
289
281
|
log.error("Failed to fetch public key from JWKS: %s", e)
|
|
290
282
|
return None
|
|
291
283
|
|
|
292
|
-
def decode(
|
|
293
|
-
|
|
294
|
-
) -> Optional[Dict[str, Any]]:
|
|
295
|
-
"""Decode the JWT token and return the claims."""
|
|
284
|
+
def decode(self, event: Dict[str, Any]) -> Dict[str, Any]:
|
|
285
|
+
"""Decode the JWT token and return the claims, or anonymous claims if no token."""
|
|
296
286
|
try:
|
|
297
287
|
token = self.parse_token_from_event(event)
|
|
298
|
-
|
|
288
|
+
if token:
|
|
289
|
+
return self.decode_token(token)
|
|
290
|
+
elif self.anonymous_role:
|
|
291
|
+
log.debug("No token found, using anonymous role: %s", self.anonymous_role)
|
|
292
|
+
return {"roles": [self.anonymous_role]}
|
|
293
|
+
else:
|
|
294
|
+
log.error("No token found and no anonymous role configured")
|
|
295
|
+
raise ValueError("Authentication required: no token provided and no anonymous access configured")
|
|
299
296
|
except ValueError as e:
|
|
300
|
-
if
|
|
301
|
-
|
|
297
|
+
if self.anonymous_role and "No authorization header" in str(e):
|
|
298
|
+
log.debug("Token parsing failed, using anonymous role: %s", str(e))
|
|
299
|
+
return {"roles": [self.anonymous_role]}
|
|
302
300
|
else:
|
|
303
|
-
|
|
304
|
-
return None
|
|
301
|
+
raise
|
|
305
302
|
|
|
306
|
-
def parse_token_from_event(self, event: Dict[str, Any]) -> str:
|
|
303
|
+
def parse_token_from_event(self, event: Dict[str, Any]) -> Optional[str]:
|
|
307
304
|
"""Extract the Bearer token from the authorization header."""
|
|
308
305
|
log.debug("Parsing JWT token from event")
|
|
309
306
|
|
|
@@ -315,13 +312,9 @@ class JWTDecoder:
|
|
|
315
312
|
|
|
316
313
|
log.debug("Authorization header found: %s", "Yes" if auth_header else "No")
|
|
317
314
|
|
|
318
|
-
log.debug("require_authentication: %s", self.require_authentication)
|
|
319
315
|
if not auth_header:
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
raise ValueError("No authorization header found")
|
|
323
|
-
else:
|
|
324
|
-
return ""
|
|
316
|
+
log.debug("No authorization header found")
|
|
317
|
+
raise ValueError("No authorization header found")
|
|
325
318
|
|
|
326
319
|
auth_token_parts = auth_header.split(" ")
|
|
327
320
|
log.debug("Authorization header parts: %d", len(auth_token_parts))
|
|
@@ -341,6 +334,9 @@ class JWTDecoder:
|
|
|
341
334
|
def decode_token(self, token: str) -> Dict[str, Any]:
|
|
342
335
|
"""Validate and decode the JWT using the PEM public key."""
|
|
343
336
|
log.debug("Starting JWT token validation and decoding")
|
|
337
|
+
|
|
338
|
+
if not self.public_key:
|
|
339
|
+
raise ValueError("No public key available for token validation")
|
|
344
340
|
|
|
345
341
|
import jwt
|
|
346
342
|
from jwt import (
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|