mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-17 20:39:13 +00:00
fix(api-v3): reject unknown restore option keys
CodeRabbit's review of the blueprint split (#553) asked that POST /backup/restore reject option keys outside RestoreOptions' known set. The follow-up commit fixed the bool("false")-is-True bug with _coerce_to_bool but never added the key check: a typo'd or renamed key (e.g. "restoreSecrets") is silently ignored by opts_dict.get(key, True), so the flag stays at its True default and secrets get restored despite the caller's request saying otherwise -- with no indication anything was wrong. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vmcwf5vMgYqdt8bJTZtiwb
This commit is contained in:
@@ -139,6 +139,23 @@ class TestRequestValidation:
|
||||
assert post(client, options="{}").status_code == 200
|
||||
assert restore.call_args[0][2].restore_config is True
|
||||
|
||||
def test_unknown_option_key_is_refused(self, client, restore):
|
||||
# Regression: opts_dict.get('restore_secrets', True) silently
|
||||
# ignores a typo'd/renamed key like "restoreSecrets" and keeps the
|
||||
# True default, restoring secrets a caller's request clearly meant
|
||||
# to exclude -- with no indication anything was wrong.
|
||||
response = post(client, options=json.dumps({"restoreSecrets": False}))
|
||||
assert response.status_code == 400
|
||||
assert "Unknown restore option" in response.get_json()["message"]
|
||||
assert "restoreSecrets" in response.get_json()["message"]
|
||||
restore.assert_not_called()
|
||||
|
||||
def test_known_and_unknown_keys_together_are_refused(self, client, restore):
|
||||
response = post(client, options=json.dumps({
|
||||
"restore_secrets": False, "restore_everything": True}))
|
||||
assert response.status_code == 400
|
||||
restore.assert_not_called()
|
||||
|
||||
|
||||
class TestOptionsAreBooleanAware:
|
||||
"""Regression: bool("false") is True in Python.
|
||||
|
||||
@@ -90,6 +90,14 @@ def backup_validate():
|
||||
except Exception as e:
|
||||
logger.error("backup_validate failed: %s", e, exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An internal error occurred; see logs for details'}), 500
|
||||
#: The only keys RestoreOptions recognizes. A typo'd or renamed key (e.g.
|
||||
#: "restoreSecrets") would otherwise be silently ignored by opts_dict.get(),
|
||||
#: leaving that flag at its True default -- restoring secrets a caller's
|
||||
#: request clearly meant to exclude, with no indication anything was wrong.
|
||||
_RESTORE_OPTION_KEYS = frozenset((
|
||||
'restore_config', 'restore_secrets', 'restore_wifi', 'restore_fonts',
|
||||
'restore_plugin_uploads', 'reinstall_plugins',
|
||||
))
|
||||
@api_v3.route('/backup/restore', methods=['POST'])
|
||||
def backup_restore():
|
||||
"""Restore a backup ZIP with optional RestoreOptions."""
|
||||
@@ -112,6 +120,12 @@ def backup_restore():
|
||||
'status': 'error',
|
||||
'message': 'Invalid options: expected a JSON object',
|
||||
}), 400
|
||||
unknown_keys = set(opts_dict) - _RESTORE_OPTION_KEYS
|
||||
if unknown_keys:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Unknown restore option(s): {", ".join(sorted(unknown_keys))}',
|
||||
}), 400
|
||||
# _coerce_to_bool (not bare bool()) because a request can send these
|
||||
# as JSON strings: bool("false") is True in Python, so a caller who
|
||||
# explicitly asked to skip secrets would have had them restored
|
||||
|
||||
Reference in New Issue
Block a user