py-app-runner 0.5.49.dev0__tar.gz → 0.5.50.dev0__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.
- py_app_runner-0.5.50.dev0/.version +1 -0
- {py_app_runner-0.5.49.dev0/src/py_app_runner.egg-info → py_app_runner-0.5.50.dev0}/PKG-INFO +1 -1
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/request_handler/handlers.py +13 -26
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/utils.py +27 -10
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0/src/py_app_runner.egg-info}/PKG-INFO +1 -1
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner.egg-info/SOURCES.txt +1 -0
- py_app_runner-0.5.50.dev0/tests/test_api_handler.py +68 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/tests/test_error_envelope.py +11 -32
- py_app_runner-0.5.49.dev0/.version +0 -1
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/LICENSE +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/MANIFEST.in +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/README.md +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/pyproject.toml +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/setup.cfg +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/__init__.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/audit/__init__.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/audit/_service.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/audit/_service_args.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/audit/audit.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/audit/commands.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/audit/diff.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/audit/errors.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/audit/event.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/audit/store.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/bridge/__init__.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/bridge/_service.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/bridge/_service_args.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/bridge/api.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/bridge/encoders/__init__.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/bridge/encoders/base.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/bridge/encoders/json_encoder.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/bridge/encoders/msgpack_encoder.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/bridge/web_app.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/bridge/websocket.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/colors.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/config.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/crypto/__init__.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/crypto/_service.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/crypto/_service_args.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/crypto/commands.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/crypto/envelope.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/crypto/errors.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/crypto/fields.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/crypto/passwords.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/db_pools.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/http_exception.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/logger_handlers.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/migrations/__init__.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/migrations/_service.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/migrations/_service_args.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/migrations/commands.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/migrations/discovery.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/migrations/states.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/migrations/tracker.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/py.typed +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/pybridge.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/queue/__init__.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/queue/_service.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/queue/_service_args.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/queue/commands.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/queue/driver_pg.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/queue/driver_redis.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/queue/handler.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/queue/interface.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/queue/job.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/queue/worker.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/registry.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/request_handler/__init__.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/request_handler/auth_service.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/request_handler/decorators.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/request_handler/pagination.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/return_model.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/runner.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/throttle/__init__.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/throttle/throttle.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/tick_service.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/timer.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/wbcm/__init__.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/wbcm/device_connections.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/wbcm/factory.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/wbcm/wb_connection_manager.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/wbcm/ws_interface.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner.egg-info/dependency_links.txt +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner.egg-info/requires.txt +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner.egg-info/top_level.txt +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/tests/test_api_keys.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/tests/test_audit_diff.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/tests/test_audit_store.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/tests/test_config.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/tests/test_crypto_envelope.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/tests/test_crypto_fields.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/tests/test_crypto_passwords.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/tests/test_decorators.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/tests/test_example.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/tests/test_logger_handlers.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/tests/test_migrations_commands.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/tests/test_migrations_discovery.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/tests/test_migrations_pg_harness.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/tests/test_migrations_service.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/tests/test_migrations_states.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/tests/test_migrations_tracker.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/tests/test_pybridge.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/tests/test_queue_contract.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/tests/test_queue_driver.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/tests/test_queue_driver_redis.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/tests/test_queue_worker.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/tests/test_throttle.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/tests/test_utils.py +0 -0
- {py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/tests/test_wbcm.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.5.50.dev0
|
|
@@ -153,25 +153,15 @@ class RequestHandlerBase(web.RequestHandler):
|
|
|
153
153
|
)
|
|
154
154
|
|
|
155
155
|
def get_request_data(self, force_action: str | None = None) -> dict[str, Any]:
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
"""The JSON body is the request. The URL may only name the action - anything else
|
|
157
|
+
in the query string would end up in access logs and caches as service input."""
|
|
158
|
+
request_data: dict[str, Any] = dict(self.json_args or {})
|
|
158
159
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
decoded_params[k] = v[0].decode("utf-8")
|
|
163
|
-
else:
|
|
164
|
-
# Decode each item in the list
|
|
165
|
-
decoded_params[k] = [item.decode("utf-8") for item in v]
|
|
166
|
-
|
|
167
|
-
request_body = {}
|
|
168
|
-
if hasattr(self, "json_args") and self.json_args:
|
|
169
|
-
request_body = self.json_args
|
|
160
|
+
action = force_action or request_data.get("action") or self.get_query_argument("action", None)
|
|
161
|
+
if action:
|
|
162
|
+
request_data["action"] = action
|
|
170
163
|
|
|
171
|
-
|
|
172
|
-
request_body["action"] = force_action
|
|
173
|
-
|
|
174
|
-
return {**decoded_params, **request_body}
|
|
164
|
+
return request_data
|
|
175
165
|
|
|
176
166
|
def error(
|
|
177
167
|
self,
|
|
@@ -232,7 +222,7 @@ class RequestHandlerBase(web.RequestHandler):
|
|
|
232
222
|
method,
|
|
233
223
|
path,
|
|
234
224
|
body_len,
|
|
235
|
-
args if isinstance(args, dict) else "<n/a>",
|
|
225
|
+
redact_headers(args) if isinstance(args, dict) else "<n/a>",
|
|
236
226
|
)
|
|
237
227
|
|
|
238
228
|
# details go at DEBUG unless we're already warning/error, then use same level
|
|
@@ -270,12 +260,12 @@ class RequestHandlerBase(web.RequestHandler):
|
|
|
270
260
|
await res
|
|
271
261
|
|
|
272
262
|
# Debug
|
|
273
|
-
headers = dict(self.request.headers)
|
|
274
|
-
headers.pop("Authorization", None)
|
|
275
|
-
|
|
276
263
|
self.logger.debug(f"Request: {self.request}")
|
|
277
|
-
self.logger.debug(
|
|
278
|
-
self.logger.debug(
|
|
264
|
+
self.logger.debug("Headers: %s", redact_headers(dict(self.request.headers)))
|
|
265
|
+
self.logger.debug(
|
|
266
|
+
"Body: %s",
|
|
267
|
+
format_body_for_log(self.request.body, max_chars=4000, shrink_strings_to=256, cap_list_items=50),
|
|
268
|
+
)
|
|
279
269
|
self.logger.debug(f"Arguments: {self.request.arguments.keys()}")
|
|
280
270
|
|
|
281
271
|
self._ensure_content_type()
|
|
@@ -458,9 +448,6 @@ class RequestHandlerApiKeys(RequestHandlerBase):
|
|
|
458
448
|
|
|
459
449
|
async def has_valid_api_key(self, custom_api_key: str | None = None) -> str | Literal[True]:
|
|
460
450
|
api_key: str | None = custom_api_key or self.request.headers.get("X-API-Key", None) # type: ignore
|
|
461
|
-
if api_key is None:
|
|
462
|
-
api_key = self.get_argument("api_key", None)
|
|
463
|
-
|
|
464
451
|
if api_key is None:
|
|
465
452
|
return "API key is missing"
|
|
466
453
|
|
|
@@ -278,15 +278,29 @@ async def run_blocking(fn: Callable[P, R], *args: P.args, **kwargs: P.kwargs) ->
|
|
|
278
278
|
return await loop.run_in_executor(None, partial(fn, *args, **kwargs))
|
|
279
279
|
|
|
280
280
|
|
|
281
|
+
SENSITIVE_KEY_MARKERS = (
|
|
282
|
+
"authorization",
|
|
283
|
+
"cookie",
|
|
284
|
+
"password",
|
|
285
|
+
"passwd",
|
|
286
|
+
"secret",
|
|
287
|
+
"token",
|
|
288
|
+
"api-key",
|
|
289
|
+
"api_key",
|
|
290
|
+
"apikey",
|
|
291
|
+
"credential",
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def is_sensitive_key(name: Any) -> bool:
|
|
296
|
+
"""Substring match on header names and JSON keys, so `X-Auth-Token`, `auth_token`,
|
|
297
|
+
`refresh_token` and `client_secret` are all caught without listing each one."""
|
|
298
|
+
lowered = str(name).lower()
|
|
299
|
+
return any(marker in lowered for marker in SENSITIVE_KEY_MARKERS)
|
|
300
|
+
|
|
301
|
+
|
|
281
302
|
def redact_headers(headers: dict[str, Any]) -> dict[str, Any]:
|
|
282
|
-
|
|
283
|
-
for k, v in (headers or {}).items():
|
|
284
|
-
kl = str(k).lower()
|
|
285
|
-
if kl in ("authorization", "proxy-authorization", "cookie", "set-cookie", "x-api-key"):
|
|
286
|
-
redacted[k] = "<redacted>"
|
|
287
|
-
else:
|
|
288
|
-
redacted[k] = v
|
|
289
|
-
return redacted
|
|
303
|
+
return {k: ("<redacted>" if is_sensitive_key(k) else v) for k, v in (headers or {}).items()}
|
|
290
304
|
|
|
291
305
|
|
|
292
306
|
def sha1_prefix(b: bytes, n: int = 12) -> str:
|
|
@@ -300,13 +314,16 @@ def sha256_hash(s: str) -> str:
|
|
|
300
314
|
def shrink_json(obj: Any, *, max_string: int, max_items: int) -> Any:
|
|
301
315
|
"""
|
|
302
316
|
Traverse JSON-like structures and:
|
|
317
|
+
- redact values under credential-looking keys (see is_sensitive_key),
|
|
303
318
|
- truncate any string longer than max_string (base64-aware summary),
|
|
304
319
|
- cap lists to max_items with a "… N more items" marker,
|
|
305
320
|
- leave numbers/bools/None untouched.
|
|
306
|
-
No key-based assumptions.
|
|
307
321
|
"""
|
|
308
322
|
if isinstance(obj, dict):
|
|
309
|
-
return {
|
|
323
|
+
return {
|
|
324
|
+
k: ("<redacted>" if is_sensitive_key(k) else shrink_json(v, max_string=max_string, max_items=max_items))
|
|
325
|
+
for k, v in obj.items()
|
|
326
|
+
}
|
|
310
327
|
if isinstance(obj, list):
|
|
311
328
|
trimmed = [shrink_json(x, max_string=max_string, max_items=max_items) for x in obj[:max_items]]
|
|
312
329
|
if len(obj) > max_items:
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner.egg-info/SOURCES.txt
RENAMED
|
@@ -78,6 +78,7 @@ src/py_app_runner/wbcm/device_connections.py
|
|
|
78
78
|
src/py_app_runner/wbcm/factory.py
|
|
79
79
|
src/py_app_runner/wbcm/wb_connection_manager.py
|
|
80
80
|
src/py_app_runner/wbcm/ws_interface.py
|
|
81
|
+
tests/test_api_handler.py
|
|
81
82
|
tests/test_api_keys.py
|
|
82
83
|
tests/test_audit_diff.py
|
|
83
84
|
tests/test_audit_store.py
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from tornado.testing import AsyncHTTPTestCase
|
|
2
|
+
from tornado.web import Application
|
|
3
|
+
|
|
4
|
+
from py_app_runner.http_exception import HTTPException
|
|
5
|
+
from py_app_runner.registry import AppRegistry
|
|
6
|
+
from py_app_runner.utils import json_encode
|
|
7
|
+
|
|
8
|
+
from .api_harness import FakeService, make_api_app
|
|
9
|
+
|
|
10
|
+
API_KEY = "top-secret-key"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestApiHandlerInput(AsyncHTTPTestCase):
|
|
14
|
+
def get_app(self) -> Application:
|
|
15
|
+
AppRegistry.configure(
|
|
16
|
+
config={"environment": "test", "debug": False, "api": {"key": API_KEY}, "jwt": {"secret": "jwt-secret"}},
|
|
17
|
+
users_model=object,
|
|
18
|
+
api_keys_model=object,
|
|
19
|
+
)
|
|
20
|
+
self.echo = FakeService(result={"status": "ok"})
|
|
21
|
+
self.guarded = FakeService(result={"status": "ok"}, requires_api_key=None)
|
|
22
|
+
self.rejecting = FakeService(error=HTTPException("bad credentials", code=-401, http_status=401))
|
|
23
|
+
return make_api_app({"echo": self.echo, "guarded": self.guarded, "rejecting": self.rejecting})
|
|
24
|
+
|
|
25
|
+
def test_credentials_never_reach_the_logs(self):
|
|
26
|
+
# Every HTTPException logs the headers and the full body at WARNING, so a failed
|
|
27
|
+
# login would otherwise write the password and the caller's tokens to the log.
|
|
28
|
+
body = {
|
|
29
|
+
"action": "login",
|
|
30
|
+
"auth_token": "token-in-body",
|
|
31
|
+
"data": {"email": "a@b.c", "password": "hunter2"},
|
|
32
|
+
}
|
|
33
|
+
headers = {
|
|
34
|
+
"Content-Type": "application/json",
|
|
35
|
+
"Authorization": "Bearer token-in-authorization",
|
|
36
|
+
"X-Auth-Token": "token-in-custom-header",
|
|
37
|
+
"X-Api-Secret": "secret-in-custom-header",
|
|
38
|
+
"X-API-Key": API_KEY,
|
|
39
|
+
}
|
|
40
|
+
with self.assertLogs("py_app_runner", level="DEBUG") as captured:
|
|
41
|
+
response = self.fetch("/api/rejecting", method="POST", body=json_encode(body), headers=headers)
|
|
42
|
+
|
|
43
|
+
assert response.code == 401
|
|
44
|
+
logged = "\n".join(captured.output)
|
|
45
|
+
for secret in (
|
|
46
|
+
"hunter2",
|
|
47
|
+
"token-in-body",
|
|
48
|
+
"token-in-authorization",
|
|
49
|
+
"token-in-custom-header",
|
|
50
|
+
"secret-in-custom-header",
|
|
51
|
+
API_KEY,
|
|
52
|
+
):
|
|
53
|
+
assert secret not in logged, secret
|
|
54
|
+
# Redaction is targeted, not a blanket suppression of the body.
|
|
55
|
+
assert "login" in logged
|
|
56
|
+
|
|
57
|
+
def test_api_key_is_read_from_the_header_only(self):
|
|
58
|
+
# A key in the query string lands in proxy access logs and Referer headers.
|
|
59
|
+
assert self.fetch(f"/api/guarded?action=go&api_key={API_KEY}").code == 401
|
|
60
|
+
assert self.fetch("/api/guarded?action=go", headers={"X-API-Key": API_KEY}).code == 200
|
|
61
|
+
|
|
62
|
+
def test_query_parameters_are_not_service_input(self):
|
|
63
|
+
# Only the JSON body feeds the service. A `data` from the URL is a bare string
|
|
64
|
+
# that no handler expecting a dict can use, and URLs get logged and cached.
|
|
65
|
+
response = self.fetch("/api/echo?action=go&data=from-url&extra=1")
|
|
66
|
+
|
|
67
|
+
assert response.code == 200
|
|
68
|
+
assert self.echo.calls == [("go", {})]
|
|
@@ -7,6 +7,9 @@ from py_app_runner.http_exception import HTTPException
|
|
|
7
7
|
from py_app_runner.registry import AppRegistry
|
|
8
8
|
from py_app_runner.utils import json_decode
|
|
9
9
|
|
|
10
|
+
from .api_harness import FakeService
|
|
11
|
+
from .api_harness import make_api_app as _make_api_app
|
|
12
|
+
|
|
10
13
|
|
|
11
14
|
def make_app() -> Application:
|
|
12
15
|
from py_app_runner.request_handler.handlers import WebHandlerBase
|
|
@@ -32,39 +35,15 @@ def make_app() -> Application:
|
|
|
32
35
|
)
|
|
33
36
|
|
|
34
37
|
|
|
35
|
-
class FakeService:
|
|
36
|
-
"""Stands in for a services.<name>._service_pybridge module."""
|
|
37
|
-
|
|
38
|
-
requires_api_key = False
|
|
39
|
-
|
|
40
|
-
def __init__(self, result: Any = None, error: Exception | None = None):
|
|
41
|
-
self.result = result
|
|
42
|
-
self.error = error
|
|
43
|
-
|
|
44
|
-
async def bridge_request(self, action: str, request_data: dict[str, Any], bridge_handler: Any) -> Any:
|
|
45
|
-
if self.error is not None:
|
|
46
|
-
raise self.error
|
|
47
|
-
return self.result
|
|
48
|
-
|
|
49
|
-
|
|
50
38
|
def make_api_app() -> Application:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
pybridge = PyBridge()
|
|
61
|
-
pybridge.cache_service("boom", FakeService(error=RuntimeError("service exploded")))
|
|
62
|
-
pybridge.cache_service("known", FakeService(error=HTTPException("nope", code=-7, http_status=403)))
|
|
63
|
-
pybridge.cache_service("fine", FakeService(result={"status": "ok"}))
|
|
64
|
-
pybridge.cache_service("silent", FakeService(result=None))
|
|
65
|
-
app.set_pybridge(pybridge)
|
|
66
|
-
|
|
67
|
-
return app
|
|
39
|
+
return _make_api_app(
|
|
40
|
+
{
|
|
41
|
+
"boom": FakeService(error=RuntimeError("service exploded")),
|
|
42
|
+
"known": FakeService(error=HTTPException("nope", code=-7, http_status=403)),
|
|
43
|
+
"fine": FakeService(result={"status": "ok"}),
|
|
44
|
+
"silent": FakeService(result=None),
|
|
45
|
+
}
|
|
46
|
+
)
|
|
68
47
|
|
|
69
48
|
|
|
70
49
|
class TestApiHandlerStatus(AsyncHTTPTestCase):
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.5.49.dev0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/audit/_service_args.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/bridge/__init__.py
RENAMED
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/bridge/_service.py
RENAMED
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/bridge/_service_args.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/bridge/encoders/base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/bridge/websocket.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/crypto/__init__.py
RENAMED
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/crypto/_service.py
RENAMED
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/crypto/_service_args.py
RENAMED
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/crypto/commands.py
RENAMED
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/crypto/envelope.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/crypto/passwords.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/logger_handlers.py
RENAMED
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/migrations/__init__.py
RENAMED
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/migrations/_service.py
RENAMED
|
File without changes
|
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/migrations/commands.py
RENAMED
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/migrations/discovery.py
RENAMED
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/migrations/states.py
RENAMED
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/migrations/tracker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/queue/_service_args.py
RENAMED
|
File without changes
|
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/queue/driver_pg.py
RENAMED
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/queue/driver_redis.py
RENAMED
|
File without changes
|
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/queue/interface.py
RENAMED
|
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
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/throttle/__init__.py
RENAMED
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/throttle/throttle.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/wbcm/device_connections.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner/wbcm/ws_interface.py
RENAMED
|
File without changes
|
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner.egg-info/requires.txt
RENAMED
|
File without changes
|
{py_app_runner-0.5.49.dev0 → py_app_runner-0.5.50.dev0}/src/py_app_runner.egg-info/top_level.txt
RENAMED
|
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
|