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.
Files changed (38) hide show
  1. {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/PKG-INFO +1 -1
  2. api_foundry_query_engine-0.8.35/api_foundry_query_engine/__init__.py +1 -0
  3. {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/connectors/postgres_connection.py +21 -1
  4. {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/utils/token_decoder.py +67 -71
  5. {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/pyproject.toml +1 -1
  6. api_foundry_query_engine-0.8.34/api_foundry_query_engine/__init__.py +0 -0
  7. {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/.gitignore +0 -0
  8. {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/LICENSE +0 -0
  9. {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/.pre-commit-config.yaml +0 -0
  10. {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/adapters/adapter.py +0 -0
  11. {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
  12. {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/adapters/gateway_adapter.py +0 -0
  13. {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/adapters/security_adapter.py +0 -0
  14. {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/connectors/connection.py +0 -0
  15. {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/connectors/connection_factory.py +0 -0
  16. {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/connectors/oracle_connector.py +0 -0
  17. {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
  18. {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/dao/dao.py +0 -0
  19. {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/dao/operation_dao.py +0 -0
  20. {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
  21. {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
  22. {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
  23. {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
  24. {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
  25. {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
  26. {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
  27. {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
  28. {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/lambda_handler.py +0 -0
  29. {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/operation.py +0 -0
  30. {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/services/service.py +0 -0
  31. {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/services/transactional_service.py +0 -0
  32. {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/utils/api_model.py +0 -0
  33. {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/utils/app_exception.py +0 -0
  34. {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/utils/claims_check.py +0 -0
  35. {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/utils/dependency_resolver.py +0 -0
  36. {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/utils/gateway_operation.py +0 -0
  37. {api_foundry_query_engine-0.8.34 → api_foundry_query_engine-0.8.35}/api_foundry_query_engine/utils/logger.py +0 -0
  38. {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.34
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["host"]
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("REQUIRE_TOKEN: %s", os.getenv("REQUIRE_AUTHENTICATION"))
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
- require_authentication: Optional[bool] = None,
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
- require_authentication: Whether to require a token (default: True).
57
- If False, missing tokens are allowed for unsecured
58
- endpoints Returns:
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 - no token required
74
- @token_decoder(require_authentication=False)
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
- config_require_token = require_authentication
82
- if os.getenv("REQUIRE_AUTHENTICATION") is not None:
83
- config_require_token = os.getenv("REQUIRE_AUTHENTICATION", "").lower() in (
84
- "true",
85
- "1",
86
- "yes",
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
- config_require_authentication = require_authentication or os.getenv(
120
- "REQUIRE_AUTHENTICATION", ""
121
- ).lower() in ("true", "1", "yes")
122
- log.debug("require_authentication %s", require_authentication)
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
- require_authentication=config_require_token,
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
- if decoded_token is not None:
170
- log.debug(
171
- "JWT token successfully decoded: %s",
172
- json.dumps(decoded_token, default=str),
173
- )
174
- # Populate requestContext for Lambda handler compatibility
175
- if "requestContext" not in event:
176
- event["requestContext"] = {}
177
- if "authorizer" not in event["requestContext"]:
178
- event["requestContext"]["authorizer"] = {}
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
- require_authentication: Optional[bool] = None,
192
+ anonymous_role: Optional[str] = None,
206
193
  ):
207
194
  log.debug(
208
- "Initializing JWTDecoder with jwks_url: %s, issuer: %s", jwks_url, issuer
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.require_authentication = require_authentication or True
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
- self, event: Dict[str, Any], require_token: bool = True
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
- return self.decode_token(token)
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 require_token:
301
- raise
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
- log.debug("No token found but not required: %s", str(e))
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
- if self.require_authentication:
321
- log.error("No authorization header found in event")
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 (
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "api-foundry-query-engine"
7
- version = "0.8.34"
7
+ version = "0.8.35"
8
8
  authors = [
9
9
  { name="Daniel Repik", email="danrepik@icloud.com" },
10
10
  ]